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

# Conflicts:
#	app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
This commit is contained in:
IndusAryan 2024-05-26 01:04:10 +05:30
commit dd80b31fcb
24 changed files with 393 additions and 276 deletions

View file

@ -29,6 +29,7 @@ import com.google.android.material.chip.ChipGroup
import com.google.android.material.navigationrail.NavigationRailView import com.google.android.material.navigationrail.NavigationRailView
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.MainActivity.Companion.resumeApps
import com.lagradost.cloudstream3.databinding.ToastBinding import com.lagradost.cloudstream3.databinding.ToastBinding
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.player.PlayerEventType import com.lagradost.cloudstream3.ui.player.PlayerEventType

View file

@ -31,19 +31,16 @@ import java.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
const val USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
//val baseHeader = mapOf("User-Agent" to USER_AGENT)
val mapper = JsonMapper.builder().addModule(kotlinModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!!
/** /**
* Defines the constant for the all languages preference, if this is set then it is * Defines the constant for the all languages preference, if this is set then it is
* the equivalent of all languages being set * the equivalent of all languages being set
**/ **/
const val AllLanguagesName = "universal" const val AllLanguagesName = "universal"
//val baseHeader = mapOf("User-Agent" to USER_AGENT)
val mapper = JsonMapper.builder().addModule(kotlinModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!!
object APIHolder { object APIHolder {
val unixTime: Long val unixTime: Long
get() = System.currentTimeMillis() / 1000L get() = System.currentTimeMillis() / 1000L
@ -121,7 +118,8 @@ object APIHolder {
fun LoadResponse.getId(): Int { fun LoadResponse.getId(): Int {
// this fixes an issue with outdated api as getLoadResponseIdFromUrl might be fucked // this fixes an issue with outdated api as getLoadResponseIdFromUrl might be fucked
return (if (this is ResultViewModel2.LoadResponseFromSearch) this.id else null) ?: getLoadResponseIdFromUrl(url, apiName) return (if (this is ResultViewModel2.LoadResponseFromSearch) this.id else null)
?: getLoadResponseIdFromUrl(url, apiName)
} }
/** /**
@ -222,10 +220,15 @@ object APIHolder {
} ?: false } ?: false
val matchingTypes = types?.any { it.name.equals(media.format, true) } == true val matchingTypes = types?.any { it.name.equals(media.format, true) } == true
if(lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears if (lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears
} ?: return null } ?: return null
Tracker(res.idMal, res.id.toString(), res.coverImage?.extraLarge ?: res.coverImage?.large, res.bannerImage) Tracker(
res.idMal,
res.id.toString(),
res.coverImage?.extraLarge ?: res.coverImage?.large,
res.bannerImage
)
} catch (t: Throwable) { } catch (t: Throwable) {
logError(t) logError(t)
null null
@ -866,6 +869,7 @@ enum class TvType(value: Int?) {
Others(12), Others(12),
Music(13), Music(13),
AudioBook(14), AudioBook(14),
/** Wont load the built in player, make your own interaction */ /** Wont load the built in player, make your own interaction */
CustomMedia(15), CustomMedia(15),
} }
@ -1253,13 +1257,15 @@ interface LoadResponse {
fun LoadResponse.getImdbId(): String? { fun LoadResponse.getImdbId(): String? {
return normalSafeApiCall { return normalSafeApiCall {
SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Imdb) SimklApi.readIdFromString(this.syncData[simklIdPrefix])
?.get(SimklApi.Companion.SyncServices.Imdb)
} }
} }
fun LoadResponse.getTMDbId(): String? { fun LoadResponse.getTMDbId(): String? {
return normalSafeApiCall { return normalSafeApiCall {
SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Tmdb) SimklApi.readIdFromString(this.syncData[simklIdPrefix])
?.get(SimklApi.Companion.SyncServices.Tmdb)
} }
} }
@ -1556,8 +1562,26 @@ data class TorrentLoadResponse(
posterHeaders: Map<String, String>? = null, posterHeaders: Map<String, String>? = null,
backgroundPosterUrl: String? = null, backgroundPosterUrl: String? = null,
) : this( ) : this(
name, url, apiName, magnet, torrent, plot, type, posterUrl, year, rating, tags, duration, trailers, name,
recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl, null url,
apiName,
magnet,
torrent,
plot,
type,
posterUrl,
year,
rating,
tags,
duration,
trailers,
recommendations,
actors,
comingSoon,
syncData,
posterHeaders,
backgroundPosterUrl,
null
) )
} }
@ -1609,7 +1633,8 @@ data class AnimeLoadResponse(
return this.episodes.maxOf { (_, episodes) -> return this.episodes.maxOf { (_, episodes) ->
episodes.count { episodeData -> episodes.count { episodeData ->
// Prioritize display season as actual season may be something random to fit multiple seasons into one. // 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 val episodeSeason =
displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE
// Count all episodes from season 1 to below the current season. // Count all episodes from season 1 to below the current season.
episodeSeason in 1..<season episodeSeason in 1..<season
} }
@ -1646,9 +1671,31 @@ data class AnimeLoadResponse(
seasonNames: List<SeasonData>? = null, seasonNames: List<SeasonData>? = null,
backgroundPosterUrl: String? = null, backgroundPosterUrl: String? = null,
) : this( ) : this(
engName, japName, name, url, apiName, type, posterUrl, year, episodes, showStatus, plot, tags, engName,
synonyms, rating, duration, trailers, recommendations, actors, comingSoon, syncData, posterHeaders, japName,
nextAiring, seasonNames, backgroundPosterUrl, null name,
url,
apiName,
type,
posterUrl,
year,
episodes,
showStatus,
plot,
tags,
synonyms,
rating,
duration,
trailers,
recommendations,
actors,
comingSoon,
syncData,
posterHeaders,
nextAiring,
seasonNames,
backgroundPosterUrl,
null
) )
} }
@ -1780,7 +1827,7 @@ data class MovieLoadResponse(
backgroundPosterUrl: String? = null, backgroundPosterUrl: String? = null,
) : this( ) : this(
name, url, apiName, type, dataUrl, posterUrl, year, plot, rating, tags, duration, trailers, name, url, apiName, type, dataUrl, posterUrl, year, plot, rating, tags, duration, trailers,
recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl,null recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl, null
) )
} }
@ -1923,7 +1970,8 @@ data class TvSeriesLoadResponse(
return episodes.count { episodeData -> return episodes.count { episodeData ->
// Prioritize display season as actual season may be something random to fit multiple seasons into one. // 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 val episodeSeason =
displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE
// Count all episodes from season 1 to below the current season. // Count all episodes from season 1 to below the current season.
episodeSeason in 1..<season episodeSeason in 1..<season
} + episode } + episode
@ -1956,9 +2004,28 @@ data class TvSeriesLoadResponse(
seasonNames: List<SeasonData>? = null, seasonNames: List<SeasonData>? = null,
backgroundPosterUrl: String? = null, backgroundPosterUrl: String? = null,
) : this( ) : this(
name, url, apiName, type, episodes, posterUrl, year, plot, showStatus, rating, tags, duration, name,
trailers, recommendations, actors, comingSoon, syncData, posterHeaders, nextAiring, seasonNames, url,
backgroundPosterUrl, null apiName,
type,
episodes,
posterUrl,
year,
plot,
showStatus,
rating,
tags,
duration,
trailers,
recommendations,
actors,
comingSoon,
syncData,
posterHeaders,
nextAiring,
seasonNames,
backgroundPosterUrl,
null
) )
} }
@ -2022,6 +2089,7 @@ data class AniSearch(
@JsonProperty("extraLarge") var extraLarge: String? = null, @JsonProperty("extraLarge") var extraLarge: String? = null,
@JsonProperty("large") var large: String? = null, @JsonProperty("large") var large: String? = null,
) )
data class Title( data class Title(
@JsonProperty("romaji") var romaji: String? = null, @JsonProperty("romaji") var romaji: String? = null,
@JsonProperty("english") var english: String? = null, @JsonProperty("english") var english: String? = null,

View file

@ -174,7 +174,6 @@ import java.net.URLDecoder
import java.nio.charset.Charset import java.nio.charset.Charset
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.reflect.KClass
import kotlin.system.exitProcess import kotlin.system.exitProcess
//https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898 //https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898
@ -187,116 +186,92 @@ import kotlin.system.exitProcess
//https://github.com/jellyfin/jellyfin-android/blob/6cbf0edf84a3da82347c8d59b5d5590749da81a9/app/src/main/java/org/jellyfin/mobile/bridge/ExternalPlayer.kt#L225 //https://github.com/jellyfin/jellyfin-android/blob/6cbf0edf84a3da82347c8d59b5d5590749da81a9/app/src/main/java/org/jellyfin/mobile/bridge/ExternalPlayer.kt#L225
const val VLC_PACKAGE = "org.videolan.vlc" class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAuthCallback {
const val MPV_PACKAGE = "is.xyz.mpv"
const val WEB_VIDEO_CAST_PACKAGE = "com.instantbits.cast.webvideo"
val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlayerActivity")
val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity")
//TODO REFACTOR AF
open class ResultResume(
val packageString: String,
val action: String = Intent.ACTION_VIEW,
val position: String? = null,
val duration: String? = null,
var launcher: ActivityResultLauncher<Intent>? = null,
) {
val defaultTime = -1L
val lastId get() = "${packageString}_last_open_id"
suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) {
val intent = Intent(action)
if (id != null)
setKey(lastId, id)
else
removeKey(lastId)
intent.setPackage(packageString)
callback.invoke(intent)
launcher?.launch(intent)
}
open fun getPosition(intent: Intent?): Long {
return defaultTime
}
open fun getDuration(intent: Intent?): Long {
return defaultTime
}
}
val VLC = object : ResultResume(
VLC_PACKAGE,
// Android 13 intent restrictions fucks up specifically launching the VLC player
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
"org.videolan.vlc.player.result"
} else {
Intent.ACTION_VIEW
},
"extra_position",
"extra_duration",
) {
override fun getPosition(intent: Intent?): Long {
return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime
}
override fun getDuration(intent: Intent?): Long {
return intent?.getLongExtra(this.duration, defaultTime) ?: defaultTime
}
}
val MPV = object : ResultResume(
MPV_PACKAGE,
//"is.xyz.mpv.MPVActivity.result", // resume not working :pensive:
position = "position",
duration = "duration",
) {
override fun getPosition(intent: Intent?): Long {
return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong() ?: defaultTime
}
override fun getDuration(intent: Intent?): Long {
return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong() ?: defaultTime
}
}
val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE)
val resumeApps = arrayOf(
VLC, MPV, WEB_VIDEO
)
// Short name for requests client to make it nicer to use
var app = Requests(responseParser = object : ResponseParser {
val mapper: ObjectMapper = jacksonObjectMapper().configure(
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
false
)
override fun <T : Any> parse(text: String, kClass: KClass<T>): T {
return mapper.readValue(text, kClass.java)
}
override fun <T : Any> parseSafe(text: String, kClass: KClass<T>): T? {
return try {
mapper.readValue(text, kClass.java)
} catch (e: Exception) {
null
}
}
override fun writeValueAsString(obj: Any): String {
return mapper.writeValueAsString(obj)
}
}).apply {
defaultHeaders = mapOf("user-agent" to USER_AGENT)
}
class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCallback {
companion object { companion object {
const val VLC_PACKAGE = "org.videolan.vlc"
const val MPV_PACKAGE = "is.xyz.mpv"
const val WEB_VIDEO_CAST_PACKAGE = "com.instantbits.cast.webvideo"
val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlayerActivity")
val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity")
//TODO REFACTOR AF
open class ResultResume(
val packageString: String,
val action: String = Intent.ACTION_VIEW,
val position: String? = null,
val duration: String? = null,
var launcher: ActivityResultLauncher<Intent>? = null,
) {
val defaultTime = -1L
val lastId get() = "${packageString}_last_open_id"
suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) {
val intent = Intent(action)
if (id != null)
setKey(lastId, id)
else
removeKey(lastId)
intent.setPackage(packageString)
callback.invoke(intent)
launcher?.launch(intent)
}
open fun getPosition(intent: Intent?): Long {
return defaultTime
}
open fun getDuration(intent: Intent?): Long {
return defaultTime
}
}
val VLC = object : ResultResume(
VLC_PACKAGE,
// Android 13 intent restrictions fucks up specifically launching the VLC player
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
"org.videolan.vlc.player.result"
} else {
Intent.ACTION_VIEW
},
"extra_position",
"extra_duration",
) {
override fun getPosition(intent: Intent?): Long {
return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime
}
override fun getDuration(intent: Intent?): Long {
return intent?.getLongExtra(this.duration, defaultTime) ?: defaultTime
}
}
val MPV = object : ResultResume(
MPV_PACKAGE,
//"is.xyz.mpv.MPVActivity.result", // resume not working :pensive:
position = "position",
duration = "duration",
) {
override fun getPosition(intent: Intent?): Long {
return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong()
?: defaultTime
}
override fun getDuration(intent: Intent?): Long {
return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong()
?: defaultTime
}
}
val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE)
val resumeApps = arrayOf(
VLC, MPV, WEB_VIDEO
)
const val TAG = "MAINACT" const val TAG = "MAINACT"
const val ANIMATED_OUTLINE: Boolean = false const val ANIMATED_OUTLINE: Boolean = false
var lastError: String? = null var lastError: String? = null
@ -1402,7 +1377,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
} }
} }
observe(viewModel.watchStatus,::setWatchStatus) observe(viewModel.watchStatus, ::setWatchStatus)
observe(syncViewModel.userData, ::setUserData) observe(syncViewModel.userData, ::setUserData)
observeNullable(viewModel.subscribeStatus, ::setSubscribeStatus) observeNullable(viewModel.subscribeStatus, ::setSubscribeStatus)
@ -1830,7 +1805,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
} }
override fun onAuthenticationError() { override fun onAuthenticationError() {
finish() finish()
} }
private var backPressedCallback: OnBackPressedCallback? = null private var backPressedCallback: OnBackPressedCallback? = null

