Merge remote-tracking branch 'origin/master'

# Conflicts:
#	app/build.gradle.kts
#	app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
#	app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt
#	app/src/main/res/values/colors.xml
#	app/src/main/res/values/strings.xml
#	app/src/main/res/values/styles.xml
#	app/src/main/res/xml/settings_providers.xml
This commit is contained in:
KillerDogeEmpire 2022-12-19 22:09:59 -08:00
commit e4cc642c81
171 changed files with 6952 additions and 4493 deletions

76
.github/workflows/build_to_archive.yml vendored Normal file
View File

@ -0,0 +1,76 @@
name: Archive build
on:
push:
branches: [ master ]
paths-ignore:
- '*.md'
- '*.json'
- '**/wcokey.txt'
workflow_dispatch:
concurrency:
group: "Archive-build"
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Generate access token
id: generate_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/secrets"
- name: Generate access token (archive)
id: generate_archive_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/cloudstream-archive"
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v2
with:
java-version: '11'
distribution: 'adopt'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Fetch keystore
id: fetch_keystore
run: |
TMP_KEYSTORE_FILE_PATH="${RUNNER_TEMP}"/keystore
mkdir -p "${TMP_KEYSTORE_FILE_PATH}"
curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "${TMP_KEYSTORE_FILE_PATH}/prerelease_keystore.keystore" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore.jks"
curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "keystore_password.txt" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore_password.txt"
KEY_PWD="$(cat keystore_password.txt)"
echo "::add-mask::${KEY_PWD}"
echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
- name: Run Gradle
run: |
./gradlew assemblePrerelease
env:
SIGNING_KEY_ALIAS: "key0"
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
- uses: actions/checkout@v3
with:
repository: "recloudstream/cloudstream-archive"
token: ${{ steps.generate_archive_token.outputs.token }}
path: "archive"
- name: Move build
run: |
cp app/build/outputs/apk/prerelease/release/*.apk "archive/$(git rev-parse --short HEAD).apk"
- name: Push archive
run: |
cd $GITHUB_WORKSPACE/archive
git config --local user.email "actions@github.com"
git config --local user.name "GitHub Actions"
git add .
git commit --amend -m "Build $GITHUB_SHA" || exit 0 # do not error if nothing to commit
git push --force

View File

@ -5,21 +5,29 @@
[![Discord](https://invidget.switchblade.xyz/W8yeGuTwWQ)](https://discord.gg/W8yeGuTwWQ)
***Features:***
### Features:
+ **AdFree**, No ads whatsoever
+ No tracking/analytics
+ Bookmarks
+ Download and stream movies, tv-shows and anime
+ Chromecast
***Screenshots:***
### Screenshots:
<img src="./.github/home.jpg" height="400"/><img src="./.github/search.jpg" height="400"/><img src="./.github/downloads.jpg" height="400"/><img src="./.github/results.jpg" height="400"/>
<img src="./.github/player.jpg" height="200"/>
### Supported languages:
<a href="https://hosted.weblate.org/engage/cloudstream/">
<img src="https://hosted.weblate.org/widgets/cloudstream/-/app/multi-auto.svg" alt="Translation status" />
</a>
<!--
***The list of supported languages:***
* 🇱🇧 Arabic
* 🇧🇬 Bulgarian
* 🇨🇳 Chinese Simplified
* 🇹🇼 Chinese Traditional
* 🇭🇷 Croatian
* 🇨🇿 Czech
* 🇳🇱 Dutch
@ -41,4 +49,4 @@
* 🇵🇭 Tagalog
* 🇹🇷 Turkish
* 🇻🇳 Vietnamese
-->

View File

@ -39,16 +39,16 @@ android {
}
}
compileSdk = 31
compileSdk = 33
buildToolsVersion = "30.0.3"
defaultConfig {
applicationId = "com.killerdogeempire.aquastream"
applicationId = "com.lagradost.cloudstream3"
minSdk = 21
targetSdk = 30
targetSdk = 33
versionCode = 52
versionName = "1.0.2"
versionCode = 55
versionName = "3.3.0"
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
@ -155,10 +155,10 @@ dependencies {
// implementation("androidx.leanback:leanback-paging:1.1.0-alpha09")
// Exoplayer
implementation("com.google.android.exoplayer:exoplayer:2.16.1")
implementation("com.google.android.exoplayer:extension-cast:2.16.1")
implementation("com.google.android.exoplayer:extension-mediasession:2.16.1")
implementation("com.google.android.exoplayer:extension-okhttp:2.16.1")
implementation("com.google.android.exoplayer:exoplayer:2.18.2")
implementation("com.google.android.exoplayer:extension-cast:2.18.2")
implementation("com.google.android.exoplayer:extension-mediasession:2.18.2")
implementation("com.google.android.exoplayer:extension-okhttp:2.18.2")
//implementation("com.google.android.exoplayer:extension-leanback:2.14.0")
@ -176,7 +176,9 @@ dependencies {
implementation("com.jaredrummler:colorpicker:1.1.0")
//run JS
implementation("org.mozilla:rhino:1.7.14")
// do not upgrade to 1.7.14, since in 1.7.14 Rhino uses the `SourceVersion` class, which is not
// available on Android (even when using desugaring), and `NoClassDefFoundError` is thrown
implementation("org.mozilla:rhino:1.7.13")
// TorrentStream
//implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0")
@ -188,8 +190,9 @@ dependencies {
// Networking
// implementation("com.squareup.okhttp3:okhttp:4.9.2")
// implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1")
implementation("com.github.Blatzar:NiceHttp:0.3.3")
implementation("com.github.Blatzar:NiceHttp:0.4.1")
// To fix SSL fuckery on android 9
implementation("org.conscrypt:conscrypt-android:2.2.1")
// Util to skip the URI file fuckery 🙏
implementation("com.github.tachiyomiorg:unifile:17bec43")
@ -211,9 +214,9 @@ dependencies {
// slow af yt
//implementation("com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT")
// newpipe yt
implementation("com.github.TeamNewPipe:NewPipeExtractor:dev-SNAPSHOT")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
// newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L190
implementation("com.github.TeamNewPipe:NewPipeExtractor:9ffdd0948b2ecd82655f5ff2a3e127b2b7695d5b")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6")
// Library/extensions searching with Levenshtein distance
implementation("me.xdrop:fuzzywuzzy:1.4.0")
@ -247,4 +250,4 @@ tasks.withType<DokkaTask>().configureEach {
}
}
}
}
}

View File

@ -180,7 +180,7 @@ class ExampleInstrumentedTest {
@Test
fun providerCorrectHomepage() {
runBlocking {
getAllProviders().apmap { api ->
getAllProviders().amap { api ->
if (api.hasMainPage) {
try {
val homepage = api.getMainPage()
@ -217,7 +217,7 @@ class ExampleInstrumentedTest {
runBlocking {
val invalidProvider = ArrayList<Pair<MainAPI, Exception?>>()
val providers = getAllProviders()
providers.apmap { api ->
providers.amap { api ->
try {
println("Trying $api")
if (testSingleProviderApi(api)) {

View File

@ -10,7 +10,11 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <!-- some dependency needs this -->
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <!-- Used for player vertical slide -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <!-- Used for app update -->
<!-- <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> not used atm, but code exist that requires it that are not run -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- Used for app notifications on Android 13+ -->
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> <!-- Used for Android TV watch next -->
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- Used for updates without prompt -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Used for update service -->
<!-- <permission android:name="android.permission.QUERY_ALL_PACKAGES" /> &lt;!&ndash; Used for getting if vlc is installed &ndash;&gt; -->
<!-- Fixes android tv fuckery -->
<uses-feature
@ -110,6 +114,30 @@
<data android:scheme="cloudstreamrepo" />
</intent-filter>
<!-- Allow searching with intents: cloudstreamsearch://Your%20Name -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="cloudstreamsearch" />
</intent-filter>
<!--
Allow opening from continue watching with intents: cloudstreamsearch://1234
Used on Android TV Watch Next
-->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="cloudstreamcontinuewatching" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@ -145,6 +173,10 @@
android:name=".ui.ControllerActivity"
android:exported="false" />
<service
android:name=".utils.PackageInstallerService"
android:exported="false" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"

View File

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3
import android.Manifest
import android.app.Activity
import android.app.PictureInPictureParams
import android.content.Context
@ -16,6 +17,7 @@ import androidx.annotation.MainThread
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import com.google.android.gms.cast.framework.CastSession
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
@ -61,7 +63,9 @@ object CommonActivity {
}
}
fun showToast(act: Activity?, @StringRes message: Int, duration: Int) {
/** duration is Toast.LENGTH_SHORT if null*/
@MainThread
fun showToast(act: Activity?, @StringRes message: Int, duration: Int? = null) {
if (act == null) return
showToast(act, act.getString(message), duration)
}
@ -69,6 +73,7 @@ object CommonActivity {
const val TAG = "COMPACT"
/** duration is Toast.LENGTH_SHORT if null*/
@MainThread
fun showToast(act: Activity?, message: String?, duration: Int? = null) {
if (act == null || message == null) {
Log.w(TAG, "invalid showToast act = $act message = $message")
@ -105,9 +110,18 @@ object CommonActivity {
}
}
/**
* Not all languages can be fetched from locale with a code.
* This map allows sidestepping the default Locale(languageCode)
* when setting the app language.
**/
val appLanguageExceptions = hashMapOf(
"zh_TW" to Locale.TRADITIONAL_CHINESE
)
fun setLocale(context: Context?, languageCode: String?) {
if (context == null || languageCode == null) return
val locale = Locale(languageCode)
val locale = appLanguageExceptions[languageCode] ?: Locale(languageCode)
val resources: Resources = context.resources
val config = resources.configuration
Locale.setDefault(locale)
@ -143,8 +157,8 @@ object CommonActivity {
val resultCode = result.resultCode
val data = result.data
if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) {
val pos = data.getLongExtra(resumeApp.position, -1L)
val dur = data.getLongExtra(resumeApp.duration, -1L)
val pos = resumeApp.getPosition(data)
val dur = resumeApp.getDuration(data)
if (dur > 0L && pos > 0L)
DataStoreHelper.setViewPos(getKey(resumeApp.lastId), pos, dur)
removeKey(resumeApp.lastId)
@ -152,6 +166,23 @@ object CommonActivity {
}
}
}
// Ask for notification permissions on Android 13
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(
act,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
val requestPermissionLauncher = act.registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
Log.d(TAG, "Notification permission: $isGranted")
}
requestPermissionLauncher.launch(
Manifest.permission.POST_NOTIFICATIONS
)
}
}
private fun Activity.enterPIPMode() {
@ -339,6 +370,9 @@ object CommonActivity {
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_4 -> {
PlayerEventType.SkipOp
}
KeyEvent.KEYCODE_V, KeyEvent.KEYCODE_NUMPAD_5, KeyEvent.KEYCODE_5 -> {
PlayerEventType.SkipCurrentChapter
}
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_ENTER -> { // space is not captured due to navigation
PlayerEventType.PlayPauseToggle
}
@ -417,4 +451,4 @@ object CommonActivity {
}
return null
}
}
}

View File

