diff --git a/.github/ISSUE_TEMPLATE/application-bug.yml b/.github/ISSUE_TEMPLATE/application-bug.yml index f3590067..931db3bd 100644 --- a/.github/ISSUE_TEMPLATE/application-bug.yml +++ b/.github/ISSUE_TEMPLATE/application-bug.yml @@ -80,13 +80,13 @@ body: label: Acknowledgements description: Your issue will be closed if you haven't done these steps. options: - - label: I am sure my issue is related to the app and **NOT some extension**. - required: true - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue. required: true - label: I have written a short but informative title. required: true - label: I have updated the app to pre-release version **[Latest](https://github.com/recloudstream/cloudstream/releases)**. required: true + - label: If related to a provider, I have checked the site and it works, but not the app. + required: true - label: I will fill out all of the requested information in this form. required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index b56cdf8e..250734cd 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,7 +2,7 @@ blank_issues_enabled: false contact_links: - name: Request a new provider or report bug with an existing provider url: https://github.com/recloudstream - about: EXTREMELY IMPORTANT - Please do not report any provider bugs here or request new providers. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the discord. + about: Please do not report any provider bugs here or request new providers. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the discord. - name: Discord url: https://discord.gg/5Hus6fM about: Join our discord for faster support on smaller issues. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index e18daebb..9c35ba56 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -27,7 +27,9 @@ body: label: Acknowledgements description: Your issue will be closed if you haven't done these steps. options: - - label: My suggestion is **NOT** about adding a new provider - required: true - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue. - required: true \ No newline at end of file + required: true + - label: I have written a short but informative title. + required: true + - label: I will fill out all of the requested information in this form. + required: true diff --git a/.github/locales.py b/.github/locales.py index a74d7258..7d6d6b90 100644 --- a/.github/locales.py +++ b/.github/locales.py @@ -1,7 +1,6 @@ import re import glob import requests -import os import lxml.etree as ET # builtin library doesn't preserve comments @@ -54,16 +53,11 @@ for file in glob.glob(f"{XML_NAME}*/strings.xml"): try: tree = ET.parse(file) for child in tree.getroot(): - if not child.text: - continue if child.text.startswith("\\@string/"): print(f"[{file}] fixing {child.attrib['name']}") child.text = child.text.replace("\\@string/", "@string/") with open(file, 'wb') as fp: fp.write(b'\n') tree.write(fp, encoding="utf-8", method="xml", pretty_print=True, xml_declaration=False) - # Remove trailing new line to be consistent with weblate - fp.seek(-1, os.SEEK_END) - fp.truncate() except ET.ParseError as ex: print(f"[{file}] {ex}") diff --git a/.idea/gradle.xml b/.idea/gradle.xml index d7c08c9c..c5c0ff3b 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,16 +4,17 @@ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d0c86bab..c2ba2907 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,5 @@ import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties import org.jetbrains.dokka.gradle.DokkaTask -import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.util.archivesName import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.io.ByteArrayOutputStream import java.net.URL @@ -42,8 +41,8 @@ android { }*/ signingConfigs { - if (prereleaseStoreFile != null) { - create("prerelease") { + create("prerelease") { + if (prereleaseStoreFile != null) { storeFile = file(prereleaseStoreFile) storePassword = System.getenv("SIGNING_STORE_PASSWORD") keyAlias = System.getenv("SIGNING_KEY_ALIAS") @@ -60,8 +59,8 @@ android { minSdk = 21 targetSdk = 33 /* Android 14 is Fu*ked ^ https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading*/ - versionCode = 64 - versionName = "4.4.0" + versionCode = 63 + versionName = "4.3.2" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") @@ -71,9 +70,9 @@ android { val localProperties = gradleLocalProperties(rootDir) buildConfigField( - "long", - "BUILD_DATE", - "${System.currentTimeMillis()}" + "String", + "BUILDDATE", + "new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));" ) buildConfigField( "String", @@ -124,11 +123,7 @@ android { resValue("bool", "is_prerelease", "true") buildConfigField("boolean", "BETA", "true") applicationIdSuffix = ".prerelease" - if (signingConfigs.names.contains("prerelease")) { - signingConfig = signingConfigs.getByName("prerelease") - } else { - logger.warn("No prerelease signing config!") - } + signingConfig = signingConfigs.getByName("prerelease") versionNameSuffix = "-PRE" versionCode = (System.currentTimeMillis() / 60000).toInt() } @@ -144,7 +139,7 @@ android { abortOnError = false checkReleaseBuilds = false } - + buildFeatures { buildConfig = true } @@ -159,24 +154,24 @@ repositories { dependencies { // Testing testImplementation("junit:junit:4.13.2") - testImplementation("org.json:json:20240303") + testImplementation("org.json:json:20231013") androidTestImplementation("androidx.test:core") - implementation("androidx.test.ext:junit-ktx:1.2.1") - androidTestImplementation("androidx.test.ext:junit:1.2.1") - androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") + implementation("androidx.test.ext:junit-ktx:1.1.5") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") // Android Core & Lifecycle - implementation("androidx.core:core-ktx:1.13.1") - implementation("androidx.appcompat:appcompat:1.7.0") + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.appcompat:appcompat:1.6.1") implementation("androidx.navigation:navigation-ui-ktx:2.7.7") - implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.3") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0") implementation("androidx.navigation:navigation-fragment-ktx:2.7.7") // Design & UI implementation("jp.wasabeef:glide-transformations:4.3.0") implementation("androidx.preference:preference-ktx:1.2.1") - implementation("com.google.android.material:material:1.12.0") + implementation("com.google.android.material:material:1.11.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") @@ -186,9 +181,9 @@ dependencies { implementation("com.github.bumptech.glide:okhttp3-integration:4.16.0") // For KSP -> Official Annotation Processors are Not Yet Supported for KSP - ksp("dev.zacsweers.autoservice:auto-service-ksp:1.2.0") - implementation("com.google.guava:guava:33.2.1-android") - implementation("dev.zacsweers.autoservice:auto-service-ksp:1.2.0") + ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0") + implementation("com.google.guava:guava:32.1.3-android") + implementation("dev.zacsweers.autoservice:auto-service-ksp:1.1.0") // Media 3 (ExoPlayer) implementation("androidx.media3:media3-ui:1.1.1") @@ -204,9 +199,9 @@ dependencies { // PlayBack implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs - implementation("com.github.teamnewpipe:NewPipeExtractor:176da72") /* For Trailers + implementation("com.github.teamnewpipe:NewPipeExtractor:6dc25f7") /* For Trailers ^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */ - implementation("com.github.albfernandez:juniversalchardet:2.5.0") // Subtitle Decoding + implementation("com.github.albfernandez:juniversalchardet:2.4.0") // Subtitle Decoding // Crash Reports (AcraApplication.kt) implementation("ch.acra:acra-core:5.11.3") @@ -217,17 +212,18 @@ dependencies { implementation("androidx.palette:palette-ktx:1.0.0") // Palette For Images -> Colors implementation("androidx.tvprovider:tvprovider:1.0.0") implementation("com.github.discord:OverlappingPanels:0.1.5") // Gestures - implementation("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication + implementation ("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview - implementation("io.github.g0dkar:qrcode-kotlin:4.2.0") // QR code for PIN Auth on TV // Extensions & Other Libs - implementation("org.mozilla:rhino:1.7.15") // run JavaScript + implementation("org.mozilla:rhino:1.7.13") /* run JavaScript + ^ Don't Bump RhinoJS to 1.7.14,`NoClassDefFoundError` Occurs and Trailers won't play (even with Desugaring) + NewPipeExtractor Issue */ implementation("me.xdrop:fuzzywuzzy:1.4.0") // Library/Ext Searching with Levenshtein Distance implementation("com.github.LagradOst:SafeFile:0.0.6") // To Prevent the URI File Fu*kery implementation("org.conscrypt:conscrypt-android:2.5.2") // To Fix SSL Fu*kery on Android 9 - implementation("com.uwetrottmann.tmdb2:tmdb-java:2.11.0") // TMDB API v3 Wrapper Made with RetroFit - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs_nio:2.0.4") //nio flavor needed for NewPipeExtractor + implementation("com.uwetrottmann.tmdb2:tmdb-java:2.10.0") // TMDB API v3 Wrapper Made with RetroFit + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") /* JSON Parser ^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API Level 25 or Less. */ @@ -236,46 +232,18 @@ dependencies { implementation("androidx.work:work-runtime:2.9.0") implementation("androidx.work:work-runtime-ktx:2.9.0") implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib - - implementation(project(":library") { - // There does not seem to be a good way of getting the android flavor. - val isDebug = gradle.startParameter.taskRequests.any { task -> - task.args.any { arg -> - arg.contains("debug", true) - } - } - - this.extra.set("isDebug", isDebug) - }) } -tasks.register("androidSourcesJar") { +tasks.register("androidSourcesJar", Jar::class) { archiveClassifier.set("sources") from(android.sourceSets.getByName("main").java.srcDirs) // Full Sources } -tasks.register("copyJar") { - from( - "build/intermediates/compile_app_classes_jar/prereleaseDebug", - "../library/build/libs" - ) - into("build/app-classes") - include("classes.jar", "library-jvm*.jar") - // Remove the version - rename("library-jvm.*.jar", "library-jvm.jar") -} - -// Merge the app classes and the library classes into classes.jar -tasks.register("makeJar") { - // Duplicates cause hard to catch errors, better to fail at compile time. - duplicatesStrategy = DuplicatesStrategy.FAIL - dependsOn(tasks.getByName("copyJar")) - from( - zipTree("build/app-classes/classes.jar"), - zipTree("build/app-classes/library-jvm.jar") - ) - destinationDirectory.set(layout.buildDirectory) - archivesName = "classes" +// For GradLew Plugin +tasks.register("makeJar", Copy::class) { + from("build/intermediates/compile_app_classes_jar/prereleaseDebug") + into("build") + include("classes.jar") } tasks.withType { diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt index c7f02baf..faacdf50 100644 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt @@ -154,7 +154,7 @@ class ExampleInstrumentedTest { fun providerCorrectHomepage() { runBlocking { getAllProviders().toList().amap { api -> - TestingUtils.testHomepage(api, TestingUtils.Logger()) + TestingUtils.testHomepage(api, ::println) } } println("Done providerCorrectHomepage") @@ -166,6 +166,7 @@ class ExampleInstrumentedTest { TestingUtils.getDeferredProviderTests( this, getAllProviders(), + ::println ) { _, _ -> } } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 888be999..a23ef725 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -97,7 +97,7 @@ --> Unit)) : ACRA.errorReporter.handleException(error) try { PrintStream(errorFile).use { ps -> - ps.println("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}") - ps.println("Fatal exception on thread ${thread.name} (${thread.id})") + ps.println(String.format("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}")) + ps.println( + String.format( + "Fatal exception on thread %s (%d)", + thread.name, + thread.id + ) + ) error.printStackTrace(ps) } } catch (ignored: FileNotFoundException) { @@ -101,6 +105,7 @@ class AcraApplication : Application() { override fun onCreate() { super.onCreate() + //NativeCrashHandler.initCrashHandler() ExceptionHandler(filesDir.resolve("last_error")) { val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName) startActivity(Intent.makeRestartActivityTask(intent!!.component)) @@ -146,7 +151,6 @@ class AcraApplication : Application() { get() = _context?.get() private set(value) { _context = WeakReference(value) - setContext(WeakReference(value)) } fun getKeyClass(path: String, valueType: Class): T? { diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index ee3a5d12..4dc78dc7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -5,7 +5,6 @@ import android.app.Activity import android.app.PictureInPictureParams import android.content.Context import android.content.pm.PackageManager -import android.content.res.Configuration import android.content.res.Resources import android.os.Build import android.util.DisplayMetrics @@ -30,14 +29,13 @@ import com.google.android.material.chip.ChipGroup import com.google.android.material.navigationrail.NavigationRailView import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey -import com.lagradost.cloudstream3.MainActivity.Companion.resumeApps import com.lagradost.cloudstream3.databinding.ToastBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.player.PlayerEventType import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.result.UiText import com.lagradost.cloudstream3.ui.settings.Globals.updateTv -import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl +import com.lagradost.cloudstream3.utils.AppUtils.isRtl import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.UIHelper @@ -165,7 +163,7 @@ object CommonActivity { val toast = Toast(act) toast.duration = duration ?: Toast.LENGTH_SHORT toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx) - toast.view = binding.root //fixme Find an alternative using default Toasts since custom toasts are deprecated and won't appear with api30 set as minSDK version. + toast.view = binding.root currentToast = toast toast.show() @@ -277,35 +275,12 @@ object CommonActivity { } } - fun updateTheme(act: Activity) { - val settingsManager = PreferenceManager.getDefaultSharedPreferences(act) - if (settingsManager - .getString(act.getString(R.string.app_theme_key), "AmoledLight") == "System" - && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - loadThemes(act) - } - } - - private fun mapSystemTheme(act: Activity): Int { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - val currentNightMode = - act.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK - return when (currentNightMode) { - Configuration.UI_MODE_NIGHT_NO -> R.style.LightMode // Night mode is not active, we're using the light theme - else -> R.style.AppTheme // Night mode is active, we're using dark theme - } - } else { - return R.style.AppTheme - } - } - fun loadThemes(act: Activity?) { if (act == null) return val settingsManager = PreferenceManager.getDefaultSharedPreferences(act) val currentTheme = when (settingsManager.getString(act.getString(R.string.app_theme_key), "AmoledLight")) { - "System" -> mapSystemTheme(act) "Black" -> R.style.AppTheme "Light" -> R.style.LightMode "Amoled" -> R.style.AmoledMode @@ -376,8 +351,8 @@ object CommonActivity { currentLook = currentLook.parent as? View ?: break }*/ - private fun View.hasContent(): Boolean { - return isShown && when (this) { + private fun View.hasContent() : Boolean { + return isShown && when(this) { //is RecyclerView -> this.childCount > 0 is ViewGroup -> this.childCount > 0 else -> true @@ -488,6 +463,20 @@ object CommonActivity { fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) { + //println("Keycode: $keyCode") + //showToast( + // this, + // "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}", + // Toast.LENGTH_LONG + //) + + // Tested keycodes on remote: + // KeyEvent.KEYCODE_MEDIA_FAST_FORWARD + // KeyEvent.KEYCODE_MEDIA_REWIND + // KeyEvent.KEYCODE_MENU + // KeyEvent.KEYCODE_MEDIA_NEXT + // KeyEvent.KEYCODE_MEDIA_PREVIOUS + // KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE // 149 keycode_numpad 5 when (keyCode) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt b/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt index 8da7ca38..0a2db2bd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt @@ -2,7 +2,6 @@ package com.lagradost.cloudstream3 import okhttp3.OkHttpClient import okhttp3.RequestBody -import okhttp3.RequestBody.Companion.toRequestBody import org.schabi.newpipe.extractor.downloader.Downloader import org.schabi.newpipe.extractor.downloader.Request import org.schabi.newpipe.extractor.downloader.Response @@ -11,7 +10,7 @@ import java.util.concurrent.TimeUnit class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Downloader() { - private val client: OkHttpClient = builder.readTimeout(30, TimeUnit.SECONDS).build() + private val client: OkHttpClient override fun execute(request: Request): Response { val httpMethod: String = request.httpMethod() val url: String = request.url() @@ -19,7 +18,7 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do val dataToSend: ByteArray? = request.dataToSend() var requestBody: RequestBody? = null if (dataToSend != null) { - requestBody = dataToSend.toRequestBody(null, 0, dataToSend.size) + requestBody = RequestBody.create(null, dataToSend) } val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder() .method(httpMethod, requestBody).url(url) @@ -74,4 +73,8 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do return instance } } + + init { + client = builder.readTimeout(30, TimeUnit.SECONDS).build() + } } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt similarity index 84% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt rename to app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 50dd667b..273e267b 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -1,43 +1,49 @@ package com.lagradost.cloudstream3 +import android.annotation.SuppressLint +import android.content.Context +import android.net.Uri +import android.util.Base64.encodeToString +import androidx.annotation.WorkerThread +import androidx.preference.PreferenceManager import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.kotlin.kotlinModule import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklApi import com.lagradost.cloudstream3.syncproviders.SyncIdName +import com.lagradost.cloudstream3.syncproviders.providers.SimklApi +import com.lagradost.cloudstream3.ui.player.SubtitleData +import com.lagradost.cloudstream3.ui.result.ResultViewModel2 import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.toJson -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.Coroutines.mainWork import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.nicehttp.RequestBodyTypes import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.RequestBody.Companion.toRequestBody -import java.net.URI import java.text.SimpleDateFormat import java.util.* -import kotlin.io.encoding.Base64 -import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.math.absoluteValue +const val USER_AGENT = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" + +//val baseHeader = mapOf("User-Agent" to USER_AGENT) +val mapper = JsonMapper.builder().addModule(kotlinModule()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!! + /** * Defines the constant for the all languages preference, if this is set then it is * the equivalent of all languages being set **/ const val AllLanguagesName = "universal" -const val USER_AGENT = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" - -class ErrorLoadingException(message: String? = null) : Exception(message) - -//val baseHeader = mapOf("User-Agent" to USER_AGENT) -val mapper = JsonMapper.builder().addModule(kotlinModule()) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!! - object APIHolder { val unixTime: Long get() = System.currentTimeMillis() / 1000L @@ -108,6 +114,16 @@ object APIHolder { return null } + private fun getLoadResponseIdFromUrl(url: String, apiName: String): Int { + return url.replace(getApiFromNameNull(apiName)?.mainUrl ?: "", "").replace("/", "") + .hashCode() + } + + fun LoadResponse.getId(): Int { + // this fixes an issue with outdated api as getLoadResponseIdFromUrl might be fucked + return (if (this is ResultViewModel2.LoadResponseFromSearch) this.id else null) ?: getLoadResponseIdFromUrl(url, apiName) + } + /** * Gets the website captcha token * discovered originally by https://github.com/ahmedgamal17 @@ -123,9 +139,10 @@ object APIHolder { // To get the key suspend fun getCaptchaToken(url: String, key: String, referer: String? = null): String? { try { - val uri = URI.create(url) - val domain = base64Encode( + val uri = Uri.parse(url) + val domain = encodeToString( (uri.scheme + "://" + uri.host + ":443").encodeToByteArray(), + 0 ).replace("\n", "").replace("=", ".") val vToken = @@ -205,15 +222,10 @@ object APIHolder { } ?: false val matchingTypes = types?.any { it.name.equals(media.format, true) } == true - if (lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears + if(lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears } ?: return null - Tracker( - res.idMal, - res.id.toString(), - res.coverImage?.extraLarge ?: res.coverImage?.large, - res.bannerImage - ) + Tracker(res.idMal, res.id.toString(), res.coverImage?.extraLarge ?: res.coverImage?.large, res.bannerImage) } catch (t: Throwable) { logError(t) null @@ -260,6 +272,165 @@ object APIHolder { return app.post("https://graphql.anilist.co", requestBody = data) .parsedSafe() } + + + fun Context.getApiSettings(): HashSet { + //val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + + val hashSet = HashSet() + val activeLangs = getApiProviderLangSettings() + val hasUniversal = activeLangs.contains(AllLanguagesName) + hashSet.addAll(synchronized(apis) { apis.filter { hasUniversal || activeLangs.contains(it.lang) } } + .map { it.name }) + + /*val set = settingsManager.getStringSet( + this.getString(R.string.search_providers_list_key), + hashSet + )?.toHashSet() ?: hashSet + + val list = HashSet() + for (name in set) { + val api = getApiFromNameNull(name) ?: continue + if (activeLangs.contains(api.lang)) { + list.add(name) + } + }*/ + //if (list.isEmpty()) return hashSet + //return list + return hashSet + } + + fun Context.getApiDubstatusSettings(): HashSet { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + val hashSet = HashSet() + hashSet.addAll(DubStatus.values()) + val list = settingsManager.getStringSet( + this.getString(R.string.display_sub_key), + hashSet.map { it.name }.toMutableSet() + ) ?: return hashSet + + val names = DubStatus.values().map { it.name }.toHashSet() + //if(realSet.isEmpty()) return hashSet + + return list.filter { names.contains(it) }.map { DubStatus.valueOf(it) }.toHashSet() + } + + fun Context.getApiProviderLangSettings(): HashSet { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + val hashSet = hashSetOf(AllLanguagesName) // def is all languages +// hashSet.add("en") // def is only en + val list = settingsManager.getStringSet( + this.getString(R.string.provider_lang_key), + hashSet + ) + + if (list.isNullOrEmpty()) return hashSet + return list.toHashSet() + } + + fun Context.getApiTypeSettings(): HashSet { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + val hashSet = HashSet() + hashSet.addAll(TvType.values()) + val list = settingsManager.getStringSet( + this.getString(R.string.search_types_list_key), + hashSet.map { it.name }.toMutableSet() + ) + + if (list.isNullOrEmpty()) return hashSet + + val names = TvType.values().map { it.name }.toHashSet() + val realSet = list.filter { names.contains(it) }.map { TvType.valueOf(it) }.toHashSet() + if (realSet.isEmpty()) return hashSet + + return realSet + } + + fun Context.updateHasTrailers() { + LoadResponse.isTrailersEnabled = getHasTrailers() + } + + private fun Context.getHasTrailers(): Boolean { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + return settingsManager.getBoolean(this.getString(R.string.show_trailers_key), true) + } + + fun Context.filterProviderByPreferredMedia(hasHomePageIsRequired: Boolean = true): List { + // We are getting the weirdest crash ever done: + // java.lang.ClassCastException: com.lagradost.cloudstream3.TvType cannot be cast to com.lagradost.cloudstream3.TvType + // Trying fixing using classloader fuckery + val oldLoader = Thread.currentThread().contextClassLoader + Thread.currentThread().contextClassLoader = TvType::class.java.classLoader + + val default = TvType.values() + .sorted() + .filter { it != TvType.NSFW } + .map { it.ordinal } + + Thread.currentThread().contextClassLoader = oldLoader + + val defaultSet = default.map { it.toString() }.toSet() + val currentPrefMedia = try { + PreferenceManager.getDefaultSharedPreferences(this) + .getStringSet(this.getString(R.string.prefer_media_type_key), defaultSet) + ?.mapNotNull { it.toIntOrNull() ?: return@mapNotNull null } + } catch (e: Throwable) { + null + } ?: default + val langs = this.getApiProviderLangSettings() + val hasUniversal = langs.contains(AllLanguagesName) + val allApis = synchronized(apis) { + apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) } + } + return if (currentPrefMedia.isEmpty()) { + allApis + } else { + // Filter API depending on preferred media type + allApis.filter { api -> api.supportedTypes.any { currentPrefMedia.contains(it.ordinal) } } + } + } + + fun Context.filterSearchResultByFilmQuality(data: List): List { + // Filter results omitting entries with certain quality + if (data.isNotEmpty()) { + val filteredSearchQuality = PreferenceManager.getDefaultSharedPreferences(this) + ?.getStringSet(getString(R.string.pref_filter_search_quality_key), setOf()) + ?.mapNotNull { entry -> + entry.toIntOrNull() ?: return@mapNotNull null + } ?: listOf() + if (filteredSearchQuality.isNotEmpty()) { + return data.filter { item -> + val searchQualVal = item.quality?.ordinal ?: -1 + //Log.i("filterSearch", "QuickSearch item => ${item.toJson()}") + !filteredSearchQuality.contains(searchQualVal) + } + } + } + return data + } + + fun Context.filterHomePageListByFilmQuality(data: HomePageList): HomePageList { + // Filter results omitting entries with certain quality + if (data.list.isNotEmpty()) { + val filteredSearchQuality = PreferenceManager.getDefaultSharedPreferences(this) + ?.getStringSet(getString(R.string.pref_filter_search_quality_key), setOf()) + ?.mapNotNull { entry -> + entry.toIntOrNull() ?: return@mapNotNull null + } ?: listOf() + if (filteredSearchQuality.isNotEmpty()) { + return HomePageList( + name = data.name, + isHorizontalImages = data.isHorizontalImages, + list = data.list.filter { item -> + val searchQualVal = item.quality?.ordinal ?: -1 + //Log.i("filterSearch", "QuickSearch item => ${item.toJson()}") + !filteredSearchQuality.contains(searchQualVal) + } + ) + } + } + return data + } } /* @@ -448,7 +619,7 @@ abstract class MainAPI { /**Used for testing and can be used to disable the providers if WebView is not available*/ open val usesWebView = false - /** Determines which plugin a given provider is from. This is the full path to the plugin. */ + /** Determines which plugin a given provider is from */ var sourcePlugin: String? = null open val hasMainPage = false @@ -482,7 +653,7 @@ abstract class MainAPI { //emptyList() // open val mainPage = listOf(MainPageData("", "", false)) - // @WorkerThread + @WorkerThread open suspend fun getMainPage( page: Int, request: MainPageRequest, @@ -490,17 +661,17 @@ abstract class MainAPI { throw NotImplementedError() } - // @WorkerThread + @WorkerThread open suspend fun search(query: String): List? { throw NotImplementedError() } - // @WorkerThread + @WorkerThread open suspend fun quickSearch(query: String): List? { throw NotImplementedError() } - // @WorkerThread + @WorkerThread /** * Based on data from search() or getMainPage() it generates a LoadResponse, * basically opening the info page from a link. @@ -518,13 +689,13 @@ abstract class MainAPI { * This function might be updated to include exoplayer timestamps etc in the future * if the need arises. * */ - // @WorkerThread + @WorkerThread open suspend fun extractorVerifierJob(extractorData: String?) { throw NotImplementedError() } /**Callback is fired once a link is found, will return true if method is executed successfully*/ - // @WorkerThread + @WorkerThread open suspend fun loadLinks( data: String, isCasting: Boolean, @@ -549,18 +720,31 @@ abstract class MainAPI { } /** Might need a different implementation for desktop*/ +@SuppressLint("NewApi") fun base64Decode(string: String): String { return String(base64DecodeArray(string), Charsets.ISO_8859_1) } -@OptIn(ExperimentalEncodingApi::class) + +@SuppressLint("NewApi") fun base64DecodeArray(string: String): ByteArray { - return Base64.decode(string) + return try { + android.util.Base64.decode(string, android.util.Base64.DEFAULT) + } catch (e: Exception) { + Base64.getDecoder().decode(string) + } } -@OptIn(ExperimentalEncodingApi::class) + +@SuppressLint("NewApi") fun base64Encode(array: ByteArray): String { - return Base64.encode(array) + return try { + String(android.util.Base64.encode(array, android.util.Base64.NO_WRAP), Charsets.ISO_8859_1) + } catch (e: Exception) { + String(Base64.getEncoder().encode(array)) + } } +class ErrorLoadingException(message: String? = null) : Exception(message) + fun MainAPI.fixUrlNull(url: String?): String? { if (url.isNullOrEmpty()) { return null @@ -594,6 +778,10 @@ fun sortUrls(urls: Set): List { return urls.sortedBy { t -> -t.quality } } +fun sortSubs(subs: Set): List { + return subs.sortedBy { it.name } +} + fun capitalizeString(str: String): String { return capitalizeStringNullable(str) ?: str } @@ -677,12 +865,7 @@ enum class TvType(value: Int?) { AsianDrama(9), Live(10), NSFW(11), - Others(12), - Music(13), - AudioBook(14), - - /** Wont load the built in player, make your own interaction */ - CustomMedia(15), + Others(12) } public enum class AutoDownloadMode(val value: Int) { @@ -1015,25 +1198,11 @@ interface LoadResponse { var contentRating: String? companion object { - var malIdPrefix = "" //malApi.idPrefix - var aniListIdPrefix = "" //aniListApi.idPrefix - var simklIdPrefix = "" //simklApi.idPrefix + private val malIdPrefix = malApi.idPrefix + private val aniListIdPrefix = aniListApi.idPrefix + private val simklIdPrefix = simklApi.idPrefix var isTrailersEnabled = true - /** - * The ID string is a way to keep a collection of services in one single ID using a map - * This adds a database service (like imdb) to the string and returns the new string. - */ - fun addIdToString(idString: String?, database: SimklSyncServices, id: String?): String? { - if (id == null) return idString - return (readIdFromString(idString) + mapOf(database to id)).toJson() - } - - /** Read the id string to get all other ids */ - fun readIdFromString(idString: String?): Map { - return tryParseJson(idString) ?: return emptyMap() - } - fun LoadResponse.isMovie(): Boolean { return this.type.isMovieType() || this is MovieLoadResponse } @@ -1057,12 +1226,12 @@ interface LoadResponse { * Internal helper function to add simkl ids from other databases. */ private fun LoadResponse.addSimklId( - database: SimklSyncServices, + database: SimklApi.Companion.SyncServices, id: String? ) { normalSafeApiCall { this.syncData[simklIdPrefix] = - addIdToString(this.syncData[simklIdPrefix], database, id.toString()) + SimklApi.addIdToString(this.syncData[simklIdPrefix], database, id.toString()) ?: return@normalSafeApiCall } } @@ -1082,28 +1251,28 @@ interface LoadResponse { fun LoadResponse.getImdbId(): String? { return normalSafeApiCall { - readIdFromString(this.syncData[simklIdPrefix])[SimklSyncServices.Imdb] + SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Imdb) } } fun LoadResponse.getTMDbId(): String? { return normalSafeApiCall { - readIdFromString(this.syncData[simklIdPrefix])[SimklSyncServices.Tmdb] + SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Tmdb) } } fun LoadResponse.addMalId(id: Int?) { this.syncData[malIdPrefix] = (id ?: return).toString() - this.addSimklId(SimklSyncServices.Mal, id.toString()) + this.addSimklId(SimklApi.Companion.SyncServices.Mal, id.toString()) } fun LoadResponse.addAniListId(id: Int?) { this.syncData[aniListIdPrefix] = (id ?: return).toString() - this.addSimklId(SimklSyncServices.AniList, id.toString()) + this.addSimklId(SimklApi.Companion.SyncServices.AniList, id.toString()) } fun LoadResponse.addSimklId(id: Int?) { - this.addSimklId(SimklSyncServices.Simkl, id.toString()) + this.addSimklId(SimklApi.Companion.SyncServices.Simkl, id.toString()) } fun LoadResponse.addImdbUrl(url: String?) { @@ -1185,7 +1354,7 @@ interface LoadResponse { fun LoadResponse.addImdbId(id: String?) { // TODO add imdb sync - this.addSimklId(SimklSyncServices.Imdb, id) + this.addSimklId(SimklApi.Companion.SyncServices.Imdb, id) } fun LoadResponse.addTrackId(id: String?) { @@ -1198,7 +1367,7 @@ interface LoadResponse { fun LoadResponse.addTMDbId(id: String?) { // TODO add TMDb sync - this.addSimklId(SimklSyncServices.Tmdb, id) + this.addSimklId(SimklApi.Companion.SyncServices.Tmdb, id) } fun LoadResponse.addRating(text: String?) { @@ -1277,24 +1446,11 @@ fun TvType?.isEpisodeBased(): Boolean { return (this == TvType.TvSeries || this == TvType.Anime || this == TvType.AsianDrama) } + data class NextAiring( val episode: Int, val unixTime: Long, - val season: Int? = null, -) { - /** - * Secondary constructor for backwards compatibility without season. - * TODO Remove this constructor after there is a new stable release and extensions are updated to support season. - */ - constructor( - episode: Int, - unixTime: Long, - ) : this( - episode, - unixTime, - null - ) -} +) /** * @param season To be mapped with episode season, not shown in UI if displaySeason is defined @@ -1385,26 +1541,8 @@ data class TorrentLoadResponse( posterHeaders: Map? = null, backgroundPosterUrl: String? = null, ) : this( - name, - url, - apiName, - magnet, - torrent, - plot, - type, - posterUrl, - year, - rating, - tags, - duration, - trailers, - recommendations, - actors, - comingSoon, - syncData, - posterHeaders, - backgroundPosterUrl, - null + name, url, apiName, magnet, torrent, plot, type, posterUrl, year, rating, tags, duration, trailers, + recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl, null ) } @@ -1456,8 +1594,7 @@ data class AnimeLoadResponse( return this.episodes.maxOf { (_, episodes) -> episodes.count { episodeData -> // Prioritize display season as actual season may be something random to fit multiple seasons into one. - val episodeSeason = - displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE + val episodeSeason = displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE // Count all episodes from season 1 to below the current season. episodeSeason in 1..? = null, backgroundPosterUrl: String? = null, ) : this( - engName, - japName, - name, - url, - apiName, - type, - posterUrl, - year, - episodes, - showStatus, - plot, - tags, - synonyms, - rating, - duration, - trailers, - recommendations, - actors, - comingSoon, - syncData, - posterHeaders, - nextAiring, - seasonNames, - backgroundPosterUrl, - null + engName, japName, name, url, apiName, type, posterUrl, year, episodes, showStatus, plot, tags, + synonyms, rating, duration, trailers, recommendations, actors, comingSoon, syncData, posterHeaders, + nextAiring, seasonNames, backgroundPosterUrl, null ) } @@ -1650,7 +1765,7 @@ data class MovieLoadResponse( backgroundPosterUrl: String? = null, ) : this( name, url, apiName, type, dataUrl, posterUrl, year, plot, rating, tags, duration, trailers, - recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl, null + recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl,null ) } @@ -1700,17 +1815,7 @@ suspend fun MainAPI.newMovieLoadResponse( builder.initializer() return builder } -/** Episode information that will be passed to LoadLinks function & showed on UI - * @property data string used as main LoadLinks fun parameter. - * @property name Name of the Episode. - * @property season Season number. - * @property episode Episode number. - * @property posterUrl URL of Episode's poster image. - * @property rating Episode rating. - * @property date Episode air date, see addDate. - * @property runTime Episode runtime in seconds. - * @see[addDate] - * */ + data class Episode( var data: String, var name: String? = null, @@ -1720,25 +1825,7 @@ data class Episode( var rating: Int? = null, var description: String? = null, var date: Long? = null, - var runTime: Int? = null, -) { - /** - * Secondary constructor for backwards compatibility without runTime. - * TODO Remove this constructor after there is a new stable release and extensions are updated to support runTime. - */ - constructor( - data: String, - name: String? = null, - season: Int? = null, - episode: Int? = null, - posterUrl: String? = null, - rating: Int? = null, - description: String? = null, - date: Long? = null, - ) : this( - data, name, season, episode, posterUrl, rating, description, date, null - ) -} +) fun Episode.addDate(date: String?, format: String = "yyyy-MM-dd") { try { @@ -1780,28 +1867,6 @@ fun MainAPI.newEpisode( return builder } -interface IDownloadableMinimum { - val url: String - val referer: String - val headers: Map -} - -fun IDownloadableMinimum.getId(): Int { - return url.hashCode() -} - -/** - * Set of sync services simkl is compatible with. - * Add more as required: https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id - */ -enum class SimklSyncServices(val originalName: String) { - Simkl("simkl"), - Imdb("imdb"), - Tmdb("tmdb"), - AniList("anilist"), - Mal("mal"), -} - data class TvSeriesLoadResponse( override var name: String, override var url: String, @@ -1843,8 +1908,7 @@ data class TvSeriesLoadResponse( return episodes.count { episodeData -> // Prioritize display season as actual season may be something random to fit multiple seasons into one. - val episodeSeason = - displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE + val episodeSeason = displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE // Count all episodes from season 1 to below the current season. episodeSeason in 1..? = null, backgroundPosterUrl: String? = null, ) : this( - name, - url, - apiName, - type, - episodes, - posterUrl, - year, - plot, - showStatus, - rating, - tags, - duration, - trailers, - recommendations, - actors, - comingSoon, - syncData, - posterHeaders, - nextAiring, - seasonNames, - backgroundPosterUrl, - null + name, url, apiName, type, episodes, posterUrl, year, plot, showStatus, rating, tags, duration, + trailers, recommendations, actors, comingSoon, syncData, posterHeaders, nextAiring, seasonNames, + backgroundPosterUrl, null ) } @@ -1962,7 +2007,6 @@ data class AniSearch( @JsonProperty("extraLarge") var extraLarge: String? = null, @JsonProperty("large") var large: String? = null, ) - data class Title( @JsonProperty("romaji") var romaji: String? = null, @JsonProperty("english") var english: String? = null, diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 5408d2a8..67bf19fb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -44,6 +44,9 @@ import androidx.preference.PreferenceManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearSnapHelper import androidx.recyclerview.widget.RecyclerView +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.Session import com.google.android.gms.cast.framework.SessionManager @@ -56,7 +59,9 @@ import com.google.common.collect.Comparators.min import com.jaredrummler.android.colorpicker.ColorPickerDialogListener import com.lagradost.cloudstream3.APIHolder.allProviders import com.lagradost.cloudstream3.APIHolder.apis +import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings import com.lagradost.cloudstream3.APIHolder.initAll +import com.lagradost.cloudstream3.APIHolder.updateHasTrailers import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -68,7 +73,6 @@ import com.lagradost.cloudstream3.CommonActivity.screenHeight import com.lagradost.cloudstream3.CommonActivity.setActivityInstance import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.updateLocale -import com.lagradost.cloudstream3.CommonActivity.updateTheme import com.lagradost.cloudstream3.databinding.ActivityMainBinding import com.lagradost.cloudstream3.databinding.ActivityMainTvBinding import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding @@ -83,22 +87,20 @@ import com.lagradost.cloudstream3.plugins.PluginManager.loadAllOnlinePlugins import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver import com.lagradost.cloudstream3.services.SubscriptionWorkManager -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_PLAYER -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_REPO -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_RESUME_WATCHING -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_SEARCH import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringPlayer +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringSearch import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.localListApi import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO import com.lagradost.cloudstream3.ui.home.HomeViewModel -import com.lagradost.cloudstream3.ui.library.LibraryViewModel import com.lagradost.cloudstream3.ui.player.BasicLink import com.lagradost.cloudstream3.ui.player.GeneratorPlayer import com.lagradost.cloudstream3.ui.player.LinkGenerator @@ -108,7 +110,6 @@ import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST import com.lagradost.cloudstream3.ui.result.SyncViewModel import com.lagradost.cloudstream3.ui.result.setImage import com.lagradost.cloudstream3.ui.result.setText -import com.lagradost.cloudstream3.ui.result.setTextHtml import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.search.SearchFragment import com.lagradost.cloudstream3.ui.search.SearchResultBuilder @@ -121,27 +122,20 @@ import com.lagradost.cloudstream3.ui.settings.SettingsGeneral import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions import com.lagradost.cloudstream3.utils.ApkInstaller -import com.lagradost.cloudstream3.utils.AppContextUtils.getApiDubstatusSettings -import com.lagradost.cloudstream3.utils.AppContextUtils.html -import com.lagradost.cloudstream3.utils.AppContextUtils.isCastApiAvailable -import com.lagradost.cloudstream3.utils.AppContextUtils.isLtr -import com.lagradost.cloudstream3.utils.AppContextUtils.isNetworkAvailable -import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl -import com.lagradost.cloudstream3.utils.AppContextUtils.loadCache -import com.lagradost.cloudstream3.utils.AppContextUtils.loadRepository -import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult -import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult -import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus -import com.lagradost.cloudstream3.utils.AppContextUtils.updateHasTrailers -import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback -import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback +import com.lagradost.cloudstream3.utils.AppUtils.html +import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable +import com.lagradost.cloudstream3.utils.AppUtils.isLtr +import com.lagradost.cloudstream3.utils.AppUtils.isNetworkAvailable +import com.lagradost.cloudstream3.utils.AppUtils.isRtl +import com.lagradost.cloudstream3.utils.AppUtils.loadCache +import com.lagradost.cloudstream3.utils.AppUtils.loadRepository +import com.lagradost.cloudstream3.utils.AppUtils.loadResult +import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult +import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.BackupUtils.backup import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt +import com.lagradost.cloudstream3.utils.BiometricAuthenticator import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main @@ -153,7 +147,6 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog -import com.lagradost.cloudstream3.utils.SnackbarHelper.showSnackbar import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState import com.lagradost.cloudstream3.utils.UIHelper.checkWrite import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute @@ -165,7 +158,8 @@ import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.USER_PROVIDER_API import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API -import com.lagradost.cloudstream3.utils.fcast.FcastManager +import com.lagradost.nicehttp.Requests +import com.lagradost.nicehttp.ResponseParser import com.lagradost.safefile.SafeFile import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -176,6 +170,7 @@ import java.net.URLDecoder import java.nio.charset.Charset import kotlin.math.abs import kotlin.math.absoluteValue +import kotlin.reflect.KClass import kotlin.system.exitProcess //https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898 @@ -188,92 +183,117 @@ import kotlin.system.exitProcess //https://github.com/jellyfin/jellyfin-android/blob/6cbf0edf84a3da82347c8d59b5d5590749da81a9/app/src/main/java/org/jellyfin/mobile/bridge/ExternalPlayer.kt#L225 -class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCallback { +const val VLC_PACKAGE = "org.videolan.vlc" +const val MPV_PACKAGE = "is.xyz.mpv" +const val WEB_VIDEO_CAST_PACKAGE = "com.instantbits.cast.webvideo" + +val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlayerActivity") +val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity") + +//TODO REFACTOR AF +open class ResultResume( + val packageString: String, + val action: String = Intent.ACTION_VIEW, + val position: String? = null, + val duration: String? = null, + var launcher: ActivityResultLauncher? = null, +) { + val defaultTime = -1L + + val lastId get() = "${packageString}_last_open_id" + suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) { + val intent = Intent(action) + + if (id != null) + setKey(lastId, id) + else + removeKey(lastId) + + intent.setPackage(packageString) + callback.invoke(intent) + launcher?.launch(intent) + } + + open fun getPosition(intent: Intent?): Long { + return defaultTime + } + + open fun getDuration(intent: Intent?): Long { + return defaultTime + } +} + +val VLC = object : ResultResume( + VLC_PACKAGE, + // Android 13 intent restrictions fucks up specifically launching the VLC player + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + "org.videolan.vlc.player.result" + } else { + Intent.ACTION_VIEW + }, + "extra_position", + "extra_duration", +) { + override fun getPosition(intent: Intent?): Long { + return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime + } + + override fun getDuration(intent: Intent?): Long { + return intent?.getLongExtra(this.duration, defaultTime) ?: defaultTime + } +} + +val MPV = object : ResultResume( + MPV_PACKAGE, + //"is.xyz.mpv.MPVActivity.result", // resume not working :pensive: + position = "position", + duration = "duration", +) { + override fun getPosition(intent: Intent?): Long { + return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong() ?: defaultTime + } + + override fun getDuration(intent: Intent?): Long { + return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong() ?: defaultTime + } +} + +val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE) + +val resumeApps = arrayOf( + VLC, MPV, WEB_VIDEO +) + +// Short name for requests client to make it nicer to use + +var app = Requests(responseParser = object : ResponseParser { + val mapper: ObjectMapper = jacksonObjectMapper().configure( + DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, + false + ) + + override fun parse(text: String, kClass: KClass): T { + return mapper.readValue(text, kClass.java) + } + + override fun parseSafe(text: String, kClass: KClass): T? { + return try { + mapper.readValue(text, kClass.java) + } catch (e: Exception) { + null + } + } + + override fun writeValueAsString(obj: Any): String { + return mapper.writeValueAsString(obj) + } +}).apply { + defaultHeaders = mapOf("user-agent" to USER_AGENT) +} + +class MainActivity : AppCompatActivity(), ColorPickerDialogListener, + BiometricAuthenticator.BiometricAuthCallback { companion object { - const val VLC_PACKAGE = "org.videolan.vlc" - const val MPV_PACKAGE = "is.xyz.mpv" - const val WEB_VIDEO_CAST_PACKAGE = "com.instantbits.cast.webvideo" - - val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlayerActivity") - val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity") - - //TODO REFACTOR AF - open class ResultResume( - val packageString: String, - val action: String = Intent.ACTION_VIEW, - val position: String? = null, - val duration: String? = null, - var launcher: ActivityResultLauncher? = null, - ) { - val defaultTime = -1L - - val lastId get() = "${packageString}_last_open_id" - suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) { - val intent = Intent(action) - - if (id != null) - setKey(lastId, id) - else - removeKey(lastId) - - intent.setPackage(packageString) - callback.invoke(intent) - launcher?.launch(intent) - } - - open fun getPosition(intent: Intent?): Long { - return defaultTime - } - - open fun getDuration(intent: Intent?): Long { - return defaultTime - } - } - - val VLC = object : ResultResume( - VLC_PACKAGE, - // Android 13 intent restrictions fucks up specifically launching the VLC player - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - "org.videolan.vlc.player.result" - } else { - Intent.ACTION_VIEW - }, - "extra_position", - "extra_duration", - ) { - override fun getPosition(intent: Intent?): Long { - return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime - } - - override fun getDuration(intent: Intent?): Long { - return intent?.getLongExtra(this.duration, defaultTime) ?: defaultTime - } - } - - val MPV = object : ResultResume( - MPV_PACKAGE, - //"is.xyz.mpv.MPVActivity.result", // resume not working :pensive: - position = "position", - duration = "duration", - ) { - override fun getPosition(intent: Intent?): Long { - return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong() - ?: defaultTime - } - - override fun getDuration(intent: Intent?): Long { - return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong() - ?: defaultTime - } - } - - val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE) - - val resumeApps = arrayOf( - VLC, MPV, WEB_VIDEO - ) - - const val TAG = "MAINACT" const val ANIMATED_OUTLINE: Boolean = false var lastError: String? = null @@ -351,7 +371,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa println("Repository url: $realUrl") loadRepository(realUrl) return true - } else if (str.contains(APP_STRING)) { + } else if (str.contains(appString)) { for (api in OAuth2Apis) { if (str.contains("/${api.redirectUrl}")) { ioSafe { @@ -381,15 +401,15 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } // This specific intent is used for the gradle deployWithAdb // https://github.com/recloudstream/gradle/blob/master/src/main/kotlin/com/lagradost/cloudstream3/gradle/tasks/DeployWithAdbTask.kt#L46 - if (str == "$APP_STRING:") { + if (str == "$appString:") { PluginManager.hotReloadAllLocalPlugins(activity) } - } else if (safeURI(str)?.scheme == APP_STRING_REPO) { - val url = str.replaceFirst(APP_STRING_REPO, "https") + } else if (safeURI(str)?.scheme == appStringRepo) { + val url = str.replaceFirst(appStringRepo, "https") loadRepository(url) return true - } else if (safeURI(str)?.scheme == APP_STRING_SEARCH) { - val query = str.substringAfter("$APP_STRING_SEARCH://") + } else if (safeURI(str)?.scheme == appStringSearch) { + val query = str.substringAfter("$appStringSearch://") nextSearchQuery = try { URLDecoder.decode(query, "UTF-8") @@ -403,7 +423,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa R.id.navigation_search activity?.findViewById(R.id.nav_rail_view)?.selectedItemId = R.id.navigation_search - } else if (safeURI(str)?.scheme == APP_STRING_PLAYER) { + } else if (safeURI(str)?.scheme == appStringPlayer) { val uri = Uri.parse(str) val name = uri.getQueryParameter("name") val url = URLDecoder.decode(uri.authority, "UTF-8") @@ -417,9 +437,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa ) ) ) - } else if (safeURI(str)?.scheme == APP_STRING_RESUME_WATCHING) { + } else if (safeURI(str)?.scheme == appStringResumeWatching) { val id = - str.substringAfter("$APP_STRING_RESUME_WATCHING://").toIntOrNull() + str.substringAfter("$appStringResumeWatching://").toIntOrNull() ?: return false ioSafe { val resumeWatchingCard = @@ -473,7 +493,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa ) DubStatus.Dubbed else DubStatus.Subbed, null ) } else { - viewModel.loadSmall(result) + viewModel.loadSmall(this, result) } } @@ -488,7 +508,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) updateLocale() // android fucks me by chaining lang when rotating the phone - updateTheme(this) // Update if system theme val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment @@ -573,44 +592,23 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa false } } - binding?.apply { - navRailView.isVisible = isNavVisible && landscape navView.isVisible = isNavVisible && !landscape + navRailView.isVisible = isNavVisible && landscape - /** - * We need to make sure if we return to a sub-fragment, - * the correct navigation item is selected so that it does not - * highlight the wrong one in UI. - */ - when (destination.id) { - in listOf(R.id.navigation_downloads, R.id.navigation_download_child) -> { - navRailView.menu.findItem(R.id.navigation_downloads).isChecked = true - navView.menu.findItem(R.id.navigation_downloads).isChecked = true - } - in listOf( - R.id.navigation_settings, - R.id.navigation_subtitles, - R.id.navigation_chrome_subtitles, - R.id.navigation_settings_player, - R.id.navigation_settings_updates, - R.id.navigation_settings_ui, - R.id.navigation_settings_account, - R.id.navigation_settings_providers, - R.id.navigation_settings_general, - R.id.navigation_settings_extensions, - R.id.navigation_settings_plugins, - R.id.navigation_test_providers - ) -> { - navRailView.menu.findItem(R.id.navigation_settings).isChecked = true - navView.menu.findItem(R.id.navigation_settings).isChecked = true - } - } + // Hide library on TV since it is not supported yet :( + //val isTrueTv = isTrueTvSettings() + //navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv + //navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv + + // Hide downloads on TV + //navView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv + //navRailView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv } } //private var mCastSession: CastSession? = null - var mSessionManager: SessionManager? = null + lateinit var mSessionManager: SessionManager private val mSessionManagerListener: SessionManagerListener by lazy { SessionManagerListenerImpl() } private inner class SessionManagerListenerImpl : SessionManagerListener { @@ -650,7 +648,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa setActivityInstance(this) try { if (isCastApiAvailable()) { - mSessionManager?.addSessionManagerListener(mSessionManagerListener) + //mCastSession = mSessionManager.currentCastSession + mSessionManager.addSessionManagerListener(mSessionManagerListener) } } catch (e: Exception) { logError(e) @@ -666,7 +665,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } try { if (isCastApiAvailable()) { - mSessionManager?.removeSessionManagerListener(mSessionManagerListener) + mSessionManager.removeSessionManagerListener(mSessionManagerListener) //mCastSession = null } } catch (e: Exception) { @@ -674,7 +673,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } } - override fun dispatchKeyEvent(event: KeyEvent): Boolean { + override fun dispatchKeyEvent(event: KeyEvent?): Boolean { val response = CommonActivity.dispatchKeyEvent(this, event) if (response != null) return response @@ -770,7 +769,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa list.forEach { custom -> allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass } ?.let { - allProviders.add(it.javaClass.getDeclaredConstructor().newInstance().apply { + allProviders.add(it.javaClass.newInstance().apply { name = custom.name lang = custom.lang mainUrl = custom.url.trimEnd('/') @@ -793,14 +792,14 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa lateinit var viewModel: ResultViewModel2 lateinit var syncViewModel: SyncViewModel - private var libraryViewModel: LibraryViewModel? = null /** kinda dirty, however it signals that we should use the watch status as sync or not*/ var isLocalList: Boolean = false override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? { - - viewModel = ViewModelProvider(this)[ResultViewModel2::class.java] - syncViewModel = ViewModelProvider(this)[SyncViewModel::class.java] + viewModel = + ViewModelProvider(this)[ResultViewModel2::class.java] + syncViewModel = + ViewModelProvider(this)[SyncViewModel::class.java] return super.onCreateView(name, context, attrs) } @@ -1151,7 +1150,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa super.onCreate(savedInstanceState) try { if (isCastApiAvailable()) { - CastContext.getSharedInstance(this) {it.run()}.addOnSuccessListener { mSessionManager = it.sessionManager } + mSessionManager = CastContext.getSharedInstance(this).sessionManager } } catch (t: Throwable) { logError(t) @@ -1232,17 +1231,18 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa changeStatusBarState(isLayout(EMULATOR)) /** Biometric stuff for users without accounts **/ + val authEnabled = settingsManager.getBoolean(getString(R.string.biometric_key), false) val noAccounts = settingsManager.getBoolean( getString(R.string.skip_startup_account_select_key), false ) || accounts.count() <= 1 - if (isLayout(PHONE) && isAuthEnabled(this) && noAccounts) { + if (isLayout(PHONE) && authEnabled && noAccounts) { if (deviceHasPasswordPinLock(this)) { startBiometricAuthentication(this, R.string.biometric_authentication_title, false) - promptInfo?.let { prompt -> - biometricPrompt?.authenticate(prompt) + BiometricAuthenticator.promptInfo?.let { promt -> + BiometricAuthenticator.biometricPrompt?.authenticate(promt) } // hide background while authenticating, Sorry moms & dads 🙏 @@ -1257,12 +1257,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa this.setKey(getString(R.string.jsdelivr_proxy_key), false) } else { this.setKey(getString(R.string.jsdelivr_proxy_key), true) - showSnackbar( - this@MainActivity, - R.string.jsdelivr_enabled, - Snackbar.LENGTH_LONG, - R.string.revert - ) { setKey(getString(R.string.jsdelivr_proxy_key), false) } + val parentView: View = findViewById(android.R.id.content) + Snackbar.make(parentView, R.string.jsdelivr_enabled, Snackbar.LENGTH_LONG) + .let { snackbar -> + snackbar.setAction(R.string.revert) { + setKey(getString(R.string.jsdelivr_proxy_key), false) + } + snackbar.setBackgroundTint(colorFromAttribute(R.attr.primaryGrayBackground)) + snackbar.setTextColor(colorFromAttribute(R.attr.textColor)) + snackbar.setActionTextColor(colorFromAttribute(R.attr.colorPrimary)) + snackbar.show() + } } } } @@ -1395,7 +1400,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } } - observe(viewModel.watchStatus, ::setWatchStatus) + observe(viewModel.watchStatus,::setWatchStatus) observe(syncViewModel.userData, ::setUserData) observeNullable(viewModel.subscribeStatus, ::setSubscribeStatus) @@ -1433,7 +1438,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa resultviewPreviewMetaDuration.setText(d.durationText) resultviewPreviewMetaRating.setText(d.ratingText) - resultviewPreviewDescription.setTextHtml(d.plotText) + resultviewPreviewDescription.setText(d.plotText) resultviewPreviewPoster.setImage( d.posterImage ?: d.posterBackgroundImage ) @@ -1448,13 +1453,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa val value = viewModel.watchStatus.value ?: WatchType.NONE this@MainActivity.showBottomDialog( - WatchType.entries.map { getString(it.stringRes) }.toList(), + WatchType.values().map { getString(it.stringRes) }.toList(), value.ordinal, this@MainActivity.getString(R.string.action_add_to_bookmarks), showApply = false, {}) { viewModel.updateWatchStatus( - WatchType.entries[it], + WatchType.values()[it], this@MainActivity ) } @@ -1464,12 +1469,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa ?: SyncWatchType.NONE this@MainActivity.showBottomDialog( - SyncWatchType.entries.map { getString(it.stringRes) }.toList(), + SyncWatchType.values().map { getString(it.stringRes) }.toList(), value.ordinal, this@MainActivity.getString(R.string.action_add_to_bookmarks), showApply = false, {}) { - syncViewModel.setStatus(SyncWatchType.entries[it].internalId) + syncViewModel.setStatus(SyncWatchType.values()[it].internalId) syncViewModel.publishUserData() } } @@ -1550,26 +1555,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa logError(e) } } - - // we need to run this after we init all apis, otherwise currentSyncApi will fuck itself - this@MainActivity.runOnUiThread { - // Change library icon with logo of current api in sync - libraryViewModel = ViewModelProvider(this@MainActivity)[LibraryViewModel::class.java] - libraryViewModel?.currentApiName?.observe(this@MainActivity) { - val syncAPI = libraryViewModel?.currentSyncApi - Log.i("SYNC_API", "${syncAPI?.name}, ${syncAPI?.idPrefix}") - val icon = if (syncAPI?.idPrefix == localListApi.idPrefix) { - R.drawable.library_icon - } else { - syncAPI?.icon ?: R.drawable.library_icon - } - - binding?.apply { - navRailView.menu.findItem(R.id.navigation_library)?.setIcon(icon) - navView.menu.findItem(R.id.navigation_library)?.setIcon(icon) - } - } - } } SearchResultBuilder.updateCache(this) @@ -1601,12 +1586,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa if (isLayout(TV or EMULATOR)) { if (navDestination.matchDestination(R.id.navigation_home)) { - attachBackPressedCallback { - showConfirmExitDialog() - window?.navigationBarColor = - colorFromAttribute(R.attr.primaryGrayBackground) - updateLocale() - } + attachBackPressedCallback() } else detachBackPressedCallback() } } @@ -1774,8 +1754,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa runAutoUpdate() } - FcastManager().init(this, false) - APIRepository.dubStatusActive = getApiDubstatusSettings() try { @@ -1847,8 +1825,26 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa binding?.navHostFragment?.isInvisible = false } - override fun onAuthenticationError() { - finish() + private var backPressedCallback: OnBackPressedCallback? = null + + private fun attachBackPressedCallback() { + if (backPressedCallback == null) { + backPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + showConfirmExitDialog() + window?.navigationBarColor = + colorFromAttribute(R.attr.primaryGrayBackground) + updateLocale() + } + } + } + + backPressedCallback?.isEnabled = true + onBackPressedDispatcher.addCallback(this, backPressedCallback ?: return) + } + + private fun detachBackPressedCallback() { + backPressedCallback?.isEnabled = false } suspend fun checkGithubConnectivity(): Boolean { @@ -1861,4 +1857,4 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa false } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt new file mode 100644 index 00000000..7be90440 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt @@ -0,0 +1,53 @@ +package com.lagradost.cloudstream3 + +import com.lagradost.cloudstream3.MainActivity.Companion.lastError +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.plugins.PluginManager.checkSafeModeFile +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +object NativeCrashHandler { + // external fun triggerNativeCrash() + /*private external fun initNativeCrashHandler() + private external fun getSignalStatus(): Int + + private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch { + + //launch { + // delay(10000) + // triggerNativeCrash() + //} + + while (true) { + delay(10_000) + val signal = getSignalStatus() + // Signal is initialized to zero + if (signal == 0) continue + + // Do not crash in safe mode! + if (lastError != null) continue + if (checkSafeModeFile()) continue + + AcraApplication.exceptionHandler?.uncaughtException( + Thread.currentThread(), + RuntimeException("Native crash with code: $signal. Try uninstalling extensions.\n") + ) + } + } + + fun initCrashHandler() { + try { + System.loadLibrary("native-lib") + initNativeCrashHandler() + } catch (t: Throwable) { + // Make debug crash. + if (BuildConfig.DEBUG) throw t + logError(t) + return + } + + initSignalPolling() + }*/ +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/ParCollections.kt b/app/src/main/java/com/lagradost/cloudstream3/ParCollections.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/ParCollections.kt rename to app/src/main/java/com/lagradost/cloudstream3/ParCollections.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AStreamHub.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/AStreamHub.kt similarity index 97% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AStreamHub.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/AStreamHub.kt index 23f8dcf4..b0051ba7 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AStreamHub.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/AStreamHub.kt @@ -1,6 +1,6 @@ package com.lagradost.cloudstream3.extractors -import com.lagradost.api.Log +import android.util.Log import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Acefile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Acefile.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Acefile.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Acefile.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AsianLoad.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AsianLoad.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Blogger.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Blogger.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Blogger.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Blogger.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/BullStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/BullStream.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/BullStream.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/BullStream.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByteShare.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/ByteShare.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByteShare.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/ByteShare.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Cda.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Cda.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Chillx.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt similarity index 68% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Chillx.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt index dd22efb2..f03a5525 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Chillx.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt @@ -2,11 +2,12 @@ package com.lagradost.cloudstream3.extractors import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.extractors.helper.* import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler +import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper -import kotlin.run class Moviesapi : Chillx() { override val name = "Moviesapi" @@ -27,44 +28,30 @@ open class Chillx : ExtractorApi() { override val name = "Chillx" override val mainUrl = "https://chillx.top" override val requiresReferer = true + private var key: String? = null - companion object { - private val keySource = "https://rowdy-avocado.github.io/multi-keys/" - - private var key: String? = null - - private suspend fun fetchKey(): String { - return key - ?: run { - val res = - app.get(keySource).parsedSafe() - ?: throw ErrorLoadingException("Unable to get keys") - key = res.keys.get(0) - res.keys.get(0) - } - } - - private data class KeysData(@JsonProperty("chillx") val keys: List) - } - - @Suppress("NAME_SHADOWING") override suspend fun getUrl( url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val master = Regex("""JScript[\w+]?\s*=\s*'([^']+)""").find( + val master = Regex("\\s*=\\s*'([^']+)").find( app.get( url, - referer = url, + referer = referer ?: "", + headers = mapOf( + "Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "Accept-Language" to "en-US,en;q=0.5", + ) ).text )?.groupValues?.get(1) - val key = fetchKey() - val decrypt = cryptoAESHandler(master ?: "", key.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") + val decrypt = cryptoAESHandler(master ?: return, getKey().toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") + val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1) + val subtitles = Regex("""subtitle"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1) - val subtitlePattern = """\[(.*?)](https?://[^\s,]+)""".toRegex() + val subtitlePattern = """\[(.*?)\](https?://[^\s,]+)""".toRegex() val matches = subtitlePattern.findAll(subtitles ?: "") val languageUrlPairs = matches.map { matchResult -> val (language, url) = matchResult.destructured @@ -96,11 +83,23 @@ open class Chillx : ExtractorApi() { headers = headers ).forEach(callback) } - + private fun decodeUnicodeEscape(input: String): String { val regex = Regex("u([0-9a-fA-F]{4})") return regex.replace(input) { it.groupValues[1].toInt(16).toChar().toString() } } + + suspend fun getKey() = key ?: fetchKey().also { key = it } + + private suspend fun fetchKey(): String { + return app.get("https://raw.githubusercontent.com/Sofie99/Resources/main/chillix_key.json").parsed() + } + + data class Tracks( + @JsonProperty("file") val file: String? = null, + @JsonProperty("label") val label: String? = null, + @JsonProperty("kind") val kind: String? = null, + ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt new file mode 100644 index 00000000..b7f84af1 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt @@ -0,0 +1,69 @@ +// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır. + +package com.lagradost.cloudstream3.extractors + +import android.util.Log +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.* + +open class ContentX : ExtractorApi() { + override val name = "ContentX" + override val mainUrl = "https://contentx.me" + override val requiresReferer = true + + override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { + val ext_ref = referer ?: "" + Log.d("Kekik_${this.name}", "url » ${url}") + + val i_source = app.get(url, referer=ext_ref).text + val i_extract = Regex("""window\.openPlayer\('([^']+)'""").find(i_source)!!.groups[1]?.value ?: throw ErrorLoadingException("i_extract is null") + + val sub_urls = mutableSetOf() + Regex("""\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(i_source).forEach { + val (sub_url, sub_lang) = it.destructured + + if (sub_url in sub_urls) { return@forEach } + sub_urls.add(sub_url) + + subtitleCallback.invoke( + SubtitleFile( + lang = sub_lang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), + url = fixUrl(sub_url.replace("\\", "")) + ) + ) + } + + val vid_source = app.get("${mainUrl}/source2.php?v=${i_extract}", referer=ext_ref).text + val vid_extract = Regex("""file\":\"([^\"]+)""").find(vid_source)!!.groups[1]?.value ?: throw ErrorLoadingException("vid_extract is null") + val m3u_link = vid_extract.replace("\\", "") + + callback.invoke( + ExtractorLink( + source = this.name, + name = this.name, + url = m3u_link, + referer = url, + quality = Qualities.Unknown.value, + isM3u8 = true + ) + ) + + val i_dublaj = Regex(""",\"([^']+)\",\"Türkçe""").find(i_source)!!.groups[1]?.value + if (i_dublaj != null) { + val dublaj_source = app.get("${mainUrl}/source2.php?v=${i_dublaj}", referer=ext_ref).text + val dublaj_extract = Regex("""file\":\"([^\"]+)""").find(dublaj_source)!!.groups[1]?.value ?: throw ErrorLoadingException("dublaj_extract is null") + val dublaj_link = dublaj_extract.replace("\\", "") + + callback.invoke( + ExtractorLink( + source = "${this.name} Türkçe Dublaj", + name = "${this.name} Türkçe Dublaj", + url = dublaj_link, + referer = url, + quality = Qualities.Unknown.value, + isM3u8 = true + ) + ) + } + } +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Dailymotion.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt similarity index 84% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Dailymotion.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt index 2343a92e..0df93dc5 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Dailymotion.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt @@ -9,16 +9,10 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 import java.net.URL -class Geodailymotion : Dailymotion() { - override val name = "GeoDailymotion" - override val mainUrl = "https://geo.dailymotion.com" -} - open class Dailymotion : ExtractorApi() { override val mainUrl = "https://www.dailymotion.com" override val name = "Dailymotion" override val requiresReferer = false - private val baseUrl = "https://www.dailymotion.com" @Suppress("RegExpSimplifiable") private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex() @@ -40,7 +34,7 @@ open class Dailymotion : ExtractorApi() { val dmV1st = config.dmInternalData.v1st val dmTs = config.dmInternalData.ts val embedder = config.context.embedder - val metaDataUrl = "$baseUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0" + val metaDataUrl = "$mainUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0" val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = req.cookies) .parsedSafe() ?: return metaData.qualities.forEach { (_, video) -> @@ -51,19 +45,16 @@ open class Dailymotion : ExtractorApi() { } private fun getEmbedUrl(url: String): String? { - if (url.contains("/embed/") || url.contains("/video/")) { - return url + if (url.contains("/embed/")) { + return url + } + val vid = getVideoId(url) ?: return null + return "$mainUrl/embed/video/$vid" } - if (url.contains("geo.dailymotion.com")) { - val videoId = url.substringAfter("video=") - return "$baseUrl/embed/video/$videoId" - } - return null - } private fun getVideoId(url: String): String? { val path = URL(url).path - val id = path.substringAfter("/video/") + val id = path.substringAfter("video/") if (id.matches(videoIdRegex)) { return id } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt similarity index 73% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt index 370dcaca..8dcfb859 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt @@ -7,18 +7,6 @@ import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.getQualityFromName import kotlinx.coroutines.delay -class D0000d : DoodLaExtractor() { - override var mainUrl = "https://d0000d.com" -} - -class D000dCom : DoodLaExtractor() { - override var mainUrl = "https://d000d.com" -} - -class DoodstreamCom : DoodLaExtractor() { - override var mainUrl = "https://doodstream.com" -} - class Dooood : DoodLaExtractor() { override var mainUrl = "https://dooood.com" } @@ -68,10 +56,9 @@ open class DoodLaExtractor : ExtractorApi() { } override suspend fun getUrl(url: String, referer: String?): List? { - val newUrl= url.replace(mainUrl, "https://d0000d.com") - val response0 = app.get(newUrl).text // html of DoodStream page to look for /pass_md5/... - val md5 ="https://d0000d.com"+(Regex("/pass_md5/[^']*").find(response0)?.value ?: return null) // get https://dood.ws/pass_md5/... - val trueUrl = app.get(md5, referer = newUrl).text + "zUEJeL3mUN?token=" + md5.substringAfterLast("/") //direct link to extract (zUEJeL3mUN is random) + val response0 = app.get(url).text // html of DoodStream page to look for /pass_md5/... + val md5 =mainUrl+(Regex("/pass_md5/[^']*").find(response0)?.value ?: return null) // get https://dood.ws/pass_md5/... + val trueUrl = app.get(md5, referer = url).text + "zUEJeL3mUN?token=" + md5.substringAfterLast("/") //direct link to extract (zUEJeL3mUN is random) val quality = Regex("\\d{3,4}p").find(response0.substringAfter("").substringBefore(""))?.groupValues?.get(0) return listOf( ExtractorLink( diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Embedgram.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Embedgram.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Embedgram.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Embedgram.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/EmturbovidExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/EmturbovidExtractor.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/EmturbovidExtractor.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/EmturbovidExtractor.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Evolaod.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Evolaod.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Evolaod.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Evolaod.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Fastream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Fastream.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Fastream.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Fastream.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Filesim.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Filesim.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GMPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/GMPlayer.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GMPlayer.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/GMPlayer.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GenericM3U8.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/GenericM3U8.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GenericM3U8.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/GenericM3U8.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gofile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gofile.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GuardareStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GuardareStream.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt similarity index 76% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt index 1152cb4b..03586386 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import com.lagradost.api.Log +import android.util.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.extractors.helper.AesHelper @@ -16,23 +16,24 @@ open class HDMomPlayer : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val m3uLink:String? - val extRef = referer ?: "" - val iSource = app.get(url, referer=extRef).text + val m3u_link:String? + val ext_ref = referer ?: "" + val i_source = app.get(url, referer=ext_ref).text - val bePlayer = Regex("""bePlayer\('([^']+)',\s*'(\{[^\}]+\})'\);""").find(iSource)?.groupValues + val bePlayer = Regex("""bePlayer\('([^']+)',\s*'(\{[^\}]+\})'\);""").find(i_source)?.groupValues if (bePlayer != null) { val bePlayerPass = bePlayer.get(1) val bePlayerData = bePlayer.get(2) val encrypted = AesHelper.cryptoAESHandler(bePlayerData, bePlayerPass.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") + Log.d("Kekik_${this.name}", "encrypted » ${encrypted}") - m3uLink = Regex("""video_location\":\"([^\"]+)""").find(encrypted)?.groupValues?.get(1) + m3u_link = Regex("""video_location\":\"([^\"]+)""").find(encrypted)?.groupValues?.get(1) } else { - m3uLink = Regex("""file:\"([^\"]+)""").find(iSource)?.groupValues?.get(1) + m3u_link = Regex("""file:\"([^\"]+)""").find(i_source)?.groupValues?.get(1) - val trackStr = Regex("""tracks:\[([^\]]+)""").find(iSource)?.groupValues?.get(1) - if (trackStr != null) { - val tracks:List = jacksonObjectMapper().readValue("[${trackStr}]") + val track_str = Regex("""tracks:\[([^\]]+)""").find(i_source)?.groupValues?.get(1) + if (track_str != null) { + val tracks:List = jacksonObjectMapper().readValue("[${track_str}]") for (track in tracks) { if (track.file == null || track.label == null) continue @@ -52,7 +53,7 @@ open class HDMomPlayer : ExtractorApi() { ExtractorLink( source = this.name, name = this.name, - url = m3uLink ?: throw ErrorLoadingException("m3u link not found"), + url = m3u_link ?: throw ErrorLoadingException("m3u link not found"), referer = url, quality = Qualities.Unknown.value, isM3u8 = true diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt similarity index 70% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt index e3cf3aee..14333d35 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import com.lagradost.api.Log +import android.util.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty @@ -13,36 +13,37 @@ open class HDPlayerSystem : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val extRef = referer ?: "" - val vidId = if (url.contains("video/")) { + val ext_ref = referer ?: "" + val vid_id = if (url.contains("video/")) { url.substringAfter("video/") } else { url.substringAfter("?data=") } - val postUrl = "${mainUrl}/player/index.php?data=${vidId}&do=getVideo" + val post_url = "${mainUrl}/player/index.php?data=${vid_id}&do=getVideo" + Log.d("Kekik_${this.name}", "post_url » ${post_url}") val response = app.post( - postUrl, + post_url, data = mapOf( - "hash" to vidId, - "r" to extRef + "hash" to vid_id, + "r" to ext_ref ), - referer = extRef, + referer = ext_ref, headers = mapOf( "Content-Type" to "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With" to "XMLHttpRequest" ) ) - val videoResponse = response.parsedSafe() ?: throw ErrorLoadingException("failed to parse response") - val m3uLink = videoResponse.securedLink + val video_response = response.parsedSafe() ?: throw ErrorLoadingException("failed to parse response") + val m3u_link = video_response.securedLink callback.invoke( ExtractorLink( source = this.name, name = this.name, - url = m3uLink, - referer = extRef, + url = m3u_link, + referer = ext_ref, quality = Qualities.Unknown.value, type = INFER_TYPE ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDStreamAbleExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/HDStreamAbleExtractor.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDStreamAbleExtractor.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/HDStreamAbleExtractor.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt similarity index 70% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt index 11f8ccaf..b557a53e 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt @@ -20,14 +20,4 @@ class PlayRu : ContentX() { class FourPlayRu : ContentX() { override var name = "FourPlayRu" override var mainUrl = "https://four.playru.net" -} - -class Pichive : ContentX() { - override var name = "Pichive" - override var mainUrl = "https://pichive.online" -} - -class FourPichive : ContentX() { - override var name = "FourPichive" - override var mainUrl = "https://four.pichive.online" -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Hxfile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Hxfile.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Hxfile.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Hxfile.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/JWPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/JWPlayer.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/JWPlayer.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/JWPlayer.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jawcloud.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Jawcloud.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jawcloud.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Jawcloud.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jeniusplay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Jeniusplay.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jeniusplay.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Jeniusplay.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Krakenfiles.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Krakenfiles.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Krakenfiles.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Krakenfiles.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Linkbox.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Linkbox.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Linkbox.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Linkbox.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt similarity index 62% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt index 07346c70..766c7762 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import com.lagradost.api.Log +import android.util.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty @@ -13,25 +13,28 @@ open class MailRu : ExtractorApi() { override val requiresReferer = false override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val extRef = referer ?: "" + val ext_ref = referer ?: "" + Log.d("Kekik_${this.name}", "url » ${url}") - val vidId = url.substringAfter("video/embed/").trim() - val videoReq = app.get("${mainUrl}/+/video/meta/${vidId}", referer=url) - val videoKey = videoReq.cookies["video_key"].toString() + val vid_id = url.substringAfter("video/embed/").trim() + val video_req = app.get("${mainUrl}/+/video/meta/${vid_id}", referer=url) + val video_key = video_req.cookies["video_key"].toString() + Log.d("Kekik_${this.name}", "video_key » ${video_key}") - val videoData = AppUtils.tryParseJson(videoReq.text) ?: throw ErrorLoadingException("Video not found") + val video_data = AppUtils.tryParseJson(video_req.text) ?: throw ErrorLoadingException("Video not found") - for (video in videoData.videos) { + for (video in video_data.videos) { + Log.d("Kekik_${this.name}", "video » ${video}") - val videoUrl = if (video.url.startsWith("//")) "https:${video.url}" else video.url + val video_url = if (video.url.startsWith("//")) "https:${video.url}" else video.url callback.invoke( ExtractorLink( source = this.name, name = this.name, - url = videoUrl, + url = video_url, referer = url, - headers = mapOf("Cookie" to "video_key=${videoKey}"), + headers = mapOf("Cookie" to "video_key=${video_key}"), quality = getQualityFromName(video.key), isM3u8 = false ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Maxstream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Maxstream.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Maxstream.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Maxstream.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mediafire.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Mediafire.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mediafire.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Mediafire.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Minoplres.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Minoplres.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Minoplres.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Minoplres.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MixDrop.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/MixDrop.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MixDrop.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/MixDrop.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Moviehab.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Moviehab.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mp4Upload.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mp4Upload.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MultiQuality.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MultiQuality.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mvidoo.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Mvidoo.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mvidoo.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Mvidoo.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt similarity index 65% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt index 31b3d50b..46f6ad0f 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import com.lagradost.api.Log +import android.util.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty @@ -13,20 +13,22 @@ open class Odnoklassniki : ExtractorApi() { override val requiresReferer = false override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val extRef = referer ?: "" + val ext_ref = referer ?: "" + Log.d("Kekik_${this.name}", "url » ${url}") - val userAgent = mapOf("User-Agent" to "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36") + val user_agent = mapOf("User-Agent" to "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36") - val videoReq = app.get(url, headers=userAgent).text.replace("\\"", "\"").replace("\\\\", "\\") + val video_req = app.get(url, headers=user_agent).text.replace("\\"", "\"").replace("\\\\", "\\") .replace(Regex("\\\\u([0-9A-Fa-f]{4})")) { matchResult -> Integer.parseInt(matchResult.groupValues[1], 16).toChar().toString() } - val videosStr = Regex("""\"videos\":(\[[^\]]*\])""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("Video not found") - val videos = AppUtils.tryParseJson>(videosStr) ?: throw ErrorLoadingException("Video not found") + val videos_str = Regex("""\"videos\":(\[[^\]]*\])""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("Video not found") + val videos = AppUtils.tryParseJson>(videos_str) ?: throw ErrorLoadingException("Video not found") for (video in videos) { + Log.d("Kekik_${this.name}", "video » ${video}") - val videoUrl = if (video.url.startsWith("//")) "https:${video.url}" else video.url + val video_url = if (video.url.startsWith("//")) "https:${video.url}" else video.url val quality = video.name.uppercase() .replace("MOBILE", "144p") @@ -42,10 +44,10 @@ open class Odnoklassniki : ExtractorApi() { ExtractorLink( source = this.name, name = this.name, - url = videoUrl, + url = video_url, referer = url, quality = getQualityFromName(quality), - headers = userAgent, + headers = user_agent, isM3u8 = false ) ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OkRuExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/OkRuExtractor.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OkRuExtractor.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/OkRuExtractor.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Okrulink.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Okrulink.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Okrulink.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Okrulink.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt similarity index 68% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt index 3a5cf727..b57449bf 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import com.lagradost.api.Log +import android.util.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty @@ -13,38 +13,39 @@ open class PeaceMakerst : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val m3uLink:String? - val extRef = referer ?: "" - val postUrl = "${url}?do=getVideo" + val m3u_link:String? + val ext_ref = referer ?: "" + val post_url = "${url}?do=getVideo" + Log.d("Kekik_${this.name}", "post_url » ${post_url}") val response = app.post( - postUrl, + post_url, data = mapOf( "hash" to url.substringAfter("video/"), - "r" to extRef, + "r" to ext_ref, "s" to "" ), - referer = extRef, + referer = ext_ref, headers = mapOf( "Content-Type" to "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With" to "XMLHttpRequest" ) ) if (response.text.contains("teve2.com.tr\\/embed\\/")) { - val teve2Id = response.text.substringAfter("teve2.com.tr\\/embed\\/").substringBefore("\"") - val teve2Response = app.get( - "https://www.teve2.com.tr/action/media/${teve2Id}", - referer = "https://www.teve2.com.tr/embed/${teve2Id}" + val teve2_id = response.text.substringAfter("teve2.com.tr\\/embed\\/").substringBefore("\"") + val teve2_response = app.get( + "https://www.teve2.com.tr/action/media/${teve2_id}", + referer = "https://www.teve2.com.tr/embed/${teve2_id}" ).parsedSafe() ?: throw ErrorLoadingException("teve2 response is null") - m3uLink = teve2Response.media.link.serviceUrl + "//" + teve2Response.media.link.securePath + m3u_link = teve2_response.media.link.serviceUrl + "//" + teve2_response.media.link.securePath } else { - val videoResponse = response.parsedSafe() ?: throw ErrorLoadingException("peace response is null") - val videoSources = videoResponse.videoSources - if (videoSources.isNotEmpty()) { - m3uLink = videoSources.lastOrNull()?.file + val video_response = response.parsedSafe() ?: throw ErrorLoadingException("peace response is null") + val video_sources = video_response.videoSources + if (video_sources.isNotEmpty()) { + m3u_link = video_sources.lastOrNull()?.file } else { - m3uLink = null + m3u_link = null } } @@ -52,8 +53,8 @@ open class PeaceMakerst : ExtractorApi() { ExtractorLink( source = this.name, name = this.name, - url = m3uLink ?: throw ErrorLoadingException("m3u link not found"), - referer = extRef, + url = m3u_link ?: throw ErrorLoadingException("m3u link not found"), + referer = ext_ref, quality = Qualities.Unknown.value, type = INFER_TYPE ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Pelisplus.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Pelisplus.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PixelDrainExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/PixelDrainExtractor.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PixelDrainExtractor.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/PixelDrainExtractor.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt similarity index 99% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt index a4dc694e..2b286abb 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt @@ -1,6 +1,6 @@ package com.lagradost.cloudstream3.extractors -import com.lagradost.api.Log +import android.util.Log import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.* diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt new file mode 100644 index 00000000..a0d830cf --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt @@ -0,0 +1,50 @@ +// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır. + +package com.lagradost.cloudstream3.extractors + +import android.util.Log +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.* + +open class RapidVid : ExtractorApi() { + override val name = "RapidVid" + override val mainUrl = "https://rapidvid.net" + override val requiresReferer = true + + override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { + val ext_ref = referer ?: "" + val video_req = app.get(url, referer=ext_ref).text + + val sub_urls = mutableSetOf() + Regex("""captions\",\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(video_req).forEach { + val (sub_url, sub_lang) = it.destructured + + if (sub_url in sub_urls) { return@forEach } + sub_urls.add(sub_url) + + subtitleCallback.invoke( + SubtitleFile( + lang = sub_lang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), + url = fixUrl(sub_url.replace("\\", "")) + ) + ) + } + + val extracted_value = Regex("""file": "(.*)",""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") + + val bytes = extracted_value.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray() + val decoded = String(bytes, Charsets.UTF_8) + Log.d("Kekik_${this.name}", "decoded » ${decoded}") + + callback.invoke( + ExtractorLink( + source = this.name, + name = this.name, + url = decoded, + referer = ext_ref, + quality = Qualities.Unknown.value, + isM3u8 = true + ) + ) + } +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SBPlay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/SBPlay.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SBPlay.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/SBPlay.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Sendvid.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Sendvid.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Sendvid.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Sendvid.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt similarity index 65% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt index 89f731f7..a8bcee31 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import com.lagradost.api.Log +import android.util.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* @@ -12,17 +12,18 @@ open class SibNet : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val extRef = referer ?: "" - val iSource = app.get(url, referer=extRef).text - var m3uLink = Regex("""player.src\(\[\{src: \"([^\"]+)""").find(iSource)?.groupValues?.get(1) ?: throw ErrorLoadingException("m3u link not found") + val ext_ref = referer ?: "" + val i_source = app.get(url, referer=ext_ref).text + var m3u_link = Regex("""player.src\(\[\{src: \"([^\"]+)""").find(i_source)?.groupValues?.get(1) ?: throw ErrorLoadingException("m3u link not found") - m3uLink = "${mainUrl}${m3uLink}" + m3u_link = "${mainUrl}${m3u_link}" + Log.d("Kekik_${this.name}", "m3u_link » ${m3u_link}") callback.invoke( ExtractorLink( source = this.name, name = this.name, - url = m3uLink, + url = m3u_link, referer = url, quality = Qualities.Unknown.value, type = INFER_TYPE diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Solidfiles.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Solidfiles.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Solidfiles.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Solidfiles.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamSB.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamSB.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamTape.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamTape.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamTape.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/StreamTape.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt new file mode 100644 index 00000000..77d98e49 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt @@ -0,0 +1,34 @@ +package com.lagradost.cloudstream3.extractors + +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.network.WebViewResolver +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities + +open class StreamWishExtractor : ExtractorApi() { + override var name = "StreamWish" + override var mainUrl = "https://streamwish.to" + override val requiresReferer = false + + override suspend fun getUrl(url: String, referer: String?): List? { + val response = app.get( + url, referer = referer ?: "$mainUrl/", interceptor = WebViewResolver( + Regex("""master\.m3u8""") + ) + ) + val sources = mutableListOf() + if (response.url.contains("m3u8")) + sources.add( + ExtractorLink( + source = name, + name = name, + url = response.url, + referer = referer ?: "$mainUrl/", + quality = Qualities.Unknown.value, + isM3u8 = true + ) + ) + return sources + } +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamhub.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamhub.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamhub.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Streamhub.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamlare.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamlare.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamlare.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Streamlare.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamoUpload.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamoUpload.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamoUpload.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/StreamoUpload.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamplay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamplay.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamplay.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Streamplay.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Supervideo.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Supervideo.kt similarity index 97% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Supervideo.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Supervideo.kt index e70cae6b..dd49d994 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Supervideo.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Supervideo.kt @@ -13,7 +13,7 @@ data class Files( open class Supervideo : ExtractorApi() { override var name = "Supervideo" - override var mainUrl = "https://supervideo.cc" + override var mainUrl = "https://supervideo.tv" override val requiresReferer = false override suspend fun getUrl(url: String, referer: String?): List? { val extractedLinksList: MutableList = mutableListOf() diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt similarity index 66% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt index f2a75b94..645d7c0e 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import com.lagradost.api.Log +import android.util.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty @@ -13,13 +13,13 @@ open class TRsTX : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val extRef = referer ?: "" + val ext_ref = referer ?: "" - val videoReq = app.get(url, referer=extRef).text + val video_req = app.get(url, referer=ext_ref).text - val file = Regex("""file\":\"([^\"]+)""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") + val file = Regex("""file\":\"([^\"]+)""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") val postLink = "${mainUrl}/" + file.replace("\\", "") - val rawList = app.post(postLink, referer=extRef).parsedSafe>() ?: throw ErrorLoadingException("Post link not found") + val rawList = app.post(postLink, referer=ext_ref).parsedSafe>() ?: throw ErrorLoadingException("Post link not found") val postJson: List = rawList.drop(1).map { item -> val mapItem = item as Map<*, *> @@ -28,35 +28,37 @@ open class TRsTX : ExtractorApi() { file = mapItem["file"] as? String ) } + Log.d("Kekik_${this.name}", "postJson » ${postJson}") - val vidLinks = mutableSetOf() - val vidMap = mutableListOf>() + val vid_links = mutableSetOf() + val vid_map = mutableListOf>() for (item in postJson) { if (item.file == null || item.title == null) continue val fileUrl = "${mainUrl}/playlist/" + item.file.substring(1) + ".txt" - val videoData = app.post(fileUrl, referer=extRef).text + val videoData = app.post(fileUrl, referer=ext_ref).text - if (videoData in vidLinks) { continue } - vidLinks.add(videoData) + if (videoData in vid_links) { continue } + vid_links.add(videoData) - vidMap.add(mapOf( + vid_map.add(mapOf( "title" to item.title, "videoData" to videoData )) } - for (mapEntry in vidMap) { + for (mapEntry in vid_map) { + Log.d("Kekik_${this.name}", "mapEntry » ${mapEntry}") val title = mapEntry["title"] ?: continue - val m3uLink = mapEntry["videoData"] ?: continue + val m3u_link = mapEntry["videoData"] ?: continue callback.invoke( ExtractorLink( source = this.name, name = "${this.name} - ${title}", - url = m3uLink, - referer = extRef, + url = m3u_link, + referer = ext_ref, quality = Qualities.Unknown.value, type = INFER_TYPE ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tantifilm.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tantifilm.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt similarity index 75% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt index 0893b4de..2478edc1 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import com.lagradost.api.Log +import android.util.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty @@ -13,11 +13,12 @@ open class TauVideo : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val extRef = referer ?: "" - val videoKey = url.split("/").last() - val videoUrl = "${mainUrl}/api/video/${videoKey}" + val ext_ref = referer ?: "" + val video_key = url.split("/").last() + val video_url = "${mainUrl}/api/video/${video_key}" + Log.d("Kekik_${this.name}", "video_url » ${video_url}") - val api = app.get(videoUrl).parsedSafe() ?: throw ErrorLoadingException("TauVideo") + val api = app.get(video_url).parsedSafe() ?: throw ErrorLoadingException("TauVideo") for (video in api.urls) { callback.invoke( @@ -25,7 +26,7 @@ open class TauVideo : ExtractorApi() { source = this.name, name = this.name, url = video.url, - referer = extRef, + referer = ext_ref, quality = getQualityFromName(video.label), type = INFER_TYPE ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tomatomatela.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Tomatomatela.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tomatomatela.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Tomatomatela.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Uqload.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Uqload.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Uqload.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Uqload.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userload.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Userload.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userload.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Userload.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userscloud.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Userscloud.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userscloud.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Userscloud.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Uservideo.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Uservideo.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Uservideo.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Uservideo.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vicloud.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vicloud.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vicloud.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Vicloud.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt new file mode 100644 index 00000000..b963fe56 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt @@ -0,0 +1,50 @@ +// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır. + +package com.lagradost.cloudstream3.extractors + +import android.util.Log +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.* + +open class VidMoxy : ExtractorApi() { + override val name = "VidMoxy" + override val mainUrl = "https://vidmoxy.com" + override val requiresReferer = true + + override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { + val ext_ref = referer ?: "" + val video_req = app.get(url, referer=ext_ref).text + + val sub_urls = mutableSetOf() + Regex("""captions\",\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(video_req).forEach { + val (sub_url, sub_lang) = it.destructured + + if (sub_url in sub_urls) { return@forEach } + sub_urls.add(sub_url) + + subtitleCallback.invoke( + SubtitleFile( + lang = sub_lang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), + url = fixUrl(sub_url.replace("\\", "")) + ) + ) + } + + val extracted_value = Regex("""file": "(.*)",""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") + + val bytes = extracted_value.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray() + val decoded = String(bytes, Charsets.UTF_8) + Log.d("Kekik_${this.name}", "decoded » ${decoded}") + + callback.invoke( + ExtractorLink( + source = this.name, + name = this.name, + url = decoded, + referer = ext_ref, + quality = Qualities.Unknown.value, + isM3u8 = true + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt new file mode 100644 index 00000000..a27bf188 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt @@ -0,0 +1,100 @@ +package com.lagradost.cloudstream3.extractors + +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.amap +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.* +import kotlinx.coroutines.delay +import java.net.URI + +class VidSrcExtractor2 : VidSrcExtractor() { + override val mainUrl = "https://vidsrc.me/embed" + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val newUrl = url.lowercase().replace(mainUrl, super.mainUrl) + super.getUrl(newUrl, referer, subtitleCallback, callback) + } +} + +open class VidSrcExtractor : ExtractorApi() { + override val name = "VidSrc" + private val absoluteUrl = "https://v2.vidsrc.me" + override val mainUrl = "$absoluteUrl/embed" + override val requiresReferer = false + + companion object { + /** Infinite function to validate the vidSrc pass */ + suspend fun validatePass(url: String) { + val uri = URI(url) + val host = uri.host + + // Basically turn https://tm3p.vidsrc.stream/ -> https://vidsrc.stream/ + val referer = host.split(".").let { + val size = it.size + "https://" + it.subList(maxOf(0, size - 2), size).joinToString(".") + "/" + } + + while (true) { + app.get(url, referer = referer) + delay(60_000) + } + } + } + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val iframedoc = app.get(url).document + + val serverslist = + iframedoc.select("div#sources.button_content div#content div#list div").map { + val datahash = it.attr("data-hash") + if (datahash.isNotBlank()) { + val links = try { + app.get( + "$absoluteUrl/srcrcp/$datahash", + referer = "https://rcp.vidsrc.me/" + ).url + } catch (e: Exception) { + "" + } + links + } else "" + } + + serverslist.amap { server -> + val linkfixed = server.replace("https://vidsrc.xyz/", "https://embedsito.com/") + if (linkfixed.contains("/prorcp")) { + val srcresponse = app.get(server, referer = absoluteUrl).text + val m3u8Regex = Regex("((https:|http:)//.*\\.m3u8)") + val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@amap + val passRegex = Regex("""['"](.*set_pass[^"']*)""") + val pass = passRegex.find(srcresponse)?.groupValues?.get(1)?.replace( + Regex("""^//"""), "https://" + ) + + callback.invoke( + ExtractorLink( + this.name, + this.name, + srcm3u8, + "https://vidsrc.stream/", + Qualities.Unknown.value, + extractorData = pass, + isM3u8 = true + ) + ) + } else { + loadExtractor(linkfixed, url, subtitleCallback, callback) + } + } + } + +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt similarity index 77% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt index c85e6416..3060bb92 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import com.lagradost.api.Log +import android.util.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty @@ -15,13 +15,14 @@ open class VideoSeyred : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val extRef = referer ?: "" - val videoId = url.substringAfter("embed/").substringBefore("?") - val videoUrl = "${mainUrl}/playlist/${videoId}.json" + val ext_ref = referer ?: "" + val video_id = url.substringAfter("embed/").substringBefore("?") + val video_url = "${mainUrl}/playlist/${video_id}.json" + Log.d("Kekik_${this.name}", "video_url » ${video_url}") - val responseRaw = app.get(videoUrl) - val responseList:List = jacksonObjectMapper().readValue(responseRaw.text) ?: throw ErrorLoadingException("VideoSeyred") - val response = responseList[0] ?: throw ErrorLoadingException("VideoSeyred") + val response_raw = app.get(video_url) + val response_list:List = jacksonObjectMapper().readValue(response_raw.text) ?: throw ErrorLoadingException("VideoSeyred") + val response = response_list[0] ?: throw ErrorLoadingException("VideoSeyred") for (track in response.tracks) { if (track.label != null && track.kind == "captions") { @@ -40,7 +41,7 @@ open class VideoSeyred : ExtractorApi() { source = this.name, name = this.name, url = source.file, - referer = "${mainUrl}/", + referer = ext_ref, quality = Qualities.Unknown.value, type = INFER_TYPE ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoVard.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VideoVard.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoVard.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/VideoVard.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidhideExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidhideExtractor.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidhideExtractor.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/VidhideExtractor.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidmoly.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt similarity index 89% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidmoly.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt index 979fd8c5..615cfd74 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidmoly.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt @@ -25,13 +25,9 @@ open class Vidmoly : ExtractorApi() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val headers = mapOf( - "User-Agent" to "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", - "Sec-Fetch-Dest" to "iframe" - ) + val script = app.get( url, - headers = headers, referer = referer, ).document.select("script") .find { it.data().contains("sources:") }?.data() @@ -70,4 +66,4 @@ open class Vidmoly : ExtractorApi() { @JsonProperty("kind") val kind: String? = null, ) -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vido.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vido.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vido.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Vido.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt new file mode 100644 index 00000000..b9a07a6d --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt @@ -0,0 +1,116 @@ +package com.lagradost.cloudstream3.extractors + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.base64Encode +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.M3u8Helper +import javax.crypto.Cipher +import javax.crypto.spec.SecretKeySpec + +// Code found in https://github.com/KillerDogeEmpire/vidplay-keys +// special credits to @KillerDogeEmpire for providing key + +class MyCloud : Vidplay() { + override val name = "MyCloud" + override val mainUrl = "https://mcloud.bz" +} + +class VidplayOnline : Vidplay() { + override val mainUrl = "https://vidplay.online" +} + +open class Vidplay : ExtractorApi() { + override val name = "Vidplay" + override val mainUrl = "https://vidplay.site" + override val requiresReferer = true + open val key = + "https://raw.githubusercontent.com/KillerDogeEmpire/vidplay-keys/keys/keys.json" + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val id = url.substringBefore("?").substringAfterLast("/") + val encodeId = encodeId(id, getKeys()) + val mediaUrl = callFutoken(encodeId, url) + val res = app.get( + "$mediaUrl", headers = mapOf( + "Accept" to "application/json, text/javascript, */*; q=0.01", + "X-Requested-With" to "XMLHttpRequest", + ), referer = url + ).parsedSafe()?.result + + res?.sources?.map { + M3u8Helper.generateM3u8( + this.name, + it.file ?: return@map, + "$mainUrl/" + ).forEach(callback) + } + + res?.tracks?.filter { it.kind == "captions" }?.map { + subtitleCallback.invoke( + SubtitleFile(it.label ?: return@map, it.file ?: return@map) + ) + } + + } + + private suspend fun getKeys(): List { + return app.get(key).parsed() + } + + private suspend fun callFutoken(id: String, url: String): String? { + val script = app.get("$mainUrl/futoken").text + val k = "k='(\\S+)'".toRegex().find(script)?.groupValues?.get(1) ?: return null + val a = mutableListOf(k) + for (i in id.indices) { + a.add((k[i % k.length].code + id[i].code).toString()) + } + return "$mainUrl/mediainfo/${a.joinToString(",")}?${url.substringAfter("?")}" + } + + private fun encodeId(id: String, keyList: List): String { + val cipher1 = Cipher.getInstance("RC4") + val cipher2 = Cipher.getInstance("RC4") + cipher1.init( + Cipher.DECRYPT_MODE, + SecretKeySpec(keyList[0].toByteArray(), "RC4"), + cipher1.parameters + ) + cipher2.init( + Cipher.DECRYPT_MODE, + SecretKeySpec(keyList[1].toByteArray(), "RC4"), + cipher2.parameters + ) + var input = id.toByteArray() + input = cipher1.doFinal(input) + input = cipher2.doFinal(input) + return base64Encode(input).replace("/", "_") + } + + data class Tracks( + @JsonProperty("file") val file: String? = null, + @JsonProperty("label") val label: String? = null, + @JsonProperty("kind") val kind: String? = null, + ) + + data class Sources( + @JsonProperty("file") val file: String? = null, + ) + + data class Result( + @JsonProperty("sources") val sources: ArrayList? = arrayListOf(), + @JsonProperty("tracks") val tracks: ArrayList? = arrayListOf(), + ) + + data class Response( + @JsonProperty("result") val result: Result? = null, + ) + +} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidstream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidstream.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt new file mode 100644 index 00000000..2c6998de --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt @@ -0,0 +1,36 @@ +package com.lagradost.cloudstream3.extractors + +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.M3u8Helper + +class Tubeless : Voe() { + override var mainUrl = "https://tubelessceliolymph.com" +} + +open class Voe : ExtractorApi() { + override val name = "Voe" + override val mainUrl = "https://voe.sx" + override val requiresReferer = true + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val res = app.get(url, referer = referer).document + val script = res.select("script").find { it.data().contains("sources =") }?.data() + val link = Regex("[\"']hls[\"']:\\s*[\"'](.*)[\"']").find(script ?: return)?.groupValues?.get(1) + + M3u8Helper.generateM3u8( + name, + link ?: return, + "$mainUrl/", + headers = mapOf("Origin" to "$mainUrl/") + ).forEach(callback) + + } +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/WatchSB.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/WatchSB.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/WatchSB.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/WatchSB.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/WcoStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/WcoStream.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/WcoStream.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/WcoStream.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Wibufile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Wibufile.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/XStreamCdn.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/XStreamCdn.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/XStreamCdn.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/XStreamCdn.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YourUpload.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/YourUpload.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YourUpload.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/YourUpload.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Zorofile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Zorofile.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Zorofile.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Zorofile.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Zplayer.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Zplayer.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Zplayer.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Zplayer.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt similarity index 97% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt index bd42424f..0b401c06 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt @@ -1,6 +1,6 @@ package com.lagradost.cloudstream3.extractors.helper -import com.lagradost.api.Log +import android.util.Log import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.app diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/VstreamhubHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/VstreamhubHelper.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/VstreamhubHelper.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/helper/VstreamhubHelper.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt similarity index 76% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt index 35aec2b1..768fa1f6 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt @@ -1,6 +1,8 @@ package com.lagradost.cloudstream3.extractors.helper import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.app class WcoHelper { @@ -28,7 +30,9 @@ class WcoHelper { private suspend fun getKeys() { keys = keys ?: app.get("https://raw.githubusercontent.com/reduplicated/Cloudstream/master/docs/keys.json") - .parsedSafe() + .parsedSafe()?.also { setKey(BACKUP_KEY_DATA, it) } ?: getKey( + BACKUP_KEY_DATA + ) } suspend fun getWcoKey(): ExternalKeys? { @@ -39,7 +43,9 @@ class WcoHelper { private suspend fun getNewKeys() { newKeys = newKeys ?: app.get("https://raw.githubusercontent.com/chekaslowakiya/BruhFlow/main/keys.json") - .parsedSafe() + .parsedSafe()?.also { setKey(BACKUP_KEY_DATA, it) } ?: getKey( + BACKUP_KEY_DATA + ) } suspend fun getNewWcoKey(): NewExternalKeys? { diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt index bc646a8d..75e96bec 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt @@ -2,13 +2,15 @@ package com.lagradost.cloudstream3.metaproviders import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis import com.lagradost.cloudstream3.syncproviders.SyncIdName object SyncRedirector { + val syncApis = SyncApis private val syncIds = listOf( - SyncIdName.MyAnimeList to Regex("""myanimelist\.net/anime/(\d+)"""), - SyncIdName.Anilist to Regex("""anilist\.co/anime/(\d+)""") + SyncIdName.MyAnimeList to Regex("""myanimelist\.net\/anime\/(\d+)"""), + SyncIdName.Anilist to Regex("""anilist\.co\/anime\/(\d+)""") ) suspend fun redirect( diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt index c5b4d453..50301e22 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt @@ -105,7 +105,6 @@ open class TmdbProvider : MainAPI() { this.id, episode.episode_number, episode.season_number, - this.name ?: this.original_name, ).toJson(), episode.name, episode.season_number, @@ -123,7 +122,6 @@ open class TmdbProvider : MainAPI() { this.id, episodeNum, season.season_number, - this.name ?: this.original_name, ).toJson(), season = season.season_number ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt deleted file mode 100644 index addee9a0..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ /dev/null @@ -1,471 +0,0 @@ -package com.lagradost.cloudstream3.metaproviders - -import android.net.Uri -import com.fasterxml.jackson.annotation.JsonAlias -import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.APIHolder -import com.lagradost.cloudstream3.APIHolder.unixTimeMS -import com.lagradost.cloudstream3.Actor -import com.lagradost.cloudstream3.ActorData -import com.lagradost.cloudstream3.Episode -import com.lagradost.cloudstream3.HomePageResponse -import com.lagradost.cloudstream3.LoadResponse -import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId -import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId -import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer -import com.lagradost.cloudstream3.MainAPI -import com.lagradost.cloudstream3.MainPageRequest -import com.lagradost.cloudstream3.NextAiring -import com.lagradost.cloudstream3.ProviderType -import com.lagradost.cloudstream3.SearchResponse -import com.lagradost.cloudstream3.ShowStatus -import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.addDate -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.base64Decode -import com.lagradost.cloudstream3.mainPageOf -import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.newHomePageResponse -import com.lagradost.cloudstream3.newMovieLoadResponse -import com.lagradost.cloudstream3.newMovieSearchResponse -import com.lagradost.cloudstream3.newTvSeriesLoadResponse -import com.lagradost.cloudstream3.newTvSeriesSearchResponse -import com.lagradost.cloudstream3.utils.AppUtils.parseJson -import com.lagradost.cloudstream3.utils.AppUtils.toJson -import java.text.SimpleDateFormat -import java.util.Locale -import kotlin.math.roundToInt - -open class TraktProvider : MainAPI() { - override var name = "Trakt" - override val hasMainPage = true - override val providerType = ProviderType.MetaProvider - override val supportedTypes = setOf( - TvType.Movie, - TvType.TvSeries, - TvType.Anime, - ) - - private val traktClientId = - base64Decode("N2YzODYwYWQzNGI4ZTZmOTdmN2I5MTA0ZWQzMzEwOGI0MmQ3MTdlMTM0MmM2NGMxMTg5NGE1MjUyYTQ3NjE3Zg==") - private val traktApiUrl = base64Decode("aHR0cHM6Ly9hcGl6LnRyYWt0LnR2") - - override val mainPage = mainPageOf( - "$traktApiUrl/movies/trending" to "Trending Movies", //Most watched movies right now - "$traktApiUrl/movies/popular" to "Popular Movies", //The most popular movies for all time - "$traktApiUrl/shows/trending" to "Trending Shows", //Most watched Shows right now - "$traktApiUrl/shows/popular" to "Popular Shows", //The most popular Shows for all time - ) - - override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { - - val apiResponse = getApi("${request.data}?extended=cloud9,full&page=$page") - - val results = parseJson>(apiResponse).map { element -> - element.toSearchResponse() - } - return newHomePageResponse(request.name, results) - } - - private fun MediaDetails.toSearchResponse(): SearchResponse { - - val media = this.media ?: this - val mediaType = if (media.ids?.tvdb == null) TvType.Movie else TvType.TvSeries - val poster = media.images?.poster?.firstOrNull() - - if (mediaType == TvType.Movie) { - return newMovieSearchResponse( - name = media.title!!, - url = Data( - type = mediaType, - mediaDetails = media, - ).toJson(), - type = TvType.Movie, - ) { - posterUrl = fixPath(poster) - } - } else { - return newTvSeriesSearchResponse( - name = media.title!!, - url = Data( - type = mediaType, - mediaDetails = media, - ).toJson(), - type = TvType.TvSeries, - ) { - this.posterUrl = fixPath(poster) - } - } - } - - override suspend fun search(query: String): List? { - val apiResponse = - getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query") - - val results = parseJson>(apiResponse).map { element -> - element.toSearchResponse() - } - - return results - } - - override suspend fun load(url: String): LoadResponse { - - val data = parseJson(url) - val mediaDetails = data.mediaDetails - val moviesOrShows = if (data.type == TvType.Movie) "movies" else "shows" - - val posterUrl = mediaDetails?.images?.poster?.firstOrNull() - val backDropUrl = mediaDetails?.images?.fanart?.firstOrNull() - - val resActor = - getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=cloud9,full") - - val actors = parseJson(resActor).cast?.map { - ActorData( - Actor( - name = it.person?.name!!, - image = getWidthImageUrl(it.person.images?.headshot?.firstOrNull(), "w500") - ), - roleString = it.character - ) - } - - val resRelated = - getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=cloud9,full&limit=20") - - val relatedMedia = parseJson>(resRelated).map { it.toSearchResponse() } - - val isCartoon = - mediaDetails?.genres?.contains("animation") == true || mediaDetails?.genres?.contains("anime") == true - val isAnime = - isCartoon && (mediaDetails?.language == "zh" || mediaDetails?.language == "ja") - val isAsian = !isAnime && (mediaDetails?.language == "zh" || mediaDetails?.language == "ko") - val isBollywood = mediaDetails?.country == "in" - - if (data.type == TvType.Movie) { - - val linkData = LinkData( - id = mediaDetails?.ids?.tmdb, - traktId = mediaDetails?.ids?.trakt, - traktSlug = mediaDetails?.ids?.slug, - tmdbId = mediaDetails?.ids?.tmdb, - imdbId = mediaDetails?.ids?.imdb.toString(), - tvdbId = mediaDetails?.ids?.tvdb, - tvrageId = mediaDetails?.ids?.tvrage, - type = data.type.toString(), - title = mediaDetails?.title, - year = mediaDetails?.year, - orgTitle = mediaDetails?.title, - isAnime = isAnime, - //jpTitle = later if needed as it requires another network request, - airedDate = mediaDetails?.released - ?: mediaDetails?.firstAired, - isAsian = isAsian, - isBollywood = isBollywood, - ).toJson() - - return newMovieLoadResponse( - name = mediaDetails?.title!!, - url = data.toJson(), - dataUrl = linkData.toJson(), - type = if (isAnime) TvType.AnimeMovie else TvType.Movie, - ) { - this.name = mediaDetails.title - this.type = if (isAnime) TvType.AnimeMovie else TvType.Movie - this.posterUrl = getOriginalWidthImageUrl(posterUrl) - this.year = mediaDetails.year - this.plot = mediaDetails.overview - this.rating = mediaDetails.rating?.times(1000)?.roundToInt() - this.tags = mediaDetails.genres - this.duration = mediaDetails.runtime - this.recommendations = relatedMedia - this.actors = actors - this.comingSoon = isUpcoming(mediaDetails.released) - //posterHeaders - this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl) - this.contentRating = mediaDetails.certification - addTrailer(mediaDetails.trailer) - addImdbId(mediaDetails.ids?.imdb) - addTMDbId(mediaDetails.ids?.tmdb.toString()) - } - } else { - - val resSeasons = - getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes") - val episodes = mutableListOf() - val seasons = parseJson>(resSeasons) - var nextAir: NextAiring? = null - - seasons.forEach { season -> - - season.episodes?.map { episode -> - - val linkData = LinkData( - id = mediaDetails?.ids?.tmdb, - traktId = mediaDetails?.ids?.trakt, - traktSlug = mediaDetails?.ids?.slug, - tmdbId = mediaDetails?.ids?.tmdb, - imdbId = mediaDetails?.ids?.imdb.toString(), - tvdbId = mediaDetails?.ids?.tvdb, - tvrageId = mediaDetails?.ids?.tvrage, - type = data.type.toString(), - season = episode.season, - episode = episode.number, - title = mediaDetails?.title, - year = mediaDetails?.year, - orgTitle = mediaDetails?.title, - isAnime = isAnime, - airedYear = mediaDetails?.year, - lastSeason = seasons.size, - epsTitle = episode.title, - //jpTitle = later if needed as it requires another network request, - date = episode.firstAired, - airedDate = episode.firstAired, - isAsian = isAsian, - isBollywood = isBollywood, - isCartoon = isCartoon - ).toJson() - - episodes.add( - Episode( - data = linkData.toJson(), - name = episode.title, - season = episode.season, - episode = episode.number, - posterUrl = fixPath(episode.images?.screenshot?.firstOrNull()), - rating = episode.rating?.times(10)?.roundToInt(), - description = episode.overview, - runTime = episode.runtime - ).apply { - this.addDate(episode.firstAired, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") - if (nextAir == null && this.date != null && this.date!! > unixTimeMS && this.season != 0) { - nextAir = NextAiring( - episode = this.episode!!, - unixTime = this.date!!.div(1000L), - season = if (this.season == 1) null else this.season, - ) - } - } - ) - } - } - - return newTvSeriesLoadResponse( - name = mediaDetails?.title!!, - url = data.toJson(), - type = if (isAnime) TvType.Anime else TvType.TvSeries, - episodes = episodes - ) { - this.name = mediaDetails.title - this.type = if (isAnime) TvType.Anime else TvType.TvSeries - this.episodes = episodes - this.posterUrl = getOriginalWidthImageUrl(posterUrl) - this.year = mediaDetails.year - this.plot = mediaDetails.overview - this.showStatus = getStatus(mediaDetails.status) - this.rating = mediaDetails.rating?.times(1000)?.roundToInt() - this.tags = mediaDetails.genres - this.duration = mediaDetails.runtime - this.recommendations = relatedMedia - this.actors = actors - this.comingSoon = isUpcoming(mediaDetails.released) - //posterHeaders - this.nextAiring = nextAir - this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl) - this.contentRating = mediaDetails.certification - addTrailer(mediaDetails.trailer) - addImdbId(mediaDetails.ids?.imdb) - addTMDbId(mediaDetails.ids?.tmdb.toString()) - } - } - } - - private suspend fun getApi(url: String): String { - return app.get( - url = url, - headers = mapOf( - "Content-Type" to "application/json", - "trakt-api-version" to "2", - "trakt-api-key" to traktClientId, - ) - ).toString() - } - - private fun isUpcoming(dateString: String?): Boolean { - return try { - val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) - val dateTime = dateString?.let { format.parse(it)?.time } ?: return false - unixTimeMS < dateTime - } catch (t: Throwable) { - logError(t) - false - } - } - - private fun getStatus(t: String?): ShowStatus { - return when (t) { - "returning series" -> ShowStatus.Ongoing - "continuing" -> ShowStatus.Ongoing - else -> ShowStatus.Completed - } - } - - private fun fixPath(url: String?): String? { - url ?: return null - return "https://$url" - } - - private fun getWidthImageUrl(path: String?, width: String): String? { - if (path == null) return null - if (!path.contains("image.tmdb.org")) return fixPath(path) - val fileName = Uri.parse(path).lastPathSegment ?: return null - return "https://image.tmdb.org/t/p/${width}/${fileName}" - } - - private fun getOriginalWidthImageUrl(path: String?): String? { - if (path == null) return null - if (!path.contains("image.tmdb.org")) return fixPath(path) - return getWidthImageUrl(path, "original") - } - - data class Data( - val type: TvType? = null, - val mediaDetails: MediaDetails? = null, - ) - - data class MediaDetails( - @JsonProperty("title") val title: String? = null, - @JsonProperty("year") val year: Int? = null, - @JsonProperty("ids") val ids: Ids? = null, - @JsonProperty("tagline") val tagline: String? = null, - @JsonProperty("overview") val overview: String? = null, - @JsonProperty("released") val released: String? = null, - @JsonProperty("runtime") val runtime: Int? = null, - @JsonProperty("country") val country: String? = null, - @JsonProperty("updatedAt") val updatedAt: String? = null, - @JsonProperty("trailer") val trailer: String? = null, - @JsonProperty("homepage") val homepage: String? = null, - @JsonProperty("status") val status: String? = null, - @JsonProperty("rating") val rating: Double? = null, - @JsonProperty("votes") val votes: Long? = null, - @JsonProperty("comment_count") val commentCount: Long? = null, - @JsonProperty("language") val language: String? = null, - @JsonProperty("languages") val languages: List? = null, - @JsonProperty("available_translations") val availableTranslations: List? = null, - @JsonProperty("genres") val genres: List? = null, - @JsonProperty("certification") val certification: String? = null, - @JsonProperty("aired_episodes") val airedEpisodes: Int? = null, - @JsonProperty("first_aired") val firstAired: String? = null, - @JsonProperty("airs") val airs: Airs? = null, - @JsonProperty("network") val network: String? = null, - @JsonProperty("images") val images: Images? = null, - @JsonProperty("movie") @JsonAlias("show") val media: MediaDetails? = null - ) - - data class Airs( - @JsonProperty("day") val day: String? = null, - @JsonProperty("time") val time: String? = null, - @JsonProperty("timezone") val timezone: String? = null, - ) - - data class Ids( - @JsonProperty("trakt") val trakt: Int? = null, - @JsonProperty("slug") val slug: String? = null, - @JsonProperty("tvdb") val tvdb: Int? = null, - @JsonProperty("imdb") val imdb: String? = null, - @JsonProperty("tmdb") val tmdb: Int? = null, - @JsonProperty("tvrage") val tvrage: String? = null, - ) - - data class Images( - @JsonProperty("fanart") val fanart: List? = null, - @JsonProperty("poster") val poster: List? = null, - @JsonProperty("logo") val logo: List? = null, - @JsonProperty("clearart") val clearart: List? = null, - @JsonProperty("banner") val banner: List? = null, - @JsonProperty("thumb") val thumb: List? = null, - @JsonProperty("screenshot") val screenshot: List? = null, - @JsonProperty("headshot") val headshot: List? = null, - ) - - data class People( - @JsonProperty("cast") val cast: List? = null, - ) - - data class Cast( - @JsonProperty("character") val character: String? = null, - @JsonProperty("characters") val characters: List? = null, - @JsonProperty("episode_count") val episodeCount: Long? = null, - @JsonProperty("person") val person: Person? = null, - @JsonProperty("images") val images: Images? = null, - ) - - data class Person( - @JsonProperty("name") val name: String? = null, - @JsonProperty("ids") val ids: Ids? = null, - @JsonProperty("images") val images: Images? = null, - ) - - data class Seasons( - @JsonProperty("aired_episodes") val airedEpisodes: Int? = null, - @JsonProperty("episode_count") val episodeCount: Int? = null, - @JsonProperty("episodes") val episodes: List? = null, - @JsonProperty("first_aired") val firstAired: String? = null, - @JsonProperty("ids") val ids: Ids? = null, - @JsonProperty("images") val images: Images? = null, - @JsonProperty("network") val network: String? = null, - @JsonProperty("number") val number: Int? = null, - @JsonProperty("overview") val overview: String? = null, - @JsonProperty("rating") val rating: Double? = null, - @JsonProperty("title") val title: String? = null, - @JsonProperty("updated_at") val updatedAt: String? = null, - @JsonProperty("votes") val votes: Int? = null, - ) - - data class TraktEpisode( - @JsonProperty("available_translations") val availableTranslations: List? = null, - @JsonProperty("comment_count") val commentCount: Int? = null, - @JsonProperty("episode_type") val episodeType: String? = null, - @JsonProperty("first_aired") val firstAired: String? = null, - @JsonProperty("ids") val ids: Ids? = null, - @JsonProperty("images") val images: Images? = null, - @JsonProperty("number") val number: Int? = null, - @JsonProperty("number_abs") val numberAbs: Int? = null, - @JsonProperty("overview") val overview: String? = null, - @JsonProperty("rating") val rating: Double? = null, - @JsonProperty("runtime") val runtime: Int? = null, - @JsonProperty("season") val season: Int? = null, - @JsonProperty("title") val title: String? = null, - @JsonProperty("updated_at") val updatedAt: String? = null, - @JsonProperty("votes") val votes: Int? = null, - ) - - data class LinkData( - val id: Int? = null, - val traktId: Int? = null, - val traktSlug: String? = null, - val tmdbId: Int? = null, - val imdbId: String? = null, - val tvdbId: Int? = null, - val tvrageId: String? = null, - val type: String? = null, - val season: Int? = null, - val episode: Int? = null, - val aniId: String? = null, - val animeId: String? = null, - val title: String? = null, - val year: Int? = null, - val orgTitle: String? = null, - val isAnime: Boolean = false, - val airedYear: Int? = null, - val lastSeason: Int? = null, - val epsTitle: String? = null, - val jpTitle: String? = null, - val date: String? = null, - val airedDate: String? = null, - val isAsian: Boolean = false, - val isBollywood: Boolean = false, - val isCartoon: Boolean = false, - ) -} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt b/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt similarity index 86% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt rename to app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt index d3b4999a..817d7db3 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt @@ -1,7 +1,10 @@ package com.lagradost.cloudstream3.mvvm -import com.lagradost.api.BuildConfig -import com.lagradost.api.Log +import android.util.Log +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import com.bumptech.glide.load.HttpException +import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.ErrorLoadingException import kotlinx.coroutines.* import java.io.InterruptedIOException @@ -46,6 +49,18 @@ inline fun debugWarning(assert: () -> Boolean, message: () -> String) { } } +/** NOTE: Only one observer at a time per value */ +fun LifecycleOwner.observe(liveData: LiveData, action: (t: T) -> Unit) { + liveData.removeObservers(this) + liveData.observe(this) { it?.let { t -> action(t) } } +} + +/** NOTE: Only one observer at a time per value */ +fun LifecycleOwner.observeNullable(liveData: LiveData, action: (t: T) -> Unit) { + liveData.removeObservers(this) + liveData.observe(this) { action(it) } +} + sealed class Resource { data class Success(val value: T) : Resource() data class Failure( @@ -143,14 +158,14 @@ fun throwAbleToResource( "Connection Timeout\nPlease try again later." ) } -// is HttpException -> { -// Resource.Failure( -// false, -// throwable.statusCode, -// null, -// throwable.message ?: "HttpException" -// ) -// } + is HttpException -> { + Resource.Failure( + false, + throwable.statusCode, + null, + throwable.message ?: "HttpException" + ) + } is UnknownHostException -> { Resource.Failure(true, null, null, "Cannot connect to server, try again later.\n${throwable.message}") } diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt b/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt deleted file mode 100644 index 3df5197c..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.lagradost.cloudstream3.mvvm - -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData - -/** NOTE: Only one observer at a time per value */ -fun LifecycleOwner.observe(liveData: LiveData, action: (t: T) -> Unit) { - liveData.removeObservers(this) - liveData.observe(this) { it?.let { t -> action(t) } } -} - -/** NOTE: Only one observer at a time per value */ -fun LifecycleOwner.observeNullable(liveData: LiveData, action: (t: T) -> Unit) { - liveData.removeObservers(this) - liveData.observe(this) { action(it) } -} diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt b/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt index 85a9db5d..c8c385cf 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt @@ -9,10 +9,7 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.nicehttp.Requests.Companion.await import com.lagradost.nicehttp.cookies import kotlinx.coroutines.runBlocking -import okhttp3.Headers -import okhttp3.Interceptor -import okhttp3.Request -import okhttp3.Response +import okhttp3.* import java.net.URI diff --git a/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt b/app/src/main/java/com/lagradost/cloudstream3/network/WebViewResolver.kt similarity index 90% rename from library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt rename to app/src/main/java/com/lagradost/cloudstream3/network/WebViewResolver.kt index 0fbc5749..90872d94 100644 --- a/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/network/WebViewResolver.kt @@ -1,12 +1,13 @@ package com.lagradost.cloudstream3.network import android.annotation.SuppressLint -import android.content.Context import android.net.http.SslError import android.os.Handler import android.os.Looper import android.webkit.* -import com.lagradost.api.getContext +import com.lagradost.cloudstream3.AcraApplication +import com.lagradost.cloudstream3.AcraApplication.Companion.context +import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.debugException import com.lagradost.cloudstream3.mvvm.logError @@ -32,24 +33,40 @@ import java.net.URI * @param scriptCallback will be called with the result from custom js * @param timeout close webview after timeout * */ -actual class WebViewResolver actual constructor( +class WebViewResolver( val interceptUrl: Regex, - val additionalUrls: List, - val userAgent: String?, - val useOkhttp: Boolean, - val script: String?, - val scriptCallback: ((String) -> Unit)?, - val timeout: Long + val additionalUrls: List = emptyList(), + val userAgent: String? = USER_AGENT, + val useOkhttp: Boolean = true, + val script: String? = null, + val scriptCallback: ((String) -> Unit)? = null, + val timeout: Long = DEFAULT_TIMEOUT ) : Interceptor { - actual companion object { + constructor( + interceptUrl: Regex, + additionalUrls: List = emptyList(), + userAgent: String? = USER_AGENT, + useOkhttp: Boolean = true, + script: String? = null, + scriptCallback: ((String) -> Unit)? = null, + ) : this(interceptUrl, additionalUrls, userAgent, useOkhttp, script, scriptCallback, DEFAULT_TIMEOUT) + + constructor( + interceptUrl: Regex, + additionalUrls: List = emptyList(), + userAgent: String? = USER_AGENT, + useOkhttp: Boolean = true + ) : this(interceptUrl, additionalUrls, userAgent, useOkhttp, null, null, DEFAULT_TIMEOUT) + + companion object { + private const val DEFAULT_TIMEOUT = 60_000L var webViewUserAgent: String? = null - actual val DEFAULT_TIMEOUT = 60_000L @JvmName("getWebViewUserAgent1") fun getWebViewUserAgent(): String? { - return webViewUserAgent ?: (getContext() as? Context)?.let { ctx -> + return webViewUserAgent ?: context?.let { ctx -> runBlocking { mainWork { WebView(ctx).settings.userAgentString.also { userAgent -> @@ -120,7 +137,7 @@ actual class WebViewResolver actual constructor( WebView.setWebContentsDebuggingEnabled(true) try { webView = WebView( - (getContext() as? Context) + AcraApplication.context ?: throw RuntimeException("No base context in WebViewResolver") ).apply { // Bare minimum to bypass captcha diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt index ddf5b286..e89ccfeb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt @@ -2,4 +2,5 @@ package com.lagradost.cloudstream3.plugins @Suppress("unused") @Target(AnnotationTarget.CLASS) -annotation class CloudstreamPlugin \ No newline at end of file +annotation class CloudstreamPlugin( +) \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt index fc836587..6b7dc90b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt @@ -34,7 +34,7 @@ abstract class Plugin { */ fun registerMainAPI(element: MainAPI) { Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI") - element.sourcePlugin = this.filename + element.sourcePlugin = this.__filename // Race condition causing which would case duplicates if not for distinctBy synchronized(APIHolder.allProviders) { APIHolder.allProviders.add(element) @@ -48,7 +48,7 @@ abstract class Plugin { */ fun registerExtractorAPI(element: ExtractorApi) { Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) ExtractorApi") - element.sourcePlugin = this.filename + element.sourcePlugin = this.__filename extractorApis.add(element) } @@ -67,12 +67,7 @@ abstract class Plugin { * This will contain your resources if you specified requiresResources in gradle */ var resources: Resources? = null - /** Full file path to the plugin. */ - @Deprecated("Renamed to `filename` to follow conventions", replaceWith = ReplaceWith("filename")) - var __filename: String? - get() = filename - set(value) {filename = value} - var filename: String? = null + var __filename: String? = null /** * This will add a button in the settings allowing you to add custom settings diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt index bc2a1780..025e6fb6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -1,25 +1,24 @@ package com.lagradost.cloudstream3.plugins -import android.Manifest import android.app.* import android.content.Context -import android.content.pm.PackageManager import android.content.res.AssetManager import android.content.res.Resources import android.os.Build import android.os.Environment import android.util.Log import android.widget.Toast -import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.fragment.app.FragmentActivity import com.fasterxml.jackson.annotation.JsonProperty import com.google.gson.Gson import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.APIHolder.removePluginMapping import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider @@ -35,7 +34,6 @@ import com.lagradost.cloudstream3.ui.result.UiText import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData -import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute @@ -166,7 +164,7 @@ object PluginManager { private val LOCAL_PLUGINS_PATH = CLOUD_STREAM_FOLDER + "plugins" - var currentlyLoading: String? = null + public var currentlyLoading: String? = null // Maps filepath to plugin val plugins: MutableMap = @@ -342,7 +340,7 @@ object PluginManager { //Omit non-NSFW if mode is set to NSFW only if (mode == AutoDownloadMode.NsfwOnly) { - if (!tvtypes.contains(TvType.NSFW.name)) { + if (tvtypes.contains(TvType.NSFW.name) == false) { return@mapNotNull null } } @@ -431,6 +429,7 @@ object PluginManager { **/ fun loadAllLocalPlugins(context: Context, forceReload: Boolean) { val dir = File(LOCAL_PLUGINS_PATH) + removeKey(PLUGINS_KEY_LOCAL) if (!dir.exists()) { val res = dir.mkdirs() @@ -507,12 +506,10 @@ object PluginManager { val version: Int = manifest.version ?: PLUGIN_VERSION_NOT_SET.also { Log.d(TAG, "No manifest version for ${data.internalName}") } - - @Suppress("UNCHECKED_CAST") val pluginClass: Class<*> = loader.loadClass(manifest.pluginClassName) as Class val pluginInstance: Plugin = - pluginClass.getDeclaredConstructor().newInstance() as Plugin + pluginClass.newInstance() as Plugin // Sets with the proper version setPluginData(data.copy(version = version)) @@ -522,16 +519,14 @@ object PluginManager { return true } - pluginInstance.filename = file.absolutePath + pluginInstance.__filename = fileName if (manifest.requiresResources) { Log.d(TAG, "Loading resources for ${data.internalName}") // based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk - val assets = AssetManager::class.java.getDeclaredConstructor().newInstance() + val assets = AssetManager::class.java.newInstance() val addAssetPath = AssetManager::class.java.getMethod("addAssetPath", String::class.java) addAssetPath.invoke(assets, file.absolutePath) - - @Suppress("DEPRECATION") pluginInstance.resources = Resources( assets, context.resources.displayMetrics, @@ -573,14 +568,14 @@ object PluginManager { // remove all registered apis synchronized(APIHolder.apis) { - APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach { + APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach { removePluginMapping(it) } } synchronized(APIHolder.allProviders) { - APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.filename } + APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.__filename } } - extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.filename } + extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.__filename } classLoaders.values.removeIf { v -> v == plugin } @@ -727,14 +722,9 @@ object PluginManager { } val notification = builder.build() - // notificationId is a unique int for each notification that you must define - if (ActivityCompat.checkSelfPermission( - context, - Manifest.permission.POST_NOTIFICATIONS - ) == PackageManager.PERMISSION_GRANTED - ) { - NotificationManagerCompat.from(context) - .notify((System.currentTimeMillis() / 1000).toInt(), notification) + with(NotificationManagerCompat.from(context)) { + // notificationId is a unique int for each notification that you must define + notify((System.currentTimeMillis() / 1000).toInt(), notification) } return notification } catch (e: Exception) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt index c6ec9df7..b80a590e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt @@ -73,7 +73,7 @@ object RepositoryManager { val PREBUILT_REPOSITORIES: Array by lazy { getKey("PREBUILT_REPOSITORIES") ?: emptyArray() } - private val GH_REGEX = Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$") + val GH_REGEX = Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$") /* Convert raw.githubusercontent.com urls to cdn.jsdelivr.net if enabled in settings */ fun convertRawGitUrl(url: String): String { diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt index d1b702f4..a45ab5f0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.sync.withLock object VotingApi { // please do not cheat the votes lol private const val LOGKEY = "VotingApi" - private const val API_DOMAIN = "https://counterapi.com/api" + private const val apiDomain = "https://counterapi.com/api" private fun transformUrl(url: String): String = // dont touch or all votes get reset MessageDigest @@ -49,13 +49,13 @@ object VotingApi { // please do not cheat the votes lol .joinToString("-") private suspend fun readVote(pluginUrl: String): Int { - val url = "${API_DOMAIN}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}?readOnly=true" + var url = "${apiDomain}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}?readOnly=true" Log.d(LOGKEY, "Requesting: $url") return app.get(url).parsedSafe()?.value ?: 0 } private suspend fun writeVote(pluginUrl: String): Boolean { - val url = "${API_DOMAIN}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}" + var url = "${apiDomain}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}" Log.d(LOGKEY, "Requesting: $url") return app.get(url).parsedSafe()?.value != null } @@ -69,7 +69,8 @@ object VotingApi { // please do not cheat the votes lol getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: false fun canVote(pluginUrl: String): Boolean { - return PluginManager.urlPlugins.contains(pluginUrl) + if (!PluginManager.urlPlugins.contains(pluginUrl)) return false + return true } private val voteLock = Mutex() diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt index 4ef841f5..6ed7a447 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt @@ -10,7 +10,7 @@ import androidx.work.PeriodicWorkRequest import androidx.work.WorkManager import androidx.work.WorkerParameters import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.utils.AppContextUtils.createNotificationChannel +import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel import com.lagradost.cloudstream3.utils.BackupUtils import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import java.util.concurrent.TimeUnit diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt index 00c74dff..e2bcd6e1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt @@ -10,13 +10,13 @@ import androidx.core.app.NotificationCompat import androidx.core.net.toUri import androidx.work.* import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.ui.result.txt -import com.lagradost.cloudstream3.utils.AppContextUtils.createNotificationChannel -import com.lagradost.cloudstream3.utils.AppContextUtils.getApiDubstatusSettings +import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel import com.lagradost.cloudstream3.utils.Coroutines.ioWork import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions diff --git a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt index df64caab..857fba11 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt @@ -59,7 +59,7 @@ class SubtitleResource { return file } - private fun unzip(file: File): List> { + fun unzip(file: File): List> { val entries = mutableListOf>() ZipInputStream(file.inputStream()).use { zipInputStream -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt index 685b499b..f6424c4c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt @@ -19,11 +19,8 @@ class AbstractSubtitleEntities { data class SubtitleSearch( var query: String = "", + var imdb: Long? = null, var lang: String? = null, - var imdbId: String? = null, - var tmdbId: Int? = null, - var malId: Int? = null, - var aniListId: Int? = null, var epNumber: Int? = null, var seasonNumber: Int? = null, var year: Int? = null diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt index 2e14c3c4..bae8a5df 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -3,26 +3,20 @@ package com.lagradost.cloudstream3.syncproviders import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.AcraApplication.Companion.setKey -import com.lagradost.cloudstream3.LoadResponse +import com.lagradost.cloudstream3.syncproviders.providers.SubScene import com.lagradost.cloudstream3.syncproviders.providers.* import java.util.concurrent.TimeUnit abstract class AccountManager(private val defIndex: Int) : AuthAPI { companion object { - val malApi = MALApi(0).also { api -> - LoadResponse.Companion.malIdPrefix = api.idPrefix - } - val aniListApi = AniListApi(0).also { api -> - LoadResponse.Companion.aniListIdPrefix = api.idPrefix - } - val simklApi = SimklApi(0).also { api -> - LoadResponse.Companion.simklIdPrefix = api.idPrefix - } + val malApi = MALApi(0) + val aniListApi = AniListApi(0) val openSubtitlesApi = OpenSubtitlesApi(0) + val simklApi = SimklApi(0) + val indexSubtitlesApi = IndexSubtitleApi() val addic7ed = Addic7ed() - val subDlApi = SubDlApi(0) + val subScene = SubScene() val localListApi = LocalList() - val subSourceApi = SubSourceApi() // used to login via app intent val OAuth2Apis @@ -33,7 +27,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { // this needs init with context and can be accessed in settings val accountManagers get() = listOf( - malApi, aniListApi, openSubtitlesApi, subDlApi, simklApi //nginxApi + malApi, aniListApi, openSubtitlesApi, simklApi //nginxApi ) // used for active syncing @@ -43,35 +37,32 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { ) val inAppAuths - get() = listOf( - openSubtitlesApi, - subDlApi - )//, nginxApi) + get() = listOf(openSubtitlesApi)//, nginxApi) val subtitleProviders get() = listOf( openSubtitlesApi, + indexSubtitlesApi, // they got anti scraping measures in place :( addic7ed, - subDlApi, - subSourceApi + subScene ) - const val APP_STRING = "cloudstreamapp" - const val APP_STRING_REPO = "cloudstreamrepo" - const val APP_STRING_PLAYER = "cloudstreamplayer" + const val appString = "cloudstreamapp" + const val appStringRepo = "cloudstreamrepo" + const val appStringPlayer = "cloudstreamplayer" // Instantly start the search given a query - const val APP_STRING_SEARCH = "cloudstreamsearch" + const val appStringSearch = "cloudstreamsearch" // Instantly resume watching a show - const val APP_STRING_RESUME_WATCHING = "cloudstreamcontinuewatching" + const val appStringResumeWatching = "cloudstreamcontinuewatching" val unixTime: Long get() = System.currentTimeMillis() / 1000L val unixTimeMs: Long get() = System.currentTimeMillis() - const val MAX_STALE = 60 * 10 + const val maxStale = 60 * 10 fun secondsToReadable(seconds: Int, completedValue: String): String { var secondsLong = seconds.toLong() diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt index 3d0bb940..ef74edfc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt @@ -5,23 +5,7 @@ import androidx.fragment.app.FragmentActivity interface OAuth2API : AuthAPI { val key: String val redirectUrl: String - val supportDeviceAuth: Boolean suspend fun handleRedirect(url: String) : Boolean fun authenticate(activity: FragmentActivity?) - suspend fun getDevicePin() : PinAuthData? { - return null - } - - suspend fun handleDeviceAuth(pinAuthData: PinAuthData) : Boolean { - return false - } - - data class PinAuthData( - val deviceCode: String, - val userCode: String, - val verificationUrl: String, - val expiresIn: Int, - val interval: Int, - ) } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt similarity index 95% rename from app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt rename to app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt index dcb8bbea..045fdc94 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt @@ -2,10 +2,19 @@ package com.lagradost.cloudstream3.syncproviders import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.ui.SyncWatchType +import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.UiText import me.xdrop.fuzzywuzzy.FuzzySearch -import java.util.Date + +enum class SyncIdName { + Anilist, + MyAnimeList, + Trakt, + Imdb, + Simkl, + LocalList, +} interface SyncAPI : OAuth2API { /** @@ -125,8 +134,6 @@ interface SyncAPI : OAuth2API { ListSorting.AlphabeticalZ -> items.sortedBy { it.name }.reversed() ListSorting.UpdatedNew -> items.sortedBy { it.lastUpdatedUnixTime?.times(-1) } ListSorting.UpdatedOld -> items.sortedBy { it.lastUpdatedUnixTime } - ListSorting.ReleaseDateNew -> items.sortedByDescending { it.releaseDate } - ListSorting.ReleaseDateOld -> items.sortedBy { it.releaseDate } else -> items } } @@ -161,10 +168,9 @@ interface SyncAPI : OAuth2API { override var posterUrl: String?, override var posterHeaders: Map?, override var quality: SearchQuality?, - val releaseDate: Date?, override var id: Int? = null, val plot : String? = null, val rating: Int? = null, - val tags: List? = null + val tags: List? = null, ) : SearchResponse } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt index db467639..507c5e2a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt @@ -18,13 +18,13 @@ class Addic7ed : AbstractSubApi { override fun logOut() {} companion object { - const val HOST = "https://www.addic7ed.com" + const val host = "https://www.addic7ed.com" const val TAG = "ADDIC7ED" } private fun fixUrl(url: String): String { - return if (url.startsWith("/")) HOST + url - else if (!url.startsWith("http")) "$HOST/$url" + return if (url.startsWith("/")) host + url + else if (!url.startsWith("http")) "$host/$url" else url } @@ -62,7 +62,7 @@ class Addic7ed : AbstractSubApi { } val title = queryText.substringBefore("(").trim() - val url = "$HOST/search.php?search=${title}&Submit=Search" + val url = "$host/search.php?search=${title}&Submit=Search" val hostDocument = app.get(url).document var searchResult = "" if (!hostDocument.select("span:contains($title)").isNullOrEmpty()) searchResult = url @@ -74,8 +74,8 @@ class Addic7ed : AbstractSubApi { hostDocument.selectFirst("#sl button")?.attr("onmouseup")?.substringAfter("(") ?.substringBefore(",") val doc = app.get( - "$HOST/ajax_loadShow.php?show=$show&season=$seasonNum&langs=&hd=undefined&hi=undefined", - referer = "$HOST/" + "$host/ajax_loadShow.php?show=$show&season=$seasonNum&langs=&hd=undefined&hi=undefined", + referer = "$host/" ).document doc.select("#season tr:contains($queryLang)").mapNotNull { node -> if (node.selectFirst("td")?.text() @@ -97,7 +97,7 @@ class Addic7ed : AbstractSubApi { val link = fixUrl(node.select("a.buttonDownload").attr("href")) val isHearingImpaired = !node.parent()!!.select("tr:last-child [title=\"Hearing Impaired\"]").isNullOrEmpty() - cleanResources(results, name, link, mapOf("referer" to "$HOST/"), isHearingImpaired) + cleanResources(results, name, link, mapOf("referer" to "$host/"), isHearingImpaired) } return results } diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt index 6112c7db..5c02e7f7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt @@ -16,16 +16,15 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.txt -import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.splitQuery import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject -import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear import java.net.URL import java.net.URLEncoder -import java.util.Locale +import java.util.* class AniListApi(index: Int) : AccountManager(index), SyncAPI { override var name = "AniList" @@ -33,7 +32,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { override val redirectUrl = "anilistlogin" override val idPrefix = "anilist" override var requireLibraryRefresh = true - override val supportDeviceAuth = false override var mainUrl = "https://anilist.co" override val icon = R.drawable.ic_anilist_icon override val requiresLogin = false @@ -64,7 +62,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun handleRedirect(url: String): Boolean { val sanitizer = - splitQuery(URL(url.replace(APP_STRING, "https").replace("/#", "?"))) // FIX ERROR + splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR val token = sanitizer["access_token"]!! val expiresIn = sanitizer["expires_in"]!! @@ -88,7 +86,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun search(name: String): List? { val data = searchShows(name) ?: return null - return data.data?.page?.media?.map { + return data.data?.Page?.media?.map { SyncAPI.SyncSearchResult( it.title.romaji ?: return null, this.name, @@ -102,7 +100,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun getResult(id: String): SyncAPI.SyncResult { val internalId = (Regex("anilist\\.co/anime/(\\d*)").find(id)?.groupValues?.getOrNull(1) ?: id).toIntOrNull() ?: throw ErrorLoadingException("Invalid internalId") - val season = getSeason(internalId).data.media + val season = getSeason(internalId).data.Media return SyncAPI.SyncResult( season.id.toString(), @@ -302,12 +300,12 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { //println("NAME $name NEW NAME ${name.replace(blackListRegex, "")}") val shows = searchShows(name.replace(blackListRegex, "")) - shows?.data?.page?.media?.find { + shows?.data?.Page?.media?.find { (malId ?: "NONE") == it.idMal.toString() }?.let { return it } val filtered = - shows?.data?.page?.media?.filter { + shows?.data?.Page?.media?.filter { (((it.startDate.year ?: year.toString()) == year.toString() || year == null)) } @@ -497,7 +495,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { val data = postApi(q, true) val d = parseJson(data ?: return null) - val main = d.data?.media + val main = d.data?.Media if (main?.mediaListEntry != null) { return AniListTitleHolder( title = main.title, @@ -537,7 +535,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { headers = mapOf( "Authorization" to "Bearer " + (getAuth() ?: return@suspendSafeApiCall null), - if (cache) "Cache-Control" to "max-stale=$MAX_STALE" else "Cache-Control" to "no-cache" + if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache" ), cacheTime = 0, data = mapOf( @@ -632,9 +630,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ?: this.media.coverImage.medium, null, null, - this.media.seasonYear.toYear(), null, - plot = this.media.description, + plot = this.media.description ) } } @@ -649,7 +646,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class Data( - @JsonProperty("MediaListCollection") val mediaListCollection: MediaListCollection + @JsonProperty("MediaListCollection") val MediaListCollection: MediaListCollection ) private fun getAniListListCached(): Array? { @@ -661,7 +658,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { if (checkToken()) return null return if (requireLibraryRefresh) { - val list = getFullAniListList()?.data?.mediaListCollection?.lists?.toTypedArray() + val list = getFullAniListList()?.data?.MediaListCollection?.lists?.toTypedArray() if (list != null) { setKey(ANILIST_CACHED_LIST, list) } @@ -680,7 +677,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { // To fill empty lists when AniList does not return them val baseMap = - AniListStatusType.entries.filter { it.value >= 0 }.associate { + AniListStatusType.values().filter { it.value >= 0 }.associate { it.stringRes to emptyList() } @@ -691,8 +688,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ListSorting.AlphabeticalZ, ListSorting.UpdatedNew, ListSorting.UpdatedOld, - ListSorting.ReleaseDateNew, - ListSorting.ReleaseDateOld, ListSorting.RatingHigh, ListSorting.RatingLow, ) @@ -768,7 +763,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { /** Used to query a saved MediaItem on the list to get the id for removal */ data class MediaListItemRoot(@JsonProperty("data") val data: MediaListItem? = null) - data class MediaListItem(@JsonProperty("MediaList") val mediaList: MediaListId? = null) + data class MediaListItem(@JsonProperty("MediaList") val MediaList: MediaListId? = null) data class MediaListId(@JsonProperty("id") val id: Long? = null) private suspend fun postDataAboutId( @@ -791,7 +786,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { """ val response = postApi(idQuery) val listId = - tryParseJson(response)?.data?.mediaList?.id ?: return false + tryParseJson(response)?.data?.MediaList?.id ?: return false """ mutation(${'$'}id: Int = $listId) { DeleteMediaListEntry(id: ${'$'}id) { @@ -840,7 +835,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { val data = postApi(q) if (data.isNullOrBlank()) return null val userData = parseJson(data) - val u = userData.data?.viewer + val u = userData.data?.Viewer val user = AniListUser( u?.id, u?.name, @@ -862,8 +857,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { suspend fun getSeasonRecursive(id: Int) { val season = getSeason(id) seasons.add(season) - if (season.data.media.format?.startsWith("TV") == true) { - season.data.media.relations?.edges?.forEach { + if (season.data.Media.format?.startsWith("TV") == true) { + season.data.Media.relations?.edges?.forEach { if (it.node?.format != null) { if (it.relationType == "SEQUEL" && it.node.format.startsWith("TV")) { getSeasonRecursive(it.node.id) @@ -882,7 +877,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class SeasonData( - @JsonProperty("Media") val media: SeasonMedia, + @JsonProperty("Media") val Media: SeasonMedia, ) data class SeasonMedia( @@ -1054,7 +1049,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class AniListData( - @JsonProperty("Viewer") val viewer: AniListViewer?, + @JsonProperty("Viewer") val Viewer: AniListViewer?, ) data class AniListRoot( @@ -1094,7 +1089,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class LikeData( - @JsonProperty("Viewer") val viewer: LikeViewer?, + @JsonProperty("Viewer") val Viewer: LikeViewer?, ) data class LikeRoot( @@ -1134,7 +1129,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class GetDataData( - @JsonProperty("Media") val media: GetDataMedia?, + @JsonProperty("Media") val Media: GetDataMedia?, ) data class GetDataRoot( @@ -1167,7 +1162,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class GetSearchPage( - @JsonProperty("Page") val page: GetSearchData?, + @JsonProperty("Page") val Page: GetSearchData?, ) data class GetSearchData( diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt index 94537ea3..7ec168da 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt @@ -11,7 +11,6 @@ class Dropbox : OAuth2API { override val key = "zlqsamadlwydvb2" override val redirectUrl = "dropboxlogin" override val requiresLogin = true - override val supportDeviceAuth = false override val createAccountUrl: String? = null override val icon: Int diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt new file mode 100644 index 00000000..1adecce9 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt @@ -0,0 +1,265 @@ +package com.lagradost.cloudstream3.syncproviders.providers + +import android.util.Log +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.imdbUrlToIdNullable +import com.lagradost.cloudstream3.subtitles.AbstractSubApi +import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities +import com.lagradost.cloudstream3.utils.SubtitleHelper + +class IndexSubtitleApi : AbstractSubApi { + override val name = "IndexSubtitle" + override val idPrefix = "indexsubtitle" + override val requiresLogin = false + override val icon: Nothing? = null + override val createAccountUrl: Nothing? = null + + override fun loginInfo(): Nothing? = null + + override fun logOut() {} + + + companion object { + const val host = "https://indexsubtitle.com" + const val TAG = "INDEXSUBS" + + fun getOrdinal(num: Int?): String? { + return when (num) { + 1 -> "First" + 2 -> "Second" + 3 -> "Third" + 4 -> "Fourth" + 5 -> "Fifth" + 6 -> "Sixth" + 7 -> "Seventh" + 8 -> "Eighth" + 9 -> "Ninth" + 10 -> "Tenth" + 11 -> "Eleventh" + 12 -> "Twelfth" + 13 -> "Thirteenth" + 14 -> "Fourteenth" + 15 -> "Fifteenth" + 16 -> "Sixteenth" + 17 -> "Seventeenth" + 18 -> "Eighteenth" + 19 -> "Nineteenth" + 20 -> "Twentieth" + 21 -> "Twenty-First" + 22 -> "Twenty-Second" + 23 -> "Twenty-Third" + 24 -> "Twenty-Fourth" + 25 -> "Twenty-Fifth" + 26 -> "Twenty-Sixth" + 27 -> "Twenty-Seventh" + 28 -> "Twenty-Eighth" + 29 -> "Twenty-Ninth" + 30 -> "Thirtieth" + 31 -> "Thirty-First" + 32 -> "Thirty-Second" + 33 -> "Thirty-Third" + 34 -> "Thirty-Fourth" + 35 -> "Thirty-Fifth" + else -> null + } + } + } + + private fun fixUrl(url: String): String { + if (url.startsWith("http")) { + return url + } + if (url.isEmpty()) { + return "" + } + + val startsWithNoHttp = url.startsWith("//") + if (startsWithNoHttp) { + return "https:$url" + } else { + if (url.startsWith('/')) { + return host + url + } + return "$host/$url" + } + } + + private fun isRightEps(text: String, seasonNum: Int?, epNum: Int?): Boolean { + val FILTER_EPS_REGEX = + Regex("(?i)((Chapter\\s?0?${epNum})|((Season)?\\s?0?${seasonNum}?\\s?(Episode)\\s?0?${epNum}[^0-9]))|(?i)((S?0?${seasonNum}?E0?${epNum}[^0-9])|(0?${seasonNum}[a-z]0?${epNum}[^0-9]))") + return text.contains(FILTER_EPS_REGEX) + } + + private fun haveEps(text: String): Boolean { + val HAVE_EPS_REGEX = + Regex("(?i)((Chapter\\s?0?\\d)|((Season)?\\s?0?\\d?\\s?(Episode)\\s?0?\\d))|(?i)((S?0?\\d?E0?\\d)|(0?\\d[a-z]0?\\d))") + return text.contains(HAVE_EPS_REGEX) + } + + override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List { + val imdbId = query.imdb ?: 0 + val lang = query.lang + val queryLang = SubtitleHelper.fromTwoLettersToLanguage(lang.toString()) + val queryText = query.query + val epNum = query.epNumber ?: 0 + val seasonNum = query.seasonNumber ?: 0 + val yearNum = query.year ?: 0 + + val urlItems = ArrayList() + + fun cleanResources( + results: MutableList, + name: String, + link: String + ) { + results.add( + AbstractSubtitleEntities.SubtitleEntity( + idPrefix = idPrefix, + name = name, + lang = queryLang.toString(), + data = link, + source = this.name, + type = if (seasonNum > 0) TvType.TvSeries else TvType.Movie, + epNumber = epNum, + seasonNumber = seasonNum, + year = yearNum, + ) + ) + } + + val document = app.get("$host/?search=$queryText").document + + document.select("div.my-3.p-3 div.media").map { block -> + if (seasonNum > 0) { + val name = block.select("strong.text-primary, strong.text-info").text().trim() + val season = getOrdinal(seasonNum) + if ((block.selectFirst("a")?.attr("href") + ?.contains( + "$season", + ignoreCase = true + )!! || name.contains( + "$season", + ignoreCase = true + )) && name.contains(queryText, ignoreCase = true) + ) { + block.select("div.media").mapNotNull { + urlItems.add( + fixUrl( + it.selectFirst("a")!!.attr("href") + ) + ) + } + } + } else { + if (block.selectFirst("strong")!!.text().trim() + .matches(Regex("(?i)^$queryText\$")) + ) { + if (block.select("span[title=Release]").isNullOrEmpty()) { + block.select("div.media").mapNotNull { + val urlItem = fixUrl( + it.selectFirst("a")!!.attr("href") + ) + val itemDoc = app.get(urlItem).document + val id = imdbUrlToIdNullable( + itemDoc.selectFirst("div.d-flex span.badge.badge-primary")?.parent() + ?.attr("href") + )?.toLongOrNull() + val year = itemDoc.selectFirst("div.d-flex span.badge.badge-success") + ?.ownText() + ?.trim().toString() + Log.i(TAG, "id => $id \nyear => $year||$yearNum") + if (imdbId > 0) { + if (id == imdbId) { + urlItems.add(urlItem) + } + } else { + if (year.contains("$yearNum")) { + urlItems.add(urlItem) + } + } + } + } else { + if (block.select("span[title=Release]").text().trim() + .contains("$yearNum") + ) { + block.select("div.media").mapNotNull { + urlItems.add( + fixUrl( + it.selectFirst("a")!!.attr("href") + ) + ) + } + } + } + } + } + } + Log.i(TAG, "urlItems => $urlItems") + val results = mutableListOf() + + urlItems.forEach { url -> + val request = app.get(url) + if (request.isSuccessful) { + request.document.select("div.my-3.p-3 div.media").map { block -> + if (block.select("span.d-block span[data-original-title=Language]").text() + .trim() + .contains("$queryLang") + ) { + var name = block.select("strong.text-primary, strong.text-info").text().trim() + val link = fixUrl(block.selectFirst("a")!!.attr("href")) + if (seasonNum > 0) { + when { + isRightEps(name, seasonNum, epNum) -> { + cleanResources(results, name, link) + } + !(haveEps(name)) -> { + name = "$name (S${seasonNum}:E${epNum})" + cleanResources(results, name, link) + } + } + } else { + cleanResources(results, name, link) + } + } + } + } + } + return results + } + + override suspend fun load(data: AbstractSubtitleEntities.SubtitleEntity): String? { + val seasonNum = data.seasonNumber + val epNum = data.epNumber + + val req = app.get(data.data) + + if (req.isSuccessful) { + val document = req.document + val link = if (document.select("div.my-3.p-3 div.media").size == 1) { + fixUrl( + document.selectFirst("div.my-3.p-3 div.media a")!!.attr("href") + ) + } else { + document.select("div.my-3.p-3 div.media").firstNotNullOf { block -> + val name = + block.selectFirst("strong.d-block")?.text()?.trim().toString() + if (seasonNum!! > 0) { + if (isRightEps(name, seasonNum, epNum)) { + fixUrl(block.selectFirst("a")!!.attr("href")) + } else { + null + } + } else { + fixUrl(block.selectFirst("a")!!.attr("href")) + } + } + } + return link + } + + return null + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt index 0d9a4d13..7552fe9d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt @@ -21,7 +21,6 @@ class LocalList : SyncAPI { override val name = "Local" override val icon: Int = R.drawable.ic_baseline_storage_24 override val requiresLogin = false - override val supportDeviceAuth = false override val createAccountUrl: Nothing? = null override val idPrefix = "local" override var requireLibraryRefresh = true @@ -119,11 +118,8 @@ class LocalList : SyncAPI { ListSorting.AlphabeticalZ, ListSorting.UpdatedNew, ListSorting.UpdatedOld, - ListSorting.ReleaseDateNew, - ListSorting.ReleaseDateOld, // ListSorting.RatingHigh, // ListSorting.RatingLow, - ) ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt index 08c18653..fdbe763a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt @@ -19,19 +19,14 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.txt -import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.splitQuery import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject import java.net.URL import java.security.SecureRandom import java.text.ParseException import java.text.SimpleDateFormat -import java.time.Instant -import java.time.format.DateTimeFormatter -import java.util.Calendar -import java.util.Date -import java.util.Locale -import java.util.TimeZone +import java.util.* /** max 100 via https://myanimelist.net/apiconfig/references/api/v2#tag/anime */ const val MAL_MAX_SEARCH_LIMIT = 25 @@ -45,7 +40,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { private val apiUrl = "https://api.myanimelist.net" override val icon = R.drawable.mal_logo override val requiresLogin = false - override val supportDeviceAuth = false override val syncIdName = SyncIdName.MyAnimeList override var requireLibraryRefresh = true override val createAccountUrl = "$mainUrl/register.php" @@ -56,6 +50,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { } override fun loginInfo(): AuthAPI.LoginInfo? { + //getMalUser(true)? getKey(accountId, MAL_USER_KEY)?.let { user -> return AuthAPI.LoginInfo( profilePicture = user.picture, @@ -88,7 +83,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { this.name, node.id.toString(), "$mainUrl/anime/${node.id}/", - node.mainPicture?.large ?: node.mainPicture?.medium + node.main_picture?.large ?: node.main_picture?.medium ) } } @@ -182,7 +177,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { private fun parseDate(string: String?): Long? { return try { - SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(string ?: return null)?.time + SimpleDateFormat("yyyy-MM-dd")?.parse(string ?: return null)?.time } catch (e: Exception) { null } @@ -194,7 +189,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { apiName = this.name, syncId = node.id.toString(), url = "$mainUrl/anime/${node.id}", - posterUrl = node.mainPicture?.large + posterUrl = node.main_picture?.large ) } @@ -248,12 +243,12 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { val internalId = id.toIntOrNull() ?: return null val data = - getDataAboutMalId(internalId)?.myListStatus //?: throw ErrorLoadingException("No my_list_status") + getDataAboutMalId(internalId)?.my_list_status //?: throw ErrorLoadingException("No my_list_status") return SyncAPI.SyncStatus( score = data?.score, - status = SyncWatchType.fromInternalId(malStatusAsString.indexOf(data?.status)), + status = SyncWatchType.fromInternalId(malStatusAsString.indexOf(data?.status)) , isFavorite = null, - watchedEpisodes = data?.numEpisodesWatched, + watchedEpisodes = data?.num_episodes_watched, ) } @@ -295,7 +290,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { private fun parseDateLong(string: String?): Long? { return try { - SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()).parse( + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse( string ?: return null )?.time?.div(1000) } catch (e: Exception) { @@ -306,7 +301,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun handleRedirect(url: String): Boolean { val sanitizer = - splitQuery(URL(url.replace(APP_STRING, "https").replace("/#", "?"))) // FIX ERROR + splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR val state = sanitizer["state"]!! if (state == "RequestID$requestId") { val currentCode = sanitizer["code"]!! @@ -355,9 +350,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { try { if (response != "") { val token = parseJson(response) - setKey(accountId, MAL_UNIXTIME_KEY, (token.expiresIn + unixTime)) - setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refreshToken) - setKey(accountId, MAL_TOKEN_KEY, token.accessToken) + setKey(accountId, MAL_UNIXTIME_KEY, (token.expires_in + unixTime)) + setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refresh_token) + setKey(accountId, MAL_TOKEN_KEY, token.access_token) requireLibraryRefresh = true } } catch (e: Exception) { @@ -399,62 +394,56 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class Node( @JsonProperty("id") val id: Int, @JsonProperty("title") val title: String, - @JsonProperty("main_picture") val mainPicture: MainPicture?, - @JsonProperty("alternative_titles") val alternativeTitles: AlternativeTitles?, - @JsonProperty("media_type") val mediaType: String?, - @JsonProperty("num_episodes") val numEpisodes: Int?, + @JsonProperty("main_picture") val main_picture: MainPicture?, + @JsonProperty("alternative_titles") val alternative_titles: AlternativeTitles?, + @JsonProperty("media_type") val media_type: String?, + @JsonProperty("num_episodes") val num_episodes: Int?, @JsonProperty("status") val status: String?, - @JsonProperty("start_date") val startDate: String?, - @JsonProperty("end_date") val endDate: String?, - @JsonProperty("average_episode_duration") val averageEpisodeDuration: Int?, + @JsonProperty("start_date") val start_date: String?, + @JsonProperty("end_date") val end_date: String?, + @JsonProperty("average_episode_duration") val average_episode_duration: Int?, @JsonProperty("synopsis") val synopsis: String?, @JsonProperty("mean") val mean: Double?, @JsonProperty("genres") val genres: List?, @JsonProperty("rank") val rank: Int?, @JsonProperty("popularity") val popularity: Int?, - @JsonProperty("num_list_users") val numListUsers: Int?, - @JsonProperty("num_favorites") val numFavorites: Int?, - @JsonProperty("num_scoring_users") val numScoringUsers: Int?, - @JsonProperty("start_season") val startSeason: StartSeason?, + @JsonProperty("num_list_users") val num_list_users: Int?, + @JsonProperty("num_favorites") val num_favorites: Int?, + @JsonProperty("num_scoring_users") val num_scoring_users: Int?, + @JsonProperty("start_season") val start_season: StartSeason?, @JsonProperty("broadcast") val broadcast: Broadcast?, @JsonProperty("nsfw") val nsfw: String?, - @JsonProperty("created_at") val createdAt: String?, - @JsonProperty("updated_at") val updatedAt: String? + @JsonProperty("created_at") val created_at: String?, + @JsonProperty("updated_at") val updated_at: String? ) data class ListStatus( @JsonProperty("status") val status: String?, @JsonProperty("score") val score: Int, - @JsonProperty("num_episodes_watched") val numEpisodesWatched: Int, - @JsonProperty("is_rewatching") val isRewatching: Boolean, - @JsonProperty("updated_at") val updatedAt: String, + @JsonProperty("num_episodes_watched") val num_episodes_watched: Int, + @JsonProperty("is_rewatching") val is_rewatching: Boolean, + @JsonProperty("updated_at") val updated_at: String, ) data class Data( @JsonProperty("node") val node: Node, - @JsonProperty("list_status") val listStatus: ListStatus?, + @JsonProperty("list_status") val list_status: ListStatus?, ) { fun toLibraryItem(): SyncAPI.LibraryItem { return SyncAPI.LibraryItem( this.node.title, "https://myanimelist.net/anime/${this.node.id}/", this.node.id.toString(), - this.listStatus?.numEpisodesWatched, - this.node.numEpisodes, - this.listStatus?.score?.times(10), - parseDateLong(this.listStatus?.updatedAt), + this.list_status?.num_episodes_watched, + this.node.num_episodes, + this.list_status?.score?.times(10), + parseDateLong(this.list_status?.updated_at), "MAL", TvType.Anime, - this.node.mainPicture?.large ?: this.node.mainPicture?.medium, + this.node.main_picture?.large ?: this.node.main_picture?.medium, null, null, plot = this.node.synopsis, - releaseDate = if (this.node.startDate == null) null else try {Date.from( - Instant.from( - DateTimeFormatter.ofPattern(if (this.node.startDate.length == 4) "yyyy" else if (this.node.startDate.length == 7) "yyyy-MM" else "yyyy-MM-dd") - .parse(this.node.startDate) - ) - )} catch (_: RuntimeException) {null} ) } } @@ -480,8 +469,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { ) data class Broadcast( - @JsonProperty("day_of_the_week") val dayOfTheWeek: String?, - @JsonProperty("start_time") val startTime: String? + @JsonProperty("day_of_the_week") val day_of_the_week: String?, + @JsonProperty("start_time") val start_time: String? ) private fun getMalAnimeListCached(): Array? { @@ -501,14 +490,14 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata { val list = getMalAnimeListSmart()?.groupBy { - convertToStatus(it.listStatus?.status ?: "").stringRes + convertToStatus(it.list_status?.status ?: "").stringRes }?.mapValues { group -> group.value.map { it.toLibraryItem() } } ?: emptyMap() // To fill empty lists when MAL does not return them val baseMap = - MalStatusType.entries.filter { it.value >= 0 }.associate { + MalStatusType.values().filter { it.value >= 0 }.associate { it.stringRes to emptyList() } @@ -519,8 +508,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { ListSorting.AlphabeticalZ, ListSorting.UpdatedNew, ListSorting.UpdatedOld, - ListSorting.ReleaseDateNew, - ListSorting.ReleaseDateOld, ListSorting.RatingHigh, ListSorting.RatingLow, ) @@ -585,7 +572,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { ).text val values = parseJson(res) val titles = - values.data.map { MalTitleHolder(it.listStatus, it.node.id, it.node.title) } + values.data.map { MalTitleHolder(it.list_status, it.node.id, it.node.title) } for (t in titles) { allTitles[t.id] = t } @@ -594,13 +581,11 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { } } - private fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? { + fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? { // No time remaining if the show has already ended try { endDate?.let { - if (SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(it) - ?.before(Date.from(Instant.now())) != false - ) return@convertJapanTimeToTimeRemaining null + if (SimpleDateFormat("yyyy-MM-dd").parse(it).time < System.currentTimeMillis()) return@convertJapanTimeToTimeRemaining null } } catch (e: ParseException) { logError(e) @@ -617,7 +602,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { val currentWeek = currentDate.get(Calendar.WEEK_OF_MONTH) val currentYear = currentDate.get(Calendar.YEAR) - val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm", Locale.getDefault()) + val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm") dateFormat.timeZone = TimeZone.getTimeZone("Japan") val parsedDate = dateFormat.parse("$currentYear $currentMonth $currentWeek $date") ?: return null @@ -661,13 +646,13 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { id: Int, status: MalStatusType? = null, score: Int? = null, - numWatchedEpisodes: Int? = null, + num_watched_episodes: Int? = null, ): Boolean { val res = setScoreRequest( id, if (status == null) null else malStatusAsString[maxOf(0, status.value)], score, - numWatchedEpisodes + num_watched_episodes ) return if (res.isNullOrBlank()) { @@ -684,18 +669,17 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { } } - @Suppress("UNCHECKED_CAST") private suspend fun setScoreRequest( id: Int, status: String? = null, score: Int? = null, - numWatchedEpisodes: Int? = null, + num_watched_episodes: Int? = null, ): String? { val data = mapOf( "status" to status, "score" to score?.toString(), - "num_watched_episodes" to numWatchedEpisodes?.toString() - ).filterValues { it != null } as Map + "num_watched_episodes" to num_watched_episodes?.toString() + ).filter { it.value != null } as Map return app.put( "$apiUrl/v2/anime/$id/my_list_status", @@ -708,10 +692,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class ResponseToken( - @JsonProperty("token_type") val tokenType: String, - @JsonProperty("expires_in") val expiresIn: Int, - @JsonProperty("access_token") val accessToken: String, - @JsonProperty("refresh_token") val refreshToken: String, + @JsonProperty("token_type") val token_type: String, + @JsonProperty("expires_in") val expires_in: Int, + @JsonProperty("access_token") val access_token: String, + @JsonProperty("refresh_token") val refresh_token: String, ) data class MalRoot( @@ -720,7 +704,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class MalDatum( @JsonProperty("node") val node: MalNode, - @JsonProperty("list_status") val listStatus: MalStatus, + @JsonProperty("list_status") val list_status: MalStatus, ) data class MalNode( @@ -737,16 +721,16 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class MalStatus( @JsonProperty("status") val status: String, @JsonProperty("score") val score: Int, - @JsonProperty("num_episodes_watched") val numEpisodesWatched: Int, - @JsonProperty("is_rewatching") val isRewatching: Boolean, - @JsonProperty("updated_at") val updatedAt: String, + @JsonProperty("num_episodes_watched") val num_episodes_watched: Int, + @JsonProperty("is_rewatching") val is_rewatching: Boolean, + @JsonProperty("updated_at") val updated_at: String, ) data class MalUser( @JsonProperty("id") val id: Int, @JsonProperty("name") val name: String, @JsonProperty("location") val location: String, - @JsonProperty("joined_at") val joinedAt: String, + @JsonProperty("joined_at") val joined_at: String, @JsonProperty("picture") val picture: String?, ) @@ -759,9 +743,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class SmallMalAnime( @JsonProperty("id") val id: Int, @JsonProperty("title") val title: String?, - @JsonProperty("num_episodes") val numEpisodes: Int, - @JsonProperty("my_list_status") val myListStatus: MalStatus?, - @JsonProperty("main_picture") val mainPicture: MalMainPicture?, + @JsonProperty("num_episodes") val num_episodes: Int, + @JsonProperty("my_list_status") val my_list_status: MalStatus?, + @JsonProperty("main_picture") val main_picture: MalMainPicture?, ) data class MalSearchNode( diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt index 37b95614..4030649d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt @@ -29,10 +29,10 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi companion object { const val OPEN_SUBTITLES_USER_KEY: String = "open_subtitles_user" // user data like profile - const val API_KEY = "uyBLgFD17MgrYmA0gSXoKllMJBelOYj2" - const val HOST = "https://api.opensubtitles.com/api/v1" + const val apiKey = "uyBLgFD17MgrYmA0gSXoKllMJBelOYj2" + const val host = "https://api.opensubtitles.com/api/v1" const val TAG = "OPENSUBS" - const val COOLDOWN_DURATION: Long = 1000L * 30L // CoolDown if 429 error code in ms + const val coolDownDuration: Long = 1000L * 30L // CoolDown if 429 error code in ms var currentCoolDown: Long = 0L var currentSession: SubtitleOAuthEntity? = null } @@ -48,7 +48,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi chain.request().newBuilder() .removeHeader("user-agent") .addHeader("user-agent", userAgent) - .addHeader("Api-Key", API_KEY) + .addHeader("Api-Key", apiKey) .build() ) } @@ -65,7 +65,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi } private fun throwGotTooManyRequests() { - currentCoolDown = unixTimeMs + COOLDOWN_DURATION + currentCoolDown = unixTimeMs + coolDownDuration throw ErrorLoadingException("Too many requests") } @@ -114,7 +114,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi private suspend fun initLogin(username: String, password: String): Boolean { //Log.i(TAG, "DATA = [$username] [$password]") val response = app.post( - url = "$HOST/login", + url = "$host/login", headers = mapOf( "Content-Type" to "application/json", ), @@ -133,7 +133,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi SubtitleOAuthEntity( user = username, pass = password, - accessToken = token.token ?: run { + access_token = token.token ?: run { return false }) ) @@ -185,7 +185,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi throwIfCantDoRequest() val fixedLang = fixLanguage(query.lang) - val imdbId = query.imdbId?.replace("tt", "")?.toInt() ?: 0 + val imdbId = query.imdb ?: 0 val queryText = query.query val epNum = query.epNumber ?: 0 val seasonNum = query.seasonNumber ?: 0 @@ -196,8 +196,8 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi val searchQueryUrl = when (imdbId > 0) { //Use imdb_id to search if its valid - true -> "$HOST/subtitles?imdb_id=$imdbId&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" - false -> "$HOST/subtitles?query=${queryText}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" + true -> "$host/subtitles?imdb_id=$imdbId&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" + false -> "$host/subtitles?query=${queryText}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" } val req = app.get( @@ -232,7 +232,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber val year = featureDetails?.year ?: query.year val type = if ((resSeasonNum ?: 0) > 0) TvType.TvSeries else TvType.Movie - val isHearingImpaired = attr.hearingImpaired ?: false + val isHearingImpaired = attr.hearing_impaired ?: false //Log.i(TAG, "Result id/name => ${item.id} / $name") item.attributes?.files?.forEach { file -> val resultData = file.fileId?.toString() ?: "" @@ -265,11 +265,11 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi throwIfCantDoRequest() val req = app.post( - url = "$HOST/download", + url = "$host/download", headers = mapOf( Pair( "Authorization", - "Bearer ${currentSession?.accessToken ?: throw ErrorLoadingException("No access token active in current session")}" + "Bearer ${currentSession?.access_token ?: throw ErrorLoadingException("No access token active in current session")}" ), Pair("Content-Type", "application/json"), Pair("Accept", "*/*") @@ -298,7 +298,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi data class SubtitleOAuthEntity( var user: String, var pass: String, - var accessToken: String, + var access_token: String, ) data class OAuthToken( @@ -323,7 +323,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi @JsonProperty("url") var url: String? = null, @JsonProperty("files") var files: List? = listOf(), @JsonProperty("feature_details") var featDetails: ResultFeatureDetails? = ResultFeatureDetails(), - @JsonProperty("hearing_impaired") var hearingImpaired: Boolean? = null, + @JsonProperty("hearing_impaired") var hearing_impaired: Boolean? = null, ) data class ResultFiles( diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index 50517f9d..08c8588b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -12,9 +12,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.BuildConfig -import com.lagradost.cloudstream3.LoadResponse.Companion.readIdFromString import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.SimklSyncServices import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mapper @@ -24,14 +22,13 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AuthAPI -import com.lagradost.cloudstream3.syncproviders.OAuth2API import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.AppUtils.toJson -import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import okhttp3.Interceptor import okhttp3.Response import java.math.BigInteger @@ -39,7 +36,6 @@ import java.security.SecureRandom import java.text.SimpleDateFormat import java.time.Instant import java.util.Date -import java.util.Locale import java.util.TimeZone import kotlin.time.Duration import kotlin.time.DurationUnit @@ -49,7 +45,6 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { override var name = "Simkl" override val key = "simkl-key" override val redirectUrl = "simkl" - override val supportDeviceAuth = true override val idPrefix = "simkl" override var requireLibraryRefresh = true override var mainUrl = "https://api.simkl.com" @@ -146,8 +141,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } companion object { - private const val CLIENT_ID: String = BuildConfig.SIMKL_CLIENT_ID - private const val CLIENT_SECRET: String = BuildConfig.SIMKL_CLIENT_SECRET + private const val clientId: String = BuildConfig.SIMKL_CLIENT_ID + private const val clientSecret: String = BuildConfig.SIMKL_CLIENT_SECRET private var lastLoginState = "" const val SIMKL_TOKEN_KEY: String = "simkl_token" @@ -156,10 +151,10 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { const val SIMKL_CACHED_LIST_TIME: String = "simkl_cached_time" /** 2014-09-01T09:10:11Z -> 1409562611 */ - private const val SIMKL_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'" + private const val simklDateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" fun getUnixTime(string: String?): Long? { return try { - SimpleDateFormat(SIMKL_DATE_FORMAT, Locale.getDefault()).apply { + SimpleDateFormat(simklDateFormat).apply { this.timeZone = TimeZone.getTimeZone("UTC") }.parse( string ?: return null @@ -173,7 +168,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { /** 1409562611 -> 2014-09-01T09:10:11Z */ fun getDateTime(unixTime: Long?): String? { return try { - SimpleDateFormat(SIMKL_DATE_FORMAT, Locale.getDefault()).apply { + SimpleDateFormat(simklDateFormat).apply { this.timeZone = TimeZone.getTimeZone("UTC") }.format( Date.from( @@ -187,6 +182,32 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } } + /** + * Set of sync services simkl is compatible with. + * Add more as required: https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id + */ + enum class SyncServices(val originalName: String) { + Simkl("simkl"), + Imdb("imdb"), + Tmdb("tmdb"), + AniList("anilist"), + Mal("mal"), + } + + /** + * The ID string is a way to keep a collection of services in one single ID using a map + * This adds a database service (like imdb) to the string and returns the new string. + */ + fun addIdToString(idString: String?, database: SyncServices, id: String?): String? { + if (id == null) return idString + return (readIdFromString(idString) + mapOf(database to id)).toJson() + } + + /** Read the id string to get all other ids */ + fun readIdFromString(idString: String?): Map { + return tryParseJson(idString) ?: return emptyMap() + } + fun getPosterUrl(poster: String): String { return "https://wsrv.nl/?url=https://simkl.in/posters/${poster}_m.webp" } @@ -210,7 +231,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { companion object { fun fromString(string: String): SimklListStatusType? { - return SimklListStatusType.entries.firstOrNull { + return SimklListStatusType.values().firstOrNull { it.originalName == string } } @@ -221,17 +242,17 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonInclude(JsonInclude.Include.NON_EMPTY) data class TokenRequest( @JsonProperty("code") val code: String, - @JsonProperty("client_id") val clientId: String = CLIENT_ID, - @JsonProperty("client_secret") val clientSecret: String = CLIENT_SECRET, - @JsonProperty("redirect_uri") val redirectUri: String = "$APP_STRING://simkl", - @JsonProperty("grant_type") val grantType: String = "authorization_code" + @JsonProperty("client_id") val client_id: String = clientId, + @JsonProperty("client_secret") val client_secret: String = clientSecret, + @JsonProperty("redirect_uri") val redirect_uri: String = "$appString://simkl", + @JsonProperty("grant_type") val grant_type: String = "authorization_code" ) data class TokenResponse( /** No expiration date */ - @JsonProperty("access_token") val accessToken: String, - @JsonProperty("token_type") val tokenType: String, - @JsonProperty("scope") val scope: String + val access_token: String, + val token_type: String, + val scope: String ) // ------------------- @@ -246,32 +267,17 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { ) } - data class PinAuthResponse( - @JsonProperty("result") val result: String, - @JsonProperty("device_code") val deviceCode: String, - @JsonProperty("user_code") val userCode: String, - @JsonProperty("verification_url") val verificationUrl: String, - @JsonProperty("expires_in") val expiresIn: Int, - @JsonProperty("interval") val interval: Int, - ) - - data class PinExchangeResponse( - @JsonProperty("result") val result: String, - @JsonProperty("message") val message: String? = null, - @JsonProperty("access_token") val accessToken: String? = null, - ) - // ------------------- data class ActivitiesResponse( - @JsonProperty("all") val all: String?, - @JsonProperty("tv_shows") val tvShows: UpdatedAt, - @JsonProperty("anime") val anime: UpdatedAt, - @JsonProperty("movies") val movies: UpdatedAt, + val all: String?, + val tv_shows: UpdatedAt, + val anime: UpdatedAt, + val movies: UpdatedAt, ) { data class UpdatedAt( - @JsonProperty("all") val all: String?, - @JsonProperty("removed_from_list") val removedFromList: String?, - @JsonProperty("rated_at") val ratedAt: String?, + val all: String?, + val removed_from_list: String?, + val rated_at: String?, ) } @@ -310,7 +316,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("title") val title: String?, @JsonProperty("year") val year: Int?, @JsonProperty("ids") val ids: Ids?, - @JsonProperty("total_episodes") val totalEpisodes: Int? = null, + @JsonProperty("total_episodes") val total_episodes: Int? = null, @JsonProperty("status") val status: String? = null, @JsonProperty("poster") val poster: String? = null, @JsonProperty("type") val type: String? = null, @@ -338,13 +344,13 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("anilist") val anilist: String? = null, ) { companion object { - fun fromMap(map: Map): Ids { + fun fromMap(map: Map): Ids { return Ids( - simkl = map[SimklSyncServices.Simkl]?.toIntOrNull(), - imdb = map[SimklSyncServices.Imdb], - tmdb = map[SimklSyncServices.Tmdb], - mal = map[SimklSyncServices.Mal], - anilist = map[SimklSyncServices.AniList] + simkl = map[SyncServices.Simkl]?.toIntOrNull(), + imdb = map[SyncServices.Imdb], + tmdb = map[SyncServices.Tmdb], + mal = map[SyncServices.Mal], + anilist = map[SyncServices.AniList] ) } } @@ -542,7 +548,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } debugPrint { "Requesting episodes from $url" } - return app.get(url, params = mapOf("client_id" to CLIENT_ID)) + return app.get(url, params = mapOf("client_id" to clientId)) .parsedSafe>()?.also { val cacheTime = if (hasEnded == true) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value @@ -560,7 +566,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("seasons") seasons: List? = null, @JsonProperty("episodes") episodes: List? = null, @JsonProperty("rating") val rating: Int? = null, - @JsonProperty("rated_at") val ratedAt: String? = null, + @JsonProperty("rated_at") val rated_at: String? = null, ) : MediaObject(title, year, ids, seasons = seasons, episodes = episodes) @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -569,7 +575,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("year") year: Int?, @JsonProperty("ids") ids: Ids?, @JsonProperty("rating") val rating: Int, - @JsonProperty("rated_at") val ratedAt: String? = getDateTime(unixTime) + @JsonProperty("rated_at") val rated_at: String? = getDateTime(unixTime) ) : MediaObject(title, year, ids) @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -578,7 +584,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("year") year: Int?, @JsonProperty("ids") ids: Ids?, @JsonProperty("to") val to: String, - @JsonProperty("watched_at") val watchedAt: String? = getDateTime(unixTime) + @JsonProperty("watched_at") val watched_at: String? = getDateTime(unixTime) ) : MediaObject(title, year, ids) @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -633,24 +639,24 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } interface Metadata { - val lastWatchedAt: String? + val last_watched_at: String? val status: String? - val userRating: Int? - val lastWatched: String? - val watchedEpisodesCount: Int? - val totalEpisodesCount: Int? + val user_rating: Int? + val last_watched: String? + val watched_episodes_count: Int? + val total_episodes_count: Int? fun getIds(): ShowMetadata.Show.Ids fun toLibraryItem(): SyncAPI.LibraryItem } data class MovieMetadata( - @JsonProperty("last_watched_at") override val lastWatchedAt: String?, - @JsonProperty("status") override val status: String, - @JsonProperty("user_rating") override val userRating: Int?, - @JsonProperty("last_watched") override val lastWatched: String?, - @JsonProperty("watched_episodes_count") override val watchedEpisodesCount: Int?, - @JsonProperty("total_episodes_count") override val totalEpisodesCount: Int?, + override val last_watched_at: String?, + override val status: String, + override val user_rating: Int?, + override val last_watched: String?, + override val watched_episodes_count: Int?, + override val total_episodes_count: Int?, val movie: ShowMetadata.Show ) : Metadata { override fun getIds(): ShowMetadata.Show.Ids { @@ -662,28 +668,27 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { this.movie.title, "https://simkl.com/tv/${movie.ids.simkl}", movie.ids.simkl.toString(), - this.watchedEpisodesCount, - this.totalEpisodesCount, - this.userRating?.times(10), - getUnixTime(lastWatchedAt) ?: 0, + this.watched_episodes_count, + this.total_episodes_count, + this.user_rating?.times(10), + getUnixTime(last_watched_at) ?: 0, "Simkl", TvType.Movie, this.movie.poster?.let { getPosterUrl(it) }, null, null, - this.movie.year?.toYear(), - movie.ids.simkl + movie.ids.simkl, ) } } data class ShowMetadata( - @JsonProperty("last_watched_at") override val lastWatchedAt: String?, + @JsonProperty("last_watched_at") override val last_watched_at: String?, @JsonProperty("status") override val status: String, - @JsonProperty("user_rating") override val userRating: Int?, - @JsonProperty("last_watched") override val lastWatched: String?, - @JsonProperty("watched_episodes_count") override val watchedEpisodesCount: Int?, - @JsonProperty("total_episodes_count") override val totalEpisodesCount: Int?, + @JsonProperty("user_rating") override val user_rating: Int?, + @JsonProperty("last_watched") override val last_watched: String?, + @JsonProperty("watched_episodes_count") override val watched_episodes_count: Int?, + @JsonProperty("total_episodes_count") override val total_episodes_count: Int?, @JsonProperty("show") val show: Show ) : Metadata { override fun getIds(): Show.Ids { @@ -695,16 +700,15 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { this.show.title, "https://simkl.com/tv/${show.ids.simkl}", show.ids.simkl.toString(), - this.watchedEpisodesCount, - this.totalEpisodesCount, - this.userRating?.times(10), - getUnixTime(lastWatchedAt) ?: 0, + this.watched_episodes_count, + this.total_episodes_count, + this.user_rating?.times(10), + getUnixTime(last_watched_at) ?: 0, "Simkl", TvType.Anime, this.show.poster?.let { getPosterUrl(it) }, null, null, - this.show.year?.toYear(), show.ids.simkl ) } @@ -728,13 +732,13 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("anilist") val anilist: String?, @JsonProperty("traktslug") val traktslug: String? ) { - fun matchesId(database: SimklSyncServices, id: String): Boolean { + fun matchesId(database: SyncServices, id: String): Boolean { return when (database) { - SimklSyncServices.Simkl -> this.simkl == id.toIntOrNull() - SimklSyncServices.AniList -> this.anilist == id - SimklSyncServices.Mal -> this.mal == id - SimklSyncServices.Tmdb -> this.tmdb == id - SimklSyncServices.Imdb -> this.imdb == id + SyncServices.Simkl -> this.simkl == id.toIntOrNull() + SyncServices.AniList -> this.anilist == id + SyncServices.Mal -> this.mal == id + SyncServices.Tmdb -> this.tmdb == id + SyncServices.Imdb -> this.imdb == id } } } @@ -753,7 +757,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { chain.request() .newBuilder() .addHeader("Authorization", "Bearer $token") - .addHeader("simkl-api-key", CLIENT_ID) + .addHeader("simkl-api-key", clientId) .build() ) } @@ -814,7 +818,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { val episodeConstructor = SimklEpisodeConstructor( searchResult.ids?.simkl, searchResult.type, - searchResult.totalEpisodes, + searchResult.total_episodes, searchResult.hasEnded() ) @@ -836,12 +840,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { ) } ?: return null, - score = foundItem.userRating, - watchedEpisodes = foundItem.watchedEpisodesCount, - maxEpisodes = searchResult.totalEpisodes, + score = foundItem.user_rating, + watchedEpisodes = foundItem.watched_episodes_count, + maxEpisodes = searchResult.total_episodes, episodeConstructor = episodeConstructor, - oldEpisodes = foundItem.watchedEpisodesCount ?: 0, - oldScore = foundItem.userRating, + oldEpisodes = foundItem.watched_episodes_count ?: 0, + oldScore = foundItem.user_rating, oldStatus = foundItem.status ) } else { @@ -849,7 +853,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { status = SyncWatchType.fromInternalId(SimklListStatusType.None.value), score = 0, watchedEpisodes = 0, - maxEpisodes = if (searchResult.type == "movie") 0 else searchResult.totalEpisodes, + maxEpisodes = if (searchResult.type == "movie") 0 else searchResult.total_episodes, episodeConstructor = episodeConstructor, oldEpisodes = 0, oldStatus = null, @@ -895,12 +899,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { /** See https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id */ - private suspend fun searchByIds(serviceMap: Map): Array? { + suspend fun searchByIds(serviceMap: Map): Array? { if (serviceMap.isEmpty()) return emptyArray() return app.get( "$mainUrl/search/id", - params = mapOf("client_id" to CLIENT_ID) + serviceMap.map { (service, id) -> + params = mapOf("client_id" to clientId) + serviceMap.map { (service, id) -> service.originalName to id } ).parsedSafe() @@ -908,14 +912,14 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun search(name: String): List? { return app.get( - "$mainUrl/search/", params = mapOf("client_id" to CLIENT_ID, "q" to name) + "$mainUrl/search/", params = mapOf("client_id" to clientId, "q" to name) ).parsedSafe>()?.mapNotNull { it.toSyncSearchResult() } } override fun authenticate(activity: FragmentActivity?) { lastLoginState = BigInteger(130, SecureRandom()).toString(32) val url = - "https://simkl.com/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$APP_STRING://${redirectUrl}&state=$lastLoginState" + "https://simkl.com/oauth/authorize?response_type=code&client_id=$clientId&redirect_uri=$appString://${redirectUrl}&state=$lastLoginState" openBrowser(url, activity) } @@ -965,15 +969,15 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { val activities = getActivities() val lastCacheUpdate = getKey(accountId, SIMKL_CACHED_LIST_TIME) val lastRemoval = listOf( - activities?.tvShows?.removedFromList, - activities?.anime?.removedFromList, - activities?.movies?.removedFromList + activities?.tv_shows?.removed_from_list, + activities?.anime?.removed_from_list, + activities?.movies?.removed_from_list ).maxOf { getUnixTime(it) ?: -1 } val lastRealUpdate = listOf( - activities?.tvShows?.all, + activities?.tv_shows?.all, activities?.anime?.all, activities?.movies?.all, ).maxOf { @@ -1030,8 +1034,6 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { ListSorting.AlphabeticalZ, ListSorting.UpdatedNew, ListSorting.UpdatedOld, - ListSorting.ReleaseDateNew, - ListSorting.ReleaseDateOld, ListSorting.RatingHigh, ListSorting.RatingLow, ) @@ -1043,44 +1045,6 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { return simklUrlRegex.find(url)?.groupValues?.get(1) ?: "" } - override suspend fun getDevicePin(): OAuth2API.PinAuthData? { - val pinAuthResp = app.get( - "$mainUrl/oauth/pin?client_id=$CLIENT_ID&redirect_uri=$APP_STRING://${redirectUrl}" - ).parsedSafe() ?: return null - - return OAuth2API.PinAuthData( - deviceCode = pinAuthResp.deviceCode, - userCode = pinAuthResp.userCode, - verificationUrl = pinAuthResp.verificationUrl, - expiresIn = pinAuthResp.expiresIn, - interval = pinAuthResp.interval - ) - } - - override suspend fun handleDeviceAuth(pinAuthData: OAuth2API.PinAuthData): Boolean { - val pinAuthResp = app.get( - "$mainUrl/oauth/pin/${pinAuthData.userCode}?client_id=$CLIENT_ID" - ).parsedSafe() ?: return false - - if (pinAuthResp.accessToken != null) { - switchToNewAccount() - setKey(accountId, SIMKL_TOKEN_KEY, pinAuthResp.accessToken) - - val user = getUser() - if (user == null) { - removeKey(accountId, SIMKL_TOKEN_KEY) - switchToOldAccount() - return false - } - - setKey(accountId, SIMKL_USER_KEY, user) - registerAccount() - requireLibraryRefresh = true - return true - } - return false - } - override suspend fun handleRedirect(url: String): Boolean { val uri = url.toUri() val state = uri.getQueryParameter("state") @@ -1094,7 +1058,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { ).parsedSafe() ?: return false switchToNewAccount() - setKey(accountId, SIMKL_TOKEN_KEY, token.accessToken) + setKey(accountId, SIMKL_TOKEN_KEY, token.access_token) val user = getUser() if (user == null) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubScene.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubScene.kt new file mode 100644 index 00000000..fbe05026 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubScene.kt @@ -0,0 +1,118 @@ +package com.lagradost.cloudstream3.syncproviders.providers + +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mvvm.debugPrint +import com.lagradost.cloudstream3.subtitles.AbstractSubProvider +import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities +import com.lagradost.cloudstream3.subtitles.SubtitleResource +import com.lagradost.cloudstream3.syncproviders.providers.IndexSubtitleApi.Companion.getOrdinal +import com.lagradost.cloudstream3.utils.SubtitleHelper + +class SubScene : AbstractSubProvider { + val mainUrl = "https://subscene.com" + val name = "Subscene" + override val idPrefix = "subscene" + + override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List? { + val seasonName = + query.seasonNumber?.let { number -> + // Need to translate "7" to "Seventh Season" + getOrdinal(number)?.let { words -> " - $words Season" } + } ?: "" + + val fullQuery = query.query + seasonName + + val doc = app.post( + "$mainUrl/subtitles/searchbytitle", + data = mapOf("query" to fullQuery, "l" to "") + ).document + + return doc.select("div.title a").map { element -> + val href = "$mainUrl${element.attr("href")}" + val title = element.text() + + AbstractSubtitleEntities.SubtitleEntity( + idPrefix = idPrefix, + name = title, + source = name, + data = href, + lang = query.lang ?: "en", + epNumber = query.epNumber + ) + }.distinctBy { it.data } + } + + override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) { + val resultDoc = app.get(data.data).document + val queryLanguage = SubtitleHelper.fromTwoLettersToLanguage(data.lang) ?: "English" + + val results = resultDoc.select("table tbody tr").mapNotNull { element -> + val anchor = element.select("a") + val href = anchor.attr("href") ?: return@mapNotNull null + val fixedHref = "$mainUrl${href}" + val spans = anchor.select("span") + val language = spans.firstOrNull()?.text() + val title = spans.getOrNull(1)?.text() + val isPositive = anchor.select("span.positive-icon").isNotEmpty() + + TableElement(title, language, fixedHref, isPositive) + }.sortedBy { + it.getScore(queryLanguage, data.epNumber) + } + + debugPrint { "$name found subtitles: ${results.takeLast(3)}" } + // Last = highest score + val selectedResult = results.lastOrNull() ?: return + + val subtitleDocument = app.get(selectedResult.href).document + val subtitleDownloadUrl = + "$mainUrl${subtitleDocument.select("div.download a").attr("href")}" + + this.addZipUrl(subtitleDownloadUrl) { name, _ -> + name + } + } + + /** + * Class to manage the various different subtitle results and rank them. + */ + data class TableElement( + val title: String?, + val language: String?, + val href: String, + val isPositive: Boolean + ) { + private fun matchesLanguage(other: String): Boolean { + return language != null && (language.contains(other, ignoreCase = true) || + other.contains(language, ignoreCase = true)) + } + + /** + * Scores in this order: + * Preferred Language > Episode number > Positive rating > English Language + */ + fun getScore(queryLanguage: String, episodeNum: Int?): Int { + var score = 0 + if (this.matchesLanguage(queryLanguage)) { + score += 8 + } + // Matches Episode 7 using "E07" with any number of leading zeroes + if (episodeNum != null && title != null && title.contains( + Regex( + """E0*${episodeNum}""", + RegexOption.IGNORE_CASE + ) + ) + ) { + score += 4 + } + if (isPositive) { + score += 2 + } + if (this.matchesLanguage("English")) { + score += 1 + } + return score + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt deleted file mode 100644 index 8dad1f88..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt +++ /dev/null @@ -1,159 +0,0 @@ -package com.lagradost.cloudstream3.syncproviders.providers - -import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.subtitles.AbstractSubProvider -import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities -import com.lagradost.cloudstream3.subtitles.SubtitleResource -import com.lagradost.cloudstream3.utils.AppUtils.parseJson -import com.lagradost.cloudstream3.utils.AppUtils.toJson -import com.lagradost.cloudstream3.utils.SubtitleHelper - -class SubSourceApi : AbstractSubProvider { - override val idPrefix = "subsource" - val name = "SubSource" - - companion object { - const val APIURL = "https://api.subsource.net/api" - const val DOWNLOADENDPOINT = "https://api.subsource.net/api/downloadSub" - } - - override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List? { - - //Only supports Imdb Id search for now - if (query.imdbId == null) return null - val queryLang = SubtitleHelper.fromTwoLettersToLanguage(query.lang!!) - val type = if ((query.seasonNumber ?: 0) > 0) TvType.TvSeries else TvType.Movie - - val searchRes = app.post( - url = "$APIURL/searchMovie", - data = mapOf( - "query" to query.imdbId!! - ) - ).parsedSafe() ?: return null - - val postData = if (type == TvType.TvSeries) { - mapOf( - "langs" to "[]", - "movieName" to searchRes.found.first().linkName, - "season" to "season-${query.seasonNumber}" - ) - } else { - mapOf( - "langs" to "[]", - "movieName" to searchRes.found.first().linkName, - ) - } - - val getMovieRes = app.post( - url = "$APIURL/getMovie", - data = postData - ).parsedSafe().let { - // api doesn't has episode number or lang filtering - if (type == TvType.Movie) { - it?.subs?.filter { sub -> - sub.lang == queryLang - } - } else { - it?.subs?.filter { sub -> - sub.releaseName!!.contains( - String.format( - null, - "E%02d", - query.epNumber - ) - ) && sub.lang == queryLang - } - } - } ?: return null - - return getMovieRes.map { subtitle -> - AbstractSubtitleEntities.SubtitleEntity( - idPrefix = this.idPrefix, - name = subtitle.releaseName!!, - lang = subtitle.lang!!, - data = SubData( - movie = subtitle.linkName!!, - lang = subtitle.lang, - id = subtitle.subId.toString(), - ).toJson(), - type = type, - source = this.name, - epNumber = query.epNumber, - seasonNumber = query.seasonNumber, - isHearingImpaired = subtitle.hi == 1, - ) - } - } - - override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) { - - val parsedSub = parseJson(data.data) - - val subRes = app.post( - url = "$APIURL/getSub", - data = mapOf( - "movie" to parsedSub.movie, - "lang" to data.lang, - "id" to parsedSub.id - ) - ).parsedSafe() ?: return - - this.addZipUrl( - "$DOWNLOADENDPOINT/${subRes.sub.downloadToken}" - ) { name, _ -> - name - } - } - - data class ApiSearch( - @JsonProperty("success") val success: Boolean, - @JsonProperty("found") val found: List, - ) - - data class Found( - @JsonProperty("id") val id: Long, - @JsonProperty("title") val title: String, - @JsonProperty("seasons") val seasons: Long, - @JsonProperty("type") val type: String, - @JsonProperty("releaseYear") val releaseYear: Long, - @JsonProperty("linkName") val linkName: String, - ) - - data class ApiResponse( - @JsonProperty("success") val success: Boolean, - @JsonProperty("movie") val movie: Movie, - @JsonProperty("subs") val subs: List, - ) - - data class Movie( - @JsonProperty("id") val id: Long? = null, - @JsonProperty("type") val type: String? = null, - @JsonProperty("year") val year: Long? = null, - @JsonProperty("fullName") val fullName: String? = null, - ) - - data class Sub( - @JsonProperty("hi") val hi: Int? = null, - @JsonProperty("fullLink") val fullLink: String? = null, - @JsonProperty("linkName") val linkName: String? = null, - @JsonProperty("lang") val lang: String? = null, - @JsonProperty("releaseName") val releaseName: String? = null, - @JsonProperty("subId") val subId: Long? = null, - ) - - data class SubData( - @JsonProperty("movie") val movie: String, - @JsonProperty("lang") val lang: String, - @JsonProperty("id") val id: String, - ) - - data class SubTitleLink( - @JsonProperty("sub") val sub: SubToken, - ) - - data class SubToken( - @JsonProperty("downloadToken") val downloadToken: String, - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt deleted file mode 100644 index 29544e65..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt +++ /dev/null @@ -1,247 +0,0 @@ -package com.lagradost.cloudstream3.syncproviders.providers - -import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.AcraApplication.Companion.getKey -import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey -import com.lagradost.cloudstream3.AcraApplication.Companion.setKey -import com.lagradost.cloudstream3.ErrorLoadingException -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.subtitles.AbstractSubApi -import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities -import com.lagradost.cloudstream3.subtitles.SubtitleResource -import com.lagradost.cloudstream3.syncproviders.AuthAPI.LoginInfo -import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI -import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager - -class SubDlApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi { - override val idPrefix = "subdl" - override val name = "SubDL" - override val icon = R.drawable.subdl_logo_big - override val requiresPassword = true - override val requiresEmail = true - override val createAccountUrl = "https://subdl.com/login" - - companion object { - const val APIURL = "https://api.subdl.com" - const val APIENDPOINT = "$APIURL/api/v1/subtitles" - const val DOWNLOADENDPOINT = "https://dl.subdl.com" - const val SUBDL_SUBTITLES_USER_KEY: String = "subdl_user" - var currentSession: SubtitleOAuthEntity? = null - } - - override suspend fun initialize() { - currentSession = getAuthKey() - } - - override fun logOut() { - setAuthKey(null) - removeAccountKeys() - currentSession = getAuthKey() - } - override suspend fun login(data: InAppAuthAPI.LoginData): Boolean { - val email = data.email ?: throw ErrorLoadingException("Requires Email") - val password = data.password ?: throw ErrorLoadingException("Requires Password") - switchToNewAccount() - try { - if (initLogin(email, password)) { - registerAccount() - return true - } - } catch (e: Exception) { - logError(e) - switchToOldAccount() - } - switchToOldAccount() - return false - } - - override fun getLatestLoginData(): InAppAuthAPI.LoginData? { - val current = getAuthKey() ?: return null - return InAppAuthAPI.LoginData( - email = current.userEmail, - password = current.pass - ) - } - - override fun loginInfo(): LoginInfo? { - getAuthKey()?.let { user -> - return LoginInfo( - profilePicture = null, - name = user.name ?: user.userEmail, - accountIndex = accountIndex - ) - } - return null - } - - override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List? { - - val queryText = query.query - val epNum = query.epNumber ?: 0 - val seasonNum = query.seasonNumber ?: 0 - val yearNum = query.year ?: 0 - - val idQuery = when { - query.imdbId != null -> "&imdb_id=${query.imdbId}" - query.tmdbId != null -> "&tmdb_id=${query.tmdbId}" - else -> null - } - - val epQuery = if (epNum > 0) "&episode_number=$epNum" else "" - val seasonQuery = if (seasonNum > 0) "&season_number=$seasonNum" else "" - val yearQuery = if (yearNum > 0) "&year=$yearNum" else "" - - val searchQueryUrl = when (idQuery) { - //Use imdb/tmdb id to search if its valid - null -> "$APIENDPOINT?api_key=${currentSession?.apiKey}&film_name=$queryText&languages=${query.lang}$epQuery$seasonQuery$yearQuery" - else -> "$APIENDPOINT?api_key=${currentSession?.apiKey}$idQuery&languages=${query.lang}$epQuery$seasonQuery$yearQuery" - } - - val req = app.get( - url = searchQueryUrl, - headers = mapOf( - "Accept" to "application/json" - ) - ) - - return req.parsedSafe()?.subtitles?.map { subtitle -> - - val lang = subtitle.lang.replaceFirstChar { it.uppercase() } - val resEpNum = subtitle.episode ?: query.epNumber - val resSeasonNum = subtitle.season ?: query.seasonNumber - val type = if ((resSeasonNum ?: 0) > 0) TvType.TvSeries else TvType.Movie - - AbstractSubtitleEntities.SubtitleEntity( - idPrefix = this.idPrefix, - name = subtitle.releaseName, - lang = lang, - data = "${DOWNLOADENDPOINT}${subtitle.url}", - type = type, - source = this.name, - epNumber = resEpNum, - seasonNumber = resSeasonNum, - isHearingImpaired = subtitle.hearingImpaired ?: false, - ) - } - } - - override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) { - this.addZipUrl(data.data) { name, _ -> - name - } - } - - private suspend fun initLogin(useremail: String, password: String): Boolean { - - val tokenResponse = app.post( - url = "$APIURL/login", - data = mapOf( - "email" to useremail, - "password" to password - ) - ).parsedSafe() - - if (tokenResponse?.token == null) return false - - val apiResponse = app.get( - url = "$APIURL/user/userApi", - headers = mapOf( - "Authorization" to "Bearer ${tokenResponse.token}" - ) - ).parsedSafe() - - if (apiResponse?.ok == false) return false - - setAuthKey( - SubtitleOAuthEntity( - userEmail = useremail, - pass = password, - name = tokenResponse.userData?.username ?: tokenResponse.userData?.name, - accessToken = tokenResponse.token, - apiKey = apiResponse?.apiKey - ) - ) - return true - } - - private fun getAuthKey(): SubtitleOAuthEntity? { - return getKey(accountId, SUBDL_SUBTITLES_USER_KEY) - } - - private fun setAuthKey(data: SubtitleOAuthEntity?) { - if (data == null) removeKey( - accountId, - SUBDL_SUBTITLES_USER_KEY - ) - currentSession = data - setKey(accountId, SUBDL_SUBTITLES_USER_KEY, data) - } - - data class SubtitleOAuthEntity( - @JsonProperty("userEmail") var userEmail: String, - @JsonProperty("pass") var pass: String, - @JsonProperty("name") var name: String? = null, - @JsonProperty("accessToken") var accessToken: String? = null, - @JsonProperty("apiKey") var apiKey: String? = null, - ) - - data class OAuthTokenResponse( - @JsonProperty("token") val token: String? = null, - @JsonProperty("userData") val userData: UserData? = null, - @JsonProperty("status") val status: Boolean? = null, - @JsonProperty("message") val message: String? = null, - ) - - data class UserData( - @JsonProperty("email") val email: String, - @JsonProperty("name") val name: String, - @JsonProperty("country") val country: String, - @JsonProperty("scStepCode") val scStepCode: String, - @JsonProperty("scVerified") val scVerified: Boolean, - @JsonProperty("username") val username: String? = null, - @JsonProperty("scUsername") val scUsername: String, - ) - - data class ApiKeyResponse( - @JsonProperty("ok") val ok: Boolean? = false, - @JsonProperty("api_key") val apiKey: String? = null, - @JsonProperty("usage") val usage: Usage? = null, - ) - - data class Usage( - @JsonProperty("total") val total: Long? = 0, - @JsonProperty("today") val today: Long? = 0, - ) - - data class ApiResponse( - @JsonProperty("status") val status: Boolean? = null, - @JsonProperty("results") val results: List? = null, - @JsonProperty("subtitles") val subtitles: List? = null, - ) - - data class Result( - @JsonProperty("sd_id") val sdId: Int? = null, - @JsonProperty("type") val type: String? = null, - @JsonProperty("name") val name: String? = null, - @JsonProperty("imdb_id") val imdbId: String? = null, - @JsonProperty("tmdb_id") val tmdbId: Long? = null, - @JsonProperty("first_air_date") val firstAirDate: String? = null, - @JsonProperty("year") val year: Int? = null, - ) - - data class Subtitle( - @JsonProperty("release_name") val releaseName: String, - @JsonProperty("name") val name: String, - @JsonProperty("lang") val lang: String, - @JsonProperty("author") val author: String? = null, - @JsonProperty("url") val url: String? = null, - @JsonProperty("subtitlePage") val subtitlePage: String? = null, - @JsonProperty("season") val season: Int? = null, - @JsonProperty("episode") val episode: Int? = null, - @JsonProperty("language") val language: String? = null, - @JsonProperty("hi") val hearingImpaired: Boolean? = null, - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt index 9150cfc5..a075cc2e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt @@ -50,7 +50,7 @@ class APIRepository(val api: MainAPI) { private val cache = threadSafeListOf() private var cacheIndex: Int = 0 - const val CACHE_SIZE = 20 + const val cacheSize = 20 } private fun afterPluginsLoaded(forceReload: Boolean) { @@ -94,9 +94,9 @@ class APIRepository(val api: MainAPI) { val add = SavedLoadResponse(unixTime, response, lookingForHash) synchronized(cache) { - if (cache.size > CACHE_SIZE) { + if (cache.size > cacheSize) { cache[cacheIndex] = add // rolling cache - cacheIndex = (cacheIndex + 1) % CACHE_SIZE + cacheIndex = (cacheIndex + 1) % cacheSize } else { cache.add(add) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt index e930961c..7439bfdf 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt @@ -85,7 +85,7 @@ abstract class BaseAdapter< AsyncDifferConfig.Builder(diffCallback).build() ) - open fun submitList(list: List?) { + fun submitList(list: List?) { // deep copy at least the top list, because otherwise adapter can go crazy mDiffer.submitList(list?.let { CopyOnWriteArrayList(it) }) } @@ -112,7 +112,6 @@ abstract class BaseAdapter< holder.onViewDetachedFromWindow() } - @Suppress("UNCHECKED_CAST") fun save(recyclerView: RecyclerView) { for (child in recyclerView.children) { val holder = @@ -125,7 +124,6 @@ abstract class BaseAdapter< stateViewModel.layoutManagerStates[id]?.clear() } - @Suppress("UNCHECKED_CAST") private fun getState(holder: ViewHolderState): S? = stateViewModel.layoutManagerStates[id]?.get(holder.absoluteAdapterPosition) as? S diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt index 1eaac505..688363e9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt @@ -6,7 +6,6 @@ import android.view.Menu import android.view.View.* import android.widget.* import androidx.appcompat.app.AlertDialog -import androidx.media3.common.util.UnstableApi import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.kotlin.kotlinModule @@ -24,13 +23,13 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safeApiCall +import com.lagradost.cloudstream3.sortSubs import com.lagradost.cloudstream3.sortUrls import com.lagradost.cloudstream3.ui.player.LoadType import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment -import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.CastHelper.awaitLinks import com.lagradost.cloudstream3.utils.CastHelper.getMediaInfo @@ -264,7 +263,6 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi var isLoadingMore = false - override fun onMediaStatusUpdated() { super.onMediaStatusUpdated() val meta = getCurrentMetaData() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt index 78ad2a6b..1a9549e1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt @@ -8,8 +8,8 @@ import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import kotlin.math.abs -class GrdLayoutManager(val context: Context, spanCount: Int) : - GridLayoutManager(context, spanCount) { +class GrdLayoutManager(val context: Context, _spanCount: Int) : + GridLayoutManager(context, _spanCount) { override fun onFocusSearchFailed( focused: View, focusDirection: Int, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt index 4879d2e0..c7041776 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt @@ -51,7 +51,7 @@ class EasterEggMonke : AppCompatActivity() { FrameLayout.LayoutParams.WRAP_CONTENT) binding.frame.addView(newStar) - newStar.scaleX += Math.random().toFloat() * 1.5f + newStar.scaleX = Math.random().toFloat() * 1.5f + newStar.scaleX newStar.scaleY = newStar.scaleX starW *= newStar.scaleX starH *= newStar.scaleY diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt index 12a5ae2a..f721401e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt @@ -15,7 +15,7 @@ open class NonFinalAdapterListUpdateCallback /** * Creates an AdapterListUpdateCallback that will dispatch update events to the given adapter. * - * @param mAdapter The Adapter to send updates to. + * @param adapter The Adapter to send updates to. */(private var mAdapter: RecyclerView.Adapter<*>) : ListUpdateCallback { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt index b778ba5a..9532d1a9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt @@ -13,7 +13,7 @@ enum class WatchType(val internalId: Int, @StringRes val stringRes: Int, @Drawab NONE(5, R.string.type_none, R.drawable.ic_baseline_add_24); companion object { - fun fromInternalId(id: Int?) = entries.find { value -> value.internalId == id } ?: NONE + fun fromInternalId(id: Int?) = values().find { value -> value.internalId == id } ?: NONE } } @@ -36,6 +36,6 @@ enum class SyncWatchType(val internalId: Int, @StringRes val stringRes: Int, @Dr REWATCHING(5, R.string.type_re_watching, R.drawable.ic_baseline_bookmark_24); companion object { - fun fromInternalId(id: Int?) = entries.find { value -> value.internalId == id } ?: NONE + fun fromInternalId(id: Int?) = values().find { value -> value.internalId == id } ?: NONE } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt index 5e2b97e5..9ed58e2c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt @@ -8,16 +8,14 @@ import android.webkit.JavascriptInterface import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient -import androidx.annotation.OptIn import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity -import androidx.media3.common.util.UnstableApi import androidx.navigation.fragment.findNavController import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.databinding.FragmentWebviewBinding import com.lagradost.cloudstream3.network.WebViewResolver -import com.lagradost.cloudstream3.utils.AppContextUtils.loadRepository +import com.lagradost.cloudstream3.utils.AppUtils.loadRepository class WebviewFragment : Fragment() { @@ -31,7 +29,6 @@ class WebviewFragment : Fragment() { } binding?.webView?.webViewClient = object : WebViewClient() { - @OptIn(UnstableApi::class) override fun shouldOverrideUrlLoading( view: WebView?, request: WebResourceRequest? diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt index d2aca862..1db49e27 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt @@ -27,7 +27,7 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.result.setImage import com.lagradost.cloudstream3.ui.result.setLinearListLayout -import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.getDefaultAccount import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt index 0da69f9c..41aef176 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt @@ -23,18 +23,14 @@ import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.BiometricAuthenticator -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication import com.lagradost.cloudstream3.utils.DataStoreHelper.accounts import com.lagradost.cloudstream3.utils.DataStoreHelper.selectedKeyIndex import com.lagradost.cloudstream3.utils.DataStoreHelper.setAccount import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute -class AccountSelectActivity : AppCompatActivity(), BiometricCallback { +class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.BiometricAuthCallback { lateinit var viewModel: AccountViewModel @@ -52,6 +48,7 @@ class AccountSelectActivity : AppCompatActivity(), BiometricCallback { ) val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + val authEnabled = settingsManager.getBoolean(getString(R.string.biometric_key), false) val skipStartup = settingsManager.getBoolean(getString(R.string.skip_startup_account_select_key), false ) || accounts.count() <= 1 @@ -59,7 +56,7 @@ class AccountSelectActivity : AppCompatActivity(), BiometricCallback { fun askBiometricAuth() { - if (isLayout(PHONE) && isAuthEnabled(this)) { + if (isLayout(PHONE) && authEnabled) { if (deviceHasPasswordPinLock(this)) { startBiometricAuthentication( this, @@ -67,8 +64,8 @@ class AccountSelectActivity : AppCompatActivity(), BiometricCallback { false ) - promptInfo?.let { prompt -> - biometricPrompt?.authenticate(prompt) + BiometricAuthenticator.promptInfo?.let { promt -> + BiometricAuthenticator.biometricPrompt?.authenticate(promt) } } } @@ -192,8 +189,4 @@ class AccountSelectActivity : AppCompatActivity(), BiometricCallback { override fun onAuthenticationSuccess() { Log.i(BiometricAuthenticator.TAG,"Authentication successful in AccountSelectActivity") } - - override fun onAuthenticationError() { - finish() - } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt deleted file mode 100644 index d211cb87..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt +++ /dev/null @@ -1,414 +0,0 @@ -package com.lagradost.cloudstream3.ui.download - -import android.text.format.Formatter.formatShortFileSize -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.CheckBox -import androidx.core.content.ContextCompat -import androidx.core.view.isVisible -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import androidx.viewbinding.ViewBinding -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.databinding.DownloadChildEpisodeBinding -import com.lagradost.cloudstream3.databinding.DownloadHeaderEpisodeBinding -import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.ui.download.button.DownloadStatusTell -import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull -import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual -import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos -import com.lagradost.cloudstream3.utils.UIHelper.setImage -import com.lagradost.cloudstream3.utils.VideoDownloadHelper - -const val DOWNLOAD_ACTION_PLAY_FILE = 0 -const val DOWNLOAD_ACTION_DELETE_FILE = 1 -const val DOWNLOAD_ACTION_RESUME_DOWNLOAD = 2 -const val DOWNLOAD_ACTION_PAUSE_DOWNLOAD = 3 -const val DOWNLOAD_ACTION_DOWNLOAD = 4 -const val DOWNLOAD_ACTION_LONG_CLICK = 5 - -const val DOWNLOAD_ACTION_GO_TO_CHILD = 0 -const val DOWNLOAD_ACTION_LOAD_RESULT = 1 - -sealed class VisualDownloadCached { - abstract val currentBytes: Long - abstract val totalBytes: Long - abstract val data: VideoDownloadHelper.DownloadCached - abstract var isSelected: Boolean - - data class Child( - override val currentBytes: Long, - override val totalBytes: Long, - override val data: VideoDownloadHelper.DownloadEpisodeCached, - override var isSelected: Boolean, - ) : VisualDownloadCached() - - data class Header( - override val currentBytes: Long, - override val totalBytes: Long, - override val data: VideoDownloadHelper.DownloadHeaderCached, - override var isSelected: Boolean, - val child: VideoDownloadHelper.DownloadEpisodeCached?, - val currentOngoingDownloads: Int, - val totalDownloads: Int, - ) : VisualDownloadCached() -} - -data class DownloadClickEvent( - val action: Int, - val data: VideoDownloadHelper.DownloadEpisodeCached -) - -data class DownloadHeaderClickEvent( - val action: Int, - val data: VideoDownloadHelper.DownloadHeaderCached -) - -class DownloadAdapter( - private val onHeaderClickEvent: (DownloadHeaderClickEvent) -> Unit, - private val onItemClickEvent: (DownloadClickEvent) -> Unit, - private val onItemSelectionChanged: (Int, Boolean) -> Unit, -) : ListAdapter(DiffCallback()) { - - private var isMultiDeleteState: Boolean = false - - companion object { - private const val VIEW_TYPE_HEADER = 0 - private const val VIEW_TYPE_CHILD = 1 - } - - inner class DownloadViewHolder( - private val binding: ViewBinding - ) : RecyclerView.ViewHolder(binding.root) { - - fun bind(card: VisualDownloadCached?) { - when (binding) { - is DownloadHeaderEpisodeBinding -> bindHeader(card as? VisualDownloadCached.Header) - is DownloadChildEpisodeBinding -> bindChild(card as? VisualDownloadCached.Child) - } - } - - private fun bindHeader(card: VisualDownloadCached.Header?) { - if (binding !is DownloadHeaderEpisodeBinding || card == null) return - - val data = card.data - binding.apply { - episodeHolder.apply { - if (isMultiDeleteState) { - setOnClickListener { - toggleIsChecked(deleteCheckbox, data.id) - } - } - - setOnLongClickListener { - toggleIsChecked(deleteCheckbox, data.id) - true - } - } - - downloadHeaderPoster.apply { - setImage(data.poster) - if (isMultiDeleteState) { - setOnClickListener { - toggleIsChecked(deleteCheckbox, data.id) - } - } else { - setOnClickListener { - onHeaderClickEvent.invoke( - DownloadHeaderClickEvent( - DOWNLOAD_ACTION_LOAD_RESULT, - data - ) - ) - } - } - - setOnLongClickListener { - toggleIsChecked(deleteCheckbox, data.id) - true - } - } - downloadHeaderTitle.text = data.name - val formattedSize = formatShortFileSize(itemView.context, card.totalBytes) - - if (card.child != null) { - handleChildDownload(card, formattedSize) - } else handleParentDownload(card, formattedSize) - - if (isMultiDeleteState) { - deleteCheckbox.setOnCheckedChangeListener { _, isChecked -> - onItemSelectionChanged.invoke(data.id, isChecked) - } - } else deleteCheckbox.setOnCheckedChangeListener(null) - - deleteCheckbox.apply { - isVisible = isMultiDeleteState - isChecked = card.isSelected - } - } - } - - private fun DownloadHeaderEpisodeBinding.handleChildDownload( - card: VisualDownloadCached.Header, - formattedSize: String - ) { - card.child ?: return - downloadHeaderGotoChild.isVisible = false - - val posDur = getViewPos(card.data.id) - downloadHeaderEpisodeProgress.apply { - isVisible = posDur != null - posDur?.let { - val visualPos = it.fixVisual() - max = (visualPos.duration / 1000).toInt() - progress = (visualPos.position / 1000).toInt() - } - } - - val status = downloadButton.getStatus(card.child.id, card.currentBytes, card.totalBytes) - if (status == DownloadStatusTell.IsDone) { - // We do this here instead if we are finished downloading - // so that we can use the value from the view model - // rather than extra unneeded disk operations and to prevent a - // delay in updating download icon state. - downloadButton.setProgress(card.currentBytes, card.totalBytes) - downloadButton.applyMetaData(card.child.id, card.currentBytes, card.totalBytes) - // We will let the view model handle this - downloadButton.doSetProgress = false - downloadButton.progressBar.progressDrawable = - downloadButton.getDrawableFromStatus(status) - ?.let { ContextCompat.getDrawable(downloadButton.context, it) } - downloadHeaderInfo.text = formattedSize - } else { - // We need to make sure we restore the correct progress - // when we refresh data in the adapter. - downloadButton.resetView() - val drawable = downloadButton.getDrawableFromStatus(status)?.let { - ContextCompat.getDrawable(downloadButton.context, it) - } - downloadButton.statusView.setImageDrawable(drawable) - downloadButton.progressBar.progressDrawable = - ContextCompat.getDrawable( - downloadButton.context, - downloadButton.progressDrawable - ) - } - - downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, onItemClickEvent) - downloadButton.isVisible = !isMultiDeleteState - - if (!isMultiDeleteState) { - episodeHolder.setOnClickListener { - onItemClickEvent.invoke( - DownloadClickEvent( - DOWNLOAD_ACTION_PLAY_FILE, - card.child - ) - ) - } - } - } - - private fun DownloadHeaderEpisodeBinding.handleParentDownload( - card: VisualDownloadCached.Header, - formattedSize: String - ) { - downloadButton.isVisible = false - downloadHeaderEpisodeProgress.isVisible = false - downloadHeaderGotoChild.isVisible = !isMultiDeleteState - - try { - downloadHeaderInfo.text = - downloadHeaderInfo.context.getString(R.string.extra_info_format).format( - card.totalDownloads, - downloadHeaderInfo.context.resources.getQuantityString( - R.plurals.episodes, - card.totalDownloads - ), - formattedSize - ) - } catch (e: Exception) { - downloadHeaderInfo.text = null - logError(e) - } - - if (!isMultiDeleteState) { - episodeHolder.setOnClickListener { - onHeaderClickEvent.invoke( - DownloadHeaderClickEvent( - DOWNLOAD_ACTION_GO_TO_CHILD, - card.data - ) - ) - } - } - } - - private fun bindChild(card: VisualDownloadCached.Child?) { - if (binding !is DownloadChildEpisodeBinding || card == null) return - - val data = card.data - binding.apply { - val posDur = getViewPos(data.id) - downloadChildEpisodeProgress.apply { - isVisible = posDur != null - posDur?.let { - val visualPos = it.fixVisual() - max = (visualPos.duration / 1000).toInt() - progress = (visualPos.position / 1000).toInt() - } - } - - val status = downloadButton.getStatus(data.id, card.currentBytes, card.totalBytes) - if (status == DownloadStatusTell.IsDone) { - // We do this here instead if we are finished downloading - // so that we can use the value from the view model - // rather than extra unneeded disk operations and to prevent a - // delay in updating download icon state. - downloadButton.setProgress(card.currentBytes, card.totalBytes) - downloadButton.applyMetaData(data.id, card.currentBytes, card.totalBytes) - // We will let the view model handle this - downloadButton.doSetProgress = false - downloadButton.progressBar.progressDrawable = - downloadButton.getDrawableFromStatus(status) - ?.let { ContextCompat.getDrawable(downloadButton.context, it) } - downloadChildEpisodeTextExtra.text = - formatShortFileSize(downloadChildEpisodeTextExtra.context, card.totalBytes) - } else { - // We need to make sure we restore the correct progress - // when we refresh data in the adapter. - downloadButton.resetView() - val drawable = downloadButton.getDrawableFromStatus(status)?.let { - ContextCompat.getDrawable(downloadButton.context, it) - } - downloadButton.statusView.setImageDrawable(drawable) - downloadButton.progressBar.progressDrawable = - ContextCompat.getDrawable( - downloadButton.context, - downloadButton.progressDrawable - ) - } - - downloadButton.setDefaultClickListener( - data, - downloadChildEpisodeTextExtra, - onItemClickEvent - ) - downloadButton.isVisible = !isMultiDeleteState - - downloadChildEpisodeText.apply { - text = context.getNameFull(data.name, data.episode, data.season) - isSelected = true // Needed for text repeating - } - - downloadChildEpisodeHolder.setOnClickListener { - onItemClickEvent.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, data)) - } - - downloadChildEpisodeHolder.apply { - when { - isMultiDeleteState -> { - setOnClickListener { - toggleIsChecked(deleteCheckbox, data.id) - } - } - - else -> { - setOnClickListener { - onItemClickEvent.invoke( - DownloadClickEvent( - DOWNLOAD_ACTION_PLAY_FILE, - data - ) - ) - } - } - } - - setOnLongClickListener { - toggleIsChecked(deleteCheckbox, data.id) - true - } - } - - if (isMultiDeleteState) { - deleteCheckbox.setOnCheckedChangeListener { _, isChecked -> - onItemSelectionChanged.invoke(data.id, isChecked) - } - } else deleteCheckbox.setOnCheckedChangeListener(null) - - deleteCheckbox.apply { - isVisible = isMultiDeleteState - isChecked = card.isSelected - } - } - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadViewHolder { - val inflater = LayoutInflater.from(parent.context) - val binding = when (viewType) { - VIEW_TYPE_HEADER -> DownloadHeaderEpisodeBinding.inflate(inflater, parent, false) - VIEW_TYPE_CHILD -> DownloadChildEpisodeBinding.inflate(inflater, parent, false) - else -> throw IllegalArgumentException("Invalid view type") - } - return DownloadViewHolder(binding) - } - - override fun onBindViewHolder(holder: DownloadViewHolder, position: Int) { - holder.bind(getItem(position)) - } - - override fun getItemViewType(position: Int): Int { - return when (getItem(position)) { - is VisualDownloadCached.Child -> VIEW_TYPE_CHILD - is VisualDownloadCached.Header -> VIEW_TYPE_HEADER - else -> throw IllegalArgumentException("Invalid data type at position $position") - } - } - - fun setIsMultiDeleteState(value: Boolean) { - if (isMultiDeleteState == value) return - isMultiDeleteState = value - notifyItemRangeChanged(0, itemCount) - } - - fun notifyAllSelected() { - currentList.indices.forEach { index -> - if (!currentList[index].isSelected) { - notifyItemChanged(index) - } - } - } - - fun notifySelectionStates() { - currentList.indices.forEach { index -> - if (currentList[index].isSelected) { - notifyItemChanged(index) - } - } - } - - private fun toggleIsChecked(checkbox: CheckBox, itemId: Int) { - val isChecked = !checkbox.isChecked - checkbox.isChecked = isChecked - onItemSelectionChanged.invoke(itemId, isChecked) - } - - class DiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: VisualDownloadCached, - newItem: VisualDownloadCached - ): Boolean { - return oldItem.data.id == newItem.data.id - } - - override fun areContentsTheSame( - oldItem: VisualDownloadCached, - newItem: VisualDownloadCached - ): Boolean { - return oldItem == newItem - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt index 494e82e5..10ce67a7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt @@ -1,30 +1,28 @@ package com.lagradost.cloudstream3.ui.download +import android.app.Activity import android.content.DialogInterface -import android.net.Uri +import android.widget.Toast import androidx.appcompat.app.AlertDialog -import com.google.android.material.snackbar.Snackbar import com.lagradost.cloudstream3.AcraApplication.Companion.getKey -import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys import com.lagradost.cloudstream3.CommonActivity.activity +import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.player.DownloadFileGenerator -import com.lagradost.cloudstream3.ui.player.ExtractorUri import com.lagradost.cloudstream3.ui.player.GeneratorPlayer -import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull -import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus -import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE +import com.lagradost.cloudstream3.utils.AppUtils.getNameFull +import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE -import com.lagradost.cloudstream3.utils.SnackbarHelper.showSnackbar +import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager -import kotlinx.coroutines.MainScope object DownloadButtonSetup { fun handleDownloadClick(click: DownloadClickEvent) { val id = click.data.id + if (click.data !is VideoDownloadHelper.DownloadEpisodeCached) return when (click.action) { DOWNLOAD_ACTION_DELETE_FILE -> { activity?.let { ctx -> @@ -33,15 +31,9 @@ object DownloadButtonSetup { DialogInterface.OnClickListener { _, which -> when (which) { DialogInterface.BUTTON_POSITIVE -> { - VideoDownloadManager.deleteFilesAndUpdateSettings( - ctx, - setOf(id), - MainScope() - ) + VideoDownloadManager.deleteFileAndUpdateSettings(ctx, id) } - DialogInterface.BUTTON_NEGATIVE -> { - // Do nothing on cancel } } } @@ -66,13 +58,11 @@ object DownloadButtonSetup { } } } - DOWNLOAD_ACTION_PAUSE_DOWNLOAD -> { VideoDownloadManager.downloadEvent.invoke( Pair(click.data.id, VideoDownloadManager.DownloadActionType.Pause) ) } - DOWNLOAD_ACTION_RESUME_DOWNLOAD -> { activity?.let { ctx -> if (VideoDownloadManager.downloadStatus.containsKey(id) && VideoDownloadManager.downloadStatus[id] == VideoDownloadManager.DownloadType.IsPaused) { @@ -91,7 +81,6 @@ object DownloadButtonSetup { } } } - DOWNLOAD_ACTION_LONG_CLICK -> { activity?.let { act -> val length = @@ -101,80 +90,64 @@ object DownloadButtonSetup { )?.fileLength ?: 0 if (length > 0) { - showSnackbar( - act, - R.string.offline_file, - Snackbar.LENGTH_LONG - ) + showToast(R.string.delete, Toast.LENGTH_LONG) + } else { + showToast(R.string.download, Toast.LENGTH_LONG) } } } - DOWNLOAD_ACTION_PLAY_FILE -> { activity?.let { act -> + val info = + VideoDownloadManager.getDownloadFileInfoAndUpdateSettings( + act, + click.data.id + ) ?: return + val keyInfo = getKey( + VideoDownloadManager.KEY_DOWNLOAD_INFO, + click.data.id.toString() + ) ?: return val parent = getKey( DOWNLOAD_HEADER_CACHE, click.data.parentId.toString() ) ?: return - val episodes = getKeys(DOWNLOAD_EPISODE_CACHE) - ?.mapNotNull { - getKey(it) - } - ?.filter { it.parentId == click.data.parentId } - - val currentSeason = click.data.season ?: 0 - val currentEpisode = click.data.episode - - val items = mutableListOf() - - // Make sure we only get this episode and episodes after it, - // and that we can go to the next season if we need to. - val allRelevantEpisodes = episodes - ?.sortedWith( - compareByDescending { it.id == click.data.id } - .thenBy { it.season ?: 0 } - .thenBy { it.episode } - ) - ?.filter { - if (it.season == null) return@filter true - val isCurrentOrLaterInSeason = it.season == currentSeason && (it.episode >= currentEpisode || it.id == click.data.id) - val isInFutureSeasons = it.season > currentSeason - - isCurrentOrLaterInSeason || isInFutureSeasons - } - - allRelevantEpisodes?.forEach { - val keyInfo = getKey( - VideoDownloadManager.KEY_DOWNLOAD_INFO, - it.id.toString() - ) ?: return@forEach - - items.add( - ExtractorUri( - // We just use a temporary placeholder for the URI, - // it will be updated in generateLinks(). - // We just do this for performance since getting - // all paths at once can be quite expensive. - uri = Uri.EMPTY, - id = it.id, - parentId = it.parentId, - name = act.getString(R.string.downloaded_file), - season = it.season, - episode = it.episode, - headerName = parent.name, - tvType = parent.type, - basePath = keyInfo.basePath, - displayName = keyInfo.displayName, - relativePath = keyInfo.relativePath, - ) - ) - } - act.navigate( R.id.global_to_navigation_player, GeneratorPlayer.newInstance( - DownloadFileGenerator(items) + DownloadFileGenerator( + listOf( + ExtractorUri( + uri = info.path, + + id = click.data.id, + parentId = click.data.parentId, + name = act.getString(R.string.downloaded_file), //click.data.name ?: keyInfo.displayName + season = click.data.season, + episode = click.data.episode, + headerName = parent.name, + tvType = parent.type, + + basePath = keyInfo.basePath, + displayName = keyInfo.displayName, + relativePath = keyInfo.relativePath, + ) + ) + ) ) + //R.id.global_to_navigation_player, PlayerFragment.newInstance( + // UriData( + // info.path.toString(), + // keyInfo.basePath, + // keyInfo.relativePath, + // keyInfo.displayName, + // click.data.parentId, + // click.data.id, + // headerName ?: "null", + // if (click.data.episode <= 0) null else click.data.episode, + // click.data.season + // ), + // getViewPos(click.data.id)?.position ?: 0 + //) ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt new file mode 100644 index 00000000..1d7b5a83 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt @@ -0,0 +1,94 @@ +package com.lagradost.cloudstream3.ui.download + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.databinding.DownloadChildEpisodeBinding +import com.lagradost.cloudstream3.utils.AppUtils.getNameFull +import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual +import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos +import com.lagradost.cloudstream3.utils.VideoDownloadHelper + +const val DOWNLOAD_ACTION_PLAY_FILE = 0 +const val DOWNLOAD_ACTION_DELETE_FILE = 1 +const val DOWNLOAD_ACTION_RESUME_DOWNLOAD = 2 +const val DOWNLOAD_ACTION_PAUSE_DOWNLOAD = 3 +const val DOWNLOAD_ACTION_DOWNLOAD = 4 +const val DOWNLOAD_ACTION_LONG_CLICK = 5 + +data class VisualDownloadChildCached( + val currentBytes: Long, + val totalBytes: Long, + val data: VideoDownloadHelper.DownloadEpisodeCached, +) + +data class DownloadClickEvent(val action: Int, val data: VideoDownloadHelper.DownloadEpisodeCached) + +class DownloadChildAdapter( + var cardList: List, + private val clickCallback: (DownloadClickEvent) -> Unit, +) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return DownloadChildViewHolder( + DownloadChildEpisodeBinding.inflate(LayoutInflater.from(parent.context), parent, false), + clickCallback + ) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is DownloadChildViewHolder -> { + holder.bind(cardList[position]) + } + } + } + + override fun getItemCount(): Int { + return cardList.size + } + + class DownloadChildViewHolder + constructor( + val binding: DownloadChildEpisodeBinding, + private val clickCallback: (DownloadClickEvent) -> Unit, + ) : RecyclerView.ViewHolder(binding.root) { + + /*private val title: TextView = itemView.download_child_episode_text + private val extraInfo: TextView = itemView.download_child_episode_text_extra + private val holder: CardView = itemView.download_child_episode_holder + private val progressBar: ContentLoadingProgressBar = itemView.download_child_episode_progress + private val progressBarDownload: ContentLoadingProgressBar = itemView.download_child_episode_progress_downloaded + private val downloadImage: ImageView = itemView.download_child_episode_download*/ + + + fun bind(card: VisualDownloadChildCached) { + val d = card.data + + val posDur = getViewPos(d.id) + binding.downloadChildEpisodeProgress.apply { + if (posDur != null) { + val visualPos = posDur.fixVisual() + max = (visualPos.duration / 1000).toInt() + progress = (visualPos.position / 1000).toInt() + visibility = View.VISIBLE + } else { + visibility = View.GONE + } + } + + binding.downloadButton.setDefaultClickListener(card.data, binding.downloadChildEpisodeTextExtra, clickCallback) + + binding.downloadChildEpisodeText.apply { + text = context.getNameFull(d.name, d.episode, d.season) + isSelected = true // is needed for text repeating + } + + + binding.downloadChildEpisodeHolder.setOnClickListener { + clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d)) + } + } + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt index 09c48a04..c3ec2bbd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt @@ -1,33 +1,26 @@ package com.lagradost.cloudstream3.ui.download import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.text.format.Formatter.formatShortFileSize import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.view.isVisible import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding -import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout -import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR -import com.lagradost.cloudstream3.ui.settings.Globals.PHONE -import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback -import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback +import com.lagradost.cloudstream3.utils.Coroutines.main +import com.lagradost.cloudstream3.utils.DataStore.getKey +import com.lagradost.cloudstream3.utils.DataStore.getKeys import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar -import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV +import com.lagradost.cloudstream3.utils.VideoDownloadHelper +import com.lagradost.cloudstream3.utils.VideoDownloadManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext class DownloadChildFragment : Fragment() { - private lateinit var downloadsViewModel: DownloadViewModel - private var binding: FragmentChildDownloadsBinding? = null - companion object { fun newInstance(headerName: String, folder: String): Bundle { return Bundle().apply { @@ -38,170 +31,92 @@ class DownloadChildFragment : Fragment() { } override fun onDestroyView() { - detachBackPressedCallback() + downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent -= it } binding = null super.onDestroyView() } + var binding: FragmentChildDownloadsBinding? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - downloadsViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] val localBinding = FragmentChildDownloadsBinding.inflate(inflater, container, false) binding = localBinding - return localBinding.root + return localBinding.root//inflater.inflate(R.layout.fragment_child_downloads, container, false) } + private fun updateList(folder: String) = main { + context?.let { ctx -> + val data = withContext(Dispatchers.IO) { ctx.getKeys(folder) } + val eps = withContext(Dispatchers.IO) { + data.mapNotNull { key -> + context?.getKey(key) + }.mapNotNull { + val info = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(ctx, it.id) + ?: return@mapNotNull null + VisualDownloadChildCached(info.fileLength, info.totalBytes, it) + } + }.sortedBy { it.data.episode + (it.data.season ?: 0) * 100000 } + if (eps.isEmpty()) { + activity?.onBackPressedDispatcher?.onBackPressed() + return@main + } + + (binding?.downloadChildList?.adapter as DownloadChildAdapter? ?: return@main).cardList = + eps + binding?.downloadChildList?.adapter?.notifyDataSetChanged() + } + } + + private var downloadDeleteEventListener: ((Int) -> Unit)? = null + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - /** - * We never want to retain multi-delete state - * when navigating to downloads. Setting this state - * immediately can sometimes result in the observer - * not being notified in time to update the UI. - * - * By posting to the main looper, we ensure that this - * operation is executed after the view has been fully created - * and all initializations are completed, allowing the - * observer to properly receive and handle the state change. - */ - Handler(Looper.getMainLooper()).post { - downloadsViewModel.setIsMultiDeleteState(false) - } - - /** - * We have to make sure selected items are - * cleared here as well so we don't run in an - * inconsistent state where selected items do - * not match the multi delete state we are in. - */ - downloadsViewModel.clearSelectedItems() - val folder = arguments?.getString("folder") val name = arguments?.getString("name") if (folder == null) { - activity?.onBackPressedDispatcher?.onBackPressed() + activity?.onBackPressedDispatcher?.onBackPressed() // TODO FIX return } + fixPaddingStatusbar(binding?.downloadChildRoot) binding?.downloadChildToolbar?.apply { title = name - if (isLayout(PHONE or EMULATOR)) { - setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) - setNavigationOnClickListener { - activity?.onBackPressedDispatcher?.onBackPressed() - } - } - setAppBarNoScrollFlagsOnTV() - } - - binding?.downloadDeleteAppbar?.setAppBarNoScrollFlagsOnTV() - - observe(downloadsViewModel.childCards) { - if (it.isEmpty()) { + setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) + setNavigationOnClickListener { activity?.onBackPressedDispatcher?.onBackPressed() - return@observe - } - - (binding?.downloadChildList?.adapter as? DownloadAdapter)?.submitList(it) - } - observe(downloadsViewModel.isMultiDeleteState) { isMultiDeleteState -> - val adapter = binding?.downloadChildList?.adapter as? DownloadAdapter - adapter?.setIsMultiDeleteState(isMultiDeleteState) - binding?.downloadDeleteAppbar?.isVisible = isMultiDeleteState - if (!isMultiDeleteState) { - detachBackPressedCallback() - downloadsViewModel.clearSelectedItems() - binding?.downloadChildToolbar?.isVisible = true } } - observe(downloadsViewModel.selectedBytes) { - updateDeleteButton(downloadsViewModel.selectedItemIds.value?.count() ?: 0, it) - } - observe(downloadsViewModel.selectedItemIds) { - handleSelectedChange(it) - updateDeleteButton(it.count(), downloadsViewModel.selectedBytes.value ?: 0L) - binding?.btnDelete?.isVisible = it.isNotEmpty() - binding?.selectItemsText?.isVisible = it.isEmpty() - val allSelected = downloadsViewModel.isAllSelected() - if (allSelected) { - binding?.btnToggleAll?.setText(R.string.deselect_all) - } else binding?.btnToggleAll?.setText(R.string.select_all) - } - - val adapter = DownloadAdapter( - {}, - { click -> - if (click.action == DOWNLOAD_ACTION_DELETE_FILE) { - context?.let { ctx -> - downloadsViewModel.handleSingleDelete(ctx, click.data.id) - } - } else handleDownloadClick(click) - }, - { itemId, isChecked -> - if (isChecked) { - downloadsViewModel.addSelected(itemId) - } else downloadsViewModel.removeSelected(itemId) - } - ) - - binding?.downloadChildList?.apply { - setHasFixedSize(true) - setItemViewCacheSize(20) - this.adapter = adapter - setLinearListLayout( - isHorizontal = false, - nextRight = FOCUS_SELF, - nextDown = FOCUS_SELF, - ) - } - - context?.let { downloadsViewModel.updateChildList(it, folder) } - fixPaddingStatusbar(binding?.downloadChildRoot) - } - - private fun handleSelectedChange(selected: MutableSet) { - if (selected.isNotEmpty()) { - binding?.downloadDeleteAppbar?.isVisible = true - binding?.downloadChildToolbar?.isVisible = false - activity?.attachBackPressedCallback { - downloadsViewModel.setIsMultiDeleteState(false) + val adapter: RecyclerView.Adapter = + DownloadChildAdapter( + ArrayList(), + ) { click -> + handleDownloadClick(click) } - binding?.btnDelete?.setOnClickListener { - context?.let { ctx -> - downloadsViewModel.handleMultiDelete(ctx) + downloadDeleteEventListener = { id: Int -> + val list = (binding?.downloadChildList?.adapter as DownloadChildAdapter?)?.cardList + if (list != null) { + if (list.any { it.data.id == id }) { + updateList(folder) } } - - binding?.btnCancel?.setOnClickListener { - downloadsViewModel.setIsMultiDeleteState(false) - } - - binding?.btnToggleAll?.setOnClickListener { - val allSelected = downloadsViewModel.isAllSelected() - val adapter = binding?.downloadChildList?.adapter as? DownloadAdapter - if (allSelected) { - adapter?.notifySelectionStates() - downloadsViewModel.clearSelectedItems() - } else { - adapter?.notifyAllSelected() - downloadsViewModel.selectAllItems() - } - } - - downloadsViewModel.setIsMultiDeleteState(true) } - } - private fun updateDeleteButton(count: Int, selectedBytes: Long) { - val formattedSize = formatShortFileSize(context, selectedBytes) - binding?.btnDelete?.text = - getString(R.string.delete_format).format(count, formattedSize) + downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } + + binding?.downloadChildList?.adapter = adapter + binding?.downloadChildList?.setLinearListLayout( + isHorizontal = false, + nextDown = FOCUS_SELF, + nextRight = FOCUS_SELF + )//layoutManager = GridLayoutManager(context, 1) + + updateList(folder) } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index 447b4f13..e08eb772 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -1,62 +1,55 @@ package com.lagradost.cloudstream3.ui.download -import android.app.Activity import android.app.Dialog import android.content.ClipboardManager import android.content.Context -import android.content.Intent -import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION import android.os.Build import android.os.Bundle -import android.os.Handler -import android.os.Looper import android.text.format.Formatter.formatShortFileSize import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.LinearLayout -import android.widget.TextView import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContracts -import androidx.annotation.StringRes +import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentDownloadsBinding import com.lagradost.cloudstream3.databinding.StreamInputBinding -import com.lagradost.cloudstream3.isEpisodeBased +import com.lagradost.cloudstream3.isMovieType import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick import com.lagradost.cloudstream3.ui.player.BasicLink import com.lagradost.cloudstream3.ui.player.GeneratorPlayer import com.lagradost.cloudstream3.ui.player.LinkGenerator -import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playUri import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult -import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback -import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback +import com.lagradost.cloudstream3.utils.AppUtils.loadResult +import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE -import com.lagradost.cloudstream3.utils.DataStore.getFolderName +import com.lagradost.cloudstream3.utils.DataStore import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate -import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV +import com.lagradost.cloudstream3.utils.VideoDownloadHelper +import com.lagradost.cloudstream3.utils.VideoDownloadManager import java.net.URI + const val DOWNLOAD_NAVIGATE_TO = "downloadpage" class DownloadFragment : Fragment() { private lateinit var downloadsViewModel: DownloadViewModel - private var binding: FragmentDownloadsBinding? = null private fun View.setLayoutWidth(weight: Long) { val param = LinearLayout.LayoutParams( @@ -67,325 +60,221 @@ class DownloadFragment : Fragment() { this.layoutParams = param } + private fun setList(list: List) { + main { + (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList = list + binding?.downloadList?.adapter?.notifyDataSetChanged() + } + } + override fun onDestroyView() { - detachBackPressedCallback() + if (downloadDeleteEventListener != null) { + VideoDownloadManager.downloadDeleteEvent -= downloadDeleteEventListener!! + downloadDeleteEventListener = null + } binding = null super.onDestroyView() } + var binding: FragmentDownloadsBinding? = null + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View { - downloadsViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] + ): View? { + downloadsViewModel = + ViewModelProvider(this)[DownloadViewModel::class.java] + val localBinding = FragmentDownloadsBinding.inflate(inflater, container, false) binding = localBinding - return localBinding.root + return localBinding.root//inflater.inflate(R.layout.fragment_downloads, container, false) } + private var downloadDeleteEventListener: ((Int) -> Unit)? = null + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) hideKeyboard() - binding?.downloadStorageAppbar?.setAppBarNoScrollFlagsOnTV() - binding?.downloadDeleteAppbar?.setAppBarNoScrollFlagsOnTV() - /** - * We never want to retain multi-delete state - * when navigating to downloads. Setting this state - * immediately can sometimes result in the observer - * not being notified in time to update the UI. - * - * By posting to the main looper, we ensure that this - * operation is executed after the view has been fully created - * and all initializations are completed, allowing the - * observer to properly receive and handle the state change. - */ - Handler(Looper.getMainLooper()).post { - downloadsViewModel.setIsMultiDeleteState(false) + observe(downloadsViewModel.noDownloadsText) { + binding?.textNoDownloads?.text = it } - - /** - * We have to make sure selected items are - * cleared here as well so we don't run in an - * inconsistent state where selected items do - * not match the multi delete state we are in. - */ - downloadsViewModel.clearSelectedItems() - observe(downloadsViewModel.headerCards) { - (binding?.downloadList?.adapter as? DownloadAdapter)?.submitList(it) + setList(it) binding?.downloadLoading?.isVisible = false - binding?.textNoDownloads?.isVisible = it.isEmpty() } observe(downloadsViewModel.availableBytes) { - updateStorageInfo( - view.context, - it, - R.string.free_storage, - binding?.downloadFreeTxt, - binding?.downloadFree - ) + binding?.downloadFreeTxt?.text = + getString(R.string.storage_size_format).format( + getString(R.string.free_storage), + formatShortFileSize(view.context, it) + ) + binding?.downloadFree?.setLayoutWidth(it) } observe(downloadsViewModel.usedBytes) { - updateStorageInfo( - view.context, - it, - R.string.used_storage, - binding?.downloadUsedTxt, - binding?.downloadUsed - ) - - // Prevent race condition and make sure - // we don't display it early - if ( - downloadsViewModel.isMultiDeleteState.value == null || - downloadsViewModel.isMultiDeleteState.value == false - ) binding?.downloadStorageAppbar?.isVisible = it > 0 + binding?.apply { + downloadUsedTxt.text = + getString(R.string.storage_size_format).format( + getString(R.string.used_storage), + formatShortFileSize(view.context, it) + ) + downloadUsed.setLayoutWidth(it) + downloadStorageAppbar.isVisible = it > 0 + } } observe(downloadsViewModel.downloadBytes) { - updateStorageInfo( - view.context, - it, - R.string.app_storage, - binding?.downloadAppTxt, - binding?.downloadApp + binding?.apply { + downloadAppTxt.text = + getString(R.string.storage_size_format).format( + getString(R.string.app_storage), + formatShortFileSize(view.context, it) + ) + downloadApp.setLayoutWidth(it) + } + } + + val adapter: RecyclerView.Adapter = + DownloadHeaderAdapter( + ArrayList(), + { click -> + when (click.action) { + 0 -> { + if (click.data.type.isMovieType()) { + //wont be called + } else { + val folder = DataStore.getFolderName( + DOWNLOAD_EPISODE_CACHE, + click.data.id.toString() + ) + activity?.navigate( + R.id.action_navigation_downloads_to_navigation_download_child, + DownloadChildFragment.newInstance(click.data.name, folder) + ) + } + } + + 1 -> { + (activity as AppCompatActivity?)?.loadResult( + click.data.url, + click.data.apiName + ) + } + } + + }, + { downloadClickEvent -> + if (downloadClickEvent.data !is VideoDownloadHelper.DownloadEpisodeCached) return@DownloadHeaderAdapter + handleDownloadClick(downloadClickEvent) + if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { + context?.let { ctx -> + downloadsViewModel.updateList(ctx) + } + } + } ) - } - observe(downloadsViewModel.selectedBytes) { - updateDeleteButton(downloadsViewModel.selectedItemIds.value?.count() ?: 0, it) - } - observe(downloadsViewModel.isMultiDeleteState) { isMultiDeleteState -> - val adapter = binding?.downloadList?.adapter as? DownloadAdapter - adapter?.setIsMultiDeleteState(isMultiDeleteState) - binding?.downloadDeleteAppbar?.isVisible = isMultiDeleteState - if (!isMultiDeleteState) { - detachBackPressedCallback() - downloadsViewModel.clearSelectedItems() - // Prevent race condition and make sure - // we don't display it early - if (downloadsViewModel.usedBytes.value?.let { it > 0 } == true) { - binding?.downloadStorageAppbar?.isVisible = true + + downloadDeleteEventListener = { id -> + val list = (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList + if (list != null) { + if (list.any { it.data.id == id }) { + context?.let { ctx -> + setList(ArrayList()) + downloadsViewModel.updateList(ctx) + } } } } - observe(downloadsViewModel.selectedItemIds) { - handleSelectedChange(it) - updateDeleteButton(it.count(), downloadsViewModel.selectedBytes.value ?: 0L) - binding?.btnDelete?.isVisible = it.isNotEmpty() - binding?.selectItemsText?.isVisible = it.isEmpty() - - val allSelected = downloadsViewModel.isAllSelected() - if (allSelected) { - binding?.btnToggleAll?.setText(R.string.deselect_all) - } else binding?.btnToggleAll?.setText(R.string.select_all) - } - - val adapter = DownloadAdapter( - { click -> handleItemClick(click) }, - { click -> - if (click.action == DOWNLOAD_ACTION_DELETE_FILE) { - context?.let { ctx -> - downloadsViewModel.handleSingleDelete(ctx, click.data.id) - } - } else handleDownloadClick(click) - }, - { itemId, isChecked -> - if (isChecked) { - downloadsViewModel.addSelected(itemId) - } else downloadsViewModel.removeSelected(itemId) - } - ) + downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } binding?.downloadList?.apply { - setHasFixedSize(true) - setItemViewCacheSize(20) this.adapter = adapter setLinearListLayout( isHorizontal = false, nextRight = FOCUS_SELF, - nextDown = FOCUS_SELF, + nextUp = FOCUS_SELF, + nextDown = FOCUS_SELF ) + //layoutManager = GridLayoutManager(context, 1) } - binding?.apply { - openLocalVideoButton.apply { - isGone = isLayout(TV) - setOnClickListener { openLocalVideo() } - } - downloadStreamButton.apply { - isGone = isLayout(TV) - setOnClickListener { showStreamInputDialog(it.context) } - } - } + // Should be visible in emulator layout + binding?.downloadStreamButton?.isGone = isLayout(TV) + binding?.downloadStreamButton?.setOnClickListener { + val dialog = + Dialog(it.context ?: return@setOnClickListener, R.style.AlertDialogCustom) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - binding?.downloadList?.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> - handleScroll(scrollY - oldScrollY) - } - } + val binding = StreamInputBinding.inflate(dialog.layoutInflater) - context?.let { downloadsViewModel.updateHeaderList(it) } - fixPaddingStatusbar(binding?.downloadRoot) - } + dialog.setContentView(binding.root) - private fun handleItemClick(click: DownloadHeaderClickEvent) { - when (click.action) { - DOWNLOAD_ACTION_GO_TO_CHILD -> { - if (click.data.type.isEpisodeBased()) { - val folder = - getFolderName(DOWNLOAD_EPISODE_CACHE, click.data.id.toString()) - activity?.navigate( - R.id.action_navigation_downloads_to_navigation_download_child, - DownloadChildFragment.newInstance(click.data.name, folder) - ) - } + dialog.show() + + // If user has clicked the switch do not interfere + var preventAutoSwitching = false + binding.hlsSwitch.setOnClickListener { + preventAutoSwitching = true } - DOWNLOAD_ACTION_LOAD_RESULT -> { - activity?.loadResult(click.data.url, click.data.apiName) - } - } - } - - private fun handleSelectedChange(selected: MutableSet) { - if (selected.isNotEmpty()) { - binding?.downloadDeleteAppbar?.isVisible = true - binding?.downloadStorageAppbar?.isVisible = false - activity?.attachBackPressedCallback { - downloadsViewModel.setIsMultiDeleteState(false) + fun activateSwitchOnHls(text: String?) { + binding.hlsSwitch.isChecked = normalSafeApiCall { + URI(text).path?.substringAfterLast(".")?.contains("m3u") + } == true } - binding?.btnDelete?.setOnClickListener { - context?.let { ctx -> - downloadsViewModel.handleMultiDelete(ctx) - } + binding.streamReferer.doOnTextChanged { text, _, _, _ -> + if (!preventAutoSwitching) + activateSwitchOnHls(text?.toString()) } - binding?.btnCancel?.setOnClickListener { - downloadsViewModel.setIsMultiDeleteState(false) + (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?)?.primaryClip?.getItemAt( + 0 + )?.text?.toString()?.let { copy -> + val fixedText = copy.trim() + binding.streamUrl.setText(fixedText) + activateSwitchOnHls(fixedText) } - binding?.btnToggleAll?.setOnClickListener { - val allSelected = downloadsViewModel.isAllSelected() - val adapter = binding?.downloadList?.adapter as? DownloadAdapter - if (allSelected) { - adapter?.notifySelectionStates() - downloadsViewModel.clearSelectedItems() + binding.applyBtt.setOnClickListener { + val url = binding.streamUrl.text?.toString() + if (url.isNullOrEmpty()) { + showToast(R.string.error_invalid_url, Toast.LENGTH_SHORT) } else { - adapter?.notifyAllSelected() - downloadsViewModel.selectAllItems() - } - } + val referer = binding.streamReferer.text?.toString() - downloadsViewModel.setIsMultiDeleteState(true) - } - } - - private fun updateDeleteButton(count: Int, selectedBytes: Long) { - val formattedSize = formatShortFileSize(context, selectedBytes) - binding?.btnDelete?.text = - getString(R.string.delete_format).format(count, formattedSize) - } - - private fun updateStorageInfo( - context: Context, - bytes: Long, - @StringRes stringRes: Int, - textView: TextView?, - view: View? - ) { - textView?.text = getString(R.string.storage_size_format).format( - getString(stringRes), - formatShortFileSize(context, bytes) - ) - view?.setLayoutWidth(bytes) - } - - private fun openLocalVideo() { - val intent = Intent() - .setAction(Intent.ACTION_GET_CONTENT) - .setType("video/*") - .addCategory(Intent.CATEGORY_OPENABLE) - .addFlags(FLAG_GRANT_READ_URI_PERMISSION) // Request temporary access - normalSafeApiCall { - videoResultLauncher.launch( - Intent.createChooser( - intent, - getString(R.string.open_local_video) - ) - ) - } - } - - private fun showStreamInputDialog(context: Context) { - val dialog = Dialog(context, R.style.AlertDialogCustom) - val binding = StreamInputBinding.inflate(dialog.layoutInflater) - dialog.setContentView(binding.root) - dialog.show() - - var preventAutoSwitching = false - binding.hlsSwitch.setOnClickListener { preventAutoSwitching = true } - - binding.streamReferer.doOnTextChanged { text, _, _, _ -> - if (!preventAutoSwitching) activateSwitchOnHls(text?.toString(), binding) - } - - (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)?.primaryClip?.getItemAt( - 0 - )?.text?.toString()?.let { copy -> - val fixedText = copy.trim() - binding.streamUrl.setText(fixedText) - activateSwitchOnHls(fixedText, binding) - } - - binding.applyBtt.setOnClickListener { - val url = binding.streamUrl.text?.toString() - if (url.isNullOrEmpty()) { - showToast(R.string.error_invalid_url, Toast.LENGTH_SHORT) - } else { - val referer = binding.streamReferer.text?.toString() - activity?.navigate( - R.id.global_to_navigation_player, - GeneratorPlayer.newInstance( - LinkGenerator( - listOf(BasicLink(url)), - extract = true, - referer = referer, - isM3u8 = binding.hlsSwitch.isChecked + activity?.navigate( + R.id.global_to_navigation_player, + GeneratorPlayer.newInstance( + LinkGenerator( + listOf(BasicLink(url)), + extract = true, + referer = referer, + isM3u8 = binding.hlsSwitch.isChecked + ) ) ) - ) + + dialog.dismissSafe(activity) + } + } + + binding.cancelBtt.setOnClickListener { dialog.dismissSafe(activity) } } - - binding.cancelBtt.setOnClickListener { - dialog.dismissSafe(activity) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + binding?.downloadList?.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> + val dy = scrollY - oldScrollY + if (dy > 0) { //check for scroll down + binding?.downloadStreamButton?.shrink() // hide + } else if (dy < -5) { + binding?.downloadStreamButton?.extend() // show + } + } } - } + downloadsViewModel.updateList(requireContext()) - private fun activateSwitchOnHls(text: String?, binding: StreamInputBinding) { - binding.hlsSwitch.isChecked = normalSafeApiCall { - URI(text).path?.substringAfterLast(".")?.contains("m3u") - } == true - } - - private fun handleScroll(dy: Int) { - if (dy > 0) { - binding?.downloadStreamButton?.shrink() - } else if (dy < -5) { - binding?.downloadStreamButton?.extend() - } - } - - // Open local video from files using content provider x safeFile - private val videoResultLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult - val selectedVideoUri = result?.data?.data ?: return@registerForActivityResult - playUri(activity ?: return@registerForActivityResult, selectedVideoUri) + fixPaddingStatusbar(binding?.downloadRoot) } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt new file mode 100644 index 00000000..65a6441f --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt @@ -0,0 +1,149 @@ +package com.lagradost.cloudstream3.ui.download + +import android.annotation.SuppressLint +import android.text.format.Formatter.formatShortFileSize +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.DownloadHeaderEpisodeBinding +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.UIHelper.setImage +import com.lagradost.cloudstream3.utils.VideoDownloadHelper +import java.util.* + +data class VisualDownloadHeaderCached( + val currentOngoingDownloads: Int, + val totalDownloads: Int, + val totalBytes: Long, + val currentBytes: Long, + val data: VideoDownloadHelper.DownloadHeaderCached, + val child: VideoDownloadHelper.DownloadEpisodeCached?, +) + +data class DownloadHeaderClickEvent( + val action: Int, + val data: VideoDownloadHelper.DownloadHeaderCached +) + +class DownloadHeaderAdapter( + var cardList: List, + private val clickCallback: (DownloadHeaderClickEvent) -> Unit, + private val movieClickCallback: (DownloadClickEvent) -> Unit, +) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return DownloadHeaderViewHolder( + DownloadHeaderEpisodeBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ), + clickCallback, + movieClickCallback + ) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is DownloadHeaderViewHolder -> { + holder.bind(cardList[position]) + } + } + } + + override fun getItemCount(): Int { + return cardList.size + } + + class DownloadHeaderViewHolder + constructor( + val binding: DownloadHeaderEpisodeBinding, + private val clickCallback: (DownloadHeaderClickEvent) -> Unit, + private val movieClickCallback: (DownloadClickEvent) -> Unit, + ) : RecyclerView.ViewHolder(binding.root) { + + /*private val poster: ImageView? = itemView.download_header_poster + private val title: TextView = itemView.download_header_title + private val extraInfo: TextView = itemView.download_header_info + private val holder: CardView = itemView.episode_holder + + private val downloadBar: ContentLoadingProgressBar = itemView.download_header_progress_downloaded + private val downloadImage: ImageView = itemView.download_header_episode_download + private val normalImage: ImageView = itemView.download_header_goto_child*/ + + @SuppressLint("SetTextI18n") + fun bind(card: VisualDownloadHeaderCached) { + val d = card.data + + binding.downloadHeaderPoster.apply { + setImage(d.poster) + setOnClickListener { + clickCallback.invoke(DownloadHeaderClickEvent(1, d)) + } + } + + binding.apply { + + binding.downloadHeaderTitle.text = d.name + val mbString = formatShortFileSize(itemView.context, card.totalBytes) + + //val isMovie = d.type.isMovieType() + if (card.child != null) { + //downloadHeaderProgressDownloaded.visibility = View.VISIBLE + + // downloadHeaderEpisodeDownload.visibility = View.VISIBLE + binding.downloadHeaderGotoChild.visibility = View.GONE + + downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, movieClickCallback) + downloadButton.isVisible = true + /*setUpButton( + card.currentBytes, + card.totalBytes, + downloadBar, + downloadImage, + extraInfo, + card.child, + movieClickCallback + )*/ + + episodeHolder.setOnClickListener { + movieClickCallback.invoke( + DownloadClickEvent( + DOWNLOAD_ACTION_PLAY_FILE, + card.child + ) + ) + } + } else { + downloadButton.isVisible = false + // downloadHeaderProgressDownloaded.visibility = View.GONE + // downloadHeaderEpisodeDownload.visibility = View.GONE + binding.downloadHeaderGotoChild.visibility = View.VISIBLE + + try { + downloadHeaderInfo.text = + downloadHeaderInfo.context.getString(R.string.extra_info_format).format( + card.totalDownloads, + if (card.totalDownloads == 1) downloadHeaderInfo.context.getString(R.string.episode) else downloadHeaderInfo.context.getString( + R.string.episodes + ), + mbString + ) + } catch (t: Throwable) { + // you probably formatted incorrectly + downloadHeaderInfo.text = "Error" + logError(t) + } + + + episodeHolder.setOnClickListener { + clickCallback.invoke(DownloadHeaderClickEvent(0, d)) + } + } + } + } + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt index 137f1355..3a74a715 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt @@ -1,439 +1,122 @@ package com.lagradost.cloudstream3.ui.download import android.content.Context -import android.content.DialogInterface import android.os.Environment import android.os.StatFs -import androidx.appcompat.app.AlertDialog import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.isEpisodeBased +import com.lagradost.cloudstream3.isMovieType import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull -import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE import com.lagradost.cloudstream3.utils.DataStore.getFolderName import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKeys import com.lagradost.cloudstream3.utils.VideoDownloadHelper -import com.lagradost.cloudstream3.utils.VideoDownloadManager.deleteFilesAndUpdateSettings -import com.lagradost.cloudstream3.utils.VideoDownloadManager.getDownloadFileInfoAndUpdateSettings +import com.lagradost.cloudstream3.utils.VideoDownloadManager import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class DownloadViewModel : ViewModel() { + private val _noDownloadsText = MutableLiveData().apply { + value = "" + } + val noDownloadsText: LiveData = _noDownloadsText - private val _headerCards = MutableLiveData>() - val headerCards: LiveData> = _headerCards - - private val _childCards = MutableLiveData>() - val childCards: LiveData> = _childCards + private val _headerCards = + MutableLiveData>().apply { listOf() } + val headerCards: LiveData> = _headerCards private val _usedBytes = MutableLiveData() - val usedBytes: LiveData = _usedBytes - private val _availableBytes = MutableLiveData() - val availableBytes: LiveData = _availableBytes - private val _downloadBytes = MutableLiveData() + + val usedBytes: LiveData = _usedBytes + val availableBytes: LiveData = _availableBytes val downloadBytes: LiveData = _downloadBytes - private val _selectedBytes = MutableLiveData(0) - val selectedBytes: LiveData = _selectedBytes - - private val _isMultiDeleteState = MutableLiveData(false) - val isMultiDeleteState: LiveData = _isMultiDeleteState - - private val _selectedItemIds = MutableLiveData>(mutableSetOf()) - val selectedItemIds: LiveData> = _selectedItemIds - - private var previousVisual: List? = null - - fun setIsMultiDeleteState(value: Boolean) { - _isMultiDeleteState.postValue(value) - } - - fun addSelected(itemId: Int) { - updateSelectedItems { it.add(itemId) } - } - - fun removeSelected(itemId: Int) { - updateSelectedItems { it.remove(itemId) } - } - - fun selectAllItems() { - val items = headerCards.value.orEmpty() + childCards.value.orEmpty() - updateSelectedItems { it.addAll(items.map { item -> item.data.id }) } - } - - fun clearSelectedItems() { - // We need this to be done immediately - // so we can't use postValue - _selectedItemIds.value = mutableSetOf() - updateSelectedItems { it.clear() } - } - - fun isAllSelected(): Boolean { - val currentSelected = selectedItemIds.value ?: return false - val items = headerCards.value.orEmpty() + childCards.value.orEmpty() - return items.count() == currentSelected.count() && items.all { it.data.id in currentSelected } - } - - private fun updateSelectedItems(action: (MutableSet) -> Unit) { - val currentSelected = selectedItemIds.value ?: mutableSetOf() - action(currentSelected) - _selectedItemIds.postValue(currentSelected) - updateSelectedBytes() - updateSelectedCards() - } - - private fun updateSelectedBytes() = viewModelScope.launchSafe { - val selectedItemsList = getSelectedItemsData() ?: return@launchSafe - val totalSelectedBytes = selectedItemsList.sumOf { it.totalBytes } - _selectedBytes.postValue(totalSelectedBytes) - } - - private fun updateSelectedCards() = viewModelScope.launchSafe { - val currentSelected = selectedItemIds.value ?: return@launchSafe - - headerCards.value?.let { headers -> - headers.forEach { header -> - header.isSelected = header.data.id in currentSelected - } - _headerCards.postValue(headers) - } - - childCards.value?.let { children -> - children.forEach { child -> - child.isSelected = child.data.id in currentSelected - } - _childCards.postValue(children) - } - } - - fun updateHeaderList(context: Context) = viewModelScope.launchSafe { - val visual = withContext(Dispatchers.IO) { - val children = context.getKeys(DOWNLOAD_EPISODE_CACHE) - .mapNotNull { context.getKey(it) } + fun updateList(context: Context) = viewModelScope.launchSafe { + val children = withContext(Dispatchers.IO) { + val headers = context.getKeys(DOWNLOAD_EPISODE_CACHE) + headers.mapNotNull { context.getKey(it) } .distinctBy { it.id } // Remove duplicates - - val (totalBytesUsedByChild, currentBytesUsedByChild, totalDownloads) = - calculateDownloadStats(context, children) - - val cached = context.getKeys(DOWNLOAD_HEADER_CACHE) - .mapNotNull { context.getKey(it) } - - createVisualDownloadList( - context, cached, totalBytesUsedByChild, currentBytesUsedByChild, totalDownloads - ) } - if (visual != previousVisual) { - previousVisual = visual - updateStorageStats(visual) - _headerCards.postValue(visual) - } - } - - private fun calculateDownloadStats( - context: Context, - children: List - ): Triple, Map, Map> { // parentId : bytes - val totalBytesUsedByChild = mutableMapOf() + val totalBytesUsedByChild = HashMap() // parentId : bytes - val currentBytesUsedByChild = mutableMapOf() + val currentBytesUsedByChild = HashMap() // parentId : downloadsCount - val totalDownloads = mutableMapOf() + val totalDownloads = HashMap() - children.forEach { child -> - val childFile = getDownloadFileInfoAndUpdateSettings(context, child.id) ?: return@forEach - if (childFile.fileLength <= 1) return@forEach - val len = childFile.totalBytes - val flen = childFile.fileLength + // Gets all children downloads + withContext(Dispatchers.IO) { + for (c in children) { + val childFile = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(context, c.id) ?: continue - totalBytesUsedByChild.merge(child.parentId, len, Long::plus) - currentBytesUsedByChild.merge(child.parentId, flen, Long::plus) - totalDownloads.merge(child.parentId, 1, Int::plus) + if (childFile.fileLength <= 1) continue + val len = childFile.totalBytes + val flen = childFile.fileLength + + totalBytesUsedByChild[c.parentId] = totalBytesUsedByChild[c.parentId]?.plus(len) ?: len + currentBytesUsedByChild[c.parentId] = currentBytesUsedByChild[c.parentId]?.plus(flen) ?: flen + totalDownloads[c.parentId] = totalDownloads[c.parentId]?.plus(1) ?: 1 + } } - return Triple(totalBytesUsedByChild, currentBytesUsedByChild, totalDownloads) - } - private fun createVisualDownloadList( - context: Context, - cached: List, - totalBytesUsedByChild: Map, - currentBytesUsedByChild: Map, - totalDownloads: Map - ): List { - return cached.mapNotNull { - val downloads = totalDownloads[it.id] ?: 0 - val bytes = totalBytesUsedByChild[it.id] ?: 0 - val currentBytes = currentBytesUsedByChild[it.id] ?: 0 - if (bytes <= 0 || downloads <= 0) return@mapNotNull null - - val isSelected = selectedItemIds.value?.contains(it.id) ?: false - val movieEpisode = if (it.type.isEpisodeBased()) null else context.getKey( - DOWNLOAD_EPISODE_CACHE, - getFolderName(it.id.toString(), it.id.toString()) - ) - - VisualDownloadCached.Header( - currentBytes = currentBytes, - totalBytes = bytes, - data = it, - child = movieEpisode, - currentOngoingDownloads = 0, - totalDownloads = downloads, - isSelected = isSelected, - ) - // Prevent order being almost completely random, - // making things difficult to find. - }.sortedWith(compareBy { - // Sort by isEpisodeBased() ascending. We put those that - // are episode based at the bottom for UI purposes and to - // make it easier to find by grouping them together. - it.data.type.isEpisodeBased() - }.thenBy { - // Then we sort alphabetically by name (case-insensitive). - // Again, we do this to make things easier to find. - it.data.name.lowercase() - }) - } - - fun updateChildList(context: Context, folder: String) = viewModelScope.launchSafe { - val visual = withContext(Dispatchers.IO) { - context.getKeys(folder).mapNotNull { key -> - context.getKey(key) - }.mapNotNull { - val isSelected = selectedItemIds.value?.contains(it.id) ?: false - val info = getDownloadFileInfoAndUpdateSettings(context, it.id) ?: return@mapNotNull null - VisualDownloadCached.Child( - currentBytes = info.fileLength, - totalBytes = info.totalBytes, - isSelected = isSelected, - data = it, + val cached = withContext(Dispatchers.IO) { // wont fetch useless keys + totalDownloads.entries.filter { it.value > 0 }.mapNotNull { + context.getKey( + DOWNLOAD_HEADER_CACHE, + it.key.toString() ) } - }.sortedWith(compareBy( - // Sort by season first, and then by episode number, - // to ensure sorting is consistent. - { it.data.season ?: 0 }, - { it.data.episode } - )) - - if (previousVisual != visual) { - previousVisual = visual - _childCards.postValue(visual) } - } - private fun removeItems(idsToRemove: Set) = viewModelScope.launchSafe { - val updatedHeaders = headerCards.value.orEmpty().filter { it.data.id !in idsToRemove } - val updatedChildren = childCards.value.orEmpty().filter { it.data.id !in idsToRemove } - _headerCards.postValue(updatedHeaders) - _childCards.postValue(updatedChildren) - } - - private fun updateStorageStats(visual: List) { + val visual = withContext(Dispatchers.IO) { + cached.mapNotNull { // TODO FIX + val downloads = totalDownloads[it.id] ?: 0 + val bytes = totalBytesUsedByChild[it.id] ?: 0 + val currentBytes = currentBytesUsedByChild[it.id] ?: 0 + if (bytes <= 0 || downloads <= 0) return@mapNotNull null + val movieEpisode = + if (!it.type.isMovieType()) null + else context.getKey( + DOWNLOAD_EPISODE_CACHE, + getFolderName(it.id.toString(), it.id.toString()) + ) + VisualDownloadHeaderCached( + 0, + downloads, + bytes, + currentBytes, + it, + movieEpisode + ) + }.sortedBy { + (it.child?.episode ?: 0) + (it.child?.season?.times(10000) ?: 0) + } // episode sorting by episode, lowest to highest + } try { val stat = StatFs(Environment.getExternalStorageDirectory().path) - val localBytesAvailable = stat.availableBytes + + val localBytesAvailable = stat.availableBytes//stat.blockSizeLong * stat.blockCountLong val localTotalBytes = stat.blockSizeLong * stat.blockCountLong val localDownloadedBytes = visual.sumOf { it.totalBytes } - val localUsedBytes = localTotalBytes - localBytesAvailable - _usedBytes.postValue(localUsedBytes) + + _usedBytes.postValue(localTotalBytes - localBytesAvailable - localDownloadedBytes) _availableBytes.postValue(localBytesAvailable) _downloadBytes.postValue(localDownloadedBytes) - } catch (t: Throwable) { + } catch (t : Throwable) { _downloadBytes.postValue(0) logError(t) } + + _headerCards.postValue(visual) } - - fun handleMultiDelete(context: Context) = viewModelScope.launchSafe { - val selectedItemsList = getSelectedItemsData().orEmpty() - val deleteData = processSelectedItems(context, selectedItemsList) - val message = buildDeleteMessage(context, deleteData) - showDeleteConfirmationDialog(context, message, deleteData.ids, deleteData.parentIds) - } - - fun handleSingleDelete( - context: Context, - itemId: Int - ) = viewModelScope.launchSafe { - val itemData = getItemDataFromId(itemId) - val deleteData = processSelectedItems(context, itemData) - val message = buildDeleteMessage(context, deleteData) - showDeleteConfirmationDialog(context, message, deleteData.ids, deleteData.parentIds) - } - - private fun processSelectedItems( - context: Context, - selectedItemsList: List - ): DeleteData { - val names = mutableListOf() - val seriesNames = mutableListOf() - - val ids = mutableSetOf() - val parentIds = mutableSetOf() - - var parentName: String? = null - - selectedItemsList.forEach { item -> - when (item) { - is VisualDownloadCached.Header -> { - if (item.data.type.isEpisodeBased()) { - val episodes = context.getKeys(DOWNLOAD_EPISODE_CACHE) - .mapNotNull { - context.getKey( - it - ) - } - .filter { it.parentId == item.data.id } - .map { it.id } - ids.addAll(episodes) - parentIds.add(item.data.id) - - val episodeInfo = "${item.data.name} (${item.totalDownloads} ${ - context.resources.getQuantityString( - R.plurals.episodes, - item.totalDownloads - ).lowercase() - })" - seriesNames.add(episodeInfo) - } else { - ids.add(item.data.id) - names.add(item.data.name) - } - } - - is VisualDownloadCached.Child -> { - ids.add(item.data.id) - val parent = context.getKey( - DOWNLOAD_HEADER_CACHE, - item.data.parentId.toString() - ) - parentName = parent?.name - names.add( - context.getNameFull( - item.data.name, - item.data.episode, - item.data.season - ) - ) - } - } - } - - return DeleteData(ids, parentIds, seriesNames, names, parentName) - } - - private fun buildDeleteMessage( - context: Context, - data: DeleteData - ): String { - val formattedNames = data.names.sortedBy { it.lowercase() } - .joinToString(separator = "\n") { "• $it" } - val formattedSeriesNames = data.seriesNames.sortedBy { it.lowercase() } - .joinToString(separator = "\n") { "• $it" } - - return when { - data.ids.count() == 1 -> { - context.getString(R.string.delete_message).format( - data.names.firstOrNull() - ) - } - - data.seriesNames.isNotEmpty() && data.names.isEmpty() -> { - context.getString(R.string.delete_message_series_only).format(formattedSeriesNames) - } - - data.parentName != null && data.names.isNotEmpty() -> { - context.getString(R.string.delete_message_series_episodes) - .format(data.parentName, formattedNames) - } - - data.seriesNames.isNotEmpty() -> { - val seriesSection = context.getString(R.string.delete_message_series_section) - .format(formattedSeriesNames) - context.getString(R.string.delete_message_multiple) - .format(formattedNames) + "\n\n" + seriesSection - } - - else -> context.getString(R.string.delete_message_multiple).format(formattedNames) - } - } - - private fun showDeleteConfirmationDialog( - context: Context, - message: String, - ids: Set, - parentIds: Set - ) { - val builder = AlertDialog.Builder(context) - val dialogClickListener = - DialogInterface.OnClickListener { _, which -> - when (which) { - DialogInterface.BUTTON_POSITIVE -> { - viewModelScope.launchSafe { - setIsMultiDeleteState(false) - deleteFilesAndUpdateSettings(context, ids, this) { successfulIds -> - // We always remove parent because if we are deleting from here - // and we have it as non-empty, it was triggered on - // parent header card - removeItems(successfulIds + parentIds) - } - } - } - - DialogInterface.BUTTON_NEGATIVE -> { - // Do nothing on cancel - } - } - } - - try { - val title = if (ids.count() == 1) { - R.string.delete_file - } else R.string.delete_files - builder.setTitle(title) - .setMessage(message) - .setPositiveButton(R.string.delete, dialogClickListener) - .setNegativeButton(R.string.cancel, dialogClickListener) - .show().setDefaultFocus() - } catch (e: Exception) { - logError(e) - } - } - - private fun getSelectedItemsData(): List? { - val headers = headerCards.value.orEmpty() - val children = childCards.value.orEmpty() - - return selectedItemIds.value?.mapNotNull { id -> - headers.find { it.data.id == id } ?: children.find { it.data.id == id } - } - } - - private fun getItemDataFromId(itemId: Int): List { - val headers = headerCards.value.orEmpty() - val children = childCards.value.orEmpty() - - return (headers + children).filter { it.data.id == itemId } - } - - private data class DeleteData( - val ids: Set, - val parentIds: Set, - val seriesNames: List, - val names: List, - val parentName: String? - ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt index 908e3a80..b43f1aac 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt @@ -1,7 +1,7 @@ package com.lagradost.cloudstream3.ui.download.button import android.content.Context -import android.text.format.Formatter.formatShortFileSize +import android.text.format.Formatter import android.util.AttributeSet import android.widget.FrameLayout import android.widget.TextView @@ -9,8 +9,6 @@ import androidx.annotation.LayoutRes import androidx.core.view.isVisible import androidx.core.widget.ContentLoadingProgressBar import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.utils.Coroutines.ioSafe -import com.lagradost.cloudstream3.utils.Coroutines.mainWork import com.lagradost.cloudstream3.utils.VideoDownloadManager typealias DownloadStatusTell = VideoDownloadManager.DownloadType @@ -36,7 +34,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : lateinit var progressBar: ContentLoadingProgressBar var progressText: TextView? = null - /* val gid: String? get() = sessionIdToGid[persistentId] + /*val gid: String? get() = sessionIdToGid[persistentId] // used for resuming data var _lastRequestOverride: UriRequest? = null @@ -46,7 +44,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : _lastRequestOverride = value } - var files: List = emptyList() */ + var files: List = emptyList()*/ protected var isZeroBytes: Boolean = true fun inflate(@LayoutRes layout: Int) { @@ -54,16 +52,12 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : } init { - @Suppress("LeakingThis") resetViewData() } - var doSetProgress = true - open fun resetViewData() { // lastRequest = null isZeroBytes = true - doSetProgress = true persistentId = null } @@ -74,45 +68,37 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : persistentId = id currentMetaData.id = id - if (!doSetProgress) return + VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(context, id)?.let { savedData -> + val downloadedBytes = savedData.fileLength + val totalBytes = savedData.totalBytes - ioSafe { - val savedData = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(context, id) + /*lastRequest = savedData.uriRequest + files = savedData.files - mainWork { - if (savedData != null) { - val downloadedBytes = savedData.fileLength - val totalBytes = savedData.totalBytes - - setProgress(downloadedBytes, totalBytes) - applyMetaData(id, downloadedBytes, totalBytes) - } else run { resetView() } + var totalBytes: Long = 0 + var downloadedBytes: Long = 0 + for (file in savedData.files) { + downloadedBytes += file.completedLength + totalBytes += file.length + }*/ + setProgress(downloadedBytes, totalBytes) + // some extra padding for just in case + val status = VideoDownloadManager.downloadStatus[id] + ?: if (downloadedBytes > 1024L && downloadedBytes + 1024L >= totalBytes) DownloadStatusTell.IsDone else DownloadStatusTell.IsPaused + currentMetaData.apply { + this.id = id + this.downloadedLength = downloadedBytes + this.totalLength = totalBytes + this.status = status } + setStatus(status) + } ?: run { + resetView() } } abstract fun setStatus(status: VideoDownloadManager.DownloadType?) - fun getStatus(id: Int, downloadedBytes: Long, totalBytes: Long): DownloadStatusTell { - // some extra padding for just in case - return VideoDownloadManager.downloadStatus[id] - ?: if (downloadedBytes > 1024L && downloadedBytes + 1024L >= totalBytes) { - DownloadStatusTell.IsDone - } else DownloadStatusTell.IsPaused - } - - fun applyMetaData(id: Int, downloadedBytes: Long, totalBytes: Long) { - val status = getStatus(id, downloadedBytes, totalBytes) - - currentMetaData.apply { - this.id = id - this.downloadedLength = downloadedBytes - this.totalLength = totalBytes - this.status = status - } - setStatus(status) - } - open fun setProgress(downloadedBytes: Long, totalBytes: Long) { isZeroBytes = downloadedBytes == 0L progressBar.post { @@ -138,16 +124,13 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : if (isZeroBytes) { progressText?.isVisible = false } else { - if (doSetProgress) { - progressText?.apply { - val currentFormattedSizeString = - formatShortFileSize(context, downloadedBytes) - val totalFormattedSizeString = formatShortFileSize(context, totalBytes) - text = - // if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else - context?.getString(R.string.download_size_format) - ?.format(currentFormattedSizeString, totalFormattedSizeString) - } + progressText?.apply { + val currentMbString = Formatter.formatShortFileSize(context, downloadedBytes) + val totalMbString = Formatter.formatShortFileSize(context, totalBytes) + text = + //if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else + context?.getString(R.string.download_size_format) + ?.format(currentMbString, totalMbString) } } @@ -184,8 +167,8 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : override fun onAttachedToWindow() { VideoDownloadManager.downloadStatusEvent += ::downloadStatusEvent - // VideoDownloadManager.downloadDeleteEvent += ::downloadDeleteEvent - // VideoDownloadManager.downloadEvent += ::downloadEvent + //VideoDownloadManager.downloadDeleteEvent += ::downloadDeleteEvent + //VideoDownloadManager.downloadEvent += ::downloadEvent VideoDownloadManager.downloadProgressEvent += ::downloadProgressEvent val pid = persistentId @@ -199,8 +182,8 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : override fun onDetachedFromWindow() { VideoDownloadManager.downloadStatusEvent -= ::downloadStatusEvent - // VideoDownloadManager.downloadDeleteEvent -= ::downloadDeleteEvent - // VideoDownloadManager.downloadEvent -= ::downloadEvent + //VideoDownloadManager.downloadDeleteEvent -= ::downloadDeleteEvent + //VideoDownloadManager.downloadEvent -= ::downloadEvent VideoDownloadManager.downloadProgressEvent -= ::downloadProgressEvent super.onDetachedFromWindow() @@ -215,4 +198,5 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : * Get a clean slate again, might be useful in recyclerview? * */ abstract fun resetView() + } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt index 20a44461..d97a4b88 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt @@ -13,7 +13,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadHelper class DownloadButton(context: Context, attributeSet: AttributeSet) : PieFetchButton(context, attributeSet) { - private var mainText: TextView? = null + var mainText: TextView? = null override fun onAttachedToWindow() { super.onAttachedToWindow() progressText = findViewById(R.id.result_movie_download_text_precentage) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt index 29c2daa2..a729f33a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.ui.download.button import android.content.Context +import android.graphics.drawable.Drawable import android.os.Looper import android.util.AttributeSet import android.util.Log @@ -12,7 +13,6 @@ import androidx.annotation.MainThread import androidx.core.content.ContextCompat import androidx.core.view.isGone import androidx.core.view.isVisible -import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DELETE_FILE @@ -25,7 +25,7 @@ import com.lagradost.cloudstream3.ui.download.DownloadClickEvent import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager -import com.lagradost.cloudstream3.utils.VideoDownloadManager.KEY_RESUME_PACKAGES + open class PieFetchButton(context: Context, attributeSet: AttributeSet) : BaseFetchButton(context, attributeSet) { @@ -44,8 +44,6 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : private var iconPaused: Int = 0 private var hideWhenIcon: Boolean = true - var progressDrawable: Int = 0 - var overrideLayout: Int? = null companion object { @@ -58,7 +56,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : } private var progressBarBackground: View - var statusView: ImageView + private var statusView: ImageView open fun onInflate() {} @@ -116,10 +114,10 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : R.styleable.PieFetchButton_download_icon_complete, R.drawable.download_icon_done ) iconPaused = getResourceId( - R.styleable.PieFetchButton_download_icon_paused, 0 // R.drawable.download_icon_pause + R.styleable.PieFetchButton_download_icon_paused, 0//R.drawable.download_icon_pause ) iconActive = getResourceId( - R.styleable.PieFetchButton_download_icon_active, 0 // R.drawable.download_icon_load + R.styleable.PieFetchButton_download_icon_active, 0 //R.drawable.download_icon_load ) iconWaiting = getResourceId( R.styleable.PieFetchButton_download_icon_waiting, 0 @@ -130,7 +128,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : val fillIndex = getInt(R.styleable.PieFetchButton_download_fill, 0) - progressDrawable = getResourceId( + val progressDrawable = getResourceId( R.styleable.PieFetchButton_download_fill_override, fillArray[fillIndex] ) @@ -169,9 +167,8 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : this.setPersistentId(card.id) view.setOnClickListener { if (isZeroBytes) { - removeKey(KEY_RESUME_PACKAGES, card.id.toString()) callback(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, card)) - // callback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data)) + //callback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data)) } else { val list = arrayListOf( Pair(DOWNLOAD_ACTION_PLAY_FILE, R.string.popup_play_file), @@ -198,7 +195,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : list ) { callback(DownloadClickEvent(itemId, card)) - // callback.invoke(DownloadClickEvent(itemId, data)) + //callback.invoke(DownloadClickEvent(itemId, data)) } } } @@ -206,7 +203,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : view.setOnLongClickListener { callback(DownloadClickEvent(DOWNLOAD_ACTION_LONG_CLICK, card)) - // clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_LONG_CLICK, data)) + //clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_LONG_CLICK, data)) return@setOnLongClickListener true } } @@ -219,7 +216,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : setDefaultClickListener(this, textView, card, callback) } - /* open fun setDefaultClickListener(requestGetter: suspend BaseFetchButton.() -> List) { + /*open fun setDefaultClickListener(requestGetter: suspend BaseFetchButton.() -> List) { this.setOnClickListener { when (this.currentStatus) { null -> { @@ -245,10 +242,10 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : else -> {} } } - } */ + }*/ @MainThread - private fun setStatusInternal(status: DownloadStatusTell?) { + private fun setStatusInternal(status : DownloadStatusTell?) { val isPreActive = isZeroBytes && status == DownloadStatusTell.IsDownloading if (animateWaiting && (status == DownloadStatusTell.IsPending || isPreActive)) { val animation = AnimationUtils.loadAnimation(context, waitingAnimation) @@ -263,8 +260,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : progressBarBackground.background = ContextCompat.getDrawable(context, progressDrawable) - val drawable = - getDrawableFromStatus(status)?.let { ContextCompat.getDrawable(this.context, it) } + val drawable = getDrawableFromStatus(status) statusView.setImageDrawable(drawable) val isDrawable = drawable != null @@ -282,12 +278,12 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : override fun setStatus(status: DownloadStatusTell?) { currentStatus = status - // Runs on the main thread, but also instant if it already is + // runs on the main thread, but also instant if it already is if (Looper.myLooper() == Looper.getMainLooper()) { try { setStatusInternal(status) - } catch (t: Throwable) { - logError(t) // Just in case setStatusInternal throws because thread + } catch (t : Throwable) { + logError(t) // just in case setStatusInternal throws because thread progressBarBackground.post { setStatusInternal(status) } @@ -303,7 +299,6 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : setStatus(null) currentMetaData = DownloadMetadata(0, 0, 0, null) isZeroBytes = true - doSetProgress = true progressBar.progress = 0 } @@ -327,13 +322,19 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : } } - open fun getDrawableFromStatus(status: DownloadStatusTell?): Int? = when (status) { - DownloadStatusTell.IsPaused -> iconPaused - DownloadStatusTell.IsPending -> iconWaiting - DownloadStatusTell.IsDownloading -> iconActive - DownloadStatusTell.IsFailed -> iconError - DownloadStatusTell.IsDone -> iconComplete - DownloadStatusTell.IsStopped -> iconRemoved - else -> iconInit - }.takeIf { it != 0 } + open fun getDrawableFromStatus(status: DownloadStatusTell?): Drawable? { + val drawableInt = when (status) { + DownloadStatusTell.IsPaused -> iconPaused + DownloadStatusTell.IsPending -> iconWaiting + DownloadStatusTell.IsDownloading -> iconActive + DownloadStatusTell.IsFailed -> iconError + DownloadStatusTell.IsDone -> iconComplete + DownloadStatusTell.IsStopped -> iconRemoved + null -> iconInit + } + if (drawableInt == 0) { + return null + } + return ContextCompat.getDrawable(this.context, drawableInt) + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt index b25486eb..ebed901f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt @@ -15,7 +15,7 @@ import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchResultBuilder import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.UIHelper.isBottomLayout +import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout import com.lagradost.cloudstream3.utils.UIHelper.toPx class HomeScrollViewHolderState(view: ViewBinding) : ViewHolderState(view) { @@ -54,7 +54,7 @@ class HomeChildItemAdapter( var hasNext: Boolean = false override fun onCreateContent(parent: ViewGroup): ViewHolderState { - val expanded = parent.context.isBottomLayout() + val expanded = parent.context.IsBottomLayout() /* val layout = if (bottom) R.layout.home_result_grid_expanded else R.layout.home_result_grid val root = LayoutInflater.from(parent.context).inflate(layout, parent, false) @@ -133,6 +133,7 @@ class HomeChildItemAdapter( item, position, holder.itemView, + null, // nextFocusBehavior, nextFocusUp, nextFocusDown ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index 49de2503..12185cbf 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -17,6 +17,7 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.lifecycle.* import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -24,6 +25,8 @@ import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.chip.Chip import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.apis +import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia +import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.databinding.FragmentHomeBinding import com.lagradost.cloudstream3.databinding.HomeEpisodesExpandedBinding @@ -43,13 +46,11 @@ import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppContextUtils.filterProviderByPreferredMedia -import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings -import com.lagradost.cloudstream3.utils.AppContextUtils.isRecyclerScrollable -import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult -import com.lagradost.cloudstream3.utils.AppContextUtils.ownHide -import com.lagradost.cloudstream3.utils.AppContextUtils.ownShow -import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable +import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult +import com.lagradost.cloudstream3.utils.AppUtils.ownHide +import com.lagradost.cloudstream3.utils.AppUtils.ownShow +import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.Event @@ -233,7 +234,7 @@ class HomeFragment : Fragment() { return bottomSheetDialogBuilder } - private fun getPairList( + fun getPairList( anime: Chip?, cartoons: Chip?, tvs: Chip?, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt index 8bc0aa28..fb75e772 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt @@ -1,8 +1,6 @@ package com.lagradost.cloudstream3.ui.home -import android.os.Build import android.os.Bundle -import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -24,7 +22,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppContextUtils.isRecyclerScrollable +import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable class LoadClickCallback( val action: Int = 0, @@ -55,20 +53,16 @@ open class ParentItemAdapter( "value", recyclerView?.layoutManager?.onSaveInstanceState() ) - (recyclerView?.adapter as? BaseAdapter<*, *>)?.save(recyclerView) + (recyclerView?.adapter as? BaseAdapter<*,*>)?.save(recyclerView) } override fun restore(state: Bundle) { (binding as? HomepageParentBinding)?.homeChildRecyclerview?.layoutManager?.onRestoreInstanceState( - state.getSafeParcelable("value") + state.getParcelable("value") ) } } - override fun submitList(list: List?) { - super.submitList(list?.sortedBy { it.list.list.isEmpty() }) - } - override fun onUpdateContent( holder: ViewHolderState, item: HomeViewModel.ExpandableHomepageList, @@ -171,9 +165,4 @@ open class ParentItemAdapter( submitList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) } .toMutableList()) } -} - -@Suppress("DEPRECATION") -inline fun Bundle.getSafeParcelable(key: String): T? = - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) getParcelable(key) - else getParcelable(key, T::class.java) \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt index 339ef1e1..52ec06db 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt @@ -16,6 +16,7 @@ import androidx.viewbinding.ViewBinding import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup +import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.HomePageList @@ -35,7 +36,6 @@ import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.selectHomepage import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.ResultViewModel2 import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST -import com.lagradost.cloudstream3.ui.result.getId import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA @@ -117,12 +117,15 @@ class HomeParentItemAdapterPreview( } override fun restore(state: Bundle) { - state.getSafeParcelable("resumeRecyclerView")?.let { recycle -> + state.getParcelable("resumeRecyclerView")?.let { recycle -> resumeRecyclerView.layoutManager?.onRestoreInstanceState(recycle) } - state.getSafeParcelable("bookmarkRecyclerView")?.let { recycle -> + state.getParcelable("bookmarkRecyclerView")?.let { recycle -> bookmarkRecyclerView.layoutManager?.onRestoreInstanceState(recycle) } + //state.getInt("previewViewpager").let { recycle -> + // previewViewpager.setCurrentItem(recycle,true) + //} } val previewAdapter = HomeScrollAdapter(fragment = fragment) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index 24ca4df2..a2c7583f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -6,6 +6,9 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.lagradost.cloudstream3.APIHolder.apis +import com.lagradost.cloudstream3.APIHolder.filterHomePageListByFilmQuality +import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia +import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.getKey @@ -33,11 +36,8 @@ import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppContextUtils.addProgramsToContinueWatching -import com.lagradost.cloudstream3.utils.AppContextUtils.filterHomePageListByFilmQuality -import com.lagradost.cloudstream3.utils.AppContextUtils.filterProviderByPreferredMedia -import com.lagradost.cloudstream3.utils.AppContextUtils.filterSearchResultByFilmQuality -import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult +import com.lagradost.cloudstream3.utils.AppUtils.addProgramsToContinueWatching +import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE import com.lagradost.cloudstream3.utils.DataStoreHelper @@ -152,7 +152,7 @@ class HomeViewModel : ViewModel() { } }?.distinctBy { it.first } ?: return@launchSafe - val length = WatchType.entries.size + val length = WatchType.values().size val currentWatchTypes = mutableSetOf() for (watch in watchStatusIds) { @@ -387,9 +387,7 @@ class HomeViewModel : ViewModel() { } is Resource.Failure -> { - @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") _page.postValue(data!!) - @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") _preview.postValue(data!!) } @@ -399,7 +397,9 @@ class HomeViewModel : ViewModel() { } fun click(callback: SearchClickCallback) { - if (callback.action != SEARCH_ACTION_FOCUSED) { + if (callback.action == SEARCH_ACTION_FOCUSED) { + //focusCallback(callback.card) + } else { SearchHelper.handleSearchClickCallback(callback) } } @@ -516,7 +516,7 @@ class HomeViewModel : ViewModel() { } else { _page.postValue(Resource.Loading()) if (preferredApiName != null) - _apiName.postValue(preferredApiName!!) + _apiName.postValue(preferredApiName) } } else { // if the api is found, then set it to it and save key diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt index 5b240693..90e57ef4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -53,9 +53,9 @@ import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult -import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult -import com.lagradost.cloudstream3.utils.AppContextUtils.reduceDragSensitivity +import com.lagradost.cloudstream3.utils.AppUtils.loadResult +import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult +import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar @@ -600,4 +600,8 @@ class LibraryFragment : Fragment() { } } -class MenuSearchView(context: Context) : SearchView(context) \ No newline at end of file +class MenuSearchView(context: Context) : SearchView(context) { + override fun onActionViewCollapsed() { + super.onActionViewCollapsed() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt index 6c602e6c..1bd01c86 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt @@ -23,8 +23,6 @@ enum class ListSorting(@StringRes val stringRes: Int) { UpdatedOld(R.string.sort_updated_old), AlphabeticalA(R.string.sort_alphabetical_a), AlphabeticalZ(R.string.sort_alphabetical_z), - ReleaseDateNew(R.string.sort_release_date_new), - ReleaseDateOld(R.string.sort_release_date_old), } const val LAST_SYNC_API_KEY = "last_sync_api" @@ -134,4 +132,4 @@ class LibraryViewModel : ViewModel() { MainActivity.reloadLibraryEvent -= ::reloadPages super.onCleared() } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt index b2de307f..b8feb656 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt @@ -16,7 +16,7 @@ import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.AutofitRecyclerView import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchResultBuilder -import com.lagradost.cloudstream3.utils.AppContextUtils +import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.UIHelper.toPx import kotlin.math.roundToInt @@ -26,7 +26,7 @@ class PageAdapter( private val resView: AutofitRecyclerView, val clickCallback: (SearchClickCallback) -> Unit ) : - AppContextUtils.DiffAdapter(items) { + AppUtils.DiffAdapter(items) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return LibraryItemViewHolder( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt index 0110187f..cfd22220 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt @@ -15,7 +15,6 @@ import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.BaseAdapter import com.lagradost.cloudstream3.ui.BaseDiffCallback import com.lagradost.cloudstream3.ui.ViewHolderState -import com.lagradost.cloudstream3.ui.home.getSafeParcelable import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV @@ -33,7 +32,7 @@ class ViewpagerAdapterViewHolderState(val binding: LibraryViewpagerPageBinding) } override fun restore(state: Bundle) { - state.getSafeParcelable("pageRecyclerview")?.let { recycle -> + state.getParcelable("pageRecyclerview")?.let { recycle -> binding.pageRecyclerview.layoutManager?.onRestoreInstanceState(recycle) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index ee987f44..cfa6682d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -1,10 +1,7 @@ package com.lagradost.cloudstream3.ui.player import android.annotation.SuppressLint -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter +import android.content.* import android.graphics.drawable.AnimatedImageDrawable import android.graphics.drawable.AnimatedVectorDrawable import android.media.metrics.PlaybackErrorEvent @@ -48,8 +45,8 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.unixTimeMs import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment -import com.lagradost.cloudstream3.utils.AppContextUtils -import com.lagradost.cloudstream3.utils.AppContextUtils.requestLocalAudioFocus +import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.AppUtils.requestLocalAudioFocus import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.UIHelper @@ -220,7 +217,7 @@ abstract class AbstractPlayerFragment( return } player.handleEvent( - CSPlayerEvent.entries[intent.getIntExtra( + CSPlayerEvent.values()[intent.getIntExtra( EXTRA_CONTROL_TYPE, 0 )], source = PlayerEventSource.UI @@ -262,7 +259,7 @@ abstract class AbstractPlayerFragment( private fun requestAudioFocus() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - activity?.requestLocalAudioFocus(AppContextUtils.getFocusRequest()) + activity?.requestLocalAudioFocus(AppUtils.getFocusRequest()) } } @@ -445,9 +442,6 @@ abstract class AbstractPlayerFragment( is VideoEndedEvent -> { context?.let { ctx -> - // Resets subtitle delay on ended video - player.setSubtitleOffset(0) - // Only play next episode if autoplay is on (default) if (PreferenceManager.getDefaultSharedPreferences(ctx) ?.getBoolean( @@ -607,12 +601,12 @@ abstract class AbstractPlayerFragment( } fun nextResize() { - resizeMode = (resizeMode + 1) % PlayerResize.entries.size + resizeMode = (resizeMode + 1) % PlayerResize.values().size resize(resizeMode, true) } fun resize(resize: Int, showToast: Boolean) { - resize(PlayerResize.entries[resize], showToast) + resize(PlayerResize.values()[resize], showToast) } @SuppressLint("UnsafeOptInUsageError") diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 86d67b28..210bfdca 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -9,11 +9,7 @@ import android.os.Looper import android.util.Log import android.util.Rational import android.widget.FrameLayout -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.C.* import androidx.media3.common.Format import androidx.media3.common.MediaItem import androidx.media3.common.MimeTypes @@ -23,10 +19,9 @@ import androidx.media3.common.TrackGroup import androidx.media3.common.TrackSelectionOverride import androidx.media3.common.Tracks import androidx.media3.common.VideoSize -import androidx.media3.common.util.UnstableApi import androidx.media3.database.StandaloneDatabaseProvider import androidx.media3.datasource.DataSource -import androidx.media3.datasource.DefaultDataSource +import androidx.media3.datasource.DefaultDataSourceFactory import androidx.media3.datasource.DefaultHttpDataSource import androidx.media3.datasource.HttpDataSource import androidx.media3.datasource.cache.CacheDataSource @@ -62,15 +57,17 @@ import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle -import com.lagradost.cloudstream3.utils.AppContextUtils.isUsingMobileData +import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.DrmExtractorLink import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList import com.lagradost.cloudstream3.utils.ExtractorLinkType +import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import java.io.File +import java.lang.IllegalArgumentException import java.util.UUID import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLContext @@ -88,7 +85,7 @@ const val toleranceBeforeUs = 300_000L * seek position, in microseconds. Must be non-negative. */ const val toleranceAfterUs = 300_000L -@OptIn(UnstableApi::class) + class CS3IPlayer : IPlayer { private var isPlaying = false private var exoPlayer: ExoPlayer? = null @@ -261,6 +258,7 @@ class CS3IPlayer : IPlayer { private var currentSubtitles: SubtitleData? = null + @SuppressLint("UnsafeOptInUsageError") private fun List.getTrack(id: String?): Pair? { if (id == null) return null // This beast of an expression does: @@ -345,6 +343,7 @@ class CS3IPlayer : IPlayer { }.flatten() } + @SuppressLint("UnsafeOptInUsageError") private fun Tracks.Group.getFormats(): List> { return (0 until this.mediaTrackGroup.length).mapNotNull { i -> if (this.isSupported) @@ -373,6 +372,7 @@ class CS3IPlayer : IPlayer { ) } + @SuppressLint("UnsafeOptInUsageError") override fun getVideoTracks(): CurrentTracks { val allTracks = exoPlayer?.currentTracks?.groups ?: emptyList() val videoTracks = allTracks.filter { it.type == TRACK_TYPE_VIDEO } @@ -392,6 +392,7 @@ class CS3IPlayer : IPlayer { /** * @return True if the player should be reloaded * */ + @SuppressLint("UnsafeOptInUsageError") override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean { Log.i(TAG, "setPreferredSubtitles init $subtitle") currentSubtitles = subtitle @@ -451,7 +452,7 @@ class CS3IPlayer : IPlayer { } ?: false } - private var currentSubtitleOffset: Long = 0 + var currentSubtitleOffset: Long = 0 override fun setSubtitleOffset(offset: Long) { currentSubtitleOffset = offset @@ -459,7 +460,7 @@ class CS3IPlayer : IPlayer { } override fun getSubtitleOffset(): Long { - return currentSubtitleOffset + return currentSubtitleOffset //currentTextRenderer?.getRenderOffsetMs() ?: currentSubtitleOffset } override fun getCurrentPreferredSubtitle(): SubtitleData? { @@ -470,6 +471,7 @@ class CS3IPlayer : IPlayer { } } + @SuppressLint("UnsafeOptInUsageError") override fun getAspectRatio(): Rational? { return exoPlayer?.videoFormat?.let { format -> Rational(format.width, format.height) @@ -480,13 +482,14 @@ class CS3IPlayer : IPlayer { subtitleHelper.setSubStyle(style) } + @SuppressLint("UnsafeOptInUsageError") override fun saveData() { Log.i(TAG, "saveData") updatedTime() exoPlayer?.let { exo -> playbackPosition = exo.currentPosition - currentWindow = exo.currentMediaItemIndex + currentWindow = exo.currentWindowIndex isPlaying = exo.isPlaying } } @@ -498,7 +501,7 @@ class CS3IPlayer : IPlayer { updatedTime() exoPlayer?.apply { - playWhenReady = false + setPlayWhenReady(false) stop() release() } @@ -561,6 +564,7 @@ class CS3IPlayer : IPlayer { var requestSubtitleUpdate: (() -> Unit)? = null + @SuppressLint("UnsafeOptInUsageError") private fun createOnlineSource(headers: Map): HttpDataSource.Factory { val source = OkHttpDataSource.Factory(app.baseClient).setUserAgent(USER_AGENT) return source.apply { @@ -568,6 +572,7 @@ class CS3IPlayer : IPlayer { } } + @SuppressLint("UnsafeOptInUsageError") private fun createOnlineSource(link: ExtractorLink): HttpDataSource.Factory { val provider = getApiFromNameNull(link.source) val interceptor = provider?.getVideoInterceptor(link) @@ -600,10 +605,53 @@ class CS3IPlayer : IPlayer { } } + @SuppressLint("UnsafeOptInUsageError") private fun Context.createOfflineSource(): DataSource.Factory { - return DefaultDataSource.Factory(this, DefaultHttpDataSource.Factory().setUserAgent(USER_AGENT)) + return DefaultDataSourceFactory(this, USER_AGENT) } + /*private fun getSubSources( + onlineSourceFactory: DataSource.Factory?, + offlineSourceFactory: DataSource.Factory?, + subHelper: PlayerSubtitleHelper, + ): Pair, List> { + val activeSubtitles = ArrayList() + val subSources = subHelper.getAllSubtitles().mapNotNull { sub -> + val subConfig = MediaItem.SubtitleConfiguration.Builder(Uri.parse(sub.url)) + .setMimeType(sub.mimeType) + .setLanguage("_${sub.name}") + .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) + .build() + when (sub.origin) { + SubtitleOrigin.DOWNLOADED_FILE -> { + if (offlineSourceFactory != null) { + activeSubtitles.add(sub) + SingleSampleMediaSource.Factory(offlineSourceFactory) + .createMediaSource(subConfig, C.TIME_UNSET) + } else { + null + } + } + SubtitleOrigin.URL -> { + if (onlineSourceFactory != null) { + activeSubtitles.add(sub) + SingleSampleMediaSource.Factory(onlineSourceFactory) + .createMediaSource(subConfig, C.TIME_UNSET) + } else { + null + } + } + SubtitleOrigin.OPEN_SUBTITLES -> { + // TODO + throw NotImplementedError() + } + } + } + println("SUBSRC: ${subSources.size} activeSubtitles : ${activeSubtitles.size} of ${subHelper.getAllSubtitles().size} ") + return Pair(subSources, activeSubtitles) + }*/ + + @SuppressLint("UnsafeOptInUsageError") private fun getCache(context: Context, cacheSize: Long): SimpleCache? { return try { val databaseProvider = StandaloneDatabaseProvider(context) @@ -635,6 +683,7 @@ class CS3IPlayer : IPlayer { return getMediaItemBuilder(mimeType).setUri(url).build() } + @SuppressLint("UnsafeOptInUsageError") private fun getTrackSelector(context: Context, maxVideoHeight: Int?): TrackSelector { val trackSelector = DefaultTrackSelector(context) trackSelector.parameters = trackSelector.buildUponParameters() @@ -648,6 +697,7 @@ class CS3IPlayer : IPlayer { var currentTextRenderer: CustomTextRenderer? = null + @SuppressLint("UnsafeOptInUsageError") private fun buildExoPlayer( context: Context, mediaItemSlices: List, @@ -687,7 +737,7 @@ class CS3IPlayer : IPlayer { textRendererOutput, eventHandler.looper, CustomSubtitleDecoderFactory() - ).also { renderer -> this.currentTextRenderer = renderer } + ).also { this.currentTextRenderer = it } currentTextRenderer } else it }.toTypedArray() @@ -863,11 +913,7 @@ class CS3IPlayer : IPlayer { } CSPlayerEvent.SeekForward -> seekTime(seekActionTime, source) - CSPlayerEvent.SeekBack -> seekTime(-seekActionTime, source) - - CSPlayerEvent.Restart -> seekTo(0, source) - CSPlayerEvent.NextEpisode -> event( EpisodeSeekEvent( offset = 1, @@ -984,7 +1030,7 @@ class CS3IPlayer : IPlayer { } } - //fixme: Use onPlaybackStateChanged(int) and onPlayWhenReadyChanged(boolean, int) instead. + @SuppressLint("UnsafeOptInUsageError") override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { exoPlayer?.let { exo -> event( @@ -1072,9 +1118,6 @@ class CS3IPlayer : IPlayer { } Player.STATE_ENDED -> { - // Resets subtitle delay on ended video - setSubtitleOffset(0) - // Only play next episode if autoplay is on (default) if (PreferenceManager.getDefaultSharedPreferences(context) ?.getBoolean( @@ -1120,6 +1163,7 @@ class CS3IPlayer : IPlayer { private var lastTimeStamps: List = emptyList() + @SuppressLint("UnsafeOptInUsageError") override fun addTimeStamps(timeStamps: List) { lastTimeStamps = timeStamps timeStamps.forEach { timestamp -> @@ -1137,6 +1181,7 @@ class CS3IPlayer : IPlayer { updatedTime(source = PlayerEventSource.Player) } + @SuppressLint("UnsafeOptInUsageError") fun onRenderFirst() { if (hasUsedFirstRender) { // this insures that we only call this once per player load return @@ -1203,6 +1248,7 @@ class CS3IPlayer : IPlayer { } } + @SuppressLint("UnsafeOptInUsageError") private fun getSubSources( onlineSourceFactory: HttpDataSource.Factory?, offlineSourceFactory: DataSource.Factory?, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt index 07ce413e..20d093a6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt @@ -2,7 +2,6 @@ package com.lagradost.cloudstream3.ui.player import android.content.Context import android.util.Log -import androidx.annotation.OptIn import androidx.preference.PreferenceManager import androidx.media3.common.Format import androidx.media3.common.MimeTypes @@ -32,7 +31,7 @@ import java.nio.charset.Charset * @param fallbackFormat used to create a decoder based on mimetype if the subtitle string is not * enough to identify the subtitle format. **/ -@OptIn(UnstableApi::class) +@UnstableApi class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder { companion object { fun updateForcedEncoding(context: Context) { @@ -73,7 +72,7 @@ class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder { RegexOption.IGNORE_CASE ), ) - val captionRegex = listOf(Regex("""(-\s?|)[\[({][\w\s]*?[])}]\s*""")) + val captionRegex = listOf(Regex("""(-\s?|)[\[({][\w\d\s]*?[])}]\s*""")) //https://emptycharacter.com/ //https://www.fileformat.info/info/unicode/char/200b/index.htm @@ -263,7 +262,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 */ -@OptIn(UnstableApi::class) +@UnstableApi class CustomSubtitleDecoderFactory : SubtitleDecoderFactory { override fun supportsFormat(format: Format): Boolean { // return SubtitleDecoderFactory.DEFAULT.supportsFormat(format) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt index f2b863fb..d6b0735d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt @@ -1,12 +1,11 @@ package com.lagradost.cloudstream3.ui.player import android.os.Looper -import androidx.annotation.OptIn import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.text.SubtitleDecoderFactory import androidx.media3.exoplayer.text.TextOutput -@OptIn(UnstableApi::class) +@UnstableApi class CustomTextRenderer( offset: Long, output: TextOutput?, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt index c7db7d04..5585924e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt @@ -1,15 +1,11 @@ package com.lagradost.cloudstream3.ui.player -import android.net.Uri import com.lagradost.cloudstream3.AcraApplication.Companion.context -import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.SubtitleUtils.cleanDisplayName -import com.lagradost.cloudstream3.utils.SubtitleUtils.isMatchingSubtitle -import com.lagradost.cloudstream3.utils.VideoDownloadManager.getDownloadFileInfoAndUpdateSettings -import com.lagradost.cloudstream3.utils.VideoDownloadManager.getFolder +import com.lagradost.cloudstream3.utils.ExtractorUri +import com.lagradost.cloudstream3.utils.VideoDownloadManager import kotlin.math.max import kotlin.math.min @@ -18,7 +14,6 @@ class DownloadFileGenerator( private var currentIndex: Int = 0 ) : IGenerator { override val hasCache = false - override val canSkipLoading = false override fun hasNext(): Boolean { return currentIndex < episodes.size - 1 @@ -55,6 +50,10 @@ class DownloadFileGenerator( return null } + fun cleanDisplayName(name: String): String { + return name.substringBeforeLast('.').trim() + } + override suspend fun generateLinks( clearCache: Boolean, type: LoadType, @@ -63,21 +62,7 @@ class DownloadFileGenerator( offset: Int ): Boolean { val meta = episodes[currentIndex + offset] - - if (meta.uri == Uri.EMPTY) { - // We do this here so that we only load it when - // we actually need it as it can be more expensive. - val info = meta.id?.let { id -> - activity?.let { act -> - getDownloadFileInfoAndUpdateSettings(act, id) - } - } - - if (info != null) { - val newMeta = meta.copy(uri = info.path) - callback(null to newMeta) - } else callback(null to meta) - } else callback(null to meta) + callback(null to meta) val ctx = context ?: return true val relative = meta.relativePath ?: return true @@ -85,9 +70,28 @@ class DownloadFileGenerator( val cleanDisplay = cleanDisplayName(display) - getFolder(ctx, relative, meta.basePath)?.forEach { (name, uri) -> - if (isMatchingSubtitle(name, display, cleanDisplay)) { + VideoDownloadManager.getFolder(ctx, relative, meta.basePath) + ?.forEach { (name, uri) -> + // only these files are allowed, so no videos as subtitles + if (listOf( + ".vtt", + ".srt", + ".txt", + ".ass", + ".ttml", + ".sbv", + ".dfxp" + ).none { name.contains(it, true) } + ) return@forEach + + // cant have the exact same file as a subtitle + if (name.equals(display, true)) return@forEach + val cleanName = cleanDisplayName(name) + + // we only want files with the approx same name + if (!cleanName.startsWith(cleanDisplay, true)) return@forEach + val realName = cleanName.removePrefix(cleanDisplay) subtitleCallback( @@ -101,7 +105,6 @@ class DownloadFileGenerator( ) ) } - } return true } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt index c38160c2..1e2ea540 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt @@ -1,21 +1,23 @@ package com.lagradost.cloudstream3.ui.player +import android.content.ContentUris import android.content.Intent +import android.net.Uri import android.os.Bundle import android.util.Log import android.view.KeyEvent +import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.mvvm.normalSafeApiCall -import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playLink -import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playUri -import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback +import com.lagradost.cloudstream3.utils.ExtractorUri +import com.lagradost.cloudstream3.utils.UIHelper.navigate +import com.lagradost.safefile.SafeFile + +const val DTAG = "PlayerActivity" class DownloadedPlayerActivity : AppCompatActivity() { - private val dTAG = "DownloadedPlayerAct" - - override fun dispatchKeyEvent(event: KeyEvent): Boolean { + override fun dispatchKeyEvent(event: KeyEvent?): Boolean { CommonActivity.dispatchKeyEvent(this, event)?.let { return it } @@ -33,18 +35,53 @@ class DownloadedPlayerActivity : AppCompatActivity() { CommonActivity.onUserLeaveHint(this) } + private fun playLink(url: String) { + this.navigate( + R.id.global_to_navigation_player, GeneratorPlayer.newInstance( + LinkGenerator( + listOf( + BasicLink(url) + ) + ) + ) + ) + } + + private fun playUri(uri: Uri) { + val name = SafeFile.fromUri(this, uri)?.name() + this.navigate( + R.id.global_to_navigation_player, GeneratorPlayer.newInstance( + DownloadFileGenerator( + listOf( + ExtractorUri( + uri = uri, + name = name ?: getString(R.string.downloaded_file), + // well not the same as a normal id, but we take it as users may want to + // play downloaded files and save the location + id = kotlin.runCatching { ContentUris.parseId(uri) }.getOrNull()?.hashCode() + ) + ) + ) + ) + ) + } + override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) + Log.i(DTAG, "onCreate") + CommonActivity.loadThemes(this) + super.onCreate(savedInstanceState) CommonActivity.init(this) + setContentView(R.layout.empty_layout) - Log.i(dTAG, "onCreate") val data = intent.data if (intent?.action == Intent.ACTION_SEND) { - val extraText = normalSafeApiCall { // I dont trust android + val extraText = try { // I dont trust android intent.getStringExtra(Intent.EXTRA_TEXT) + } catch (e: Exception) { + null } val cd = intent.clipData val item = if (cd != null && cd.itemCount > 0) cd.getItemAt(0) else null @@ -52,25 +89,32 @@ class DownloadedPlayerActivity : AppCompatActivity() { // idk what I am doing, just hope any of these work if (item?.uri != null) - playUri(this, item.uri) + playUri(item.uri) else if (url != null) - playLink(this, url) + playLink(url) else if (data != null) - playUri(this, data) + playUri(data) else if (extraText != null) - playLink(this, extraText) + playLink(extraText) else { finish() return } } else if (data?.scheme == "content") { - playUri(this, data) + playUri(data) } else { finish() return } - attachBackPressedCallback { finish() } + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + finish() + } + } + ) } override fun onResume() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt index ec485f1c..d8d2d537 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt @@ -1,13 +1,13 @@ package com.lagradost.cloudstream3.ui.player import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.ExtractorUri class ExtractorLinkGenerator( private val links: List, private val subtitles: List, ) : IGenerator { override val hasCache = false - override val canSkipLoading = true override fun getCurrentId(): Int? { return null diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt index b2e80749..d79c44b7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt @@ -25,25 +25,19 @@ import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHO import android.view.animation.AlphaAnimation import android.view.animation.Animation import android.view.animation.AnimationUtils -import androidx.annotation.OptIn -import android.widget.LinearLayout import androidx.appcompat.app.AlertDialog import androidx.core.graphics.blue import androidx.core.graphics.green import androidx.core.graphics.red -import androidx.core.view.children import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged -import androidx.media3.common.util.UnstableApi import androidx.preference.PreferenceManager -import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.CommonActivity.keyEventListener import com.lagradost.cloudstream3.CommonActivity.playerEventListener import com.lagradost.cloudstream3.CommonActivity.screenHeight import com.lagradost.cloudstream3.CommonActivity.screenWidth -import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding import com.lagradost.cloudstream3.databinding.SubtitleOffsetBinding @@ -52,10 +46,12 @@ import com.lagradost.cloudstream3.ui.player.GeneratorPlayer.Companion.subsProvid import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.result.txt +import com.lagradost.cloudstream3.ui.settings.Globals import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppContextUtils.isUsingMobileData +import com.lagradost.cloudstream3.ui.settings.SettingsFragment +import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute @@ -68,11 +64,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.showSystemUI import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.UserPreferenceDelegate import com.lagradost.cloudstream3.utils.Vector2 -import kotlin.math.abs -import kotlin.math.ceil -import kotlin.math.max -import kotlin.math.min -import kotlin.math.round +import kotlin.math.* const val MINIMUM_SEEK_TIME = 7000L // when swipe seeking const val MINIMUM_VERTICAL_SWIPE = 2.0f // in percentage @@ -91,8 +83,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { protected open var isFullScreenPlayer = true protected var playerBinding: PlayerCustomLayoutBinding? = null - private var durationMode: Boolean by UserPreferenceDelegate("duration_mode", false) - + private var durationMode : Boolean by UserPreferenceDelegate("duration_mode", false) // state of player UI protected var isShowing = false protected var isLocked = false @@ -124,7 +115,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() { protected var doubleTapPauseEnabled = true protected var playerRotateEnabled = false protected var autoPlayerRotateEnabled = false - private var hideControlsNames = false protected var subtitleDelay set(value) = try { @@ -194,7 +184,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { open fun openOnlineSubPicker( context: Context, - loadResponse: LoadResponse?, + imdbId: Long?, dismissCallback: (() -> Unit) ) { throw NotImplementedError() @@ -246,7 +236,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() { fadeAnimation.duration = 100 fadeAnimation.fillAfter = true - @OptIn(UnstableApi::class) val sView = subView val sStyle = subStyle if (sView != null && sStyle != null) { @@ -260,6 +249,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { val playerSourceMove = if (isShowing) 0f else -50.toPx.toFloat() + playerBinding?.apply { playerOpenSource.let { ObjectAnimator.ofFloat(it, "translationY", playerSourceMove).apply { @@ -300,42 +290,44 @@ open class FullScreenPlayer : AbstractPlayerFragment() { player.getCurrentPreferredSubtitle() == null } - private fun restoreOrientationWithSensor(activity: Activity) { + private fun restoreOrientationWithSensor(activity: Activity){ val currentOrientation = activity.resources.configuration.orientation - val orientation = when (currentOrientation) { + var orientation = 0 + when (currentOrientation) { Configuration.ORIENTATION_LANDSCAPE -> - ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + + Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED -> + orientation = dynamicOrientation() Configuration.ORIENTATION_PORTRAIT -> - ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT - - else -> dynamicOrientation() + orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT } activity.requestedOrientation = orientation } - private fun toggleOrientationWithSensor(activity: Activity) { + private fun toggleOrientationWithSensor(activity: Activity){ val currentOrientation = activity.resources.configuration.orientation - val orientation: Int = when (currentOrientation) { + var orientation = 0 + when (currentOrientation) { Configuration.ORIENTATION_LANDSCAPE -> - ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT + orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT + + Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED -> + orientation = dynamicOrientation() Configuration.ORIENTATION_PORTRAIT -> - ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE - - else -> dynamicOrientation() + orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE } activity.requestedOrientation = orientation } open fun lockOrientation(activity: Activity) { - @Suppress("DEPRECATION") - val display = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) + val display = (activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay - else activity.display!! val rotation = display.rotation val currentOrientation = activity.resources.configuration.orientation - val orientation: Int + var orientation = 0 when (currentOrientation) { Configuration.ORIENTATION_LANDSCAPE -> orientation = @@ -344,25 +336,27 @@ open class FullScreenPlayer : AbstractPlayerFragment() { else ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE + Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED -> + orientation = dynamicOrientation() + Configuration.ORIENTATION_PORTRAIT -> orientation = if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_270) ActivityInfo.SCREEN_ORIENTATION_PORTRAIT else ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT - - else -> orientation = dynamicOrientation() } activity.requestedOrientation = orientation } private fun updateOrientation(ignoreDynamicOrientation: Boolean = false) { activity?.apply { - if (lockRotation) { - if (isLocked) { + if(lockRotation) { + if(isLocked) { lockOrientation(this) - } else { - if (ignoreDynamicOrientation) { + } + else { + if(ignoreDynamicOrientation){ // restore when lock is disabled restoreOrientationWithSensor(this) } else { @@ -504,11 +498,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() { dialog.dismissSafe(activity) player.seekTime(1L) } - resetBtt.setOnClickListener { - subtitleDelay = 0 - dialog.dismissSafe(activity) - player.seekTime(1L) - } cancelBtt.setOnClickListener { subtitleDelay = beforeOffset dialog.dismissSafe(activity) @@ -740,15 +729,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private var currentTapIndex = 0 protected fun autoHide() { currentTapIndex++ - delayHide() - } - - override fun playerStatusChanged() { - super.playerStatusChanged() - delayHide() - } - - private fun delayHide() { val index = currentTapIndex playerBinding?.playerHolder?.postDelayed({ if (!isCurrentTouchValid && isShowing && index == currentTapIndex && player.getIsPlaying()) { @@ -970,10 +950,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } else -> { - player.handleEvent( - CSPlayerEvent.PlayPauseToggle, - PlayerEventSource.UI - ) + player.handleEvent(CSPlayerEvent.PlayPauseToggle, PlayerEventSource.UI) } } } else if (doubleTapEnabled && isFullScreenPlayer) { @@ -1166,7 +1143,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() { return true } - @SuppressLint("GestureBackNavigation") private fun handleKeyEvent(event: KeyEvent, hasNavigated: Boolean): Boolean { if (hasNavigated) { autoHide() @@ -1183,7 +1159,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } } - KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_DPAD_UP -> { if (!isShowing) { onClickChange() @@ -1250,7 +1225,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() { // if nothing has loaded these buttons should not be visible playerBinding?.apply { playerSkipEpisode.isVisible = false - playerGoForward.isVisible = false playerTracksBtt.isVisible = false playerSkipOp.isVisible = false shadowOverlay.isVisible = false @@ -1324,10 +1298,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() { player.handleEvent(CSPlayerEvent.SeekBack) } - PlayerEventType.Restart -> { - player.handleEvent(CSPlayerEvent.Restart) - } - PlayerEventType.ToggleMute -> { player.handleEvent(CSPlayerEvent.ToggleMute) } @@ -1423,8 +1393,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() { false ) - hideControlsNames = settingsManager.getBoolean(ctx.getString(R.string.hide_player_control_names_key), false) - val profiles = QualityDataHelper.getProfiles() val type = if (ctx.isUsingMobileData()) QualityDataHelper.QualityProfileType.Data @@ -1445,34 +1413,12 @@ open class FullScreenPlayer : AbstractPlayerFragment() { playerSpeedBtt.isVisible = playBackSpeedEnabled playerResizeBtt.isVisible = playerResizeEnabled playerRotateBtt.isVisible = playerRotateEnabled - if (hideControlsNames) { - hideControlsNames() - } } } catch (e: Exception) { logError(e) } playerBinding?.apply { - - if (isLayout(TV or EMULATOR)) { - mapOf( - playerGoBack to playerGoBackText, - playerRestart to playerRestartText, - playerGoForward to playerGoForwardText - ).forEach { (button, text) -> - button.setOnFocusChangeListener { _, hasFocus -> - if (!hasFocus) { - text.isSelected = false - text.isVisible = false - return@setOnFocusChangeListener - } - text.isSelected = true - text.isVisible = true - } - } - } - playerPausePlay.setOnClickListener { autoHide() player.handleEvent(CSPlayerEvent.PlayPauseToggle) @@ -1516,16 +1462,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() { player.handleEvent(CSPlayerEvent.NextEpisode) } - playerGoForward.setOnClickListener { - autoHide() - player.handleEvent(CSPlayerEvent.NextEpisode) - } - - playerRestart.setOnClickListener { - autoHide() - player.handleEvent(CSPlayerEvent.Restart) - } - playerLock.setOnClickListener { autoHide() toggleLock() @@ -1581,7 +1517,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } } // cs3 is peak media center - setRemainingTimeCounter(durationMode || isLayout(TV)) + setRemainingTimeCounter(durationMode || Globals.isLayout(Globals.TV)) playerBinding?.exoPosition?.doOnTextChanged { _, _, _, _ -> updateRemainingTime() } @@ -1600,22 +1536,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } } - private fun PlayerCustomLayoutBinding.hideControlsNames() { - fun iterate(layout: LinearLayout) { - layout.children.forEach { - if (it is MaterialButton) { - it.textSize = 0f - it.iconPadding = 0 - it.iconGravity = MaterialButton.ICON_GRAVITY_TEXT_START - it.setPadding(0,0,0,0) - } else if (it is LinearLayout) { - iterate(it) - } - } - } - iterate(playerLockHolder.parent as LinearLayout) - } - override fun playerDimensionsLoaded(width: Int, height: Int) { isVerticalOrientation = height > width updateOrientation() @@ -1635,7 +1555,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private fun setRemainingTimeCounter(showRemaining: Boolean) { durationMode = showRemaining - playerBinding?.exoDuration?.isInvisible = showRemaining + playerBinding?.exoDuration?.isInvisible= showRemaining playerBinding?.timeLeft?.isVisible = showRemaining } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index d4fd047c..7ff56886 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -6,7 +6,6 @@ import android.app.Dialog import android.content.Context import android.content.Intent import android.content.res.ColorStateList -import android.os.Build import android.os.Bundle import android.util.Log import android.view.LayoutInflater @@ -14,7 +13,6 @@ import android.view.View import android.view.ViewGroup import android.widget.* import androidx.activity.result.contract.ActivityResultContracts -import androidx.annotation.OptIn import androidx.core.animation.addListener import androidx.core.content.ContextCompat import androidx.core.view.isGone @@ -23,15 +21,10 @@ import androidx.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager import androidx.media3.common.Format.NO_VALUE import androidx.media3.common.MimeTypes -import androidx.media3.common.util.UnstableApi import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.showToast -import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId -import com.lagradost.cloudstream3.LoadResponse.Companion.getImdbId -import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId -import com.lagradost.cloudstream3.LoadResponse.Companion.getTMDbId import com.lagradost.cloudstream3.databinding.DialogOnlineSubtitlesBinding import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding import com.lagradost.cloudstream3.databinding.PlayerSelectSourceAndSubsBinding @@ -46,14 +39,13 @@ import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSub import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog import com.lagradost.cloudstream3.ui.result.* +import com.lagradost.cloudstream3.ui.settings.Globals import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR -import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.subtitles.SUBTITLE_AUTO_SELECT_KEY import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1 import com.lagradost.cloudstream3.utils.* -import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog @@ -66,7 +58,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.safefile.SafeFile import kotlinx.coroutines.Job -import java.io.Serializable import java.util.* import kotlin.math.abs @@ -163,7 +154,6 @@ class GeneratorPlayer : FullScreenPlayer() { } override fun playerStatusChanged() { - super.playerStatusChanged() if (player.getIsPlaying()) { viewModel.forceClearCache = false } @@ -238,7 +228,7 @@ class GeneratorPlayer : FullScreenPlayer() { private fun closestQuality(target: Int?): Qualities { if (target == null) return Qualities.Unknown - return Qualities.entries.minBy { abs(it.value - target) } + return Qualities.values().minBy { abs(it.value - target) } } private fun getLinkPriority( @@ -268,7 +258,6 @@ class GeneratorPlayer : FullScreenPlayer() { var episode: Int? = null, var season: Int? = null, var name: String? = null, - var imdbId: String? = null, ) private fun getMetaData(): TempMetaData { @@ -295,7 +284,7 @@ class GeneratorPlayer : FullScreenPlayer() { } override fun openOnlineSubPicker( - context: Context, loadResponse: LoadResponse?, dismissCallback: (() -> Unit) + context: Context, imdbId: Long?, dismissCallback: (() -> Unit) ) { val providers = subsProviders val isSingleProvider = subsProviders.size == 1 @@ -371,6 +360,8 @@ class GeneratorPlayer : FullScreenPlayer() { binding.subtitleAdapter.choiceMode = AbsListView.CHOICE_MODE_SINGLE binding.subtitleAdapter.adapter = arrayAdapter + val adapter = + binding.subtitleAdapter.adapter as? ArrayAdapter binding.subtitleAdapter.setOnItemClickListener { _, _, position, _ -> currentSubtitle = currentSubtitles.getOrNull(position) ?: return@setOnItemClickListener @@ -381,12 +372,11 @@ class GeneratorPlayer : FullScreenPlayer() { fun setSubtitlesList(list: List) { currentSubtitles = list - arrayAdapter.clear() - arrayAdapter.addAll(currentSubtitles) + adapter?.clear() + adapter?.addAll(currentSubtitles) } val currentTempMeta = getMetaData() - // bruh idk why it is not correct val color = ColorStateList.valueOf(context.colorFromAttribute(R.attr.colorAccent)) binding.searchLoadingBar.progressTintList = color @@ -434,10 +424,7 @@ class GeneratorPlayer : FullScreenPlayer() { val search = AbstractSubtitleEntities.SubtitleSearch( query = query ?: return@ioSafe, - imdbId = loadResponse?.getImdbId(), - tmdbId = loadResponse?.getTMDbId()?.toInt(), - malId = loadResponse?.getMalId()?.toInt(), - aniListId = loadResponse?.getAniListId()?.toInt(), + imdb = imdbId, epNumber = currentTempMeta.episode, seasonNumber = currentTempMeta.season, lang = currentLanguageTwoLetters.ifBlank { null }, @@ -524,7 +511,7 @@ class GeneratorPlayer : FullScreenPlayer() { //TODO: Set year text from currently loaded movie on Player //dialog.subtitles_search_year?.setText(currentTempMeta.year) } - @OptIn(UnstableApi::class) + private fun openSubPicker() { try { subsPathPicker.launch( @@ -646,8 +633,6 @@ class GeneratorPlayer : FullScreenPlayer() { } if (subsProvidersIsActive) { - val currentLoadResponse = viewModel.getLoadResponse() - val loadFromOpenSubsFooter: TextView = layoutInflater.inflate( R.layout.sort_bottom_footer_add_choice, null ) as TextView @@ -658,7 +643,7 @@ class GeneratorPlayer : FullScreenPlayer() { loadFromOpenSubsFooter.setOnClickListener { shouldDismiss = false sourceDialog.dismissSafe(activity) - openOnlineSubPicker(it.context, currentLoadResponse) { + openOnlineSubPicker(it.context, null) { dismiss() } } @@ -797,6 +782,7 @@ class GeneratorPlayer : FullScreenPlayer() { settingsManager.edit().putString( ctx.getString(R.string.subtitles_encoding_key), prefValues[it] ).apply() + updateForcedEncoding(ctx) dismiss() player.seekTime(-1) // to update subtitles, a dirty trick @@ -1099,15 +1085,8 @@ class GeneratorPlayer : FullScreenPlayer() { } playerBinding?.playerSkipOp?.isVisible = isOpVisible - - when { - isLayout(PHONE) -> - playerBinding?.playerSkipEpisode?.isVisible = - !isOpVisible && viewModel.hasNextEpisode() == true - - else -> - playerBinding?.playerGoForward?.isVisible = viewModel.hasNextEpisode() == true - } + playerBinding?.playerSkipEpisode?.isVisible = + !isOpVisible && viewModel.hasNextEpisode() == true if (percentage >= PRELOAD_NEXT_EPISODE_PERCENTAGE) { viewModel.preLoadNextLinks() @@ -1263,7 +1242,7 @@ class GeneratorPlayer : FullScreenPlayer() { fun setPlayerDimen(widthHeight: Pair?) { val extra = if (widthHeight != null) { val (width, height) = widthHeight - "- ${width}x${height}" + "${width}x${height}" } else { "" } @@ -1274,7 +1253,7 @@ class GeneratorPlayer : FullScreenPlayer() { 0 -> "" 1 -> extra 2 -> source - 3 -> "$source $extra" + 3 -> "$source - $extra" else -> "" } playerBinding?.playerVideoTitleRez?.apply { @@ -1291,7 +1270,7 @@ class GeneratorPlayer : FullScreenPlayer() { private fun unwrapBundle(savedInstanceState: Bundle?) { Log.i(TAG, "unwrapBundle = $savedInstanceState") savedInstanceState?.let { bundle -> - sync.addSyncs(bundle.getSafeSerializable>("syncData")) + sync.addSyncs(bundle.getSerializable("syncData") as? HashMap?) } } @@ -1299,8 +1278,7 @@ class GeneratorPlayer : FullScreenPlayer() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // this is used instead of layout-television to follow the settings and some TV devices are not classified as TV for some reason - layout = - if (isLayout(TV or EMULATOR)) R.layout.fragment_player_tv else R.layout.fragment_player + layout = if (isLayout(TV or EMULATOR)) R.layout.fragment_player_tv else R.layout.fragment_player viewModel = ViewModelProvider(this)[PlayerGeneratorViewModel::class.java] sync = ViewModelProvider(this)[SyncViewModel::class.java] @@ -1462,7 +1440,7 @@ class GeneratorPlayer : FullScreenPlayer() { observe(viewModel.currentLinks) { currentLinks = it - val turnVisible = it.isNotEmpty() && lastUsedGenerator?.canSkipLoading == true + val turnVisible = it.isNotEmpty() val wasGone = binding?.overlayLoadingSkipButton?.isGone == true binding?.overlayLoadingSkipButton?.isVisible = turnVisible @@ -1508,6 +1486,3 @@ class GeneratorPlayer : FullScreenPlayer() { } } } - -@Suppress("DEPRECATION") -inline fun Bundle.getSafeSerializable(key: String) : T? = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) getSerializable(key) as? T else getSerializable(key, T::class.java) \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt index 6b8e6ea8..af74cb57 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt @@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.ui.player import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkType +import com.lagradost.cloudstream3.utils.ExtractorUri enum class LoadType { Unknown, @@ -9,8 +10,7 @@ enum class LoadType { InAppDownload, ExternalApp, Browser, - Chromecast, - Fcast + Chromecast } fun LoadType.toSet() : Set { @@ -29,23 +29,17 @@ fun LoadType.toSet() : Set { ExtractorLinkType.VIDEO, ExtractorLinkType.M3U8 ) - LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.entries.toSet() + LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.values().toSet() LoadType.Chromecast -> setOf( ExtractorLinkType.VIDEO, ExtractorLinkType.DASH, ExtractorLinkType.M3U8 ) - LoadType.Fcast -> setOf( - ExtractorLinkType.VIDEO, - ExtractorLinkType.DASH, - ExtractorLinkType.M3U8 - ) } } interface IGenerator { val hasCache: Boolean - val canSkipLoading: Boolean fun hasNext(): Boolean fun hasPrev(): Boolean diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt index 89c6f73b..0e54e2cb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt @@ -6,8 +6,10 @@ import android.util.Rational import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.ExtractorUri enum class PlayerEventType(val value: Int) { + //Stop(-1), Pause(0), Play(1), SeekForward(2), @@ -25,7 +27,6 @@ enum class PlayerEventType(val value: Int) { Resize(13), SearchSubtitlesOnline(14), SkipOp(15), - Restart(16), } enum class CSPlayerEvent(val value: Int) { @@ -39,7 +40,6 @@ enum class CSPlayerEvent(val value: Int) { PrevEpisode(6), PlayPauseToggle(7), ToggleMute(8), - Restart(9), } enum class CSPlayerLoading { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt index 20feae41..ca2d9c81 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt @@ -1,29 +1,9 @@ package com.lagradost.cloudstream3.ui.player -import android.net.Uri -import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.amap -import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.INFER_TYPE -import com.lagradost.cloudstream3.utils.Qualities -import com.lagradost.cloudstream3.utils.loadExtractor -import com.lagradost.cloudstream3.utils.unshortenLinkSafe - -data class ExtractorUri( - val uri: Uri, - val name: String, - - val basePath: String? = null, - val relativePath: String? = null, - val displayName: String? = null, - - val id: Int? = null, - val parentId: Int? = null, - val episode: Int? = null, - val season: Int? = null, - val headerName: String? = null, - val tvType: TvType? = null, -) +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall +import com.lagradost.cloudstream3.utils.* +import java.net.URI /** * Used to open the player more easily with the LinkGenerator @@ -39,7 +19,6 @@ class LinkGenerator( private val isM3u8: Boolean? = null ) : IGenerator { override val hasCache = false - override val canSkipLoading = true override fun getCurrentId(): Int? { return null diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java b/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java index 232440cc..3482f21c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java @@ -29,7 +29,6 @@ import android.os.Message; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.OptIn; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.text.Cue; @@ -67,7 +66,7 @@ import java.util.stream.Collectors; * obtained from a {@link SubtitleDecoderFactory}. The actual rendering of the subtitle {@link Cue}s * is delegated to a {@link TextOutput}. */ -@OptIn(markerClass = UnstableApi.class) +@UnstableApi public class NonFinalTextRenderer extends BaseRenderer implements Callback { private static final String TAG = "TextRenderer"; @@ -75,7 +74,7 @@ public class NonFinalTextRenderer extends BaseRenderer implements Callback { /** * @param trackType The track type that the renderer handles. One of the {@link C} {@code * TRACK_TYPE_*} constants. - * @param outputHandler todo description + * @param outputHandler */ public NonFinalTextRenderer(int trackType, @Nullable Handler outputHandler) { super(trackType); @@ -417,11 +416,13 @@ public class NonFinalTextRenderer extends BaseRenderer implements Callback { @SuppressWarnings("unchecked") @Override public boolean handleMessage(Message msg) { - if (msg.what == MSG_UPDATE_OUTPUT) { - invokeUpdateOutputInternal((List) msg.obj); - return true; + switch (msg.what) { + case MSG_UPDATE_OUTPUT: + invokeUpdateOutputInternal((List) msg.obj); + return true; + default: + throw new IllegalStateException(); } - throw new IllegalStateException(); } private void invokeUpdateOutputInternal(List cues) { @@ -440,6 +441,7 @@ public class NonFinalTextRenderer extends BaseRenderer implements Callback { } ).collect(Collectors.toList()); + output.onCues(fixedCues); output.onCues(new CueGroup(fixedCues, 0L)); } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt deleted file mode 100644 index f00f8a61..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.lagradost.cloudstream3.ui.player - -import android.app.Activity -import android.content.ContentUris -import android.net.Uri -import androidx.core.content.ContextCompat.getString -import androidx.media3.common.util.UnstableApi -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.utils.UIHelper.navigate -import com.lagradost.safefile.SafeFile - -object OfflinePlaybackHelper { - fun playLink(activity: Activity, url: String) { - activity.navigate( - R.id.global_to_navigation_player, GeneratorPlayer.newInstance( - LinkGenerator( - listOf( - BasicLink(url) - ) - ) - ) - ) - } - - fun playUri(activity: Activity, uri: Uri) { - val name = SafeFile.fromUri(activity, uri)?.name() - activity.navigate( - R.id.global_to_navigation_player, GeneratorPlayer.newInstance( - DownloadFileGenerator( - listOf( - ExtractorUri( - uri = uri, - name = name ?: getString(activity, R.string.downloaded_file), - // well not the same as a normal id, but we take it as users may want to - // play downloaded files and save the location - id = kotlin.runCatching { ContentUris.parseId(uri) }.getOrNull()?.hashCode() - ) - ) - ) - ) - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index 122eaa97..0d98f205 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError @@ -15,12 +14,13 @@ import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.ExtractorUri import kotlinx.coroutines.Job import kotlinx.coroutines.launch class PlayerGeneratorViewModel : ViewModel() { companion object { - const val TAG = "PlayViewGen" + val TAG = "PlayViewGen" } private var generator: IGenerator? = null @@ -111,9 +111,6 @@ class PlayerGeneratorViewModel : ViewModel() { } } } - fun getLoadResponse(): LoadResponse? { - return normalSafeApiCall { (generator as? RepoLinkGenerator?)?.page } - } fun getMeta(): Any? { return normalSafeApiCall { generator?.getCurrent() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt index 02a7ee03..25d7e3dd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt @@ -4,9 +4,7 @@ import android.util.Log import android.util.TypedValue import android.view.ViewGroup import android.widget.FrameLayout -import androidx.annotation.OptIn import androidx.media3.common.MimeTypes -import androidx.media3.common.util.UnstableApi import androidx.media3.ui.SubtitleView import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveBloat @@ -49,7 +47,6 @@ data class SubtitleData( } } -@OptIn(UnstableApi::class) class PlayerSubtitleHelper { private var activeSubtitles: Set = emptySet() private var allSubtitles: Set = emptySet() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt index ae800dbd..fb600ef1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt @@ -8,15 +8,15 @@ import android.os.Build import android.util.Log import androidx.annotation.WorkerThread import androidx.core.graphics.scale -import androidx.preference.PreferenceManager -import com.lagradost.cloudstream3.AcraApplication -import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.ui.settings.Globals import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout +import com.lagradost.cloudstream3.ui.settings.SettingsFragment import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkType +import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.M3u8Helper import com.lagradost.cloudstream3.utils.M3u8Helper2 import kotlinx.coroutines.CoroutineScope @@ -65,12 +65,8 @@ interface IPreviewGenerator { companion object { fun new(): IPreviewGenerator { - val userDisabled = AcraApplication.context?.let { ctx -> - PreferenceManager.getDefaultSharedPreferences(ctx)?.getBoolean( - ctx.getString(R.string.preview_seekbar_key), true) == false - } ?: false /** because TV has low ram + not show we disable this for now */ - return if (isLayout(TV) || userDisabled) { + return if (isLayout(TV)) { empty() } else { PreviewGenerator() @@ -246,11 +242,7 @@ private class M3u8PreviewGenerator(override var params: ImageParams) : IPreviewG // generated images 1:1 to idx of hsl private var images: Array = arrayOf() - companion object { - private const val TAG = "PreviewImgM3u8" - } - - + private val TAG = "PreviewImgM3u8" // prefixSum[i] = sum(hsl.ts[0..i].time) // where [0] = 0, [1] = hsl.ts[0].time aka time at start of segment, do [b] - [a] for range a,b @@ -399,6 +391,13 @@ private class M3u8PreviewGenerator(override var params: ImageParams) : IPreviewG logError(t) continue } + + /* + val buffer = hsl.resolveLinkSafe(index) ?: continue + tmpFile?.writeBytes(buffer) + val buff = FileOutputStream(tmpFile) + retriever.setDataSource(buff.fd) + val frame = retriever.getFrameAtTime(0L)*/ } } @@ -416,16 +415,14 @@ private class Mp4PreviewGenerator(override var params: ImageParams) : IPreviewGe null } - companion object { - private const val TAG = "PreviewImgMp4" - } - override fun hasPreview(): Boolean { synchronized(images) { return loadedLod >= MIN_LOD } } + val TAG = "PreviewImgMp4" + override fun getPreviewImage(fraction: Float): Bitmap? { synchronized(images) { if (loadedLod < MIN_LOD) { @@ -530,7 +527,7 @@ private class Mp4PreviewGenerator(override var params: ImageParams) : IPreviewGe val fraction = (1.0f.div((1 shl l).toFloat()) + i * 1.0f.div(items.toFloat())) Log.i(TAG, "Generating preview for ${fraction * 100}%") val frame = durationUs * fraction - val img = retriever.image(frame.toLong(), params) + val img = retriever.image(frame.toLong(), params); if (!scope.isActive) return if (img == null || img.width <= 1 || img.height <= 1) continue synchronized(images) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt index 588afbb5..0a194785 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt @@ -1,13 +1,13 @@ package com.lagradost.cloudstream3.ui.player import android.util.Log -import androidx.media3.common.util.UnstableApi import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.ExtractorUri import kotlin.math.max import kotlin.math.min @@ -29,7 +29,6 @@ class RepoLinkGenerator( } override val hasCache = true - override val canSkipLoading = true override fun hasNext(): Boolean { return currentIndex < episodes.size - 1 diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt index ce457740..fb60ccce 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt @@ -4,7 +4,7 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.databinding.PlayerPrioritizeItemBinding -import com.lagradost.cloudstream3.utils.AppContextUtils +import com.lagradost.cloudstream3.utils.AppUtils data class SourcePriority( val data: T, @@ -13,10 +13,11 @@ data class SourcePriority( ) class PriorityAdapter(override val items: MutableList>) : - AppContextUtils.DiffAdapter>(items) { + AppUtils.DiffAdapter>(items) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return PriorityViewHolder( PlayerPrioritizeItemBinding.inflate(LayoutInflater.from(parent.context),parent,false), + //LayoutInflater.from(parent.context).inflate(R.layout.player_prioritize_item, parent, false) ) } @@ -30,6 +31,10 @@ class PriorityAdapter(override val items: MutableList>) : val binding: PlayerPrioritizeItemBinding, ) : RecyclerView.ViewHolder(binding.root) { fun bind(item: SourcePriority) { + /* val plusButton: ImageView = itemView.add_button + val subtractButton: ImageView = itemView.subtract_button + val priorityText: TextView = itemView.priority_text + val priorityNumber: TextView = itemView.priority_number*/ binding.priorityText.text = item.name fun updatePriority() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt index 45f6aa66..8153d7a1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt @@ -13,7 +13,7 @@ import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.PlayerQualityProfileItemBinding import com.lagradost.cloudstream3.ui.result.UiImage -import com.lagradost.cloudstream3.utils.AppContextUtils +import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.UIHelper.setImage class ProfilesAdapter( @@ -21,7 +21,7 @@ class ProfilesAdapter( val usedProfile: Int, val clickCallback: (oldIndex: Int?, newIndex: Int) -> Unit, ) : - AppContextUtils.DiffAdapter( + AppUtils.DiffAdapter( items, comparison = { first: QualityDataHelper.QualityProfile, second: QualityDataHelper.QualityProfile -> first.id == second.id @@ -29,6 +29,8 @@ class ProfilesAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return ProfilesViewHolder( PlayerQualityProfileItemBinding.inflate(LayoutInflater.from(parent.context),parent,false) + //LayoutInflater.from(parent.context) + // .inflate(R.layout.player_quality_profile_item, parent, false) ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt index 3267efd7..96249db4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.ui.player.source_priority +import android.content.Context import androidx.annotation.StringRes import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey @@ -103,7 +104,7 @@ object QualityDataHelper { * Must under all circumstances at least return one profile **/ fun getProfiles(): List { - val availableTypes = QualityProfileType.entries.toMutableList() + val availableTypes = QualityProfileType.values().toMutableList() val profiles = (1..PROFILE_COUNT).map { profileNumber -> // Get the real type val type = getQualityProfileType(profileNumber) @@ -139,12 +140,12 @@ object QualityDataHelper { } } - QualityProfileType.entries.forEach { + QualityProfileType.values().forEach { if (it.unique) insertType(profiles, it) } debugAssert({ - !QualityProfileType.entries.all { type -> + !QualityProfileType.values().all { type -> !type.unique || profiles.any { it.type == type } } }, { "All unique quality types do not exist" }) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt index 0537092c..e3629158 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt @@ -65,7 +65,7 @@ class QualityProfileDialog( setDefaultBtt.setOnClickListener { val currentProfile = getCurrentProfile() ?: return@setOnClickListener - val choices = QualityDataHelper.QualityProfileType.entries + val choices = QualityDataHelper.QualityProfileType.values() .filter { it != QualityDataHelper.QualityProfileType.None } val choiceNames = choices.map { txt(it.stringRes).asString(context) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt index bc6282af..1b59882e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt @@ -47,7 +47,7 @@ class SourcePriorityDialog( ) qualitiesRecyclerView.adapter = PriorityAdapter( - Qualities.entries.mapNotNull { + Qualities.values().mapNotNull { SourcePriority( it, Qualities.getStringByIntFull(it.value).ifBlank { return@mapNotNull null }, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt index 12adc040..e9e00736 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt @@ -17,6 +17,8 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.GridLayoutManager import com.google.android.material.bottomsheet.BottomSheetDialog +import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia +import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.HomePageList @@ -32,13 +34,9 @@ import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.search.SearchViewModel -import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR -import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppContextUtils.filterProviderByPreferredMedia -import com.lagradost.cloudstream3.utils.AppContextUtils.filterSearchResultByFilmQuality -import com.lagradost.cloudstream3.utils.AppContextUtils.ownShow +import com.lagradost.cloudstream3.utils.AppUtils.ownShow import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount @@ -276,13 +274,8 @@ class QuickSearchFragment : Fragment() { // UIHelper.showInputMethod(view.findFocus()) // } //} - if (isLayout(PHONE or EMULATOR)) { - binding?.quickSearchBack?.apply { - isVisible = true - setOnClickListener { - activity?.popCurrentPage() - } - } + binding?.quickSearchBack?.setOnClickListener { + activity?.popCurrentPage() } if (isLayout(TV)) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt index 0ca326dd..7b743388 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt @@ -3,6 +3,8 @@ package com.lagradost.cloudstream3.ui.result import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.annotation.IdRes +import androidx.annotation.LayoutRes import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView @@ -68,7 +70,8 @@ class ActorAdaptor( } } - private inner class CardViewHolder( + private inner class CardViewHolder + constructor( val binding: CastItemBinding, private val focusCallback: (View?) -> Unit = {} ) : @@ -135,7 +138,7 @@ class ActorAdaptor( voiceActorImageHolder.isVisible = false voiceActorName.isVisible = false } else { - voiceActorName.text = actor.voiceActor?.name + voiceActorName.text = actor.voiceActor.name voiceActorImageHolder.isVisible = voiceActorImage.setImage(vaImage) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index d12521b3..fad349c8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -9,11 +9,9 @@ import androidx.core.view.isVisible import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.ResultEpisodeBinding import com.lagradost.cloudstream3.databinding.ResultEpisodeLargeBinding -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.secondsToReadable import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK import com.lagradost.cloudstream3.ui.download.DownloadClickEvent @@ -21,14 +19,11 @@ import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppContextUtils.html +import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.VideoDownloadHelper -import java.text.DateFormat -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale +import java.util.* const val ACTION_PLAY_EPISODE_IN_PLAYER = 1 const val ACTION_PLAY_EPISODE_IN_VLC_PLAYER = 2 @@ -56,10 +51,8 @@ const val ACTION_PLAY_EPISODE_IN_WEB_VIDEO = 16 const val ACTION_PLAY_EPISODE_IN_MPV = 17 const val ACTION_MARK_AS_WATCHED = 18 -const val ACTION_FCAST = 19 - -const val TV_EP_SIZE = 400 - +const val TV_EP_SIZE_LARGE = 400 +const val TV_EP_SIZE_SMALL = 300 data class EpisodeClickEvent(val action: Int, val data: ResultEpisode) class EpisodeAdapter( @@ -111,7 +104,7 @@ class EpisodeAdapter( override fun getItemViewType(position: Int): Int { val item = getItem(position) - return if (item.poster.isNullOrBlank() && item.description.isNullOrBlank()) 0 else 1 + return if (item.poster.isNullOrBlank()) 0 else 1 } @@ -169,7 +162,8 @@ class EpisodeAdapter( return cardList.size } - class EpisodeCardViewHolderLarge( + class EpisodeCardViewHolderLarge + constructor( val binding: ResultEpisodeLargeBinding, private val hasDownloadSupport: Boolean, private val clickCallback: (EpisodeClickEvent) -> Unit, @@ -181,7 +175,7 @@ class EpisodeAdapter( fun bind(card: ResultEpisode) { localCard = card val setWidth = - if (isLayout(TV or EMULATOR)) TV_EP_SIZE.toPx else ViewGroup.LayoutParams.MATCH_PARENT + if (isLayout(TV or EMULATOR)) TV_EP_SIZE_LARGE.toPx else ViewGroup.LayoutParams.MATCH_PARENT binding.episodeLinHolder.layoutParams.width = setWidth binding.episodeHolderLarge.layoutParams.width = setWidth @@ -192,15 +186,15 @@ class EpisodeAdapter( downloadButton.isVisible = hasDownloadSupport downloadButton.setDefaultClickListener( VideoDownloadHelper.DownloadEpisodeCached( - name = card.name, - poster = card.poster, - episode = card.episode, - season = card.season, - id = card.id, - parentId = card.parentId, - rating = card.rating, - description = card.description, - cacheTime = System.currentTimeMillis(), + card.name, + card.poster, + card.episode, + card.season, + card.id, + card.parentId, + card.rating, + card.description, + System.currentTimeMillis(), ), null ) { when (it.action) { @@ -266,42 +260,6 @@ class EpisodeAdapter( } } - if (card.airDate != null) { - val isUpcoming = unixTimeMS < card.airDate - - if (isUpcoming) { - episodePlayIcon.isVisible = false - episodeUpcomingIcon.isVisible = !episodePoster.isVisible - episodeDate.setText( - txt( - R.string.episode_upcoming_format, - secondsToReadable( - card.airDate.minus(unixTimeMS).div(1000).toInt(), - "" - ) - ) - ) - } else { - episodeUpcomingIcon.isVisible = false - - val formattedAirDate = SimpleDateFormat.getDateInstance( - DateFormat.LONG, - Locale.getDefault() - ).apply { - }.format(Date(card.airDate)) - - episodeDate.setText(txt(formattedAirDate)) - } - } else { - episodeDate.isVisible = false - } - - episodeRuntime.setText( - txt( - card.runTime?.times(60L)?.toInt()?.let { secondsToReadable(it, "") } - ) - ) - if (isLayout(EMULATOR or PHONE)) { episodePoster.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) @@ -313,7 +271,6 @@ class EpisodeAdapter( } } } - itemView.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) } @@ -334,7 +291,8 @@ class EpisodeAdapter( } } - class EpisodeCardViewHolderSmall( + class EpisodeCardViewHolderSmall + constructor( val binding: ResultEpisodeBinding, private val hasDownloadSupport: Boolean, private val clickCallback: (EpisodeClickEvent) -> Unit, @@ -344,22 +302,22 @@ class EpisodeAdapter( fun bind(card: ResultEpisode) { binding.episodeHolder.layoutParams.apply { width = - if (isLayout(TV or EMULATOR)) TV_EP_SIZE.toPx else ViewGroup.LayoutParams.MATCH_PARENT + if (isLayout(TV or EMULATOR)) TV_EP_SIZE_SMALL.toPx else ViewGroup.LayoutParams.MATCH_PARENT } binding.apply { downloadButton.isVisible = hasDownloadSupport downloadButton.setDefaultClickListener( VideoDownloadHelper.DownloadEpisodeCached( - name = card.name, - poster = card.poster, - episode = card.episode, - season = card.season, - id = card.id, - parentId = card.parentId, - rating = card.rating, - description = card.description, - cacheTime = System.currentTimeMillis(), + card.name, + card.poster, + card.episode, + card.season, + card.id, + card.parentId, + card.rating, + card.description, + System.currentTimeMillis(), ), null ) { when (it.action) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt index eecd6262..7b7bae43 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt @@ -8,6 +8,18 @@ import com.lagradost.cloudstream3.databinding.ResultMiniImageBinding import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout +/* +class ImageAdapter(context: Context, val resource: Int) : ArrayAdapter(context, resource) { + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val newConvertView = convertView ?: run { + val mInflater = context + .getSystemService(Activity.LAYOUT_INFLATER_SERVICE) as LayoutInflater + mInflater.inflate(resource, null) + } + getItem(position)?.let { (newConvertView as? ImageView?)?.setImageResource(it) } + return newConvertView + } +}*/ const val IMAGE_CLICK = 0 const val IMAGE_LONG_CLICK = 1 @@ -54,7 +66,8 @@ class ImageAdapter( diffResult.dispatchUpdatesTo(this) } - class ImageViewHolder(val binding: ResultMiniImageBinding) : + class ImageViewHolder + constructor(val binding: ResultMiniImageBinding) : RecyclerView.ViewHolder(binding.root) { fun bind( img: Int, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index 3eab0c71..a1574eec 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -3,12 +3,12 @@ package com.lagradost.cloudstream3.ui.result import android.os.Bundle import androidx.fragment.app.Fragment import androidx.preference.PreferenceManager +import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction -import com.lagradost.cloudstream3.utils.AppContextUtils.getApiDubstatusSettings import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.getVideoWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos @@ -50,8 +50,6 @@ data class ResultEpisode( val videoWatchState: VideoWatchState, /** Sum of all previous season episode counts + episode */ val totalEpisodeIndex: Int? = null, - val airDate: Long? = null, - val runTime: Int? = null, ) fun ResultEpisode.getRealPosition(): Long { @@ -87,8 +85,6 @@ fun buildResultEpisode( tvType: TvType, parentId: Int, totalEpisodeIndex: Int? = null, - airDate: Long? = null, - runTime: Int? = null, ): ResultEpisode { val posDur = getViewPos(id) val videoWatchState = getVideoWatchState(id) ?: VideoWatchState.None @@ -111,9 +107,7 @@ fun buildResultEpisode( tvType, parentId, videoWatchState, - totalEpisodeIndex, - airDate, - runTime, + totalEpisodeIndex ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt index 97bc49ea..8d0ca37b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt @@ -23,14 +23,14 @@ import androidx.core.widget.NestedScrollView import androidx.core.widget.doOnTextChanged import androidx.lifecycle.ViewModelProvider import com.discord.panels.OverlappingPanelsLayout -import com.discord.panels.PanelState import com.discord.panels.PanelsChildGestureRegionObserver import com.google.android.gms.cast.framework.CastButtonFactory import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastState import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.APIHolder -import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.APIHolder.updateHasTrailers +import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent @@ -57,12 +57,10 @@ import com.lagradost.cloudstream3.ui.result.ResultFragment.getStoredData import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchHelper -import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull -import com.lagradost.cloudstream3.utils.AppContextUtils.isCastApiAvailable -import com.lagradost.cloudstream3.utils.AppContextUtils.loadCache -import com.lagradost.cloudstream3.utils.AppContextUtils.openBrowser -import com.lagradost.cloudstream3.utils.AppContextUtils.updateHasTrailers -import com.lagradost.cloudstream3.utils.BatteryOptimizationChecker.openBatteryOptimizationSettings +import com.lagradost.cloudstream3.utils.AppUtils.getNameFull +import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable +import com.lagradost.cloudstream3.utils.AppUtils.loadCache +import com.lagradost.cloudstream3.utils.AppUtils.openBrowser import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant @@ -79,12 +77,11 @@ import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.VideoDownloadHelper open class ResultFragmentPhone : FullScreenPlayer() { - private val gestureRegionsListener = - object : PanelsChildGestureRegionObserver.GestureRegionsListener { - override fun onGestureRegionsUpdate(gestureRegions: List) { - binding?.resultOverlappingPanels?.setChildGestureRegions(gestureRegions) - } + private val gestureRegionsListener = object : PanelsChildGestureRegionObserver.GestureRegionsListener { + override fun onGestureRegionsUpdate(gestureRegions: List) { + binding?.resultOverlappingPanels?.setChildGestureRegions(gestureRegions) } + } protected lateinit var viewModel: ResultViewModel2 protected lateinit var syncModel: SyncViewModel @@ -119,14 +116,6 @@ open class ResultFragmentPhone : FullScreenPlayer() { return root } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - PanelsChildGestureRegionObserver.Provider.get().apply { - resultBinding?.resultCastItems?.let { register(it) } - } - } - var currentTrailers: List = emptyList() var currentTrailerIndex = 0 @@ -195,6 +184,8 @@ open class ResultFragmentPhone : FullScreenPlayer() { } binding?.resultFullscreenHolder?.isVisible = !isSuccess && isFullScreenPlayer } + + //player_view?.apply { //alpha = 0.0f //ObjectAnimator.ofFloat(player_view, "alpha", 1f).apply { @@ -208,7 +199,9 @@ open class ResultFragmentPhone : FullScreenPlayer() { // fillAfter = true //} //startAnimation(fadeIn) - //} + // } + + } private fun setTrailers(trailers: List?) { @@ -219,6 +212,9 @@ open class ResultFragmentPhone : FullScreenPlayer() { } override fun onDestroyView() { + + //somehow this still leaks and I dont know why???? + // todo look at https://github.com/discord/OverlappingPanels/blob/70b4a7cf43c6771873b1e091029d332896d41a1a/sample_app/src/main/java/com/discord/sampleapp/MainActivity.kt PanelsChildGestureRegionObserver.Provider.get().let { obs -> resultBinding?.resultCastItems?.let { obs.unregister(it) @@ -335,18 +331,14 @@ open class ResultFragmentPhone : FullScreenPlayer() { syncModel.addFromUrl(storedData.url) val api = APIHolder.getApiFromNameNull(storedData.apiName) - // This may not be 100% reliable, and may delay for small period - // before resultCastItems will be scrollable again, but this does work - // most of the time. - binding?.resultOverlappingPanels?.registerEndPanelStateListeners( - object : OverlappingPanelsLayout.PanelStateListener { - override fun onPanelStateChange(panelState: PanelState) { - PanelsChildGestureRegionObserver.Provider.get().apply { - resultBinding?.resultCastItems?.let { register(it) } - } - } + PanelsChildGestureRegionObserver.Provider.get().apply { + resultBinding?.resultCastItems?.let { + register(it) } - ) + addGestureRegionsUpdateListener(gestureRegionsListener) + } + + // ===== ===== ===== @@ -441,18 +433,17 @@ open class ResultFragmentPhone : FullScreenPlayer() { if (newStatus == null) return@toggleSubscriptionStatus val message = if (newStatus) { - // Kinda icky to have this here, but it works. - SubscriptionWorkManager.enqueuePeriodicWork(context) - R.string.subscription_new - } else { - R.string.subscription_deleted - } + // Kinda icky to have this here, but it works. + SubscriptionWorkManager.enqueuePeriodicWork(context) + R.string.subscription_new + } else { + R.string.subscription_deleted + } - val name = (viewModel.page.value as? Resource.Success)?.value?.title - ?: txt(R.string.no_data).asStringNull(context) ?: "" - showToast(txt(message, name), Toast.LENGTH_SHORT) + val name = (viewModel.page.value as? Resource.Success)?.value?.title + ?: txt(R.string.no_data).asStringNull(context) ?: "" + CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) } - context?.let { openBatteryOptimizationSettings(it) } } resultFavorite.setOnClickListener { viewModel.toggleFavoriteStatus(context) { newStatus: Boolean? -> @@ -466,7 +457,7 @@ open class ResultFragmentPhone : FullScreenPlayer() { val name = (viewModel.page.value as? Resource.Success)?.value?.title ?: txt(R.string.no_data).asStringNull(context) ?: "" - showToast(txt(message, name), Toast.LENGTH_SHORT) + CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) } } mediaRouteButton.apply { @@ -474,7 +465,7 @@ open class ResultFragmentPhone : FullScreenPlayer() { alpha = if (chromecastSupport) 1f else 0.3f if (!chromecastSupport) { setOnClickListener { - showToast( + CommonActivity.showToast( R.string.no_chromecast_support_toast, Toast.LENGTH_LONG ) @@ -484,16 +475,8 @@ open class ResultFragmentPhone : FullScreenPlayer() { if (act.isCastApiAvailable()) { try { CastButtonFactory.setUpMediaRouteButton(act, this) - CastContext.getSharedInstance(act.applicationContext) { - it.run() - }.addOnCompleteListener { - isGone = if (it.isSuccessful) { - it.result.castState == CastState.NO_DEVICES_AVAILABLE - } else { - true - } - - } + val castContext = CastContext.getSharedInstance(act.applicationContext) + isGone = castContext.castState == CastState.NO_DEVICES_AVAILABLE // this shit leaks for some reason //castContext.addCastStateListener { state -> // media_route_button?.isGone = state == CastState.NO_DEVICES_AVAILABLE @@ -645,20 +628,18 @@ open class ResultFragmentPhone : FullScreenPlayer() { } downloadButton.setDefaultClickListener( VideoDownloadHelper.DownloadEpisodeCached( - name = ep.name, - poster = ep.poster, - episode = 0, - season = null, - id = ep.id, - parentId = ep.id, - rating = null, - description = null, - cacheTime = System.currentTimeMillis(), + ep.name, + ep.poster, + 0, + null, + ep.id, + ep.id, + null, + null, + System.currentTimeMillis(), ), null ) { click -> - context?.let { openBatteryOptimizationSettings(it) } - when (click.action) { DOWNLOAD_ACTION_DOWNLOAD -> { viewModel.handleAction( @@ -685,9 +666,6 @@ open class ResultFragmentPhone : FullScreenPlayer() { observe(viewModel.page) { data -> if (data == null) return@observe resultBinding?.apply { - PanelsChildGestureRegionObserver.Provider.get().apply { - register(resultCastItems) - } (data as? Resource.Success)?.value?.let { d -> resultVpn.setText(d.vpnText) resultInfo.setText(d.metaText) @@ -983,12 +961,12 @@ open class ResultFragmentPhone : FullScreenPlayer() { setOnClickListener { fab -> activity?.showBottomDialog( - WatchType.entries.map { fab.context.getString(it.stringRes) }.toList(), + WatchType.values().map { fab.context.getString(it.stringRes) }.toList(), watchType.ordinal, fab.context.getString(R.string.action_add_to_bookmarks), showApply = false, {}) { - viewModel.updateWatchStatus(WatchType.entries[it], context) + viewModel.updateWatchStatus(WatchType.values()[it], context) } } } @@ -1068,7 +1046,7 @@ open class ResultFragmentPhone : FullScreenPlayer() { text?.asStringNull(ctx) ?: return@mapNotNull null ) }) { - viewModel.changeDubStatus(DubStatus.entries[itemId]) + viewModel.changeDubStatus(DubStatus.values()[itemId]) } } } @@ -1125,8 +1103,7 @@ open class ResultFragmentPhone : FullScreenPlayer() { override fun onPause() { super.onPause() - PanelsChildGestureRegionObserver.Provider.get() - .addGestureRegionsUpdateListener(gestureRegionsListener) + PanelsChildGestureRegionObserver.Provider.get().addGestureRegionsUpdateListener(gestureRegionsListener) } private fun setRecommendations(rec: List?, validApiName: String?) { @@ -1181,4 +1158,4 @@ open class ResultFragmentPhone : FullScreenPlayer() { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index 1878f0b8..3263ee93 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -17,6 +17,7 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetDialog +import com.lagradost.cloudstream3.APIHolder.updateHasTrailers import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.LoadResponse @@ -32,20 +33,19 @@ import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator import com.lagradost.cloudstream3.ui.player.GeneratorPlayer -import com.lagradost.cloudstream3.ui.player.NEXT_WATCH_EPISODE_PERCENTAGE import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.result.ResultFragment.getStoredData import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchHelper +import com.lagradost.cloudstream3.ui.settings.Globals import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppContextUtils.html -import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl -import com.lagradost.cloudstream3.utils.AppContextUtils.loadCache -import com.lagradost.cloudstream3.utils.AppContextUtils.updateHasTrailers +import com.lagradost.cloudstream3.utils.AppUtils.html +import com.lagradost.cloudstream3.utils.AppUtils.isRtl +import com.lagradost.cloudstream3.utils.AppUtils.loadCache import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant import com.lagradost.cloudstream3.utils.UIHelper @@ -56,7 +56,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.setImage class ResultFragmentTv : Fragment() { - private lateinit var viewModel: ResultViewModel2 + protected lateinit var viewModel: ResultViewModel2 private var binding: FragmentResultTvBinding? = null override fun onDestroyView() { @@ -418,6 +418,10 @@ class ResultFragmentTv : Fragment() { resultCastItems.layoutManager = object : LinearListLayout(view.context) { + override fun onInterceptFocusSearch(focused: View, direction: Int): View? { + return super.onInterceptFocusSearch(focused, direction) + } + override fun onRequestChildFocus( parent: RecyclerView, state: RecyclerView.State, @@ -645,7 +649,7 @@ class ResultFragmentTv : Fragment() { binding?.apply { - (data as? Resource.Success)?.value?.let { (_, ep) -> + (data as? Resource.Success)?.value?.let { (text, ep) -> resultPlayMovieButton.setOnClickListener { viewModel.handleAction( @@ -777,31 +781,25 @@ class ResultFragmentTv : Fragment() { // resultEpisodeLoading.isVisible = episodes is Resource.Loading if (episodes is Resource.Success) { - - val lastWatchedIndex = episodes.value.indexOfLast { ep -> - ep.getWatchProgress() >= NEXT_WATCH_EPISODE_PERCENTAGE.toFloat() / 100.0f || ep.videoWatchState == VideoWatchState.Watched - } - - val firstUnwatched = episodes.value.getOrElse(lastWatchedIndex + 1) { episodes.value.firstOrNull() } - - if (firstUnwatched != null) { + val first = episodes.value.firstOrNull() + if (first != null) { resultPlaySeriesText.text = when { - firstUnwatched.season != null -> - "${getString(R.string.season_short)}${firstUnwatched.season}:${getString(R.string.episode_short)}${firstUnwatched.episode}" - else -> "${getString(R.string.episode)} ${firstUnwatched.episode}" + first.season != null -> + "${getString(R.string.season_short)}${first.season}:${getString(R.string.episode_short)}${first.episode}" + else -> "${getString(R.string.episode)} ${first.episode}" } resultPlaySeriesButton.setOnClickListener { viewModel.handleAction( EpisodeClickEvent( ACTION_CLICK_DEFAULT, - firstUnwatched + first ) ) } resultPlaySeriesButton.setOnLongClickListener { viewModel.handleAction( - EpisodeClickEvent(ACTION_SHOW_OPTIONS, firstUnwatched) + EpisodeClickEvent(ACTION_SHOW_OPTIONS, first) ) return@setOnLongClickListener true } @@ -813,8 +811,45 @@ class ResultFragmentTv : Fragment() { } } + /* + * Okay so what is this fuckery? + * Basically Android TV will crash if you request a new focus while + * the adapter gets updated. + * + * This means that if you load thumbnails and request a next focus at the same time + * the app will crash without any way to catch it! + * + * How to bypass this? + * This code basically steals the focus for 500ms and puts it in an inescapable view + * then lets out the focus by requesting focus to result_episodes + */ + + val hasEpisodes = + !(resultEpisodes.adapter as? EpisodeAdapter?)?.cardList.isNullOrEmpty() + /*val focus = activity?.currentFocus + + if (hasEpisodes) { + // Make it impossible to focus anywhere else! + temporaryNoFocus.isFocusable = true + temporaryNoFocus.requestFocus() + }*/ (resultEpisodes.adapter as? EpisodeAdapter)?.updateList(episodes.value) + + /* if (hasEpisodes) main { + + delay(500) + // This might make some people sad as it changes the focus when leaving an episode :( + if(focus?.requestFocus() == true) { + temporaryNoFocus.isFocusable = false + return@main + } + temporaryNoFocus.isFocusable = false + temporaryNoFocus.requestFocus() + } + + if (hasNoFocus()) + binding?.resultEpisodes?.requestFocus()*/ } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt index 2ab60c2f..ef3db0b4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt @@ -7,17 +7,15 @@ import android.os.Bundle import android.view.View import android.view.ViewGroup import android.widget.FrameLayout +import androidx.activity.OnBackPressedCallback import androidx.core.view.isGone import androidx.core.view.isVisible import com.lagradost.cloudstream3.CommonActivity.screenHeight import com.lagradost.cloudstream3.CommonActivity.screenWidth -import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.player.CSPlayerEvent import com.lagradost.cloudstream3.ui.player.PlayerEventSource import com.lagradost.cloudstream3.ui.player.SubtitleData -import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback -import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback open class ResultTrailerPlayer : ResultFragmentPhone() { @@ -112,7 +110,7 @@ open class ResultTrailerPlayer : ResultFragmentPhone() { override fun openOnlineSubPicker( context: Context, - loadResponse: LoadResponse?, + imdbId: Long?, dismissCallback: () -> Unit ) { } @@ -157,9 +155,7 @@ open class ResultTrailerPlayer : ResultFragmentPhone() { uiReset() if (isFullScreenPlayer) { - activity?.attachBackPressedCallback { - updateFullscreen(false) - } + attachBackPressedCallback() } else detachBackPressedCallback() } @@ -178,4 +174,27 @@ open class ResultTrailerPlayer : ResultFragmentPhone() { fixPlayerSize() } } + + private var backPressedCallback: OnBackPressedCallback? = null + + private fun attachBackPressedCallback() { + if (backPressedCallback == null) { + backPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + updateFullscreen(false) + } + } + } + + backPressedCallback?.isEnabled = true + + activity?.onBackPressedDispatcher?.addCallback( + activity ?: return, + backPressedCallback ?: return + ) + } + + private fun detachBackPressedCallback() { + backPressedCallback?.isEnabled = false + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index 6443a923..c90e01d0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -5,7 +5,6 @@ import android.content.* import android.net.Uri import android.os.Build import android.os.Bundle -import android.text.format.Formatter.formatFileSize import android.util.Log import android.widget.Toast import androidx.annotation.MainThread @@ -18,9 +17,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.apis -import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull +import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.APIHolder.unixTimeMS +import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.CommonActivity.getCastSession @@ -29,15 +29,6 @@ import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie -import com.lagradost.cloudstream3.LoadResponse.Companion.readIdFromString -import com.lagradost.cloudstream3.MainActivity.Companion.MPV -import com.lagradost.cloudstream3.MainActivity.Companion.MPV_COMPONENT -import com.lagradost.cloudstream3.MainActivity.Companion.MPV_PACKAGE -import com.lagradost.cloudstream3.MainActivity.Companion.VLC -import com.lagradost.cloudstream3.MainActivity.Companion.VLC_COMPONENT -import com.lagradost.cloudstream3.MainActivity.Companion.VLC_PACKAGE -import com.lagradost.cloudstream3.MainActivity.Companion.WEB_VIDEO -import com.lagradost.cloudstream3.MainActivity.Companion.WEB_VIDEO_CAST_PACKAGE import com.lagradost.cloudstream3.metaproviders.SyncRedirector import com.lagradost.cloudstream3.mvvm.* import com.lagradost.cloudstream3.syncproviders.AccountManager @@ -56,11 +47,10 @@ import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.utils.* -import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull -import com.lagradost.cloudstream3.utils.AppContextUtils.isAppInstalled -import com.lagradost.cloudstream3.utils.AppContextUtils.isConnectedToChromecast -import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus -import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs +import com.lagradost.cloudstream3.utils.AppUtils.getNameFull +import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled +import com.lagradost.cloudstream3.utils.AppUtils.isConnectedToChromecast +import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.CastHelper.startCast import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioWork @@ -93,10 +83,6 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.setVideoWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.updateSubscribedData import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper import com.lagradost.cloudstream3.utils.UIHelper.navigate -import com.lagradost.cloudstream3.utils.fcast.FcastManager -import com.lagradost.cloudstream3.utils.fcast.FcastSession -import com.lagradost.cloudstream3.utils.fcast.Opcode -import com.lagradost.cloudstream3.utils.fcast.PlayMessage import kotlinx.coroutines.* import java.io.File import java.util.concurrent.TimeUnit @@ -211,11 +197,7 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { else -> null }?.also { - nextAiringEpisode = when (airing.season) { - - null -> txt(R.string.next_episode_format, airing.episode) - else -> txt(R.string.next_season_episode_format, airing.season, airing.episode) - } + nextAiringEpisode = txt(R.string.next_episode_format, airing.episode) } } } @@ -264,9 +246,6 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { TvType.Live -> R.string.live_singular TvType.Others -> R.string.other_singular TvType.NSFW -> R.string.nsfw_singular - TvType.Music -> R.string.music_singlar - TvType.AudioBook -> R.string.audio_book_singular - TvType.CustomMedia -> R.string.custom_media_singluar } ), yearText = txt(year?.toString()), @@ -302,23 +281,6 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { ) } -data class ExtractorSubtitleLink( - val name: String, - override val url: String, - override val referer: String, - override val headers: Map = mapOf() -) : IDownloadableMinimum - -fun LoadResponse.getId(): Int { - // this fixes an issue with outdated api as getLoadResponseIdFromUrl might be fucked - return (if (this is ResultViewModel2.LoadResponseFromSearch) this.id else null) - ?: getLoadResponseIdFromUrl(url, apiName) -} - -private fun getLoadResponseIdFromUrl(url: String, apiName: String): Int { - return url.replace(getApiFromNameNull(apiName)?.mainUrl ?: "", "").replace("/", "") - .hashCode() -} data class LinkProgress( val linksLoaded: Int, @@ -665,9 +627,6 @@ class ResultViewModel2 : ViewModel() { TvType.Live -> "LiveStreams" TvType.NSFW -> "NSFW" TvType.Others -> "Others" - TvType.Music -> "Music" - TvType.AudioBook -> "AudioBooks" - TvType.CustomMedia -> "Media" } } @@ -723,13 +682,13 @@ class ResultViewModel2 : ViewModel() { DOWNLOAD_HEADER_CACHE, parentId.toString(), VideoDownloadHelper.DownloadHeaderCached( - apiName = apiName, - url = url, - type = currentType, - name = currentHeaderName, - poster = currentPoster, - id = parentId, - cacheTime = System.currentTimeMillis(), + apiName, + url, + currentType, + currentHeaderName, + currentPoster, + parentId, + System.currentTimeMillis(), ) ) @@ -740,15 +699,15 @@ class ResultViewModel2 : ViewModel() { ), // 3 deep folder for faster acess episode.id.toString(), VideoDownloadHelper.DownloadEpisodeCached( - name = episode.name, - poster = episode.poster, - episode = episode.episode, - season = episode.season, - id = episode.id, - parentId = parentId, - rating = episode.rating, - description = episode.description, - cacheTime = System.currentTimeMillis(), + episode.name, + episode.poster, + episode.episode, + episode.season, + episode.id, + parentId, + episode.rating, + episode.description, + System.currentTimeMillis(), ) ) @@ -874,7 +833,7 @@ class ResultViewModel2 : ViewModel() { loadResponse: LoadResponse? = null, statusChangedCallback: ((statusChanged: Boolean) -> Unit)? = null ) { - val (response, currentId) = loadResponse?.let { load -> + val (response,currentId) = loadResponse?.let { load -> (load to load.getId()) } ?: ((currentResponse ?: return) to (currentId ?: return)) @@ -1134,14 +1093,13 @@ class ResultViewModel2 : ViewModel() { val duplicateEntries = data.filter { it: DataStoreHelper.LibrarySearchResponse -> val librarySyncData = it.syncData - val yearCheck = year == it.year || year == null || it.year == null val checks = listOf( { imdbId != null && getImdbIdFromSyncData(librarySyncData) == imdbId }, { tmdbId != null && getTMDbIdFromSyncData(librarySyncData) == tmdbId }, { malId != null && librarySyncData?.get(AccountManager.malApi.idPrefix) == malId }, { aniListId != null && librarySyncData?.get(AccountManager.aniListApi.idPrefix) == aniListId }, - { normalizedName == normalizeString(it.name) && yearCheck } + { normalizedName == normalizeString(it.name) && year == it.year } ) checks.any { it() } @@ -1158,16 +1116,12 @@ class ResultViewModel2 : ViewModel() { val message = if (duplicateEntries.size == 1) { val list = when (listType) { - LibraryListType.BOOKMARKS -> getResultWatchState( - duplicateEntries[0].id ?: 0 - ).stringRes - + LibraryListType.BOOKMARKS -> getResultWatchState(duplicateEntries[0].id ?: 0).stringRes LibraryListType.FAVORITES -> R.string.favorites_list_name LibraryListType.SUBSCRIPTIONS -> R.string.subscription_list_name } - context.getString( - R.string.duplicate_message_single, + context.getString(R.string.duplicate_message_single, "${normalizeString(duplicateEntries[0].name)} (${context.getString(list)}) — ${duplicateEntries[0].apiName}" ) } else { @@ -1192,11 +1146,9 @@ class ResultViewModel2 : ViewModel() { DialogInterface.BUTTON_POSITIVE -> { checkDuplicatesCallback.invoke(true, emptyList()) } - DialogInterface.BUTTON_NEGATIVE -> { checkDuplicatesCallback.invoke(false, emptyList()) } - DialogInterface.BUTTON_NEUTRAL -> { checkDuplicatesCallback.invoke(true, duplicateEntries.map { it.id }) } @@ -1213,17 +1165,17 @@ class ResultViewModel2 : ViewModel() { private fun getImdbIdFromSyncData(syncData: Map?): String? { return normalSafeApiCall { - readIdFromString( + SimklApi.readIdFromString( syncData?.get(AccountManager.simklApi.idPrefix) - )[SimklSyncServices.Imdb] + )[SimklApi.Companion.SyncServices.Imdb] } } private fun getTMDbIdFromSyncData(syncData: Map?): String? { return normalSafeApiCall { - readIdFromString( + SimklApi.readIdFromString( syncData?.get(AccountManager.simklApi.idPrefix) - )[SimklSyncServices.Tmdb] + )[SimklApi.Companion.SyncServices.Tmdb] } } @@ -1322,15 +1274,9 @@ class ResultViewModel2 : ViewModel() { callback: (Pair) -> Unit, ) { loadLinks(result, isVisible = true, type) { links -> - // Could not find a better way to do this - val context = AcraApplication.context postPopup( text, - links.links.apmap { - val size = - it.getVideoSize()?.let { size -> " " + formatFileSize(context, size) } ?: "" - txt("${it.name} ${Qualities.getStringByInt(it.quality)}$size") - }) { + links.links.map { txt("${it.name} ${Qualities.getStringByInt(it.quality)}") }) { callback.invoke(links to (it ?: return@postPopup)) } } @@ -1388,7 +1334,7 @@ class ResultViewModel2 : ViewModel() { private fun launchActivity( activity: Activity?, - resumeApp: MainActivity.Companion.ResultResume, + resumeApp: ResultResume, id: Int? = null, work: suspend (Intent.(Activity) -> Unit) ): Job? { @@ -1557,13 +1503,6 @@ class ResultViewModel2 : ViewModel() { ) ) } - - if (FcastManager.currentDevices.isNotEmpty()) { - options.add( - txt(R.string.player_settings_play_in_fcast) to ACTION_FCAST - ) - } - options.add(txt(R.string.episode_action_play_in_app) to ACTION_PLAY_EPISODE_IN_PLAYER) for (app in apps) { @@ -1739,39 +1678,6 @@ class ResultViewModel2 : ViewModel() { } } - ACTION_FCAST -> { - val devices = FcastManager.currentDevices.toList() - postPopup( - txt(R.string.player_settings_select_cast_device), - devices.map { txt(it.name) }) { index -> - if (index == null) return@postPopup - val device = devices.getOrNull(index) - - acquireSingleLink( - click.data, - LoadType.Fcast, - txt(R.string.episode_action_cast_mirror) - ) { (result, index) -> - val host = device?.host ?: return@acquireSingleLink - val link = result.links.getOrNull(index) ?: return@acquireSingleLink - - FcastSession(host).use { session -> - session.sendMessage( - Opcode.Play, - PlayMessage( - link.type.getMimeType(), - link.url, - headers = mapOf( - "referer" to link.referer, - "user-agent" to USER_AGENT - ) + link.headers - ) - ) - } - } - } - } - ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink( click.data, LoadType.Browser, @@ -1853,28 +1759,20 @@ class ResultViewModel2 : ViewModel() { val data = currentResponse?.syncData?.toList() ?: emptyList() val list = HashMap().apply { putAll(data) } - generator?.also { - it.getAll() // I know kinda shit to iterate all, but it is 100% sure to work - ?.indexOfFirst { value -> value is ResultEpisode && value.id == click.data.id } - ?.let { index -> - if (index >= 0) - it.goto(index) - } - } - if (currentResponse?.type == TvType.CustomMedia) { - generator?.generateLinks( - clearCache = true, - LoadType.Unknown, - callback = {}, - subtitleCallback = {}) - } else { - activity?.navigate( - R.id.global_to_navigation_player, - GeneratorPlayer.newInstance( - generator ?: return, list - ) + + activity?.navigate( + R.id.global_to_navigation_player, + GeneratorPlayer.newInstance( + generator?.also { + it.getAll() // I know kinda shit to iterate all, but it is 100% sure to work + ?.indexOfFirst { value -> value is ResultEpisode && value.id == click.data.id } + ?.let { index -> + if (index >= 0) + it.goto(index) + } + } ?: return, list ) - } + ) } ACTION_MARK_AS_WATCHED -> { @@ -1953,8 +1851,7 @@ class ResultViewModel2 : ViewModel() { .distinct().map { // this actually would be nice if we improved a bit as 3rd season == season 3 == III ect // right now it just removes the dubbed status - it.lowercase().replace(Regex("""\(?[ds]ub(bed)?\)?(\s|$)"""), "") - .trim() + it.lowercase().replace(Regex("""\(?[ds]ub(bed)?\)?(\s|$)""") , "").trim() }, TrackerType.getTypes(this.type), this.year @@ -2163,7 +2060,7 @@ class ResultViewModel2 : ViewModel() { // lets say that we have subscribed, then we must be able to unsubscribe no matter what else if (data != null) { _subscribeStatus.postValue(true) - } else _subscribeStatus.postValue(null) + } } private fun postFavorites(loadResponse: LoadResponse) { @@ -2302,7 +2199,7 @@ class ResultViewModel2 : ViewModel() { private suspend fun postSuccessful( loadResponse: LoadResponse, - mainId: Int, + mainId : Int, apiRepository: APIRepository, updateEpisodes: Boolean, updateFillers: Boolean, @@ -2318,11 +2215,7 @@ class ResultViewModel2 : ViewModel() { postEpisodes(loadResponse, mainId, updateFillers) } - private suspend fun postEpisodes( - loadResponse: LoadResponse, - mainId: Int, - updateFillers: Boolean - ) { + private suspend fun postEpisodes(loadResponse: LoadResponse, mainId : Int, updateFillers: Boolean) { _episodes.postValue(Resource.Loading()) if (updateFillers && loadResponse is AnimeLoadResponse) { @@ -2343,12 +2236,7 @@ class ResultViewModel2 : ViewModel() { ?: 0) val totalIndex = - i.season?.let { season -> - loadResponse.getTotalEpisodeIndex( - episode, - season - ) - } + i.season?.let { season -> loadResponse.getTotalEpisodeIndex(episode, season) } if (!existingEpisodes.contains(id)) { existingEpisodes.add(id) @@ -2370,9 +2258,7 @@ class ResultViewModel2 : ViewModel() { fillers.getOrDefault(episode, false), loadResponse.type, mainId, - totalIndex, - airDate = i.date, - runTime = i.runTime, + totalIndex ) val season = eps.seasonIndex ?: 0 @@ -2402,12 +2288,7 @@ class ResultViewModel2 : ViewModel() { loadResponse.seasonNames.getSeason(episode.season) val totalIndex = - episode.season?.let { season -> - loadResponse.getTotalEpisodeIndex( - episodeIndex, - season - ) - } + episode.season?.let { season -> loadResponse.getTotalEpisodeIndex(episodeIndex, season) } val ep = buildResultEpisode( @@ -2426,9 +2307,7 @@ class ResultViewModel2 : ViewModel() { null, loadResponse.type, mainId, - totalIndex, - airDate = episode.date, - runTime = episode.runTime, + totalIndex ) val season = ep.seasonIndex ?: 0 @@ -2460,7 +2339,7 @@ class ResultViewModel2 : ViewModel() { null, loadResponse.type, mainId, - null, + null ) ) } @@ -2588,13 +2467,7 @@ class ResultViewModel2 : ViewModel() { ResumeProgress( progress = (viewPos.position / 1000).toInt(), maxProgress = (viewPos.duration / 1000).toInt(), - txt( - R.string.resume_remaining, - secondsToReadable( - ((viewPos.duration - viewPos.position) / 1_000).toInt(), - "0 mins" - ) - ) + txt(R.string.resume_remaining, secondsToReadable(((viewPos.duration - viewPos.position) / 1_000).toInt(), "0 mins")) ) } @@ -2720,26 +2593,17 @@ class ResultViewModel2 : ViewModel() { override var posterHeaders: Map? = null, override var backgroundPosterUrl: String? = null, override var contentRating: String? = null, - val id: Int?, + val id : Int?, ) : LoadResponse - fun loadSmall(searchResponse: SearchResponse) = ioSafe { + fun loadSmall(activity: Activity?, searchResponse : SearchResponse) = ioSafe { val url = searchResponse.url _page.postValue(Resource.Loading(url)) _episodes.postValue(Resource.Loading()) - val api = - APIHolder.getApiFromNameNull(searchResponse.apiName) ?: APIHolder.getApiFromUrlNull( - searchResponse.url - ) ?: APIRepository.noneApi + val api = APIHolder.getApiFromNameNull(searchResponse.apiName) ?: APIHolder.getApiFromUrlNull(searchResponse.url) ?: APIRepository.noneApi val repo = APIRepository(api) - val response = LoadResponseFromSearch( - name = searchResponse.name, - url = searchResponse.url, - apiName = api.name, - type = searchResponse.type ?: TvType.Others, - posterUrl = searchResponse.posterUrl, - id = searchResponse.id - ).apply { + val response = LoadResponseFromSearch(name = searchResponse.name, url = searchResponse.url, apiName = api.name, type = searchResponse.type ?: TvType.Others, + posterUrl = searchResponse.posterUrl, id = searchResponse.id).apply { if (searchResponse is SyncAPI.LibraryItem) { this.plot = searchResponse.plot this.rating = searchResponse.personalRating?.times(100) ?: searchResponse.rating @@ -2758,8 +2622,7 @@ class ResultViewModel2 : ViewModel() { mainId = mainId, apiRepository = repo, updateEpisodes = false, - updateFillers = false - ) + updateFillers = false) } fun load( @@ -2834,13 +2697,13 @@ class ResultViewModel2 : ViewModel() { DOWNLOAD_HEADER_CACHE, mainId.toString(), VideoDownloadHelper.DownloadHeaderCached( - apiName = apiName, - url = validUrl, - type = loadResponse.type, - name = loadResponse.name, - poster = loadResponse.posterUrl, - id = mainId, - cacheTime = System.currentTimeMillis(), + apiName, + validUrl, + loadResponse.type, + loadResponse.name, + loadResponse.posterUrl, + mainId, + System.currentTimeMillis(), ) ) if (loadTrailers) @@ -2861,4 +2724,4 @@ class ResultViewModel2 : ViewModel() { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt index 8752e275..5a23bfc1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt @@ -63,7 +63,8 @@ class SelectAdaptor(val callback: (Any) -> Unit) : RecyclerView.Adapter setImageImage(value, fadeIn) is UiImage.Drawable -> setImageDrawable(value) - is UiImage.Bitmap -> setImageBitmap(value) null -> { this?.isVisible = false } @@ -110,12 +107,6 @@ fun ImageView?.setImageDrawable(value: UiImage.Drawable) { this.setImage(UiImage.Drawable(value.resId)) } -fun ImageView?.setImageBitmap(value: UiImage.Bitmap) { - if (this == null) return - this.isVisible = true - this.setImageBitmap(value.bitmap) -} - @JvmName("imgNull") fun img( url: String?, @@ -138,10 +129,6 @@ fun img(@DrawableRes drawable: Int): UiImage { return UiImage.Drawable(drawable) } -fun img(bitmap: Bitmap): UiImage { - return UiImage.Bitmap(bitmap) -} - fun txt(value: String): UiText { return UiText.DynamicString(value) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt index f318401c..b516348d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt @@ -11,7 +11,7 @@ import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.databinding.SearchResultGridBinding import com.lagradost.cloudstream3.databinding.SearchResultGridExpandedBinding import com.lagradost.cloudstream3.ui.AutofitRecyclerView -import com.lagradost.cloudstream3.utils.UIHelper.isBottomLayout +import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout import com.lagradost.cloudstream3.utils.UIHelper.toPx import kotlin.math.roundToInt @@ -41,7 +41,7 @@ class SearchAdapter( val inflater = LayoutInflater.from(parent.context) val layout = - if (parent.context.isBottomLayout()) SearchResultGridExpandedBinding.inflate( + if (parent.context.IsBottomLayout()) SearchResultGridExpandedBinding.inflate( inflater, parent, false @@ -83,7 +83,8 @@ class SearchAdapter( diffResult.dispatchUpdatesTo(this) } - class CardViewHolder( + class CardViewHolder + constructor( val binding: ViewBinding, private val clickCallback: (SearchClickCallback) -> Unit, resView: AutofitRecyclerView diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt index ef10fcee..24e87d30 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt @@ -24,7 +24,11 @@ import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.button.MaterialButton +import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia +import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull +import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings +import com.lagradost.cloudstream3.APIHolder.getApiSettings import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.AllLanguagesName @@ -54,13 +58,9 @@ import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppContextUtils.filterProviderByPreferredMedia -import com.lagradost.cloudstream3.utils.AppContextUtils.filterSearchResultByFilmQuality -import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings -import com.lagradost.cloudstream3.utils.AppContextUtils.getApiSettings -import com.lagradost.cloudstream3.utils.AppContextUtils.ownHide -import com.lagradost.cloudstream3.utils.AppContextUtils.ownShow -import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppUtils.ownHide +import com.lagradost.cloudstream3.utils.AppUtils.ownShow +import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt index ef1b8719..5b943105 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt @@ -11,7 +11,7 @@ import com.lagradost.cloudstream3.ui.download.DownloadClickEvent import com.lagradost.cloudstream3.ui.result.START_ACTION_LOAD_EP import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult +import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.VideoDownloadHelper @@ -25,7 +25,7 @@ object SearchHelper { SEARCH_ACTION_PLAY_FILE -> { if (card is DataStoreHelper.ResumeWatchingResult) { val id = card.id - if (id == null) { + if(id == null) { showToast(R.string.error_invalid_id, Toast.LENGTH_SHORT) } else { if (card.isFromDownload) { @@ -33,15 +33,15 @@ object SearchHelper { DownloadClickEvent( DOWNLOAD_ACTION_PLAY_FILE, VideoDownloadHelper.DownloadEpisodeCached( - name = card.name, - poster = card.posterUrl, - episode = card.episode ?: 0, - season = card.season, - id = id, - parentId = card.parentId ?: return, - rating = null, - description = null, - cacheTime = System.currentTimeMillis(), + card.name, + card.posterUrl, + card.episode ?: 0, + card.season, + id, + card.parentId ?: return, + null, + null, + System.currentTimeMillis() ) ) ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt index 4ef5fa69..0a2ecb81 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt @@ -1,11 +1,16 @@ package com.lagradost.cloudstream3.ui.search import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.databinding.AccountSingleBinding import com.lagradost.cloudstream3.databinding.SearchHistoryItemBinding data class SearchHistoryItem( @@ -58,7 +63,8 @@ class SearchHistoryAdaptor( diffResult.dispatchUpdatesTo(this) } - class CardViewHolder( + class CardViewHolder + constructor( val binding: SearchHistoryItemBinding, private val clickCallback: (SearchHistoryCallback) -> Unit, ) : diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt index 92575e58..d18c0197 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.ui.search -import android.annotation.SuppressLint import android.content.Context import android.view.View import android.widget.ImageView @@ -20,7 +19,7 @@ import com.lagradost.cloudstream3.isMovieType import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull +import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual import com.lagradost.cloudstream3.utils.SubtitleHelper @@ -38,12 +37,16 @@ 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( clickCallback: (SearchClickCallback) -> Unit, card: SearchResponse, position: Int, itemView: View, + nextFocusBehavior: Boolean? = null, nextFocusUp: Int? = null, nextFocusDown: Int? = null, colorCallback : ((Palette) -> Unit)? = null diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SyncSearchViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SyncSearchViewModel.kt index 71077e91..9e03079f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SyncSearchViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SyncSearchViewModel.kt @@ -3,9 +3,11 @@ package com.lagradost.cloudstream3.ui.search import com.lagradost.cloudstream3.SearchQuality import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis -//TODO Relevance of this class since it's not used class SyncSearchViewModel { + private val repos = SyncApis + data class SyncSearchResultSearchResponse( override val name: String, override val url: String, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt index d7bd69f1..1dc79dc0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.ui.settings -import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -14,7 +13,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.setImage class AccountClickCallback(val action: Int, val view: View, val card: AuthAPI.LoginInfo) class AccountAdapter( - private val cardList: List, + val cardList: List, private val clickCallback: (AccountClickCallback) -> Unit ) : RecyclerView.Adapter() { @@ -43,12 +42,12 @@ class AccountAdapter( return cardList[position].accountIndex.toLong() } - class CardViewHolder(val binding: AccountSingleBinding, private val clickCallback: (AccountClickCallback) -> Unit) : + class CardViewHolder + constructor(val binding: AccountSingleBinding, private val clickCallback: (AccountClickCallback) -> Unit) : RecyclerView.ViewHolder(binding.root) { // private val pfp: ImageView = itemView.findViewById(R.id.account_profile_picture)!! // private val accountName: TextView = itemView.findViewById(R.id.account_name)!! - @SuppressLint("StringFormatInvalid") fun bind(card: AuthAPI.LoginInfo) { // just in case name is null account index will show, should never happened binding.accountName.text = card.name ?: "%s %d".format( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 15f8735f..298431ee 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -1,10 +1,8 @@ package com.lagradost.cloudstream3.ui.settings -import android.graphics.Bitmap import android.os.Bundle -import android.os.CountDownTimer import android.view.View -import android.view.View.FOCUS_DOWN +import android.view.View.* import android.view.inputmethod.EditorInfo import android.widget.TextView import androidx.annotation.UiThread @@ -14,7 +12,6 @@ import androidx.core.view.isVisible import androidx.fragment.app.FragmentActivity import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager -import androidx.preference.SwitchPreferenceCompat import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent @@ -23,48 +20,31 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.AccountManagmentBinding import com.lagradost.cloudstream3.databinding.AccountSwitchBinding import com.lagradost.cloudstream3.databinding.AddAccountInputBinding -import com.lagradost.cloudstream3.databinding.DeviceAuthBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.openSubtitlesApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklApi -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subDlApi import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.OAuth2API -import com.lagradost.cloudstream3.ui.result.img -import com.lagradost.cloudstream3.ui.result.setImage -import com.lagradost.cloudstream3.ui.result.setText -import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR -import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import com.lagradost.cloudstream3.utils.AppContextUtils.html +import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.BackupUtils -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.authCallback -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo -import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText -import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.setImage -import qrcode.QRCode -class SettingsAccount : PreferenceFragmentCompat(), BiometricCallback { +class SettingsAccount : PreferenceFragmentCompat() { companion object { /** Used by nginx plugin too */ fun showLoginInfo( @@ -143,109 +123,7 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricCallback { try { when (api) { is OAuth2API -> { - if (isLayout(PHONE) || !api.supportDeviceAuth) { - api.authenticate(activity) - } else if (api.supportDeviceAuth && activity != null) { - - val binding: DeviceAuthBinding = - DeviceAuthBinding.inflate(activity.layoutInflater, null, false) - - val builder = - AlertDialog.Builder(activity) - .setView(binding.root) - - builder.apply { - setNegativeButton(R.string.cancel) { _, _ -> } - setPositiveButton(R.string.auth_locally) { _, _ -> - api.authenticate(activity) - } - } - - val dialog = builder.create() - - ioSafe { - try { - val pinCodeData = api.getDevicePin() - if (pinCodeData == null) { - showToast(R.string.device_pin_error_message) - api.authenticate(activity) - return@ioSafe - } - - /*val logoBytes = ContextCompat.getDrawable( - activity, - R.drawable.cloud_2_solid - )?.toBitmapOrNull()?.let { bitmap -> - val csLogo = ByteArrayOutputStream() - bitmap.compress(Bitmap.CompressFormat.PNG, 100, csLogo) - csLogo.toByteArray() - }*/ - - val qrCodeImage = QRCode.ofRoundedSquares() - .withColor(activity.colorFromAttribute(R.attr.textColor)) - .withBackgroundColor(activity.colorFromAttribute(R.attr.primaryBlackBackground)) - //.withLogo(logoBytes, 200.toPx, 200.toPx) //For later if logo needed anytime - .build(pinCodeData.verificationUrl) - .render().nativeImage() as Bitmap - - activity.runOnUiThread { - dialog.show() - binding.apply { - devicePinCode.setText(txt(pinCodeData.userCode)) - deviceAuthMessage.setText( - txt( - R.string.device_pin_url_message, - pinCodeData.verificationUrl - ) - ) - deviceAuthQrcode.setImage( - img(qrCodeImage) - ) - } - - val expirationMillis = - pinCodeData.expiresIn.times(1000).toLong() - - object : CountDownTimer(expirationMillis, 1000) { - - override fun onTick(millisUntilFinished: Long) { - val secondsUntilFinished = - millisUntilFinished.div(1000).toInt() - - binding.deviceAuthValidationCounter.setText( - txt( - R.string.device_pin_counter_text, - secondsUntilFinished.div(60), - secondsUntilFinished.rem(60) - ) - ) - - ioSafe { - if (secondsUntilFinished.rem(pinCodeData.interval) == 0 && api.handleDeviceAuth(pinCodeData)) { - showToast( - txt( - R.string.authenticated_user, - api.name - ) - ) - dialog.dismissSafe(activity) - cancel() - } - } - } - - override fun onFinish() { - showToast(R.string.device_pin_expired_message) - dialog.dismissSafe(activity) - } - - }.start() - } - } catch (e: Exception) { - logError(e) - } - } - } + api.authenticate(activity) } is InAppAuthAPI -> { @@ -338,15 +216,23 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricCallback { server = if (api.requiresServer) binding.loginServerInput.text?.toString() else null, ) ioSafe { - try { - showToast( - txt( - if (api.login(loginData)) R.string.authenticated_user else R.string.authenticated_user_fail, - api.name - ) - ) + val isSuccessful = try { + api.login(loginData) } catch (e: Exception) { logError(e) + false + } + activity.runOnUiThread { + try { + showToast( + activity.getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail) + .format( + api.name + ) + ) + } catch (e: Exception) { + logError(e) // format might fail + } } } dialog.dismissSafe(activity) @@ -366,31 +252,6 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricCallback { } } - private fun updateAuthPreference(enabled: Boolean) { - val biometricKey = getString(R.string.biometric_key) - - PreferenceManager.getDefaultSharedPreferences(context ?: return).edit() - .putBoolean(biometricKey, enabled).apply() - findPreference(biometricKey)?.isChecked = enabled - } - - override fun onAuthenticationError() { - updateAuthPreference(!isAuthEnabled(context ?: return)) - } - - override fun onAuthenticationSuccess() { - if (isAuthEnabled(context?: return)) { - updateAuthPreference(true) - BackupUtils.backup(activity) - activity?.showBottomDialogText( - getString(R.string.biometric_setting), - getString(R.string.biometric_warning).html() - ) { onDialogDismissedEvent } - } else { - updateAuthPreference(false) - } - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setUpToolbar(R.string.category_account) @@ -402,25 +263,22 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricCallback { hideKeyboard() setPreferencesFromResource(R.xml.settings_account, rootKey) - //Hides the security category on TV as it's only Biometric for now - getPref(R.string.pref_category_security_key)?.hideOn(TV or EMULATOR) + getPref(R.string.biometric_key)?.setOnPreferenceClickListener { + val authEnabled = PreferenceManager.getDefaultSharedPreferences( + context ?: return@setOnPreferenceClickListener false + ) + .getBoolean(getString(R.string.biometric_key), false) - getPref(R.string.biometric_key)?.hideOn(TV or EMULATOR)?.setOnPreferenceClickListener { - val ctx = context ?: return@setOnPreferenceClickListener false - - if (deviceHasPasswordPinLock(ctx)) { - startBiometricAuthentication( - activity?: return@setOnPreferenceClickListener false, - R.string.biometric_authentication_title, - false - ) - promptInfo?.let { - authCallback = this - biometricPrompt?.authenticate(it) - } + if (authEnabled) { + BackupUtils.backup(activity) + val title = activity?.getString(R.string.biometric_setting) + val warning = activity?.getString(R.string.biometric_warning) + activity?.showBottomDialogText( + title as String, + warning.html() + ) { onDialogDismissedEvent } } - - false + true } val syncApis = @@ -429,12 +287,12 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricCallback { R.string.anilist_key to aniListApi, R.string.simkl_key to simklApi, R.string.opensubtitles_key to openSubtitlesApi, - R.string.subdl_key to subDlApi, ) for ((key, api) in syncApis) { getPref(key)?.apply { - title = api.name + title = + getString(R.string.login_format).format(api.name, getString(R.string.account)) setOnPreferenceClickListener { val info = api.loginInfo() if (info != null) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index 88335eea..72e22269 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -1,13 +1,13 @@ package com.lagradost.cloudstream3.ui.settings import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import androidx.annotation.StringRes import androidx.core.view.children +import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.preference.Preference @@ -18,26 +18,18 @@ import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.MainSettingsBinding import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers import com.lagradost.cloudstream3.ui.home.HomeFragment import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR -import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx import java.io.File -import java.text.DateFormat -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import java.util.TimeZone class SettingsFragment : Fragment() { companion object { @@ -53,30 +45,6 @@ class SettingsFragment : Fragment() { } } - /** - * Hide many Preferences on selected layouts. - **/ - fun PreferenceFragmentCompat?.hidePrefs(ids: List, layoutFlags: Int) { - if (this == null) return - - try { - ids.forEach { - getPref(it)?.isVisible = !isLayout(layoutFlags) - } - } catch (e: Exception) { - logError(e) - } - } - - /** - * Hide the Preference on selected layouts. - **/ - fun Preference?.hideOn(layoutFlags: Int): Preference? { - if (this == null) return null - this.isVisible = !isLayout(layoutFlags) - return this - } - /** * On TV you cannot properly scroll to the bottom of settings, this fixes that. * */ @@ -109,11 +77,9 @@ class SettingsFragment : Fragment() { settingsToolbar.apply { setTitle(title) - if (isLayout(PHONE or EMULATOR)) { - setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) - setNavigationOnClickListener { - activity?.onBackPressedDispatcher?.onBackPressed() - } + setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) + setNavigationOnClickListener { + activity?.onBackPressedDispatcher?.onBackPressed() } } UIHelper.fixPaddingStatusbar(settingsToolbar) @@ -125,12 +91,10 @@ class SettingsFragment : Fragment() { settingsToolbar.apply { setTitle(title) - if (isLayout(PHONE or EMULATOR)) { - setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) - children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag) - setNavigationOnClickListener { - activity?.onBackPressedDispatcher?.onBackPressed() - } + setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) + children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag) + setNavigationOnClickListener { + activity?.onBackPressedDispatcher?.onBackPressed() } } UIHelper.fixPaddingStatusbar(settingsToolbar) @@ -164,6 +128,7 @@ class SettingsFragment : Fragment() { val localBinding = MainSettingsBinding.inflate(inflater, container, false) binding = localBinding return localBinding.root + //return inflater.inflate(R.layout.main_settings, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -171,44 +136,21 @@ class SettingsFragment : Fragment() { activity?.navigate(id, Bundle()) } - /** used to debug leaks - showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : - ${VideoDownloadManager.downloadProgressEvent.size}") **/ + // used to debug leaks showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : ${VideoDownloadManager.downloadProgressEvent.size}") - fun hasProfilePictureFromAccountManagers(accountManagers: List): Boolean { - for (syncApi in accountManagers) { - val login = syncApi.loginInfo() - val pic = login?.profilePicture ?: continue - - if (binding?.settingsProfilePic?.setImage( - pic, - errorImageDrawable = HomeFragment.errorProfilePic - ) == true - ) { - binding?.settingsProfileText?.text = login.name - return true // sync profile exists - } + for (syncApi in accountManagers) { + val login = syncApi.loginInfo() + val pic = login?.profilePicture ?: continue + if (binding?.settingsProfilePic?.setImage( + pic, + errorImageDrawable = HomeFragment.errorProfilePic + ) == true + ) { + binding?.settingsProfileText?.text = login.name + binding?.settingsProfile?.isVisible = true + break } - return false // not syncing } - - // display local account information if not syncing - if (!hasProfilePictureFromAccountManagers(accountManagers)) { - val activity = activity ?: return - val currentAccount = try { - DataStoreHelper.accounts.firstOrNull { - it.keyIndex == DataStoreHelper.selectedKeyIndex - } ?: activity.let { DataStoreHelper.getDefaultAccount(activity) } - - } catch (t: IllegalStateException) { - Log.e("AccountManager", "Activity not found", t) - null - } - - binding?.settingsProfilePic?.setImage(currentAccount?.image) - binding?.settingsProfileText?.text = currentAccount?.name - } - binding?.apply { listOf( settingsGeneral to R.id.action_navigation_global_to_navigation_settings_general, @@ -238,14 +180,12 @@ class SettingsFragment : Fragment() { val appVersion = getString(R.string.app_version) val commitInfo = getString(R.string.commit_hash) - val buildTimestamp = SimpleDateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, - Locale.getDefault() - ).apply { timeZone = TimeZone.getTimeZone("UTC") - }.format(Date(BuildConfig.BUILD_DATE)).replace("UTC", "") + val buildDate = BuildConfig.BUILDDATE - binding?.buildDate?.text = buildTimestamp - binding?.appVersionInfo?.setOnLongClickListener { - clipboardHelper(txt(R.string.extension_version), "$appVersion $commitInfo $buildTimestamp") + binding?.buildDate?.text = buildDate + + binding?.appVersionInfo?.setOnLongClickListener{ + clipboardHelper(txt(R.string.extension_version), "$appVersion $commitInfo") true } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index 7cb1a848..6cf00375 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -27,16 +27,11 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.ui.EasterEggMonke -import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR -import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.beneneCount import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import com.lagradost.cloudstream3.utils.BatteryOptimizationChecker.isAppRestricted -import com.lagradost.cloudstream3.utils.BatteryOptimizationChecker.showBatteryOptimizationDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog @@ -50,6 +45,7 @@ import com.lagradost.safefile.SafeFile // Change local language settings in the app. fun getCurrentLocale(context: Context): String { + // val dm = res.displayMetrics val res = context.resources val conf = res.configuration @@ -72,7 +68,6 @@ val appLanguages = arrayListOf( Triple("", "አማርኛ", "am"), Triple("", "العربية", "ar"), Triple("", "اللهجة النجدية", "ars"), - Triple("", "অসমীয়া", "as"), Triple("", "български", "bg"), Triple("", "বাংলা", "bn"), Triple("\uD83C\uDDE7\uD83C\uDDF7", "português brasileiro", "bp"), @@ -100,7 +95,6 @@ val appLanguages = arrayListOf( Triple("", "македонски", "mk"), Triple("", "മലയാളം", "ml"), Triple("", "bahasa Melayu", "ms"), - Triple("", "Malti", "mt"), Triple("", "ဗမာစာ", "my"), Triple("", "नेपाली", "ne"), Triple("", "Nederlands", "nl"), @@ -210,18 +204,6 @@ class SettingsGeneral : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } - getPref(R.string.battery_optimisation_key)?.hideOn(TV or EMULATOR)?.setOnPreferenceClickListener { - val ctx = context ?: return@setOnPreferenceClickListener false - - if (isAppRestricted(ctx)) { - showBatteryOptimizationDialog(ctx) - } else { - showToast(R.string.app_unrestricted_toast) - } - - true - } - fun showAdd() { val providers = synchronized(allProviders) { allProviders.distinctBy { it.javaClass }.sortedBy { it.name } } activity?.showDialog( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt index 1753032a..3d0bcb1f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt @@ -7,13 +7,8 @@ import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR -import com.lagradost.cloudstream3.ui.settings.Globals.PHONE -import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getFolderSize import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hidePrefs import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar @@ -36,19 +31,6 @@ class SettingsPlayer : PreferenceFragmentCompat() { setPreferencesFromResource(R.xml.settings_player, rootKey) val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()) - //Hide specific prefs on TV/EMULATOR - hidePrefs( - listOf( - R.string.pref_category_gestures_key, - R.string.rotate_video_key, - R.string.auto_rotate_video_key - ), - TV or EMULATOR - ) - - getPref(R.string.preview_seekbar_key)?.hideOn(TV) - getPref(R.string.pref_category_android_tv_key)?.hideOn(PHONE) - getPref(R.string.video_buffer_length_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.video_buffer_length_names) val prefValues = resources.getIntArray(R.array.video_buffer_length_values) @@ -87,6 +69,10 @@ class SettingsPlayer : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } + /*(getPref(R.string.double_tap_seek_time_key) as? SeekBarPreference?)?.let { + + }*/ + getPref(R.string.prefer_limit_title_rez_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.limit_title_rez_pref_names) val prefValues = resources.getIntArray(R.array.limit_title_rez_pref_values) @@ -105,10 +91,8 @@ class SettingsPlayer : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } - getPref(R.string.hide_player_control_names_key)?.hideOn(TV) - getPref(R.string.quality_pref_key)?.setOnPreferenceClickListener { - val prefValues = Qualities.entries.map { it.value }.reversed().toMutableList() + val prefValues = Qualities.values().map { it.value }.reversed().toMutableList() prefValues.remove(Qualities.Unknown.value) val prefNames = prefValues.map { Qualities.getStringByInt(it) } @@ -116,7 +100,7 @@ class SettingsPlayer : PreferenceFragmentCompat() { val currentQuality = settingsManager.getInt( getString(R.string.quality_pref_key), - Qualities.entries.last().value + Qualities.values().last().value ) activity?.showBottomDialog( @@ -132,7 +116,7 @@ class SettingsPlayer : PreferenceFragmentCompat() { } getPref(R.string.quality_pref_mobile_data_key)?.setOnPreferenceClickListener { - val prefValues = Qualities.entries.map { it.value }.reversed().toMutableList() + val prefValues = Qualities.values().map { it.value }.reversed().toMutableList() prefValues.remove(Qualities.Unknown.value) val prefNames = prefValues.map { Qualities.getStringByInt(it) } @@ -140,7 +124,7 @@ class SettingsPlayer : PreferenceFragmentCompat() { val currentQuality = settingsManager.getInt( getString(R.string.quality_pref_mobile_data_key), - Qualities.entries.last().value + Qualities.values().last().value ) activity?.showBottomDialog( @@ -243,5 +227,6 @@ class SettingsPlayer : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } } + } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt index cb7d25fd..7dc73a46 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt @@ -7,17 +7,19 @@ import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings +import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings +import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import com.lagradost.cloudstream3.utils.AppContextUtils.getApiDubstatusSettings -import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard +import com.lagradost.cloudstream3.utils.UIHelper.navigate class SettingsProviders : PreferenceFragmentCompat() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -34,7 +36,7 @@ class SettingsProviders : PreferenceFragmentCompat() { getPref(R.string.display_sub_key)?.setOnPreferenceClickListener { activity?.getApiDubstatusSettings()?.let { current -> - val dublist = DubStatus.entries + val dublist = DubStatus.values() val names = dublist.map { it.name } val currentList = ArrayList() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt index 8c3ad0ad..cc14e761 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt @@ -88,9 +88,10 @@ class SettingsUI : PreferenceFragmentCompat() { getPref(R.string.app_theme_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.themes_names).toMutableList() val prefValues = resources.getStringArray(R.array.themes_names_values).toMutableList() - val removeIncompatible = { text: String -> + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // remove monet on android 11 and less val toRemove = prefValues - .mapIndexed { idx, s -> if (s.startsWith(text)) idx else null } + .mapIndexed { idx, s -> if (s.startsWith("Monet")) idx else null } .filterNotNull() var offset = 0 toRemove.forEach { idx -> @@ -99,12 +100,6 @@ class SettingsUI : PreferenceFragmentCompat() { offset += 1 } } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // remove monet on android 11 and less - removeIncompatible("Monet") - } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { // Remove system on android 9 and less - removeIncompatible("System") - } val currentLayout = settingsManager.getString(getString(R.string.app_theme_key), prefValues.first()) @@ -128,8 +123,7 @@ class SettingsUI : PreferenceFragmentCompat() { } getPref(R.string.primary_color_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.themes_overlay_names).toMutableList() - val prefValues = - resources.getStringArray(R.array.themes_overlay_names_values).toMutableList() + val prefValues = resources.getStringArray(R.array.themes_overlay_names_values).toMutableList() if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // remove monet on android 11 and less val toRemove = prefValues diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt index 260c6674..fb24c185 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt @@ -35,9 +35,6 @@ import okhttp3.internal.closeQuietly import java.io.BufferedReader import java.io.InputStreamReader import java.io.OutputStream -import java.lang.System.currentTimeMillis -import java.text.SimpleDateFormat -import java.util.* class SettingsUpdates : PreferenceFragmentCompat() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -128,12 +125,12 @@ class SettingsUpdates : PreferenceFragmentCompat() { } binding.saveBtt.setOnClickListener { - val date = SimpleDateFormat("yyyy_MM_dd_HH_mm", Locale.getDefault()).format(Date(currentTimeMillis())) var fileStream: OutputStream? = null try { - fileStream = VideoDownloadManager.setupStream( + fileStream = + VideoDownloadManager.setupStream( it.context, - "logcat_${date}", + "logcat", null, "txt", false @@ -169,10 +166,10 @@ class SettingsUpdates : PreferenceFragmentCompat() { prefValues.indexOf(currentInstaller), getString(R.string.apk_installer_settings), true, - {}) { num -> + {}) { try { settingsManager.edit() - .putInt(getString(R.string.apk_installer_key), prefValues[num]) + .putInt(getString(R.string.apk_installer_key), prefValues[it]) .apply() } catch (e: Exception) { logError(e) @@ -209,9 +206,9 @@ class SettingsUpdates : PreferenceFragmentCompat() { prefValues.indexOf(current), getString(R.string.automatic_plugin_download_mode_title), true, - {}) { num -> + {}) { settingsManager.edit() - .putInt(getString(R.string.auto_download_plugins_key), prefValues[num]).apply() + .putInt(getString(R.string.auto_download_plugins_key), prefValues[it]).apply() (context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) } } return@setOnPreferenceClickListener true diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt index 1b487629..ebd3260f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt @@ -33,8 +33,8 @@ import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import com.lagradost.cloudstream3.utils.AppContextUtils.addRepositoryDialog -import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppUtils.downloadAllPluginsDialog +import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe @@ -273,9 +273,9 @@ class ExtensionsFragment : Fragment() { if (plugins.isNullOrEmpty()) { showToast(R.string.no_plugins_found_error, Toast.LENGTH_LONG) } else { - this@ExtensionsFragment.activity?.addRepositoryDialog( - fixedName, + this@ExtensionsFragment.activity?.downloadAllPluginsDialog( url, + fixedName ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt index d159539d..04da30c7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt @@ -1,11 +1,9 @@ package com.lagradost.cloudstream3.ui.settings.extensions -import android.annotation.SuppressLint import android.text.format.Formatter.formatShortFileSize import android.util.Log import android.view.LayoutInflater import android.view.ViewGroup -import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isGone import androidx.core.view.isVisible @@ -14,7 +12,6 @@ import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.RepositoryItemBinding import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.VotingApi.getVotes @@ -22,19 +19,19 @@ import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppContextUtils.html +import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx +import org.junit.Assert +import org.junit.Test import java.text.DecimalFormat import kotlin.math.floor import kotlin.math.log10 -import kotlin.math.pow -import org.junit.Test -import org.junit.Assert + data class PluginViewData( val plugin: Plugin, @@ -103,8 +100,6 @@ class PluginAdapter( return findClosestBase2(target, current * 2, max) } - // DO NOT MOVE, as running this test will result in ExceptionInInitializerError on prerelease due to static variables using Resources.getSystem() - // this test function is only to show how the function works @Test fun testFindClosestBase2() { Assert.assertEquals(16, findClosestBase2(0)) @@ -126,7 +121,10 @@ class PluginAdapter( val base = value / 3 return if (value >= 3 && base < suffix.size) { DecimalFormat("#0.00").format( - numValue / 10.0.pow((base * 3).toDouble()) + numValue / Math.pow( + 10.0, + (base * 3).toDouble() + ) ) + suffix[base] } else { DecimalFormat().format(numValue) @@ -137,7 +135,6 @@ class PluginAdapter( inner class PluginViewHolder(val binding: RepositoryItemBinding) : RecyclerView.ViewHolder(binding.root) { - @SuppressLint("SetTextI18n") fun bind( data: PluginViewData, ) { @@ -153,7 +150,7 @@ class PluginAdapter( R.drawable.ic_baseline_delete_outline_24 else R.drawable.netflix_download - binding.nsfwMarker.isVisible = metadata.tvTypes?.contains(TvType.NSFW.name) ?: false + binding.nsfwMarker.isVisible = metadata.tvTypes?.contains("NSFW") ?: false binding.actionButton.setImageResource(drawableInt) binding.actionButton.setOnClickListener { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt index 4878049b..acfbc584 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt @@ -8,8 +8,8 @@ import androidx.appcompat.widget.SearchView import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.AllLanguagesName -import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.FragmentPluginsBinding @@ -23,7 +23,6 @@ import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar import com.lagradost.cloudstream3.ui.settings.appLanguages -import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.toPx @@ -71,8 +70,6 @@ class PluginsFragment : Fragment() { val name = arguments?.getString(PLUGINS_BUNDLE_NAME) val url = arguments?.getString(PLUGINS_BUNDLE_URL) val isLocal = arguments?.getBoolean(PLUGINS_BUNDLE_LOCAL) == true - // download all extensions button - val downloadAllButton = binding?.settingsToolbar?.menu?.findItem(R.id.download_all) if (url == null || name == null) { activity?.onBackPressedDispatcher?.onBackPressed() @@ -174,7 +171,7 @@ class PluginsFragment : Fragment() { if (isLocal) { // No download button and no categories on local - downloadAllButton?.isVisible = false + binding?.settingsToolbar?.menu?.findItem(R.id.download_all)?.isVisible = false binding?.settingsToolbar?.menu?.findItem(R.id.lang_filter)?.isVisible = false pluginViewModel.updatePluginListLocal() @@ -182,15 +179,11 @@ class PluginsFragment : Fragment() { } else { pluginViewModel.updatePluginList(context, url) binding?.tvtypesChipsScroll?.root?.isVisible = true - // not needed for users but may be useful for devs - downloadAllButton?.isVisible = BuildConfig.DEBUG - - bindChips( binding?.tvtypesChipsScroll?.tvtypesChips, emptyList(), - TvType.entries.toList(), + TvType.values().toList(), callback = { list -> pluginViewModel.tvTypes.clear() pluginViewModel.tvTypes.addAll(list.map { it.name }) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt index fd5422b2..151c8d57 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt @@ -8,11 +8,9 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.plugins.PluginManager @@ -98,7 +96,6 @@ class PluginsViewModel : ViewModel() { R.string.batch_download_nothing_to_download_format, txt(R.string.plugin) ) - else -> txt( R.string.batch_download_start_format, list.size, @@ -184,15 +181,8 @@ class PluginsViewModel : ViewModel() { } private suspend fun updatePluginListPrivate(context: Context, repositoryUrl: String) { - val isAdult = PreferenceManager.getDefaultSharedPreferences(context) - .getStringSet(context.getString(R.string.prefer_media_type_key), emptySet()) - ?.contains(TvType.NSFW.ordinal.toString()) == true - val plugins = getPlugins(repositoryUrl) - val list = plugins.filter { - // Show all non-nsfw plugins or all if nsfw is enabled - it.second.tvTypes?.contains(TvType.NSFW.name) != true || isAdult - }.map { plugin -> + val list = plugins.map { plugin -> PluginViewData(plugin, isDownloaded(context, plugin.second.internalName, plugin.first)) } @@ -207,7 +197,7 @@ class PluginsViewModel : ViewModel() { if (tvTypes.isEmpty()) return this return this.filter { (it.plugin.second.tvTypes?.any { type -> tvTypes.contains(type) } == true) || - (tvTypes.contains(TvType.Others.name) && (it.plugin.second.tvTypes + (tvTypes.contains("Others") && (it.plugin.second.tvTypes ?: emptyList()).isEmpty()) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt index bad58a0e..83480542 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt @@ -2,31 +2,26 @@ package com.lagradost.cloudstream3.ui.settings.testing import android.app.AlertDialog import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView -import android.widget.Toast import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView -import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.ProviderTestItemBinding import com.lagradost.cloudstream3.mvvm.getAllMessages import com.lagradost.cloudstream3.mvvm.getStackTracePretty -import com.lagradost.cloudstream3.plugins.PluginManager -import com.lagradost.cloudstream3.utils.AppContextUtils -import com.lagradost.cloudstream3.utils.Coroutines.ioSafe -import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread +import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso import com.lagradost.cloudstream3.utils.TestingUtils -import java.io.File class TestResultAdapter(override val items: MutableList>) : - AppContextUtils.DiffAdapter>(items) { + AppUtils.DiffAdapter>(items) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return ProviderTestViewHolder( - ProviderTestItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ProviderTestItemBinding.inflate(LayoutInflater.from(parent.context), parent,false) //LayoutInflater.from(parent.context) // .inflate(R.layout.provider_test_item, parent, false), ) @@ -41,8 +36,7 @@ class TestResultAdapter(override val items: MutableList } - - api.sourcePlugin?.let { path -> - val pluginFile = File(path) - // Cannot delete a deleted plugin - if (!pluginFile.exists()) return@let - - builder.setNegativeButton(R.string.delete_plugin) { _, _ -> - ioSafe { - val success = PluginManager.deletePlugin(pluginFile) - - runOnMainThread { - if (success) { - showToast(R.string.plugin_deleted, Toast.LENGTH_SHORT) - } else { - showToast(R.string.error, Toast.LENGTH_SHORT) - } - } - } - } - } - - builder.show() + .show() } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestView.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestView.kt index eea495a2..26513f4a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestView.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestView.kt @@ -13,7 +13,7 @@ import androidx.core.view.isVisible import androidx.core.widget.ContentLoadingProgressBar import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.utils.AppContextUtils.animateProgressTo +import com.lagradost.cloudstream3.utils.AppUtils.animateProgressTo class TestView @JvmOverloads constructor( context: Context, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt index 818f1fd7..9e6f8a06 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt @@ -95,7 +95,7 @@ class TestViewModel : ViewModel() { providers.clear() updateProgress() - TestingUtils.getDeferredProviderTests(scope ?: return, apis) { api, result -> + TestingUtils.getDeferredProviderTests(scope ?: return, apis, ::println) { api, result -> addProvider(api, result) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt index 49a93608..f9197213 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt @@ -10,6 +10,7 @@ import androidx.core.util.forEach import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager +import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.FragmentSetupMediaBinding diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt index c12e9eb8..59dcc402 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt @@ -11,11 +11,11 @@ import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.APIHolder +import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.AllLanguagesName import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentSetupProviderLanguagesBinding import com.lagradost.cloudstream3.mvvm.normalSafeApiCall -import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt index c76a218e..bb9558b8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt @@ -15,11 +15,8 @@ import android.widget.Toast import androidx.fragment.app.Fragment import androidx.media3.common.text.Cue import com.fasterxml.jackson.annotation.JsonProperty -import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_DEPRESSED -import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_DROP_SHADOW -import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_NONE -import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_OUTLINE -import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_RAISED +import com.google.android.gms.cast.TextTrackStyle +import com.google.android.gms.cast.TextTrackStyle.* import com.jaredrummler.android.colorpicker.ColorPickerDialog import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent @@ -45,7 +42,7 @@ data class SaveChromeCaptionStyle( @JsonProperty("fontGenericFamily") var fontGenericFamily: Int? = null, @JsonProperty("backgroundColor") var backgroundColor: Int = 0x00FFFFFF, // transparent @JsonProperty("edgeColor") var edgeColor: Int = Color.BLACK, // BLACK - @JsonProperty("edgeType") var edgeType: Int = EDGE_TYPE_OUTLINE, + @JsonProperty("edgeType") var edgeType: Int = TextTrackStyle.EDGE_TYPE_OUTLINE, @JsonProperty("foregroundColor") var foregroundColor: Int = Color.WHITE, @JsonProperty("fontScale") var fontScale: Float = 1.05f, @JsonProperty("windowColor") var windowColor: Int = Color.TRANSPARENT, @@ -102,7 +99,7 @@ class ChromecastSubtitlesFragment : Fragment() { } private fun onColorSelected(stuff: Pair) { - setColor(stuff.first, stuff.second) + context?.setColor(stuff.first, stuff.second) if (hide) activity?.hideSystemUI() } @@ -125,7 +122,7 @@ class ChromecastSubtitlesFragment : Fragment() { return if (color == Color.TRANSPARENT) Color.BLACK else color } - private fun setColor(id: Int, color: Int?) { + private fun Context.setColor(id: Int, color: Int?) { val realColor = color ?: getDefColor(id) when (id) { 0 -> state.foregroundColor = realColor @@ -138,7 +135,7 @@ class ChromecastSubtitlesFragment : Fragment() { updateState() } - private fun updateState() { + private fun Context.updateState() { //subtitle_text?.setStyle(fromSaveToStyle(state)) } @@ -176,7 +173,7 @@ class ChromecastSubtitlesFragment : Fragment() { fixPaddingStatusbar(binding?.subsRoot) state = getCurrentSavedStyle() - updateState() + context?.updateState() val isTvSettings = isLayout(TV or EMULATOR) @@ -198,7 +195,7 @@ class ChromecastSubtitlesFragment : Fragment() { } this.setOnLongClickListener { - setColor(id, null) + it.context.setColor(id, null) showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true } @@ -250,13 +247,13 @@ class ChromecastSubtitlesFragment : Fragment() { dismissCallback ) { index -> state.edgeType = edgeTypes.map { it.first }[index] - updateState() + textView.context.updateState() } } binding?.subsEdgeType?.setOnLongClickListener { state.edgeType = defaultState.edgeType - updateState() + it.context.updateState() showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true } @@ -326,12 +323,12 @@ class ChromecastSubtitlesFragment : Fragment() { dismissCallback ) { index -> state.fontFamily = fontTypes.map { it.first }[index] - updateState() + textView.context.updateState() } } - binding?.subsFont?.setOnLongClickListener { _ -> + binding?.subsFont?.setOnLongClickListener { textView -> state.fontFamily = defaultState.fontFamily - updateState() + textView.context.updateState() showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt index 8821905e..1466afed 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt @@ -14,13 +14,11 @@ import android.view.ViewGroup import android.widget.TextView import android.widget.Toast import androidx.annotation.FontRes -import androidx.annotation.OptIn import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.Fragment import androidx.preference.PreferenceManager import com.fasterxml.jackson.annotation.JsonProperty import androidx.media3.common.text.Cue -import androidx.media3.common.util.UnstableApi import androidx.media3.ui.CaptionStyleCompat import com.jaredrummler.android.colorpicker.ColorPickerDialog import com.lagradost.cloudstream3.AcraApplication.Companion.getKey @@ -30,6 +28,7 @@ import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.SubtitleSettingsBinding +import com.lagradost.cloudstream3.ui.settings.Globals import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.DataStore.setKey @@ -47,7 +46,7 @@ const val SUBTITLE_KEY = "subtitle_settings" const val SUBTITLE_AUTO_SELECT_KEY = "subs_auto_select" const val SUBTITLE_DOWNLOAD_KEY = "subs_auto_download" -data class SaveCaptionStyle @OptIn(UnstableApi::class) constructor( +data class SaveCaptionStyle( @JsonProperty("foregroundColor") var foregroundColor: Int, @JsonProperty("backgroundColor") var backgroundColor: Int, @JsonProperty("windowColor") var windowColor: Int, @@ -68,7 +67,7 @@ data class SaveCaptionStyle @OptIn(UnstableApi::class) constructor( const val DEF_SUBS_ELEVATION = 20 -@OptIn(androidx.media3.common.util.UnstableApi::class) +@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) class SubtitlesFragment : Fragment() { companion object { val applyStyleEvent = Event() @@ -168,7 +167,7 @@ class SubtitlesFragment : Fragment() { activity?.hideSystemUI() } - private fun onDialogDismissed(@Suppress("UNUSED_PARAMETER") id: Int) { + private fun onDialogDismissed(id: Int) { if (hide) activity?.hideSystemUI() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt index f0c948a4..e9b69c5b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt @@ -83,7 +83,7 @@ object EpisodeSkip { startMs = start, endMs = end ) - }.let { list -> + }?.let { list -> out.addAll(list) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt similarity index 78% rename from app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt rename to app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt index 8d65acf7..ff27b192 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -43,6 +43,7 @@ import androidx.recyclerview.widget.RecyclerView import androidx.tvprovider.media.tv.* import androidx.tvprovider.media.tv.WatchNextProgram.fromCursor import androidx.viewpager2.widget.ViewPager2 +import com.fasterxml.jackson.module.kotlin.readValue import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastState import com.google.android.gms.common.ConnectionResult @@ -50,20 +51,18 @@ import com.google.android.gms.common.GoogleApiAvailability import com.google.android.gms.common.wrappers.Wrappers import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEvent import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.plugins.RepositoryManager -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_RESUME_WATCHING +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching import com.lagradost.cloudstream3.syncproviders.providers.Kitsu import com.lagradost.cloudstream3.ui.WebviewFragment -import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.settings.Globals -import com.lagradost.cloudstream3.ui.settings.extensions.PluginsFragment +import com.lagradost.cloudstream3.ui.settings.extensions.PluginsViewModel.Companion.downloadAll import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main @@ -79,7 +78,7 @@ import java.io.* import java.net.URL import java.net.URLDecoder -object AppContextUtils { +object AppUtils { fun RecyclerView.setMaxViewPoolSize(maxViewTypeId: Int, maxPoolSize: Int) { for (i in 0..maxViewTypeId) recycledViewPool.setMaxRecycledViews(i, maxPoolSize) @@ -160,7 +159,7 @@ object AppContextUtils { .setTitle(title) .setPosterArtUri(Uri.parse(card.posterUrl)) .setIntentUri(Uri.parse(card.id?.let { - "$APP_STRING_RESUME_WATCHING://$it" + "$appStringResumeWatching://$it" } ?: card.url)) .setInternalProviderId(card.url) .setLastEngagementTimeUtcMillis( @@ -371,168 +370,6 @@ object AppContextUtils { } } - fun sortSubs(subs: Set): List { - return subs.sortedBy { it.name } - } - - fun Context.getApiSettings(): HashSet { - //val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - - val hashSet = HashSet() - val activeLangs = getApiProviderLangSettings() - val hasUniversal = activeLangs.contains(AllLanguagesName) - hashSet.addAll(synchronized(apis) { apis.filter { hasUniversal || activeLangs.contains(it.lang) } } - .map { it.name }) - - /*val set = settingsManager.getStringSet( - this.getString(R.string.search_providers_list_key), - hashSet - )?.toHashSet() ?: hashSet - - val list = HashSet() - for (name in set) { - val api = getApiFromNameNull(name) ?: continue - if (activeLangs.contains(api.lang)) { - list.add(name) - } - }*/ - //if (list.isEmpty()) return hashSet - //return list - return hashSet - } - - fun Context.getApiDubstatusSettings(): HashSet { - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - val hashSet = HashSet() - hashSet.addAll(DubStatus.values()) - val list = settingsManager.getStringSet( - this.getString(R.string.display_sub_key), - hashSet.map { it.name }.toMutableSet() - ) ?: return hashSet - - val names = DubStatus.values().map { it.name }.toHashSet() - //if(realSet.isEmpty()) return hashSet - - return list.filter { names.contains(it) }.map { DubStatus.valueOf(it) }.toHashSet() - } - - fun Context.getApiProviderLangSettings(): HashSet { - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - val hashSet = hashSetOf(AllLanguagesName) // def is all languages -// hashSet.add("en") // def is only en - val list = settingsManager.getStringSet( - this.getString(R.string.provider_lang_key), - hashSet - ) - - if (list.isNullOrEmpty()) return hashSet - return list.toHashSet() - } - - fun Context.getApiTypeSettings(): HashSet { - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - val hashSet = HashSet() - hashSet.addAll(TvType.values()) - val list = settingsManager.getStringSet( - this.getString(R.string.search_types_list_key), - hashSet.map { it.name }.toMutableSet() - ) - - if (list.isNullOrEmpty()) return hashSet - - val names = TvType.values().map { it.name }.toHashSet() - val realSet = list.filter { names.contains(it) }.map { TvType.valueOf(it) }.toHashSet() - if (realSet.isEmpty()) return hashSet - - return realSet - } - - fun Context.updateHasTrailers() { - LoadResponse.isTrailersEnabled = getHasTrailers() - } - - private fun Context.getHasTrailers(): Boolean { - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - return settingsManager.getBoolean(this.getString(R.string.show_trailers_key), true) - } - - fun Context.filterProviderByPreferredMedia(hasHomePageIsRequired: Boolean = true): List { - // We are getting the weirdest crash ever done: - // java.lang.ClassCastException: com.lagradost.cloudstream3.TvType cannot be cast to com.lagradost.cloudstream3.TvType - // Trying fixing using classloader fuckery - val oldLoader = Thread.currentThread().contextClassLoader - Thread.currentThread().contextClassLoader = TvType::class.java.classLoader - - val default = TvType.values() - .sorted() - .filter { it != TvType.NSFW } - .map { it.ordinal } - - Thread.currentThread().contextClassLoader = oldLoader - - val defaultSet = default.map { it.toString() }.toSet() - val currentPrefMedia = try { - PreferenceManager.getDefaultSharedPreferences(this) - .getStringSet(this.getString(R.string.prefer_media_type_key), defaultSet) - ?.mapNotNull { it.toIntOrNull() ?: return@mapNotNull null } - } catch (e: Throwable) { - null - } ?: default - val langs = this.getApiProviderLangSettings() - val hasUniversal = langs.contains(AllLanguagesName) - val allApis = synchronized(apis) { - apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) } - } - return if (currentPrefMedia.isEmpty()) { - allApis - } else { - // Filter API depending on preferred media type - allApis.filter { api -> api.supportedTypes.any { currentPrefMedia.contains(it.ordinal) } } - } - } - - fun Context.filterSearchResultByFilmQuality(data: List): List { - // Filter results omitting entries with certain quality - if (data.isNotEmpty()) { - val filteredSearchQuality = PreferenceManager.getDefaultSharedPreferences(this) - ?.getStringSet(getString(R.string.pref_filter_search_quality_key), setOf()) - ?.mapNotNull { entry -> - entry.toIntOrNull() ?: return@mapNotNull null - } ?: listOf() - if (filteredSearchQuality.isNotEmpty()) { - return data.filter { item -> - val searchQualVal = item.quality?.ordinal ?: -1 - //Log.i("filterSearch", "QuickSearch item => ${item.toJson()}") - !filteredSearchQuality.contains(searchQualVal) - } - } - } - return data - } - - fun Context.filterHomePageListByFilmQuality(data: HomePageList): HomePageList { - // Filter results omitting entries with certain quality - if (data.list.isNotEmpty()) { - val filteredSearchQuality = PreferenceManager.getDefaultSharedPreferences(this) - ?.getStringSet(getString(R.string.pref_filter_search_quality_key), setOf()) - ?.mapNotNull { entry -> - entry.toIntOrNull() ?: return@mapNotNull null - } ?: listOf() - if (filteredSearchQuality.isNotEmpty()) { - return HomePageList( - name = data.name, - isHorizontalImages = data.isHorizontalImages, - list = data.list.filter { item -> - val searchQualVal = item.quality?.ordinal ?: -1 - //Log.i("filterSearch", "QuickSearch item => ${item.toJson()}") - !filteredSearchQuality.contains(searchQualVal) - } - ) - } - } - return data - } - fun Activity.loadRepository(url: String) { ioSafe { val repo = RepositoryManager.parseRepository(url) ?: return@ioSafe @@ -549,7 +386,7 @@ object AppContextUtils { ) } afterRepositoryLoadedEvent.invoke(true) - addRepositoryDialog(repo.name, url) + downloadAllPluginsDialog(url, repo.name) } } @@ -592,36 +429,25 @@ object AppContextUtils { } } - fun Activity.addRepositoryDialog( - repositoryName: String, - repositoryURL: String, - ) { - val repos = RepositoryManager.getRepositories() - - // navigate to newly added repository on pressing Open Repository - fun openAddedRepo() { - if (repos.isNotEmpty()) { - navigate( - R.id.global_to_navigation_settings_plugins, - PluginsFragment.newInstance( - repositoryName, - repositoryURL, - false, - ) - ) - } - } + fun Activity.downloadAllPluginsDialog(repositoryUrl: String, repositoryName: String) { runOnUiThread { - AlertDialog.Builder(this).apply { - setTitle(repositoryName) - setMessage(R.string.download_all_plugins_from_repo) - setPositiveButton(R.string.open_downloaded_repo) { _, _ -> - openAddedRepo() + val context = this + val builder: AlertDialog.Builder = AlertDialog.Builder(this) + builder.setTitle( + repositoryName + ) + builder.setMessage( + R.string.download_all_plugins_from_repo + ) + builder.apply { + setPositiveButton(R.string.download) { _, _ -> + downloadAll(context, repositoryUrl, null) } - setNegativeButton(R.string.dismiss, null) - show().setDefaultFocus() + + setNegativeButton(R.string.no) { _, _ -> } } + builder.show().setDefaultFocus() } } @@ -677,15 +503,9 @@ object AppContextUtils { } fun Context.isNetworkAvailable(): Boolean { - val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val network = connectivityManager.activeNetwork ?: return false - val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false - networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - } else { - @Suppress("DEPRECATION") - connectivityManager.activeNetworkInfo?.isConnected == true - } + val manager = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val activeNetworkInfo = manager.activeNetworkInfo + return activeNetworkInfo != null && activeNetworkInfo.isConnected || manager.allNetworkInfo?.any { it.isConnected } ?: false } fun splitQuery(url: URL): Map { @@ -700,6 +520,24 @@ object AppContextUtils { return queryPairs } + /** Any object as json string */ + fun Any.toJson(): String { + if (this is String) return this + return mapper.writeValueAsString(this) + } + + inline fun parseJson(value: String): T { + return mapper.readValue(value) + } + + inline fun tryParseJson(value: String?): T? { + return try { + parseJson(value ?: return null) + } catch (_: Exception) { + null + } + } + /**| S1:E2 Hello World * | Episode 2. Hello world * | Hello World @@ -769,7 +607,7 @@ object AppContextUtils { val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) Kitsu.isEnabled = settingsManager.getBoolean(this.getString(R.string.show_kitsu_posters_key), true) - } catch (t: Throwable) { + }catch (t : Throwable) { logError(t) } @@ -1024,4 +862,4 @@ object AppContextUtils { } return currentAudioFocusRequest } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackPressedCallbackHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackPressedCallbackHelper.kt deleted file mode 100644 index 1326ab27..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackPressedCallbackHelper.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.lagradost.cloudstream3.utils - -import androidx.activity.ComponentActivity -import androidx.activity.OnBackPressedCallback - -object BackPressedCallbackHelper { - private var backPressedCallback: OnBackPressedCallback? = null - - fun ComponentActivity.attachBackPressedCallback(callback: () -> Unit) { - if (backPressedCallback == null) { - backPressedCallback = object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - callback.invoke() - } - } - } - - backPressedCallback?.isEnabled = true - - onBackPressedDispatcher.addCallback( - this@attachBackPressedCallback, - backPressedCallback ?: return - ) - } - - fun detachBackPressedCallback() { - backPressedCallback?.isEnabled = false - backPressedCallback = null - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt index b25be59f..87d17a2b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -26,7 +26,6 @@ import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_T import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_UNIXTIME_KEY import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_USER_KEY import com.lagradost.cloudstream3.syncproviders.providers.OpenSubtitlesApi.Companion.OPEN_SUBTITLES_USER_KEY -import com.lagradost.cloudstream3.syncproviders.providers.SubDlApi.Companion.SUBDL_SUBTITLES_USER_KEY import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main @@ -41,7 +40,7 @@ import java.io.OutputStream import java.io.PrintWriter import java.lang.System.currentTimeMillis import java.text.SimpleDateFormat -import java.util.Date +import java.util.* object BackupUtils { @@ -65,11 +64,11 @@ object BackupUtils { PLUGINS_KEY_LOCAL, OPEN_SUBTITLES_USER_KEY, - SUBDL_SUBTITLES_USER_KEY, + + DOWNLOAD_EPISODE_CACHE, "biometric_key", // can lock down users if backup is shared on a incompatible device - "nginx_user", // Nginx user key - "download_path_key" // No access rights after restore data from backup + "nginx_user" // Nginx user key ) /** false if key should not be contained in backup */ @@ -81,12 +80,12 @@ object BackupUtils { // Kinda hack, but I couldn't think of a better way data class BackupVars( - @JsonProperty("_Bool") val bool: Map?, - @JsonProperty("_Int") val int: Map?, - @JsonProperty("_String") val string: Map?, - @JsonProperty("_Float") val float: Map?, - @JsonProperty("_Long") val long: Map?, - @JsonProperty("_StringSet") val stringSet: Map?>?, + @JsonProperty("_Bool") val _Bool: Map?, + @JsonProperty("_Int") val _Int: Map?, + @JsonProperty("_String") val _String: Map?, + @JsonProperty("_Float") val _Float: Map?, + @JsonProperty("_Long") val _Long: Map?, + @JsonProperty("_StringSet") val _StringSet: Map?>?, ) data class BackupFile( @@ -134,21 +133,21 @@ object BackupUtils { ) { if (context == null) return if (restoreSettings) { - context.restoreMap(backupFile.settings.bool, true) - context.restoreMap(backupFile.settings.int, true) - context.restoreMap(backupFile.settings.string, true) - context.restoreMap(backupFile.settings.float, true) - context.restoreMap(backupFile.settings.long, true) - context.restoreMap(backupFile.settings.stringSet, true) + context.restoreMap(backupFile.settings._Bool, true) + context.restoreMap(backupFile.settings._Int, true) + context.restoreMap(backupFile.settings._String, true) + context.restoreMap(backupFile.settings._Float, true) + context.restoreMap(backupFile.settings._Long, true) + context.restoreMap(backupFile.settings._StringSet, true) } if (restoreDataStore) { - context.restoreMap(backupFile.datastore.bool) - context.restoreMap(backupFile.datastore.int) - context.restoreMap(backupFile.datastore.string) - context.restoreMap(backupFile.datastore.float) - context.restoreMap(backupFile.datastore.long) - context.restoreMap(backupFile.datastore.stringSet) + context.restoreMap(backupFile.datastore._Bool) + context.restoreMap(backupFile.datastore._Int) + context.restoreMap(backupFile.datastore._String) + context.restoreMap(backupFile.datastore._Float) + context.restoreMap(backupFile.datastore._Long) + context.restoreMap(backupFile.datastore._StringSet) } } @@ -264,4 +263,4 @@ object BackupUtils { } editor.apply() } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt index 45acbab4..de9b9963 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt @@ -12,21 +12,21 @@ import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL import androidx.biometric.BiometricPrompt import androidx.core.content.ContextCompat -import androidx.core.content.ContextCompat.getString import androidx.fragment.app.FragmentActivity -import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R object BiometricAuthenticator { - const val TAG = "cs3Auth" private const val MAX_FAILED_ATTEMPTS = 3 private var failedAttempts = 0 + const val TAG = "cs3Auth" + private var biometricManager: BiometricManager? = null var biometricPrompt: BiometricPrompt? = null var promptInfo: BiometricPrompt.PromptInfo? = null - var authCallback: BiometricCallback? = null // listen to authentication success + + var authCallback: BiometricAuthCallback? = null // listen to authentication success private fun initializeBiometrics(activity: Activity) { val executor = ContextCompat.getMainExecutor(activity) @@ -37,12 +37,20 @@ object BiometricAuthenticator { activity as FragmentActivity, executor, object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { super.onAuthenticationError(errorCode, errString) showToast("$errString") Log.e(TAG, "$errorCode") - authCallback?.onAuthenticationError() - //activity.finish() + failedAttempts++ + + if (failedAttempts >= MAX_FAILED_ATTEMPTS) { + failedAttempts = 0 + activity.finish() + } else { + failedAttempts = 0 + activity.finish() + } } override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { @@ -81,6 +89,7 @@ object BiometricAuthenticator { .setDescription(description) .setAllowedAuthenticators(authFlag) .build() + } else { // for apis < 30 promptInfo = BiometricPrompt.PromptInfo.Builder() @@ -89,6 +98,7 @@ object BiometricAuthenticator { .setDeviceCredentialAllowed(true) .build() } + } else { // fallback for A12+ when both fingerprint & Face unlock is absent but PIN is set promptInfo = BiometricPrompt.PromptInfo.Builder() @@ -104,6 +114,7 @@ object BiometricAuthenticator { var result = false if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + when (biometricManager?.canAuthenticate( DEVICE_CREDENTIAL or BIOMETRIC_STRONG or BIOMETRIC_WEAK )) { @@ -115,6 +126,7 @@ object BiometricAuthenticator { BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> result = true BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> result = false } + } else { @Suppress("DEPRECATION") when (biometricManager?.canAuthenticate()) { @@ -141,14 +153,15 @@ object BiometricAuthenticator { // function to start authentication in any fragment or activity fun startBiometricAuthentication(activity: Activity, title: Int, setDeviceCred: Boolean) { initializeBiometrics(activity) - authCallback = activity as? BiometricCallback + if (isBiometricHardWareAvailable()) { - authCallback = activity as? BiometricCallback + authCallback = activity as? BiometricAuthCallback authenticationDialog(activity, title, setDeviceCred) promptInfo?.let { biometricPrompt?.authenticate(it) } + } else { if (deviceHasPasswordPinLock(activity)) { - authCallback = activity as? BiometricCallback + authCallback = activity as? BiometricAuthCallback authenticationDialog(activity, R.string.password_pin_authentication_title, true) promptInfo?.let { biometricPrompt?.authenticate(it) } @@ -158,15 +171,7 @@ object BiometricAuthenticator { } } - fun isAuthEnabled(ctx: Context):Boolean { - return ctx.let { - PreferenceManager.getDefaultSharedPreferences(ctx) - .getBoolean(getString(ctx, R.string.biometric_key), false) - } - } - - interface BiometricCallback { + interface BiometricAuthCallback { fun onAuthenticationSuccess() - fun onAuthenticationError() } } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt similarity index 90% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt rename to app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt index f87ddc6a..c3b244c2 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt @@ -1,11 +1,12 @@ package com.lagradost.cloudstream3.utils +import android.os.Handler +import android.os.Looper import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError import kotlinx.coroutines.* import java.util.Collections.synchronizedList -expect fun runOnMainThreadNative(work: (() -> Unit)) object Coroutines { fun T.main(work: suspend ((T) -> Unit)): Job { val value = this @@ -49,7 +50,10 @@ object Coroutines { } fun runOnMainThread(work: (() -> Unit)) { - runOnMainThreadNative(work) + val mainHandler = Handler(Looper.getMainLooper()) + mainHandler.post { + work() + } } /** diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt index b5192aae..19c817b9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt @@ -56,27 +56,16 @@ data class Editor( ) { /** Always remember to call apply after */ fun setKeyRaw(path: String, value: T) { - @Suppress("UNCHECKED_CAST") - if (isStringSet(value)) { - editor.putStringSet(path, value as Set) - } else { - when (value) { - is Boolean -> editor.putBoolean(path, value) - is Int -> editor.putInt(path, value) - is String -> editor.putString(path, value) - is Float -> editor.putFloat(path, value) - is Long -> editor.putLong(path, value) - } + when (value) { + is Boolean -> editor.putBoolean(path, value) + is Int -> editor.putInt(path, value) + is String -> editor.putString(path, value) + is Float -> editor.putFloat(path, value) + is Long -> editor.putLong(path, value) + (value as? Set != null) -> editor.putStringSet(path, value as Set) } } - private fun isStringSet(value: Any?) : Boolean { - if (value is Set<*>) { - return value.filterIsInstance().size == value.size - } - return false - } - fun apply() { editor.apply() System.gc() diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 2fa5f6a3..04387d80 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -2,8 +2,9 @@ package com.lagradost.cloudstream3.utils import android.content.Context import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia import com.lagradost.cloudstream3.APIHolder.unixTimeMS -import com.lagradost.cloudstream3.AcraApplication import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys @@ -11,23 +12,12 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.showToast -import com.lagradost.cloudstream3.DubStatus -import com.lagradost.cloudstream3.EpisodeResponse -import com.lagradost.cloudstream3.MainActivity -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.SearchQuality -import com.lagradost.cloudstream3.SearchResponse -import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.UiImage import com.lagradost.cloudstream3.ui.result.VideoWatchState -import com.lagradost.cloudstream3.utils.AppContextUtils.filterProviderByPreferredMedia -import java.util.Calendar -import java.util.Date -import java.util.GregorianCalendar import kotlin.reflect.KClass import kotlin.reflect.KProperty @@ -205,8 +195,6 @@ object DataStoreHelper { return this } - fun Int.toYear() : Date = GregorianCalendar.getInstance().also { it.set(Calendar.YEAR, this) }.time - /** * Used to display notifications on new episodes and posters in library. **/ @@ -254,7 +242,7 @@ object DataStoreHelper { null, null, latestUpdatedTime, - apiName, type, posterUrl, posterHeaders, quality, year?.toYear(), this.id, plot = this.plot, rating = this.rating, tags = this.tags + apiName, type, posterUrl, posterHeaders, quality, this.id, plot = this.plot, rating = this.rating, tags = this.tags ) } } @@ -285,7 +273,7 @@ object DataStoreHelper { null, null, latestUpdatedTime, - apiName, type, posterUrl, posterHeaders, quality, year?.toYear(), this.id, plot = this.plot, rating = this.rating, tags = this.tags + apiName, type, posterUrl, posterHeaders, quality, this.id, plot = this.plot, rating = this.rating, tags = this.tags ) } } @@ -316,7 +304,7 @@ object DataStoreHelper { null, null, latestUpdatedTime, - apiName, type, posterUrl, posterHeaders, quality, year?.toYear(), this.id, plot = this.plot, rating = this.rating, tags = this.tags + apiName, type, posterUrl, posterHeaders, quality, this.id, plot = this.plot, rating = this.rating, tags = this.tags ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt index c92da214..421e4420 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt @@ -7,6 +7,7 @@ import androidx.work.ForegroundInfo import androidx.work.WorkerParameters import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.VideoDownloadManager.WORK_KEY_INFO diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt similarity index 91% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt rename to app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 0df73a0e..637f65b9 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -1,8 +1,9 @@ package com.lagradost.cloudstream3.utils +import android.net.Uri import com.fasterxml.jackson.annotation.JsonIgnore -import com.lagradost.cloudstream3.IDownloadableMinimum import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.extractors.AStreamHub @@ -16,7 +17,6 @@ import com.lagradost.cloudstream3.extractors.BullStream import com.lagradost.cloudstream3.extractors.ByteShare import com.lagradost.cloudstream3.extractors.Cda import com.lagradost.cloudstream3.extractors.Cdnplayer -import com.lagradost.cloudstream3.extractors.CdnwishCom import com.lagradost.cloudstream3.extractors.Chillx import com.lagradost.cloudstream3.extractors.CineGrabber import com.lagradost.cloudstream3.extractors.Cinestart @@ -53,7 +53,6 @@ import com.lagradost.cloudstream3.extractors.FileMoonIn import com.lagradost.cloudstream3.extractors.FileMoonSx import com.lagradost.cloudstream3.extractors.Filesim import com.lagradost.cloudstream3.extractors.Fplayer -import com.lagradost.cloudstream3.extractors.Geodailymotion import com.lagradost.cloudstream3.extractors.GMPlayer import com.lagradost.cloudstream3.extractors.Gdriveplayer import com.lagradost.cloudstream3.extractors.Gdriveplayerapi @@ -67,7 +66,6 @@ import com.lagradost.cloudstream3.extractors.Gdriveplayerorg import com.lagradost.cloudstream3.extractors.Gdriveplayerus import com.lagradost.cloudstream3.extractors.Gofile import com.lagradost.cloudstream3.extractors.GuardareStream -import com.lagradost.cloudstream3.extractors.GoodstreamExtractor import com.lagradost.cloudstream3.extractors.Guccihide import com.lagradost.cloudstream3.extractors.Hxfile import com.lagradost.cloudstream3.extractors.JWPlayer @@ -85,7 +83,6 @@ import com.lagradost.cloudstream3.extractors.Maxstream import com.lagradost.cloudstream3.extractors.Mcloud import com.lagradost.cloudstream3.extractors.Megacloud import com.lagradost.cloudstream3.extractors.Meownime -import com.lagradost.cloudstream3.extractors.MetaGnathTuggers import com.lagradost.cloudstream3.extractors.Minoplres import com.lagradost.cloudstream3.extractors.MixDrop import com.lagradost.cloudstream3.extractors.MixDropBz @@ -100,23 +97,17 @@ import com.lagradost.cloudstream3.extractors.Mp4Upload import com.lagradost.cloudstream3.extractors.Mvidoo import com.lagradost.cloudstream3.extractors.MwvnVizcloudInfo import com.lagradost.cloudstream3.extractors.MyCloud -import com.lagradost.cloudstream3.extractors.MegaF import com.lagradost.cloudstream3.extractors.Neonime7n import com.lagradost.cloudstream3.extractors.Neonime8n import com.lagradost.cloudstream3.extractors.Odnoklassniki import com.lagradost.cloudstream3.extractors.TauVideo import com.lagradost.cloudstream3.extractors.SibNet import com.lagradost.cloudstream3.extractors.ContentX -import com.lagradost.cloudstream3.extractors.D0000d -import com.lagradost.cloudstream3.extractors.D000dCom -import com.lagradost.cloudstream3.extractors.DoodstreamCom import com.lagradost.cloudstream3.extractors.EmturbovidExtractor import com.lagradost.cloudstream3.extractors.Hotlinger import com.lagradost.cloudstream3.extractors.FourCX import com.lagradost.cloudstream3.extractors.PlayRu import com.lagradost.cloudstream3.extractors.FourPlayRu -import com.lagradost.cloudstream3.extractors.Pichive -import com.lagradost.cloudstream3.extractors.FourPichive import com.lagradost.cloudstream3.extractors.HDMomPlayer import com.lagradost.cloudstream3.extractors.HDPlayerSystem import com.lagradost.cloudstream3.extractors.VideoSeyred @@ -125,7 +116,6 @@ import com.lagradost.cloudstream3.extractors.HDStreamAble import com.lagradost.cloudstream3.extractors.RapidVid import com.lagradost.cloudstream3.extractors.TRsTX import com.lagradost.cloudstream3.extractors.VidMoxy -import com.lagradost.cloudstream3.extractors.Sobreatsesuyp import com.lagradost.cloudstream3.extractors.PixelDrain import com.lagradost.cloudstream3.extractors.MailRu import com.lagradost.cloudstream3.extractors.Mediafire @@ -149,7 +139,6 @@ import com.lagradost.cloudstream3.extractors.Sbspeed import com.lagradost.cloudstream3.extractors.Sbthe import com.lagradost.cloudstream3.extractors.Sendvid import com.lagradost.cloudstream3.extractors.ShaveTape -import com.lagradost.cloudstream3.extractors.Simpulumlamerop import com.lagradost.cloudstream3.extractors.Solidfiles import com.lagradost.cloudstream3.extractors.Ssbstream import com.lagradost.cloudstream3.extractors.StreamM4u @@ -186,7 +175,6 @@ import com.lagradost.cloudstream3.extractors.UpstreamExtractor import com.lagradost.cloudstream3.extractors.Uqload import com.lagradost.cloudstream3.extractors.Uqload1 import com.lagradost.cloudstream3.extractors.Uqload2 -import com.lagradost.cloudstream3.extractors.Urochsunloath import com.lagradost.cloudstream3.extractors.Userload import com.lagradost.cloudstream3.extractors.Userscloud import com.lagradost.cloudstream3.extractors.Uservideo @@ -194,12 +182,10 @@ import com.lagradost.cloudstream3.extractors.Vanfem import com.lagradost.cloudstream3.extractors.Vicloud import com.lagradost.cloudstream3.extractors.VidSrcExtractor import com.lagradost.cloudstream3.extractors.VidSrcExtractor2 -import com.lagradost.cloudstream3.extractors.VidSrcTo import com.lagradost.cloudstream3.extractors.VideoVard import com.lagradost.cloudstream3.extractors.VideovardSX import com.lagradost.cloudstream3.extractors.Vidgomunime import com.lagradost.cloudstream3.extractors.Vidgomunimesb -import com.lagradost.cloudstream3.extractors.Vidguardto import com.lagradost.cloudstream3.extractors.VidhideExtractor import com.lagradost.cloudstream3.extractors.Vidmoly import com.lagradost.cloudstream3.extractors.Vidmolyme @@ -221,7 +207,6 @@ import com.lagradost.cloudstream3.extractors.Watchx import com.lagradost.cloudstream3.extractors.WcoStream import com.lagradost.cloudstream3.extractors.Wibufile import com.lagradost.cloudstream3.extractors.XStreamCdn -import com.lagradost.cloudstream3.extractors.Yipsu import com.lagradost.cloudstream3.extractors.YourUpload import com.lagradost.cloudstream3.extractors.YoutubeExtractor import com.lagradost.cloudstream3.extractors.YoutubeMobileExtractor @@ -232,11 +217,6 @@ import com.lagradost.cloudstream3.extractors.Zorofile import com.lagradost.cloudstream3.extractors.Zplayer import com.lagradost.cloudstream3.extractors.ZplayerV2 import com.lagradost.cloudstream3.extractors.Ztreamhub -import com.lagradost.cloudstream3.extractors.EPlayExtractor -import com.lagradost.cloudstream3.extractors.FlaswishCom -import com.lagradost.cloudstream3.extractors.SfastwishCom -import com.lagradost.cloudstream3.extractors.Vtbe -import com.lagradost.cloudstream3.extractors.WishembedPro import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import kotlinx.coroutines.delay @@ -319,18 +299,7 @@ enum class ExtractorLinkType { /** No support at the moment */ TORRENT, /** No support at the moment */ - MAGNET; - - // See https://www.iana.org/assignments/media-types/media-types.xhtml - fun getMimeType(): String { - return when (this) { - VIDEO -> "video/mp4" - M3U8 -> "application/x-mpegURL" - DASH -> "application/dash+xml" - TORRENT -> "application/x-bittorrent" - MAGNET -> "application/x-bittorrent" - } - } + MAGNET, } private fun inferTypeFromUrl(url: String): ExtractorLinkType { @@ -432,30 +401,10 @@ open class ExtractorLink constructor( /** Used for getExtractorVerifierJob() */ open val extractorData: String? = null, open val type: ExtractorLinkType, -) : IDownloadableMinimum { - val isM3u8: Boolean get() = type == ExtractorLinkType.M3U8 - val isDash: Boolean get() = type == ExtractorLinkType.DASH - - // Cached video size - private var videoSize: Long? = null - - /** - * Get video size in bytes with one head request. Only available for ExtractorLinkType.Video - * @param timeoutSeconds timeout of the head request. - */ - suspend fun getVideoSize(timeoutSeconds: Long = 3L): Long? { - // Content-Length is not applicable to other types of formats - if (this.type != ExtractorLinkType.VIDEO) return null - - videoSize = videoSize ?: runCatching { - val response = - app.head(this.url, headers = headers, referer = referer, timeout = timeoutSeconds) - response.headers["Content-Length"]?.toLong() - }.getOrNull() - - return videoSize - } - +) : VideoDownloadManager.IDownloadableMinimum { + val isM3u8 : Boolean get() = type == ExtractorLinkType.M3U8 + val isDash : Boolean get() = type == ExtractorLinkType.DASH + @JsonIgnore fun getAllHeaders() : Map { if (referer.isBlank()) { @@ -531,6 +480,29 @@ open class ExtractorLink constructor( } } +data class ExtractorUri( + val uri: Uri, + val name: String, + + val basePath: String? = null, + val relativePath: String? = null, + val displayName: String? = null, + + val id: Int? = null, + val parentId: Int? = null, + val episode: Int? = null, + val season: Int? = null, + val headerName: String? = null, + val tvType: TvType? = null, +) + +data class ExtractorSubtitleLink( + val name: String, + override val url: String, + override val referer: String, + override val headers: Map = mapOf() +) : VideoDownloadManager.IDownloadableMinimum + /** * Removes https:// and www. * To match urls regardless of schema, perhaps Uri() can be used? @@ -736,8 +708,6 @@ val extractorApis: MutableList = arrayListOf( FourCX(), PlayRu(), FourPlayRu(), - Pichive(), - FourPichive(), HDMomPlayer(), HDPlayerSystem(), VideoSeyred(), @@ -746,7 +716,6 @@ val extractorApis: MutableList = arrayListOf( RapidVid(), TRsTX(), VidMoxy(), - Sobreatsesuyp(), PixelDrain(), MailRu(), @@ -765,9 +734,6 @@ val extractorApis: MutableList = arrayListOf( DoodSoExtractor(), DoodLaExtractor(), Dooood(), - D0000d(), - D000dCom(), - DoodstreamCom(), DoodWsExtractor(), DoodShExtractor(), DoodWatchExtractor(), @@ -845,7 +811,6 @@ val extractorApis: MutableList = arrayListOf( Guccihide(), FileMoon(), FileMoonSx(), - Vido(), Linkbox(), Acefile(), @@ -872,7 +837,6 @@ val extractorApis: MutableList = arrayListOf( Gdriveplayerorg(), Gdriveplayerus(), Gdriveplayerco(), - GoodstreamExtractor(), Gdriveplayer(), DatabaseGdrive(), DatabaseGdrive2(), @@ -885,13 +849,11 @@ val extractorApis: MutableList = arrayListOf( Streamlare(), VidSrcExtractor(), VidSrcExtractor2(), - VidSrcTo(), PlayLtXyz(), AStreamHub(), Vidplay(), VidplayOnline(), MyCloud(), - MegaF(), Cda(), Dailymotion(), @@ -902,20 +864,7 @@ val extractorApis: MutableList = arrayListOf( Megacloud(), VidhideExtractor(), StreamWishExtractor(), - WishembedPro(), - CdnwishCom(), - FlaswishCom(), - SfastwishCom(), - EmturbovidExtractor(), - Vtbe(), - EPlayExtractor(), - Vidguardto(), - Simpulumlamerop(), - Urochsunloath(), - Yipsu(), - MetaGnathTuggers(), - Geodailymotion(), - + EmturbovidExtractor() ) @@ -997,7 +946,7 @@ abstract class ExtractorApi { abstract val mainUrl: String abstract val requiresReferer: Boolean - /** Determines which plugin a given provider is from. This is the full path to the plugin. */ + /** Determines which plugin a given extractor is from */ var sourcePlugin: String? = null //suspend fun getSafeUrl(url: String, referer: String? = null): List? { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt index 59f534ff..d9a31b4e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt @@ -24,7 +24,7 @@ import okio.sink import java.io.File import android.text.TextUtils import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit -import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import java.io.BufferedReader import java.io.IOException import java.io.InputStreamReader @@ -32,26 +32,26 @@ import java.io.InputStreamReader class InAppUpdater { companion object { - private const val GITHUB_USER_NAME = "recloudstream" - private const val GITHUB_REPO = "cloudstream" + const val GITHUB_USER_NAME = "recloudstream" + const val GITHUB_REPO = "cloudstream" - private const val LOG_TAG = "InAppUpdater" + const val LOG_TAG = "InAppUpdater" // === IN APP UPDATER === data class GithubAsset( @JsonProperty("name") val name: String, @JsonProperty("size") val size: Int, // Size bytes - @JsonProperty("browser_download_url") val browserDownloadUrl: String, // download link - @JsonProperty("content_type") val contentType: String, // application/vnd.android.package-archive + @JsonProperty("browser_download_url") val browser_download_url: String, // download link + @JsonProperty("content_type") val content_type: String, // application/vnd.android.package-archive ) data class GithubRelease( - @JsonProperty("tag_name") val tagName: String, // Version code + @JsonProperty("tag_name") val tag_name: String, // Version code @JsonProperty("body") val body: String, // Desc @JsonProperty("assets") val assets: List, - @JsonProperty("target_commitish") val targetCommitish: String, // branch + @JsonProperty("target_commitish") val target_commitish: String, // branch @JsonProperty("prerelease") val prerelease: Boolean, - @JsonProperty("node_id") val nodeId: String //Node Id + @JsonProperty("node_id") val node_id: String //Node Id ) data class GithubObject( @@ -61,7 +61,7 @@ class InAppUpdater { ) data class GithubTag( - @JsonProperty("object") val githubObject: GithubObject, + @JsonProperty("object") val github_object: GithubObject, ) data class Update( @@ -114,7 +114,7 @@ class InAppUpdater { response.filter { rel -> !rel.prerelease }.sortedWith(compareBy { release -> - release.assets.firstOrNull { it.contentType == "application/vnd.android.package-archive" }?.name?.let { it1 -> + release.assets.firstOrNull { it.content_type == "application/vnd.android.package-archive" }?.name?.let { it1 -> versionRegex.find( it1 )?.groupValues?.let { @@ -134,7 +134,7 @@ class InAppUpdater { foundAsset?.name?.let { assetName -> val foundVersion = versionRegex.find(assetName) val shouldUpdate = - if (foundAsset.browserDownloadUrl != "" && foundVersion != null) currentVersion?.versionName?.let { versionName -> + if (foundAsset.browser_download_url != "" && foundVersion != null) currentVersion?.versionName?.let { versionName -> versionRegexLocal.find(versionName)?.groupValues?.let { it[3].toInt() * 100_000_000 + it[4].toInt() * 10_000 + it[5].toInt() } @@ -146,10 +146,10 @@ class InAppUpdater { return if (foundVersion != null) { Update( shouldUpdate, - foundAsset.browserDownloadUrl, + foundAsset.browser_download_url, foundVersion.groupValues[2], found.body, - found.nodeId + found.node_id ) } else { Update(false, null, null, null, null) @@ -168,33 +168,33 @@ class InAppUpdater { val found = response.lastOrNull { rel -> - rel.prerelease || rel.tagName == "pre-release" + rel.prerelease || rel.tag_name == "pre-release" } val foundAsset = found?.assets?.filter { it -> - it.contentType == "application/vnd.android.package-archive" + it.content_type == "application/vnd.android.package-archive" }?.getOrNull(0) val tagResponse = parseJson(app.get(tagUrl, headers = headers).text) - Log.d(LOG_TAG, "Fetched GitHub tag: ${tagResponse.githubObject.sha.take(7)}") + Log.d(LOG_TAG, "Fetched GitHub tag: ${tagResponse.github_object.sha.take(7)}") val shouldUpdate = (getString(R.string.commit_hash) .trim { c -> c.isWhitespace() } .take(7) != - tagResponse.githubObject.sha + tagResponse.github_object.sha .trim { c -> c.isWhitespace() } .take(7)) return if (foundAsset != null) { Update( shouldUpdate, - foundAsset.browserDownloadUrl, - tagResponse.githubObject.sha.take(10), + foundAsset.browser_download_url, + tagResponse.github_object.sha.take(10), found.body, - found.nodeId + found.node_id ) } else { Update(false, null, null, null, null) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsHunter.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/JsHunter.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsHunter.kt rename to app/src/main/java/com/lagradost/cloudstream3/utils/JsHunter.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsUnpacker.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt similarity index 99% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsUnpacker.kt rename to app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt index d9f0b382..153dbd3e 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsUnpacker.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt @@ -50,7 +50,7 @@ class JsUnpacker(packedJS: String?) { throw Exception("Unknown p.a.c.k.e.r. encoding") } val unbase = Unbase(radix) - p = Pattern.compile("""\b[a-zA-Z0-9_]+\b""") + p = Pattern.compile("\\b\\w+\\b") m = p.matcher(payload) val decoded = StringBuilder(payload) var replaceOffset = 0 diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt similarity index 100% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/M3u8Helper.kt rename to app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstaller.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstaller.kt index 4b3f02f1..bc81a5b9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstaller.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstaller.kt @@ -11,6 +11,7 @@ import android.os.Build import android.widget.Toast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.utils.Coroutines.main import java.io.InputStream @@ -56,7 +57,7 @@ class ApkInstaller(private val service: PackageInstallerService) { PackageInstaller.STATUS_FAILURE )) { PackageInstaller.STATUS_PENDING_USER_ACTION -> { - val userAction = intent.getSafeParcelableExtra(Intent.EXTRA_INTENT) + val userAction = intent.getParcelableExtra(Intent.EXTRA_INTENT) userAction?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) context.startActivity(userAction) } @@ -145,5 +146,3 @@ class ApkInstaller(private val service: PackageInstallerService) { } } -@Suppress("DEPRECATION") -inline fun Intent.getSafeParcelableExtra(key: String): T? = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) getParcelableExtra(key) else getParcelableExtra(key, T::class.java) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstallerService.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstallerService.kt index 57b98dc2..322547f4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstallerService.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstallerService.kt @@ -15,7 +15,7 @@ import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.utils.AppContextUtils.createNotificationChannel +import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import kotlinx.coroutines.delay diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt deleted file mode 100644 index 0d3da8e7..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.lagradost.cloudstream3.utils - -import android.content.ActivityNotFoundException -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.os.PowerManager -import android.provider.Settings -import android.util.Log -import androidx.appcompat.app.AlertDialog -import androidx.preference.PreferenceManager -import com.lagradost.cloudstream3.BuildConfig -import com.lagradost.cloudstream3.CommonActivity.showToast -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.ui.settings.Globals.PHONE -import com.lagradost.cloudstream3.ui.settings.Globals.isLayout - -private const val PACKAGE_NAME = BuildConfig.APPLICATION_ID -private const val TAG = "PowerManagerAPI" - -object BatteryOptimizationChecker { - - fun isAppRestricted(context: Context?): Boolean { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context != null) { - val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager - return !powerManager.isIgnoringBatteryOptimizations(context.packageName) - } - - return false // below Marshmallow, it's always unrestricted when app is in background - } - - fun openBatteryOptimizationSettings(context: Context) { - if (shouldShowBatteryOptimizationDialog(context)) { - showBatteryOptimizationDialog(context) - } - } - - fun showBatteryOptimizationDialog(context: Context) { - val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) - - try { - context.let { - AlertDialog.Builder(it) - .setTitle(R.string.battery_dialog_title) - .setIcon(R.drawable.ic_battery) - .setMessage(R.string.battery_dialog_message) - .setPositiveButton(R.string.ok) { _, _ -> - intentOpenAppInfo(it) - } - .setNegativeButton(R.string.cancel) { _, _ -> - settingsManager.edit() - .putBoolean(context.getString(R.string.battery_optimisation_key), false) - .apply() - } - .show() - } - } catch (t: Throwable) { - Log.e(TAG, "Error showing battery optimization dialog", t) - } - } - - private fun shouldShowBatteryOptimizationDialog(context: Context): Boolean { - val isRestricted = isAppRestricted(context) - val isOptimizedNotShown = PreferenceManager.getDefaultSharedPreferences(context) - .getBoolean(context.getString(R.string.battery_optimisation_key), true) - return isRestricted && isOptimizedNotShown && isLayout(PHONE) - } - - private fun intentOpenAppInfo(context: Context) { - val intent = Intent() - try { - intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) - .setData(Uri.fromParts("package", PACKAGE_NAME, null)) - context.startActivity(intent, Bundle()) - } catch (t: Throwable) { - Log.e(TAG, "Unable to invoke any intent", t) - if (t is ActivityNotFoundException) { - showToast("Exception: Activity Not Found") - } else { - showToast(R.string.app_info_intent_error) - } - } - } -} diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SnackbarHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SnackbarHelper.kt deleted file mode 100644 index e6a77795..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SnackbarHelper.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.lagradost.cloudstream3.utils - -import android.app.Activity -import android.view.View -import androidx.annotation.MainThread -import androidx.annotation.StringRes -import com.google.android.material.snackbar.Snackbar -import com.lagradost.api.Log -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.ui.result.UiText -import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute - -object SnackbarHelper { - - private const val TAG = "COMPACT" - private var currentSnackbar: Snackbar? = null - - @MainThread - fun showSnackbar( - act: Activity?, - message: UiText, - duration: Int = Snackbar.LENGTH_SHORT, - actionText: UiText? = null, - actionCallback: (() -> Unit)? = null - ) { - if (act == null) return - showSnackbar(act, message.asString(act), duration, - actionText?.asString(act), actionCallback) - } - - @MainThread - fun showSnackbar( - act: Activity?, - @StringRes message: Int, - duration: Int = Snackbar.LENGTH_SHORT, - @StringRes actionText: Int? = null, - actionCallback: (() -> Unit)? = null - ) { - if (act == null) return - showSnackbar(act, act.getString(message), duration, - actionText?.let { act.getString(it) }, actionCallback) - } - - @MainThread - fun showSnackbar( - act: Activity?, - message: String?, - duration: Int = Snackbar.LENGTH_SHORT, - actionText: String? = null, - actionCallback: (() -> Unit)? = null - ) { - if (act == null || message == null) { - Log.w(TAG, "Invalid showSnackbar: act = $act, message = $message") - return - } - Log.i(TAG, "showSnackbar: $message") - - try { - currentSnackbar?.dismiss() - } catch (e: Exception) { - logError(e) - } - - try { - val parentView = act.findViewById(android.R.id.content) - val snackbar = Snackbar.make(parentView, message, duration) - - actionCallback?.let { - snackbar.setAction(actionText) { actionCallback.invoke() } - } - - snackbar.show() - currentSnackbar = snackbar - - snackbar.setBackgroundTint(act.colorFromAttribute(R.attr.primaryBlackBackground)) - snackbar.setTextColor(act.colorFromAttribute(R.attr.textColor)) - snackbar.setActionTextColor(act.colorFromAttribute(R.attr.colorPrimary)) - - } catch (e: Exception) { - logError(e) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleUtils.kt deleted file mode 100644 index 93a53395..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleUtils.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.lagradost.cloudstream3.utils - -import android.content.Context -import com.lagradost.api.Log -import com.lagradost.cloudstream3.utils.VideoDownloadManager.getFolder -import com.lagradost.safefile.SafeFile - -object SubtitleUtils { - - // Only these files are allowed, so no videos as subtitles - private val allowedExtensions = listOf( - ".vtt", ".srt", ".txt", ".ass", - ".ttml", ".sbv", ".dfxp" - ) - - fun deleteMatchingSubtitles(context: Context, info: VideoDownloadManager.DownloadedFileInfo) { - val relative = info.relativePath - val display = info.displayName - val cleanDisplay = cleanDisplayName(display) - - getFolder(context, relative, info.basePath)?.forEach { (name, uri) -> - if (isMatchingSubtitle(name, display, cleanDisplay)) { - val subtitleFile = SafeFile.fromUri(context, uri) - if (subtitleFile == null || !subtitleFile.delete()) { - Log.e("SubtitleDeletion", "Failed to delete subtitle file: ${subtitleFile?.name()}") - } - } - } - } - - /** - * @param name the file name of the subtitle - * @param display the file name of the video - * @param cleanDisplay the cleanDisplayName of the video file name - */ - fun isMatchingSubtitle( - name: String, - display: String, - cleanDisplay: String - ): Boolean { - // Check if the file has a valid subtitle extension - val hasValidExtension = allowedExtensions.any { name.contains(it, ignoreCase = true) } - - // We can't have the exact same file as a subtitle - val isNotDisplayName = !name.equals(display, ignoreCase = true) - - // Check if the file name starts with a cleaned version of the display name - val startsWithCleanDisplay = cleanDisplayName(name).startsWith(cleanDisplay, ignoreCase = true) - - return hasValidExtension && isNotDisplayName && startsWithCleanDisplay - } - - fun cleanDisplayName(name: String): String { - return name.substringBeforeLast('.').trim() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt index 351e77c8..71d3a1ef 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt @@ -73,8 +73,8 @@ object SyncUtil { val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).text val mapped = parseJson(response) - val overrideMal = mapped?.malId ?: mapped?.mal?.id ?: mapped?.anilist?.malId - val overrideAnilist = mapped?.aniId ?: mapped?.anilist?.id + val overrideMal = mapped?.malId ?: mapped?.Mal?.id ?: mapped?.Anilist?.malId + val overrideAnilist = mapped?.aniId ?: mapped?.Anilist?.id if (overrideMal != null) { return overrideMal.toString() to overrideAnilist?.toString() @@ -135,8 +135,8 @@ object SyncUtil { @JsonProperty("createdAt") val createdAt: String?, @JsonProperty("updatedAt") val updatedAt: String?, @JsonProperty("deletedAt") val deletedAt: String?, - @JsonProperty("Mal") val mal: Mal?, - @JsonProperty("Anilist") val anilist: Anilist?, + @JsonProperty("Mal") val Mal: Mal?, + @JsonProperty("Anilist") val Anilist: Anilist?, @JsonProperty("malUrl") val malUrl: String? ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt index 049f92fb..dd973538 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt @@ -4,7 +4,6 @@ import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.mvvm.logError import kotlinx.coroutines.* import org.junit.Assert -import kotlin.random.Random object TestingUtils { open class TestResult(val success: Boolean) { @@ -14,55 +13,16 @@ object TestingUtils { } } - class Logger { - enum class LogLevel { - Normal, - Warning, - Error; - } + class TestResultSearch(val results: List) : TestResult(true) + class TestResultLoad(val extractorData: String) : TestResult(true) - data class Message(val level: LogLevel, val message: String) { - override fun toString(): String { - val level = when (this.level) { - LogLevel.Normal -> "" - LogLevel.Warning -> "Warning: " - LogLevel.Error -> "Error: " - } - return "$level$message" - } - } - - private val messageLog = mutableListOf() - - fun getRawLog(): List = messageLog - - fun log(message: String) { - messageLog.add(Message(LogLevel.Normal, message)) - } - - fun warn(message: String) { - messageLog.add(Message(LogLevel.Warning, message)) - } - - fun error(message: String) { - messageLog.add(Message(LogLevel.Error, message)) - } - } - - class TestResultList(val results: List) : TestResult(true) - class TestResultLoad(val extractorData: String, val shouldLoadLinks: Boolean) : TestResult(true) - - class TestResultProvider( - success: Boolean, - val log: List, - val exception: Throwable? - ) : + class TestResultProvider(success: Boolean, val log: String, val exception: Throwable?) : TestResult(success) @Throws(AssertionError::class, CancellationException::class) suspend fun testHomepage( api: MainAPI, - logger: Logger + logger: (String) -> Unit ): TestResult { if (api.hasMainPage) { try { @@ -71,33 +31,22 @@ object TestingUtils { api.getMainPage(1, MainPageRequest(f.name, f.data, f.horizontalImages)) when { homepage == null -> { - logger.error("Provider ${api.name} did not correctly load homepage!") + logger.invoke("Homepage provider ${api.name} did not correctly load homepage!") } - homepage.items.isEmpty() -> { - logger.warn("Provider ${api.name} does not contain any homepage rows!") + logger.invoke("Homepage provider ${api.name} does not contain any items!") } - homepage.items.any { it.list.isEmpty() } -> { - logger.warn("Provider ${api.name} does not have any items in a homepage row!") + logger.invoke("Homepage provider ${api.name} does not have any items on result!") } } - val homePageList = homepage?.items?.flatMap { it.list } ?: emptyList() - return TestResultList(homePageList) } catch (e: Throwable) { - when (e) { - is NotImplementedError -> { - Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented") - } - - is CancellationException -> { - throw e - } - - else -> { - e.message?.let { logger.warn("Exception thrown when loading homepage: \"$it\"") } - } + if (e is NotImplementedError) { + Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented") + } else if (e is CancellationException) { + throw e } + logError(e) } } return TestResult.Pass @@ -105,13 +54,11 @@ object TestingUtils { @Throws(AssertionError::class, CancellationException::class) private suspend fun testSearch( - api: MainAPI, - testQueries: List, - logger: Logger, + api: MainAPI ): TestResult { - val searchResults = testQueries.firstNotNullOfOrNull { query -> + val searchQueries = listOf("over", "iron", "guy") + val searchResults = searchQueries.firstNotNullOfOrNull { query -> try { - logger.log("Searching for: $query") api.search(query).takeIf { !it.isNullOrEmpty() } } catch (e: Throwable) { if (e is NotImplementedError) { @@ -125,11 +72,12 @@ object TestingUtils { } return if (searchResults.isNullOrEmpty()) { - Assert.fail("Api ${api.name} did not return any search responses") + Assert.fail("Api ${api.name} did not return any valid search responses") TestResult.Fail // Should not be reached } else { - TestResultList(searchResults) + TestResultSearch(searchResults) } + } @@ -137,27 +85,31 @@ object TestingUtils { private suspend fun testLoad( api: MainAPI, result: SearchResponse, - logger: Logger + logger: (String) -> Unit ): TestResult { try { - if (result.apiName != api.name) { - logger.warn("Wrong apiName on SearchResponse: ${api.name} != ${result.apiName}") - } + Assert.assertEquals( + "Invalid apiName on SearchResponse on ${api.name}", + result.apiName, + api.name + ) val loadResponse = api.load(result.url) if (loadResponse == null) { - logger.error("Returned null loadResponse on ${result.url} on ${api.name}") + logger.invoke("Returned null loadResponse on ${result.url} on ${api.name}") return TestResult.Fail } - if (loadResponse.apiName != api.name) { - logger.warn("Wrong apiName on LoadResponse: ${api.name} != ${loadResponse.apiName}") - } - - if (!api.supportedTypes.contains(loadResponse.type)) { - logger.warn("Api ${api.name} on load does not contain any of the supportedTypes: ${loadResponse.type}") - } + Assert.assertEquals( + "Invalid apiName on LoadResponse on ${api.name}", + loadResponse.apiName, + result.apiName + ) + Assert.assertTrue( + "Api ${api.name} on load does not contain any of the supportedTypes: ${loadResponse.type}", + api.supportedTypes.contains(loadResponse.type) + ) val url = when (loadResponse) { is AnimeLoadResponse -> { @@ -165,43 +117,39 @@ object TestingUtils { loadResponse.episodes.keys.isEmpty() || loadResponse.episodes.keys.any { loadResponse.episodes[it].isNullOrEmpty() } if (gotNoEpisodes) { - logger.error("Api ${api.name} got no episodes on ${loadResponse.url}") + logger.invoke("Api ${api.name} got no episodes on ${loadResponse.url}") return TestResult.Fail } (loadResponse.episodes[loadResponse.episodes.keys.firstOrNull()])?.firstOrNull()?.data } - is MovieLoadResponse -> { val gotNoEpisodes = loadResponse.dataUrl.isBlank() if (gotNoEpisodes) { - logger.error("Api ${api.name} got no movie on ${loadResponse.url}") + logger.invoke("Api ${api.name} got no movie on ${loadResponse.url}") return TestResult.Fail } loadResponse.dataUrl } - is TvSeriesLoadResponse -> { val gotNoEpisodes = loadResponse.episodes.isEmpty() if (gotNoEpisodes) { - logger.error("Api ${api.name} got no episodes on ${loadResponse.url}") + logger.invoke("Api ${api.name} got no episodes on ${loadResponse.url}") return TestResult.Fail } loadResponse.episodes.firstOrNull()?.data } - is LiveStreamLoadResponse -> { loadResponse.dataUrl } - else -> { - logger.error("Unknown load response: ${loadResponse.javaClass.name}") + logger.invoke("Unknown load response: ${loadResponse.javaClass.name}") return TestResult.Fail } } ?: return TestResult.Fail - return TestResultLoad(url, loadResponse.type != TvType.CustomMedia) + return TestResultLoad(url) // val loadTest = testLoadResponse(api, load, logger) // if (loadTest is TestResultLoad) { @@ -226,7 +174,7 @@ object TestingUtils { private suspend fun testLinkLoading( api: MainAPI, url: String?, - logger: Logger + logger: (String) -> Unit ): TestResult { Assert.assertNotNull("Api ${api.name} has invalid url on episode", url) if (url == null) return TestResult.Fail // Should never trigger @@ -234,7 +182,7 @@ object TestingUtils { var linksLoaded = 0 try { val success = api.loadLinks(url, false, {}) { link -> - logger.log("Video loaded: ${link.name}") + logger.invoke("Video loaded: ${link.name}") Assert.assertTrue( "Api ${api.name} returns link with invalid url ${link.url}", link.url.length > 4 @@ -242,7 +190,7 @@ object TestingUtils { linksLoaded++ } if (success) { - logger.log("Links loaded: $linksLoaded") + logger.invoke("Links loaded: $linksLoaded") return TestResult(linksLoaded > 0) } else { Assert.fail("Api ${api.name} returns false on loadLinks() with $linksLoaded links loaded") @@ -252,9 +200,8 @@ object TestingUtils { is NotImplementedError -> { Assert.fail("Provider has not implemented loadLinks()") } - else -> { - logger.error("Failed link loading on ${api.name} using data: $url") + logger.invoke("Failed link loading on ${api.name} using data: $url") throw e } } @@ -265,57 +212,53 @@ object TestingUtils { fun getDeferredProviderTests( scope: CoroutineScope, providers: Array, + logger: (String) -> Unit, callback: (MainAPI, TestResultProvider) -> Unit ) { providers.forEach { api -> scope.launch { - val logger = Logger() + var log = "" + fun addToLog(string: String) { + log += string + "\n" + logger.invoke(string) + } + fun getLog(): String { + return log.removeSuffix("\n") + } val result = try { - logger.log("Trying ${api.name}") + addToLog("Trying ${api.name}") // Test Homepage - val homepage = testHomepage(api, logger) - Assert.assertTrue("Homepage failed to load", homepage.success) - val homePageList = (homepage as? TestResultList)?.results ?: emptyList() + val homepage = testHomepage(api, logger).success + Assert.assertTrue("Homepage failed to load", homepage) // Test Search Results - val searchQueries = - // Use the random 3 home page results as queries since they are guaranteed to exist - (homePageList.shuffled(Random).take(3).map { it.name.split(" ").first() } + - // If home page is sparse then use generic search queries - listOf("over", "iron", "guy")).take(3) - - val searchResults = testSearch(api, searchQueries, logger) + val searchResults = testSearch(api) Assert.assertTrue("Failed to get search results", searchResults.success) - searchResults as TestResultList + searchResults as TestResultSearch // Test Load and LoadLinks // Only try the first 3 search results to prevent spamming val success = searchResults.results.take(3).any { searchResponse -> - logger.log("Testing search result: ${searchResponse.url}") - val loadResponse = testLoad(api, searchResponse, logger) + addToLog("Testing search result: ${searchResponse.url}") + val loadResponse = testLoad(api, searchResponse, ::addToLog) if (loadResponse !is TestResultLoad) { false } else { - if (loadResponse.shouldLoadLinks) { - testLinkLoading(api, loadResponse.extractorData, logger).success - } else { - logger.log("Skipping link loading test") - true - } + testLinkLoading(api, loadResponse.extractorData, ::addToLog).success } } if (success) { - logger.log("Success ${api.name}") - TestResultProvider(true, logger.getRawLog(), null) + logger.invoke("Success ${api.name}") + TestResultProvider(true, getLog(), null) } else { - logger.error("Link loading failed") - TestResultProvider(false, logger.getRawLog(), null) + logger.invoke("Error ${api.name}") + TestResultProvider(false, getLog(), null) } } catch (e: Throwable) { - TestResultProvider(false, logger.getRawLog(), e) + TestResultProvider(false, getLog(), e) } callback.invoke(api, result) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt index ad1b6502..eedb626a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -16,8 +16,6 @@ import android.graphics.Color import android.graphics.drawable.Drawable import android.os.Build import android.os.Bundle -import android.os.Handler -import android.os.Looper import android.os.TransactionTooLargeException import android.util.Log import android.view.* @@ -47,7 +45,6 @@ import androidx.core.view.marginBottom import androidx.core.view.marginLeft import androidx.core.view.marginRight import androidx.core.view.marginTop -import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.navigation.fragment.NavHostFragment @@ -61,7 +58,6 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestOptions.bitmapTransform import com.bumptech.glide.request.target.Target -import com.google.android.material.appbar.AppBarLayout import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipDrawable import com.google.android.material.chip.ChipGroup @@ -212,14 +208,6 @@ object UIHelper { } } - fun View?.setAppBarNoScrollFlagsOnTV() { - if (isLayout(Globals.TV or EMULATOR)) { - this?.updateLayoutParams { - scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL - } - } - } - fun Activity.hideKeyboard() { window?.decorView?.clearFocus() this.findViewById(android.R.id.content)?.rootView?.let { @@ -477,23 +465,7 @@ object UIHelper { } fun FragmentActivity.popCurrentPage() { - // Post the back press action to the main thread handler to ensure it executes - // after any currently pending UI updates or fragment transactions. - Handler(Looper.getMainLooper()).post { - // Check if the FragmentManager state is saved. If it is, we cannot perform - // fragment transactions safely because the state may be inconsistent. - if (!supportFragmentManager.isStateSaved) { - // If the state is not saved, it's safe to perform the back press action. - this.onBackPressedDispatcher.onBackPressed() - } else { - // If the state is saved, retry the back press action after a slight delay. - // This gives the FragmentManager time to complete any ongoing state-saving - // operations or transactions, ensuring that we do not encounter an IllegalStateException. - Handler(Looper.getMainLooper()).postDelayed({ - this.onBackPressedDispatcher.onBackPressed() - }, 100) - } - } + this.onBackPressedDispatcher.onBackPressed() } fun Context.getStatusBarHeight(): Int { @@ -553,7 +525,7 @@ object UIHelper { return result } - fun Context?.isBottomLayout(): Boolean { + fun Context?.IsBottomLayout(): Boolean { if (this == null) return true val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) return settingsManager.getBoolean(getString(R.string.bottom_title_key), true) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/UnshortenUrl.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/UnshortenUrl.kt similarity index 96% rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/UnshortenUrl.kt rename to app/src/main/java/com/lagradost/cloudstream3/utils/UnshortenUrl.kt index b13e88e5..46b232f6 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/UnshortenUrl.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UnshortenUrl.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.utils +import android.util.Base64 import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.base64Decode import com.lagradost.nicehttp.NiceResponse @@ -90,12 +91,13 @@ object ShortLink { } val encodedbytearray = encodedUri.map { it.code.toByte() }.toByteArray() var decodedUri = - base64Decode(encodedbytearray.toString()).dropLast(16) + Base64.decode(encodedbytearray, Base64.DEFAULT).decodeToString().dropLast(16) .drop(16) if (Regex("""go\.php\?u=""").find(decodedUri) != null) { decodedUri = - base64Decode(decodedUri.replace(Regex("""(.*?)u="""), "")) + Base64.decode(decodedUri.replace(Regex("""(.*?)u="""), ""), Base64.DEFAULT) + .decodeToString() } return decodedUri diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt index 30f66f83..d1614bc1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt @@ -3,21 +3,17 @@ package com.lagradost.cloudstream3.utils import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.TvType object VideoDownloadHelper { - abstract class DownloadCached( - @JsonProperty("id") open val id: Int, - ) - data class DownloadEpisodeCached( @JsonProperty("name") val name: String?, @JsonProperty("poster") val poster: String?, @JsonProperty("episode") val episode: Int, @JsonProperty("season") val season: Int?, + @JsonProperty("id") val id: Int, @JsonProperty("parentId") val parentId: Int, @JsonProperty("rating") val rating: Int?, @JsonProperty("description") val description: String?, @JsonProperty("cacheTime") val cacheTime: Long, - override val id: Int, - ): DownloadCached(id) + ) data class DownloadHeaderCached( @JsonProperty("apiName") val apiName: String, @@ -25,9 +21,9 @@ object VideoDownloadHelper { @JsonProperty("type") val type: TvType, @JsonProperty("name") val name: String, @JsonProperty("poster") val poster: String?, + @JsonProperty("id") val id: Int, @JsonProperty("cacheTime") val cacheTime: Long, - override val id: Int, - ): DownloadCached(id) + ) data class ResumeWatching( @JsonProperty("parentId") val parentId: Int, diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 2190e03f..50a8df02 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -17,27 +17,23 @@ import androidx.work.Data import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequest import androidx.work.WorkManager -import com.bumptech.glide.Glide import com.bumptech.glide.load.model.GlideUrl import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.api.Log import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.BuildConfig -import com.lagradost.cloudstream3.IDownloadableMinimum import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.services.VideoDownloadService import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.removeKey -import com.lagradost.cloudstream3.utils.SubtitleUtils.deleteMatchingSubtitles import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.safefile.MediaFileContentType import com.lagradost.safefile.SafeFile @@ -45,8 +41,6 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.isActive @@ -113,6 +107,16 @@ object VideoDownloadManager { Stop, } + interface IDownloadableMinimum { + val url: String + val referer: String + val headers: Map + } + + fun IDownloadableMinimum.getId(): Int { + return url.hashCode() + } + data class DownloadEpisodeMetadata( @JsonProperty("id") val id: Int, @JsonProperty("mainName") val mainName: String, @@ -183,7 +187,7 @@ object VideoDownloadManager { private val DOWNLOAD_BAD_CONFIG = DownloadStatus(retrySame = false, tryNext = false, success = false) - const val KEY_RESUME_PACKAGES = "download_resume" + private const val KEY_RESUME_PACKAGES = "download_resume" const val KEY_DOWNLOAD_INFO = "download_info" private const val KEY_RESUME_QUEUE_PACKAGES = "download_q_resume" @@ -230,10 +234,10 @@ object VideoDownloadManager { return cachedBitmaps[url] } - val bitmap = Glide.with(this) + val bitmap = com.bumptech.glide.Glide.with(this) .asBitmap() .load(GlideUrl(url) { headers ?: emptyMap() }) - .submit(720, 720) + .into(720, 720) .get() if (bitmap != null) { @@ -298,7 +302,6 @@ object VideoDownloadManager { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) } else { - //fixme Specify a better flag PendingIntent.getActivity(context, 0, intent, 0) } builder.setContentIntent(pendingIntent) @@ -481,10 +484,10 @@ object VideoDownloadManager { } } - private const val RESERVED_CHARS = "|\\?*<\":>+[]/\'" + private const val reservedChars = "|\\?*<\":>+[]/\'" fun sanitizeFilename(name: String, removeSpaces: Boolean = false): String { var tempName = name - for (c in RESERVED_CHARS) { + for (c in reservedChars) { tempName = tempName.replace(c, ' ') } if (removeSpaces) tempName = tempName.replace(" ", "") @@ -552,8 +555,7 @@ object VideoDownloadManager { tryResume: Boolean, ): StreamData { return setupStream( - context.getBasePath().first ?: getDefaultDir(context) - ?: throw IOException("Bad config"), + context.getBasePath().first ?: getDefaultDir(context) ?: throw IOException("Bad config"), name, folder, extension, @@ -952,7 +954,7 @@ object VideoDownloadManager { bufferSize: Int = DEFAULT_BUFFER_SIZE, /** how many bytes bytes it should require to use the parallel downloader instead, * if we download a very small file we don't want it parallel */ - maximumSmallSize: Long = chuckSize * 2 + maximumSmallSize : Long = chuckSize * 2 ): LazyStreamDownloadData { // we don't want to make a separate connection for every 1kb require(chuckSize > 1000) @@ -1035,10 +1037,7 @@ object VideoDownloadManager { tryResume: Boolean, parentId: Int?, createNotificationCallback: (CreateNotificationMetadata) -> Unit, - parallelConnections: Int = 3, - /** how many bytes a valid file must be in bytes, - * this should be different for subtitles and video */ - minimumSize: Long = 100 + parallelConnections: Int = 3 ): DownloadStatus = withContext(Dispatchers.IO) { if (parallelConnections < 1) { return@withContext DOWNLOAD_INVALID_INPUT @@ -1084,13 +1083,6 @@ object VideoDownloadManager { ) ) - if (items.totalLength != null && items.totalLength < minimumSize) { - fileStream.closeQuietly() - metadata.onDelete() - stream.delete() - return@withContext DOWNLOAD_INVALID_INPUT - } - metadata.totalBytes = items.totalLength metadata.type = DownloadType.IsDownloading metadata.setDownloadFileInfoTemplate( @@ -1240,16 +1232,6 @@ object VideoDownloadManager { return@withContext DOWNLOAD_STOPPED } - // in case the head request lies about content-size, - // then we don't want shit output - if (metadata.bytesDownloaded < minimumSize) { - // we need to close before delete - fileStream.closeQuietly() - metadata.onDelete() - stream.delete() - return@withContext DOWNLOAD_INVALID_INPUT - } - metadata.type = DownloadType.IsDone return@withContext DOWNLOAD_SUCCESS } catch (e: IOException) { @@ -1301,7 +1283,6 @@ object VideoDownloadManager { val displayName = getDisplayName(name, extension) val stream = setupStream(baseFile, name, folder, extension, startAt > 0) - if (!stream.resume) startAt = 0 fileStream = stream.open() @@ -1328,7 +1309,6 @@ object VideoDownloadManager { ) + if (link.referer.isNotBlank()) mapOf("referer" to link.referer) else emptyMap() ) ) - val items = M3u8Helper2.hslLazy(listOf(m3u8)) metadata.hlsTotal = items.size @@ -1426,7 +1406,7 @@ object VideoDownloadManager { try { // may cause java.lang.IllegalStateException: Mutex is not locked because of cancelling fileMutex.unlock() - } catch (t: Throwable) { + } catch (t : Throwable) { logError(t) } } @@ -1553,7 +1533,7 @@ object VideoDownloadManager { tryResume: Boolean = false, ): DownloadStatus { // no support for these file formats - if (link.type == ExtractorLinkType.MAGNET || link.type == ExtractorLinkType.TORRENT || link.type == ExtractorLinkType.DASH) { + if(link.type == ExtractorLinkType.MAGNET || link.type == ExtractorLinkType.TORRENT || link.type == ExtractorLinkType.DASH) { return DOWNLOAD_INVALID_INPUT } @@ -1585,7 +1565,7 @@ object VideoDownloadManager { } try { - when (link.type) { + when(link.type) { ExtractorLinkType.M3U8 -> { val startIndex = if (tryResume) { context.getKey( @@ -1605,7 +1585,6 @@ object VideoDownloadManager { callback, parallelConnections = maxConcurrentConnections ) } - ExtractorLinkType.VIDEO -> { return downloadThing( context, @@ -1615,13 +1594,9 @@ object VideoDownloadManager { "mp4", tryResume, ep.id, - callback, - parallelConnections = maxConcurrentConnections, - /** We require at least 10 MB video files */ - minimumSize = (1 shl 20) * 10 + callback, parallelConnections = maxConcurrentConnections ) } - else -> throw IllegalArgumentException("unsuported download type") } } catch (t: Throwable) { @@ -1705,7 +1680,7 @@ object VideoDownloadManager { } */ fun getDownloadFileInfoAndUpdateSettings(context: Context, id: Int): DownloadedFileInfoResult? = - getDownloadFileInfo(context, id) + getDownloadFileInfo(context, id, removeKeys = true) private fun DownloadedFileInfo.toFile(context: Context): SafeFile? { return basePathToFile(context, this.basePath)?.gotoDirectory(relativePath) @@ -1715,6 +1690,7 @@ object VideoDownloadManager { private fun getDownloadFileInfo( context: Context, id: Int, + removeKeys: Boolean = false ): DownloadedFileInfoResult? { try { val info = @@ -1738,37 +1714,7 @@ object VideoDownloadManager { } } - fun deleteFilesAndUpdateSettings( - context: Context, - ids: Set, - scope: CoroutineScope, - onComplete: (Set) -> Unit = {} - ) { - scope.launchSafe(Dispatchers.IO) { - val deleteJobs = ids.map { id -> - async { - id to deleteFileAndUpdateSettings(context, id) - } - } - val results = deleteJobs.awaitAll() - - val (successfulResults, failedResults) = results.partition { it.second } - val successfulIds = successfulResults.map { it.first }.toSet() - - if (failedResults.isNotEmpty()) { - failedResults.forEach { (id, _) -> - // TODO show a toast if some failed? - Log.e("FileDeletion", "Failed to delete file with ID: $id") - } - } else { - Log.i("FileDeletion", "All files deleted successfully") - } - - onComplete.invoke(successfulIds) - } - } - - private fun deleteFileAndUpdateSettings(context: Context, id: Int): Boolean { + fun deleteFileAndUpdateSettings(context: Context, id: Int): Boolean { val success = deleteFile(context, id) if (success) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) return success @@ -1794,17 +1740,11 @@ object VideoDownloadManager { private fun deleteFile(context: Context, id: Int): Boolean { val info = context.getKey(KEY_DOWNLOAD_INFO, id.toString()) ?: return false - val file = info.toFile(context) - downloadEvent.invoke(id to DownloadActionType.Stop) downloadProgressEvent.invoke(Triple(id, 0, 0)) downloadStatusEvent.invoke(id to DownloadType.IsStopped) downloadDeleteEvent.invoke(id) - - val isFileDeleted = file?.delete() == true || file?.exists() == false - if (isFileDeleted) deleteMatchingSubtitles(context, info) - - return isFileDeleted + return info.toFile(context)?.delete() ?: false } fun getDownloadResumePackage(context: Context, id: Int): DownloadResumePackage? { @@ -1926,4 +1866,4 @@ object VideoDownloadManager { @JsonProperty("ep") val ep: DownloadEpisodeMetadata, @JsonProperty("links") val links: List ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt deleted file mode 100644 index e7c36a87..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.lagradost.cloudstream3.utils.fcast - -import android.content.Context -import android.net.nsd.NsdManager -import android.net.nsd.NsdManager.ResolveListener -import android.net.nsd.NsdServiceInfo -import android.os.Build -import android.util.Log -import com.lagradost.cloudstream3.utils.Coroutines.ioSafe - -class FcastManager { - private var nsdManager: NsdManager? = null - - // Used for receiver - private val registrationListenerTcp = DefaultRegistrationListener() - private fun getDeviceName(): String { - return "${Build.MANUFACTURER}-${Build.MODEL}" - } - - /** - * Start the fcast service - * @param registerReceiver If true will register the app as a compatible fcast receiver for discovery in other app - */ - fun init(context: Context, registerReceiver: Boolean) = ioSafe { - nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager - val serviceType = "_fcast._tcp" - - if (registerReceiver) { - val serviceName = "$APP_PREFIX-${getDeviceName()}" - - val serviceInfo = NsdServiceInfo().apply { - this.serviceName = serviceName - this.serviceType = serviceType - this.port = TCP_PORT - } - - nsdManager?.registerService( - serviceInfo, - NsdManager.PROTOCOL_DNS_SD, - registrationListenerTcp - ) - } - - nsdManager?.discoverServices( - serviceType, - NsdManager.PROTOCOL_DNS_SD, - DefaultDiscoveryListener() - ) - } - - fun stop() { - nsdManager?.unregisterService(registrationListenerTcp) - } - - inner class DefaultDiscoveryListener : NsdManager.DiscoveryListener { - val tag = "DiscoveryListener" - override fun onStartDiscoveryFailed(serviceType: String?, errorCode: Int) { - Log.d(tag, "Discovery failed: $serviceType, error code: $errorCode") - } - - override fun onStopDiscoveryFailed(serviceType: String?, errorCode: Int) { - Log.d(tag, "Stop discovery failed: $serviceType, error code: $errorCode") - } - - override fun onDiscoveryStarted(serviceType: String?) { - Log.d(tag, "Discovery started: $serviceType") - } - - override fun onDiscoveryStopped(serviceType: String?) { - Log.d(tag, "Discovery stopped: $serviceType") - } - - override fun onServiceFound(serviceInfo: NsdServiceInfo?) { - if (serviceInfo == null) return - nsdManager?.resolveService(serviceInfo, object : ResolveListener { - override fun onResolveFailed(serviceInfo: NsdServiceInfo?, errorCode: Int) { - } - - override fun onServiceResolved(serviceInfo: NsdServiceInfo?) { - if (serviceInfo == null) return - - synchronized(_currentDevices) { - _currentDevices.add(PublicDeviceInfo(serviceInfo)) - } - - Log.d( - tag, - "Service found: ${serviceInfo.serviceName}, Net: ${serviceInfo.host.hostAddress}" - ) - } - }) - } - - override fun onServiceLost(serviceInfo: NsdServiceInfo?) { - if (serviceInfo == null) return - - // May remove duplicates, but net and port is null here, preventing device specific identification - synchronized(_currentDevices) { - _currentDevices.removeAll { - it.rawName == serviceInfo.serviceName - } - } - - Log.d(tag, "Service lost: ${serviceInfo.serviceName}") - } - } - - companion object { - const val APP_PREFIX = "CloudStream" - private val _currentDevices: MutableList = mutableListOf() - val currentDevices: List = _currentDevices - - class DefaultRegistrationListener : NsdManager.RegistrationListener { - val tag = "DiscoveryService" - override fun onServiceRegistered(serviceInfo: NsdServiceInfo) { - Log.d(tag, "Service registered: ${serviceInfo.serviceName}") - } - - override fun onRegistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { - Log.e(tag, "Service registration failed: errorCode=$errorCode") - } - - override fun onServiceUnregistered(serviceInfo: NsdServiceInfo) { - Log.d(tag, "Service unregistered: ${serviceInfo.serviceName}") - } - - override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { - Log.e(tag, "Service unregistration failed: errorCode=$errorCode") - } - } - - const val TCP_PORT = 46899 - } -} - -class PublicDeviceInfo(serviceInfo: NsdServiceInfo) { - val rawName: String = serviceInfo.serviceName - val host: String? = serviceInfo.host.hostAddress - val name = rawName.replace("-", " ") + host?.let { " $it" } -} diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt deleted file mode 100644 index 1f33bca4..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.lagradost.cloudstream3.utils.fcast - -import android.util.Log -import androidx.annotation.WorkerThread -import com.lagradost.cloudstream3.utils.AppUtils.toJson -import com.lagradost.cloudstream3.utils.Coroutines.ioSafe -import com.lagradost.safefile.closeQuietly -import java.io.DataOutputStream -import java.net.Socket -import kotlin.jvm.Throws - -class FcastSession(private val hostAddress: String): AutoCloseable { - val tag = "FcastSession" - - private var socket: Socket? = null - @Throws - @WorkerThread - fun open(): Socket { - val socket = Socket(hostAddress, FcastManager.TCP_PORT) - this.socket = socket - return socket - } - - override fun close() { - socket?.closeQuietly() - socket = null - } - - @Throws - private fun acquireSocket(): Socket { - return socket ?: open() - } - - fun ping() { - sendMessage(Opcode.Ping, null) - } - - fun sendMessage(opcode: Opcode, message: T) { - ioSafe { - val socket = acquireSocket() - val outputStream = DataOutputStream(socket.getOutputStream()) - - val json = message?.toJson() - val content = json?.toByteArray() ?: ByteArray(0) - - // Little endian starting from 1 - // https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1 - val size = content.size + 1 - - val sizeArray = ByteArray(4) { num -> - (size shr 8 * num and 0xff).toByte() - } - - Log.d(tag, "Sending message with size: $size, opcode: $opcode") - outputStream.write(sizeArray) - outputStream.write(ByteArray(1) { opcode.value }) - outputStream.write(content) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt deleted file mode 100644 index 61c00d6e..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.lagradost.cloudstream3.utils.fcast - -// See https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1 -enum class Opcode(val value: Byte) { - None(0), - Play(1), - Pause(2), - Resume(3), - Stop(4), - Seek(5), - PlaybackUpdate(6), - VolumeUpdate(7), - SetVolume(8), - PlaybackError(9), - SetSpeed(10), - Version(11), - Ping(12), - Pong(13); -} - - -data class PlayMessage( - val container: String, - val url: String? = null, - val content: String? = null, - val time: Double? = null, - val speed: Double? = null, - val headers: Map? = null -) - -data class SeekMessage( - val time: Double -) - -data class PlaybackUpdateMessage( - val generationTime: Long, - val time: Double, - val duration: Double, - val state: Int, - val speed: Double -) - -data class VolumeUpdateMessage( - val generationTime: Long, - val volume: Double -) - -data class PlaybackErrorMessage( - val message: String -) - -data class SetSpeedMessage( - val speed: Double -) - -data class SetVolumeMessage( - val volume: Double -) - -data class VersionMessage( - val version: Long -) diff --git a/app/src/main/java/com/lagradost/cloudstream3/widget/FlowLayout.kt b/app/src/main/java/com/lagradost/cloudstream3/widget/FlowLayout.kt index 2aea0b8d..d4725d53 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/widget/FlowLayout.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/widget/FlowLayout.kt @@ -19,7 +19,7 @@ class FlowLayout : ViewGroup { @SuppressLint("CustomViewStyleable") internal constructor(c: Context, attrs: AttributeSet?) : super(c, attrs) { val t = c.obtainStyledAttributes(attrs, R.styleable.FlowLayout_Layout) - itemSpacing = t.getDimensionPixelSize(R.styleable.FlowLayout_Layout_itemSpacing, 0) + itemSpacing = t.getDimensionPixelSize(R.styleable.FlowLayout_Layout_itemSpacing, 0); t.recycle() } diff --git a/app/src/main/res/drawable/cloud_2_solid.xml b/app/src/main/res/drawable/cloud_2_solid.xml deleted file mode 100644 index 3810b4bf..00000000 --- a/app/src/main/res/drawable/cloud_2_solid.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/example_qr.png b/app/src/main/res/drawable/example_qr.png deleted file mode 100644 index 764cb966..00000000 Binary files a/app/src/main/res/drawable/example_qr.png and /dev/null differ diff --git a/app/src/main/res/drawable/hourglass_24.xml b/app/src/main/res/drawable/hourglass_24.xml deleted file mode 100644 index 7bd1ebbd..00000000 --- a/app/src/main/res/drawable/hourglass_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_baseline_equalizer_24.xml b/app/src/main/res/drawable/ic_baseline_equalizer_24.xml deleted file mode 100644 index cd20ad15..00000000 --- a/app/src/main/res/drawable/ic_baseline_equalizer_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_baseline_replay_24.xml b/app/src/main/res/drawable/ic_baseline_replay_24.xml deleted file mode 100644 index e247aa92..00000000 --- a/app/src/main/res/drawable/ic_baseline_replay_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_baseline_restart_24.xml b/app/src/main/res/drawable/ic_baseline_restart_24.xml deleted file mode 100644 index aed3a562..00000000 --- a/app/src/main/res/drawable/ic_baseline_restart_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_baseline_skip_next_24_big.xml b/app/src/main/res/drawable/ic_baseline_skip_next_24_big.xml deleted file mode 100644 index a8c43bbd..00000000 --- a/app/src/main/res/drawable/ic_baseline_skip_next_24_big.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_baseline_skip_next_rounded_24.xml b/app/src/main/res/drawable/ic_baseline_skip_next_rounded_24.xml deleted file mode 100644 index 452c4dd9..00000000 --- a/app/src/main/res/drawable/ic_baseline_skip_next_rounded_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_battery.xml b/app/src/main/res/drawable/ic_battery.xml deleted file mode 100644 index 24d0a77f..00000000 --- a/app/src/main/res/drawable/ic_battery.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_network_stream.xml b/app/src/main/res/drawable/ic_network_stream.xml deleted file mode 100644 index 8e21fd25..00000000 --- a/app/src/main/res/drawable/ic_network_stream.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/preview_seekbar_24.xml b/app/src/main/res/drawable/preview_seekbar_24.xml deleted file mode 100644 index 657f6247..00000000 --- a/app/src/main/res/drawable/preview_seekbar_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/rounded_outline.xml b/app/src/main/res/drawable/rounded_outline.xml deleted file mode 100644 index b85ace8e..00000000 --- a/app/src/main/res/drawable/rounded_outline.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/drawable/subdl_logo_big.xml b/app/src/main/res/drawable/subdl_logo_big.xml deleted file mode 100644 index a6cbb311..00000000 --- a/app/src/main/res/drawable/subdl_logo_big.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - diff --git a/app/src/main/res/layout/account_managment.xml b/app/src/main/res/layout/account_managment.xml index e7afb382..389a3406 100644 --- a/app/src/main/res/layout/account_managment.xml +++ b/app/src/main/res/layout/account_managment.xml @@ -62,16 +62,14 @@ + android:id="@+id/account_switch_account" + android:text="@string/switch_account" + style="@style/SettingsItem" /> + android:id="@+id/account_logout" + android:text="@string/logout" + style="@style/SettingsItem"> diff --git a/app/src/main/res/layout/account_single.xml b/app/src/main/res/layout/account_single.xml index c4f7fa39..cbfb9f18 100644 --- a/app/src/main/res/layout/account_single.xml +++ b/app/src/main/res/layout/account_single.xml @@ -1,11 +1,10 @@ + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:foreground="?android:attr/selectableItemBackgroundBorderless" + android:orientation="horizontal" + android:layout_height="wrap_content" + android:layout_width="match_parent"> + android:id="@+id/account_profile_picture" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:ignore="ContentDescription" /> + android:foreground="@null" + android:id="@+id/account_name" + tools:text="Account 1" + style="@style/SettingsItem" /> diff --git a/app/src/main/res/layout/account_switch.xml b/app/src/main/res/layout/account_switch.xml index 5153f0e3..659ad840 100644 --- a/app/src/main/res/layout/account_switch.xml +++ b/app/src/main/res/layout/account_switch.xml @@ -7,20 +7,18 @@ android:layout_height="match_parent"> + android:id="@+id/account_list" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + android:background="?attr/primaryBlackBackground" + tools:listitem="@layout/account_single" + android:layout_width="match_parent" + android:layout_rowWeight="1" + android:layout_height="wrap_content" /> + android:id="@+id/account_add" + android:text="@string/add_account" + style="@style/SettingsItem"> diff --git a/app/src/main/res/layout/add_remove_sites.xml b/app/src/main/res/layout/add_remove_sites.xml index 653f607f..9ef6ad6a 100644 --- a/app/src/main/res/layout/add_remove_sites.xml +++ b/app/src/main/res/layout/add_remove_sites.xml @@ -1,21 +1,19 @@ + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent"> + android:id="@+id/add_site" + android:text="@string/add_site_pref" + style="@style/SettingsItem"> + android:id="@+id/remove_site" + android:text="@string/remove_site_pref" + style="@style/SettingsItem" /> \ No newline at end of file diff --git a/app/src/main/res/layout/add_repo_input.xml b/app/src/main/res/layout/add_repo_input.xml index cb4224d1..6f6b4d5b 100644 --- a/app/src/main/res/layout/add_repo_input.xml +++ b/app/src/main/res/layout/add_repo_input.xml @@ -72,7 +72,7 @@ android:inputType="text" android:nextFocusLeft="@id/apply_btt" android:nextFocusRight="@id/cancel_btt" - android:nextFocusDown="@id/repo_url_input" + android:nextFocusDown="@id/site_url_input" android:requiresFadingEdge="vertical" android:textColorHint="?attr/grayTextColor" tools:ignore="LabelFor" /> @@ -85,8 +85,9 @@ android:inputType="textUri" android:nextFocusLeft="@id/apply_btt" android:nextFocusRight="@id/cancel_btt" - android:nextFocusUp="@id/repo_name_input" - android:nextFocusDown="@id/apply_btt" + + android:nextFocusUp="@id/site_name_input" + android:nextFocusDown="@id/site_lang_input" android:requiresFadingEdge="vertical" android:textColorHint="?attr/grayTextColor" tools:ignore="LabelFor" /> diff --git a/app/src/main/res/layout/device_auth.xml b/app/src/main/res/layout/device_auth.xml deleted file mode 100644 index 38ff1325..00000000 --- a/app/src/main/res/layout/device_auth.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_online_subtitles.xml b/app/src/main/res/layout/dialog_online_subtitles.xml index e0eac5e0..7803e261 100644 --- a/app/src/main/res/layout/dialog_online_subtitles.xml +++ b/app/src/main/res/layout/dialog_online_subtitles.xml @@ -40,7 +40,7 @@ android:layout_width="match_parent" android:layout_height="30dp" android:layout_gravity="center_vertical" - android:layout_marginEnd="40dp"> + android:layout_marginEnd="30dp"> @@ -107,8 +106,7 @@ android:layout_margin="10dp" android:background="?selectableItemBackgroundBorderless" android:contentDescription="@string/change_providers_img_des" - android:focusable="true" - android:nextFocusLeft="@id/year_btt" + android:nextFocusLeft="@id/main_search" android:nextFocusRight="@id/main_search" android:nextFocusUp="@id/nav_rail_view" android:nextFocusDown="@id/search_autofit_results" diff --git a/app/src/main/res/layout/download_child_episode.xml b/app/src/main/res/layout/download_child_episode.xml index e53e63d3..fd845ee8 100644 --- a/app/src/main/res/layout/download_child_episode.xml +++ b/app/src/main/res/layout/download_child_episode.xml @@ -2,12 +2,13 @@ @@ -73,26 +77,14 @@ tools:text="128MB / 237MB" /> + - - \ No newline at end of file diff --git a/app/src/main/res/layout/download_header_episode.xml b/app/src/main/res/layout/download_header_episode.xml index 385fb2e0..226c1632 100644 --- a/app/src/main/res/layout/download_header_episode.xml +++ b/app/src/main/res/layout/download_header_episode.xml @@ -9,22 +9,9 @@ android:layout_marginTop="10dp" android:layout_marginEnd="10dp" android:foreground="@drawable/outline_drawable" - android:focusable="true" - android:nextFocusRight="@id/download_button" app:cardBackgroundColor="?attr/boxItemBackground" app:cardCornerRadius="@dimen/rounded_image_radius"> - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_child_downloads.xml b/app/src/main/res/layout/fragment_child_downloads.xml index 64ed1d70..9afaea0b 100644 --- a/app/src/main/res/layout/fragment_child_downloads.xml +++ b/app/src/main/res/layout/fragment_child_downloads.xml @@ -7,69 +7,13 @@ android:layout_height="match_parent" android:background="?attr/primaryGrayBackground" android:orientation="vertical" - tools:context=".ui.download.DownloadChildFragment"> + tools:context=".ui.download.DownloadFragment"> - - - - -