View file

@ -4,6 +4,7 @@ import android.net.Uri
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.fasterxml.jackson.annotation.JsonAlias import com.fasterxml.jackson.annotation.JsonAlias
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId
import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
@ -166,6 +167,7 @@ open class TraktProvider : MainAPI() {
val episodes = mutableListOf<Episode>() val episodes = mutableListOf<Episode>()
val seasons = parseJson<List<Seasons>>(resSeasons) val seasons = parseJson<List<Seasons>>(resSeasons)
val seasonsNames = mutableListOf<SeasonData>() val seasonsNames = mutableListOf<SeasonData>()
var nextAir: NextAiring? = null
seasons.forEach { season -> seasons.forEach { season ->
@ -215,6 +217,13 @@ open class TraktProvider : MainAPI() {
description = episode.overview, description = episode.overview,
).apply { ).apply {
this.addDate(episode.firstAired) this.addDate(episode.firstAired)
if (nextAir == null && this.date != null && this.date!! > unixTimeMS) {
nextAir = NextAiring(
episode = this.episode!!,
unixTime = this.date!!.div(1000L),
season = if (this.season == 1) null else this.season,
)
}
} }
) )
} }
@ -240,6 +249,7 @@ open class TraktProvider : MainAPI() {
this.actors = actors this.actors = actors
this.comingSoon = isUpcoming(mediaDetails.released) this.comingSoon = isUpcoming(mediaDetails.released)
//posterHeaders //posterHeaders
this.nextAiring = nextAir
this.seasonNames = seasonsNames this.seasonNames = seasonsNames
this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl) this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl)
this.contentRating = mediaDetails.certification this.contentRating = mediaDetails.certification

