mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Merge branch 'recloudstream:master' into master
This commit is contained in:
commit
ad0431433e
56 changed files with 1093 additions and 665 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import com.google.android.material.chip.ChipGroup
|
|||
import com.google.android.material.navigationrail.NavigationRailView
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.resumeApps
|
||||
import com.lagradost.cloudstream3.databinding.ToastBinding
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.ui.player.PlayerEventType
|
||||
|
|
|
|||
|
|
@ -31,19 +31,16 @@ import java.text.SimpleDateFormat
|
|||
import java.util.*
|
||||
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
|
||||
* the equivalent of all languages being set
|
||||
**/
|
||||
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 {
|
||||
val unixTime: Long
|
||||
get() = System.currentTimeMillis() / 1000L
|
||||
|
|
@ -121,7 +118,8 @@ object APIHolder {
|
|||
|
||||
fun LoadResponse.getId(): Int {
|
||||
// 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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -225,7 +223,12 @@ object APIHolder {
|
|||
if (lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears
|
||||
} ?: 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) {
|
||||
logError(t)
|
||||
null
|
||||
|
|
@ -866,6 +869,7 @@ enum class TvType(value: Int?) {
|
|||
Others(12),
|
||||
Music(13),
|
||||
AudioBook(14),
|
||||
|
||||
/** Wont load the built in player, make your own interaction */
|
||||
CustomMedia(15),
|
||||
}
|
||||
|
|
@ -1253,13 +1257,15 @@ interface LoadResponse {
|
|||
|
||||
fun LoadResponse.getImdbId(): String? {
|
||||
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? {
|
||||
return normalSafeApiCall {
|
||||
SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Tmdb)
|
||||
SimklApi.readIdFromString(this.syncData[simklIdPrefix])
|
||||
?.get(SimklApi.Companion.SyncServices.Tmdb)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1448,11 +1454,24 @@ fun TvType?.isEpisodeBased(): Boolean {
|
|||
return (this == TvType.TvSeries || this == TvType.Anime || this == TvType.AsianDrama)
|
||||
}
|
||||
|
||||
|
||||
data class NextAiring(
|
||||
val episode: Int,
|
||||
val unixTime: Long,
|
||||
val season: Int? = null,
|
||||
) {
|
||||
/**
|
||||
* Secondary constructor for backwards compatibility without season.
|
||||
* TODO Remove this constructor after there is a new stable release and extensions are updated to support season.
|
||||
*/
|
||||
constructor(
|
||||
episode: Int,
|
||||
unixTime: Long,
|
||||
) : this (
|
||||
episode,
|
||||
unixTime,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param season To be mapped with episode season, not shown in UI if displaySeason is defined
|
||||
|
|
@ -1543,8 +1562,26 @@ data class TorrentLoadResponse(
|
|||
posterHeaders: Map<String, String>? = null,
|
||||
backgroundPosterUrl: String? = null,
|
||||
) : this(
|
||||
name, url, apiName, magnet, torrent, plot, type, posterUrl, year, rating, tags, duration, trailers,
|
||||
recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl, null
|
||||
name,
|
||||
url,
|
||||
apiName,
|
||||
magnet,
|
||||
torrent,
|
||||
plot,
|
||||
type,
|
||||
posterUrl,
|
||||
year,
|
||||
rating,
|
||||
tags,
|
||||
duration,
|
||||
trailers,
|
||||
recommendations,
|
||||
actors,
|
||||
comingSoon,
|
||||
syncData,
|
||||
posterHeaders,
|
||||
backgroundPosterUrl,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1596,7 +1633,8 @@ data class AnimeLoadResponse(
|
|||
return this.episodes.maxOf { (_, episodes) ->
|
||||
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
|
||||
val episodeSeason =
|
||||
displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE
|
||||
// Count all episodes from season 1 to below the current season.
|
||||
episodeSeason in 1..<season
|
||||
}
|
||||
|
|
@ -1633,9 +1671,31 @@ data class AnimeLoadResponse(
|
|||
seasonNames: List<SeasonData>? = null,
|
||||
backgroundPosterUrl: String? = null,
|
||||
) : this(
|
||||
engName, japName, name, url, apiName, type, posterUrl, year, episodes, showStatus, plot, tags,
|
||||
synonyms, rating, duration, trailers, recommendations, actors, comingSoon, syncData, posterHeaders,
|
||||
nextAiring, seasonNames, backgroundPosterUrl, null
|
||||
engName,
|
||||
japName,
|
||||
name,
|
||||
url,
|
||||
apiName,
|
||||
type,
|
||||
posterUrl,
|
||||
year,
|
||||
episodes,
|
||||
showStatus,
|
||||
plot,
|
||||
tags,
|
||||
synonyms,
|
||||
rating,
|
||||
duration,
|
||||
trailers,
|
||||
recommendations,
|
||||
actors,
|
||||
comingSoon,
|
||||
syncData,
|
||||
posterHeaders,
|
||||
nextAiring,
|
||||
seasonNames,
|
||||
backgroundPosterUrl,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1910,7 +1970,8 @@ data class TvSeriesLoadResponse(
|
|||
|
||||
return 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
|
||||
val episodeSeason =
|
||||
displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE
|
||||
// Count all episodes from season 1 to below the current season.
|
||||
episodeSeason in 1..<season
|
||||
} + episode
|
||||
|
|
@ -1943,9 +2004,28 @@ data class TvSeriesLoadResponse(
|
|||
seasonNames: List<SeasonData>? = null,
|
||||
backgroundPosterUrl: String? = null,
|
||||
) : this(
|
||||
name, url, apiName, type, episodes, posterUrl, year, plot, showStatus, rating, tags, duration,
|
||||
trailers, recommendations, actors, comingSoon, syncData, posterHeaders, nextAiring, seasonNames,
|
||||
backgroundPosterUrl, null
|
||||
name,
|
||||
url,
|
||||
apiName,
|
||||
type,
|
||||
episodes,
|
||||
posterUrl,
|
||||
year,
|
||||
plot,
|
||||
showStatus,
|
||||
rating,
|
||||
tags,
|
||||
duration,
|
||||
trailers,
|
||||
recommendations,
|
||||
actors,
|
||||
comingSoon,
|
||||
syncData,
|
||||
posterHeaders,
|
||||
nextAiring,
|
||||
seasonNames,
|
||||
backgroundPosterUrl,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -2009,6 +2089,7 @@ data class AniSearch(
|
|||
@JsonProperty("extraLarge") var extraLarge: String? = null,
|
||||
@JsonProperty("large") var large: String? = null,
|
||||
)
|
||||
|
||||
data class Title(
|
||||
@JsonProperty("romaji") var romaji: String? = null,
|
||||
@JsonProperty("english") var english: String? = null,
|
||||
|
|
|
|||
|
|
@ -161,6 +161,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.requestRW
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
|
||||
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
|
||||
import com.lagradost.cloudstream3.utils.fcast.FcastManager
|
||||
import com.lagradost.nicehttp.Requests
|
||||
import com.lagradost.nicehttp.ResponseParser
|
||||
import com.lagradost.safefile.SafeFile
|
||||
|
|
@ -173,7 +174,6 @@ import java.net.URLDecoder
|
|||
import java.nio.charset.Charset
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
//https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898
|
||||
|
|
@ -186,6 +186,9 @@ import kotlin.system.exitProcess
|
|||
|
||||
//https://github.com/jellyfin/jellyfin-android/blob/6cbf0edf84a3da82347c8d59b5d5590749da81a9/app/src/main/java/org/jellyfin/mobile/bridge/ExternalPlayer.kt#L225
|
||||
|
||||
class MainActivity : AppCompatActivity(), ColorPickerDialogListener,
|
||||
BiometricAuthenticator.BiometricAuthCallback {
|
||||
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"
|
||||
|
|
@ -253,11 +256,13 @@ val MPV = object : ResultResume(
|
|||
duration = "duration",
|
||||
) {
|
||||
override fun getPosition(intent: Intent?): Long {
|
||||
return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong() ?: defaultTime
|
||||
return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong()
|
||||
?: defaultTime
|
||||
}
|
||||
|
||||
override fun getDuration(intent: Intent?): Long {
|
||||
return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong() ?: defaultTime
|
||||
return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong()
|
||||
?: defaultTime
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -267,36 +272,7 @@ 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,
|
||||
BiometricAuthenticator.BiometricAuthCallback {
|
||||
companion object {
|
||||
const val TAG = "MAINACT"
|
||||
const val ANIMATED_OUTLINE: Boolean = false
|
||||
var lastError: String? = null
|
||||
|
|
@ -676,7 +652,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener,
|
|||
}
|
||||
}
|
||||
|
||||
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||
val response = CommonActivity.dispatchKeyEvent(this, event)
|
||||
if (response != null)
|
||||
return response
|
||||
|
|
@ -1756,6 +1732,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener,
|
|||
runAutoUpdate()
|
||||
}
|
||||
|
||||
FcastManager().init(this, false)
|
||||
|
||||
APIRepository.dubStatusActive = getApiDubstatusSettings()
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@ package com.lagradost.cloudstream3.extractors
|
|||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.extractors.helper.*
|
||||
import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler
|
||||
import com.lagradost.cloudstream3.utils.AppUtils
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||
|
|
@ -28,30 +26,39 @@ open class Chillx : ExtractorApi() {
|
|||
override val name = "Chillx"
|
||||
override val mainUrl = "https://chillx.top"
|
||||
override val requiresReferer = true
|
||||
|
||||
companion object {
|
||||
private var key: String? = null
|
||||
|
||||
suspend fun fetchKey(): String {
|
||||
return if (key != null) {
|
||||
key!!
|
||||
} else {
|
||||
val fetch = app.get("https://raw.githubusercontent.com/rushi-chavan/multi-keys/keys/keys.json").parsedSafe<Keys>()?.key?.get(0) ?: throw ErrorLoadingException("Unable to get key")
|
||||
key = fetch
|
||||
key!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("NAME_SHADOWING")
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val master = Regex("\\s*=\\s*'([^']+)").find(
|
||||
val master = Regex("""JScript[\w+]?\s*=\s*'([^']+)""").find(
|
||||
app.get(
|
||||
url,
|
||||
referer = referer ?: "",
|
||||
headers = mapOf(
|
||||
"Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
||||
"Accept-Language" to "en-US,en;q=0.5",
|
||||
)
|
||||
referer = url,
|
||||
).text
|
||||
)?.groupValues?.get(1)
|
||||
val decrypt = cryptoAESHandler(master ?: return, getKey().toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt")
|
||||
|
||||
val key = fetchKey()
|
||||
val decrypt = cryptoAESHandler(master ?: "", key.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt")
|
||||
val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1)
|
||||
|
||||
val subtitles = Regex("""subtitle"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1)
|
||||
val subtitlePattern = """\[(.*?)\](https?://[^\s,]+)""".toRegex()
|
||||
val subtitlePattern = """\[(.*?)](https?://[^\s,]+)""".toRegex()
|
||||
val matches = subtitlePattern.findAll(subtitles ?: "")
|
||||
val languageUrlPairs = matches.map { matchResult ->
|
||||
val (language, url) = matchResult.destructured
|
||||
|
|
@ -91,15 +98,10 @@ open class Chillx : ExtractorApi() {
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun getKey() = key ?: fetchKey().also { key = it }
|
||||
|
||||
private suspend fun fetchKey(): String {
|
||||
return app.get("https://raw.githubusercontent.com/Sofie99/Resources/main/chillix_key.json").parsed()
|
||||
}
|
||||
|
||||
data class Tracks(
|
||||
@JsonProperty("file") val file: String? = null,
|
||||
@JsonProperty("label") val label: String? = null,
|
||||
@JsonProperty("kind") val kind: String? = null,
|
||||
data class Keys(
|
||||
@JsonProperty("chillx") val key: List<String>
|
||||
)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,16 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
|
|||
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
|
||||
import java.net.URL
|
||||
|
||||
class Geodailymotion : Dailymotion() {
|
||||
override val name = "GeoDailymotion"
|
||||
override val mainUrl = "https://geo.dailymotion.com"
|
||||
}
|
||||
|
||||
open class Dailymotion : ExtractorApi() {
|
||||
override val mainUrl = "https://www.dailymotion.com"
|
||||
override val name = "Dailymotion"
|
||||
override val requiresReferer = false
|
||||
private val baseUrl = "https://www.dailymotion.com"
|
||||
|
||||
@Suppress("RegExpSimplifiable")
|
||||
private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex()
|
||||
|
|
@ -34,7 +40,7 @@ open class Dailymotion : ExtractorApi() {
|
|||
val dmV1st = config.dmInternalData.v1st
|
||||
val dmTs = config.dmInternalData.ts
|
||||
val embedder = config.context.embedder
|
||||
val metaDataUrl = "$mainUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0"
|
||||
val metaDataUrl = "$baseUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0"
|
||||
val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = req.cookies)
|
||||
.parsedSafe<MetaData>() ?: return
|
||||
metaData.qualities.forEach { (_, video) ->
|
||||
|
|
@ -45,16 +51,19 @@ open class Dailymotion : ExtractorApi() {
|
|||
}
|
||||
|
||||
private fun getEmbedUrl(url: String): String? {
|
||||
if (url.contains("/embed/")) {
|
||||
if (url.contains("/embed/") || url.contains("/video/")) {
|
||||
return url
|
||||
}
|
||||
val vid = getVideoId(url) ?: return null
|
||||
return "$mainUrl/embed/video/$vid"
|
||||
if (url.contains("geo.dailymotion.com")) {
|
||||
val videoId = url.substringAfter("video=")
|
||||
return "$baseUrl/embed/video/$videoId"
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getVideoId(url: String): String? {
|
||||
val path = URL(url).path
|
||||
val id = path.substringAfter("video/")
|
||||
val id = path.substringAfter("/video/")
|
||||
if (id.matches(videoIdRegex)) {
|
||||
return id
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import android.util.Log
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
|
|
|
|||
|
|
@ -21,3 +21,8 @@ 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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import android.util.Log
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import android.net.Uri
|
|||
import com.lagradost.cloudstream3.*
|
||||
import com.fasterxml.jackson.annotation.JsonAlias
|
||||
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.addTMDbId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||
|
|
@ -118,8 +119,12 @@ open class TraktProvider : MainAPI() {
|
|||
|
||||
val linkData = LinkData(
|
||||
id = mediaDetails?.ids?.tmdb,
|
||||
traktId = mediaDetails?.ids?.trakt,
|
||||
traktSlug = mediaDetails?.ids?.slug,
|
||||
tmdbId = mediaDetails?.ids?.tmdb,
|
||||
imdbId = mediaDetails?.ids?.imdb.toString(),
|
||||
tvdbId = mediaDetails?.ids?.tvdb,
|
||||
tvrageId = mediaDetails?.ids?.tvrage,
|
||||
type = data.type.toString(),
|
||||
title = mediaDetails?.title,
|
||||
year = mediaDetails?.year,
|
||||
|
|
@ -139,7 +144,6 @@ open class TraktProvider : MainAPI() {
|
|||
type = if (isAnime) TvType.AnimeMovie else TvType.Movie,
|
||||
) {
|
||||
this.name = mediaDetails.title
|
||||
this.apiName = "Trakt"
|
||||
this.type = if (isAnime) TvType.AnimeMovie else TvType.Movie
|
||||
this.posterUrl = getOriginalWidthImageUrl(posterUrl)
|
||||
this.year = mediaDetails.year
|
||||
|
|
@ -163,6 +167,7 @@ open class TraktProvider : MainAPI() {
|
|||
val episodes = mutableListOf<Episode>()
|
||||
val seasons = parseJson<List<Seasons>>(resSeasons)
|
||||
val seasonsNames = mutableListOf<SeasonData>()
|
||||
var nextAir: NextAiring? = null
|
||||
|
||||
seasons.forEach { season ->
|
||||
|
||||
|
|
@ -177,8 +182,12 @@ open class TraktProvider : MainAPI() {
|
|||
|
||||
val linkData = LinkData(
|
||||
id = mediaDetails?.ids?.tmdb,
|
||||
traktId = mediaDetails?.ids?.trakt,
|
||||
traktSlug = mediaDetails?.ids?.slug,
|
||||
tmdbId = mediaDetails?.ids?.tmdb,
|
||||
imdbId = mediaDetails?.ids?.imdb.toString(),
|
||||
tvdbId = mediaDetails?.ids?.tvdb,
|
||||
tvrageId = mediaDetails?.ids?.tvrage,
|
||||
type = data.type.toString(),
|
||||
season = episode.season,
|
||||
episode = episode.number,
|
||||
|
|
@ -208,6 +217,13 @@ open class TraktProvider : MainAPI() {
|
|||
description = episode.overview,
|
||||
).apply {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -220,7 +236,6 @@ open class TraktProvider : MainAPI() {
|
|||
episodes = episodes
|
||||
) {
|
||||
this.name = mediaDetails.title
|
||||
this.apiName = "Trakt"
|
||||
this.type = if (isAnime) TvType.Anime else TvType.TvSeries
|
||||
this.episodes = episodes
|
||||
this.posterUrl = getOriginalWidthImageUrl(posterUrl)
|
||||
|
|
@ -234,6 +249,7 @@ open class TraktProvider : MainAPI() {
|
|||
this.actors = actors
|
||||
this.comingSoon = isUpcoming(mediaDetails.released)
|
||||
//posterHeaders
|
||||
this.nextAiring = nextAir
|
||||
this.seasonNames = seasonsNames
|
||||
this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl)
|
||||
this.contentRating = mediaDetails.certification
|
||||
|
|
@ -406,8 +422,12 @@ open class TraktProvider : MainAPI() {
|
|||
|
||||
data class LinkData(
|
||||
val id: Int? = null,
|
||||
val traktId: Int? = null,
|
||||
val traktSlug: String? = null,
|
||||
val tmdbId: Int? = null,
|
||||
val imdbId: String? = null,
|
||||
val tvdbId: Int? = null,
|
||||
val tvrageId: String? = null,
|
||||
val type: String? = null,
|
||||
val season: Int? = null,
|
||||
val episode: Int? = null,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.lagradost.cloudstream3.subtitles
|
||||
|
||||
import com.lagradost.cloudstream3.LoadResponse
|
||||
import com.lagradost.cloudstream3.TvType
|
||||
|
||||
class AbstractSubtitleEntities {
|
||||
|
|
@ -19,8 +20,11 @@ class AbstractSubtitleEntities {
|
|||
|
||||
data class SubtitleSearch(
|
||||
var query: String = "",
|
||||
var imdb: Long? = 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 seasonNumber: Int? = null,
|
||||
var year: Int? = null
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ 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
|
||||
|
||||
|
|
@ -13,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 subScene = SubScene()
|
||||
val subDlApi = SubDlApi(0)
|
||||
val localListApi = LocalList()
|
||||
|
||||
// used to login via app intent
|
||||
|
|
@ -27,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
|
||||
|
|
@ -37,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,
|
||||
subScene
|
||||
subDlApi
|
||||
)
|
||||
|
||||
const val appString = "cloudstreamapp"
|
||||
|
|
|
|||
|
|
@ -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.imdb ?: 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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -185,7 +185,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
|||
throwIfCantDoRequest()
|
||||
val fixedLang = fixLanguage(query.lang)
|
||||
|
||||
val imdbId = query.imdb ?: 0
|
||||
val imdbId = query.imdbId?.replace("tt", "")?.toInt() ?: 0
|
||||
val queryText = query.query
|
||||
val epNum = query.epNumber ?: 0
|
||||
val seasonNum = query.seasonNumber ?: 0
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,247 @@
|
|||
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.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 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 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>? {
|
||||
|
||||
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=${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(
|
||||
url = searchQueryUrl,
|
||||
headers = mapOf(
|
||||
"Accept" to "application/json"
|
||||
)
|
||||
)
|
||||
|
||||
return req.parsedSafe<ApiResponse>()?.subtitles?.map { subtitle ->
|
||||
|
||||
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 = subtitle.releaseName,
|
||||
lang = lang,
|
||||
data = "${DOWNLOADENDPOINT}${subtitle.url}",
|
||||
type = type,
|
||||
source = this.name,
|
||||
epNumber = resEpNum,
|
||||
seasonNumber = resSeasonNum,
|
||||
isHearingImpaired = subtitle.hearingImpaired ?: false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) {
|
||||
this.addZipUrl(data.data) { name, _ ->
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@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("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,
|
||||
)
|
||||
}
|
||||
|
|
@ -11,6 +11,9 @@ import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding
|
|||
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
|
||||
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
||||
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||
import com.lagradost.cloudstream3.utils.DataStore.getKeys
|
||||
|
|
@ -86,10 +89,12 @@ class DownloadChildFragment : Fragment() {
|
|||
|
||||
binding?.downloadChildToolbar?.apply {
|
||||
title = name
|
||||
if (isLayout(PHONE or EMULATOR)) {
|
||||
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
|
||||
setNavigationOnClickListener {
|
||||
activity?.onBackPressedDispatcher?.onBackPressed()
|
||||
}
|
||||
}
|
||||
setAppBarNoScrollFlagsOnTV()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import androidx.annotation.MainThread
|
|||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DELETE_FILE
|
||||
|
|
@ -25,6 +27,7 @@ import com.lagradost.cloudstream3.ui.download.DownloadClickEvent
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.KEY_RESUME_PACKAGES
|
||||
|
||||
|
||||
open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||
|
|
@ -167,6 +170,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
|
|||
this.setPersistentId(card.id)
|
||||
view.setOnClickListener {
|
||||
if (isZeroBytes) {
|
||||
removeKey(KEY_RESUME_PACKAGES, card.id.toString())
|
||||
callback(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, card))
|
||||
//callback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data))
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import com.lagradost.cloudstream3.CommonActivity.keyEventListener
|
|||
import com.lagradost.cloudstream3.CommonActivity.playerEventListener
|
||||
import com.lagradost.cloudstream3.CommonActivity.screenHeight
|
||||
import com.lagradost.cloudstream3.CommonActivity.screenWidth
|
||||
import com.lagradost.cloudstream3.LoadResponse
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
|
||||
import com.lagradost.cloudstream3.databinding.SubtitleOffsetBinding
|
||||
|
|
@ -177,7 +178,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
|
||||
open fun openOnlineSubPicker(
|
||||
context: Context,
|
||||
imdbId: Long?,
|
||||
loadResponse: LoadResponse?,
|
||||
dismissCallback: (() -> Unit)
|
||||
) {
|
||||
throw NotImplementedError()
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ import com.lagradost.cloudstream3.*
|
|||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
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.FragmentPlayerBinding
|
||||
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.QualityProfileDialog
|
||||
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.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
|
|
@ -258,6 +261,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
var episode: Int? = null,
|
||||
var season: Int? = null,
|
||||
var name: String? = null,
|
||||
var imdbId: String? = null,
|
||||
)
|
||||
|
||||
private fun getMetaData(): TempMetaData {
|
||||
|
|
@ -284,7 +288,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
override fun openOnlineSubPicker(
|
||||
context: Context, imdbId: Long?, dismissCallback: (() -> Unit)
|
||||
context: Context, loadResponse: LoadResponse?, dismissCallback: (() -> Unit)
|
||||
) {
|
||||
val providers = subsProviders
|
||||
val isSingleProvider = subsProviders.size == 1
|
||||
|
|
@ -377,6 +381,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
val currentTempMeta = getMetaData()
|
||||
|
||||
// bruh idk why it is not correct
|
||||
val color = ColorStateList.valueOf(context.colorFromAttribute(R.attr.colorAccent))
|
||||
binding.searchLoadingBar.progressTintList = color
|
||||
|
|
@ -424,7 +429,10 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
val search =
|
||||
AbstractSubtitleEntities.SubtitleSearch(
|
||||
query = query ?: return@ioSafe,
|
||||
imdb = imdbId,
|
||||
imdbId = loadResponse?.getImdbId(),
|
||||
tmdbId = loadResponse?.getTMDbId()?.toInt(),
|
||||
malId = loadResponse?.getMalId()?.toInt(),
|
||||
aniListId = loadResponse?.getAniListId()?.toInt(),
|
||||
epNumber = currentTempMeta.episode,
|
||||
seasonNumber = currentTempMeta.season,
|
||||
lang = currentLanguageTwoLetters.ifBlank { null },
|
||||
|
|
@ -633,6 +641,8 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
if (subsProvidersIsActive) {
|
||||
val currentLoadResponse = viewModel.getLoadResponse()
|
||||
|
||||
val loadFromOpenSubsFooter: TextView = layoutInflater.inflate(
|
||||
R.layout.sort_bottom_footer_add_choice, null
|
||||
) as TextView
|
||||
|
|
@ -643,7 +653,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
loadFromOpenSubsFooter.setOnClickListener {
|
||||
shouldDismiss = false
|
||||
sourceDialog.dismissSafe(activity)
|
||||
openOnlineSubPicker(it.context, null) {
|
||||
openOnlineSubPicker(it.context, currentLoadResponse) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ enum class LoadType {
|
|||
InAppDownload,
|
||||
ExternalApp,
|
||||
Browser,
|
||||
Chromecast
|
||||
Chromecast,
|
||||
Fcast
|
||||
}
|
||||
|
||||
fun LoadType.toSet() : Set<ExtractorLinkType> {
|
||||
|
|
@ -29,12 +30,17 @@ fun LoadType.toSet() : Set<ExtractorLinkType> {
|
|||
ExtractorLinkType.VIDEO,
|
||||
ExtractorLinkType.M3U8
|
||||
)
|
||||
LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.values().toSet()
|
||||
LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.entries.toSet()
|
||||
LoadType.Chromecast -> setOf(
|
||||
ExtractorLinkType.VIDEO,
|
||||
ExtractorLinkType.DASH,
|
||||
ExtractorLinkType.M3U8
|
||||
)
|
||||
LoadType.Fcast -> setOf(
|
||||
ExtractorLinkType.VIDEO,
|
||||
ExtractorLinkType.DASH,
|
||||
ExtractorLinkType.M3U8
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.lifecycle.LiveData
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.lagradost.cloudstream3.LoadResponse
|
||||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.launchSafe
|
||||
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? {
|
||||
return normalSafeApiCall { generator?.getCurrent() }
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@ import com.lagradost.cloudstream3.ui.search.SearchAdapter
|
|||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||
import com.lagradost.cloudstream3.ui.search.SearchHelper
|
||||
import com.lagradost.cloudstream3.ui.search.SearchViewModel
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals
|
||||
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.utils.AppUtils.ownShow
|
||||
|
|
@ -274,9 +277,14 @@ class QuickSearchFragment : Fragment() {
|
|||
// UIHelper.showInputMethod(view.findFocus())
|
||||
// }
|
||||
//}
|
||||
binding?.quickSearchBack?.setOnClickListener {
|
||||
if (isLayout(PHONE or EMULATOR)) {
|
||||
binding?.quickSearchBack?.apply {
|
||||
isVisible = true
|
||||
setOnClickListener {
|
||||
activity?.popCurrentPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isLayout(TV)) {
|
||||
binding?.quickSearch?.requestFocus()
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ const val ACTION_PLAY_EPISODE_IN_WEB_VIDEO = 16
|
|||
const val ACTION_PLAY_EPISODE_IN_MPV = 17
|
||||
|
||||
const val ACTION_MARK_AS_WATCHED = 18
|
||||
const val ACTION_FCAST = 19
|
||||
|
||||
const val TV_EP_SIZE_LARGE = 400
|
||||
const val TV_EP_SIZE_SMALL = 300
|
||||
data class EpisodeClickEvent(val action: Int, val data: ResultEpisode)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import androidx.core.view.isGone
|
|||
import androidx.core.view.isVisible
|
||||
import com.lagradost.cloudstream3.CommonActivity.screenHeight
|
||||
import com.lagradost.cloudstream3.CommonActivity.screenWidth
|
||||
import com.lagradost.cloudstream3.LoadResponse
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.ui.player.CSPlayerEvent
|
||||
import com.lagradost.cloudstream3.ui.player.PlayerEventSource
|
||||
|
|
@ -110,7 +111,7 @@ open class ResultTrailerPlayer : ResultFragmentPhone() {
|
|||
|
||||
override fun openOnlineSubPicker(
|
||||
context: Context,
|
||||
imdbId: Long?,
|
||||
loadResponse: LoadResponse?,
|
||||
dismissCallback: () -> Unit
|
||||
) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,8 +27,17 @@ import com.lagradost.cloudstream3.CommonActivity.getCastSession
|
|||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||
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.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.mvvm.*
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
||||
|
|
@ -83,6 +92,10 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.setVideoWatchState
|
|||
import com.lagradost.cloudstream3.utils.DataStoreHelper.updateSubscribedData
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||
import com.lagradost.cloudstream3.utils.fcast.FcastManager
|
||||
import com.lagradost.cloudstream3.utils.fcast.FcastSession
|
||||
import com.lagradost.cloudstream3.utils.fcast.Opcode
|
||||
import com.lagradost.cloudstream3.utils.fcast.PlayMessage
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
|
@ -197,7 +210,11 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData {
|
|||
|
||||
else -> null
|
||||
}?.also {
|
||||
nextAiringEpisode = txt(R.string.next_episode_format, airing.episode)
|
||||
nextAiringEpisode = when (airing.season) {
|
||||
|
||||
null -> txt(R.string.next_episode_format, airing.episode)
|
||||
else -> txt(R.string.next_season_episode_format, airing.season, airing.episode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1346,7 +1363,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
|
||||
private fun launchActivity(
|
||||
activity: Activity?,
|
||||
resumeApp: ResultResume,
|
||||
resumeApp: MainActivity.Companion.ResultResume,
|
||||
id: Int? = null,
|
||||
work: suspend (Intent.(Activity) -> Unit)
|
||||
): Job? {
|
||||
|
|
@ -1515,6 +1532,13 @@ class ResultViewModel2 : ViewModel() {
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (FcastManager.currentDevices.isNotEmpty()) {
|
||||
options.add(
|
||||
txt(R.string.player_settings_play_in_fcast) to ACTION_FCAST
|
||||
)
|
||||
}
|
||||
|
||||
options.add(txt(R.string.episode_action_play_in_app) to ACTION_PLAY_EPISODE_IN_PLAYER)
|
||||
|
||||
for (app in apps) {
|
||||
|
|
@ -1690,6 +1714,39 @@ class ResultViewModel2 : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
ACTION_FCAST -> {
|
||||
val devices = FcastManager.currentDevices.toList()
|
||||
postPopup(
|
||||
txt(R.string.player_settings_select_cast_device),
|
||||
devices.map { txt(it.name) }) { index ->
|
||||
if (index == null) return@postPopup
|
||||
val device = devices.getOrNull(index)
|
||||
|
||||
acquireSingleLink(
|
||||
click.data,
|
||||
LoadType.Fcast,
|
||||
txt(R.string.episode_action_cast_mirror)
|
||||
) { (result, index) ->
|
||||
val host = device?.host ?: return@acquireSingleLink
|
||||
val link = result.links.getOrNull(index) ?: return@acquireSingleLink
|
||||
|
||||
FcastSession(host).use { session ->
|
||||
session.sendMessage(
|
||||
Opcode.Play,
|
||||
PlayMessage(
|
||||
link.type.getMimeType(),
|
||||
link.url,
|
||||
headers = mapOf(
|
||||
"referer" to link.referer,
|
||||
"user-agent" to USER_AGENT
|
||||
) + link.headers
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink(
|
||||
click.data,
|
||||
LoadType.Browser,
|
||||
|
|
@ -2361,7 +2418,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
null,
|
||||
loadResponse.type,
|
||||
mainId,
|
||||
null
|
||||
null,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -324,6 +325,7 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.Biome
|
|||
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) {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.account
|
|||
import com.lagradost.cloudstream3.ui.home.HomeFragment
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
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.utils.DataStoreHelper
|
||||
|
|
@ -84,11 +85,13 @@ class SettingsFragment : Fragment() {
|
|||
|
||||
settingsToolbar.apply {
|
||||
setTitle(title)
|
||||
if (isLayout(PHONE or EMULATOR)) {
|
||||
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
|
||||
setNavigationOnClickListener {
|
||||
activity?.onBackPressedDispatcher?.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
UIHelper.fixPaddingStatusbar(settingsToolbar)
|
||||
}
|
||||
|
||||
|
|
@ -98,12 +101,14 @@ class SettingsFragment : Fragment() {
|
|||
|
||||
settingsToolbar.apply {
|
||||
setTitle(title)
|
||||
if (isLayout(PHONE or EMULATOR)) {
|
||||
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
|
||||
children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag)
|
||||
setNavigationOnClickListener {
|
||||
activity?.onBackPressedDispatcher?.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
UIHelper.fixPaddingStatusbar(settingsToolbar)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ import okhttp3.internal.closeQuietly
|
|||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.io.OutputStream
|
||||
import java.lang.System.currentTimeMillis
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class SettingsUpdates : PreferenceFragmentCompat() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
|
@ -125,12 +128,12 @@ class SettingsUpdates : PreferenceFragmentCompat() {
|
|||
}
|
||||
|
||||
binding.saveBtt.setOnClickListener {
|
||||
val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis()))
|
||||
var fileStream: OutputStream? = null
|
||||
try {
|
||||
fileStream =
|
||||
VideoDownloadManager.setupStream(
|
||||
fileStream = VideoDownloadManager.setupStream(
|
||||
it.context,
|
||||
"logcat",
|
||||
"logcat_${date}",
|
||||
null,
|
||||
"txt",
|
||||
false
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ import com.lagradost.cloudstream3.extractors.FileMoonIn
|
|||
import com.lagradost.cloudstream3.extractors.FileMoonSx
|
||||
import com.lagradost.cloudstream3.extractors.Filesim
|
||||
import com.lagradost.cloudstream3.extractors.Fplayer
|
||||
import com.lagradost.cloudstream3.extractors.Geodailymotion
|
||||
import com.lagradost.cloudstream3.extractors.GMPlayer
|
||||
import com.lagradost.cloudstream3.extractors.Gdriveplayer
|
||||
import com.lagradost.cloudstream3.extractors.Gdriveplayerapi
|
||||
|
|
@ -109,6 +110,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
|
||||
|
|
@ -307,7 +309,18 @@ enum class ExtractorLinkType {
|
|||
/** No support at the moment */
|
||||
TORRENT,
|
||||
/** No support at the moment */
|
||||
MAGNET,
|
||||
MAGNET;
|
||||
|
||||
// See https://www.iana.org/assignments/media-types/media-types.xhtml
|
||||
fun getMimeType(): String {
|
||||
return when (this) {
|
||||
VIDEO -> "video/mp4"
|
||||
M3U8 -> "application/x-mpegURL"
|
||||
DASH -> "application/dash+xml"
|
||||
TORRENT -> "application/x-bittorrent"
|
||||
MAGNET -> "application/x-bittorrent"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun inferTypeFromUrl(url: String): ExtractorLinkType {
|
||||
|
|
@ -736,6 +749,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
|
|||
FourCX(),
|
||||
PlayRu(),
|
||||
FourPlayRu(),
|
||||
FourPichive(),
|
||||
HDMomPlayer(),
|
||||
HDPlayerSystem(),
|
||||
VideoSeyred(),
|
||||
|
|
@ -900,7 +914,9 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
|
|||
Simpulumlamerop(),
|
||||
Urochsunloath(),
|
||||
Yipsu(),
|
||||
MetaGnathTuggers()
|
||||
MetaGnathTuggers(),
|
||||
Geodailymotion(),
|
||||
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ object VideoDownloadManager {
|
|||
private val DOWNLOAD_BAD_CONFIG =
|
||||
DownloadStatus(retrySame = false, tryNext = false, success = false)
|
||||
|
||||
private const val KEY_RESUME_PACKAGES = "download_resume"
|
||||
const val KEY_RESUME_PACKAGES = "download_resume"
|
||||
const val KEY_DOWNLOAD_INFO = "download_info"
|
||||
private const val KEY_RESUME_QUEUE_PACKAGES = "download_q_resume"
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
package com.lagradost.cloudstream3.utils.fcast
|
||||
|
||||
import android.content.Context
|
||||
import android.net.nsd.NsdManager
|
||||
import android.net.nsd.NsdManager.ResolveListener
|
||||
import android.net.nsd.NsdServiceInfo
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
|
||||
class FcastManager {
|
||||
private var nsdManager: NsdManager? = null
|
||||
|
||||
// Used for receiver
|
||||
private val registrationListenerTcp = DefaultRegistrationListener()
|
||||
private fun getDeviceName(): String {
|
||||
return "${Build.MANUFACTURER}-${Build.MODEL}"
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the fcast service
|
||||
* @param registerReceiver If true will register the app as a compatible fcast receiver for discovery in other app
|
||||
*/
|
||||
fun init(context: Context, registerReceiver: Boolean) = ioSafe {
|
||||
nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager
|
||||
val serviceType = "_fcast._tcp"
|
||||
|
||||
if (registerReceiver) {
|
||||
val serviceName = "$APP_PREFIX-${getDeviceName()}"
|
||||
|
||||
val serviceInfo = NsdServiceInfo().apply {
|
||||
this.serviceName = serviceName
|
||||
this.serviceType = serviceType
|
||||
this.port = TCP_PORT
|
||||
}
|
||||
|
||||
nsdManager?.registerService(
|
||||
serviceInfo,
|
||||
NsdManager.PROTOCOL_DNS_SD,
|
||||
registrationListenerTcp
|
||||
)
|
||||
}
|
||||
|
||||
nsdManager?.discoverServices(
|
||||
serviceType,
|
||||
NsdManager.PROTOCOL_DNS_SD,
|
||||
DefaultDiscoveryListener()
|
||||
)
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
nsdManager?.unregisterService(registrationListenerTcp)
|
||||
}
|
||||
|
||||
inner class DefaultDiscoveryListener : NsdManager.DiscoveryListener {
|
||||
val tag = "DiscoveryListener"
|
||||
override fun onStartDiscoveryFailed(serviceType: String?, errorCode: Int) {
|
||||
Log.d(tag, "Discovery failed: $serviceType, error code: $errorCode")
|
||||
}
|
||||
|
||||
override fun onStopDiscoveryFailed(serviceType: String?, errorCode: Int) {
|
||||
Log.d(tag, "Stop discovery failed: $serviceType, error code: $errorCode")
|
||||
}
|
||||
|
||||
override fun onDiscoveryStarted(serviceType: String?) {
|
||||
Log.d(tag, "Discovery started: $serviceType")
|
||||
}
|
||||
|
||||
override fun onDiscoveryStopped(serviceType: String?) {
|
||||
Log.d(tag, "Discovery stopped: $serviceType")
|
||||
}
|
||||
|
||||
override fun onServiceFound(serviceInfo: NsdServiceInfo?) {
|
||||
if (serviceInfo == null) return
|
||||
nsdManager?.resolveService(serviceInfo, object : ResolveListener {
|
||||
override fun onResolveFailed(serviceInfo: NsdServiceInfo?, errorCode: Int) {
|
||||
}
|
||||
|
||||
override fun onServiceResolved(serviceInfo: NsdServiceInfo?) {
|
||||
if (serviceInfo == null) return
|
||||
|
||||
currentDevices.add(PublicDeviceInfo(serviceInfo))
|
||||
|
||||
Log.d(
|
||||
tag,
|
||||
"Service found: ${serviceInfo.serviceName}, Net: ${serviceInfo.host.hostAddress}"
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onServiceLost(serviceInfo: NsdServiceInfo?) {
|
||||
if (serviceInfo == null) return
|
||||
|
||||
// May remove duplicates, but net and port is null here, preventing device specific identification
|
||||
currentDevices.removeAll {
|
||||
it.rawName == serviceInfo.serviceName
|
||||
}
|
||||
|
||||
Log.d(tag, "Service lost: ${serviceInfo.serviceName}")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val APP_PREFIX = "CloudStream"
|
||||
val currentDevices: MutableList<PublicDeviceInfo> = mutableListOf()
|
||||
|
||||
class DefaultRegistrationListener : NsdManager.RegistrationListener {
|
||||
val tag = "DiscoveryService"
|
||||
override fun onServiceRegistered(serviceInfo: NsdServiceInfo) {
|
||||
Log.d(tag, "Service registered: ${serviceInfo.serviceName}")
|
||||
}
|
||||
|
||||
override fun onRegistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
|
||||
Log.e(tag, "Service registration failed: errorCode=$errorCode")
|
||||
}
|
||||
|
||||
override fun onServiceUnregistered(serviceInfo: NsdServiceInfo) {
|
||||
Log.d(tag, "Service unregistered: ${serviceInfo.serviceName}")
|
||||
}
|
||||
|
||||
override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
|
||||
Log.e(tag, "Service unregistration failed: errorCode=$errorCode")
|
||||
}
|
||||
}
|
||||
|
||||
const val TCP_PORT = 46899
|
||||
}
|
||||
}
|
||||
|
||||
class PublicDeviceInfo(serviceInfo: NsdServiceInfo) {
|
||||
val rawName: String = serviceInfo.serviceName
|
||||
val host: String? = serviceInfo.host.hostAddress
|
||||
val name = rawName.replace("-", " ") + host?.let { " $it" }
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.lagradost.cloudstream3.utils.fcast
|
||||
|
||||
import android.util.Log
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.safefile.closeQuietly
|
||||
import java.io.DataOutputStream
|
||||
import java.net.Socket
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
class FcastSession(private val hostAddress: String): AutoCloseable {
|
||||
val tag = "FcastSession"
|
||||
|
||||
private var socket: Socket? = null
|
||||
@Throws
|
||||
@WorkerThread
|
||||
fun open(): Socket {
|
||||
val socket = Socket(hostAddress, FcastManager.TCP_PORT)
|
||||
this.socket = socket
|
||||
return socket
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
socket?.closeQuietly()
|
||||
socket = null
|
||||
}
|
||||
|
||||
@Throws
|
||||
private fun acquireSocket(): Socket {
|
||||
return socket ?: open()
|
||||
}
|
||||
|
||||
fun ping() {
|
||||
sendMessage(Opcode.Ping, null)
|
||||
}
|
||||
|
||||
fun <T> sendMessage(opcode: Opcode, message: T) {
|
||||
ioSafe {
|
||||
val socket = acquireSocket()
|
||||
val outputStream = DataOutputStream(socket.getOutputStream())
|
||||
|
||||
val json = message?.toJson()
|
||||
val content = json?.toByteArray() ?: ByteArray(0)
|
||||
|
||||
// Little endian starting from 1
|
||||
// https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1
|
||||
val size = content.size + 1
|
||||
|
||||
val sizeArray = ByteArray(4) { num ->
|
||||
(size shr 8 * num and 0xff).toByte()
|
||||
}
|
||||
|
||||
Log.d(tag, "Sending message with size: $size, opcode: $opcode")
|
||||
outputStream.write(sizeArray)
|
||||
outputStream.write(ByteArray(1) { opcode.value })
|
||||
outputStream.write(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package com.lagradost.cloudstream3.utils.fcast
|
||||
|
||||
// See https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1
|
||||
enum class Opcode(val value: Byte) {
|
||||
None(0),
|
||||
Play(1),
|
||||
Pause(2),
|
||||
Resume(3),
|
||||
Stop(4),
|
||||
Seek(5),
|
||||
PlaybackUpdate(6),
|
||||
VolumeUpdate(7),
|
||||
SetVolume(8),
|
||||
PlaybackError(9),
|
||||
SetSpeed(10),
|
||||
Version(11),
|
||||
Ping(12),
|
||||
Pong(13);
|
||||
}
|
||||
|
||||
|
||||
data class PlayMessage(
|
||||
val container: String,
|
||||
val url: String? = null,
|
||||
val content: String? = null,
|
||||
val time: Double? = null,
|
||||
val speed: Double? = null,
|
||||
val headers: Map<String, String>? = null
|
||||
)
|
||||
|
||||
data class SeekMessage(
|
||||
val time: Double
|
||||
)
|
||||
|
||||
data class PlaybackUpdateMessage(
|
||||
val generationTime: Long,
|
||||
val time: Double,
|
||||
val duration: Double,
|
||||
val state: Int,
|
||||
val speed: Double
|
||||
)
|
||||
|
||||
data class VolumeUpdateMessage(
|
||||
val generationTime: Long,
|
||||
val volume: Double
|
||||
)
|
||||
|
||||
data class PlaybackErrorMessage(
|
||||
val message: String
|
||||
)
|
||||
|
||||
data class SetSpeedMessage(
|
||||
val speed: Double
|
||||
)
|
||||
|
||||
data class SetVolumeMessage(
|
||||
val volume: Double
|
||||
)
|
||||
|
||||
data class VersionMessage(
|
||||
val version: Long
|
||||
)
|
||||
10
app/src/main/res/drawable/subdl_logo_big.xml
Normal file
10
app/src/main/res/drawable/subdl_logo_big.xml
Normal 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>
|
||||
|
|
@ -64,12 +64,14 @@
|
|||
<TextView
|
||||
android:id="@+id/account_switch_account"
|
||||
android:text="@string/switch_account"
|
||||
style="@style/SettingsItem" />
|
||||
style="@style/SettingsItem"
|
||||
android:focusable="true"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/account_logout"
|
||||
android:text="@string/logout"
|
||||
style="@style/SettingsItem">
|
||||
style="@style/SettingsItem"
|
||||
android:focusable="true">
|
||||
|
||||
<requestFocus />
|
||||
</TextView>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
android:foreground="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:orientation="horizontal"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent">
|
||||
android:layout_width="match_parent"
|
||||
android:focusable="true">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/account_profile_picture_holder"
|
||||
|
|
|
|||
|
|
@ -13,12 +13,14 @@
|
|||
tools:listitem="@layout/account_single"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_rowWeight="1"
|
||||
android:layout_height="wrap_content" />
|
||||
android:layout_height="wrap_content"
|
||||
android:focusable="true"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/account_add"
|
||||
android:text="@string/add_account"
|
||||
style="@style/SettingsItem">
|
||||
style="@style/SettingsItem"
|
||||
android:focusable="true">
|
||||
|
||||
<requestFocus />
|
||||
</TextView>
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="30dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="30dp">
|
||||
android:layout_marginEnd="40dp">
|
||||
|
||||
<androidx.appcompat.widget.SearchView
|
||||
android:id="@+id/subtitles_search"
|
||||
|
|
@ -91,7 +91,8 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:nextFocusLeft="@id/subtitles_search"
|
||||
android:nextFocusRight="@id/search_filter"
|
||||
android:text="@string/none"
|
||||
/>
|
||||
</LinearLayout>
|
||||
|
|
@ -106,7 +107,7 @@
|
|||
android:layout_margin="10dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/change_providers_img_des"
|
||||
android:nextFocusLeft="@id/main_search"
|
||||
android:nextFocusLeft="@id/year_btt"
|
||||
android:nextFocusRight="@id/main_search"
|
||||
android:nextFocusUp="@id/nav_rail_view"
|
||||
android:nextFocusDown="@id/search_autofit_results"
|
||||
|
|
|
|||
|
|
@ -178,42 +178,40 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
|
|||
android:textStyle="bold"
|
||||
tools:text="The Perfect Run The Perfect Run" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/result_next_airing_holder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/result_episodes_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="17sp"
|
||||
android:textStyle="normal"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
tools:text="8 Episodes" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/result_next_airing_holder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/result_next_airing"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:gravity="center"
|
||||
android:gravity="start"
|
||||
android:textColor="?attr/grayTextColor"
|
||||
android:textSize="17sp"
|
||||
android:textStyle="normal"
|
||||
tools:text="Episode 1022 will be released in" />
|
||||
android:layout_marginEnd="5dp"
|
||||
tools:text="Season 2 Episode 1022 will be released in" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/result_next_airing_time"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:gravity="start"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="17sp"
|
||||
|
|
|
|||
|
|
@ -23,11 +23,10 @@
|
|||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_baseline_arrow_back_24"
|
||||
app:tint="@android:color/white"
|
||||
android:focusable="true"
|
||||
android:visibility="gone"
|
||||
android:layout_width="25dp"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<requestFocus />
|
||||
android:layout_height="wrap_content"
|
||||
tools:visibility="visible">
|
||||
</ImageView>
|
||||
|
||||
<FrameLayout
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@
|
|||
<string name="app_dub_sub_episode_text_format" formatted="true">%1$s Ep %2$d</string>
|
||||
<string name="cast_format" formatted="true">Cast: %s</string>
|
||||
<string name="next_episode_format" formatted="true">Episode %d will be released in</string>
|
||||
<string name="next_season_episode_format" formatted="true">Season %1$d Episode %2$d will be released in</string>
|
||||
<string name="next_episode_time_day_format" formatted="true">%1$dd %2$dh %3$dm</string>
|
||||
<string name="next_episode_time_hour_format" formatted="true">%1$dh %2$dm</string>
|
||||
<string name="next_episode_time_min_format" formatted="true">%dm</string>
|
||||
|
|
@ -356,6 +357,7 @@
|
|||
<string name="storage_error">Download error, check storage permissions</string>
|
||||
<string name="episode_action_chromecast_episode">Chromecast episode</string>
|
||||
<string name="episode_action_chromecast_mirror">Chromecast mirror</string>
|
||||
<string name="episode_action_cast_mirror">Cast mirror</string>
|
||||
<string name="episode_action_play_in_app">Play in app</string>
|
||||
<string name="episode_action_play_in_format">Play in %s</string>
|
||||
<string name="episode_action_play_in_browser">Play in browser</string>
|
||||
|
|
@ -469,6 +471,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>
|
||||
|
|
@ -633,7 +636,9 @@
|
|||
<string name="player_settings_play_in_vlc">VLC</string>
|
||||
<string name="player_settings_play_in_mpv">MPV</string>
|
||||
<string name="player_settings_play_in_web">Web Video Cast</string>
|
||||
<string name="player_settings_play_in_fcast">Fcast</string>
|
||||
<string name="player_settings_play_in_browser">Web browser</string>
|
||||
<string name="player_settings_select_cast_device">Select cast device</string>
|
||||
<string name="app_not_found_error">App not found</string>
|
||||
<string name="all_languages_preference">All Languages</string>
|
||||
<string name="skip_type_format" formatted="true">Skip %s</string>
|
||||
|
|
@ -769,4 +774,5 @@
|
|||
<string name="audio_book_singular">Audio Book</string>
|
||||
<string name="custom_media_singluar">Media</string>
|
||||
<string name="reset_btn">Reset</string>
|
||||
<string name="cs3wiki">CloudStream Wiki</string>
|
||||
</resources>
|
||||
|
|
@ -17,6 +17,10 @@
|
|||
android:icon="@drawable/open_subtitles_icon"
|
||||
android:key="@string/opensubtitles_key" />
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/subdl_logo_big"
|
||||
android:key="@string/subdl_key" />
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:icon="@drawable/ic_outline_account_circle_24"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
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)
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
package com.lagradost.cloudstream3.utils
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import com.lagradost.cloudstream3.mvvm.launchSafe
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.Collections.synchronizedList
|
||||
|
||||
expect fun runOnMainThreadNative(work: (() -> Unit))
|
||||
object Coroutines {
|
||||
fun <T> T.main(work: suspend ((T) -> Unit)): Job {
|
||||
val value = this
|
||||
|
|
@ -50,10 +49,7 @@ object Coroutines {
|
|||
}
|
||||
|
||||
fun runOnMainThread(work: (() -> Unit)) {
|
||||
val mainHandler = Handler(Looper.getMainLooper())
|
||||
mainHandler.post {
|
||||
work()
|
||||
}
|
||||
runOnMainThreadNative(work)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.lagradost.cloudstream3.utils
|
||||
|
||||
actual fun runOnMainThreadNative(work: () -> Unit) {
|
||||
work.invoke()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue