Merge remote-tracking branch 'origin/master' into dialog2

This commit is contained in:
IndusAryan 2024-06-17 15:25:09 +05:30
commit 7f541a89aa
29 changed files with 403 additions and 382 deletions

View file

@ -164,7 +164,7 @@ dependencies {
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
// Android Core & Lifecycle
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.navigation:navigation-ui-ktx:2.7.7")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0")
@ -174,7 +174,7 @@ dependencies {
// Design & UI
implementation("jp.wasabeef:glide-transformations:4.3.0")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("com.google.android.material:material:1.11.0")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
@ -185,7 +185,7 @@ dependencies {
// For KSP -> Official Annotation Processors are Not Yet Supported for KSP
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0")
implementation("com.google.guava:guava:32.1.3-android")
implementation("com.google.guava:guava:33.2.0-android")
implementation("dev.zacsweers.autoservice:auto-service-ksp:1.1.0")
// Media 3 (ExoPlayer)
@ -202,7 +202,7 @@ dependencies {
// PlayBack
implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker
implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs
implementation("com.github.TeamNewPipe.NewPipeExtractor:NewPipeExtractor:6dc25f7b97") /* For Trailers
implementation("com.github.teamnewpipe:NewPipeExtractor:fafd471") /* For Trailers
^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */
implementation("com.github.albfernandez:juniversalchardet:2.4.0") // Subtitle Decoding
@ -219,9 +219,7 @@ dependencies {
implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview
// Extensions & Other Libs
implementation("org.mozilla:rhino:1.7.13") /* run JavaScript
^ Don't Bump RhinoJS to 1.7.14,`NoClassDefFoundError` Occurs and Trailers won't play (even with Desugaring)
NewPipeExtractor Issue */
implementation("org.mozilla:rhino:1.7.15") // run JavaScript
implementation("me.xdrop:fuzzywuzzy:1.4.0") // Library/Ext Searching with Levenshtein Distance
implementation("com.github.LagradOst:SafeFile:0.0.6") // To Prevent the URI File Fu*kery
implementation("org.conscrypt:conscrypt-android:2.5.2") // To Fix SSL Fu*kery on Android 9

View file

@ -651,7 +651,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
}
}
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
val response = CommonActivity.dispatchKeyEvent(this, event)
if (response != null)
return response

View file

@ -0,0 +1,37 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
class GoodstreamExtractor : ExtractorApi() {
override var name = "Goodstream"
override val mainUrl = "https://goodstream.uno"
override val requiresReferer = false
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
app.get(url).document.select("script").map { script ->
if (script.data().contains(Regex("file|player"))) {
val urlRegex = Regex("file: \"(https:\\/\\/[a-z0-9.\\/-_?=&]+)\",")
urlRegex.find(script.data())?.groupValues?.get(1).let { link ->
callback.invoke(
ExtractorLink(
name,
name,
link!!,
mainUrl,
Qualities.Unknown.value,
)
)
}
}
}
}
}

View file

@ -20,4 +20,9 @@ class PlayRu : ContentX() {
class FourPlayRu : ContentX() {
override var name = "FourPlayRu"
override var mainUrl = "https://four.playru.net"
}
}
class FourPichive : ContentX() {
override var name = "FourPichive"
override var mainUrl = "https://four.pichive.online"
}

View file

@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import java.net.URLDecoder
@ -26,12 +27,16 @@ class VidSrcTo : ExtractorApi() {
val res = app.get("$mainUrl/ajax/embed/episode/$mediaId/sources").parsedSafe<VidsrctoEpisodeSources>() ?: return
if (res.status != 200) return
res.result?.amap { source ->
val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe<VidsrctoEmbedSource>() ?: return@amap
val finalUrl = DecryptUrl(embedRes.result.encUrl)
if(finalUrl.equals(embedRes.result.encUrl)) return@amap
when (source.title) {
"Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback)
"Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback)
try {
val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe<VidsrctoEmbedSource>() ?: return@amap
val finalUrl = DecryptUrl(embedRes.result.encUrl)
if(finalUrl.equals(embedRes.result.encUrl)) return@amap
when (source.title) {
"Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback)
"Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback)
}
} catch (e: Exception) {
logError(e)
}
}
}

View file

@ -166,18 +166,10 @@ open class TraktProvider : MainAPI() {
val resSeasons = getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes")
val episodes = mutableListOf<Episode>()
val seasons = parseJson<List<Seasons>>(resSeasons)
val seasonsNames = mutableListOf<SeasonData>()
var nextAir: NextAiring? = null
seasons.forEach { season ->
seasonsNames.add(
SeasonData(
season.number!!,
season.title
)
)
season.episodes?.map { episode ->
val linkData = LinkData(
@ -250,7 +242,6 @@ open class TraktProvider : MainAPI() {
this.comingSoon = isUpcoming(mediaDetails.released)
//posterHeaders
this.nextAiring = nextAir
this.seasonNames = seasonsNames
this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl)
this.contentRating = mediaDetails.certification
addTrailer(mediaDetails.trailer)

View file

@ -12,9 +12,8 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
val aniListApi = AniListApi(0)
val openSubtitlesApi = OpenSubtitlesApi(0)
val simklApi = SimklApi(0)
val indexSubtitlesApi = IndexSubtitleApi()
val addic7ed = Addic7ed()
val subDl = SubDL()
val subDlApi = SubDlApi(0)
val localListApi = LocalList()
// used to login via app intent
@ -26,7 +25,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
// this needs init with context and can be accessed in settings
val accountManagers
get() = listOf(
malApi, aniListApi, openSubtitlesApi, simklApi //nginxApi
malApi, aniListApi, openSubtitlesApi, subDlApi, simklApi //nginxApi
)
// used for active syncing
@ -36,14 +35,16 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
)
val inAppAuths
get() = listOf(openSubtitlesApi)//, nginxApi)
get() = listOf<InAppAuthAPIManager>(
openSubtitlesApi,
subDlApi
)//, nginxApi)
val subtitleProviders
get() = listOf(
openSubtitlesApi,
indexSubtitlesApi, // they got anti scraping measures in place :(
addic7ed,
subDl
subDlApi
)
const val appString = "cloudstreamapp"

View file

@ -1,265 +0,0 @@
package com.lagradost.cloudstream3.syncproviders.providers
import android.util.Log
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.imdbUrlToIdNullable
import com.lagradost.cloudstream3.subtitles.AbstractSubApi
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
import com.lagradost.cloudstream3.utils.SubtitleHelper
class IndexSubtitleApi : AbstractSubApi {
override val name = "IndexSubtitle"
override val idPrefix = "indexsubtitle"
override val requiresLogin = false
override val icon: Nothing? = null
override val createAccountUrl: Nothing? = null
override fun loginInfo(): Nothing? = null
override fun logOut() {}
companion object {
const val host = "https://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 {
if (url.startsWith("http")) {
return url
}
if (url.isEmpty()) {
return ""
}
val startsWithNoHttp = url.startsWith("//")
if (startsWithNoHttp) {
return "https:$url"
} else {
if (url.startsWith('/')) {
return host + url
}
return "$host/$url"
}
}
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]))")
return text.contains(FILTER_EPS_REGEX)
}
private fun haveEps(text: String): Boolean {
val HAVE_EPS_REGEX =
Regex("(?i)((Chapter\\s?0?\\d)|((Season)?\\s?0?\\d?\\s?(Episode)\\s?0?\\d))|(?i)((S?0?\\d?E0?\\d)|(0?\\d[a-z]0?\\d))")
return text.contains(HAVE_EPS_REGEX)
}
override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List<AbstractSubtitleEntities.SubtitleEntity> {
val imdbId = query.imdbId?.replace("tt", "")?.toLong() ?: 0
val lang = query.lang
val queryLang = SubtitleHelper.fromTwoLettersToLanguage(lang.toString())
val queryText = query.query
val epNum = query.epNumber ?: 0
val seasonNum = query.seasonNumber ?: 0
val yearNum = query.year ?: 0
val urlItems = ArrayList<String>()
fun cleanResources(
results: MutableList<AbstractSubtitleEntities.SubtitleEntity>,
name: String,
link: String
) {
results.add(
AbstractSubtitleEntities.SubtitleEntity(
idPrefix = idPrefix,
name = name,
lang = queryLang.toString(),
data = link,
source = this.name,
type = if (seasonNum > 0) TvType.TvSeries else TvType.Movie,
epNumber = epNum,
seasonNumber = seasonNum,
year = yearNum,
)
)
}
val document = app.get("$host/?search=$queryText").document
document.select("div.my-3.p-3 div.media").map { block ->
if (seasonNum > 0) {
val name = block.select("strong.text-primary, strong.text-info").text().trim()
val season = getOrdinal(seasonNum)
if ((block.selectFirst("a")?.attr("href")
?.contains(
"$season",
ignoreCase = true
)!! || name.contains(
"$season",
ignoreCase = true
)) && name.contains(queryText, ignoreCase = true)
) {
block.select("div.media").mapNotNull {
urlItems.add(
fixUrl(
it.selectFirst("a")!!.attr("href")
)
)
}
}
} else {
if (block.selectFirst("strong")!!.text().trim()
.matches(Regex("(?i)^$queryText\$"))
) {
if (block.select("span[title=Release]").isNullOrEmpty()) {
block.select("div.media").mapNotNull {
val urlItem = fixUrl(
it.selectFirst("a")!!.attr("href")
)
val itemDoc = app.get(urlItem).document
val id = imdbUrlToIdNullable(
itemDoc.selectFirst("div.d-flex span.badge.badge-primary")?.parent()
?.attr("href")
)?.toLongOrNull()
val year = itemDoc.selectFirst("div.d-flex span.badge.badge-success")
?.ownText()
?.trim().toString()
Log.i(TAG, "id => $id \nyear => $year||$yearNum")
if (imdbId > 0) {
if (id == imdbId) {
urlItems.add(urlItem)
}
} else {
if (year.contains("$yearNum")) {
urlItems.add(urlItem)
}
}
}
} else {
if (block.select("span[title=Release]").text().trim()
.contains("$yearNum")
) {
block.select("div.media").mapNotNull {
urlItems.add(
fixUrl(
it.selectFirst("a")!!.attr("href")
)
)
}
}
}
}
}
}
Log.i(TAG, "urlItems => $urlItems")
val results = mutableListOf<AbstractSubtitleEntities.SubtitleEntity>()
urlItems.forEach { url ->
val request = app.get(url)
if (request.isSuccessful) {
request.document.select("div.my-3.p-3 div.media").map { block ->
if (block.select("span.d-block span[data-original-title=Language]").text()
.trim()
.contains("$queryLang")
) {
var name = block.select("strong.text-primary, strong.text-info").text().trim()
val link = fixUrl(block.selectFirst("a")!!.attr("href"))
if (seasonNum > 0) {
when {
isRightEps(name, seasonNum, epNum) -> {
cleanResources(results, name, link)
}
!(haveEps(name)) -> {
name = "$name (S${seasonNum}:E${epNum})"
cleanResources(results, name, link)
}
}
} else {
cleanResources(results, name, link)
}
}
}
}
}
return results
}
override suspend fun load(data: AbstractSubtitleEntities.SubtitleEntity): String? {
val seasonNum = data.seasonNumber
val epNum = data.epNumber
val req = app.get(data.data)
if (req.isSuccessful) {
val document = req.document
val link = if (document.select("div.my-3.p-3 div.media").size == 1) {
fixUrl(
document.selectFirst("div.my-3.p-3 div.media a")!!.attr("href")
)
} else {
document.select("div.my-3.p-3 div.media").firstNotNullOf { block ->
val name =
block.selectFirst("strong.d-block")?.text()?.trim().toString()
if (seasonNum!! > 0) {
if (isRightEps(name, seasonNum, epNum)) {
fixUrl(block.selectFirst("a")!!.attr("href"))
} else {
null
}
} else {
fixUrl(block.selectFirst("a")!!.attr("href"))
}
}
}
return link
}
return null
}
}

View file

@ -1,21 +1,80 @@
package com.lagradost.cloudstream3.syncproviders.providers
import com.fasterxml.jackson.annotation.JsonProperty
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.ErrorLoadingException
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.subtitles.AbstractSubProvider
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.subtitles.AbstractSubApi
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
import com.lagradost.cloudstream3.subtitles.SubtitleResource
import com.lagradost.cloudstream3.syncproviders.AuthAPI.LoginInfo
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager
class SubDL : AbstractSubProvider {
//API Documentation: https://subdl.com/api-doc
val mainUrl = "https://subdl.com/"
val name = "SubDL"
class SubDlApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi {
override val idPrefix = "subdl"
override val name = "SubDL"
override val icon = R.drawable.subdl_logo_big
override val requiresPassword = true
override val requiresEmail = true
override val createAccountUrl = "https://subdl.com/login"
companion object {
const val APIKEY = "zRJl5QA-8jNA2i0pE8cxANbEukANp7IM"
const val APIENDPOINT = "https://api.subdl.com/api/v1/subtitles"
const val APIURL = "https://api.subdl.com"
const val APIENDPOINT = "$APIURL/api/v1/subtitles"
const val DOWNLOADENDPOINT = "https://dl.subdl.com"
const val SUBDL_SUBTITLES_USER_KEY: String = "subdl_user"
var currentSession: SubtitleOAuthEntity? = null
}
override suspend fun initialize() {
currentSession = getAuthKey()
}
override fun logOut() {
setAuthKey(null)
removeAccountKeys()
currentSession = getAuthKey()
}
override suspend fun login(data: InAppAuthAPI.LoginData): Boolean {
val email = data.email ?: throw ErrorLoadingException("Requires Email")
val password = data.password ?: throw ErrorLoadingException("Requires Password")
switchToNewAccount()
try {
if (initLogin(email, password)) {
registerAccount()
return true
}
} catch (e: Exception) {
logError(e)
switchToOldAccount()
}
switchToOldAccount()
return false
}
override fun getLatestLoginData(): InAppAuthAPI.LoginData? {
val current = getAuthKey() ?: return null
return InAppAuthAPI.LoginData(
email = current.userEmail,
password = current.pass
)
}
override fun loginInfo(): LoginInfo? {
getAuthKey()?.let { user ->
return LoginInfo(
profilePicture = null,
name = user.name ?: user.userEmail,
accountIndex = accountIndex
)
}
return null
}
override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List<AbstractSubtitleEntities.SubtitleEntity>? {
@ -37,8 +96,8 @@ class SubDL : AbstractSubProvider {
val searchQueryUrl = when (idQuery) {
//Use imdb/tmdb id to search if its valid
null -> "$APIENDPOINT?api_key=$APIKEY&film_name=$queryText&languages=${query.lang}$epQuery$seasonQuery$yearQuery"
else -> "$APIENDPOINT?api_key=$APIKEY$idQuery&languages=${query.lang}$epQuery$seasonQuery$yearQuery"
null -> "$APIENDPOINT?api_key=${currentSession?.apiKey}&film_name=$queryText&languages=${query.lang}$epQuery$seasonQuery$yearQuery"
else -> "$APIENDPOINT?api_key=${currentSession?.apiKey}$idQuery&languages=${query.lang}$epQuery$seasonQuery$yearQuery"
}
val req = app.get(
@ -49,7 +108,7 @@ class SubDL : AbstractSubProvider {
)
return req.parsedSafe<ApiResponse>()?.subtitles?.map { subtitle ->
val name = subtitle.releaseName
val lang = subtitle.lang.replaceFirstChar { it.uppercase() }
val resEpNum = subtitle.episode ?: query.epNumber
val resSeasonNum = subtitle.season ?: query.seasonNumber
@ -57,13 +116,14 @@ class SubDL : AbstractSubProvider {
AbstractSubtitleEntities.SubtitleEntity(
idPrefix = this.idPrefix,
name = name,
name = subtitle.releaseName,
lang = lang,
data = "${DOWNLOADENDPOINT}${subtitle.url}",
type = type,
source = this.name,
epNumber = resEpNum,
seasonNumber = resSeasonNum,
isHearingImpaired = subtitle.hearingImpaired ?: false,
)
}
}
@ -74,6 +134,88 @@ class SubDL : AbstractSubProvider {
}
}
private suspend fun initLogin(useremail: String, password: String): Boolean {
val tokenResponse = app.post(
url = "$APIURL/login",
data = mapOf(
"email" to useremail,
"password" to password
)
).parsedSafe<OAuthTokenResponse>()
if (tokenResponse?.token == null) return false
val apiResponse = app.get(
url = "$APIURL/user/userApi",
headers = mapOf(
"Authorization" to "Bearer ${tokenResponse.token}"
)
).parsedSafe<ApiKeyResponse>()
if (apiResponse?.ok == false) return false
setAuthKey(
SubtitleOAuthEntity(
userEmail = useremail,
pass = password,
name = tokenResponse.userData?.username ?: tokenResponse.userData?.name,
accessToken = tokenResponse.token,
apiKey = apiResponse?.apiKey
)
)
return true
}
private fun getAuthKey(): SubtitleOAuthEntity? {
return getKey(accountId, SUBDL_SUBTITLES_USER_KEY)
}
private fun setAuthKey(data: SubtitleOAuthEntity?) {
if (data == null) removeKey(
accountId,
SUBDL_SUBTITLES_USER_KEY
)
currentSession = data
setKey(accountId, SUBDL_SUBTITLES_USER_KEY, data)
}
data class SubtitleOAuthEntity(
@JsonProperty("userEmail") var userEmail: String,
@JsonProperty("pass") var pass: String,
@JsonProperty("name") var name: String? = null,
@JsonProperty("accessToken") var accessToken: String? = null,
@JsonProperty("apiKey") var apiKey: String? = null,
)
data class OAuthTokenResponse(
@JsonProperty("token") val token: String? = null,
@JsonProperty("userData") val userData: UserData? = null,
@JsonProperty("status") val status: Boolean? = null,
@JsonProperty("message") val message: String? = null,
)
data class UserData(
@JsonProperty("email") val email: String,
@JsonProperty("name") val name: String,
@JsonProperty("country") val country: String,
@JsonProperty("scStepCode") val scStepCode: String,
@JsonProperty("scVerified") val scVerified: Boolean,
@JsonProperty("username") val username: String? = null,
@JsonProperty("scUsername") val scUsername: String,
)
data class ApiKeyResponse(
@JsonProperty("ok") val ok: Boolean? = false,
@JsonProperty("api_key") val apiKey: String? = null,
@JsonProperty("usage") val usage: Usage? = null,
)
data class Usage(
@JsonProperty("total") val total: Long? = 0,
@JsonProperty("today") val today: Long? = 0,
)
data class ApiResponse(
@JsonProperty("status") val status: Boolean? = null,
@JsonProperty("results") val results: List<Result>? = null,
@ -96,7 +238,10 @@ class SubDL : AbstractSubProvider {
@JsonProperty("lang") val lang: String,
@JsonProperty("author") val author: String? = null,
@JsonProperty("url") val url: String? = null,
@JsonProperty("subtitlePage") val subtitlePage: String? = null,
@JsonProperty("season") val season: Int? = null,
@JsonProperty("episode") val episode: Int? = null,
@JsonProperty("language") val language: String? = null,
@JsonProperty("hi") val hearingImpaired: Boolean? = null,
)
}

View file

@ -17,7 +17,7 @@ import com.lagradost.safefile.SafeFile
const val DTAG = "PlayerActivity"
class DownloadedPlayerActivity : AppCompatActivity() {
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
CommonActivity.dispatchKeyEvent(this, event)?.let {
return it
}

View file

@ -1728,7 +1728,7 @@ class ResultViewModel2 : ViewModel() {
txt(R.string.episode_action_cast_mirror)
) { (result, index) ->
val host = device?.host ?: return@acquireSingleLink
val link = result.links.firstOrNull() ?: return@acquireSingleLink
val link = result.links.getOrNull(index) ?: return@acquireSingleLink
FcastSession(host).use { session ->
session.sendMessage(

View file

@ -27,6 +27,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniList
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.openSubtitlesApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subDlApi
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
import com.lagradost.cloudstream3.syncproviders.OAuth2API
@ -35,6 +36,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
@ -297,10 +299,10 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricCallback {
hideKeyboard()
setPreferencesFromResource(R.xml.settings_account, rootKey)
// hide preference on tvs and emulators
getPref(R.string.biometric_key)?.isEnabled = isLayout(PHONE)
//Hides the security category on TV as it's only Biometric for now
getPref(R.string.pref_category_security_key)?.hideOn(TV or EMULATOR)
getPref(R.string.biometric_key)?.setOnPreferenceClickListener {
getPref(R.string.biometric_key)?.hideOn(TV or EMULATOR)?.setOnPreferenceClickListener {
val ctx = context ?: return@setOnPreferenceClickListener false
if (deviceHasPasswordPinLock(ctx)) {
@ -324,12 +326,12 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricCallback {
R.string.anilist_key to aniListApi,
R.string.simkl_key to simklApi,
R.string.opensubtitles_key to openSubtitlesApi,
R.string.subdl_key to subDlApi,
)
for ((key, api) in syncApis) {
getPref(key)?.apply {
title =
getString(R.string.login_format).format(api.name, getString(R.string.account))
title = api.name
setOnPreferenceClickListener {
val info = api.loginInfo()
if (info != null) {

View file

@ -26,6 +26,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
@ -53,6 +54,30 @@ class SettingsFragment : Fragment() {
}
}
/**
* Hide many Preferences on selected layouts.
**/
fun PreferenceFragmentCompat?.hidePrefs(ids: List<Int>, layoutFlags: Int) {
if (this == null) return
try {
ids.forEach {
getPref(it)?.isVisible = !isLayout(layoutFlags)
}
} catch (e: Exception) {
logError(e)
}
}
/**
* Hide the Preference on selected layouts.
**/
fun Preference?.hideOn(layoutFlags: Int): Preference? {
if (this == null) return null
this.isVisible = !isLayout(layoutFlags)
return this
}
/**
* On TV you cannot properly scroll to the bottom of settings, this fixes that.
* */

View file

@ -27,10 +27,13 @@ import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.ui.EasterEggMonke
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.beneneCount
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
@ -208,9 +211,7 @@ class SettingsGeneral : PreferenceFragmentCompat() {
return@setOnPreferenceClickListener true
}
// disable preference on tvs and emulators
getPref(R.string.battery_optimisation_key)?.isEnabled = isLayout(PHONE)
getPref(R.string.battery_optimisation_key)?.setOnPreferenceClickListener {
getPref(R.string.battery_optimisation_key)?.hideOn(TV or EMULATOR)?.setOnPreferenceClickListener {
val ctx = context ?: return@setOnPreferenceClickListener false
if (isAppRestricted(ctx)) {

View file

@ -7,8 +7,14 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getFolderSize
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hidePrefs
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
@ -31,6 +37,18 @@ class SettingsPlayer : PreferenceFragmentCompat() {
setPreferencesFromResource(R.xml.settings_player, rootKey)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
//Hide specific prefs on TV/EMULATOR
hidePrefs(
listOf(
R.string.pref_category_gestures_key,
R.string.rotate_video_key,
R.string.auto_rotate_video_key
),
TV or EMULATOR
)
getPref(R.string.pref_category_android_tv_key)?.hideOn(PHONE)
getPref(R.string.video_buffer_length_key)?.setOnPreferenceClickListener {
val prefNames = resources.getStringArray(R.array.video_buffer_length_names)
val prefValues = resources.getIntArray(R.array.video_buffer_length_values)
@ -227,6 +245,5 @@ class SettingsPlayer : PreferenceFragmentCompat() {
return@setOnPreferenceClickListener true
}
}
}
}

View file

@ -9,6 +9,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider
import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.amap
@ -181,8 +182,11 @@ class PluginsViewModel : ViewModel() {
}
private suspend fun updatePluginListPrivate(context: Context, repositoryUrl: String) {
val isAdult = settingsForProvider.enableAdult
val plugins = getPlugins(repositoryUrl)
val list = plugins.map { plugin ->
val list = plugins.filter {
return@filter !(it.second.tvTypes?.contains("NSFW") == true && !isAdult)
}.map { plugin ->
PluginViewData(plugin, isDownloaded(context, plugin.second.internalName, plugin.first))
}

View file

@ -26,6 +26,7 @@ import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_T
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_UNIXTIME_KEY
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_USER_KEY
import com.lagradost.cloudstream3.syncproviders.providers.OpenSubtitlesApi.Companion.OPEN_SUBTITLES_USER_KEY
import com.lagradost.cloudstream3.syncproviders.providers.SubDlApi.Companion.SUBDL_SUBTITLES_USER_KEY
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
@ -64,6 +65,7 @@ object BackupUtils {
PLUGINS_KEY_LOCAL,
OPEN_SUBTITLES_USER_KEY,
SUBDL_SUBTITLES_USER_KEY,
DOWNLOAD_EPISODE_CACHE,

View file

@ -67,6 +67,7 @@ import com.lagradost.cloudstream3.extractors.Gdriveplayerorg
import com.lagradost.cloudstream3.extractors.Gdriveplayerus
import com.lagradost.cloudstream3.extractors.Gofile
import com.lagradost.cloudstream3.extractors.GuardareStream
import com.lagradost.cloudstream3.extractors.GoodstreamExtractor
import com.lagradost.cloudstream3.extractors.Guccihide
import com.lagradost.cloudstream3.extractors.Hxfile
import com.lagradost.cloudstream3.extractors.JWPlayer
@ -110,6 +111,7 @@ 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.FourPichive
import com.lagradost.cloudstream3.extractors.HDMomPlayer
import com.lagradost.cloudstream3.extractors.HDPlayerSystem
import com.lagradost.cloudstream3.extractors.VideoSeyred
@ -748,6 +750,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
FourCX(),
PlayRu(),
FourPlayRu(),
FourPichive(),
HDMomPlayer(),
HDPlayerSystem(),
VideoSeyred(),
@ -877,6 +880,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
Gdriveplayerorg(),
Gdriveplayerus(),
Gdriveplayerco(),
GoodstreamExtractor(),
Gdriveplayer(),
DatabaseGdrive(),
DatabaseGdrive2(),

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="20dp"
android:viewportHeight="320"
android:viewportWidth="320"
android:width="20dp">
<path android:fillColor="@color/white"
android:pathData="m107.87,39.3l-8.44,8.59l0,35.68l0,35.83l18.95,22.5c10.36,12.44 30.35,36.27 44.41,53l25.46,30.5l0,12.14l0,12.29l-24.43,-0l-24.43,-0l0,-11.84l0,-11.84l-19.99,-0l-19.99,-0l0,23.24l0,23.24l8.44,8.59l8.44,8.59l48.26,-0l48.26,-0l7.7,-7.85l7.7,-7.85l0,-36.86l-0.15,-37.01l-23.98,-28.28c-13.18,-15.54 -33.16,-39.23 -44.26,-52.55l-20.43,-24.13l0,-12.29l0,-12.29l24.43,-0l24.43,-0l0,12.58l0,12.58l19.99,-0l19.99,-0l0,-24.87l0,-24.87l-7.85,-7.7l-7.85,-7.7l-48.11,-0l-48.11,-0l-8.44,8.59z"/>
</vector>

View file

@ -62,14 +62,16 @@
</LinearLayout>
<TextView
android:id="@+id/account_switch_account"
android:text="@string/switch_account"
style="@style/SettingsItem" />
android:id="@+id/account_switch_account"
android:text="@string/switch_account"
style="@style/SettingsItem"
android:focusable="true"/>
<TextView
android:id="@+id/account_logout"
android:text="@string/logout"
style="@style/SettingsItem">
android:id="@+id/account_logout"
android:text="@string/logout"
style="@style/SettingsItem"
android:focusable="true">
<requestFocus />
</TextView>

View file

@ -1,10 +1,11 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:orientation="horizontal"
android:layout_height="wrap_content"
android:layout_width="match_parent">
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:orientation="horizontal"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:focusable="true">
<androidx.cardview.widget.CardView
android:id="@+id/account_profile_picture_holder"
@ -15,16 +16,16 @@
android:layout_height="30dp">
<ImageView
android:id="@+id/account_profile_picture"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="ContentDescription" />
android:id="@+id/account_profile_picture"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="ContentDescription" />
</androidx.cardview.widget.CardView>
<TextView
android:foreground="@null"
android:id="@+id/account_name"
tools:text="Account 1"
style="@style/SettingsItem" />
android:foreground="@null"
android:id="@+id/account_name"
tools:text="Account 1"
style="@style/SettingsItem" />
</LinearLayout>

View file

@ -7,18 +7,20 @@
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/account_list"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:background="?attr/primaryBlackBackground"
tools:listitem="@layout/account_single"
android:layout_width="match_parent"
android:layout_rowWeight="1"
android:layout_height="wrap_content" />
android:id="@+id/account_list"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:background="?attr/primaryBlackBackground"
tools:listitem="@layout/account_single"
android:layout_width="match_parent"
android:layout_rowWeight="1"
android:layout_height="wrap_content"
android:focusable="true"/>
<TextView
android:id="@+id/account_add"
android:text="@string/add_account"
style="@style/SettingsItem">
android:id="@+id/account_add"
android:text="@string/add_account"
style="@style/SettingsItem"
android:focusable="true">
<requestFocus />
</TextView>

View file

@ -107,6 +107,7 @@
android:layout_margin="10dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/change_providers_img_des"
android:focusable="true"
android:nextFocusLeft="@id/year_btt"
android:nextFocusRight="@id/main_search"
android:nextFocusUp="@id/nav_rail_view"

View file

@ -441,7 +441,12 @@
<string name="pref_category_actions">Actions</string>
<string name="pref_category_cache">Cache</string>
<string name="pref_category_android_tv">Android TV</string>
<string name="pref_category_android_tv_key" translatable="false" >pref_category_android_tv_key</string>
<string name="pref_category_gestures">Gestures</string>
<string name="pref_category_gestures_key" translatable="false">pref_category_gestures_key</string>
<string name="pref_category_security">Security</string>
<string name="pref_category_security_key" translatable="false">pref_category_security_key</string>
<string name="pref_category_accounts">Accounts</string>
<string name="pref_category_player_features">Player features</string>
<string name="pref_category_subtitles">Subtitles</string>
<string name="pref_category_player_layout">Layout</string>
@ -474,6 +479,7 @@
<string name="simkl_key" translatable="false">simkl_key</string>
<string name="mal_key" translatable="false">mal_key</string>
<string name="opensubtitles_key" translatable="false">opensubtitles_key</string>
<string name="subdl_key" translatable="false">subdl_key</string>
<string name="nginx_key" translatable="false">nginx_key</string>
<string name="example_password">password123</string>
<string name="example_username">Username</string>
@ -775,4 +781,5 @@
<string name="biometric_prompt_description">After a few failed attempts, the prompt will close. Simply restart the app to try again.</string>
<string name="biometric_warning">Your CloudStream data has been backed up now. Although the possibility of this is very low, all devices can behave differently. In the rare case, that you get locked out from accessing the app, clear the app data completely and restore from a backup. We are very sorry for any inconvenience arising from this.</string>
<string name="reset_btn">Reset</string>
<string name="cs3wiki">CloudStream Wiki</string>
</resources>

View file

@ -1,33 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
android:icon="@drawable/mal_logo"
android:key="@string/mal_key" />
<PreferenceCategory
android:title="@string/pref_category_accounts">
<Preference
android:icon="@drawable/ic_anilist_icon"
android:key="@string/anilist_key" />
<Preference
android:icon="@drawable/mal_logo"
android:key="@string/mal_key" />
<Preference
android:icon="@drawable/simkl_logo"
android:key="@string/simkl_key" />
<Preference
android:icon="@drawable/ic_anilist_icon"
android:key="@string/anilist_key" />
<Preference
android:icon="@drawable/open_subtitles_icon"
android:key="@string/opensubtitles_key" />
<Preference
android:icon="@drawable/simkl_logo"
android:key="@string/simkl_key" />
<SwitchPreference
android:defaultValue="false"
android:icon="@drawable/ic_outline_account_circle_24"
android:key="@string/skip_startup_account_select_key"
android:title="@string/skip_startup_account_select_pref" />
<Preference
android:icon="@drawable/open_subtitles_icon"
android:key="@string/opensubtitles_key" />
<SwitchPreferenceCompat
android:key="@string/biometric_key"
android:defaultValue="false"
android:summary="@string/biometric_setting_summary"
android:icon="@drawable/ic_fingerprint"
android:title="@string/biometric_setting" />
<Preference
android:icon="@drawable/subdl_logo_big"
android:key="@string/subdl_key" />
<SwitchPreference
android:defaultValue="false"
android:icon="@drawable/ic_outline_account_circle_24"
android:key="@string/skip_startup_account_select_key"
android:title="@string/skip_startup_account_select_pref" />
</PreferenceCategory>
<PreferenceCategory
android:title="@string/pref_category_security"
app:key="@string/pref_category_security_key">
<SwitchPreferenceCompat
android:key="@string/biometric_key"
android:defaultValue="false"
android:summary="@string/biometric_setting_summary"
android:icon="@drawable/ic_fingerprint"
android:title="@string/biometric_setting" />
</PreferenceCategory>
</PreferenceScreen>

View file

@ -86,6 +86,14 @@
android:action="android.intent.action.VIEW"
android:data="https://discord.gg/5Hus6fM" />
</Preference>
<Preference
android:title="@string/cs3wiki"
android:icon="@drawable/baseline_description_24"
app:summary="https://cloudstream.miraheze.org/">
<intent
android:action="android.intent.action.VIEW"
android:data="https://cloudstream.miraheze.org/" />
</Preference>
</PreferenceCategory>
</PreferenceScreen>

View file

@ -101,7 +101,8 @@
</PreferenceCategory>
<PreferenceCategory
android:title="@string/pref_category_gestures">
android:title="@string/pref_category_gestures"
app:key="@string/pref_category_gestures_key">
<SwitchPreference
android:icon="@drawable/ic_baseline_ondemand_video_24"
android:summary="@string/swipe_to_seek_settings_des"
@ -166,7 +167,8 @@
android:title="@string/video_buffer_clear_settings" />
</PreferenceCategory>
<PreferenceCategory
android:title="@string/pref_category_android_tv">
android:title="@string/pref_category_android_tv"
android:key="@string/pref_category_android_tv_key" >
<SeekBarPreference
android:defaultValue="10"
android:max="60"

View file

@ -1,13 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<SwitchPreference
android:defaultValue="true"
android:icon="@drawable/ic_baseline_bug_report_24"
android:key="acra.disable"
android:summaryOff="@string/bug_report_settings_off"
android:summaryOn="@string/bug_report_settings_on"
android:title="@string/pref_disable_acra" />
<PreferenceCategory
android:title="@string/pref_category_app_updates">
@ -80,5 +73,12 @@
android:icon="@drawable/ic_baseline_construction_24"
android:title="@string/redo_setup_process"
app:key="@string/redo_setup_key" />
<SwitchPreference
android:defaultValue="true"
android:icon="@drawable/ic_baseline_bug_report_24"
android:key="acra.disable"
android:summaryOff="@string/bug_report_settings_off"
android:summaryOn="@string/bug_report_settings_on"
android:title="@string/pref_disable_acra" />
</PreferenceCategory>
</PreferenceScreen>

View file

@ -6,7 +6,7 @@ buildscript {
dependencies {
classpath("com.android.tools.build:gradle:8.2.2")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23")
classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.9.10")
// Universal build config
classpath("com.codingfeline.buildkonfig:buildkonfig-gradle-plugin:0.15.1")