pre 2.6.8, fixed many bugs and crashes

This commit is contained in:
LagradOst 2022-01-30 23:02:57 +01:00
parent 4bc86374d7
commit dcb97a1f63
19 changed files with 464 additions and 134 deletions

View file

@ -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'

View file

@ -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")
} }
} }

View file

@ -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 {

View file

@ -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,

View file

@ -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("&nbsp;", "").replace(" ", "") != "") { if (li.text().replace("&nbsp;", "").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("&nbsp;", "").trim() != "") { val element = parent.selectFirst("a").text().plus(" ")
Pair(element.plus(a.text()), fixUrl(serverurl)) if (a.text().replace("&nbsp;", "").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

View file

@ -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)

View file

@ -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]
}

View file

@ -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
) )
} }

View file

@ -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(

View file

@ -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")

View file

@ -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() {

View file

@ -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

View file

@ -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(

View file

@ -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?>>()

View file

@ -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))

View file

@ -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)
} }

View file

@ -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)
} }
} }

View file

@ -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)

View file

@ -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))
} }
} }