diff --git a/app/build.gradle b/app/build.gradle index 5a80144d..813029ad 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,8 +35,8 @@ android { minSdkVersion 21 targetSdkVersion 30 - versionCode 41 - versionName "2.5.8" + versionCode 42 + versionName "2.6.9" resValue "string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}" @@ -88,9 +88,9 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.7.0' - implementation 'androidx.appcompat:appcompat:1.4.0' - implementation 'com.google.android.material:material:1.4.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.2' + implementation 'androidx.appcompat:appcompat:1.4.1' + implementation 'com.google.android.material:material:1.5.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.3' implementation 'androidx.navigation:navigation-fragment-ktx:2.4.0-rc01' implementation 'androidx.navigation:navigation-ui-ktx:2.4.0-rc01' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0' @@ -103,7 +103,7 @@ dependencies { implementation 'org.jsoup:jsoup:1.13.1' 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" @@ -147,7 +147,7 @@ dependencies { implementation "androidx.work:work-runtime-ktx:2.7.1" // 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.google.android.exoplayer:extension-okhttp:2.16.1' diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt index d9970b51..f0cb0a02 100644 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt @@ -1,13 +1,14 @@ package com.lagradost.cloudstream3 -import androidx.test.platform.app.InstrumentationRegistry 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.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * @@ -15,10 +16,232 @@ import org.junit.Assert.* */ @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.lagradost.cloudstream3", appContext.packageName) + //@Test + //fun useAppContext() { + // // Context of the app under test. + // val appContext = InstrumentationRegistry.getInstrumentation().targetContext + // assertEquals("com.lagradost.cloudstream3", appContext.packageName) + //} + + private fun getAllProviders(): List { + val allApis = APIHolder.apis + allApis.addAll(APIHolder.restrictedApis) + return allApis //.filter { !it.usesWebView } } -} \ No newline at end of file + + 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? = 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 + fun providersExist() { + Assert.assertTrue(getAllProviders().isNotEmpty()) + println("Done providersExist") + } + + @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>() + 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") + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 455a6f3f..7761e66a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -184,12 +184,12 @@ object APIHolder { return realSet } - fun Context.filterProviderByPreferredMedia(): List { + fun Context.filterProviderByPreferredMedia(hasHomePageIsRequired : Boolean = true): List { val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) val currentPrefMedia = settingsManager.getInt(this.getString(R.string.prefer_media_type_key), 0) 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) { allApis } else { diff --git a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/VfFilmProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/VfFilmProvider.kt index b5e0bd12..6b6a3048 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/VfFilmProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/VfFilmProvider.kt @@ -45,7 +45,7 @@ class VfFilmProvider : MainAPI() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ): Boolean { - if (data == "") return false + if (data.length <= 4) return false callback.invoke( ExtractorLink( this.name, @@ -100,7 +100,7 @@ class VfFilmProvider : MainAPI() { number_player += 1 } } - if (found == false) { + if (!found) { number_player = 0 } val i = number_player.toString() @@ -108,7 +108,6 @@ class VfFilmProvider : MainAPI() { val data = getDirect("$mainUrl/?trembed=$i&trid=$trid&trtype=1") - return MovieLoadResponse( title, url, diff --git a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/french-stream.kt b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/french-stream.kt index fa3c9f30..f87dc2ce 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/french-stream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/french-stream.kt @@ -61,13 +61,13 @@ class FrenchStreamProvider : MainAPI() { val listEpisode = soup.selectFirst("div.elink") if (isMovie) { - val trailer = soup.selectFirst("div.fleft > span > a").attr("href").toString() - val date = soup.select("ul.flist-col > li")[2].text().toIntOrNull() - val ratingAverage = soup.select("div.fr-count > div").text().toIntOrNull() - val tags = soup.select("ul.flist-col > li")[1] - val tagsList = tags.select("a") - .map { // all the tags like action, thriller ...; unused variable - it.text() + val trailer = soup.selectFirst("div.fleft > span > a")?.attr("href") + val date = soup.select("ul.flist-col > li")?.getOrNull(2)?.text()?.toIntOrNull() + val ratingAverage = soup.select("div.fr-count > div")?.text()?.toIntOrNull() + val tags = soup.select("ul.flist-col > li")?.getOrNull(1) + val tagsList = tags?.select("a") + ?.mapNotNull { // all the tags like action, thriller ...; unused variable + it?.text() } return MovieLoadResponse( title, @@ -185,10 +185,10 @@ class FrenchStreamProvider : MainAPI() { val serversvo = // Original version servers soup.select("div#episode$translated > div.selink > ul.btnss $div> li") .mapNotNull { li -> - val serverurl = fixUrl(li.selectFirst("a").attr("href")) - if (serverurl != "") { + val serverUrl = fixUrlNull(li.selectFirst("a")?.attr("href")) + if (!serverUrl.isNullOrEmpty()) { if (li.text().replace(" ", "").replace(" ", "") != "") { - Pair(li.text().replace(" ", ""), fixUrl(serverurl)) + Pair(li.text().replace(" ", ""), fixUrl(serverUrl)) } else { null } @@ -198,22 +198,22 @@ class FrenchStreamProvider : MainAPI() { } serversvf + serversvo } else { // it's a movie - val soup = app.get(fixUrl(data)).document val movieServers = - soup.select("nav#primary_nav_wrap > ul > li > ul > li > a").mapNotNull { a -> - val serverurl = fixUrl(a.attr("href")) - val parent = a.parents()[2] - val element = parent.selectFirst("a").text().plus(" ") - if (a.text().replace(" ", "").trim() != "") { - Pair(element.plus(a.text()), fixUrl(serverurl)) - } else { - null + app.get(fixUrl(data)).document.select("nav#primary_nav_wrap > ul > li > ul > li > a") + .mapNotNull { a -> + val serverurl = fixUrlNull(a.attr("href")) ?: return@mapNotNull null + val parent = a.parents()[2] + val element = parent.selectFirst("a").text().plus(" ") + if (a.text().replace(" ", "").trim() != "") { + Pair(element.plus(a.text()), fixUrl(serverurl)) + } else { + null + } } - } movieServers } - servers.forEach { + servers.apmap { for (extractor in extractorApis) { if (it.first.contains(extractor.name, ignoreCase = true)) { // val name = it.first diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index 1baad3a5..bba25e69 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -438,7 +438,7 @@ class HomeFragment : Fragment() { val d = data.value currentHomePage = d - (home_master_recycler?.adapter as ParentItemAdapter?)?.items = + (home_master_recycler?.adapter as ParentItemAdapter?)?.updateList( d?.items?.mapNotNull { try { HomePageList(it.name, it.list.filterSearchResponse()) @@ -446,9 +446,7 @@ class HomeFragment : Fragment() { logError(e) null } - } ?: listOf() - - home_master_recycler?.adapter?.notifyDataSetChanged() + } ?: listOf()) home_loading?.isVisible = false home_loading_error?.isVisible = false @@ -470,9 +468,13 @@ class HomeFragment : Fragment() { api.name ) }) { - val i = Intent(Intent.ACTION_VIEW) - i.data = Uri.parse(validAPIs[itemId].mainUrl) - startActivity(i) + try { + val i = Intent(Intent.ACTION_VIEW) + 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 = - ParentItemAdapter(listOf(), { callback -> + ParentItemAdapter(mutableListOf(), { callback -> homeHandleSearch(callback) }, { item -> activity?.loadHomepageList(item) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt index adc53da5..8425dcca 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt @@ -5,6 +5,7 @@ import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.R @@ -12,14 +13,16 @@ import com.lagradost.cloudstream3.ui.search.SearchClickCallback import kotlinx.android.synthetic.main.homepage_parent.view.* class ParentItemAdapter( - var items: List, + private var items: MutableList, private val clickCallback: (SearchClickCallback) -> Unit, private val moreInfoClickCallback: (HomePageList) -> Unit, ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, i: Int): ParentViewHolder { val layout = R.layout.homepage_parent 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 } + override fun getItemId(position: Int): Long { + return items[position].name.hashCode().toLong() + } + + fun updateList(newList: List) { + val diffResult = DiffUtil.calculateDiff( + SearchDiffCallback(this.items, newList)) + + items.clear() + items.addAll(newList) + + diffResult.dispatchUpdatesTo(this) + } + class ParentViewHolder constructor( itemView: View, @@ -60,4 +77,17 @@ class ParentItemAdapter( } } } +} + +class SearchDiffCallback(private val oldList: List, private val newList: List) : + 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] } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index 6012ed77..d3bcf47c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -197,6 +197,7 @@ abstract class AbstractPlayerFragment( } private fun playerError(exception: Exception) { + val ctx = context ?: return when (exception) { is PlaybackException -> { 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 -> { showToast( 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 ) 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 -> { showToast( 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 ) 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 -> { showToast( 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 ) nextMirror() @@ -229,7 +230,7 @@ abstract class AbstractPlayerFragment( else -> { showToast( 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 ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt index 0a84668e..cedb711e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt @@ -42,8 +42,8 @@ class DownloadFileGenerator( return episodes[currentIndex].id } - override fun getCurrent(): Any { - return episodes[currentIndex] + override fun getCurrent(offset: Int): Any? { + return episodes.getOrNull(currentIndex + offset) } override suspend fun generateLinks( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt index 709911d8..b5edf225 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt @@ -6,6 +6,7 @@ import android.view.KeyEvent import androidx.appcompat.app.AppCompatActivity import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.AppUtils.getUri import com.lagradost.cloudstream3.utils.ExtractorUri @@ -72,8 +73,12 @@ class DownloadedPlayerActivity : AppCompatActivity() { "NULL" } - val realUri = AppUtils.getVideoContentUri(this, realPath) - val tryUri = realUri ?: uri + val tryUri = try { + AppUtils.getVideoContentUri(this, realPath) ?: uri + } catch (e: Exception) { + logError(e) + uri + } setContentView(R.layout.empty_layout) Log.i(DTAG, "navigating") diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt index eeb8db9e..255122bc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt @@ -352,7 +352,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } activity?.hideSystemUI() animateLayoutChanges() - player_pause_play.requestFocus() + player_pause_play?.requestFocus() } private fun toggleLock() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index b3f0a7a9..91b32b50 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -71,6 +71,15 @@ class GeneratorPlayer : FullScreenPlayer() { 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?, sameEpisode: Boolean) { if (link == null) return @@ -93,8 +102,7 @@ class GeneratorPlayer : FullScreenPlayer() { url, uri, startPosition = if (sameEpisode) null else { - if (isNextEpisode) 0L else (DataStoreHelper.getViewPos(viewModel.getId())?.position - ?: 0L) + if (isNextEpisode) 0L else getPos() }, currentSubs, ) @@ -272,11 +280,16 @@ class GeneratorPlayer : FullScreenPlayer() { init = init || if (subtitleIndex <= 0) { noSubtitles() } else { - setSubtitles(currentSubtitles[subtitleIndex - 1]) + currentSubtitles.getOrNull(subtitleIndex - 1)?.let { + setSubtitles(it) + true + } ?: false } } if (init) { - loadLink(sortedUrls[sourceIndex], true) + sortedUrls.getOrNull(sourceIndex)?.let { + loadLink(it, true) + } } sourceDialog.dismissSafe(activity) } @@ -304,11 +317,13 @@ class GeneratorPlayer : FullScreenPlayer() { override fun nextEpisode() { isNextEpisode = true + player.release() viewModel.loadLinksNext() } override fun prevEpisode() { isNextEpisode = true + player.release() viewModel.loadLinksPrev() } @@ -336,6 +351,7 @@ class GeneratorPlayer : FullScreenPlayer() { override fun playerPositionChanged(posDur: Pair) { val (position, duration) = posDur viewModel.getId()?.let { + println("SET VIEW ID: $it ($position/$duration)") DataStoreHelper.setViewPos(it, position, duration) } val percentage = position * 100L / duration diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt index 40f99372..dbc4ff76 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt @@ -13,7 +13,7 @@ interface IGenerator { fun goto(index: Int) 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 */ suspend fun generateLinks( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index e1a113fa..e29b10f1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.ui.player +import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -13,6 +14,10 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch class PlayerGeneratorViewModel : ViewModel() { + companion object { + val TAG = "PlayViewGen" + } + private var generator: IGenerator? = null private val _currentLinks = MutableLiveData>>(setOf()) @@ -34,6 +39,7 @@ class PlayerGeneratorViewModel : ViewModel() { } fun loadLinksPrev() { + Log.i(TAG, "loadLinksPrev") if (generator?.hasPrev() == true) { generator?.prev() loadLinks() @@ -41,6 +47,7 @@ class PlayerGeneratorViewModel : ViewModel() { } fun loadLinksNext() { + Log.i(TAG, "loadLinksNext") if (generator?.hasNext() == true) { generator?.next() loadLinks() @@ -52,11 +59,18 @@ class PlayerGeneratorViewModel : ViewModel() { } fun preLoadNextLinks() { + Log.i(TAG, "preLoadNextLinks") currentJob?.cancel() currentJob = viewModelScope.launch { if (generator?.hasCache == true && generator?.hasNext() == true) { 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? { return normalSafeApiCall { if (generator?.hasNext() == false) return@normalSafeApiCall null - generator?.next() - val next = generator?.getCurrent() - generator?.prev() - next + generator?.getCurrent(offset = 1) } } @@ -91,6 +102,7 @@ class PlayerGeneratorViewModel : ViewModel() { private var currentJob: Job? = null fun loadLinks(clearCache: Boolean = false, isCasting: Boolean = false) { + Log.i(TAG, "loadLinks") currentJob?.cancel() currentJob = viewModelScope.launch { val currentLinks = mutableSetOf>() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt index 62242b4e..2eb25644 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.ui.player +import android.util.Log import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.result.ResultEpisode @@ -8,7 +9,14 @@ import com.lagradost.cloudstream3.utils.ExtractorUri import kotlin.math.max import kotlin.math.min -class RepoLinkGenerator(private val episodes: List, private var currentIndex: Int = 0) : IGenerator { +class RepoLinkGenerator( + private val episodes: List, + private var currentIndex: Int = 0 +) : IGenerator { + companion object { + val TAG = "RepoLink" + } + override val hasCache = true override fun hasNext(): Boolean { @@ -20,16 +28,19 @@ class RepoLinkGenerator(private val episodes: List, private var c } override fun next() { + Log.i(TAG, "next") if (hasNext()) currentIndex++ } override fun prev() { + Log.i(TAG, "prev") if (hasPrev()) currentIndex-- } override fun goto(index: Int) { + Log.i(TAG, "goto $index") // clamps value currentIndex = min(episodes.size - 1, max(0, index)) } @@ -38,8 +49,8 @@ class RepoLinkGenerator(private val episodes: List, private var c return episodes[currentIndex].id } - override fun getCurrent(): Any { - return episodes[currentIndex] + override fun getCurrent(offset: Int): Any? { + return episodes.getOrNull(currentIndex + offset) } // 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, private var c isCasting: Boolean, callback: (Pair) -> Unit, subtitleCallback: (SubtitleData) -> Unit, - offset : Int, + offset: Int, ): Boolean { 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 currentSubsCache = if (clearCache) mutableSetOf() else subsCache[index].toMutableSet() @@ -76,7 +87,7 @@ class RepoLinkGenerator(private val episodes: List, private var c // this stops all execution if links are cached // no extra get requests - if(currentLinkCache.size > 0) { + if (currentLinkCache.size > 0) { return true } @@ -86,13 +97,13 @@ class RepoLinkGenerator(private val episodes: List, private var c isCasting, { file -> val correctFile = PlayerSubtitleHelper.getSubtitleData(file) - if(!currentSubsUrls.contains(correctFile.url)) { + if (!currentSubsUrls.contains(correctFile.url)) { currentSubsUrls.add(correctFile.url) // this part makes sure that all names are unique for UX var name = correctFile.name var count = 0 - while(currentSubsNames.contains(name)) { + while (currentSubsNames.contains(name)) { count++ name = "${correctFile.name} $count" } @@ -108,7 +119,7 @@ class RepoLinkGenerator(private val episodes: List, private var c } }, { link -> - if(!currentLinks.contains(link.url)) { + if (!currentLinks.contains(link.url)) { if (!currentLinkCache.contains(link)) { currentLinks.add(link.url) callback(Pair(link, null)) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt index 947a8c76..07bae455 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt @@ -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 activity.navigate(R.id.global_to_navigation_quick_search, Bundle().apply { 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 listLock.lock() (quick_search_master_recycler?.adapter as ParentItemAdapter?)?.apply { - items = list.map { ongoing -> + updateList(list.map { ongoing -> val ongoingList = HomePageList( ongoing.apiName, if (ongoing.data is Resource.Success) ongoing.data.value.filterSearchResponse() else ArrayList() ) ongoingList - } - notifyDataSetChanged() + }) } } catch (e: Exception) { logError(e) @@ -98,31 +101,38 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() { } } - val masterAdapter: RecyclerView.Adapter = ParentItemAdapter(listOf(), { callback -> - when (callback.action) { - SEARCH_ACTION_LOAD -> { - if (isMainApis) { - activity?.popCurrentPage() + val masterAdapter: RecyclerView.Adapter = + ParentItemAdapter(mutableListOf(), { callback -> + when (callback.action) { + SEARCH_ACTION_LOAD -> { + if (isMainApis) { + activity?.popCurrentPage() - SearchHelper.handleSearchClickCallback(activity, callback) - } else { - clickCallback?.invoke(callback) + SearchHelper.handleSearchClickCallback(activity, callback) + } else { + 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(androidx.appcompat.R.id.search_close_btn) - val searchMagIcon = quick_search.findViewById(androidx.appcompat.R.id.search_mag_icon) + val searchExitIcon = + quick_search.findViewById(androidx.appcompat.R.id.search_close_btn) + val searchMagIcon = + quick_search.findViewById(androidx.appcompat.R.id.search_mag_icon) searchMagIcon.scaleX = 0.65f searchMagIcon.scaleY = 0.65f quick_search.setOnQueryTextListener(object : SearchView.OnQueryTextListener { 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 { UIHelper.hideKeyboard(it) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index 3c71c90d..7c7c909c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -1132,7 +1132,7 @@ class ResultFragment : Fragment() { try { startActivity(i) } catch (e: Exception) { - e.printStackTrace() + logError(e) } } @@ -1394,7 +1394,7 @@ class ResultFragment : Fragment() { try { startActivity(i) } catch (e: Exception) { - e.printStackTrace() + logError(e) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt index 97b415d5..4dc7d09f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt @@ -145,7 +145,7 @@ class SearchFragment : Fragment() { search_filter.setOnClickListener { searchView -> searchView?.context?.let { ctx -> - val validAPIs = ctx.filterProviderByPreferredMedia() + val validAPIs = ctx.filterProviderByPreferredMedia(hasHomePageIsRequired = false) var currentValidApis = listOf() val currentSelectedApis = if (selectedApis.isEmpty()) validAPIs.map { it.name } .toMutableSet() else selectedApis @@ -213,7 +213,7 @@ class SearchFragment : Fragment() { fun updateList() { arrayAdapter.clear() currentValidApis = validAPIs.filter { api -> - api.hasMainPage && api.supportedTypes.any { + api.supportedTypes.any { selectedSearchTypes.contains(it) } }.sortedBy { it.name } @@ -224,7 +224,7 @@ class SearchFragment : Fragment() { listView?.setItemChecked(index, currentSelectedApis.contains(api)) } - arrayAdapter.notifyDataSetChanged() + //arrayAdapter.notifyDataSetChanged() arrayAdapter.addAll(names) arrayAdapter.notifyDataSetChanged() } @@ -373,14 +373,16 @@ class SearchFragment : Fragment() { // https://stackoverflow.com/questions/6866238/concurrent-modification-exception-adding-to-an-arraylist listLock.lock() (search_master_recycler?.adapter as ParentItemAdapter?)?.apply { - items = list.map { ongoing -> + val newItems = list.map { ongoing -> val ongoingList = HomePageList( ongoing.apiName, if (ongoing.data is Resource.Success) ongoing.data.value.filterSearchResponse() else ArrayList() ) ongoingList } - notifyDataSetChanged() + updateList(newItems) + + //notifyDataSetChanged() } } catch (e: Exception) { logError(e) @@ -399,7 +401,7 @@ class SearchFragment : Fragment() { //main_search.onActionViewExpanded()*/ val masterAdapter: RecyclerView.Adapter = - ParentItemAdapter(listOf(), { callback -> + ParentItemAdapter(mutableListOf(), { callback -> SearchHelper.handleSearchClickCallback(activity, callback) }, { item -> activity?.loadHomepageList(item) diff --git a/app/src/test/java/com/lagradost/cloudstream3/ProviderTests.kt b/app/src/test/java/com/lagradost/cloudstream3/ProviderTests.kt index 6c374e34..be95d2be 100644 --- a/app/src/test/java/com/lagradost/cloudstream3/ProviderTests.kt +++ b/app/src/test/java/com/lagradost/cloudstream3/ProviderTests.kt @@ -3,6 +3,7 @@ package com.lagradost.cloudstream3 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 @@ -23,7 +24,10 @@ class ProviderTests { "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.length > 4) + Assert.assertTrue( + "Api ${api.name} returns link with invalid url", + link.url.length > 4 + ) linksLoaded++ } if (success) { @@ -69,9 +73,17 @@ class ProviderTests { try { var validResults = false 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 - 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( "Api ${api.name} on load does not contain any of the supportedTypes", api.supportedTypes.contains(load.type) @@ -137,33 +149,41 @@ class ProviderTests { 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()) + 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() + ) } } @Test fun providerCorrectHomepage() { - getAllProviders().apmap { api -> - if (api.hasMainPage) { - try { - val homepage = api.getMainPage() - when { - homepage == null -> { - Assert.fail("Homepage provider ${api.name} did not correctly load homepage!") + runBlocking { + getAllProviders().apmap { api -> + if (api.hasMainPage) { + try { + val homepage = api.getMainPage() + when { + 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() -> { - 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!") + } catch (e: Exception) { + if (e.cause is NotImplementedError) { + Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented") } + 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 suspend fun providerCorrect() { - val invalidProvider = ArrayList>() + val invalidProvider = ArrayList>() val providers = getAllProviders() providers.apmap { api -> try { @@ -189,7 +209,7 @@ class ProviderTests { } } catch (e: Exception) { logError(e) - invalidProvider.add(Pair(api,e)) + invalidProvider.add(Pair(api, e)) } }