Merge remote-tracking branch 'origin/master'

This commit is contained in:
Blatzar 2022-06-16 21:38:15 +02:00
commit 86aed5b830
158 changed files with 7487 additions and 1874 deletions

View File

@ -6,6 +6,7 @@ on:
paths-ignore:
- '*.md'
- '*.json'
- '**/wcokey.txt'
concurrency:
group: "pre-release"

View File

@ -15,7 +15,6 @@
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings>
</option>
</component>

View File

@ -35,8 +35,8 @@ android {
minSdkVersion 21
targetSdkVersion 30
versionCode 47
versionName "2.10.25"
versionCode 48
versionName "2.10.28"
resValue "string", "app_version",
"${defaultConfig.versionName}${versionNameSuffix ?: ""}"
@ -93,12 +93,12 @@ dependencies {
testImplementation 'org.json:json:20180813'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'com.google.android.material:material:1.5.0' // dont change this to 1.6.0 it looks ugly af
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.0-beta01'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.0-beta01'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.0-rc01'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.0-rc01'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
testImplementation 'junit:junit:4.13.2'
@ -171,4 +171,10 @@ dependencies {
implementation 'com.facebook.shimmer:shimmer:0.5.0'
implementation "androidx.tvprovider:tvprovider:1.0.0"
// used for subtitle decoding https://github.com/albfernandez/juniversalchardet
implementation 'com.github.albfernandez:juniversalchardet:2.4.0'
// play yt
implementation 'com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT'
}

View File

@ -33,7 +33,6 @@ object CommonActivity {
var canShowPipMode: Boolean = false
var isInPIPMode: Boolean = false
val backEvent = Event<Boolean>()
val onColorSelectedEvent = Event<Pair<Int, Int>>()
val onDialogDismissedEvent = Event<Int>()
@ -282,6 +281,10 @@ object CommonActivity {
KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_NUMPAD_9 -> {
PlayerEventType.ShowMirrors
}
// OpenSubtitles shortcut
KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8 -> {
PlayerEventType.SearchSubtitlesOnline
}
KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3 -> {
PlayerEventType.ShowSpeed
}

View File

@ -14,8 +14,8 @@ import com.lagradost.cloudstream3.animeproviders.*
import com.lagradost.cloudstream3.metaproviders.CrossTmdbProvider
import com.lagradost.cloudstream3.movieproviders.*
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.ExtractorLink
@ -42,6 +42,8 @@ object APIHolder {
val allProviders by lazy {
arrayListOf(
// Movie providers
ElifilmsProvider(),
EstrenosDoramasProvider(),
PelisplusProvider(),
PelisplusHDProvider(),
PeliSmartProvider(),
@ -53,7 +55,6 @@ object APIHolder {
PelisflixProvider(),
SeriesflixProvider(),
IHaveNoTvProvider(), // Documentaries provider
LookMovieProvider(), // RECAPTCHA (Please allow up to 5 seconds...)
VMoveeProvider(),
AllMoviesForYouProvider(),
VidEmbedProvider(),
@ -76,7 +77,7 @@ object APIHolder {
TwoEmbedProvider(),
DramaSeeProvider(),
WatchAsianProvider(),
DramaidProvider(),
DramaidProvider(),
KdramaHoodProvider(),
AkwamProvider(),
MyCimaProvider(),
@ -87,9 +88,12 @@ object APIHolder {
TheFlixToProvider(),
StreamingcommunityProvider(),
TantifilmProvider(),
CineblogProvider(),
AltadefinizioneProvider(),
HDMovie5(),
RebahinProvider(),
LayarKaca21Provider(),
LayarKacaProvider(),
HDTodayProvider(),
// Metadata providers
//TmdbProvider(),
@ -104,6 +108,9 @@ object APIHolder {
//ShiroProvider(), // v2 fucked me
AnimeFlickProvider(),
AnimeflvnetProvider(),
AnimefenixProvider(),
AnimeflvIOProvider(),
JKAnimeProvider(),
TenshiProvider(),
WcoProvider(),
AnimePaheProvider(),
@ -113,19 +120,27 @@ object APIHolder {
ZoroProvider(),
DubbedAnimeProvider(),
MonoschinosProvider(),
MundoDonghuaProvider(),
KawaiifuProvider(), // disabled due to cloudflare
NeonimeProvider(),
NeonimeProvider(),
KuramanimeProvider(),
OploverzProvider(),
GomunimeProvider(),
NontonAnimeIDProvider(),
KuronimeProvider(),
//MultiAnimeProvider(),
NginxProvider(),
NginxProvider(),
OlgplyProvider(),
)
}
fun initAll() {
for (api in allProviders) {
api.init()
}
apiMap = null
}
var apis: List<MainAPI> = arrayListOf()
private var apiMap: Map<String, Int>? = null
@ -141,7 +156,6 @@ object APIHolder {
fun getApiFromNameNull(apiName: String?): MainAPI? {
if (apiName == null) return null
initMap()
return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
}
@ -154,12 +168,12 @@ object APIHolder {
return null
}
fun getLoadResponseIdFromUrl(url : String, apiName: String) : Int {
fun getLoadResponseIdFromUrl(url: String, apiName: String): Int {
return url.replace(getApiFromName(apiName).mainUrl, "").replace("/", "").hashCode()
}
fun LoadResponse.getId(): Int {
return getLoadResponseIdFromUrl(url,apiName)
return getLoadResponseIdFromUrl(url, apiName)
}
/**
@ -336,18 +350,19 @@ abstract class MainAPI {
var overrideData: HashMap<String, ProvidersInfoJson>? = null
}
fun overrideWithNewData(data: ProvidersInfoJson) {
this.name = data.name
this.mainUrl = data.url
this.storedCredentials = data.credentials
}
init {
fun init() {
overrideData?.get(this.javaClass.simpleName)?.let { data ->
overrideWithNewData(data)
}
}
fun overrideWithNewData(data: ProvidersInfoJson) {
this.name = data.name
if (data.url.isNotBlank() && data.url != "NONE")
this.mainUrl = data.url
this.storedCredentials = data.credentials
}
open var name = "NONE"
open var mainUrl = "NONE"
open var storedCredentials: String? = null
@ -463,12 +478,6 @@ fun base64Encode(array: ByteArray): String {
class ErrorLoadingException(message: String? = null) : Exception(message)
fun parseRating(ratingString: String?): Int? {
if (ratingString == null) return null
val floatRating = ratingString.toFloatOrNull() ?: return null
return (floatRating * 10).toInt()
}
fun MainAPI.fixUrlNull(url: String?): String? {
if (url.isNullOrEmpty()) {
return null
@ -763,12 +772,12 @@ fun AnimeSearchResponse.addDubStatus(isDub: Boolean, episodes: Int? = null) {
}
fun AnimeSearchResponse.addDub(episodes: Int?) {
if(episodes == null || episodes <= 0) return
if (episodes == null || episodes <= 0) return
addDubStatus(DubStatus.Dubbed, episodes)
}
fun AnimeSearchResponse.addSub(episodes: Int?) {
if(episodes == null || episodes <= 0) return
if (episodes == null || episodes <= 0) return
addDubStatus(DubStatus.Subbed, episodes)
}
@ -840,7 +849,7 @@ interface LoadResponse {
var posterUrl: String?
var year: Int?
var plot: String?
var rating: Int? // 1-1000
var rating: Int? // 0-10000
var tags: List<String>?
var duration: Int? // in minutes
var trailers: List<String>?
@ -898,6 +907,17 @@ interface LoadResponse {
}
}
fun LoadResponse.addTrailer(trailerUrls: List<String>?) {
if(trailerUrls == null) return
if (this.trailers == null) {
this.trailers = trailerUrls
} else {
val update = this.trailers?.toMutableList()
update?.addAll(trailerUrls)
this.trailers = update
}
}
fun LoadResponse.addImdbId(id: String?) {
// TODO add imdb sync
}
@ -919,7 +939,7 @@ interface LoadResponse {
}
fun LoadResponse.addRating(value: Int?) {
if (value ?: return < 0 || value > 1000) {
if ((value ?: return) < 0 || value > 10000) {
return
}
this.rating = value

View File

@ -27,20 +27,20 @@ import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
import com.lagradost.cloudstream3.APIHolder.allProviders
import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
import com.lagradost.cloudstream3.CommonActivity.backEvent
import com.lagradost.cloudstream3.APIHolder.initAll
import com.lagradost.cloudstream3.CommonActivity.loadThemes
import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.CommonActivity.updateLocale
import com.lagradost.cloudstream3.movieproviders.NginxProvider
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.OAuth2Apis
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.OAuth2accountApis
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.appString
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
import com.lagradost.cloudstream3.ui.result.ResultFragment
@ -59,6 +59,7 @@ import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching
import com.lagradost.cloudstream3.utils.DataStoreHelper.setViewPos
import com.lagradost.cloudstream3.utils.IOnBackPressed
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
@ -131,12 +132,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
R.id.navigation_download_child,
R.id.navigation_subtitles,
R.id.navigation_chrome_subtitles,
R.id.navigation_settings_nginx,
R.id.navigation_settings_player,
R.id.navigation_settings_updates,
R.id.navigation_settings_ui,
R.id.navigation_settings_account,
R.id.navigation_settings_lang,
R.id.navigation_settings_general,
).contains(destination.id)
val landscape = when (resources.configuration.orientation) {
@ -234,15 +235,23 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
onUserLeaveHint(this)
}
override fun onBackPressed() {
private fun backPressed() {
this.window?.navigationBarColor =
this.colorFromAttribute(R.attr.primaryGrayBackground)
this.updateLocale()
backEvent.invoke(true)
super.onBackPressed()
this.updateLocale()
}
override fun onBackPressed() {
((supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment?)?.childFragmentManager?.primaryNavigationFragment as? IOnBackPressed)?.onBackPressed()
?.let { runNormal ->
if (runNormal) backPressed()
} ?: run {
backPressed()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (VLC_REQUEST_CODE == requestCode) {
if (resultCode == RESULT_OK && data != null) {
@ -354,12 +363,67 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
}
fun test() {
//val youtubeLink = "https://www.youtube.com/watch?v=TxB48MEAmZw"
/*
runBlocking {
val query = """
query {
searchShows(search: "spider", limit: 10) {
id
name
originalName
}
}
"""
val data =
mapOf(
"query" to query,
//"variables" to
// mapOf(
// "name" to name,
// ).toJson()
)
val txt = app.post(
"http://api.anime-skip.com/graphql",
headers = mapOf(
"X-Client-ID" to "",
"Content-Type" to "application/json",
"Accept" to "application/json",
),
json = data
)
println("TEXT: $txt")
}*/
/*runBlocking {
//https://test.api.anime-skip.com/graphiql
val txt = app.get(
"https://api.anime-skip.com/status",
headers = mapOf("X-Client-ID" to "")
)
println("TEXT: $txt")
}*/
}
override fun onCreate(savedInstanceState: Bundle?) {
// init accounts
for (api in OAuth2accountApis) {
for (api in accountManagers) {
api.init()
}
ioSafe {
inAppAuths.apmap { api ->
try {
api.initialize()
} catch (e: Exception) {
logError(e)
}
}
}
SearchResultBuilder.updateCache(this)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
@ -379,68 +443,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
false
}
fun addNginxToJson(data: java.util.HashMap<String, ProvidersInfoJson>): java.util.HashMap<String, ProvidersInfoJson>? {
try {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val nginxUrl =
settingsManager.getString(getString(R.string.nginx_url_key), "nginx_url_key")
.toString()
val nginxCredentials =
settingsManager.getString(
getString(R.string.nginx_credentials),
"nginx_credentials"
)
.toString()
val StoredNginxProvider = NginxProvider()
if (nginxUrl == "nginx_url_key" || nginxUrl == "") { // if key is default value, or empty:
data[StoredNginxProvider.javaClass.simpleName] = ProvidersInfoJson(
url = nginxUrl,
name = StoredNginxProvider.name,
status = PROVIDER_STATUS_DOWN, // the provider will not be display
credentials = nginxCredentials
)
} else { // valid url
data[StoredNginxProvider.javaClass.simpleName] = ProvidersInfoJson(
url = nginxUrl,
name = StoredNginxProvider.name,
status = PROVIDER_STATUS_OK,
credentials = nginxCredentials
)
}
return data
} catch (e: Exception) {
logError(e)
return data
}
}
fun createNginxJson(): ProvidersInfoJson? { //java.util.HashMap<String, ProvidersInfoJson>
return try {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val nginxUrl =
settingsManager.getString(getString(R.string.nginx_url_key), "nginx_url_key")
.toString()
val nginxCredentials = settingsManager.getString(
getString(R.string.nginx_credentials),
"nginx_credentials"
).toString()
if (nginxUrl == "nginx_url_key" || nginxUrl == "") { // if key is default value or empty:
null // don't overwrite anything
} else {
ProvidersInfoJson(
url = nginxUrl,
name = NginxProvider().name,
status = PROVIDER_STATUS_OK,
credentials = nginxCredentials
)
}
} catch (e: Exception) {
logError(e)
null
}
}
// this pulls the latest data so ppl don't have to update to simply change provider url
if (downloadFromGithub) {
try {
@ -460,11 +462,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
setKey(PROVIDER_STATUS_KEY, txt)
MainAPI.overrideData = newCache // update all new providers
val newUpdatedCache =
newCache?.let { addNginxToJson(it) ?: it }
initAll()
for (api in apis) { // update current providers
newUpdatedCache?.get(api.javaClass.simpleName)
newCache?.get(api.javaClass.simpleName)
?.let { data ->
api.overrideWithNewData(data)
}
@ -482,14 +482,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
newCache
}?.let { providersJsonMap ->
MainAPI.overrideData = providersJsonMap
val providersJsonMapUpdated = addNginxToJson(providersJsonMap)
?: providersJsonMap // if return null, use unchanged one
initAll()
val acceptableProviders =
providersJsonMapUpdated.filter { it.value.status == PROVIDER_STATUS_OK || it.value.status == PROVIDER_STATUS_SLOW }
providersJsonMap.filter { it.value.status == PROVIDER_STATUS_OK || it.value.status == PROVIDER_STATUS_SLOW }
.map { it.key }.toSet()
val restrictedApis =
if (hasBenene) providersJsonMapUpdated.filter { it.value.status == PROVIDER_STATUS_BETA_ONLY }
if (hasBenene) providersJsonMap.filter { it.value.status == PROVIDER_STATUS_BETA_ONLY }
.map { it.key }.toSet() else emptySet()
apis = allProviders.filter { api ->
@ -506,23 +505,14 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
}
} catch (e: Exception) {
initAll()
apis = allProviders
e.printStackTrace()
logError(e)
}
} else {
initAll()
apis = allProviders
try {
val nginxProviderName = NginxProvider().name
val nginxProviderIndex = apis.indexOf(APIHolder.getApiFromName(nginxProviderName))
val createdJsonProvider = createNginxJson()
if (createdJsonProvider != null) {
apis[nginxProviderIndex].overrideWithNewData(createdJsonProvider) // people will have access to it if they disable metadata check (they are not filtered)
}
} catch (e: Exception) {
logError(e)
}
}
loadThemes(this)
@ -585,7 +575,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
loadCache()
test()
/*nav_view.setOnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.navigation_home -> {

View File

@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.animeproviders
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
import com.lagradost.cloudstream3.LoadResponse.Companion.addDuration
import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId
import com.lagradost.cloudstream3.LoadResponse.Companion.addRating
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
@ -119,16 +120,6 @@ class AnimeWorldProvider : MainAPI() {
}
override suspend fun load(url: String): LoadResponse {
fun String.parseDuration(): Int? {
val arr = this.split(" e ")
return if (arr.size == 1)
arr[0].split(' ')[0].toIntOrNull()
else
arr[1].split(' ')[0].toIntOrNull()?.let {
arr[0].removeSuffix("h").toIntOrNull()?.times(60)!!.plus(it)
}
}
val document = request(url).document
val widget = document.select("div.widget.info")
@ -140,7 +131,7 @@ class AnimeWorldProvider : MainAPI() {
val type: TvType = getType(widget.select("dd").first()?.text())
val genres = widget.select(".meta").select("a[href*=\"/genre/\"]").map { it.text() }
val rating = widget.select("#average-vote")?.text()
val rating = widget.select("#average-vote").text()
val trailerUrl = document.select(".trailer[data-url]").attr("data-url")
val malId = document.select("#mal-button").attr("href")
@ -151,7 +142,7 @@ class AnimeWorldProvider : MainAPI() {
var dub = false
var year: Int? = null
var status: ShowStatus? = null
var duration: Int? = null
var duration: String? = null
for (meta in document.select(".meta dt, .meta dd")) {
val text = meta.text()
@ -162,7 +153,7 @@ class AnimeWorldProvider : MainAPI() {
else if (status == null && text.contains("Stato"))
status = getStatus(meta.nextElementSibling()?.text())
else if (status == null && text.contains("Durata"))
duration = meta.nextElementSibling()?.text()?.parseDuration()
duration = meta.nextElementSibling()?.text()
}
val servers = document.select(".widget.servers")
@ -183,7 +174,7 @@ class AnimeWorldProvider : MainAPI() {
return newAnimeLoadResponse(title, url, type) {
engName = title
japName = otherTitle
posterUrl = poster
addPoster(poster)
this.year = year
addEpisodes(if (dub) DubStatus.Dubbed else DubStatus.Subbed, episodes)
showStatus = status
@ -192,7 +183,7 @@ class AnimeWorldProvider : MainAPI() {
addMalId(malId)
addAniListId(anlId)
addRating(rating)
this.duration = duration
addDuration(duration)
addTrailer(trailerUrl)
this.recommendations = recommendations
this.comingSoon = comingSoon

View File

@ -0,0 +1,248 @@
package com.lagradost.cloudstream3.animeproviders
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import org.jsoup.Jsoup
import java.util.*
import kotlin.collections.ArrayList
class AnimefenixProvider:MainAPI() {
override var mainUrl = "https://animefenix.com"
override var name = "Animefenix"
override val lang = "es"
override val hasMainPage = true
override val hasChromecastSupport = true
override val hasDownloadSupport = true
override val supportedTypes = setOf(
TvType.AnimeMovie,
TvType.OVA,
TvType.Anime,
)
fun getDubStatus(title: String): DubStatus {
return if (title.contains("Latino") || title.contains("Castellano"))
DubStatus.Dubbed
else DubStatus.Subbed
}
override suspend fun getMainPage(): HomePageResponse {
val urls = listOf(
Pair("$mainUrl/", "Animes"),
Pair("$mainUrl/animes?type[]=movie&order=default", "Peliculas", ),
Pair("$mainUrl/animes?type[]=ova&order=default", "OVA's", ),
)
val items = ArrayList<HomePageList>()
items.add(
HomePageList(
"Últimos episodios",
app.get(mainUrl).document.select(".capitulos-grid div.item").map {
val title = it.selectFirst("div.overtitle")?.text()
val poster = it.selectFirst("a img")?.attr("src")
val epRegex = Regex("(-(\\d+)\$|-(\\d+)\\.(\\d+))")
val url = it.selectFirst("a")?.attr("href")?.replace(epRegex,"")
?.replace("/ver/","/")
val epNum = it.selectFirst(".is-size-7")?.text()?.replace("Episodio ","")?.toIntOrNull()
newAnimeSearchResponse(title!!, url!!) {
this.posterUrl = poster
addDubStatus(getDubStatus(title), epNum)
}
})
)
urls.apmap { (url, name) ->
val response = app.get(url)
val soup = Jsoup.parse(response.text)
val home = soup.select(".list-series article").map {
val title = it.selectFirst("h3 a")?.text()
val poster = it.selectFirst("figure img")?.attr("src")
AnimeSearchResponse(
title!!,
it.selectFirst("a")?.attr("href") ?: "",
this.name,
TvType.Anime,
poster,
null,
if (title.contains("Latino")) EnumSet.of(DubStatus.Dubbed) else EnumSet.of(DubStatus.Subbed),
)
}
items.add(HomePageList(name, home))
}
if (items.size <= 0) throw ErrorLoadingException()
return HomePageResponse(items)
}
override suspend fun search(query: String): List<SearchResponse> {
return app.get("$mainUrl/animes?q=$query").document.select(".list-series article").map {
val title = it.selectFirst("h3 a")?.text()
val href = it.selectFirst("a")?.attr("href")
val image = it.selectFirst("figure img")?.attr("src")
AnimeSearchResponse(
title!!,
href!!,
this.name,
TvType.Anime,
fixUrl(image ?: ""),
null,
if (title.contains("Latino") || title.contains("Castellano")) EnumSet.of(DubStatus.Dubbed) else EnumSet.of(
DubStatus.Subbed),
)
}
}
override suspend fun load(url: String): LoadResponse {
val doc = Jsoup.parse(app.get(url, timeout = 120).text)
val poster = doc.selectFirst(".image > img")?.attr("src")
val title = doc.selectFirst("h1.title.has-text-orange")?.text()
val description = doc.selectFirst("p.has-text-light")?.text()
val genres = doc.select(".genres a").map { it.text() }
val status = when (doc.selectFirst(".is-narrow-desktop a.button")?.text()) {
"Emisión" -> ShowStatus.Ongoing
"Finalizado" -> ShowStatus.Completed
else -> null
}
val episodes = doc.select(".anime-page__episode-list li").map {
val name = it.selectFirst("span")?.text()
val link = it.selectFirst("a")?.attr("href")
Episode(link!!, name)
}.reversed()
val type = if (doc.selectFirst("ul.has-text-light")?.text()
!!.contains("Película") && episodes.size == 1
) TvType.AnimeMovie else TvType.Anime
return newAnimeLoadResponse(title!!, url, type) {
japName = null
engName = title
posterUrl = poster
addEpisodes(DubStatus.Subbed, episodes)
plot = description
tags = genres
showStatus = status
}
}
private fun cleanStreamID(input: String): String = input.replace(Regex("player=.*&amp;code=|&"),"")
data class Amazon (
@JsonProperty("file") var file : String? = null,
@JsonProperty("type") var type : String? = null,
@JsonProperty("label") var label : String? = null
)
private fun cleanExtractor(
source: String,
name: String,
url: String,
callback: (ExtractorLink) -> Unit
): Boolean {
callback(
ExtractorLink(
source,
name,
url,
"",
Qualities.Unknown.value,
false
)
)
return true
}
override suspend fun loadLinks(
data: String,
isCasting: Boolean,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
val soup = app.get(data).document
val script = soup.selectFirst(".player-container script")?.data()
if (script!!.contains("var tabsArray =")) {
val sourcesRegex = Regex("player=.*&amp;code(.*)&")
val test = sourcesRegex.findAll(script).toList()
test.apmap {
val codestream = it.value
val links = when {
codestream.contains("player=2&amp") -> "https://embedsito.com/v/"+cleanStreamID(codestream)
codestream.contains("player=3&amp") -> "https://www.mp4upload.com/embed-"+cleanStreamID(codestream)+".html"
codestream.contains("player=6&amp") -> "https://www.yourupload.com/embed/"+cleanStreamID(codestream)
codestream.contains("player=12&amp") -> "http://ok.ru/videoembed/"+cleanStreamID(codestream)
codestream.contains("player=4&amp") -> "https://sendvid.com/"+cleanStreamID(codestream)
codestream.contains("player=9&amp") -> "AmaNormal https://www.animefenix.com/stream/amz.php?v="+cleanStreamID(codestream)
codestream.contains("player=11&amp") -> "AmazonES https://www.animefenix.com/stream/amz.php?v="+cleanStreamID(codestream)
codestream.contains("player=22&amp") -> "Fireload https://www.animefenix.com/stream/fl.php?v="+cleanStreamID(codestream)
else -> ""
}
loadExtractor(links, data, callback)
argamap({
if (links.contains("AmaNormal")) {
val doc = app.get(links.replace("AmaNormal ","")).document
doc.select("script").map { script ->
if (script.data().contains("sources: [{\"file\"")) {
val text = script.data().substringAfter("sources:").substringBefore("]").replace("[","")
val json = parseJson<Amazon>(text)
if (json.file != null) {
cleanExtractor(
"Amazon",
"Amazon ${json.label}",
json.file!!,
callback
)
}
}
}
}
if (links.contains("AmazonES")) {
val amazonES = links.replace("AmazonES ", "")
val doc = app.get("$amazonES&ext=es").document
doc.select("script").map { script ->
if (script.data().contains("sources: [{\"file\"")) {
val text = script.data().substringAfter("sources:").substringBefore("]").replace("[","")
val json = parseJson<Amazon>(text)
if (json.file != null) {
cleanExtractor(
"AmazonES",
"AmazonES ${json.label}",
json.file!!,
callback
)
}
}
}
}
if (links.contains("Fireload")) {
val doc = app.get(links.replace("Fireload ", "")).document
doc.select("script").map { script ->
if (script.data().contains("sources: [{\"file\"")) {
val text = script.data().substringAfter("sources:").substringBefore("]").replace("[","")
val json = parseJson<Amazon>(text)
val testurl = if (json.file?.contains("fireload") == true) {
app.get("https://${json.file}").text
} else null
if (testurl?.contains("error") == true) {
//
} else if (json.file?.contains("fireload") == true) {
cleanExtractor(
"Fireload",
"Fireload ${json.label}",
"https://"+json.file!!,
callback
)
}
}
}
}
})
}
}
return true
}
}

View File

@ -0,0 +1,227 @@
package com.lagradost.cloudstream3.movieproviders
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
import java.util.*
import kotlin.collections.ArrayList
class AnimeflvIOProvider:MainAPI() {
override var mainUrl = "https://animeflv.io" //Also scrapes from animeid.to
override var name = "Animeflv.io"
override val lang = "es"
override val hasMainPage = true
override val hasChromecastSupport = true
override val hasDownloadSupport = true
override val supportedTypes = setOf(
TvType.AnimeMovie,
TvType.OVA,
TvType.Anime,
)
override suspend fun getMainPage(): HomePageResponse {
val items = ArrayList<HomePageList>()
val urls = listOf(
Pair("$mainUrl/series", "Series actualizadas",),
Pair("$mainUrl/peliculas", "Peliculas actualizadas"),
)
items.add(HomePageList("Estrenos", app.get(mainUrl).document.select("div#owl-demo-premiere-movies .pull-left").map{
val title = it.selectFirst("p")?.text() ?: ""
AnimeSearchResponse(
title,
fixUrl(it.selectFirst("a")?.attr("href") ?: ""),
this.name,
TvType.Anime,
it.selectFirst("img")?.attr("src"),
it.selectFirst("span.year").toString().toIntOrNull(),
EnumSet.of(DubStatus.Subbed),
)
}))
urls.apmap { (url, name) ->
val soup = app.get(url).document
val home = soup.select("div.item-pelicula").map {
val title = it.selectFirst(".item-detail p")?.text() ?: ""
val poster = it.selectFirst("figure img")?.attr("src")
AnimeSearchResponse(
title,
fixUrl(it.selectFirst("a")?.attr("href") ?: ""),
this.name,
TvType.Anime,
poster,
null,
if (title.contains("Latino") || title.contains("Castellano")) EnumSet.of(DubStatus.Dubbed) else EnumSet.of(DubStatus.Subbed),
)
}
items.add(HomePageList(name, home))
}
if (items.size <= 0) throw ErrorLoadingException()
return HomePageResponse(items)
}
override suspend fun search(query: String): List<SearchResponse> {
val headers = mapOf(
"Host" to "animeflv.io",
"User-Agent" to USER_AGENT,
"X-Requested-With" to "XMLHttpRequest",
"DNT" to "1",
"Alt-Used" to "animeflv.io",
"Connection" to "keep-alive",
"Referer" to "https://animeflv.io",
)
val url = "$mainUrl/search.html?keyword=$query"
val document = app.get(
url,
headers = headers
).document
return document.select(".item-pelicula.pull-left").map {
val title = it.selectFirst("div.item-detail p")?.text() ?: ""
val href = fixUrl(it.selectFirst("a")?.attr("href") ?: "")
var image = it.selectFirst("figure img")?.attr("src") ?: ""
val isMovie = href.contains("/pelicula/")
if (image.contains("/static/img/picture.png")) { image = ""}
if (isMovie) {
MovieSearchResponse(
title,
href,
this.name,
TvType.AnimeMovie,
image,
null
)
} else {
AnimeSearchResponse(
title,
href,
this.name,
TvType.Anime,
image,
null,
EnumSet.of(DubStatus.Subbed),
)
}
}
}
override suspend fun load(url: String): LoadResponse? {
// Gets the url returned from searching.
val soup = app.get(url).document
val title = soup.selectFirst(".info-content h1")?.text()
val description = soup.selectFirst("span.sinopsis")?.text()?.trim()
val poster: String? = soup.selectFirst(".poster img")?.attr("src")
val episodes = soup.select(".item-season-episodes a").map { li ->
val href = fixUrl(li.selectFirst("a")?.attr("href") ?: "")
val name = li.selectFirst("a")?.text() ?: ""
Episode(
href, name,
)
}.reversed()
val year = Regex("(\\d*)").find(soup.select(".info-half").text())
val tvType = if (url.contains("/pelicula/")) TvType.AnimeMovie else TvType.Anime
val genre = soup.select(".content-type-a a")
.map { it?.text()?.trim().toString().replace(", ","") }
val duration = Regex("""(\d*)""").find(
soup.select("p.info-half:nth-child(4)").text())
return when (tvType) {
TvType.Anime -> {
return newAnimeLoadResponse(title ?: "", url, tvType) {
japName = null
engName = title
posterUrl = poster
this.year = null
addEpisodes(DubStatus.Subbed, episodes)
plot = description
tags = genre
showStatus = null
}
}
TvType.AnimeMovie -> {
MovieLoadResponse(
title ?: "",
url,
this.name,
tvType,
url,
poster,
year.toString().toIntOrNull(),
description,
null,
genre,
duration.toString().toIntOrNull(),
)
}
else -> null
}
}
data class MainJson (
@JsonProperty("source") val source: List<Source>,
@JsonProperty("source_bk") val sourceBk: String?,
@JsonProperty("track") val track: List<String>?,
@JsonProperty("advertising") val advertising: List<String>?,
@JsonProperty("linkiframe") val linkiframe: String?
)
data class Source (
@JsonProperty("file") val file: String,
@JsonProperty("label") val label: String,
@JsonProperty("default") val default: String,
@JsonProperty("type") val type: String
)
override suspend fun loadLinks(
data: String,
isCasting: Boolean,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
app.get(data).document.select("li.tab-video").apmap {
val url = fixUrl(it.attr("data-video"))
if (url.contains("animeid")) {
val ajaxurl = url.replace("streaming.php","ajax.php")
val ajaxurltext = app.get(ajaxurl).text
val json = parseJson<MainJson>(ajaxurltext)
json.source.forEach { source ->
if (source.file.contains("m3u8")) {
generateM3u8(
"Animeflv.io",
source.file,
"https://animeid.to",
headers = mapOf("Referer" to "https://animeid.to")
).apmap {
callback(
ExtractorLink(
"Animeflv.io",
"Animeflv.io",
it.url,
"https://animeid.to",
getQualityFromName(it.quality.toString()),
it.url.contains("m3u8")
)
)
}
} else {
callback(
ExtractorLink(
name,
"$name ${source.label}",
source.file,
"https://animeid.to",
Qualities.Unknown.value,
isM3u8 = source.file.contains("m3u8")
)
)
}
}
}
loadExtractor(url, data, callback)
}
return true
}
}

View File

@ -183,7 +183,7 @@ class GogoanimeProvider : MainAPI() {
}
}
override var mainUrl = "https://gogoanime.film"
override var mainUrl = "https://gogoanime.sk"
override var name = "GogoAnime"
override val hasQuickSearch = false
override val hasMainPage = true

View File

@ -3,20 +3,11 @@ package com.lagradost.cloudstream3.animeproviders
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.*
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import java.util.*
import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.movieproviders.SflixProvider
import com.lagradost.cloudstream3.movieproviders.SflixProvider.Companion.extractRabbitStream
import com.lagradost.cloudstream3.movieproviders.SflixProvider.Companion.toExtractorLink
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.nicehttp.Requests.Companion.await
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import java.net.URI
class GomunimeProvider : MainAPI() {
override var mainUrl = "https://185.231.223.76"
@ -210,7 +201,8 @@ class GomunimeProvider : MainAPI() {
M3u8Helper.generateM3u8(
this.name,
link,
mainUrl,
"$mainUrl/",
headers = mapOf("Origin" to mainUrl)
).forEach(callback)
}
}

View File

@ -0,0 +1,276 @@
package com.lagradost.cloudstream3.animeproviders
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.List
class JKAnimeProvider : MainAPI() {
companion object {
fun getType(t: String): TvType {
return if (t.contains("OVA") || t.contains("Especial")) TvType.OVA
else if (t.contains("Pelicula")) TvType.AnimeMovie
else TvType.Anime
}
}
override var mainUrl = "https://jkanime.net"
override var name = "JKAnime"
override val lang = "es"
override val hasMainPage = true
override val hasChromecastSupport = true
override val hasDownloadSupport = true
override val supportedTypes = setOf(
TvType.AnimeMovie,
TvType.OVA,
TvType.Anime,
)
override suspend fun getMainPage(): HomePageResponse {
val urls = listOf(
Pair("$mainUrl/directorio/?filtro=fecha&tipo=TV&estado=1&fecha=none&temporada=none&orden=desc", "En emisión"),
Pair("$mainUrl/directorio/?filtro=fecha&tipo=none&estado=none&fecha=none&temporada=none&orden=none", "Animes"),
Pair("$mainUrl/directorio/?filtro=fecha&tipo=Movie&estado=none&fecha=none&temporada=none&orden=none", "Películas"),
)
val items = ArrayList<HomePageList>()
items.add(
HomePageList(
"Últimos episodios",
app.get(mainUrl).document.select(".listadoanime-home a.bloqq").map {
val title = it.selectFirst("h5")?.text()
val dubstat =if (title!!.contains("Latino") || title.contains("Castellano"))
DubStatus.Dubbed else DubStatus.Subbed
val poster = it.selectFirst(".anime__sidebar__comment__item__pic img")?.attr("src") ?: ""
val epRegex = Regex("/(\\d+)/|/especial/|/ova/")
val url = it.attr("href").replace(epRegex, "")
val epNum = it.selectFirst("h6")?.text()?.replace("Episodio ", "")?.toIntOrNull()
newAnimeSearchResponse(title, url) {
this.posterUrl = poster
addDubStatus(dubstat, epNum)
}
})
)
urls.apmap { (url, name) ->
val soup = app.get(url).document
val home = soup.select(".g-0").map {
val title = it.selectFirst("h5 a")?.text()
val poster = it.selectFirst("img")?.attr("src") ?: ""
AnimeSearchResponse(
title!!,
fixUrl(it.selectFirst("a")?.attr("href") ?: ""),
this.name,
TvType.Anime,
fixUrl(poster),
null,
if (title.contains("Latino") || title.contains("Castellano")) EnumSet.of(
DubStatus.Dubbed
) else EnumSet.of(DubStatus.Subbed),
)
}
items.add(HomePageList(name, home))
}
if (items.size <= 0) throw ErrorLoadingException()
return HomePageResponse(items)
}
data class MainSearch (
@JsonProperty("animes") val animes: List<Animes>,
@JsonProperty("anime_types") val animeTypes: AnimeTypes
)
data class Animes (
@JsonProperty("id") val id: String,
@JsonProperty("slug") val slug: String,
@JsonProperty("title") val title: String,
@JsonProperty("image") val image: String,
@JsonProperty("synopsis") val synopsis: String,
@JsonProperty("type") val type: String,
@JsonProperty("status") val status: String,
@JsonProperty("thumbnail") val thumbnail: String
)
data class AnimeTypes (
@JsonProperty("TV") val TV: String,
@JsonProperty("OVA") val OVA: String,
@JsonProperty("Movie") val Movie: String,
@JsonProperty("Special") val Special: String,
@JsonProperty("ONA") val ONA: String,
@JsonProperty("Music") val Music: String
)
override suspend fun search(query: String): List<SearchResponse> {
val main = app.get("$mainUrl/ajax/ajax_search/?q=$query").text
val json = parseJson<MainSearch>(main)
return json.animes.map {
val title = it.title
val href = "$mainUrl/${it.slug}"
val image = "https://cdn.jkanime.net/assets/images/animes/image/${it.slug}.jpg"
AnimeSearchResponse(
title,
href,
this.name,
TvType.Anime,
image,
null,
if (title.contains("Latino") || title.contains("Castellano")) EnumSet.of(
DubStatus.Dubbed
) else EnumSet.of(DubStatus.Subbed),
)
}
}
override suspend fun load(url: String): LoadResponse {
val doc = app.get(url, timeout = 120).document
val poster = doc.selectFirst(".set-bg")?.attr("data-setbg")
val title = doc.selectFirst(".anime__details__title > h3")?.text()
val type = doc.selectFirst(".anime__details__text")?.text()
val description = doc.selectFirst(".anime__details__text > p")?.text()
val genres = doc.select("div.col-lg-6:nth-child(1) > ul:nth-child(1) > li:nth-child(2) > a").map { it.text() }
val status = when (doc.selectFirst("span.enemision")?.text()) {
"En emisión" -> ShowStatus.Ongoing
"Concluido" -> ShowStatus.Completed
else -> null
}
val animeID = doc.selectFirst("div.ml-2")?.attr("data-anime")?.toInt()
val animeeps = "$mainUrl/ajax/last_episode/$animeID/"
val jsoneps = app.get(animeeps).text
val lastepnum = jsoneps.substringAfter("{\"number\":\"").substringBefore("\",\"title\"").toInt()
val episodes = (1..lastepnum).map {
val link = "${url.removeSuffix("/")}/$it"
Episode(link)
}
return newAnimeLoadResponse(title!!, url, getType(type!!)) {
posterUrl = poster
addEpisodes(DubStatus.Subbed, episodes)
showStatus = status
plot = description
tags = genres
}
}
data class Nozomi (
@JsonProperty("file") val file: String?
)
private fun streamClean(
name: String,
url: String,
referer: String,
quality: String?,
callback: (ExtractorLink) -> Unit,
m3u8: Boolean
): Boolean {
callback(
ExtractorLink(
name,
name,
url,
referer,
getQualityFromName(quality),
m3u8
)
)
return true
}
override suspend fun loadLinks(
data: String,
isCasting: Boolean,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
app.get(data).document.select("script").apmap { script ->
if (script.data().contains("var video = []")) {
val videos = script.data().replace("\\/", "/")
fetchUrls(videos).map {
it.replace("$mainUrl/jkfembed.php?u=","https://embedsito.com/v/")
.replace("$mainUrl/jkokru.php?u=","http://ok.ru/videoembed/")
.replace("$mainUrl/jkvmixdrop.php?u=","https://mixdrop.co/e/")
.replace("$mainUrl/jk.php?u=","$mainUrl/")
}.apmap { link ->
loadExtractor(link, data, callback)
if (link.contains("um2.php")) {
val doc = app.get(link, referer = data).document
val gsplaykey = doc.select("form input[value]").attr("value")
val postgsplay = app.post("$mainUrl/gsplay/redirect_post.php",
headers = mapOf(
"Host" to "jkanime.net",
"User-Agent" to USER_AGENT,
"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" to link,
"Content-Type" to "application/x-www-form-urlencoded",
"Origin" to "https://jkanime.net",
"DNT" to "1",
"Connection" to "keep-alive",
"Upgrade-Insecure-Requests" to "1",
"Sec-Fetch-Dest" to "iframe",
"Sec-Fetch-Mode" to "navigate",
"Sec-Fetch-Site" to "same-origin",
"TE" to "trailers",
"Pragma" to "no-cache",
"Cache-Control" to "no-cache",),
data = mapOf(Pair("data",gsplaykey)),
allowRedirects = false).okhttpResponse.headers.values("location").apmap { loc ->
val postkey = loc.replace("/gsplay/player.html#","")
val nozomitext = app.post("$mainUrl/gsplay/api.php",
headers = mapOf(
"Host" to "jkanime.net",
"User-Agent" to USER_AGENT,
"Accept" to "application/json, text/javascript, */*; q=0.01",
"Accept-Language" to "en-US,en;q=0.5",
"Content-Type" to "application/x-www-form-urlencoded; charset=UTF-8",
"X-Requested-With" to "XMLHttpRequest",
"Origin" to "https://jkanime.net",
"DNT" to "1",
"Connection" to "keep-alive",
"Sec-Fetch-Dest" to "empty",
"Sec-Fetch-Mode" to "cors",
"Sec-Fetch-Site" to "same-origin",),
data = mapOf(Pair("v",postkey)),
allowRedirects = false
).text
val json = parseJson<Nozomi>(nozomitext)
val nozomiurl = listOf(json.file)
if (nozomiurl.isEmpty()) null else
nozomiurl.forEach { url ->
val nozominame = "Nozomi"
streamClean(nozominame, url!!, "", null, callback, url.contains(".m3u8"))
}
}
}
if (link.contains("um.php")) {
val desutext = app.get(link, referer = data).text
val desuRegex = Regex("((https:|http:)\\/\\/.*\\.m3u8)")
val file = desuRegex.find(desutext)?.value
val namedesu = "Desu"
generateM3u8(
namedesu,
file!!,
mainUrl,
).forEach { desurl ->
streamClean(namedesu, desurl.url, mainUrl, desurl.quality.toString(), callback, true)
}
}
if (link.contains("jkmedia")) {
app.get(link, referer = data, allowRedirects = false).okhttpResponse.headers.values("location").apmap { xtremeurl ->
val namex = "Xtreme S"
streamClean(namex, xtremeurl, "", null, callback, xtremeurl.contains(".m3u8"))
}
}
}
}
}
return true
}
}

View File

@ -1,6 +1,5 @@
package com.lagradost.cloudstream3.animeproviders
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
import com.lagradost.cloudstream3.mvvm.safeApiCall
@ -157,6 +156,8 @@ class KuronimeProvider : MainAPI() {
val token = data.substringAfter("var token = \"").substringBefore("\";")
val pat = data.substringAfter("var pat = \"").substringBefore("\";")
val link = "$doma$token$pat/index.m3u8"
val quality =
Regex("\\d{3,4}p").find(doc.select("title").text())?.groupValues?.get(0)
sourceCallback.invoke(
ExtractorLink(
@ -164,7 +165,8 @@ class KuronimeProvider : MainAPI() {
this.name,
link,
referer = "https://animeku.org/",
quality = Qualities.Unknown.value,
quality = getQualityFromName(quality),
headers = mapOf("Origin" to "https://animeku.org"),
isM3u8 = true
)
)
@ -186,7 +188,7 @@ class KuronimeProvider : MainAPI() {
sources.apmap {
safeApiCall {
when {
it.contains("animeku.org") -> invokeKuroSource(it, callback)
it.startsWith("https://animeku.org") -> invokeKuroSource(it, callback)
else -> loadExtractor(it, mainUrl, callback)
}
}

View File

@ -0,0 +1,217 @@
package com.lagradost.cloudstream3.animeproviders
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
import java.util.*
import kotlin.collections.ArrayList
class MundoDonghuaProvider : MainAPI() {
override var mainUrl = "https://www.mundodonghua.com"
override var name = "MundoDonghua"
override val lang = "es"
override val hasMainPage = true
override val hasChromecastSupport = true
override val hasDownloadSupport = true
override val supportedTypes = setOf(
TvType.Anime,
)
override suspend fun getMainPage(): HomePageResponse {
val urls = listOf(
Pair("$mainUrl/lista-donghuas", "Donghuas"),
)
val items = ArrayList<HomePageList>()
items.add(
HomePageList(
"Últimos episodios",
app.get(mainUrl, timeout = 120).document.select("div.row .col-xs-4").map {
val title = it.selectFirst("h5")?.text() ?: ""
val poster = it.selectFirst(".fit-1 img")?.attr("src")
val epRegex = Regex("(\\/(\\d+)\$)")
val url = it.selectFirst("a")?.attr("href")?.replace(epRegex,"")?.replace("/ver/","/donghua/")
val epnumRegex = Regex("((\\d+)$)")
val epNum = epnumRegex.find(title)?.value?.toIntOrNull()
val dubstat = if (title.contains("Latino") || title.contains("Castellano")) DubStatus.Dubbed else DubStatus.Subbed
newAnimeSearchResponse(title.replace(Regex("Episodio|(\\d+)"),"").trim(), fixUrl(url ?: "")) {
this.posterUrl = fixUrl(poster ?: "")
addDubStatus(dubstat, epNum)
}
})
)
urls.apmap { (url, name) ->
val home = app.get(url, timeout = 120).document.select(".col-xs-4").map {
val title = it.selectFirst(".fs-14")?.text() ?: ""
val poster = it.selectFirst(".fit-1 img")?.attr("src") ?: ""
AnimeSearchResponse(
title,
fixUrl(it.selectFirst("a")?.attr("href") ?: ""),
this.name,
TvType.Anime,
fixUrl(poster),
null,
if (title.contains("Latino") || title.contains("Castellano")) EnumSet.of(
DubStatus.Dubbed
) else EnumSet.of(DubStatus.Subbed),
)
}
items.add(HomePageList(name, home))
}
if (items.size <= 0) throw ErrorLoadingException()
return HomePageResponse(items)
}
override suspend fun search(query: String): List<SearchResponse> {
return app.get("$mainUrl/busquedas/$query", timeout = 120).document.select(".col-xs-4").map {
val title = it.selectFirst(".fs-14")?.text() ?: ""
val href = fixUrl(it.selectFirst("a")?.attr("href") ?: "")
val image = it.selectFirst(".fit-1 img")?.attr("src")
AnimeSearchResponse(
title,
href,
this.name,
TvType.Anime,
fixUrl(image ?: ""),
null,
if (title.contains("Latino") || title.contains("Castellano")) EnumSet.of(
DubStatus.Dubbed
) else EnumSet.of(DubStatus.Subbed),
)
}
}
override suspend fun load(url: String): LoadResponse {
val doc = app.get(url, timeout = 120).document
val poster = doc.selectFirst("head meta[property=og:image]")?.attr("content") ?: ""
val title = doc.selectFirst(".ls-title-serie")?.text() ?: ""
val description = doc.selectFirst("p.text-justify.fc-dark")?.text() ?: ""
val genres = doc.select("span.label.label-primary.f-bold").map { it.text() }
val status = when (doc.selectFirst("div.col-md-6.col-xs-6.align-center.bg-white.pt-10.pr-15.pb-0.pl-15 p span.badge.bg-default")?.text()) {
"En Emisión" -> ShowStatus.Ongoing
"Finalizada" -> ShowStatus.Completed
else -> null
}
val episodes = doc.select("ul.donghua-list a").map {
val name = it.selectFirst(".fs-16")?.text()
val link = it.attr("href")
Episode(fixUrl(link), name)
}.reversed()
val typeinfo = doc.select("div.row div.col-md-6.pl-15 p.fc-dark").text()
val tvType = if (typeinfo.contains(Regex("Tipo.*Pel.cula"))) TvType.AnimeMovie else TvType.Anime
return newAnimeLoadResponse(title, url, tvType) {
posterUrl = poster
addEpisodes(DubStatus.Subbed, episodes)
showStatus = status
plot = description
tags = genres
}
}
data class Protea (
@JsonProperty("source") val source: List<Source>,
@JsonProperty("poster") val poster: String?
)
data class Source (
@JsonProperty("file") val file: String,
@JsonProperty("label") val label: String?,
@JsonProperty("type") val type: String?,
@JsonProperty("default") val default: String?
)
private fun cleanStream(
name: String,
url: String,
qualityString: String?,
callback: (ExtractorLink) -> Unit,
isM3U8: Boolean
): Boolean {
callback(
ExtractorLink(
name,
name,
url,
"",
getQualityFromName(qualityString),
isM3U8
)
)
return true
}
override suspend fun loadLinks(
data: String,
isCasting: Boolean,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
app.get(data).document.select("script").apmap { script ->
if (script.data().contains("eval(function(p,a,c,k,e")) {
val packedRegex = Regex("eval\\(function\\(p,a,c,k,e,.*\\)\\)")
packedRegex.findAll(script.data()).map {
it.value
}.toList().apmap {
val unpack = getAndUnpack(it).replace("diasfem","embedsito")
fetchUrls(unpack).apmap { url ->
loadExtractor(url, data, callback)
}
if (unpack.contains("protea_tab")) {
val protearegex = Regex("(protea_tab.*slug.*,type)")
val slug = protearegex.findAll(unpack).map {
it.value.replace(Regex("(protea_tab.*slug\":\")"),"").replace("\"},type","")
}.first()
val requestlink = "$mainUrl/api_donghua.php?slug=$slug"
val response = app.get(requestlink, headers =
mapOf("Host" to "www.mundodonghua.com",
"User-Agent" to USER_AGENT,
"Accept" to "*/*",
"Accept-Language" to "en-US,en;q=0.5",
"Referer" to data,
"X-Requested-With" to "XMLHttpRequest",
"DNT" to "1",
"Connection" to "keep-alive",
"Sec-Fetch-Dest" to "empty",
"Sec-Fetch-Mode" to "no-cors",
"Sec-Fetch-Site" to "same-origin",
"TE" to "trailers",
"Pragma" to "no-cache",
"Cache-Control" to "no-cache",)
).text.removePrefix("[").removeSuffix("]")
val json = parseJson<Protea>(response)
json.source.forEach { source ->
val protename = "Protea"
cleanStream(protename, fixUrl(source.file), source.label, callback, false)
}
}
if (unpack.contains("asura_player")) {
val asuraRegex = Regex("(asura_player.*type)")
asuraRegex.findAll(unpack).map {
it.value
}.toList().apmap { protea ->
val asuraname = "Asura"
val file = protea.substringAfter("{file:\"").substringBefore("\"")
generateM3u8(
asuraname,
file,
""
).forEach {
cleanStream(asuraname, it.url, it.quality.toString(), callback, true)
}
}
}
}
}
}
return true
}
}

View File

@ -1,8 +1,6 @@
package com.lagradost.cloudstream3.animeproviders
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.loadExtractor
import org.jsoup.nodes.Element
@ -33,6 +31,7 @@ class NeonimeProvider : MainAPI() {
return when (t) {
"Ended" -> ShowStatus.Completed
"OnGoing" -> ShowStatus.Ongoing
"Ongoing" -> ShowStatus.Ongoing
"In Production" -> ShowStatus.Ongoing
"Returning Series" -> ShowStatus.Ongoing
else -> ShowStatus.Completed

View File

@ -64,7 +64,7 @@ class NineAnimeProvider : MainAPI() {
}
//Credits to https://github.com/jmir1
private val key = "0wMrYU+ixjJ4QdzgfN2HlyIVAt3sBOZnCT9Lm7uFDovkb/EaKpRWhqXS5168ePcG"
private val key = "c/aUAorINHBLxWTy3uRiPt8J+vjsOheFG1E0q2X9CYwDZlnmd4Kb5M6gSVzfk7pQ" //key credits to @Modder4869
private fun getVrf(id: String): String? {
val reversed = ue(encode(id) + "0000000").slice(0..5).reversed()
@ -283,7 +283,8 @@ class NineAnimeProvider : MainAPI() {
jsonservers.vidstream,
jsonservers.mcloud,
jsonservers.mp4upload,
jsonservers.streamtape
jsonservers.streamtape,
jsonservers.videovard
).mapNotNull {
try {
val epserver = app.get("$mainUrl/ajax/anime/episode?id=$it").text

View File

@ -47,7 +47,6 @@ class TenshiProvider : MainAPI() {
override suspend fun getMainPage(): HomePageResponse {
val items = ArrayList<HomePageList>()
val soup = app.get(mainUrl, interceptor = ddosGuardKiller).document
println(soup)
for (section in soup.select("#content > section")) {
try {
if (section.attr("id") == "toplist-tabs") {

View File

@ -4,12 +4,17 @@ import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.getQualityFromName
import kotlinx.coroutines.delay
class DoodCxExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.cx"
}
class DoodShExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.sh"
}
class DoodPmExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.pm"
}
@ -40,13 +45,14 @@ open class DoodLaExtractor : ExtractorApi() {
val response0 = app.get(url).text // html of DoodStream page to look for /pass_md5/...
val md5 =mainUrl+(Regex("/pass_md5/[^']*").find(response0)?.value ?: return null) // get https://dood.ws/pass_md5/...
val trueUrl = app.get(md5, referer = url).text + "zUEJeL3mUN?token=" + md5.substringAfterLast("/") //direct link to extract (zUEJeL3mUN is random)
val quality = Regex("\\d{3,4}p").find(response0.substringAfter("<title>").substringBefore("</title>"))?.groupValues?.get(0)
return listOf(
ExtractorLink(
trueUrl,
this.name,
trueUrl,
mainUrl,
Qualities.Unknown.value,
getQualityFromName(quality),
false
)
) // links are valid in 8h

View File

@ -0,0 +1,36 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
open class GuardareStream : ExtractorApi() {
override var name = "Guardare"
override var mainUrl = "https://guardare.stream"
override val requiresReferer = false
data class GuardareJsonData (
@JsonProperty("data") val data : List<GuardareData>,
)
data class GuardareData (
@JsonProperty("file") val file : String,
@JsonProperty("label") val label : String,
@JsonProperty("type") val type : String
)
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val response = app.post(url.replace("/v/","/api/source/"), data = mapOf("d" to mainUrl)).text
val jsonvideodata = AppUtils.parseJson<GuardareJsonData>(response)
return jsonvideodata.data.map {
ExtractorLink(
it.file+".${it.type}",
this.name,
it.file+".${it.type}",
mainUrl,
it.label.filter{ it.isDigit() }.toInt(),
false
)
}
}
}

View File

@ -0,0 +1,29 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
open class Maxstream : ExtractorApi() {
override var name = "Maxstream"
override var mainUrl = "https://maxstream.video/"
override val requiresReferer = false
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
val response = app.get(url).text
val jstounpack = Regex("cript\">eval((.|\\n)*?)</script>").find(response)?.groups?.get(1)?.value
val unpacjed = JsUnpacker(jstounpack).unpack()
val extractedUrl = unpacjed?.let { Regex("""src:"((.|\n)*?)",type""").find(it) }?.groups?.get(1)?.value.toString()
M3u8Helper.generateM3u8(
name,
extractedUrl,
url,
headers = mapOf("referer" to url)
).forEach { link ->
extractedLinksList.add(link)
}
return extractedLinksList
}
}

View File

@ -4,10 +4,15 @@ import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.extractors.WcoStream.Companion.cipher
import com.lagradost.cloudstream3.extractors.WcoStream.Companion.encrypt
import com.lagradost.cloudstream3.extractors.WcoStream.Companion.keytwo
import com.lagradost.cloudstream3.extractors.helper.WcoHelper.Companion.getNewWcoKey
import com.lagradost.cloudstream3.extractors.helper.WcoHelper.Companion.getWcoKey
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
open class Mcloud : ExtractorApi() {
override var name = "Mcloud"
@ -27,42 +32,49 @@ open class Mcloud : ExtractorApi() {
"Referer" to "https://animekisa.in/", //Referer works for wco and animekisa, probably with others too
"Pragma" to "no-cache",
"Cache-Control" to "no-cache",)
private val key = "LCbu3iYC7ln24K7P" // key credits @Modder4869
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val link = url.replace("$mainUrl/e/","$mainUrl/info/")
val response = app.get(link, headers = headers).text
val id = url.substringAfter("e/").substringAfter("embed/").substringBefore("?")
val keys = getNewWcoKey()
keytwo = keys?.encryptKey ?: return null
val encryptedid = encrypt(cipher(keys.cipherkey!!, encrypt(id))).replace("/", "_").replace("=","")
val link = "$mainUrl/mediainfo/$encryptedid?key=${keys.mainKey}"
val response = app.get(link, referer = "https://animekisa.in/").text
if(response.startsWith("<!DOCTYPE html>")) {
// TODO decrypt html for link
return emptyList()
}
data class Sources (
@JsonProperty("file") val file: String
data class SourcesMcloud (
@JsonProperty("file" ) val file : String
)
data class Media (
@JsonProperty("sources") val sources: List<Sources>
data class MediaMcloud (
@JsonProperty("sources" ) val sources : ArrayList<SourcesMcloud> = arrayListOf()
)
data class DataMcloud (
@JsonProperty("media" ) val media : MediaMcloud? = MediaMcloud()
)
data class JsonMcloud (
@JsonProperty("success") val success: Boolean,
@JsonProperty("media") val media: Media,
@JsonProperty("status" ) val status : Int? = null,
@JsonProperty("data" ) val data : DataMcloud = DataMcloud()
)
val mapped = parseJson<JsonMcloud>(response)
val sources = mutableListOf<ExtractorLink>()
if (mapped.success)
mapped.media.sources.apmap {
val checkfile = mapped.status == 200
if (checkfile)
mapped.data.media?.sources?.apmap {
if (it.file.contains("m3u8")) {
M3u8Helper.generateM3u8(
name,
it.file,
url,
headers = app.get(url).headers.toMap()
).forEach { link ->
sources.add(link)
}
sources.addAll(
generateM3u8(
name,
it.file,
url,
headers = mapOf("Referer" to url)
)
)
}
}
return sources

View File

@ -0,0 +1,45 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName
class Solidfiles : ExtractorApi() {
override val name = "Solidfiles"
override val mainUrl = "https://www.solidfiles.com"
override val requiresReferer = false
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val sources = mutableListOf<ExtractorLink>()
with(app.get(url).document) {
this.select("script").map { script ->
if (script.data().contains("\"streamUrl\":")) {
val data = script.data().substringAfter("constant('viewerOptions', {").substringBefore("});")
val source = tryParseJson<ResponseSource>("{$data}")
val quality = Regex("\\d{3,4}p").find(source!!.nodeName)?.groupValues?.get(0)
sources.add(
ExtractorLink(
name,
name,
source.streamUrl,
referer = url,
quality = getQualityFromName(quality)
)
)
}
}
}
return sources
}
private data class ResponseSource(
@JsonProperty("streamUrl") val streamUrl: String,
@JsonProperty("nodeName") val nodeName: String
)
}

View File

@ -0,0 +1,41 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
data class Files(
@JsonProperty("file") val id: String,
@JsonProperty("label") val label: String? = null,
)
open class Supervideo : ExtractorApi() {
override var name = "Supervideo"
override var mainUrl = "https://supervideo.tv"
override val requiresReferer = false
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
val response = app.get(url).text
val jstounpack = Regex("eval((.|\\n)*?)</script>").find(response)?.groups?.get(1)?.value
val unpacjed = JsUnpacker(jstounpack).unpack()
val extractedUrl = unpacjed?.let { Regex("""sources:((.|\n)*?)image""").find(it) }?.groups?.get(1)?.value.toString().replace("file",""""file"""").replace("label",""""label"""").substringBeforeLast(",")
val parsedlinks = parseJson<List<Files>>(extractedUrl)
parsedlinks.forEach { data ->
if (data.label.isNullOrBlank()){ // mp4 links (with labels) are slow. Use only m3u8 link.
M3u8Helper.generateM3u8(
name,
data.id,
url,
headers = mapOf("referer" to url)
).forEach { link ->
extractedLinksList.add(link)
}
}
}
return extractedLinksList
}
}

View File

@ -0,0 +1,42 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.ExtractorLink
open class Tantifilm : ExtractorApi() {
override var name = "Tantifilm"
override var mainUrl = "https://cercafilm.net"
override val requiresReferer = false
data class TantifilmJsonData (
@JsonProperty("success") val success : Boolean,
@JsonProperty("data") val data : List<TantifilmData>,
@JsonProperty("captions")val captions : List<String>,
@JsonProperty("is_vr") val is_vr : Boolean
)
data class TantifilmData (
@JsonProperty("file") val file : String,
@JsonProperty("label") val label : String,
@JsonProperty("type") val type : String
)
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val link = "$mainUrl/api/source/${url.substringAfterLast("/")}"
val response = app.post(link).text.replace("""\""","")
val jsonvideodata = parseJson<TantifilmJsonData>(response)
return jsonvideodata.data.map {
ExtractorLink(
it.file+".${it.type}",
this.name,
it.file+".${it.type}",
mainUrl,
it.label.filter{ it.isDigit() }.toInt(),
false
)
}
}
}

View File

@ -0,0 +1,117 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
import org.mozilla.javascript.Context
import org.mozilla.javascript.EvaluatorException
import org.mozilla.javascript.Scriptable
import java.util.*
open class Userload : ExtractorApi() {
override var name = "Userload"
override var mainUrl = "https://userload.co"
override val requiresReferer = false
private fun splitInput(input: String): List<String> {
var counter = 0
val array = ArrayList<String>()
var buffer = ""
for (c in input) {
when (c) {
'(' -> counter++
')' -> counter--
else -> {}
}
buffer += c
if (counter == 0) {
if (buffer.isNotBlank() && buffer != "+")
array.add(buffer)
buffer = ""
}
}
return array
}
private fun evaluateMath(mathExpression : String): String {
val rhino = Context.enter()
rhino.initStandardObjects()
rhino.optimizationLevel = -1
val scope: Scriptable = rhino.initStandardObjects()
return try {
rhino.evaluateString(scope, "eval($mathExpression)", "JavaScript", 1, null).toString()
}
catch (e: EvaluatorException){
""
}
}
private fun decodeVideoJs(text: String): List<String> {
text.replace("""\s+|/\*.*?\*/""".toRegex(), "")
val data = text.split("""+(゚Д゚)[゚o゚]""")[1]
val chars = data.split("""+ (゚Д゚)[゚ε゚]+""").drop(1)
val newchars = chars.map { char ->
char.replace("(o゚ー゚o)", "u")
.replace("c", "0")
.replace("(゚Д゚)['0']", "c")
.replace("゚Θ゚", "1")
.replace("!+[]", "1")
.replace("-~", "1+")
.replace("o", "3")
.replace("_", "3")
.replace("゚ー゚", "4")
.replace("(+", "(")
}
val subchar = mutableListOf<String>()
newchars.dropLast(1).forEach { v ->
subchar.add(splitInput(v).map { evaluateMath(it).substringBefore(".") }.toString().filter { it.isDigit() })
}
var txtresult = ""
subchar.forEach{
txtresult = txtresult.plus(Char(it.toInt(8)))
}
val val1 = Regex(""""morocco="((.|\\n)*?)"&mycountry="""").find(txtresult)?.groups?.get(1)?.value.toString().drop(1).dropLast(1)
val val2 = txtresult.substringAfter("""&mycountry="+""").substringBefore(")")
return listOf(
val1,
val2
)
}
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
val response = app.get(url).text
val jsToUnpack = Regex("ext/javascript\">eval((.|\\n)*?)</script>").find(response)?.groups?.get(1)?.value
val unpacked = JsUnpacker(jsToUnpack).unpack()
val videoJs = app.get("$mainUrl/api/assets/userload/js/videojs.js")
val videoJsToDecode = videoJs.text
val values = decodeVideoJs(videoJsToDecode)
val morocco = unpacked!!.split(";").filter { it.contains(values[0]) }[0].split("=")[1].drop(1).dropLast(1)
val mycountry = unpacked.split(";").filter { it.contains(values[1]) }[0].split("=")[1].drop(1).dropLast(1)
val videoLinkPage = app.post("$mainUrl/api/request/", data = mapOf(
"morocco" to morocco,
"mycountry" to mycountry
))
val videoLink = videoLinkPage.text
val nameSource = app.get(url).document.head().selectFirst("title")!!.text()
extractedLinksList.add(
ExtractorLink(
name,
name,
videoLink,
mainUrl,
getQualityFromName(nameSource),
)
)
return extractedLinksList
}
}

View File

@ -0,0 +1,271 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
import kotlinx.coroutines.delay
import java.math.BigInteger
class VideovardSX : WcoStream() {
override var mainUrl = "https://videovard.sx"
}
class VideoVard : ExtractorApi() {
override var name = "Videovard" // Cause works for animekisa and wco
override var mainUrl = "https://videovard.to"
override val requiresReferer = false
//The following code was extracted from https://github.com/saikou-app/saikou/blob/main/app/src/main/java/ani/saikou/parsers/anime/extractors/VideoVard.kt
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val id = url.substringAfter("e/").substringBefore("/")
val sources = mutableListOf<ExtractorLink>()
val hash = app.get("$mainUrl/api/make/download/$id").parsed<HashResponse>()
delay(11_000)
val resm3u8 = app.post(
"$mainUrl/api/player/setup",
mapOf("Referer" to "$mainUrl/"),
data = mapOf(
"cmd" to "get_stream",
"file_code" to id,
"hash" to hash.hash!!
)
).parsed<SetupResponse>()
val m3u8 = decode(resm3u8.src!!, resm3u8.seed)
sources.addAll(
generateM3u8(
name,
m3u8,
mainUrl,
headers = mapOf("Referer" to mainUrl)
)
)
return sources
}
companion object {
private val big0 = 0.toBigInteger()
private val big3 = 3.toBigInteger()
private val big4 = 4.toBigInteger()
private val big15 = 15.toBigInteger()
private val big16 = 16.toBigInteger()
private val big255 = 255.toBigInteger()
private fun decode(dataFile: String, seed: String): String {
val dataSeed = replace(seed)
val newDataSeed = binaryDigest(dataSeed)
val newDataFile = bytes2blocks(ascii2bytes(dataFile))
var list = listOf(1633837924, 1650680933).map { it.toBigInteger() }
val xorList = mutableListOf<BigInteger>()
for (i in newDataFile.indices step 2) {
val temp = newDataFile.slice(i..i + 1)
xorList += xorBlocks(list, tearDecode(temp, newDataSeed))
list = temp
}
val result = replace(unPad(blocks2bytes(xorList)).map { it.toInt().toChar() }.joinToString(""))
return padLastChars(result)
}
private fun binaryDigest(input: String): List<BigInteger> {
val keys = listOf(1633837924, 1650680933, 1667523942, 1684366951).map { it.toBigInteger() }
var list1 = keys.slice(0..1)
var list2 = list1
val blocks = bytes2blocks(digestPad(input))
for (i in blocks.indices step 4) {
list1 = tearCode(xorBlocks(blocks.slice(i..i + 1), list1), keys).toMutableList()
list2 = tearCode(xorBlocks(blocks.slice(i + 2..i + 3), list2), keys).toMutableList()
val temp = list1[0]
list1[0] = list1[1]
list1[1] = list2[0]
list2[0] = list2[1]
list2[1] = temp
}
return listOf(list1[0], list1[1], list2[0], list2[1])
}
private fun tearDecode(a90: List<BigInteger>, a91: List<BigInteger>): MutableList<BigInteger> {
var (a95, a96) = a90
var a97 = (-957401312).toBigInteger()
for (_i in 0 until 32) {
a96 -= ((((a95 shl 4) xor rShift(a95, 5)) + a95) xor (a97 + a91[rShift(a97, 11).and(3.toBigInteger()).toInt()]))
a97 += 1640531527.toBigInteger()
a95 -= ((((a96 shl 4) xor rShift(a96, 5)) + a96) xor (a97 + a91[a97.and(3.toBigInteger()).toInt()]))
}
return mutableListOf(a95, a96)
}
private fun digestPad(string: String): List<BigInteger> {
val empList = mutableListOf<BigInteger>()
val length = string.length
val extra = big15 - (length.toBigInteger() % big16)
empList.add(extra)
for (i in 0 until length) {
empList.add(string[i].code.toBigInteger())
}
for (i in 0 until extra.toInt()) {
empList.add(big0)
}
return empList
}
private fun bytes2blocks(a22: List<BigInteger>): List<BigInteger> {
val empList = mutableListOf<BigInteger>()
val length = a22.size
var listIndex = 0
for (i in 0 until length) {
val subIndex = i % 4
val shiftedByte = a22[i] shl (3 - subIndex) * 8
if (subIndex == 0) {
empList.add(shiftedByte)
} else {
empList[listIndex] = empList[listIndex] or shiftedByte
}
if (subIndex == 3) listIndex += 1
}
return empList
}
private fun blocks2bytes(inp: List<BigInteger>): List<BigInteger> {
val tempList = mutableListOf<BigInteger>()
inp.indices.forEach { i ->
tempList += (big255 and rShift(inp[i], 24))
tempList += (big255 and rShift(inp[i], 16))
tempList += (big255 and rShift(inp[i], 8))
tempList += (big255 and inp[i])
}
return tempList
}
private fun unPad(a46: List<BigInteger>): List<BigInteger> {
val evenOdd = a46[0].toInt().mod(2)
return (1 until (a46.size - evenOdd)).map {
a46[it]
}
}
private fun xorBlocks(a76: List<BigInteger>, a77: List<BigInteger>): List<BigInteger> {
return listOf(a76[0] xor a77[0], a76[1] xor a77[1])
}
private fun rShift(input: BigInteger, by: Int): BigInteger {
return (input.mod(4294967296.toBigInteger()) shr by)
}
private fun tearCode(list1: List<BigInteger>, list2: List<BigInteger>): MutableList<BigInteger> {
var a1 = list1[0]
var a2 = list1[1]
var temp = big0
for (_i in 0 until 32) {
a1 += (a2 shl 4 xor rShift(a2, 5)) + a2 xor temp + list2[(temp and big3).toInt()]
temp -= 1640531527.toBigInteger()
a2 += (a1 shl 4 xor rShift(a1, 5)) + a1 xor temp + list2[(rShift(temp, 11) and big3).toInt()]
}
return mutableListOf(a1, a2)
}
private fun ascii2bytes(input: String): List<BigInteger> {
val abc = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
val abcMap = abc.mapIndexed { i, c -> c to i.toBigInteger() }.toMap()
var index = -1
val length = input.length
var listIndex = 0
val bytes = mutableListOf<BigInteger>()
while (true) {
for (i in input) {
if (abc.contains(i)) {
index++
break
}
}
bytes.add((abcMap[input.getOrNull(index)?:return bytes]!! * big4))
while (true) {
index++
if (abc.contains(input[index])) {
break
}
}
var temp = abcMap[input[index]]!!
bytes[listIndex] = bytes[listIndex] or rShift(temp, 4)
listIndex++
temp = (big15.and(temp))
if ((temp == big0) && (index == (length - 1))) return bytes
bytes.add((temp * big4 * big4))
while (true) {
index++
if (index >= length) return bytes
if (abc.contains(input[index])) break
}
temp = abcMap[input[index]]!!
bytes[listIndex] = bytes[listIndex] or rShift(temp, 2)
listIndex++
temp = (big3 and temp)
if ((temp == big0) && (index == (length - 1))) {
return bytes
}
bytes.add((temp shl 6))
for (i in input) {
index++
if (abc.contains(input[index])) {
break
}
}
bytes[listIndex] = bytes[listIndex] or abcMap[input[index]]!!
listIndex++
}
}
private fun replace(a: String): String {
val map = mapOf(
'0' to '5',
'1' to '6',
'2' to '7',
'5' to '0',
'6' to '1',
'7' to '2'
)
var b = ""
a.forEach {
b += if (map.containsKey(it)) map[it] else it
}
return b
}
private fun padLastChars(input:String):String{
return if(input.reversed()[3].isDigit()) input
else input.dropLast(4)
}
private data class HashResponse(
val hash: String? = null,
val version:String? = null
)
private data class SetupResponse(
val seed: String,
val src: String?=null,
val link:String?=null
)
}
}

View File

@ -3,6 +3,8 @@ package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.extractors.helper.WcoHelper.Companion.getNewWcoKey
import com.lagradost.cloudstream3.extractors.helper.WcoHelper.Companion.getWcoKey
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
@ -45,43 +47,103 @@ class VizcloudDigital : WcoStream() {
override var mainUrl = "https://vizcloud.digital"
}
class VizcloudCloud : WcoStream() {
override var mainUrl = "https://vizcloud.cloud"
}
open class WcoStream : ExtractorApi() {
override var name = "VidStream" // Cause works for animekisa and wco
override var mainUrl = "https://vidstream.pro"
override val requiresReferer = false
companion object {
var keytwo = ""
fun encrypt(input: String): String {
if (input.any { it.code >= 256 }) throw Exception("illegal characters!")
var output = ""
for (i in input.indices step 3) {
val a = intArrayOf(-1, -1, -1, -1)
a[0] = input[i].code shr 2
a[1] = (3 and input[i].code) shl 4
if (input.length > i + 1) {
a[1] = a[1] or (input[i + 1].code shr 4)
a[2] = (15 and input[i + 1].code) shl 2
}
if (input.length > i + 2) {
a[2] = a[2] or (input[i + 2].code shr 6)
a[3] = 63 and input[i + 2].code
}
for (n in a) {
if (n == -1) output += "="
else {
if (n in 0..63) output += keytwo[n]
}
}
}
return output;
}
fun cipher(inputOne: String, inputTwo: String): String {
val arr = IntArray(256) { it }
var output = ""
var u = 0
var r: Int
for (a in arr.indices) {
u = (u + arr[a] + inputOne[a % inputOne.length].code) % 256
r = arr[a]
arr[a] = arr[u]
arr[u] = r
}
u = 0
var c = 0
for (f in inputTwo.indices) {
c = (c + f) % 256
u = (u + arr[c]) % 256
r = arr[c]
arr[c] = arr[u]
arr[u] = r
output += (inputTwo[f].code xor arr[(arr[c] + arr[u]) % 256]).toChar()
}
return output
}
}
private val key = "LCbu3iYC7ln24K7P" // key credits @Modder4869
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val baseUrl = url.split("/e/")[0]
val html = app.get(url, headers = mapOf("Referer" to "https://wcostream.cc/")).text
val (Id) = (Regex("/e/(.*?)?domain").find(url)?.destructured ?: Regex("""/e/(.*)""").find(
url
)?.destructured) ?: return emptyList()
val (skey) = Regex("""skey\s=\s['"](.*?)['"];""").find(html)?.destructured
?: return emptyList()
val apiLink = "$baseUrl/info/$Id?domain=wcostream.cc&skey=$skey"
// val (skey) = Regex("""skey\s=\s['"](.*?)['"];""").find(html)?.destructured
// ?: return emptyList()
val keys = getNewWcoKey()
keytwo = keys?.encryptKey ?: return emptyList()
val encryptedID = encrypt(cipher(keys.cipherkey!!, encrypt(Id))).replace("/", "_").replace("=","")
val apiLink = "$baseUrl/mediainfo/$encryptedID?key=${keys.mainKey}"
val referrer = "$baseUrl/e/$Id?domain=wcostream.cc"
data class Sources(
@JsonProperty("file") val file: String,
@JsonProperty("label") val label: String?
data class SourcesWco (
@JsonProperty("file" ) val file : String
)
data class Media(
@JsonProperty("sources") val sources: List<Sources>
data class MediaWco (
@JsonProperty("sources" ) val sources : ArrayList<SourcesWco> = arrayListOf()
)
data class WcoResponse(
@JsonProperty("success") val success: Boolean,
@JsonProperty("media") val media: Media
data class DataWco (
@JsonProperty("media" ) val media : MediaWco? = MediaWco()
)
data class WcoResponse (
@JsonProperty("status" ) val status : Int? = null,
@JsonProperty("data" ) val data : DataWco? = DataWco()
)
val mapped = app.get(apiLink, headers = mapOf("Referer" to referrer)).parsed<WcoResponse>()
val sources = mutableListOf<ExtractorLink>()
if (mapped.success) {
mapped.media.sources.forEach {
val check = mapped.status == 200
if (check) {
mapped.data?.media?.sources?.forEach {
if (mainUrl == "https://vizcloud2.ru" || mainUrl == "https://vizcloud.online") {
if (it.file.contains("vizcloud2.ru") || it.file.contains("vizcloud.online")) {
// Had to do this thing 'cause "list.m3u8#.mp4" gives 404 error so no quality is added
@ -128,7 +190,8 @@ open class WcoStream : ExtractorApi() {
"https://vizcloud.live",
"https://vizcloud.info",
"https://mwvn.vizcloud.info",
"https://vizcloud.digital"
"https://vizcloud.digital",
"https://vizcloud.cloud"
).contains(mainUrl)
) {
if (it.file.contains("m3u8")) {

View File

@ -0,0 +1,56 @@
package com.lagradost.cloudstream3.extractors.helper
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.app
class WcoHelper {
companion object {
private const val BACKUP_KEY_DATA = "github_keys_backup"
data class ExternalKeys(
@JsonProperty("wco_key")
val wcoKey: String? = null,
@JsonProperty("wco_cipher_key")
val wcocipher: String? = null
)
data class NewExternalKeys(
@JsonProperty("cipherKey")
val cipherkey: String? = null,
@JsonProperty("encryptKey")
val encryptKey: String? = null,
@JsonProperty("mainKey")
val mainKey: String? = null,
)
private var keys: ExternalKeys? = null
private var newKeys: NewExternalKeys? = null
private suspend fun getKeys() {
keys = keys
?: app.get("https://raw.githubusercontent.com/LagradOst/CloudStream-3/master/docs/keys.json")
.parsedSafe<ExternalKeys>()?.also { setKey(BACKUP_KEY_DATA, it) } ?: getKey(
BACKUP_KEY_DATA
)
}
suspend fun getWcoKey(): ExternalKeys? {
getKeys()
return keys
}
private suspend fun getNewKeys() {
newKeys = newKeys
?: app.get("https://raw.githubusercontent.com/chekaslowakiya/BruhFlow/main/keys.json")
.parsedSafe<NewExternalKeys>()?.also { setKey(BACKUP_KEY_DATA, it) } ?: getKey(
BACKUP_KEY_DATA
)
}
suspend fun getNewWcoKey(): NewExternalKeys? {
getNewKeys()
return newKeys
}
}
}

View File

@ -1,9 +1,9 @@
package com.lagradost.cloudstream3.metaproviders
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
import com.lagradost.cloudstream3.utils.SyncUtil
object SyncRedirector {

View File

@ -3,7 +3,7 @@ package com.lagradost.cloudstream3.metaproviders
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
import com.lagradost.cloudstream3.syncproviders.OAuth2API
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi
import com.lagradost.cloudstream3.syncproviders.providers.MALApi
@ -15,7 +15,7 @@ class MultiAnimeProvider : MainAPI() {
override val lang = "en"
override val usesWebView = true
override val supportedTypes = setOf(TvType.Anime)
private val syncApi: SyncAPI = OAuth2API.aniListApi
private val syncApi: SyncAPI = aniListApi
private val syncUtilType by lazy {
when (syncApi) {

View File

@ -4,10 +4,12 @@ import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.LoadResponse.Companion.addActors
import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.uwetrottmann.tmdb2.Tmdb
import com.uwetrottmann.tmdb2.entities.*
import com.uwetrottmann.tmdb2.enumerations.AppendToResponseItem
import com.uwetrottmann.tmdb2.enumerations.VideoType
import retrofit2.awaitResponse
import java.util.*
@ -24,6 +26,8 @@ data class TmdbLink(
)
open class TmdbProvider : MainAPI() {
// This should always be false, but might as well make it easier for forks
open val includeAdult = false
// Use the LoadResponse from the metadata provider
open val useMetaLoadResponse = false
@ -142,6 +146,7 @@ open class TmdbProvider : MainAPI() {
tags = genres?.mapNotNull { it.name }
duration = episode_run_time?.average()?.toInt()
rating = this@toLoadResponse.rating
addTrailer(videos.toTrailers())
recommendations = (this@toLoadResponse.recommendations
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
@ -149,6 +154,19 @@ open class TmdbProvider : MainAPI() {
}
}
private fun Videos?.toTrailers(): List<String>? {
return this?.results?.filter { it.type != VideoType.OPENING_CREDITS && it.type != VideoType.FEATURETTE }
?.sortedBy { it.type?.ordinal ?: 10000 }
?.mapNotNull {
when (it.site?.trim()?.lowercase()) {
"youtube" -> { // TODO FILL SITES
"https://www.youtube.com/watch?v=${it.key}"
}
else -> null
}
}
}
private fun Movie.toLoadResponse(): MovieLoadResponse {
return newMovieLoadResponse(
this.title ?: this.original_title, getUrl(id, false), TvType.Movie, TmdbLink(
@ -170,6 +188,7 @@ open class TmdbProvider : MainAPI() {
tags = genres?.mapNotNull { it.name }
duration = runtime
rating = this@toLoadResponse.rating
addTrailer(videos.toTrailers())
recommendations = (this@toLoadResponse.recommendations
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
@ -259,7 +278,16 @@ open class TmdbProvider : MainAPI() {
return if (useMetaLoadResponse) {
return if (isTvSeries) {
val body = tmdb.tvService().tv(id, "en-US", AppendToResponse(AppendToResponseItem.EXTERNAL_IDS)).awaitResponse().body()
val body = tmdb.tvService()
.tv(
id,
"en-US",
AppendToResponse(
AppendToResponseItem.EXTERNAL_IDS,
AppendToResponseItem.VIDEOS
)
)
.awaitResponse().body()
val response = body?.toLoadResponse()
if (response != null) {
if (response.recommendations.isNullOrEmpty())
@ -278,7 +306,16 @@ open class TmdbProvider : MainAPI() {
response
} else {
val body = tmdb.moviesService().summary(id, "en-US", AppendToResponse(AppendToResponseItem.EXTERNAL_IDS)).awaitResponse().body()
val body = tmdb.moviesService()
.summary(
id,
"en-US",
AppendToResponse(
AppendToResponseItem.EXTERNAL_IDS,
AppendToResponseItem.VIDEOS
)
)
.awaitResponse().body()
val response = body?.toLoadResponse()
if (response != null) {
if (response.recommendations.isNullOrEmpty())
@ -319,7 +356,7 @@ open class TmdbProvider : MainAPI() {
}
override suspend fun search(query: String): List<SearchResponse>? {
return tmdb.searchService().multi(query, 1, "en-Us", "US", true).awaitResponse()
return tmdb.searchService().multi(query, 1, "en-Us", "US", includeAdult).awaitResponse()
.body()?.results?.mapNotNull {
it.movie?.toSearchResponse() ?: it.tvShow?.toSearchResponse()
}

View File

@ -0,0 +1,158 @@
package com.lagradost.cloudstream3.movieproviders
import androidx.core.text.parseAsHtml
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
class AltadefinizioneProvider : MainAPI() {
override val lang = "it"
override var mainUrl = "https://altadefinizione.hair"
override var name = "Altadefinizione"
override val hasMainPage = true
override val hasChromecastSupport = true
override val supportedTypes = setOf(
TvType.Movie
)
override suspend fun getMainPage(): HomePageResponse {
val items = ArrayList<HomePageList>()
val urls = listOf(
Pair("$mainUrl/azione/", "Azione"),
Pair("$mainUrl/avventura/", "Avventura"),
)
for ((url, name) in urls) {
try {
val soup = app.get(url).document
val home = soup.select("div.box").map {
val title = it.selectFirst("img")!!.attr("alt")
val link = it.selectFirst("a")!!.attr("href")
val image = mainUrl + it.selectFirst("img")!!.attr("src")
val quality = getQualityFromString(it.selectFirst("span")!!.text())
MovieSearchResponse(
title,
link,
this.name,
TvType.Movie,
image,
null,
null,
quality,
)
}
items.add(HomePageList(name, home))
} catch (e: Exception) {
logError(e)
}
}
if (items.size <= 0) throw ErrorLoadingException()
return HomePageResponse(items)
}
override suspend fun search(query: String): List<SearchResponse> {
val doc = app.post("$mainUrl/index.php", data = mapOf(
"do" to "search",
"subaction" to "search",
"story" to query,
"sortby" to "news_read"
)).document
return doc.select("div.box").map {
val title = it.selectFirst("img")!!.attr("alt")
val link = it.selectFirst("a")!!.attr("href")
val image = mainUrl+it.selectFirst("img")!!.attr("src")
val quality = getQualityFromString(it.selectFirst("span")!!.text())
MovieSearchResponse(
title,
link,
this.name,
TvType.Movie,
image,
null,
null,
quality,
)
}
}
override suspend fun load(url: String): LoadResponse {
val page = app.get(url)
val document = page.document
val title = document.selectFirst(" h1 > a")!!.text().replace("streaming","")
val description = document.select("#sfull").toString().substringAfter("altadefinizione").substringBeforeLast("fonte trama").parseAsHtml().toString()
val rating = null
val year = document.selectFirst("#details > li:nth-child(2)")!!.childNode(2).toString().filter { it.isDigit() }.toInt()
val poster = fixUrl(document.selectFirst("div.thumbphoto > img")!!.attr("src"))
val recomm = document.select("ul.related-list > li").map {
val href = it.selectFirst("a")!!.attr("href")
val posterUrl = mainUrl + it.selectFirst("img")!!.attr("src")
val name = it.selectFirst("img")!!.attr("alt")
MovieSearchResponse(
name,
href,
this.name,
TvType.Movie,
posterUrl,
null
)
}
val actors: List<ActorData> =
document.select("#staring > a").map {
ActorData(actor = Actor(it.text()))
}
val tags: List<String> = document.select("#details > li:nth-child(1) > a").map { it.text() }
return newMovieLoadResponse(
title,
url,
TvType.Movie,
url
) {
posterUrl = fixUrlNull(poster)
this.year = year
this.plot = description
this.rating = rating
this.recommendations = recomm
this.duration = null
this.actors = actors
this.tags = tags
}
}
override suspend fun loadLinks(
data: String,
isCasting: Boolean,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
val doc = app.get(data).document
if (doc.select("div.guardahd-player").isNullOrEmpty()){
val videoUrl = doc.select("input").filter { it.hasAttr("data-mirror") }.last().attr("value")
loadExtractor(videoUrl, data, callback)
doc.select("#mirrors > li > a").forEach {
loadExtractor(fixUrl(it.attr("data-target")), data, callback)
}
}
else{
val pagelinks = doc.select("div.guardahd-player").select("iframe").attr("src")
val docLinks = app.get(pagelinks).document
docLinks.select("body > div > ul > li").forEach {
loadExtractor(fixUrl(it.attr("data-link")), data, callback)
}
}
return true
}
}

View File

@ -57,7 +57,7 @@ open class BflixProvider : MainAPI() {
}
//Credits to https://github.com/jmir1
val key = "eST4kCjadnvlAm5b1BOGyLJzrE90Q6oKgRfhV+M8NDYtcxW3IP/qp2i7XHuwZFUs"
private val key = "5uLKesbh0nkrpPq9VwMC6+tQBdomjJ4HNl/fWOSiREvAYagT8yIG7zx2D13UZFXc" //key credits to @Modder4869
private fun getVrf(id: String): String? {
val reversed = ue(encode(id) + "0000000").slice(0..5).reversed()
@ -354,7 +354,8 @@ open class BflixProvider : MainAPI() {
jsonservers.vidstream,
jsonservers.mcloud,
jsonservers.mp4upload,
jsonservers.streamtape
jsonservers.streamtape,
jsonservers.videovard,
).mapNotNull {
val epserver = app.get("$mainUrl/ajax/episode/info?id=$it").text
(if (epserver.contains("url")) {

View File

@ -0,0 +1,212 @@
package com.lagradost.cloudstream3.movieproviders
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.loadExtractor
class CineblogProvider : MainAPI() {
override val lang = "it"
override var mainUrl = "https://cb01.rip"
override var name = "CineBlog"
override val hasMainPage = true
override val hasChromecastSupport = true
override val supportedTypes = setOf(
TvType.Movie,
TvType.TvSeries,
)
override suspend fun getMainPage(): HomePageResponse {
val items = ArrayList<HomePageList>()
val urls = listOf(
Pair("$mainUrl/genere/azione/", "Azione"),
Pair("$mainUrl/genere/avventura/", "Avventura"),
)
for ((url, name) in urls) {
try {
val soup = app.get(url).document
val home = soup.select("article.item.movies").map {
val title = it.selectFirst("div.data > h3 > a")!!.text().substringBefore("(")
val link = it.selectFirst("div.poster > a")!!.attr("href")
TvSeriesSearchResponse(
title,
link,
this.name,
TvType.Movie,
it.selectFirst("img")!!.attr("src"),
null,
null,
)
}
items.add(HomePageList(name, home))
} catch (e: Exception) {
logError(e)
}
}
try {
val soup = app.get("$mainUrl/serietv/").document
val home = soup.select("article.item.tvshows").map {
val title = it.selectFirst("div.data > h3 > a")!!.text().substringBefore("(")
val link = it.selectFirst("div.poster > a")!!.attr("href")
TvSeriesSearchResponse(
title,
link,
this.name,
TvType.Movie,
it.selectFirst("img")!!.attr("src"),
null,
null,
)
}
items.add(HomePageList("Serie tv", home))
} catch (e: Exception) {
logError(e)
}
if (items.size <= 0) throw ErrorLoadingException()
return HomePageResponse(items)
}
override suspend fun search(query: String): List<SearchResponse> {
val queryformatted = query.replace(" ", "+")
val url = "$mainUrl?s=$queryformatted"
val doc = app.get(url,referer= mainUrl ).document
return doc.select("div.result-item").map {
val href = it.selectFirst("div.image > div > a")!!.attr("href")
val poster = it.selectFirst("div.image > div > a > img")!!.attr("src")
val name = it.selectFirst("div.details > div.title > a")!!.text().substringBefore("(")
MovieSearchResponse(
name,
href,
this.name,
TvType.Movie,
poster,
null
)
}
}
override suspend fun load(url: String): LoadResponse {
val page = app.get(url)
val document = page.document
val type = if (url.contains("film")) TvType.Movie else TvType.TvSeries
val title = document.selectFirst("div.data > h1")!!.text().substringBefore("(")
val description = document.select("#info > div.wp-content > p").html().toString()
val rating = null
var year = document.selectFirst(" div.data > div.extra > span.date")!!.text().substringAfter(",")
.filter { it.isDigit() }
if (year.length > 4) {
year = year.dropLast(4)
}
val poster = document.selectFirst("div.poster > img")!!.attr("src")
val recomm = document.select("#single_relacionados >article").map {
val href = it.selectFirst("a")!!.attr("href")
val posterUrl = it.selectFirst("a > img")!!.attr("src")
val name = it.selectFirst("a > img")!!.attr("alt").substringBeforeLast("(")
MovieSearchResponse(
name,
href,
this.name,
TvType.Movie,
posterUrl,
null
)
}
if (type == TvType.TvSeries) {
val episodeList = ArrayList<Episode>()
document.select("#seasons > div").reversed().map { element ->
val season = element.selectFirst("div.se-q > span.se-t")!!.text().toInt()
element.select("div.se-a > ul > li").filter { it.text()!="There are still no episodes this season" }.map{ episode ->
val href = episode.selectFirst("div.episodiotitle > a")!!.attr("href")
val epNum =episode.selectFirst("div.numerando")!!.text().substringAfter("-").filter { it.isDigit() }.toIntOrNull()
val epTitle = episode.selectFirst("div.episodiotitle > a")!!.text()
val posterUrl = episode.selectFirst("div.imagen > img")!!.attr("src")
episodeList.add(
Episode(
href,
epTitle,
season,
epNum,
posterUrl,
)
)
}
}
return TvSeriesLoadResponse(
title,
url,
this.name,
type,
episodeList,
fixUrlNull(poster),
year.toIntOrNull(),
description,
null,
rating,
null,
null,
null,
recomm
)
} else {
val actors: List<ActorData> =
document.select("div.person").filter{it.selectFirst("div.img > a > img")?.attr("src")!!.contains("/no/cast.png").not()}.map { actordata ->
val actorName = actordata.selectFirst("div.data > div.name > a")!!.text()
val actorImage : String? = actordata.selectFirst("div.img > a > img")?.attr("src")
val roleActor = actordata.selectFirst("div.data > div.caracter")!!.text()
ActorData(actor = Actor(actorName, image = actorImage), roleString = roleActor )
}
return newMovieLoadResponse(
title,
url,
type,
url
) {
posterUrl = fixUrlNull(poster)
this.year = year.toIntOrNull()
this.plot = description
this.rating = rating
this.recommendations = recomm
this.duration = null
this.actors = actors
}
}
}
override suspend fun loadLinks(
data: String,
isCasting: Boolean,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
val doc = app.get(data).document
val type = if( data.contains("film") ){"movie"} else {"tv"}
val idpost=doc.select("#player-option-1").attr("data-post")
val test = app.post("$mainUrl/wp-admin/admin-ajax.php", headers = mapOf(
"content-type" to "application/x-www-form-urlencoded; charset=UTF-8",
"accept" to "*/*",
"X-Requested-With" to "XMLHttpRequest",
), data = mapOf(
"action" to "doo_player_ajax",
"post" to idpost,
"nume" to "1",
"type" to type,
))
val url2= Regex("""src='((.|\\n)*?)'""").find(test.text)?.groups?.get(1)?.value.toString()
val trueUrl = app.get(url2, headers = mapOf("referer" to mainUrl)).url
loadExtractor(trueUrl, data, callback)
return true
}
}

View File

@ -28,8 +28,7 @@ class DoramasYTProvider : MainAPI() {
override val hasChromecastSupport = true
override val hasDownloadSupport = true
override val supportedTypes = setOf(
TvType.TvSeries,
TvType.Movie,
TvType.AsianDrama,
)
override suspend fun getMainPage(): HomePageResponse {

View File

@ -161,7 +161,6 @@ class EgyBestProvider : MainAPI() {
@JsonProperty("link") val link: String
)
override suspend fun loadLinks(
data: String,
isCasting: Boolean,

View File

@ -0,0 +1,90 @@
package com.lagradost.cloudstream3.movieproviders
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.utils.*
import kotlin.collections.ArrayList
class ElifilmsProvider:MainAPI() {
override var mainUrl: String = "https://elifilms.net"
override var name: String = "Elifilms"
override val lang = "es"
override val hasMainPage = true
override val hasChromecastSupport = true
override val hasDownloadSupport = true
override val supportedTypes = setOf(
TvType.Movie,
)
override suspend fun getMainPage(): HomePageResponse {
val items = ArrayList<HomePageList>()
val newest = app.get(mainUrl).document.selectFirst("a.fav_link.premiera")?.attr("href")
val urls = listOf(
Pair(mainUrl, "Películas recientes"),
Pair("$mainUrl/4k-peliculas/", "Películas en 4k"),
Pair(newest, "Últimos estrenos"),
)
urls.apmap { (url, name) ->
val soup = app.get(url ?: "").document
val home = soup.select("article.shortstory.cf").map {
val title = it.selectFirst(".short_header")?.text() ?: ""
val link = it.selectFirst("div a")?.attr("href") ?: ""
TvSeriesSearchResponse(
title,
link,
this.name,
TvType.Movie,
it.selectFirst("a.ah-imagge img")?.attr("data-src"),
null,
null,
)
}
items.add(HomePageList(name, home))
}
if (items.size <= 0) throw ErrorLoadingException()
return HomePageResponse(items)
}
override suspend fun search(query: String): List<SearchResponse> {
val url = "$mainUrl/?s=$query"
val doc = app.get(url).document
return doc.select("article.cf").map {
val href = it.selectFirst("div.short_content a")?.attr("href") ?: ""
val poster = it.selectFirst("a.ah-imagge img")?.attr("data-src")
val name = it.selectFirst(".short_header")?.text() ?: ""
(MovieSearchResponse(name, href, this.name, TvType.Movie, poster, null))
}
}
override suspend fun load(url: String): LoadResponse {
val document = app.get(url, timeout = 120).document
val title = document.selectFirst(".post_title h1")?.text() ?: ""
val rating = document.select("span.imdb.rki").toString().toIntOrNull()
val poster = document.selectFirst(".poster img")?.attr("src")
val desc = document.selectFirst("div.notext .actors p")?.text()
val tags = document.select("td.notext a")
.map { it?.text()?.trim().toString() }
return MovieLoadResponse(
title,
url,
this.name,
TvType.Movie,
url,
poster,
null,
desc,
rating,
tags
)
}
override suspend fun loadLinks(
data: String,
isCasting: Boolean,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
app.get(data).document.select("li.change-server a").apmap {
val encodedurl = it.attr("data-id")
val urlDecoded = base64Decode(encodedurl)
val url = fixUrl(urlDecoded)
loadExtractor(url, data, callback)
}
return true
}
}

View File

@ -0,0 +1,286 @@
package com.lagradost.cloudstream3.movieproviders
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.network.WebViewResolver
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import java.util.*
import kotlin.collections.ArrayList
class EstrenosDoramasProvider : MainAPI() {
companion object {
fun getType(t: String): TvType {
return if (t.contains("OVA") || t.contains("Especial")) TvType.OVA
else if (t.contains("Pelicula")) TvType.Movie
else TvType.TvSeries
}
}
override var mainUrl = "https://www23.estrenosdoramas.net"
override var name = "EstrenosDoramas"
override val lang = "es"
override val hasMainPage = true
override val hasChromecastSupport = true
override val hasDownloadSupport = true
override val supportedTypes = setOf(
TvType.AsianDrama,
)
override suspend fun getMainPage(): HomePageResponse {
val urls = listOf(
Pair(mainUrl, "Últimas series"),
Pair("$mainUrl/category/peliculas", "Películas"),
)
val items = ArrayList<HomePageList>()
urls.apmap { (url, name) ->
val home = app.get(url, timeout = 120).document.select("div.clearfix").map {
val title = cleanTitle(it.selectFirst("h3 a")?.text()!!)
val poster = it.selectFirst("img.cate_thumb")?.attr("src")
AnimeSearchResponse(
title,
it.selectFirst("a")?.attr("href")!!,
this.name,
TvType.AsianDrama,
poster,
null,
if (title.contains("Latino") || title.contains("Castellano")) EnumSet.of(
DubStatus.Dubbed
) else EnumSet.of(DubStatus.Subbed),
)
}
items.add(HomePageList(name, home))
}
if (items.size <= 0) throw ErrorLoadingException()
return HomePageResponse(items)
}
override suspend fun search(query: String): List<SearchResponse> {
val searchob = ArrayList<AnimeSearchResponse>()
val search =
app.get("$mainUrl/?s=$query", timeout = 120).document.select("div.clearfix").map {
val title = cleanTitle(it.selectFirst("h3 a")?.text()!!)
val href = it.selectFirst("a")?.attr("href")
val image = it.selectFirst("img.cate_thumb")?.attr("src")
val lists =
AnimeSearchResponse(
title,
href!!,
this.name,
TvType.AsianDrama,
image,
null,
if (title.contains("Latino") || title.contains("Castellano")) EnumSet.of(
DubStatus.Dubbed
) else EnumSet.of(DubStatus.Subbed),
)
if (href.contains("capitulo")) {
//nothing
}
else {
searchob.add(lists)
}
}
return searchob
}
override suspend fun load(url: String): LoadResponse? {
val doc = app.get(url, timeout = 120).document
val poster = doc.selectFirst("head meta[property]")?.attr("content")
val title = doc.selectFirst("h1.titulo")?.text()
val description = try {
doc.selectFirst("div.post div.highlight div.font")?.text()
} catch (e:Exception){
null
}
val finaldesc = description?.substringAfter("Sinopsis")?.replace(": ", "")?.trim()
val epi = ArrayList<Episode>()
val episodes = doc.select("div.post .lcp_catlist a").map {
val name = it.selectFirst("a")?.text()
val link = it.selectFirst("a")?.attr("href")
val test = Episode(link!!, name)
if (!link.equals(url)) {
epi.add(test)
}
}.reversed()
return when (val type = if (episodes.isEmpty()) TvType.Movie else TvType.AsianDrama) {
TvType.AsianDrama -> {
return newAnimeLoadResponse(title!!, url, type) {
japName = null
engName = title.replace(Regex("[Pp]elicula |[Pp]elicula"),"")
posterUrl = poster
addEpisodes(DubStatus.Subbed, epi.reversed())
plot = finaldesc
}
}
TvType.Movie -> {
MovieLoadResponse(
cleanTitle(title!!),
url,
this.name,
TvType.Movie,
url,
poster,
null,
finaldesc,
null,
null,
)
}
else -> null
}
}
data class ReproDoramas (
@JsonProperty("link") val link: String,
@JsonProperty("time") val time: Int
)
private fun cleanTitle(title: String): String = title.replace(Regex("[Pp]elicula |[Pp]elicula"),"")
private fun cleanExtractor(
source: String,
name: String,
url: String,
referer: String,
m3u8: Boolean,
callback: (ExtractorLink) -> Unit
): Boolean {
callback(
ExtractorLink(
source,
name,
url,
referer,
Qualities.Unknown.value,
m3u8
)
)
return true
}
override suspend fun loadLinks(
data: String,
isCasting: Boolean,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
val headers = mapOf("Host" to "repro3.estrenosdoramas.us",
"User-Agent" to USER_AGENT,
"Accept" to "*/*",
"Accept-Language" to "en-US,en;q=0.5",
"Content-Type" to "application/x-www-form-urlencoded; charset=UTF-8",
"X-Requested-With" to "XMLHttpRequest",
"Origin" to "https://repro3.estrenosdoramas.us",
"DNT" to "1",
"Connection" to "keep-alive",
"Sec-Fetch-Dest" to "empty",
"Sec-Fetch-Mode" to "cors",
"Sec-Fetch-Site" to "same-origin",
"Cache-Control" to "max-age=0",)
val document = app.get(data).document
document.select("div.tab_container iframe").apmap { container ->
val directlink = fixUrl(container.attr("src"))
loadExtractor(directlink, data, callback)
if (directlink.contains("/repro/amz/")) {
val amzregex = Regex("https:\\/\\/repro3\\.estrenosdoramas\\.us\\/repro\\/amz\\/examples\\/.*\\.php\\?key=.*\$")
amzregex.findAll(directlink).map {
it.value.replace(Regex("https:\\/\\/repro3\\.estrenosdoramas\\.us\\/repro\\/amz\\/examples\\/.*\\.php\\?key="),"")
}.toList().apmap { key ->
val response = app.post("https://repro3.estrenosdoramas.us/repro/amz/examples/player/api/indexDCA.php",
headers = headers,
data = mapOf(
Pair("key",key),
Pair("token","MDAwMDAwMDAwMA=="),
),
allowRedirects = false
).text
val reprojson = parseJson<ReproDoramas>(response)
val decodeurl = base64Decode(reprojson.link)
if (decodeurl.contains("m3u8"))
cleanExtractor(
name,
name,
decodeurl,
"https://repro3.estrenosdoramas.us",
decodeurl.contains(".m3u8"),
callback
)
}
}
if (directlink.contains("reproducir14")) {
val regex = Regex("(https:\\/\\/repro.\\.estrenosdoramas\\.us\\/repro\\/reproducir14\\.php\\?key=[a-zA-Z0-9]{0,8}[a-zA-Z0-9_-]+)")
regex.findAll(directlink).map {
it.value
}.toList().apmap {
val doc = app.get(it).text
val videoid = doc.substringAfter("vid=\"").substringBefore("\" n")
val token = doc.substringAfter("name=\"").substringBefore("\" s")
val acctkn = doc.substringAfter("{ acc: \"").substringBefore("\", id:")
val link = app.post("https://repro3.estrenosdoramas.us/repro/proto4.php",
headers = headers,
data = mapOf(
Pair("acc",acctkn),
Pair("id",videoid),
Pair("tk",token)),
allowRedirects = false
).text
val extracteklink = link.substringAfter("\"urlremoto\":\"").substringBefore("\"}")
.replace("\\/", "/").replace("//ok.ru/","http://ok.ru/")
loadExtractor(extracteklink, data, callback)
}
}
if (directlink.contains("reproducir120")) {
val regex = Regex("(https:\\/\\/repro3.estrenosdoramas.us\\/repro\\/reproducir120\\.php\\?\\nkey=[a-zA-Z0-9]{0,8}[a-zA-Z0-9_-]+)")
regex.findAll(directlink).map {
it.value
}.toList().apmap {
val doc = app.get(it).text
val videoid = doc.substringAfter("var videoid = '").substringBefore("';")
val token = doc.substringAfter("var tokens = '").substringBefore("';")
val acctkn = doc.substringAfter("{ acc: \"").substringBefore("\", id:")
val link = app.post("https://repro3.estrenosdoramas.us/repro/api3.php",
headers = headers,
data = mapOf(
Pair("acc",acctkn),
Pair("id",videoid),
Pair("tk",token)),
allowRedirects = false
).text
val extractedlink = link.substringAfter("\"{file:'").substringBefore("',label:")
.replace("\\/", "/")
val quality = link.substringAfter(",label:'").substringBefore("',type:")
val type = link.substringAfter("type: '").substringBefore("'}\"")
if (extractedlink.isNotBlank())
if (quality.contains("File not found", ignoreCase = true)) {
//Nothing
} else {
cleanExtractor(
"Movil",
"Movil $quality",
extractedlink,
"",
!type.contains("mp4"),
callback
)
}
}
}
}
return true
}
}

View File

@ -5,11 +5,10 @@ import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.httpsify
import com.lagradost.cloudstream3.utils.loadExtractor
import okhttp3.Interceptor
import org.jsoup.Jsoup
class HDMovie5 : MainAPI() {
override var mainUrl = "https://hdmovie5.tv"
override var mainUrl = "https://hdmovie5.mba"
override var name = "HDMovie"
override val lang = "hi"
@ -34,6 +33,7 @@ class HDMovie5 : MainAPI() {
MovieSearchResponse(
a.text(),
a.attr("href"),
this.name,
TvType.Movie,
it.select("img").attr("src"),
@ -137,7 +137,8 @@ class HDMovie5 : MainAPI() {
callback: (ExtractorLink) -> Unit
): Boolean {
return data.split(",").apmapIndexed { index, it ->
val html = app.post(
//println("loadLinks:::: $index $it")
val p = app.post(
"$mainUrl/wp-admin/admin-ajax.php",
data = mapOf(
"action" to "doo_player_ajax",
@ -145,10 +146,12 @@ class HDMovie5 : MainAPI() {
"nume" to "${index + 1}",
"type" to "movie"
)
).parsed<PlayerAjaxResponse>().embedURL ?: return@apmapIndexed false
)
// println("TEXT::::: ${p.text}")
val html = p.parsedSafe<PlayerAjaxResponse>()?.embedURL ?: return@apmapIndexed false
val doc = Jsoup.parse(html)
val link = doc.select("iframe").attr("src")
loadExtractor(httpsify(link), "$mainUrl/",callback)
loadExtractor(httpsify(link), "$mainUrl/", callback)
}.contains(true)
}
}

View File

@ -0,0 +1,6 @@
package com.lagradost.cloudstream3.movieproviders
class HDTodayProvider : SflixProvider() {
override var mainUrl = "https://hdtoday.cc"
override var name = "HDToday"
}

View File

@ -1,23 +1,22 @@
package com.lagradost.cloudstream3.movieproviders
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.httpsify
import com.lagradost.cloudstream3.utils.loadExtractor
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import java.util.*
class LayarKaca21Provider : MainAPI() {
override var mainUrl = "https://149.56.24.226/"
override var name = "LayarKaca21"
class LayarKacaProvider : MainAPI() {
override var mainUrl = "https://149.56.24.226"
override var name = "LayarKaca"
override val hasMainPage = true
override val lang = "id"
override val hasDownloadSupport = true
override val supportedTypes = setOf(
TvType.Movie,
TvType.TvSeries,
TvType.AsianDrama
)
override suspend fun getMainPage(): HomePageResponse {

View File

@ -1,298 +0,0 @@
package com.lagradost.cloudstream3.movieproviders
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.unixTime
import com.lagradost.cloudstream3.extractors.M3u8Manifest
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName
import org.jsoup.Jsoup
//BE AWARE THAT weboas.is is a clone of lookmovie
class LookMovieProvider : MainAPI() {
override val hasQuickSearch = true
override var name = "LookMovie"
override var mainUrl = "https://lookmovie.io"
override val supportedTypes = setOf(
TvType.Movie,
TvType.TvSeries,
)
data class LookMovieSearchResult(
@JsonProperty("backdrop") val backdrop: String?,
@JsonProperty("imdb_rating") val imdb_rating: String,
@JsonProperty("poster") val poster: String?,
@JsonProperty("slug") val slug: String,
@JsonProperty("title") val title: String,
@JsonProperty("year") val year: String?,
// @JsonProperty("flag_quality") val flag_quality: Int?,
)
data class LookMovieTokenRoot(
@JsonProperty("data") val data: LookMovieTokenResult?,
@JsonProperty("success") val success: Boolean,
)
data class LookMovieTokenResult(
@JsonProperty("accessToken") val accessToken: String,
@JsonProperty("subtitles") val subtitles: List<LookMovieTokenSubtitle>?,
)
data class LookMovieTokenSubtitle(
@JsonProperty("language") val language: String,
@JsonProperty("source") val source: String?,
//@JsonProperty("source_id") val source_id: String,
//@JsonProperty("kind") val kind: String,
//@JsonProperty("id") val id: String,
@JsonProperty("file") val file: String,
)
data class LookMovieSearchResultRoot(
// @JsonProperty("per_page") val per_page: Int?,
// @JsonProperty("total") val total: Int?,
@JsonProperty("result") val result: List<LookMovieSearchResult>?,
)
data class LookMovieEpisode(
@JsonProperty("title") var title: String,
@JsonProperty("index") var index: String,
@JsonProperty("episode") var episode: String,
@JsonProperty("id_episode") var idEpisode: Int,
@JsonProperty("season") var season: String,
)
override suspend fun quickSearch(query: String): List<SearchResponse> {
val movieUrl = "$mainUrl/api/v1/movies/search/?q=$query"
val movieResponse = app.get(movieUrl).text
val movies = mapper.readValue<LookMovieSearchResultRoot>(movieResponse).result
val showsUrl = "$mainUrl/api/v1/shows/search/?q=$query"
val showsResponse = app.get(showsUrl).text
val shows = mapper.readValue<LookMovieSearchResultRoot>(showsResponse).result
val returnValue = ArrayList<SearchResponse>()
if (!movies.isNullOrEmpty()) {
for (m in movies) {
val url = "$mainUrl/movies/view/${m.slug}"
returnValue.add(
MovieSearchResponse(
m.title,
url,
this.name,
TvType.Movie,
m.poster ?: m.backdrop,
m.year?.toIntOrNull()
)
)
}
}
if (!shows.isNullOrEmpty()) {
for (s in shows) {
val url = "$mainUrl/shows/view/${s.slug}"
returnValue.add(
MovieSearchResponse(
s.title,
url,
this.name,
TvType.TvSeries,
s.poster ?: s.backdrop,
s.year?.toIntOrNull()
)
)
}
}
return returnValue
}
override suspend fun search(query: String): List<SearchResponse> {
suspend fun search(query: String, isMovie: Boolean): List<SearchResponse> {
val url = "$mainUrl/${if (isMovie) "movies" else "shows"}/search/?q=$query"
val response = app.get(url).text
val document = Jsoup.parse(response)
val items = document.select("div.flex-wrap-movielist > div.movie-item-style-1")
return items.map { item ->
val titleHolder = item.selectFirst("> div.mv-item-infor > h6 > a")
val href = fixUrl(titleHolder!!.attr("href"))
val name = titleHolder.text()
val posterHolder = item.selectFirst("> div.image__placeholder > a")
val poster = posterHolder!!.selectFirst("> img")?.attr("data-src")
val year = posterHolder.selectFirst("> p.year")?.text()?.toIntOrNull()
if (isMovie) {
MovieSearchResponse(
name, href, this.name, TvType.Movie, poster, year
)
} else
TvSeriesSearchResponse(
name, href, this.name, TvType.TvSeries, poster, year, null
)
}
}
val movieList = search(query, true).toMutableList()
val seriesList = search(query, false)
movieList.addAll(seriesList)
return movieList
}
data class LookMovieLinkLoad(val url: String, val extraUrl: String, val isMovie: Boolean)
private fun addSubtitles(
subs: List<LookMovieTokenSubtitle>?,
subtitleCallback: (SubtitleFile) -> Unit
) {
if (subs == null) return
subs.forEach {
if (it.file.endsWith(".vtt"))
subtitleCallback.invoke(SubtitleFile(it.language, fixUrl(it.file)))
}
}
private suspend fun loadCurrentLinks(url: String, callback: (ExtractorLink) -> Unit) {
val response = app.get(url.replace("\$unixtime", unixTime.toString())).text
M3u8Manifest.extractLinks(response).forEach {
callback.invoke(
ExtractorLink(
this.name,
"${this.name} - ${it.second}",
fixUrl(it.first),
"",
getQualityFromName(it.second),
true
)
)
}
}
override suspend fun loadLinks(
data: String,
isCasting: Boolean,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
val localData: LookMovieLinkLoad = mapper.readValue(data)
if (localData.isMovie) {
val tokenResponse = app.get(localData.url).text
val root = mapper.readValue<LookMovieTokenRoot>(tokenResponse)
val accessToken = root.data?.accessToken ?: return false
addSubtitles(root.data.subtitles, subtitleCallback)
loadCurrentLinks(localData.extraUrl.replace("\$accessToken", accessToken), callback)
return true
} else {
loadCurrentLinks(localData.url, callback)
val subResponse = app.get(localData.extraUrl).text
val subs = mapper.readValue<List<LookMovieTokenSubtitle>>(subResponse)
addSubtitles(subs, subtitleCallback)
}
return true
}
override suspend fun load(url: String): LoadResponse? {
val response = app.get(url).text
val document = Jsoup.parse(response)
val isMovie = url.contains("/movies/")
val watchHeader = document.selectFirst("div.watch-heading")
val nameHeader = watchHeader!!.selectFirst("> h1.bd-hd")
val year = nameHeader!!.selectFirst("> span")?.text()?.toIntOrNull()
val title = nameHeader.ownText()
val rating =
parseRating(watchHeader.selectFirst("> div.movie-rate > div.rate > p > span")!!.text())
val imgElement = document.selectFirst("div.movie-img > p.movie__poster")
val img = imgElement?.attr("style")
var poster = if (img.isNullOrEmpty()) null else "url\\((.*?)\\)".toRegex()
.find(img)?.groupValues?.get(1)
if (poster.isNullOrEmpty()) poster = imgElement?.attr("data-background-image")
val descript = document.selectFirst("p.description-short")!!.text()
val id = "${if (isMovie) "id_movie" else "id_show"}:(.*?),".toRegex()
.find(response)?.groupValues?.get(1)
?.replace(" ", "")
?: return null
val realSlug = url.replace("$mainUrl/${if (isMovie) "movies" else "shows"}/view/", "")
val realUrl =
"$mainUrl/api/v1/security/${if (isMovie) "movie" else "show"}-access?${if (isMovie) "id_movie=$id" else "slug=$realSlug"}&token=1&sk=&step=1"
if (isMovie) {
val localData =
LookMovieLinkLoad(
realUrl,
"$mainUrl/manifests/movies/json/$id/\$unixtime/\$accessToken/master.m3u8",
true
).toJson()
return MovieLoadResponse(
title,
url,
this.name,
TvType.Movie,
localData,
poster,
year,
descript,
rating
)
} else {
val tokenResponse = app.get(realUrl).text
val root = mapper.readValue<LookMovieTokenRoot>(tokenResponse)
val accessToken = root.data?.accessToken ?: return null
val window =
"window\\['show_storage'] =((.|\\n)*?<)".toRegex().find(response)?.groupValues?.get(
1
)
?: return null
// val id = "id_show:(.*?),".toRegex().find(response.text)?.groupValues?.get(1) ?: return null
val season = "seasons:.*\\[((.|\\n)*?)]".toRegex().find(window)?.groupValues?.get(1)
?: return null
fun String.fixSeasonJson(replace: String): String {
return this.replace("$replace:", "\"$replace\":")
}
val json = season
.replace("\'", "\"")
.fixSeasonJson("title")
.fixSeasonJson("id_episode")
.fixSeasonJson("episode")
.fixSeasonJson("index")
.fixSeasonJson("season")
val realJson = "[" + json.substring(0, json.lastIndexOf(',')) + "]"
val episodes = mapper.readValue<List<LookMovieEpisode>>(realJson).map {
val localData =
LookMovieLinkLoad(
"$mainUrl/manifests/shows/json/$accessToken/\$unixtime/${it.idEpisode}/master.m3u8",
"https://lookmovie.io/api/v1/shows/episode-subtitles/?id_episode=${it.idEpisode}",
false
).toJson()
Episode(
localData,
it.title,
it.season.toIntOrNull(),
it.episode.toIntOrNull(),
)
}.toList()
return TvSeriesLoadResponse(
title,
url,
this.name,
TvType.TvSeries,
ArrayList(episodes),
poster,
year,
descript,
null,
rating
)
}
}
}

View File

@ -1,13 +1,8 @@
package com.lagradost.cloudstream3.movieproviders
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.LoadResponse.Companion.addDuration
import com.lagradost.cloudstream3.LoadResponse.Companion.addRating
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.Qualities
import java.lang.Exception
class NginxProvider : MainAPI() {
override var name = "Nginx"
@ -15,23 +10,40 @@ class NginxProvider : MainAPI() {
override val hasMainPage = true
override val supportedTypes = setOf(TvType.AnimeMovie, TvType.TvSeries, TvType.Movie)
companion object {
var loginCredentials: String? = null
var overrideUrl: String? = null
const val ERROR_STRING = "No nginx url specified in the settings"
}
fun getAuthHeader(storedCredentials: String?): Map<String, String> {
if (storedCredentials == null) {
return mapOf(Pair("Authorization", "Basic ")) // no Authorization headers
private fun getAuthHeader(): Map<String, String> {
val url = overrideUrl ?: throw ErrorLoadingException(ERROR_STRING)
mainUrl = url
println("OVERRIDING URL TO $overrideUrl")
if (mainUrl == "NONE" || mainUrl.isBlank()) {
throw ErrorLoadingException(ERROR_STRING)
}
val basicAuthToken = base64Encode(storedCredentials.toByteArray()) // will this be loaded when not using the provider ??? can increase load
return mapOf(Pair("Authorization", "Basic $basicAuthToken"))
val localCredentials = loginCredentials
if (localCredentials == null || localCredentials.trim() == ":") {
return mapOf("Authorization" to "Basic ") // no Authorization headers
}
val basicAuthToken =
base64Encode(localCredentials.toByteArray()) // will this be loaded when not using the provider ??? can increase load
return mapOf("Authorization" to "Basic $basicAuthToken")
}
override suspend fun load(url: String): LoadResponse {
val authHeader = getAuthHeader(storedCredentials) // call again because it isn't reloaded if in main class and storedCredentials loads after
val authHeader =
getAuthHeader() // call again because it isn't reloaded if in main class and storedCredentials loads after
// url can be tvshow.nfo for series or mediaRootUrl for movies
val mediaRootDocument = app.get(url, authHeader).document
val mainRootDocument = app.get(url, authHeader).document
val nfoUrl = url + mediaRootDocument.getElementsByAttributeValueContaining("href", ".nfo").attr("href") // metadata url file
val nfoUrl = url + mainRootDocument.getElementsByAttributeValueContaining("href", ".nfo")
.attr("href") // metadata url file
val metadataDocument = app.get(nfoUrl, authHeader).document // get the metadata nfo file
@ -44,27 +56,34 @@ class NginxProvider : MainAPI() {
if (isMovie) {
val poster = metadataDocument.selectFirst("thumb")!!.text()
val trailer = metadataDocument.select("trailer").mapNotNull {
it?.text()?.replace(
"plugin://plugin.video.youtube/play/?video_id=",
"https://www.youtube.com/watch?v="
)
it?.text()?.replace(
"plugin://plugin.video.youtube/play/?video_id=",
"https://www.youtube.com/watch?v="
)
}
val partialUrl = mediaRootDocument.getElementsByAttributeValueContaining("href", ".nfo").attr("href").replace(".nfo", ".")
val partialUrl =
mainRootDocument.getElementsByAttributeValueContaining("href", ".nfo").attr("href")
.replace(".nfo", ".")
val date = metadataDocument.selectFirst("year")?.text()?.toIntOrNull()
val ratingAverage = metadataDocument.selectFirst("value")?.text()?.toIntOrNull()
val tagsList = metadataDocument.select("genre")
?.mapNotNull { // all the tags like action, thriller ...
.mapNotNull { // all the tags like action, thriller ...
it?.text()
}
val dataList = mediaRootDocument.getElementsByAttributeValueContaining( // list of all urls of the webpage
"href",
partialUrl
)
val dataList =
mainRootDocument.getElementsByAttributeValueContaining( // list of all urls of the webpage
"href",
partialUrl
)
val data = url + dataList.firstNotNullOf { item -> item.takeIf { (!it.attr("href").contains(".nfo") && !it.attr("href").contains(".jpg"))} }.attr("href").toString() // exclude poster and nfo (metadata) file
val data = url + dataList.firstNotNullOf { item ->
item.takeIf {
(!it.attr("href").contains(".nfo") && !it.attr("href").contains(".jpg"))
}
}.attr("href").toString() // exclude poster and nfo (metadata) file
return newMovieLoadResponse(
title,
@ -81,7 +100,6 @@ class NginxProvider : MainAPI() {
}
} else // a tv serie
{
val list = ArrayList<Pair<Int, String>>()
val mediaRootUrl = url.replace("tvshow.nfo", "")
val posterUrl = mediaRootUrl + "poster.jpg"
@ -91,7 +109,7 @@ class NginxProvider : MainAPI() {
val tagsList = metadataDocument.select("genre")
?.mapNotNull { // all the tags like action, thriller ...; unused variable
.mapNotNull { // all the tags like action, thriller ...; unused variable
it?.text()
}
@ -102,7 +120,7 @@ class NginxProvider : MainAPI() {
seasons.forEach { element ->
val season =
element.attr("href")?.replace("Season%20", "")?.replace("/", "")?.toIntOrNull()
element.attr("href").replace("Season%20", "").replace("/", "").toIntOrNull()
val href = mediaRootUrl + element.attr("href")
if (season != null && season > 0 && href.isNotBlank()) {
list.add(Pair(season, href))
@ -120,33 +138,40 @@ class NginxProvider : MainAPI() {
"href",
".nfo"
) // get metadata
episodes.forEach { episode ->
val nfoDocument = app.get(seasonString + episode.attr("href"), authHeader).document // get episode metadata file
val epNum = nfoDocument.selectFirst("episode")?.text()?.toIntOrNull()
val poster =
seasonString + episode.attr("href").replace(".nfo", "-thumb.jpg")
val name = nfoDocument.selectFirst("title")!!.text()
// val seasonInt = nfoDocument.selectFirst("season").text().toIntOrNull()
val date = nfoDocument.selectFirst("aired")?.text()
val plot = nfoDocument.selectFirst("plot")?.text()
episodes.forEach { episode ->
val nfoDocument = app.get(
seasonString + episode.attr("href"),
authHeader
).document // get episode metadata file
val epNum = nfoDocument.selectFirst("episode")?.text()?.toIntOrNull()
val poster =
seasonString + episode.attr("href").replace(".nfo", "-thumb.jpg")
val name = nfoDocument.selectFirst("title")!!.text()
// val seasonInt = nfoDocument.selectFirst("season").text().toIntOrNull()
val date = nfoDocument.selectFirst("aired")?.text()
val plot = nfoDocument.selectFirst("plot")?.text()
val dataList = seasonDocument.getElementsByAttributeValueContaining(
"href",
episode.attr("href").replace(".nfo", "")
)
val data = seasonString + dataList.firstNotNullOf { item -> item.takeIf { (!it.attr("href").contains(".nfo") && !it.attr("href").contains(".jpg"))} }.attr("href").toString() // exclude poster and nfo (metadata) file
val dataList = seasonDocument.getElementsByAttributeValueContaining(
"href",
episode.attr("href").replace(".nfo", "")
)
val data = seasonString + dataList.firstNotNullOf { item ->
item.takeIf {
(!it.attr("href").contains(".nfo") && !it.attr("href").contains(".jpg"))
}
}.attr("href").toString() // exclude poster and nfo (metadata) file
episodeList.add(
newEpisode(data) {
this.name = name
this.season = seasonInt
this.episode = epNum
this.posterUrl = poster // will require headers too
this.description = plot
addDate(date)
}
)
}
episodeList.add(
newEpisode(data) {
this.name = name
this.season = seasonInt
this.episode = epNum
this.posterUrl = poster // will require headers too
this.description = plot
addDate(date)
}
)
}
}
return newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodeList) {
this.name = title
@ -168,8 +193,9 @@ class NginxProvider : MainAPI() {
callback: (ExtractorLink) -> Unit
): Boolean {
// loadExtractor(data, null) { callback(it.copy(headers=authHeader)) }
val authHeader = getAuthHeader(storedCredentials) // call again because it isn't reloaded if in main class and storedCredentials loads after
callback.invoke (
val authHeader =
getAuthHeader() // call again because it isn't reloaded if in main class and storedCredentials loads after
callback.invoke(
ExtractorLink(
name,
name,
@ -185,19 +211,23 @@ class NginxProvider : MainAPI() {
}
override suspend fun getMainPage(): HomePageResponse {
val authHeader = getAuthHeader(storedCredentials) // call again because it isn't reloaded if in main class and storedCredentials loads after
if (mainUrl == "NONE"){
throw ErrorLoadingException("No nginx url specified in the settings: Nginx Settigns > Nginx server url, try again in a few seconds")
}
val authHeader =
getAuthHeader() // call again because it isn't reloaded if in main class and storedCredentials loads after
val document = app.get(mainUrl, authHeader).document
val categories = document.select("a")
val returnList = categories.mapNotNull {
val categoryPath = mainUrl + it.attr("href") ?: return@mapNotNull null // get the url of the category; like http://192.168.1.10/media/Movies/
val categoryTitle = it.text() // get the category title like Movies or Series
if (categoryTitle != "../" && categoryTitle != "Music/") { // exclude parent dir and Music dir
val categoryDocument = app.get(categoryPath, authHeader).document // queries the page http://192.168.1.10/media/Movies/
val href = it?.attr("href")
val categoryPath = fixUrlNull(href?.trim())
?: return@mapNotNull null // get the url of the category; like http://192.168.1.10/media/Movies/
val categoryDocument = app.get(
categoryPath,
authHeader
).document // queries the page http://192.168.1.10/media/Movies/
val contentLinks = categoryDocument.select("a")
val currentList = contentLinks.mapNotNull { head ->
if (head.attr("href") != "../") {
@ -215,7 +245,6 @@ class NginxProvider : MainAPI() {
val nfoContent =
app.get(nfoPath, authHeader).document // all the metadata
if (isMovieType) {
val movieName = nfoContent.select("title").text()
val posterUrl = mediaRootUrl + "poster.jpg"
@ -238,15 +267,11 @@ class NginxProvider : MainAPI() {
) {
addPoster(posterUrl, authHeader)
}
}
} catch (e: Exception) { // can cause issues invisible errors
null
//logError(e) // not working because it changes the return type of currentList to Any
}
} else null
}
if (currentList.isNotEmpty() && categoryTitle != "../") { // exclude upper dir

View File

@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.movieproviders
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.metaproviders.TmdbLink
import com.lagradost.cloudstream3.metaproviders.TmdbProvider
@ -14,6 +15,7 @@ class OlgplyProvider : TmdbProvider() {
override var name = "Olgply"
override val instantLinkLoading = true
override val useMetaLoadResponse = true
override val supportedTypes = setOf(TvType.TvSeries, TvType.Movie)
override suspend fun loadLinks(
data: String,

View File

@ -20,6 +20,8 @@ class RebahinProvider : MainAPI() {
override val supportedTypes = setOf(
TvType.Movie,
TvType.TvSeries,
TvType.Anime,
TvType.AsianDrama
)
override suspend fun getMainPage(): HomePageResponse {
@ -168,6 +170,7 @@ class RebahinProvider : MainAPI() {
private suspend fun invokeLokalSource(
url: String,
name: String,
ref: String,
subCallback: (SubtitleFile) -> Unit,
sourceCallback: (ExtractorLink) -> Unit
) {
@ -182,11 +185,21 @@ class RebahinProvider : MainAPI() {
if (script.data().contains("sources: [")) {
val source = tryParseJson<ResponseLocal>(
script.data().substringAfter("sources: [").substringBefore("],"))
M3u8Helper.generateM3u8(
name,
source!!.file,
"http://172.96.161.72",
).forEach(sourceCallback)
val m3uData = app.get(source!!.file, referer = ref).text
val quality = Regex("\\d{3,4}\\.m3u8").findAll(m3uData).map { it.value }.toList()
quality.forEach {
sourceCallback.invoke(
ExtractorLink(
source = name,
name = name,
url = source.file.replace("video.m3u8", it),
referer = ref,
quality = getQualityFromName("${it.replace(".m3u8", "")}p"),
isM3u8 = true
)
)
}
val trackJson = script.data().substringAfter("tracks: [").substringBefore("],")
val track = tryParseJson<List<Tracks>>("[$trackJson]")
@ -291,6 +304,7 @@ class RebahinProvider : MainAPI() {
it.startsWith("http://172.96.161.72") -> invokeLokalSource(
it,
this.name,
"http://172.96.161.72/",
subtitleCallback,
callback
)

View File

@ -52,24 +52,26 @@ data class Image(
@JsonProperty("url") val url: String,
@JsonProperty("type") val type: String,
@JsonProperty("sc_url") val scURL: String,
@JsonProperty("proxy") val proxy: Proxy,
@JsonProperty("server") val server: Proxy
// @JsonProperty("proxy") val proxy: Proxy,
// @JsonProperty("server") val server: Proxy
)
data class Proxy(
@JsonProperty("id") val id: Long,
@JsonProperty("type") val type: String,
@JsonProperty("ip") val ip: String,
@JsonProperty("number") val number: Long,
@JsonProperty("storage") val storage: Long,
@JsonProperty("max_storage") val maxStorage: Long,
@JsonProperty("max_conversions") val maxConversions: Any? = null,
@JsonProperty("max_publications") val maxPublications: Any? = null,
@JsonProperty("created_at") val createdAt: String,
@JsonProperty("updated_at") val updatedAt: String,
@JsonProperty("upload_bandwidth") val uploadBandwidth: Any? = null,
@JsonProperty("upload_bandwidth_limit") val uploadBandwidthLimit: Any? = null
)
// Proxy is not used and crashes otherwise
//data class Proxy(
// @JsonProperty("id") val id: Long,
// @JsonProperty("type") val type: String,
// @JsonProperty("ip") val ip: String,
// @JsonProperty("number") val number: Long,
// @JsonProperty("storage") val storage: Long,
// @JsonProperty("max_storage") val maxStorage: Long,
// @JsonProperty("max_conversions") val maxConversions: Any? = null,
// @JsonProperty("max_publications") val maxPublications: Any? = null,
// @JsonProperty("created_at") val createdAt: String,
// @JsonProperty("updated_at") val updatedAt: String,
// @JsonProperty("upload_bandwidth") val uploadBandwidth: Any? = null,
// @JsonProperty("upload_bandwidth_limit") val uploadBandwidthLimit: Any? = null
//)
data class Season(
@JsonProperty("id") val id: Long,
@ -126,7 +128,7 @@ data class TrailerElement(
class StreamingcommunityProvider : MainAPI() {
override val lang = "it"
override var mainUrl = "https://streamingcommunity.top"
override var mainUrl = "https://streamingcommunity.press"
override var name = "Streamingcommunity"
override val hasMainPage = true
override val hasChromecastSupport = true

View File

@ -56,7 +56,7 @@ class TantifilmProvider : MainAPI() {
return doc.select("div.film.film-2").map {
val href = it.selectFirst("a")!!.attr("href")
val poster = it.selectFirst("img")!!.attr("src")
val name = it.selectFirst("a")!!.text().substringBefore("(")
val name = it.selectFirst("a > p")!!.text().substringBeforeLast("(")
MovieSearchResponse(
name,
href,
@ -95,7 +95,7 @@ class TantifilmProvider : MainAPI() {
val recomm = document.select("div.mediaWrap.mediaWrapAlt.recomended_videos").map {
val href = it.selectFirst("a")!!.attr("href")
val poster = it.selectFirst("img")!!.attr("src")
val name = it.selectFirst("a")!!.attr("title").substringBeforeLast("(")
val name = it.selectFirst("a > p")!!.text().substringBeforeLast("(")
MovieSearchResponse(
name,
href,

View File

@ -10,7 +10,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.loadExtractor
class WatchAsianProvider : MainAPI() {
override var mainUrl = "https://watchasian.sh"
override var mainUrl = "https://watchasian.cx"
override var name = "WatchAsian"
override val hasQuickSearch = false
override val hasMainPage = true
@ -244,4 +244,4 @@ class WatchAsianProvider : MainAPI() {
fixUrlNull(it?.attr("data-video")) ?: return@mapNotNull null
}?.toJson() ?: ""
}
}
}

View File

@ -0,0 +1,17 @@
package com.lagradost.cloudstream3.subtitles
import androidx.annotation.WorkerThread
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleEntity
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch
interface AbstractSubProvider {
@WorkerThread
suspend fun search(query: SubtitleSearch): List<SubtitleEntity>? {
throw NotImplementedError()
}
@WorkerThread
suspend fun load(data: SubtitleEntity): String? {
throw NotImplementedError()
}
}

View File

@ -0,0 +1,25 @@
package com.lagradost.cloudstream3.subtitles
import com.lagradost.cloudstream3.TvType
class AbstractSubtitleEntities {
data class SubtitleEntity(
var idPrefix : String,
var name: String = "", //Title of movie/series. This is the one to be displayed when choosing.
var lang: String = "en",
var data: String = "", //Id or link, depends on provider how to process
var type: TvType = TvType.Movie, //Movie, TV series, etc..
var epNumber: Int? = null,
var seasonNumber: Int? = null,
var year: Int? = null
)
data class SubtitleSearch(
var query: String = "",
var imdb: Long? = null,
var lang: String? = null,
var epNumber: Int? = null,
var seasonNumber: Int? = null,
var year: Int? = null
)
}

View File

@ -3,9 +3,77 @@ 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.AniListApi
import com.lagradost.cloudstream3.syncproviders.providers.MALApi
import com.lagradost.cloudstream3.syncproviders.providers.NginxApi
import com.lagradost.cloudstream3.syncproviders.providers.OpenSubtitlesApi
import java.util.concurrent.TimeUnit
abstract class AccountManager(private val defIndex: Int) : AuthAPI {
companion object {
val malApi = MALApi(0)
val aniListApi = AniListApi(0)
val openSubtitlesApi = OpenSubtitlesApi(0)
val nginxApi = NginxApi(0)
// used to login via app intent
val OAuth2Apis
get() = listOf<OAuth2API>(
malApi, aniListApi
)
// this needs init with context and can be accessed in settings
val accountManagers
get() = listOf(
malApi, aniListApi, openSubtitlesApi, nginxApi
)
// used for active syncing
val SyncApis
get() = listOf(
SyncRepo(malApi), SyncRepo(aniListApi)
)
val inAppAuths
get() = listOf(openSubtitlesApi, nginxApi)
val subtitleProviders
get() = listOf(
openSubtitlesApi
)
const val appString = "cloudstreamapp"
val unixTime: Long
get() = System.currentTimeMillis() / 1000L
val unixTimeMs: Long
get() = System.currentTimeMillis()
const val maxStale = 60 * 10
fun secondsToReadable(seconds: Int, completedValue: String): String {
var secondsLong = seconds.toLong()
val days = TimeUnit.SECONDS
.toDays(secondsLong)
secondsLong -= TimeUnit.DAYS.toSeconds(days)
val hours = TimeUnit.SECONDS
.toHours(secondsLong)
secondsLong -= TimeUnit.HOURS.toSeconds(hours)
val minutes = TimeUnit.SECONDS
.toMinutes(secondsLong)
secondsLong -= TimeUnit.MINUTES.toSeconds(minutes)
if (minutes < 0) {
return completedValue
}
//println("$days $hours $minutes")
return "${if (days != 0L) "$days" + "d " else ""}${if (hours != 0L) "$hours" + "h " else ""}${minutes}m"
}
}
abstract class AccountManager(private val defIndex: Int) : OAuth2API {
var accountIndex = defIndex
private var lastAccountIndex = defIndex
protected val accountId get() = "${idPrefix}_account_$accountIndex"
private val accountActiveKey get() = "${idPrefix}_active"
@ -35,8 +103,12 @@ abstract class AccountManager(private val defIndex: Int) : OAuth2API {
protected fun switchToNewAccount() {
val accounts = getAccounts()
lastAccountIndex = accountIndex
accountIndex = (accounts?.maxOrNull() ?: 0) + 1
}
protected fun switchToOldAccount() {
accountIndex = lastAccountIndex
}
protected fun registerAccount() {
setKey(accountActiveKey, accountIndex)

View File

@ -0,0 +1,23 @@
package com.lagradost.cloudstream3.syncproviders
interface AuthAPI {
val name: String
val icon: Int?
val requiresLogin: Boolean
val createAccountUrl : String?
// don't change this as all keys depend on it
val idPrefix: String
// if this returns null then you are not logged in
fun loginInfo(): LoginInfo?
fun logOut()
class LoginInfo(
val profilePicture: String? = null,
val name: String?,
val accountIndex: Int,
)
}

View File

@ -0,0 +1,66 @@
package com.lagradost.cloudstream3.syncproviders
import androidx.annotation.WorkerThread
interface InAppAuthAPI : AuthAPI {
data class LoginData(
val username: String? = null,
val password: String? = null,
val server: String? = null,
val email: String? = null,
)
// this is for displaying the UI
val requiresPassword: Boolean
val requiresUsername: Boolean
val requiresServer: Boolean
val requiresEmail: Boolean
// if this is false we can assume that getLatestLoginData returns null and wont be called
// this is used in case for some reason it is not preferred to store any login data besides the "token" or encrypted data
val storesPasswordInPlainText: Boolean
// return true if logged in successfully
suspend fun login(data: LoginData): Boolean
// used to fill the UI if you want to edit any data about your login info
fun getLatestLoginData(): LoginData?
}
abstract class InAppAuthAPIManager(defIndex: Int) : AccountManager(defIndex), InAppAuthAPI {
override val requiresPassword = false
override val requiresUsername = false
override val requiresEmail = false
override val requiresServer = false
override val storesPasswordInPlainText = true
override val requiresLogin = true
// runs on startup
@WorkerThread
open suspend fun initialize() {
}
override fun logOut() {
throw NotImplementedError()
}
override val idPrefix: String
get() = throw NotImplementedError()
override val name: String
get() = throw NotImplementedError()
override val icon: Int? = null
override suspend fun login(data: InAppAuthAPI.LoginData): Boolean {
throw NotImplementedError()
}
override fun getLatestLoginData(): InAppAuthAPI.LoginData? {
throw NotImplementedError()
}
override fun loginInfo(): AuthAPI.LoginInfo? {
throw NotImplementedError()
}
}

View File

@ -1,77 +1,9 @@
package com.lagradost.cloudstream3.syncproviders
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi
import com.lagradost.cloudstream3.syncproviders.providers.MALApi
import java.util.concurrent.TimeUnit
interface OAuth2API {
interface OAuth2API : AuthAPI {
val key: String
val name: String
val redirectUrl: String
// don't change this as all keys depend on it
val idPrefix: String
suspend fun handleRedirect(url: String) : Boolean
fun authenticate()
fun loginInfo(): LoginInfo?
fun logOut()
class LoginInfo(
val profilePicture: String?,
val name: String?,
val accountIndex: Int,
)
companion object {
val malApi = MALApi(0)
val aniListApi = AniListApi(0)
// used to login via app intent
val OAuth2Apis
get() = listOf<OAuth2API>(
malApi, aniListApi
)
// this needs init with context and can be accessed in settings
val OAuth2accountApis
get() = listOf<AccountManager>(
malApi, aniListApi
)
// used for active syncing
val SyncApis
get() = listOf(
SyncRepo(malApi), SyncRepo(aniListApi)
)
const val appString = "cloudstreamapp"
val unixTime: Long
get() = System.currentTimeMillis() / 1000L
const val maxStale = 60 * 10
fun secondsToReadable(seconds: Int, completedValue: String): String {
var secondsLong = seconds.toLong()
val days = TimeUnit.SECONDS
.toDays(secondsLong)
secondsLong -= TimeUnit.DAYS.toSeconds(days)
val hours = TimeUnit.SECONDS
.toHours(secondsLong)
secondsLong -= TimeUnit.HOURS.toSeconds(hours)
val minutes = TimeUnit.SECONDS
.toMinutes(secondsLong)
secondsLong -= TimeUnit.MINUTES.toSeconds(minutes)
if (minutes < 0) {
return completedValue
}
//println("$days $hours $minutes")
return "${if (days != 0L) "$days" + "d " else ""}${if (hours != 0L) "$hours" + "h " else ""}${minutes}m"
}
}
}

View File

@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.syncproviders
import com.lagradost.cloudstream3.*
interface SyncAPI : OAuth2API {
val icon: Int
val mainUrl: String
/**

View File

@ -12,10 +12,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.OAuth2API
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.appString
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.maxStale
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.unixTime
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
import com.lagradost.cloudstream3.utils.AppUtils.toJson
@ -32,11 +29,13 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
override val idPrefix = "anilist"
override var mainUrl = "https://anilist.co"
override val icon = R.drawable.ic_anilist_icon
override val requiresLogin = true
override val createAccountUrl = "$mainUrl/signup"
override fun loginInfo(): OAuth2API.LoginInfo? {
override fun loginInfo(): AuthAPI.LoginInfo? {
// context.getUser(true)?.
getKey<AniListUser>(accountId, ANILIST_USER_KEY)?.let { user ->
return OAuth2API.LoginInfo(
return AuthAPI.LoginInfo(
profilePicture = user.picture,
name = user.name,
accountIndex = accountIndex

View File

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.syncproviders.providers
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.OAuth2API
//TODO dropbox sync
@ -8,6 +9,11 @@ class Dropbox : OAuth2API {
override var name = "Dropbox"
override val key = "zlqsamadlwydvb2"
override val redirectUrl = "dropboxlogin"
override val requiresLogin = true
override val createAccountUrl: String? = null
override val icon: Int
get() = TODO("Not yet implemented")
override fun authenticate() {
TODO("Not yet implemented")
@ -21,7 +27,7 @@ class Dropbox : OAuth2API {
TODO("Not yet implemented")
}
override fun loginInfo(): OAuth2API.LoginInfo? {
override fun loginInfo(): AuthAPI.LoginInfo? {
TODO("Not yet implemented")
}
}

View File

@ -14,10 +14,7 @@ import com.lagradost.cloudstream3.ShowStatus
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.OAuth2API
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.appString
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.secondsToReadable
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.unixTime
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
@ -37,15 +34,18 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
override val idPrefix = "mal"
override var mainUrl = "https://myanimelist.net"
override val icon = R.drawable.mal_logo
override val requiresLogin = true
override val createAccountUrl = "$mainUrl/register.php"
override fun logOut() {
removeAccountKeys()
}
override fun loginInfo(): OAuth2API.LoginInfo? {
override fun loginInfo(): AuthAPI.LoginInfo? {
//getMalUser(true)?
getKey<MalUser>(accountId, MAL_USER_KEY)?.let { user ->
return OAuth2API.LoginInfo(
return AuthAPI.LoginInfo(
profilePicture = user.picture,
name = user.name,
accountIndex = accountIndex

View File

@ -0,0 +1,60 @@
package com.lagradost.cloudstream3.syncproviders.providers
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.movieproviders.NginxProvider
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager
class NginxApi(index: Int) : InAppAuthAPIManager(index) {
override val name = "Nginx"
override val idPrefix = "nginx"
override val icon = R.drawable.nginx
override val requiresUsername = true
override val requiresPassword = true
override val requiresServer = true
override val createAccountUrl = "https://www.sarlays.com/use-nginx-with-cloudstream/"
companion object {
const val NGINX_USER_KEY: String = "nginx_user"
}
override fun getLatestLoginData(): InAppAuthAPI.LoginData? {
return getKey(accountId, NGINX_USER_KEY)
}
override fun loginInfo(): AuthAPI.LoginInfo? {
val data = getLatestLoginData() ?: return null
return AuthAPI.LoginInfo(name = data.username ?: data.server, accountIndex = accountIndex)
}
override suspend fun login(data: InAppAuthAPI.LoginData): Boolean {
if (data.server.isNullOrBlank()) return false // we require a server
switchToNewAccount()
setKey(accountId, NGINX_USER_KEY, data)
registerAccount()
initialize()
return true
}
override fun logOut() {
removeAccountKeys()
initializeData()
}
private fun initializeData() {
val data = getLatestLoginData() ?: run {
NginxProvider.overrideUrl = null
NginxProvider.loginCredentials = null
return
}
NginxProvider.overrideUrl = data.server?.removeSuffix("/")
NginxProvider.loginCredentials = "${data.username ?: ""}:${data.password ?: ""}"
}
override suspend fun initialize() {
initializeData()
}
}

View File

@ -0,0 +1,311 @@
package com.lagradost.cloudstream3.syncproviders.providers
import android.util.Log
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.AbstractSubProvider
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager
import com.lagradost.cloudstream3.utils.AppUtils
class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubProvider {
override val idPrefix = "opensubtitles"
override val name = "OpenSubtitles"
override val icon = R.drawable.open_subtitles_icon
override val requiresPassword = true
override val requiresUsername = true
override val createAccountUrl = "https://www.opensubtitles.com/en/users/sign_up"
companion object {
const val OPEN_SUBTITLES_USER_KEY: String = "open_subtitles_user" // user data like profile
const val apiKey = "uyBLgFD17MgrYmA0gSXoKllMJBelOYj2"
const val host = "https://api.opensubtitles.com/api/v1"
const val TAG = "OPENSUBS"
const val coolDownDuration: Long = 1000L * 30L // CoolDown if 429 error code in ms
var currentCoolDown: Long = 0L
var currentSession: SubtitleOAuthEntity? = null
}
private fun canDoRequest(): Boolean {
return unixTimeMs > currentCoolDown
}
private fun throwIfCantDoRequest() {
if (!canDoRequest()) {
throw ErrorLoadingException("Too many requests wait for ${(currentCoolDown - unixTimeMs) / 1000L}s")
}
}
private fun throwGotTooManyRequests() {
currentCoolDown = unixTimeMs + coolDownDuration
throw ErrorLoadingException("Too many requests")
}
private fun getAuthKey(): SubtitleOAuthEntity? {
return getKey(accountId, OPEN_SUBTITLES_USER_KEY)
}
private fun setAuthKey(data: SubtitleOAuthEntity?) {
if (data == null) removeKey(accountId, OPEN_SUBTITLES_USER_KEY)
currentSession = data
setKey(accountId, OPEN_SUBTITLES_USER_KEY, data)
}
override fun loginInfo(): AuthAPI.LoginInfo? {
getAuthKey()?.let { user ->
return AuthAPI.LoginInfo(
profilePicture = null,
name = user.user,
accountIndex = accountIndex
)
}
return null
}
override fun getLatestLoginData(): InAppAuthAPI.LoginData? {
val current = getAuthKey() ?: return null
return InAppAuthAPI.LoginData(username = current.user, current.pass)
}
/*
Authorize app to connect to API, using username/password.
Required to run at startup.
Returns OAuth entity with valid access token.
*/
override suspend fun initialize() {
currentSession = getAuthKey() ?: return // just in case the following fails
initLogin(currentSession?.user ?: return, currentSession?.pass ?: return)
}
override fun logOut() {
setAuthKey(null)
removeAccountKeys()
currentSession = getAuthKey()
}
private suspend fun initLogin(username: String, password: String): Boolean {
//Log.i(TAG, "DATA = [$username] [$password]")
val response = app.post(
url = "$host/login",
headers = mapOf(
"Api-Key" to apiKey,
"Content-Type" to "application/json"
),
data = mapOf(
"username" to username,
"password" to password
)
)
//Log.i(TAG, "Responsecode = ${response.code}")
//Log.i(TAG, "Result => ${response.text}")
if (response.isSuccessful) {
AppUtils.tryParseJson<OAuthToken>(response.text)?.let { token ->
setAuthKey(
SubtitleOAuthEntity(
user = username,
pass = password,
access_token = token.token ?: run {
return false
})
)
}
return true
}
return false
}
override suspend fun login(data: InAppAuthAPI.LoginData): Boolean {
val username = data.username ?: throw ErrorLoadingException("Requires Username")
val password = data.password ?: throw ErrorLoadingException("Requires Password")
switchToNewAccount()
try {
if (initLogin(username, password)) {
registerAccount()
return true
}
} catch (e: Exception) {
logError(e)
switchToOldAccount()
}
switchToOldAccount()
return false
}
/*
Fetch subtitles using token authenticated on previous method (see authorize).
Returns list of Subtitles which user can select to download (see load).
*/
override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List<AbstractSubtitleEntities.SubtitleEntity>? {
throwIfCantDoRequest()
val imdbId = query.imdb ?: 0
val queryText = query.query.replace(" ", "+")
val epNum = query.epNumber ?: 0
val seasonNum = query.seasonNumber ?: 0
val yearNum = query.year ?: 0
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 (imdbId > 0) {
//Use imdb_id to search if its valid
true -> "$host/subtitles?imdb_id=$imdbId&languages=${query.lang}$yearQuery$epQuery$seasonQuery"
false -> "$host/subtitles?query=$queryText&languages=${query.lang}$yearQuery$epQuery$seasonQuery"
}
val req = app.get(
url = searchQueryUrl,
headers = mapOf(
Pair("Api-Key", apiKey),
Pair("Content-Type", "application/json")
)
)
Log.i(TAG, "Search Req => ${req.text}")
if (!req.isSuccessful) {
if (req.code == 429)
throwGotTooManyRequests()
return null
}
val results = mutableListOf<AbstractSubtitleEntities.SubtitleEntity>()
AppUtils.tryParseJson<Results>(req.text)?.let {
it.data?.forEach { item ->
val attr = item.attributes ?: return@forEach
val featureDetails = attr.featDetails
//Use any valid name/title in hierarchy
val name = featureDetails?.movieName ?: featureDetails?.title
?: featureDetails?.parentTitle ?: attr.release ?: ""
val lang = attr.language ?: ""
val resEpNum = featureDetails?.episodeNumber ?: query.epNumber
val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber
val year = featureDetails?.year ?: query.year
val type = if ((resSeasonNum ?: 0) > 0) TvType.TvSeries else TvType.Movie
//Log.i(TAG, "Result id/name => ${item.id} / $name")
item.attributes?.files?.forEach { file ->
val resultData = file.fileId?.toString() ?: ""
//Log.i(TAG, "Result file => ${file.fileId} / ${file.fileName}")
results.add(
AbstractSubtitleEntities.SubtitleEntity(
idPrefix = this.idPrefix,
name = name,
lang = lang,
data = resultData,
type = type,
epNumber = resEpNum,
seasonNumber = resSeasonNum,
year = year
)
)
}
}
}
return results
}
/*
Process data returned from search.
Returns string url for the subtitle file.
*/
override suspend fun load(data: AbstractSubtitleEntities.SubtitleEntity): String? {
throwIfCantDoRequest()
val req = app.post(
url = "$host/download",
headers = mapOf(
Pair(
"Authorization",
"Bearer ${currentSession?.access_token ?: throw ErrorLoadingException("No access token active in current session")}"
),
Pair("Api-Key", apiKey),
Pair("Content-Type", "application/json"),
Pair("Accept", "*/*")
),
data = mapOf(
Pair("file_id", data.data)
)
)
Log.i(TAG, "Request result => (${req.code}) ${req.text}")
//Log.i(TAG, "Request headers => ${req.headers}")
if (req.isSuccessful) {
AppUtils.tryParseJson<ResultDownloadLink>(req.text)?.let {
val link = it.link ?: ""
Log.i(TAG, "Request load link => $link")
return link
}
} else {
if (req.code == 429)
throwGotTooManyRequests()
}
return null
}
data class SubtitleOAuthEntity(
var user: String,
var pass: String,
var access_token: String,
)
data class OAuthToken(
@JsonProperty("token") var token: String? = null,
@JsonProperty("status") var status: Int? = null
)
data class Results(
@JsonProperty("data") var data: List<ResultData>? = listOf()
)
data class ResultData(
@JsonProperty("id") var id: String? = null,
@JsonProperty("type") var type: String? = null,
@JsonProperty("attributes") var attributes: ResultAttributes? = ResultAttributes()
)
data class ResultAttributes(
@JsonProperty("subtitle_id") var subtitleId: String? = null,
@JsonProperty("language") var language: String? = null,
@JsonProperty("release") var release: String? = null,
@JsonProperty("url") var url: String? = null,
@JsonProperty("files") var files: List<ResultFiles>? = listOf(),
@JsonProperty("feature_details") var featDetails: ResultFeatureDetails? = ResultFeatureDetails()
)
data class ResultFiles(
@JsonProperty("file_id") var fileId: Int? = null,
@JsonProperty("file_name") var fileName: String? = null
)
data class ResultDownloadLink(
@JsonProperty("link") var link: String? = null,
@JsonProperty("file_name") var fileName: String? = null,
@JsonProperty("requests") var requests: Int? = null,
@JsonProperty("remaining") var remaining: Int? = null,
@JsonProperty("message") var message: String? = null,
@JsonProperty("reset_time") var resetTime: String? = null,
@JsonProperty("reset_time_utc") var resetTimeUtc: String? = null
)
data class ResultFeatureDetails(
@JsonProperty("year") var year: Int? = null,
@JsonProperty("title") var title: String? = null,
@JsonProperty("movie_name") var movieName: String? = null,
@JsonProperty("imdb_id") var imdbId: Int? = null,
@JsonProperty("tmdb_id") var tmdbId: Int? = null,
@JsonProperty("season_number") var seasonNumber: Int? = null,
@JsonProperty("episode_number") var episodeNumber: Int? = null,
@JsonProperty("parent_imdb_id") var parentImdbId: Int? = null,
@JsonProperty("parent_title") var parentTitle: String? = null,
@JsonProperty("parent_tmdb_id") var parentTmdbId: Int? = null,
@JsonProperty("parent_feature_id") var parentFeatureId: Int? = null
)
}

View File

@ -33,7 +33,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.syncproviders.OAuth2API
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi
import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
@ -227,7 +227,7 @@ class HomeFragment : Fragment() {
listView?.choiceMode = AbsListView.CHOICE_MODE_SINGLE
listView?.setOnItemClickListener { _, _, i, _ ->
if (!currentValidApis.isNullOrEmpty()) {
if (currentValidApis.isNotEmpty()) {
currentApiName = currentValidApis[i].name
//to switch to apply simply remove this
currentApiName?.let(callback)
@ -882,7 +882,7 @@ class HomeFragment : Fragment() {
home_change_api_loading?.isVisible = false
}
for (syncApi in OAuth2API.OAuth2Apis) {
for (syncApi in OAuth2Apis) {
val login = syncApi.loginInfo()
val pic = login?.profilePicture
if (home_profile_picture?.setImage(

View File

@ -68,6 +68,8 @@ abstract class AbstractPlayerFragment(
var subStyle: SaveCaptionStyle? = null
var subView: SubtitleView? = null
var isBuffering = true
protected open var hasPipModeSupport = true
@LayoutRes
protected var layout: Int = R.layout.fragment_player
@ -154,7 +156,7 @@ abstract class AbstractPlayerFragment(
}
}
canEnterPipMode = isPlayingRightNow
canEnterPipMode = isPlayingRightNow && hasPipModeSupport
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isInPIPMode) {
activity?.let { act ->
PlayerPipHelper.updatePIPModeActions(act, isPlayingRightNow)
@ -213,7 +215,13 @@ abstract class AbstractPlayerFragment(
throw NotImplementedError()
}
private fun playerError(exception: Exception) {
private fun requestAudioFocus() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activity?.requestLocalAudioFocus(AppUtils.getFocusRequest())
}
}
open fun playerError(exception: Exception) {
val ctx = context ?: return
when (exception) {
is PlaybackException -> {
@ -267,12 +275,6 @@ abstract class AbstractPlayerFragment(
}
}
private fun requestAudioFocus() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activity?.requestLocalAudioFocus(AppUtils.getFocusRequest())
}
}
private fun onSubStyleChanged(style: SaveCaptionStyle) {
if (player is CS3IPlayer) {
player.updateSubtitleStyle(style)
@ -394,6 +396,7 @@ abstract class AbstractPlayerFragment(
override fun onDestroy() {
playerEventListener = null
keyEventListener = null
canEnterPipMode = false
SubtitlesFragment.applyStyleEvent -= ::onSubStyleChanged
keepScreenOn(false)

View File

@ -1,11 +1,17 @@
package com.lagradost.cloudstream3.ui.player
import android.annotation.SuppressLint
import android.content.Context
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.util.SparseArray
import android.widget.FrameLayout
import androidx.core.util.forEach
import at.huber.youtubeExtractor.VideoMeta
import at.huber.youtubeExtractor.YouTubeExtractor
import at.huber.youtubeExtractor.YtFile
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource
@ -23,6 +29,7 @@ import com.google.android.exoplayer2.upstream.cache.CacheDataSource
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
import com.google.android.exoplayer2.upstream.cache.SimpleCache
import com.google.android.exoplayer2.util.MimeTypes
import com.google.android.exoplayer2.video.VideoSize
import com.lagradost.cloudstream3.APIHolder.getApiFromName
import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.app
@ -31,6 +38,7 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorUri
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
import java.io.File
import javax.net.ssl.HttpsURLConnection
@ -153,7 +161,8 @@ class CS3IPlayer : IPlayer {
data: ExtractorUri?,
startPosition: Long?,
subtitles: Set<SubtitleData>,
subtitle: SubtitleData?
subtitle: SubtitleData?,
autoPlay: Boolean?
) {
Log.i(TAG, "loadPlayer")
if (sameEpisode) {
@ -168,7 +177,7 @@ class CS3IPlayer : IPlayer {
}
// we want autoplay because of TV and UX
isPlaying = true
isPlaying = autoPlay ?: isPlaying
// release the current exoplayer and cache
releasePlayer()
@ -322,6 +331,7 @@ class CS3IPlayer : IPlayer {
}
companion object {
private var ytVideos: MutableMap<String, YtFile> = mutableMapOf()
private var simpleCache: SimpleCache? = null
var requestSubtitleUpdate: (() -> Unit)? = null
@ -686,6 +696,14 @@ class CS3IPlayer : IPlayer {
isPlaying = exo.isPlaying
}
when (playbackState) {
Player.STATE_READY -> {
onRenderFirst()
}
else -> {}
}
if (playWhenReady) {
when (playbackState) {
Player.STATE_READY -> {
@ -715,45 +733,41 @@ class CS3IPlayer : IPlayer {
// super.onCues(cues.map { cue -> cue.buildUpon().setText("Hello world").setSize(Cue.DIMEN_UNSET).build() })
//}
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying)
if (isPlaying) {
onRenderFirst()
}
}
override fun onPlaybackStateChanged(playbackState: Int) {
super.onPlaybackStateChanged(playbackState)
when (playbackState) {
Player.STATE_READY -> {
requestAutoFocus?.invoke()
}
Player.STATE_ENDED -> {
handleEvent(CSPlayerEvent.NextEpisode)
}
Player.STATE_BUFFERING -> {
updatedTime()
}
Player.STATE_IDLE -> {
// IDLE
}
else -> Unit
}
}
override fun onVideoSizeChanged(videoSize: VideoSize) {
super.onVideoSizeChanged(videoSize)
playerDimensionsLoaded?.invoke(Pair(videoSize.width, videoSize.height))
}
override fun onRenderedFirstFrame() {
updatedTime()
if (!hasUsedFirstRender) { // this insures that we only call this once per player load
Log.i(TAG, "Rendered first frame")
val invalid = exoPlayer?.duration?.let { duration ->
// Only errors short playback when not playing downloaded files
duration < 20_000L && currentDownloadedFile == null
} ?: false
if (invalid) {
releasePlayer(saveTime = false)
playerError?.invoke(InvalidFileException("Too short playback"))
return
}
setPreferredSubtitles(currentSubtitles)
hasUsedFirstRender = true
val format = exoPlayer?.videoFormat
val width = format?.width
val height = format?.height
if (height != null && width != null) {
playerDimensionsLoaded?.invoke(Pair(width, height))
updatedTime()
exoPlayer?.apply {
requestedListeningPercentages?.forEach { percentage ->
createMessage { _, _ ->
updatedTime()
}
.setLooper(Looper.getMainLooper())
.setPosition( /* positionMs= */contentDuration * percentage / 100)
// .setPayload(customPayloadData)
.setDeleteAfterDelivery(false)
.send()
}
}
}
}
super.onRenderedFirstFrame()
onRenderFirst()
}
})
} catch (e: Exception) {
@ -762,6 +776,45 @@ class CS3IPlayer : IPlayer {
}
}
fun onRenderFirst() {
if (!hasUsedFirstRender) { // this insures that we only call this once per player load
Log.i(TAG, "Rendered first frame")
val invalid = exoPlayer?.duration?.let { duration ->
// Only errors short playback when not playing downloaded files
duration < 20_000L && currentDownloadedFile == null
} ?: false
if (invalid) {
releasePlayer(saveTime = false)
playerError?.invoke(InvalidFileException("Too short playback"))
return
}
setPreferredSubtitles(currentSubtitles)
hasUsedFirstRender = true
val format = exoPlayer?.videoFormat
val width = format?.width
val height = format?.height
if (height != null && width != null) {
playerDimensionsLoaded?.invoke(Pair(width, height))
updatedTime()
exoPlayer?.apply {
requestedListeningPercentages?.forEach { percentage ->
createMessage { _, _ ->
updatedTime()
}
.setLooper(Looper.getMainLooper())
.setPosition( /* positionMs= */contentDuration * percentage / 100)
// .setPayload(customPayloadData)
.setDeleteAfterDelivery(false)
.send()
}
}
}
}
}
private fun loadOfflinePlayer(context: Context, data: ExtractorUri) {
Log.i(TAG, "loadOfflinePlayer")
try {
@ -815,10 +868,6 @@ class CS3IPlayer : IPlayer {
null
}
}
SubtitleOrigin.OPEN_SUBTITLES -> {
// TODO
throw NotImplementedError()
}
SubtitleOrigin.EMBEDDED_IN_VIDEO -> {
if (offlineSourceFactory != null) {
activeSubtitles.add(sub)
@ -833,9 +882,55 @@ class CS3IPlayer : IPlayer {
return Pair(subSources, activeSubtitles)
}
fun loadYtFile(context: Context, yt: YtFile) {
loadOnlinePlayer(
context,
ExtractorLink(
"YouTube",
"",
yt.url,
"",
yt.format?.height ?: Qualities.Unknown.value
)
)
}
private fun loadOnlinePlayer(context: Context, link: ExtractorLink) {
Log.i(TAG, "loadOnlinePlayer")
Log.i(TAG, "loadOnlinePlayer $link")
try {
if (link.url.contains("youtube.com")) {
val ytLink = link.url.replace("/embed/", "/watch?v=")
ytVideos[ytLink]?.let {
loadYtFile(context, it)
return
}
val ytExtractor =
@SuppressLint("StaticFieldLeak")
object : YouTubeExtractor(context) {
override fun onExtractionComplete(
ytFiles: SparseArray<YtFile>?,
videoMeta: VideoMeta?
) {
var yt: YtFile? = null
ytFiles?.forEach { _, value ->
if ((yt?.format?.height ?: 0) < (value.format?.height
?: -1) && (value.format?.audioBitrate ?: -1) > 0
) {
yt = value
}
}
yt?.let { ytf ->
ytVideos[ytLink] = ytf
loadYtFile(context, ytf)
}
}
}
Log.i(TAG, "YouTube extraction on $ytLink")
ytExtractor.extract(ytLink)
return
}
currentLink = link
if (ignoreSSL) {

View File

@ -1,6 +1,8 @@
package com.lagradost.cloudstream3.ui.player
import android.content.Context
import android.util.Log
import androidx.preference.PreferenceManager
import com.google.android.exoplayer2.Format
import com.google.android.exoplayer2.text.SubtitleDecoder
import com.google.android.exoplayer2.text.SubtitleDecoderFactory
@ -11,14 +13,32 @@ import com.google.android.exoplayer2.text.subrip.SubripDecoder
import com.google.android.exoplayer2.text.ttml.TtmlDecoder
import com.google.android.exoplayer2.text.webvtt.WebvttDecoder
import com.google.android.exoplayer2.util.MimeTypes
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import org.mozilla.universalchardet.UniversalDetector
import java.nio.ByteBuffer
import java.nio.charset.Charset
class CustomDecoder : SubtitleDecoder {
companion object {
fun updateForcedEncoding(context: Context) {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
val value = settingsManager.getString(
context.getString(R.string.subtitles_encoding_key),
null
)
overrideEncoding = if (value.isNullOrBlank()) {
null
} else {
value
}
}
private const val UTF_8 = "UTF-8"
private const val TAG = "CustomDecoder"
private var overrideEncoding: String? = null
var regexSubtitlesToRemoveCaptions = false
var regexSubtitlesToRemoveBloat = false
val bloatRegex =
listOf(
Regex(
@ -40,6 +60,8 @@ class CustomDecoder : SubtitleDecoder {
)
val captionRegex = listOf(Regex("""(-\s?|)[\[({][\w\d\s]*?[])}]\s*"""))
//https://emptycharacter.com/
//https://www.fileformat.info/info/unicode/char/200b/index.htm
fun trimStr(string: String): String {
return string.trimStart().trim('\uFEFF', '\u200B').replace(
Regex("[\u00A0\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u205F]"),
@ -59,73 +81,118 @@ class CustomDecoder : SubtitleDecoder {
return realDecoder?.dequeueInputBuffer() ?: SubtitleInputBuffer()
}
private fun getStr(byteArray: ByteArray): Pair<String, Charset> {
val encoding = try {
val encoding = overrideEncoding ?: run {
val detector = UniversalDetector()
detector.handleData(byteArray, 0, byteArray.size)
detector.dataEnd()
detector.detectedCharset // "windows-1256"
}
Log.i(
TAG,
"Detected encoding with charset $encoding and override = $overrideEncoding"
)
encoding ?: UTF_8
} catch (e: Exception) {
Log.e(TAG, "Failed to detect encoding throwing error")
logError(e)
UTF_8
}
return try {
val set = charset(encoding)
Pair(String(byteArray, set), set)
} catch (e: Exception) {
Log.e(TAG, "Failed to parse using encoding $encoding")
logError(e)
Pair(byteArray.decodeToString(), charset(UTF_8))
}
}
private fun getStr(input: SubtitleInputBuffer): String? {
try {
val data = input.data ?: return null
data.position(0)
val fullDataArr = ByteArray(data.remaining())
data.get(fullDataArr)
return trimStr(getStr(fullDataArr).first)
} catch (e: Exception) {
Log.e(TAG, "Failed to parse text returning plain data")
logError(e)
return null
}
}
override fun queueInputBuffer(inputBuffer: SubtitleInputBuffer) {
Log.i(TAG, "queueInputBuffer")
try {
if (realDecoder == null) {
inputBuffer.data?.let { data ->
// this way we read the subtitle file and decide what decoder to use instead of relying on mimetype
val pos = data.position()
data.position(0)
val arr = ByteArray(minOf(data.remaining(), 100))
data.get(arr)
data.position(pos)
//https://emptycharacter.com/
//https://www.fileformat.info/info/unicode/char/200b/index.htm
val str = trimStr(arr.decodeToString())
Log.i(TAG, "Got data from queueInputBuffer")
Log.i(TAG, "first string is >>>$str<<<")
if (str.isNotEmpty()) {
//https://github.com/LagradOst/CloudStream-2/blob/ddd774ee66810137ff7bd65dae70bcf3ba2d2489/CloudStreamForms/CloudStreamForms/Script/MainChrome.cs#L388
realDecoder = when {
str.startsWith("WEBVTT", ignoreCase = true) -> WebvttDecoder()
str.startsWith("<?xml version=\"", ignoreCase = true) -> TtmlDecoder()
(str.startsWith(
"[Script Info]",
ignoreCase = true
) || str.startsWith("Title:", ignoreCase = true)) -> SsaDecoder()
str.startsWith("1", ignoreCase = true) -> SubripDecoder()
else -> null
val inputString = getStr(inputBuffer)
if (realDecoder == null && !inputString.isNullOrBlank()) {
var str: String = inputString
// this way we read the subtitle file and decide what decoder to use instead of relying on mimetype
Log.i(TAG, "Got data from queueInputBuffer")
//https://github.com/LagradOst/CloudStream-2/blob/ddd774ee66810137ff7bd65dae70bcf3ba2d2489/CloudStreamForms/CloudStreamForms/Script/MainChrome.cs#L388
realDecoder = when {
str.startsWith("WEBVTT", ignoreCase = true) -> WebvttDecoder()
str.startsWith("<?xml version=\"", ignoreCase = true) -> TtmlDecoder()
(str.startsWith(
"[Script Info]",
ignoreCase = true
) || str.startsWith("Title:", ignoreCase = true)) -> SsaDecoder()
str.startsWith("1", ignoreCase = true) -> SubripDecoder()
else -> null
}
Log.i(
TAG,
"Decoder selected: $realDecoder"
)
realDecoder?.let { decoder ->
decoder.dequeueInputBuffer()?.let { buff ->
if (decoder::class.java != SsaDecoder::class.java) {
if (regexSubtitlesToRemoveCaptions)
captionRegex.forEach { rgx ->
str = str.replace(rgx, "\n")
}
if (regexSubtitlesToRemoveBloat)
bloatRegex.forEach { rgx ->
str = str.replace(rgx, "\n")
}
}
buff.data = ByteBuffer.wrap(str.toByteArray(charset(UTF_8)))
decoder.queueInputBuffer(buff)
Log.i(
TAG,
"Decoder selected: $realDecoder"
"Decoder queueInputBuffer successfully"
)
val decoder = realDecoder
if (decoder != null) {
decoder.dequeueInputBuffer()?.let { buff ->
if (regexSubtitlesToRemoveCaptions && decoder::class.java != SsaDecoder::class.java) {
try {
data.position(0)
val fullDataArr = ByteArray(data.remaining())
data.get(fullDataArr)
var fullStr = trimStr(fullDataArr.decodeToString())
bloatRegex.forEach { rgx ->
fullStr = fullStr.replace(rgx, "\n")
}
captionRegex.forEach { rgx ->
fullStr = fullStr.replace(rgx, "\n")
}
fullStr.replace(Regex("(\r\n|\r|\n){2,}"), "\n")
buff.data = ByteBuffer.wrap(fullStr.toByteArray())
} catch (e: Exception) {
data.position(pos)
buff.data = data
}
} else {
buff.data = data
}
decoder.queueInputBuffer(buff)
}
CS3IPlayer.requestSubtitleUpdate?.invoke()
}
}
CS3IPlayer.requestSubtitleUpdate?.invoke()
}
} else {
Log.i(
TAG,
"Decoder else queueInputBuffer successfully"
)
if (!inputString.isNullOrBlank()) {
var str: String = inputString
if (realDecoder!!::class.java != SsaDecoder::class.java) {
if (regexSubtitlesToRemoveCaptions)
captionRegex.forEach { rgx ->
str = str.replace(rgx, "\n")
}
if (regexSubtitlesToRemoveBloat)
bloatRegex.forEach { rgx ->
str = str.replace(rgx, "\n")
}
}
inputBuffer.data = ByteBuffer.wrap(str.toByteArray(charset(UTF_8)))
}
realDecoder?.queueInputBuffer(inputBuffer)
}
} catch (e: Exception) {

View File

@ -38,6 +38,7 @@ import com.lagradost.cloudstream3.CommonActivity.keyEventListener
import com.lagradost.cloudstream3.CommonActivity.playerEventListener
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer.Companion.subsProvidersIsActive
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
@ -50,6 +51,28 @@ import com.lagradost.cloudstream3.utils.UIHelper.showSystemUI
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import com.lagradost.cloudstream3.utils.Vector2
import kotlinx.android.synthetic.main.player_custom_layout.*
import kotlinx.android.synthetic.main.player_custom_layout.bottom_player_bar
import kotlinx.android.synthetic.main.player_custom_layout.exo_ffwd
import kotlinx.android.synthetic.main.player_custom_layout.exo_ffwd_text
import kotlinx.android.synthetic.main.player_custom_layout.exo_progress
import kotlinx.android.synthetic.main.player_custom_layout.exo_rew
import kotlinx.android.synthetic.main.player_custom_layout.exo_rew_text
import kotlinx.android.synthetic.main.player_custom_layout.player_center_menu
import kotlinx.android.synthetic.main.player_custom_layout.player_ffwd_holder
import kotlinx.android.synthetic.main.player_custom_layout.player_holder
import kotlinx.android.synthetic.main.player_custom_layout.player_pause_play
import kotlinx.android.synthetic.main.player_custom_layout.player_pause_play_holder
import kotlinx.android.synthetic.main.player_custom_layout.player_progressbar_left
import kotlinx.android.synthetic.main.player_custom_layout.player_progressbar_left_holder
import kotlinx.android.synthetic.main.player_custom_layout.player_progressbar_left_icon
import kotlinx.android.synthetic.main.player_custom_layout.player_progressbar_right
import kotlinx.android.synthetic.main.player_custom_layout.player_progressbar_right_holder
import kotlinx.android.synthetic.main.player_custom_layout.player_progressbar_right_icon
import kotlinx.android.synthetic.main.player_custom_layout.player_rew_holder
import kotlinx.android.synthetic.main.player_custom_layout.player_time_text
import kotlinx.android.synthetic.main.player_custom_layout.player_video_bar
import kotlinx.android.synthetic.main.player_custom_layout.shadow_overlay
import kotlinx.android.synthetic.main.trailer_custom_layout.*
import kotlin.math.*
const val MINIMUM_SEEK_TIME = 7000L // when swipe seeking
@ -63,6 +86,9 @@ const val DOUBLE_TAB_PAUSE_PERCENTAGE = 0.15 // in both directions
// All the UI Logic for the player
open class FullScreenPlayer : AbstractPlayerFragment() {
protected open var lockRotation = true
protected open var isFullScreenPlayer = true
// state of player UI
protected var isShowing = false
protected var isLocked = false
@ -99,11 +125,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
// screenWidth and screenHeight does always
// refer to the screen while in landscape mode
private val screenWidth: Int
protected val screenWidth: Int
get() {
return max(displayMetrics.widthPixels, displayMetrics.heightPixels)
}
private val screenHeight: Int
protected val screenHeight: Int
get() {
return min(displayMetrics.widthPixels, displayMetrics.heightPixels)
}
@ -138,6 +164,14 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
throw NotImplementedError()
}
open fun openOnlineSubPicker(
context: Context,
imdbId: Long?,
dismissCallback: (() -> Unit)
) {
throw NotImplementedError()
}
/** Returns false if the touch is on the status bar or navigation bar*/
private fun isValidTouch(rawX: Float, rawY: Float): Boolean {
val statusHeight = statusBarHeight ?: 0
@ -150,7 +184,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
animateLayoutChanges()
}
private fun animateLayoutChanges() {
protected fun animateLayoutChanges() {
if (isShowing) {
updateUIVisibility()
} else {
@ -199,7 +233,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
player_ffwd_holder?.alpha = 1f
player_rew_holder?.alpha = 1f
// player_pause_play_holder?.alpha = 1f
shadow_overlay?.isVisible = true
shadow_overlay?.startAnimation(fadeAnimation)
player_ffwd_holder?.startAnimation(fadeAnimation)
player_rew_holder?.startAnimation(fadeAnimation)
@ -224,20 +258,22 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
player_subtitle_offset_btt?.isGone = player.getCurrentPreferredSubtitle() == null
}
override fun onResume() {
activity?.hideSystemUI()
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && fullscreenNotch) {
val params = activity?.window?.attributes
params?.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
activity?.window?.attributes = params
protected fun enterFullscreen() {
if (isFullScreenPlayer) {
activity?.hideSystemUI()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && fullscreenNotch) {
val params = activity?.window?.attributes
params?.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
activity?.window?.attributes = params
}
}
super.onResume()
if (lockRotation)
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
}
override fun onDestroy() {
protected fun exitFullscreen() {
activity?.showSystemUI()
//if (lockRotation)
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
// simply resets brightness and notch settings that might have been overridden
@ -248,6 +284,15 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
}
activity?.window?.attributes = lp
}
override fun onResume() {
enterFullscreen()
super.onResume()
}
override fun onDestroy() {
exitFullscreen()
super.onDestroy()
}
@ -327,7 +372,8 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
}
dialog.setOnDismissListener {
activity?.hideSystemUI()
if (isFullScreenPlayer)
activity?.hideSystemUI()
}
applyButton.setOnClickListener {
dialog.dismissSafe(activity)
@ -365,9 +411,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
act.getString(R.string.player_speed),
false,
{
activity?.hideSystemUI()
if (isFullScreenPlayer)
activity?.hideSystemUI()
}) { index ->
activity?.hideSystemUI()
if (isFullScreenPlayer)
activity?.hideSystemUI()
setPlayBackSpeed(speedsNumbers[index])
}
}
@ -446,9 +494,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
private fun onClickChange() {
isShowing = !isShowing
if (isShowing) {
player_intro_play?.isGone = true
autoHide()
}
activity?.hideSystemUI()
if (isFullScreenPlayer)
activity?.hideSystemUI()
animateLayoutChanges()
player_pause_play?.requestFocus()
}
@ -492,6 +542,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
player_lock_holder?.startAnimation(fadeAnimation)
//player_go_back_holder?.startAnimation(fadeAnimation)
shadow_overlay?.isVisible = true
shadow_overlay?.startAnimation(fadeAnimation)
updateLockUI()
@ -683,7 +734,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
if (event == null || view == null) return false
val currentTouch = Vector2(event.x, event.y)
val startTouch = currentTouchStart
player_intro_play?.isGone = true
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// validates if the touch is inside of the player area
@ -708,7 +759,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
}
}
MotionEvent.ACTION_UP -> {
if (isCurrentTouchValid && !isLocked) {
if (isCurrentTouchValid && !isLocked && isFullScreenPlayer) {
// seek time
if (swipeHorizontalEnabled && currentTouchAction == TouchAction.Time) {
val startTime = currentTouchStartPlayerTime
@ -737,7 +788,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
if (currentClickCount >= 1) { // have double clicked
currentDoubleTapIndex++
if (doubleTapPauseEnabled) { // you can pause if your tap is in the middle of the screen
if (doubleTapPauseEnabled && isFullScreenPlayer) { // you can pause if your tap is in the middle of the screen
when {
currentTouch.x < screenWidth / 2 - (DOUBLE_TAB_PAUSE_PERCENTAGE * screenWidth) -> {
if (doubleTapEnabled)
@ -751,7 +802,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
player.handleEvent(CSPlayerEvent.PlayPauseToggle)
}
}
} else if (doubleTapEnabled) {
} else if (doubleTapEnabled && isFullScreenPlayer) {
if (currentTouch.x < screenWidth / 2) {
rewind()
} else {
@ -789,7 +840,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
}
MotionEvent.ACTION_MOVE -> {
// if current touch is valid
if (startTouch != null && isCurrentTouchValid && !isLocked) {
if (startTouch != null && isCurrentTouchValid && !isLocked && isFullScreenPlayer) {
// action is unassigned and can therefore be assigned
if (currentTouchAction == null) {
val diffFromStart = startTouch - currentTouch
@ -1013,6 +1064,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
// if nothing has loaded these buttons should not be visible
player_skip_episode?.isVisible = false
player_skip_op?.isVisible = false
shadow_overlay?.isVisible = false
updateLockUI()
updateUIVisibility()
@ -1070,6 +1122,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
PlayerEventType.ShowMirrors -> {
showMirrorsDialogue()
}
PlayerEventType.SearchSubtitlesOnline -> {
if (subsProvidersIsActive) {
openOnlineSubPicker(view.context, null) {}
}
}
}
}
@ -1187,6 +1244,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
showMirrorsDialogue()
}
player_intro_play?.setOnClickListener {
player_intro_play?.isGone = true
player.handleEvent(CSPlayerEvent.Play)
}
// it is !not! a bug that you cant touch the right side, it does not register inputs on navbar or status bar
player_holder?.setOnTouchListener { callView, event ->
return@setOnTouchListener handleMotionEvent(callView, event)

View File

@ -1,7 +1,10 @@
package com.lagradost.cloudstream3.ui.player
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
@ -15,7 +18,6 @@ import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
import com.google.android.exoplayer2.util.MimeTypes
import com.google.android.material.button.MaterialButton
import com.hippo.unifile.UniFile
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
@ -24,19 +26,32 @@ import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subtitleProviders
import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.updateForcedEncoding
import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.ui.result.SyncViewModel
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
import com.lagradost.cloudstream3.utils.SubtitleHelper.languages
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
import kotlinx.android.synthetic.main.dialog_online_subtitles.*
import kotlinx.android.synthetic.main.dialog_online_subtitles.apply_btt
import kotlinx.android.synthetic.main.dialog_online_subtitles.cancel_btt
import kotlinx.android.synthetic.main.fragment_player.*
import kotlinx.android.synthetic.main.player_custom_layout.*
import kotlinx.android.synthetic.main.player_select_source_and_subs.*
import kotlinx.coroutines.Job
class GeneratorPlayer : FullScreenPlayer() {
@ -50,8 +65,14 @@ class GeneratorPlayer : FullScreenPlayer() {
putSerializable("syncData", syncData)
}
}
val subsProviders
get() = subtitleProviders.filter { !it.requiresLogin || it.loginInfo() != null }
val subsProvidersIsActive
get() = subsProviders.isNotEmpty()
}
private var titleRez = 3
private var limitTitle = 0
@ -161,6 +182,174 @@ class GeneratorPlayer : FullScreenPlayer() {
}
}
data class TempMetaData(
var episode: Int? = null,
var season: Int? = null,
var name: String? = null,
)
private fun getMetaData(): TempMetaData {
val meta = TempMetaData()
when (val newMeta = currentMeta) {
is ResultEpisode -> {
if (!newMeta.tvType.isMovieType()) {
meta.episode = newMeta.episode
meta.season = newMeta.season
}
meta.name = newMeta.headerName
}
is ExtractorUri -> {
if (newMeta.tvType?.isMovieType() == false) {
meta.episode = newMeta.episode
meta.season = newMeta.season
}
meta.name = newMeta.headerName
}
}
return meta
}
override fun openOnlineSubPicker(
context: Context,
imdbId: Long?,
dismissCallback: (() -> Unit)
) {
val providers = subsProviders
val dialog = Dialog(context, R.style.AlertDialogCustomBlack)
dialog.setContentView(R.layout.dialog_online_subtitles)
val arrayAdapter =
ArrayAdapter<String>(dialog.context, R.layout.sort_bottom_single_choice)
dialog.show()
dialog.cancel_btt.setOnClickListener {
dialog.dismissSafe()
}
dialog.subtitle_adapter.choiceMode = AbsListView.CHOICE_MODE_SINGLE
dialog.subtitle_adapter.adapter = arrayAdapter
val adapter = dialog.subtitle_adapter.adapter as? ArrayAdapter<String>
var currentSubtitles: List<AbstractSubtitleEntities.SubtitleEntity> = emptyList()
var currentSubtitle: AbstractSubtitleEntities.SubtitleEntity? = null
dialog.subtitle_adapter.setOnItemClickListener { _, _, position, _ ->
currentSubtitle = currentSubtitles.getOrNull(position) ?: return@setOnItemClickListener
}
var currentLanguageTwoLetters: String = getAutoSelectLanguageISO639_1()
fun getName(entry: AbstractSubtitleEntities.SubtitleEntity): String {
return if (entry.lang.isBlank()) {
entry.name
} else {
val language = fromTwoLettersToLanguage(entry.lang.trim()) ?: entry.lang
return "$language ${entry.name}"
}
}
fun setSubtitlesList(list: List<AbstractSubtitleEntities.SubtitleEntity>) {
currentSubtitles = list
adapter?.clear()
adapter?.addAll(currentSubtitles.map { getName(it) })
}
val currentTempMeta = getMetaData()
// bruh idk why it is not correct
val color = ColorStateList.valueOf(context.colorFromAttribute(R.attr.colorAccent))
dialog.search_loading_bar.progressTintList = color
dialog.search_loading_bar.indeterminateTintList = color
dialog.subtitles_search.setOnQueryTextListener(object :
androidx.appcompat.widget.SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
dialog.search_loading_bar?.show()
ioSafe {
val search = AbstractSubtitleEntities.SubtitleSearch(
query = query ?: return@ioSafe,
imdb = imdbId,
epNumber = currentTempMeta.episode,
seasonNumber = currentTempMeta.season,
lang = currentLanguageTwoLetters.ifBlank { null }
)
val results = providers.apmap {
try {
it.search(search)
} catch (e: Exception) {
null
}
}.filterNotNull()
val max = results.map { it.size }.maxOrNull() ?: return@ioSafe
// very ugly
val items = ArrayList<AbstractSubtitleEntities.SubtitleEntity>()
val arrays = results.size
for (index in 0 until max) {
for (i in 0 until arrays) {
items.add(results[i].getOrNull(index) ?: continue)
}
}
// ugly ik
activity?.runOnUiThread {
setSubtitlesList(items)
dialog.search_loading_bar?.hide()
}
}
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
return true
}
})
dialog.search_filter.setOnClickListener {
val lang639_1 = languages.map { it.ISO_639_1 }
activity?.showDialog(
languages.map { it.languageName },
lang639_1.indexOf(currentLanguageTwoLetters),
context.getString(R.string.subs_subtitle_languages),
true,
{ }
) { index ->
currentLanguageTwoLetters = lang639_1[index]
dialog.subtitles_search.setQuery(dialog.subtitles_search.query, true)
}
}
dialog.apply_btt.setOnClickListener {
currentSubtitle?.let { currentSubtitle ->
providers.firstOrNull { it.idPrefix == currentSubtitle.idPrefix }?.let { api ->
ioSafe {
val url = api.load(currentSubtitle) ?: return@ioSafe
val subtitle = SubtitleData(
name = getName(currentSubtitle),
url = url,
origin = SubtitleOrigin.URL,
mimeType = url.toSubtitleMimeType()
)
runOnMainThread {
addAndSelectSubtitles(subtitle)
}
}
}
}
dialog.dismissSafe()
}
dialog.setOnDismissListener {
dismissCallback.invoke()
}
dialog.show()
dialog.subtitles_search.setQuery(currentTempMeta.name, true)
}
private fun openSubPicker() {
try {
subsPathPicker.launch(
@ -181,6 +370,27 @@ class GeneratorPlayer : FullScreenPlayer() {
}
}
private fun addAndSelectSubtitles(subtitleData: SubtitleData) {
val ctx = context ?: return
setSubtitles(subtitleData)
// this is used instead of observe, because observe is too slow
val subs = currentSubs.toMutableSet()
subs.add(subtitleData)
player.setActiveSubtitles(subs)
player.reloadPlayer(ctx)
viewModel.addSubtitles(setOf(subtitleData))
selectSourceDialog?.dismissSafe()
showToast(
activity,
String.format(ctx.getString(R.string.player_loaded_subtitles), subtitleData.name),
Toast.LENGTH_LONG
)
}
// Open file picker
private val subsPathPicker =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
@ -206,23 +416,7 @@ class GeneratorPlayer : FullScreenPlayer() {
name.toSubtitleMimeType()
)
setSubtitles(subtitleData)
// this is used instead of observe, because observe is too slow
val subs = currentSubs.toMutableSet()
subs.add(subtitleData)
player.setActiveSubtitles(subs)
player.reloadPlayer(ctx)
viewModel.addSubtitles(setOf(subtitleData))
selectSourceDialog?.dismissSafe()
showToast(
activity,
String.format(ctx.getString(R.string.player_loaded_subtitles), name),
Toast.LENGTH_LONG
)
addAndSelectSubtitles(subtitleData)
}
}
@ -230,6 +424,7 @@ class GeneratorPlayer : FullScreenPlayer() {
override fun showMirrorsDialogue() {
try {
currentSelectedSubtitles = player.getCurrentPreferredSubtitle()
//println("CURRENT SELECTED :$currentSelectedSubtitles of $currentSubs")
context?.let { ctx ->
val isPlaying = player.getIsPlaying()
player.handleEvent(CSPlayerEvent.Pause)
@ -241,22 +436,46 @@ class GeneratorPlayer : FullScreenPlayer() {
val sourceDialog = sourceBuilder.create()
selectSourceDialog = sourceDialog
sourceDialog.show()
val providerList =
sourceDialog.findViewById<ListView>(R.id.sort_providers)!!
val subtitleList =
sourceDialog.findViewById<ListView>(R.id.sort_subtitles)!!
val applyButton =
sourceDialog.findViewById<MaterialButton>(R.id.apply_btt)!!
val cancelButton =
sourceDialog.findViewById<MaterialButton>(R.id.cancel_btt)!!
val providerList = sourceDialog.sort_providers
val subtitleList = sourceDialog.sort_subtitles
val footer: TextView =
val loadFromFileFooter: TextView =
layoutInflater.inflate(R.layout.sort_bottom_footer_add_choice, null) as TextView
footer.text = ctx.getString(R.string.player_load_subtitles)
footer.setOnClickListener {
loadFromFileFooter.text = ctx.getString(R.string.player_load_subtitles)
loadFromFileFooter.setOnClickListener {
openSubPicker()
}
subtitleList.addFooterView(footer)
subtitleList.addFooterView(loadFromFileFooter)
var shouldDismiss = true
fun dismiss() {
if (isPlaying) {
player.handleEvent(CSPlayerEvent.Play)
}
activity?.hideSystemUI()
}
if (subsProvidersIsActive) {
val loadFromOpenSubsFooter: TextView =
layoutInflater.inflate(
R.layout.sort_bottom_footer_add_choice,
null
) as TextView
loadFromOpenSubsFooter.text =
ctx.getString(R.string.player_load_subtitles_online)
loadFromOpenSubsFooter.setOnClickListener {
shouldDismiss = false
sourceDialog.dismissSafe(activity)
openOnlineSubPicker(it.context, null) {
dismiss()
}
}
subtitleList.addFooterView(loadFromOpenSubsFooter)
}
var sourceIndex = 0
var startSource = 0
@ -288,10 +507,7 @@ class GeneratorPlayer : FullScreenPlayer() {
}
sourceDialog.setOnDismissListener {
if (isPlaying) {
player.handleEvent(CSPlayerEvent.Play)
}
activity?.hideSystemUI()
if (shouldDismiss) dismiss()
selectSourceDialog = null
}
@ -314,11 +530,60 @@ class GeneratorPlayer : FullScreenPlayer() {
subtitleList.setItemChecked(which, true)
}
cancelButton.setOnClickListener {
sourceDialog.cancel_btt?.setOnClickListener {
sourceDialog.dismissSafe(activity)
}
applyButton.setOnClickListener {
sourceDialog.subtitles_encoding_format?.apply {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
val prefNames = ctx.resources.getStringArray(R.array.subtitles_encoding_list)
val prefValues = ctx.resources.getStringArray(R.array.subtitles_encoding_values)
val value = settingsManager.getString(
ctx.getString(R.string.subtitles_encoding_key),
null
)
val index = prefValues.indexOf(value)
text = prefNames[if (index == -1) 0 else index]
}
sourceDialog.subtitles_click_settings?.setOnClickListener {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
val prefNames = ctx.resources.getStringArray(R.array.subtitles_encoding_list)
val prefValues = ctx.resources.getStringArray(R.array.subtitles_encoding_values)
val currentPrefMedia =
settingsManager.getString(
getString(R.string.subtitles_encoding_key),
null
)
shouldDismiss = false
sourceDialog.dismissSafe(activity)
val index = prefValues.indexOf(currentPrefMedia)
activity?.showDialog(
prefNames.toList(),
if (index == -1) 0 else index,
ctx.getString(R.string.subtitles_encoding),
true,
{}) {
settingsManager.edit()
.putString(
ctx.getString(R.string.subtitles_encoding_key),
prefValues[it]
)
.apply()
updateForcedEncoding(ctx)
dismiss()
player.seekTime(-1) // to update subtitles, a dirty trick
}
}
sourceDialog.apply_btt?.setOnClickListener {
var init = false
if (sourceIndex != startSource) {
init = true
@ -477,14 +742,24 @@ class GeneratorPlayer : FullScreenPlayer() {
): SubtitleData? {
val langCode = preferredAutoSelectSubtitles ?: return null
val lang = SubtitleHelper.fromTwoLettersToLanguage(langCode) ?: return null
if (settings)
subtitles.firstOrNull { sub ->
sub.name.startsWith(lang)
|| sub.name.trim() == langCode
}?.let { sub ->
return sub
if (downloads) {
return subtitles.firstOrNull { sub ->
(sub.origin == SubtitleOrigin.DOWNLOADED_FILE && sub.name == context?.getString(
R.string.default_subtitles
))
}
}
sortSubs(subtitles).firstOrNull { sub ->
val t = sub.name.replace(Regex("[^A-Za-z]"), " ").trim()
(settings || (downloads && sub.origin == SubtitleOrigin.DOWNLOADED_FILE)) && t == lang || t.startsWith(
"$lang "
) || t == langCode
}?.let { sub ->
return sub
}
// post check in case both did not catch anything
if (downloads) {
return subtitles.firstOrNull { sub ->
(sub.origin == SubtitleOrigin.DOWNLOADED_FILE || sub.name == context?.getString(
@ -492,10 +767,11 @@ class GeneratorPlayer : FullScreenPlayer() {
))
}
}
return null
}
private fun autoSelectFromSettings() {
private fun autoSelectFromSettings(): Boolean {
// auto select subtitle based of settings
val langCode = preferredAutoSelectSubtitles
@ -505,44 +781,46 @@ class GeneratorPlayer : FullScreenPlayer() {
if (setSubtitles(sub)) {
player.reloadPlayer(ctx)
player.handleEvent(CSPlayerEvent.Play)
return true
}
}
}
}
return false
}
private fun autoSelectFromDownloads() {
private fun autoSelectFromDownloads(): Boolean {
if (player.getCurrentPreferredSubtitle() == null) {
getAutoSelectSubtitle(currentSubs, settings = false, downloads = true)?.let { sub ->
context?.let { ctx ->
if (setSubtitles(sub)) {
player.reloadPlayer(ctx)
player.handleEvent(CSPlayerEvent.Play)
return true
}
}
}
}
return false
}
private fun autoSelectSubtitles() {
normalSafeApiCall {
autoSelectFromSettings()
autoSelectFromDownloads()
if (!autoSelectFromSettings()) {
autoSelectFromDownloads()
}
}
}
@SuppressLint("SetTextI18n")
fun setTitle() {
private fun getPlayerVideoTitle(): String {
var headerName: String? = null
var subName: String? = null
var episode: Int? = null
var season: Int? = null
var tvType: TvType? = null
var isFiller: Boolean? = null
when (val meta = currentMeta) {
is ResultEpisode -> {
isFiller = meta.isFiller
headerName = meta.headerName
subName = meta.name
episode = meta.episode
@ -559,7 +837,7 @@ class GeneratorPlayer : FullScreenPlayer() {
}
//Generate video title
var playerVideoTitle = if (headerName != null) {
val playerVideoTitle = if (headerName != null) {
(headerName +
if (tvType.isEpisodeBased() && episode != null)
if (season == null)
@ -570,6 +848,13 @@ class GeneratorPlayer : FullScreenPlayer() {
} else {
""
}
return playerVideoTitle
}
@SuppressLint("SetTextI18n")
fun setTitle() {
var playerVideoTitle = getPlayerVideoTitle()
//Hide title, if set in setting
if (limitTitle < 0) {
@ -582,6 +867,7 @@ class GeneratorPlayer : FullScreenPlayer() {
playerVideoTitle = playerVideoTitle.substring(0, limitTitle - 1) + "..."
}
}
val isFiller: Boolean? = (currentMeta as? ResultEpisode)?.isFiller
player_episode_filler_holder?.isVisible = isFiller ?: false
player_video_title?.text = playerVideoTitle
@ -645,6 +931,7 @@ class GeneratorPlayer : FullScreenPlayer() {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
titleRez = settingsManager.getInt(getString(R.string.prefer_limit_title_rez_key), 3)
limitTitle = settingsManager.getInt(getString(R.string.prefer_limit_title_key), 0)
updateForcedEncoding(ctx)
}
unwrapBundle(savedInstanceState)

View File

@ -22,6 +22,7 @@ enum class PlayerEventType(val value: Int) {
ShowSpeed(11),
ShowMirrors(12),
Resize(13),
SearchSubtitlesOnline(14),
}
enum class CSPlayerEvent(val value: Int) {
@ -97,6 +98,7 @@ interface IPlayer {
startPosition: Long? = null,
subtitles : Set<SubtitleData>,
subtitle : SubtitleData?,
autoPlay : Boolean? = true
)
fun reloadPlayer(context: Context)

View File

@ -1,14 +1,13 @@
package com.lagradost.cloudstream3.ui.player
import android.content.Context
import android.net.Uri
import android.util.TypedValue
import android.view.ViewGroup
import android.widget.FrameLayout
import com.google.android.exoplayer2.ui.SubtitleView
import com.google.android.exoplayer2.util.MimeTypes
import com.hippo.unifile.UniFile
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveBloat
import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveCaptions
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.fromSaveToStyle
import com.lagradost.cloudstream3.utils.UIHelper.toPx
@ -22,7 +21,6 @@ enum class SubtitleStatus {
enum class SubtitleOrigin {
URL,
DOWNLOADED_FILE,
OPEN_SUBTITLES,
EMBEDDED_IN_VIDEO
}
@ -66,28 +64,6 @@ class PlayerSubtitleHelper {
}
}
private fun getSubtitleMimeType(context: Context, url: String, origin: SubtitleOrigin): String {
return when (origin) {
// The url can look like .../document/4294 when the name is EnglishSDH.srt
SubtitleOrigin.DOWNLOADED_FILE -> {
UniFile.fromUri(
context,
Uri.parse(url)
).name?.toSubtitleMimeType() ?: MimeTypes.APPLICATION_SUBRIP
}
SubtitleOrigin.URL -> {
return url.toSubtitleMimeType()
}
SubtitleOrigin.OPEN_SUBTITLES -> {
// TODO
throw NotImplementedError()
}
SubtitleOrigin.EMBEDDED_IN_VIDEO -> {
throw NotImplementedError()
}
}
}
fun getSubtitleData(subtitleFile: SubtitleFile): SubtitleData {
return SubtitleData(
name = subtitleFile.lang,
@ -109,6 +85,8 @@ class PlayerSubtitleHelper {
}
fun setSubStyle(style: SaveCaptionStyle) {
regexSubtitlesToRemoveBloat = style.removeBloat
regexSubtitlesToRemoveCaptions = style.removeCaptions
subtitleView?.context?.let { ctx ->
subStyle = style
subtitleView?.setStyle(ctx.fromSaveToStyle(style))

View File

@ -15,7 +15,7 @@ class RepoLinkGenerator(
) : IGenerator {
companion object {
const val TAG = "RepoLink"
val cache: HashMap<Int, Pair<MutableSet<ExtractorLink>, MutableSet<SubtitleData>>> = hashMapOf()
val cache: HashMap<Pair<String, Int>, Pair<MutableSet<ExtractorLink>, MutableSet<SubtitleData>>> = hashMapOf()
}
override val hasCache = true
@ -71,7 +71,7 @@ class RepoLinkGenerator(
val (currentLinkCache, currentSubsCache) = if (clearCache) {
Pair(mutableSetOf(), mutableSetOf())
} else {
cache[current.id] ?: Pair(mutableSetOf(), mutableSetOf())
cache[Pair(current.apiName, current.id)] ?: Pair(mutableSetOf(), mutableSetOf())
}
//val currentLinkCache = if (clearCache) mutableSetOf() else linkCache[index].toMutableSet()
@ -137,7 +137,7 @@ class RepoLinkGenerator(
}
}
)
cache[current.id] = Pair(currentLinkCache, currentSubsCache)
cache[Pair(current.apiName, current.id)] = Pair(currentLinkCache, currentSubsCache)
return result
}

View File

@ -24,12 +24,10 @@ import android.widget.*
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.core.content.FileProvider
import androidx.core.graphics.drawable.toBitmap
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
@ -41,7 +39,6 @@ import com.google.android.gms.cast.framework.CastContext
import com.google.android.gms.cast.framework.CastState
import com.google.android.material.button.MaterialButton
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
import com.lagradost.cloudstream3.APIHolder.getApiFromName
import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
@ -87,7 +84,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.UIHelper.setImageBlur
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getFileName
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
import kotlinx.android.synthetic.main.fragment_result.*
@ -185,7 +181,7 @@ fun ResultEpisode.getWatchProgress(): Float {
return (getDisplayPosition() / 1000).toFloat() / (duration / 1000).toFloat()
}
class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegionsListener {
class ResultFragment : ResultTrailerPlayer() {
companion object {
const val URL_BUNDLE = "url"
const val API_NAME_BUNDLE = "apiName"
@ -603,6 +599,59 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
setFormatText(result_meta_rating, R.string.rating_format, rating?.div(1000f))
}
var currentTrailers: List<String> = emptyList()
var currentTrailerIndex = 0
override fun nextMirror() {
currentTrailerIndex++
loadTrailer()
}
override fun playerError(exception: Exception) {
if (player.getIsPlaying()) // because we dont want random toasts in player
super.playerError(exception)
}
private fun loadTrailer(index: Int? = null) {
currentTrailers.getOrNull(index ?: currentTrailerIndex)?.let { trailer ->
//if(trailer.contains("youtube.com")) { // wont load in exo
// nextMirror()
// return
//}
context?.let { ctx ->
player.onPause()
player.loadPlayer(
ctx,
false,
ExtractorLink(
"",
"Trailer",
trailer,
"",
Qualities.Unknown.value
),
null,
startPosition = 0L,
subtitles = emptySet(),
subtitle = null,
autoPlay = false
)
}
}
}
private fun setTrailers(trailers: List<String>?) {
context?.let { ctx ->
if (ctx.isTvSettings()) return
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
val showTrailers =
settingsManager.getBoolean(ctx.getString(R.string.show_trailers_key), true)
if (!showTrailers) return
currentTrailers = trailers ?: emptyList()
loadTrailer()
}
}
private fun setActors(actors: List<ActorData>?) {
if (actors.isNullOrEmpty()) {
result_cast_text?.isVisible = false
@ -779,7 +828,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
} else if (dy < -5) {
result_bookmark_fab?.extend()
}
result_poster_blur_holder?.translationY = -scrollY.toFloat()
//result_poster_blur_holder?.translationY = -scrollY.toFloat()
})
result_back.setOnClickListener {
@ -1353,7 +1402,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
val newList = list.filter { it.isSynced && it.hasAccount }
result_mini_sync?.isVisible = newList.isNotEmpty()
(result_mini_sync?.adapter as? ImageAdapter?)?.updateList(newList.map { it.icon })
(result_mini_sync?.adapter as? ImageAdapter?)?.updateList(newList.mapNotNull { it.icon })
}
observe(syncModel.syncIds) {
@ -1508,7 +1557,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
when (startAction) {
START_ACTION_RESUME_LATEST -> {
for (ep in episodeList) {
println("WATCH STATUS::: S${ep.season} E ${ep.episode} - ${ep.getWatchProgress()}")
//println("WATCH STATUS::: S${ep.season} E ${ep.episode} - ${ep.getWatchProgress()}")
if (ep.getWatchProgress() > 0.90f) { // watched too much
continue
}
@ -1528,7 +1577,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
var found = false
for (ep in episodeList) {
if (ep.id == startValue) { // watched too much
println("WATCH STATUS::: START_ACTION_LOAD_EP S${ep.season} E ${ep.episode} - ${ep.getWatchProgress()}")
//println("WATCH STATUS::: START_ACTION_LOAD_EP S${ep.season} E ${ep.episode} - ${ep.getWatchProgress()}")
handleAction(EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, ep))
found = true
break
@ -1537,7 +1586,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
if (!found)
for (ep in episodeList) {
if (ep.episode == resumeEpisode && ep.season == resumeSeason) {
println("WATCH STATUS::: START_ACTION_LOAD_EP S${ep.season} E ${ep.episode} - ${ep.getWatchProgress()}")
//println("WATCH STATUS::: START_ACTION_LOAD_EP S${ep.season} E ${ep.episode} - ${ep.getWatchProgress()}")
handleAction(
EpisodeClickEvent(
ACTION_PLAY_EPISODE_IN_PLAYER,
@ -1729,6 +1778,8 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
setRecommendations(d.recommendations, null)
setActors(d.actors)
setTrailers(d.trailers)
if (syncModel.addSyncs(d.syncData)) {
syncModel.updateMetaAndUser()
syncModel.updateSynced()
@ -1741,7 +1792,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
val posterImageLink = d.posterUrl
if (!posterImageLink.isNullOrEmpty()) {
result_poster?.setImage(posterImageLink, d.posterHeaders)
result_poster_blur?.setImageBlur(posterImageLink, 10, 3, d.posterHeaders)
//result_poster_blur?.setImageBlur(posterImageLink, 10, 3, d.posterHeaders)
//Full screen view of Poster image
if (context?.isTrueTvSettings() == false) // Poster not clickable on tv
result_poster_holder?.setOnClickListener {
@ -1770,7 +1821,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
} else {
result_poster?.setImageResource(R.drawable.default_cover)
result_poster_blur?.setImageResource(R.drawable.default_cover)
//result_poster_blur?.setImageResource(R.drawable.default_cover)
}
result_poster_holder?.visibility = VISIBLE
@ -1788,7 +1839,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
}
result_description.setOnClickListener {
val builder: AlertDialog.Builder =
AlertDialog.Builder(requireContext())
AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom)
builder.setMessage(d.plot)
.setTitle(if (d.type == TvType.Torrent) R.string.torrent_plot else R.string.result_plot)
.show()

View File

@ -0,0 +1,124 @@
package com.lagradost.cloudstream3.ui.result
import android.content.res.Configuration
import android.graphics.Rect
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.view.isVisible
import com.discord.panels.PanelsChildGestureRegionObserver
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.utils.IOnBackPressed
import kotlinx.android.synthetic.main.fragment_result.*
import kotlinx.android.synthetic.main.fragment_result_swipe.*
import kotlinx.android.synthetic.main.fragment_trailer.*
import kotlinx.android.synthetic.main.trailer_custom_layout.*
open class ResultTrailerPlayer : com.lagradost.cloudstream3.ui.player.FullScreenPlayer(),
PanelsChildGestureRegionObserver.GestureRegionsListener, IOnBackPressed {
override var lockRotation = false
override var isFullScreenPlayer = false
override var hasPipModeSupport = false
companion object {
const val TAG = "RESULT_TRAILER"
}
var playerWidthHeight: Pair<Int, Int>? = null
override fun nextEpisode() {}
override fun prevEpisode() {}
override fun playerPositionChanged(posDur: Pair<Long, Long>) {}
override fun nextMirror() {}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
uiReset()
fixPlayerSize()
}
private fun fixPlayerSize() {
playerWidthHeight?.let { (w, h) ->
val orientation = context?.resources?.configuration?.orientation ?: return
val sw = if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
screenWidth
} else {
screenHeight
}
player_background?.apply {
isVisible = true
layoutParams =
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
if (isFullScreenPlayer) FrameLayout.LayoutParams.MATCH_PARENT else sw * h / w
)
}
}
}
override fun playerDimensionsLoaded(widthHeight: Pair<Int, Int>) {
playerWidthHeight = widthHeight
fixPlayerSize()
}
override fun subtitlesChanged() {}
override fun embeddedSubtitlesFetched(subtitles: List<SubtitleData>) {}
override fun exitedPipMode() {}
override fun onGestureRegionsUpdate(gestureRegions: List<Rect>) {}
private fun updateFullscreen(fullscreen: Boolean) {
isFullScreenPlayer = fullscreen
lockRotation = fullscreen
player_fullscreen?.setImageResource(if (fullscreen) R.drawable.baseline_fullscreen_exit_24 else R.drawable.baseline_fullscreen_24)
uiReset()
if (fullscreen) {
enterFullscreen()
result_top_bar?.isVisible = false
result_fullscreen_holder?.isVisible = true
result_main_holder?.isVisible = false
player_background?.let { view ->
(view.parent as ViewGroup?)?.removeView(view)
result_fullscreen_holder?.addView(view)
}
} else {
result_top_bar?.isVisible = true
result_fullscreen_holder?.isVisible = false
result_main_holder?.isVisible = true
player_background?.let { view ->
(view.parent as ViewGroup?)?.removeView(view)
result_smallscreen_holder?.addView(view)
}
exitFullscreen()
}
fixPlayerSize()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
player_fullscreen?.setOnClickListener {
updateFullscreen(!isFullScreenPlayer)
}
updateFullscreen(isFullScreenPlayer)
uiReset()
}
override fun onBackPressed(): Boolean {
return if (isFullScreenPlayer) {
updateFullscreen(false)
false
} else {
true
}
}
}

View File

@ -7,9 +7,9 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.utils.SyncUtil
import kotlinx.coroutines.launch
@ -20,7 +20,7 @@ data class CurrentSynced(
val idPrefix: String,
val isSynced: Boolean,
val hasAccount: Boolean,
val icon: Int,
val icon: Int?,
)
class SyncViewModel : ViewModel() {

View File

@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.ui.search
import android.app.Activity
import android.widget.Toast
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_PLAY_FILE
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
import com.lagradost.cloudstream3.ui.download.DownloadClickEvent
@ -20,25 +21,30 @@ object SearchHelper {
}
SEARCH_ACTION_PLAY_FILE -> {
if (card is DataStoreHelper.ResumeWatchingResult) {
if (card.isFromDownload) {
handleDownloadClick(
activity, card.name, DownloadClickEvent(
DOWNLOAD_ACTION_PLAY_FILE,
VideoDownloadHelper.DownloadEpisodeCached(
card.name,
card.posterUrl,
card.episode ?: 0,
card.season,
card.id!!,
card.parentId ?: return,
null,
null,
System.currentTimeMillis()
val id = card.id
if(id == null) {
showToast(activity, R.string.error_invalid_id, Toast.LENGTH_SHORT)
} else {
if (card.isFromDownload) {
handleDownloadClick(
activity, card.name, DownloadClickEvent(
DOWNLOAD_ACTION_PLAY_FILE,
VideoDownloadHelper.DownloadEpisodeCached(
card.name,
card.posterUrl,
card.episode ?: 0,
card.season,
id,
card.parentId ?: return,
null,
null,
System.currentTimeMillis()
)
)
)
)
} else {
activity.loadSearchResult(card, START_ACTION_LOAD_EP, card.id)
} else {
activity.loadSearchResult(card, START_ACTION_LOAD_EP, id)
}
}
} else {
handleSearchClickCallback(

View File

@ -3,10 +3,10 @@ package com.lagradost.cloudstream3.ui.search
import com.lagradost.cloudstream3.SearchQuality
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.syncproviders.OAuth2API
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
class SyncSearchViewModel {
private val repos = OAuth2API.SyncApis
private val repos = SyncApis
data class SyncSearchResultSearchResponse(
override val name: String,
@ -18,5 +18,4 @@ class SyncSearchViewModel {
override var quality: SearchQuality? = null,
override var posterHeaders: Map<String, String>? = null,
) : SearchResponse
}

View File

@ -8,13 +8,13 @@ import android.widget.TextView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.syncproviders.OAuth2API
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.utils.UIHelper.setImage
class AccountClickCallback(val action: Int, val view : View, val card: OAuth2API.LoginInfo)
class AccountClickCallback(val action: Int, val view: View, val card: AuthAPI.LoginInfo)
class AccountAdapter(
val cardList: List<OAuth2API.LoginInfo>,
val cardList: List<AuthAPI.LoginInfo>,
val layout: Int = R.layout.account_single,
private val clickCallback: (AccountClickCallback) -> Unit
) :
@ -48,15 +48,13 @@ class AccountAdapter(
private val pfp: ImageView = itemView.findViewById(R.id.account_profile_picture)!!
private val accountName: TextView = itemView.findViewById(R.id.account_name)!!
fun bind(card: OAuth2API.LoginInfo) {
fun bind(card: AuthAPI.LoginInfo) {
// just in case name is null account index will show, should never happened
accountName.text = card.name ?: "%s %d".format(accountName.context.getString(R.string.account), card.accountIndex)
if(card.profilePicture.isNullOrEmpty()) {
pfp.isVisible = false
} else {
pfp.isVisible = true
pfp.setImage(card.profilePicture)
}
accountName.text = card.name ?: "%s %d".format(
accountName.context.getString(R.string.account),
card.accountIndex
)
pfp.isVisible = pfp.setImage(card.profilePicture)
itemView.setOnClickListener {
clickCallback.invoke(AccountClickCallback(0, itemView, card))

View File

@ -1,32 +1,54 @@
package com.lagradost.cloudstream3.ui.settings
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.ImageView
import android.view.View
import android.widget.TextView
import androidx.annotation.UiThread
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.nginxApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.openSubtitlesApi
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
import com.lagradost.cloudstream3.syncproviders.OAuth2API
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.beneneCount
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import kotlinx.android.synthetic.main.account_managment.*
import kotlinx.android.synthetic.main.account_switch.*
import kotlinx.android.synthetic.main.add_account_input.*
class SettingsAccount : PreferenceFragmentCompat() {
private fun showLoginInfo(api: AccountManager, info: OAuth2API.LoginInfo) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setUpToolbar(R.string.category_account)
}
private fun showLoginInfo(api: AccountManager, info: AuthAPI.LoginInfo) {
val builder =
AlertDialog.Builder(context ?: return, R.style.AlertDialogCustom)
.setView(R.layout.account_managment)
val dialog = builder.show()
dialog.findViewById<ImageView>(R.id.account_profile_picture)?.setImage(info.profilePicture)
dialog.findViewById<TextView>(R.id.account_logout)?.setOnClickListener {
dialog.account_main_profile_picture_holder?.isVisible =
dialog.account_main_profile_picture?.setImage(info.profilePicture) == true
dialog.account_logout?.setOnClickListener {
api.logOut()
dialog.dismissSafe(activity)
}
@ -34,13 +56,93 @@ class SettingsAccount : PreferenceFragmentCompat() {
(info.name ?: context?.getString(R.string.no_data))?.let {
dialog.findViewById<TextView>(R.id.account_name)?.text = it
}
dialog.findViewById<TextView>(R.id.account_site)?.text = api.name
dialog.findViewById<TextView>(R.id.account_switch_account)?.setOnClickListener {
dialog.account_site?.text = api.name
dialog.account_switch_account?.setOnClickListener {
dialog.dismissSafe(activity)
showAccountSwitch(it.context, api)
}
}
@UiThread
private fun addAccount(api: AccountManager) {
try {
when (api) {
is OAuth2API -> {
api.authenticate()
}
is InAppAuthAPI -> {
val builder =
AlertDialog.Builder(context ?: return, R.style.AlertDialogCustom)
.setView(R.layout.add_account_input)
val dialog = builder.show()
dialog.login_email_input?.isVisible = api.requiresEmail
dialog.login_password_input?.isVisible = api.requiresPassword
dialog.login_server_input?.isVisible = api.requiresServer
dialog.login_username_input?.isVisible = api.requiresUsername
dialog.create_account?.isGone = api.createAccountUrl.isNullOrBlank()
dialog.create_account?.setOnClickListener {
val i = Intent(Intent.ACTION_VIEW)
i.data = Uri.parse(api.createAccountUrl)
try {
startActivity(i)
} catch (e: Exception) {
logError(e)
}
}
dialog.text1?.text = api.name
if (api.storesPasswordInPlainText) {
api.getLatestLoginData()?.let { data ->
dialog.login_email_input?.setText(data.email ?: "")
dialog.login_server_input?.setText(data.server ?: "")
dialog.login_username_input?.setText(data.username ?: "")
dialog.login_password_input?.setText(data.password ?: "")
}
}
dialog.apply_btt?.setOnClickListener {
val loginData = InAppAuthAPI.LoginData(
username = if (api.requiresUsername) dialog.login_username_input?.text?.toString() else null,
password = if (api.requiresPassword) dialog.login_password_input?.text?.toString() else null,
email = if (api.requiresEmail) dialog.login_email_input?.text?.toString() else null,
server = if (api.requiresServer) dialog.login_server_input?.text?.toString() else null,
)
ioSafe {
val isSuccessful = try {
api.login(loginData)
} catch (e: Exception) {
logError(e)
false
}
activity?.runOnUiThread {
try {
showToast(
activity,
getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format(
api.name
)
)
} catch (e: Exception) {
logError(e) // format might fail
}
}
}
dialog.dismissSafe(activity)
}
dialog.cancel_btt?.setOnClickListener {
dialog.dismissSafe(activity)
}
}
else -> {
throw NotImplementedError("You are trying to add an account that has an unknown login method")
}
}
} catch (e: Exception) {
logError(e)
}
}
private fun showAccountSwitch(context: Context, api: AccountManager) {
val accounts = api.getAccounts() ?: return
@ -48,17 +150,14 @@ class SettingsAccount : PreferenceFragmentCompat() {
AlertDialog.Builder(context, R.style.AlertDialogCustom).setView(R.layout.account_switch)
val dialog = builder.show()
dialog.findViewById<TextView>(R.id.account_add)?.setOnClickListener {
try {
api.authenticate()
} catch (e: Exception) {
logError(e)
}
dialog.account_add?.setOnClickListener {
addAccount(api)
dialog?.dismissSafe(activity)
}
val ogIndex = api.accountIndex
val items = ArrayList<OAuth2API.LoginInfo>()
val items = ArrayList<AuthAPI.LoginInfo>()
for (index in accounts) {
api.accountIndex = index
@ -78,72 +177,30 @@ class SettingsAccount : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
hideKeyboard()
setPreferencesFromResource(R.xml.settings_credits_account, rootKey)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
getPref(R.string.legal_notice_key)?.setOnPreferenceClickListener {
val builder: AlertDialog.Builder = AlertDialog.Builder(it.context)
builder.setTitle(R.string.legal_notice)
builder.setMessage(R.string.legal_notice_text)
builder.show()
return@setOnPreferenceClickListener true
}
setPreferencesFromResource(R.xml.settings_account, rootKey)
val syncApis =
listOf(
Pair(R.string.mal_key, OAuth2API.malApi), Pair(
R.string.anilist_key,
OAuth2API.aniListApi
)
R.string.mal_key to malApi,
R.string.anilist_key to aniListApi,
R.string.opensubtitles_key to openSubtitlesApi,
R.string.nginx_key to nginxApi,
)
for ((key, api) in syncApis) {
getPref(key)?.apply {
title =
getString(R.string.login_format).format(api.name, getString(R.string.account))
setOnPreferenceClickListener { _ ->
setOnPreferenceClickListener {
val info = api.loginInfo()
if (info != null) {
showLoginInfo(api, info)
} else {
try {
api.authenticate()
} catch (e: Exception) {
logError(e)
}
addAccount(api)
}
return@setOnPreferenceClickListener true
}
}
}
try {
beneneCount = settingsManager.getInt(getString(R.string.benene_count), 0)
getPref(R.string.benene_count)?.let { pref ->
pref.summary =
if (beneneCount <= 0) getString(R.string.benene_count_text_none) else getString(
R.string.benene_count_text
).format(
beneneCount
)
pref.setOnPreferenceClickListener {
try {
beneneCount++
settingsManager.edit().putInt(getString(R.string.benene_count), beneneCount)
.apply()
it.summary = getString(R.string.benene_count_text).format(beneneCount)
} catch (e: Exception) {
logError(e)
}
return@setOnPreferenceClickListener true
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}

View File

@ -8,14 +8,21 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
import com.lagradost.cloudstream3.ui.home.HomeFragment
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import kotlinx.android.synthetic.main.main_settings.*
import kotlinx.android.synthetic.main.settings_title_top.*
import java.io.File
class SettingsFragment : Fragment() {
@ -33,6 +40,18 @@ class SettingsFragment : Fragment() {
}
}
fun PreferenceFragmentCompat?.setUpToolbar(@StringRes title: Int) {
if (this == null) return
settings_toolbar?.apply {
setTitle(title)
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
setNavigationOnClickListener {
activity?.onBackPressed()
}
}
context.fixPaddingStatusbar(settings_toolbar)
}
fun getFolderSize(dir: File): Long {
var size: Long = 0
dir.listFiles()?.let {
@ -75,9 +94,10 @@ class SettingsFragment : Fragment() {
private fun Context.isAutoTv(): Boolean {
val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager?
// AFT = Fire TV
val model = Build.MODEL.lowercase()
return uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION || Build.MODEL.contains(
"AFT"
)
) || model.contains("firestick") || model.contains("fire tv") || model.contains("chromecast")
}
}
@ -94,28 +114,39 @@ class SettingsFragment : Fragment() {
activity?.navigate(id, Bundle())
}
settings_player?.setOnClickListener {
navigate(R.id.action_navigation_settings_to_navigation_settings_player)
val isTrueTv = context?.isTrueTvSettings() == true
for (syncApi in accountManagers) {
val login = syncApi.loginInfo()
val pic = login?.profilePicture ?: continue
if (settings_profile_pic?.setImage(
pic,
errorImageDrawable = HomeFragment.errorProfilePic
) == true
) {
settings_profile_text?.text = login.name
settings_profile?.isVisible = true
break
}
}
settings_credits?.setOnClickListener {
navigate(R.id.action_navigation_settings_to_navigation_settings_account)
}
settings_ui?.setOnClickListener {
navigate(R.id.action_navigation_settings_to_navigation_settings_ui)
}
settings_lang?.setOnClickListener {
navigate(R.id.action_navigation_settings_to_navigation_settings_lang)
}
settings_nginx?.setOnClickListener {
navigate(R.id.action_navigation_settings_to_navigation_settings_nginx)
}
settings_updates?.setOnClickListener {
navigate(R.id.action_navigation_settings_to_navigation_settings_updates)
listOf(
Pair(settings_general, R.id.action_navigation_settings_to_navigation_settings_general),
Pair(settings_player, R.id.action_navigation_settings_to_navigation_settings_player),
Pair(settings_credits, R.id.action_navigation_settings_to_navigation_settings_account),
Pair(settings_ui, R.id.action_navigation_settings_to_navigation_settings_ui),
Pair(settings_lang, R.id.action_navigation_settings_to_navigation_settings_lang),
Pair(settings_updates, R.id.action_navigation_settings_to_navigation_settings_updates),
).forEach { (view, navigationId) ->
view?.apply {
setOnClickListener {
navigate(navigationId)
}
if (isTrueTv) {
isFocusable = true
isFocusableInTouchMode = true
}
}
}
}
}

View File

@ -0,0 +1,182 @@
package com.lagradost.cloudstream3.ui.settings
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.hippo.unifile.UniFile
import com.lagradost.cloudstream3.AcraApplication
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.VideoDownloadManager
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath
import java.io.File
class SettingsGeneral : PreferenceFragmentCompat() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setUpToolbar(R.string.category_general)
}
// Open file picker
private val pathPicker =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
// It lies, it can be null if file manager quits.
if (uri == null) return@registerForActivityResult
val context = context ?: AcraApplication.context ?: return@registerForActivityResult
// RW perms for the path
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
context.contentResolver.takePersistableUriPermission(uri, flags)
val file = UniFile.fromUri(context, uri)
println("Selected URI path: $uri - Full path: ${file.filePath}")
// Stores the real URI using download_path_key
// Important that the URI is stored instead of filepath due to permissions.
PreferenceManager.getDefaultSharedPreferences(context)
.edit().putString(getString(R.string.download_path_key), uri.toString()).apply()
// From URI -> File path
// File path here is purely for cosmetic purposes in settings
(file.filePath ?: uri.toString()).let {
PreferenceManager.getDefaultSharedPreferences(context)
.edit().putString(getString(R.string.download_path_pref), it).apply()
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
hideKeyboard()
setPreferencesFromResource(R.xml.settins_general, rootKey)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
getPref(R.string.legal_notice_key)?.setOnPreferenceClickListener {
val builder: AlertDialog.Builder =
AlertDialog.Builder(it.context, R.style.AlertDialogCustom)
builder.setTitle(R.string.legal_notice)
builder.setMessage(R.string.legal_notice_text)
builder.show()
return@setOnPreferenceClickListener true
}
getPref(R.string.dns_key)?.setOnPreferenceClickListener {
val prefNames = resources.getStringArray(R.array.dns_pref)
val prefValues = resources.getIntArray(R.array.dns_pref_values)
val currentDns =
settingsManager.getInt(getString(R.string.dns_pref), 0)
activity?.showBottomDialog(
prefNames.toList(),
prefValues.indexOf(currentDns),
getString(R.string.dns_pref),
true,
{}) {
settingsManager.edit().putInt(getString(R.string.dns_pref), prefValues[it]).apply()
(context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) }
}
return@setOnPreferenceClickListener true
}
fun getDownloadDirs(): List<String> {
return normalSafeApiCall {
val defaultDir = VideoDownloadManager.getDownloadDir()?.filePath
// app_name_download_path = Cloudstream and does not change depending on release.
// DOES NOT WORK ON SCOPED STORAGE.
val secondaryDir =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) null else Environment.getExternalStorageDirectory().absolutePath +
File.separator + resources.getString(R.string.app_name_download_path)
val first = listOf(defaultDir, secondaryDir)
(try {
val currentDir = context?.getBasePath()?.let { it.first?.filePath ?: it.second }
(first +
requireContext().getExternalFilesDirs("").mapNotNull { it.path } +
currentDir)
} catch (e: Exception) {
first
}).filterNotNull().distinct()
} ?: emptyList()
}
getPref(R.string.download_path_key)?.setOnPreferenceClickListener {
val dirs = getDownloadDirs()
val currentDir =
settingsManager.getString(getString(R.string.download_path_pref), null)
?: VideoDownloadManager.getDownloadDir().toString()
activity?.showBottomDialog(
dirs + listOf("Custom"),
dirs.indexOf(currentDir),
getString(R.string.download_path_pref),
true,
{}) {
// Last = custom
if (it == dirs.size) {
try {
pathPicker.launch(Uri.EMPTY)
} catch (e: Exception) {
logError(e)
}
} else {
// Sets both visual and actual paths.
// key = used path
// pref = visual path
settingsManager.edit()
.putString(getString(R.string.download_path_key), dirs[it]).apply()
settingsManager.edit()
.putString(getString(R.string.download_path_pref), dirs[it]).apply()
}
}
return@setOnPreferenceClickListener true
}
try {
SettingsFragment.beneneCount =
settingsManager.getInt(getString(R.string.benene_count), 0)
getPref(R.string.benene_count)?.let { pref ->
pref.summary =
if (SettingsFragment.beneneCount <= 0) getString(R.string.benene_count_text_none) else getString(
R.string.benene_count_text
).format(
SettingsFragment.beneneCount
)
pref.setOnPreferenceClickListener {
try {
SettingsFragment.beneneCount++
settingsManager.edit().putInt(
getString(R.string.benene_count),
SettingsFragment.beneneCount
)
.apply()
it.summary =
getString(R.string.benene_count_text).format(SettingsFragment.beneneCount)
} catch (e: Exception) {
logError(e)
}
return@setOnPreferenceClickListener true
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}

View File

@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.ui.settings
import android.os.Bundle
import android.view.View
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.*
@ -10,6 +11,7 @@ import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
import com.lagradost.cloudstream3.utils.HOMEPAGE_API
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
@ -44,6 +46,7 @@ class SettingsLang : PreferenceFragmentCompat() {
Triple("", "Italian", "it"),
Triple("", "Chinese", "zh"),
Triple("", "Indonesian", "id"),
Triple("", "Czech", "cs"),
).sortedBy { it.second } //ye, we go alphabetical, so ppl don't put their lang on top
private fun getCurrentLocale(): String {
@ -54,6 +57,11 @@ class SettingsLang : PreferenceFragmentCompat() {
return conf?.locale?.language ?: "en"
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setUpToolbar(R.string.category_preferred_media_and_lang)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
hideKeyboard()
setPreferencesFromResource(R.xml.settings_media_lang, rootKey)
@ -109,7 +117,7 @@ class SettingsLang : PreferenceFragmentCompat() {
return@setOnPreferenceClickListener true
}
getPref(R.string.locale_key)?.setOnPreferenceClickListener { pref ->
getPref(R.string.locale_key)?.setOnPreferenceClickListener {
val tempLangs = languages.toMutableList()
//if (beneneCount > 100) {
// tempLangs.add(Triple("\uD83E\uDD8D", "mmmm... monke", "mo"))

View File

@ -1,54 +0,0 @@
package com.lagradost.cloudstream3.ui.settings
import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.AcraApplication
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.utils.HOMEPAGE_API
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showNginxTextInputDialog
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
class SettingsNginx : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
hideKeyboard()
setPreferencesFromResource(R.xml.settings_nginx, rootKey)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
getPref(R.string.nginx_credentials)?.setOnPreferenceClickListener {
activity?.showNginxTextInputDialog(
settingsManager.getString(
getString(R.string.nginx_credentials_title),
"Nginx Credentials"
).toString(),
settingsManager.getString(getString(R.string.nginx_credentials), "")
.toString(), // key: the actual you use rn
android.text.InputType.TYPE_TEXT_VARIATION_URI,
{}) {
settingsManager.edit()
.putString(getString(R.string.nginx_credentials), it)
.apply() // change the stored url in nginx_url_key to it
}
return@setOnPreferenceClickListener true
}
getPref(R.string.nginx_url_key)?.setOnPreferenceClickListener {
activity?.showNginxTextInputDialog(
settingsManager.getString(getString(R.string.nginx_url_pref), "Nginx server url")
.toString(),
settingsManager.getString(getString(R.string.nginx_url_key), "")
.toString(), // key: the actual you use rn
android.text.InputType.TYPE_TEXT_VARIATION_URI, // uri
{}) {
settingsManager.edit()
.putString(getString(R.string.nginx_url_key), it)
.apply() // change the stored url in nginx_url_key to it
}
return@setOnPreferenceClickListener true
}
}
}

View File

@ -1,61 +1,26 @@
package com.lagradost.cloudstream3.ui.settings
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import androidx.activity.result.contract.ActivityResultContracts
import android.view.View
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.hippo.unifile.UniFile
import com.lagradost.cloudstream3.AcraApplication
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getFolderSize
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.VideoDownloadManager
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath
import java.io.File
class SettingsPlayer : PreferenceFragmentCompat() {
// Open file picker
private val pathPicker =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
// It lies, it can be null if file manager quits.
if (uri == null) return@registerForActivityResult
val context = context ?: AcraApplication.context ?: return@registerForActivityResult
// RW perms for the path
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
context.contentResolver.takePersistableUriPermission(uri, flags)
val file = UniFile.fromUri(context, uri)
println("Selected URI path: $uri - Full path: ${file.filePath}")
// Stores the real URI using download_path_key
// Important that the URI is stored instead of filepath due to permissions.
PreferenceManager.getDefaultSharedPreferences(context)
.edit().putString(getString(R.string.download_path_key), uri.toString()).apply()
// From URI -> File path
// File path here is purely for cosmetic purposes in settings
(file.filePath ?: uri.toString()).let {
PreferenceManager.getDefaultSharedPreferences(context)
.edit().putString(getString(R.string.download_path_pref), it).apply()
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setUpToolbar(R.string.category_player)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
hideKeyboard()
setPreferencesFromResource(R.xml.settings_player, rootKey)
@ -80,24 +45,6 @@ class SettingsPlayer : PreferenceFragmentCompat() {
}
return@setOnPreferenceClickListener true
}
getPref(R.string.dns_key)?.setOnPreferenceClickListener {
val prefNames = resources.getStringArray(R.array.dns_pref)
val prefValues = resources.getIntArray(R.array.dns_pref_values)
val currentDns =
settingsManager.getInt(getString(R.string.dns_pref), 0)
activity?.showBottomDialog(
prefNames.toList(),
prefValues.indexOf(currentDns),
getString(R.string.dns_pref),
true,
{}) {
settingsManager.edit().putInt(getString(R.string.dns_pref), prefValues[it]).apply()
(context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) }
}
return@setOnPreferenceClickListener true
}
getPref(R.string.prefer_limit_title_key)?.setOnPreferenceClickListener {
val prefNames = resources.getStringArray(R.array.limit_title_pref_names)
@ -236,60 +183,6 @@ class SettingsPlayer : PreferenceFragmentCompat() {
return@setOnPreferenceClickListener true
}
}
fun getDownloadDirs(): List<String> {
return normalSafeApiCall {
val defaultDir = VideoDownloadManager.getDownloadDir()?.filePath
// app_name_download_path = Cloudstream and does not change depending on release.
// DOES NOT WORK ON SCOPED STORAGE.
val secondaryDir =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) null else Environment.getExternalStorageDirectory().absolutePath +
File.separator + resources.getString(R.string.app_name_download_path)
val first = listOf(defaultDir, secondaryDir)
(try {
val currentDir = context?.getBasePath()?.let { it.first?.filePath ?: it.second }
(first +
requireContext().getExternalFilesDirs("").mapNotNull { it.path } +
currentDir)
} catch (e: Exception) {
first
}).filterNotNull().distinct()
} ?: emptyList()
}
getPref(R.string.download_path_key)?.setOnPreferenceClickListener {
val dirs = getDownloadDirs()
val currentDir =
settingsManager.getString(getString(R.string.download_path_pref), null)
?: VideoDownloadManager.getDownloadDir().toString()
activity?.showBottomDialog(
dirs + listOf("Custom"),
dirs.indexOf(currentDir),
getString(R.string.download_path_pref),
true,
{}) {
// Last = custom
if (it == dirs.size) {
try {
pathPicker.launch(Uri.EMPTY)
} catch (e: Exception) {
logError(e)
}
} else {
// Sets both visual and actual paths.
// key = used path
// pref = visual path
settingsManager.edit()
.putString(getString(R.string.download_path_key), dirs[it]).apply()
settingsManager.edit()
.putString(getString(R.string.download_path_pref), dirs[it]).apply()
}
}
return@setOnPreferenceClickListener true
}
}
}

View File

@ -1,18 +1,24 @@
package com.lagradost.cloudstream3.ui.settings
import android.os.Bundle
import android.view.View
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
class SettingsUI : PreferenceFragmentCompat() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setUpToolbar(R.string.category_ui)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
hideKeyboard()
setPreferencesFromResource(R.xml.settins_ui, rootKey)

View File

@ -4,14 +4,15 @@ import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
import com.lagradost.cloudstream3.utils.BackupUtils.backup
import com.lagradost.cloudstream3.utils.BackupUtils.restorePrompt
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
@ -26,11 +27,14 @@ import java.io.OutputStream
import kotlin.concurrent.thread
class SettingsUpdates : PreferenceFragmentCompat() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setUpToolbar(R.string.category_updates)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
hideKeyboard()
setPreferencesFromResource(R.xml.settings_updates, rootKey)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
//val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
getPref(R.string.backup_key)?.setOnPreferenceClickListener {
activity?.backup()

View File

@ -57,6 +57,8 @@ data class SaveCaptionStyle(
@JsonProperty("elevation") var elevation: Int,
/**in sp**/
@JsonProperty("fixedTextSize") var fixedTextSize: Float?,
@JsonProperty("removeCaptions") var removeCaptions: Boolean = false,
@JsonProperty("removeBloat") var removeBloat: Boolean = true,
)
const val DEF_SUBS_ELEVATION = 20
@ -397,6 +399,15 @@ class SubtitlesFragment : Fragment() {
}
}
subtitles_remove_bloat?.isChecked = state.removeBloat
subtitles_remove_bloat?.setOnCheckedChangeListener { _, b ->
state.removeBloat = b
}
subtitles_remove_captions?.isChecked = state.removeCaptions
subtitles_remove_captions?.setOnCheckedChangeListener { _, b ->
state.removeCaptions = b
}
subs_font_size.setOnLongClickListener { _ ->
state.fixedTextSize = null
//textView.context.updateState() // font size not changed

View File

@ -6,6 +6,7 @@ import androidx.preference.PreferenceManager
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.lagradost.cloudstream3.mvvm.logError
const val DOWNLOAD_HEADER_CACHE = "download_header_cache"
@ -13,9 +14,8 @@ const val DOWNLOAD_HEADER_CACHE = "download_header_cache"
const val DOWNLOAD_EPISODE_CACHE = "download_episode_cache"
const val VIDEO_PLAYER_BRIGHTNESS = "video_player_alpha_key"
const val HOMEPAGE_API = "home_api_used"
const val SEARCH_PROVIDER_TOGGLE = "settings_providers_toggle"
const val PREFERENCES_NAME: String = "rebuild_preference"
const val PREFERENCES_NAME = "rebuild_preference"
object DataStore {
val mapper: JsonMapper = JsonMapper.builder().addModule(KotlinModule())
@ -34,17 +34,21 @@ object DataStore {
}
fun <T> Context.setKeyRaw(path: String, value: T, isEditingAppSettings: Boolean = false) {
val editor: SharedPreferences.Editor =
if (isEditingAppSettings) getDefaultSharedPrefs().edit() else getSharedPrefs().edit()
when (value) {
is Boolean -> editor.putBoolean(path, value)
is Int -> editor.putInt(path, value)
is String -> editor.putString(path, value)
is Float -> editor.putFloat(path, value)
is Long -> editor.putLong(path, value)
(value as? Set<String> != null) -> editor.putStringSet(path, value as Set<String>)
try {
val editor: SharedPreferences.Editor =
if (isEditingAppSettings) getDefaultSharedPrefs().edit() else getSharedPrefs().edit()
when (value) {
is Boolean -> editor.putBoolean(path, value)
is Int -> editor.putInt(path, value)
is String -> editor.putString(path, value)
is Float -> editor.putFloat(path, value)
is Long -> editor.putLong(path, value)
(value as? Set<String> != null) -> editor.putStringSet(path, value as Set<String>)
}
editor.apply()
} catch (e: Exception) {
logError(e)
}
editor.apply()
}
fun Context.getDefaultSharedPrefs(): SharedPreferences {
@ -69,11 +73,15 @@ object DataStore {
}
fun Context.removeKey(path: String) {
val prefs = getSharedPrefs()
if (prefs.contains(path)) {
val editor: SharedPreferences.Editor = prefs.edit()
editor.remove(path)
editor.apply()
try {
val prefs = getSharedPrefs()
if (prefs.contains(path)) {
val editor: SharedPreferences.Editor = prefs.edit()
editor.remove(path)
editor.apply()
}
} catch (e: Exception) {
logError(e)
}
}
@ -86,9 +94,13 @@ object DataStore {
}
fun <T> Context.setKey(path: String, value: T) {
val editor: SharedPreferences.Editor = getSharedPrefs().edit()
editor.putString(path, mapper.writeValueAsString(value))
editor.apply()
try {
val editor: SharedPreferences.Editor = getSharedPrefs().edit()
editor.putString(path, mapper.writeValueAsString(value))
editor.apply()
} catch (e: Exception) {
logError(e)
}
}
fun <T> Context.setKey(folder: String, path: String, value: T) {

View File

@ -132,7 +132,7 @@ object DataStoreHelper {
)
}
fun removeLastWatchedOld(parentId: Int?) {
private fun removeLastWatchedOld(parentId: Int?) {
if (parentId == null) return
removeKey("$currentAccount/$RESULT_RESUME_WATCHING_OLD", parentId.toString())
}

View File

@ -61,6 +61,7 @@ enum class Qualities(var value: Int) {
0 -> "Auto"
Unknown.value -> ""
P2160.value -> "4K"
null -> ""
else -> "${qual}p"
}
}
@ -117,6 +118,9 @@ val extractorApis: Array<ExtractorApi> = arrayOf(
VizcloudInfo(),
MwvnVizcloudInfo(),
VizcloudDigital(),
VizcloudCloud(),
VideoVard(),
VideovardSX(),
Mp4Upload(),
StreamTape(),
@ -168,6 +172,7 @@ val extractorApis: Array<ExtractorApi> = arrayOf(
DoodSoExtractor(),
DoodLaExtractor(),
DoodWsExtractor(),
DoodShExtractor(),
AsianLoad(),
@ -177,6 +182,11 @@ val extractorApis: Array<ExtractorApi> = arrayOf(
ZplayerV2(),
Upstream(),
Maxstream(),
Tantifilm(),
Userload(),
Supervideo(),
GuardareStream(),
// StreamSB.kt works
// SBPlay(),
@ -189,6 +199,7 @@ val extractorApis: Array<ExtractorApi> = arrayOf(
GMPlayer(),
Blogger(),
Solidfiles(),
Hxfile(),
KotakAnimeid(),

View File

@ -0,0 +1,5 @@
package com.lagradost.cloudstream3.utils
interface IOnBackPressed {
fun onBackPressed(): Boolean
}

View File

@ -45,7 +45,6 @@ object SubtitleHelper {
* @param looseCheck will use .contains in addition to .equals
* */
fun fromLanguageToTwoLetters(input: String, looseCheck: Boolean): String? {
languages.forEach {
if (it.languageName.equals(input, ignoreCase = true)
|| it.nativeName.equals(input, ignoreCase = true)

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M7,14L5,14v5h5v-2L7,17v-3zM5,10h2L7,7h3L10,5L5,5v5zM17,17h-3v2h5v-5h-2v3zM14,5v2h3v3h2L19,5h-5z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M5,16h3v3h2v-5L5,14v2zM8,8L5,8v2h5L10,5L8,5v3zM14,19h2v-3h3v-2h-5v5zM16,8L16,5h-2v5h5L19,8h-3z"/>
</vector>

View File

@ -1,12 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
app:tint="?attr/white">
<path
android:fillColor="@android:color/white"
android:pathData="M3,3v8h8L11,3L3,3zM9,9L5,9L5,5h4v4zM3,13v8h8v-8L3,13zM9,19L5,19v-4h4v4zM13,3v8h8L21,3h-8zM19,9h-4L15,5h4v4zM13,13v8h8v-8h-8zM19,19h-4v-4h4v4z"
android:fillType="evenOdd"/>
android:tint="?attr/white">
<path
android:fillColor="@android:color/white"
android:pathData="M3,3v8h8L11,3L3,3zM9,9L5,9L5,5h4v4zM3,13v8h8v-8L3,13zM9,19L5,19v-4h4v4zM13,3v8h8L21,3h-8zM19,9h-4L15,5h4v4zM13,13v8h8v-8h-8zM19,19h-4v-4h4v4z"
android:fillType="evenOdd" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/white">
<path
android:fillColor="@android:color/white"
android:pathData="M18,3v2h-2L16,3L8,3v2L6,5L6,3L4,3v18h2v-2h2v2h8v-2h2v2h2L20,3h-2zM8,17L6,17v-2h2v2zM8,13L6,13v-2h2v2zM8,9L6,9L6,7h2v2zM18,17h-2v-2h2v2zM18,13h-2v-2h2v2zM18,9h-2L16,7h2v2z"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="24dp"
android:height="24dp"
android:tint="?attr/white"
android:viewportWidth="283"
android:viewportHeight="283">
<path
android:name="path"
android:pathData="M 253.41 62.61 L 154.22 5.34 C 150.42 3.146 146.108 1.991 141.72 1.991 C 137.332 1.991 133.02 3.146 129.22 5.34 L 30 62.61 C 26.202 64.807 23.049 67.966 20.858 71.768 C 18.668 75.57 17.516 79.882 17.52 84.27 L 17.52 198.8 C 17.516 203.188 18.668 207.5 20.858 211.302 C 23.049 215.104 26.202 218.263 30 220.46 L 129.19 277.72 C 132.99 279.914 137.302 281.069 141.69 281.069 C 146.078 281.069 150.39 279.914 154.19 277.72 L 253.38 220.46 C 257.183 218.266 260.343 215.109 262.539 211.307 C 264.735 207.505 265.891 203.191 265.89 198.8 L 265.89 84.27 C 265.894 79.882 264.742 75.57 262.552 71.768 C 260.361 67.966 257.208 64.807 253.41 62.61 Z M 203.28 185.33 Q 203.28 200.61 187.03 200.61 C 184.56 200.637 182.098 200.331 179.71 199.7 C 177.529 199.086 175.467 198.109 173.61 196.81 C 171.687 195.463 169.917 193.91 168.33 192.18 Q 165.9 189.52 163.45 186.76 L 106.86 119.16 L 106.86 187.16 Q 106.86 193.81 102.86 197.22 C 100.004 199.558 96.388 200.768 92.7 200.62 Q 86.3 200.62 82.44 197.18 Q 78.58 193.74 78.58 187.18 L 78.58 97.63 C 78.438 94.563 78.992 91.503 80.2 88.68 C 81.685 86.126 83.925 84.093 86.61 82.86 C 89.603 81.356 92.911 80.585 96.26 80.61 C 98.633 80.541 101.001 80.879 103.26 81.61 C 105.096 82.243 106.813 83.179 108.34 84.38 C 109.979 85.728 111.477 87.239 112.81 88.89 C 114.33 90.74 115.91 92.66 117.53 94.67 L 175.53 163.06 L 175.53 94.06 Q 175.53 87.34 179.24 83.97 Q 182.95 80.6 189.24 80.61 C 193.57 80.61 197 81.73 199.5 83.97 C 202 86.21 203.26 89.58 203.26 94.06 Z"
android:fillColor="#000"
android:strokeWidth="1" />
</vector>

View File

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:tint="?attr/white"
android:width="35dp"
android:height="26dp"
android:viewportWidth="379"
android:viewportHeight="279">
<path
android:name="path"
android:pathData="M 235.89 60.62 L 136.7 3.35 C 132.9 1.156 128.588 0.001 124.2 0.001 C 119.812 0.001 115.5 1.156 111.7 3.35 L 12.48 60.62 C 8.682 62.817 5.529 65.976 3.338 69.778 C 1.148 73.58 -0.004 77.892 0 82.28 L 0 196.81 C -0.004 201.198 1.148 205.51 3.338 209.312 C 5.529 213.114 8.682 216.273 12.48 218.47 L 111.67 275.73 C 115.47 277.924 119.782 279.079 124.17 279.079 C 128.558 279.079 132.87 277.924 136.67 275.73 L 235.86 218.47 C 239.663 216.276 242.823 213.119 245.019 209.317 C 247.215 205.515 248.371 201.201 248.37 196.81 L 248.37 82.28 C 248.374 77.892 247.222 73.58 245.032 69.778 C 242.841 65.976 239.688 62.817 235.89 60.62 Z M 185.76 183.34 Q 185.76 198.62 169.51 198.62 C 167.04 198.647 164.578 198.341 162.19 197.71 C 160.009 197.096 157.947 196.119 156.09 194.82 C 154.167 193.473 152.397 191.92 150.81 190.19 Q 148.38 187.53 145.93 184.77 L 89.34 117.17 L 89.34 185.17 Q 89.34 191.82 85.34 195.23 C 82.484 197.568 78.868 198.778 75.18 198.63 Q 68.78 198.63 64.92 195.19 Q 61.06 191.75 61.06 185.19 L 61.06 95.64 C 60.918 92.573 61.472 89.513 62.68 86.69 C 64.165 84.136 66.405 82.103 69.09 80.87 C 72.083 79.366 75.391 78.595 78.74 78.62 C 81.113 78.551 83.481 78.889 85.74 79.62 C 87.576 80.253 89.293 81.189 90.82 82.39 C 92.459 83.738 93.957 85.249 95.29 86.9 C 96.81 88.75 98.39 90.67 100.01 92.68 L 158.01 161.07 L 158.01 92.07 Q 158.01 85.35 161.72 81.98 Q 165.43 78.61 171.72 78.62 C 176.05 78.62 179.48 79.74 181.98 81.98 C 184.48 84.22 185.74 87.59 185.74 92.07 Z"
android:fillColor="#000"
android:strokeWidth="1" />
<path
android:name="path_1"
android:pathData="M 312.84 143.37 C 320.84 128.98 336.13 120.49 345.04 107.75 C 354.48 94.4 349.18 69.45 322.48 69.45 C 304.98 69.45 296.39 82.7 292.77 93.67 L 265.94 82.39 C 273.29 60.39 293.27 41.39 322.37 41.39 C 346.69 41.39 363.37 52.47 371.85 66.34 C 379.1 78.25 383.34 100.51 372.16 117.07 C 359.74 135.39 347.83 140.98 341.41 152.79 C 338.83 157.55 337.79 160.65 337.79 175.98 L 307.87 175.98 C 307.77 167.9 306.53 154.75 312.84 143.37 Z M 343.17 217.37 C 343.175 222.063 341.584 226.62 338.661 230.291 C 335.737 233.962 331.651 236.533 327.077 237.579 C 322.502 238.625 317.705 238.086 313.477 236.05 C 309.249 234.015 305.835 230.601 303.8 226.373 C 301.764 222.145 301.225 217.348 302.271 212.773 C 303.317 208.199 305.888 204.113 309.559 201.189 C 313.23 198.266 317.787 196.675 322.48 196.68 C 327.963 196.698 333.221 198.888 337.097 202.767 C 340.972 206.646 343.157 211.907 343.17 217.39 Z"
android:fillColor="#ffffff"
android:strokeWidth="1" />
</vector>

View File

@ -0,0 +1,32 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="283"
android:viewportHeight="283"
android:tint="?attr/white">
<group android:name="group">
<path
android:name="path"
android:pathData="M 16.72 227.55 L 53.82 227.55 L 53.82 264.65 L 16.72 264.65 Z M 70.41 227.55 L 107.51 227.55 L 107.51 264.65 L 70.41 264.65 Z M 123.18 227.55 L 160.28 227.55 L 160.28 264.65 L 123.18 264.65 Z"
android:fillColor="@color/white"
android:strokeWidth="1" />
<path
android:name="path_1"
android:pathData="M 123.18 227.55 L 160.28 227.55 L 160.28 264.65 L 123.18 264.65 Z M 176.87 227.55 L 213.97 227.55 L 213.97 264.65 L 176.87 264.65 Z M 229.65 227.55 L 266.75 227.55 L 266.75 264.65 L 229.65 264.65 Z M 16.22 15.49 L 53.32 15.49 L 53.32 52.59 L 16.22 52.59 Z M 69.91 15.49 L 107.01 15.49 L 107.01 52.59 L 69.91 52.59 Z M 122.68 15.49 L 159.78 15.49 L 159.78 52.59 L 122.68 52.59 Z"
android:fillColor="@color/white"
android:strokeWidth="1" />
<path
android:name="path_2"
android:pathData="M 122.68 15.49 L 159.78 15.49 L 159.78 52.59 L 122.68 52.59 Z M 176.38 15.49 L 213.48 15.49 L 213.48 52.59 L 176.38 52.59 Z M 229.15 15.49 L 266.25 15.49 L 266.25 52.59 L 229.15 52.59 Z"
android:fillColor="@color/white"
android:strokeWidth="1" />
</group>
<group android:name="text">
<path
android:name="path_3"
android:pathData="M 35 139.88 Q 35 113.69 52.32 96.64 Q 69.64 79.59 93.39 79.58 Q 119.67 79.58 136.86 96.73 Q 154.05 113.88 154.05 140.06 Q 154.05 166.06 137.05 183.26 Q 120.05 200.46 94.2 200.45 Q 68.37 200.45 51.68 183.35 Q 34.99 166.25 35 139.88 Z M 94.57 103.16 Q 79.72 102.89 70.64 113.42 Q 61.56 123.95 61.54 140.6 Q 61.54 156.35 70.81 166.7 C 73.705 170.042 77.303 172.703 81.347 174.493 C 85.39 176.282 89.78 177.155 94.2 177.05 Q 109.05 177.05 118.2 166.83 Q 127.35 156.61 127.33 139.83 Q 127.33 123.36 118.38 113.37 Q 109.43 103.38 94.56 103.16 Z M 245.3 91.55 L 229.46 108.92 Q 216.95 101.54 211.46 101.54 C 210.088 101.531 208.73 101.812 207.474 102.363 C 206.218 102.914 205.092 103.724 204.17 104.74 C 203.182 105.741 202.402 106.928 201.877 108.233 C 201.352 109.537 201.091 110.934 201.11 112.34 Q 201.11 121.07 216.95 127.46 C 223.058 129.928 228.932 132.94 234.5 136.46 C 238.642 139.329 242.048 143.136 244.44 147.57 C 247.095 152.262 248.475 157.569 248.44 162.96 Q 248.44 178.17 236.16 189.42 C 228.316 196.779 217.915 200.814 207.16 200.67 Q 188.8 200.67 170.9 183.39 L 187.63 163.86 Q 198.88 175.47 208.69 175.47 Q 213.28 175.47 217.51 171.38 Q 221.74 167.29 221.74 162.81 Q 221.74 153.57 202.21 146.51 Q 191.05 142.44 186.37 138.89 C 182.948 136.128 180.247 132.576 178.5 128.54 C 176.412 124.199 175.319 119.447 175.3 114.63 Q 175.3 98.88 185.56 89.07 Q 195.82 79.26 212.38 79.27 Q 232 79.23 245.3 91.55 Z"
android:fillColor="@color/white"
android:strokeWidth="1" />
</group>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/white">
<path
android:fillColor="@android:color/white"
android:pathData="M11.07,12.85c0.77,-1.39 2.25,-2.21 3.11,-3.44c0.91,-1.29 0.4,-3.7 -2.18,-3.7c-1.69,0 -2.52,1.28 -2.87,2.34L6.54,6.96C7.25,4.83 9.18,3 11.99,3c2.35,0 3.96,1.07 4.78,2.41c0.7,1.15 1.11,3.3 0.03,4.9c-1.2,1.77 -2.35,2.31 -2.97,3.45c-0.25,0.46 -0.35,0.76 -0.35,2.24h-2.89C10.58,15.22 10.46,13.95 11.07,12.85zM14,20c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2c0,-1.1 0.9,-2 2,-2S14,18.9 14,20z"/>
</vector>

View File

@ -16,13 +16,14 @@
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:id="@+id/account_main_profile_picture_holder"
app:cardCornerRadius="100dp"
android:layout_gravity="center_vertical"
android:layout_width="35dp"
android:layout_height="35dp">
<ImageView
android:id="@+id/account_profile_picture"
android:id="@+id/account_main_profile_picture"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="ContentDescription" />

View File

@ -7,6 +7,7 @@
android:layout_width="match_parent">
<androidx.cardview.widget.CardView
android:id="@+id/account_profile_picture_holder"
android:layout_marginStart="10dp"
app:cardCornerRadius="100dp"
android:layout_gravity="center_vertical"

View File

@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_marginTop="20dp"
android:layout_marginBottom="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_rowWeight="1"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:textColor="?attr/textColor"
android:textSize="20sp"
android:textStyle="bold"
tools:text="Test" />
<com.google.android.material.button.MaterialButton
style="@style/WhiteButton"
android:layout_gravity="center_vertical|end"
app:icon="@drawable/ic_baseline_add_24"
android:text="@string/create_account"
android:id="@+id/create_account"
android:layout_width="wrap_content" />
</FrameLayout>
<LinearLayout
android:orientation="vertical"
android:layout_marginBottom="60dp"
android:layout_marginHorizontal="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:textColorHint="?attr/grayTextColor"
android:hint="@string/example_username"
android:autofillHints="username"
android:id="@+id/login_username_input"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusLeft="@id/apply_btt"
android:nextFocusDown="@id/login_email_input"
android:requiresFadingEdge="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
tools:ignore="LabelFor" />
<EditText
android:textColorHint="?attr/grayTextColor"
android:autofillHints="emailAddress"
android:hint="@string/example_email"
android:id="@+id/login_email_input"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusLeft="@id/apply_btt"
android:nextFocusUp="@id/login_username_input"
android:nextFocusDown="@id/login_server_input"
android:requiresFadingEdge="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textEmailAddress"
tools:ignore="LabelFor" />
<EditText
android:textColorHint="?attr/grayTextColor"
android:hint="@string/example_ip"
android:id="@+id/login_server_input"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusLeft="@id/apply_btt"
android:nextFocusUp="@id/login_email_input"
android:nextFocusDown="@id/login_password_input"
android:requiresFadingEdge="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
tools:ignore="LabelFor" />
<EditText
android:textColorHint="?attr/grayTextColor"
android:hint="@string/example_password"
android:id="@+id/login_password_input"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusLeft="@id/apply_btt"
android:nextFocusUp="@id/login_server_input"
android:nextFocusDown="@id/apply_btt"
android:requiresFadingEdge="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textVisiblePassword"
tools:ignore="LabelFor"
android:autofillHints="password" />
</LinearLayout>
<LinearLayout
android:id="@+id/apply_btt_holder"
android:orientation="horizontal"
android:layout_gravity="bottom"
android:gravity="bottom|end"
android:layout_marginTop="-60dp"
android:layout_width="match_parent"
android:layout_height="60dp">
<com.google.android.material.button.MaterialButton
style="@style/WhiteButton"
android:layout_gravity="center_vertical|end"
android:text="@string/login"
android:id="@+id/apply_btt"
android:layout_width="wrap_content" />
<com.google.android.material.button.MaterialButton
style="@style/BlackButton"
android:layout_gravity="center_vertical|end"
android:text="@string/sort_cancel"
android:id="@+id/cancel_btt"
android:layout_width="wrap_content" />
</LinearLayout>
</LinearLayout>

Some files were not shown because too many files have changed in this diff Show More