@ -18,7 +18,6 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSet
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.SubtitleHelper
import okhttp3.Interceptor
import java.text.SimpleDateFormat
import java.util.*
@ -31,6 +30,12 @@ const val 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"
object APIHolder {
val unixTime: Long
get() = System.currentTimeMillis() / 1000L
@ -76,7 +81,8 @@ object APIHolder {
synchronized(allProviders) {
initMap()
return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
?: allProviders.firstOrNull { it.name == apiName }
// Leave the ?. null check, it can crash regardless
?: allProviders.firstOrNull { it?.name == apiName }
}
}
@ -159,7 +165,9 @@ object APIHolder {
val hashSet = HashSet<String>()
val activeLangs = getApiProviderLangSettings()
hashSet.addAll(apis.filter { activeLangs.contains(it.lang) }.map { it.name })
val hasUniversal = activeLangs.contains(AllLanguagesName)
hashSet.addAll(apis.filter { hasUniversal || activeLangs.contains(it.lang) }
.map { it.name })
/*val set = settingsManager.getStringSet(
this.getString(R.string.search_providers_list_key),
@ -193,26 +201,17 @@ object APIHolder {
return list.filter { names.contains(it) }.map { DubStatus.valueOf(it) }.toHashSet()
}
/**
* Gets all the activated provider languages
* Used to obey the preference provider_lang_key
* but it turned out too complicated and unnecessary with extensions.
**/
fun Context.getApiProviderLangSettings(): HashSet<String> {
//val langs = apis.map { it.lang }.toSet()
//.sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) }
//return langs.toHashSet()
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val hashSet = HashSet<String>()
hashSet.add("en") // def is only en
val list = settingsManager.getStringSet(
this.getString(R.string.provider_lang_key),
hashSet.toMutableSet()
)
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()
return list.toHashSet()
}
fun Context.getApiTypeSettings(): HashSet<TvType> {
@ -244,7 +243,19 @@ object APIHolder {
}
fun Context.filterProviderByPreferredMedia(hasHomePageIsRequired: Boolean = true): List<MainAPI> {
val default = enumValues<TvType>().sorted().filter { it != TvType.NSFW }.map { it.ordinal }
// 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)
@ -254,7 +265,8 @@ object APIHolder {
null
} ?: default
val langs = this.getApiProviderLangSettings()
val allApis = apis.filter { langs.contains(it.lang) }
val hasUniversal = langs.contains(AllLanguagesName)
val allApis = apis.filter { hasUniversal || langs.contains(it.lang) }
.filter { api -> api.hasMainPage || !hasHomePageIsRequired }
return if (currentPrefMedia.isEmpty()) {
allApis
@ -1066,7 +1078,7 @@ interface LoadResponse {
) {
if (!isTrailersEnabled || trailerUrls == null) return
trailers.addAll(trailerUrls.map { TrailerData(it, referer, addRaw) })
/*val trailers = trailerUrls.filter { it.isNotBlank() }.apmap { trailerUrl ->
/*val trailers = trailerUrls.filter { it.isNotBlank() }.amap { trailerUrl ->
val links = arrayListOf<ExtractorLink>()
val subs = arrayListOf<SubtitleFile>()
if (!loadExtractor(
@ -1127,18 +1139,43 @@ interface LoadResponse {
fun getDurationFromString(input: String?): Int? {
val cleanInput = input?.trim()?.replace(" ", "") ?: return null
//Use first as sometimes the text passes on the 2 other Regex, but failed to provide accurate return value
Regex("(\\d+\\shr)|(\\d+\\shour)|(\\d+\\smin)|(\\d+\\ssec)").findAll(input).let { values ->
var seconds = 0
values.forEach {
val time_text = it.value
if (time_text.isNotBlank()) {
val time = time_text.filter { s -> s.isDigit() }.trim().toInt()
val scale = time_text.filter { s -> !s.isDigit() }.trim()
//println("Scale: $scale")
val timeval = when (scale) {
"hr", "hour" -> time * 60 * 60
"min" -> time * 60
"sec" -> time
else -> 0
}
seconds += timeval
}
}
if (seconds > 0) {
return seconds / 60
}
}
Regex("([0-9]*)h.*?([0-9]*)m").find(cleanInput)?.groupValues?.let { values ->
if (values.size == 3) {
val hours = values[1].toIntOrNull()
val minutes = values[2].toIntOrNull()
return if (minutes != null && hours != null) {
hours * 60 + minutes
} else null
if (minutes != null && hours != null) {
return hours * 60 + minutes
}
}
}
Regex("([0-9]*)m").find(cleanInput)?.groupValues?.let { values ->
if (values.size == 2) {
return values[1].toIntOrNull()
val return_value = values[1].toIntOrNull()
if (return_value != null) {
return return_value
}
}
}
return null

View File

@ -44,19 +44,27 @@ import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.CommonActivity.updateLocale
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.PluginManager.loadAllOnlinePlugins
import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.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.ui.APIRepository
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
import com.lagradost.cloudstream3.ui.home.HomeViewModel
import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
import com.lagradost.cloudstream3.ui.search.SearchFragment
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsGeneral
import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY
@ -65,6 +73,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
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.BackupUtils.setUpBackup
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DataStore.getKey
@ -90,6 +99,7 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.File
import java.net.URI
import java.net.URLDecoder
import java.nio.charset.Charset
import kotlin.reflect.KClass
@ -112,13 +122,15 @@ val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlay
val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity")
//TODO REFACTOR AF
data class ResultResume(
open class ResultResume(
val packageString: String,
val action: String = Intent.ACTION_VIEW,
val position: String? = null,
val duration: String? = null,
var launcher: ActivityResultLauncher<Intent>? = null,
) {
val defaultTime = -1L
val lastId get() = "${packageString}_last_open_id"
suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) {
val intent = Intent(action)
@ -132,21 +144,45 @@ data class ResultResume(
callback.invoke(intent)
launcher?.launch(intent)
}
open fun getPosition(intent: Intent?): Long {
return defaultTime
}
open fun getDuration(intent: Intent?): Long {
return defaultTime
}
}
val VLC = ResultResume(
val VLC = object : ResultResume(
VLC_PACKAGE,
"org.videolan.vlc.player.result",
"extra_position",
"extra_duration",
)
) {
override fun getPosition(intent: Intent?): Long {
return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime
}
val MPV = ResultResume(
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",
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)
@ -185,8 +221,20 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
companion object {
const val TAG = "MAINACT"
/**
* Setting this will automatically enter the query in the search
* next time the search fragment is opened.
* This variable will clear itself after one use. Null does nothing.
*
* This is a very bad solution but I was unable to find a better one.
**/
private var nextSearchQuery: String? = null
/**
* Fires every time a new batch of plugins have been loaded, no guarantee about how often this is run and on which thread
* Boolean signifies if stuff should be force reloaded (true if force reload, false if reload when necessary).
*
* The force reloading are used for plugin development to instantly reload the page on deployWithAdb
* */
val afterPluginsLoadedEvent = Event<Boolean>()
val mainPluginsLoadedEvent =
@ -203,6 +251,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
isWebview: Boolean
): Boolean =
with(activity) {
// Invalid URIs can crash
fun safeURI(uri: String) = normalSafeApiCall { URI(uri) }
if (str != null && this != null) {
if (str.startsWith("https://cs.repo")) {
val realUrl = "https://" + str.substringAfter("?")
@ -238,10 +289,32 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
return true
}
}
} else if (URI(str).scheme == appStringRepo) {
// This specific intent is used for the gradle deployWithAdb
// https://github.com/recloudstream/gradle/blob/master/src/main/kotlin/com/lagradost/cloudstream3/gradle/tasks/DeployWithAdbTask.kt#L46
if (str == "$appString:") {
PluginManager.hotReloadAllLocalPlugins(activity)
}
} else if (safeURI(str)?.scheme == appStringRepo) {
val url = str.replaceFirst(appStringRepo, "https")
loadRepository(url)
return true
} else if (safeURI(str)?.scheme == appStringSearch) {
nextSearchQuery =
URLDecoder.decode(str.substringAfter("$appStringSearch://"), "UTF-8")
nav_view.selectedItemId = R.id.navigation_search
} else if (safeURI(str)?.scheme == appStringResumeWatching) {
val id =
str.substringAfter("$appStringResumeWatching://").toIntOrNull()
?: return false
ioSafe {
val resumeWatchingCard =
HomeViewModel.getResumeWatching()?.firstOrNull { it.id == id }
?: return@ioSafe
activity.loadSearchResult(
resumeWatchingCard,
START_ACTION_RESUME_LATEST
)
}
} else if (!isWebview) {
if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) {
this.navigate(R.id.navigation_downloads)
@ -402,12 +475,33 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
onUserLeaveHint(this)
}
private fun showConfirmExitDialog() {
val builder: AlertDialog.Builder = AlertDialog.Builder(this)
builder.setTitle(R.string.confirm_exit_dialog)
builder.apply {
setPositiveButton(R.string.yes) { _, _ -> super.onBackPressed() }
setNegativeButton(R.string.no) { _, _ -> }
}
builder.show()
}
private fun backPressed() {
this.window?.navigationBarColor =
this.colorFromAttribute(R.attr.primaryGrayBackground)
this.updateLocale()
super.onBackPressed()
this.updateLocale()
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment
val navController = navHostFragment?.navController
val isAtHome =
navController?.currentDestination?.matchDestination(R.id.navigation_home) == true
if (isAtHome && isTrueTvSettings()) {
showConfirmExitDialog()
} else {
super.onBackPressed()
}
}
override fun onBackPressed() {
@ -550,12 +644,21 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
) {
PluginManager.updateAllOnlinePluginsAndLoadThem(this@MainActivity)
} else {
PluginManager.loadAllOnlinePlugins(this@MainActivity)
loadAllOnlinePlugins(this@MainActivity)
}
//Automatically download not existing plugins
if (settingsManager.getBoolean(
getString(R.string.auto_download_plugins_key),
false
)
) {
PluginManager.downloadNotExistingPluginsAndLoad(this@MainActivity)
}
}
ioSafe {
PluginManager.loadAllLocalPlugins(this@MainActivity)
PluginManager.loadAllLocalPlugins(this@MainActivity, false)
}
}
} else {
@ -592,7 +695,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
api.init()
}
inAppAuths.apmap { api ->
inAppAuths.amap { api ->
try {
api.initialize()
} catch (e: Exception) {
@ -616,6 +719,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
navController.addOnDestinationChangedListener { _: NavController, navDestination: NavDestination, bundle: Bundle? ->
// Intercept search and add a query
if (navDestination.matchDestination(R.id.navigation_search) && !nextSearchQuery.isNullOrBlank()) {
bundle?.apply {
this.putString(SearchFragment.SEARCH_QUERY, nextSearchQuery)
nextSearchQuery = null
}
}
}
//val navController = findNavController(R.id.nav_host_fragment)
/*navOptions = NavOptions.Builder()

View File

@ -1,8 +1,7 @@
package com.lagradost.cloudstream3
import com.lagradost.cloudstream3.mvvm.logError
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.*
//https://stackoverflow.com/questions/34697828/parallel-operations-on-kotlin-collections
/*
@ -26,10 +25,25 @@ fun <T, R> Iterable<T>.pmap(
return ArrayList<R>(destination)
}*/
@OptIn(DelicateCoroutinesApi::class)
suspend fun <K, V, R> Map<out K, V>.amap(f: suspend (Map.Entry<K, V>) -> R): List<R> =
with(CoroutineScope(GlobalScope.coroutineContext)) {
map { async { f(it) } }.map { it.await() }
}
fun <K, V, R> Map<out K, V>.apmap(f: suspend (Map.Entry<K, V>) -> R): List<R> = runBlocking {
map { async { f(it) } }.map { it.await() }
}
@OptIn(DelicateCoroutinesApi::class)
suspend fun <A, B> List<A>.amap(f: suspend (A) -> B): List<B> =
with(CoroutineScope(GlobalScope.coroutineContext)) {
map { async { f(it) } }.map { it.await() }
}
fun <A, B> List<A>.apmap(f: suspend (A) -> B): List<B> = runBlocking {
map { async { f(it) } }.map { it.await() }
}
@ -38,6 +52,12 @@ fun <A, B> List<A>.apmapIndexed(f: suspend (index: Int, A) -> B): List<B> = runB
mapIndexed { index, a -> async { f(index, a) } }.map { it.await() }
}
@OptIn(DelicateCoroutinesApi::class)
suspend fun <A, B> List<A>.amapIndexed(f: suspend (index: Int, A) -> B): List<B> =
with(CoroutineScope(GlobalScope.coroutineContext)) {
mapIndexed { index, a -> async { f(index, a) } }.map { it.await() }
}
// run code in parallel
/*fun <R> argpmap(
vararg transforms: () -> R,

View File

@ -2,10 +2,11 @@ package com.lagradost.cloudstream3.extractors
import android.util.Log
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.base64Decode
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
class AStreamHub : ExtractorApi() {
open class AStreamHub : ExtractorApi() {
override val name = "AStreamHub"
override val mainUrl = "https://astreamhub.com"
override val requiresReferer = true

View File

@ -4,7 +4,7 @@ import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.base64Decode
import com.lagradost.cloudstream3.utils.*
class Acefile : ExtractorApi() {
open class Acefile : ExtractorApi() {
override val name = "Acefile"
override val mainUrl = "https://acefile.co"
override val requiresReferer = false

View File

@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.M3u8Helper
import com.lagradost.cloudstream3.utils.getQualityFromName
import java.net.URI
class AsianLoad : ExtractorApi() {
open class AsianLoad : ExtractorApi() {
override var name = "AsianLoad"
override var mainUrl = "https://asianembed.io"
override val requiresReferer = true

View File

@ -5,7 +5,7 @@ import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
class Blogger : ExtractorApi() {
open class Blogger : ExtractorApi() {
override val name = "Blogger"
override val mainUrl = "https://www.blogger.com"
override val requiresReferer = false

View File

@ -5,7 +5,7 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
class BullStream : ExtractorApi() {
open class BullStream : ExtractorApi() {
override val name = "BullStream"
override val mainUrl = "https://bullstream.xyz"
override val requiresReferer = false
@ -18,7 +18,7 @@ class BullStream : ExtractorApi() {
?: return null
val m3u8 = "$mainUrl/m3u8/${data[1]}/${data[2]}/master.txt?s=1&cache=${data[4]}"
println("shiv : $m3u8")
//println("shiv : $m3u8")
return M3u8Helper.generateM3u8(
name,
m3u8,

View File

@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName
import com.lagradost.cloudstream3.utils.httpsify
class Embedgram : ExtractorApi() {
open class Embedgram : ExtractorApi() {
override val name = "Embedgram"
override val mainUrl = "https://embedgram.com"
override val requiresReferer = true

View File

@ -1,12 +1,12 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
class Fastream: ExtractorApi() {
open class Fastream: ExtractorApi() {
override var mainUrl = "https://fastream.to"
override var name = "Fastream"
override val requiresReferer = false
@ -21,10 +21,10 @@ class Fastream: ExtractorApi() {
Pair("file_code",id),
Pair("auto","1")
)).document
response.select("script").apmap { script ->
response.select("script").amap { script ->
if (script.data().contains("sources")) {
val m3u8regex = Regex("((https:|http:)\\/\\/.*\\.m3u8)")
val m3u8 = m3u8regex.find(script.data())?.value ?: return@apmap
val m3u8 = m3u8regex.find(script.data())?.value ?: return@amap
generateM3u8(
name,
m3u8,

View File

@ -5,7 +5,7 @@ import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
class Filesim : ExtractorApi() {
open class Filesim : ExtractorApi() {
override val name = "Filesim"
override val mainUrl = "https://files.im"
override val requiresReferer = false

View File

@ -3,10 +3,9 @@ package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
import com.lagradost.cloudstream3.utils.Qualities
class GMPlayer : ExtractorApi() {
open class GMPlayer : ExtractorApi() {
override val name = "GM Player"
override val mainUrl = "https://gmplayer.xyz"
override val requiresReferer = true

View File

@ -0,0 +1,72 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
open class Jeniusplay : ExtractorApi() {
override val name = "Jeniusplay"
override val mainUrl = "https://jeniusplay.com"
override val requiresReferer = true
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val document = app.get(url, referer = "$mainUrl/").document
val hash = url.split("/").last().substringAfter("data=")
val m3uLink = app.post(
url = "$mainUrl/player/index.php?data=$hash&do=getVideo",
data = mapOf("hash" to hash, "r" to "$referer"),
referer = url,
headers = mapOf("X-Requested-With" to "XMLHttpRequest")
).parsed<ResponseSource>().videoSource
M3u8Helper.generateM3u8(
this.name,
m3uLink,
url,
).forEach(callback)
document.select("script").map { script ->
if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
val subData =
getAndUnpack(script.data()).substringAfter("\"tracks\":[").substringBefore("],")
tryParseJson<List<Tracks>>("[$subData]")?.map { subtitle ->
subtitleCallback.invoke(
SubtitleFile(
getLanguage(subtitle.label ?: ""),
subtitle.file
)
)
}
}
}
}
private fun getLanguage(str: String): String {
return when {
str.contains("indonesia", true) || str
.contains("bahasa", true) -> "Indonesian"
else -> str
}
}
data class ResponseSource(
@JsonProperty("hls") val hls: Boolean,
@JsonProperty("videoSource") val videoSource: String,
@JsonProperty("securedLink") val securedLink: String?,
)
data class Tracks(
@JsonProperty("kind") val kind: String?,
@JsonProperty("file") val file: String,
@JsonProperty("label") val label: String?,
)
}

View File

@ -6,7 +6,7 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName
class Linkbox : ExtractorApi() {
open class Linkbox : ExtractorApi() {
override val name = "Linkbox"
override val mainUrl = "https://www.linkbox.to"
override val requiresReferer = true

View File

@ -1,7 +0,0 @@
package com.lagradost.cloudstream3.extractors
open class Mcloud : WcoStream() {
override var name = "Mcloud"
override var mainUrl = "https://mcloud.to"
override val requiresReferer = true
}

View File

@ -6,7 +6,11 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
class Moviehab : ExtractorApi() {
class MoviehabNet : Moviehab() {
override var mainUrl = "https://play.moviehab.net"
}
open class Moviehab : ExtractorApi() {
override var name = "Moviehab"
override var mainUrl = "https://play.moviehab.com"
override val requiresReferer = false

View File

@ -6,7 +6,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.getAndUnpack
class Mp4Upload : ExtractorApi() {
open class Mp4Upload : ExtractorApi() {
override var name = "Mp4Upload"
override var mainUrl = "https://www.mp4upload.com"
private val srcRegex = Regex("""player\.src\("(.*?)"""")

View File

@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.getQualityFromName
import java.net.URI
class MultiQuality : ExtractorApi() {
open class MultiQuality : ExtractorApi() {
override var name = "MultiQuality"
override var mainUrl = "https://gogo-play.net"
private val sourceRegex = Regex("""file:\s*['"](.*?)['"],label:\s*['"](.*?)['"]""")

View File

@ -6,7 +6,7 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
class Mvidoo : ExtractorApi() {
open class Mvidoo : ExtractorApi() {
override val name = "Mvidoo"
override val mainUrl = "https://mvidoo.com"
override val requiresReferer = true

View File

@ -1,7 +1,7 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.utils.ExtractorLink
@ -14,7 +14,7 @@ import org.jsoup.Jsoup
* overrideMainUrl is necessary for for other vidstream clones like vidembed.cc
* If they diverge it'd be better to make them separate.
* */
class Pelisplus(val mainUrl: String) {
open class Pelisplus(val mainUrl: String) {
val name: String = "Vidstream"
private fun getExtractorUrl(id: String): String {
@ -35,7 +35,7 @@ class Pelisplus(val mainUrl: String) {
callback: (ExtractorLink) -> Unit
): Boolean {
try {
normalApis.apmap { api ->
normalApis.amap { api ->
val url = api.getExtractorUrl(id)
api.getSafeUrl(url, subtitleCallback = subtitleCallback, callback = callback)
}
@ -51,8 +51,8 @@ class Pelisplus(val mainUrl: String) {
val qualityRegex = Regex("(\\d+)P")
//a[download]
pageDoc.select(".dowload > a")?.apmap { element ->
val href = element.attr("href") ?: return@apmap
pageDoc.select(".dowload > a")?.amap { element ->
val href = element.attr("href") ?: return@amap
val qual = if (element.text()
.contains("HDP")
) "1080" else qualityRegex.find(element.text())?.destructured?.component1()
@ -84,7 +84,7 @@ class Pelisplus(val mainUrl: String) {
//val name = element.text()
// Matches vidstream links with extractors
extractorApis.filter { !it.requiresReferer || !isCasting }.apmap { api ->
extractorApis.filter { !it.requiresReferer || !isCasting }.amap { api ->
if (link.startsWith(api.mainUrl)) {
api.getSafeUrl(link, extractorUrl, subtitleCallback, callback)
}

View File

@ -6,7 +6,7 @@ import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
class PlayLtXyz: ExtractorApi() {
open class PlayLtXyz: ExtractorApi() {
override val name: String = "PlayLt"
override val mainUrl: String = "https://play.playlt.xyz"
override val requiresReferer = true

View File

@ -8,7 +8,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName
class Solidfiles : ExtractorApi() {
open class Solidfiles : ExtractorApi() {
override val name = "Solidfiles"
override val mainUrl = "https://www.solidfiles.com"
override val requiresReferer = false

View File

@ -130,7 +130,7 @@ open class StreamSB : ExtractorApi() {
it.value.replace(Regex("(embed-|/e/)"), "")
}.first()
// val master = "$mainUrl/sources48/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362"
val master = "$mainUrl/sources48/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/"
val master = "$mainUrl/sources49/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/"
val headers = mapOf(
"watchsb" to "sbstream",
)

View File

@ -5,7 +5,15 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
class StreamTape : ExtractorApi() {
class StreamTapeNet : StreamTape() {
override var mainUrl = "https://streamtape.net"
}
class ShaveTape : StreamTape(){
override var mainUrl = "https://shavetape.cash"
}
open class StreamTape : ExtractorApi() {
override var name = "StreamTape"
override var mainUrl = "https://streamtape.com"
override val requiresReferer = false
@ -16,7 +24,8 @@ class StreamTape : ExtractorApi() {
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
with(app.get(url)) {
linkRegex.find(this.text)?.let {
val extractedUrl = "https:${it.groups[1]!!.value + it.groups[2]!!.value.substring(3,)}"
val extractedUrl =
"https:${it.groups[1]!!.value + it.groups[2]!!.value.substring(3)}"
return listOf(
ExtractorLink(
name,

View File

@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.JsUnpacker
import com.lagradost.cloudstream3.utils.Qualities
import java.net.URI
class Streamhub : ExtractorApi() {
open class Streamhub : ExtractorApi() {
override var mainUrl = "https://streamhub.to"
override var name = "Streamhub"
override val requiresReferer = false

View File

@ -9,7 +9,7 @@ import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import java.net.URI
class Streamplay : ExtractorApi() {
open class Streamplay : ExtractorApi() {
override val name = "Streamplay"
override val mainUrl = "https://streamplay.to"
override val requiresReferer = true

View File

@ -11,7 +11,7 @@ data class Files(
@JsonProperty("label") val label: String? = null,
)
open class Supervideo : ExtractorApi() {
open class Supervideo : ExtractorApi() {
override var name = "Supervideo"
override var mainUrl = "https://supervideo.tv"
override val requiresReferer = false
@ -20,10 +20,13 @@ data class Files(
val response = app.get(url).text
val jstounpack = Regex("eval((.|\\n)*?)</script>").find(response)?.groups?.get(1)?.value
val unpacjed = JsUnpacker(jstounpack).unpack()
val extractedUrl = unpacjed?.let { Regex("""sources:((.|\n)*?)image""").find(it) }?.groups?.get(1)?.value.toString().replace("file",""""file"""").replace("label",""""label"""").substringBeforeLast(",")
val extractedUrl =
unpacjed?.let { Regex("""sources:((.|\n)*?)image""").find(it) }?.groups?.get(1)?.value.toString()
.replace("file", """"file"""").replace("label", """"label"""")
.substringBeforeLast(",")
val parsedlinks = parseJson<List<Files>>(extractedUrl)
parsedlinks.forEach { data ->
if (data.label.isNullOrBlank()){ // mp4 links (with labels) are slow. Use only m3u8 link.
if (data.label.isNullOrBlank()) { // mp4 links (with labels) are slow. Use only m3u8 link.
M3u8Helper.generateM3u8(
name,
data.id,
@ -34,8 +37,6 @@ data class Files(
}
}
}
return extractedLinksList
}
}

View File

@ -6,7 +6,7 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
class UpstreamExtractor : ExtractorApi() {
open class UpstreamExtractor : ExtractorApi() {
override val name: String = "Upstream"
override val mainUrl: String = "https://upstream.to"
override val requiresReferer = true

View File

@ -1,7 +1,7 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
import kotlinx.coroutines.delay
@ -69,12 +69,12 @@ open class VidSrcExtractor : ExtractorApi() {
} else ""
}
serverslist.apmap { server ->
serverslist.amap { server ->
val linkfixed = server.replace("https://vidsrc.xyz/", "https://embedsito.com/")
if (linkfixed.contains("/pro")) {
val srcresponse = app.get(server, referer = absoluteUrl).text
val m3u8Regex = Regex("((https:|http:)//.*\\.m3u8)")
val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@apmap
val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@amap
val passRegex = Regex("""['"](.*set_pass[^"']*)""")
val pass = passRegex.find(srcresponse)?.groupValues?.get(1)?.replace(
Regex("""^//"""), "https://"

View File

@ -11,7 +11,7 @@ class VideovardSX : WcoStream() {
override var mainUrl = "https://videovard.sx"
}
class VideoVard : ExtractorApi() {
open class VideoVard : ExtractorApi() {
override var name = "Videovard" // Cause works for animekisa and wco
override var mainUrl = "https://videovard.to"
override val requiresReferer = false

View File

@ -1,7 +1,7 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.argamap
import com.lagradost.cloudstream3.utils.ExtractorLink
@ -37,7 +37,7 @@ class Vidstream(val mainUrl: String) {
val extractorUrl = getExtractorUrl(id)
argamap(
{
normalApis.apmap { api ->
normalApis.amap { api ->
val url = api.getExtractorUrl(id)
api.getSafeUrl(
url,
@ -55,8 +55,8 @@ class Vidstream(val mainUrl: String) {
val qualityRegex = Regex("(\\d+)P")
//a[download]
pageDoc.select(".dowload > a")?.apmap { element ->
val href = element.attr("href") ?: return@apmap
pageDoc.select(".dowload > a")?.amap { element ->
val href = element.attr("href") ?: return@amap
val qual = if (element.text()
.contains("HDP")
) "1080" else qualityRegex.find(element.text())?.destructured?.component1()
@ -87,7 +87,7 @@ class Vidstream(val mainUrl: String) {
//val name = element.text()
// Matches vidstream links with extractors
extractorApis.filter { !it.requiresReferer || !isCasting }.apmap { api ->
extractorApis.filter { !it.requiresReferer || !isCasting }.amap { api ->
if (link.startsWith(api.mainUrl)) {
api.getSafeUrl(link, extractorUrl, subtitleCallback, callback)
}

View File

@ -13,39 +13,42 @@ open class VoeExtractor : ExtractorApi() {
override val requiresReferer = false
private data class ResponseLinks(
@JsonProperty("hls") val url: String?,
@JsonProperty("hls") val hls: String?,
@JsonProperty("mp4") val mp4: String?,
@JsonProperty("video_height") val label: Int?
//val type: String // Mp4
)
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
val doc = app.get(url).text
if (doc.isNotBlank()) {
val start = "const sources ="
var src = doc.substring(doc.indexOf(start))
src = src.substring(start.length, src.indexOf(";"))
val html = app.get(url).text
if (html.isNotBlank()) {
val src = html.substringAfter("const sources =").substringBefore(";")
// Remove last comma, it is not proper json otherwise
.replace("0,", "0")
.trim()
// Make json use the proper quotes
.replace("'", "\"")
//Log.i(this.name, "Result => (src) ${src}")
parseJson<ResponseLinks?>(src)?.let { voelink ->
//Log.i(this.name, "Result => (voelink) ${voelink}")
val linkUrl = voelink.url
val linkLabel = voelink.label?.toString() ?: ""
parseJson<ResponseLinks?>(src)?.let { voeLink ->
//Log.i(this.name, "Result => (voeLink) ${voeLink}")
// Always defaults to the hls link, but returns the mp4 if null
val linkUrl = voeLink.hls ?: voeLink.mp4
val linkLabel = voeLink.label?.toString() ?: ""
if (!linkUrl.isNullOrEmpty()) {
extractedLinksList.add(
return listOf(
ExtractorLink(
name = this.name,
source = this.name,
url = linkUrl,
quality = getQualityFromName(linkLabel),
referer = url,
isM3u8 = true
isM3u8 = voeLink.hls != null
)
)
}
}
}
return extractedLinksList
return emptyList()
}
}

View File

@ -53,6 +53,12 @@ class VizcloudSite : WcoStream() {
override var mainUrl = "https://vizcloud.site"
}
class Mcloud : WcoStream() {
override var name = "Mcloud"
override var mainUrl = "https://mcloud.to"
override val requiresReferer = true
}
open class WcoStream : ExtractorApi() {
override var name = "VidStream" // Cause works for animekisa and wco
override var mainUrl = "https://vidstream.pro"

View File

@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName
class YourUpload: ExtractorApi() {
open class YourUpload: ExtractorApi() {
override val name = "Yourupload"
override val mainUrl = "https://www.yourupload.com"
override val requiresReferer = false

View File

@ -10,7 +10,7 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
class Zorofile : ExtractorApi() {
open class Zorofile : ExtractorApi() {
override val name = "Zorofile"
override val mainUrl = "https://zorofile.com"
override val requiresReferer = true

View File

@ -1,6 +1,6 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
@ -36,7 +36,7 @@ open class ZplayerV2 : ExtractorApi() {
val m3u8regex = Regex("((https:|http:)\\/\\/.*\\.m3u8)")
m3u8regex.findAll(testdata).map {
it.value
}.toList().apmap { urlm3u8 ->
}.toList().amap { urlm3u8 ->
if (urlm3u8.contains("m3u8")) {
val testurl = app.get(urlm3u8, headers = mapOf("Referer" to url)).text
if (testurl.contains("EXTM3U")) {

View File

@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors.helper
import android.util.Log
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.loadExtractor
@ -18,7 +18,7 @@ class AsianEmbedHelper {
val doc = app.get(url).document
val links = doc.select("div#list-server-more > ul > li.linkserver")
if (!links.isNullOrEmpty()) {
links.apmap {
links.amap {
val datavid = it.attr("data-video") ?: ""
//Log.i("AsianEmbed", "Result => (datavid) ${datavid}")
if (datavid.isNotBlank()) {

View File

@ -39,7 +39,7 @@ class CrossTmdbProvider : TmdbProvider() {
): Boolean {
tryParseJson<CrossMetaData>(data)?.let { metaData ->
if (!metaData.isSuccess) return false
metaData.movies?.apmap { (apiName, data) ->
metaData.movies?.amap { (apiName, data) ->
getApiFromNameNull(apiName)?.let {
try {
it.loadLinks(data, isCasting, subtitleCallback, callback)
@ -64,10 +64,10 @@ class CrossTmdbProvider : TmdbProvider() {
val matchName = filterName(this.name)
when (this) {
is MovieLoadResponse -> {
val data = validApis.apmap { api ->
val data = validApis.amap { api ->
try {
if (api.supportedTypes.contains(TvType.Movie)) { //|| api.supportedTypes.contains(TvType.AnimeMovie)
return@apmap api.search(this.name)?.first {
return@amap api.search(this.name)?.first {
if (filterName(it.name).equals(
matchName,
ignoreCase = true

View File

@ -45,7 +45,7 @@ class MultiAnimeProvider : MainAPI() {
override suspend fun load(url: String): LoadResponse? {
return syncApi.getResult(url)?.let { res ->
val data = SyncUtil.getUrlsFromId(res.id, syncUtilType).apmap { url ->
val data = SyncUtil.getUrlsFromId(res.id, syncUtilType).amap { url ->
validApis.firstOrNull { api -> url.startsWith(api.mainUrl) }?.load(url)
}.filterNotNull()

View File

@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.network
import androidx.annotation.AnyThread
import com.lagradost.cloudstream3.app
import com.lagradost.nicehttp.Requests.Companion.await
import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.cookies
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
@ -41,7 +41,8 @@ class DdosGuardKiller(private val alwaysBypass: Boolean) : Interceptor {
savedCookiesMap[request.url.host]
// If no cookies are found fetch and save em.
?: (request.url.scheme + "://" + request.url.host + (ddosBypassPath ?: "")).let {
app.get(it, cacheTime = 0).cookies.also { cookies ->
// Somehow app.get fails
Requests().get(it).cookies.also { cookies ->
savedCookiesMap[request.url.host] = cookies
}
}
@ -51,6 +52,6 @@ class DdosGuardKiller(private val alwaysBypass: Boolean) : Interceptor {
request.newBuilder()
.headers(headers)
.build()
).await()
).execute()
}
}

View File

@ -4,16 +4,19 @@ import android.content.Context
import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.ignoreAllSSLErrors
import okhttp3.Cache
import okhttp3.Headers
import okhttp3.Headers.Companion.toHeaders
import okhttp3.OkHttpClient
import org.conscrypt.Conscrypt
import java.io.File
import java.security.Security
fun Requests.initClient(context: Context): OkHttpClient {
normalSafeApiCall { Security.insertProviderAt(Conscrypt.newProvider(), 1) }
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
val dns = settingsManager.getInt(context.getString(R.string.dns_pref), 0)
baseClient = OkHttpClient.Builder()

View File

@ -1,40 +1,44 @@
package com.lagradost.cloudstream3.plugins
import android.app.*
import dalvik.system.PathClassLoader
import com.google.gson.Gson
import android.content.Context
import android.content.res.AssetManager
import android.content.res.Resources
import android.os.Environment
import android.widget.Toast
import android.content.Context
import android.os.Build
import android.os.Environment
import android.util.Log
import android.widget.Toast
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.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.plugins.RepositoryManager.ONLINE_PLUGINS_FOLDER
import com.lagradost.cloudstream3.plugins.RepositoryManager.downloadPluginToFile
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.plugins.RepositoryManager.getRepoPlugins
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
import com.lagradost.cloudstream3.APIHolder.removePluginMapping
import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.mvvm.debugPrint
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.plugins.RepositoryManager.ONLINE_PLUGINS_FOLDER
import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.plugins.RepositoryManager.downloadPluginToFile
import com.lagradost.cloudstream3.plugins.RepositoryManager.getRepoPlugins
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.Coroutines.main
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
import com.lagradost.cloudstream3.utils.extractorApis
import dalvik.system.PathClassLoader
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.File
@ -220,10 +224,7 @@ object PluginManager {
fun updateAllOnlinePluginsAndLoadThem(activity: Activity) {
// Load all plugins as fast as possible!
loadAllOnlinePlugins(activity)
ioSafe {
afterPluginsLoadedEvent.invoke(true)
}
afterPluginsLoadedEvent.invoke(false)
val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY)
?: emptyArray()) + PREBUILT_REPOSITORIES
@ -267,16 +268,102 @@ object PluginManager {
}
main {
createNotification(activity, updatedPlugins)
val uitext = txt(R.string.plugins_updated, updatedPlugins.size)
createNotification(activity, uitext, updatedPlugins)
}
ioSafe {
afterPluginsLoadedEvent.invoke(true)
}
// ioSafe {
afterPluginsLoadedEvent.invoke(false)
// }
Log.i(TAG, "Plugin update done!")
}
/**
* Automatically download plugins not yet existing on local
* 1. Gets all online data from online plugins repo
* 2. Fetch all not downloaded plugins
* 3. Download them and reload plugins
**/
fun downloadNotExistingPluginsAndLoad(activity: Activity) {
val newDownloadPlugins = mutableListOf<String>()
val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY)
?: emptyArray()) + PREBUILT_REPOSITORIES
val onlinePlugins = urls.toList().apmap {
getRepoPlugins(it.url)?.toList() ?: emptyList()
}.flatten().distinctBy { it.second.url }
val providerLang = activity.getApiProviderLangSettings()
//Log.i(TAG, "providerLang => ${providerLang.toJson()}")
// Iterate online repos and returns not downloaded plugins
val notDownloadedPlugins = onlinePlugins.mapNotNull { onlineData ->
val sitePlugin = onlineData.second
//Don't include empty urls
if (sitePlugin.url.isBlank()) {
return@mapNotNull null
}
if (sitePlugin.repositoryUrl.isNullOrBlank()) {
return@mapNotNull null
}
//Omit already existing plugins
if (getPluginPath(activity, sitePlugin.internalName, onlineData.first).exists()) {
Log.i(TAG, "Skip > ${sitePlugin.internalName}")
return@mapNotNull null
}
//Omit lang not selected on language setting
val lang = sitePlugin.language ?: return@mapNotNull null
//If set to 'universal', don't skip any language
if (!providerLang.contains(AllLanguagesName) && !providerLang.contains(lang)) {
return@mapNotNull null
}
//Log.i(TAG, "sitePlugin lang => $lang")
//Omit NSFW, if disabled
sitePlugin.tvTypes?.let { tvtypes ->
if (!settingsForProvider.enableAdult) {
if (tvtypes.contains(TvType.NSFW.name)) {
return@mapNotNull null
}
}
}
val savedData = PluginData(
url = sitePlugin.url,
internalName = sitePlugin.internalName,
isOnline = true,
filePath = "",
version = sitePlugin.version
)
OnlinePluginData(savedData, onlineData)
}
//Log.i(TAG, "notDownloadedPlugins => ${notDownloadedPlugins.toJson()}")
notDownloadedPlugins.apmap { pluginData ->
downloadAndLoadPlugin(
activity,
pluginData.onlineData.second.url,
pluginData.savedData.internalName,
pluginData.onlineData.first
).let { success ->
if (success)
newDownloadPlugins.add(pluginData.onlineData.second.name)
}
}
main {
val uitext = txt(R.string.plugins_downloaded, newDownloadPlugins.size)
createNotification(activity, uitext, newDownloadPlugins)
}
// ioSafe {
afterPluginsLoadedEvent.invoke(false)
// }
Log.i(TAG, "Plugin download done!")
}
/**
* Use updateAllOnlinePluginsAndLoadThem
* */
@ -291,7 +378,23 @@ object PluginManager {
}
}
fun loadAllLocalPlugins(activity: Activity) {
/**
* Reloads all local plugins and forces a page update, used for hot reloading with deployWithAdb
**/
fun hotReloadAllLocalPlugins(activity: FragmentActivity?) {
Log.d(TAG, "Reloading all local plugins!")
if (activity == null) return
getPluginsLocal().forEach {
unloadPlugin(it.filePath)
}
loadAllLocalPlugins(activity, true)
}
/**
* @param forceReload see afterPluginsLoadedEvent, basically a way to load all local plugins
* and reload all pages even if they are previously valid
**/
fun loadAllLocalPlugins(activity: Activity, forceReload: Boolean) {
val dir = File(LOCAL_PLUGINS_PATH)
removeKey(PLUGINS_KEY_LOCAL)
@ -313,7 +416,7 @@ object PluginManager {
}
loadedLocalPlugins = true
afterPluginsLoadedEvent.invoke(true)
afterPluginsLoadedEvent.invoke(forceReload)
}
/**
@ -494,7 +597,8 @@ object PluginManager {
}
suspend fun deletePlugin(file: File): Boolean {
val list = (getPluginsLocal() + getPluginsOnline()).filter { it.filePath == file.absolutePath }
val list =
(getPluginsLocal() + getPluginsOnline()).filter { it.filePath == file.absolutePath }
return try {
if (File(file.absolutePath).delete()) {
@ -529,12 +633,14 @@ object PluginManager {
private fun createNotification(
context: Context,
extensionNames: List<String>
uitext: UiText,
extensions: List<String>
): Notification? {
try {
if (extensionNames.isEmpty()) return null
val content = extensionNames.joinToString(", ")
if (extensions.isEmpty()) return null
val content = extensions.joinToString(", ")
// main { // DON'T WANT TO SLOW IT DOWN
val builder = NotificationCompat.Builder(context, EXTENSIONS_CHANNEL_ID)
.setAutoCancel(false)
@ -543,7 +649,8 @@ object PluginManager {
.setSilent(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setColor(context.colorFromAttribute(R.attr.colorPrimary))
.setContentTitle(context.getString(R.string.plugins_updated, extensionNames.size))
.setContentTitle(uitext.asString(context))
//.setContentTitle(context.getString(title, extensionNames.size))
.setSmallIcon(R.drawable.ic_baseline_extension_24)
.setStyle(
NotificationCompat.BigTextStyle()

View File

@ -4,7 +4,7 @@ import android.content.Context
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
@ -70,6 +70,28 @@ object RepositoryManager {
getKey("PREBUILT_REPOSITORIES") ?: emptyArray()
}
suspend fun parseRepoUrl(url: String): String? {
val fixedUrl = url.trim()
return if (fixedUrl.contains("^https?://".toRegex())) {
fixedUrl
} else if (fixedUrl.contains("^(cloudstreamrepo://)|(https://cs\\.repo/\\??)".toRegex())) {
fixedUrl.replace("^(cloudstreamrepo://)|(https://cs\\.repo/\\??)".toRegex(), "").let {
return@let if (!it.contains("^https?://".toRegex()))
"https://${it}"
else fixedUrl
}
} else if (fixedUrl.matches("^[a-zA-Z0-9!_-]+$".toRegex())) {
suspendSafeApiCall {
app.get("https://l.cloudstream.cf/${fixedUrl}").let {
return@let if (it.isSuccessful && !it.url.startsWith("https://cutt.ly/branded-domains")) it.url
else app.get("https://cutt.ly/${fixedUrl}").let let2@{ it2 ->
return@let2 if (it2.isSuccessful) it2.url else null
}
}
}
} else null
}
suspend fun parseRepository(url: String): Repository? {
return suspendSafeApiCall {
// Take manifestVersion and such into account later
@ -95,7 +117,7 @@ object RepositoryManager {
* */
suspend fun getRepoPlugins(repositoryUrl: String): List<Pair<String, SitePlugin>>? {
val repo = parseRepository(repositoryUrl) ?: return null
return repo.pluginLists.apmap { url ->
return repo.pluginLists.amap { url ->
parsePlugins(url).map {
repositoryUrl to it
}
@ -191,4 +213,4 @@ object RepositoryManager {
output.write(dataBuffer, 0, readBytes)
}
}
}
}

View File

@ -12,6 +12,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
val aniListApi = AniListApi(0)
val openSubtitlesApi = OpenSubtitlesApi(0)
val indexSubtitlesApi = IndexSubtitleApi()
val addic7ed = Addic7ed()
// used to login via app intent
val OAuth2Apis
@ -37,12 +38,19 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
val subtitleProviders
get() = listOf(
openSubtitlesApi,
indexSubtitlesApi // they got anti scraping measures in place :(
indexSubtitlesApi, // they got anti scraping measures in place :(
addic7ed
)
const val appString = "cloudstreamapp"
const val appStringRepo = "cloudstreamrepo"
// Instantly start the search given a query
const val appStringSearch = "cloudstreamsearch"
// Instantly resume watching a show
const val appStringResumeWatching = "cloudstreamcontinuewatching"
val unixTime: Long
get() = System.currentTimeMillis() / 1000L
val unixTimeMs: Long

View File

@ -0,0 +1,108 @@
package com.lagradost.cloudstream3.syncproviders.providers
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.subtitles.AbstractSubApi
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
import com.lagradost.cloudstream3.utils.SubtitleHelper
class Addic7ed : AbstractSubApi {
override val name = "Addic7ed"
override val idPrefix = "addic7ed"
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://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"
else url
}
override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List<AbstractSubtitleEntities.SubtitleEntity> {
val lang = query.lang
val queryLang = SubtitleHelper.fromTwoLettersToLanguage(lang.toString())
val queryText = query.query.trim()
val epNum = query.epNumber ?: 0
val seasonNum = query.seasonNumber ?: 0
val yearNum = query.year ?: 0
fun cleanResources(
results: MutableList<AbstractSubtitleEntities.SubtitleEntity>,
name: String,
link: String,
headers: Map<String, String>,
isHearingImpaired: Boolean
) {
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,
headers = headers,
isHearingImpaired = isHearingImpaired
)
)
}
val title = queryText.substringBefore("(").trim()
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
else if (!hostDocument.select("table.tabel")
.isNullOrEmpty()
) searchResult = hostDocument.select("a:contains($title)").attr("href").toString()
else {
val show =
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/"
).document
doc.select("#season tr:contains($queryLang)").mapNotNull { node ->
if (node.selectFirst("td")?.text()
?.toIntOrNull() == seasonNum && node.select("td:eq(1)")
.text()
.toIntOrNull() == epNum
) searchResult = fixUrl(node.select("a").attr("href"))
}
}
val results = mutableListOf<AbstractSubtitleEntities.SubtitleEntity>()
val document = app.get(
url = fixUrl(searchResult),
).document
document.select(".tabel95 .tabel95 tr:contains($queryLang)").mapNotNull { node ->
val name = if (seasonNum > 0) "${document.select(".titulo").text().replace("Subtitle","").trim()}${
node.parent()!!.select(".NewsTitle").text().substringAfter("Version").substringBefore(", Duration")
}" else "${document.select(".titulo").text().replace("Subtitle","").trim()}${node.parent()!!.select(".NewsTitle").text().substringAfter("Version").substringBefore(", Duration")}"
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)
}
return results
}
override suspend fun load(data: AbstractSubtitleEntities.SubtitleEntity): String {
return data.data
}
}

View File

@ -2,15 +2,13 @@ package com.lagradost.cloudstream3.syncproviders.providers
import androidx.fragment.app.FragmentActivity
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.*
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.SyncAPI
@ -21,6 +19,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
import java.net.URL
import java.net.URLEncoder
import java.util.*
class AniListApi(index: Int) : AccountManager(index), SyncAPI {
@ -522,19 +521,26 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
}
private suspend fun postApi(q: String, cache: Boolean = false): String? {
return if (!checkToken()) {
app.post(
"https://graphql.anilist.co/",
headers = mapOf(
"Authorization" to "Bearer " + (getAuth() ?: return null),
if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache"
),
cacheTime = 0,
data = mapOf("query" to q),//(if (vars == null) mapOf("query" to q) else mapOf("query" to q, "variables" to vars))
timeout = 5 // REASONABLE TIMEOUT
).text.replace("\\/", "/")
} else {
null
return suspendSafeApiCall {
if (!checkToken()) {
app.post(
"https://graphql.anilist.co/",
headers = mapOf(
"Authorization" to "Bearer " + (getAuth() ?: return@suspendSafeApiCall null),
if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache"
),
cacheTime = 0,
data = mapOf(
"query" to URLEncoder.encode(
q,
"UTF-8"
)
), //(if (vars == null) mapOf("query" to q) else mapOf("query" to q, "variables" to vars))
timeout = 5 // REASONABLE TIMEOUT
).text.replace("\\/", "/")
} else {
null
}
}
}

View File

@ -15,6 +15,8 @@ import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager
import com.lagradost.cloudstream3.utils.AppUtils
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi {
override val idPrefix = "opensubtitles"
@ -175,7 +177,7 @@ 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"
false -> "$host/subtitles?query=${URLEncoder.encode(queryText.lowercase(), StandardCharsets.UTF_8.toString())}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
}
val req = app.get(
@ -198,8 +200,12 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
it.data?.forEach { item ->
val attr = item.attributes ?: return@forEach
val featureDetails = attr.featDetails
//Use filename as name, if its valid
val filename = attr.files?.firstNotNullOfOrNull { subfile ->
subfile.fileName
}
//Use any valid name/title in hierarchy
val name = featureDetails?.movieName ?: featureDetails?.title
val name = filename ?: featureDetails?.movieName ?: featureDetails?.title
?: featureDetails?.parentTitle ?: attr.release ?: ""
val lang = fixLanguageReverse(attr.language)?: ""
val resEpNum = featureDetails?.episodeNumber ?: query.epNumber
@ -328,4 +334,4 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
@JsonProperty("parent_tmdb_id") var parentTmdbId: Int? = null,
@JsonProperty("parent_feature_id") var parentFeatureId: Int? = null
)
}
}

View File

@ -1,11 +1,18 @@
package com.lagradost.cloudstream3.ui
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.unixTime
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import com.lagradost.cloudstream3.utils.ExtractorLink
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope.coroutineContext
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
class APIRepository(val api: MainAPI) {
@ -27,26 +34,63 @@ class APIRepository(val api: MainAPI) {
return data.isEmpty() || data == "[]" || data == "about:blank"
}
private val cacheHash: HashMap<Pair<String,String>, LoadResponse> = hashMapOf()
data class SavedLoadResponse(
val unixTime: Long,
val response: LoadResponse,
val hash: Pair<String, String>
)
private val cache = threadSafeListOf<SavedLoadResponse>()
private var cacheIndex: Int = 0
const val cacheSize = 20
}
private fun afterPluginsLoaded(forceReload: Boolean) {
if (forceReload) {
synchronized(cache) {
cache.clear()
}
}
}
init {
afterPluginsLoadedEvent += ::afterPluginsLoaded
}
val hasMainPage = api.hasMainPage
val providerType = api.providerType
val name = api.name
val mainUrl = api.mainUrl
val mainPage = api.mainPage
val hasQuickSearch = api.hasQuickSearch
val vpnStatus = api.vpnStatus
val providerType = api.providerType
suspend fun load(url: String): Resource<LoadResponse> {
return safeApiCall {
if (isInvalidData(url)) throw ErrorLoadingException()
val fixedUrl = api.fixUrl(url)
val key = Pair(api.name,url)
cacheHash[key] ?: api.load(fixedUrl)?.also {
// we cache 20 responses because ppl often go back to the same shit + 20 because I dont want to cause too much memory leak
if (cacheHash.size > 20) cacheHash.remove(cacheHash.keys.random())
cacheHash[key] = it
val lookingForHash = Pair(api.name, fixedUrl)
synchronized(cache) {
for (item in cache) {
// 10 min save
if (item.hash == lookingForHash && (unixTime - item.unixTime) < 60 * 10) {
return@safeApiCall item.response
}
}
}
api.load(fixedUrl)?.also { response ->
val add = SavedLoadResponse(unixTime, response, lookingForHash)
synchronized(cache) {
if (cache.size > cacheSize) {
cache[cacheIndex] = add // rolling cache
cacheIndex = (cacheIndex + 1) % cacheSize
} else {
cache.add(add)
}
}
} ?: throw ErrorLoadingException()
}
}
@ -78,6 +122,7 @@ class APIRepository(val api: MainAPI) {
delay(delta)
}
@OptIn(DelicateCoroutinesApi::class)
suspend fun getMainPage(page: Int, nameIndex: Int? = null): Resource<List<HomePageResponse?>> {
return safeApiCall {
api.lastHomepageRequest = unixTimeMS
@ -103,11 +148,15 @@ class APIRepository(val api: MainAPI) {
)
}
} else {
api.mainPage.apmap { data ->
api.getMainPage(
page,
MainPageRequest(data.name, data.data, data.horizontalImages)
)
with(CoroutineScope(coroutineContext)) {
api.mainPage.map { data ->
async {
api.getMainPage(
page,
MainPageRequest(data.name, data.data, data.horizontalImages)
)
}
}.map { it.await() }
}
}
}

View File

@ -150,7 +150,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
} else {
ChromecastSubtitlesFragment.getCurrentSavedStyle().apply {
val font = TextTrackStyle()
font.fontFamily = fontFamily ?: "Google Sans"
font.setFontFamily(fontFamily ?: "Google Sans")
fontGenericFamily?.let {
font.fontGenericFamily = it
}
@ -183,7 +183,9 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
val contentUrl = (remoteMediaClient?.currentItem?.media?.contentUrl
?: remoteMediaClient?.currentItem?.media?.contentId)
val sortingMethods = items.map { "${it.name} ${Qualities.getStringByInt(it.quality)}" }.toTypedArray()
val sortingMethods =
items.map { "${it.name} ${Qualities.getStringByInt(it.quality)}" }
.toTypedArray()
val sotringIndex = items.indexOfFirst { it.url == contentUrl }
val arrayAdapter =
@ -279,7 +281,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
val currentPosition = remoteMediaClient?.approximateStreamPosition
if (currentDuration != null && currentPosition != null)
DataStoreHelper.setViewPos(epData.id, currentPosition, currentDuration)
} catch (t : Throwable) {
} catch (t: Throwable) {
logError(t)
}
@ -358,10 +360,8 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
}
}
override fun onSessionConnected(castSession: CastSession?) {
castSession?.let {
super.onSessionConnected(it)
}
override fun onSessionConnected(castSession: CastSession) {
super.onSessionConnected(castSession)
remoteMediaClient?.queueSetRepeatMode(REPEAT_MODE_REPEAT_OFF, JSONObject())
}
}

View File

@ -69,7 +69,7 @@ class EasterEggMonke : AppCompatActivity() {
set.duration = (Math.random() * 1500 + 2500).toLong()
set.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
override fun onAnimationEnd(animation: Animator) {
frame.removeView(newStar)
}
})

View File

@ -7,6 +7,7 @@ import android.content.DialogInterface
import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -14,7 +15,6 @@ import android.view.ViewGroup
import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getDrawable
import androidx.core.view.isGone
import androidx.core.view.isVisible
@ -23,8 +23,10 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSnapHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.ChangeBounds
import androidx.transition.TransitionManager
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton
@ -55,6 +57,7 @@ import com.lagradost.cloudstream3.ui.search.*
import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.AppUtils.addProgramsToContinueWatching
import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
@ -73,6 +76,8 @@ import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showOptionSelectSt
import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView
import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
import com.lagradost.cloudstream3.utils.UIHelper.setImage
@ -104,11 +109,13 @@ import kotlinx.android.synthetic.main.fragment_home.home_watch_holder
import kotlinx.android.synthetic.main.fragment_home.home_watch_parent_item_title
import kotlinx.android.synthetic.main.fragment_home.result_error_text
import kotlinx.android.synthetic.main.fragment_home_tv.*
import kotlinx.android.synthetic.main.fragment_search.*
import kotlinx.android.synthetic.main.home_episodes_expanded.*
import kotlinx.android.synthetic.main.tvtypes_chips.*
import kotlinx.android.synthetic.main.tvtypes_chips.view.*
import java.util.*
const val HOME_BOOKMARK_VALUE_LIST = "home_bookmarked_last_list"
const val HOME_PREF_HOMEPAGE = "home_pref_homepage"
@ -345,7 +352,9 @@ class HomeFragment : Fragment() {
builder.setContentView(R.layout.home_select_mainpage)
builder.show()
builder.let { dialog ->
val isMultiLang = getApiProviderLangSettings().size > 1
val isMultiLang = getApiProviderLangSettings().let { set ->
set.size > 1 || set.contains(AllLanguagesName)
}
//dialog.window?.setGravity(Gravity.BOTTOM)
var currentApiName = selectedApiName
@ -435,6 +444,7 @@ class HomeFragment : Fragment() {
home_main_poster_recyclerview?.isVisible = visible
}
@SuppressLint("NotifyDataSetChanged") // we need to notify to change poster
private fun fixGrid() {
activity?.getSpanCount()?.let {
currentSpan = it
@ -457,19 +467,20 @@ class HomeFragment : Fragment() {
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
(home_preview_viewpager?.adapter as? HomeScrollAdapter)?.notifyDataSetChanged()
fixGrid()
}
override fun onResume() {
super.onResume()
reloadStored()
afterPluginsLoadedEvent += ::firstLoadHomePage
mainPluginsLoadedEvent += ::firstLoadHomePage
afterPluginsLoadedEvent += ::afterPluginsLoaded
mainPluginsLoadedEvent += ::afterMainPluginsLoaded
}
override fun onStop() {
afterPluginsLoadedEvent -= ::firstLoadHomePage
mainPluginsLoadedEvent -= ::firstLoadHomePage
afterPluginsLoadedEvent -= ::afterPluginsLoaded
mainPluginsLoadedEvent -= ::afterMainPluginsLoaded
super.onStop()
}
@ -482,15 +493,18 @@ class HomeFragment : Fragment() {
homeViewModel.loadStoredData(list)
}
private fun firstLoadHomePage(successful: Boolean = false) {
// dirty hack to make it only load once
private fun afterMainPluginsLoaded(unused: Boolean = false) {
loadHomePage(false)
}
private fun loadHomePage(forceReload: Boolean = true) {
private fun afterPluginsLoaded(forceReload: Boolean) {
loadHomePage(forceReload)
}
private fun loadHomePage(forceReload: Boolean) {
val apiName = context?.getKey<String>(USER_SELECTED_HOMEPAGE_API)
if (homeViewModel.apiName.value != apiName || apiName == null) {
if (homeViewModel.apiName.value != apiName || apiName == null || forceReload) {
//println("Caught home: " + homeViewModel.apiName.value + " at " + apiName)
homeViewModel.loadAndCancel(apiName, forceReload)
}
@ -541,50 +555,123 @@ class HomeFragment : Fragment() {
}
observe(homeViewModel.preview) { preview ->
// Always reset the padding, otherwise the will move lower and lower
// home_fix_padding?.setPadding(0, 0, 0, 0)
home_fix_padding?.let { v ->
val params = v.layoutParams
params.height = 0
v.layoutParams = params
}
when (preview) {
is Resource.Success -> {
home_preview?.isVisible = true
preview.value.apply {
home_preview_tags?.text = tags?.joinToString("") ?: ""
home_preview_tags?.isGone = tags.isNullOrEmpty()
home_preview_image?.setImage(posterUrl, posterHeaders)
home_preview_title?.text = name
home_preview_play?.setOnClickListener {
activity?.loadResult(url, apiName, START_ACTION_RESUME_LATEST)
//activity.loadSearchResult(url, START_ACTION_RESUME_LATEST)
}
home_preview_info?.setOnClickListener {
activity?.loadResult(url, apiName)
//activity.loadSearchResult(random)
}
// very ugly code, but I dont care
val watchType = DataStoreHelper.getResultWatchState(preview.value.getId())
home_preview_bookmark?.setText(watchType.stringRes)
home_preview_bookmark?.setCompoundDrawablesWithIntrinsicBounds(null,getDrawable(home_preview_bookmark.context, watchType.iconRes),null,null)
home_preview_bookmark?.setOnClickListener { fab ->
activity?.showBottomDialog(
WatchType.values().map { fab.context.getString(it.stringRes) }
.toList(),
DataStoreHelper.getResultWatchState(preview.value.getId()).ordinal,
fab.context.getString(R.string.action_add_to_bookmarks),
showApply = false,
{}) {
val newValue = WatchType.values()[it]
home_preview_bookmark?.setCompoundDrawablesWithIntrinsicBounds(null,getDrawable(home_preview_bookmark.context, newValue.iconRes),null,null)
home_preview_bookmark?.setText(newValue.stringRes)
updateWatchStatus(preview.value, newValue)
reloadStored()
}
(home_preview_viewpager?.adapter as? HomeScrollAdapter)?.apply {
if (!setItems(preview.value.second, preview.value.first)) {
home_preview_viewpager?.setCurrentItem(0, false)
}
// home_preview_viewpager?.setCurrentItem(1000, false)
}
//.also {
//home_preview_viewpager?.adapter =
//}
}
else -> {
(home_preview_viewpager?.adapter as? HomeScrollAdapter)?.setItems(
listOf(),
false
)
home_preview?.isVisible = false
context?.fixPaddingStatusbarView(home_fix_padding)
}
}
}
val searchText =
home_search?.findViewById<SearchView.SearchAutoComplete>(androidx.appcompat.R.id.search_src_text)
searchText?.context?.getResourceColor(R.attr.white)?.let { color ->
searchText.setTextColor(color)
searchText.setHintTextColor(color)
}
home_preview_viewpager?.apply {
setPageTransformer(HomeScrollTransformer())
val callback: OnPageChangeCallback = object : OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
// home_search?.isIconified = true
//home_search?.isVisible = true
//home_search?.clearFocus()
(home_preview_viewpager?.adapter as? HomeScrollAdapter)?.apply {
if (position >= itemCount - 1 && hasMoreItems) {
hasMoreItems = false // dont make two requests
homeViewModel.loadMoreHomeScrollResponses()
}
getItem(position)
?.apply {
home_preview_title_holder?.let { parent ->
TransitionManager.beginDelayedTransition(parent, ChangeBounds())
}
// home_preview_tags?.text = tags?.joinToString(" • ") ?: ""
// home_preview_tags?.isGone = tags.isNullOrEmpty()
// home_preview_image?.setImage(posterUrl, posterHeaders)
// home_preview_title?.text = name
home_preview_play?.setOnClickListener {
activity?.loadResult(url, apiName, START_ACTION_RESUME_LATEST)
//activity.loadSearchResult(url, START_ACTION_RESUME_LATEST)
}
home_preview_info?.setOnClickListener {
activity?.loadResult(url, apiName)
//activity.loadSearchResult(random)
}
// very ugly code, but I dont care
val watchType = DataStoreHelper.getResultWatchState(this.getId())
home_preview_bookmark?.setText(watchType.stringRes)
home_preview_bookmark?.setCompoundDrawablesWithIntrinsicBounds(
null,
getDrawable(home_preview_bookmark.context, watchType.iconRes),
null,
null
)
home_preview_bookmark?.setOnClickListener { fab ->
activity?.showBottomDialog(
WatchType.values()
.map { fab.context.getString(it.stringRes) }
.toList(),
DataStoreHelper.getResultWatchState(this.getId()).ordinal,
fab.context.getString(R.string.action_add_to_bookmarks),
showApply = false,
{}) {
val newValue = WatchType.values()[it]
home_preview_bookmark?.setCompoundDrawablesWithIntrinsicBounds(
null,
getDrawable(
home_preview_bookmark.context,
newValue.iconRes
),
null,
null
)
home_preview_bookmark?.setText(newValue.stringRes)
updateWatchStatus(this, newValue)
reloadStored()
}
}
}
}
}
}
registerOnPageChangeCallback(callback)
adapter = HomeScrollAdapter()
}
observe(homeViewModel.apiName) { apiName ->
currentApiName = apiName
// setKey(USER_SELECTED_HOMEPAGE_API, apiName)
@ -749,14 +836,32 @@ class HomeFragment : Fragment() {
Pair(home_type_on_hold_btt, WatchType.ONHOLD),
Pair(home_plan_to_watch_btt, WatchType.PLANTOWATCH),
)
val currentSet = getKey<IntArray>(HOME_BOOKMARK_VALUE_LIST)
?.map { WatchType.fromInternalId(it) }?.toSet() ?: emptySet()
for ((chip, watch) in toggleList) {
chip.isChecked = currentSet.contains(watch)
chip?.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
homeViewModel.loadStoredData(
setOf(watch)
// If we filter all buttons then two can be checked at the same time
// Revert this if you want to go back to multi selection
// toggleList.filter { it.first?.isChecked == true }.map { it.second }.toSet()
)
}
// Else if all are unchecked -> Do not load data
else if (toggleList.all { it.first?.isChecked != true }) {
homeViewModel.loadStoredData(emptySet())
}
}
/*chip?.setOnClickListener {
for (item in toggleList) {
val watch = item.second
item.first?.setOnClickListener {
homeViewModel.loadStoredData(EnumSet.of(watch))
}
item.first?.setOnLongClickListener { itemView ->
chip?.setOnLongClickListener { itemView ->
val list = EnumSet.noneOf(WatchType::class.java)
itemView.context.getKey<IntArray>(HOME_BOOKMARK_VALUE_LIST)
?.map { WatchType.fromInternalId(it) }?.let {
@ -770,7 +875,7 @@ class HomeFragment : Fragment() {
}
homeViewModel.loadStoredData(list)
return@setOnLongClickListener true
}
}*/
}
observe(homeViewModel.availableWatchStatusTypes) { availableWatchStatusTypes ->
@ -826,11 +931,13 @@ class HomeFragment : Fragment() {
resumeWatching
)
//if (context?.isTvSettings() == true) {
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// context?.addProgramsToContinueWatching(resumeWatching.mapNotNull { it as? DataStoreHelper.ResumeWatchingResult })
// }
//}
if (isTrueTvSettings()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ioSafe {
activity?.addProgramsToContinueWatching(resumeWatching.mapNotNull { it as? DataStoreHelper.ResumeWatchingResult })
}
}
}
home_watch_child_more_info?.setOnClickListener {
activity?.loadHomepageList(
@ -993,6 +1100,7 @@ class HomeFragment : Fragment() {
}
//context?.fixPaddingStatusbarView(home_statusbar)
context?.fixPaddingStatusbar(home_padding)
context?.fixPaddingStatusbar(home_loading_statusbar)
home_master_recycler.adapter =
@ -1014,7 +1122,7 @@ class HomeFragment : Fragment() {
} // GridLayoutManager(context, 1).also { it.supportsPredictiveItemAnimations() }
reloadStored()
loadHomePage()
loadHomePage(false)
home_loaded.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { v, _, scrollY, _, oldScrollY ->
val dy = scrollY - oldScrollY

View File

@ -0,0 +1,94 @@
package com.lagradost.cloudstream3.ui.home
import android.content.res.Configuration
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isGone
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import kotlinx.android.synthetic.main.home_scroll_view.view.*
class HomeScrollAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var items: MutableList<LoadResponse> = mutableListOf()
var hasMoreItems: Boolean = false
fun getItem(position: Int): LoadResponse? {
return items.getOrNull(position)
}
fun setItems(newItems: List<LoadResponse>, hasNext: Boolean): Boolean {
val isSame = newItems.firstOrNull()?.url == items.firstOrNull()?.url
hasMoreItems = hasNext
val diffResult = DiffUtil.calculateDiff(
HomeScrollDiffCallback(this.items, newItems)
)
items.clear()
items.addAll(newItems)
diffResult.dispatchUpdatesTo(this)
return isSame
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return CardViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.home_scroll_view, parent, false),
)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is CardViewHolder -> {
holder.bind(items[position])
}
}
}
class CardViewHolder
constructor(
itemView: View,
) :
RecyclerView.ViewHolder(itemView) {
fun bind(card: LoadResponse) {
card.apply {
val isHorizontal =
itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
val posterUrl = if (isHorizontal) backgroundPosterUrl ?: posterUrl else posterUrl
?: backgroundPosterUrl
itemView.home_scroll_preview_tags?.text = tags?.joinToString("") ?: ""
itemView.home_scroll_preview_tags?.isGone = tags.isNullOrEmpty()
itemView.home_scroll_preview?.setImage(posterUrl, posterHeaders)
itemView.home_scroll_preview_title?.text = name
}
}
}
class HomeScrollDiffCallback(
private val oldList: List<LoadResponse>,
private val newList: List<LoadResponse>
) :
DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
oldList[oldItemPosition].url == newList[newItemPosition].url
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
oldList[oldItemPosition] == newList[newItemPosition]
}
override fun getItemCount(): Int {
return items.size
}
}

View File

@ -0,0 +1,23 @@
package com.lagradost.cloudstream3.ui.home
import android.view.View
import androidx.viewpager2.widget.ViewPager2
class HomeScrollTransformer : ViewPager2.PageTransformer {
override fun transformPage(page: View, position: Float) {
//page.translationX = -position * page.width / 2.0f
//val params = RecyclerView.LayoutParams(
// RecyclerView.LayoutParams.MATCH_PARENT,
// 0
//)
//page.layoutParams = params
//progressBar?.layoutParams = params
val padding = (-position * page.width / 2).toInt()
page.setPadding(
padding, 0,
-padding, 0
)
}
}

View File

@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.APIHolder.filterHomePageListByFilmQuality
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
@ -12,15 +13,12 @@ import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.HomePageList
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.mvvm.*
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi
import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllResumeStateIds
@ -38,6 +36,42 @@ import java.util.*
import kotlin.collections.set
class HomeViewModel : ViewModel() {
companion object {
suspend fun getResumeWatching(): List<DataStoreHelper.ResumeWatchingResult>? {
val resumeWatching = withContext(Dispatchers.IO) {
getAllResumeStateIds()?.mapNotNull { id ->
getLastWatched(id)
}?.sortedBy { -it.updateTime }
}
val resumeWatchingResult = withContext(Dispatchers.IO) {
resumeWatching?.mapNotNull { resume ->
val data = getKey<VideoDownloadHelper.DownloadHeaderCached>(
DOWNLOAD_HEADER_CACHE,
resume.parentId.toString()
) ?: return@mapNotNull null
val watchPos = getViewPos(resume.episodeId)
DataStoreHelper.ResumeWatchingResult(
data.name,
data.url,
data.apiName,
data.type,
data.poster,
watchPos,
resume.episodeId,
resume.parentId,
resume.episode,
resume.season,
resume.isFromDownload
)
}
}
return resumeWatchingResult
}
}
private var repo: APIRepository? = null
private val _apiName = MutableLiveData<String>()
@ -46,59 +80,35 @@ class HomeViewModel : ViewModel() {
private val _randomItems = MutableLiveData<List<SearchResponse>?>(null)
val randomItems: LiveData<List<SearchResponse>?> = _randomItems
private var currentShuffledList: List<SearchResponse> = listOf()
private fun autoloadRepo(): APIRepository {
return APIRepository(apis.first { it.hasMainPage })
}
private val _availableWatchStatusTypes =
MutableLiveData<Pair<EnumSet<WatchType>, EnumSet<WatchType>>>()
val availableWatchStatusTypes: LiveData<Pair<EnumSet<WatchType>, EnumSet<WatchType>>> =
MutableLiveData<Pair<Set<WatchType>, Set<WatchType>>>()
val availableWatchStatusTypes: LiveData<Pair<Set<WatchType>, Set<WatchType>>> =
_availableWatchStatusTypes
private val _bookmarks = MutableLiveData<Pair<Boolean, List<SearchResponse>>>()
val bookmarks: LiveData<Pair<Boolean, List<SearchResponse>>> = _bookmarks
private val _resumeWatching = MutableLiveData<List<SearchResponse>>()
private val _preview = MutableLiveData<Resource<LoadResponse>>()
private val _preview = MutableLiveData<Resource<Pair<Boolean, List<LoadResponse>>>>()
private val previewResponses = mutableListOf<LoadResponse>()
private val previewResponsesAdded = mutableSetOf<String>()
val resumeWatching: LiveData<List<SearchResponse>> = _resumeWatching
val preview: LiveData<Resource<LoadResponse>> = _preview
val preview: LiveData<Resource<Pair<Boolean, List<LoadResponse>>>> = _preview
fun loadResumeWatching() = viewModelScope.launchSafe {
val resumeWatching = withContext(Dispatchers.IO) {
getAllResumeStateIds()?.mapNotNull { id ->
getLastWatched(id)
}?.sortedBy { -it.updateTime }
}
// val resumeWatchingResult = ArrayList<DataStoreHelper.ResumeWatchingResult>()
val resumeWatchingResult = withContext(Dispatchers.IO) {
resumeWatching?.map { resume ->
val data = getKey<VideoDownloadHelper.DownloadHeaderCached>(
DOWNLOAD_HEADER_CACHE,
resume.parentId.toString()
) ?: return@map null
val watchPos = getViewPos(resume.episodeId)
DataStoreHelper.ResumeWatchingResult(
data.name,
data.url,
data.apiName,
data.type,
data.poster,
watchPos,
resume.episodeId,
resume.parentId,
resume.episode,
resume.season,
resume.isFromDownload
)
}?.filterNotNull()
}
val resumeWatchingResult = getResumeWatching()
resumeWatchingResult?.let {
_resumeWatching.postValue(it)
}
}
fun loadStoredData(preferredWatchStatus: EnumSet<WatchType>?) = viewModelScope.launchSafe {
fun loadStoredData(preferredWatchStatus: Set<WatchType>?) = viewModelScope.launchSafe {
val watchStatusIds = withContext(Dispatchers.IO) {
getAllWatchStateIds()?.map { id ->
Pair(id, getResultWatchState(id))
@ -106,7 +116,7 @@ class HomeViewModel : ViewModel() {
}?.distinctBy { it.first } ?: return@launchSafe
val length = WatchType.values().size
val currentWatchTypes = EnumSet.noneOf(WatchType::class.java)
val currentWatchTypes = mutableSetOf<WatchType>()
for (watch in watchStatusIds) {
currentWatchTypes.add(watch.second)
@ -209,8 +219,42 @@ class HomeViewModel : ViewModel() {
expandAndReturn(name)
}
// returns the amount of items added and modifies current
private suspend fun updatePreviewResponses(
current: MutableList<LoadResponse>,
alreadyAdded: MutableSet<String>,
shuffledList: List<SearchResponse>,
size: Int
): Int {
var count = 0
private fun load(api: MainAPI?) = viewModelScope.launchSafe {
val addItems = arrayListOf<SearchResponse>()
for (searchResponse in shuffledList) {
if (!alreadyAdded.contains(searchResponse.url)) {
addItems.add(searchResponse)
previewResponsesAdded.add(searchResponse.url)
if (++count >= size) {
break
}
}
}
val add = addItems.amap { searchResponse ->
repo?.load(searchResponse.url)
}.mapNotNull { if (it != null && it is Resource.Success) it.value else null }
current.addAll(add)
return add.size
}
private var addJob: Job? = null
fun loadMoreHomeScrollResponses() {
addJob = ioSafe {
updatePreviewResponses(previewResponses, previewResponsesAdded, currentShuffledList, 1)
_preview.postValue(Resource.Success((previewResponsesAdded.size < currentShuffledList.size) to previewResponses))
}
}
private fun load(api: MainAPI?) = ioSafe {
repo = if (api != null) {
APIRepository(api)
} else {
@ -223,6 +267,7 @@ class HomeViewModel : ViewModel() {
if (repo?.hasMainPage == true) {
_page.postValue(Resource.Loading())
_preview.postValue(Resource.Loading())
addJob?.cancel()
when (val data = repo?.getMainPage(1, null)) {
is Resource.Success -> {
@ -236,38 +281,12 @@ class HomeViewModel : ViewModel() {
ExpandableHomepageList(filteredList, 1, home.hasNext)
}
}
val items = data.value.mapNotNull { it?.items }.flatten()
items.randomOrNull()?.list?.randomOrNull()?.url?.let { url ->
// backup request in case first fails
var first = repo?.load(url)
if(first == null ||first is Resource.Failure) {
first = repo?.load(items.random().list.random().url)
}
first?.let {
_preview.postValue(it)
} ?: run {
_preview.postValue(
Resource.Failure(
false,
null,
null,
"No repo found, this should never happen"
)
)
}
} ?: run {
_preview.postValue(
Resource.Failure(
false,
null,
null,
"No homepage items"
)
)
}
_page.postValue(Resource.Success(expandable))
previewResponses.clear()
previewResponsesAdded.clear()
//val home = data.value
if (items.isNotEmpty()) {
@ -282,9 +301,30 @@ class HomeViewModel : ViewModel() {
context?.filterSearchResultByFilmQuality(currentList.shuffled())
?: currentList.shuffled()
updatePreviewResponses(
previewResponses,
previewResponsesAdded,
randomItems,
3
)
_randomItems.postValue(randomItems)
currentShuffledList = randomItems
}
}
if (previewResponses.isEmpty()) {
_preview.postValue(
Resource.Failure(
false,
null,
null,
"No homepage responses"
)
)
} else {
_preview.postValue(Resource.Success((previewResponsesAdded.size < currentShuffledList.size) to previewResponses))
}
_page.postValue(Resource.Success(expandable))
} catch (e: Exception) {
_randomItems.postValue(emptyList())
logError(e)

View File

@ -38,6 +38,7 @@ import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
import com.lagradost.cloudstream3.utils.AppUtils
import com.lagradost.cloudstream3.utils.AppUtils.requestLocalAudioFocus
import com.lagradost.cloudstream3.utils.EpisodeSkip
import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
@ -103,6 +104,14 @@ abstract class AbstractPlayerFragment(
throw NotImplementedError()
}
open fun onTimestamp(timestamp: EpisodeSkip.SkipStamp?) {
}
open fun onTimestampSkipped(timestamp: EpisodeSkip.SkipStamp) {
}
open fun exitedPipMode() {
throw NotImplementedError()
}
@ -373,7 +382,9 @@ abstract class AbstractPlayerFragment(
),
subtitlesUpdates = ::subtitlesChanged,
embeddedSubtitlesFetched = ::embeddedSubtitlesFetched,
onTracksInfoChanged = ::onTracksInfoChanged
onTracksInfoChanged = ::onTracksInfoChanged,
onTimestampInvoked = ::onTimestamp,
onTimestampSkipped = ::onTimestampSkipped
)
if (player is CS3IPlayer) {

View File

@ -8,16 +8,19 @@ import android.util.Log
import android.widget.FrameLayout
import androidx.preference.PreferenceManager
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.C.TRACK_TYPE_AUDIO
import com.google.android.exoplayer2.C.TRACK_TYPE_VIDEO
import com.google.android.exoplayer2.C.*
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource
import com.google.android.exoplayer2.source.*
import com.google.android.exoplayer2.text.TextRenderer
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.trackselection.TrackSelectionOverride
import com.google.android.exoplayer2.trackselection.TrackSelector
import com.google.android.exoplayer2.ui.SubtitleView
import com.google.android.exoplayer2.upstream.*
import com.google.android.exoplayer2.upstream.DataSource
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
import com.google.android.exoplayer2.upstream.HttpDataSource
import com.google.android.exoplayer2.upstream.cache.CacheDataSource
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
import com.google.android.exoplayer2.upstream.cache.SimpleCache
@ -31,6 +34,7 @@ import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
import com.lagradost.cloudstream3.utils.EpisodeSkip
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList
import com.lagradost.cloudstream3.utils.ExtractorUri
@ -84,10 +88,10 @@ class CS3IPlayer : IPlayer {
/**
* Tracks reported to be used by exoplayer, since sometimes it has a mind of it's own when selecting subs.
* String = lowercase language as set by .setLanguage("_$langId")
* String = id
* Boolean = if it's active
* */
private var exoPlayerSelectedTracks = listOf<Pair<String, Boolean>>()
private var playerSelectedSubtitleTracks = listOf<Pair<String, Boolean>>()
/** isPlaying */
private var updateIsPlaying: ((Pair<CSPlayerLoading, CSPlayerLoading>) -> Unit)? = null
@ -112,6 +116,8 @@ class CS3IPlayer : IPlayer {
private var playerUpdated: ((Any?) -> Unit)? = null
private var embeddedSubtitlesFetched: ((List<SubtitleData>) -> Unit)? = null
private var onTracksInfoChanged: (() -> Unit)? = null
private var onTimestampInvoked: ((EpisodeSkip.SkipStamp?) -> Unit)? = null
private var onTimestampSkipped: ((EpisodeSkip.SkipStamp) -> Unit)? = null
override fun releaseCallbacks() {
playerUpdated = null
@ -125,7 +131,9 @@ class CS3IPlayer : IPlayer {
prevEpisode = null
subtitlesUpdates = null
onTracksInfoChanged = null
onTimestampInvoked = null
requestSubtitleUpdate = null
onTimestampSkipped = null
}
override fun initCallbacks(
@ -141,6 +149,8 @@ class CS3IPlayer : IPlayer {
subtitlesUpdates: (() -> Unit)?,
embeddedSubtitlesFetched: ((List<SubtitleData>) -> Unit)?,
onTracksInfoChanged: (() -> Unit)?,
onTimestampInvoked: ((EpisodeSkip.SkipStamp?) -> Unit)?,
onTimestampSkipped: ((EpisodeSkip.SkipStamp) -> Unit)?,
) {
this.playerUpdated = playerUpdated
this.updateIsPlaying = updateIsPlaying
@ -154,6 +164,8 @@ class CS3IPlayer : IPlayer {
this.subtitlesUpdates = subtitlesUpdates
this.embeddedSubtitlesFetched = embeddedSubtitlesFetched
this.onTracksInfoChanged = onTracksInfoChanged
this.onTimestampInvoked = onTimestampInvoked
this.onTimestampSkipped = onTimestampSkipped
}
// I know, this is not a perfect solution, however it works for fixing subs
@ -218,7 +230,43 @@ class CS3IPlayer : IPlayer {
var currentSubtitles: SubtitleData? = null
override fun setMaxVideoSize(width: Int, height: Int) {
private fun List<Tracks.Group>.getTrack(id: String?): Pair<TrackGroup, Int>? {
if (id == null) return null
// This beast of an expression does:
// 1. Filter all audio tracks
// 2. Get all formats in said audio tacks
// 3. Gets all ids of the formats
// 4. Filters to find the first audio track with the same id as the audio track we are looking for
// 5. Returns the media group and the index of the audio track in the group
return this.firstNotNullOfOrNull { group ->
(0 until group.mediaTrackGroup.length).map {
group.getTrackFormat(it) to it
}.firstOrNull { it.first.id == id }
?.let { group.mediaTrackGroup to it.second }
}
}
override fun setMaxVideoSize(width: Int, height: Int, id: String?) {
if (id != null) {
val videoTrack =
exoPlayer?.currentTracks?.groups?.filter { it.type == TRACK_TYPE_VIDEO }
?.getTrack(id)
if (videoTrack != null) {
exoPlayer?.trackSelectionParameters = exoPlayer?.trackSelectionParameters
?.buildUpon()
?.setOverrideForType(
TrackSelectionOverride(
videoTrack.first,
videoTrack.second
)
)
?.build()
?: return
return
}
}
exoPlayer?.trackSelectionParameters = exoPlayer?.trackSelectionParameters
?.buildUpon()
?.setMaxVideoSize(width, height)
@ -226,8 +274,29 @@ class CS3IPlayer : IPlayer {
?: return
}
override fun setPreferredAudioTrack(trackLanguage: String?) {
override fun setPreferredAudioTrack(trackLanguage: String?, id: String?) {
preferredAudioTrackLanguage = trackLanguage
if (id != null) {
val audioTrack =
exoPlayer?.currentTracks?.groups?.filter { it.type == TRACK_TYPE_AUDIO }
?.getTrack(id)
if (audioTrack != null) {
exoPlayer?.trackSelectionParameters = exoPlayer?.trackSelectionParameters
?.buildUpon()
?.setOverrideForType(
TrackSelectionOverride(
audioTrack.first,
audioTrack.second
)
)
?.build()
?: return
return
}
}
exoPlayer?.trackSelectionParameters = exoPlayer?.trackSelectionParameters
?.buildUpon()
?.setPreferredAudioLanguage(trackLanguage)
@ -239,16 +308,20 @@ class CS3IPlayer : IPlayer {
/**
* Gets all supported formats in a list
* */
private fun List<TracksInfo.TrackGroupInfo>.getFormats(): List<Format> {
private fun List<Tracks.Group>.getFormats(): List<Pair<Format, Int>> {
return this.map {
(0 until it.trackGroup.length).mapNotNull { i ->
if (it.isSupported)
it.trackGroup.getFormat(i) // to it.isSelected
else null
}
it.getFormats()
}.flatten()
}
private fun Tracks.Group.getFormats(): List<Pair<Format, Int>> {
return (0 until this.mediaTrackGroup.length).mapNotNull { i ->
if (this.isSupported)
this.mediaTrackGroup.getFormat(i) to i
else null
}
}
private fun Format.toAudioTrack(): AudioTrack {
return AudioTrack(
this.id,
@ -270,11 +343,12 @@ class CS3IPlayer : IPlayer {
}
override fun getVideoTracks(): CurrentTracks {
val allTracks = exoPlayer?.currentTracksInfo?.trackGroupInfos ?: emptyList()
val videoTracks = allTracks.filter { it.trackType == TRACK_TYPE_VIDEO }.getFormats()
.map { it.toVideoTrack() }
val audioTracks = allTracks.filter { it.trackType == TRACK_TYPE_AUDIO }.getFormats()
.map { it.toAudioTrack() }
val allTracks = exoPlayer?.currentTracks?.groups ?: emptyList()
val videoTracks = allTracks.filter { it.type == TRACK_TYPE_VIDEO }
.getFormats()
.map { it.first.toVideoTrack() }
val audioTracks = allTracks.filter { it.type == TRACK_TYPE_AUDIO }.getFormats()
.map { it.first.toAudioTrack() }
return CurrentTracks(
exoPlayer?.videoFormat?.toVideoTrack(),
@ -290,12 +364,17 @@ class CS3IPlayer : IPlayer {
override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean {
Log.i(TAG, "setPreferredSubtitles init $subtitle")
currentSubtitles = subtitle
fun getTextTrack(id: String) =
exoPlayer?.currentTracks?.groups?.filter { it.type == TRACK_TYPE_TEXT }
?.getTrack(id)
return (exoPlayer?.trackSelector as? DefaultTrackSelector?)?.let { trackSelector ->
val name = subtitle?.name
if (name.isNullOrBlank()) {
if (subtitle == null) {
trackSelector.setParameters(
trackSelector.buildUponParameters()
.setPreferredTextLanguage(null)
.clearOverridesOfType(TRACK_TYPE_TEXT)
)
} else {
when (subtitleHelper.subtitleStatus(subtitle)) {
@ -309,12 +388,15 @@ class CS3IPlayer : IPlayer {
trackSelector.setParameters(
trackSelector.buildUponParameters()
.apply {
if (subtitle.origin == SubtitleOrigin.EMBEDDED_IN_VIDEO)
// The real Language (two letter) is in the url
// No underscore as the .url is the actual exoplayer designated language
setPreferredTextLanguage(subtitle.url)
else
setPreferredTextLanguage("_$name")
val track = getTextTrack(subtitle.getId())
if (track != null) {
setOverrideForType(
TrackSelectionOverride(
track.first,
track.second
)
)
}
}
)
@ -348,17 +430,8 @@ class CS3IPlayer : IPlayer {
override fun getCurrentPreferredSubtitle(): SubtitleData? {
return subtitleHelper.getAllSubtitles().firstOrNull { sub ->
exoPlayerSelectedTracks.any {
// When embedded the real language is in .url as the real name is a two letter code
val realName =
if (sub.origin == SubtitleOrigin.EMBEDDED_IN_VIDEO) sub.url else sub.name
// The replace is needed as exoplayer translates _ to -
// Also we prefix the languages with _
it.second && it.first.replace("-", "").equals(
realName.replace("-", ""),
ignoreCase = true
)
playerSelectedSubtitleTracks.any { (id, isSelected) ->
isSelected && sub.getId() == id
}
}
}
@ -611,7 +684,12 @@ class CS3IPlayer : IPlayer {
} else it
}.toTypedArray()
}
.setTrackSelector(trackSelector ?: getTrackSelector(context, maxVideoHeight))
.setTrackSelector(
trackSelector ?: getTrackSelector(
context,
maxVideoHeight
)
)
.setLoadControl(
DefaultLoadControl.Builder()
.setTargetBufferBytes(
@ -655,7 +733,7 @@ class CS3IPlayer : IPlayer {
source
}
println("PLAYBACK POS $playbackPosition")
//println("PLAYBACK POS $playbackPosition")
return exoPlayerBuilder.build().apply {
setPlayWhenReady(playWhenReady)
seekTo(currentWindow, playbackPosition)
@ -671,8 +749,22 @@ class CS3IPlayer : IPlayer {
}
}
fun updatedTime() {
val position = exoPlayer?.currentPosition
private fun getCurrentTimestamp(writePosition: Long? = null): EpisodeSkip.SkipStamp? {
val position = writePosition ?: this@CS3IPlayer.getPosition() ?: return null
for (lastTimeStamp in lastTimeStamps) {
if (lastTimeStamp.startMs <= position && position < lastTimeStamp.endMs) {
return lastTimeStamp
}
}
return null
}
fun updatedTime(writePosition: Long? = null) {
getCurrentTimestamp(writePosition)?.let { timestamp ->
onTimestampInvoked?.invoke(timestamp)
}
val position = writePosition ?: exoPlayer?.currentPosition
val duration = exoPlayer?.contentDuration
if (duration != null && position != null) {
playerPositionChanged?.invoke(Pair(position, duration))
@ -684,12 +776,12 @@ class CS3IPlayer : IPlayer {
}
override fun seekTo(time: Long) {
updatedTime()
updatedTime(time)
exoPlayer?.seekTo(time)
}
private fun ExoPlayer.seekTime(time: Long) {
updatedTime()
updatedTime(currentPosition + time)
seekTo(currentPosition + time)
}
@ -725,6 +817,17 @@ class CS3IPlayer : IPlayer {
CSPlayerEvent.SeekBack -> seekTime(-seekActionTime)
CSPlayerEvent.NextEpisode -> nextEpisode?.invoke()
CSPlayerEvent.PrevEpisode -> prevEpisode?.invoke()
CSPlayerEvent.SkipCurrentChapter -> {
//val dur = this@CS3IPlayer.getDuration() ?: return@apply
getCurrentTimestamp()?.let { lastTimeStamp ->
if (lastTimeStamp.skipToNextEpisode) {
handleEvent(CSPlayerEvent.NextEpisode)
} else {
seekTo(lastTimeStamp.endMs + 1L)
}
onTimestampSkipped?.invoke(lastTimeStamp)
}
}
}
}
} catch (e: Exception) {
@ -781,53 +884,42 @@ class CS3IPlayer : IPlayer {
isPlaying = exo.isPlaying
}
exoPlayer?.addListener(object : Player.Listener {
/**
* Records the current used subtitle/track. Needed as exoplayer seems to have loose track language selection.
* */
override fun onTracksInfoChanged(tracksInfo: TracksInfo) {
fun Format.isSubtitle(): Boolean {
return this.sampleMimeType?.contains("video/") == false &&
this.sampleMimeType?.contains("audio/") == false
}
override fun onTracksChanged(tracks: Tracks) {
normalSafeApiCall {
exoPlayerSelectedTracks =
tracksInfo.trackGroupInfos.mapNotNull {
val format = it.trackGroup.getFormat(0)
if (format.isSubtitle())
format.language?.let { lang -> lang to it.isSelected }
else null
}
val textTracks = tracks.groups.filter { it.type == TRACK_TYPE_TEXT }
val exoPlayerReportedTracks = tracksInfo.trackGroupInfos.mapNotNull {
// Filter out unsupported tracks
if (it.isSupported)
it.trackGroup.getFormat(0)
else
null
}.mapNotNull {
// Filter out non subs, already used subs and subs without languages
if (!it.isSubtitle() ||
// Anything starting with - is not embedded
it.language?.startsWith("-") == true ||
it.language == null
) return@mapNotNull null
return@mapNotNull SubtitleData(
// Nicer looking displayed names
fromTwoLettersToLanguage(it.language!!) ?: it.language!!,
// See setPreferredTextLanguage
it.language!!,
SubtitleOrigin.EMBEDDED_IN_VIDEO,
it.sampleMimeType ?: MimeTypes.APPLICATION_SUBRIP,
emptyMap()
)
}
playerSelectedSubtitleTracks =
textTracks.map { group ->
group.getFormats().mapNotNull { (format, _) ->
(format.id ?: return@mapNotNull null) to group.isSelected
}
}.flatten()
val exoPlayerReportedTracks =
tracks.groups.filter { it.type == TRACK_TYPE_TEXT }.getFormats()
.mapNotNull { (format, _) ->
// Filter out non subs, already used subs and subs without languages
if (format.id == null ||
format.language == null ||
format.language?.startsWith("-") == true
) return@mapNotNull null
return@mapNotNull SubtitleData(
// Nicer looking displayed names
fromTwoLettersToLanguage(format.language!!)
?: format.language!!,
// See setPreferredTextLanguage
format.id!!,
SubtitleOrigin.EMBEDDED_IN_VIDEO,
format.sampleMimeType ?: MimeTypes.APPLICATION_SUBRIP,
emptyMap()
)
}
embeddedSubtitlesFetched?.invoke(exoPlayerReportedTracks)
onTracksInfoChanged?.invoke()
subtitlesUpdates?.invoke()
}
super.onTracksInfoChanged(tracksInfo)
}
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
@ -881,7 +973,7 @@ class CS3IPlayer : IPlayer {
// This is to switch mirrors automatically if the stream has not been fetched, but
// allow playing the buffer without internet as then the duration is fetched.
if (error.errorCode == PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED
&& exoPlayer?.duration != C.TIME_UNSET
&& exoPlayer?.duration != TIME_UNSET
) {
exoPlayer?.prepare()
} else {
@ -947,6 +1039,24 @@ class CS3IPlayer : IPlayer {
}
}
private var lastTimeStamps: List<EpisodeSkip.SkipStamp> = emptyList()
override fun addTimeStamps(timeStamps: List<EpisodeSkip.SkipStamp>) {
lastTimeStamps = timeStamps
timeStamps.forEach { timestamp ->
exoPlayer?.createMessage { _, _ ->
updatedTime()
//if (payload is EpisodeSkip.SkipStamp) // this should always be true
// onTimestampInvoked?.invoke(payload)
}
?.setLooper(Looper.getMainLooper())
?.setPosition(timestamp.startMs)
//?.setPayload(timestamp)
?.setDeleteAfterDelivery(false)
?.send()
}
updatedTime()
}
fun onRenderFirst() {
if (!hasUsedFirstRender) { // this insures that we only call this once per player load
Log.i(TAG, "Rendered first frame")
@ -1022,14 +1132,15 @@ class CS3IPlayer : IPlayer {
val subConfig = MediaItem.SubtitleConfiguration.Builder(Uri.parse(sub.url))
.setMimeType(sub.mimeType)
.setLanguage("_${sub.name}")
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
.setId(sub.getId())
.setSelectionFlags(SELECTION_FLAG_DEFAULT)
.build()
when (sub.origin) {
SubtitleOrigin.DOWNLOADED_FILE -> {
if (offlineSourceFactory != null) {
activeSubtitles.add(sub)
SingleSampleMediaSource.Factory(offlineSourceFactory)
.createMediaSource(subConfig, C.TIME_UNSET)
.createMediaSource(subConfig, TIME_UNSET)
} else {
null
}
@ -1041,7 +1152,7 @@ class CS3IPlayer : IPlayer {
if (sub.headers.isNotEmpty())
this.setDefaultRequestProperties(sub.headers)
})
.createMediaSource(subConfig, C.TIME_UNSET)
.createMediaSource(subConfig, TIME_UNSET)
} else {
null
}
@ -1050,7 +1161,7 @@ class CS3IPlayer : IPlayer {
if (offlineSourceFactory != null) {
activeSubtitles.add(sub)
SingleSampleMediaSource.Factory(offlineSourceFactory)
.createMediaSource(subConfig, C.TIME_UNSET)
.createMediaSource(subConfig, TIME_UNSET)
} else {
null
}

View File

@ -587,7 +587,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
updateLockUI()
}
private fun updateUIVisibility() {
fun updateUIVisibility() {
val isGone = isLocked || !isShowing
var togglePlayerTitleGone = isGone
context?.let {
@ -612,6 +612,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
//player_media_route_button?.isClickable = !isGone
player_go_back_holder?.isGone = isGone
player_sources_btt?.isGone = isGone
player_skip_episode?.isClickable = !isGone
}
private fun updateLockUI() {
@ -1101,7 +1102,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
}
protected fun uiReset() {
isLocked = false
isShowing = false
// if nothing has loaded these buttons should not be visible
@ -1141,6 +1141,9 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
PlayerEventType.Play -> {
player.handleEvent(CSPlayerEvent.Play)
}
PlayerEventType.SkipCurrentChapter -> {
player.handleEvent(CSPlayerEvent.SkipCurrentChapter)
}
PlayerEventType.Resize -> {
nextResize()
}
@ -1254,6 +1257,10 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
player.handleEvent(CSPlayerEvent.PlayPauseToggle)
}
skip_chapter_button?.setOnClickListener {
player.handleEvent(CSPlayerEvent.SkipCurrentChapter)
}
// init clicks
player_resize_btt?.setOnClickListener {
autoHide()
@ -1306,12 +1313,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
showTracksDialogue()
}
player_intro_play?.setOnClickListener {
player_intro_play?.isGone = true
player.handleEvent(CSPlayerEvent.Play)
updateUIVisibility()
}
// it is !not! a bug that you cant touch the right side, it does not register inputs on navbar or status bar
player_holder?.setOnTouchListener { callView, event ->
return@setOnTouchListener handleMotionEvent(callView, event)

View File

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.ui.player
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Context
@ -13,6 +14,7 @@ import android.view.ViewGroup
import android.widget.*
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.animation.addListener
import androidx.core.content.ContextCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
@ -36,8 +38,8 @@ import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSub
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.ui.result.SyncViewModel
import com.lagradost.cloudstream3.ui.result.setText
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
@ -49,6 +51,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import kotlinx.android.synthetic.main.dialog_online_subtitles.*
import kotlinx.android.synthetic.main.dialog_online_subtitles.apply_btt
import kotlinx.android.synthetic.main.dialog_online_subtitles.cancel_btt
@ -58,7 +61,6 @@ import kotlinx.android.synthetic.main.player_select_source_and_subs.*
import kotlinx.android.synthetic.main.player_select_source_and_subs.subtitles_click_settings
import kotlinx.android.synthetic.main.player_select_tracks.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
class GeneratorPlayer : FullScreenPlayer() {
companion object {
@ -67,8 +69,7 @@ class GeneratorPlayer : FullScreenPlayer() {
Log.i(TAG, "newInstance = $syncData")
lastUsedGenerator = generator
return Bundle().apply {
if (syncData != null)
putSerializable("syncData", syncData)
if (syncData != null) putSerializable("syncData", syncData)
}
}
@ -165,6 +166,8 @@ class GeneratorPlayer : FullScreenPlayer() {
isActive = true
setPlayerDimen(null)
setTitle()
if (!sameEpisode)
hasRequestedStamps = false
loadExtractorJob(link.first)
// load player
@ -180,12 +183,13 @@ class GeneratorPlayer : FullScreenPlayer() {
},
currentSubs,
(if (sameEpisode) currentSelectedSubtitles else null) ?: getAutoSelectSubtitle(
currentSubs,
settings = true,
downloads = true
currentSubs, settings = true, downloads = true
),
)
}
if (!sameEpisode)
player.addTimeStamps(listOf()) // clear stamps
}
private fun sortLinks(useQualitySettings: Boolean = true): List<Pair<ExtractorLink?, ExtractorUri?>> {
@ -231,9 +235,7 @@ class GeneratorPlayer : FullScreenPlayer() {
}
override fun openOnlineSubPicker(
context: Context,
imdbId: Long?,
dismissCallback: (() -> Unit)
context: Context, imdbId: Long?, dismissCallback: (() -> Unit)
) {
val providers = subsProviders
val isSingleProvider = subsProviders.size == 1
@ -256,8 +258,7 @@ class GeneratorPlayer : FullScreenPlayer() {
val arrayAdapter =
object : ArrayAdapter<AbstractSubtitleEntities.SubtitleEntity>(dialog.context, layout) {
fun setHearingImpairedIcon(
imageViewEnd: ImageView?,
position: Int
imageViewEnd: ImageView?, position: Int
) {
if (imageViewEnd == null) return
val isHearingImpaired =
@ -265,13 +266,11 @@ class GeneratorPlayer : FullScreenPlayer() {
val drawableEnd = if (isHearingImpaired) {
ContextCompat.getDrawable(
context,
R.drawable.ic_baseline_hearing_24
context, R.drawable.ic_baseline_hearing_24
)?.apply {
setTint(
ContextCompat.getColor(
context,
R.color.textColor
context, R.color.textColor
)
)
}
@ -281,8 +280,7 @@ class GeneratorPlayer : FullScreenPlayer() {
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: LayoutInflater.from(context)
.inflate(layout, null)
val view = convertView ?: LayoutInflater.from(context).inflate(layout, null)
val item = getItem(position)
@ -337,14 +335,13 @@ class GeneratorPlayer : FullScreenPlayer() {
override fun onQueryTextSubmit(query: String?): Boolean {
dialog.search_loading_bar?.show()
ioSafe {
val search = AbstractSubtitleEntities.SubtitleSearch(
query = query ?: return@ioSafe,
imdb = imdbId,
epNumber = currentTempMeta.episode,
seasonNumber = currentTempMeta.season,
lang = currentLanguageTwoLetters.ifBlank { null }
)
val results = providers.apmap {
val search =
AbstractSubtitleEntities.SubtitleSearch(query = query ?: return@ioSafe,
imdb = imdbId,
epNumber = currentTempMeta.episode,
seasonNumber = currentTempMeta.season,
lang = currentLanguageTwoLetters.ifBlank { null })
val results = providers.amap {
try {
it.search(search)
} catch (e: Exception) {
@ -379,14 +376,12 @@ class GeneratorPlayer : FullScreenPlayer() {
dialog.search_filter.setOnClickListener { view ->
val lang639_1 = languages.map { it.ISO_639_1 }
activity?.showDialog(
languages.map { it.languageName },
activity?.showDialog(languages.map { it.languageName },
lang639_1.indexOf(currentLanguageTwoLetters),
view?.context?.getString(R.string.subs_subtitle_languages)
?: return@setOnClickListener,
true,
{ }
) { index ->
{ }) { index ->
currentLanguageTwoLetters = lang639_1[index]
dialog.subtitles_search.setQuery(dialog.subtitles_search.query, true)
}
@ -443,16 +438,17 @@ class GeneratorPlayer : FullScreenPlayer() {
private fun addAndSelectSubtitles(subtitleData: SubtitleData) {
val ctx = context ?: return
setSubtitles(subtitleData)
// this is used instead of observe, because observe is too slow
val subs = currentSubs + subtitleData
// this is used instead of observe(viewModel._currentSubs), because observe is too slow
player.setActiveSubtitles(subs)
// Save current time as to not reset player to 00:00
player.saveData()
player.setActiveSubtitles(subs)
player.reloadPlayer(ctx)
setSubtitles(subtitleData)
viewModel.addSubtitles(setOf(subtitleData))
selectSourceDialog?.dismissSafe()
@ -472,8 +468,8 @@ class GeneratorPlayer : FullScreenPlayer() {
if (uri == null) return@normalSafeApiCall
val ctx = context ?: AcraApplication.context ?: return@normalSafeApiCall
// RW perms for the path
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
val flags =
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
ctx.contentResolver.takePersistableUriPermission(uri, flags)
@ -536,11 +532,9 @@ class GeneratorPlayer : FullScreenPlayer() {
}
if (subsProvidersIsActive) {
val loadFromOpenSubsFooter: TextView =
layoutInflater.inflate(
R.layout.sort_bottom_footer_add_choice,
null
) as TextView
val loadFromOpenSubsFooter: TextView = layoutInflater.inflate(
R.layout.sort_bottom_footer_add_choice, null
) as TextView
loadFromOpenSubsFooter.text =
ctx.getString(R.string.player_load_subtitles_online)
@ -592,8 +586,7 @@ class GeneratorPlayer : FullScreenPlayer() {
val subtitleIndexStart = currentSubtitles.indexOf(currentSelectedSubtitles) + 1
var subtitleIndex = subtitleIndexStart
val subsArrayAdapter =
ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
val subsArrayAdapter = ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
subsArrayAdapter.add(ctx.getString(R.string.no_subtitles))
subsArrayAdapter.addAll(currentSubtitles.map { it.name })
@ -631,8 +624,7 @@ class GeneratorPlayer : FullScreenPlayer() {
val prefValues = ctx.resources.getStringArray(R.array.subtitles_encoding_values)
val value = settingsManager.getString(
ctx.getString(R.string.subtitles_encoding_key),
null
ctx.getString(R.string.subtitles_encoding_key), null
)
val index = prefValues.indexOf(value)
text = prefNames[if (index == -1) 0 else index]
@ -644,28 +636,22 @@ class GeneratorPlayer : FullScreenPlayer() {
val prefNames = ctx.resources.getStringArray(R.array.subtitles_encoding_list)
val prefValues = ctx.resources.getStringArray(R.array.subtitles_encoding_values)
val currentPrefMedia =
settingsManager.getString(
ctx.getString(R.string.subtitles_encoding_key),
null
)
val currentPrefMedia = settingsManager.getString(
ctx.getString(R.string.subtitles_encoding_key), null
)
shouldDismiss = false
sourceDialog.dismissSafe(activity)
val index = prefValues.indexOf(currentPrefMedia)
activity?.showDialog(
prefNames.toList(),
activity?.showDialog(prefNames.toList(),
if (index == -1) 0 else index,
ctx.getString(R.string.subtitles_encoding),
true,
{}) {
settingsManager.edit()
.putString(
ctx.getString(R.string.subtitles_encoding_key),
prefValues[it]
)
.apply()
settingsManager.edit().putString(
ctx.getString(R.string.subtitles_encoding_key), prefValues[it]
).apply()
updateForcedEncoding(ctx)
dismiss()
@ -796,15 +782,16 @@ class GeneratorPlayer : FullScreenPlayer() {
}
tracksDialog.apply_btt?.setOnClickListener {
val currentTrack = currentAudioTracks.getOrNull(audioIndexStart)
player.setPreferredAudioTrack(
currentAudioTracks.getOrNull(audioIndexStart)?.language
currentTrack?.language, currentTrack?.id
)
val currentVideo = currentVideoTracks.getOrNull(videoIndex)
val width = currentVideo?.width ?: NO_VALUE
val height = currentVideo?.height ?: NO_VALUE
if (width != NO_VALUE && height != NO_VALUE) {
player.setMaxVideoSize(width, height)
player.setMaxVideoSize(width, height, currentVideo?.id)
}
tracksDialog.dismissSafe(activity)
@ -877,7 +864,7 @@ class GeneratorPlayer : FullScreenPlayer() {
}
var maxEpisodeSet: Int? = null
var hasRequestedStamps: Boolean = false
override fun playerPositionChanged(posDur: Pair<Long, Long>) {
// Don't save livestream data
if ((currentMeta as? ResultEpisode)?.tvType?.isLiveStream() == true) return
@ -886,11 +873,24 @@ class GeneratorPlayer : FullScreenPlayer() {
if ((currentMeta as? ResultEpisode)?.tvType == TvType.NSFW) return
val (position, duration) = posDur
if (duration == 0L) return // idk how you achieved this, but div by zero crash
if (duration <= 0L) return // idk how you achieved this, but div by zero crash
if (!hasRequestedStamps) {
hasRequestedStamps = true
val fetchStamps = context?.let { ctx ->
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
settingsManager.getBoolean(
ctx.getString(R.string.enable_skip_op_from_database),
true
)
} ?: true
if (fetchStamps)
viewModel.loadStamps(duration)
}
viewModel.getId()?.let {
DataStoreHelper.setViewPos(it, position, duration)
}
val percentage = position * 100L / duration
val nextEp = percentage >= NEXT_WATCH_EPISODE_PERCENTAGE
@ -938,17 +938,14 @@ class GeneratorPlayer : FullScreenPlayer() {
context?.let { ctx ->
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
if (settingsManager.getBoolean(
ctx.getString(R.string.episode_sync_enabled_key),
true
ctx.getString(R.string.episode_sync_enabled_key), true
)
)
maxEpisodeSet = meta.episode
) maxEpisodeSet = meta.episode
sync.modifyMaxEpisode(meta.episode)
}
}
if (meta.tvType.isAnimeOp())
isOpVisible = percentage < SKIP_OP_VIDEO_PERCENTAGE
if (meta.tvType.isAnimeOp()) isOpVisible = percentage < SKIP_OP_VIDEO_PERCENTAGE
}
}
player_skip_op?.isVisible = isOpVisible
@ -960,12 +957,10 @@ class GeneratorPlayer : FullScreenPlayer() {
}
private fun getAutoSelectSubtitle(
subtitles: Set<SubtitleData>,
settings: Boolean,
downloads: Boolean
subtitles: Set<SubtitleData>, settings: Boolean, downloads: Boolean
): SubtitleData? {
val langCode = preferredAutoSelectSubtitles ?: return null
val lang = SubtitleHelper.fromTwoLettersToLanguage(langCode) ?: return null
val lang = fromTwoLettersToLanguage(langCode) ?: return null
if (downloads) {
return subtitles.firstOrNull { sub ->
(sub.origin == SubtitleOrigin.DOWNLOADED_FILE && sub.name == context?.getString(
@ -976,22 +971,11 @@ class GeneratorPlayer : FullScreenPlayer() {
sortSubs(subtitles).firstOrNull { sub ->
val t = sub.name.replace(Regex("[^A-Za-z]"), " ").trim()
(settings || (downloads && sub.origin == SubtitleOrigin.DOWNLOADED_FILE)) && t == lang || t.startsWith(
"$lang "
) || t == langCode
(settings) && t == lang || t.startsWith(lang) || t == langCode
}?.let { sub ->
return sub
}
// post check in case both did not catch anything
if (downloads) {
return subtitles.firstOrNull { sub ->
(sub.origin == SubtitleOrigin.DOWNLOADED_FILE || sub.name == context?.getString(
R.string.default_subtitles
))
}
}
return null
}
@ -1008,23 +992,18 @@ class GeneratorPlayer : FullScreenPlayer() {
player.handleEvent(CSPlayerEvent.Play)
return true
}
} else
if (!langCode.isNullOrEmpty()) {
getAutoSelectSubtitle(
currentSubs,
settings = true,
downloads = false
)?.let { sub ->
if (setSubtitles(sub)) {
player.saveData()
player.reloadPlayer(ctx)
player.handleEvent(CSPlayerEvent.Play)
return true
}
} else if (!langCode.isNullOrEmpty()) {
getAutoSelectSubtitle(
currentSubs, settings = true, downloads = false
)?.let { sub ->
if (setSubtitles(sub)) {
player.saveData()
player.reloadPlayer(ctx)
player.handleEvent(CSPlayerEvent.Play)
return true
}
}
}
}
return false
}
@ -1080,17 +1059,17 @@ class GeneratorPlayer : FullScreenPlayer() {
context?.let { ctx ->
//Generate video title
val playerVideoTitle = if (headerName != null) {
(headerName +
if (tvType.isEpisodeBased() && episode != null)
if (season == null)
" - ${ctx.getString(R.string.episode)} $episode"
else
" \"${ctx.getString(R.string.season_short)}${season}:${
ctx.getString(
R.string.episode_short
)
}${episode}\""
else "") + if (subName.isNullOrBlank() || subName == headerName) "" else " - $subName"
(headerName + if (tvType.isEpisodeBased() && episode != null) if (season == null) " - ${
ctx.getString(
R.string.episode
)
} $episode"
else " \"${ctx.getString(R.string.season_short)}${season}:${
ctx.getString(
R.string.episode_short
)
}${episode}\""
else "") + if (subName.isNullOrBlank() || subName == headerName) "" else " - $subName"
} else {
""
}
@ -1130,8 +1109,7 @@ class GeneratorPlayer : FullScreenPlayer() {
""
}
val source = currentSelectedLink?.first?.name ?: currentSelectedLink?.second?.name
?: "NULL"
val source = currentSelectedLink?.first?.name ?: currentSelectedLink?.second?.name ?: "NULL"
player_video_title_rez?.text = when (titleRez) {
0 -> ""
@ -1154,14 +1132,11 @@ class GeneratorPlayer : FullScreenPlayer() {
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
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
isTv = isTvSettings()
layout =
if (isTv) R.layout.fragment_player_tv else R.layout.fragment_player
layout = if (isTv) R.layout.fragment_player_tv else R.layout.fragment_player
viewModel = ViewModelProvider(this)[PlayerGeneratorViewModel::class.java]
sync = ViewModelProvider(this)[SyncViewModel::class.java]
@ -1173,6 +1148,68 @@ class GeneratorPlayer : FullScreenPlayer() {
return super.onCreateView(inflater, container, savedInstanceState)
}
var timestampShowState = false
var skipAnimator: ValueAnimator? = null
var skipIndex = 0
private fun displayTimeStamp(show: Boolean) {
if (timestampShowState == show) return
skipIndex++
println("displayTimeStamp = $show")
timestampShowState = show
skip_chapter_button?.apply {
val showWidth = 170.toPx
val noShowWidth = 10.toPx
//if((show && width == showWidth) || (!show && width == noShowWidth)) {
// return
//}
val to = if (show) showWidth else noShowWidth
val from = if (!show) showWidth else noShowWidth
skipAnimator?.cancel()
isVisible = true
// just in case
val lay = layoutParams
lay.width = from
layoutParams = lay
skipAnimator = ValueAnimator.ofInt(
from, to
).apply {
addListener(onEnd = {
if (!show) skip_chapter_button?.isVisible = false
})
addUpdateListener { valueAnimator ->
val value = valueAnimator.animatedValue as Int
val layoutParams: ViewGroup.LayoutParams = layoutParams
layoutParams.width = value
setLayoutParams(layoutParams)
}
duration = 500
start()
}
}
}
override fun onTimestampSkipped(timestamp: EpisodeSkip.SkipStamp) {
displayTimeStamp(false)
}
override fun onTimestamp(timestamp: EpisodeSkip.SkipStamp?) {
if (timestamp != null) {
skip_chapter_button.setText(timestamp.uiText)
displayTimeStamp(true)
val currentIndex = skipIndex
skip_chapter_button?.handler?.postDelayed({
if (skipIndex == currentIndex)
displayTimeStamp(false)
}, 6000)
} else {
displayTimeStamp(false)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
var langFilterList = listOf<String>()
@ -1188,8 +1225,7 @@ class GeneratorPlayer : FullScreenPlayer() {
settingsManager.getBoolean(getString(R.string.filter_sub_lang_key), false)
if (filterSubByLang) {
val langFromPrefMedia = settingsManager.getStringSet(
this.getString(R.string.provider_lang_key),
mutableSetOf("en")
this.getString(R.string.provider_lang_key), mutableSetOf("en")
)
langFilterList = langFromPrefMedia?.mapNotNull {
fromTwoLettersToLanguage(it)?.lowercase() ?: return@mapNotNull null
@ -1202,7 +1238,7 @@ class GeneratorPlayer : FullScreenPlayer() {
sync.updateUserData()
preferredAutoSelectSubtitles = SubtitlesFragment.getAutoSelectLanguageISO639_1()
preferredAutoSelectSubtitles = getAutoSelectLanguageISO639_1()
if (currentSelectedLink == null) {
viewModel.loadLinks()
@ -1217,6 +1253,10 @@ class GeneratorPlayer : FullScreenPlayer() {
activity?.popCurrentPage()
}
observe(viewModel.currentStamps) { stamps ->
player.addTimeStamps(stamps)
}
observe(viewModel.loadingLinks) {
when (it) {
is Resource.Loading -> {
@ -1252,8 +1292,10 @@ class GeneratorPlayer : FullScreenPlayer() {
Log.i("subfilter", "Filtering subtitle")
langFilterList.forEach { lang ->
Log.i("subfilter", "Lang: $lang")
setOfSub += set.filter { it.name.contains(lang, ignoreCase = true) }
.toMutableSet()
setOfSub += set.filter {
it.name.contains(lang, ignoreCase = true) ||
it.origin != SubtitleOrigin.URL
}
}
currentSubs = setOfSub
} else {
@ -1261,7 +1303,13 @@ class GeneratorPlayer : FullScreenPlayer() {
}
player.setActiveSubtitles(set)
autoSelectSubtitles()
// If the file is downloaded then do not select auto select the subtitles
// Downloaded subtitles cannot be selected immediately after loading since
// player.getCurrentPreferredSubtitle() cannot fetch data from non-loaded subtitles
// Resulting in unselecting the downloaded subtitle
if (set.lastOrNull()?.origin != SubtitleOrigin.DOWNLOADED_FILE) {
autoSelectSubtitles()
}
}
}
}

View File

@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.ui.player
import android.content.Context
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
@ -12,9 +13,9 @@ enum class PlayerEventType(val value: Int) {
SeekForward(2),
SeekBack(3),
//SkipCurrentChapter(4),
SkipCurrentChapter(4),
NextEpisode(5),
PrevEpisode(5),
PrevEpisode(6),
PlayPauseToggle(7),
ToggleMute(8),
Lock(9),
@ -32,7 +33,7 @@ enum class CSPlayerEvent(val value: Int) {
SeekForward(2),
SeekBack(3),
//SkipCurrentChapter(4),
SkipCurrentChapter(4),
NextEpisode(5),
PrevEpisode(6),
PlayPauseToggle(7),
@ -54,7 +55,8 @@ interface Track {
**/
val id: String?
val label: String?
// val isCurrentlyPlaying: Boolean
// val isCurrentlyPlaying: Boolean
val language: String?
}
@ -124,6 +126,8 @@ interface IPlayer {
subtitlesUpdates: (() -> Unit)? = null, // callback from player to inform that subtitles have updated in some way
embeddedSubtitlesFetched: ((List<SubtitleData>) -> Unit)? = null, // callback from player to give all embedded subtitles
onTracksInfoChanged: (() -> Unit)? = null, // Callback when tracks are changed, used for UI changes
onTimestampInvoked: ((EpisodeSkip.SkipStamp?) -> Unit)? = null, // Callback when timestamps appear, null when it should disappear
onTimestampSkipped: ((EpisodeSkip.SkipStamp) -> Unit)? = null, // callback for when a chapter is skipped, aka when event is handled (or for future use when skip automatically ads/sponsor)
)
fun releaseCallbacks()
@ -131,6 +135,8 @@ interface IPlayer {
fun updateSubtitleStyle(style: SaveCaptionStyle)
fun saveData()
fun addTimeStamps(timeStamps: List<EpisodeSkip.SkipStamp>)
fun loadPlayer(
context: Context,
sameEpisode: Boolean,
@ -161,9 +167,9 @@ interface IPlayer {
fun getVideoTracks(): CurrentTracks
/** If no parameters are set it'll default to no set size */
fun setMaxVideoSize(width: Int = Int.MAX_VALUE, height: Int = Int.MAX_VALUE)
/** If no parameters are set it'll default to no set size, Specifying the id allows for track overrides to force the player to pick the quality. */
fun setMaxVideoSize(width: Int = Int.MAX_VALUE, height: Int = Int.MAX_VALUE, id: String? = null)
/** If no trackLanguage is set it'll default to first track */
fun setPreferredAudioTrack(trackLanguage: String?)
/** If no trackLanguage is set it'll default to first track. Specifying the id allows for track overrides as the language can be identical. */
fun setPreferredAudioTrack(trackLanguage: String?, id: String? = null)
}

View File

@ -1,6 +1,6 @@
package com.lagradost.cloudstream3.ui.player
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.utils.*
import java.net.URI
@ -46,7 +46,7 @@ class LinkGenerator(
subtitleCallback: (SubtitleData) -> Unit,
offset: Int
): Boolean {
links.apmap { link ->
links.amap { link ->
if (!extract || !loadExtractor(link, referer, {
subtitleCallback(PlayerSubtitleHelper.getSubtitleData(it))
}) {

View File

@ -0,0 +1,456 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lagradost.cloudstream3.ui.player;
import static com.google.android.exoplayer2.text.Cue.DIMEN_UNSET;
import static com.google.android.exoplayer2.text.Cue.LINE_TYPE_NUMBER;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Handler;
import android.os.Handler.Callback;
import android.os.Looper;
import android.os.Message;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.BaseRenderer;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.source.SampleStream.ReadDataResult;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.CueGroup;
import com.google.android.exoplayer2.text.Subtitle;
import com.google.android.exoplayer2.text.SubtitleDecoder;
import com.google.android.exoplayer2.text.SubtitleDecoderException;
import com.google.android.exoplayer2.text.SubtitleDecoderFactory;
import com.google.android.exoplayer2.text.SubtitleInputBuffer;
import com.google.android.exoplayer2.text.SubtitleOutputBuffer;
import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
// DO NOT CONVERT TO KOTLIN AUTOMATICALLY, IT FUCKS UP AND DOES NOT DISPLAY SUBS FOR SOME REASON
// IF YOU CHANGE THE CODE MAKE SURE YOU GET THE CUES CORRECT!
/**
* A renderer for text.
*
* <p>{@link Subtitle}s are decoded from sample data using {@link SubtitleDecoder} instances
* obtained from a {@link SubtitleDecoderFactory}. The actual rendering of the subtitle {@link Cue}s
* is delegated to a {@link TextOutput}.
*/
public class NonFinalTextRenderer extends BaseRenderer implements Callback {
private static final String TAG = "TextRenderer";
/**
* @param trackType The track type that the renderer handles. One of the {@link C} {@code
* TRACK_TYPE_*} constants.
* @param outputHandler
*/
public NonFinalTextRenderer(int trackType, @Nullable Handler outputHandler) {
super(trackType);
this.outputHandler = outputHandler;
}
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
REPLACEMENT_STATE_NONE,
REPLACEMENT_STATE_SIGNAL_END_OF_STREAM,
REPLACEMENT_STATE_WAIT_END_OF_STREAM
})
private @interface ReplacementState {
}
/**
* The decoder does not need to be replaced.
*/
private static final int REPLACEMENT_STATE_NONE = 0;
/**
* The decoder needs to be replaced, but we haven't yet signaled an end of stream to the existing
* decoder. We need to do so in order to ensure that it outputs any remaining buffers before we
* release it.
*/
private static final int REPLACEMENT_STATE_SIGNAL_END_OF_STREAM = 1;
/**
* The decoder needs to be replaced, and we've signaled an end of stream to the existing decoder.
* We're waiting for the decoder to output an end of stream signal to indicate that it has output
* any remaining buffers before we release it.
*/
private static final int REPLACEMENT_STATE_WAIT_END_OF_STREAM = 2;
private static final int MSG_UPDATE_OUTPUT = 0;
@Nullable
private final Handler outputHandler;
private TextOutput output = null;
private SubtitleDecoderFactory decoderFactory = null;
private FormatHolder formatHolder = null;
private boolean inputStreamEnded;
private boolean outputStreamEnded;
private boolean waitingForKeyFrame;
private @ReplacementState int decoderReplacementState;
@Nullable
private Format streamFormat;
@Nullable
private SubtitleDecoder decoder;
@Nullable
private SubtitleInputBuffer nextInputBuffer;
@Nullable
private SubtitleOutputBuffer subtitle;
@Nullable
private SubtitleOutputBuffer nextSubtitle;
private int nextSubtitleEventIndex;
private long finalStreamEndPositionUs;
/**
* @param output The output.
* @param outputLooper The looper associated with the thread on which the output should be called.
* If the output makes use of standard Android UI components, then this should normally be the
* looper associated with the application's main thread, which can be obtained using {@link
* android.app.Activity#getMainLooper()}. Null may be passed if the output should be called
* directly on the player's internal rendering thread.
*/
public NonFinalTextRenderer(TextOutput output, @Nullable Looper outputLooper) {
this(output, outputLooper, SubtitleDecoderFactory.DEFAULT);
}
/**
* @param output The output.
* @param outputLooper The looper associated with the thread on which the output should be called.
* If the output makes use of standard Android UI components, then this should normally be the
* looper associated with the application's main thread, which can be obtained using {@link
* android.app.Activity#getMainLooper()}. Null may be passed if the output should be called
* directly on the player's internal rendering thread.
* @param decoderFactory A factory from which to obtain {@link SubtitleDecoder} instances.
*/
public NonFinalTextRenderer(
TextOutput output, @Nullable Looper outputLooper, SubtitleDecoderFactory decoderFactory) {
super(C.TRACK_TYPE_TEXT);
this.output = checkNotNull(output);
this.outputHandler =
outputLooper == null ? null : Util.createHandler(outputLooper, /* callback= */ this);
this.decoderFactory = decoderFactory;
formatHolder = new FormatHolder();
finalStreamEndPositionUs = C.TIME_UNSET;
}
@Override
public String getName() {
return TAG;
}
@Override
public @Capabilities int supportsFormat(Format format) {
if (decoderFactory.supportsFormat(format)) {
return RendererCapabilities.create(
format.cryptoType == C.CRYPTO_TYPE_NONE ? C.FORMAT_HANDLED : C.FORMAT_UNSUPPORTED_DRM);
} else if (MimeTypes.isText(format.sampleMimeType)) {
return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_SUBTYPE);
} else {
return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE);
}
}
/**
* Sets the position at which to stop rendering the current stream.
*
* <p>Must be called after {@link #setCurrentStreamFinal()}.
*
* @param streamEndPositionUs The position to stop rendering at or {@link C#LENGTH_UNSET} to
* render until the end of the current stream.
*/
// TODO(internal b/181312195): Remove this when it's no longer needed once subtitles are decoded
// on the loading side of SampleQueue.
public void setFinalStreamEndPositionUs(long streamEndPositionUs) {
checkState(isCurrentStreamFinal());
this.finalStreamEndPositionUs = streamEndPositionUs;
}
@Override
protected void onStreamChanged(Format[] formats, long startPositionUs, long offsetUs) {
streamFormat = formats[0];
if (decoder != null) {
decoderReplacementState = REPLACEMENT_STATE_SIGNAL_END_OF_STREAM;
} else {
initDecoder();
}
}
@Override
protected void onPositionReset(long positionUs, boolean joining) {
clearOutput();
inputStreamEnded = false;
outputStreamEnded = false;
finalStreamEndPositionUs = C.TIME_UNSET;
if (decoderReplacementState != REPLACEMENT_STATE_NONE) {
replaceDecoder();
} else {
releaseBuffers();
checkNotNull(decoder).flush();
}
}
@Override
public void render(long positionUs, long elapsedRealtimeUs) {
if (isCurrentStreamFinal()
&& finalStreamEndPositionUs != C.TIME_UNSET
&& positionUs >= finalStreamEndPositionUs) {
releaseBuffers();
outputStreamEnded = true;
}
if (outputStreamEnded) {
return;
}
if (nextSubtitle == null) {
checkNotNull(decoder).setPositionUs(positionUs);
try {
nextSubtitle = checkNotNull(decoder).dequeueOutputBuffer();
} catch (SubtitleDecoderException e) {
handleDecoderError(e);
return;
}
}
if (getState() != STATE_STARTED) {
return;
}
boolean textRendererNeedsUpdate = false;
if (subtitle != null) {
// We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we
// advance to the next event.
long subtitleNextEventTimeUs = getNextEventTime();
while (subtitleNextEventTimeUs <= positionUs) {
nextSubtitleEventIndex++;
subtitleNextEventTimeUs = getNextEventTime();
textRendererNeedsUpdate = true;
}
}
if (nextSubtitle != null) {
SubtitleOutputBuffer nextSubtitle = this.nextSubtitle;
if (nextSubtitle.isEndOfStream()) {
if (!textRendererNeedsUpdate && getNextEventTime() == Long.MAX_VALUE) {
if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) {
replaceDecoder();
} else {
releaseBuffers();
outputStreamEnded = true;
}
}
} else if (nextSubtitle.timeUs <= positionUs) {
// Advance to the next subtitle. Sync the next event index and trigger an update.
if (subtitle != null) {
subtitle.release();
}
nextSubtitleEventIndex = nextSubtitle.getNextEventTimeIndex(positionUs);
subtitle = nextSubtitle;
this.nextSubtitle = null;
textRendererNeedsUpdate = true;
}
}
if (textRendererNeedsUpdate) {
// If textRendererNeedsUpdate then subtitle must be non-null.
checkNotNull(subtitle);
// textRendererNeedsUpdate is set and we're playing. Update the renderer.
updateOutput(subtitle.getCues(positionUs));
}
if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) {
return;
}
try {
while (!inputStreamEnded) {
@Nullable SubtitleInputBuffer nextInputBuffer = this.nextInputBuffer;
if (nextInputBuffer == null) {
nextInputBuffer = checkNotNull(decoder).dequeueInputBuffer();
if (nextInputBuffer == null) {
return;
}
this.nextInputBuffer = nextInputBuffer;
}
if (decoderReplacementState == REPLACEMENT_STATE_SIGNAL_END_OF_STREAM) {
nextInputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
checkNotNull(decoder).queueInputBuffer(nextInputBuffer);
this.nextInputBuffer = null;
decoderReplacementState = REPLACEMENT_STATE_WAIT_END_OF_STREAM;
return;
}
// Try and read the next subtitle from the source.
@ReadDataResult int result = readSource(formatHolder, nextInputBuffer, /* readFlags= */ 0);
if (result == C.RESULT_BUFFER_READ) {
if (nextInputBuffer.isEndOfStream()) {
inputStreamEnded = true;
waitingForKeyFrame = false;
} else {
@Nullable Format format = formatHolder.format;
if (format == null) {
// We haven't received a format yet.
return;
}
nextInputBuffer.subsampleOffsetUs = format.subsampleOffsetUs;
nextInputBuffer.flip();
waitingForKeyFrame &= !nextInputBuffer.isKeyFrame();
}
if (!waitingForKeyFrame) {
checkNotNull(decoder).queueInputBuffer(nextInputBuffer);
this.nextInputBuffer = null;
}
} else if (result == C.RESULT_NOTHING_READ) {
return;
}
}
} catch (SubtitleDecoderException e) {
handleDecoderError(e);
}
}
@Override
protected void onDisabled() {
streamFormat = null;
finalStreamEndPositionUs = C.TIME_UNSET;
clearOutput();
releaseDecoder();
}
@Override
public boolean isEnded() {
return outputStreamEnded;
}
@Override
public boolean isReady() {
// Don't block playback whilst subtitles are loading.
// Note: To change this behavior, it will be necessary to consider [Internal: b/12949941].
return true;
}
private void releaseBuffers() {
nextInputBuffer = null;
nextSubtitleEventIndex = C.INDEX_UNSET;
if (subtitle != null) {
subtitle.release();
subtitle = null;
}
if (nextSubtitle != null) {
nextSubtitle.release();
nextSubtitle = null;
}
}
private void releaseDecoder() {
releaseBuffers();
checkNotNull(decoder).release();
decoder = null;
decoderReplacementState = REPLACEMENT_STATE_NONE;
}
private void initDecoder() {
waitingForKeyFrame = true;
decoder = decoderFactory.createDecoder(checkNotNull(streamFormat));
}
private void replaceDecoder() {
releaseDecoder();
initDecoder();
}
private long getNextEventTime() {
if (nextSubtitleEventIndex == C.INDEX_UNSET) {
return Long.MAX_VALUE;
}
checkNotNull(subtitle);
return nextSubtitleEventIndex >= subtitle.getEventTimeCount()
? Long.MAX_VALUE
: subtitle.getEventTime(nextSubtitleEventIndex);
}
private void updateOutput(List<Cue> cues) {
if (outputHandler != null) {
outputHandler.obtainMessage(MSG_UPDATE_OUTPUT, cues).sendToTarget();
} else {
invokeUpdateOutputInternal(cues);
}
}
private void clearOutput() {
updateOutput(Collections.emptyList());
}
@SuppressWarnings("unchecked")
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_UPDATE_OUTPUT:
invokeUpdateOutputInternal((List<Cue>) msg.obj);
return true;
default:
throw new IllegalStateException();
}
}
private void invokeUpdateOutputInternal(List<Cue> cues) {
// See https://github.com/google/ExoPlayer/issues/7934
// SubripDecoder texts tend to be DIMEN_UNSET which pushes up the
// subs unlike WEBVTT which creates an inconsistency
List<Cue> fixedCues = cues.stream().map(
cue -> {
Cue.Builder builder = cue.buildUpon();
if (cue.line == DIMEN_UNSET)
builder.setLine(-1f, LINE_TYPE_NUMBER);
return builder.setSize(DIMEN_UNSET).build();
}
).collect(Collectors.toList());
output.onCues(fixedCues);
output.onCues(new CueGroup(fixedCues, 0L));
}
/**
* Called when {@link #decoder} throws an exception, so it can be logged and playback can
* continue.
*
* <p>Logs {@code e} and resets state to allow decoding the next sample.
*/
private void handleDecoderError(SubtitleDecoderException e) {
Log.e(TAG, "Subtitle decoding failed. streamFormat=" + streamFormat, e);
clearOutput();
replaceDecoder();
}
}

View File

@ -1,382 +0,0 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lagradost.cloudstream3.ui.player
import android.os.Handler
import android.os.Looper
import android.os.Message
import androidx.annotation.IntDef
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.source.SampleStream.ReadDataResult
import com.google.android.exoplayer2.text.*
import com.google.android.exoplayer2.text.Cue.DIMEN_UNSET
import com.google.android.exoplayer2.text.Cue.LINE_TYPE_NUMBER
import com.google.android.exoplayer2.util.Assertions
import com.google.android.exoplayer2.util.Log
import com.google.android.exoplayer2.util.MimeTypes
import com.google.android.exoplayer2.util.Util
/**
* A renderer for text.
*
*
* [Subtitle]s are decoded from sample data using [SubtitleDecoder] instances
* obtained from a [SubtitleDecoderFactory]. The actual rendering of the subtitle [Cue]s
* is delegated to a [TextOutput].
*/
open class NonFinalTextRenderer @JvmOverloads constructor(
output: TextOutput?,
outputLooper: Looper?,
private val decoderFactory: SubtitleDecoderFactory = SubtitleDecoderFactory.DEFAULT
) :
BaseRenderer(C.TRACK_TYPE_TEXT), Handler.Callback {
@MustBeDocumented
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
@IntDef(
REPLACEMENT_STATE_NONE,
REPLACEMENT_STATE_SIGNAL_END_OF_STREAM,
REPLACEMENT_STATE_WAIT_END_OF_STREAM
)
private annotation class ReplacementState
private val outputHandler: Handler? = if (outputLooper == null) null else Util.createHandler(
outputLooper, /* callback= */
this
)
private val output: TextOutput = Assertions.checkNotNull(output)
private val formatHold: FormatHolder = FormatHolder()
private var inputStreamEnded = false
private var outputStreamEnded = false
private var waitingForKeyFrame = false
@ReplacementState
private var decoderReplacementState = 0
private var streamFormat: Format? = null
private var decoder: SubtitleDecoder? = null
private var nextInputBuffer: SubtitleInputBuffer? = null
private var subtitle: SubtitleOutputBuffer? = null
private var nextSubtitle: SubtitleOutputBuffer? = null
private var nextSubtitleEventIndex = 0
private var finalStreamEndPositionUs: Long
override fun getName(): String {
return TAG
}
@RendererCapabilities.Capabilities
override fun supportsFormat(format: Format): Int {
return if (decoderFactory.supportsFormat(format)) {
RendererCapabilities.create(
if (format.cryptoType == C.CRYPTO_TYPE_NONE) C.FORMAT_HANDLED else C.FORMAT_UNSUPPORTED_DRM
)
} else if (MimeTypes.isText(format.sampleMimeType)) {
RendererCapabilities.create(C.FORMAT_UNSUPPORTED_SUBTYPE)
} else {
RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE)
}
}
/**
* Sets the position at which to stop rendering the current stream.
*
*
* Must be called after [.setCurrentStreamFinal].
*
* @param streamEndPositionUs The position to stop rendering at or [C.LENGTH_UNSET] to
* render until the end of the current stream.
*/
override fun onStreamChanged(formats: Array<Format>, startPositionUs: Long, offsetUs: Long) {
streamFormat = formats[0]
if (decoder != null) {
decoderReplacementState = REPLACEMENT_STATE_SIGNAL_END_OF_STREAM
} else {
initDecoder()
}
}
override fun onPositionReset(positionUs: Long, joining: Boolean) {
clearOutput()
inputStreamEnded = false
outputStreamEnded = false
finalStreamEndPositionUs = C.TIME_UNSET
if (decoderReplacementState != REPLACEMENT_STATE_NONE) {
replaceDecoder()
} else {
releaseBuffers()
Assertions.checkNotNull(decoder).flush()
}
}
override fun render(positionUs: Long, elapsedRealtimeUs: Long) {
if (isCurrentStreamFinal
&& finalStreamEndPositionUs != C.TIME_UNSET && positionUs >= finalStreamEndPositionUs
) {
releaseBuffers()
outputStreamEnded = true
}
if (outputStreamEnded) {
return
}
if (nextSubtitle == null) {
Assertions.checkNotNull(decoder).setPositionUs(positionUs)
nextSubtitle = try {
Assertions.checkNotNull(decoder).dequeueOutputBuffer()
} catch (e: SubtitleDecoderException) {
handleDecoderError(e)
return
}
}
if (state != STATE_STARTED) {
return
}
var textRendererNeedsUpdate = false
if (subtitle != null) {
// We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we
// advance to the next event.
var subtitleNextEventTimeUs = nextEventTime
while (subtitleNextEventTimeUs <= positionUs) {
nextSubtitleEventIndex++
subtitleNextEventTimeUs = nextEventTime
textRendererNeedsUpdate = true
}
}
if (nextSubtitle != null) {
val nextSubtitle = nextSubtitle
if (nextSubtitle!!.isEndOfStream) {
if (!textRendererNeedsUpdate && nextEventTime == Long.MAX_VALUE) {
if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) {
replaceDecoder()
} else {
releaseBuffers()
outputStreamEnded = true
}
}
} else if (nextSubtitle.timeUs <= positionUs) {
// Advance to the next subtitle. Sync the next event index and trigger an update.
if (subtitle != null) {
subtitle!!.release()
}
nextSubtitleEventIndex = nextSubtitle.getNextEventTimeIndex(positionUs)
subtitle = nextSubtitle
this.nextSubtitle = null
textRendererNeedsUpdate = true
}
}
if (textRendererNeedsUpdate) {
// If textRendererNeedsUpdate then subtitle must be non-null.
Assertions.checkNotNull(subtitle)
// textRendererNeedsUpdate is set and we're playing. Update the renderer.
updateOutput(subtitle!!.getCues(positionUs))
}
if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) {
return
}
try {
while (!inputStreamEnded) {
var nextInputBuffer = nextInputBuffer
if (nextInputBuffer == null) {
nextInputBuffer = Assertions.checkNotNull(decoder).dequeueInputBuffer()
if (nextInputBuffer == null) {
return
}
this.nextInputBuffer = nextInputBuffer
}
if (decoderReplacementState == REPLACEMENT_STATE_SIGNAL_END_OF_STREAM) {
nextInputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM)
Assertions.checkNotNull(decoder).queueInputBuffer(nextInputBuffer)
this.nextInputBuffer = null
decoderReplacementState = REPLACEMENT_STATE_WAIT_END_OF_STREAM
return
}
// Try and read the next subtitle from the source.
@ReadDataResult val result =
readSource(formatHold, nextInputBuffer, /* readFlags= */0)
if (result == C.RESULT_BUFFER_READ) {
if (nextInputBuffer.isEndOfStream) {
inputStreamEnded = true
waitingForKeyFrame = false
} else {
val format = formatHold.format
?: // We haven't received a format yet.
return
nextInputBuffer.subsampleOffsetUs = format.subsampleOffsetUs
nextInputBuffer.flip()
waitingForKeyFrame = waitingForKeyFrame and !nextInputBuffer.isKeyFrame
}
if (!waitingForKeyFrame) {
Assertions.checkNotNull(decoder).queueInputBuffer(nextInputBuffer)
this.nextInputBuffer = null
}
} else if (result == C.RESULT_NOTHING_READ) {
return
}
}
} catch (e: SubtitleDecoderException) {
handleDecoderError(e)
}
}
override fun onDisabled() {
streamFormat = null
finalStreamEndPositionUs = C.TIME_UNSET
clearOutput()
releaseDecoder()
}
override fun isEnded(): Boolean {
return outputStreamEnded
}
override fun isReady(): Boolean {
// Don't block playback whilst subtitles are loading.
// Note: To change this behavior, it will be necessary to consider [Internal: b/12949941].
return true
}
private fun releaseBuffers() {
nextInputBuffer = null
nextSubtitleEventIndex = C.INDEX_UNSET
if (subtitle != null) {
subtitle!!.release()
subtitle = null
}
if (nextSubtitle != null) {
nextSubtitle!!.release()
nextSubtitle = null
}
}
private fun releaseDecoder() {
releaseBuffers()
Assertions.checkNotNull(decoder).release()
decoder = null
decoderReplacementState = REPLACEMENT_STATE_NONE
}
private fun initDecoder() {
waitingForKeyFrame = true
decoder = decoderFactory.createDecoder(Assertions.checkNotNull(streamFormat))
}
private fun replaceDecoder() {
releaseDecoder()
initDecoder()
}
private val nextEventTime: Long
get() {
if (nextSubtitleEventIndex == C.INDEX_UNSET) {
return Long.MAX_VALUE
}
Assertions.checkNotNull(subtitle)
return if (nextSubtitleEventIndex >= subtitle!!.eventTimeCount) Long.MAX_VALUE else subtitle!!.getEventTime(
nextSubtitleEventIndex
)
}
private fun updateOutput(cues: List<Cue>) {
if (outputHandler != null) {
outputHandler.obtainMessage(MSG_UPDATE_OUTPUT, cues).sendToTarget()
} else {
invokeUpdateOutputInternal(cues)
}
}
private fun clearOutput() {
updateOutput(emptyList())
}
override fun handleMessage(msg: Message): Boolean {
return when (msg.what) {
MSG_UPDATE_OUTPUT -> {
invokeUpdateOutputInternal(msg.obj as List<Cue>)
true
}
else -> throw IllegalStateException()
}
}
private fun invokeUpdateOutputInternal(cues: List<Cue>) {
output.onCues(cues.map { cue ->
val builder = cue.buildUpon()
// See https://github.com/google/ExoPlayer/issues/7934
// SubripDecoder texts tend to be DIMEN_UNSET which pushes up the
// subs unlike WEBVTT which creates an inconsistency
if (cue.line == DIMEN_UNSET)
builder.setLine(-1f, LINE_TYPE_NUMBER)
// this fixes https://github.com/LagradOst/CloudStream-3/issues/717
builder.setSize(DIMEN_UNSET).build()
})
}
/**
* Called when [.decoder] throws an exception, so it can be logged and playback can
* continue.
*
*
* Logs `e` and resets state to allow decoding the next sample.
*/
private fun handleDecoderError(e: SubtitleDecoderException) {
Log.e(
TAG,
"Subtitle decoding failed. streamFormat=$streamFormat", e
)
clearOutput()
replaceDecoder()
}
companion object {
private const val TAG = "TextRenderer"
/** The decoder does not need to be replaced. */
private const val REPLACEMENT_STATE_NONE = 0
/**
* The decoder needs to be replaced, but we haven't yet signaled an end of stream to the existing
* decoder. We need to do so in order to ensure that it outputs any remaining buffers before we
* release it.
*/
private const val REPLACEMENT_STATE_SIGNAL_END_OF_STREAM = 1
/**
* The decoder needs to be replaced, and we've signaled an end of stream to the existing decoder.
* We're waiting for the decoder to output an end of stream signal to indicate that it has output
* any remaining buffers before we release it.
*/
private const val REPLACEMENT_STATE_WAIT_END_OF_STREAM = 2
private const val MSG_UPDATE_OUTPUT = 0
}
/**
* @param output The output.
* @param outputLooper The looper associated with the thread on which the output should be called.
* If the output makes use of standard Android UI components, then this should normally be the
* looper associated with the application's main thread, which can be obtained using [ ][android.app.Activity.getMainLooper]. Null may be passed if the output should be called
* directly on the player's internal rendering thread.
* @param decoderFactory A factory from which to obtain [SubtitleDecoder] instances.
*/
/**
* @param output The output.
* @param outputLooper The looper associated with the thread on which the output should be called.
* If the output makes use of standard Android UI components, then this should normally be the
* looper associated with the application's main thread, which can be obtained using [ ][android.app.Activity.getMainLooper]. Null may be passed if the output should be called
* directly on the player's internal rendering thread.
*/
init {
finalStreamEndPositionUs = C.TIME_UNSET
}
}

View File

@ -9,10 +9,12 @@ import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.launchSafe
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.EpisodeSkip
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorUri
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
class PlayerGeneratorViewModel : ViewModel() {
companion object {
@ -30,6 +32,9 @@ class PlayerGeneratorViewModel : ViewModel() {
private val _loadingLinks = MutableLiveData<Resource<Boolean?>>()
val loadingLinks: LiveData<Resource<Boolean?>> = _loadingLinks
private val _currentStamps = MutableLiveData<List<EpisodeSkip.SkipStamp>>(emptyList())
val currentStamps: LiveData<List<EpisodeSkip.SkipStamp>> = _currentStamps
fun getId(): Int? {
return generator?.getCurrentId()
}
@ -108,15 +113,37 @@ class PlayerGeneratorViewModel : ViewModel() {
// Do not post if there's nothing new
// Posting will refresh subtitles which will in turn
// make the subs to english if previously unselected
if (allSubs != currentSubs)
if (allSubs != currentSubs) {
_currentSubs.postValue(allSubs)
}
}
private var currentJob: Job? = null
private var currentStampJob: Job? = null
fun loadStamps(duration: Long) {
//currentStampJob?.cancel()
currentStampJob = ioSafe {
val meta = generator?.getCurrent()
val page = (generator as? RepoLinkGenerator?)?.page
if (page != null && meta is ResultEpisode) {
_currentStamps.postValue(listOf())
_currentStamps.postValue(
EpisodeSkip.getStamps(
page,
meta,
duration,
hasNextEpisode() ?: false
)
)
}
}
}
fun loadLinks(clearCache: Boolean = false, isCasting: Boolean = false) {
Log.i(TAG, "loadLinks")
currentJob?.cancel()
currentJob = viewModelScope.launchSafe {
val currentLinks = mutableSetOf<Pair<ExtractorLink?, ExtractorUri?>>()
val currentSubs = mutableSetOf<SubtitleData>()
@ -138,9 +165,11 @@ class PlayerGeneratorViewModel : ViewModel() {
}
_loadingLinks.postValue(loadingState)
_currentLinks.postValue(currentLinks)
_currentSubs.postValue(currentSubs)
_currentSubs.postValue(
currentSubs.union(_currentSubs.value ?: emptySet())
)
}
}
}

View File

@ -28,7 +28,7 @@ enum class SubtitleOrigin {
/**
* @param name To be displayed in the player
* @param url Url for the subtitle, when EMBEDDED_IN_VIDEO this variable is used as the real backend language
* @param url Url for the subtitle, when EMBEDDED_IN_VIDEO this variable is used as the real backend id
* @param headers if empty it will use the base onlineDataSource headers else only the specified headers
* */
data class SubtitleData(
@ -37,7 +37,13 @@ data class SubtitleData(
val origin: SubtitleOrigin,
val mimeType: String,
val headers: Map<String, String>
)
) {
/** Internal ID for exoplayer, unique for each link*/
fun getId(): String {
return if (origin == SubtitleOrigin.EMBEDDED_IN_VIDEO) url
else "$url|$name"
}
}
class PlayerSubtitleHelper {
private var activeSubtitles: Set<SubtitleData> = emptySet()
@ -79,11 +85,11 @@ class PlayerSubtitleHelper {
}
}
fun subtitleStatus(sub : SubtitleData?): SubtitleStatus {
if(activeSubtitles.contains(sub)) {
fun subtitleStatus(sub: SubtitleData?): SubtitleStatus {
if (activeSubtitles.contains(sub)) {
return SubtitleStatus.IS_ACTIVE
}
if(allSubtitles.contains(sub)) {
if (allSubtitles.contains(sub)) {
return SubtitleStatus.REQUIRES_RELOAD
}
return SubtitleStatus.NOT_FOUND
@ -95,7 +101,7 @@ class PlayerSubtitleHelper {
regexSubtitlesToRemoveCaptions = style.removeCaptions
subtitleView?.context?.let { ctx ->
subStyle = style
Log.i(TAG,"SET STYLE = $style")
Log.i(TAG, "SET STYLE = $style")
subtitleView?.setStyle(ctx.fromSaveToStyle(style))
subtitleView?.translationY = -style.elevation.toPx.toFloat()
val size = style.fixedTextSize

View File

@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.ui.player
import android.util.Log
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.ExtractorLink
@ -11,7 +12,8 @@ import kotlin.math.min
class RepoLinkGenerator(
private val episodes: List<ResultEpisode>,
private var currentIndex: Int = 0
private var currentIndex: Int = 0,
val page: LoadResponse? = null,
) : IGenerator {
companion object {
const val TAG = "RepoLink"

View File

@ -58,6 +58,7 @@ const val ACTION_DOWNLOAD_EPISODE_SUBTITLE_MIRROR = 14
const val ACTION_PLAY_EPISODE_IN_WEB_VIDEO = 16
const val ACTION_PLAY_EPISODE_IN_MPV = 17
const val ACTION_MARK_AS_WATCHED = 18
data class EpisodeClickEvent(val action: Int, val data: ResultEpisode)

View File

@ -22,7 +22,8 @@ import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
import com.discord.panels.OverlappingPanelsLayout
import com.google.android.material.button.MaterialButton
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipDrawable
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
@ -96,10 +97,10 @@ import kotlinx.android.synthetic.main.fragment_result.result_vpn
import kotlinx.android.synthetic.main.fragment_result_swipe.*
import kotlinx.android.synthetic.main.fragment_result_tv.*
import kotlinx.android.synthetic.main.result_sync.*
import kotlinx.android.synthetic.main.trailer_custom_layout.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
const val START_ACTION_RESUME_LATEST = 1
const val START_ACTION_LOAD_EP = 2
@ -490,11 +491,10 @@ open class ResultFragment : ResultTrailerPlayer() {
return StoredData(url, apiName, showFillers, dubStatus, start, playerAction)
}
private fun reloadViewModel(success: Boolean = false) {
if (!viewModel.hasLoaded()) {
private fun reloadViewModel(forceReload: Boolean) {
if (!viewModel.hasLoaded() || forceReload) {
val storedData = getStoredData(activity ?: context ?: return) ?: return
//viewModel.clear()
viewModel.load(
activity,
storedData.url ?: return,
@ -838,6 +838,8 @@ open class ResultFragment : ResultTrailerPlayer() {
result_next_airing.setText(d.nextAiringEpisode)
result_next_airing_time.setText(d.nextAiringDate)
result_poster.setImage(d.posterImage)
result_poster_background.setImage(d.posterBackgroundImage)
//result_trailer_thumbnail.setImage(d.posterBackgroundImage, fadeIn = false)
if (d.posterImage != null && !isTrueTvSettings())
result_poster_holder?.setOnClickListener {
@ -926,18 +928,38 @@ open class ResultFragment : ResultTrailerPlayer() {
val tags = d.tags
result_tag_holder?.isVisible = tags.isNotEmpty()
if (tags.isNotEmpty()) {
//result_tag_holder?.visibility = VISIBLE
val isOnTv = isTrueTvSettings()
for ((index, tag) in tags.withIndex()) {
result_tag?.apply {
tags.forEach { tag ->
val chip = Chip(context)
val chipDrawable = ChipDrawable.createFromAttributes(
context,
null,
0,
R.style.ChipFilled
)
chip.setChipDrawable(chipDrawable)
chip.text = tag
chip.isChecked = false
chip.isCheckable = false
chip.isFocusable = false
chip.isClickable = false
addView(chip)
}
}
// if (tags.isNotEmpty()) {
//result_tag_holder?.visibility = VISIBLE
//val isOnTv = isTrueTvSettings()
/*for ((index, tag) in tags.withIndex()) {
val viewBtt = layoutInflater.inflate(R.layout.result_tag, null)
val btt = viewBtt.findViewById<MaterialButton>(R.id.result_tag_card)
btt.text = tag
btt.isFocusable = !isOnTv
btt.isClickable = !isOnTv
result_tag?.addView(viewBtt, index)
}
}
}*/
//}
}
is Resource.Failure -> {
result_error_text.text = storedData?.url?.plus("\n") + data.errorString

View File

@ -5,6 +5,9 @@ import android.graphics.Rect
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.animation.DecelerateInterpolator
import android.widget.Toast
import androidx.core.view.isGone
import androidx.core.view.isVisible
@ -20,7 +23,6 @@ import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
import com.lagradost.cloudstream3.mvvm.Some
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.player.CSPlayerEvent
import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchHelper
@ -29,16 +31,25 @@ 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
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
import kotlinx.android.synthetic.main.fragment_result.*
import kotlinx.android.synthetic.main.fragment_result.result_cast_items
import kotlinx.android.synthetic.main.fragment_result.result_episodes_text
import kotlinx.android.synthetic.main.fragment_result.result_resume_parent
import kotlinx.android.synthetic.main.fragment_result.result_scroll
import kotlinx.android.synthetic.main.fragment_result.result_smallscreen_holder
import kotlinx.android.synthetic.main.fragment_result_swipe.*
import kotlinx.android.synthetic.main.fragment_result_swipe.result_back
import kotlinx.android.synthetic.main.fragment_result_tv.*
import kotlinx.android.synthetic.main.fragment_trailer.*
import kotlinx.android.synthetic.main.result_recommendations.*
import kotlinx.android.synthetic.main.result_recommendations.result_recommendations
import kotlinx.android.synthetic.main.trailer_custom_layout.*
class ResultFragmentPhone : ResultFragment() {
var currentTrailers: List<ExtractorLink> = emptyList()
var currentTrailerIndex = 0
@ -82,8 +93,36 @@ class ResultFragmentPhone : ResultFragment() {
} ?: run {
false
}
//result_trailer_thumbnail?.setImageBitmap(result_poster_background?.drawable?.toBitmap())
result_trailer_loading?.isVisible = isSuccess
result_smallscreen_holder?.isVisible = !isSuccess && !isFullScreenPlayer
val turnVis = !isSuccess && !isFullScreenPlayer
result_smallscreen_holder?.isVisible = turnVis
result_poster_background_holder?.apply {
val fadeIn: Animation = AlphaAnimation(alpha, if (turnVis) 1.0f else 0.0f).apply {
interpolator = DecelerateInterpolator()
duration = 200
fillAfter = true
}
clearAnimation()
startAnimation(fadeIn)
}
//player_view?.apply {
//alpha = 0.0f
//ObjectAnimator.ofFloat(player_view, "alpha", 1f).apply {
// duration = 200
// start()
//}
//val fadeIn: Animation = AlphaAnimation(0.0f, 1f).apply {
// interpolator = DecelerateInterpolator()
// duration = 2000
// fillAfter = true
//}
//startAnimation(fadeIn)
// }
// We don't want the trailer to be focusable if it's not visible
result_smallscreen_holder?.descendantFocusability = if (isSuccess) {
@ -127,6 +166,8 @@ class ResultFragmentPhone : ResultFragment() {
down.nextFocusUpId = upper.id
}
var selectSeason: String? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val apiName = arguments?.getString(API_NAME_BUNDLE) ?: return
@ -297,12 +338,14 @@ class ResultFragmentPhone : ResultFragment() {
observe(viewModel.selectedSeason) { text ->
result_season_button.setText(text)
selectSeason =
(if (text is Some.Success) text.value else null)?.asStringNull(result_season_button?.context)
// If the season button is visible the result season button will be next focus down
if (result_season_button?.isVisible == true)
if (result_resume_parent?.isVisible == true)
setFocusUpAndDown(result_resume_series_button, result_season_button)
//else
// setFocusUpAndDown(result_bookmark_button, result_season_button)
//else
// setFocusUpAndDown(result_bookmark_button, result_season_button)
}
observe(viewModel.selectedDubStatus) { status ->
@ -312,8 +355,8 @@ class ResultFragmentPhone : ResultFragment() {
if (result_season_button?.isVisible != true && result_episode_select?.isVisible != true) {
if (result_resume_parent?.isVisible == true)
setFocusUpAndDown(result_resume_series_button, result_dub_select)
//else
// setFocusUpAndDown(result_bookmark_button, result_dub_select)
//else
// setFocusUpAndDown(result_bookmark_button, result_dub_select)
}
}
observe(viewModel.selectedRange) { range ->
@ -366,17 +409,28 @@ class ResultFragmentPhone : ResultFragment() {
observe(viewModel.seasonSelections) { seasonList ->
result_season_button?.setOnClickListener { view ->
view?.context?.let { ctx ->
val names = seasonList
.mapNotNull { (text, r) ->
r to (text?.asStringNull(ctx) ?: return@mapNotNull null)
}
view.popupMenuNoIconsAndNoStringRes(names.mapIndexed { index, (_, name) ->
index to name
}) {
activity?.showDialog(
names.map { it.second },
names.indexOfFirst { it.second == selectSeason },
"",
false,
{}) { itemId ->
viewModel.changeSeason(names[itemId].first)
}
//view.popupMenuNoIconsAndNoStringRes(names.mapIndexed { index, (_, name) ->
// index to name
//}) {
// viewModel.changeSeason(names[itemId].first)
//}
}
}
}

View File

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.ui.result
import android.animation.ValueAnimator
import android.content.Context
import android.content.res.Configuration
import android.graphics.Rect
@ -7,16 +8,21 @@ import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.view.isGone
import androidx.core.view.isVisible
import com.discord.panels.PanelsChildGestureRegionObserver
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ui.player.CSPlayerEvent
import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.utils.IOnBackPressed
import kotlinx.android.synthetic.main.fragment_result.*
import kotlinx.android.synthetic.main.fragment_result.result_smallscreen_holder
import kotlinx.android.synthetic.main.fragment_result_swipe.*
import kotlinx.android.synthetic.main.fragment_result_tv.*
import kotlinx.android.synthetic.main.fragment_trailer.*
import kotlinx.android.synthetic.main.trailer_custom_layout.*
open class ResultTrailerPlayer : com.lagradost.cloudstream3.ui.player.FullScreenPlayer(),
PanelsChildGestureRegionObserver.GestureRegionsListener, IOnBackPressed {
@ -58,14 +64,43 @@ open class ResultTrailerPlayer : com.lagradost.cloudstream3.ui.player.FullScreen
result_smallscreen_holder?.isVisible = !isFullScreenPlayer
result_fullscreen_holder?.isVisible = isFullScreenPlayer
val to = sw * h / w
player_background?.apply {
isVisible = true
layoutParams =
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
if (isFullScreenPlayer) FrameLayout.LayoutParams.MATCH_PARENT else sw * h / w
if (isFullScreenPlayer) FrameLayout.LayoutParams.MATCH_PARENT else to
)
}
player_intro_play?.apply {
layoutParams =
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
result_top_holder?.measuredHeight ?: FrameLayout.LayoutParams.MATCH_PARENT
)
}
if (player_intro_play?.isGone == true) {
result_top_holder?.apply {
val anim = ValueAnimator.ofInt(
measuredHeight,
if (isFullScreenPlayer) ViewGroup.LayoutParams.MATCH_PARENT else to
)
anim.addUpdateListener { valueAnimator ->
val `val` = valueAnimator.animatedValue as Int
val layoutParams: ViewGroup.LayoutParams =
layoutParams
layoutParams.height = `val`
setLayoutParams(layoutParams)
}
anim.duration = 200
anim.start()
}
}
}
}
@ -77,7 +112,12 @@ open class ResultTrailerPlayer : com.lagradost.cloudstream3.ui.player.FullScreen
override fun showMirrorsDialogue() {}
override fun showTracksDialogue() {}
override fun openOnlineSubPicker(context: Context, imdbId: Long?, dismissCallback: () -> Unit) {}
override fun openOnlineSubPicker(
context: Context,
imdbId: Long?,
dismissCallback: () -> Unit
) {
}
override fun subtitlesChanged() {}
@ -122,6 +162,13 @@ open class ResultTrailerPlayer : com.lagradost.cloudstream3.ui.player.FullScreen
}
updateFullscreen(isFullScreenPlayer)
uiReset()
player_intro_play?.setOnClickListener {
player_intro_play?.isGone = true
player.handleEvent(CSPlayerEvent.Play)
updateUIVisibility()
fixPlayerSize()
}
}
override fun onBackPressed(): Boolean {

View File

@ -88,6 +88,7 @@ data class ResultData(
var syncData: Map<String, String>,
val posterImage: UiImage?,
val posterBackgroundImage: UiImage?,
val plotText: UiText,
val apiName: UiText,
val ratingText: UiText?,
@ -170,6 +171,9 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData {
posterImage = img(
posterUrl, posterHeaders
) ?: img(R.drawable.default_cover),
posterBackgroundImage = img(
backgroundPosterUrl ?: posterUrl, posterHeaders
) ?: img(R.drawable.default_cover),
titleText = txt(name),
url = url,
tags = tags ?: emptyList(),
@ -412,7 +416,7 @@ class ResultViewModel2 : ViewModel() {
return this?.firstOrNull { it.season == season }
}
fun updateWatchStatus(currentResponse : LoadResponse, status: WatchType) {
fun updateWatchStatus(currentResponse: LoadResponse, status: WatchType) {
val currentId = currentResponse.getId()
val resultPage = currentResponse
@ -789,7 +793,7 @@ class ResultViewModel2 : ViewModel() {
fun updateWatchStatus(status: WatchType) {
updateWatchStatus(currentResponse ?: return,status)
updateWatchStatus(currentResponse ?: return, status)
_watchStatus.postValue(status)
}
@ -1142,6 +1146,7 @@ class ResultViewModel2 : ViewModel() {
txt(R.string.episode_action_download_mirror) to ACTION_DOWNLOAD_MIRROR,
txt(R.string.episode_action_download_subtitle) to ACTION_DOWNLOAD_EPISODE_SUBTITLE_MIRROR,
txt(R.string.episode_action_reload_links) to ACTION_RELOAD_EPISODE,
// txt(R.string.action_mark_as_watched) to ACTION_MARK_AS_WATCHED,
)
)
@ -1361,7 +1366,7 @@ class ResultViewModel2 : ViewModel() {
R.id.global_to_navigation_player,
GeneratorPlayer.newInstance(
generator?.also {
it.getAll() // I know kinda shit to itterate all, but it is 100% sure to work
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)
@ -1372,6 +1377,10 @@ class ResultViewModel2 : ViewModel() {
)
)
}
ACTION_MARK_AS_WATCHED -> {
// TODO FIX
// DataStoreHelper.setViewPos(click.data.id, 1, 1)
}
}
}
@ -1578,7 +1587,6 @@ class ResultViewModel2 : ViewModel() {
return
}
val episodes = currentEpisodes[indexer]
val ranges = currentRanges[indexer]
if (ranges?.contains(range) != true) {
@ -1590,7 +1598,6 @@ class ResultViewModel2 : ViewModel() {
}
}
val size = episodes?.size
val isMovie = currentResponse?.isMovie() == true
currentIndex = indexer
currentRange = range
@ -1600,6 +1607,7 @@ class ResultViewModel2 : ViewModel() {
text to r
} ?: emptyList())
val size = currentEpisodes[indexer]?.size
_episodesCountText.postValue(
some(
if (isMovie) null else
@ -1677,11 +1685,14 @@ class ResultViewModel2 : ViewModel() {
preferDubStatus = indexer.dubStatus
generator = if (isMovie) {
getMovie()?.let { RepoLinkGenerator(listOf(it)) }
getMovie()?.let { RepoLinkGenerator(listOf(it), page = currentResponse) }
} else {
episodes?.let { list ->
RepoLinkGenerator(list)
}
val episodes = currentEpisodes.filter { it.key.dubStatus == indexer.dubStatus }
.toList()
.sortedBy { it.first.season }
.flatMap { it.second }
RepoLinkGenerator(episodes, page = currentResponse)
}
if (isMovie) {
@ -1971,42 +1982,42 @@ class ResultViewModel2 : ViewModel() {
limit: Int = 0
): List<ExtractedTrailerData> =
coroutineScope {
var currentCount = 0
return@coroutineScope loadResponse.trailers.apmap { trailerData ->
try {
val links = arrayListOf<ExtractorLink>()
val subs = arrayListOf<SubtitleFile>()
if (!loadExtractor(
trailerData.extractorUrl,
trailerData.referer,
{ subs.add(it) },
{ links.add(it) }) && trailerData.raw
) {
arrayListOf(
ExtractorLink(
"",
"Trailer",
val returnlist = ArrayList<ExtractedTrailerData>()
loadResponse.trailers.windowed(limit, limit, true).takeWhile { list ->
list.amap { trailerData ->
try {
val links = arrayListOf<ExtractorLink>()
val subs = arrayListOf<SubtitleFile>()
if (!loadExtractor(
trailerData.extractorUrl,
trailerData.referer ?: "",
Qualities.Unknown.value,
trailerData.extractorUrl.contains(".m3u8")
)
) to arrayListOf()
} else {
links to subs
}.also { (extractor, _) ->
if (extractor.isNotEmpty() && limit != 0) {
currentCount++
if (currentCount >= limit) {
cancel()
}
trailerData.referer,
{ subs.add(it) },
{ links.add(it) }) && trailerData.raw
) {
arrayListOf(
ExtractorLink(
"",
"Trailer",
trailerData.extractorUrl,
trailerData.referer ?: "",
Qualities.Unknown.value,
trailerData.extractorUrl.contains(".m3u8")
)
) to arrayListOf()
} else {
links to subs
}
} catch (e: Throwable) {
logError(e)
null
}
} catch (e: Throwable) {
logError(e)
null
}.filterNotNull().map { (links, subs) -> ExtractedTrailerData(links, subs) }.let {
returnlist.addAll(it)
}
}.filterNotNull().map { (links, subs) -> ExtractedTrailerData(links, subs) }
returnlist.size < limit
}
return@coroutineScope returnlist
}

View File

@ -4,7 +4,7 @@ import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
@ -197,7 +197,7 @@ class SyncViewModel : ViewModel() {
/// modifies the current sync data, return null if you don't want to change it
private fun modifyData(update: ((SyncAPI.SyncStatus) -> (SyncAPI.SyncStatus?))) =
ioSafe {
syncs.apmap { (prefix, id) ->
syncs.amap { (prefix, id) ->
repos.firstOrNull { it.idPrefix == prefix }?.let { repo ->
if (repo.hasAccount()) {
val result = repo.getStatus(id)

View File

@ -70,9 +70,9 @@ sealed class UiImage {
data class Drawable(@DrawableRes val resId: Int) : UiImage()
}
fun ImageView?.setImage(value: UiImage?) {
fun ImageView?.setImage(value: UiImage?, fadeIn: Boolean = true) {
when (value) {
is UiImage.Image -> setImageImage(value)
is UiImage.Image -> setImageImage(value,fadeIn)
is UiImage.Drawable -> setImageDrawable(value)
null -> {
this?.isVisible = false
@ -80,9 +80,9 @@ fun ImageView?.setImage(value: UiImage?) {
}
}
fun ImageView?.setImageImage(value: UiImage.Image) {
fun ImageView?.setImageImage(value: UiImage.Image, fadeIn: Boolean = true) {
if (this == null) return
this.isVisible = setImage(value.url, value.headers, value.errorDrawable)
this.isVisible = setImage(value.url, value.headers, value.errorDrawable, fadeIn)
}
fun ImageView?.setImageDrawable(value: UiImage.Drawable) {

View File

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.ui.search
import android.content.DialogInterface
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
@ -10,6 +11,7 @@ import android.widget.AbsListView
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.ListView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
@ -28,6 +30,7 @@ import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
import com.lagradost.cloudstream3.APIHolder.getApiSettings
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.mvvm.Resource
@ -70,6 +73,14 @@ class SearchFragment : Fragment() {
}
}
}
const val SEARCH_QUERY = "search_query"
fun newInstance(query: String): Bundle {
return Bundle().apply {
putString(SEARCH_QUERY, query)
}
}
}
private val searchViewModel: SearchViewModel by activityViewModels()
@ -117,18 +128,37 @@ class SearchFragment : Fragment() {
var selectedSearchTypes = mutableListOf<TvType>()
var selectedApis = mutableSetOf<String>()
/**
* Will filter all providers by preferred media and selectedSearchTypes.
* If that results in no available providers then only filter
* providers by preferred media
**/
fun search(query: String?) {
if (query == null) return
context?.getApiSettings()?.let { settings ->
context?.let { ctx ->
val default = enumValues<TvType>().sorted().filter { it != TvType.NSFW }
.map { it.ordinal.toString() }.toSet()
val preferredTypes = (PreferenceManager.getDefaultSharedPreferences(ctx)
.getStringSet(this.getString(R.string.prefer_media_type_key), default)
?.ifEmpty { default } ?: default)
.mapNotNull { it.toIntOrNull() ?: return@mapNotNull null }
val settings = ctx.getApiSettings()
val notFilteredBySelectedTypes = selectedApis.filter { name ->
settings.contains(name)
}.map { name ->
name to getApiFromNameNull(name)?.supportedTypes
}.filter { (_, types) ->
types?.any { preferredTypes.contains(it.ordinal) } == true
}
searchViewModel.searchAndCancel(
query = query,
providersActive = selectedApis.filter { name ->
settings.contains(name) && getApiFromNameNull(name)?.supportedTypes?.any {
selectedSearchTypes.contains(
it
)
} == true
}.toSet()
providersActive = notFilteredBySelectedTypes.filter { (_, types) ->
types?.any { selectedSearchTypes.contains(it) } == true
}.ifEmpty { notFilteredBySelectedTypes }.map { it.first }.toSet()
)
}
}
@ -203,7 +233,9 @@ class SearchFragment : Fragment() {
builder.setContentView(R.layout.home_select_mainpage)
builder.show()
builder.let { dialog ->
val isMultiLang = ctx.getApiProviderLangSettings().size > 1
val isMultiLang = ctx.getApiProviderLangSettings().let { set ->
set.size > 1 || set.contains(AllLanguagesName)
}
val cancelBtt = dialog.findViewById<MaterialButton>(R.id.cancel_btt)
val applyBtt = dialog.findViewById<MaterialButton>(R.id.apply_btt)
@ -227,7 +259,7 @@ class SearchFragment : Fragment() {
}
fun updateList(types: List<TvType>) {
setKey(SEARCH_PREF_TAGS, types.map {it.name})
setKey(SEARCH_PREF_TAGS, types.map { it.name })
arrayAdapter.clear()
currentValidApis = validAPIs.filter { api ->
@ -323,7 +355,7 @@ class SearchFragment : Fragment() {
searchViewModel.updateHistory()
}
search_history_recycler?.isVisible = showHistory
search_history_holder?.isVisible = showHistory
search_master_recycler?.isVisible = !showHistory && isAdvancedSearch
search_autofit_results?.isVisible = !showHistory && !isAdvancedSearch
@ -332,7 +364,41 @@ class SearchFragment : Fragment() {
}
})
search_clear_call_history?.setOnClickListener {
activity?.let { ctx ->
val builder: AlertDialog.Builder = AlertDialog.Builder(ctx)
val dialogClickListener =
DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
removeKeys(SEARCH_HISTORY_KEY)
searchViewModel.updateHistory()
}
DialogInterface.BUTTON_NEGATIVE -> {
}
}
}
try {
builder.setTitle(R.string.clear_history).setMessage(
ctx.getString(R.string.delete_message).format(
ctx.getString(R.string.history)
)
)
.setPositiveButton(R.string.sort_clear, dialogClickListener)
.setNegativeButton(R.string.cancel, dialogClickListener)
.show()
} catch (e: Exception) {
logError(e)
// ye you somehow fucked up formatting did you?
}
}
}
observe(searchViewModel.currentHistory) { list ->
search_clear_call_history?.isVisible = list.isNotEmpty()
(search_history_recycler.adapter as? SearchHistoryAdaptor?)?.updateList(list)
}
@ -430,6 +496,14 @@ class SearchFragment : Fragment() {
search_master_recycler?.adapter = masterAdapter
search_master_recycler?.layoutManager = GridLayoutManager(context, 1)
// Automatically search the specified query, this allows the app search to launch from intent
arguments?.getString(SEARCH_QUERY)?.let { query ->
if (query.isBlank()) return@let
main_search?.setQuery(query, true)
// Clear the query as to not make it request the same query every time the page is opened
arguments?.putString(SEARCH_QUERY, null)
}
// SubtitlesFragment.push(activity)
//searchViewModel.search("iron man")
//(activity as AppCompatActivity).loadResult("https://shiro.is/overlord-dubbed", "overlord-dubbed", "Shiro")

View File

@ -9,7 +9,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.launchSafe
import com.lagradost.cloudstream3.ui.APIRepository
@ -108,9 +108,9 @@ class SearchViewModel : ViewModel() {
repos.filter { a ->
(ignoreSettings || (providersActive.isEmpty() || providersActive.contains(a.name))) && (!isQuickSearch || a.hasQuickSearch)
}.apmap { a -> // Parallel
}.amap { a -> // Parallel
val search = if (isQuickSearch) a.quickSearch(query) else a.search(query)
if (currentSearchIndex != currentIndex) return@apmap
if (currentSearchIndex != currentIndex) return@amap
currentList.add(OnGoingSearch(a.name, search))
_currentSearch.postValue(currentList)
}

View File

@ -47,7 +47,7 @@ fun getCurrentLocale(context: Context): String {
// Change locale settings in the app.
// val dm = res.displayMetrics
val conf = res.configuration
return conf?.locale?.language ?: "en"
return conf?.locale?.toString() ?: "en"
}
// idk, if you find a way of automating this it would be great
@ -75,11 +75,13 @@ val appLanguages = arrayListOf(
Triple("\uD83C\uDDE7\uD83C\uDDF7", "Brazilian Portuguese", "bp"),
Triple("", "Romanian", "ro"),
Triple("", "Italian", "it"),
Triple("", "Chinese", "zh"),
Triple("", "Chinese Simplified", "zh"),
Triple("\uD83C\uDDF9\uD83C\uDDFC", "Chinese Traditional", "zh_TW"),
Triple("\uD83C\uDDEE\uD83C\uDDE9", "Indonesian", "in"),
Triple("", "Czech", "cs"),
Triple("", "Croatian", "hr"),
Triple("", "Bulgarian", "bg"),
Triple("", "Bengali", "bn"),
).sortedBy { it.second } //ye, we go alphabetical, so ppl don't put their lang on top
class SettingsGeneral : PreferenceFragmentCompat() {
@ -368,4 +370,4 @@ class SettingsGeneral : PreferenceFragmentCompat() {
e.printStackTrace()
}
}
}
}

View File

@ -1,6 +1,5 @@
package com.lagradost.cloudstream3.ui.settings
import android.content.Context
import android.os.Bundle
import android.view.View
import androidx.preference.PreferenceFragmentCompat
@ -9,19 +8,15 @@ 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.mvvm.logError
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog
import com.lagradost.cloudstream3.utils.SubtitleHelper
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
class SettingsProviders : PreferenceFragmentCompat() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -63,13 +58,17 @@ class SettingsProviders : PreferenceFragmentCompat() {
getPref(R.string.prefer_media_type_key)?.setOnPreferenceClickListener {
val names = enumValues<TvType>().sorted().map { it.name }
val default = enumValues<TvType>().sorted().filter { it != TvType.NSFW }.map { it.ordinal }
val default =
enumValues<TvType>().sorted().filter { it != TvType.NSFW }.map { it.ordinal }
val defaultSet = default.map { it.toString() }.toSet()
val currentList = try {
settingsManager.getStringSet(getString(R.string.prefer_media_type_key), defaultSet)?.map {
it.toInt()
}
} catch (e: Throwable) { null } ?: default
settingsManager.getStringSet(getString(R.string.prefer_media_type_key), defaultSet)
?.map {
it.toInt()
}
} catch (e: Throwable) {
null
} ?: default
activity?.showMultiDialog(
names,
@ -89,19 +88,22 @@ class SettingsProviders : PreferenceFragmentCompat() {
getPref(R.string.provider_lang_key)?.setOnPreferenceClickListener {
activity?.getApiProviderLangSettings()?.let { current ->
val langs = APIHolder.apis.map { it.lang }.toSet()
.sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) }
val languages = APIHolder.apis.map { it.lang }.toSet()
.sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName
val currentList = ArrayList<Int>()
for (i in current) {
currentList.add(langs.indexOf(i))
val currentList = current.map {
languages.indexOf(it)
}
val names = langs.map {
val emoji = SubtitleHelper.getFlagFromIso(it)
val name = SubtitleHelper.fromTwoLettersToLanguage(it)
val fullName = "$emoji $name"
Pair(it, fullName)
val names = languages.map {
if (it == AllLanguagesName) {
Pair(it, getString(R.string.all_languages_preference))
} else {
val emoji = SubtitleHelper.getFlagFromIso(it)
val name = SubtitleHelper.fromTwoLettersToLanguage(it)
val fullName = "$emoji $name"
Pair(it, fullName)
}
}
activity?.showMultiDialog(

View File

@ -4,12 +4,14 @@ import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.os.TransactionTooLargeException
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceFragmentCompat
import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
@ -81,12 +83,17 @@ class SettingsUpdates : PreferenceFragmentCompat() {
dialog.text1?.text = text
dialog.copy_btt?.setOnClickListener {
val serviceClipboard =
(activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?)
?: return@setOnClickListener
val clip = ClipData.newPlainText("logcat", text)
serviceClipboard.setPrimaryClip(clip)
dialog.dismissSafe(activity)
// Can crash on too much text
try {
val serviceClipboard =
(activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?)
?: return@setOnClickListener
val clip = ClipData.newPlainText("logcat", text)
serviceClipboard.setPrimaryClip(clip)
dialog.dismissSafe(activity)
} catch (e: TransactionTooLargeException) {
showToast(activity, R.string.clipboard_too_large)
}
}
dialog.clear_btt?.setOnClickListener {
Runtime.getRuntime().exec("logcat -c")
@ -121,7 +128,7 @@ class SettingsUpdates : PreferenceFragmentCompat() {
ioSafe {
if (activity?.runAutoUpdate(false) == false) {
activity?.runOnUiThread {
CommonActivity.showToast(
showToast(
activity,
R.string.no_update_found,
Toast.LENGTH_SHORT

View File

@ -33,8 +33,6 @@ import com.lagradost.cloudstream3.widget.LinearRecycleViewLayoutManager
import kotlinx.android.synthetic.main.add_repo_input.*
import kotlinx.android.synthetic.main.fragment_extensions.*
const val PUBLIC_REPOSITORIES_LIST = "https://recloudstream.github.io/repos/"
class ExtensionsFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
@ -186,15 +184,7 @@ class ExtensionsFragment : Fragment() {
(activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?)?.primaryClip?.getItemAt(
0
)?.text?.toString()?.let { copy ->
// Fix our own repo links and only paste the text if it's a link.
if (copy.startsWith("http")) {
val fixedUrl = if (copy.startsWith("https://cs.repo")) {
"https://" + copy.substringAfter("?")
} else {
copy
}
dialog.repo_url_input?.setText(fixedUrl)
}
dialog.repo_url_input?.setText(copy)
}
// dialog.list_repositories?.setOnClickListener {
@ -206,21 +196,23 @@ class ExtensionsFragment : Fragment() {
// dialog.text2?.text = provider.name
dialog.apply_btt?.setOnClickListener secondListener@{
val name = dialog.repo_name_input?.text?.toString()
val url = dialog.repo_url_input?.text?.toString()
if (url.isNullOrBlank()) {
showToast(activity, R.string.error_invalid_data, Toast.LENGTH_SHORT)
return@secondListener
}
ioSafe {
val fixedName = if (!name.isNullOrBlank()) name
else RepositoryManager.parseRepository(url)?.name ?: "No name"
val url = dialog.repo_url_input?.text?.toString()
?.let { it1 -> RepositoryManager.parseRepoUrl(it1) }
if (url.isNullOrBlank()) {
main {
showToast(activity, R.string.error_invalid_data, Toast.LENGTH_SHORT)
}
} else {
val fixedName = if (!name.isNullOrBlank()) name
else RepositoryManager.parseRepository(url)?.name ?: "No name"
val newRepo = RepositoryData(fixedName, url)
RepositoryManager.addRepository(newRepo)
extensionViewModel.loadStats()
extensionViewModel.loadRepositories()
this@ExtensionsFragment.activity?.downloadAllPluginsDialog(url, fixedName)
val newRepo = RepositoryData(fixedName, url)
RepositoryManager.addRepository(newRepo)
extensionViewModel.loadStats()
extensionViewModel.loadRepositories()
this@ExtensionsFragment.activity?.downloadAllPluginsDialog(url, fixedName)
}
}
dialog.dismissSafe(activity)
}

View File

@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.mvvm.Some
import com.lagradost.cloudstream3.mvvm.debugAssert
import com.lagradost.cloudstream3.plugins.PluginManager
@ -49,7 +49,7 @@ class ExtensionsViewModel : ViewModel() {
val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY)
?: emptyArray()) + PREBUILT_REPOSITORIES
val onlinePlugins = urls.toList().apmap {
val onlinePlugins = urls.toList().amap {
RepositoryManager.getRepoPlugins(it.url)?.toList() ?: emptyList()
}.flatten().distinctBy { it.second.url }

View File

@ -8,6 +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.R
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.mvvm.observe
@ -45,6 +47,15 @@ class PluginsFragment : Fragment() {
pluginViewModel.languages = listOf()
pluginViewModel.search(null)
// Filter by language set on preferred media
activity?.let {
val providerLangs = it.getApiProviderLangSettings().toList()
if (!providerLangs.contains(AllLanguagesName)) {
pluginViewModel.languages = mutableListOf("none") + providerLangs
//Log.i("DevDebug", "providerLang => ${pluginViewModel.languages.toJson()}")
}
}
val name = arguments?.getString(PLUGINS_BUNDLE_NAME)
val url = arguments?.getString(PLUGINS_BUNDLE_URL)
val isLocal = arguments?.getBoolean(PLUGINS_BUNDLE_LOCAL) == true

View File

@ -10,7 +10,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.mvvm.launchSafe
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.PluginManager.getPluginPath
@ -101,7 +101,7 @@ class PluginsViewModel : ViewModel() {
Toast.LENGTH_SHORT
)
}
}.apmap { (repo, metadata) ->
}.amap { (repo, metadata) ->
PluginManager.downloadAndLoadPlugin(
activity,
metadata.url,

View File

@ -7,6 +7,9 @@ import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
import com.lagradost.cloudstream3.AllLanguagesName
import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEvent
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.plugins.RepositoryManager
@ -96,7 +99,14 @@ class SetupFragmentExtensions : Fragment() {
next_btt?.setOnClickListener {
// Continue setup
if (isSetup)
findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_provider_languages)
if (
// If any available languages
apis.distinctBy { it.lang }.size > 1
) {
findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_provider_languages)
} else {
findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_media)
}
else
findNavController().navigate(R.id.navigation_home)
}

View File

@ -15,7 +15,6 @@ import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES
import com.lagradost.cloudstream3.ui.settings.appLanguages
import com.lagradost.cloudstream3.ui.settings.getCurrentLocale
import com.lagradost.cloudstream3.utils.SubtitleHelper

View File

@ -12,6 +12,7 @@ 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.utils.SubtitleHelper
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
@ -39,14 +40,21 @@ class SetupFragmentProviderLanguage : Fragment() {
val current = this.getApiProviderLangSettings()
val langs = APIHolder.apis.map { it.lang }.toSet()
.sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) }
.sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName
val currentList =
current.map { langs.indexOf(it) }.filter { it != -1 } // TODO LOOK INTO
val currentList = current.map { langs.indexOf(it) }.filter { it != -1 } // TODO LOOK INTO
val languageNames = langs.map {
val emoji = SubtitleHelper.getFlagFromIso(it)
val name = SubtitleHelper.fromTwoLettersToLanguage(it)
"$emoji $name"
if (it == AllLanguagesName) {
getString(R.string.all_languages_preference)
} else {
val emoji = SubtitleHelper.getFlagFromIso(it)
val name = SubtitleHelper.fromTwoLettersToLanguage(it)
"$emoji $name"
}
}
arrayAdapter.addAll(languageNames)
listview1?.adapter = arrayAdapter

View File

@ -49,8 +49,7 @@ data class SaveCaptionStyle(
@JsonProperty("foregroundColor") var foregroundColor: Int,
@JsonProperty("backgroundColor") var backgroundColor: Int,
@JsonProperty("windowColor") var windowColor: Int,
@CaptionStyleCompat.EdgeType
@JsonProperty("edgeType") var edgeType: Int,
@JsonProperty("edgeType") var edgeType: @CaptionStyleCompat.EdgeType Int,
@JsonProperty("edgeColor") var edgeColor: Int,
@FontRes
@JsonProperty("typeface") var typeface: Int?,

View File

@ -0,0 +1,140 @@
package com.lagradost.cloudstream3.utils
import android.util.Log
import androidx.annotation.StringRes
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.ui.result.txt
import java.lang.Long.min
object EpisodeSkip {
private const val TAG = "EpisodeSkip"
enum class SkipType(@StringRes name: Int) {
Opening(R.string.skip_type_op),
Ending(R.string.skip_type_ed),
Recap(R.string.skip_type_recap),
MixedOpening(R.string.skip_type_mixed_op),
MixedEnding(R.string.skip_type_mixed_ed),
Credits(R.string.skip_type_creddits),
Intro(R.string.skip_type_creddits),
}
data class SkipStamp(
val type: SkipType,
val skipToNextEpisode: Boolean,
val startMs: Long,
val endMs: Long,
) {
val uiText = if (skipToNextEpisode) txt(R.string.next_episode) else txt(
R.string.skip_type_format,
txt(type.name)
)
}
private val cachedStamps = HashMap<Int, List<SkipStamp>>()
private fun shouldSkipToNextEpisode(endMs: Long, episodeDurationMs: Long): Boolean {
return episodeDurationMs - endMs < 20_000L // some might have outro that we don't care about tbh
}
suspend fun getStamps(
data: LoadResponse,
episode: ResultEpisode,
episodeDurationMs: Long,
hasNextEpisode: Boolean,
): List<SkipStamp> {
cachedStamps[episode.id]?.let { list ->
return list
}
val out = mutableListOf<SkipStamp>()
Log.i(TAG, "Requesting SkipStamp from ${data.syncData}")
if (data is AnimeLoadResponse && (data.type == TvType.Anime || data.type == TvType.OVA)) {
data.getMalId()?.toIntOrNull()?.let { malId ->
val (resultLength, stamps) = AniSkip.getResult(
malId,
episode.episode,
episodeDurationMs
) ?: return@let null
// because it also returns an expected episode length we use that just in case it is mismatched with like 2s next episode will still work
val dur = min(episodeDurationMs, resultLength)
stamps.mapNotNull { stamp ->
val skipType = when (stamp.skipType) {
"op" -> SkipType.Opening
"ed" -> SkipType.Ending
"recap" -> SkipType.Recap
"mixed-ed" -> SkipType.MixedEnding
"mixed-op" -> SkipType.MixedOpening
else -> null
} ?: return@mapNotNull null
val end = (stamp.interval.endTime * 1000.0).toLong()
val start = (stamp.interval.startTime * 1000.0).toLong()
SkipStamp(
type = skipType,
skipToNextEpisode = hasNextEpisode && shouldSkipToNextEpisode(
end,
dur
),
startMs = start,
endMs = end
)
}?.let { list ->
out.addAll(list)
}
}
}
if (out.isNotEmpty())
cachedStamps[episode.id] = out
return out
}
}
// taken from https://github.com/saikou-app/saikou/blob/3803f8a7a59b826ca193664d46af3a22bbc989f7/app/src/main/java/ani/saikou/others/AniSkip.kt
// the following is GPLv3 code https://github.com/saikou-app/saikou/blob/main/LICENSE.md
object AniSkip {
private const val TAG = "AniSkip"
suspend fun getResult(
malId: Int,
episodeNumber: Int,
episodeLength: Long
): Pair<Long, List<Stamp>>? {
return try {
val url =
"https://api.aniskip.com/v2/skip-times/$malId/$episodeNumber?types[]=ed&types[]=mixed-ed&types[]=mixed-op&types[]=op&types[]=recap&episodeLength=${episodeLength / 1000L}"
Log.i(TAG, "Requesting $url")
val a = app.get(url)
val res = a.parsed<AniSkipResponse>()
Log.i(TAG, "Found ${res.found} with ${res.results?.size} results")
if (res.found && !res.results.isNullOrEmpty()) (res.results[0].episodeLength * 1000).toLong() to res.results else null
} catch (t: Throwable) {
Log.i(TAG, "error = ${t.message}")
logError(t)
null
}
}
data class AniSkipResponse(
@JsonSerialize val found: Boolean,
@JsonSerialize val results: List<Stamp>?,
@JsonSerialize val message: String?,
@JsonSerialize val statusCode: Int
)
data class Stamp(
@JsonSerialize val interval: AniSkipInterval,
@JsonSerialize val skipType: String,
@JsonSerialize val skipId: String,
@JsonSerialize val episodeLength: Double
)
data class AniSkipInterval(
@JsonSerialize val startTime: Double,
@JsonSerialize val endTime: Double
)
}

View File

@ -3,9 +3,7 @@ package com.lagradost.cloudstream3.utils
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Activity.RESULT_CANCELED
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.content.*
import android.content.pm.PackageManager
import android.database.Cursor
import android.media.AudioAttributes
@ -26,7 +24,6 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.annotation.WorkerThread
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat
import androidx.core.text.toSpanned
@ -35,9 +32,7 @@ import androidx.fragment.app.FragmentActivity
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.tvprovider.media.tv.PreviewChannelHelper
import androidx.tvprovider.media.tv.TvContractCompat
import androidx.tvprovider.media.tv.WatchNextProgram
import androidx.tvprovider.media.tv.*
import androidx.tvprovider.media.tv.WatchNextProgram.fromCursor
import com.fasterxml.jackson.module.kotlin.readValue
import com.google.android.gms.cast.framework.CastContext
@ -51,6 +46,7 @@ import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEv
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.plugins.RepositoryManager
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching
import com.lagradost.cloudstream3.ui.WebviewFragment
import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
@ -58,9 +54,13 @@ import com.lagradost.cloudstream3.ui.settings.extensions.PluginsViewModel.Compan
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllResumeStateIds
import com.lagradost.cloudstream3.utils.DataStoreHelper.getLastWatched
import com.lagradost.cloudstream3.utils.FillerEpisodeCheck.toClassDir
import com.lagradost.cloudstream3.utils.JsUnpacker.Companion.load
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.Cache
import java.io.*
import java.net.URL
@ -110,7 +110,8 @@ object AppUtils {
@SuppressLint("RestrictedApi")
private fun buildWatchNextProgramUri(
context: Context,
card: DataStoreHelper.ResumeWatchingResult
card: DataStoreHelper.ResumeWatchingResult,
resumeWatching: VideoDownloadHelper.ResumeWatching?
): WatchNextProgram {
val isSeries = card.type?.isMovieType() == false
val title = if (isSeries) {
@ -129,15 +130,18 @@ object AppUtils {
.setWatchNextType(TvContractCompat.WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE)
.setTitle(title)
.setPosterArtUri(Uri.parse(card.posterUrl))
.setIntentUri(Uri.parse(card.url)) //TODO FIX intent
.setIntentUri(Uri.parse(card.id?.let {
"$appStringResumeWatching://$it"
} ?: card.url))
.setInternalProviderId(card.url)
//.setLastEngagementTimeUtcMillis(System.currentTimeMillis())
.setLastEngagementTimeUtcMillis(
resumeWatching?.updateTime ?: System.currentTimeMillis()
)
card.watchPos?.let {
builder.setDurationMillis(it.duration.toInt())
builder.setLastPlaybackPositionMillis(it.position.toInt())
}
// .setLastEngagementTimeUtcMillis() //TODO
if (isSeries)
card.episode?.let {
@ -147,6 +151,27 @@ object AppUtils {
return builder.build()
}
@SuppressLint("RestrictedApi")
fun getAllWatchNextPrograms(context: Context): Set<Long> {
val COLUMN_WATCH_NEXT_ID_INDEX = 0
val cursor = context.contentResolver.query(
TvContractCompat.WatchNextPrograms.CONTENT_URI,
WatchNextProgram.PROJECTION,
/* selection = */ null,
/* selectionArgs = */ null,
/* sortOrder = */ null
)
val set = mutableSetOf<Long>()
cursor?.use {
if (it.moveToFirst()) {
do {
set.add(cursor.getLong(COLUMN_WATCH_NEXT_ID_INDEX))
} while (it.moveToNext())
}
}
return set
}
/**
* Find the Watch Next program for given id.
* Returns the first instance available.
@ -164,7 +189,7 @@ object AppUtils {
WatchNextProgram.PROJECTION,
/* selection = */ null,
/* selectionArgs = */ null,
/* sortOrder= */ null
/* sortOrder = */ null
)
cursor?.use {
if (it.moveToFirst()) {
@ -195,17 +220,32 @@ object AppUtils {
}
}
/** Prevents losing data when removing and adding simultaneously */
private val continueWatchingLock = Mutex()
// https://github.com/googlearchive/leanback-homescreen-channels/blob/master/app/src/main/java/com/google/android/tvhomescreenchannels/SampleTvProvider.java
@SuppressLint("RestrictedApi")
@WorkerThread
fun Context.addProgramsToContinueWatching(data: List<DataStoreHelper.ResumeWatchingResult>) {
suspend fun Context.addProgramsToContinueWatching(data: List<DataStoreHelper.ResumeWatchingResult>) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val context = this
ioSafe {
data.forEach { episodeInfo ->
continueWatchingLock.withLock {
// A way to get all last watched timestamps
val timeStampHashMap = HashMap<Int, VideoDownloadHelper.ResumeWatching>()
getAllResumeStateIds()?.forEach { id ->
val lastWatched = getLastWatched(id) ?: return@forEach
timeStampHashMap[lastWatched.parentId] = lastWatched
}
val currentProgramIds = data.mapNotNull { episodeInfo ->
try {
val (program, id) = getWatchNextProgramByVideoId(episodeInfo.url, context)
val nextProgram = buildWatchNextProgramUri(context, episodeInfo)
val customId = "${episodeInfo.id}|${episodeInfo.apiName}|${episodeInfo.url}"
val (program, id) = getWatchNextProgramByVideoId(customId, context)
val nextProgram = buildWatchNextProgramUri(
context,
episodeInfo,
timeStampHashMap[episodeInfo.id]
)
// If the program is already in the Watch Next row, update it
if (program != null && id != null) {
@ -213,13 +253,25 @@ object AppUtils {
nextProgram,
id,
)
id
} else {
PreviewChannelHelper(context)
.publishWatchNextProgram(nextProgram)
}
} catch (e: Exception) {
logError(e)
null
}
}.toSet()
val allOldPrograms = getAllWatchNextPrograms(context) - currentProgramIds
// Ensures synced watch next progress by deleting all old programs.
allOldPrograms.forEach {
context.contentResolver.delete(
TvContractCompat.buildWatchNextProgramUri(it),
null, null
)
}
}
}
@ -279,7 +331,7 @@ object AppUtils {
downloadAll(context, repositoryUrl, null)
}
setNegativeButton(R.string.cancel) { _, _ -> }
setNegativeButton(R.string.no) { _, _ -> }
}
builder.show()
}

View File

@ -13,7 +13,6 @@ import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.plugins.PLUGINS_KEY
import com.lagradost.cloudstream3.plugins.PLUGINS_KEY_LOCAL
@ -28,7 +27,6 @@ import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_S
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_TOKEN_KEY
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
import com.lagradost.cloudstream3.syncproviders.providers.OpenSubtitlesApi.Companion.OPEN_SUBTITLES_USER_KEY
import com.lagradost.cloudstream3.utils.DataStore.getDefaultSharedPrefs
import com.lagradost.cloudstream3.utils.DataStore.getSharedPrefs

View File

@ -20,10 +20,13 @@ class CastOptionsProvider : OptionsProvider {
MediaIntentReceiver.ACTION_FORWARD,
MediaIntentReceiver.ACTION_STOP_CASTING
)
val name = ControllerActivity::class.qualifiedName!!
val compatButtonAction = intArrayOf(1, 3)
val notificationOptions =
NotificationOptions.Builder()
.setTargetActivityClassName(ControllerActivity::class.qualifiedName)
.setTargetActivityClassName(name)
.setActions(buttonActions, compatButtonAction)
.setForward30DrawableResId(R.drawable.go_forward_30)
.setRewind30DrawableResId(R.drawable.go_back_30)
@ -32,7 +35,7 @@ class CastOptionsProvider : OptionsProvider {
val mediaOptions = CastMediaOptions.Builder()
.setNotificationOptions(notificationOptions)
.setExpandedControllerActivityClassName(ControllerActivity::class.qualifiedName)
.setExpandedControllerActivityClassName(name)
.build()
return CastOptions.Builder()
@ -44,7 +47,7 @@ class CastOptionsProvider : OptionsProvider {
.build()
}
override fun getAdditionalSessionProviders(p0: Context?): MutableList<SessionProvider> {
override fun getAdditionalSessionProviders(p0: Context): MutableList<SessionProvider> {
return Collections.emptyList()
}
}

View File

@ -205,6 +205,8 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
VideovardSX(),
Mp4Upload(),
StreamTape(),
StreamTapeNet(),
ShaveTape(),
//mixdrop extractors
MixDropBz(),
@ -329,6 +331,8 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
Vidmolyme(),
Voe(),
Moviehab(),
MoviehabNet(),
Jeniusplay(),
Gdriveplayerapi(),
Gdriveplayerapp(),

View File

@ -7,16 +7,14 @@ import android.net.Uri
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.preference.PreferenceManager
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okio.BufferedSink
@ -116,6 +114,7 @@ class InAppUpdater {
)?.groupValues?.get(2)
}
}).toList().lastOrNull()
val foundAsset = found?.assets?.getOrNull(0)
val currentVersion = packageName?.let {
packageManager.getPackageInfo(
@ -245,6 +244,9 @@ class InAppUpdater {
}
}
/**
* @param checkAutoUpdate if the update check was launched automatically
**/
suspend fun Activity.runAutoUpdate(checkAutoUpdate: Boolean = true): Boolean {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
@ -254,13 +256,20 @@ class InAppUpdater {
)
) {
val update = getAppUpdate()
if (update.shouldUpdate && update.updateURL != null) {
//Check if update should be skipped
if (
update.shouldUpdate &&
update.updateURL != null) {
// Check if update should be skipped
val updateNodeId =
settingsManager.getString(getString(R.string.skip_update_key), "")
if (update.updateNodeId.equals(updateNodeId)) {
// Skips the update if its an automatic update and the update is skipped
// This allows updating manually
if (update.updateNodeId.equals(updateNodeId) && checkAutoUpdate) {
return false
}
runOnUiThread {
try {
val currentVersion = packageName?.let {
@ -283,16 +292,23 @@ class InAppUpdater {
builder.apply {
setPositiveButton(R.string.update) { _, _ ->
showToast(context, R.string.download_started, Toast.LENGTH_LONG)
ioSafe {
if (!downloadUpdate(update.updateURL))
runOnUiThread {
showToast(
context,
R.string.download_failed,
Toast.LENGTH_LONG
)
}
}
val intent = PackageInstallerService.getIntent(
context,
update.updateURL
)
ContextCompat.startForegroundService(context, intent)
// ioSafe {
// if (
// !downloadUpdate(update.updateURL)
// )
// runOnUiThread {
// showToast(
// context,
// R.string.download_failed,
// Toast.LENGTH_LONG
// )
// }
// }
}
setNegativeButton(R.string.cancel) { _, _ -> }
@ -302,8 +318,7 @@ class InAppUpdater {
settingsManager.edit().putString(
getString(R.string.skip_update_key),
update.updateNodeId ?: ""
)
.apply()
).apply()
}
}
}

View File

@ -0,0 +1,106 @@
package com.lagradost.cloudstream3.utils
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInstaller
import android.os.Build
import com.lagradost.cloudstream3.mvvm.logError
import java.io.InputStream
const val INSTALL_ACTION = "ApkInstaller.INSTALL_ACTION"
class ApkInstaller(private val service: PackageInstallerService) {
private val packageInstaller = service.packageManager.packageInstaller
enum class InstallProgressStatus {
Preparing,
Downloading,
Installing,
Failed,
}
private val installActionReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.getIntExtra(
PackageInstaller.EXTRA_STATUS,
PackageInstaller.STATUS_FAILURE
)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val userAction = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
userAction?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(userAction)
}
}
}
}
fun installApk(
context: Context,
inputStream: InputStream,
size: Long,
installProgress: (bytesRead: Int) -> Unit,
installProgressStatus: (InstallProgressStatus) -> Unit
) {
installProgressStatus.invoke(InstallProgressStatus.Preparing)
var activeSession: Int? = null
try {
val installParams =
PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
installParams.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
}
activeSession = packageInstaller.createSession(installParams)
installParams.setSize(size)
val session = packageInstaller.openSession(activeSession)
installProgressStatus.invoke(InstallProgressStatus.Downloading)
session.openWrite(context.packageName, 0, size)
.use { outputStream ->
val buffer = ByteArray(1024)
var bytesRead = inputStream.read(buffer)
while (bytesRead >= 0) {
outputStream.write(buffer, 0, bytesRead)
bytesRead = inputStream.read(buffer)
installProgress.invoke(bytesRead)
}
inputStream.close()
}
installProgressStatus.invoke(InstallProgressStatus.Installing)
val intentSender = PendingIntent.getBroadcast(
service,
activeSession,
Intent(INSTALL_ACTION),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0,
).intentSender
session.commit(intentSender)
} catch (e: Exception) {
logError(e)
service.unregisterReceiver(installActionReceiver)
installProgressStatus.invoke(InstallProgressStatus.Failed)
activeSession?.let { sessionId ->
packageInstaller.abandonSession(sessionId)
}
}
}
init {
service.registerReceiver(installActionReceiver, IntentFilter(INSTALL_ACTION))
service.receivers.add(installActionReceiver)
}
}

View File

@ -0,0 +1,200 @@
package com.lagradost.cloudstream3.utils
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import kotlinx.coroutines.delay
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.math.roundToInt
class PackageInstallerService : Service() {
val receivers = mutableListOf<BroadcastReceiver>()
private val baseNotification by lazy {
val flag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE
} else 0
val intent = Intent(this, MainActivity::class.java)
val pendingIntent =
PendingIntent.getActivity(this, 0, intent, flag)
NotificationCompat.Builder(this, UPDATE_CHANNEL_ID)
.setAutoCancel(false)
.setColorized(true)
.setOnlyAlertOnce(true)
.setSilent(true)
// If low priority then the notification might not show :(
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setColor(this.colorFromAttribute(R.attr.colorPrimary))
.setContentTitle(getString(R.string.update_notification_downloading))
.setContentIntent(pendingIntent)
.setSmallIcon(R.drawable.rdload)
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel =
NotificationChannel(UPDATE_CHANNEL_ID, UPDATE_CHANNEL_NAME, importance).apply {
description = UPDATE_CHANNEL_DESCRIPTION
}
// Register the channel with the system
val notificationManager: NotificationManager =
this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
override fun onCreate() {
createNotificationChannel()
startForeground(UPDATE_NOTIFICATION_ID, baseNotification.build())
}
private val updateLock = Mutex()
private suspend fun downloadUpdate(url: String): Boolean {
try {
Log.d(LOG_TAG, "Downloading update: $url")
// Delete all old updates
ioSafe {
val appUpdateName = "CloudStream"
val appUpdateSuffix = "apk"
this@PackageInstallerService.cacheDir.listFiles()?.filter {
it.name.startsWith(appUpdateName) && it.extension == appUpdateSuffix
}?.forEach {
it.deleteOnExit()
}
}
updateLock.withLock {
updateNotificationProgress(
0f,
ApkInstaller.InstallProgressStatus.Downloading
)
val body = app.get(url).body
val inputStream = body.byteStream()
val installer = ApkInstaller(this)
val totalSize = body.contentLength()
var currentSize = 0
installer.installApk(this, inputStream, totalSize, {
currentSize += it
// Prevent div 0
if (totalSize == 0L) return@installApk
val percentage = currentSize / totalSize.toFloat()
updateNotificationProgress(
percentage,
ApkInstaller.InstallProgressStatus.Downloading
)
}) { status ->
updateNotificationProgress(0f, status)
}
}
return true
} catch (e: Exception) {
updateNotificationProgress(0f, ApkInstaller.InstallProgressStatus.Failed)
return false
}
}
private fun updateNotificationProgress(
percentage: Float,
state: ApkInstaller.InstallProgressStatus
) {
// Log.d(LOG_TAG, "Downloading app update progress $percentage | $state")
val text = when (state) {
ApkInstaller.InstallProgressStatus.Installing -> R.string.update_notification_installing
ApkInstaller.InstallProgressStatus.Preparing, ApkInstaller.InstallProgressStatus.Downloading -> R.string.update_notification_downloading
ApkInstaller.InstallProgressStatus.Failed -> R.string.update_notification_failed
}
val newNotification = baseNotification
.setContentTitle(getString(text))
.apply {
if (state == ApkInstaller.InstallProgressStatus.Failed) {
setSmallIcon(R.drawable.rderror)
setAutoCancel(true)
} else {
setProgress(
10000, (10000 * percentage).roundToInt(),
state != ApkInstaller.InstallProgressStatus.Downloading
)
}
}
.build()
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// Persistent notification on failure
val id =
if (state == ApkInstaller.InstallProgressStatus.Failed) UPDATE_NOTIFICATION_ID + 1 else UPDATE_NOTIFICATION_ID
notificationManager.notify(id, newNotification)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val url = intent?.getStringExtra(EXTRA_URL) ?: return START_NOT_STICKY
ioSafe {
downloadUpdate(url)
// Close the service after the update is done
// If no sleep then the install prompt may not appear and the notification
// will disappear instantly
delay(10_000)
this@PackageInstallerService.stopSelf()
}
return START_NOT_STICKY
}
override fun onDestroy() {
receivers.forEach {
try {
this.unregisterReceiver(it)
} catch (_: IllegalArgumentException) {
// Receiver not registered
}
}
super.onDestroy()
}
override fun onBind(i: Intent?): IBinder? = null
companion object {
private const val EXTRA_URL = "EXTRA_URL"
private const val LOG_TAG = "PackageInstallerService"
const val UPDATE_CHANNEL_ID = "cloudstream3.updates"
const val UPDATE_CHANNEL_NAME = "App Updates"
const val UPDATE_CHANNEL_DESCRIPTION = "App updates notification channel"
const val UPDATE_NOTIFICATION_ID = -68454136
fun getIntent(
context: Context,
url: String,
): Intent {
return Intent(context, PackageInstallerService::class.java)
.putExtra(EXTRA_URL, url)
}
}
}

View File

@ -136,7 +136,7 @@ object UIHelper {
navigation, arguments
)
}
} catch (t : Throwable) {
} catch (t: Throwable) {
logError(t)
}
}
@ -159,17 +159,20 @@ object UIHelper {
url: String?,
headers: Map<String, String>? = null,
@DrawableRes
errorImageDrawable: Int? = null
errorImageDrawable: Int? = null,
fadeIn: Boolean = true
): Boolean {
if (this == null || url.isNullOrBlank()) return false
return try {
val builder = GlideApp.with(this)
.load(GlideUrl(url) { headers ?: emptyMap() }).transition(
DrawableTransitionOptions.withCrossFade()
)
.load(GlideUrl(url) { headers ?: emptyMap() })
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.diskCacheStrategy(DiskCacheStrategy.ALL).let { req ->
if (fadeIn)
req.transition(DrawableTransitionOptions.withCrossFade())
else req
}
val res = if (errorImageDrawable != null)
builder.error(errorImageDrawable).into(this)
@ -325,7 +328,9 @@ object UIHelper {
)
}
fun Context.fixPaddingStatusbarView(v: View) {
fun Context.fixPaddingStatusbarView(v: View?) {
if (v == null) return
val params = v.layoutParams
params.height = getStatusBarHeight()
v.layoutParams = params

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