From 9ea7674a0f4e2882af0df4c23464809e227afaad Mon Sep 17 00:00:00 2001 From: Osten <11805592+LagradOst@users.noreply.github.com> Date: Fri, 2 Feb 2024 22:18:54 +0100 Subject: [PATCH 1/7] Update MainAPI.kt --- .../java/com/lagradost/cloudstream3/MainAPI.kt | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index a4edec33..7a25b738 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -1587,8 +1587,15 @@ data class AnimeLoadResponse( } override fun getTotalEpisodeIndex(episode: Int, season: Int): Int { + val displayMap = this.seasonNames?.associate { it.season to it.displaySeason } ?: emptyMap() + return this.episodes.maxOf { (_, episodes) -> - episodes.count { ((it.season ?: Int.MIN_VALUE) < season) && it.season != 0 } + episodes.count { episodeData -> + // Prioritize display season as actual season may be something random to fit multiple seasons into one. + val episodeSeason = displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE + // Count all episodes from season 1 to below the current season. + episodeSeason in 1.. + // Prioritize display season as actual season may be something random to fit multiple seasons into one. + val episodeSeason = displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE + // Count all episodes from season 1 to below the current season. + episodeSeason in 1.. Date: Tue, 6 Feb 2024 22:27:35 +0000 Subject: [PATCH 2/7] Add SubScene (#923) * Lower targetSdk to get all installed packages * Update sdk version * Let's not be too radical * Many fixes * Revert targetSdk * Make account homepage persistent * Add SubScene and change subtitle API Co-authored-by: Aymanbest <51868001+aymanbest@users.noreply.github.com> * Fix file deletion --------- Co-authored-by: Aymanbest <51868001+aymanbest@users.noreply.github.com> --- .../lagradost/cloudstream3/MainActivity.kt | 32 ++++- .../subtitles/AbstractSubProvider.kt | 104 +++++++++++++++ .../syncproviders/AccountManager.kt | 5 +- .../providers/IndexSubtitleApi.kt | 82 ++++++------ .../syncproviders/providers/SubScene.kt | 118 ++++++++++++++++++ .../cloudstream3/ui/player/CS3IPlayer.kt | 3 +- .../cloudstream3/ui/player/GeneratorPlayer.kt | 48 ++++--- .../cloudstream3/utils/InAppUpdater.kt | 3 +- .../utils/PackageInstallerService.kt | 3 +- 9 files changed, 333 insertions(+), 65 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubScene.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 63183d73..afb2f76f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -61,6 +61,7 @@ import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings import com.lagradost.cloudstream3.APIHolder.initAll import com.lagradost.cloudstream3.APIHolder.updateHasTrailers +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.loadThemes @@ -287,9 +288,27 @@ var app = Requests(responseParser = object : ResponseParser { class MainActivity : AppCompatActivity(), ColorPickerDialogListener { companion object { const val TAG = "MAINACT" - const val ANIMATED_OUTLINE : Boolean = false + const val ANIMATED_OUTLINE: Boolean = false var lastError: String? = null + private const val FILE_DELETE_KEY = "FILES_TO_DELETE_KEY" + + /** + * Transient files to delete on application exit. + * Deletes files on onDestroy(). + */ + private var filesToDelete: Set + // This needs to be persistent because the application may exit without calling onDestroy. + get() = getKey>(FILE_DELETE_KEY) ?: setOf() + private set(value) = setKey(FILE_DELETE_KEY, value) + + /** + * Add file to delete on Exit. + */ + fun deleteFileOnExit(file: File) { + filesToDelete = filesToDelete + file.path + } + /** * Setting this will automatically enter the query in the search * next time the search fragment is opened. @@ -676,6 +695,15 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } override fun onDestroy() { + filesToDelete.forEach { path -> + val result = File(path).deleteRecursively() + if (result) { + Log.d(TAG, "Deleted temporary file: $path") + } else { + Log.d(TAG, "Failed to delete temporary file: $path") + } + } + filesToDelete = setOf() val broadcastIntent = Intent() broadcastIntent.action = "restart_service" broadcastIntent.setClass(this, VideoDownloadRestartReceiver::class.java) @@ -1654,7 +1682,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { // this ensures that no unnecessary space is taken loadCache() File(filesDir, "exoplayer").deleteRecursively() // old cache - File(cacheDir, "exoplayer").deleteOnExit() // current cache + deleteFileOnExit(File(cacheDir, "exoplayer")) // current cache } catch (e: Exception) { logError(e) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt index 77a1b0b5..857fba11 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt @@ -1,11 +1,23 @@ package com.lagradost.cloudstream3.subtitles import androidx.annotation.WorkerThread +import androidx.core.net.toUri +import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit +import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleEntity import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch import com.lagradost.cloudstream3.syncproviders.AuthAPI +import com.lagradost.cloudstream3.ui.player.SubtitleOrigin +import okio.BufferedSource +import okio.buffer +import okio.sink +import okio.source +import java.io.File +import java.util.zip.ZipInputStream interface AbstractSubProvider { + val idPrefix: String + @WorkerThread suspend fun search(query: SubtitleSearch): List? { throw NotImplementedError() @@ -15,6 +27,98 @@ interface AbstractSubProvider { suspend fun load(data: SubtitleEntity): String? { throw NotImplementedError() } + + @WorkerThread + suspend fun SubtitleResource.getResources(data: SubtitleEntity) { + this.addUrl(load(data)) + } + + @WorkerThread + suspend fun getResource(data: SubtitleEntity): SubtitleResource { + return SubtitleResource().apply { + this.getResources(data) + } + } +} + +/** + * A builder for subtitle files. + * @see addUrl + * @see addFile + */ +class SubtitleResource { + fun downloadFile(source: BufferedSource): File { + val file = File.createTempFile("temp-subtitle", ".tmp").apply { + deleteFileOnExit(this) + } + val sink = file.sink().buffer() + sink.writeAll(source) + sink.close() + source.close() + + return file + } + + fun unzip(file: File): List> { + val entries = mutableListOf>() + + ZipInputStream(file.inputStream()).use { zipInputStream -> + var zipEntry = zipInputStream.nextEntry + + while (zipEntry != null) { + val tempFile = File.createTempFile("unzipped-subtitle", ".tmp").apply { + deleteFileOnExit(this) + } + entries.add(zipEntry.name to tempFile) + + tempFile.sink().buffer().use { buffer -> + buffer.writeAll(zipInputStream.source()) + } + + zipEntry = zipInputStream.nextEntry + } + } + return entries + } + + data class SingleSubtitleResource( + val name: String?, + val url: String, + val origin: SubtitleOrigin + ) + + private var resources: MutableList = mutableListOf() + + fun getSubtitles(): List { + return resources.toList() + } + + fun addUrl(url: String?, name: String? = null) { + if (url == null) return + this.resources.add( + SingleSubtitleResource(name, url, SubtitleOrigin.URL) + ) + } + + fun addFile(file: File, name: String? = null) { + this.resources.add( + SingleSubtitleResource(name, file.toUri().toString(), SubtitleOrigin.DOWNLOADED_FILE) + ) + deleteFileOnExit(file) + } + + suspend fun addZipUrl( + url: String, + nameGenerator: (String, File) -> String? = { _, _ -> null } + ) { + val source = app.get(url).okhttpResponse.body.source() + val zip = downloadFile(source) + val realFiles = unzip(zip) + zip.deleteRecursively() + realFiles.forEach { (name, subtitleFile) -> + addFile(subtitleFile, nameGenerator(name, subtitleFile)) + } + } } interface AbstractSubApi : AbstractSubProvider, AuthAPI \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt index 8bf8dffa..bae8a5df 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.syncproviders import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.syncproviders.providers.SubScene import com.lagradost.cloudstream3.syncproviders.providers.* import java.util.concurrent.TimeUnit @@ -14,6 +15,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val simklApi = SimklApi(0) val indexSubtitlesApi = IndexSubtitleApi() val addic7ed = Addic7ed() + val subScene = SubScene() val localListApi = LocalList() // used to login via app intent @@ -41,7 +43,8 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { get() = listOf( openSubtitlesApi, indexSubtitlesApi, // they got anti scraping measures in place :( - addic7ed + addic7ed, + subScene ) const val appString = "cloudstreamapp" diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt index 668d10bd..1adecce9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt @@ -23,6 +23,47 @@ class IndexSubtitleApi : AbstractSubApi { companion object { const val host = "https://indexsubtitle.com" const val TAG = "INDEXSUBS" + + fun getOrdinal(num: Int?): String? { + return when (num) { + 1 -> "First" + 2 -> "Second" + 3 -> "Third" + 4 -> "Fourth" + 5 -> "Fifth" + 6 -> "Sixth" + 7 -> "Seventh" + 8 -> "Eighth" + 9 -> "Ninth" + 10 -> "Tenth" + 11 -> "Eleventh" + 12 -> "Twelfth" + 13 -> "Thirteenth" + 14 -> "Fourteenth" + 15 -> "Fifteenth" + 16 -> "Sixteenth" + 17 -> "Seventeenth" + 18 -> "Eighteenth" + 19 -> "Nineteenth" + 20 -> "Twentieth" + 21 -> "Twenty-First" + 22 -> "Twenty-Second" + 23 -> "Twenty-Third" + 24 -> "Twenty-Fourth" + 25 -> "Twenty-Fifth" + 26 -> "Twenty-Sixth" + 27 -> "Twenty-Seventh" + 28 -> "Twenty-Eighth" + 29 -> "Twenty-Ninth" + 30 -> "Thirtieth" + 31 -> "Thirty-First" + 32 -> "Thirty-Second" + 33 -> "Thirty-Third" + 34 -> "Thirty-Fourth" + 35 -> "Thirty-Fifth" + else -> null + } + } } private fun fixUrl(url: String): String { @@ -44,47 +85,6 @@ class IndexSubtitleApi : AbstractSubApi { } } - private fun getOrdinal(num: Int?): String? { - return when (num) { - 1 -> "First" - 2 -> "Second" - 3 -> "Third" - 4 -> "Fourth" - 5 -> "Fifth" - 6 -> "Sixth" - 7 -> "Seventh" - 8 -> "Eighth" - 9 -> "Ninth" - 10 -> "Tenth" - 11 -> "Eleventh" - 12 -> "Twelfth" - 13 -> "Thirteenth" - 14 -> "Fourteenth" - 15 -> "Fifteenth" - 16 -> "Sixteenth" - 17 -> "Seventeenth" - 18 -> "Eighteenth" - 19 -> "Nineteenth" - 20 -> "Twentieth" - 21 -> "Twenty-First" - 22 -> "Twenty-Second" - 23 -> "Twenty-Third" - 24 -> "Twenty-Fourth" - 25 -> "Twenty-Fifth" - 26 -> "Twenty-Sixth" - 27 -> "Twenty-Seventh" - 28 -> "Twenty-Eighth" - 29 -> "Twenty-Ninth" - 30 -> "Thirtieth" - 31 -> "Thirty-First" - 32 -> "Thirty-Second" - 33 -> "Thirty-Third" - 34 -> "Thirty-Fourth" - 35 -> "Thirty-Fifth" - else -> null - } - } - private fun isRightEps(text: String, seasonNum: Int?, epNum: Int?): Boolean { val FILTER_EPS_REGEX = Regex("(?i)((Chapter\\s?0?${epNum})|((Season)?\\s?0?${seasonNum}?\\s?(Episode)\\s?0?${epNum}[^0-9]))|(?i)((S?0?${seasonNum}?E0?${epNum}[^0-9])|(0?${seasonNum}[a-z]0?${epNum}[^0-9]))") diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubScene.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubScene.kt new file mode 100644 index 00000000..fbe05026 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubScene.kt @@ -0,0 +1,118 @@ +package com.lagradost.cloudstream3.syncproviders.providers + +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mvvm.debugPrint +import com.lagradost.cloudstream3.subtitles.AbstractSubProvider +import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities +import com.lagradost.cloudstream3.subtitles.SubtitleResource +import com.lagradost.cloudstream3.syncproviders.providers.IndexSubtitleApi.Companion.getOrdinal +import com.lagradost.cloudstream3.utils.SubtitleHelper + +class SubScene : AbstractSubProvider { + val mainUrl = "https://subscene.com" + val name = "Subscene" + override val idPrefix = "subscene" + + override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List? { + val seasonName = + query.seasonNumber?.let { number -> + // Need to translate "7" to "Seventh Season" + getOrdinal(number)?.let { words -> " - $words Season" } + } ?: "" + + val fullQuery = query.query + seasonName + + val doc = app.post( + "$mainUrl/subtitles/searchbytitle", + data = mapOf("query" to fullQuery, "l" to "") + ).document + + return doc.select("div.title a").map { element -> + val href = "$mainUrl${element.attr("href")}" + val title = element.text() + + AbstractSubtitleEntities.SubtitleEntity( + idPrefix = idPrefix, + name = title, + source = name, + data = href, + lang = query.lang ?: "en", + epNumber = query.epNumber + ) + }.distinctBy { it.data } + } + + override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) { + val resultDoc = app.get(data.data).document + val queryLanguage = SubtitleHelper.fromTwoLettersToLanguage(data.lang) ?: "English" + + val results = resultDoc.select("table tbody tr").mapNotNull { element -> + val anchor = element.select("a") + val href = anchor.attr("href") ?: return@mapNotNull null + val fixedHref = "$mainUrl${href}" + val spans = anchor.select("span") + val language = spans.firstOrNull()?.text() + val title = spans.getOrNull(1)?.text() + val isPositive = anchor.select("span.positive-icon").isNotEmpty() + + TableElement(title, language, fixedHref, isPositive) + }.sortedBy { + it.getScore(queryLanguage, data.epNumber) + } + + debugPrint { "$name found subtitles: ${results.takeLast(3)}" } + // Last = highest score + val selectedResult = results.lastOrNull() ?: return + + val subtitleDocument = app.get(selectedResult.href).document + val subtitleDownloadUrl = + "$mainUrl${subtitleDocument.select("div.download a").attr("href")}" + + this.addZipUrl(subtitleDownloadUrl) { name, _ -> + name + } + } + + /** + * Class to manage the various different subtitle results and rank them. + */ + data class TableElement( + val title: String?, + val language: String?, + val href: String, + val isPositive: Boolean + ) { + private fun matchesLanguage(other: String): Boolean { + return language != null && (language.contains(other, ignoreCase = true) || + other.contains(language, ignoreCase = true)) + } + + /** + * Scores in this order: + * Preferred Language > Episode number > Positive rating > English Language + */ + fun getScore(queryLanguage: String, episodeNum: Int?): Int { + var score = 0 + if (this.matchesLanguage(queryLanguage)) { + score += 8 + } + // Matches Episode 7 using "E07" with any number of leading zeroes + if (episodeNum != null && title != null && title.contains( + Regex( + """E0*${episodeNum}""", + RegexOption.IGNORE_CASE + ) + ) + ) { + score += 4 + } + if (isPositive) { + score += 2 + } + if (this.matchesLanguage("English")) { + score += 1 + } + return score + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 031f14fe..210bfdca 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -50,6 +50,7 @@ import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.debugAssert @@ -657,7 +658,7 @@ class CS3IPlayer : IPlayer { SimpleCache( File( context.cacheDir, "exoplayer" - ).also { it.deleteOnExit() }, // Ensures always fresh file + ).also { deleteFileOnExit(it) }, // Ensures always fresh file LeastRecentlyUsedCacheEvictor(cacheSize), databaseProvider ) 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 ceb4728d..01069f66 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 @@ -30,6 +30,7 @@ import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding import com.lagradost.cloudstream3.databinding.PlayerSelectSourceAndSubsBinding import com.lagradost.cloudstream3.databinding.PlayerSelectTracksBinding import com.lagradost.cloudstream3.mvvm.* +import com.lagradost.cloudstream3.subtitles.AbstractSubApi import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subtitleProviders import com.lagradost.cloudstream3.ui.player.CS3IPlayer.Companion.preferredAudioTrackLanguage @@ -69,7 +70,10 @@ class GeneratorPlayer : FullScreenPlayer() { } val subsProviders - get() = subtitleProviders.filter { !it.requiresLogin || it.loginInfo() != null } + get() = subtitleProviders.filter { provider -> + (provider as? AbstractSubApi)?.let { !it.requiresLogin || it.loginInfo() != null } + ?: true + } val subsProvidersIsActive get() = subsProviders.isNotEmpty() } @@ -147,7 +151,7 @@ class GeneratorPlayer : FullScreenPlayer() { } override fun playerStatusChanged() { - if(player.getIsPlaying()){ + if (player.getIsPlaying()) { viewModel.forceClearCache = false } } @@ -473,17 +477,21 @@ class GeneratorPlayer : FullScreenPlayer() { currentSubtitle?.let { currentSubtitle -> providers.firstOrNull { it.idPrefix == currentSubtitle.idPrefix }?.let { api -> ioSafe { - val url = api.load(currentSubtitle) ?: return@ioSafe - val subtitle = SubtitleData( - name = getName(currentSubtitle, true), - url = url, - origin = SubtitleOrigin.URL, - mimeType = url.toSubtitleMimeType(), - headers = currentSubtitle.headers, - currentSubtitle.lang - ) - runOnMainThread { - addAndSelectSubtitles(subtitle) + val subtitles = + api.getResource(currentSubtitle).getSubtitles().map { resource -> + SubtitleData( + name = resource.name ?: getName(currentSubtitle, true), + url = resource.url, + origin = resource.origin, + mimeType = resource.url.toSubtitleMimeType(), + headers = currentSubtitle.headers, + currentSubtitle.lang + ) + } + if (subtitles.isNotEmpty()) { + runOnMainThread { + addAndSelectSubtitles(*subtitles.toTypedArray()) + } } } } @@ -521,7 +529,11 @@ class GeneratorPlayer : FullScreenPlayer() { } } - private fun addAndSelectSubtitles(subtitleData: SubtitleData) { + private fun addAndSelectSubtitles( + vararg subtitleData: SubtitleData + ) { + if (subtitleData.isEmpty()) return + val selectedSubtitle = subtitleData.first() val ctx = context ?: return val subs = currentSubs + subtitleData @@ -533,13 +545,13 @@ class GeneratorPlayer : FullScreenPlayer() { player.saveData() player.reloadPlayer(ctx) - setSubtitles(subtitleData) - viewModel.addSubtitles(setOf(subtitleData)) + setSubtitles(selectedSubtitle) + viewModel.addSubtitles(subtitleData.toSet()) selectSourceDialog?.dismissSafe() showToast( - String.format(ctx.getString(R.string.player_loaded_subtitles), subtitleData.name), + String.format(ctx.getString(R.string.player_loaded_subtitles), selectedSubtitle.name), Toast.LENGTH_LONG ) } @@ -919,7 +931,7 @@ class GeneratorPlayer : FullScreenPlayer() { override fun playerError(exception: Throwable) { Log.i(TAG, "playerError = $currentSelectedLink") - if(!hasNextMirror()){ + if (!hasNextMirror()) { viewModel.forceClearCache = true } super.playerError(exception) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt index 28c18fec..d9a31b4e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt @@ -23,6 +23,7 @@ import okio.buffer import okio.sink import java.io.File import android.text.TextUtils +import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import java.io.BufferedReader import java.io.IOException @@ -213,7 +214,7 @@ class InAppUpdater { this.cacheDir.listFiles()?.filter { it.name.startsWith(appUpdateName) && it.extension == appUpdateSuffix }?.forEach { - it.deleteOnExit() + deleteFileOnExit(it) } val downloadedFile = File.createTempFile(appUpdateName, ".$appUpdateSuffix") diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstallerService.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstallerService.kt index 7ff7b067..322547f4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstallerService.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstallerService.kt @@ -12,6 +12,7 @@ import android.os.IBinder import android.util.Log import androidx.core.app.NotificationCompat import com.lagradost.cloudstream3.MainActivity +import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel @@ -75,7 +76,7 @@ class PackageInstallerService : Service() { this@PackageInstallerService.cacheDir.listFiles()?.filter { it.name.startsWith(appUpdateName) && it.extension == appUpdateSuffix }?.forEach { - it.deleteOnExit() + deleteFileOnExit(it) } } From eea6e13346a7e8ae3dfa253a2725c8e0ed675313 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Thu, 15 Feb 2024 22:40:44 +0200 Subject: [PATCH 3/7] Make Rating non-focusable (Old API) (#935) --- app/src/main/res/layout/fragment_result_tv.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index 7b5fc291..b8f5b5ca 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -399,6 +399,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit android:id="@+id/result_meta_content_rating" android:layout_gravity="center_vertical" style="@style/SmallWhiteButton" + android:focusable="false" tools:text="PG-13" /> Date: Thu, 15 Feb 2024 20:41:34 +0000 Subject: [PATCH 4/7] Fixed Chillx subtitles (#931) * Fixed subtitles tracks for Chillx.kt * Update Chillx.kt --- .../cloudstream3/extractors/Chillx.kt | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt index 68d543d3..f03a5525 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt @@ -49,8 +49,23 @@ open class Chillx : ExtractorApi() { val decrypt = cryptoAESHandler(master ?: return, getKey().toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1) - val tracks = Regex("""tracks:\s*\[(.+)]""").find(decrypt)?.groupValues?.get(1) + val subtitles = Regex("""subtitle"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1) + val subtitlePattern = """\[(.*?)\](https?://[^\s,]+)""".toRegex() + val matches = subtitlePattern.findAll(subtitles ?: "") + val languageUrlPairs = matches.map { matchResult -> + val (language, url) = matchResult.destructured + decodeUnicodeEscape(language) to url + }.toList() + + languageUrlPairs.forEach{ (name, file) -> + subtitleCallback.invoke( + SubtitleFile( + name, + file + ) + ) + } // required val headers = mapOf( "Accept" to "*/*", @@ -67,18 +82,15 @@ open class Chillx : ExtractorApi() { "$mainUrl/", headers = headers ).forEach(callback) - - AppUtils.tryParseJson>("[$tracks]") - ?.filter { it.kind == "captions" }?.map { track -> - subtitleCallback.invoke( - SubtitleFile( - track.label ?: "", - track.file ?: return@map null - ) - ) - } } - + + private fun decodeUnicodeEscape(input: String): String { + val regex = Regex("u([0-9a-fA-F]{4})") + return regex.replace(input) { + it.groupValues[1].toInt(16).toChar().toString() + } + } + suspend fun getKey() = key ?: fetchKey().also { key = it } private suspend fun fetchKey(): String { From d1db4c33707322363639d2a9dc829fec4a653f30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Faruk=20Sancak?= Date: Thu, 15 Feb 2024 23:42:11 +0300 Subject: [PATCH 5/7] Extractor: Added PlayRu (#930) --- .../cloudstream3/extractors/HotlingerExtractor.kt | 10 ++++++++++ .../com/lagradost/cloudstream3/utils/ExtractorApi.kt | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt index 7389db68..b557a53e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt @@ -10,4 +10,14 @@ class Hotlinger : ContentX() { class FourCX : ContentX() { override var name = "FourCX" override var mainUrl = "https://four.contentx.me" +} + +class PlayRu : ContentX() { + override var name = "PlayRu" + override var mainUrl = "https://playru.net" +} + +class FourPlayRu : ContentX() { + override var name = "FourPlayRu" + override var mainUrl = "https://four.playru.net" } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 4f9c5e8c..637f65b9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -106,6 +106,8 @@ import com.lagradost.cloudstream3.extractors.ContentX import com.lagradost.cloudstream3.extractors.EmturbovidExtractor import com.lagradost.cloudstream3.extractors.Hotlinger import com.lagradost.cloudstream3.extractors.FourCX +import com.lagradost.cloudstream3.extractors.PlayRu +import com.lagradost.cloudstream3.extractors.FourPlayRu import com.lagradost.cloudstream3.extractors.HDMomPlayer import com.lagradost.cloudstream3.extractors.HDPlayerSystem import com.lagradost.cloudstream3.extractors.VideoSeyred @@ -704,6 +706,8 @@ val extractorApis: MutableList = arrayListOf( ContentX(), Hotlinger(), FourCX(), + PlayRu(), + FourPlayRu(), HDMomPlayer(), HDPlayerSystem(), VideoSeyred(), From 012d38398ec71389d59d7ffe8860a2bf3061e50a Mon Sep 17 00:00:00 2001 From: Sofie <117321707+Sofie99@users.noreply.github.com> Date: Fri, 16 Feb 2024 03:42:47 +0700 Subject: [PATCH 6/7] fix Acefile & Gofile (#926) --- .../cloudstream3/extractors/Acefile.kt | 24 +++++++++---------- .../cloudstream3/extractors/Gofile.kt | 4 ++-- .../cloudstream3/extractors/Rabbitstream.kt | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Acefile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Acefile.kt index c3af8bee..3f910cd4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Acefile.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Acefile.kt @@ -2,7 +2,6 @@ package com.lagradost.cloudstream3.extractors import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.base64Decode import com.lagradost.cloudstream3.utils.* open class Acefile : ExtractorApi() { @@ -16,22 +15,19 @@ open class Acefile : ExtractorApi() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val script = getAndUnpack(app.get(url).text) - val id = script.substringAfter("\"code\":\"").substringBefore("\",") - val doc = app.get("https://drive.google.com/uc?id=${base64Decode(id)}&export=download").document - val form = doc.select("form#download-form").attr("action") - val uc = doc.select("input#uc-download-link").attr("value") - val video = app.post( - form, data = mapOf( - "uc-download-link" to uc - ) - ).url + val id = "/(?:d|download|player|f|file)/(\\w+)".toRegex().find(url)?.groupValues?.get(1) + val script = getAndUnpack(app.get("$mainUrl/player/${id ?: return}").text) + val service = """service\s*=\s*['"]([^'"]+)""".toRegex().find(script)?.groupValues?.get(1) + val serverUrl = """['"](\S+check&id\S+?)['"]""".toRegex().find(script)?.groupValues?.get(1) + ?.replace("\"+service+\"", service ?: return) + + val video = app.get(serverUrl ?: return, referer = "$mainUrl/").parsedSafe()?.data callback.invoke( ExtractorLink( this.name, this.name, - video, + video ?: return, "", Qualities.Unknown.value, INFER_TYPE @@ -40,4 +36,8 @@ open class Acefile : ExtractorApi() { } + data class Source( + val data: String? = null, + ) + } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt index eaf9c65f..8d78c1a4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt @@ -22,9 +22,9 @@ open class Gofile : ExtractorApi() { val id = Regex("/(?:\\?c=|d/)([\\da-zA-Z-]+)").find(url)?.groupValues?.get(1) val token = app.get("$mainApi/createAccount").parsedSafe()?.data?.get("token") val websiteToken = app.get("$mainUrl/dist/js/alljs.js").text.let { - Regex("websiteToken\\s*=\\s*\"([^\"]+)").find(it)?.groupValues?.get(1) + Regex("fetchData.wt\\s*=\\s*\"([^\"]+)").find(it)?.groupValues?.get(1) } - app.get("$mainApi/getContent?contentId=$id&token=$token&websiteToken=$websiteToken") + app.get("$mainApi/getContent?contentId=$id&token=$token&wt=$websiteToken") .parsedSafe()?.data?.contents?.forEach { callback.invoke( ExtractorLink( diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt index d5b52dd7..74009b70 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt @@ -47,7 +47,7 @@ open class Rabbitstream : ExtractorApi() { val response = app.get( "$mainUrl/$embed/getSources?id=$id", - referer = mainUrl, + referer = url, headers = mapOf("X-Requested-With" to "XMLHttpRequest") ) @@ -72,7 +72,7 @@ open class Rabbitstream : ExtractorApi() { ).forEach(callback) } - decryptedSources?.tracks?.map { track -> + decryptedSources?.tracks?.filter { it?.kind == "captions" }?.map { track -> subtitleCallback.invoke( SubtitleFile( track?.label ?: "", From 09779b4ee0aefd13292dd2ee37f5c9fe50f8928b Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Fri, 16 Feb 2024 02:15:34 +0530 Subject: [PATCH 7/7] chore: add tooltips on results toolbar and rename speed mode (#925) * add tooltips on results toolbar and better summaries, rename speed mode * remove redundant space --- .../main/res/layout/fragment_result_swipe.xml | 12 +++---- app/src/main/res/values/strings.xml | 31 ++++++++++--------- app/src/main/res/xml/settings_player.xml | 2 +- app/src/main/res/xml/settings_providers.xml | 3 +- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/app/src/main/res/layout/fragment_result_swipe.xml b/app/src/main/res/layout/fragment_result_swipe.xml index eb2653d0..bb8a5c10 100644 --- a/app/src/main/res/layout/fragment_result_swipe.xml +++ b/app/src/main/res/layout/fragment_result_swipe.xml @@ -83,7 +83,7 @@ android:layout_height="25dp" android:layout_margin="5dp" android:elevation="10dp" - + android:tooltipText="@string/subscribe_tooltip" android:background="?android:attr/selectableItemBackgroundBorderless" android:src="@drawable/baseline_notifications_none_24" android:layout_gravity="end|center_vertical" @@ -100,7 +100,7 @@ android:layout_height="25dp" android:layout_margin="5dp" android:elevation="10dp" - + android:tooltipText="@string/action_add_to_favorites" android:background="?android:attr/selectableItemBackgroundBorderless" android:src="@drawable/ic_baseline_favorite_border_24" android:layout_gravity="end|center_vertical" @@ -117,7 +117,7 @@ android:layout_height="25dp" android:layout_margin="5dp" android:elevation="10dp" - + android:tooltipText="@string/result_share" android:background="?android:attr/selectableItemBackgroundBorderless" android:src="@drawable/ic_outline_share_24" android:layout_gravity="end|center_vertical" @@ -135,7 +135,7 @@ android:layout_height="25dp" android:layout_margin="5dp" android:elevation="10dp" - + android:tooltipText="@string/result_open_in_browser" android:background="?android:attr/selectableItemBackgroundBorderless" android:src="@drawable/ic_baseline_public_24" android:layout_gravity="end|center_vertical" @@ -153,7 +153,7 @@ android:layout_height="30dp" android:layout_margin="5dp" android:elevation="10dp" - + android:tooltipText="@string/result_search_tooltip" android:background="?android:attr/selectableItemBackgroundBorderless" android:src="@drawable/search_icon" android:layout_gravity="end|center_vertical" @@ -171,7 +171,7 @@ android:layout_height="25dp" android:layout_margin="5dp" android:elevation="10dp" - + android:tooltipText="@string/recommendations_tooltip" android:background="?android:attr/selectableItemBackgroundBorderless" android:src="@drawable/baseline_list_alt_24" android:layout_gravity="end|center_vertical" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eb0dc3a8..e68aea7b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -148,7 +148,7 @@ Download Done %s - %s Update Started - Stream + Network stream Error Loading Links Links Reloaded Internal Storage @@ -174,6 +174,9 @@ Clear Save Title copied! + New episode notification + Search in other extensions + Show recommendations Player Speed Subtitle Settings Text Color @@ -213,8 +216,8 @@ Player subtitles settings Chromecast Subtitles Chromecast subtitles settings - Eigengravy Mode - Adds a speed option in the player + Playback speed + Adds a speed option in the player Swipe to seek Swipe from side to side to control your position in a video Swipe to change settings @@ -391,9 +394,9 @@ Causes problems if set too high on devices with low storage space, such as Android TV. DNS over HTTPS Useful for bypassing ISP blocks - raw.githubusercontent.com Proxy + GitHub Proxy Could not reach GitHub. Turning on jsDelivr proxy… - Bypasses blocking of GitHub using jsDelivr. May cause updates to be delayed by few days. + Bypass blocking of raw github URLs using jsDelivr. May cause updates to be delayed by few days. Clone site Remove site Add a clone of an existing site, with a different URL @@ -439,13 +442,15 @@ General Random Button Show random button on Homepage and Library - Provider languages + Extension languages App Layout Preferred media - Enable NSFW on supported providers + Enable NSFW on supported Extensions Subtitle encoding Providers Provider test + Test all Extensions + This Test is meant for developers only and does not verifies or denies working of any extension. Layout Auto TV layout @@ -462,11 +467,11 @@ opensubtitles_key nginx_key password123 - MyCoolUsername + Username hello@world.com 127.0.0.1 - MyCoolSite - example.com + NewSiteName + https://example.com Language code (en)