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:
commit
e4cc642c81
|
@ -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
|
14
README.md
14
README.md
|
@ -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
|
||||
|
||||
-->
|
||||
|
|
|
@ -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 {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)) {
|
||||
|
|
|
@ -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" /> <!– Used for getting if vlc is installed –> -->
|
||||
<!-- 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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?,
|
||||
)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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\("(.*?)"""")
|
||||
|
|
|
@ -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*['"](.*?)['"]""")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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://"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")) {
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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))
|
||||
}) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?,
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue