mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
pre 2.6.8, fixed many bugs and crashes
This commit is contained in:
parent
4bc86374d7
commit
dcb97a1f63
19 changed files with 464 additions and 134 deletions
|
@ -35,8 +35,8 @@ android {
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
|
|
||||||
versionCode 41
|
versionCode 42
|
||||||
versionName "2.5.8"
|
versionName "2.6.9"
|
||||||
|
|
||||||
resValue "string", "app_version",
|
resValue "string", "app_version",
|
||||||
"${defaultConfig.versionName}${versionNameSuffix ?: ""}"
|
"${defaultConfig.versionName}${versionNameSuffix ?: ""}"
|
||||||
|
@ -88,9 +88,9 @@ dependencies {
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
implementation 'androidx.core:core-ktx:1.7.0'
|
implementation 'androidx.core:core-ktx:1.7.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.4.0'
|
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||||
implementation 'com.google.android.material:material:1.4.0'
|
implementation 'com.google.android.material:material:1.5.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.4.0-rc01'
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.4.0-rc01'
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.4.0-rc01'
|
implementation 'androidx.navigation:navigation-ui-ktx:2.4.0-rc01'
|
||||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0'
|
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0'
|
||||||
|
@ -103,7 +103,7 @@ dependencies {
|
||||||
implementation 'org.jsoup:jsoup:1.13.1'
|
implementation 'org.jsoup:jsoup:1.13.1'
|
||||||
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.12.3"
|
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.12.3"
|
||||||
|
|
||||||
implementation("com.google.android.material:material:1.4.0")
|
implementation "com.google.android.material:material:1.5.0"
|
||||||
|
|
||||||
implementation "androidx.preference:preference-ktx:1.1.1"
|
implementation "androidx.preference:preference-ktx:1.1.1"
|
||||||
|
|
||||||
|
@ -147,7 +147,7 @@ dependencies {
|
||||||
implementation "androidx.work:work-runtime-ktx:2.7.1"
|
implementation "androidx.work:work-runtime-ktx:2.7.1"
|
||||||
|
|
||||||
// Networking
|
// Networking
|
||||||
implementation "com.squareup.okhttp3:okhttp:4.9.1"
|
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.google.android.exoplayer:extension-okhttp:2.16.1'
|
implementation 'com.google.android.exoplayer:extension-okhttp:2.16.1'
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
package com.lagradost.cloudstream3
|
package com.lagradost.cloudstream3
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
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 kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Assert
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
*
|
*
|
||||||
|
@ -15,10 +16,232 @@ import org.junit.Assert.*
|
||||||
*/
|
*/
|
||||||
@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> {
|
||||||
|
val allApis = APIHolder.apis
|
||||||
|
allApis.addAll(APIHolder.restrictedApis)
|
||||||
|
return allApis //.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()?.url
|
||||||
|
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 useAppContext() {
|
fun providersExist() {
|
||||||
// Context of the app under test.
|
Assert.assertTrue(getAllProviders().isNotEmpty())
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
println("Done providersExist")
|
||||||
assertEquals("com.lagradost.cloudstream3", appContext.packageName)
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun providerCorrectData() {
|
||||||
|
val isoNames = SubtitleHelper.languages.map { it.ISO_639_1 }
|
||||||
|
Assert.assertFalse("ISO does not contain any languages", isoNames.isNullOrEmpty())
|
||||||
|
for (api in getAllProviders()) {
|
||||||
|
Assert.assertTrue("Api does not contain a mainUrl", api.mainUrl != "NONE")
|
||||||
|
Assert.assertTrue("Api does not contain a name", api.name != "NONE")
|
||||||
|
Assert.assertTrue(
|
||||||
|
"Api ${api.name} does not contain a valid language code",
|
||||||
|
isoNames.contains(api.lang)
|
||||||
|
)
|
||||||
|
Assert.assertTrue(
|
||||||
|
"Api ${api.name} does not contain any supported types",
|
||||||
|
api.supportedTypes.isNotEmpty()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
println("Done providerCorrectData")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun providerCorrectHomepage() {
|
||||||
|
runBlocking {
|
||||||
|
getAllProviders().apmap { api ->
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Test
|
||||||
|
// fun testSingleProvider() {
|
||||||
|
// testSingleProviderApi(ThenosProvider())
|
||||||
|
// }
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun providerCorrect() {
|
||||||
|
runBlocking {
|
||||||
|
val invalidProvider = ArrayList<Pair<MainAPI, Exception?>>()
|
||||||
|
val providers = getAllProviders()
|
||||||
|
providers.apmap { api ->
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -184,12 +184,12 @@ object APIHolder {
|
||||||
return realSet
|
return realSet
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.filterProviderByPreferredMedia(): List<MainAPI> {
|
fun Context.filterProviderByPreferredMedia(hasHomePageIsRequired : Boolean = true): List<MainAPI> {
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
val currentPrefMedia =
|
val currentPrefMedia =
|
||||||
settingsManager.getInt(this.getString(R.string.prefer_media_type_key), 0)
|
settingsManager.getInt(this.getString(R.string.prefer_media_type_key), 0)
|
||||||
val langs = this.getApiProviderLangSettings()
|
val langs = this.getApiProviderLangSettings()
|
||||||
val allApis = apis.filter { langs.contains(it.lang) }.filter { api -> api.hasMainPage }
|
val allApis = apis.filter { langs.contains(it.lang) }.filter { api -> api.hasMainPage || !hasHomePageIsRequired}
|
||||||
return if (currentPrefMedia < 1) {
|
return if (currentPrefMedia < 1) {
|
||||||
allApis
|
allApis
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -45,7 +45,7 @@ class VfFilmProvider : MainAPI() {
|
||||||
subtitleCallback: (SubtitleFile) -> Unit,
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
callback: (ExtractorLink) -> Unit
|
callback: (ExtractorLink) -> Unit
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (data == "") return false
|
if (data.length <= 4) return false
|
||||||
callback.invoke(
|
callback.invoke(
|
||||||
ExtractorLink(
|
ExtractorLink(
|
||||||
this.name,
|
this.name,
|
||||||
|
@ -100,7 +100,7 @@ class VfFilmProvider : MainAPI() {
|
||||||
number_player += 1
|
number_player += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (found == false) {
|
if (!found) {
|
||||||
number_player = 0
|
number_player = 0
|
||||||
}
|
}
|
||||||
val i = number_player.toString()
|
val i = number_player.toString()
|
||||||
|
@ -108,7 +108,6 @@ class VfFilmProvider : MainAPI() {
|
||||||
|
|
||||||
val data = getDirect("$mainUrl/?trembed=$i&trid=$trid&trtype=1")
|
val data = getDirect("$mainUrl/?trembed=$i&trid=$trid&trtype=1")
|
||||||
|
|
||||||
|
|
||||||
return MovieLoadResponse(
|
return MovieLoadResponse(
|
||||||
title,
|
title,
|
||||||
url,
|
url,
|
||||||
|
|
|
@ -61,13 +61,13 @@ class FrenchStreamProvider : MainAPI() {
|
||||||
val listEpisode = soup.selectFirst("div.elink")
|
val listEpisode = soup.selectFirst("div.elink")
|
||||||
|
|
||||||
if (isMovie) {
|
if (isMovie) {
|
||||||
val trailer = soup.selectFirst("div.fleft > span > a").attr("href").toString()
|
val trailer = soup.selectFirst("div.fleft > span > a")?.attr("href")
|
||||||
val date = soup.select("ul.flist-col > li")[2].text().toIntOrNull()
|
val date = soup.select("ul.flist-col > li")?.getOrNull(2)?.text()?.toIntOrNull()
|
||||||
val ratingAverage = soup.select("div.fr-count > div").text().toIntOrNull()
|
val ratingAverage = soup.select("div.fr-count > div")?.text()?.toIntOrNull()
|
||||||
val tags = soup.select("ul.flist-col > li")[1]
|
val tags = soup.select("ul.flist-col > li")?.getOrNull(1)
|
||||||
val tagsList = tags.select("a")
|
val tagsList = tags?.select("a")
|
||||||
.map { // all the tags like action, thriller ...; unused variable
|
?.mapNotNull { // all the tags like action, thriller ...; unused variable
|
||||||
it.text()
|
it?.text()
|
||||||
}
|
}
|
||||||
return MovieLoadResponse(
|
return MovieLoadResponse(
|
||||||
title,
|
title,
|
||||||
|
@ -185,10 +185,10 @@ class FrenchStreamProvider : MainAPI() {
|
||||||
val serversvo = // Original version servers
|
val serversvo = // Original version servers
|
||||||
soup.select("div#episode$translated > div.selink > ul.btnss $div> li")
|
soup.select("div#episode$translated > div.selink > ul.btnss $div> li")
|
||||||
.mapNotNull { li ->
|
.mapNotNull { li ->
|
||||||
val serverurl = fixUrl(li.selectFirst("a").attr("href"))
|
val serverUrl = fixUrlNull(li.selectFirst("a")?.attr("href"))
|
||||||
if (serverurl != "") {
|
if (!serverUrl.isNullOrEmpty()) {
|
||||||
if (li.text().replace(" ", "").replace(" ", "") != "") {
|
if (li.text().replace(" ", "").replace(" ", "") != "") {
|
||||||
Pair(li.text().replace(" ", ""), fixUrl(serverurl))
|
Pair(li.text().replace(" ", ""), fixUrl(serverUrl))
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
@ -198,22 +198,22 @@ class FrenchStreamProvider : MainAPI() {
|
||||||
}
|
}
|
||||||
serversvf + serversvo
|
serversvf + serversvo
|
||||||
} else { // it's a movie
|
} else { // it's a movie
|
||||||
val soup = app.get(fixUrl(data)).document
|
|
||||||
val movieServers =
|
val movieServers =
|
||||||
soup.select("nav#primary_nav_wrap > ul > li > ul > li > a").mapNotNull { a ->
|
app.get(fixUrl(data)).document.select("nav#primary_nav_wrap > ul > li > ul > li > a")
|
||||||
val serverurl = fixUrl(a.attr("href"))
|
.mapNotNull { a ->
|
||||||
val parent = a.parents()[2]
|
val serverurl = fixUrlNull(a.attr("href")) ?: return@mapNotNull null
|
||||||
val element = parent.selectFirst("a").text().plus(" ")
|
val parent = a.parents()[2]
|
||||||
if (a.text().replace(" ", "").trim() != "") {
|
val element = parent.selectFirst("a").text().plus(" ")
|
||||||
Pair(element.plus(a.text()), fixUrl(serverurl))
|
if (a.text().replace(" ", "").trim() != "") {
|
||||||
} else {
|
Pair(element.plus(a.text()), fixUrl(serverurl))
|
||||||
null
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
movieServers
|
movieServers
|
||||||
}
|
}
|
||||||
|
|
||||||
servers.forEach {
|
servers.apmap {
|
||||||
for (extractor in extractorApis) {
|
for (extractor in extractorApis) {
|
||||||
if (it.first.contains(extractor.name, ignoreCase = true)) {
|
if (it.first.contains(extractor.name, ignoreCase = true)) {
|
||||||
// val name = it.first
|
// val name = it.first
|
||||||
|
|
|
@ -438,7 +438,7 @@ class HomeFragment : Fragment() {
|
||||||
val d = data.value
|
val d = data.value
|
||||||
|
|
||||||
currentHomePage = d
|
currentHomePage = d
|
||||||
(home_master_recycler?.adapter as ParentItemAdapter?)?.items =
|
(home_master_recycler?.adapter as ParentItemAdapter?)?.updateList(
|
||||||
d?.items?.mapNotNull {
|
d?.items?.mapNotNull {
|
||||||
try {
|
try {
|
||||||
HomePageList(it.name, it.list.filterSearchResponse())
|
HomePageList(it.name, it.list.filterSearchResponse())
|
||||||
|
@ -446,9 +446,7 @@ class HomeFragment : Fragment() {
|
||||||
logError(e)
|
logError(e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
} ?: listOf()
|
} ?: listOf())
|
||||||
|
|
||||||
home_master_recycler?.adapter?.notifyDataSetChanged()
|
|
||||||
|
|
||||||
home_loading?.isVisible = false
|
home_loading?.isVisible = false
|
||||||
home_loading_error?.isVisible = false
|
home_loading_error?.isVisible = false
|
||||||
|
@ -470,9 +468,13 @@ class HomeFragment : Fragment() {
|
||||||
api.name
|
api.name
|
||||||
)
|
)
|
||||||
}) {
|
}) {
|
||||||
val i = Intent(Intent.ACTION_VIEW)
|
try {
|
||||||
i.data = Uri.parse(validAPIs[itemId].mainUrl)
|
val i = Intent(Intent.ACTION_VIEW)
|
||||||
startActivity(i)
|
i.data = Uri.parse(validAPIs[itemId].mainUrl)
|
||||||
|
startActivity(i)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -489,9 +491,8 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
|
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
|
||||||
ParentItemAdapter(listOf(), { callback ->
|
ParentItemAdapter(mutableListOf(), { callback ->
|
||||||
homeHandleSearch(callback)
|
homeHandleSearch(callback)
|
||||||
}, { item ->
|
}, { item ->
|
||||||
activity?.loadHomepageList(item)
|
activity?.loadHomepageList(item)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.lagradost.cloudstream3.HomePageList
|
import com.lagradost.cloudstream3.HomePageList
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
|
@ -12,14 +13,16 @@ import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||||
import kotlinx.android.synthetic.main.homepage_parent.view.*
|
import kotlinx.android.synthetic.main.homepage_parent.view.*
|
||||||
|
|
||||||
class ParentItemAdapter(
|
class ParentItemAdapter(
|
||||||
var items: List<HomePageList>,
|
private var items: MutableList<HomePageList>,
|
||||||
private val clickCallback: (SearchClickCallback) -> Unit,
|
private val clickCallback: (SearchClickCallback) -> Unit,
|
||||||
private val moreInfoClickCallback: (HomePageList) -> Unit,
|
private val moreInfoClickCallback: (HomePageList) -> Unit,
|
||||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, i: Int): ParentViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, i: Int): ParentViewHolder {
|
||||||
val layout = R.layout.homepage_parent
|
val layout = R.layout.homepage_parent
|
||||||
return ParentViewHolder(
|
return ParentViewHolder(
|
||||||
LayoutInflater.from(parent.context).inflate(layout, parent, false), clickCallback, moreInfoClickCallback
|
LayoutInflater.from(parent.context).inflate(layout, parent, false),
|
||||||
|
clickCallback,
|
||||||
|
moreInfoClickCallback
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +38,20 @@ class ParentItemAdapter(
|
||||||
return items.size
|
return items.size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getItemId(position: Int): Long {
|
||||||
|
return items[position].name.hashCode().toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateList(newList: List<HomePageList>) {
|
||||||
|
val diffResult = DiffUtil.calculateDiff(
|
||||||
|
SearchDiffCallback(this.items, newList))
|
||||||
|
|
||||||
|
items.clear()
|
||||||
|
items.addAll(newList)
|
||||||
|
|
||||||
|
diffResult.dispatchUpdatesTo(this)
|
||||||
|
}
|
||||||
|
|
||||||
class ParentViewHolder
|
class ParentViewHolder
|
||||||
constructor(
|
constructor(
|
||||||
itemView: View,
|
itemView: View,
|
||||||
|
@ -61,3 +78,16 @@ class ParentItemAdapter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SearchDiffCallback(private val oldList: List<HomePageList>, private val newList: List<HomePageList>) :
|
||||||
|
DiffUtil.Callback() {
|
||||||
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||||
|
oldList[oldItemPosition].name == newList[newItemPosition].name
|
||||||
|
|
||||||
|
override fun getOldListSize() = oldList.size
|
||||||
|
|
||||||
|
override fun getNewListSize() = newList.size
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||||
|
oldList[oldItemPosition] == newList[newItemPosition]
|
||||||
|
}
|
|
@ -197,6 +197,7 @@ abstract class AbstractPlayerFragment(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playerError(exception: Exception) {
|
private fun playerError(exception: Exception) {
|
||||||
|
val ctx = context ?: return
|
||||||
when (exception) {
|
when (exception) {
|
||||||
is PlaybackException -> {
|
is PlaybackException -> {
|
||||||
val msg = exception.message ?: ""
|
val msg = exception.message ?: ""
|
||||||
|
@ -205,7 +206,7 @@ abstract class AbstractPlayerFragment(
|
||||||
PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND, PlaybackException.ERROR_CODE_IO_NO_PERMISSION, PlaybackException.ERROR_CODE_IO_UNSPECIFIED -> {
|
PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND, PlaybackException.ERROR_CODE_IO_NO_PERMISSION, PlaybackException.ERROR_CODE_IO_UNSPECIFIED -> {
|
||||||
showToast(
|
showToast(
|
||||||
activity,
|
activity,
|
||||||
"${getString(R.string.source_error)}\n$errorName ($code)\n$msg",
|
"${ctx.getString(R.string.source_error)}\n$errorName ($code)\n$msg",
|
||||||
Toast.LENGTH_SHORT
|
Toast.LENGTH_SHORT
|
||||||
)
|
)
|
||||||
nextMirror()
|
nextMirror()
|
||||||
|
@ -213,7 +214,7 @@ abstract class AbstractPlayerFragment(
|
||||||
PlaybackException.ERROR_CODE_REMOTE_ERROR, PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS, PlaybackException.ERROR_CODE_TIMEOUT, PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE -> {
|
PlaybackException.ERROR_CODE_REMOTE_ERROR, PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS, PlaybackException.ERROR_CODE_TIMEOUT, PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE -> {
|
||||||
showToast(
|
showToast(
|
||||||
activity,
|
activity,
|
||||||
"${getString(R.string.remote_error)}\n$errorName ($code)\n$msg",
|
"${ctx.getString(R.string.remote_error)}\n$errorName ($code)\n$msg",
|
||||||
Toast.LENGTH_SHORT
|
Toast.LENGTH_SHORT
|
||||||
)
|
)
|
||||||
nextMirror()
|
nextMirror()
|
||||||
|
@ -221,7 +222,7 @@ abstract class AbstractPlayerFragment(
|
||||||
PlaybackException.ERROR_CODE_DECODING_FAILED, PlaybackErrorEvent.ERROR_AUDIO_TRACK_INIT_FAILED, PlaybackErrorEvent.ERROR_AUDIO_TRACK_OTHER, PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED, PlaybackException.ERROR_CODE_DECODER_INIT_FAILED, PlaybackException.ERROR_CODE_DECODER_QUERY_FAILED -> {
|
PlaybackException.ERROR_CODE_DECODING_FAILED, PlaybackErrorEvent.ERROR_AUDIO_TRACK_INIT_FAILED, PlaybackErrorEvent.ERROR_AUDIO_TRACK_OTHER, PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED, PlaybackException.ERROR_CODE_DECODER_INIT_FAILED, PlaybackException.ERROR_CODE_DECODER_QUERY_FAILED -> {
|
||||||
showToast(
|
showToast(
|
||||||
activity,
|
activity,
|
||||||
"${getString(R.string.render_error)}\n$errorName ($code)\n$msg",
|
"${ctx.getString(R.string.render_error)}\n$errorName ($code)\n$msg",
|
||||||
Toast.LENGTH_SHORT
|
Toast.LENGTH_SHORT
|
||||||
)
|
)
|
||||||
nextMirror()
|
nextMirror()
|
||||||
|
@ -229,7 +230,7 @@ abstract class AbstractPlayerFragment(
|
||||||
else -> {
|
else -> {
|
||||||
showToast(
|
showToast(
|
||||||
activity,
|
activity,
|
||||||
"${getString(R.string.unexpected_error)}\n$errorName ($code)\n$msg",
|
"${ctx.getString(R.string.unexpected_error)}\n$errorName ($code)\n$msg",
|
||||||
Toast.LENGTH_SHORT
|
Toast.LENGTH_SHORT
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,8 +42,8 @@ class DownloadFileGenerator(
|
||||||
return episodes[currentIndex].id
|
return episodes[currentIndex].id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCurrent(): Any {
|
override fun getCurrent(offset: Int): Any? {
|
||||||
return episodes[currentIndex]
|
return episodes.getOrNull(currentIndex + offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun generateLinks(
|
override suspend fun generateLinks(
|
||||||
|
|
|
@ -6,6 +6,7 @@ import android.view.KeyEvent
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.lagradost.cloudstream3.CommonActivity
|
import com.lagradost.cloudstream3.CommonActivity
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils
|
import com.lagradost.cloudstream3.utils.AppUtils
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.getUri
|
import com.lagradost.cloudstream3.utils.AppUtils.getUri
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorUri
|
import com.lagradost.cloudstream3.utils.ExtractorUri
|
||||||
|
@ -72,8 +73,12 @@ class DownloadedPlayerActivity : AppCompatActivity() {
|
||||||
"NULL"
|
"NULL"
|
||||||
}
|
}
|
||||||
|
|
||||||
val realUri = AppUtils.getVideoContentUri(this, realPath)
|
val tryUri = try {
|
||||||
val tryUri = realUri ?: uri
|
AppUtils.getVideoContentUri(this, realPath) ?: uri
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
|
uri
|
||||||
|
}
|
||||||
|
|
||||||
setContentView(R.layout.empty_layout)
|
setContentView(R.layout.empty_layout)
|
||||||
Log.i(DTAG, "navigating")
|
Log.i(DTAG, "navigating")
|
||||||
|
|
|
@ -352,7 +352,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
}
|
}
|
||||||
activity?.hideSystemUI()
|
activity?.hideSystemUI()
|
||||||
animateLayoutChanges()
|
animateLayoutChanges()
|
||||||
player_pause_play.requestFocus()
|
player_pause_play?.requestFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toggleLock() {
|
private fun toggleLock() {
|
||||||
|
|
|
@ -71,6 +71,15 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
return setSubtitles(null)
|
return setSubtitles(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getPos(): Long {
|
||||||
|
val durPos = DataStoreHelper.getViewPos(viewModel.getId()) ?: return 0L
|
||||||
|
if (durPos.duration == 0L) return 0L
|
||||||
|
if (durPos.position * 100L / durPos.duration > 95L) {
|
||||||
|
return 0L
|
||||||
|
}
|
||||||
|
return durPos.position
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadLink(link: Pair<ExtractorLink?, ExtractorUri?>?, sameEpisode: Boolean) {
|
private fun loadLink(link: Pair<ExtractorLink?, ExtractorUri?>?, sameEpisode: Boolean) {
|
||||||
if (link == null) return
|
if (link == null) return
|
||||||
|
|
||||||
|
@ -93,8 +102,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
url,
|
url,
|
||||||
uri,
|
uri,
|
||||||
startPosition = if (sameEpisode) null else {
|
startPosition = if (sameEpisode) null else {
|
||||||
if (isNextEpisode) 0L else (DataStoreHelper.getViewPos(viewModel.getId())?.position
|
if (isNextEpisode) 0L else getPos()
|
||||||
?: 0L)
|
|
||||||
},
|
},
|
||||||
currentSubs,
|
currentSubs,
|
||||||
)
|
)
|
||||||
|
@ -272,11 +280,16 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
init = init || if (subtitleIndex <= 0) {
|
init = init || if (subtitleIndex <= 0) {
|
||||||
noSubtitles()
|
noSubtitles()
|
||||||
} else {
|
} else {
|
||||||
setSubtitles(currentSubtitles[subtitleIndex - 1])
|
currentSubtitles.getOrNull(subtitleIndex - 1)?.let {
|
||||||
|
setSubtitles(it)
|
||||||
|
true
|
||||||
|
} ?: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (init) {
|
if (init) {
|
||||||
loadLink(sortedUrls[sourceIndex], true)
|
sortedUrls.getOrNull(sourceIndex)?.let {
|
||||||
|
loadLink(it, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sourceDialog.dismissSafe(activity)
|
sourceDialog.dismissSafe(activity)
|
||||||
}
|
}
|
||||||
|
@ -304,11 +317,13 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
|
|
||||||
override fun nextEpisode() {
|
override fun nextEpisode() {
|
||||||
isNextEpisode = true
|
isNextEpisode = true
|
||||||
|
player.release()
|
||||||
viewModel.loadLinksNext()
|
viewModel.loadLinksNext()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun prevEpisode() {
|
override fun prevEpisode() {
|
||||||
isNextEpisode = true
|
isNextEpisode = true
|
||||||
|
player.release()
|
||||||
viewModel.loadLinksPrev()
|
viewModel.loadLinksPrev()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -336,6 +351,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
override fun playerPositionChanged(posDur: Pair<Long, Long>) {
|
override fun playerPositionChanged(posDur: Pair<Long, Long>) {
|
||||||
val (position, duration) = posDur
|
val (position, duration) = posDur
|
||||||
viewModel.getId()?.let {
|
viewModel.getId()?.let {
|
||||||
|
println("SET VIEW ID: $it ($position/$duration)")
|
||||||
DataStoreHelper.setViewPos(it, position, duration)
|
DataStoreHelper.setViewPos(it, position, duration)
|
||||||
}
|
}
|
||||||
val percentage = position * 100L / duration
|
val percentage = position * 100L / duration
|
||||||
|
|
|
@ -13,7 +13,7 @@ interface IGenerator {
|
||||||
fun goto(index: Int)
|
fun goto(index: Int)
|
||||||
|
|
||||||
fun getCurrentId(): Int? // this is used to save data or read data about this id
|
fun getCurrentId(): Int? // this is used to save data or read data about this id
|
||||||
fun getCurrent(): Any? // this is used to get metadata about the current playing, can return null
|
fun getCurrent(offset : Int = 0): Any? // this is used to get metadata about the current playing, can return null
|
||||||
|
|
||||||
/* not safe, must use try catch */
|
/* not safe, must use try catch */
|
||||||
suspend fun generateLinks(
|
suspend fun generateLinks(
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.lagradost.cloudstream3.ui.player
|
package com.lagradost.cloudstream3.ui.player
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
@ -13,6 +14,10 @@ import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class PlayerGeneratorViewModel : ViewModel() {
|
class PlayerGeneratorViewModel : ViewModel() {
|
||||||
|
companion object {
|
||||||
|
val TAG = "PlayViewGen"
|
||||||
|
}
|
||||||
|
|
||||||
private var generator: IGenerator? = null
|
private var generator: IGenerator? = null
|
||||||
|
|
||||||
private val _currentLinks = MutableLiveData<Set<Pair<ExtractorLink?, ExtractorUri?>>>(setOf())
|
private val _currentLinks = MutableLiveData<Set<Pair<ExtractorLink?, ExtractorUri?>>>(setOf())
|
||||||
|
@ -34,6 +39,7 @@ class PlayerGeneratorViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadLinksPrev() {
|
fun loadLinksPrev() {
|
||||||
|
Log.i(TAG, "loadLinksPrev")
|
||||||
if (generator?.hasPrev() == true) {
|
if (generator?.hasPrev() == true) {
|
||||||
generator?.prev()
|
generator?.prev()
|
||||||
loadLinks()
|
loadLinks()
|
||||||
|
@ -41,6 +47,7 @@ class PlayerGeneratorViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadLinksNext() {
|
fun loadLinksNext() {
|
||||||
|
Log.i(TAG, "loadLinksNext")
|
||||||
if (generator?.hasNext() == true) {
|
if (generator?.hasNext() == true) {
|
||||||
generator?.next()
|
generator?.next()
|
||||||
loadLinks()
|
loadLinks()
|
||||||
|
@ -52,11 +59,18 @@ class PlayerGeneratorViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun preLoadNextLinks() {
|
fun preLoadNextLinks() {
|
||||||
|
Log.i(TAG, "preLoadNextLinks")
|
||||||
currentJob?.cancel()
|
currentJob?.cancel()
|
||||||
currentJob = viewModelScope.launch {
|
currentJob = viewModelScope.launch {
|
||||||
if (generator?.hasCache == true && generator?.hasNext() == true) {
|
if (generator?.hasCache == true && generator?.hasNext() == true) {
|
||||||
safeApiCall {
|
safeApiCall {
|
||||||
generator?.generateLinks(clearCache = false, isCasting = false, {}, {}, offset = 1)
|
generator?.generateLinks(
|
||||||
|
clearCache = false,
|
||||||
|
isCasting = false,
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
offset = 1
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,10 +83,7 @@ class PlayerGeneratorViewModel : ViewModel() {
|
||||||
fun getNextMeta(): Any? {
|
fun getNextMeta(): Any? {
|
||||||
return normalSafeApiCall {
|
return normalSafeApiCall {
|
||||||
if (generator?.hasNext() == false) return@normalSafeApiCall null
|
if (generator?.hasNext() == false) return@normalSafeApiCall null
|
||||||
generator?.next()
|
generator?.getCurrent(offset = 1)
|
||||||
val next = generator?.getCurrent()
|
|
||||||
generator?.prev()
|
|
||||||
next
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,6 +102,7 @@ class PlayerGeneratorViewModel : ViewModel() {
|
||||||
private var currentJob: Job? = null
|
private var currentJob: Job? = null
|
||||||
|
|
||||||
fun loadLinks(clearCache: Boolean = false, isCasting: Boolean = false) {
|
fun loadLinks(clearCache: Boolean = false, isCasting: Boolean = false) {
|
||||||
|
Log.i(TAG, "loadLinks")
|
||||||
currentJob?.cancel()
|
currentJob?.cancel()
|
||||||
currentJob = viewModelScope.launch {
|
currentJob = viewModelScope.launch {
|
||||||
val currentLinks = mutableSetOf<Pair<ExtractorLink?, ExtractorUri?>>()
|
val currentLinks = mutableSetOf<Pair<ExtractorLink?, ExtractorUri?>>()
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.lagradost.cloudstream3.ui.player
|
package com.lagradost.cloudstream3.ui.player
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||||
import com.lagradost.cloudstream3.ui.APIRepository
|
import com.lagradost.cloudstream3.ui.APIRepository
|
||||||
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
||||||
|
@ -8,7 +9,14 @@ import com.lagradost.cloudstream3.utils.ExtractorUri
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
class RepoLinkGenerator(private val episodes: List<ResultEpisode>, private var currentIndex: Int = 0) : IGenerator {
|
class RepoLinkGenerator(
|
||||||
|
private val episodes: List<ResultEpisode>,
|
||||||
|
private var currentIndex: Int = 0
|
||||||
|
) : IGenerator {
|
||||||
|
companion object {
|
||||||
|
val TAG = "RepoLink"
|
||||||
|
}
|
||||||
|
|
||||||
override val hasCache = true
|
override val hasCache = true
|
||||||
|
|
||||||
override fun hasNext(): Boolean {
|
override fun hasNext(): Boolean {
|
||||||
|
@ -20,16 +28,19 @@ class RepoLinkGenerator(private val episodes: List<ResultEpisode>, private var c
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun next() {
|
override fun next() {
|
||||||
|
Log.i(TAG, "next")
|
||||||
if (hasNext())
|
if (hasNext())
|
||||||
currentIndex++
|
currentIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun prev() {
|
override fun prev() {
|
||||||
|
Log.i(TAG, "prev")
|
||||||
if (hasPrev())
|
if (hasPrev())
|
||||||
currentIndex--
|
currentIndex--
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun goto(index: Int) {
|
override fun goto(index: Int) {
|
||||||
|
Log.i(TAG, "goto $index")
|
||||||
// clamps value
|
// clamps value
|
||||||
currentIndex = min(episodes.size - 1, max(0, index))
|
currentIndex = min(episodes.size - 1, max(0, index))
|
||||||
}
|
}
|
||||||
|
@ -38,8 +49,8 @@ class RepoLinkGenerator(private val episodes: List<ResultEpisode>, private var c
|
||||||
return episodes[currentIndex].id
|
return episodes[currentIndex].id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCurrent(): Any {
|
override fun getCurrent(offset: Int): Any? {
|
||||||
return episodes[currentIndex]
|
return episodes.getOrNull(currentIndex + offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is a simple array that is used to instantly load links if they are already loaded
|
// this is a simple array that is used to instantly load links if they are already loaded
|
||||||
|
@ -51,10 +62,10 @@ class RepoLinkGenerator(private val episodes: List<ResultEpisode>, private var c
|
||||||
isCasting: Boolean,
|
isCasting: Boolean,
|
||||||
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
|
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
|
||||||
subtitleCallback: (SubtitleData) -> Unit,
|
subtitleCallback: (SubtitleData) -> Unit,
|
||||||
offset : Int,
|
offset: Int,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val index = currentIndex
|
val index = currentIndex
|
||||||
val current = episodes[index + offset]
|
val current = episodes.getOrNull(index + offset) ?: return false
|
||||||
|
|
||||||
val currentLinkCache = if (clearCache) mutableSetOf() else linkCache[index].toMutableSet()
|
val currentLinkCache = if (clearCache) mutableSetOf() else linkCache[index].toMutableSet()
|
||||||
val currentSubsCache = if (clearCache) mutableSetOf() else subsCache[index].toMutableSet()
|
val currentSubsCache = if (clearCache) mutableSetOf() else subsCache[index].toMutableSet()
|
||||||
|
@ -76,7 +87,7 @@ class RepoLinkGenerator(private val episodes: List<ResultEpisode>, private var c
|
||||||
|
|
||||||
// this stops all execution if links are cached
|
// this stops all execution if links are cached
|
||||||
// no extra get requests
|
// no extra get requests
|
||||||
if(currentLinkCache.size > 0) {
|
if (currentLinkCache.size > 0) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,13 +97,13 @@ class RepoLinkGenerator(private val episodes: List<ResultEpisode>, private var c
|
||||||
isCasting,
|
isCasting,
|
||||||
{ file ->
|
{ file ->
|
||||||
val correctFile = PlayerSubtitleHelper.getSubtitleData(file)
|
val correctFile = PlayerSubtitleHelper.getSubtitleData(file)
|
||||||
if(!currentSubsUrls.contains(correctFile.url)) {
|
if (!currentSubsUrls.contains(correctFile.url)) {
|
||||||
currentSubsUrls.add(correctFile.url)
|
currentSubsUrls.add(correctFile.url)
|
||||||
|
|
||||||
// this part makes sure that all names are unique for UX
|
// this part makes sure that all names are unique for UX
|
||||||
var name = correctFile.name
|
var name = correctFile.name
|
||||||
var count = 0
|
var count = 0
|
||||||
while(currentSubsNames.contains(name)) {
|
while (currentSubsNames.contains(name)) {
|
||||||
count++
|
count++
|
||||||
name = "${correctFile.name} $count"
|
name = "${correctFile.name} $count"
|
||||||
}
|
}
|
||||||
|
@ -108,7 +119,7 @@ class RepoLinkGenerator(private val episodes: List<ResultEpisode>, private var c
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ link ->
|
{ link ->
|
||||||
if(!currentLinks.contains(link.url)) {
|
if (!currentLinks.contains(link.url)) {
|
||||||
if (!currentLinkCache.contains(link)) {
|
if (!currentLinkCache.contains(link)) {
|
||||||
currentLinks.add(link.url)
|
currentLinks.add(link.url)
|
||||||
callback(Pair(link, null))
|
callback(Pair(link, null))
|
||||||
|
|
|
@ -38,7 +38,11 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pushSync(activity: Activity?, autoSearch: String? = null, callback: (SearchClickCallback) -> Unit) {
|
fun pushSync(
|
||||||
|
activity: Activity?,
|
||||||
|
autoSearch: String? = null,
|
||||||
|
callback: (SearchClickCallback) -> Unit
|
||||||
|
) {
|
||||||
clickCallback = callback
|
clickCallback = callback
|
||||||
activity.navigate(R.id.global_to_navigation_quick_search, Bundle().apply {
|
activity.navigate(R.id.global_to_navigation_quick_search, Bundle().apply {
|
||||||
putBoolean("mainapi", false)
|
putBoolean("mainapi", false)
|
||||||
|
@ -82,14 +86,13 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() {
|
||||||
// https://stackoverflow.com/questions/6866238/concurrent-modification-exception-adding-to-an-arraylist
|
// https://stackoverflow.com/questions/6866238/concurrent-modification-exception-adding-to-an-arraylist
|
||||||
listLock.lock()
|
listLock.lock()
|
||||||
(quick_search_master_recycler?.adapter as ParentItemAdapter?)?.apply {
|
(quick_search_master_recycler?.adapter as ParentItemAdapter?)?.apply {
|
||||||
items = list.map { ongoing ->
|
updateList(list.map { ongoing ->
|
||||||
val ongoingList = HomePageList(
|
val ongoingList = HomePageList(
|
||||||
ongoing.apiName,
|
ongoing.apiName,
|
||||||
if (ongoing.data is Resource.Success) ongoing.data.value.filterSearchResponse() else ArrayList()
|
if (ongoing.data is Resource.Success) ongoing.data.value.filterSearchResponse() else ArrayList()
|
||||||
)
|
)
|
||||||
ongoingList
|
ongoingList
|
||||||
}
|
})
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
|
@ -98,31 +101,38 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val masterAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> = ParentItemAdapter(listOf(), { callback ->
|
val masterAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
|
||||||
when (callback.action) {
|
ParentItemAdapter(mutableListOf(), { callback ->
|
||||||
SEARCH_ACTION_LOAD -> {
|
when (callback.action) {
|
||||||
if (isMainApis) {
|
SEARCH_ACTION_LOAD -> {
|
||||||
activity?.popCurrentPage()
|
if (isMainApis) {
|
||||||
|
activity?.popCurrentPage()
|
||||||
|
|
||||||
SearchHelper.handleSearchClickCallback(activity, callback)
|
SearchHelper.handleSearchClickCallback(activity, callback)
|
||||||
} else {
|
} else {
|
||||||
clickCallback?.invoke(callback)
|
clickCallback?.invoke(callback)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else -> SearchHelper.handleSearchClickCallback(activity, callback)
|
||||||
}
|
}
|
||||||
else -> SearchHelper.handleSearchClickCallback(activity, callback)
|
}, { item ->
|
||||||
}
|
activity?.loadHomepageList(item)
|
||||||
}, { item ->
|
})
|
||||||
activity?.loadHomepageList(item)
|
|
||||||
})
|
|
||||||
|
|
||||||
val searchExitIcon = quick_search.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
|
val searchExitIcon =
|
||||||
val searchMagIcon = quick_search.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
|
quick_search.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
|
||||||
|
val searchMagIcon =
|
||||||
|
quick_search.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
|
||||||
|
|
||||||
searchMagIcon.scaleX = 0.65f
|
searchMagIcon.scaleX = 0.65f
|
||||||
searchMagIcon.scaleY = 0.65f
|
searchMagIcon.scaleY = 0.65f
|
||||||
quick_search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
quick_search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||||
override fun onQueryTextSubmit(query: String): Boolean {
|
override fun onQueryTextSubmit(query: String): Boolean {
|
||||||
searchViewModel.searchAndCancel(query = query, isMainApis = isMainApis, ignoreSettings = true)
|
searchViewModel.searchAndCancel(
|
||||||
|
query = query,
|
||||||
|
isMainApis = isMainApis,
|
||||||
|
ignoreSettings = true
|
||||||
|
)
|
||||||
quick_search?.let {
|
quick_search?.let {
|
||||||
UIHelper.hideKeyboard(it)
|
UIHelper.hideKeyboard(it)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1132,7 +1132,7 @@ class ResultFragment : Fragment() {
|
||||||
try {
|
try {
|
||||||
startActivity(i)
|
startActivity(i)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
logError(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1394,7 +1394,7 @@ class ResultFragment : Fragment() {
|
||||||
try {
|
try {
|
||||||
startActivity(i)
|
startActivity(i)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
logError(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -145,7 +145,7 @@ class SearchFragment : Fragment() {
|
||||||
|
|
||||||
search_filter.setOnClickListener { searchView ->
|
search_filter.setOnClickListener { searchView ->
|
||||||
searchView?.context?.let { ctx ->
|
searchView?.context?.let { ctx ->
|
||||||
val validAPIs = ctx.filterProviderByPreferredMedia()
|
val validAPIs = ctx.filterProviderByPreferredMedia(hasHomePageIsRequired = false)
|
||||||
var currentValidApis = listOf<MainAPI>()
|
var currentValidApis = listOf<MainAPI>()
|
||||||
val currentSelectedApis = if (selectedApis.isEmpty()) validAPIs.map { it.name }
|
val currentSelectedApis = if (selectedApis.isEmpty()) validAPIs.map { it.name }
|
||||||
.toMutableSet() else selectedApis
|
.toMutableSet() else selectedApis
|
||||||
|
@ -213,7 +213,7 @@ class SearchFragment : Fragment() {
|
||||||
fun updateList() {
|
fun updateList() {
|
||||||
arrayAdapter.clear()
|
arrayAdapter.clear()
|
||||||
currentValidApis = validAPIs.filter { api ->
|
currentValidApis = validAPIs.filter { api ->
|
||||||
api.hasMainPage && api.supportedTypes.any {
|
api.supportedTypes.any {
|
||||||
selectedSearchTypes.contains(it)
|
selectedSearchTypes.contains(it)
|
||||||
}
|
}
|
||||||
}.sortedBy { it.name }
|
}.sortedBy { it.name }
|
||||||
|
@ -224,7 +224,7 @@ class SearchFragment : Fragment() {
|
||||||
listView?.setItemChecked(index, currentSelectedApis.contains(api))
|
listView?.setItemChecked(index, currentSelectedApis.contains(api))
|
||||||
}
|
}
|
||||||
|
|
||||||
arrayAdapter.notifyDataSetChanged()
|
//arrayAdapter.notifyDataSetChanged()
|
||||||
arrayAdapter.addAll(names)
|
arrayAdapter.addAll(names)
|
||||||
arrayAdapter.notifyDataSetChanged()
|
arrayAdapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
@ -373,14 +373,16 @@ class SearchFragment : Fragment() {
|
||||||
// https://stackoverflow.com/questions/6866238/concurrent-modification-exception-adding-to-an-arraylist
|
// https://stackoverflow.com/questions/6866238/concurrent-modification-exception-adding-to-an-arraylist
|
||||||
listLock.lock()
|
listLock.lock()
|
||||||
(search_master_recycler?.adapter as ParentItemAdapter?)?.apply {
|
(search_master_recycler?.adapter as ParentItemAdapter?)?.apply {
|
||||||
items = list.map { ongoing ->
|
val newItems = list.map { ongoing ->
|
||||||
val ongoingList = HomePageList(
|
val ongoingList = HomePageList(
|
||||||
ongoing.apiName,
|
ongoing.apiName,
|
||||||
if (ongoing.data is Resource.Success) ongoing.data.value.filterSearchResponse() else ArrayList()
|
if (ongoing.data is Resource.Success) ongoing.data.value.filterSearchResponse() else ArrayList()
|
||||||
)
|
)
|
||||||
ongoingList
|
ongoingList
|
||||||
}
|
}
|
||||||
notifyDataSetChanged()
|
updateList(newItems)
|
||||||
|
|
||||||
|
//notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
|
@ -399,7 +401,7 @@ class SearchFragment : Fragment() {
|
||||||
//main_search.onActionViewExpanded()*/
|
//main_search.onActionViewExpanded()*/
|
||||||
|
|
||||||
val masterAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
|
val masterAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
|
||||||
ParentItemAdapter(listOf(), { callback ->
|
ParentItemAdapter(mutableListOf(), { callback ->
|
||||||
SearchHelper.handleSearchClickCallback(activity, callback)
|
SearchHelper.handleSearchClickCallback(activity, callback)
|
||||||
}, { item ->
|
}, { item ->
|
||||||
activity?.loadHomepageList(item)
|
activity?.loadHomepageList(item)
|
||||||
|
|
|
@ -3,6 +3,7 @@ package com.lagradost.cloudstream3
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
@ -23,7 +24,10 @@ class ProviderTests {
|
||||||
"Api ${api.name} returns link with invalid Quality",
|
"Api ${api.name} returns link with invalid Quality",
|
||||||
Qualities.values().map { it.value }.contains(link.quality)
|
Qualities.values().map { it.value }.contains(link.quality)
|
||||||
)
|
)
|
||||||
Assert.assertTrue("Api ${api.name} returns link with invalid url", link.url.length > 4)
|
Assert.assertTrue(
|
||||||
|
"Api ${api.name} returns link with invalid url",
|
||||||
|
link.url.length > 4
|
||||||
|
)
|
||||||
linksLoaded++
|
linksLoaded++
|
||||||
}
|
}
|
||||||
if (success) {
|
if (success) {
|
||||||
|
@ -69,9 +73,17 @@ class ProviderTests {
|
||||||
try {
|
try {
|
||||||
var validResults = false
|
var validResults = false
|
||||||
for (result in searchResult) {
|
for (result in searchResult) {
|
||||||
Assert.assertEquals("Invalid apiName on response on ${api.name}", result.apiName, api.name)
|
Assert.assertEquals(
|
||||||
|
"Invalid apiName on response on ${api.name}",
|
||||||
|
result.apiName,
|
||||||
|
api.name
|
||||||
|
)
|
||||||
val load = api.load(result.url) ?: continue
|
val load = api.load(result.url) ?: continue
|
||||||
Assert.assertEquals("Invalid apiName on load on ${api.name}", load.apiName, result.apiName)
|
Assert.assertEquals(
|
||||||
|
"Invalid apiName on load on ${api.name}",
|
||||||
|
load.apiName,
|
||||||
|
result.apiName
|
||||||
|
)
|
||||||
Assert.assertTrue(
|
Assert.assertTrue(
|
||||||
"Api ${api.name} on load does not contain any of the supportedTypes",
|
"Api ${api.name} on load does not contain any of the supportedTypes",
|
||||||
api.supportedTypes.contains(load.type)
|
api.supportedTypes.contains(load.type)
|
||||||
|
@ -137,33 +149,41 @@ class ProviderTests {
|
||||||
for (api in getAllProviders()) {
|
for (api in getAllProviders()) {
|
||||||
Assert.assertTrue("Api does not contain a mainUrl", api.mainUrl != "NONE")
|
Assert.assertTrue("Api does not contain a mainUrl", api.mainUrl != "NONE")
|
||||||
Assert.assertTrue("Api does not contain a name", api.name != "NONE")
|
Assert.assertTrue("Api does not contain a name", api.name != "NONE")
|
||||||
Assert.assertTrue("Api ${api.name} does not contain a valid language code", isoNames.contains(api.lang))
|
Assert.assertTrue(
|
||||||
Assert.assertTrue("Api ${api.name} does not contain any supported types", api.supportedTypes.isNotEmpty())
|
"Api ${api.name} does not contain a valid language code",
|
||||||
|
isoNames.contains(api.lang)
|
||||||
|
)
|
||||||
|
Assert.assertTrue(
|
||||||
|
"Api ${api.name} does not contain any supported types",
|
||||||
|
api.supportedTypes.isNotEmpty()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun providerCorrectHomepage() {
|
fun providerCorrectHomepage() {
|
||||||
getAllProviders().apmap { api ->
|
runBlocking {
|
||||||
if (api.hasMainPage) {
|
getAllProviders().apmap { api ->
|
||||||
try {
|
if (api.hasMainPage) {
|
||||||
val homepage = api.getMainPage()
|
try {
|
||||||
when {
|
val homepage = api.getMainPage()
|
||||||
homepage == null -> {
|
when {
|
||||||
Assert.fail("Homepage provider ${api.name} did not correctly load homepage!")
|
homepage == null -> {
|
||||||
|
Assert.fail("Homepage provider ${api.name} did not correctly load homepage!")
|
||||||
|
}
|
||||||
|
homepage.items.isEmpty() -> {
|
||||||
|
Assert.fail("Homepage provider ${api.name} does not contain any items!")
|
||||||
|
}
|
||||||
|
homepage.items.any { it.list.isEmpty() } -> {
|
||||||
|
Assert.fail("Homepage provider ${api.name} does not have any items on result!")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
homepage.items.isEmpty() -> {
|
} catch (e: Exception) {
|
||||||
Assert.fail("Homepage provider ${api.name} does not contain any items!")
|
if (e.cause is NotImplementedError) {
|
||||||
}
|
Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented")
|
||||||
homepage.items.any { it.list.isEmpty() } -> {
|
|
||||||
Assert.fail("Homepage provider ${api.name} does not have any items on result!")
|
|
||||||
}
|
}
|
||||||
|
logError(e)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
if (e.cause is NotImplementedError) {
|
|
||||||
Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented")
|
|
||||||
}
|
|
||||||
logError(e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -176,7 +196,7 @@ class ProviderTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
suspend fun providerCorrect() {
|
suspend fun providerCorrect() {
|
||||||
val invalidProvider = ArrayList<Pair<MainAPI,Exception?>>()
|
val invalidProvider = ArrayList<Pair<MainAPI, Exception?>>()
|
||||||
val providers = getAllProviders()
|
val providers = getAllProviders()
|
||||||
providers.apmap { api ->
|
providers.apmap { api ->
|
||||||
try {
|
try {
|
||||||
|
@ -189,7 +209,7 @@ class ProviderTests {
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
invalidProvider.add(Pair(api,e))
|
invalidProvider.add(Pair(api, e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue