Compare commits

..

7 commits

Author SHA1 Message Date
reduplicated
7f91ea5cc6 bump 2022-11-05 00:35:51 +01:00
reduplicated
e24dc692d0 small fixes 2022-11-05 00:32:40 +01:00
reduplicated
415c173524 small fix 2022-11-04 20:07:14 +01:00
reduplicated
41fd364401 bump nicehttp 2022-11-04 19:57:12 +01:00
reduplicated
a5cee36572 removed prints 2022-11-04 19:17:07 +01:00
reduplicated
f77fe0a31e working 2022-11-04 18:55:03 +01:00
reduplicated
7619b7e9d9 aniskip groundwork 2022-11-04 12:46:30 +01:00
251 changed files with 7483 additions and 19218 deletions

BIN
.github/downloads.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
.github/home.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

63
.github/locales.py vendored
View file

@ -1,63 +0,0 @@
import re
import glob
import requests
import lxml.etree as ET # builtin library doesn't preserve comments
SETTINGS_PATH = "app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt"
START_MARKER = "/* begin language list */"
END_MARKER = "/* end language list */"
XML_NAME = "app/src/main/res/values-"
ISO_MAP_URL = "https://raw.githubusercontent.com/haliaeetus/iso-639/master/data/iso_639-1.min.json"
INDENT = " "*4
iso_map = requests.get(ISO_MAP_URL, timeout=300).json()
# Load settings file
src = open(SETTINGS_PATH, "r", encoding='utf-8').read()
before_src, rest = src.split(START_MARKER)
rest, after_src = rest.split(END_MARKER)
# Load already added langs
languages = {}
for lang in re.finditer(r'Triple\("(.*)", "(.*)", "(.*)"\)', rest):
flag, name, iso = lang.groups()
languages[iso] = (flag, name)
# Add not yet added langs
for folder in glob.glob(f"{XML_NAME}*"):
iso = folder[len(XML_NAME):]
if iso not in languages.keys():
entry = iso_map.get(iso.lower(),{'nativeName':iso})
languages[iso] = ("", entry['nativeName'].split(',')[0])
# Create triples
triples = []
for iso in sorted(languages.keys()):
flag, name = languages[iso]
triples.append(f'{INDENT}Triple("{flag}", "{name}", "{iso}"),')
# Update settings file
open(SETTINGS_PATH, "w+",encoding='utf-8').write(
before_src +
START_MARKER +
"\n" +
"\n".join(triples) +
"\n" +
END_MARKER +
after_src
)
# Go through each values.xml file and fix escaped \@string
for file in glob.glob(f"{XML_NAME}*/strings.xml"):
try:
tree = ET.parse(file)
for child in tree.getroot():
if child.text.startswith("\\@string/"):
print(f"[{file}] fixing {child.attrib['name']}")
child.text = child.text.replace("\\@string/", "@string/")
with open(file, 'wb') as fp:
fp.write(b'<?xml version="1.0" encoding="utf-8"?>\n')
tree.write(fp, encoding="utf-8", method="xml", pretty_print=True, xml_declaration=False)
except ET.ParseError as ex:
print(f"[{file}] {ex}")

BIN
.github/player.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
.github/results.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

BIN
.github/search.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

View file

@ -15,7 +15,6 @@ jobs:
app_id: ${{ secrets.GH_APP_ID }} app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }} private_key: ${{ secrets.GH_APP_KEY }}
- name: Similarity analysis - name: Similarity analysis
id: similarity
uses: actions-cool/issues-similarity-analysis@v1 uses: actions-cool/issues-similarity-analysis@v1
with: with:
token: ${{ steps.generate_token.outputs.token }} token: ${{ steps.generate_token.outputs.token }}
@ -25,18 +24,6 @@ jobs:
### Your issue looks similar to these issues: ### Your issue looks similar to these issues:
Please close if duplicate. Please close if duplicate.
comment-body: '${index}. ${similarity} #${number}' comment-body: '${index}. ${similarity} #${number}'
- name: Label if possible duplicate
if: steps.similarity.outputs.similar-issues-found =='true'
uses: actions/github-script@v6
with:
github-token: ${{ steps.generate_token.outputs.token }}
script: |
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ["possible duplicate"]
})
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Automatically close issues that dont follow the issue template - name: Automatically close issues that dont follow the issue template
uses: lucasbento/auto-close-issues@v1.0.2 uses: lucasbento/auto-close-issues@v1.0.2
@ -66,18 +53,6 @@ jobs:
Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the [discord](https://discord.gg/5Hus6fM). Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the [discord](https://discord.gg/5Hus6fM).
Found provider name: `${{ steps.provider_check.outputs.name }}` Found provider name: `${{ steps.provider_check.outputs.name }}`
- name: Label if mentions provider
if: steps.provider_check.outputs.name != 'none'
uses: actions/github-script@v6
with:
github-token: ${{ steps.generate_token.outputs.token }}
script: |
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ["possible provider issue"]
})
- name: Add eyes reaction to all issues - name: Add eyes reaction to all issues
uses: actions-cool/emoji-helper@v1.0.0 uses: actions-cool/emoji-helper@v1.0.0
with: with:

View file

@ -1,42 +0,0 @@
name: Fix locale issues
on:
workflow_dispatch:
push:
paths:
- '**.xml'
branches:
- master
concurrency:
group: "locale"
cancel-in-progress: true
jobs:
create:
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/cloudstream"
- uses: actions/checkout@v2
with:
token: ${{ steps.generate_token.outputs.token }}
- name: Install dependencies
run: |
pip3 install lxml
- name: Edit files
run: |
python3 .github/locales.py
- name: Commit to the repo
run: |
git config --local user.email "111277985+recloudstream[bot]@users.noreply.github.com"
git config --local user.name "recloudstream[bot]"
git add .
# "echo" returns true so the build succeeds, even if no changed files
git commit -m 'chore(locales): fix locale issues' || echo
git push

View file

@ -5,14 +5,40 @@
[![Discord](https://invidget.switchblade.xyz/5Hus6fM)](https://discord.gg/5Hus6fM) [![Discord](https://invidget.switchblade.xyz/5Hus6fM)](https://discord.gg/5Hus6fM)
### Features: ***Features:***
+ **AdFree**, No ads whatsoever + **AdFree**, No ads whatsoever
+ No tracking/analytics + No tracking/analytics
+ Bookmarks + Bookmarks
+ Download and stream movies, tv-shows and anime + Download and stream movies, tv-shows and anime
+ Chromecast + Chromecast
### Supported languages: ***Screenshots:***
<a href="https://hosted.weblate.org/engage/cloudstream/">
<img src="https://hosted.weblate.org/widgets/cloudstream/-/app/multi-auto.svg" alt="Translation status" /> <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"/>
</a> <img src="./.github/player.jpg" height="200"/>
***The list of supported languages:***
* 🇱🇧 Arabic
* 🇧🇬 Bulgarian
* 🇭🇷 Croatian
* 🇨🇿 Czech
* 🇳🇱 Dutch
* 🇬🇧 English
* 🇫🇷 French
* 🇩🇪 German
* 🇬🇷 Greek
* 🇮🇳 Hindi
* 🇮🇩 Indonesian
* 🇮🇹 Italian
* 🇲🇰 Macedonian
* 🇮🇳 Malayalam
* 🇳🇴 Norsk
* 🇵🇱 Polish
* 🇧🇷 Portuguese (Brazil)
* 🇷🇴 Romanian
* 🇪🇸 Spanish
* 🇸🇪 Swedish
* 🇵🇭 Tagalog
* 🇹🇷 Turkish
* 🇻🇳 Vietnamese

View file

@ -39,16 +39,16 @@ android {
} }
} }
compileSdk = 33 compileSdk = 31
buildToolsVersion = "30.0.3" buildToolsVersion = "30.0.3"
defaultConfig { defaultConfig {
applicationId = "com.lagradost.cloudstream3" applicationId = "com.lagradost.cloudstream3"
minSdk = 21 minSdk = 21
targetSdk = 33 targetSdk = 30
versionCode = 57 versionCode = 54
versionName = "4.0.0" versionName = "3.2.2"
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
@ -155,12 +155,10 @@ dependencies {
// implementation("androidx.leanback:leanback-paging:1.1.0-alpha09") // implementation("androidx.leanback:leanback-paging:1.1.0-alpha09")
// Exoplayer // Exoplayer
implementation("com.google.android.exoplayer:exoplayer:2.18.2") implementation("com.google.android.exoplayer:exoplayer:2.18.1")
implementation("com.google.android.exoplayer:extension-cast:2.18.2") implementation("com.google.android.exoplayer:extension-cast:2.18.1")
implementation("com.google.android.exoplayer:extension-mediasession:2.18.2") implementation("com.google.android.exoplayer:extension-mediasession:2.18.1")
implementation("com.google.android.exoplayer:extension-okhttp:2.18.2") implementation("com.google.android.exoplayer:extension-okhttp:2.18.1")
// Use the Jellyfin ffmpeg extension for easy ffmpeg audio decoding in exoplayer. Thank you Jellyfin <3
// implementation("org.jellyfin.exoplayer:exoplayer-ffmpeg-extension:2.18.2+1")
//implementation("com.google.android.exoplayer:extension-leanback:2.14.0") //implementation("com.google.android.exoplayer:extension-leanback:2.14.0")
@ -186,15 +184,14 @@ dependencies {
//implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0") //implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0")
// Downloading // Downloading
implementation("androidx.work:work-runtime:2.8.0") implementation("androidx.work:work-runtime:2.7.1")
implementation("androidx.work:work-runtime-ktx:2.8.0") implementation("androidx.work:work-runtime-ktx:2.7.1")
// Networking // Networking
// implementation("com.squareup.okhttp3:okhttp:4.9.2") // implementation("com.squareup.okhttp3:okhttp:4.9.2")
// implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1") // implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1")
implementation("com.github.Blatzar:NiceHttp:0.4.2") implementation("com.github.Blatzar:NiceHttp:0.3.4")
// To fix SSL fuckery on android 9
implementation("org.conscrypt:conscrypt-android:2.2.1")
// Util to skip the URI file fuckery 🙏 // Util to skip the URI file fuckery 🙏
implementation("com.github.tachiyomiorg:unifile:17bec43") implementation("com.github.tachiyomiorg:unifile:17bec43")
@ -222,9 +219,6 @@ dependencies {
// Library/extensions searching with Levenshtein distance // Library/extensions searching with Levenshtein distance
implementation("me.xdrop:fuzzywuzzy:1.4.0") implementation("me.xdrop:fuzzywuzzy:1.4.0")
// color pallette for images -> colors
implementation("androidx.palette:palette-ktx:1.0.0")
} }
tasks.register("androidSourcesJar", Jar::class) { tasks.register("androidSourcesJar", Jar::class) {

View file

@ -1,8 +1,9 @@
package com.lagradost.cloudstream3 package com.lagradost.cloudstream3
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.SubtitleHelper
import com.lagradost.cloudstream3.utils.TestingUtils
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test
@ -15,11 +16,142 @@ import org.junit.runner.RunWith
*/ */
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest { class ExampleInstrumentedTest {
//@Test
//fun useAppContext() {
// // Context of the app under test.
// val appContext = InstrumentationRegistry.getInstrumentation().targetContext
// assertEquals("com.lagradost.cloudstream3", appContext.packageName)
//}
private fun getAllProviders(): List<MainAPI> { private fun getAllProviders(): List<MainAPI> {
println("Providers: ${APIHolder.allProviders.size}")
return APIHolder.allProviders //.filter { !it.usesWebView } return APIHolder.allProviders //.filter { !it.usesWebView }
} }
private suspend fun loadLinks(api: MainAPI, url: String?): Boolean {
Assert.assertNotNull("Api ${api.name} has invalid url on episode", url)
if (url == null) return true
var linksLoaded = 0
try {
val success = api.loadLinks(url, false, {}) { link ->
Assert.assertTrue(
"Api ${api.name} returns link with invalid Quality",
Qualities.values().map { it.value }.contains(link.quality)
)
Assert.assertTrue(
"Api ${api.name} returns link with invalid url ${link.url}",
link.url.length > 4
)
linksLoaded++
}
if (success) {
return linksLoaded > 0
}
Assert.assertTrue("Api ${api.name} has returns false on .loadLinks", success)
} catch (e: Exception) {
if (e.cause is NotImplementedError) {
Assert.fail("Provider has not implemented .loadLinks")
}
logError(e)
}
return true
}
private suspend fun testSingleProviderApi(api: MainAPI): Boolean {
val searchQueries = listOf("over", "iron", "guy")
var correctResponses = 0
var searchResult: List<SearchResponse>? = null
for (query in searchQueries) {
val response = try {
api.search(query)
} catch (e: Exception) {
if (e.cause is NotImplementedError) {
Assert.fail("Provider has not implemented .search")
}
logError(e)
null
}
if (!response.isNullOrEmpty()) {
correctResponses++
if (searchResult == null) {
searchResult = response
}
}
}
if (correctResponses == 0 || searchResult == null) {
System.err.println("Api ${api.name} did not return any valid search responses")
return false
}
try {
var validResults = false
for (result in searchResult) {
Assert.assertEquals(
"Invalid apiName on response on ${api.name}",
result.apiName,
api.name
)
val load = api.load(result.url) ?: continue
Assert.assertEquals(
"Invalid apiName on load on ${api.name}",
load.apiName,
result.apiName
)
Assert.assertTrue(
"Api ${api.name} on load does not contain any of the supportedTypes",
api.supportedTypes.contains(load.type)
)
when (load) {
is AnimeLoadResponse -> {
val gotNoEpisodes =
load.episodes.keys.isEmpty() || load.episodes.keys.any { load.episodes[it].isNullOrEmpty() }
if (gotNoEpisodes) {
println("Api ${api.name} got no episodes on ${load.url}")
continue
}
val url = (load.episodes[load.episodes.keys.first()])?.first()?.data
validResults = loadLinks(api, url)
if (!validResults) continue
}
is MovieLoadResponse -> {
val gotNoEpisodes = load.dataUrl.isBlank()
if (gotNoEpisodes) {
println("Api ${api.name} got no movie on ${load.url}")
continue
}
validResults = loadLinks(api, load.dataUrl)
if (!validResults) continue
}
is TvSeriesLoadResponse -> {
val gotNoEpisodes = load.episodes.isEmpty()
if (gotNoEpisodes) {
println("Api ${api.name} got no episodes on ${load.url}")
continue
}
validResults = loadLinks(api, load.episodes.first().data)
if (!validResults) continue
}
}
break
}
if(!validResults) {
System.err.println("Api ${api.name} did not load on any")
}
return validResults
} catch (e: Exception) {
if (e.cause is NotImplementedError) {
Assert.fail("Provider has not implemented .load")
}
logError(e)
return false
}
}
@Test @Test
fun providersExist() { fun providersExist() {
Assert.assertTrue(getAllProviders().isNotEmpty()) Assert.assertTrue(getAllProviders().isNotEmpty())
@ -27,7 +159,6 @@ class ExampleInstrumentedTest {
} }
@Test @Test
@Throws(AssertionError::class)
fun providerCorrectData() { fun providerCorrectData() {
val isoNames = SubtitleHelper.languages.map { it.ISO_639_1 } val isoNames = SubtitleHelper.languages.map { it.ISO_639_1 }
Assert.assertFalse("ISO does not contain any languages", isoNames.isNullOrEmpty()) Assert.assertFalse("ISO does not contain any languages", isoNames.isNullOrEmpty())
@ -50,20 +181,65 @@ class ExampleInstrumentedTest {
fun providerCorrectHomepage() { fun providerCorrectHomepage() {
runBlocking { runBlocking {
getAllProviders().amap { api -> getAllProviders().amap { api ->
TestingUtils.testHomepage(api, ::println) if (api.hasMainPage) {
try {
val homepage = api.getMainPage()
when {
homepage == null -> {
System.err.println("Homepage provider ${api.name} did not correctly load homepage!")
}
homepage.items.isEmpty() -> {
System.err.println("Homepage provider ${api.name} does not contain any items!")
}
homepage.items.any { it.list.isEmpty() } -> {
System.err.println ("Homepage provider ${api.name} does not have any items on result!")
}
}
} catch (e: Exception) {
if (e.cause is NotImplementedError) {
Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented")
}
logError(e)
}
}
} }
} }
println("Done providerCorrectHomepage") println("Done providerCorrectHomepage")
} }
// @Test
// fun testSingleProvider() {
// testSingleProviderApi(ThenosProvider())
// }
@Test @Test
fun testAllProvidersCorrect() { fun providerCorrect() {
runBlocking { runBlocking {
TestingUtils.getDeferredProviderTests( val invalidProvider = ArrayList<Pair<MainAPI, Exception?>>()
this, val providers = getAllProviders()
getAllProviders(), providers.amap { api ->
::println try {
) { _, _ -> } println("Trying $api")
if (testSingleProviderApi(api)) {
println("Success $api")
} else {
System.err.println("Error $api")
invalidProvider.add(Pair(api, null))
} }
} catch (e: Exception) {
logError(e)
invalidProvider.add(Pair(api, e))
}
}
if(invalidProvider.isEmpty()) {
println("No Invalid providers! :D")
} else {
println("Invalid providers are: ")
for (provider in invalidProvider) {
println("${provider.first}")
}
}
}
println("Done providerCorrect")
} }
} }

View file

@ -10,11 +10,7 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <!-- some dependency needs this --> <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.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="android.permission.REQUEST_INSTALL_PACKAGES" /> <!-- Used for app update -->
<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" /> not used atm, but code exist that requires it that are not run -->
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> <!-- Used for Android TV watch next -->
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- Used for updates without prompt -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Used for update service -->
<!-- <permission android:name="android.permission.QUERY_ALL_PACKAGES" /> &lt;!&ndash; Used for getting if vlc is installed &ndash;&gt; --> <!-- <permission android:name="android.permission.QUERY_ALL_PACKAGES" /> &lt;!&ndash; Used for getting if vlc is installed &ndash;&gt; -->
<!-- Fixes android tv fuckery --> <!-- Fixes android tv fuckery -->
<uses-feature <uses-feature
@ -98,16 +94,6 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" /> <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter> </intent-filter>
<!-- cloudstreamplayer://encodedUrl?name=Dune -->
<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="cloudstreamplayer" />
</intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -124,30 +110,6 @@
<data android:scheme="cloudstreamrepo" /> <data android:scheme="cloudstreamrepo" />
</intent-filter> </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> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -183,10 +145,6 @@
android:name=".ui.ControllerActivity" android:name=".ui.ControllerActivity"
android:exported="false" /> android:exported="false" />
<service
android:name=".utils.PackageInstallerService"
android:exported="false" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"

View file

@ -43,9 +43,9 @@ class CustomReportSender : ReportSender {
override fun send(context: Context, errorContent: CrashReportData) { override fun send(context: Context, errorContent: CrashReportData) {
println("Sending report") println("Sending report")
val url = val url =
"https://docs.google.com/forms/d/e/1FAIpQLSdOlbgCx7NeaxjvEGyEQlqdh2nCvwjm2vwpP1VwW7REj9Ri3Q/formResponse" "https://docs.google.com/forms/u/0/d/e/1FAIpQLSe9Vff8oHGMRXcjgCXZwkjvx3eBdNpn4DzjO0FkcWEU1gEQpA/formResponse"
val data = mapOf( val data = mapOf(
"entry.753293084" to errorContent.toJSON() "entry.1586460852" to errorContent.toJSON()
) )
thread { // to not run it on main thread thread { // to not run it on main thread

View file

@ -1,6 +1,5 @@
package com.lagradost.cloudstream3 package com.lagradost.cloudstream3
import android.Manifest
import android.app.Activity import android.app.Activity
import android.app.PictureInPictureParams import android.app.PictureInPictureParams
import android.content.Context import android.content.Context
@ -17,7 +16,6 @@ import androidx.annotation.MainThread
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.gms.cast.framework.CastSession import com.google.android.gms.cast.framework.CastSession
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
@ -63,9 +61,7 @@ object CommonActivity {
} }
} }
/** duration is Toast.LENGTH_SHORT if null*/ fun showToast(act: Activity?, @StringRes message: Int, duration: Int) {
@MainThread
fun showToast(act: Activity?, @StringRes message: Int, duration: Int? = null) {
if (act == null) return if (act == null) return
showToast(act, act.getString(message), duration) showToast(act, act.getString(message), duration)
} }
@ -73,7 +69,6 @@ object CommonActivity {
const val TAG = "COMPACT" const val TAG = "COMPACT"
/** duration is Toast.LENGTH_SHORT if null*/ /** duration is Toast.LENGTH_SHORT if null*/
@MainThread
fun showToast(act: Activity?, message: String?, duration: Int? = null) { fun showToast(act: Activity?, message: String?, duration: Int? = null) {
if (act == null || message == null) { if (act == null || message == null) {
Log.w(TAG, "invalid showToast act = $act message = $message") Log.w(TAG, "invalid showToast act = $act message = $message")
@ -110,18 +105,9 @@ 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-rTW" to Locale.TRADITIONAL_CHINESE
)
fun setLocale(context: Context?, languageCode: String?) { fun setLocale(context: Context?, languageCode: String?) {
if (context == null || languageCode == null) return if (context == null || languageCode == null) return
val locale = appLanguageExceptions[languageCode] ?: Locale(languageCode) val locale = Locale(languageCode)
val resources: Resources = context.resources val resources: Resources = context.resources
val config = resources.configuration val config = resources.configuration
Locale.setDefault(locale) Locale.setDefault(locale)
@ -157,8 +143,8 @@ object CommonActivity {
val resultCode = result.resultCode val resultCode = result.resultCode
val data = result.data val data = result.data
if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) { if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) {
val pos = resumeApp.getPosition(data) val pos = data.getLongExtra(resumeApp.position, -1L)
val dur = resumeApp.getDuration(data) val dur = data.getLongExtra(resumeApp.duration, -1L)
if (dur > 0L && pos > 0L) if (dur > 0L && pos > 0L)
DataStoreHelper.setViewPos(getKey(resumeApp.lastId), pos, dur) DataStoreHelper.setViewPos(getKey(resumeApp.lastId), pos, dur)
removeKey(resumeApp.lastId) removeKey(resumeApp.lastId)
@ -166,23 +152,6 @@ 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() { private fun Activity.enterPIPMode() {

View file

@ -1,11 +0,0 @@
package com.lagradost.cloudstream3
import android.view.LayoutInflater
import androidx.annotation.LayoutRes
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.ui.HeaderViewDecoration
fun setHeaderDecoration(view: RecyclerView, @LayoutRes headerViewRes: Int) {
val headerView = LayoutInflater.from(view.context).inflate(headerViewRes, null)
view.addItemDecoration(HeaderViewDecoration(headerView))
}

View file

@ -13,14 +13,12 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.Coroutines.mainWork
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import com.lagradost.cloudstream3.utils.ExtractorLink
import okhttp3.Interceptor import okhttp3.Interceptor
import org.mozilla.javascript.Scriptable
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@ -83,7 +81,6 @@ object APIHolder {
synchronized(allProviders) { synchronized(allProviders) {
initMap() initMap()
return apiMap?.get(apiName)?.let { apis.getOrNull(it) } return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
// Leave the ?. null check, it can crash regardless
?: allProviders.firstOrNull { it.name == apiName } ?: allProviders.firstOrNull { it.name == apiName }
} }
} }
@ -162,53 +159,6 @@ object APIHolder {
return null return null
} }
private var trackerCache: HashMap<String, AniSearch> = hashMapOf()
/**
* Get anime tracker information based on title, year and type.
* Both titles are attempted to be matched with both Romaji and English title.
* Uses the consumet api.
*
* @param titles uses first index to search, but if you have multiple titles and want extra guarantee to match you can also have that
* @param types Optional parameter to narrow down the scope to Movies, TV, etc. See TrackerType.getTypes()
* @param year Optional parameter to only get anime with a specific year
**/
suspend fun getTracker(
titles: List<String>,
types: Set<TrackerType>?,
year: Int?
): Tracker? {
return try {
require(titles.isNotEmpty()) { "titles must no be empty when calling getTracker" }
val mainTitle = titles[0]
val search =
trackerCache[mainTitle]
?: app.get("https://api.consumet.org/meta/anilist/$mainTitle")
.parsedSafe<AniSearch>()?.also {
trackerCache[mainTitle] = it
} ?: return null
val res = search.results?.find { media ->
val matchingYears = year == null || media.releaseDate == year
val matchingTitles = media.title?.let { title ->
titles.any { userTitle ->
title.isMatchingTitles(userTitle)
}
} ?: false
val matchingTypes = types?.any { it.name.equals(media.type, true) } == true
matchingTitles && matchingTypes && matchingYears
} ?: return null
Tracker(res.malId, res.aniId, res.image, res.cover)
} catch (t: Throwable) {
logError(t)
null
}
}
fun Context.getApiSettings(): HashSet<String> { fun Context.getApiSettings(): HashSet<String> {
//val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) //val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
@ -286,6 +236,7 @@ object APIHolder {
} }
private fun Context.getHasTrailers(): Boolean { private fun Context.getHasTrailers(): Boolean {
if (isTvSettings()) return false
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
return settingsManager.getBoolean(this.getString(R.string.show_trailers_key), true) return settingsManager.getBoolean(this.getString(R.string.show_trailers_key), true)
} }
@ -293,17 +244,11 @@ object APIHolder {
fun Context.filterProviderByPreferredMedia(hasHomePageIsRequired: Boolean = true): List<MainAPI> { fun Context.filterProviderByPreferredMedia(hasHomePageIsRequired: Boolean = true): List<MainAPI> {
// We are getting the weirdest crash ever done: // We are getting the weirdest crash ever done:
// java.lang.ClassCastException: com.lagradost.cloudstream3.TvType cannot be cast to com.lagradost.cloudstream3.TvType // java.lang.ClassCastException: com.lagradost.cloudstream3.TvType cannot be cast to com.lagradost.cloudstream3.TvType
// Trying fixing using classloader fuckery // enumValues<TvType>() might be the cause, hence I am trying TvType.values()
val oldLoader = Thread.currentThread().contextClassLoader
Thread.currentThread().contextClassLoader = TvType::class.java.classLoader
val default = TvType.values() val default = TvType.values()
.sorted() .sorted()
.filter { it != TvType.NSFW } .filter { it != TvType.NSFW }
.map { it.ordinal } .map { it.ordinal }
Thread.currentThread().contextClassLoader = oldLoader
val defaultSet = default.map { it.toString() }.toSet() val defaultSet = default.map { it.toString() }.toSet()
val currentPrefMedia = try { val currentPrefMedia = try {
PreferenceManager.getDefaultSharedPreferences(this) PreferenceManager.getDefaultSharedPreferences(this)
@ -367,57 +312,6 @@ object APIHolder {
} }
} }
/*
// THIS IS WORK IN PROGRESS API
interface ITag {
val name: UiText
}
data class SimpleTag(override val name: UiText, val data: String) : ITag
enum class SelectType {
SingleSelect,
MultiSelect,
MultiSelectAndExclude,
}
enum class SelectValue {
Selected,
Excluded,
}
interface GenreSelector {
val title: UiText
val id : Int
}
data class TagSelector(
override val title: UiText,
override val id : Int,
val tags: Set<ITag>,
val defaultTags : Set<ITag> = setOf(),
val selectType: SelectType = SelectType.SingleSelect,
) : GenreSelector
data class BoolSelector(
override val title: UiText,
override val id : Int,
val defaultValue : Boolean = false,
) : GenreSelector
data class InputField(
override val title: UiText,
override val id : Int,
val hint : UiText? = null,
) : GenreSelector
// This response describes how a user might filter the homepage or search results
data class GenreResponse(
val searchSelectors : List<GenreSelector>,
val filterSelectors: List<GenreSelector> = searchSelectors
) */
/* /*
0 = Site not good 0 = Site not good
@ -559,20 +453,6 @@ abstract class MainAPI {
open val hasMainPage = false open val hasMainPage = false
open val hasQuickSearch = false open val hasQuickSearch = false
/**
* A set of which ids the provider can open with getLoadUrl()
* If the set contains SyncIdName.Imdb then getLoadUrl() can be started with
* an Imdb class which inherits from SyncId.
*
* getLoadUrl() is then used to get page url based on that ID.
*
* Example:
* "tt6723592" -> getLoadUrl(ImdbSyncId("tt6723592")) -> "mainUrl/imdb/tt6723592" -> load("mainUrl/imdb/tt6723592")
*
* This is used to launch pages from personal lists or recommendations using IDs.
**/
open val supportedSyncNames = setOf<SyncIdName>()
open val supportedTypes = setOf( open val supportedTypes = setOf(
TvType.Movie, TvType.Movie,
TvType.TvSeries, TvType.TvSeries,
@ -643,14 +523,6 @@ abstract class MainAPI {
open fun getVideoInterceptor(extractorLink: ExtractorLink): Interceptor? { open fun getVideoInterceptor(extractorLink: ExtractorLink): Interceptor? {
return null return null
} }
/**
* Get the load() url based on a sync ID like IMDb or MAL.
* Only contains SyncIds based on supportedSyncUrls.
**/
open suspend fun getLoadUrl(name: SyncIdName, id: String): String? {
return null
}
} }
/** Might need a different implementation for desktop*/ /** Might need a different implementation for desktop*/
@ -736,19 +608,6 @@ fun fixTitle(str: String): String {
.replaceFirstChar { char -> if (char.isLowerCase()) char.titlecase(Locale.getDefault()) else it } .replaceFirstChar { char -> if (char.isLowerCase()) char.titlecase(Locale.getDefault()) else it }
} }
} }
/**
* Get rhino context in a safe way as it needs to be initialized on the main thread.
* Make sure you get the scope using: val scope: Scriptable = rhino.initSafeStandardObjects()
* Use like the following: rhino.evaluateString(scope, js, "JavaScript", 1, null)
**/
suspend fun getRhinoContext(): org.mozilla.javascript.Context {
return Coroutines.mainWork {
val rhino = org.mozilla.javascript.Context.enter()
rhino.initSafeStandardObjects()
rhino.optimizationLevel = -1
rhino
}
}
/** https://www.imdb.com/title/tt2861424/ -> tt2861424 */ /** https://www.imdb.com/title/tt2861424/ -> tt2861424 */
fun imdbUrlToId(url: String): String? { fun imdbUrlToId(url: String): String? {
@ -1273,43 +1132,18 @@ interface LoadResponse {
fun getDurationFromString(input: String?): Int? { fun getDurationFromString(input: String?): Int? {
val cleanInput = input?.trim()?.replace(" ", "") ?: return null 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 -> Regex("([0-9]*)h.*?([0-9]*)m").find(cleanInput)?.groupValues?.let { values ->
if (values.size == 3) { if (values.size == 3) {
val hours = values[1].toIntOrNull() val hours = values[1].toIntOrNull()
val minutes = values[2].toIntOrNull() val minutes = values[2].toIntOrNull()
if (minutes != null && hours != null) { return if (minutes != null && hours != null) {
return hours * 60 + minutes hours * 60 + minutes
} } else null
} }
} }
Regex("([0-9]*)m").find(cleanInput)?.groupValues?.let { values -> Regex("([0-9]*)m").find(cleanInput)?.groupValues?.let { values ->
if (values.size == 2) { if (values.size == 2) {
val return_value = values[1].toIntOrNull() return values[1].toIntOrNull()
if (return_value != null) {
return return_value
}
} }
} }
return null return null
@ -1327,7 +1161,7 @@ fun LoadResponse?.isAnimeBased(): Boolean {
fun TvType?.isEpisodeBased(): Boolean { fun TvType?.isEpisodeBased(): Boolean {
if (this == null) return false if (this == null) return false
return (this == TvType.TvSeries || this == TvType.Anime || this == TvType.AsianDrama) return (this == TvType.TvSeries || this == TvType.Anime)
} }
@ -1351,7 +1185,6 @@ interface EpisodeResponse {
var showStatus: ShowStatus? var showStatus: ShowStatus?
var nextAiring: NextAiring? var nextAiring: NextAiring?
var seasonNames: List<SeasonData>? var seasonNames: List<SeasonData>?
fun getLatestEpisodes(): Map<DubStatus, Int?>
} }
@JvmName("addSeasonNamesString") @JvmName("addSeasonNamesString")
@ -1420,18 +1253,7 @@ data class AnimeLoadResponse(
override var nextAiring: NextAiring? = null, override var nextAiring: NextAiring? = null,
override var seasonNames: List<SeasonData>? = null, override var seasonNames: List<SeasonData>? = null,
override var backgroundPosterUrl: String? = null, override var backgroundPosterUrl: String? = null,
) : LoadResponse, EpisodeResponse { ) : LoadResponse, EpisodeResponse
override fun getLatestEpisodes(): Map<DubStatus, Int?> {
return episodes.map { (status, episodes) ->
val maxSeason = episodes.maxOfOrNull { it.season ?: Int.MIN_VALUE }
.takeUnless { it == Int.MIN_VALUE }
status to episodes
.filter { it.season == maxSeason }
.maxOfOrNull { it.episode ?: Int.MIN_VALUE }
.takeUnless { it == Int.MIN_VALUE }
}.toMap()
}
}
/** /**
* If episodes already exist appends the list. * If episodes already exist appends the list.
@ -1629,17 +1451,7 @@ data class TvSeriesLoadResponse(
override var nextAiring: NextAiring? = null, override var nextAiring: NextAiring? = null,
override var seasonNames: List<SeasonData>? = null, override var seasonNames: List<SeasonData>? = null,
override var backgroundPosterUrl: String? = null, override var backgroundPosterUrl: String? = null,
) : LoadResponse, EpisodeResponse { ) : LoadResponse, EpisodeResponse
override fun getLatestEpisodes(): Map<DubStatus, Int?> {
val maxSeason =
episodes.maxOfOrNull { it.season ?: Int.MIN_VALUE }.takeUnless { it == Int.MIN_VALUE }
val max = episodes
.filter { it.season == maxSeason }
.maxOfOrNull { it.episode ?: Int.MIN_VALUE }
.takeUnless { it == Int.MIN_VALUE }
return mapOf(DubStatus.None to max)
}
}
suspend fun MainAPI.newTvSeriesLoadResponse( suspend fun MainAPI.newTvSeriesLoadResponse(
name: String, name: String,
@ -1671,61 +1483,3 @@ fun fetchUrls(text: String?): List<String> {
fun String?.toRatingInt(): Int? = fun String?.toRatingInt(): Int? =
this?.replace(" ", "")?.trim()?.toDoubleOrNull()?.absoluteValue?.times(1000f)?.toInt() this?.replace(" ", "")?.trim()?.toDoubleOrNull()?.absoluteValue?.times(1000f)?.toInt()
data class Tracker(
val malId: Int? = null,
val aniId: String? = null,
val image: String? = null,
val cover: String? = null,
)
data class Title(
@JsonProperty("romaji") val romaji: String? = null,
@JsonProperty("english") val english: String? = null,
) {
fun isMatchingTitles(title: String?): Boolean {
if (title == null) return false
return english.equals(title, true) || romaji.equals(title, true)
}
}
data class Results(
@JsonProperty("id") val aniId: String? = null,
@JsonProperty("malId") val malId: Int? = null,
@JsonProperty("title") val title: Title? = null,
@JsonProperty("releaseDate") val releaseDate: Int? = null,
@JsonProperty("type") val type: String? = null,
@JsonProperty("image") val image: String? = null,
@JsonProperty("cover") val cover: String? = null,
)
data class AniSearch(
@JsonProperty("results") val results: ArrayList<Results>? = arrayListOf()
)
/**
* used for the getTracker() method
**/
enum class TrackerType {
MOVIE,
TV,
TV_SHORT,
ONA,
OVA,
SPECIAL,
MUSIC;
companion object {
fun getTypes(type: TvType): Set<TrackerType> {
return when (type) {
TvType.Movie -> setOf(MOVIE)
TvType.AnimeMovie -> setOf(MOVIE)
TvType.TvSeries -> setOf(TV, TV_SHORT)
TvType.Anime -> setOf(TV, TV_SHORT, ONA, OVA)
TvType.OVA -> setOf(OVA, SPECIAL, ONA)
TvType.Others -> setOf(MUSIC)
else -> emptySet()
}
}
}
}

View file

@ -1,25 +1,21 @@
package com.lagradost.cloudstream3 package com.lagradost.cloudstream3
import android.content.ComponentName import android.content.ComponentName
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.content.res.Configuration import android.content.res.Configuration
import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.AttributeSet
import android.util.Log import android.util.Log
import android.view.* import android.view.KeyEvent
import android.widget.Toast import android.view.Menu
import android.view.MenuItem
import android.view.WindowManager
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavDestination import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavDestination.Companion.hierarchy
@ -32,9 +28,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.google.android.gms.cast.framework.* import com.google.android.gms.cast.framework.*
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.navigationrail.NavigationRailView import com.google.android.material.navigationrail.NavigationRailView
import com.google.android.material.snackbar.Snackbar
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
import com.lagradost.cloudstream3.APIHolder.allProviders import com.lagradost.cloudstream3.APIHolder.allProviders
import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.apis
@ -49,78 +43,58 @@ import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.CommonActivity.updateLocale import com.lagradost.cloudstream3.CommonActivity.updateLocale
import com.lagradost.cloudstream3.mvvm.* import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.PluginManager.loadAllOnlinePlugins
import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringPlayer
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringSearch
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths
import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
import com.lagradost.cloudstream3.ui.home.HomeViewModel
import com.lagradost.cloudstream3.ui.player.BasicLink
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
import com.lagradost.cloudstream3.ui.player.LinkGenerator
import com.lagradost.cloudstream3.ui.result.ResultViewModel2
import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
import com.lagradost.cloudstream3.ui.result.setImage
import com.lagradost.cloudstream3.ui.result.setText
import com.lagradost.cloudstream3.ui.search.SearchFragment
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings 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.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
import com.lagradost.cloudstream3.ui.settings.SettingsGeneral import com.lagradost.cloudstream3.ui.settings.SettingsGeneral
import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY
import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
import com.lagradost.cloudstream3.utils.AppUtils.isNetworkAvailable
import com.lagradost.cloudstream3.utils.AppUtils.loadCache import com.lagradost.cloudstream3.utils.AppUtils.loadCache
import com.lagradost.cloudstream3.utils.AppUtils.loadRepository import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching
import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.IOnBackPressed
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.cloudstream3.utils.UIHelper.requestRW
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.ResponseParser import com.lagradost.nicehttp.ResponseParser
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.bottom_resultview_preview.*
import kotlinx.android.synthetic.main.fragment_result_swipe.* import kotlinx.android.synthetic.main.fragment_result_swipe.*
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import okhttp3.ConnectionSpec
import okhttp3.OkHttpClient
import okhttp3.internal.applyConnectionSpec
import java.io.File import java.io.File
import java.net.URI import java.net.URI
import java.net.URLDecoder
import java.nio.charset.Charset import java.nio.charset.Charset
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.system.exitProcess
//https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898 //https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898
@ -141,15 +115,13 @@ val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlay
val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity") val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity")
//TODO REFACTOR AF //TODO REFACTOR AF
open class ResultResume( data class ResultResume(
val packageString: String, val packageString: String,
val action: String = Intent.ACTION_VIEW, val action: String = Intent.ACTION_VIEW,
val position: String? = null, val position: String? = null,
val duration: String? = null, val duration: String? = null,
var launcher: ActivityResultLauncher<Intent>? = null, var launcher: ActivityResultLauncher<Intent>? = null,
) { ) {
val defaultTime = -1L
val lastId get() = "${packageString}_last_open_id" val lastId get() = "${packageString}_last_open_id"
suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) { suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) {
val intent = Intent(action) val intent = Intent(action)
@ -163,50 +135,21 @@ open class ResultResume(
callback.invoke(intent) callback.invoke(intent)
launcher?.launch(intent) launcher?.launch(intent)
} }
open fun getPosition(intent: Intent?): Long {
return defaultTime
}
open fun getDuration(intent: Intent?): Long {
return defaultTime
}
} }
val VLC = object : ResultResume( val VLC = ResultResume(
VLC_PACKAGE, VLC_PACKAGE,
// Android 13 intent restrictions fucks up specifically launching the VLC player "org.videolan.vlc.player.result",
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
"org.videolan.vlc.player.result"
} else {
Intent.ACTION_VIEW
},
"extra_position", "extra_position",
"extra_duration", "extra_duration",
) { )
override fun getPosition(intent: Intent?): Long {
return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime
}
override fun getDuration(intent: Intent?): Long { val MPV = ResultResume(
return intent?.getLongExtra(this.duration, defaultTime) ?: defaultTime
}
}
val MPV = object : ResultResume(
MPV_PACKAGE, MPV_PACKAGE,
//"is.xyz.mpv.MPVActivity.result", // resume not working :pensive: //"is.xyz.mpv.MPVActivity.result", // resume not working :pensive:
position = "position", position = "position",
duration = "duration", duration = "duration",
) { )
override fun getPosition(intent: Intent?): Long {
return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong() ?: defaultTime
}
override fun getDuration(intent: Intent?): Long {
return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong() ?: defaultTime
}
}
val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE) val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE)
@ -245,30 +188,14 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
companion object { companion object {
const val TAG = "MAINACT" 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 * 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 afterPluginsLoadedEvent = Event<Boolean>()
val mainPluginsLoadedEvent = val mainPluginsLoadedEvent =
Event<Boolean>() // homepage api, used to speed up time to load for homepage Event<Boolean>() // homepage api, used to speed up time to load for homepage
val afterRepositoryLoadedEvent = Event<Boolean>() val afterRepositoryLoadedEvent = Event<Boolean>()
// kinda shitty solution, but cant com main->home otherwise for popups
val bookmarksUpdatedEvent = Event<Boolean>()
/** /**
* @return true if the str has launched an app task (be it successful or not) * @return true if the str has launched an app task (be it successful or not)
* @param isWebview does not handle providers and opening download page if true. Can still add repos and login. * @param isWebview does not handle providers and opening download page if true. Can still add repos and login.
@ -279,11 +206,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
isWebview: Boolean isWebview: Boolean
): Boolean = ): Boolean =
with(activity) { with(activity) {
// TODO MUCH BETTER HANDLING
// Invalid URIs can crash
fun safeURI(uri: String) = normalSafeApiCall { URI(uri) }
if (str != null && this != null) { if (str != null && this != null) {
if (str.startsWith("https://cs.repo")) { if (str.startsWith("https://cs.repo")) {
val realUrl = "https://" + str.substringAfter("?") val realUrl = "https://" + str.substringAfter("?")
@ -319,50 +241,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
return true return true
} }
} }
// This specific intent is used for the gradle deployWithAdb } else if (URI(str).scheme == appStringRepo) {
// 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") val url = str.replaceFirst(appStringRepo, "https")
loadRepository(url) loadRepository(url)
return true return true
} else if (safeURI(str)?.scheme == appStringSearch) {
nextSearchQuery =
URLDecoder.decode(str.substringAfter("$appStringSearch://"), "UTF-8")
// Use both navigation views to support both layouts.
// It might be better to use the QuickSearch.
nav_view?.selectedItemId = R.id.navigation_search
nav_rail_view?.selectedItemId = R.id.navigation_search
} else if (safeURI(str)?.scheme == appStringPlayer) {
val uri = Uri.parse(str)
val name = uri.getQueryParameter("name")
val url = URLDecoder.decode(uri.authority, "UTF-8")
navigate(
R.id.global_to_navigation_player,
GeneratorPlayer.newInstance(
LinkGenerator(
listOf(BasicLink(url, name)),
extract = true,
)
)
)
} 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) { } else if (!isWebview) {
if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) { if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) {
this.navigate(R.id.navigation_downloads) this.navigate(R.id.navigation_downloads)
@ -381,16 +263,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
} }
var lastPopup: SearchResponse? = null
fun loadPopup(result: SearchResponse) {
lastPopup = result
viewModel.load(
this, result.url, result.apiName, false, if (getApiDubstatusSettings()
.contains(DubStatus.Dubbed)
) DubStatus.Dubbed else DubStatus.Subbed, null
)
}
override fun onColorSelected(dialogId: Int, color: Int) { override fun onColorSelected(dialogId: Int, color: Int) {
onColorSelectedEvent.invoke(Pair(dialogId, color)) onColorSelectedEvent.invoke(Pair(dialogId, color))
} }
@ -422,7 +294,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
val isNavVisible = listOf( val isNavVisible = listOf(
R.id.navigation_home, R.id.navigation_home,
R.id.navigation_search, R.id.navigation_search,
R.id.navigation_library,
R.id.navigation_downloads, R.id.navigation_downloads,
R.id.navigation_settings, R.id.navigation_settings,
R.id.navigation_download_child, R.id.navigation_download_child,
@ -436,30 +307,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
R.id.navigation_settings_general, R.id.navigation_settings_general,
R.id.navigation_settings_extensions, R.id.navigation_settings_extensions,
R.id.navigation_settings_plugins, R.id.navigation_settings_plugins,
R.id.navigation_test_providers,
).contains(destination.id) ).contains(destination.id)
val dontPush = listOf(
R.id.navigation_home,
R.id.navigation_search,
R.id.navigation_results_phone,
R.id.navigation_results_tv,
R.id.navigation_player,
).contains(destination.id)
nav_host_fragment?.apply {
val params = layoutParams as ConstraintLayout.LayoutParams
params.setMargins(
if (!dontPush && isTvSettings()) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0,
params.topMargin,
params.rightMargin,
params.bottomMargin
)
layoutParams = params
}
val landscape = when (resources.configuration.orientation) { val landscape = when (resources.configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> { Configuration.ORIENTATION_LANDSCAPE -> {
true true
@ -474,11 +323,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
nav_view?.isVisible = isNavVisible && !landscape nav_view?.isVisible = isNavVisible && !landscape
nav_rail_view?.isVisible = isNavVisible && landscape nav_rail_view?.isVisible = isNavVisible && landscape
// Hide library on TV since it is not supported yet :(
val isTrueTv = isTrueTvSettings()
nav_view?.menu?.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
nav_rail_view?.menu?.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
} }
//private var mCastSession: CastSession? = null //private var mCastSession: CastSession? = null
@ -531,11 +375,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
// Start any delayed updates
if (ApkInstaller.delayedInstaller?.startInstallation() == true) {
Toast.makeText(this, R.string.update_started, Toast.LENGTH_LONG).show()
}
try { try {
if (isCastApiAvailable()) { if (isCastApiAvailable()) {
mSessionManager.removeSessionManagerListener(mSessionManagerListener) mSessionManager.removeSessionManagerListener(mSessionManagerListener)
@ -566,34 +405,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
onUserLeaveHint(this) onUserLeaveHint(this)
} }
private fun showConfirmExitDialog() {
val builder: AlertDialog.Builder = AlertDialog.Builder(this)
builder.setTitle(R.string.confirm_exit_dialog)
builder.apply {
// Forceful exit since back button can actually go back to setup
setPositiveButton(R.string.yes) { _, _ -> exitProcess(0) }
setNegativeButton(R.string.no) { _, _ -> }
}
builder.show().setDefaultFocus()
}
private fun backPressed() { private fun backPressed() {
this.window?.navigationBarColor = this.window?.navigationBarColor =
this.colorFromAttribute(R.attr.primaryGrayBackground) this.colorFromAttribute(R.attr.primaryGrayBackground)
this.updateLocale() this.updateLocale()
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() super.onBackPressed()
} this.updateLocale()
} }
override fun onBackPressed() { override fun onBackPressed() {
@ -681,37 +498,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
} }
lateinit var viewModel: ResultViewModel2
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
viewModel =
ViewModelProvider(this)[ResultViewModel2::class.java]
return super.onCreateView(name, context, attrs)
}
private fun hidePreviewPopupDialog() {
viewModel.clear()
bottomPreviewPopup.dismissSafe(this)
}
var bottomPreviewPopup: BottomSheetDialog? = null
private fun showPreviewPopupDialog(): BottomSheetDialog {
val ret = (bottomPreviewPopup ?: run {
val builder =
BottomSheetDialog(this)
builder.setContentView(R.layout.bottom_resultview_preview)
builder.setOnDismissListener {
bottomPreviewPopup = null
viewModel.clear()
}
builder.setCanceledOnTouchOutside(true)
builder.show()
builder
})
bottomPreviewPopup = ret
return ret
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
app.initClient(this) app.initClient(this)
@ -742,7 +528,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN) window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN)
updateTv()
if (isTvSettings()) { if (isTvSettings()) {
setContentView(R.layout.activity_main_tv) setContentView(R.layout.activity_main_tv)
} else { } else {
@ -751,35 +537,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
changeStatusBarState(isEmulatorSettings()) changeStatusBarState(isEmulatorSettings())
// Automatically enable jsdelivr if cant connect to raw.githubusercontent.com if (lastError == null) {
if (this.getKey<Boolean>(getString(R.string.jsdelivr_proxy_key)) == null && isNetworkAvailable()) {
main {
if (checkGithubConnectivity()) {
this.setKey(getString(R.string.jsdelivr_proxy_key), false)
} else {
this.setKey(getString(R.string.jsdelivr_proxy_key), true)
val parentView: View = findViewById(android.R.id.content)
Snackbar.make(parentView, R.string.jsdelivr_enabled, Snackbar.LENGTH_LONG)
.let { snackbar ->
snackbar.setAction(R.string.revert) {
setKey(getString(R.string.jsdelivr_proxy_key), false)
}
snackbar.setBackgroundTint(colorFromAttribute(R.attr.primaryGrayBackground))
snackbar.setTextColor(colorFromAttribute(R.attr.textColor))
snackbar.setActionTextColor(colorFromAttribute(R.attr.colorPrimary))
snackbar.show()
}
}
}
}
if (PluginManager.checkSafeModeFile()) {
normalSafeApiCall {
showToast(this, R.string.safe_mode_file, Toast.LENGTH_LONG)
}
} else if (lastError == null) {
ioSafe { ioSafe {
getKey<String>(USER_SELECTED_HOMEPAGE_API)?.let { homeApi -> getKey<String>(USER_SELECTED_HOMEPAGE_API)?.let { homeApi ->
mainPluginsLoadedEvent.invoke(loadSinglePlugin(this@MainActivity, homeApi)) mainPluginsLoadedEvent.invoke(loadSinglePlugin(this@MainActivity, homeApi))
@ -795,21 +553,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
) { ) {
PluginManager.updateAllOnlinePluginsAndLoadThem(this@MainActivity) PluginManager.updateAllOnlinePluginsAndLoadThem(this@MainActivity)
} else { } else {
loadAllOnlinePlugins(this@MainActivity) PluginManager.loadAllOnlinePlugins(this@MainActivity)
}
//Automatically download not existing plugins
if (settingsManager.getBoolean(
getString(R.string.auto_download_plugins_key),
false
)
) {
PluginManager.downloadNotExistingPluginsAndLoad(this@MainActivity)
} }
} }
ioSafe { ioSafe {
PluginManager.loadAllLocalPlugins(this@MainActivity, false) PluginManager.loadAllLocalPlugins(this@MainActivity)
} }
} }
} else { } else {
@ -826,81 +575,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
setNegativeButton("Ok") { _, _ -> } setNegativeButton("Ok") { _, _ -> }
} }
builder.show().setDefaultFocus() builder.show()
} }
observeNullable(viewModel.page) { resource ->
if (resource == null) {
bottomPreviewPopup.dismissSafe(this)
return@observeNullable
}
when (resource) {
is Resource.Failure -> {
showToast(this, R.string.error)
hidePreviewPopupDialog()
}
is Resource.Loading -> {
showPreviewPopupDialog().apply {
resultview_preview_loading?.isVisible = true
resultview_preview_result?.isVisible = false
resultview_preview_loading_shimmer?.startShimmer()
}
}
is Resource.Success -> {
val d = resource.value
showPreviewPopupDialog().apply {
resultview_preview_loading?.isVisible = false
resultview_preview_result?.isVisible = true
resultview_preview_loading_shimmer?.stopShimmer()
resultview_preview_title?.text = d.title
resultview_preview_meta_type.setText(d.typeText)
resultview_preview_meta_year.setText(d.yearText)
resultview_preview_meta_duration.setText(d.durationText)
resultview_preview_meta_rating.setText(d.ratingText)
resultview_preview_description?.setText(d.plotText)
resultview_preview_poster?.setImage(
d.posterImage ?: d.posterBackgroundImage
)
resultview_preview_poster?.setOnClickListener {
//viewModel.updateWatchStatus(WatchType.PLANTOWATCH)
val value = viewModel.watchStatus.value ?: WatchType.NONE
this@MainActivity.showBottomDialog(
WatchType.values().map { getString(it.stringRes) }.toList(),
value.ordinal,
this@MainActivity.getString(R.string.action_add_to_bookmarks),
showApply = false,
{}) {
viewModel.updateWatchStatus(WatchType.values()[it])
bookmarksUpdatedEvent(true)
}
}
if (!isTvSettings()) // dont want this clickable on tv layout
resultview_preview_description?.setOnClickListener { view ->
view.context?.let { ctx ->
val builder: AlertDialog.Builder =
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
builder.setMessage(d.plotText.asString(ctx).html())
.setTitle(d.plotHeaderText.asString(ctx))
.show()
}
}
resultview_preview_more_info?.setOnClickListener {
hidePreviewPopupDialog()
lastPopup?.let {
loadSearchResult(it)
}
}
}
}
}
}
// ioSafe { // ioSafe {
// val plugins = // val plugins =
@ -942,17 +619,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
val navHostFragment = val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController 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) //val navController = findNavController(R.id.nav_host_fragment)
/*navOptions = NavOptions.Builder() /*navOptions = NavOptions.Builder()
@ -966,12 +632,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
nav_view?.setupWithNavController(navController) nav_view?.setupWithNavController(navController)
val nav_rail = findViewById<NavigationRailView?>(R.id.nav_rail_view) val nav_rail = findViewById<NavigationRailView?>(R.id.nav_rail_view)
nav_rail?.setupWithNavController(navController) nav_rail?.setupWithNavController(navController)
if (isTvSettings()) {
nav_rail?.background?.alpha = 200
} else {
nav_rail?.background?.alpha = 255
}
nav_rail?.setOnItemSelectedListener { item -> nav_rail?.setOnItemSelectedListener { item ->
onNavDestinationSelected( onNavDestinationSelected(
item, item,
@ -1140,22 +801,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
// Used to check current focus for TV // Used to check current focus for TV
// main { // main {
// while (true) { // while (true) {
// delay(5000) // delay(1000)
// println("Current focus: $currentFocus") // println("Current focus: $currentFocus")
// showToast(this, currentFocus.toString(), Toast.LENGTH_LONG)
// } // }
// } // }
} }
suspend fun checkGithubConnectivity(): Boolean {
return try {
app.get(
"https://raw.githubusercontent.com/recloudstream/.github/master/connectivitycheck",
timeout = 5
).text.trim() == "ok"
} catch (t: Throwable) {
false
}
}
} }

View file

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

View file

@ -4,7 +4,7 @@ import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.base64Decode import com.lagradost.cloudstream3.base64Decode
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
open class Acefile : ExtractorApi() { class Acefile : ExtractorApi() {
override val name = "Acefile" override val name = "Acefile"
override val mainUrl = "https://acefile.co" override val mainUrl = "https://acefile.co"
override val requiresReferer = false override val requiresReferer = false
@ -27,6 +27,7 @@ open class Acefile : ExtractorApi() {
res.substringAfter("\"file\":\"").substringBefore("\","), res.substringAfter("\"file\":\"").substringBefore("\","),
"$mainUrl/", "$mainUrl/",
Qualities.Unknown.value, Qualities.Unknown.value,
headers = mapOf("range" to "bytes=0-")
) )
) )
} }

View file

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

View file

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

View file

@ -5,7 +5,7 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper import com.lagradost.cloudstream3.utils.M3u8Helper
open class BullStream : ExtractorApi() { class BullStream : ExtractorApi() {
override val name = "BullStream" override val name = "BullStream"
override val mainUrl = "https://bullstream.xyz" override val mainUrl = "https://bullstream.xyz"
override val requiresReferer = false override val requiresReferer = false

View file

@ -1,23 +0,0 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.utils.*
open class ByteShare : ExtractorApi() {
override val name = "ByteShare"
override val mainUrl = "https://byteshare.net"
override val requiresReferer = false
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val sources = mutableListOf<ExtractorLink>()
sources.add(
ExtractorLink(
name,
name,
url.replace("/embed/", "/download/"),
"",
Qualities.Unknown.value,
)
)
return sources
}
}

View file

@ -1,97 +0,0 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import android.util.Log
import java.net.URLDecoder
open class Cda: ExtractorApi() {
override var mainUrl = "https://ebd.cda.pl"
override var name = "Cda"
override val requiresReferer = false
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val mediaId = url
.split("/").last()
.split("?").first()
val doc = app.get("https://ebd.cda.pl/647x500/$mediaId", headers=mapOf(
"Referer" to "https://ebd.cda.pl/647x500/$mediaId",
"User-Agent" to USER_AGENT,
"Cookie" to "cda.player=html5"
)).document
val dataRaw = doc.selectFirst("[player_data]")?.attr("player_data") ?: return null
val playerData = tryParseJson<PlayerData>(dataRaw) ?: return null
return listOf(ExtractorLink(
name,
name,
getFile(playerData.video.file),
referer = "https://ebd.cda.pl/647x500/$mediaId",
quality = Qualities.Unknown.value
))
}
private fun rot13(a: String): String {
return a.map {
when {
it in 'A'..'M' || it in 'a'..'m' -> it + 13
it in 'N'..'Z' || it in 'n'..'z' -> it - 13
else -> it
}
}.joinToString("")
}
private fun cdaUggc(a: String): String {
val decoded = rot13(a)
return if (decoded.endsWith("adc.mp4")) decoded.replace("adc.mp4",".mp4")
else decoded
}
private fun cdaDecrypt(b: String): String {
var a = b
.replace("_XDDD", "")
.replace("_CDA", "")
.replace("_ADC", "")
.replace("_CXD", "")
.replace("_QWE", "")
.replace("_Q5", "")
.replace("_IKSDE", "")
a = URLDecoder.decode(a, "UTF-8")
a = a.map { char ->
if (32 < char.toInt() && char.toInt() < 127) {
return@map String.format("%c", 33 + (char.toInt() + 14) % 94)
} else {
return@map char
}
}.joinToString("")
a = a
.replace(".cda.mp4", "")
.replace(".2cda.pl", ".cda.pl")
.replace(".3cda.pl", ".cda.pl")
return if (a.contains("/upstream")) "https://" + a.replace("/upstream", ".mp4/upstream")
else "https://${a}.mp4"
}
private fun getFile(a: String) = when {
a.startsWith("uggc") -> cdaUggc(a)
!a.startsWith("http") -> cdaDecrypt(a)
else -> a
}
data class VideoPlayerData(
val file: String,
val qualities: Map<String, String> = mapOf(),
val quality: String?,
val ts: Int?,
val hash2: String?
)
data class PlayerData(
val video: VideoPlayerData
)
}

View file

@ -1,105 +0,0 @@
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.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
import com.lagradost.cloudstream3.utils.Qualities
import java.net.URL
open class Dailymotion : ExtractorApi() {
override val mainUrl = "https://www.dailymotion.com"
override val name = "Dailymotion"
override val requiresReferer = false
@Suppress("RegExpSimplifiable")
private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex()
// https://www.dailymotion.com/video/k3JAHfletwk94ayCVIu
// https://www.dailymotion.com/embed/video/k3JAHfletwk94ayCVIu
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val embedUrl = getEmbedUrl(url) ?: return
val doc = app.get(embedUrl).document
val prefix = "window.__PLAYER_CONFIG__ = "
val configStr = doc.selectFirst("script:containsData($prefix)")?.data() ?: return
val config = tryParseJson<Config>(configStr.substringAfter(prefix)) ?: return
val id = getVideoId(embedUrl) ?: return
val dmV1st = config.dmInternalData.v1st
val dmTs = config.dmInternalData.ts
val metaDataUrl =
"$mainUrl/player/metadata/video/$id?locale=en&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0"
val cookies = mapOf(
"v1st" to dmV1st,
"dmvk" to config.context.dmvk,
"ts" to dmTs.toString()
)
val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = cookies)
.parsedSafe<MetaData>() ?: return
metaData.qualities.forEach { (_, video) ->
video.forEach {
getStream(it.url, this.name, callback)
}
}
}
private fun getEmbedUrl(url: String): String? {
if (url.contains("/embed/")) {
return url
}
val vid = getVideoId(url) ?: return null
return "$mainUrl/embed/video/$vid"
}
private fun getVideoId(url: String): String? {
val path = URL(url).path
val id = path.substringAfter("video/")
if (id.matches(videoIdRegex)) {
return id
}
return null
}
private suspend fun getStream(
streamLink: String,
name: String,
callback: (ExtractorLink) -> Unit
) {
return generateM3u8(
name,
streamLink,
"",
).forEach(callback)
}
data class Config(
val context: Context,
val dmInternalData: InternalData
)
data class InternalData(
val ts: Int,
val v1st: String
)
data class Context(
@JsonProperty("access_token") val accessToken: String?,
val dmvk: String,
)
data class MetaData(
val qualities: Map<String, List<VideoLink>>
)
data class VideoLink(
val type: String,
val url: String
)
}

View file

@ -38,9 +38,6 @@ class DoodWsExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.ws" override var mainUrl = "https://dood.ws"
} }
class DoodYtExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.yt"
}
open class DoodLaExtractor : ExtractorApi() { open class DoodLaExtractor : ExtractorApi() {
override var name = "DoodStream" override var name = "DoodStream"

View file

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

View file

@ -16,7 +16,26 @@ open class Evoload : ExtractorApi() {
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> { override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val id = url.replace("https://evoload.io/e/", "") // wanted media id val lang = url.substring(0, 2)
val flag =
if (lang == "vo") {
" \uD83C\uDDEC\uD83C\uDDE7"
}
else if (lang == "vf"){
" \uD83C\uDDE8\uD83C\uDDF5"
} else {
""
}
val cleaned_url = if (lang == "ht") { // if url doesn't contain a flag and the url starts with http://
url
} else {
url.substring(2, url.length)
}
//println(lang)
//println(cleaned_url)
val id = cleaned_url.replace("https://evoload.io/e/", "") // wanted media id
val csrv_token = app.get("https://csrv.evosrv.com/captcha?m412548=").text // whatever that is val csrv_token = app.get("https://csrv.evosrv.com/captcha?m412548=").text // whatever that is
val captchaPass = app.get("https://cd2.evosrv.com/html/jsx/e.jsx").text.take(300).split("captcha_pass = '")[1].split("\'")[0] //extract the captcha pass from the js response (located in the 300 first chars) val captchaPass = app.get("https://cd2.evosrv.com/html/jsx/e.jsx").text.take(300).split("captcha_pass = '")[1].split("\'")[0] //extract the captcha pass from the js response (located in the 300 first chars)
val payload = mapOf("code" to id, "csrv_token" to csrv_token, "pass" to captchaPass) val payload = mapOf("code" to id, "csrv_token" to csrv_token, "pass" to captchaPass)
@ -25,9 +44,9 @@ open class Evoload : ExtractorApi() {
return listOf( return listOf(
ExtractorLink( ExtractorLink(
name, name,
name, name + flag,
link, link,
url, cleaned_url,
Qualities.Unknown.value, Qualities.Unknown.value,
) )
) )

View file

@ -5,50 +5,35 @@ import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
import com.lagradost.cloudstream3.utils.getAndUnpack
import org.jsoup.nodes.Document
open class Fastream: ExtractorApi() { class Fastream: ExtractorApi() {
override var mainUrl = "https://fastream.to" override var mainUrl = "https://fastream.to"
override var name = "Fastream" override var name = "Fastream"
override val requiresReferer = false override val requiresReferer = false
suspend fun getstream(
response: Document,
sources: ArrayList<ExtractorLink>): Boolean{ override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val id = Regex("emb\\.html\\?(.*)\\=(enc|)").find(url)?.destructured?.component1() ?: return emptyList()
val sources = mutableListOf<ExtractorLink>()
val response = app.post("$mainUrl/dl",
data = mapOf(
Pair("op","embed"),
Pair("file_code",id),
Pair("auto","1")
)).document
response.select("script").amap { script -> response.select("script").amap { script ->
if (script.data().contains(Regex("eval\\(function\\(p,a,c,k,e,[rd]"))) { if (script.data().contains("sources")) {
val unpacked = getAndUnpack(script.data()) val m3u8regex = Regex("((https:|http:)\\/\\/.*\\.m3u8)")
//val m3u8regex = Regex("((https:|http:)\\/\\/.*\\.m3u8)") val m3u8 = m3u8regex.find(script.data())?.value ?: return@amap
val newm3u8link = unpacked.substringAfter("file:\"").substringBefore("\"")
//val m3u8link = m3u8regex.find(unpacked)?.value ?: return@forEach
generateM3u8( generateM3u8(
name, name,
newm3u8link, m3u8,
mainUrl mainUrl
).forEach { link -> ).forEach { link ->
sources.add(link) sources.add(link)
} }
} }
} }
return true
}
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val sources = ArrayList<ExtractorLink>()
val idregex = Regex("emb.html\\?(.*)=")
if (url.contains(Regex("(emb.html.*fastream)"))) {
val id = idregex.find(url)?.destructured?.component1() ?: ""
val response = app.post("https://fastream.to/dl", allowRedirects = false,
data = mapOf(
"op" to "embed",
"file_code" to id,
"auto" to "1"
)
).document
getstream(response, sources)
}
val response = app.get(url, referer = url).document
getstream(response, sources)
return sources return sources
} }
} }

View file

@ -1,57 +1,38 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.SubtitleFile import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
class Filesim : ExtractorApi() {
class Ztreamhub : Filesim() {
override val mainUrl: String = "https://ztreamhub.com" //Here 'cause works
override val name = "Zstreamhub"
}
class FileMoon : Filesim() {
override val mainUrl = "https://filemoon.to"
override val name = "FileMoon"
}
class FileMoonSx : Filesim() {
override val mainUrl = "https://filemoon.sx"
override val name = "FileMoonSx"
}
open class Filesim : ExtractorApi() {
override val name = "Filesim" override val name = "Filesim"
override val mainUrl = "https://files.im" override val mainUrl = "https://files.im"
override val requiresReferer = false override val requiresReferer = false
override suspend fun getUrl( override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
url: String, val sources = mutableListOf<ExtractorLink>()
referer: String?, with(app.get(url).document) {
subtitleCallback: (SubtitleFile) -> Unit, this.select("script").map { script ->
callback: (ExtractorLink) -> Unit if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
) { val data = getAndUnpack(script.data()).substringAfter("sources:[").substringBefore("]")
val response = app.get(url, referer = mainUrl).document tryParseJson<List<ResponseSource>>("[$data]")?.map {
response.select("script[type=text/javascript]").map { script -> M3u8Helper.generateM3u8(
if (script.data().contains(Regex("eval\\(function\\(p,a,c,k,e,[rd]"))) {
val unpackedscript = getAndUnpack(script.data())
val m3u8Regex = Regex("file.\\\"(.*?m3u8.*?)\\\"")
val m3u8 = m3u8Regex.find(unpackedscript)?.destructured?.component1() ?: ""
if (m3u8.isNotEmpty()) {
generateM3u8(
name, name,
m3u8, it.file,
mainUrl "$mainUrl/",
).forEach(callback) ).forEach { m3uData -> sources.add(m3uData) }
} }
} }
} }
} }
return sources
}
/* private data class ResponseSource( private data class ResponseSource(
@JsonProperty("file") val file: String, @JsonProperty("file") val file: String,
@JsonProperty("type") val type: String?, @JsonProperty("type") val type: String?,
@JsonProperty("label") val label: String? @JsonProperty("label") val label: String?
) */ )
} }

View file

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

View file

@ -6,11 +6,6 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
class Vanfem : GuardareStream() {
override var name = "Vanfem"
override var mainUrl = "https://vanfem.com/"
}
class CineGrabber : GuardareStream() { class CineGrabber : GuardareStream() {
override var name = "CineGrabber" override var name = "CineGrabber"
override var mainUrl = "https://cinegrabber.com" override var mainUrl = "https://cinegrabber.com"

View file

@ -1,53 +1,46 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.getQualityFromName
open class Linkbox : ExtractorApi() { class Linkbox : ExtractorApi() {
override val name = "Linkbox" override val name = "Linkbox"
override val mainUrl = "https://www.linkbox.to" override val mainUrl = "https://www.linkbox.to"
override val requiresReferer = true override val requiresReferer = true
override suspend fun getUrl( override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
url: String, val id = url.substringAfter("id=")
referer: String?, val sources = mutableListOf<ExtractorLink>()
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit app.get("$mainUrl/api/open/get_url?itemId=$id", referer=url).parsedSafe<Responses>()?.data?.rList?.map { link ->
) { sources.add(
val id = Regex("""(?:/f/|/file/|\?id=)(\w+)""").find(url)?.groupValues?.get(1)
app.get("$mainUrl/api/file/detail?itemId=$id", referer = url)
.parsedSafe<Responses>()?.data?.itemInfo?.resolutionList?.map { link ->
callback.invoke(
ExtractorLink( ExtractorLink(
name, name,
name, name,
link.url ?: return@map null, link.url,
url, url,
getQualityFromName(link.resolution) getQualityFromName(link.resolution)
) )
) )
} }
return sources
} }
data class Resolutions( data class RList(
@JsonProperty("url") val url: String? = null, @JsonProperty("url") val url: String,
@JsonProperty("resolution") val resolution: String? = null, @JsonProperty("resolution") val resolution: String?,
)
data class ItemInfo(
@JsonProperty("resolutionList") val resolutionList: ArrayList<Resolutions>? = arrayListOf(),
) )
data class Data( data class Data(
@JsonProperty("itemInfo") val itemInfo: ItemInfo? = null, @JsonProperty("rList") val rList: List<RList>?,
) )
data class Responses( data class Responses(
@JsonProperty("data") val data: Data? = null, @JsonProperty("data") val data: Data?,
) )
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -1,39 +0,0 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
data class Okrulinkdata (
@JsonProperty("status" ) var status : String? = null,
@JsonProperty("url" ) var url : String? = null
)
open class Okrulink: ExtractorApi() {
override var mainUrl = "https://okru.link"
override var name = "Okrulink"
override val requiresReferer = false
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val sources = mutableListOf<ExtractorLink>()
val key = url.substringAfter("html?t=")
val request = app.post("https://apizz.okru.link/decoding", allowRedirects = false,
data = mapOf("video" to key)
).parsedSafe<Okrulinkdata>()
if (request?.url != null) {
sources.add(
ExtractorLink(
name,
name,
request.url!!,
"",
Qualities.Unknown.value,
isM3u8 = false
)
)
}
return sources
}
}

View file

@ -14,7 +14,7 @@ import org.jsoup.Jsoup
* overrideMainUrl is necessary for for other vidstream clones like vidembed.cc * overrideMainUrl is necessary for for other vidstream clones like vidembed.cc
* If they diverge it'd be better to make them separate. * If they diverge it'd be better to make them separate.
* */ * */
open class Pelisplus(val mainUrl: String) { class Pelisplus(val mainUrl: String) {
val name: String = "Vidstream" val name: String = "Vidstream"
private fun getExtractorUrl(id: String): String { private fun getExtractorUrl(id: String): String {

View file

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

View file

@ -1,28 +0,0 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
open class Sendvid : ExtractorApi() {
override var name = "Sendvid"
override val mainUrl = "https://sendvid.com"
override val requiresReferer = false
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val doc = app.get(url).document
val urlString = doc.select("head meta[property=og:video:secure_url]").attr("content")
if (urlString.contains("m3u8")) {
generateM3u8(
name,
urlString,
mainUrl,
).forEach(callback)
}
}
}

View file

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

View file

@ -77,10 +77,6 @@ class StreamSB10 : StreamSB() {
override var mainUrl = "https://sbplay2.xyz" override var mainUrl = "https://sbplay2.xyz"
} }
class StreamSB11 : StreamSB() {
override var mainUrl = "https://sbbrisk.com"
}
// This is a modified version of https://github.com/jmir1/aniyomi-extensions/blob/master/src/en/genoanime/src/eu/kanade/tachiyomi/animeextension/en/genoanime/extractors/StreamSBExtractor.kt // This is a modified version of https://github.com/jmir1/aniyomi-extensions/blob/master/src/en/genoanime/src/eu/kanade/tachiyomi/animeextension/en/genoanime/extractors/StreamSBExtractor.kt
// The following code is under the Apache License 2.0 https://github.com/jmir1/aniyomi-extensions/blob/master/LICENSE // The following code is under the Apache License 2.0 https://github.com/jmir1/aniyomi-extensions/blob/master/LICENSE
open class StreamSB : ExtractorApi() { open class StreamSB : ExtractorApi() {
@ -134,7 +130,7 @@ open class StreamSB : ExtractorApi() {
it.value.replace(Regex("(embed-|/e/)"), "") it.value.replace(Regex("(embed-|/e/)"), "")
}.first() }.first()
// val master = "$mainUrl/sources48/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362" // val master = "$mainUrl/sources48/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362"
val master = "$mainUrl/sources15/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/" val master = "$mainUrl/sources48/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/"
val headers = mapOf( val headers = mapOf(
"watchsb" to "sbstream", "watchsb" to "sbstream",
) )

View file

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

View file

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

View file

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

View file

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

View file

@ -1,64 +1,41 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.app
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.readValue import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.mapper
import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
class Cinestart: Tomatomatela() { class Cinestart: Tomatomatela() {
override var name: String = "Cinestart" override var name = "Cinestart"
override val mainUrl: String = "https://cinestart.net" override var mainUrl = "https://cinestart.net"
override val details = "vr.php?v=" override val details = "vr.php?v="
} }
class TomatomatelalClub: Tomatomatela() {
override var name: String = "Tomatomatela"
override val mainUrl: String = "https://tomatomatela.club"
}
open class Tomatomatela : ExtractorApi() { open class Tomatomatela : ExtractorApi() {
override var name = "Tomatomatela" override var name = "Tomatomatela"
override val mainUrl = "https://tomatomatela.com" override var mainUrl = "https://tomatomatela.com"
override val requiresReferer = false override val requiresReferer = false
private data class Tomato ( private data class Tomato (
@JsonProperty("status") val status: Int, @JsonProperty("status") val status: Int,
@JsonProperty("file") val file: String? @JsonProperty("file") val file: String
) )
open val details = "details.php?v=" open val details = "details.php?v="
open val embeddetails = "/embed.html#"
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? { override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val link = url.replace("$mainUrl$embeddetails","$mainUrl/$details") val link = url.replace("$mainUrl/embed.html#","$mainUrl/$details")
val sources = ArrayList<ExtractorLink>() val server = app.get(link, allowRedirects = false).text
val server = app.get(link, allowRedirects = false, val json = parseJson<Tomato>(server)
headers = mapOf( if (json.status == 200) return listOf(
"User-Agent" to USER_AGENT,
"Accept" to "application/json, text/javascript, */*; q=0.01",
"Accept-Language" to "en-US,en;q=0.5",
"X-Requested-With" to "XMLHttpRequest",
"DNT" to "1",
"Connection" to "keep-alive",
"Sec-Fetch-Dest" to "empty",
"Sec-Fetch-Mode" to "cors",
"Sec-Fetch-Site" to "same-origin"
)
).parsedSafe<Tomato>()
if (server?.file != null) {
sources.add(
ExtractorLink( ExtractorLink(
name, name,
name, name,
server.file, json.file,
"", "",
Qualities.Unknown.value, Qualities.Unknown.value,
isM3u8 = false isM3u8 = false
) )
) )
} return null
return sources
} }
} }

View file

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

View file

@ -7,10 +7,6 @@ class Uqload1 : Uqload() {
override var mainUrl = "https://uqload.com" override var mainUrl = "https://uqload.com"
} }
class Uqload2 : Uqload() {
override var mainUrl = "https://uqload.co"
}
open class Uqload : ExtractorApi() { open class Uqload : ExtractorApi() {
override val name: String = "Uqload" override val name: String = "Uqload"
override val mainUrl: String = "https://www.uqload.com" override val mainUrl: String = "https://www.uqload.com"
@ -19,14 +15,30 @@ open class Uqload : ExtractorApi() {
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? { override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
with(app.get(url)) { // raised error ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED (3003) is due to the response: "error_nofile" val lang = url.substring(0, 2)
val flag =
if (lang == "vo") {
" \uD83C\uDDEC\uD83C\uDDE7"
}
else if (lang == "vf"){
" \uD83C\uDDE8\uD83C\uDDF5"
} else {
""
}
val cleaned_url = if (lang == "ht") { // if url doesn't contain a flag and the url starts with http://
url
} else {
url.substring(2, url.length)
}
with(app.get(cleaned_url)) { // raised error ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED (3003) is due to the response: "error_nofile"
srcRegex.find(this.text)?.groupValues?.get(1)?.replace("\"", "")?.let { link -> srcRegex.find(this.text)?.groupValues?.get(1)?.replace("\"", "")?.let { link ->
return listOf( return listOf(
ExtractorLink( ExtractorLink(
name, name,
name, name + flag,
link, link,
url, cleaned_url,
Qualities.Unknown.value, Qualities.Unknown.value,
) )
) )

View file

@ -59,8 +59,8 @@ open class VidSrcExtractor : ExtractorApi() {
if (datahash.isNotBlank()) { if (datahash.isNotBlank()) {
val links = try { val links = try {
app.get( app.get(
"$absoluteUrl/srcrcp/$datahash", "$absoluteUrl/src/$datahash",
referer = "https://rcp.vidsrc.me/" referer = "https://source.vidsrc.me/"
).url ).url
} catch (e: Exception) { } catch (e: Exception) {
"" ""
@ -71,7 +71,7 @@ open class VidSrcExtractor : ExtractorApi() {
serverslist.amap { server -> serverslist.amap { server ->
val linkfixed = server.replace("https://vidsrc.xyz/", "https://embedsito.com/") val linkfixed = server.replace("https://vidsrc.xyz/", "https://embedsito.com/")
if (linkfixed.contains("/prorcp")) { if (linkfixed.contains("/pro")) {
val srcresponse = app.get(server, referer = absoluteUrl).text val srcresponse = app.get(server, referer = absoluteUrl).text
val m3u8Regex = Regex("((https:|http:)//.*\\.m3u8)") val m3u8Regex = Regex("((https:|http:)//.*\\.m3u8)")
val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@amap val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@amap

View file

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

View file

@ -1,34 +0,0 @@
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.Qualities
import com.lagradost.cloudstream3.utils.getAndUnpack
class Vido : ExtractorApi() {
override var name = "Vido"
override var mainUrl = "https://vido.lol"
private val srcRegex = Regex("""sources:\s*\["(.*?)"\]""")
override val requiresReferer = true
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val methode = app.get(url.replace("/e/", "/embed-")) // fix wiflix and mesfilms
with(methode) {
if (!methode.isSuccessful) return null
//val quality = unpackedText.lowercase().substringAfter(" height=").substringBefore(" ").toIntOrNull()
srcRegex.find(this.text)?.groupValues?.get(1)?.let { link ->
return listOf(
ExtractorLink(
name,
name,
link,
url,
Qualities.Unknown.value,
true,
)
)
}
}
return null
}
}

View file

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

View file

@ -53,12 +53,6 @@ class VizcloudSite : WcoStream() {
override var mainUrl = "https://vizcloud.site" 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() { open class WcoStream : ExtractorApi() {
override var name = "VidStream" // Cause works for animekisa and wco override var name = "VidStream" // Cause works for animekisa and wco
override var mainUrl = "https://vidstream.pro" override var mainUrl = "https://vidstream.pro"

View file

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

View file

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

View file

@ -0,0 +1,30 @@
package com.lagradost.cloudstream3.metaproviders
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
import com.lagradost.cloudstream3.utils.SyncUtil
object SyncRedirector {
val syncApis = SyncApis
suspend fun redirect(url: String, preferredUrl: String): String {
for (api in syncApis) {
if (url.contains(api.mainUrl)) {
val otherApi = when (api.name) {
aniListApi.name -> "anilist"
malApi.name -> "myanimelist"
else -> return url
}
return SyncUtil.getUrlsFromId(api.getIdFromUrl(url), otherApi).firstOrNull { realUrl ->
realUrl.contains(preferredUrl)
} ?: run {
throw ErrorLoadingException("Page does not exist on $preferredUrl")
}
}
}
return url
}
}

View file

@ -1,56 +0,0 @@
package com.lagradost.cloudstream3.metaproviders
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.SyncIdName
object SyncRedirector {
val syncApis = SyncApis
private val syncIds =
listOf(
SyncIdName.MyAnimeList to Regex("""myanimelist\.net\/anime\/(\d+)"""),
SyncIdName.Anilist to Regex("""anilist\.co\/anime\/(\d+)""")
)
suspend fun redirect(
url: String,
providerApi: MainAPI
): String {
// Deprecated since providers should do this instead!
// Tries built in ID -> ProviderUrl
/*
for (api in syncApis) {
if (url.contains(api.mainUrl)) {
val otherApi = when (api.name) {
aniListApi.name -> "anilist"
malApi.name -> "myanimelist"
else -> return url
}
SyncUtil.getUrlsFromId(api.getIdFromUrl(url), otherApi).firstOrNull { realUrl ->
realUrl.contains(providerApi.mainUrl)
}?.let {
return it
}
// ?: run {
// throw ErrorLoadingException("Page does not exist on $preferredUrl")
// }
}
}
*/
// Tries provider solution
// This goes through all sync ids and finds supported id by said provider
return syncIds.firstNotNullOfOrNull { (syncName, syncRegex) ->
if (providerApi.supportedSyncNames.contains(syncName)) {
syncRegex.find(url)?.value?.let {
suspendSafeApiCall {
providerApi.getLoadUrl(syncName, it)
}
}
} else null
} ?: url
}
}

View file

@ -53,10 +53,6 @@ fun <T> LifecycleOwner.observe(liveData: LiveData<T>, action: (t: T) -> Unit) {
liveData.observe(this) { it?.let { t -> action(t) } } liveData.observe(this) { it?.let { t -> action(t) } }
} }
fun <T> LifecycleOwner.observeNullable(liveData: LiveData<T>, action: (t: T) -> Unit) {
liveData.observe(this) { action(it) }
}
inline fun <reified T : Any> some(value: T?): Some<T> { inline fun <reified T : Any> some(value: T?): Some<T> {
return if (value == null) { return if (value == null) {
Some.None Some.None
@ -121,21 +117,13 @@ suspend fun <T> suspendSafeApiCall(apiCall: suspend () -> T): T? {
} }
} }
fun Throwable.getAllMessages(): String { fun <T> safeFail(throwable: Throwable): Resource<T> {
return (this.localizedMessage ?: "") + (this.cause?.getAllMessages()?.let { "\n$it" } ?: "") val stackTraceMsg =
} (throwable.localizedMessage ?: "") + "\n\n" + throwable.stackTrace.joinToString(
fun Throwable.getStackTracePretty(showMessage: Boolean = true): String {
val prefix = if (showMessage) this.localizedMessage?.let { "\n$it" } ?: "" else ""
return prefix + this.stackTrace.joinToString(
separator = "\n" separator = "\n"
) { ) {
"${it.fileName} ${it.lineNumber}" "${it.fileName} ${it.lineNumber}"
} }
}
fun <T> safeFail(throwable: Throwable): Resource<T> {
val stackTraceMsg = throwable.getStackTracePretty()
return Resource.Failure(false, null, null, stackTraceMsg) return Resource.Failure(false, null, null, stackTraceMsg)
} }

View file

@ -5,7 +5,6 @@ import android.webkit.CookieManager
import androidx.annotation.AnyThread import androidx.annotation.AnyThread
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.debugWarning import com.lagradost.cloudstream3.mvvm.debugWarning
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.nicehttp.Requests.Companion.await import com.lagradost.nicehttp.Requests.Companion.await
import com.lagradost.nicehttp.cookies import com.lagradost.nicehttp.cookies
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -27,11 +26,8 @@ class CloudflareKiller : Interceptor {
init { init {
// Needs to clear cookies between sessions to generate new cookies. // Needs to clear cookies between sessions to generate new cookies.
normalSafeApiCall {
// This can throw an exception on unsupported devices :(
CookieManager.getInstance().removeAllCookies(null) CookieManager.getInstance().removeAllCookies(null)
} }
}
val savedCookies: MutableMap<String, Map<String, String>> = mutableMapOf() val savedCookies: MutableMap<String, Map<String, String>> = mutableMapOf()
@ -64,9 +60,7 @@ class CloudflareKiller : Interceptor {
} }
private fun getWebViewCookie(url: String): String? { private fun getWebViewCookie(url: String): String? {
return normalSafeApiCall { return CookieManager.getInstance()?.getCookie(url)
CookieManager.getInstance()?.getCookie(url)
}
} }
/** /**

View file

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

View file

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

View file

@ -10,18 +10,14 @@ import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.fragment.app.FragmentActivity
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.google.gson.Gson import com.google.gson.Gson
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
import com.lagradost.cloudstream3.APIHolder.removePluginMapping import com.lagradost.cloudstream3.APIHolder.removePluginMapping
import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.mvvm.debugPrint import com.lagradost.cloudstream3.mvvm.debugPrint
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
@ -30,8 +26,6 @@ import com.lagradost.cloudstream3.plugins.RepositoryManager.ONLINE_PLUGINS_FOLDE
import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES
import com.lagradost.cloudstream3.plugins.RepositoryManager.downloadPluginToFile import com.lagradost.cloudstream3.plugins.RepositoryManager.downloadPluginToFile
import com.lagradost.cloudstream3.plugins.RepositoryManager.getRepoPlugins 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.REPOSITORIES_KEY
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.main
@ -145,10 +139,8 @@ object PluginManager {
return getKey(PLUGINS_KEY_LOCAL) ?: emptyArray() return getKey(PLUGINS_KEY_LOCAL) ?: emptyArray()
} }
private val CLOUD_STREAM_FOLDER = private val LOCAL_PLUGINS_PATH =
Environment.getExternalStorageDirectory().absolutePath + "/Cloudstream3/" Environment.getExternalStorageDirectory().absolutePath + "/Cloudstream3/plugins"
private val LOCAL_PLUGINS_PATH = CLOUD_STREAM_FOLDER + "plugins"
public var currentlyLoading: String? = null public var currentlyLoading: String? = null
@ -166,11 +158,11 @@ object PluginManager {
private var loadedLocalPlugins = false private var loadedLocalPlugins = false
private val gson = Gson() private val gson = Gson()
private suspend fun maybeLoadPlugin(context: Context, file: File) { private suspend fun maybeLoadPlugin(activity: Activity, file: File) {
val name = file.name val name = file.name
if (file.extension == "zip" || file.extension == "cs3") { if (file.extension == "zip" || file.extension == "cs3") {
loadPlugin( loadPlugin(
context, activity,
file, file,
PluginData(name, null, false, file.absolutePath, PLUGIN_VERSION_NOT_SET) PluginData(name, null, false, file.absolutePath, PLUGIN_VERSION_NOT_SET)
) )
@ -200,7 +192,7 @@ object PluginManager {
// var allCurrentOutDatedPlugins: Set<OnlinePluginData> = emptySet() // var allCurrentOutDatedPlugins: Set<OnlinePluginData> = emptySet()
suspend fun loadSinglePlugin(context: Context, apiName: String): Boolean { suspend fun loadSinglePlugin(activity: Activity, apiName: String): Boolean {
return (getPluginsOnline().firstOrNull { return (getPluginsOnline().firstOrNull {
// Most of the time the provider ends with Provider which isn't part of the api name // Most of the time the provider ends with Provider which isn't part of the api name
it.internalName.replace("provider", "", ignoreCase = true) == apiName it.internalName.replace("provider", "", ignoreCase = true) == apiName
@ -210,7 +202,7 @@ object PluginManager {
})?.let { savedData -> })?.let { savedData ->
// OnlinePluginData(savedData, onlineData) // OnlinePluginData(savedData, onlineData)
loadPlugin( loadPlugin(
context, activity,
File(savedData.filePath), File(savedData.filePath),
savedData savedData
) )
@ -227,7 +219,9 @@ object PluginManager {
fun updateAllOnlinePluginsAndLoadThem(activity: Activity) { fun updateAllOnlinePluginsAndLoadThem(activity: Activity) {
// Load all plugins as fast as possible! // Load all plugins as fast as possible!
loadAllOnlinePlugins(activity) loadAllOnlinePlugins(activity)
afterPluginsLoadedEvent.invoke(false)
afterPluginsLoadedEvent.invoke(true)
val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY) val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY)
?: emptyArray()) + PREBUILT_REPOSITORIES ?: emptyArray()) + PREBUILT_REPOSITORIES
@ -258,12 +252,11 @@ object PluginManager {
//updatedPlugins.add(activity.getString(R.string.single_plugin_disabled, pluginData.onlineData.second.name)) //updatedPlugins.add(activity.getString(R.string.single_plugin_disabled, pluginData.onlineData.second.name))
unloadPlugin(pluginData.savedData.filePath) unloadPlugin(pluginData.savedData.filePath)
} else if (pluginData.isOutdated) { } else if (pluginData.isOutdated) {
downloadPlugin( downloadAndLoadPlugin(
activity, activity,
pluginData.onlineData.second.url, pluginData.onlineData.second.url,
pluginData.savedData.internalName, pluginData.savedData.internalName,
File(pluginData.savedData.filePath), File(pluginData.savedData.filePath)
true
).let { success -> ).let { success ->
if (success) if (success)
updatedPlugins.add(pluginData.onlineData.second.name) updatedPlugins.add(pluginData.onlineData.second.name)
@ -272,134 +265,31 @@ object PluginManager {
} }
main { main {
val uitext = txt(R.string.plugins_updated, updatedPlugins.size) createNotification(activity, updatedPlugins)
createNotification(activity, uitext, updatedPlugins)
} }
// ioSafe { // ioSafe {
afterPluginsLoadedEvent.invoke(false) afterPluginsLoadedEvent.invoke(true)
// } // }
Log.i(TAG, "Plugin update done!") 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 ->
downloadPlugin(
activity,
pluginData.onlineData.second.url,
pluginData.savedData.internalName,
pluginData.onlineData.first,
!pluginData.isDisabled
).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 * Use updateAllOnlinePluginsAndLoadThem
* */ * */
fun loadAllOnlinePlugins(context: Context) { fun loadAllOnlinePlugins(activity: Activity) {
// Load all plugins as fast as possible! // Load all plugins as fast as possible!
(getPluginsOnline()).toList().apmap { pluginData -> (getPluginsOnline()).toList().apmap { pluginData ->
loadPlugin( loadPlugin(
context, activity,
File(pluginData.filePath), File(pluginData.filePath),
pluginData pluginData
) )
} }
} }
/** 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(context: Context, forceReload: Boolean) {
val dir = File(LOCAL_PLUGINS_PATH) val dir = File(LOCAL_PLUGINS_PATH)
removeKey(PLUGINS_KEY_LOCAL) removeKey(PLUGINS_KEY_LOCAL)
@ -417,39 +307,24 @@ object PluginManager {
Log.d(TAG, "Files in '${LOCAL_PLUGINS_PATH}' folder: $sortedPlugins") Log.d(TAG, "Files in '${LOCAL_PLUGINS_PATH}' folder: $sortedPlugins")
sortedPlugins?.sortedBy { it.name }?.apmap { file -> sortedPlugins?.sortedBy { it.name }?.apmap { file ->
maybeLoadPlugin(context, file) maybeLoadPlugin(activity, file)
} }
loadedLocalPlugins = true loadedLocalPlugins = true
afterPluginsLoadedEvent.invoke(forceReload) afterPluginsLoadedEvent.invoke(true)
}
/**
* This can be used to override any extension loading to fix crashes!
* @return true if safe mode file is present
**/
fun checkSafeModeFile(): Boolean {
return normalSafeApiCall {
val folder = File(CLOUD_STREAM_FOLDER)
if (!folder.exists()) return@normalSafeApiCall false
val files = folder.listFiles { _, name ->
name.equals("safe", ignoreCase = true)
}
files?.any()
} ?: false
} }
/** /**
* @return True if successful, false if not * @return True if successful, false if not
* */ * */
private suspend fun loadPlugin(context: Context, file: File, data: PluginData): Boolean { private suspend fun loadPlugin(activity: Activity, file: File, data: PluginData): Boolean {
val fileName = file.nameWithoutExtension val fileName = file.nameWithoutExtension
val filePath = file.absolutePath val filePath = file.absolutePath
currentlyLoading = fileName currentlyLoading = fileName
Log.i(TAG, "Loading plugin: $data") Log.i(TAG, "Loading plugin: $data")
return try { return try {
val loader = PathClassLoader(filePath, context.classLoader) val loader = PathClassLoader(filePath, activity.classLoader)
var manifest: Plugin.Manifest var manifest: Plugin.Manifest
loader.getResourceAsStream("manifest.json").use { stream -> loader.getResourceAsStream("manifest.json").use { stream ->
if (stream == null) { if (stream == null) {
@ -493,22 +368,22 @@ object PluginManager {
addAssetPath.invoke(assets, file.absolutePath) addAssetPath.invoke(assets, file.absolutePath)
pluginInstance.resources = Resources( pluginInstance.resources = Resources(
assets, assets,
context.resources.displayMetrics, activity.resources.displayMetrics,
context.resources.configuration activity.resources.configuration
) )
} }
plugins[filePath] = pluginInstance plugins[filePath] = pluginInstance
classLoaders[loader] = pluginInstance classLoaders[loader] = pluginInstance
urlPlugins[data.url ?: filePath] = pluginInstance urlPlugins[data.url ?: filePath] = pluginInstance
pluginInstance.load(context) pluginInstance.load(activity)
Log.i(TAG, "Loaded plugin ${data.internalName} successfully") Log.i(TAG, "Loaded plugin ${data.internalName} successfully")
currentlyLoading = null currentlyLoading = null
true true
} catch (e: Throwable) { } catch (e: Throwable) {
Log.e(TAG, "Failed to load $file: ${Log.getStackTraceString(e)}") Log.e(TAG, "Failed to load $file: ${Log.getStackTraceString(e)}")
showToast( showToast(
context.getActivity(), activity,
context.getString(R.string.plugin_load_fail).format(fileName), activity.getString(R.string.plugin_load_fail).format(fileName),
Toast.LENGTH_LONG Toast.LENGTH_LONG
) )
currentlyLoading = null currentlyLoading = null
@ -516,7 +391,7 @@ object PluginManager {
} }
} }
fun unloadPlugin(absolutePath: String) { private fun unloadPlugin(absolutePath: String) {
Log.i(TAG, "Unloading plugin: $absolutePath") Log.i(TAG, "Unloading plugin: $absolutePath")
val plugin = plugins[absolutePath] val plugin = plugins[absolutePath]
if (plugin == null) { if (plugin == null) {
@ -567,48 +442,49 @@ object PluginManager {
return File("${context.filesDir}/${ONLINE_PLUGINS_FOLDER}/${folderName}/$fileName.cs3") return File("${context.filesDir}/${ONLINE_PLUGINS_FOLDER}/${folderName}/$fileName.cs3")
} }
suspend fun downloadPlugin( /**
* Used for fresh installs
* */
suspend fun downloadAndLoadPlugin(
activity: Activity, activity: Activity,
pluginUrl: String, pluginUrl: String,
internalName: String, internalName: String,
repositoryUrl: String, repositoryUrl: String
loadPlugin: Boolean
): Boolean { ): Boolean {
val file = getPluginPath(activity, internalName, repositoryUrl) val file = getPluginPath(activity, internalName, repositoryUrl)
return downloadPlugin(activity, pluginUrl, internalName, file, loadPlugin) downloadAndLoadPlugin(activity, pluginUrl, internalName, file)
return true
} }
suspend fun downloadPlugin( /**
* Used for updates.
*
* Uses a file instead of repository url, as extensions can get moved it is better to directly
* update the files instead of getting the filepath from repo url.
* */
private suspend fun downloadAndLoadPlugin(
activity: Activity, activity: Activity,
pluginUrl: String, pluginUrl: String,
internalName: String, internalName: String,
file: File, file: File,
loadPlugin: Boolean
): Boolean { ): Boolean {
try { try {
unloadPlugin(file.absolutePath)
Log.d(TAG, "Downloading plugin: $pluginUrl to ${file.absolutePath}") Log.d(TAG, "Downloading plugin: $pluginUrl to ${file.absolutePath}")
// The plugin file needs to be salted with the repository url hash as to allow multiple repositories with the same internal plugin names // The plugin file needs to be salted with the repository url hash as to allow multiple repositories with the same internal plugin names
val newFile = downloadPluginToFile(pluginUrl, file) ?: return false val newFile = downloadPluginToFile(pluginUrl, file)
return loadPlugin(
val data = PluginData( activity,
newFile ?: return false,
PluginData(
internalName, internalName,
pluginUrl, pluginUrl,
true, true,
newFile.absolutePath, newFile.absolutePath,
PLUGIN_VERSION_NOT_SET PLUGIN_VERSION_NOT_SET
) )
return if (loadPlugin) {
unloadPlugin(file.absolutePath)
loadPlugin(
activity,
newFile,
data
) )
} else {
setPluginData(data)
true
}
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
return false return false
@ -616,8 +492,7 @@ object PluginManager {
} }
suspend fun deletePlugin(file: File): Boolean { suspend fun deletePlugin(file: File): Boolean {
val list = val list = (getPluginsLocal() + getPluginsOnline()).filter { it.filePath == file.absolutePath }
(getPluginsLocal() + getPluginsOnline()).filter { it.filePath == file.absolutePath }
return try { return try {
if (File(file.absolutePath).delete()) { if (File(file.absolutePath).delete()) {
@ -652,14 +527,12 @@ object PluginManager {
private fun createNotification( private fun createNotification(
context: Context, context: Context,
uitext: UiText, extensionNames: List<String>
extensions: List<String>
): Notification? { ): Notification? {
try { try {
if (extensionNames.isEmpty()) return null
if (extensions.isEmpty()) return null val content = extensionNames.joinToString(", ")
val content = extensions.joinToString(", ")
// main { // DON'T WANT TO SLOW IT DOWN // main { // DON'T WANT TO SLOW IT DOWN
val builder = NotificationCompat.Builder(context, EXTENSIONS_CHANNEL_ID) val builder = NotificationCompat.Builder(context, EXTENSIONS_CHANNEL_ID)
.setAutoCancel(false) .setAutoCancel(false)
@ -668,8 +541,7 @@ object PluginManager {
.setSilent(true) .setSilent(true)
.setPriority(NotificationCompat.PRIORITY_LOW) .setPriority(NotificationCompat.PRIORITY_LOW)
.setColor(context.colorFromAttribute(R.attr.colorPrimary)) .setColor(context.colorFromAttribute(R.attr.colorPrimary))
.setContentTitle(uitext.asString(context)) .setContentTitle(context.getString(R.string.plugins_updated, extensionNames.size))
//.setContentTitle(context.getString(title, extensionNames.size))
.setSmallIcon(R.drawable.ic_baseline_extension_24) .setSmallIcon(R.drawable.ic_baseline_extension_24)
.setStyle( .setStyle(
NotificationCompat.BigTextStyle() NotificationCompat.BigTextStyle()

View file

@ -2,17 +2,13 @@ package com.lagradost.cloudstream3.plugins
import android.content.Context import android.content.Context
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.plugins.PluginManager.getPluginSanitizedFileName import com.lagradost.cloudstream3.plugins.PluginManager.getPluginSanitizedFileName
import com.lagradost.cloudstream3.plugins.PluginManager.unloadPlugin
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
@ -73,15 +69,6 @@ object RepositoryManager {
val PREBUILT_REPOSITORIES: Array<RepositoryData> by lazy { val PREBUILT_REPOSITORIES: Array<RepositoryData> by lazy {
getKey("PREBUILT_REPOSITORIES") ?: emptyArray() getKey("PREBUILT_REPOSITORIES") ?: emptyArray()
} }
val GH_REGEX = Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$")
/* Convert raw.githubusercontent.com urls to cdn.jsdelivr.net if enabled in settings */
fun convertRawGitUrl(url: String): String {
if (getKey<Boolean>(context!!.getString(R.string.jsdelivr_proxy_key)) != true) return url
val match = GH_REGEX.find(url) ?: return url
val (user, repo, rest) = match.destructured
return "https://cdn.jsdelivr.net/gh/$user/$repo@$rest"
}
suspend fun parseRepoUrl(url: String): String? { suspend fun parseRepoUrl(url: String): String? {
val fixedUrl = url.trim() val fixedUrl = url.trim()
@ -95,15 +82,10 @@ object RepositoryManager {
} }
} else if (fixedUrl.matches("^[a-zA-Z0-9!_-]+$".toRegex())) { } else if (fixedUrl.matches("^[a-zA-Z0-9!_-]+$".toRegex())) {
suspendSafeApiCall { suspendSafeApiCall {
app.get("https://l.cloudstream.cf/${fixedUrl}", allowRedirects = false).let { app.get("https://l.cloudstream.cf/${fixedUrl}").let {
it.headers["Location"]?.let { url -> return@let if (it.isSuccessful && !it.url.startsWith("https://cutt.ly/branded-domains")) it.url
return@suspendSafeApiCall if (!url.startsWith("https://cutt.ly/branded-domains")) url else app.get("https://cutt.ly/${fixedUrl}").let let2@{ it2 ->
else null return@let2 if (it2.isSuccessful) it2.url else null
}
app.get("https://cutt.ly/${fixedUrl}", allowRedirects = false).let { it2 ->
it2.headers["Location"]?.let { url ->
return@suspendSafeApiCall if (url.startsWith("https://cutt.ly/404")) url else null
}
} }
} }
} }
@ -113,14 +95,14 @@ object RepositoryManager {
suspend fun parseRepository(url: String): Repository? { suspend fun parseRepository(url: String): Repository? {
return suspendSafeApiCall { return suspendSafeApiCall {
// Take manifestVersion and such into account later // Take manifestVersion and such into account later
app.get(convertRawGitUrl(url)).parsedSafe() app.get(url).parsedSafe()
} }
} }
private suspend fun parsePlugins(pluginUrls: String): List<SitePlugin> { private suspend fun parsePlugins(pluginUrls: String): List<SitePlugin> {
// Take manifestVersion and such into account later // Take manifestVersion and such into account later
return try { return try {
val response = app.get(convertRawGitUrl(pluginUrls)) val response = app.get(pluginUrls)
// Normal parsed function not working? // Normal parsed function not working?
// return response.parsedSafe() // return response.parsedSafe()
tryParseJson<Array<SitePlugin>>(response.text)?.toList() ?: emptyList() tryParseJson<Array<SitePlugin>>(response.text)?.toList() ?: emptyList()
@ -150,17 +132,43 @@ object RepositoryManager {
file.mkdirs() file.mkdirs()
// Overwrite if exists // Overwrite if exists
if (file.exists()) { if (file.exists()) { file.delete() }
file.delete()
}
file.createNewFile() file.createNewFile()
val body = app.get(convertRawGitUrl(pluginUrl)).okhttpResponse.body val body = app.get(pluginUrl).okhttpResponse.body
write(body.byteStream(), file.outputStream()) write(body.byteStream(), file.outputStream())
file file
} }
} }
suspend fun downloadPluginToFile(
context: Context,
pluginUrl: String,
/** Filename without .cs3 */
fileName: String,
folder: String
): File? {
return suspendSafeApiCall {
val extensionsDir = File(context.filesDir, ONLINE_PLUGINS_FOLDER)
if (!extensionsDir.exists())
extensionsDir.mkdirs()
val newDir = File(extensionsDir, folder)
newDir.mkdirs()
val newFile = File(newDir, "${fileName}.cs3")
// Overwrite if exists
if (newFile.exists()) {
newFile.delete()
}
newFile.createNewFile()
val body = app.get(pluginUrl).okhttpResponse.body
write(body.byteStream(), newFile.outputStream())
newFile
}
}
fun getRepositories(): Array<RepositoryData> { fun getRepositories(): Array<RepositoryData> {
return getKey(REPOSITORIES_KEY) ?: emptyArray() return getKey(REPOSITORIES_KEY) ?: emptyArray()
} }
@ -192,17 +200,9 @@ object RepositoryManager {
extensionsDir, extensionsDir,
getPluginSanitizedFileName(repository.url) getPluginSanitizedFileName(repository.url)
) )
// Unload all plugins, not using deletePlugin since we
// delete all data and files in deleteRepositoryData
normalSafeApiCall {
file.listFiles { plugin: File ->
unloadPlugin(plugin.absolutePath)
false
}
}
PluginManager.deleteRepositoryData(file.absolutePath) PluginManager.deleteRepositoryData(file.absolutePath)
file.delete()
} }
private fun write(stream: InputStream, output: OutputStream) { private fun write(stream: InputStream, output: OutputStream) {

View file

@ -1,224 +0,0 @@
package com.lagradost.cloudstream3.services
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.net.toUri
import androidx.work.*
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
import com.lagradost.cloudstream3.utils.DataStoreHelper.getDub
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getImageBitmapFromUrl
import kotlinx.coroutines.withTimeoutOrNull
import java.util.concurrent.TimeUnit
const val SUBSCRIPTION_CHANNEL_ID = "cloudstream3.subscriptions"
const val SUBSCRIPTION_WORK_NAME = "work_subscription"
const val SUBSCRIPTION_CHANNEL_NAME = "Subscriptions"
const val SUBSCRIPTION_CHANNEL_DESCRIPTION = "Notifications for new episodes on subscribed shows"
const val SUBSCRIPTION_NOTIFICATION_ID = 938712897 // Random unique
class SubscriptionWorkManager(val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
companion object {
fun enqueuePeriodicWork(context: Context?) {
if (context == null) return
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val periodicSyncDataWork =
PeriodicWorkRequest.Builder(SubscriptionWorkManager::class.java, 6, TimeUnit.HOURS)
.addTag(SUBSCRIPTION_WORK_NAME)
.setConstraints(constraints)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
SUBSCRIPTION_WORK_NAME,
ExistingPeriodicWorkPolicy.KEEP,
periodicSyncDataWork
)
// Uncomment below for testing
// val oneTimeSyncDataWork =
// OneTimeWorkRequest.Builder(SubscriptionWorkManager::class.java)
// .addTag(SUBSCRIPTION_WORK_NAME)
// .setConstraints(constraints)
// .build()
//
// WorkManager.getInstance(context).enqueue(oneTimeSyncDataWork)
}
}
private val progressNotificationBuilder =
NotificationCompat.Builder(context, SUBSCRIPTION_CHANNEL_ID)
.setAutoCancel(false)
.setColorized(true)
.setOnlyAlertOnce(true)
.setSilent(true)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setColor(context.colorFromAttribute(R.attr.colorPrimary))
.setContentTitle(context.getString(R.string.subscription_in_progress_notification))
.setSmallIcon(R.drawable.quantum_ic_refresh_white_24)
.setProgress(0, 0, true)
private val updateNotificationBuilder =
NotificationCompat.Builder(context, SUBSCRIPTION_CHANNEL_ID)
.setColorized(true)
.setOnlyAlertOnce(true)
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setColor(context.colorFromAttribute(R.attr.colorPrimary))
.setSmallIcon(R.drawable.ic_cloudstream_monochrome_big)
private val notificationManager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private fun updateProgress(max: Int, progress: Int, indeterminate: Boolean) {
notificationManager.notify(
SUBSCRIPTION_NOTIFICATION_ID, progressNotificationBuilder
.setProgress(max, progress, indeterminate)
.build()
)
}
override suspend fun doWork(): Result {
// println("Update subscriptions!")
context.createNotificationChannel(
SUBSCRIPTION_CHANNEL_ID,
SUBSCRIPTION_CHANNEL_NAME,
SUBSCRIPTION_CHANNEL_DESCRIPTION
)
setForeground(
ForegroundInfo(
SUBSCRIPTION_NOTIFICATION_ID,
progressNotificationBuilder.build()
)
)
val subscriptions = getAllSubscriptions()
if (subscriptions.isEmpty()) {
WorkManager.getInstance(context).cancelWorkById(this.id)
return Result.success()
}
val max = subscriptions.size
var progress = 0
updateProgress(max, progress, true)
// We need all plugins loaded.
PluginManager.loadAllOnlinePlugins(context)
PluginManager.loadAllLocalPlugins(context, false)
subscriptions.apmap { savedData ->
try {
val id = savedData.id ?: return@apmap null
val api = getApiFromNameNull(savedData.apiName) ?: return@apmap null
// Reasonable timeout to prevent having this worker run forever.
val response = withTimeoutOrNull(60_000) {
api.load(savedData.url) as? EpisodeResponse
} ?: return@apmap null
val dubPreference =
getDub(id) ?: if (
context.getApiDubstatusSettings().contains(DubStatus.Dubbed)
) {
DubStatus.Dubbed
} else {
DubStatus.Subbed
}
val latestEpisodes = response.getLatestEpisodes()
val latestPreferredEpisode = latestEpisodes[dubPreference]
val (shouldUpdate, latestEpisode) = if (latestPreferredEpisode != null) {
val latestSeenEpisode =
savedData.lastSeenEpisodeCount[dubPreference] ?: Int.MIN_VALUE
val shouldUpdate = latestPreferredEpisode > latestSeenEpisode
shouldUpdate to latestPreferredEpisode
} else {
val latestEpisode = latestEpisodes[DubStatus.None] ?: Int.MIN_VALUE
val latestSeenEpisode =
savedData.lastSeenEpisodeCount[DubStatus.None] ?: Int.MIN_VALUE
val shouldUpdate = latestEpisode > latestSeenEpisode
shouldUpdate to latestEpisode
}
DataStoreHelper.updateSubscribedData(
id,
savedData,
response
)
if (shouldUpdate) {
val updateHeader = savedData.name
val updateDescription = txt(
R.string.subscription_episode_released,
latestEpisode,
savedData.name
).asString(context)
val intent = Intent(context, MainActivity::class.java).apply {
data = savedData.url.toUri()
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_IMMUTABLE
)
} else {
PendingIntent.getActivity(context, 0, intent, 0)
}
val poster = ioWork {
savedData.posterUrl?.let { url ->
context.getImageBitmapFromUrl(
url,
savedData.posterHeaders
)
}
}
val updateNotification =
updateNotificationBuilder.setContentTitle(updateHeader)
.setContentText(updateDescription)
.setContentIntent(pendingIntent)
.setLargeIcon(poster)
.build()
notificationManager.notify(id, updateNotification)
}
// You can probably get some issues here since this is async but it does not matter much.
updateProgress(max, ++progress, false)
} catch (_: Throwable) {
}
}
return Result.success()
}
}

View file

@ -1,22 +1,11 @@
package com.lagradost.cloudstream3.services package com.lagradost.cloudstream3.services
import android.app.Service
import android.app.IntentService
import android.content.Intent import android.content.Intent
import android.os.IBinder
import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.VideoDownloadManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
class VideoDownloadService : Service() { class VideoDownloadService : IntentService("VideoDownloadService") {
override fun onHandleIntent(intent: Intent?) {
private val downloadScope = CoroutineScope(Dispatchers.Default)
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent != null) { if (intent != null) {
val id = intent.getIntExtra("id", -1) val id = intent.getIntExtra("id", -1)
val type = intent.getStringExtra("type") val type = intent.getStringExtra("type")
@ -25,36 +14,10 @@ class VideoDownloadService : Service() {
"resume" -> VideoDownloadManager.DownloadActionType.Resume "resume" -> VideoDownloadManager.DownloadActionType.Resume
"pause" -> VideoDownloadManager.DownloadActionType.Pause "pause" -> VideoDownloadManager.DownloadActionType.Pause
"stop" -> VideoDownloadManager.DownloadActionType.Stop "stop" -> VideoDownloadManager.DownloadActionType.Stop
else -> return START_NOT_STICKY else -> return
} }
downloadScope.launch {
VideoDownloadManager.downloadEvent.invoke(Pair(id, state)) VideoDownloadManager.downloadEvent.invoke(Pair(id, state))
} }
} }
} }
return START_NOT_STICKY
}
override fun onDestroy() {
downloadScope.coroutineContext.cancel()
super.onDestroy()
}
} }
// override fun onHandleIntent(intent: Intent?) {
// if (intent != null) {
// val id = intent.getIntExtra("id", -1)
// val type = intent.getStringExtra("type")
// if (id != -1 && type != null) {
// val state = when (type) {
// "resume" -> VideoDownloadManager.DownloadActionType.Resume
// "pause" -> VideoDownloadManager.DownloadActionType.Pause
// "stop" -> VideoDownloadManager.DownloadActionType.Stop
// else -> return
// }
// VideoDownloadManager.downloadEvent.invoke(Pair(id, state))
// }
// }
// }
//}

View file

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

View file

@ -1,31 +1,10 @@
package com.lagradost.cloudstream3.syncproviders package com.lagradost.cloudstream3.syncproviders
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.UiText
import me.xdrop.fuzzywuzzy.FuzzySearch
enum class SyncIdName {
Anilist,
MyAnimeList,
Trakt,
Imdb,
LocalList
}
interface SyncAPI : OAuth2API { interface SyncAPI : OAuth2API {
/**
* Set this to true if the user updates something on the list like watch status or score
**/
var requireLibraryRefresh: Boolean
val mainUrl: String val mainUrl: String
/**
* Allows certain providers to open pages from
* library links.
**/
val syncIdName: SyncIdName
/** /**
-1 -> None -1 -> None
0 -> Watching 0 -> Watching
@ -43,9 +22,7 @@ interface SyncAPI : OAuth2API {
suspend fun search(name: String): List<SyncSearchResult>? suspend fun search(name: String): List<SyncSearchResult>?
suspend fun getPersonalLibrary(): LibraryMetadata? fun getIdFromUrl(url : String) : String
fun getIdFromUrl(url: String): String
data class SyncSearchResult( data class SyncSearchResult(
override val name: String, override val name: String,
@ -65,7 +42,7 @@ interface SyncAPI : OAuth2API {
val score: Int?, val score: Int?,
val watchedEpisodes: Int?, val watchedEpisodes: Int?,
var isFavorite: Boolean? = null, var isFavorite: Boolean? = null,
var maxEpisodes: Int? = null, var maxEpisodes : Int? = null,
) )
data class SyncResult( data class SyncResult(
@ -86,9 +63,9 @@ interface SyncAPI : OAuth2API {
var genres: List<String>? = null, var genres: List<String>? = null,
var synonyms: List<String>? = null, var synonyms: List<String>? = null,
var trailers: List<String>? = null, var trailers: List<String>? = null,
var isAdult: Boolean? = null, var isAdult : Boolean? = null,
var posterUrl: String? = null, var posterUrl: String? = null,
var backgroundPosterUrl: String? = null, var backgroundPosterUrl : String? = null,
/** In unixtime */ /** In unixtime */
var startDate: Long? = null, var startDate: Long? = null,
@ -99,61 +76,4 @@ interface SyncAPI : OAuth2API {
var prevSeason: SyncSearchResult? = null, var prevSeason: SyncSearchResult? = null,
var actors: List<ActorData>? = null, var actors: List<ActorData>? = null,
) )
data class Page(
val title: UiText, var items: List<LibraryItem>
) {
fun sort(method: ListSorting?, query: String? = null) {
items = when (method) {
ListSorting.Query ->
if (query != null) {
items.sortedBy {
-FuzzySearch.partialRatio(
query.lowercase(), it.name.lowercase()
)
}
} else items
ListSorting.RatingHigh -> items.sortedBy { -(it.personalRating ?: 0) }
ListSorting.RatingLow -> items.sortedBy { (it.personalRating ?: 0) }
ListSorting.AlphabeticalA -> items.sortedBy { it.name }
ListSorting.AlphabeticalZ -> items.sortedBy { it.name }.reversed()
ListSorting.UpdatedNew -> items.sortedBy { it.lastUpdatedUnixTime?.times(-1) }
ListSorting.UpdatedOld -> items.sortedBy { it.lastUpdatedUnixTime }
else -> items
}
}
}
data class LibraryMetadata(
val allLibraryLists: List<LibraryList>,
val supportedListSorting: Set<ListSorting>
)
data class LibraryList(
val name: UiText,
val items: List<LibraryItem>
)
data class LibraryItem(
override val name: String,
override val url: String,
/**
* Unique unchanging string used for data storage.
* This should be the actual id when you change scores and status
* since score changes from library might get added in the future.
**/
val syncId: String,
val episodesCompleted: Int?,
val episodesTotal: Int?,
/** Out of 100 */
val personalRating: Int?,
val lastUpdatedUnixTime: Long?,
override val apiName: String,
override var type: TvType?,
override var posterUrl: String?,
override var posterHeaders: Map<String, String>?,
override var quality: SearchQuality?,
override var id: Int? = null,
) : SearchResponse
} }

View file

@ -11,38 +11,26 @@ class SyncRepo(private val repo: SyncAPI) {
val icon = repo.icon val icon = repo.icon
val mainUrl = repo.mainUrl val mainUrl = repo.mainUrl
val requiresLogin = repo.requiresLogin val requiresLogin = repo.requiresLogin
val syncIdName = repo.syncIdName
var requireLibraryRefresh: Boolean
get() = repo.requireLibraryRefresh
set(value) {
repo.requireLibraryRefresh = value
}
suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource<Boolean> { suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource<Boolean> {
return safeApiCall { repo.score(id, status) } return safeApiCall { repo.score(id, status) }
} }
suspend fun getStatus(id: String): Resource<SyncAPI.SyncStatus> { suspend fun getStatus(id : String) : Resource<SyncAPI.SyncStatus> {
return safeApiCall { repo.getStatus(id) ?: throw ErrorLoadingException("No data") } return safeApiCall { repo.getStatus(id) ?: throw ErrorLoadingException("No data") }
} }
suspend fun getResult(id: String): Resource<SyncAPI.SyncResult> { suspend fun getResult(id : String) : Resource<SyncAPI.SyncResult> {
return safeApiCall { repo.getResult(id) ?: throw ErrorLoadingException("No data") } return safeApiCall { repo.getResult(id) ?: throw ErrorLoadingException("No data") }
} }
suspend fun search(query: String): Resource<List<SyncAPI.SyncSearchResult>> { suspend fun search(query : String) : Resource<List<SyncAPI.SyncSearchResult>> {
return safeApiCall { repo.search(query) ?: throw ErrorLoadingException() } return safeApiCall { repo.search(query) ?: throw ErrorLoadingException() }
} }
suspend fun getPersonalLibrary(): Resource<SyncAPI.LibraryMetadata> { fun hasAccount() : Boolean {
return safeApiCall { repo.getPersonalLibrary() ?: throw ErrorLoadingException() }
}
fun hasAccount(): Boolean {
return normalSafeApiCall { repo.loginInfo() != null } ?: false return normalSafeApiCall { repo.loginInfo() != null } ?: false
} }
fun getIdFromUrl(url: String): String? = normalSafeApiCall { fun getIdFromUrl(url : String) : String = repo.getIdFromUrl(url)
repo.getIdFromUrl(url)
}
} }

View file

@ -1,108 +0,0 @@
package com.lagradost.cloudstream3.syncproviders.providers
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.subtitles.AbstractSubApi
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
import com.lagradost.cloudstream3.utils.SubtitleHelper
class Addic7ed : AbstractSubApi {
override val name = "Addic7ed"
override val idPrefix = "addic7ed"
override val requiresLogin = false
override val icon: Nothing? = null
override val createAccountUrl: Nothing? = null
override fun loginInfo(): Nothing? = null
override fun logOut() {}
companion object {
const val host = "https://www.addic7ed.com"
const val TAG = "ADDIC7ED"
}
private fun fixUrl(url: String): String {
return if (url.startsWith("/")) host + url
else if (!url.startsWith("http")) "$host/$url"
else url
}
override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List<AbstractSubtitleEntities.SubtitleEntity> {
val lang = query.lang
val queryLang = SubtitleHelper.fromTwoLettersToLanguage(lang.toString())
val queryText = query.query.trim()
val epNum = query.epNumber ?: 0
val seasonNum = query.seasonNumber ?: 0
val yearNum = query.year ?: 0
fun cleanResources(
results: MutableList<AbstractSubtitleEntities.SubtitleEntity>,
name: String,
link: String,
headers: Map<String, String>,
isHearingImpaired: Boolean
) {
results.add(
AbstractSubtitleEntities.SubtitleEntity(
idPrefix = idPrefix,
name = name,
lang = queryLang.toString(),
data = link,
source = this.name,
type = if (seasonNum > 0) TvType.TvSeries else TvType.Movie,
epNumber = epNum,
seasonNumber = seasonNum,
year = yearNum,
headers = headers,
isHearingImpaired = isHearingImpaired
)
)
}
val title = queryText.substringBefore("(").trim()
val url = "$host/search.php?search=${title}&Submit=Search"
val hostDocument = app.get(url).document
var searchResult = ""
if (!hostDocument.select("span:contains($title)").isNullOrEmpty()) searchResult = url
else if (!hostDocument.select("table.tabel")
.isNullOrEmpty()
) searchResult = hostDocument.select("a:contains($title)").attr("href").toString()
else {
val show =
hostDocument.selectFirst("#sl button")?.attr("onmouseup")?.substringAfter("(")
?.substringBefore(",")
val doc = app.get(
"$host/ajax_loadShow.php?show=$show&season=$seasonNum&langs=&hd=undefined&hi=undefined",
referer = "$host/"
).document
doc.select("#season tr:contains($queryLang)").mapNotNull { node ->
if (node.selectFirst("td")?.text()
?.toIntOrNull() == seasonNum && node.select("td:eq(1)")
.text()
.toIntOrNull() == epNum
) searchResult = fixUrl(node.select("a").attr("href"))
}
}
val results = mutableListOf<AbstractSubtitleEntities.SubtitleEntity>()
val document = app.get(
url = fixUrl(searchResult),
).document
document.select(".tabel95 .tabel95 tr:contains($queryLang)").mapNotNull { node ->
val name = if (seasonNum > 0) "${document.select(".titulo").text().replace("Subtitle","").trim()}${
node.parent()!!.select(".NewsTitle").text().substringAfter("Version").substringBefore(", Duration")
}" else "${document.select(".titulo").text().replace("Subtitle","").trim()}${node.parent()!!.select(".NewsTitle").text().substringAfter("Version").substringBefore(", Duration")}"
val link = fixUrl(node.select("a.buttonDownload").attr("href"))
val isHearingImpaired =
!node.parent()!!.select("tr:last-child [title=\"Hearing Impaired\"]").isNullOrEmpty()
cleanResources(results, name, link, mapOf("referer" to "$host/"), isHearingImpaired)
}
return results
}
override suspend fun load(data: AbstractSubtitleEntities.SubtitleEntity): String {
return data.data
}
}

View file

@ -1,20 +1,19 @@
package com.lagradost.cloudstream3.syncproviders.providers package com.lagradost.cloudstream3.syncproviders.providers
import androidx.annotation.StringRes
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.fasterxml.jackson.annotation.JsonProperty 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.*
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey 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.openBrowser
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.toJson
@ -22,7 +21,6 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
import java.net.URL import java.net.URL
import java.net.URLEncoder
import java.util.* import java.util.*
class AniListApi(index: Int) : AccountManager(index), SyncAPI { class AniListApi(index: Int) : AccountManager(index), SyncAPI {
@ -30,12 +28,10 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
override val key = "6871" override val key = "6871"
override val redirectUrl = "anilistlogin" override val redirectUrl = "anilistlogin"
override val idPrefix = "anilist" override val idPrefix = "anilist"
override var requireLibraryRefresh = true
override var mainUrl = "https://anilist.co" override var mainUrl = "https://anilist.co"
override val icon = R.drawable.ic_anilist_icon override val icon = R.drawable.ic_anilist_icon
override val requiresLogin = false override val requiresLogin = false
override val createAccountUrl = "$mainUrl/signup" override val createAccountUrl = "$mainUrl/signup"
override val syncIdName = SyncIdName.Anilist
override fun loginInfo(): AuthAPI.LoginInfo? { override fun loginInfo(): AuthAPI.LoginInfo? {
// context.getUser(true)?. // context.getUser(true)?.
@ -50,7 +46,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
} }
override fun logOut() { override fun logOut() {
requireLibraryRefresh = true
removeAccountKeys() removeAccountKeys()
} }
@ -70,8 +65,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
switchToNewAccount() switchToNewAccount()
setKey(accountId, ANILIST_UNIXTIME_KEY, endTime) setKey(accountId, ANILIST_UNIXTIME_KEY, endTime)
setKey(accountId, ANILIST_TOKEN_KEY, token) setKey(accountId, ANILIST_TOKEN_KEY, token)
setKey(ANILIST_SHOULD_UPDATE_LIST, true)
val user = getUser() val user = getUser()
requireLibraryRefresh = true
return user != null return user != null
} }
@ -146,8 +141,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
this.name, this.name,
recMedia.id?.toString() ?: return@mapNotNull null, recMedia.id?.toString() ?: return@mapNotNull null,
getUrlFromId(recMedia.id), getUrlFromId(recMedia.id),
recMedia.coverImage?.extraLarge ?: recMedia.coverImage?.large recMedia.coverImage?.large ?: recMedia.coverImage?.medium
?: recMedia.coverImage?.medium
) )
}, },
trailers = when (season.trailer?.site?.lowercase()?.trim()) { trailers = when (season.trailer?.site?.lowercase()?.trim()) {
@ -177,9 +171,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
fromIntToAnimeStatus(status.status), fromIntToAnimeStatus(status.status),
status.score, status.score,
status.watchedEpisodes status.watchedEpisodes
).also { )
requireLibraryRefresh = requireLibraryRefresh || it
}
} }
companion object { companion object {
@ -190,6 +182,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
const val ANILIST_TOKEN_KEY: String = "anilist_token" // anilist token for api const val ANILIST_TOKEN_KEY: String = "anilist_token" // anilist token for api
const val ANILIST_USER_KEY: String = "anilist_user" // user data like profile const val ANILIST_USER_KEY: String = "anilist_user" // user data like profile
const val ANILIST_CACHED_LIST: String = "anilist_cached_list" const val ANILIST_CACHED_LIST: String = "anilist_cached_list"
const val ANILIST_SHOULD_UPDATE_LIST: String = "anilist_should_update_list"
private fun fixName(name: String): String { private fun fixName(name: String): String {
return name.lowercase(Locale.ROOT).replace(" ", "") return name.lowercase(Locale.ROOT).replace(" ", "")
@ -227,7 +220,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
romaji romaji
} }
idMal idMal
coverImage { medium large extraLarge } coverImage { medium large }
averageScore averageScore
} }
} }
@ -240,7 +233,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
format format
id id
idMal idMal
coverImage { medium large extraLarge } coverImage { medium large }
averageScore averageScore
title { title {
english english
@ -300,13 +293,15 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
val shows = searchShows(name.replace(blackListRegex, "")) val shows = searchShows(name.replace(blackListRegex, ""))
shows?.data?.Page?.media?.find { shows?.data?.Page?.media?.find {
(malId ?: "NONE") == it.idMal.toString() malId ?: "NONE" == it.idMal.toString()
}?.let { return it } }?.let { return it }
val filtered = val filtered =
shows?.data?.Page?.media?.filter { shows?.data?.Page?.media?.filter {
(((it.startDate.year ?: year.toString()) == year.toString() (
|| year == null)) it.startDate.year ?: year.toString() == year.toString()
|| year == null
)
} }
filtered?.forEach { filtered?.forEach {
it.title.romaji?.let { romaji -> it.title.romaji?.let { romaji ->
@ -318,14 +313,14 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
} }
// Changing names of these will show up in UI // Changing names of these will show up in UI
enum class AniListStatusType(var value: Int, @StringRes val stringRes: Int) { enum class AniListStatusType(var value: Int) {
Watching(0, R.string.type_watching), Watching(0),
Completed(1, R.string.type_completed), Completed(1),
Paused(2, R.string.type_on_hold), Paused(2),
Dropped(3, R.string.type_dropped), Dropped(3),
Planning(4, R.string.type_plan_to_watch), Planning(4),
ReWatching(5, R.string.type_re_watching), ReWatching(5),
None(-1, R.string.none) None(-1)
} }
fun fromIntToAnimeStatus(inp: Int): AniListStatusType {//= AniListStatusType.values().first { it.value == inp } fun fromIntToAnimeStatus(inp: Int): AniListStatusType {//= AniListStatusType.values().first { it.value == inp }
@ -341,7 +336,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
} }
} }
fun convertAniListStringToStatus(string: String): AniListStatusType { fun convertAnilistStringToStatus(string: String): AniListStatusType {
return fromIntToAnimeStatus(aniListStatusString.indexOf(string)) return fromIntToAnimeStatus(aniListStatusString.indexOf(string))
} }
@ -527,29 +522,21 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
} }
private suspend fun postApi(q: String, cache: Boolean = false): String? { private suspend fun postApi(q: String, cache: Boolean = false): String? {
return suspendSafeApiCall { return if (!checkToken()) {
if (!checkToken()) {
app.post( app.post(
"https://graphql.anilist.co/", "https://graphql.anilist.co/",
headers = mapOf( headers = mapOf(
"Authorization" to "Bearer " + (getAuth() "Authorization" to "Bearer " + (getAuth() ?: return null),
?: return@suspendSafeApiCall null),
if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache" if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache"
), ),
cacheTime = 0, cacheTime = 0,
data = mapOf( data = mapOf("query" to q),//(if (vars == null) mapOf("query" to q) else mapOf("query" to q, "variables" to vars))
"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 timeout = 5 // REASONABLE TIMEOUT
).text.replace("\\/", "/") ).text.replace("\\/", "/")
} else { } else {
null null
} }
} }
}
data class MediaRecommendation( data class MediaRecommendation(
@JsonProperty("id") val id: Int, @JsonProperty("id") val id: Int,
@ -582,8 +569,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
data class CoverImage( data class CoverImage(
@JsonProperty("medium") val medium: String?, @JsonProperty("medium") val medium: String?,
@JsonProperty("large") val large: String?, @JsonProperty("large") val large: String?
@JsonProperty("extraLarge") val extraLarge: String?
) )
data class Media( data class Media(
@ -610,29 +596,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("score") val score: Int, @JsonProperty("score") val score: Int,
@JsonProperty("private") val private: Boolean, @JsonProperty("private") val private: Boolean,
@JsonProperty("media") val media: Media @JsonProperty("media") val media: Media
) {
fun toLibraryItem(): SyncAPI.LibraryItem {
return SyncAPI.LibraryItem(
// English title first
this.media.title.english ?: this.media.title.romaji
?: this.media.synonyms.firstOrNull()
?: "",
"https://anilist.co/anime/${this.media.id}/",
this.media.id.toString(),
this.progress,
this.media.episodes,
this.score,
this.updatedAt.toLong(),
"AniList",
TvType.Anime,
this.media.coverImage.extraLarge ?: this.media.coverImage.large
?: this.media.coverImage.medium,
null,
null,
null
) )
}
}
data class Lists( data class Lists(
@JsonProperty("status") val status: String?, @JsonProperty("status") val status: String?,
@ -647,59 +611,40 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("MediaListCollection") val MediaListCollection: MediaListCollection @JsonProperty("MediaListCollection") val MediaListCollection: MediaListCollection
) )
private fun getAniListListCached(): Array<Lists>? { fun getAnilistListCached(): Array<Lists>? {
return getKey(ANILIST_CACHED_LIST) as? Array<Lists> return getKey(ANILIST_CACHED_LIST) as? Array<Lists>
} }
private suspend fun getAniListAnimeListSmart(): Array<Lists>? { suspend fun getAnilistAnimeListSmart(): Array<Lists>? {
if (getAuth() == null) return null if (getAuth() == null) return null
if (checkToken()) return null if (checkToken()) return null
return if (requireLibraryRefresh) { return if (getKey(ANILIST_SHOULD_UPDATE_LIST, true) == true) {
val list = getFullAniListList()?.data?.MediaListCollection?.lists?.toTypedArray() val list = getFullAnilistList()?.data?.MediaListCollection?.lists?.toTypedArray()
if (list != null) { if (list != null) {
setKey(ANILIST_CACHED_LIST, list) setKey(ANILIST_CACHED_LIST, list)
setKey(ANILIST_SHOULD_UPDATE_LIST, false)
} }
list list
} else { } else {
getAniListListCached() getAnilistListCached()
} }
} }
override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata { private suspend fun getFullAnilistList(): FullAnilistList? {
val list = getAniListAnimeListSmart()?.groupBy { var userID: Int? = null
convertAniListStringToStatus(it.status ?: "").stringRes
}?.mapValues { group ->
group.value.map { it.entries.map { entry -> entry.toLibraryItem() } }.flatten()
} ?: emptyMap()
// To fill empty lists when AniList does not return them
val baseMap =
AniListStatusType.values().filter { it.value >= 0 }.associate {
it.stringRes to emptyList<SyncAPI.LibraryItem>()
}
return SyncAPI.LibraryMetadata(
(baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) },
setOf(
ListSorting.AlphabeticalA,
ListSorting.AlphabeticalZ,
ListSorting.UpdatedNew,
ListSorting.UpdatedOld,
ListSorting.RatingHigh,
ListSorting.RatingLow,
)
)
}
private suspend fun getFullAniListList(): FullAnilistList? {
/** WARNING ASSUMES ONE USER! **/ /** WARNING ASSUMES ONE USER! **/
getKeys(ANILIST_USER_KEY)?.forEach { key ->
getKey<AniListUser>(key, null)?.let {
userID = it.id
}
}
val userID = getKey<AniListUser>(accountId, ANILIST_USER_KEY)?.id ?: return null val fixedUserID = userID ?: return null
val mediaType = "ANIME" val mediaType = "ANIME"
val query = """ val query = """
query (${'$'}userID: Int = $userID, ${'$'}MEDIA: MediaType = $mediaType) { query (${'$'}userID: Int = $fixedUserID, ${'$'}MEDIA: MediaType = $mediaType) {
MediaListCollection (userId: ${'$'}userID, type: ${'$'}MEDIA) { MediaListCollection (userId: ${'$'}userID, type: ${'$'}MEDIA) {
lists { lists {
status status
@ -710,7 +655,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
startedAt { year month day } startedAt { year month day }
updatedAt updatedAt
progress progress
score (format: POINT_100) score
private private
media media
{ {
@ -726,7 +671,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
english english
romaji romaji
} }
coverImage { extraLarge large medium } coverImage { medium }
synonyms synonyms
nextAiringEpisode { nextAiringEpisode {
timeUntilAiring timeUntilAiring
@ -759,11 +704,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
return data != "" return data != ""
} }
/** Used to query a saved MediaItem on the list to get the id for removal */
data class MediaListItemRoot(@JsonProperty("data") val data: MediaListItem? = null)
data class MediaListItem(@JsonProperty("MediaList") val MediaList: MediaListId? = null)
data class MediaListId(@JsonProperty("id") val id: Long? = null)
private suspend fun postDataAboutId( private suspend fun postDataAboutId(
id: Int, id: Int,
type: AniListStatusType, type: AniListStatusType,
@ -771,28 +711,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
progress: Int? progress: Int?
): Boolean { ): Boolean {
val q = val q =
// Delete item if status type is None
if (type == AniListStatusType.None) {
val userID = getKey<AniListUser>(accountId, ANILIST_USER_KEY)?.id ?: return false
// Get list ID for deletion
val idQuery = """
query MediaList(${'$'}userId: Int = $userID, ${'$'}mediaId: Int = $id) {
MediaList(userId: ${'$'}userId, mediaId: ${'$'}mediaId) {
id
}
}
"""
val response = postApi(idQuery)
val listId =
tryParseJson<MediaListItemRoot>(response)?.data?.MediaList?.id ?: return false
"""
mutation(${'$'}id: Int = $listId) {
DeleteMediaListEntry(id: ${'$'}id) {
deleted
}
}
"""
} else {
"""mutation (${'$'}id: Int = $id, ${'$'}status: MediaListStatus = ${ """mutation (${'$'}id: Int = $id, ${'$'}status: MediaListStatus = ${
aniListStatusString[maxOf( aniListStatusString[maxOf(
0, 0,
@ -806,8 +724,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
score score
} }
}""" }"""
}
val data = postApi(q) val data = postApi(q)
return data != "" return data != ""
} }

View file

@ -4,6 +4,7 @@ import android.util.Log
import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.imdbUrlToIdNullable import com.lagradost.cloudstream3.imdbUrlToIdNullable
import com.lagradost.cloudstream3.network.CloudflareKiller
import com.lagradost.cloudstream3.subtitles.AbstractSubApi import com.lagradost.cloudstream3.subtitles.AbstractSubApi
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.SubtitleHelper
@ -21,7 +22,7 @@ class IndexSubtitleApi : AbstractSubApi {
companion object { companion object {
const val host = "https://indexsubtitle.com" const val host = "https://subscene.cyou"
const val TAG = "INDEXSUBS" const val TAG = "INDEXSUBS"
} }
@ -241,7 +242,7 @@ class IndexSubtitleApi : AbstractSubApi {
document.selectFirst("div.my-3.p-3 div.media a")!!.attr("href") document.selectFirst("div.my-3.p-3 div.media a")!!.attr("href")
) )
} else { } else {
document.select("div.my-3.p-3 div.media").firstNotNullOf { block -> document.select("div.my-3.p-3 div.media").mapNotNull { block ->
val name = val name =
block.selectFirst("strong.d-block")?.text()?.trim().toString() block.selectFirst("strong.d-block")?.text()?.trim().toString()
if (seasonNum!! > 0) { if (seasonNum!! > 0) {
@ -253,7 +254,7 @@ class IndexSubtitleApi : AbstractSubApi {
} else { } else {
fixUrl(block.selectFirst("a")!!.attr("href")) fixUrl(block.selectFirst("a")!!.attr("href"))
} }
} }.first()
} }
return link return link
} }

View file

@ -1,104 +0,0 @@
package com.lagradost.cloudstream3.syncproviders.providers
import androidx.fragment.app.FragmentActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState
class LocalList : SyncAPI {
override val name = "Local"
override val icon: Int = R.drawable.ic_baseline_storage_24
override val requiresLogin = false
override val createAccountUrl: Nothing? = null
override val idPrefix = "local"
override var requireLibraryRefresh = true
override fun loginInfo(): AuthAPI.LoginInfo {
return AuthAPI.LoginInfo(
null,
null,
0
)
}
override fun logOut() {
}
override val key: String = ""
override val redirectUrl = ""
override suspend fun handleRedirect(url: String): Boolean {
return true
}
override fun authenticate(activity: FragmentActivity?) {
}
override val mainUrl = ""
override val syncIdName = SyncIdName.LocalList
override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean {
return true
}
override suspend fun getStatus(id: String): SyncAPI.SyncStatus? {
return null
}
override suspend fun getResult(id: String): SyncAPI.SyncResult? {
return null
}
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? {
return null
}
override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata? {
val watchStatusIds = ioWork {
getAllWatchStateIds()?.map { id ->
Pair(id, getResultWatchState(id))
}
}?.distinctBy { it.first } ?: return null
val list = ioWork {
watchStatusIds.groupBy {
it.second.stringRes
}.mapValues { group ->
group.value.mapNotNull {
getBookmarkedData(it.first)?.toLibraryItem(it.first.toString())
}
} + mapOf(R.string.subscription_list_name to getAllSubscriptions().mapNotNull {
it.toLibraryItem()
})
}
val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate {
// None is not something to display
it.stringRes to emptyList<SyncAPI.LibraryItem>()
} + mapOf(R.string.subscription_list_name to emptyList())
return SyncAPI.LibraryMetadata(
(baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) },
setOf(
ListSorting.AlphabeticalA,
ListSorting.AlphabeticalZ,
// ListSorting.UpdatedNew,
// ListSorting.UpdatedOld,
// ListSorting.RatingHigh,
// ListSorting.RatingLow,
)
)
}
override fun getIdFromUrl(url: String): String {
return url
}
}

View file

@ -1,7 +1,6 @@
package com.lagradost.cloudstream3.syncproviders.providers package com.lagradost.cloudstream3.syncproviders.providers
import android.util.Base64 import android.util.Base64
import androidx.annotation.StringRes
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
@ -9,15 +8,11 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ShowStatus import com.lagradost.cloudstream3.ShowStatus
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
@ -36,15 +31,13 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
override val redirectUrl = "mallogin" override val redirectUrl = "mallogin"
override val idPrefix = "mal" override val idPrefix = "mal"
override var mainUrl = "https://myanimelist.net" override var mainUrl = "https://myanimelist.net"
private val apiUrl = "https://api.myanimelist.net" val apiUrl = "https://api.myanimelist.net"
override val icon = R.drawable.mal_logo override val icon = R.drawable.mal_logo
override val requiresLogin = false override val requiresLogin = false
override val syncIdName = SyncIdName.MyAnimeList
override var requireLibraryRefresh = true
override val createAccountUrl = "$mainUrl/register.php" override val createAccountUrl = "$mainUrl/register.php"
override fun logOut() { override fun logOut() {
requireLibraryRefresh = true
removeAccountKeys() removeAccountKeys()
} }
@ -97,9 +90,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
fromIntToAnimeStatus(status.status), fromIntToAnimeStatus(status.status),
status.score, status.score,
status.watchedEpisodes status.watchedEpisodes
).also { )
requireLibraryRefresh = requireLibraryRefresh || it
}
} }
data class MalAnime( data class MalAnime(
@ -257,45 +248,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
const val MAL_USER_KEY: String = "mal_user" // user data like profile const val MAL_USER_KEY: String = "mal_user" // user data like profile
const val MAL_CACHED_LIST: String = "mal_cached_list" const val MAL_CACHED_LIST: String = "mal_cached_list"
const val MAL_SHOULD_UPDATE_LIST: String = "mal_should_update_list"
const val MAL_UNIXTIME_KEY: String = "mal_unixtime" // When token expires const val MAL_UNIXTIME_KEY: String = "mal_unixtime" // When token expires
const val MAL_REFRESH_TOKEN_KEY: String = "mal_refresh_token" // refresh token const val MAL_REFRESH_TOKEN_KEY: String = "mal_refresh_token" // refresh token
const val MAL_TOKEN_KEY: String = "mal_token" // anilist token for api const val MAL_TOKEN_KEY: String = "mal_token" // anilist token for api
fun convertToStatus(string: String): MalStatusType {
return fromIntToAnimeStatus(malStatusAsString.indexOf(string))
}
enum class MalStatusType(var value: Int, @StringRes val stringRes: Int) {
Watching(0, R.string.type_watching),
Completed(1, R.string.type_completed),
OnHold(2, R.string.type_on_hold),
Dropped(3, R.string.type_dropped),
PlanToWatch(4, R.string.type_plan_to_watch),
None(-1, R.string.type_none)
}
private fun fromIntToAnimeStatus(inp: Int): MalStatusType {//= AniListStatusType.values().first { it.value == inp }
return when (inp) {
-1 -> MalStatusType.None
0 -> MalStatusType.Watching
1 -> MalStatusType.Completed
2 -> MalStatusType.OnHold
3 -> MalStatusType.Dropped
4 -> MalStatusType.PlanToWatch
5 -> MalStatusType.Watching
else -> MalStatusType.None
}
}
private fun parseDateLong(string: String?): Long? {
return try {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(
string ?: return null
)?.time?.div(1000)
} catch (e: Exception) {
null
}
}
} }
override suspend fun handleRedirect(url: String): Boolean { override suspend fun handleRedirect(url: String): Boolean {
@ -319,7 +275,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
switchToNewAccount() switchToNewAccount()
storeToken(res) storeToken(res)
val user = getMalUser() val user = getMalUser()
requireLibraryRefresh = true setKey(MAL_SHOULD_UPDATE_LIST, true)
return user != null return user != null
} }
} }
@ -352,10 +308,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
setKey(accountId, MAL_UNIXTIME_KEY, (token.expires_in + unixTime)) setKey(accountId, MAL_UNIXTIME_KEY, (token.expires_in + unixTime))
setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refresh_token) setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refresh_token)
setKey(accountId, MAL_TOKEN_KEY, token.access_token) setKey(accountId, MAL_TOKEN_KEY, token.access_token)
requireLibraryRefresh = true
} }
} catch (e: Exception) { } catch (e: Exception) {
logError(e) e.printStackTrace()
} }
} }
@ -374,7 +329,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
).text ).text
storeToken(res) storeToken(res)
} catch (e: Exception) { } catch (e: Exception) {
logError(e) e.printStackTrace()
} }
} }
@ -427,24 +382,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
data class Data( data class Data(
@JsonProperty("node") val node: Node, @JsonProperty("node") val node: Node,
@JsonProperty("list_status") val list_status: ListStatus?, @JsonProperty("list_status") val list_status: ListStatus?,
) {
fun toLibraryItem(): SyncAPI.LibraryItem {
return SyncAPI.LibraryItem(
this.node.title,
"https://myanimelist.net/anime/${this.node.id}/",
this.node.id.toString(),
this.list_status?.num_episodes_watched,
this.node.num_episodes,
this.list_status?.score?.times(10),
parseDateLong(this.list_status?.updated_at),
"MAL",
TvType.Anime,
this.node.main_picture?.large ?: this.node.main_picture?.medium,
null,
null,
) )
}
}
data class Paging( data class Paging(
@JsonProperty("next") val next: String? @JsonProperty("next") val next: String?
@ -475,43 +413,18 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
return getKey(MAL_CACHED_LIST) as? Array<Data> return getKey(MAL_CACHED_LIST) as? Array<Data>
} }
private suspend fun getMalAnimeListSmart(): Array<Data>? { suspend fun getMalAnimeListSmart(): Array<Data>? {
if (getAuth() == null) return null if (getAuth() == null) return null
return if (requireLibraryRefresh) { return if (getKey(MAL_SHOULD_UPDATE_LIST, true) == true) {
val list = getMalAnimeList() val list = getMalAnimeList()
setKey(MAL_CACHED_LIST, list) setKey(MAL_CACHED_LIST, list)
setKey(MAL_SHOULD_UPDATE_LIST, false)
list list
} else { } else {
getMalAnimeListCached() getMalAnimeListCached()
} }
} }
override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata {
val list = getMalAnimeListSmart()?.groupBy {
convertToStatus(it.list_status?.status ?: "").stringRes
}?.mapValues { group ->
group.value.map { it.toLibraryItem() }
} ?: emptyMap()
// To fill empty lists when MAL does not return them
val baseMap =
MalStatusType.values().filter { it.value >= 0 }.associate {
it.stringRes to emptyList<SyncAPI.LibraryItem>()
}
return SyncAPI.LibraryMetadata(
(baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) },
setOf(
ListSorting.AlphabeticalA,
ListSorting.AlphabeticalZ,
ListSorting.UpdatedNew,
ListSorting.UpdatedOld,
ListSorting.RatingHigh,
ListSorting.RatingLow,
)
)
}
private suspend fun getMalAnimeList(): Array<Data> { private suspend fun getMalAnimeList(): Array<Data> {
checkMalToken() checkMalToken()
var offset = 0 var offset = 0
@ -527,6 +440,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
return fullList.toTypedArray() return fullList.toTypedArray()
} }
fun convertToStatus(string: String): MalStatusType {
return fromIntToAnimeStatus(malStatusAsString.indexOf(string))
}
private suspend fun getMalAnimeListSlice(offset: Int = 0): MalList? { private suspend fun getMalAnimeListSlice(offset: Int = 0): MalList? {
val user = "@me" val user = "@me"
val auth = getAuth() ?: return null val auth = getAuth() ?: return null
@ -640,6 +557,28 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
return user return user
} }
enum class MalStatusType(var value: Int) {
Watching(0),
Completed(1),
OnHold(2),
Dropped(3),
PlanToWatch(4),
None(-1)
}
private fun fromIntToAnimeStatus(inp: Int): MalStatusType {//= AniListStatusType.values().first { it.value == inp }
return when (inp) {
-1 -> MalStatusType.None
0 -> MalStatusType.Watching
1 -> MalStatusType.Completed
2 -> MalStatusType.OnHold
3 -> MalStatusType.Dropped
4 -> MalStatusType.PlanToWatch
5 -> MalStatusType.Watching
else -> MalStatusType.None
}
}
private suspend fun setScoreRequest( private suspend fun setScoreRequest(
id: Int, id: Int,
status: MalStatusType? = null, status: MalStatusType? = null,

View file

@ -15,8 +15,6 @@ import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager
import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.AppUtils
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi { class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi {
override val idPrefix = "opensubtitles" override val idPrefix = "opensubtitles"
@ -166,7 +164,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
val fixedLang = fixLanguage(query.lang) val fixedLang = fixLanguage(query.lang)
val imdbId = query.imdb ?: 0 val imdbId = query.imdb ?: 0
val queryText = query.query val queryText = query.query.replace(" ", "+")
val epNum = query.epNumber ?: 0 val epNum = query.epNumber ?: 0
val seasonNum = query.seasonNumber ?: 0 val seasonNum = query.seasonNumber ?: 0
val yearNum = query.year ?: 0 val yearNum = query.year ?: 0
@ -177,7 +175,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
val searchQueryUrl = when (imdbId > 0) { val searchQueryUrl = when (imdbId > 0) {
//Use imdb_id to search if its valid //Use imdb_id to search if its valid
true -> "$host/subtitles?imdb_id=$imdbId&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" true -> "$host/subtitles?imdb_id=$imdbId&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
false -> "$host/subtitles?query=${queryText}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" false -> "$host/subtitles?query=$queryText&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
} }
val req = app.get( val req = app.get(
@ -200,13 +198,9 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
it.data?.forEach { item -> it.data?.forEach { item ->
val attr = item.attributes ?: return@forEach val attr = item.attributes ?: return@forEach
val featureDetails = attr.featDetails 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 //Use any valid name/title in hierarchy
val name = filename ?: featureDetails?.movieName ?: featureDetails?.title val name = featureDetails?.movieName ?: featureDetails?.title
?: featureDetails?.parentTitle ?: attr.release ?: query.query ?: featureDetails?.parentTitle ?: attr.release ?: ""
val lang = fixLanguageReverse(attr.language)?: "" val lang = fixLanguageReverse(attr.language)?: ""
val resEpNum = featureDetails?.episodeNumber ?: query.epNumber val resEpNum = featureDetails?.episodeNumber ?: query.epNumber
val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber

View file

@ -1,13 +1,10 @@
package com.lagradost.cloudstream3.ui package com.lagradost.cloudstream3.ui
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.unixTime
import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.APIHolder.unixTimeMS
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
@ -34,65 +31,26 @@ class APIRepository(val api: MainAPI) {
return data.isEmpty() || data == "[]" || data == "about:blank" return data.isEmpty() || data == "[]" || data == "about:blank"
} }
data class SavedLoadResponse( private val cacheHash: HashMap<Pair<String, String>, LoadResponse> = hashMapOf()
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 hasMainPage = api.hasMainPage
val providerType = api.providerType
val name = api.name val name = api.name
val mainUrl = api.mainUrl val mainUrl = api.mainUrl
val mainPage = api.mainPage val mainPage = api.mainPage
val hasQuickSearch = api.hasQuickSearch val hasQuickSearch = api.hasQuickSearch
val vpnStatus = api.vpnStatus val vpnStatus = api.vpnStatus
val providerType = api.providerType
suspend fun load(url: String): Resource<LoadResponse> { suspend fun load(url: String): Resource<LoadResponse> {
return safeApiCall { return safeApiCall {
if (isInvalidData(url)) throw ErrorLoadingException() if (isInvalidData(url)) throw ErrorLoadingException()
val fixedUrl = api.fixUrl(url) val fixedUrl = api.fixUrl(url)
val lookingForHash = Pair(api.name, fixedUrl) val key = Pair(api.name, url)
cacheHash[key] ?: api.load(fixedUrl)?.also {
synchronized(cache) { // we cache 20 responses because ppl often go back to the same shit + 20 because I dont want to cause too much memory leak
for (item in cache) { if (cacheHash.size > 20) cacheHash.remove(cacheHash.keys.random())
// 10 min save cacheHash[key] = it
if (item.hash == lookingForHash && (unixTime - item.unixTime) < 60 * 10) {
return@safeApiCall item.response
}
}
}
api.load(fixedUrl)?.also { response ->
// Remove all blank tags as early as possible
response.tags = response.tags?.filter { it.isNotBlank() }
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() } ?: throw ErrorLoadingException()
} }
} }
@ -124,6 +82,7 @@ class APIRepository(val api: MainAPI) {
delay(delta) delay(delta)
} }
@OptIn(DelicateCoroutinesApi::class)
suspend fun getMainPage(page: Int, nameIndex: Int? = null): Resource<List<HomePageResponse?>> { suspend fun getMainPage(page: Int, nameIndex: Int? = null): Resource<List<HomePageResponse?>> {
return safeApiCall { return safeApiCall {
api.lastHomepageRequest = unixTimeMS api.lastHomepageRequest = unixTimeMS

View file

@ -7,8 +7,7 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlin.math.abs import kotlin.math.abs
class GrdLayoutManager(val context: Context, _spanCount: Int) : class GrdLayoutManager(val context: Context, _spanCount: Int) : GridLayoutManager(context, _spanCount) {
GridLayoutManager(context, _spanCount) {
override fun onFocusSearchFailed( override fun onFocusSearchFailed(
focused: View, focused: View,
focusDirection: Int, focusDirection: Int,
@ -35,7 +34,7 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) :
val pos = maxOf(0, getPosition(focused!!) - 2) val pos = maxOf(0, getPosition(focused!!) - 2)
parent.scrollToPosition(pos) parent.scrollToPosition(pos)
super.onRequestChildFocus(parent, state, child, focused) super.onRequestChildFocus(parent, state, child, focused)
} catch (e: Exception) { } catch (e: Exception){
false false
} }
} }

View file

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

View file

@ -1,42 +0,0 @@
package com.lagradost.cloudstream3.ui
import android.graphics.Canvas
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
class HeaderViewDecoration(private val customView: View) : RecyclerView.ItemDecoration() {
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(c, parent, state)
customView.layout(parent.left, 0, parent.right, customView.measuredHeight)
for (i in 0 until parent.childCount) {
val view = parent.getChildAt(i)
if (parent.getChildAdapterPosition(view) == 0) {
c.save()
val height = customView.measuredHeight
val top = view.top - height
c.translate(0f, top.toFloat())
customView.draw(c)
c.restore()
break
}
}
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
if (parent.getChildAdapterPosition(view) == 0) {
customView.measure(
View.MeasureSpec.makeMeasureSpec(parent.measuredWidth, View.MeasureSpec.AT_MOST),
View.MeasureSpec.makeMeasureSpec(parent.measuredHeight, View.MeasureSpec.AT_MOST)
)
outRect.set(0, customView.measuredHeight, 0, 0)
} else {
outRect.setEmpty()
}
}
}

View file

@ -11,7 +11,6 @@ import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.player.DownloadFileGenerator import com.lagradost.cloudstream3.ui.player.DownloadFileGenerator
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.ExtractorUri
import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.navigate
@ -50,7 +49,7 @@ object DownloadButtonSetup {
) )
.setPositiveButton(R.string.delete, dialogClickListener) .setPositiveButton(R.string.delete, dialogClickListener)
.setNegativeButton(R.string.cancel, dialogClickListener) .setNegativeButton(R.string.cancel, dialogClickListener)
.show().setDefaultFocus() .show()
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
// ye you somehow fucked up formatting did you? // ye you somehow fucked up formatting did you?

View file

@ -24,6 +24,7 @@ import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
import com.lagradost.cloudstream3.ui.player.LinkGenerator import com.lagradost.cloudstream3.ui.player.LinkGenerator
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE
@ -39,8 +40,6 @@ import kotlinx.android.synthetic.main.stream_input.*
import android.text.format.Formatter.formatShortFileSize import android.text.format.Formatter.formatShortFileSize
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.ui.player.BasicLink
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import java.net.URI import java.net.URI
@ -179,9 +178,7 @@ class DownloadFragment : Fragment() {
download_list?.adapter = adapter download_list?.adapter = adapter
download_list?.layoutManager = GridLayoutManager(context, 1) download_list?.layoutManager = GridLayoutManager(context, 1)
download_stream_button?.isGone = isTvSettings()
// Should be visible in emulator layout
download_stream_button?.isGone = isTrueTvSettings()
download_stream_button?.setOnClickListener { download_stream_button?.setOnClickListener {
val dialog = val dialog =
Dialog(it.context ?: return@setOnClickListener, R.style.AlertDialogCustom) Dialog(it.context ?: return@setOnClickListener, R.style.AlertDialogCustom)
@ -225,7 +222,7 @@ class DownloadFragment : Fragment() {
R.id.global_to_navigation_player, R.id.global_to_navigation_player,
GeneratorPlayer.newInstance( GeneratorPlayer.newInstance(
LinkGenerator( LinkGenerator(
listOf(BasicLink(url)), listOf(url),
extract = true, extract = true,
referer = referer, referer = referer,
isM3u8 = dialog.hls_switch?.isChecked isM3u8 = dialog.hls_switch?.isChecked

View file

@ -7,20 +7,25 @@ import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.* import android.widget.*
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat.getDrawable
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.*
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView 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.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
@ -29,55 +34,80 @@ import com.google.android.material.chip.ChipGroup
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.MainActivity.Companion.bookmarksUpdatedEvent
import com.lagradost.cloudstream3.MainActivity.Companion.mainPluginsLoadedEvent import com.lagradost.cloudstream3.MainActivity.Companion.mainPluginsLoadedEvent
import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi
import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi
import com.lagradost.cloudstream3.ui.AutofitRecyclerView import com.lagradost.cloudstream3.ui.AutofitRecyclerView
import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
import com.lagradost.cloudstream3.ui.result.ResultViewModel2.Companion.updateWatchStatus
import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
import com.lagradost.cloudstream3.ui.search.* import com.lagradost.cloudstream3.ui.search.*
import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback 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.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings 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.isRecyclerScrollable
import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.AppUtils.ownHide import com.lagradost.cloudstream3.utils.AppUtils.setMaxViewPoolSize
import com.lagradost.cloudstream3.utils.AppUtils.ownShow
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteAllBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteAllResumeStateIds
import com.lagradost.cloudstream3.utils.DataStoreHelper.removeLastWatched
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultWatchState
import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showOptionSelectStringRes
import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar 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.getSpanCount
import com.lagradost.cloudstream3.utils.UIHelper.getStatusBarHeight
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.UIHelper.setImageBlur
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
import kotlinx.android.synthetic.main.activity_main_tv.* import com.lagradost.cloudstream3.widget.CenterZoomLayoutManager
import kotlinx.android.synthetic.main.fragment_home.* import kotlinx.android.synthetic.main.fragment_home.*
import kotlinx.android.synthetic.main.fragment_home.home_api_fab import kotlinx.android.synthetic.main.fragment_home.home_api_fab
import kotlinx.android.synthetic.main.fragment_home.home_bookmarked_child_recyclerview
import kotlinx.android.synthetic.main.fragment_home.home_bookmarked_holder
import kotlinx.android.synthetic.main.fragment_home.home_change_api_loading import kotlinx.android.synthetic.main.fragment_home.home_change_api_loading
import kotlinx.android.synthetic.main.fragment_home.home_loaded
import kotlinx.android.synthetic.main.fragment_home.home_loading import kotlinx.android.synthetic.main.fragment_home.home_loading
import kotlinx.android.synthetic.main.fragment_home.home_loading_error import kotlinx.android.synthetic.main.fragment_home.home_loading_error
import kotlinx.android.synthetic.main.fragment_home.home_loading_shimmer import kotlinx.android.synthetic.main.fragment_home.home_loading_shimmer
import kotlinx.android.synthetic.main.fragment_home.home_loading_statusbar import kotlinx.android.synthetic.main.fragment_home.home_loading_statusbar
import kotlinx.android.synthetic.main.fragment_home.home_master_recycler import kotlinx.android.synthetic.main.fragment_home.home_master_recycler
import kotlinx.android.synthetic.main.fragment_home.home_plan_to_watch_btt
import kotlinx.android.synthetic.main.fragment_home.home_provider_meta_info
import kotlinx.android.synthetic.main.fragment_home.home_provider_name
import kotlinx.android.synthetic.main.fragment_home.home_reload_connection_open_in_browser import kotlinx.android.synthetic.main.fragment_home.home_reload_connection_open_in_browser
import kotlinx.android.synthetic.main.fragment_home.home_reload_connectionerror import kotlinx.android.synthetic.main.fragment_home.home_reload_connectionerror
import kotlinx.android.synthetic.main.fragment_home.home_type_completed_btt
import kotlinx.android.synthetic.main.fragment_home.home_type_dropped_btt
import kotlinx.android.synthetic.main.fragment_home.home_type_on_hold_btt
import kotlinx.android.synthetic.main.fragment_home.home_type_watching_btt
import kotlinx.android.synthetic.main.fragment_home.home_watch_child_recyclerview
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.result_error_text
import kotlinx.android.synthetic.main.fragment_home_tv.* import kotlinx.android.synthetic.main.fragment_home_tv.*
import kotlinx.android.synthetic.main.fragment_result.*
import kotlinx.android.synthetic.main.fragment_search.* import kotlinx.android.synthetic.main.fragment_search.*
import kotlinx.android.synthetic.main.home_episodes_expanded.* import kotlinx.android.synthetic.main.home_episodes_expanded.*
import kotlinx.android.synthetic.main.tvtypes_chips.* import kotlinx.android.synthetic.main.tvtypes_chips.*
@ -109,32 +139,26 @@ class HomeFragment : Fragment() {
val errorProfilePic = errorProfilePics.random() val errorProfilePic = errorProfilePics.random()
//fun Activity.loadHomepageList( fun Activity.loadHomepageList(
// item: HomePageList, item: HomePageList,
// deleteCallback: (() -> Unit)? = null, deleteCallback: (() -> Unit)? = null,
//) { ) {
// loadHomepageList( loadHomepageList(
// expand = HomeViewModel.ExpandableHomepageList(item, 1, false), expand = HomeViewModel.ExpandableHomepageList(item, 1, false),
// deleteCallback = deleteCallback, deleteCallback = deleteCallback,
// expandCallback = null expandCallback = null
// ) )
//} }
// returns a BottomSheetDialog that will be hidden with OwnHidden upon hide, and must be saved to be able call ownShow in onCreateView
fun Activity.loadHomepageList( fun Activity.loadHomepageList(
expand: HomeViewModel.ExpandableHomepageList, expand: HomeViewModel.ExpandableHomepageList,
deleteCallback: (() -> Unit)? = null, deleteCallback: (() -> Unit)? = null,
expandCallback: (suspend (String) -> HomeViewModel.ExpandableHomepageList?)? = null, expandCallback: (suspend (String) -> HomeViewModel.ExpandableHomepageList?)? = null
dismissCallback : (() -> Unit), ) {
): BottomSheetDialog {
val context = this val context = this
val bottomSheetDialogBuilder = BottomSheetDialog(context) val bottomSheetDialogBuilder = BottomSheetDialog(context)
bottomSheetDialogBuilder.setContentView(R.layout.home_episodes_expanded) bottomSheetDialogBuilder.setContentView(R.layout.home_episodes_expanded)
val title = bottomSheetDialogBuilder.findViewById<TextView>(R.id.home_expanded_text)!! val title = bottomSheetDialogBuilder.findViewById<TextView>(R.id.home_expanded_text)!!
//title.findViewTreeLifecycleOwner().lifecycle.addObserver()
val item = expand.list val item = expand.list
title.text = item.name title.text = item.name
val recycle = val recycle =
@ -142,23 +166,6 @@ class HomeFragment : Fragment() {
val titleHolder = val titleHolder =
bottomSheetDialogBuilder.findViewById<FrameLayout>(R.id.home_expanded_drag_down)!! bottomSheetDialogBuilder.findViewById<FrameLayout>(R.id.home_expanded_drag_down)!!
// main {
//(bottomSheetDialogBuilder.ownerActivity as androidx.fragment.app.FragmentActivity?)?.supportFragmentManager?.fragments?.lastOrNull()?.viewLifecycleOwner?.apply {
// println("GOT LIFE: lifecycle $this")
// this.lifecycle.addObserver(object : DefaultLifecycleObserver {
// override fun onResume(owner: LifecycleOwner) {
// super.onResume(owner)
// println("onResume!!!!")
// bottomSheetDialogBuilder?.ownShow()
// }
// override fun onStop(owner: LifecycleOwner) {
// super.onStop(owner)
// bottomSheetDialogBuilder?.ownHide()
// }
// })
//}
// }
val delete = bottomSheetDialogBuilder.home_expanded_delete val delete = bottomSheetDialogBuilder.home_expanded_delete
delete.isGone = deleteCallback == null delete.isGone = deleteCallback == null
if (deleteCallback != null) { if (deleteCallback != null) {
@ -184,7 +191,7 @@ class HomeFragment : Fragment() {
) )
.setPositiveButton(R.string.delete, dialogClickListener) .setPositiveButton(R.string.delete, dialogClickListener)
.setNegativeButton(R.string.cancel, dialogClickListener) .setNegativeButton(R.string.cancel, dialogClickListener)
.show().setDefaultFocus() .show()
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
// ye you somehow fucked up formatting did you? // ye you somehow fucked up formatting did you?
@ -203,8 +210,7 @@ class HomeFragment : Fragment() {
recycle.adapter = SearchAdapter(item.list.toMutableList(), recycle) { callback -> recycle.adapter = SearchAdapter(item.list.toMutableList(), recycle) { callback ->
handleSearchClickCallback(this, callback) handleSearchClickCallback(this, callback)
if (callback.action == SEARCH_ACTION_LOAD || callback.action == SEARCH_ACTION_PLAY_FILE) { if (callback.action == SEARCH_ACTION_LOAD || callback.action == SEARCH_ACTION_PLAY_FILE) {
bottomSheetDialogBuilder.ownHide() // we hide here because we want to resume it later bottomSheetDialogBuilder.dismissSafe(this)
//bottomSheetDialogBuilder.dismissSafe(this)
} }
}.apply { }.apply {
hasNext = expand.hasNext hasNext = expand.hasNext
@ -245,14 +251,12 @@ class HomeFragment : Fragment() {
configEvent += spanListener configEvent += spanListener
bottomSheetDialogBuilder.setOnDismissListener { bottomSheetDialogBuilder.setOnDismissListener {
dismissCallback.invoke()
configEvent -= spanListener configEvent -= spanListener
} }
//(recycle.adapter as SearchAdapter).notifyDataSetChanged() //(recycle.adapter as SearchAdapter).notifyDataSetChanged()
bottomSheetDialogBuilder.show() bottomSheetDialogBuilder.show()
return bottomSheetDialogBuilder
} }
fun getPairList( fun getPairList(
@ -430,15 +434,13 @@ class HomeFragment : Fragment() {
): View? { ): View? {
//homeViewModel = //homeViewModel =
// ViewModelProvider(this).get(HomeViewModel::class.java) // ViewModelProvider(this).get(HomeViewModel::class.java)
bottomSheetDialog?.ownShow()
val layout = val layout =
if (isTvSettings()) R.layout.fragment_home_tv else R.layout.fragment_home if (isTvSettings()) R.layout.fragment_home_tv else R.layout.fragment_home
return inflater.inflate(layout, container, false) return inflater.inflate(layout, container, false)
} }
override fun onDestroyView() { private fun toggleMainVisibility(visible: Boolean) {
bottomSheetDialog?.ownHide() home_main_poster_recyclerview?.isVisible = visible
super.onDestroyView()
} }
private fun fixGrid() { private fun fixGrid() {
@ -463,26 +465,19 @@ class HomeFragment : Fragment() {
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
//(home_preview_viewpager?.adapter as? HomeScrollAdapter)?.notifyDataSetChanged()
fixGrid() fixGrid()
} }
fun bookmarksUpdated(_data : Boolean) {
reloadStored()
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
reloadStored() reloadStored()
bookmarksUpdatedEvent += ::bookmarksUpdated afterPluginsLoadedEvent += ::firstLoadHomePage
afterPluginsLoadedEvent += ::afterPluginsLoaded mainPluginsLoadedEvent += ::firstLoadHomePage
mainPluginsLoadedEvent += ::afterMainPluginsLoaded
} }
override fun onStop() { override fun onStop() {
bookmarksUpdatedEvent -= ::bookmarksUpdated afterPluginsLoadedEvent -= ::firstLoadHomePage
afterPluginsLoadedEvent -= ::afterPluginsLoaded mainPluginsLoadedEvent -= ::firstLoadHomePage
mainPluginsLoadedEvent -= ::afterMainPluginsLoaded
super.onStop() super.onStop()
} }
@ -495,26 +490,34 @@ class HomeFragment : Fragment() {
homeViewModel.loadStoredData(list) homeViewModel.loadStoredData(list)
} }
private fun afterMainPluginsLoaded(unused: Boolean = false) { private fun firstLoadHomePage(successful: Boolean = false) {
// dirty hack to make it only load once
loadHomePage(false) loadHomePage(false)
} }
private fun afterPluginsLoaded(forceReload: Boolean) { private fun loadHomePage(forceReload: Boolean = true) {
loadHomePage(forceReload)
}
private fun loadHomePage(forceReload: Boolean) {
val apiName = context?.getKey<String>(USER_SELECTED_HOMEPAGE_API) val apiName = context?.getKey<String>(USER_SELECTED_HOMEPAGE_API)
if (homeViewModel.apiName.value != apiName || apiName == null || forceReload) { if (homeViewModel.apiName.value != apiName || apiName == null) {
//println("Caught home: " + homeViewModel.apiName.value + " at " + apiName) //println("Caught home: " + homeViewModel.apiName.value + " at " + apiName)
homeViewModel.loadAndCancel(apiName, forceReload) homeViewModel.loadAndCancel(apiName, forceReload)
} }
} }
/*private fun handleBack(poppedFragment: Boolean) {
if (poppedFragment) {
reloadStored()
}
}*/
private fun focusCallback(card: SearchResponse) {
home_focus_text?.text = card.name
home_blur_poster?.setImageBlur(card.posterUrl, 50)
}
private fun homeHandleSearch(callback: SearchClickCallback) { private fun homeHandleSearch(callback: SearchClickCallback) {
if (callback.action == SEARCH_ACTION_FOCUSED) { if (callback.action == SEARCH_ACTION_FOCUSED) {
//focusCallback(callback.card) focusCallback(callback.card)
} else { } else {
handleSearchClickCallback(activity, callback) handleSearchClickCallback(activity, callback)
} }
@ -523,13 +526,12 @@ class HomeFragment : Fragment() {
private var currentApiName: String? = null private var currentApiName: String? = null
private var toggleRandomButton = false private var toggleRandomButton = false
private var bottomSheetDialog: BottomSheetDialog? = null
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
fixGrid() fixGrid()
home_change_api?.setOnClickListener(apiChangeClickListener)
home_change_api_loading?.setOnClickListener(apiChangeClickListener) home_change_api_loading?.setOnClickListener(apiChangeClickListener)
home_api_fab?.setOnClickListener(apiChangeClickListener) home_api_fab?.setOnClickListener(apiChangeClickListener)
home_random?.setOnClickListener { home_random?.setOnClickListener {
@ -547,18 +549,210 @@ class HomeFragment : Fragment() {
} }
observe(homeViewModel.preview) { preview -> observe(homeViewModel.preview) { preview ->
(home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setPreviewData( // Always reset the padding, otherwise the will move lower and lower
preview // 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
(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 -> observe(homeViewModel.apiName) { apiName ->
currentApiName = apiName currentApiName = apiName
// setKey(USER_SELECTED_HOMEPAGE_API, apiName)
home_api_fab?.text = apiName home_api_fab?.text = apiName
(home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setApiName( home_provider_name?.text = apiName
apiName try {
) home_search?.queryHint = getString(R.string.search_hint_site).format(apiName)
} catch (e: Exception) {
logError(e)
} }
home_provider_meta_info?.isVisible = false
getApiFromNameNull(apiName)?.let { currentApi ->
val typeChoices = listOf(
Pair(R.string.movies, listOf(TvType.Movie)),
Pair(R.string.tv_series, listOf(TvType.TvSeries)),
Pair(R.string.documentaries, listOf(TvType.Documentary)),
Pair(R.string.cartoons, listOf(TvType.Cartoon)),
Pair(R.string.anime, listOf(TvType.Anime, TvType.OVA, TvType.AnimeMovie)),
Pair(R.string.torrent, listOf(TvType.Torrent)),
Pair(R.string.asian_drama, listOf(TvType.AsianDrama)),
).filter { item -> currentApi.supportedTypes.any { type -> item.second.contains(type) } }
home_provider_meta_info?.text =
typeChoices.joinToString(separator = ", ") { getString(it.first) }
home_provider_meta_info?.isVisible = true
}
}
home_main_poster_recyclerview?.adapter =
HomeChildItemAdapter(
mutableListOf(),
R.layout.home_result_big_grid,
nextFocusUp = home_main_poster_recyclerview?.nextFocusUpId,
nextFocusDown = home_main_poster_recyclerview?.nextFocusDownId
) { callback ->
homeHandleSearch(callback)
}
home_main_poster_recyclerview?.setLinearListLayout()
observe(homeViewModel.randomItems) { items ->
if (items.isNullOrEmpty()) {
toggleMainVisibility(false)
} else {
val tempAdapter = home_main_poster_recyclerview?.adapter as? HomeChildItemAdapter?
// no need to reload if it has the same data
if (tempAdapter != null && tempAdapter.cardList == items) {
toggleMainVisibility(true)
return@observe
}
val randomSize = items.size
tempAdapter?.updateList(items)
if (!isTvSettings()) {
home_main_poster_recyclerview?.post {
(home_main_poster_recyclerview?.layoutManager as CenterZoomLayoutManager?)?.let { manager ->
manager.updateSize(forceUpdate = true)
if (randomSize > 2) {
manager.scrollToPosition(randomSize / 2)
manager.snap { dx ->
home_main_poster_recyclerview?.post {
// this is the best I can do, fuck android for not including instant scroll
home_main_poster_recyclerview?.smoothScrollBy(dx, 0)
}
}
}
}
}
} else {
items.firstOrNull()?.let {
focusCallback(it)
}
}
toggleMainVisibility(true)
}
}
home_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
QuickSearchFragment.pushSearch(activity, query, currentApiName?.let { arrayOf(it) })
return true
}
override fun onQueryTextChange(newText: String): Boolean {
//searchViewModel.quickSearch(newText)
return true
}
})
observe(homeViewModel.page) { data -> observe(homeViewModel.page) { data ->
when (data) { when (data) {
@ -569,15 +763,15 @@ class HomeFragment : Fragment() {
val mutableListOfResponse = mutableListOf<SearchResponse>() val mutableListOfResponse = mutableListOf<SearchResponse>()
listHomepageItems.clear() listHomepageItems.clear()
(home_master_recycler?.adapter as? ParentItemAdapter)?.updateList( // println("ITEMCOUNT: ${d.values.size} ${home_master_recycler?.adapter?.itemCount}")
(home_master_recycler?.adapter as? ParentItemAdapter?)?.updateList(
d.values.toMutableList(), d.values.toMutableList(),
home_master_recycler home_master_recycler
) )
home_loading?.isVisible = false home_loading?.isVisible = false
home_loading_error?.isVisible = false home_loading_error?.isVisible = false
home_master_recycler?.isVisible = true home_loaded?.isVisible = true
//home_loaded?.isVisible = true
if (toggleRandomButton) { if (toggleRandomButton) {
//Flatten list //Flatten list
d.values.forEach { dlist -> d.values.forEach { dlist ->
@ -617,90 +811,313 @@ class HomeFragment : Fragment() {
home_loading?.isVisible = false home_loading?.isVisible = false
home_loading_error?.isVisible = true home_loading_error?.isVisible = true
home_master_recycler?.isVisible = false home_loaded?.isVisible = false
//home_loaded?.isVisible = false
} }
is Resource.Loading -> { is Resource.Loading -> {
(home_master_recycler?.adapter as? ParentItemAdapter)?.updateList(listOf()) (home_master_recycler?.adapter as? ParentItemAdapter?)?.updateList(listOf())
home_loading_shimmer?.startShimmer() home_loading_shimmer?.startShimmer()
home_loading?.isVisible = true home_loading?.isVisible = true
home_loading_error?.isVisible = false home_loading_error?.isVisible = false
home_master_recycler?.isVisible = false home_loaded?.isVisible = false
//home_loaded?.isVisible = false
} }
} }
} }
val toggleList = listOf(
Pair(home_type_watching_btt, WatchType.WATCHING),
Pair(home_type_completed_btt, WatchType.COMPLETED),
Pair(home_type_dropped_btt, WatchType.DROPPED),
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 {
homeViewModel.loadStoredData(EnumSet.of(watch))
}
chip?.setOnLongClickListener { itemView ->
val list = EnumSet.noneOf(WatchType::class.java)
itemView.context.getKey<IntArray>(HOME_BOOKMARK_VALUE_LIST)
?.map { WatchType.fromInternalId(it) }?.let {
list.addAll(it)
}
if (list.contains(watch)) {
list.remove(watch)
} else {
list.add(watch)
}
homeViewModel.loadStoredData(list)
return@setOnLongClickListener true
}*/
}
observe(homeViewModel.availableWatchStatusTypes) { availableWatchStatusTypes -> observe(homeViewModel.availableWatchStatusTypes) { availableWatchStatusTypes ->
context?.setKey( context?.setKey(
HOME_BOOKMARK_VALUE_LIST, HOME_BOOKMARK_VALUE_LIST,
availableWatchStatusTypes.first.map { it.internalId }.toIntArray() availableWatchStatusTypes.first.map { it.internalId }.toIntArray()
) )
(home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setAvailableWatchStatusTypes(
availableWatchStatusTypes for (item in toggleList) {
) val watch = item.second
item.first?.apply {
isVisible = availableWatchStatusTypes.second.contains(watch)
isSelected = availableWatchStatusTypes.first.contains(watch)
}
} }
observe(homeViewModel.bookmarks) { data -> /*home_bookmark_select?.setOnClickListener {
(home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setBookmarkData( it.popupMenuNoIcons(availableWatchStatusTypes.second.map { type ->
data Pair(
type.internalId,
type.stringRes
) )
}) {
homeViewModel.loadStoredData(it.context, WatchType.fromInternalId(this.itemId))
}
}
home_bookmarked_parent_item_title?.text = getString(availableWatchStatusTypes.first.stringRes)*/
}
observe(homeViewModel.bookmarks) { (isVis, bookmarks) ->
home_bookmarked_holder.isVisible = isVis
(home_bookmarked_child_recyclerview?.adapter as? HomeChildItemAdapter?)?.updateList(
bookmarks
)
home_bookmarked_child_more_info?.setOnClickListener {
activity?.loadHomepageList(
HomePageList(
getString(R.string.error_bookmarks_text), //home_bookmarked_parent_item_title?.text?.toString() ?: getString(R.string.error_bookmarks_text),
bookmarks
)
) {
deleteAllBookmarkedData()
homeViewModel.loadStoredData(null)
}
}
} }
observe(homeViewModel.resumeWatching) { resumeWatching -> observe(homeViewModel.resumeWatching) { resumeWatching ->
(home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setResumeWatchingData( home_watch_holder?.isVisible = resumeWatching.isNotEmpty()
(home_watch_child_recyclerview?.adapter as? HomeChildItemAdapter?)?.updateList(
resumeWatching resumeWatching
) )
if (isTrueTvSettings()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { //if (context?.isTvSettings() == true) {
ioSafe { // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activity?.addProgramsToContinueWatching(resumeWatching.mapNotNull { it as? DataStoreHelper.ResumeWatchingResult }) // context?.addProgramsToContinueWatching(resumeWatching.mapNotNull { it as? DataStoreHelper.ResumeWatchingResult })
} // }
//}
home_watch_child_more_info?.setOnClickListener {
activity?.loadHomepageList(
HomePageList(
home_watch_parent_item_title?.text?.toString()
?: getString(R.string.continue_watching),
resumeWatching
)
) {
deleteAllResumeStateIds()
homeViewModel.loadResumeWatching()
} }
} }
} }
home_bookmarked_child_recyclerview.adapter = HomeChildItemAdapter(
ArrayList(),
nextFocusUp = home_bookmarked_child_recyclerview?.nextFocusUpId,
nextFocusDown = home_bookmarked_child_recyclerview?.nextFocusDownId
) { callback ->
if (callback.action == SEARCH_ACTION_SHOW_METADATA) {
activity?.showOptionSelectStringRes(
callback.view,
callback.card.posterUrl,
listOf(
R.string.action_open_watching,
R.string.action_remove_from_bookmarks,
),
listOf(
R.string.action_open_play,
R.string.action_open_watching,
R.string.action_remove_from_bookmarks
)
) { (isTv, actionId) ->
fun play() {
activity.loadSearchResult(callback.card, START_ACTION_RESUME_LATEST)
reloadStored()
}
fun remove() {
setResultWatchState(callback.card.id, WatchType.NONE.internalId)
reloadStored()
}
fun info() {
handleSearchClickCallback(
activity,
SearchClickCallback(
SEARCH_ACTION_LOAD,
callback.view,
-1,
callback.card
)
)
reloadStored()
}
if (isTv) {
when (actionId) {
0 -> {
play()
}
1 -> {
info()
}
2 -> {
remove()
}
}
} else {
when (actionId) {
0 -> {
info()
}
1 -> {
remove()
}
}
}
}
} else {
homeHandleSearch(callback)
}
}
home_watch_child_recyclerview.setLinearListLayout()
home_bookmarked_child_recyclerview.setLinearListLayout()
home_watch_child_recyclerview?.adapter = HomeChildItemAdapter(
ArrayList(),
nextFocusUp = home_watch_child_recyclerview?.nextFocusUpId,
nextFocusDown = home_watch_child_recyclerview?.nextFocusDownId
) { callback ->
if (callback.action == SEARCH_ACTION_SHOW_METADATA) {
activity?.showOptionSelectStringRes(
callback.view,
callback.card.posterUrl,
listOf(
R.string.action_open_watching,
R.string.action_remove_watching
),
listOf(
R.string.action_open_play,
R.string.action_open_watching,
R.string.action_remove_watching
)
) { (isTv, actionId) ->
fun play() {
activity.loadSearchResult(callback.card, START_ACTION_RESUME_LATEST)
reloadStored()
}
fun remove() {
val card = callback.card
if (card is DataStoreHelper.ResumeWatchingResult) {
removeLastWatched(card.parentId)
reloadStored()
}
}
fun info() {
handleSearchClickCallback(
activity,
SearchClickCallback(
SEARCH_ACTION_LOAD,
callback.view,
-1,
callback.card
)
)
reloadStored()
}
if (isTv) {
when (actionId) {
0 -> {
play()
}
1 -> {
info()
}
2 -> {
remove()
}
}
} else {
when (actionId) {
0 -> {
info()
}
1 -> {
remove()
}
}
}
}
} else {
homeHandleSearch(callback)
}
}
//context?.fixPaddingStatusbarView(home_statusbar) //context?.fixPaddingStatusbarView(home_statusbar)
//context?.fixPaddingStatusbar(home_padding) context?.fixPaddingStatusbar(home_padding)
context?.fixPaddingStatusbar(home_loading_statusbar) context?.fixPaddingStatusbar(home_loading_statusbar)
home_master_recycler?.adapter = home_master_recycler.adapter =
HomeParentItemAdapterPreview(mutableListOf(), { callback -> ParentItemAdapter(mutableListOf(), { callback ->
homeHandleSearch(callback) homeHandleSearch(callback)
}, { item -> }, { item ->
bottomSheetDialog = activity?.loadHomepageList(item, expandCallback = { activity?.loadHomepageList(item, expandCallback = {
homeViewModel.expandAndReturn(it) homeViewModel.expandAndReturn(it)
}, dismissCallback = {
bottomSheetDialog = null
}) })
}, { name -> }, { name ->
homeViewModel.expand(name) homeViewModel.expand(name)
}, { load ->
activity?.loadResult(load.response.url, load.response.apiName, load.action)
}, {
homeViewModel.loadMoreHomeScrollResponses()
}, {
apiChangeClickListener.onClick(it)
}, reloadStored = {
reloadStored()
}, loadStoredData = {
homeViewModel.loadStoredData(it)
}, { (isQuickSearch, text) ->
if (!isQuickSearch) {
QuickSearchFragment.pushSearch(
activity,
text,
currentApiName?.let { arrayOf(it) })
}
}) })
home_master_recycler.setLinearListLayout()
home_master_recycler?.setMaxViewPoolSize(0, Int.MAX_VALUE)
home_master_recycler.layoutManager = object : LinearLayoutManager(context) {
override fun supportsPredictiveItemAnimations(): Boolean {
return false
}
} // GridLayoutManager(context, 1).also { it.supportsPredictiveItemAnimations() }
reloadStored() reloadStored()
loadHomePage(false) loadHomePage()
home_master_recycler?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { home_loaded.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { v, _, scrollY, _, oldScrollY ->
val dy = scrollY - oldScrollY
if (dy > 0) { //check for scroll down if (dy > 0) { //check for scroll down
home_api_fab?.shrink() // hide home_api_fab?.shrink() // hide
home_random?.shrink() home_random?.shrink()
@ -710,29 +1127,30 @@ class HomeFragment : Fragment() {
home_random?.extend() home_random?.extend()
} }
} }
super.onScrolled(recyclerView, dx, dy)
}
}) })
// nice profile pic on homepage // nice profile pic on homepage
//home_profile_picture_holder?.isVisible = false home_profile_picture_holder?.isVisible = false
// just in case // just in case
if (isTvSettings()) { if (isTvSettings()) {
home_api_fab?.isVisible = false home_api_fab?.isVisible = false
home_change_api?.isVisible = true
if (isTrueTvSettings()) { if (isTrueTvSettings()) {
home_change_api_loading?.isVisible = true home_change_api_loading?.isVisible = true
home_change_api_loading?.isFocusable = true home_change_api_loading?.isFocusable = true
home_change_api_loading?.isFocusableInTouchMode = true home_change_api_loading?.isFocusableInTouchMode = true
home_change_api?.isFocusable = true
home_change_api?.isFocusableInTouchMode = true
} }
// home_bookmark_select?.isFocusable = true // home_bookmark_select?.isFocusable = true
// home_bookmark_select?.isFocusableInTouchMode = true // home_bookmark_select?.isFocusableInTouchMode = true
} else { } else {
home_api_fab?.isVisible = true home_api_fab?.isVisible = true
home_change_api?.isVisible = false
home_change_api_loading?.isVisible = false home_change_api_loading?.isVisible = false
} }
//TODO READD THIS
/*for (syncApi in OAuth2Apis) { for (syncApi in OAuth2Apis) {
val login = syncApi.loginInfo() val login = syncApi.loginInfo()
val pic = login?.profilePicture val pic = login?.profilePicture
if (home_profile_picture?.setImage( if (home_profile_picture?.setImage(
@ -743,6 +1161,6 @@ class HomeFragment : Fragment() {
home_profile_picture_holder?.isVisible = true home_profile_picture_holder?.isVisible = true
break break
} }
}*/ }
} }
} }

View file

@ -4,70 +4,34 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.ListUpdateCallback
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.transition.ChangeBounds
import androidx.transition.TransitionManager
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipDrawable
import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity
import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.HomePageList
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.result.LinearListLayout import com.lagradost.cloudstream3.ui.result.LinearListLayout
import com.lagradost.cloudstream3.ui.result.ResultViewModel2
import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.result.setLinearListLayout
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView
import kotlinx.android.synthetic.main.activity_main_tv.*
import kotlinx.android.synthetic.main.activity_main_tv.view.*
import kotlinx.android.synthetic.main.fragment_home.*
import kotlinx.android.synthetic.main.fragment_home.view.*
import kotlinx.android.synthetic.main.fragment_home_head_tv.*
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.*
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_viewpager
import kotlinx.android.synthetic.main.homepage_parent.view.* import kotlinx.android.synthetic.main.homepage_parent.view.*
class LoadClickCallback(
val action: Int = 0,
val view: View,
val position: Int,
val response: LoadResponse
)
open class ParentItemAdapter( class ParentItemAdapter(
private var items: MutableList<HomeViewModel.ExpandableHomepageList>, private var items: MutableList<HomeViewModel.ExpandableHomepageList>,
private val clickCallback: (SearchClickCallback) -> Unit, private val clickCallback: (SearchClickCallback) -> Unit,
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit, private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit,
private val expandCallback: ((String) -> Unit)? = null, private val expandCallback: ((String) -> Unit)? = null,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
override fun onCreateViewHolder(parent: ViewGroup, i: Int): ParentViewHolder {
//println("onCreateViewHolder $i")
val layout =
if (isTvSettings()) R.layout.homepage_parent_tv else R.layout.homepage_parent
return ParentViewHolder( return ParentViewHolder(
LayoutInflater.from(parent.context).inflate( LayoutInflater.from(parent.context).inflate(layout, parent, false),
if (isTvSettings()) R.layout.homepage_parent_tv else R.layout.homepage_parent,
parent,
false
),
clickCallback, clickCallback,
moreInfoClickCallback, moreInfoClickCallback,
expandCallback expandCallback
@ -75,6 +39,8 @@ open class ParentItemAdapter(
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
//println("onBindViewHolder $position")
when (holder) { when (holder) {
is ParentViewHolder -> { is ParentViewHolder -> {
holder.bind(items[position]) holder.bind(items[position])
@ -115,45 +81,32 @@ open class ParentItemAdapter(
items.clear() items.clear()
items.addAll(new) items.addAll(new)
//val mAdapter = this val mAdapter = this
val delta = if (this@ParentItemAdapter is HomeParentItemAdapterPreview) {
headItems
} else {
0
}
diffResult.dispatchUpdatesTo(object : ListUpdateCallback { diffResult.dispatchUpdatesTo(object : ListUpdateCallback {
override fun onInserted(position: Int, count: Int) { override fun onInserted(position: Int, count: Int) {
//notifyItemRangeChanged(position + delta, count) mAdapter.notifyItemRangeInserted(position, count)
notifyItemRangeInserted(position + delta, count)
} }
override fun onRemoved(position: Int, count: Int) { override fun onRemoved(position: Int, count: Int) {
notifyItemRangeRemoved(position + delta, count) mAdapter.notifyItemRangeRemoved(position, count)
} }
override fun onMoved(fromPosition: Int, toPosition: Int) { override fun onMoved(fromPosition: Int, toPosition: Int) {
notifyItemMoved(fromPosition + delta, toPosition + delta) mAdapter.notifyItemMoved(fromPosition, toPosition)
} }
override fun onChanged(_position: Int, count: Int, payload: Any?) { override fun onChanged(position: Int, count: Int, payload: Any?) {
val position = _position + delta
// I know kinda messy, what this does is using the update or bind instead of onCreateViewHolder -> bind // I know kinda messy, what this does is using the update or bind instead of onCreateViewHolder -> bind
recyclerView?.apply { recyclerView?.apply {
// this loops every viewHolder in the recycle view and checks the position to see if it is within the update range // this loops every viewHolder in the recycle view and checks the position to see if it is within the update range
val missingUpdates = (position until (position + count)).toMutableSet() val missingUpdates = (position until (position + count)).toMutableSet()
for (i in 0 until itemCount) { for (i in 0 until itemCount) {
val child = getChildAt(i) ?: continue val viewHolder = getChildViewHolder(getChildAt(i))
val viewHolder = getChildViewHolder(child) ?: continue val absolutePosition = viewHolder.absoluteAdapterPosition
if (viewHolder !is ParentViewHolder) continue
val absolutePosition = viewHolder.bindingAdapterPosition
if (absolutePosition >= position && absolutePosition < position + count) { if (absolutePosition >= position && absolutePosition < position + count) {
val expand = items.getOrNull(absolutePosition - delta) ?: continue val expand = items.getOrNull(absolutePosition) ?: continue
if (viewHolder is ParentViewHolder) {
missingUpdates -= absolutePosition missingUpdates -= absolutePosition
//println("Updating ${viewHolder.title.text} ($absolutePosition $position) -> ${expand.list.name}")
if (viewHolder.title.text == expand.list.name) { if (viewHolder.title.text == expand.list.name) {
viewHolder.update(expand) viewHolder.update(expand)
} else { } else {
@ -161,14 +114,14 @@ open class ParentItemAdapter(
} }
} }
} }
}
// just in case some item did not get updated // just in case some item did not get updated
for (i in missingUpdates) { for (i in missingUpdates) {
notifyItemChanged(i, payload) mAdapter.notifyItemChanged(i, payload)
} }
} ?: run { } ?: run { // in case we don't have a nice
// in case we don't have a nice mAdapter.notifyItemRangeChanged(position, count, payload)
notifyItemRangeChanged(position, count, payload)
} }
} }
}) })
@ -184,8 +137,9 @@ open class ParentItemAdapter(
private val expandCallback: ((String) -> Unit)? = null, private val expandCallback: ((String) -> Unit)? = null,
) : ) :
RecyclerView.ViewHolder(itemView) { RecyclerView.ViewHolder(itemView) {
val title: TextView = itemView.home_child_more_info val title: TextView = itemView.home_parent_item_title
private val recyclerView: RecyclerView = itemView.home_child_recyclerview val recyclerView: RecyclerView = itemView.home_child_recyclerview
private val moreInfo: FrameLayout? = itemView.home_child_more_info
fun update(expand: HomeViewModel.ExpandableHomepageList) { fun update(expand: HomeViewModel.ExpandableHomepageList) {
val info = expand.list val info = expand.list
@ -247,13 +201,12 @@ open class ParentItemAdapter(
}) })
//(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged() //(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged()
if (!isTvSettings()) {
title.setOnClickListener { moreInfo?.setOnClickListener {
moreInfoClickCallback.invoke(expand) moreInfoClickCallback.invoke(expand)
} }
} }
} }
}
} }
class SearchDiffCallback( class SearchDiffCallback(

View file

@ -1,658 +0,0 @@
package com.lagradost.cloudstream3.ui.home
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipDrawable
import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity
import com.lagradost.cloudstream3.HomePageList
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.result.ResultViewModel2
import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showOptionSelectStringRes
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import kotlinx.android.synthetic.main.activity_main.view.*
import kotlinx.android.synthetic.main.fragment_home_head.view.*
import kotlinx.android.synthetic.main.fragment_home_head.view.home_bookmarked_child_recyclerview
import kotlinx.android.synthetic.main.fragment_home_head.view.home_watch_parent_item_title
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.*
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_bookmarked_holder
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_none_padding
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_plan_to_watch_btt
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_viewpager
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_type_completed_btt
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_type_dropped_btt
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_type_on_hold_btt
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_type_watching_btt
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_watch_child_recyclerview
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_watch_holder
import kotlinx.android.synthetic.main.toast.view.*
class HomeParentItemAdapterPreview(
items: MutableList<HomeViewModel.ExpandableHomepageList>,
val clickCallback: (SearchClickCallback) -> Unit,
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit,
expandCallback: ((String) -> Unit)? = null,
private val loadCallback: (LoadClickCallback) -> Unit,
private val loadMoreCallback: (() -> Unit),
private val changeHomePageCallback: ((View) -> Unit),
private val reloadStored: (() -> Unit),
private val loadStoredData: ((Set<WatchType>) -> Unit),
private val searchQueryCallback: ((Pair<Boolean, String>) -> Unit)
) : ParentItemAdapter(items, clickCallback, moreInfoClickCallback, expandCallback) {
private var previewData: Resource<Pair<Boolean, List<LoadResponse>>> = Resource.Loading()
private var resumeWatchingData: List<SearchResponse> = listOf()
private var bookmarkData: Pair<Boolean, List<SearchResponse>> =
false to listOf()
private var apiName: String = "NONE"
val headItems = 1
private var availableWatchStatusTypes: Pair<Set<WatchType>, Set<WatchType>> =
setOf<WatchType>() to setOf()
fun setAvailableWatchStatusTypes(data: Pair<Set<WatchType>, Set<WatchType>>) {
availableWatchStatusTypes = data
holder?.setAvailableWatchStatusTypes(data)
}
companion object {
private const val VIEW_TYPE_HEADER = 2
private const val VIEW_TYPE_ITEM = 1
}
fun setResumeWatchingData(resumeWatching: List<SearchResponse>) {
resumeWatchingData = resumeWatching
holder?.updateResume(resumeWatchingData)
}
fun setPreviewData(preview: Resource<Pair<Boolean, List<LoadResponse>>>) {
previewData = preview
holder?.updatePreview(preview)
}
fun setApiName(name: String) {
apiName = name
holder?.updateApiName(name)
}
fun setBookmarkData(data: Pair<Boolean, List<SearchResponse>>) {
bookmarkData = data
holder?.updateBookmarks(data)
}
override fun getItemViewType(position: Int) = when (position) {
0 -> VIEW_TYPE_HEADER
else -> VIEW_TYPE_ITEM
}
var holder: HeaderViewHolder? = null
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is HeaderViewHolder -> {
holder.updatePreview(previewData)
holder.updateResume(resumeWatchingData)
holder.updateBookmarks(bookmarkData)
holder.setAvailableWatchStatusTypes(availableWatchStatusTypes)
holder.updateApiName(apiName)
}
else -> super.onBindViewHolder(holder, position - 1)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
println("onCreateViewHolder $viewType")
return when (viewType) {
VIEW_TYPE_HEADER -> HeaderViewHolder(
LayoutInflater.from(parent.context).inflate(
if (isTvSettings()) R.layout.fragment_home_head_tv else R.layout.fragment_home_head,
parent,
false
),
loadCallback,
loadMoreCallback,
changeHomePageCallback,
clickCallback,
reloadStored,
loadStoredData,
searchQueryCallback,
moreInfoClickCallback
).also {
this.holder = it
}
VIEW_TYPE_ITEM -> super.onCreateViewHolder(parent, viewType)
else -> error("Unhandled viewType=$viewType")
}
}
override fun getItemCount(): Int {
return super.getItemCount() + headItems
}
override fun getItemId(position: Int): Long {
if (position == 0) return previewData.hashCode().toLong()
return super.getItemId(position - headItems)
}
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
when (holder) {
is HeaderViewHolder -> {
holder.onViewDetachedFromWindow()
}
else -> super.onViewDetachedFromWindow(holder)
}
}
override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
when (holder) {
is HeaderViewHolder -> {
holder.onViewAttachedToWindow()
}
else -> super.onViewAttachedToWindow(holder)
}
}
class HeaderViewHolder
constructor(
itemView: View,
private val clickCallback: ((LoadClickCallback) -> Unit)?,
private val loadMoreCallback: (() -> Unit),
private val changeHomePageCallback: ((View) -> Unit),
private val searchClickCallback: (SearchClickCallback) -> Unit,
private val reloadStored: () -> Unit,
private val loadStoredData: ((Set<WatchType>) -> Unit),
private val searchQueryCallback: ((Pair<Boolean, String>) -> Unit),
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit
) : RecyclerView.ViewHolder(itemView) {
private var previewAdapter: HomeScrollAdapter? = null
private val previewViewpager: ViewPager2? = itemView.home_preview_viewpager
private val previewHeader: FrameLayout? = itemView.home_preview
private val previewCallback: ViewPager2.OnPageChangeCallback =
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
// home_search?.isIconified = true
//home_search?.isVisible = true
//home_search?.clearFocus()
previewAdapter?.apply {
if (position >= itemCount - 1 && hasMoreItems) {
hasMoreItems = false // dont make two requests
loadMoreCallback()
//homeViewModel.loadMoreHomeScrollResponses()
}
}
previewAdapter?.getItem(position)
?.apply {
//itemView.home_preview_title_holder?.let { parent ->
// TransitionManager.beginDelayedTransition(
// parent,
// ChangeBounds()
// )
//}
itemView.home_preview_description?.isGone =
this.plot.isNullOrBlank()
itemView.home_preview_description?.text =
this.plot ?: ""
itemView.home_preview_text?.text = this.name
itemView.home_preview_tags?.apply {
removeAllViews()
tags?.forEach { tag ->
val chip = Chip(context)
val chipDrawable =
ChipDrawable.createFromAttributes(
context,
null,
0,
R.style.ChipFilledSemiTransparent
)
chip.setChipDrawable(chipDrawable)
chip.text = tag
chip.isChecked = false
chip.isCheckable = false
chip.isFocusable = false
chip.isClickable = false
addView(chip)
}
}
itemView.home_preview_tags?.isGone =
tags.isNullOrEmpty()
itemView.home_preview_image?.setImage(
posterUrl,
posterHeaders
)
// itemView.home_preview_title?.text = name
itemView.home_preview_play?.setOnClickListener { view ->
clickCallback?.invoke(
LoadClickCallback(
START_ACTION_RESUME_LATEST,
view,
position,
this
)
)
}
itemView.home_preview_info?.setOnClickListener { view ->
clickCallback?.invoke(
LoadClickCallback(0, view, position, this)
)
}
itemView.home_preview_play_btt?.setOnClickListener { view ->
clickCallback?.invoke(
LoadClickCallback(
START_ACTION_RESUME_LATEST,
view,
position,
this
)
)
}
// This makes the hidden next buttons only available when on the info button
// Otherwise you might be able to go to the next item without being at the info button
itemView.home_preview_info_btt?.setOnFocusChangeListener { _, hasFocus ->
itemView.home_preview_hidden_next_focus?.isFocusable = hasFocus
}
itemView.home_preview_play_btt?.setOnFocusChangeListener { _, hasFocus ->
itemView.home_preview_hidden_prev_focus?.isFocusable = hasFocus
}
itemView.home_preview_info_btt?.setOnClickListener { view ->
clickCallback?.invoke(
LoadClickCallback(0, view, position, this)
)
}
itemView.home_preview_hidden_next_focus?.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
previewViewpager?.apply {
setCurrentItem(currentItem + 1, true)
}
itemView.home_preview_info_btt?.requestFocus()
}
}
itemView.home_preview_hidden_prev_focus?.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
previewViewpager?.apply {
if (currentItem <= 0) {
nav_rail_view?.menu?.getItem(0)?.actionView?.requestFocus()
} else {
setCurrentItem(currentItem - 1, true)
itemView.home_preview_play_btt?.requestFocus()
}
}
}
}
// very ugly code, but I dont care
val watchType =
DataStoreHelper.getResultWatchState(this.getId())
itemView.home_preview_bookmark?.setText(watchType.stringRes)
itemView.home_preview_bookmark?.setCompoundDrawablesWithIntrinsicBounds(
null,
ContextCompat.getDrawable(
itemView.home_preview_bookmark.context,
watchType.iconRes
),
null,
null
)
itemView.home_preview_bookmark?.setOnClickListener { fab ->
fab.context.getActivity()?.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]
itemView.home_preview_bookmark?.setCompoundDrawablesWithIntrinsicBounds(
null,
ContextCompat.getDrawable(
itemView.home_preview_bookmark.context,
newValue.iconRes
),
null,
null
)
itemView.home_preview_bookmark?.setText(newValue.stringRes)
ResultViewModel2.updateWatchStatus(
this,
newValue
)
reloadStored()
}
}
}
}
}
private var resumeAdapter: HomeChildItemAdapter? = null
private var resumeHolder: View? = itemView.home_watch_holder
private var resumeRecyclerView: RecyclerView? = itemView.home_watch_child_recyclerview
private var bookmarkHolder: View? = itemView.home_bookmarked_holder
private var bookmarkAdapter: HomeChildItemAdapter? = null
private var bookmarkRecyclerView: RecyclerView? =
itemView.home_bookmarked_child_recyclerview
fun onViewDetachedFromWindow() {
previewViewpager?.unregisterOnPageChangeCallback(previewCallback)
}
fun onViewAttachedToWindow() {
previewViewpager?.registerOnPageChangeCallback(previewCallback)
}
private val toggleList = listOf(
Pair(itemView.home_type_watching_btt, WatchType.WATCHING),
Pair(itemView.home_type_completed_btt, WatchType.COMPLETED),
Pair(itemView.home_type_dropped_btt, WatchType.DROPPED),
Pair(itemView.home_type_on_hold_btt, WatchType.ONHOLD),
Pair(itemView.home_plan_to_watch_btt, WatchType.PLANTOWATCH),
)
init {
itemView.home_preview_change_api?.setOnClickListener { view ->
changeHomePageCallback(view)
}
itemView.home_preview_change_api2?.setOnClickListener { view ->
changeHomePageCallback(view)
}
previewViewpager?.apply {
//if (!isTvSettings())
setPageTransformer(HomeScrollTransformer())
//else
// setPageTransformer(null)
if (adapter == null)
adapter = HomeScrollAdapter(
if (isTvSettings()) R.layout.home_scroll_view_tv else R.layout.home_scroll_view,
if (isTvSettings()) true else null
)
}
previewAdapter = previewViewpager?.adapter as? HomeScrollAdapter?
// previewViewpager?.registerOnPageChangeCallback(previewCallback)
if (resumeAdapter == null) {
resumeRecyclerView?.adapter = HomeChildItemAdapter(
ArrayList(),
nextFocusUp = itemView.nextFocusUpId,
nextFocusDown = itemView.nextFocusDownId
) { callback ->
if (callback.action != SEARCH_ACTION_SHOW_METADATA) {
searchClickCallback(callback)
return@HomeChildItemAdapter
}
callback.view.context?.getActivity()?.showOptionSelectStringRes(
callback.view,
callback.card.posterUrl,
listOf(
R.string.action_open_watching,
R.string.action_remove_watching
),
listOf(
R.string.action_open_play,
R.string.action_open_watching,
R.string.action_remove_watching
)
) { (isTv, actionId) ->
when (actionId + if (isTv) 0 else 1) {
// play
0 -> {
searchClickCallback.invoke(
SearchClickCallback(
START_ACTION_RESUME_LATEST,
callback.view,
-1,
callback.card
)
)
reloadStored()
}
//info
1 -> {
searchClickCallback(
SearchClickCallback(
SEARCH_ACTION_LOAD,
callback.view,
-1,
callback.card
)
)
reloadStored()
}
// remove
2 -> {
val card = callback.card
if (card is DataStoreHelper.ResumeWatchingResult) {
DataStoreHelper.removeLastWatched(card.parentId)
reloadStored()
}
}
}
}
}
}
resumeAdapter = resumeRecyclerView?.adapter as? HomeChildItemAdapter
if (bookmarkAdapter == null) {
bookmarkRecyclerView?.adapter = HomeChildItemAdapter(
ArrayList(),
nextFocusUp = itemView.nextFocusUpId,
nextFocusDown = itemView.nextFocusDownId
) { callback ->
if (callback.action != SEARCH_ACTION_SHOW_METADATA) {
searchClickCallback(callback)
return@HomeChildItemAdapter
}
callback.view.context?.getActivity()?.showOptionSelectStringRes(
callback.view,
callback.card.posterUrl,
listOf(
R.string.action_open_watching,
R.string.action_remove_from_bookmarks,
),
listOf(
R.string.action_open_play,
R.string.action_open_watching,
R.string.action_remove_from_bookmarks
)
) { (isTv, actionId) ->
when (actionId + if (isTv) 0 else 1) { // play
0 -> {
searchClickCallback.invoke(
SearchClickCallback(
START_ACTION_RESUME_LATEST,
callback.view,
-1,
callback.card
)
)
reloadStored()
}
1 -> { // info
searchClickCallback(
SearchClickCallback(
SEARCH_ACTION_LOAD,
callback.view,
-1,
callback.card
)
)
reloadStored()
}
2 -> { // remove
DataStoreHelper.setResultWatchState(
callback.card.id,
WatchType.NONE.internalId
)
reloadStored()
}
}
}
}
}
bookmarkAdapter = bookmarkRecyclerView?.adapter as? HomeChildItemAdapter
for ((chip, watch) in toggleList) {
chip?.isChecked = false
chip?.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
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 }) {
loadStoredData(emptySet())
}
}
}
itemView.home_search?.context?.fixPaddingStatusbar(itemView.home_search)
itemView.home_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
searchQueryCallback.invoke(false to query)
//QuickSearchFragment.pushSearch(activity, query, currentApiName?.let { arrayOf(it) }
return true
}
override fun onQueryTextChange(newText: String): Boolean {
searchQueryCallback.invoke(true to newText)
//searchViewModel.quickSearch(newText)
return true
}
})
}
fun updateApiName(name: String) {
itemView.home_preview_change_api2?.text = name
itemView.home_preview_change_api?.text = name
}
fun updatePreview(preview: Resource<Pair<Boolean, List<LoadResponse>>>) {
itemView.home_preview_change_api2?.isGone = preview is Resource.Success
if (preview is Resource.Success) {
itemView.home_none_padding?.apply {
val params = layoutParams
params.height = 0
layoutParams = params
}
} else {
itemView.home_none_padding?.context?.fixPaddingStatusbarView(itemView.home_none_padding)
}
when (preview) {
is Resource.Success -> {
if (true != previewAdapter?.setItems(
preview.value.second,
preview.value.first
)
) {
// this might seam weird and useless, however this prevents a very weird andrid bug were the viewpager is not rendered properly
// I have no idea why that happens, but this is my ducktape solution
previewViewpager?.setCurrentItem(0, false)
previewViewpager?.beginFakeDrag()
previewViewpager?.fakeDragBy(1f)
previewViewpager?.endFakeDrag()
previewCallback.onPageSelected(0)
previewHeader?.isVisible = true
}
}
else -> {
previewAdapter?.setItems(listOf(), false)
previewViewpager?.setCurrentItem(0, false)
previewHeader?.isVisible = false
}
}
// previewViewpager?.postDelayed({ previewViewpager?.scr(100, 0) }, 1000)
//previewViewpager?.postInvalidate()
}
fun updateResume(resumeWatching: List<SearchResponse>) {
resumeHolder?.isVisible = resumeWatching.isNotEmpty()
resumeAdapter?.updateList(resumeWatching)
if (!isTvSettings()) {
itemView.home_watch_parent_item_title?.setOnClickListener {
moreInfoClickCallback.invoke(
HomeViewModel.ExpandableHomepageList(
HomePageList(
itemView.home_watch_parent_item_title?.text.toString(),
resumeWatching,
false
), 1, false
)
)
}
}
}
fun updateBookmarks(data: Pair<Boolean, List<SearchResponse>>) {
bookmarkHolder?.isVisible = data.first
bookmarkAdapter?.updateList(data.second)
if (!isTvSettings()) {
itemView.home_bookmark_parent_item_title?.setOnClickListener {
val items = toggleList.mapNotNull { it.first }.filter { it.isChecked }
if (items.isEmpty()) return@setOnClickListener // we don't want to show an empty dialog
val textSum = items
.mapNotNull { it.text }.joinToString()
moreInfoClickCallback.invoke(
HomeViewModel.ExpandableHomepageList(
HomePageList(
textSum,
data.second,
false
), 1, false
)
)
}
}
}
fun setAvailableWatchStatusTypes(availableWatchStatusTypes: Pair<Set<WatchType>, Set<WatchType>>) {
for ((chip, watch) in toggleList) {
chip?.apply {
isVisible = availableWatchStatusTypes.second.contains(watch)
isChecked = availableWatchStatusTypes.first.contains(watch)
}
}
}
}
}

View file

@ -1,29 +1,22 @@
package com.lagradost.cloudstream3.ui.home package com.lagradost.cloudstream3.ui.home
import android.content.res.Configuration
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.setImage
import kotlinx.android.synthetic.main.fragment_home_head_tv.*
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.*
import kotlinx.android.synthetic.main.home_scroll_view.view.* import kotlinx.android.synthetic.main.home_scroll_view.view.*
class HomeScrollAdapter( class HomeScrollAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
@LayoutRes val layout: Int = R.layout.home_scroll_view,
private val forceHorizontalPosters: Boolean? = null
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var items: MutableList<LoadResponse> = mutableListOf() private var items: MutableList<LoadResponse> = mutableListOf()
var hasMoreItems: Boolean = false var hasMoreItems: Boolean = false
fun getItem(position: Int): LoadResponse? { fun getItem(position: Int) : LoadResponse? {
return items.getOrNull(position) return items.getOrNull(position)
} }
@ -46,8 +39,7 @@ class HomeScrollAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return CardViewHolder( return CardViewHolder(
LayoutInflater.from(parent.context).inflate(layout, parent, false), LayoutInflater.from(parent.context).inflate(R.layout.home_scroll_view, parent, false),
forceHorizontalPosters
) )
} }
@ -62,17 +54,11 @@ class HomeScrollAdapter(
class CardViewHolder class CardViewHolder
constructor( constructor(
itemView: View, itemView: View,
private val forceHorizontalPosters: Boolean? = null
) : ) :
RecyclerView.ViewHolder(itemView) { RecyclerView.ViewHolder(itemView) {
fun bind(card: LoadResponse) { fun bind(card: LoadResponse) {
card.apply { card.apply {
val isHorizontal =
(forceHorizontalPosters == true) || ((forceHorizontalPosters != false) && 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?.text = tags?.joinToString("") ?: ""
itemView.home_scroll_preview_tags?.isGone = tags.isNullOrEmpty() itemView.home_scroll_preview_tags?.isGone = tags.isNullOrEmpty()
itemView.home_scroll_preview?.setImage(posterUrl, posterHeaders) itemView.home_scroll_preview?.setImage(posterUrl, posterHeaders)

View file

@ -36,42 +36,6 @@ import java.util.*
import kotlin.collections.set import kotlin.collections.set
class HomeViewModel : ViewModel() { 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 var repo: APIRepository? = null
private val _apiName = MutableLiveData<String>() private val _apiName = MutableLiveData<String>()
@ -102,7 +66,36 @@ class HomeViewModel : ViewModel() {
val preview: LiveData<Resource<Pair<Boolean, List<LoadResponse>>>> = _preview val preview: LiveData<Resource<Pair<Boolean, List<LoadResponse>>>> = _preview
fun loadResumeWatching() = viewModelScope.launchSafe { fun loadResumeWatching() = viewModelScope.launchSafe {
val resumeWatchingResult = getResumeWatching() 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()
}
resumeWatchingResult?.let { resumeWatchingResult?.let {
_resumeWatching.postValue(it) _resumeWatching.postValue(it)
} }
@ -128,7 +121,6 @@ class HomeViewModel : ViewModel() {
currentWatchTypes.remove(WatchType.NONE) currentWatchTypes.remove(WatchType.NONE)
if (currentWatchTypes.size <= 0) { if (currentWatchTypes.size <= 0) {
_availableWatchStatusTypes.postValue(setOf<WatchType>() to setOf())
_bookmarks.postValue(Pair(false, ArrayList())) _bookmarks.postValue(Pair(false, ArrayList()))
return@launchSafe return@launchSafe
} }
@ -265,13 +257,7 @@ class HomeViewModel : ViewModel() {
_apiName.postValue(repo?.name) _apiName.postValue(repo?.name)
_randomItems.postValue(listOf()) _randomItems.postValue(listOf())
if (repo?.hasMainPage != true) { if (repo?.hasMainPage == true) {
_page.postValue(Resource.Success(emptyMap()))
_preview.postValue(Resource.Failure(false, null, null, "No homepage"))
return@ioSafe
}
_page.postValue(Resource.Loading()) _page.postValue(Resource.Loading())
_preview.postValue(Resource.Loading()) _preview.postValue(Resource.Loading())
addJob?.cancel() addJob?.cancel()
@ -339,10 +325,13 @@ class HomeViewModel : ViewModel() {
} }
is Resource.Failure -> { is Resource.Failure -> {
_page.postValue(data!!) _page.postValue(data!!)
_preview.postValue(data!!)
} }
else -> Unit else -> Unit
} }
} else {
_page.postValue(Resource.Success(emptyMap()))
_preview.postValue(Resource.Failure(false, null, null, "No homepage"))
}
} }
fun loadAndCancel(preferredApiName: String?, forceReload: Boolean = true) = fun loadAndCancel(preferredApiName: String?, forceReload: Boolean = true) =

View file

@ -1,395 +0,0 @@
package com.lagradost.cloudstream3.ui.library
import android.app.Activity
import android.content.Context
import android.content.res.Configuration
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AlphaAnimation
import androidx.annotation.StringRes
import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import com.google.android.material.tabs.TabLayoutMediator
import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.APIHolder.allProviders
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.debugAssert
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
import kotlinx.android.synthetic.main.fragment_library.*
import kotlin.math.abs
const val LIBRARY_FOLDER = "library_folder"
enum class LibraryOpenerType(@StringRes val stringRes: Int) {
Default(R.string.default_subtitles), // TODO FIX AFTER MERGE
Provider(R.string.none),
Browser(R.string.browser),
Search(R.string.search),
None(R.string.none),
}
/** Used to store how the user wants to open said poster */
data class LibraryOpener(
val openType: LibraryOpenerType,
val providerData: ProviderLibraryData?,
)
data class ProviderLibraryData(
val apiName: String
)
class LibraryFragment : Fragment() {
companion object {
fun newInstance() = LibraryFragment()
/**
* Store which page was last seen when exiting the fragment and returning
**/
const val VIEWPAGER_ITEM_KEY = "viewpager_item"
}
private val libraryViewModel: LibraryViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_library, container, false)
}
override fun onSaveInstanceState(outState: Bundle) {
viewpager?.currentItem?.let { currentItem ->
outState.putInt(VIEWPAGER_ITEM_KEY, currentItem)
}
super.onSaveInstanceState(outState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
context?.fixPaddingStatusbar(search_status_bar_padding)
sort_fab?.setOnClickListener {
val methods = libraryViewModel.sortingMethods.map {
txt(it.stringRes).asString(view.context)
}
activity?.showBottomDialog(methods,
libraryViewModel.sortingMethods.indexOf(libraryViewModel.currentSortingMethod),
txt(R.string.sort_by).asString(view.context),
false,
{},
{
val method = libraryViewModel.sortingMethods[it]
libraryViewModel.sort(method)
})
}
main_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
libraryViewModel.sort(ListSorting.Query, query)
return true
}
// This is required to prevent the first text change
// When this is attached it'll immediately send a onQueryTextChange("")
// Which we do not want
var hasInitialized = false
override fun onQueryTextChange(newText: String?): Boolean {
if (!hasInitialized) {
hasInitialized = true
return true
}
libraryViewModel.sort(ListSorting.Query, newText)
return true
}
})
libraryViewModel.reloadPages(false)
list_selector?.setOnClickListener {
val items = libraryViewModel.availableApiNames
val currentItem = libraryViewModel.currentApiName.value
activity?.showBottomDialog(items,
items.indexOf(currentItem),
txt(R.string.select_library).asString(it.context),
false,
{}) { index ->
val selectedItem = items.getOrNull(index) ?: return@showBottomDialog
libraryViewModel.switchList(selectedItem)
}
}
/**
* Shows a plugin selection dialogue and saves the response
**/
fun Activity.showPluginSelectionDialog(
key: String,
syncId: SyncIdName,
apiName: String? = null,
) {
val availableProviders = allProviders.filter {
it.supportedSyncNames.contains(syncId)
}.map { it.name } +
// Add the api if it exists
(APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) } ?: emptyList())
val baseOptions = listOf(
LibraryOpenerType.Default,
LibraryOpenerType.None,
LibraryOpenerType.Browser,
LibraryOpenerType.Search
)
val items = baseOptions.map { txt(it.stringRes).asString(this) } + availableProviders
val savedSelection = getKey<LibraryOpener>(LIBRARY_FOLDER, key)
val selectedIndex =
when {
savedSelection == null -> 0
// If provider
savedSelection.openType == LibraryOpenerType.Provider
&& savedSelection.providerData?.apiName != null -> {
availableProviders.indexOf(savedSelection.providerData.apiName)
.takeIf { it != -1 }
?.plus(baseOptions.size) ?: 0
}
// Else base option
else -> baseOptions.indexOf(savedSelection.openType)
}
this.showBottomDialog(
items,
selectedIndex,
txt(R.string.open_with).asString(this),
false,
{},
) {
val savedData = if (it < baseOptions.size) {
LibraryOpener(
baseOptions[it],
null
)
} else {
LibraryOpener(
LibraryOpenerType.Provider,
ProviderLibraryData(items[it])
)
}
setKey(
LIBRARY_FOLDER,
key,
savedData,
)
}
}
provider_selector?.setOnClickListener {
val syncName = libraryViewModel.currentSyncApi?.syncIdName ?: return@setOnClickListener
activity?.showPluginSelectionDialog(syncName.name, syncName)
}
viewpager?.setPageTransformer(LibraryScrollTransformer())
viewpager?.adapter =
viewpager.adapter ?: ViewpagerAdapter(mutableListOf(), { isScrollingDown: Boolean ->
if (isScrollingDown) {
sort_fab?.shrink()
} else {
sort_fab?.extend()
}
}) callback@{ searchClickCallback ->
// To prevent future accidents
debugAssert({
searchClickCallback.card !is SyncAPI.LibraryItem
}, {
"searchClickCallback ${searchClickCallback.card} is not a LibraryItem"
})
val syncId = (searchClickCallback.card as SyncAPI.LibraryItem).syncId
val syncName =
libraryViewModel.currentSyncApi?.syncIdName ?: return@callback
when (searchClickCallback.action) {
SEARCH_ACTION_SHOW_METADATA -> {
activity?.showPluginSelectionDialog(
syncId,
syncName,
searchClickCallback.card.apiName
)
}
SEARCH_ACTION_LOAD -> {
// This basically first selects the individual opener and if that is default then
// selects the whole list opener
val savedListSelection =
getKey<LibraryOpener>(LIBRARY_FOLDER, syncName.name)
val savedSelection = getKey<LibraryOpener>(LIBRARY_FOLDER, syncId).takeIf {
it?.openType != LibraryOpenerType.Default
} ?: savedListSelection
when (savedSelection?.openType) {
null, LibraryOpenerType.Default -> {
// Prevents opening MAL/AniList as a provider
if (APIHolder.getApiFromNameNull(searchClickCallback.card.apiName) != null) {
activity?.loadSearchResult(
searchClickCallback.card
)
} else {
// Search when no provider can open
QuickSearchFragment.pushSearch(
activity,
searchClickCallback.card.name
)
}
}
LibraryOpenerType.None -> {}
LibraryOpenerType.Provider ->
savedSelection.providerData?.apiName?.let { apiName ->
activity?.loadResult(
searchClickCallback.card.url,
apiName,
)
}
LibraryOpenerType.Browser ->
openBrowser(searchClickCallback.card.url)
LibraryOpenerType.Search -> {
QuickSearchFragment.pushSearch(
activity,
searchClickCallback.card.name
)
}
}
}
}
}
viewpager?.offscreenPageLimit = 2
viewpager?.reduceDragSensitivity()
val startLoading = Runnable {
gridview?.numColumns = context?.getSpanCount() ?: 3
gridview?.adapter =
context?.let { LoadingPosterAdapter(it, 6 * 3) }
library_loading_overlay?.isVisible = true
library_loading_shimmer?.startShimmer()
empty_list_textview?.isVisible = false
}
val stopLoading = Runnable {
gridview?.adapter = null
library_loading_overlay?.isVisible = false
library_loading_shimmer?.stopShimmer()
}
val handler = Handler(Looper.getMainLooper())
observe(libraryViewModel.pages) { resource ->
when (resource) {
is Resource.Success -> {
handler.removeCallbacks(startLoading)
val pages = resource.value
val showNotice = pages.all { it.items.isEmpty() }
empty_list_textview?.isVisible = showNotice
if (showNotice) {
if (libraryViewModel.availableApiNames.size > 1) {
empty_list_textview?.setText(R.string.empty_library_logged_in_message)
} else {
empty_list_textview?.setText(R.string.empty_library_no_accounts_message)
}
}
(viewpager.adapter as? ViewpagerAdapter)?.pages = pages
// Using notifyItemRangeChanged keeps the animations when sorting
viewpager.adapter?.notifyItemRangeChanged(0, viewpager.adapter?.itemCount ?: 0)
// Only stop loading after 300ms to hide the fade effect the viewpager produces when updating
// Without this there would be a flashing effect:
// loading -> show old viewpager -> black screen -> show new viewpager
handler.postDelayed(stopLoading, 300)
savedInstanceState?.getInt(VIEWPAGER_ITEM_KEY)?.let { currentPos ->
if (currentPos < 0) return@let
viewpager?.setCurrentItem(currentPos, false)
// Using remove() sets the key to 0 instead of removing it
savedInstanceState.putInt(VIEWPAGER_ITEM_KEY, -1)
}
// Since the animation to scroll multiple items is so much its better to just hide
// the viewpager a bit while the fastest animation is running
fun hideViewpager(distance: Int) {
if (distance < 3) return
val hideAnimation = AlphaAnimation(1f, 0f).apply {
duration = distance * 50L
fillAfter = true
}
val showAnimation = AlphaAnimation(0f, 1f).apply {
duration = distance * 50L
startOffset = distance * 100L
fillAfter = true
}
viewpager?.startAnimation(hideAnimation)
viewpager?.startAnimation(showAnimation)
}
TabLayoutMediator(
library_tab_layout,
viewpager,
) { tab, position ->
tab.text = pages.getOrNull(position)?.title?.asStringNull(context)
tab.view.setOnClickListener {
val currentItem = viewpager?.currentItem ?: return@setOnClickListener
val distance = abs(position - currentItem)
hideViewpager(distance)
}
}.attach()
}
is Resource.Loading -> {
// Only start loading after 200ms to prevent loading cached lists
handler.postDelayed(startLoading, 200)
}
is Resource.Failure -> {
stopLoading.run()
// No user indication it failed :(
// TODO
}
}
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
(viewpager.adapter as? ViewpagerAdapter)?.rebind()
super.onConfigurationChanged(newConfig)
}
}
class MenuSearchView(context: Context) : SearchView(context) {
override fun onActionViewCollapsed() {
super.onActionViewCollapsed()
}
}

View file

@ -1,17 +0,0 @@
package com.lagradost.cloudstream3.ui.library
import android.view.View
import androidx.viewpager2.widget.ViewPager2
import kotlinx.android.synthetic.main.library_viewpager_page.view.*
import kotlin.math.roundToInt
class LibraryScrollTransformer : ViewPager2.PageTransformer {
override fun transformPage(page: View, position: Float) {
val padding = (-position * page.width).roundToInt()
page.page_recyclerview.setPadding(
padding, 0,
-padding, 0
)
}
}

View file

@ -1,104 +0,0 @@
package com.lagradost.cloudstream3.ui.library
import androidx.annotation.StringRes
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import kotlinx.coroutines.delay
enum class ListSorting(@StringRes val stringRes: Int) {
Query(R.string.none),
RatingHigh(R.string.sort_rating_desc),
RatingLow(R.string.sort_rating_asc),
UpdatedNew(R.string.sort_updated_new),
UpdatedOld(R.string.sort_updated_old),
AlphabeticalA(R.string.sort_alphabetical_a),
AlphabeticalZ(R.string.sort_alphabetical_z),
}
const val LAST_SYNC_API_KEY = "last_sync_api"
class LibraryViewModel : ViewModel() {
private val _pages: MutableLiveData<Resource<List<SyncAPI.Page>>> = MutableLiveData(null)
val pages: LiveData<Resource<List<SyncAPI.Page>>> = _pages
private val _currentApiName: MutableLiveData<String> = MutableLiveData("")
val currentApiName: LiveData<String> = _currentApiName
private val availableSyncApis
get() = SyncApis.filter { it.hasAccount() }
var currentSyncApi = availableSyncApis.let { allApis ->
val lastSelection = getKey<String>(LAST_SYNC_API_KEY)
availableSyncApis.firstOrNull { it.name == lastSelection } ?: allApis.firstOrNull()
}
private set(value) {
field = value
setKey(LAST_SYNC_API_KEY, field?.name)
}
val availableApiNames: List<String>
get() = availableSyncApis.map { it.name }
var sortingMethods = emptyList<ListSorting>()
private set
var currentSortingMethod: ListSorting? = sortingMethods.firstOrNull()
private set
fun switchList(name: String) {
currentSyncApi = availableSyncApis[availableApiNames.indexOf(name)]
_currentApiName.postValue(currentSyncApi?.name)
reloadPages(true)
}
fun sort(method: ListSorting, query: String? = null) {
val currentList = pages.value ?: return
currentSortingMethod = method
(currentList as? Resource.Success)?.value?.forEachIndexed { _, page ->
page.sort(method, query)
}
_pages.postValue(currentList)
}
fun reloadPages(forceReload: Boolean) {
// Only skip loading if its not forced and pages is not empty
if (!forceReload && (pages.value as? Resource.Success)?.value?.isNotEmpty() == true &&
currentSyncApi?.requireLibraryRefresh != true
) return
ioSafe {
currentSyncApi?.let { repo ->
_currentApiName.postValue(repo.name)
_pages.postValue(Resource.Loading())
val libraryResource = repo.getPersonalLibrary()
if (libraryResource is Resource.Failure) {
_pages.postValue(libraryResource)
return@let
}
val library = (libraryResource as? Resource.Success)?.value ?: return@let
sortingMethods = library.supportedListSorting.toList()
currentSortingMethod = null
repo.requireLibraryRefresh = false
val pages = library.allLibraryLists.map {
SyncAPI.Page(
it.name,
it.items
)
}
_pages.postValue(Resource.Success(pages))
}
}
}
}

View file

@ -1,37 +0,0 @@
package com.lagradost.cloudstream3.ui.library
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.ListPopupWindow.MATCH_PARENT
import android.widget.RelativeLayout
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import kotlinx.android.synthetic.main.loading_poster_dynamic.view.*
import kotlin.math.roundToInt
import kotlin.math.sqrt
class LoadingPosterAdapter(context: Context, private val itemCount: Int) :
BaseAdapter() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
override fun getCount(): Int {
return itemCount
}
override fun getItem(position: Int): Any? {
return null
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
return convertView ?: inflater.inflate(R.layout.loading_poster_dynamic, parent, false)
}
}

View file

@ -1,130 +0,0 @@
package com.lagradost.cloudstream3.ui.library
import android.content.res.ColorStateList
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.AcraApplication
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.utils.AppUtils
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import kotlinx.android.synthetic.main.search_result_grid_expanded.view.*
import kotlin.math.roundToInt
class PageAdapter(
override val items: MutableList<SyncAPI.LibraryItem>,
private val resView: AutofitRecyclerView,
val clickCallback: (SearchClickCallback) -> Unit
) :
AppUtils.DiffAdapter<SyncAPI.LibraryItem>(items) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return LibraryItemViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.search_result_grid_expanded, parent, false)
)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is LibraryItemViewHolder -> {
holder.bind(items[position], position)
}
}
}
private fun isDark(color: Int): Boolean {
return ColorUtils.calculateLuminance(color) < 0.5
}
fun getDifferentColor(color: Int, ratio: Float = 0.7f): Int {
return if (isDark(color)) {
ColorUtils.blendARGB(color, Color.WHITE, ratio)
} else {
ColorUtils.blendARGB(color, Color.BLACK, ratio)
}
}
inner class LibraryItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val cardView: ImageView = itemView.imageView
private val compactView = false//itemView.context.getGridIsCompact()
private val coverHeight: Int =
if (compactView) 80.toPx else (resView.itemWidth / 0.68).roundToInt()
fun bind(item: SyncAPI.LibraryItem, position: Int) {
/** https://stackoverflow.com/questions/8817522/how-to-get-color-code-of-image-view */
SearchResultBuilder.bind(
this@PageAdapter.clickCallback,
item,
position,
itemView,
colorCallback = { palette ->
AcraApplication.context?.let { ctx ->
val defColor = ContextCompat.getColor(ctx, R.color.ratingColorBg)
var bg = palette.getDarkVibrantColor(defColor)
if (bg == defColor) {
bg = palette.getDarkMutedColor(defColor)
}
if (bg == defColor) {
bg = palette.getVibrantColor(defColor)
}
val fg =
getDifferentColor(bg)//palette.getVibrantColor(ContextCompat.getColor(ctx,R.color.ratingColor))
itemView.text_rating.apply {
setTextColor(ColorStateList.valueOf(fg))
}
itemView.text_rating_holder?.backgroundTintList = ColorStateList.valueOf(bg)
itemView.watchProgress?.apply {
progressTintList = ColorStateList.valueOf(fg)
progressBackgroundTintList = ColorStateList.valueOf(bg)
}
}
}
)
// See searchAdaptor for this, it basically fixes the height
if (!compactView) {
cardView.apply {
layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
coverHeight
)
}
}
val showProgress = item.episodesCompleted != null && item.episodesTotal != null
itemView.watchProgress.isVisible = showProgress
if (showProgress) {
itemView.watchProgress.max = item.episodesTotal!!
itemView.watchProgress.progress = item.episodesCompleted!!
}
itemView.imageText.text = item.name
val showRating = (item.personalRating ?: 0) != 0
itemView.text_rating_holder.isVisible = showRating
if (showRating) {
// We want to show 8.5 but not 8.0 hence the replace
val rating = ((item.personalRating ?: 0).toDouble() / 10).toString()
.replace(".0", "")
itemView.text_rating.text = "$rating"
}
}
}
}

View file

@ -1,90 +0,0 @@
package com.lagradost.cloudstream3.ui.library
import android.os.Build
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.doOnAttach
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.OnFlingListener
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
import kotlinx.android.synthetic.main.library_viewpager_page.view.*
class ViewpagerAdapter(
var pages: List<SyncAPI.Page>,
val scrollCallback: (isScrollingDown: Boolean) -> Unit,
val clickCallback: (SearchClickCallback) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return PageViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.library_viewpager_page, parent, false)
)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is PageViewHolder -> {
holder.bind(pages[position], unbound.remove(position))
}
}
}
private val unbound = mutableSetOf<Int>()
/**
* Used to mark all pages for re-binding and forces all items to be refreshed
* Without this the pages will still use the same adapters
**/
fun rebind() {
unbound.addAll(0..pages.size)
this.notifyItemRangeChanged(0, pages.size)
}
inner class PageViewHolder(private val itemViewTest: View) :
RecyclerView.ViewHolder(itemViewTest) {
fun bind(page: SyncAPI.Page, rebind: Boolean) {
itemView.page_recyclerview?.spanCount =
this@PageViewHolder.itemView.context.getSpanCount() ?: 3
if (itemViewTest.page_recyclerview?.adapter == null || rebind) {
// Only add the items after it has been attached since the items rely on ItemWidth
// Which is only determined after the recyclerview is attached.
// If this fails then item height becomes 0 when there is only one item
itemViewTest.page_recyclerview?.doOnAttach {
itemViewTest.page_recyclerview?.adapter = PageAdapter(
page.items.toMutableList(),
itemViewTest.page_recyclerview,
clickCallback
)
}
} else {
(itemViewTest.page_recyclerview?.adapter as? PageAdapter)?.updateList(page.items)
itemViewTest.page_recyclerview?.scrollToPosition(0)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
itemViewTest.page_recyclerview.setOnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY ->
val diff = scrollY - oldScrollY
if (diff == 0) return@setOnScrollChangeListener
scrollCallback.invoke(diff > 0)
}
} else {
itemViewTest.page_recyclerview.onFlingListener = object : OnFlingListener() {
override fun onFling(velocityX: Int, velocityY: Int): Boolean {
scrollCallback.invoke(velocityY > 0)
return false
}
}
}
}
}
override fun getItemCount(): Int {
return pages.size
}
}

View file

@ -8,12 +8,10 @@ import android.util.Log
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.exoplayer2.* import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.C.* import com.google.android.exoplayer2.C.TRACK_TYPE_AUDIO
import com.google.android.exoplayer2.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON import com.google.android.exoplayer2.C.TRACK_TYPE_VIDEO
import com.google.android.exoplayer2.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider import com.google.android.exoplayer2.database.StandaloneDatabaseProvider
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector
import com.google.android.exoplayer2.source.* import com.google.android.exoplayer2.source.*
import com.google.android.exoplayer2.text.TextRenderer import com.google.android.exoplayer2.text.TextRenderer
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
@ -38,13 +36,11 @@ import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.EpisodeSkip
import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList
import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.ExtractorUri
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
import java.io.File import java.io.File
import java.time.Duration
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSession import javax.net.ssl.SSLSession
@ -93,10 +89,10 @@ class CS3IPlayer : IPlayer {
/** /**
* Tracks reported to be used by exoplayer, since sometimes it has a mind of it's own when selecting subs. * Tracks reported to be used by exoplayer, since sometimes it has a mind of it's own when selecting subs.
* String = id * String = lowercase language as set by .setLanguage("_$langId")
* Boolean = if it's active * Boolean = if it's active
* */ * */
private var playerSelectedSubtitleTracks = listOf<Pair<String, Boolean>>() private var exoPlayerSelectedTracks = listOf<Pair<String, Boolean>>()
/** isPlaying */ /** isPlaying */
private var updateIsPlaying: ((Pair<CSPlayerLoading, CSPlayerLoading>) -> Unit)? = null private var updateIsPlaying: ((Pair<CSPlayerLoading, CSPlayerLoading>) -> Unit)? = null
@ -315,16 +311,12 @@ class CS3IPlayer : IPlayer {
* */ * */
private fun List<Tracks.Group>.getFormats(): List<Pair<Format, Int>> { private fun List<Tracks.Group>.getFormats(): List<Pair<Format, Int>> {
return this.map { return this.map {
it.getFormats() (0 until it.mediaTrackGroup.length).mapNotNull { i ->
}.flatten() if (it.isSupported)
} it.mediaTrackGroup.getFormat(i) to i
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 else null
} }
}.flatten()
} }
private fun Format.toAudioTrack(): AudioTrack { private fun Format.toAudioTrack(): AudioTrack {
@ -369,17 +361,12 @@ class CS3IPlayer : IPlayer {
override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean { override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean {
Log.i(TAG, "setPreferredSubtitles init $subtitle") Log.i(TAG, "setPreferredSubtitles init $subtitle")
currentSubtitles = subtitle currentSubtitles = subtitle
fun getTextTrack(id: String) =
exoPlayer?.currentTracks?.groups?.filter { it.type == TRACK_TYPE_TEXT }
?.getTrack(id)
return (exoPlayer?.trackSelector as? DefaultTrackSelector?)?.let { trackSelector -> return (exoPlayer?.trackSelector as? DefaultTrackSelector?)?.let { trackSelector ->
if (subtitle == null) { val name = subtitle?.name
if (name.isNullOrBlank()) {
trackSelector.setParameters( trackSelector.setParameters(
trackSelector.buildUponParameters() trackSelector.buildUponParameters()
.setPreferredTextLanguage(null) .setPreferredTextLanguage(null)
.clearOverridesOfType(TRACK_TYPE_TEXT)
) )
} else { } else {
when (subtitleHelper.subtitleStatus(subtitle)) { when (subtitleHelper.subtitleStatus(subtitle)) {
@ -393,15 +380,12 @@ class CS3IPlayer : IPlayer {
trackSelector.setParameters( trackSelector.setParameters(
trackSelector.buildUponParameters() trackSelector.buildUponParameters()
.apply { .apply {
val track = getTextTrack(subtitle.getId()) if (subtitle.origin == SubtitleOrigin.EMBEDDED_IN_VIDEO)
if (track != null) { // The real Language (two letter) is in the url
setOverrideForType( // No underscore as the .url is the actual exoplayer designated language
TrackSelectionOverride( setPreferredTextLanguage(subtitle.url)
track.first, else
track.second setPreferredTextLanguage("_$name")
)
)
}
} }
) )
@ -435,8 +419,17 @@ class CS3IPlayer : IPlayer {
override fun getCurrentPreferredSubtitle(): SubtitleData? { override fun getCurrentPreferredSubtitle(): SubtitleData? {
return subtitleHelper.getAllSubtitles().firstOrNull { sub -> return subtitleHelper.getAllSubtitles().firstOrNull { sub ->
playerSelectedSubtitleTracks.any { (id, isSelected) -> exoPlayerSelectedTracks.any {
isSelected && sub.getId() == id // 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
)
} }
} }
} }
@ -540,17 +533,15 @@ class CS3IPlayer : IPlayer {
OkHttpDataSource.Factory(client).setUserAgent(USER_AGENT) OkHttpDataSource.Factory(client).setUserAgent(USER_AGENT)
} }
// Do no include empty referer, if the provider wants those they can use the header map.
val refererMap =
if (link.referer.isBlank()) emptyMap() else mapOf("referer" to link.referer)
val headers = mapOf( val headers = mapOf(
"referer" to link.referer,
"accept" to "*/*", "accept" to "*/*",
"sec-ch-ua" to "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"", "sec-ch-ua" to "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"",
"sec-ch-ua-mobile" to "?0", "sec-ch-ua-mobile" to "?0",
"sec-fetch-user" to "?1", "sec-fetch-user" to "?1",
"sec-fetch-mode" to "navigate", "sec-fetch-mode" to "navigate",
"sec-fetch-dest" to "video" "sec-fetch-dest" to "video"
) + refererMap + link.headers // Adds the headers from the provider, e.g Authorization ) + link.headers // Adds the headers from the provider, e.g Authorization
return source.apply { return source.apply {
setDefaultRequestProperties(headers) setDefaultRequestProperties(headers)
@ -673,11 +664,7 @@ class CS3IPlayer : IPlayer {
val exoPlayerBuilder = val exoPlayerBuilder =
ExoPlayer.Builder(context) ExoPlayer.Builder(context)
.setRenderersFactory { eventHandler, videoRendererEventListener, audioRendererEventListener, textRendererOutput, metadataRendererOutput -> .setRenderersFactory { eventHandler, videoRendererEventListener, audioRendererEventListener, textRendererOutput, metadataRendererOutput ->
DefaultRenderersFactory(context).apply { DefaultRenderersFactory(context).createRenderers(
// setEnableDecoderFallback(true)
// Enable Ffmpeg extension
// setExtensionRendererMode(EXTENSION_RENDERER_MODE_ON)
}.createRenderers(
eventHandler, eventHandler,
videoRendererEventListener, videoRendererEventListener,
audioRendererEventListener, audioRendererEventListener,
@ -701,8 +688,6 @@ class CS3IPlayer : IPlayer {
maxVideoHeight maxVideoHeight
) )
) )
// Allows any seeking to be +- 0.3s to allow for faster seeking
.setSeekParameters(SeekParameters(300_000, 300_000))
.setLoadControl( .setLoadControl(
DefaultLoadControl.Builder() DefaultLoadControl.Builder()
.setTargetBufferBytes( .setTargetBufferBytes(
@ -762,7 +747,7 @@ class CS3IPlayer : IPlayer {
} }
} }
private fun getCurrentTimestamp(writePosition: Long? = null): EpisodeSkip.SkipStamp? { private fun getCurrentTimestamp(writePosition : Long? = null): EpisodeSkip.SkipStamp? {
val position = writePosition ?: this@CS3IPlayer.getPosition() ?: return null val position = writePosition ?: this@CS3IPlayer.getPosition() ?: return null
for (lastTimeStamp in lastTimeStamps) { for (lastTimeStamp in lastTimeStamps) {
if (lastTimeStamp.startMs <= position && position < lastTimeStamp.endMs) { if (lastTimeStamp.startMs <= position && position < lastTimeStamp.endMs) {
@ -772,7 +757,7 @@ class CS3IPlayer : IPlayer {
return null return null
} }
fun updatedTime(writePosition: Long? = null) { fun updatedTime(writePosition : Long? = null) {
getCurrentTimestamp(writePosition)?.let { timestamp -> getCurrentTimestamp(writePosition)?.let { timestamp ->
onTimestampInvoked?.invoke(timestamp) onTimestampInvoked?.invoke(timestamp)
} }
@ -858,7 +843,7 @@ class CS3IPlayer : IPlayer {
Log.i(TAG, "loadExo") Log.i(TAG, "loadExo")
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
val maxVideoHeight = settingsManager.getInt( val maxVideoHeight = settingsManager.getInt(
context.getString(if (context.isUsingMobileData()) com.lagradost.cloudstream3.R.string.quality_pref_mobile_data_key else com.lagradost.cloudstream3.R.string.quality_pref_key), context.getString(com.lagradost.cloudstream3.R.string.quality_pref_key),
Int.MAX_VALUE Int.MAX_VALUE
) )
@ -898,33 +883,40 @@ class CS3IPlayer : IPlayer {
} }
exoPlayer?.addListener(object : Player.Listener { exoPlayer?.addListener(object : Player.Listener {
override fun onTracksChanged(tracks: Tracks) { override fun onTracksChanged(tracks: Tracks) {
normalSafeApiCall { fun Format.isSubtitle(): Boolean {
val textTracks = tracks.groups.filter { it.type == TRACK_TYPE_TEXT } return this.sampleMimeType?.contains("video/") == false &&
this.sampleMimeType?.contains("audio/") == false
playerSelectedSubtitleTracks =
textTracks.map { group ->
group.getFormats().mapNotNull { (format, _) ->
(format.id ?: return@mapNotNull null) to group.isSelected
} }
}.flatten()
val exoPlayerReportedTracks = normalSafeApiCall {
tracks.groups.filter { it.type == TRACK_TYPE_TEXT }.getFormats() exoPlayerSelectedTracks =
.mapNotNull { (format, _) -> tracks.groups.mapNotNull {
val format = it.mediaTrackGroup.getFormat(0)
if (format.isSubtitle())
format.language?.let { lang -> lang to it.isSelected }
else null
}
val exoPlayerReportedTracks = tracks.groups.mapNotNull {
// Filter out unsupported tracks
if (it.isSupported)
it.mediaTrackGroup.getFormat(0)
else
null
}.mapNotNull {
// Filter out non subs, already used subs and subs without languages // Filter out non subs, already used subs and subs without languages
if (format.id == null || if (!it.isSubtitle() ||
format.language == null || // Anything starting with - is not embedded
format.language?.startsWith("-") == true it.language?.startsWith("-") == true ||
it.language == null
) return@mapNotNull null ) return@mapNotNull null
return@mapNotNull SubtitleData( return@mapNotNull SubtitleData(
// Nicer looking displayed names // Nicer looking displayed names
fromTwoLettersToLanguage(format.language!!) fromTwoLettersToLanguage(it.language!!) ?: it.language!!,
?: format.language!!,
// See setPreferredTextLanguage // See setPreferredTextLanguage
format.id!!, it.language!!,
SubtitleOrigin.EMBEDDED_IN_VIDEO, SubtitleOrigin.EMBEDDED_IN_VIDEO,
format.sampleMimeType ?: MimeTypes.APPLICATION_SUBRIP, it.sampleMimeType ?: MimeTypes.APPLICATION_SUBRIP,
emptyMap() emptyMap()
) )
} }
@ -986,7 +978,7 @@ class CS3IPlayer : IPlayer {
// This is to switch mirrors automatically if the stream has not been fetched, but // 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. // allow playing the buffer without internet as then the duration is fetched.
if (error.errorCode == PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED if (error.errorCode == PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED
&& exoPlayer?.duration != TIME_UNSET && exoPlayer?.duration != C.TIME_UNSET
) { ) {
exoPlayer?.prepare() exoPlayer?.prepare()
} else { } else {
@ -1145,15 +1137,14 @@ class CS3IPlayer : IPlayer {
val subConfig = MediaItem.SubtitleConfiguration.Builder(Uri.parse(sub.url)) val subConfig = MediaItem.SubtitleConfiguration.Builder(Uri.parse(sub.url))
.setMimeType(sub.mimeType) .setMimeType(sub.mimeType)
.setLanguage("_${sub.name}") .setLanguage("_${sub.name}")
.setId(sub.getId()) .setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
.setSelectionFlags(SELECTION_FLAG_DEFAULT)
.build() .build()
when (sub.origin) { when (sub.origin) {
SubtitleOrigin.DOWNLOADED_FILE -> { SubtitleOrigin.DOWNLOADED_FILE -> {
if (offlineSourceFactory != null) { if (offlineSourceFactory != null) {
activeSubtitles.add(sub) activeSubtitles.add(sub)
SingleSampleMediaSource.Factory(offlineSourceFactory) SingleSampleMediaSource.Factory(offlineSourceFactory)
.createMediaSource(subConfig, TIME_UNSET) .createMediaSource(subConfig, C.TIME_UNSET)
} else { } else {
null null
} }
@ -1165,7 +1156,7 @@ class CS3IPlayer : IPlayer {
if (sub.headers.isNotEmpty()) if (sub.headers.isNotEmpty())
this.setDefaultRequestProperties(sub.headers) this.setDefaultRequestProperties(sub.headers)
}) })
.createMediaSource(subConfig, TIME_UNSET) .createMediaSource(subConfig, C.TIME_UNSET)
} else { } else {
null null
} }
@ -1174,7 +1165,7 @@ class CS3IPlayer : IPlayer {
if (offlineSourceFactory != null) { if (offlineSourceFactory != null) {
activeSubtitles.add(sub) activeSubtitles.add(sub)
SingleSampleMediaSource.Factory(offlineSourceFactory) SingleSampleMediaSource.Factory(offlineSourceFactory)
.createMediaSource(subConfig, TIME_UNSET) .createMediaSource(subConfig, C.TIME_UNSET)
} else { } else {
null null
} }
@ -1204,10 +1195,10 @@ class CS3IPlayer : IPlayer {
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory) HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
} }
val mime = when { val mime = if (link.isM3u8) {
link.isM3u8 -> MimeTypes.APPLICATION_M3U8 MimeTypes.APPLICATION_M3U8
link.isDash -> MimeTypes.APPLICATION_MPD } else {
else -> MimeTypes.VIDEO_MP4 MimeTypes.VIDEO_MP4
} }
val mediaItems = if (link is ExtractorLinkPlayList) { val mediaItems = if (link is ExtractorLinkPlayList) {

View file

@ -4,16 +4,13 @@ import android.content.Context
import android.util.Log import android.util.Log
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.exoplayer2.Format import com.google.android.exoplayer2.Format
import com.google.android.exoplayer2.text.* import com.google.android.exoplayer2.text.SubtitleDecoder
import com.google.android.exoplayer2.text.cea.Cea608Decoder import com.google.android.exoplayer2.text.SubtitleDecoderFactory
import com.google.android.exoplayer2.text.cea.Cea708Decoder import com.google.android.exoplayer2.text.SubtitleInputBuffer
import com.google.android.exoplayer2.text.dvb.DvbDecoder import com.google.android.exoplayer2.text.SubtitleOutputBuffer
import com.google.android.exoplayer2.text.pgs.PgsDecoder
import com.google.android.exoplayer2.text.ssa.SsaDecoder import com.google.android.exoplayer2.text.ssa.SsaDecoder
import com.google.android.exoplayer2.text.subrip.SubripDecoder import com.google.android.exoplayer2.text.subrip.SubripDecoder
import com.google.android.exoplayer2.text.ttml.TtmlDecoder import com.google.android.exoplayer2.text.ttml.TtmlDecoder
import com.google.android.exoplayer2.text.tx3g.Tx3gDecoder
import com.google.android.exoplayer2.text.webvtt.Mp4WebvttDecoder
import com.google.android.exoplayer2.text.webvtt.WebvttDecoder import com.google.android.exoplayer2.text.webvtt.WebvttDecoder
import com.google.android.exoplayer2.util.MimeTypes import com.google.android.exoplayer2.util.MimeTypes
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
@ -22,11 +19,7 @@ import org.mozilla.universalchardet.UniversalDetector
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.charset.Charset import java.nio.charset.Charset
/** class CustomDecoder : SubtitleDecoder {
* @param fallbackFormat used to create a decoder based on mimetype if the subtitle string is not
* enough to identify the subtitle format.
**/
class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder {
companion object { companion object {
fun updateForcedEncoding(context: Context) { fun updateForcedEncoding(context: Context) {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
@ -146,7 +139,7 @@ class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder {
val inputString = getStr(inputBuffer) val inputString = getStr(inputBuffer)
if (realDecoder == null && !inputString.isNullOrBlank()) { if (realDecoder == null && !inputString.isNullOrBlank()) {
var str: String = inputString var str: String = inputString
// this way we read the subtitle file and decide what decoder to use instead of relying fully on mimetype // this way we read the subtitle file and decide what decoder to use instead of relying on mimetype
Log.i(TAG, "Got data from queueInputBuffer") Log.i(TAG, "Got data from queueInputBuffer")
//https://github.com/LagradOst/CloudStream-2/blob/ddd774ee66810137ff7bd65dae70bcf3ba2d2489/CloudStreamForms/CloudStreamForms/Script/MainChrome.cs#L388 //https://github.com/LagradOst/CloudStream-2/blob/ddd774ee66810137ff7bd65dae70bcf3ba2d2489/CloudStreamForms/CloudStreamForms/Script/MainChrome.cs#L388
realDecoder = when { realDecoder = when {
@ -155,31 +148,8 @@ class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder {
(str.startsWith( (str.startsWith(
"[Script Info]", "[Script Info]",
ignoreCase = true ignoreCase = true
) || str.startsWith("Title:", ignoreCase = true)) -> SsaDecoder(fallbackFormat?.initializationData) ) || str.startsWith("Title:", ignoreCase = true)) -> SsaDecoder()
str.startsWith("1", ignoreCase = true) -> SubripDecoder() str.startsWith("1", ignoreCase = true) -> SubripDecoder()
fallbackFormat != null -> {
when (val mimeType = fallbackFormat.sampleMimeType) {
MimeTypes.TEXT_VTT -> WebvttDecoder()
MimeTypes.TEXT_SSA -> SsaDecoder(fallbackFormat.initializationData)
MimeTypes.APPLICATION_MP4VTT -> Mp4WebvttDecoder()
MimeTypes.APPLICATION_TTML -> TtmlDecoder()
MimeTypes.APPLICATION_SUBRIP -> SubripDecoder()
MimeTypes.APPLICATION_TX3G -> Tx3gDecoder(fallbackFormat.initializationData)
MimeTypes.APPLICATION_CEA608, MimeTypes.APPLICATION_MP4CEA608 -> Cea608Decoder(
mimeType,
fallbackFormat.accessibilityChannel,
Cea608Decoder.MIN_DATA_CHANNEL_TIMEOUT_MS
)
MimeTypes.APPLICATION_CEA708 -> Cea708Decoder(
fallbackFormat.accessibilityChannel,
fallbackFormat.initializationData
)
MimeTypes.APPLICATION_DVBSUBS -> DvbDecoder(fallbackFormat.initializationData)
MimeTypes.APPLICATION_PGS -> PgsDecoder()
MimeTypes.TEXT_EXOPLAYER_CUES -> ExoplayerCuesDecoder()
else -> null
}
}
else -> null else -> null
} }
Log.i( Log.i(
@ -276,6 +246,28 @@ class CustomSubtitleDecoderFactory : SubtitleDecoderFactory {
} }
override fun createDecoder(format: Format): SubtitleDecoder { override fun createDecoder(format: Format): SubtitleDecoder {
return CustomDecoder(format) return CustomDecoder()
//return when (val mimeType = format.sampleMimeType) {
// MimeTypes.TEXT_VTT -> WebvttDecoder()
// MimeTypes.TEXT_SSA -> SsaDecoder(format.initializationData)
// MimeTypes.APPLICATION_MP4VTT -> Mp4WebvttDecoder()
// MimeTypes.APPLICATION_TTML -> TtmlDecoder()
// MimeTypes.APPLICATION_SUBRIP -> SubripDecoder()
// MimeTypes.APPLICATION_TX3G -> Tx3gDecoder(format.initializationData)
// MimeTypes.APPLICATION_CEA608, MimeTypes.APPLICATION_MP4CEA608 -> return Cea608Decoder(
// mimeType,
// format.accessibilityChannel,
// Cea608Decoder.MIN_DATA_CHANNEL_TIMEOUT_MS
// )
// MimeTypes.APPLICATION_CEA708 -> Cea708Decoder(
// format.accessibilityChannel,
// format.initializationData
// )
// MimeTypes.APPLICATION_DVBSUBS -> DvbDecoder(format.initializationData)
// MimeTypes.APPLICATION_PGS -> PgsDecoder()
// MimeTypes.TEXT_EXOPLAYER_CUES -> ExoplayerCuesDecoder()
// // Default WebVttDecoder
// else -> WebvttDecoder()
//}
} }
} }

View file

@ -42,7 +42,7 @@ class DownloadedPlayerActivity : AppCompatActivity() {
R.id.global_to_navigation_player, GeneratorPlayer.newInstance( R.id.global_to_navigation_player, GeneratorPlayer.newInstance(
LinkGenerator( LinkGenerator(
listOf( listOf(
BasicLink(url) url
) )
) )
) )

View file

@ -1,52 +0,0 @@
package com.lagradost.cloudstream3.ui.player
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorUri
class ExtractorLinkGenerator(
private val links: List<ExtractorLink>,
private val subtitles: List<SubtitleData>,
) : IGenerator {
override val hasCache = false
override fun getCurrentId(): Int? {
return null
}
override fun hasNext(): Boolean {
return false
}
override fun getAll(): List<Any>? {
return null
}
override fun hasPrev(): Boolean {
return false
}
override fun getCurrent(offset: Int): Any? {
return null
}
override fun goto(index: Int) {}
override fun next() {}
override fun prev() {}
override suspend fun generateLinks(
clearCache: Boolean,
isCasting: Boolean,
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
subtitleCallback: (SubtitleData) -> Unit,
offset: Int
): Boolean {
subtitles.forEach(subtitleCallback)
links.forEach {
callback.invoke(it to null)
}
return true
}
}

View file

@ -40,7 +40,6 @@ import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer.Companion.subsProvidersIsActive import com.lagradost.cloudstream3.ui.player.GeneratorPlayer.Companion.subsProvidersIsActive
import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
@ -84,7 +83,6 @@ const val HORIZONTAL_MULTIPLIER = 2.0f
const val DOUBLE_TAB_MAXIMUM_HOLD_TIME = 200L const val DOUBLE_TAB_MAXIMUM_HOLD_TIME = 200L
const val DOUBLE_TAB_MINIMUM_TIME_BETWEEN = 200L // this also affects the UI show response time const val DOUBLE_TAB_MINIMUM_TIME_BETWEEN = 200L // this also affects the UI show response time
const val DOUBLE_TAB_PAUSE_PERCENTAGE = 0.15 // in both directions const val DOUBLE_TAB_PAUSE_PERCENTAGE = 0.15 // in both directions
private const val SUBTITLE_DELAY_BUNDLE_KEY = "subtitle_delay"
// All the UI Logic for the player // All the UI Logic for the player
open class FullScreenPlayer : AbstractPlayerFragment() { open class FullScreenPlayer : AbstractPlayerFragment() {
@ -111,8 +109,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
protected var currentPrefQuality = protected var currentPrefQuality =
Qualities.P2160.value // preferred maximum quality, used for ppl w bad internet or on cell Qualities.P2160.value // preferred maximum quality, used for ppl w bad internet or on cell
protected var fastForwardTime = 10000L protected var fastForwardTime = 10000L
protected var androidTVInterfaceOffSeekTime = 10000L;
protected var androidTVInterfaceOnSeekTime = 30000L;
protected var swipeHorizontalEnabled = false protected var swipeHorizontalEnabled = false
protected var swipeVerticalEnabled = false protected var swipeVerticalEnabled = false
protected var playBackSpeedEnabled = false protected var playBackSpeedEnabled = false
@ -609,14 +605,13 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
player_top_holder?.isGone = isGone player_top_holder?.isGone = isGone
//player_episodes_button?.isVisible = !isGone && hasEpisodes //player_episodes_button?.isVisible = !isGone && hasEpisodes
player_video_title?.isGone = togglePlayerTitleGone player_video_title?.isGone = togglePlayerTitleGone
// player_video_title_rez?.isGone = isGone player_video_title_rez?.isGone = isGone
player_episode_filler?.isGone = isGone player_episode_filler?.isGone = isGone
player_center_menu?.isGone = isGone player_center_menu?.isGone = isGone
player_lock?.isGone = !isShowing player_lock?.isGone = !isShowing
//player_media_route_button?.isClickable = !isGone //player_media_route_button?.isClickable = !isGone
player_go_back_holder?.isGone = isGone player_go_back_holder?.isGone = isGone
player_sources_btt?.isGone = isGone player_sources_btt?.isGone = isGone
player_skip_episode?.isClickable = !isGone
} }
private fun updateLockUI() { private fun updateLockUI() {
@ -1055,19 +1050,19 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
} }
KeyEvent.KEYCODE_DPAD_LEFT -> { KeyEvent.KEYCODE_DPAD_LEFT -> {
if (!isShowing && !isLocked) { if (!isShowing && !isLocked) {
player.seekTime(-androidTVInterfaceOffSeekTime) player.seekTime(-10000L)
return true return true
} else if (player_pause_play?.isFocused == true) { } else if (player_pause_play?.isFocused == true) {
player.seekTime(-androidTVInterfaceOnSeekTime) player.seekTime(-30000L)
return true return true
} }
} }
KeyEvent.KEYCODE_DPAD_RIGHT -> { KeyEvent.KEYCODE_DPAD_RIGHT -> {
if (!isShowing && !isLocked) { if (!isShowing && !isLocked) {
player.seekTime(androidTVInterfaceOffSeekTime) player.seekTime(10000L)
return true return true
} else if (player_pause_play?.isFocused == true) { } else if (player_pause_play?.isFocused == true) {
player.seekTime(androidTVInterfaceOnSeekTime) player.seekTime(30000L)
return true return true
} }
} }
@ -1106,6 +1101,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
} }
protected fun uiReset() { protected fun uiReset() {
isLocked = false
isShowing = false isShowing = false
// if nothing has loaded these buttons should not be visible // if nothing has loaded these buttons should not be visible
@ -1121,20 +1117,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
resetRewindText() resetRewindText()
} }
override fun onSaveInstanceState(outState: Bundle) {
// As this is video specific it is better to not do any setKey/getKey
outState.putLong(SUBTITLE_DELAY_BUNDLE_KEY, subtitleDelay)
super.onSaveInstanceState(outState)
}
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
// init variables // init variables
setPlayBackSpeed(getKey(PLAYBACK_SPEED_KEY) ?: 1.0f) setPlayBackSpeed(getKey(PLAYBACK_SPEED_KEY) ?: 1.0f)
savedInstanceState?.getLong(SUBTITLE_DELAY_BUNDLE_KEY)?.let {
subtitleDelay = it
}
// handle tv controls // handle tv controls
playerEventListener = { eventType -> playerEventListener = { eventType ->
@ -1220,13 +1207,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
settingsManager.getInt(ctx.getString(R.string.double_tap_seek_time_key), 10) settingsManager.getInt(ctx.getString(R.string.double_tap_seek_time_key), 10)
.toLong() * 1000L .toLong() * 1000L
androidTVInterfaceOffSeekTime =
settingsManager.getInt(ctx.getString(R.string.android_tv_interface_off_seek_key), 10)
.toLong() * 1000L
androidTVInterfaceOnSeekTime =
settingsManager.getInt(ctx.getString(R.string.android_tv_interface_on_seek_key), 10)
.toLong() * 1000L
navigationBarHeight = ctx.getNavigationBarHeight() navigationBarHeight = ctx.getNavigationBarHeight()
statusBarHeight = ctx.getStatusBarHeight() statusBarHeight = ctx.getStatusBarHeight()
@ -1257,8 +1237,9 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
ctx.getString(R.string.double_tap_pause_enabled_key), ctx.getString(R.string.double_tap_pause_enabled_key),
false false
) )
currentPrefQuality = settingsManager.getInt( currentPrefQuality = settingsManager.getInt(
ctx.getString(if (ctx.isUsingMobileData()) R.string.quality_pref_mobile_data_key else R.string.quality_pref_key), ctx.getString(R.string.quality_pref_key),
currentPrefQuality currentPrefQuality
) )
// useSystemBrightness = // useSystemBrightness =

View file

@ -13,6 +13,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.* import android.widget.*
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.animation.addListener import androidx.core.animation.addListener
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isGone import androidx.core.view.isGone
@ -25,13 +26,19 @@ import com.hippo.unifile.UniFile
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.mvvm.* import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subtitleProviders import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subtitleProviders
import com.lagradost.cloudstream3.ui.player.CS3IPlayer.Companion.preferredAudioTrackLanguage import com.lagradost.cloudstream3.ui.player.CS3IPlayer.Companion.preferredAudioTrackLanguage
import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.updateForcedEncoding import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.updateForcedEncoding
import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType
import com.lagradost.cloudstream3.ui.result.* 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.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1 import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
@ -54,9 +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_source_and_subs.subtitles_click_settings
import kotlinx.android.synthetic.main.player_select_tracks.* import kotlinx.android.synthetic.main.player_select_tracks.*
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
class GeneratorPlayer : FullScreenPlayer() { class GeneratorPlayer : FullScreenPlayer() {
companion object { companion object {
@ -162,7 +166,6 @@ class GeneratorPlayer : FullScreenPlayer() {
isActive = true isActive = true
setPlayerDimen(null) setPlayerDimen(null)
setTitle() setTitle()
if (!sameEpisode)
hasRequestedStamps = false hasRequestedStamps = false
loadExtractorJob(link.first) loadExtractorJob(link.first)
@ -183,8 +186,6 @@ class GeneratorPlayer : FullScreenPlayer() {
), ),
) )
} }
if (!sameEpisode)
player.addTimeStamps(listOf()) // clear stamps player.addTimeStamps(listOf()) // clear stamps
} }
@ -326,54 +327,17 @@ class GeneratorPlayer : FullScreenPlayer() {
dialog.search_loading_bar.progressTintList = color dialog.search_loading_bar.progressTintList = color
dialog.search_loading_bar.indeterminateTintList = color dialog.search_loading_bar.indeterminateTintList = color
observeNullable(viewModel.currentSubtitleYear) {
// When year is changed search again
dialog.subtitles_search.setQuery(dialog.subtitles_search.query, true)
dialog.year_btt.text = it?.toString() ?: txt(R.string.none).asString(context)
}
dialog.year_btt?.setOnClickListener {
val none = txt(R.string.none).asString(context)
val currentYear = Calendar.getInstance().get(Calendar.YEAR)
val earliestYear = 1900
val years = (currentYear downTo earliestYear).toList()
val options = listOf(none) + years.map {
it.toString()
}
val selectedIndex = viewModel.currentSubtitleYear.value
?.let {
// + 1 since none also takes a space
years.indexOf(it) + 1
}
?.takeIf { it >= 0 } ?: 0
activity?.showDialog(
options,
selectedIndex,
txt(R.string.year).asString(context),
true, {
}, { index ->
viewModel.setSubtitleYear(years.getOrNull(index - 1))
}
)
}
dialog.subtitles_search.setOnQueryTextListener(object : dialog.subtitles_search.setOnQueryTextListener(object :
androidx.appcompat.widget.SearchView.OnQueryTextListener { androidx.appcompat.widget.SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean { override fun onQueryTextSubmit(query: String?): Boolean {
dialog.search_loading_bar?.show() dialog.search_loading_bar?.show()
ioSafe { ioSafe {
val search = val search =
AbstractSubtitleEntities.SubtitleSearch( AbstractSubtitleEntities.SubtitleSearch(query = query ?: return@ioSafe,
query = query ?: return@ioSafe,
imdb = imdbId, imdb = imdbId,
epNumber = currentTempMeta.episode, epNumber = currentTempMeta.episode,
seasonNumber = currentTempMeta.season, seasonNumber = currentTempMeta.season,
lang = currentLanguageTwoLetters.ifBlank { null }, lang = currentLanguageTwoLetters.ifBlank { null })
year = viewModel.currentSubtitleYear.value
)
val results = providers.amap { val results = providers.amap {
try { try {
it.search(search) it.search(search)
@ -381,7 +345,7 @@ class GeneratorPlayer : FullScreenPlayer() {
null null
} }
}.filterNotNull() }.filterNotNull()
val max = results.maxOfOrNull { it.size } ?: return@ioSafe val max = results.map { it.size }.maxOrNull() ?: return@ioSafe
// very ugly // very ugly
val items = ArrayList<AbstractSubtitleEntities.SubtitleEntity>() val items = ArrayList<AbstractSubtitleEntities.SubtitleEntity>()
@ -447,8 +411,6 @@ class GeneratorPlayer : FullScreenPlayer() {
dialog.show() dialog.show()
dialog.subtitles_search.setQuery(currentTempMeta.name, true) dialog.subtitles_search.setQuery(currentTempMeta.name, true)
//TODO: Set year text from currently loaded movie on Player
//dialog.subtitles_search_year?.setText(currentTempMeta.year)
} }
private fun openSubPicker() { private fun openSubPicker() {
@ -473,17 +435,16 @@ class GeneratorPlayer : FullScreenPlayer() {
private fun addAndSelectSubtitles(subtitleData: SubtitleData) { private fun addAndSelectSubtitles(subtitleData: SubtitleData) {
val ctx = context ?: return val ctx = context ?: return
setSubtitles(subtitleData)
// this is used instead of observe, because observe is too slow
val subs = currentSubs + subtitleData 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 // Save current time as to not reset player to 00:00
player.saveData() player.saveData()
player.setActiveSubtitles(subs)
player.reloadPlayer(ctx) player.reloadPlayer(ctx)
setSubtitles(subtitleData)
viewModel.addSubtitles(setOf(subtitleData)) viewModel.addSubtitles(setOf(subtitleData))
selectSourceDialog?.dismissSafe() selectSourceDialog?.dismissSafe()
@ -525,7 +486,7 @@ class GeneratorPlayer : FullScreenPlayer() {
} }
} }
var selectSourceDialog: Dialog? = null var selectSourceDialog: AlertDialog? = null
// var selectTracksDialog: AlertDialog? = null // var selectTracksDialog: AlertDialog? = null
override fun showMirrorsDialogue() { override fun showMirrorsDialogue() {
@ -537,8 +498,10 @@ class GeneratorPlayer : FullScreenPlayer() {
player.handleEvent(CSPlayerEvent.Pause) player.handleEvent(CSPlayerEvent.Pause)
val currentSubtitles = sortSubs(currentSubs) val currentSubtitles = sortSubs(currentSubs)
val sourceDialog = Dialog(ctx, R.style.AlertDialogCustomBlack) val sourceBuilder = AlertDialog.Builder(ctx, R.style.AlertDialogCustomBlack)
sourceDialog.setContentView(R.layout.player_select_source_and_subs) .setView(R.layout.player_select_source_and_subs)
val sourceDialog = sourceBuilder.create()
selectSourceDialog = sourceDialog selectSourceDialog = sourceDialog
@ -733,17 +696,19 @@ class GeneratorPlayer : FullScreenPlayer() {
} }
val currentAudioTracks = tracks.allAudioTracks val currentAudioTracks = tracks.allAudioTracks
val trackDialog = Dialog(ctx, R.style.AlertDialogCustomBlack) val trackBuilder = AlertDialog.Builder(ctx, R.style.AlertDialogCustomBlack)
trackDialog.setContentView(R.layout.player_select_tracks) .setView(R.layout.player_select_tracks)
trackDialog.show()
val tracksDialog = trackBuilder.create()
// selectTracksDialog = tracksDialog // selectTracksDialog = tracksDialog
val videosList = trackDialog.video_tracks_list tracksDialog.show()
val audioList = trackDialog.auto_tracks_list val videosList = tracksDialog.video_tracks_list
val audioList = tracksDialog.auto_tracks_list
trackDialog.video_tracks_holder.isVisible = currentVideoTracks.size > 1 tracksDialog.video_tracks_holder.isVisible = currentVideoTracks.size > 1
trackDialog.audio_tracks_holder.isVisible = currentAudioTracks.size > 1 tracksDialog.audio_tracks_holder.isVisible = currentAudioTracks.size > 1
fun dismiss() { fun dismiss() {
if (isPlaying) { if (isPlaying) {
@ -778,7 +743,7 @@ class GeneratorPlayer : FullScreenPlayer() {
videosList.setItemChecked(which, true) videosList.setItemChecked(which, true)
} }
trackDialog.setOnDismissListener { tracksDialog.setOnDismissListener {
dismiss() dismiss()
// selectTracksDialog = null // selectTracksDialog = null
} }
@ -808,11 +773,11 @@ class GeneratorPlayer : FullScreenPlayer() {
audioList.setItemChecked(which, true) audioList.setItemChecked(which, true)
} }
trackDialog.cancel_btt?.setOnClickListener { tracksDialog.cancel_btt?.setOnClickListener {
trackDialog.dismissSafe(activity) tracksDialog.dismissSafe(activity)
} }
trackDialog.apply_btt?.setOnClickListener { tracksDialog.apply_btt?.setOnClickListener {
val currentTrack = currentAudioTracks.getOrNull(audioIndexStart) val currentTrack = currentAudioTracks.getOrNull(audioIndexStart)
player.setPreferredAudioTrack( player.setPreferredAudioTrack(
currentTrack?.language, currentTrack?.id currentTrack?.language, currentTrack?.id
@ -825,7 +790,7 @@ class GeneratorPlayer : FullScreenPlayer() {
player.setMaxVideoSize(width, height, currentVideo?.id) player.setMaxVideoSize(width, height, currentVideo?.id)
} }
trackDialog.dismissSafe(activity) tracksDialog.dismissSafe(activity)
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -907,14 +872,6 @@ class GeneratorPlayer : FullScreenPlayer() {
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) { if (!hasRequestedStamps) {
hasRequestedStamps = true 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.loadStamps(duration)
} }
@ -991,7 +948,7 @@ class GeneratorPlayer : FullScreenPlayer() {
subtitles: Set<SubtitleData>, settings: Boolean, downloads: Boolean subtitles: Set<SubtitleData>, settings: Boolean, downloads: Boolean
): SubtitleData? { ): SubtitleData? {
val langCode = preferredAutoSelectSubtitles ?: return null val langCode = preferredAutoSelectSubtitles ?: return null
val lang = fromTwoLettersToLanguage(langCode) ?: return null val lang = SubtitleHelper.fromTwoLettersToLanguage(langCode) ?: return null
if (downloads) { if (downloads) {
return subtitles.firstOrNull { sub -> return subtitles.firstOrNull { sub ->
(sub.origin == SubtitleOrigin.DOWNLOADED_FILE && sub.name == context?.getString( (sub.origin == SubtitleOrigin.DOWNLOADED_FILE && sub.name == context?.getString(
@ -1002,11 +959,22 @@ class GeneratorPlayer : FullScreenPlayer() {
sortSubs(subtitles).firstOrNull { sub -> sortSubs(subtitles).firstOrNull { sub ->
val t = sub.name.replace(Regex("[^A-Za-z]"), " ").trim() val t = sub.name.replace(Regex("[^A-Za-z]"), " ").trim()
(settings) && t == lang || t.startsWith(lang) || t == langCode (settings || (downloads && sub.origin == SubtitleOrigin.DOWNLOADED_FILE)) && t == lang || t.startsWith(
"$lang "
) || t == langCode
}?.let { sub -> }?.let { sub ->
return 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 return null
} }
@ -1027,12 +995,14 @@ class GeneratorPlayer : FullScreenPlayer() {
getAutoSelectSubtitle( getAutoSelectSubtitle(
currentSubs, settings = true, downloads = false currentSubs, settings = true, downloads = false
)?.let { sub -> )?.let { sub ->
if (setSubtitles(sub)) { if (setSubtitles(sub)) {
player.saveData() player.saveData()
player.reloadPlayer(ctx) player.reloadPlayer(ctx)
player.handleEvent(CSPlayerEvent.Play) player.handleEvent(CSPlayerEvent.Play)
return true return true
} }
} }
} }
} }
@ -1142,15 +1112,13 @@ class GeneratorPlayer : FullScreenPlayer() {
val source = currentSelectedLink?.first?.name ?: currentSelectedLink?.second?.name ?: "NULL" val source = currentSelectedLink?.first?.name ?: currentSelectedLink?.second?.name ?: "NULL"
val title = when (titleRez) { player_video_title_rez?.text = when (titleRez) {
0 -> "" 0 -> ""
1 -> extra 1 -> extra
2 -> source 2 -> source
3 -> "$source - $extra" 3 -> "$source - $extra"
else -> "" else -> ""
} }
player_video_title_rez?.text = title
player_video_title_rez?.isVisible = title.isNotBlank()
} }
override fun playerDimensionsLoaded(widthHeight: Pair<Int, Int>) { override fun playerDimensionsLoaded(widthHeight: Pair<Int, Int>) {
@ -1325,10 +1293,8 @@ class GeneratorPlayer : FullScreenPlayer() {
Log.i("subfilter", "Filtering subtitle") Log.i("subfilter", "Filtering subtitle")
langFilterList.forEach { lang -> langFilterList.forEach { lang ->
Log.i("subfilter", "Lang: $lang") Log.i("subfilter", "Lang: $lang")
setOfSub += set.filter { setOfSub += set.filter { it.name.contains(lang, ignoreCase = true) }
it.name.contains(lang, ignoreCase = true) || .toMutableSet()
it.origin != SubtitleOrigin.URL
}
} }
currentSubs = setOfSub currentSubs = setOfSub
} else { } else {
@ -1336,13 +1302,7 @@ class GeneratorPlayer : FullScreenPlayer() {
} }
player.setActiveSubtitles(set) player.setActiveSubtitles(set)
// 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() autoSelectSubtitles()
} }
} }
}
} }

View file

@ -5,15 +5,8 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
import java.net.URI import java.net.URI
/**
* Used to open the player more easily with the LinkGenerator
**/
data class BasicLink(
val url: String,
val name: String? = null,
)
class LinkGenerator( class LinkGenerator(
private val links: List<BasicLink>, private val links: List<String>,
private val extract: Boolean = true, private val extract: Boolean = true,
private val referer: String? = null, private val referer: String? = null,
private val isM3u8: Boolean? = null private val isM3u8: Boolean? = null
@ -54,7 +47,7 @@ class LinkGenerator(
offset: Int offset: Int
): Boolean { ): Boolean {
links.amap { link -> links.amap { link ->
if (!extract || !loadExtractor(link.url, referer, { if (!extract || !loadExtractor(link, referer, {
subtitleCallback(PlayerSubtitleHelper.getSubtitleData(it)) subtitleCallback(PlayerSubtitleHelper.getSubtitleData(it))
}) { }) {
callback(it to null) callback(it to null)
@ -64,11 +57,11 @@ class LinkGenerator(
callback( callback(
ExtractorLink( ExtractorLink(
"", "",
link.name ?: link.url, link,
unshortenLinkSafe(link.url), // unshorten because it might be a raw link unshortenLinkSafe(link), // unshorten because it might be a raw link
referer ?: "", referer ?: "",
Qualities.Unknown.value, isM3u8 ?: normalSafeApiCall { Qualities.Unknown.value, isM3u8 ?: normalSafeApiCall {
URI(link.url).path?.substringAfterLast(".")?.contains("m3u") URI(link).path?.substringAfterLast(".")?.contains("m3u")
} ?: false } ?: false
) to null ) to null
) )

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