View file

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.subtitles package com.lagradost.cloudstream3.subtitles
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.TvType
class AbstractSubtitleEntities { class AbstractSubtitleEntities {
@ -19,8 +20,11 @@ class AbstractSubtitleEntities {
data class SubtitleSearch( data class SubtitleSearch(
var query: String = "", var query: String = "",
var imdb: Long? = null,
var lang: String? = null, var lang: String? = null,
var imdbId: String? = null,
var tmdbId: Int? = null,
var malId: Int? = null,
var aniListId: Int? = null,
var epNumber: Int? = null, var epNumber: Int? = null,
var seasonNumber: Int? = null, var seasonNumber: Int? = null,
var year: Int? = null var year: Int? = null

View file

@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.syncproviders
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.syncproviders.providers.SubScene
import com.lagradost.cloudstream3.syncproviders.providers.* import com.lagradost.cloudstream3.syncproviders.providers.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -15,7 +14,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
val simklApi = SimklApi(0) val simklApi = SimklApi(0)
val indexSubtitlesApi = IndexSubtitleApi() val indexSubtitlesApi = IndexSubtitleApi()
val addic7ed = Addic7ed() val addic7ed = Addic7ed()
val subScene = SubScene() val subDl = SubDL()
val localListApi = LocalList() val localListApi = LocalList()
// used to login via app intent // used to login via app intent
@ -44,7 +43,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
openSubtitlesApi, openSubtitlesApi,
indexSubtitlesApi, // they got anti scraping measures in place :( indexSubtitlesApi, // they got anti scraping measures in place :(
addic7ed, addic7ed,
subScene subDl
) )
const val appString = "cloudstreamapp" const val appString = "cloudstreamapp"

View file

@ -98,7 +98,7 @@ class IndexSubtitleApi : AbstractSubApi {
} }
override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List<AbstractSubtitleEntities.SubtitleEntity> { override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List<AbstractSubtitleEntities.SubtitleEntity> {
val imdbId = query.imdb ?: 0 val imdbId = query.imdbId?.replace("tt", "")?.toLong() ?: 0
val lang = query.lang val lang = query.lang
val queryLang = SubtitleHelper.fromTwoLettersToLanguage(lang.toString()) val queryLang = SubtitleHelper.fromTwoLettersToLanguage(lang.toString())
val queryText = query.query val queryText = query.query

View file

@ -185,7 +185,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
throwIfCantDoRequest() throwIfCantDoRequest()
val fixedLang = fixLanguage(query.lang) val fixedLang = fixLanguage(query.lang)
val imdbId = query.imdb ?: 0 val imdbId = query.imdbId?.replace("tt", "")?.toInt() ?: 0
val queryText = query.query val queryText = query.query
val epNum = query.epNumber ?: 0 val epNum = query.epNumber ?: 0
val seasonNum = query.seasonNumber ?: 0 val seasonNum = query.seasonNumber ?: 0

View file

@ -1,118 +0,0 @@
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<AbstractSubtitleEntities.SubtitleEntity>? {
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
}
}
}

View file

@ -0,0 +1,102 @@
package com.lagradost.cloudstream3.syncproviders.providers
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.subtitles.AbstractSubProvider
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
import com.lagradost.cloudstream3.subtitles.SubtitleResource
class SubDL : AbstractSubProvider {
//API Documentation: https://subdl.com/api-doc
val mainUrl = "https://subdl.com/"
val name = "SubDL"
override val idPrefix = "subdl"
companion object {
const val APIKEY = "zRJl5QA-8jNA2i0pE8cxANbEukANp7IM"
const val APIENDPOINT = "https://api.subdl.com/api/v1/subtitles"
const val DOWNLOADENDPOINT = "https://dl.subdl.com"
}
override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List<AbstractSubtitleEntities.SubtitleEntity>? {
val queryText = query.query
val epNum = query.epNumber ?: 0
val seasonNum = query.seasonNumber ?: 0
val yearNum = query.year ?: 0
val idQuery = when {
query.imdbId != null -> "&imdb_id=${query.imdbId}"
query.tmdbId != null -> "&tmdb_id=${query.tmdbId}"
else -> null
}
val epQuery = if (epNum > 0) "&episode_number=$epNum" else ""
val seasonQuery = if (seasonNum > 0) "&season_number=$seasonNum" else ""
val yearQuery = if (yearNum > 0) "&year=$yearNum" else ""
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"
}
val req = app.get(
url = searchQueryUrl,
headers = mapOf(
"Accept" to "application/json"
)
)
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
val type = if ((resSeasonNum ?: 0) > 0) TvType.TvSeries else TvType.Movie
AbstractSubtitleEntities.SubtitleEntity(
idPrefix = this.idPrefix,
name = name,
lang = lang,
data = "${DOWNLOADENDPOINT}${subtitle.url}",
type = type,
source = this.name,
epNumber = resEpNum,
seasonNumber = resSeasonNum,
)
}
}
override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) {
this.addZipUrl(data.data) { name, _ ->
name
}
}
data class ApiResponse(
@JsonProperty("status") val status: Boolean? = null,
@JsonProperty("results") val results: List<Result>? = null,
@JsonProperty("subtitles") val subtitles: List<Subtitle>? = null,
)
data class Result(
@JsonProperty("sd_id") val sdId: Int? = null,
@JsonProperty("type") val type: String? = null,
@JsonProperty("name") val name: String? = null,
@JsonProperty("imdb_id") val imdbId: String? = null,
@JsonProperty("tmdb_id") val tmdbId: Long? = null,
@JsonProperty("first_air_date") val firstAirDate: String? = null,
@JsonProperty("year") val year: Int? = null,
)
data class Subtitle(
@JsonProperty("release_name") val releaseName: String,
@JsonProperty("name") val name: String,
@JsonProperty("lang") val lang: String,
@JsonProperty("author") val author: String? = null,
@JsonProperty("url") val url: String? = null,
@JsonProperty("season") val season: Int? = null,
@JsonProperty("episode") val episode: Int? = null,
)
}

View file

@ -32,6 +32,7 @@ import com.lagradost.cloudstream3.CommonActivity.keyEventListener
import com.lagradost.cloudstream3.CommonActivity.playerEventListener import com.lagradost.cloudstream3.CommonActivity.playerEventListener
import com.lagradost.cloudstream3.CommonActivity.screenHeight import com.lagradost.cloudstream3.CommonActivity.screenHeight
import com.lagradost.cloudstream3.CommonActivity.screenWidth import com.lagradost.cloudstream3.CommonActivity.screenWidth
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
import com.lagradost.cloudstream3.databinding.SubtitleOffsetBinding import com.lagradost.cloudstream3.databinding.SubtitleOffsetBinding
@ -177,7 +178,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
open fun openOnlineSubPicker( open fun openOnlineSubPicker(
context: Context, context: Context,
imdbId: Long?, loadResponse: LoadResponse?,
dismissCallback: (() -> Unit) dismissCallback: (() -> Unit)
) { ) {
throw NotImplementedError() throw NotImplementedError()

View file

@ -25,6 +25,10 @@ import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId
import com.lagradost.cloudstream3.LoadResponse.Companion.getImdbId
import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId
import com.lagradost.cloudstream3.LoadResponse.Companion.getTMDbId
import com.lagradost.cloudstream3.databinding.DialogOnlineSubtitlesBinding import com.lagradost.cloudstream3.databinding.DialogOnlineSubtitlesBinding
import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
import com.lagradost.cloudstream3.databinding.PlayerSelectSourceAndSubsBinding import com.lagradost.cloudstream3.databinding.PlayerSelectSourceAndSubsBinding
@ -39,7 +43,6 @@ import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSub
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper
import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog
import com.lagradost.cloudstream3.ui.result.* import com.lagradost.cloudstream3.ui.result.*
import com.lagradost.cloudstream3.ui.settings.Globals
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
@ -258,6 +261,7 @@ class GeneratorPlayer : FullScreenPlayer() {
var episode: Int? = null, var episode: Int? = null,
var season: Int? = null, var season: Int? = null,
var name: String? = null, var name: String? = null,
var imdbId: String? = null,
) )
private fun getMetaData(): TempMetaData { private fun getMetaData(): TempMetaData {
@ -284,7 +288,7 @@ class GeneratorPlayer : FullScreenPlayer() {
} }
override fun openOnlineSubPicker( override fun openOnlineSubPicker(
context: Context, imdbId: Long?, dismissCallback: (() -> Unit) context: Context, loadResponse: LoadResponse?, dismissCallback: (() -> Unit)
) { ) {
val providers = subsProviders val providers = subsProviders
val isSingleProvider = subsProviders.size == 1 val isSingleProvider = subsProviders.size == 1
@ -377,6 +381,7 @@ class GeneratorPlayer : FullScreenPlayer() {
} }
val currentTempMeta = getMetaData() val currentTempMeta = getMetaData()
// bruh idk why it is not correct // bruh idk why it is not correct
val color = ColorStateList.valueOf(context.colorFromAttribute(R.attr.colorAccent)) val color = ColorStateList.valueOf(context.colorFromAttribute(R.attr.colorAccent))
binding.searchLoadingBar.progressTintList = color binding.searchLoadingBar.progressTintList = color
@ -424,7 +429,10 @@ class GeneratorPlayer : FullScreenPlayer() {
val search = val search =
AbstractSubtitleEntities.SubtitleSearch( AbstractSubtitleEntities.SubtitleSearch(
query = query ?: return@ioSafe, query = query ?: return@ioSafe,
imdb = imdbId, imdbId = loadResponse?.getImdbId(),
tmdbId = loadResponse?.getTMDbId()?.toInt(),
malId = loadResponse?.getMalId()?.toInt(),
aniListId = loadResponse?.getAniListId()?.toInt(),
epNumber = currentTempMeta.episode, epNumber = currentTempMeta.episode,
seasonNumber = currentTempMeta.season, seasonNumber = currentTempMeta.season,
lang = currentLanguageTwoLetters.ifBlank { null }, lang = currentLanguageTwoLetters.ifBlank { null },
@ -633,6 +641,8 @@ class GeneratorPlayer : FullScreenPlayer() {
} }
if (subsProvidersIsActive) { if (subsProvidersIsActive) {
val currentLoadResponse = viewModel.getLoadResponse()
val loadFromOpenSubsFooter: TextView = layoutInflater.inflate( val loadFromOpenSubsFooter: TextView = layoutInflater.inflate(
R.layout.sort_bottom_footer_add_choice, null R.layout.sort_bottom_footer_add_choice, null
) as TextView ) as TextView
@ -643,7 +653,7 @@ class GeneratorPlayer : FullScreenPlayer() {
loadFromOpenSubsFooter.setOnClickListener { loadFromOpenSubsFooter.setOnClickListener {
shouldDismiss = false shouldDismiss = false
sourceDialog.dismissSafe(activity) sourceDialog.dismissSafe(activity)
openOnlineSubPicker(it.context, null) { openOnlineSubPicker(it.context, currentLoadResponse) {
dismiss() dismiss()
} }
} }

View file

@ -5,6 +5,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.launchSafe
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
@ -111,6 +112,9 @@ class PlayerGeneratorViewModel : ViewModel() {
} }
} }
} }
fun getLoadResponse(): LoadResponse? {
return normalSafeApiCall { (generator as? RepoLinkGenerator?)?.page }
}
fun getMeta(): Any? { fun getMeta(): Any? {
return normalSafeApiCall { generator?.getCurrent() } return normalSafeApiCall { generator?.getCurrent() }

View file

@ -12,6 +12,7 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.lagradost.cloudstream3.CommonActivity.screenHeight import com.lagradost.cloudstream3.CommonActivity.screenHeight
import com.lagradost.cloudstream3.CommonActivity.screenWidth import com.lagradost.cloudstream3.CommonActivity.screenWidth
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ui.player.CSPlayerEvent import com.lagradost.cloudstream3.ui.player.CSPlayerEvent
import com.lagradost.cloudstream3.ui.player.PlayerEventSource import com.lagradost.cloudstream3.ui.player.PlayerEventSource
@ -110,7 +111,7 @@ open class ResultTrailerPlayer : ResultFragmentPhone() {
override fun openOnlineSubPicker( override fun openOnlineSubPicker(
context: Context, context: Context,
imdbId: Long?, loadResponse: LoadResponse?,
dismissCallback: () -> Unit dismissCallback: () -> Unit
) { ) {
} }

View file

@ -27,8 +27,17 @@ import com.lagradost.cloudstream3.CommonActivity.getCastSession
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId
import com.lagradost.cloudstream3.LoadResponse.Companion.getImdbId
import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId
import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie
import com.lagradost.cloudstream3.MainActivity.Companion.MPV
import com.lagradost.cloudstream3.MainActivity.Companion.MPV_COMPONENT
import com.lagradost.cloudstream3.MainActivity.Companion.MPV_PACKAGE
import com.lagradost.cloudstream3.MainActivity.Companion.VLC
import com.lagradost.cloudstream3.MainActivity.Companion.VLC_COMPONENT
import com.lagradost.cloudstream3.MainActivity.Companion.VLC_PACKAGE
import com.lagradost.cloudstream3.MainActivity.Companion.WEB_VIDEO
import com.lagradost.cloudstream3.MainActivity.Companion.WEB_VIDEO_CAST_PACKAGE
import com.lagradost.cloudstream3.metaproviders.SyncRedirector import com.lagradost.cloudstream3.metaproviders.SyncRedirector
import com.lagradost.cloudstream3.mvvm.* import com.lagradost.cloudstream3.mvvm.*
import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager
@ -1354,7 +1363,7 @@ class ResultViewModel2 : ViewModel() {
private fun launchActivity( private fun launchActivity(
activity: Activity?, activity: Activity?,
resumeApp: ResultResume, resumeApp: MainActivity.Companion.ResultResume,
id: Int? = null, id: Int? = null,
work: suspend (Intent.(Activity) -> Unit) work: suspend (Intent.(Activity) -> Unit)
): Job? { ): Job? {
@ -2409,7 +2418,7 @@ class ResultViewModel2 : ViewModel() {
null, null,
loadResponse.type, loadResponse.type,
mainId, mainId,
null null,
) )
) )
} }

View file

@ -40,7 +40,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="30dp" android:layout_height="30dp"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_marginEnd="30dp"> android:layout_marginEnd="40dp">
<androidx.appcompat.widget.SearchView <androidx.appcompat.widget.SearchView
android:id="@+id/subtitles_search" android:id="@+id/subtitles_search"
@ -91,7 +91,8 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center_vertical|end" android:layout_gravity="center_vertical|end"
android:layout_marginEnd="20dp" android:nextFocusLeft="@id/subtitles_search"
android:nextFocusRight="@id/search_filter"
android:text="@string/none" android:text="@string/none"
/> />
</LinearLayout> </LinearLayout>
@ -106,7 +107,7 @@
android:layout_margin="10dp" android:layout_margin="10dp"
android:background="?selectableItemBackgroundBorderless" android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/change_providers_img_des" android:contentDescription="@string/change_providers_img_des"
android:nextFocusLeft="@id/main_search" android:nextFocusLeft="@id/year_btt"
android:nextFocusRight="@id/main_search" android:nextFocusRight="@id/main_search"
android:nextFocusUp="@id/nav_rail_view" android:nextFocusUp="@id/nav_rail_view"
android:nextFocusDown="@id/search_autofit_results" android:nextFocusDown="@id/search_autofit_results"

View file

@ -0,0 +1,11 @@
package com.lagradost.cloudstream3.utils
import android.os.Handler
import android.os.Looper
actual fun runOnMainThreadNative(work: () -> Unit) {
val mainHandler = Handler(Looper.getMainLooper())
mainHandler.post {
work()
}
}

View file

@ -0,0 +1,35 @@
package com.lagradost.cloudstream3
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.ResponseParser
import kotlin.reflect.KClass
// Short name for requests client to make it nicer to use
var app = Requests(responseParser = object : ResponseParser {
val mapper: ObjectMapper = jacksonObjectMapper().configure(
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
false
)
override fun <T : Any> parse(text: String, kClass: KClass<T>): T {
return mapper.readValue(text, kClass.java)
}
override fun <T : Any> parseSafe(text: String, kClass: KClass<T>): T? {
return try {
mapper.readValue(text, kClass.java)
} catch (e: Exception) {
null
}
}
override fun writeValueAsString(obj: Any): String {
return mapper.writeValueAsString(obj)
}
}).apply {
defaultHeaders = mapOf("user-agent" to USER_AGENT)
}

View file

@ -1,3 +1,6 @@
package com.lagradost.cloudstream3 package com.lagradost.cloudstream3
const val USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
class ErrorLoadingException(message: String? = null) : Exception(message) class ErrorLoadingException(message: String? = null) : Exception(message)

View file

@ -1,12 +1,11 @@
package com.lagradost.cloudstream3.utils package com.lagradost.cloudstream3.utils
import android.os.Handler
import android.os.Looper
import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.launchSafe
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.util.Collections.synchronizedList import java.util.Collections.synchronizedList
expect fun runOnMainThreadNative(work: (() -> Unit))
object Coroutines { object Coroutines {
fun <T> T.main(work: suspend ((T) -> Unit)): Job { fun <T> T.main(work: suspend ((T) -> Unit)): Job {
val value = this val value = this
@ -50,10 +49,7 @@ object Coroutines {
} }
fun runOnMainThread(work: (() -> Unit)) { fun runOnMainThread(work: (() -> Unit)) {
val mainHandler = Handler(Looper.getMainLooper()) runOnMainThreadNative(work)
mainHandler.post {
work()
}
} }
/** /**

View file

@ -0,0 +1,5 @@
package com.lagradost.cloudstream3.utils
actual fun runOnMainThreadNative(work: () -> Unit) {
work.invoke()
}