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: paths-ignore:
- '*.md' - '*.md'
- '*.json' - '*.json'
- '**/wcokey.txt'
concurrency: concurrency:
group: "pre-release" group: "pre-release"

1
.idea/gradle.xml generated
View file

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

View file

@ -35,8 +35,8 @@ android {
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 30 targetSdkVersion 30
versionCode 47 versionCode 48
versionName "2.10.25" versionName "2.10.28"
resValue "string", "app_version", resValue "string", "app_version",
"${defaultConfig.versionName}${versionNameSuffix ?: ""}" "${defaultConfig.versionName}${versionNameSuffix ?: ""}"
@ -93,12 +93,12 @@ dependencies {
testImplementation 'org.json:json:20180813' testImplementation 'org.json:json:20180813'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.appcompat:appcompat:1.4.1' 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 '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.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.0-beta01' implementation 'androidx.navigation:navigation-fragment-ktx:2.5.0-rc01'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.0-beta01' implementation 'androidx.navigation:navigation-ui-ktx:2.5.0-rc01'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
@ -171,4 +171,10 @@ dependencies {
implementation 'com.facebook.shimmer:shimmer:0.5.0' implementation 'com.facebook.shimmer:shimmer:0.5.0'
implementation "androidx.tvprovider:tvprovider:1.0.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 canShowPipMode: Boolean = false
var isInPIPMode: Boolean = false var isInPIPMode: Boolean = false
val backEvent = Event<Boolean>()
val onColorSelectedEvent = Event<Pair<Int, Int>>() val onColorSelectedEvent = Event<Pair<Int, Int>>()
val onDialogDismissedEvent = Event<Int>() val onDialogDismissedEvent = Event<Int>()
@ -282,6 +281,10 @@ object CommonActivity {
KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_NUMPAD_9 -> { KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_NUMPAD_9 -> {
PlayerEventType.ShowMirrors PlayerEventType.ShowMirrors
} }
// OpenSubtitles shortcut
KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8 -> {
PlayerEventType.SearchSubtitlesOnline
}
KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3 -> { KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3 -> {
PlayerEventType.ShowSpeed PlayerEventType.ShowSpeed
} }

View file

@ -14,8 +14,8 @@ import com.lagradost.cloudstream3.animeproviders.*
import com.lagradost.cloudstream3.metaproviders.CrossTmdbProvider import com.lagradost.cloudstream3.metaproviders.CrossTmdbProvider
import com.lagradost.cloudstream3.movieproviders.* import com.lagradost.cloudstream3.movieproviders.*
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.aniListApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
@ -42,6 +42,8 @@ object APIHolder {
val allProviders by lazy { val allProviders by lazy {
arrayListOf( arrayListOf(
// Movie providers // Movie providers
ElifilmsProvider(),
EstrenosDoramasProvider(),
PelisplusProvider(), PelisplusProvider(),
PelisplusHDProvider(), PelisplusHDProvider(),
PeliSmartProvider(), PeliSmartProvider(),
@ -53,7 +55,6 @@ object APIHolder {
PelisflixProvider(), PelisflixProvider(),
SeriesflixProvider(), SeriesflixProvider(),
IHaveNoTvProvider(), // Documentaries provider IHaveNoTvProvider(), // Documentaries provider
LookMovieProvider(), // RECAPTCHA (Please allow up to 5 seconds...)
VMoveeProvider(), VMoveeProvider(),
AllMoviesForYouProvider(), AllMoviesForYouProvider(),
VidEmbedProvider(), VidEmbedProvider(),
@ -76,7 +77,7 @@ object APIHolder {
TwoEmbedProvider(), TwoEmbedProvider(),
DramaSeeProvider(), DramaSeeProvider(),
WatchAsianProvider(), WatchAsianProvider(),
DramaidProvider(), DramaidProvider(),
KdramaHoodProvider(), KdramaHoodProvider(),
AkwamProvider(), AkwamProvider(),
MyCimaProvider(), MyCimaProvider(),
@ -87,9 +88,12 @@ object APIHolder {
TheFlixToProvider(), TheFlixToProvider(),
StreamingcommunityProvider(), StreamingcommunityProvider(),
TantifilmProvider(), TantifilmProvider(),
CineblogProvider(),
AltadefinizioneProvider(),
HDMovie5(), HDMovie5(),
RebahinProvider(), RebahinProvider(),
LayarKaca21Provider(), LayarKacaProvider(),
HDTodayProvider(),
// Metadata providers // Metadata providers
//TmdbProvider(), //TmdbProvider(),
@ -104,6 +108,9 @@ object APIHolder {
//ShiroProvider(), // v2 fucked me //ShiroProvider(), // v2 fucked me
AnimeFlickProvider(), AnimeFlickProvider(),
AnimeflvnetProvider(), AnimeflvnetProvider(),
AnimefenixProvider(),
AnimeflvIOProvider(),
JKAnimeProvider(),
TenshiProvider(), TenshiProvider(),
WcoProvider(), WcoProvider(),
AnimePaheProvider(), AnimePaheProvider(),
@ -113,19 +120,27 @@ object APIHolder {
ZoroProvider(), ZoroProvider(),
DubbedAnimeProvider(), DubbedAnimeProvider(),
MonoschinosProvider(), MonoschinosProvider(),
MundoDonghuaProvider(),
KawaiifuProvider(), // disabled due to cloudflare KawaiifuProvider(), // disabled due to cloudflare
NeonimeProvider(), NeonimeProvider(),
KuramanimeProvider(), KuramanimeProvider(),
OploverzProvider(), OploverzProvider(),
GomunimeProvider(), GomunimeProvider(),
NontonAnimeIDProvider(), NontonAnimeIDProvider(),
KuronimeProvider(), KuronimeProvider(),
//MultiAnimeProvider(), //MultiAnimeProvider(),
NginxProvider(), NginxProvider(),
OlgplyProvider(), OlgplyProvider(),
) )
} }
fun initAll() {
for (api in allProviders) {
api.init()
}
apiMap = null
}
var apis: List<MainAPI> = arrayListOf() var apis: List<MainAPI> = arrayListOf()
private var apiMap: Map<String, Int>? = null private var apiMap: Map<String, Int>? = null
@ -141,7 +156,6 @@ object APIHolder {
fun getApiFromNameNull(apiName: String?): MainAPI? { fun getApiFromNameNull(apiName: String?): MainAPI? {
if (apiName == null) return null if (apiName == null) return null
initMap() initMap()
return apiMap?.get(apiName)?.let { apis.getOrNull(it) } return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
} }
@ -154,12 +168,12 @@ object APIHolder {
return null return null
} }
fun getLoadResponseIdFromUrl(url : String, apiName: String) : Int { fun getLoadResponseIdFromUrl(url: String, apiName: String): Int {
return url.replace(getApiFromName(apiName).mainUrl, "").replace("/", "").hashCode() return url.replace(getApiFromName(apiName).mainUrl, "").replace("/", "").hashCode()
} }
fun LoadResponse.getId(): Int { 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 var overrideData: HashMap<String, ProvidersInfoJson>? = null
} }
fun overrideWithNewData(data: ProvidersInfoJson) { fun init() {
this.name = data.name
this.mainUrl = data.url
this.storedCredentials = data.credentials
}
init {
overrideData?.get(this.javaClass.simpleName)?.let { data -> overrideData?.get(this.javaClass.simpleName)?.let { data ->
overrideWithNewData(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 name = "NONE"
open var mainUrl = "NONE" open var mainUrl = "NONE"
open var storedCredentials: String? = null open var storedCredentials: String? = null
@ -463,12 +478,6 @@ fun base64Encode(array: ByteArray): String {
class ErrorLoadingException(message: String? = null) : Exception(message) 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? { fun MainAPI.fixUrlNull(url: String?): String? {
if (url.isNullOrEmpty()) { if (url.isNullOrEmpty()) {
return null return null
@ -763,12 +772,12 @@ fun AnimeSearchResponse.addDubStatus(isDub: Boolean, episodes: Int? = null) {
} }
fun AnimeSearchResponse.addDub(episodes: Int?) { fun AnimeSearchResponse.addDub(episodes: Int?) {
if(episodes == null || episodes <= 0) return if (episodes == null || episodes <= 0) return
addDubStatus(DubStatus.Dubbed, episodes) addDubStatus(DubStatus.Dubbed, episodes)
} }
fun AnimeSearchResponse.addSub(episodes: Int?) { fun AnimeSearchResponse.addSub(episodes: Int?) {
if(episodes == null || episodes <= 0) return if (episodes == null || episodes <= 0) return
addDubStatus(DubStatus.Subbed, episodes) addDubStatus(DubStatus.Subbed, episodes)
} }
@ -840,7 +849,7 @@ interface LoadResponse {
var posterUrl: String? var posterUrl: String?
var year: Int? var year: Int?
var plot: String? var plot: String?
var rating: Int? // 1-1000 var rating: Int? // 0-10000
var tags: List<String>? var tags: List<String>?
var duration: Int? // in minutes var duration: Int? // in minutes
var trailers: List<String>? 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?) { fun LoadResponse.addImdbId(id: String?) {
// TODO add imdb sync // TODO add imdb sync
} }
@ -919,7 +939,7 @@ interface LoadResponse {
} }
fun LoadResponse.addRating(value: Int?) { fun LoadResponse.addRating(value: Int?) {
if (value ?: return < 0 || value > 1000) { if ((value ?: return) < 0 || value > 10000) {
return return
} }
this.rating = value 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.allProviders
import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings 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.loadThemes
import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.CommonActivity.updateLocale import com.lagradost.cloudstream3.CommonActivity.updateLocale
import com.lagradost.cloudstream3.movieproviders.NginxProvider
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.OAuth2Apis import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.OAuth2accountApis import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.appString 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.APIRepository
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
import com.lagradost.cloudstream3.ui.result.ResultFragment 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.DataStore.setKey
import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching
import com.lagradost.cloudstream3.utils.DataStoreHelper.setViewPos 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.InAppUpdater.Companion.runAutoUpdate
import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
@ -131,12 +132,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
R.id.navigation_download_child, R.id.navigation_download_child,
R.id.navigation_subtitles, R.id.navigation_subtitles,
R.id.navigation_chrome_subtitles, R.id.navigation_chrome_subtitles,
R.id.navigation_settings_nginx,
R.id.navigation_settings_player, R.id.navigation_settings_player,
R.id.navigation_settings_updates, R.id.navigation_settings_updates,
R.id.navigation_settings_ui, R.id.navigation_settings_ui,
R.id.navigation_settings_account, R.id.navigation_settings_account,
R.id.navigation_settings_lang, R.id.navigation_settings_lang,
R.id.navigation_settings_general,
).contains(destination.id) ).contains(destination.id)
val landscape = when (resources.configuration.orientation) { val landscape = when (resources.configuration.orientation) {
@ -234,15 +235,23 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
onUserLeaveHint(this) onUserLeaveHint(this)
} }
override fun onBackPressed() { private fun backPressed() {
this.window?.navigationBarColor = this.window?.navigationBarColor =
this.colorFromAttribute(R.attr.primaryGrayBackground) this.colorFromAttribute(R.attr.primaryGrayBackground)
this.updateLocale() this.updateLocale()
backEvent.invoke(true)
super.onBackPressed() super.onBackPressed()
this.updateLocale() 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?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (VLC_REQUEST_CODE == requestCode) { if (VLC_REQUEST_CODE == requestCode) {
if (resultCode == RESULT_OK && data != null) { 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?) { override fun onCreate(savedInstanceState: Bundle?) {
// init accounts // init accounts
for (api in OAuth2accountApis) { for (api in accountManagers) {
api.init() api.init()
} }
ioSafe {
inAppAuths.apmap { api ->
try {
api.initialize()
} catch (e: Exception) {
logError(e)
}
}
}
SearchResultBuilder.updateCache(this) SearchResultBuilder.updateCache(this)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
@ -379,68 +443,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
false 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 // this pulls the latest data so ppl don't have to update to simply change provider url
if (downloadFromGithub) { if (downloadFromGithub) {
try { try {
@ -460,11 +462,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
setKey(PROVIDER_STATUS_KEY, txt) setKey(PROVIDER_STATUS_KEY, txt)
MainAPI.overrideData = newCache // update all new providers MainAPI.overrideData = newCache // update all new providers
val newUpdatedCache = initAll()
newCache?.let { addNginxToJson(it) ?: it }
for (api in apis) { // update current providers for (api in apis) { // update current providers
newUpdatedCache?.get(api.javaClass.simpleName) newCache?.get(api.javaClass.simpleName)
?.let { data -> ?.let { data ->
api.overrideWithNewData(data) api.overrideWithNewData(data)
} }
@ -482,14 +482,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
newCache newCache
}?.let { providersJsonMap -> }?.let { providersJsonMap ->
MainAPI.overrideData = providersJsonMap MainAPI.overrideData = providersJsonMap
val providersJsonMapUpdated = addNginxToJson(providersJsonMap) initAll()
?: providersJsonMap // if return null, use unchanged one
val acceptableProviders = 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() .map { it.key }.toSet()
val restrictedApis = 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() .map { it.key }.toSet() else emptySet()
apis = allProviders.filter { api -> apis = allProviders.filter { api ->
@ -506,23 +505,14 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
initAll()
apis = allProviders apis = allProviders
e.printStackTrace() e.printStackTrace()
logError(e) logError(e)
} }
} else { } else {
initAll()
apis = allProviders 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) loadThemes(this)
@ -585,7 +575,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
loadCache() loadCache()
test()
/*nav_view.setOnNavigationItemSelectedListener { item -> /*nav_view.setOnNavigationItemSelectedListener { item ->
when (item.itemId) { when (item.itemId) {
R.id.navigation_home -> { R.id.navigation_home -> {

View file

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

View file

@ -3,20 +3,11 @@ package com.lagradost.cloudstream3.animeproviders
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import java.util.* 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.logError
import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.parseJson 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() { class GomunimeProvider : MainAPI() {
override var mainUrl = "https://185.231.223.76" override var mainUrl = "https://185.231.223.76"
@ -210,7 +201,8 @@ class GomunimeProvider : MainAPI() {
M3u8Helper.generateM3u8( M3u8Helper.generateM3u8(
this.name, this.name,
link, link,
mainUrl, "$mainUrl/",
headers = mapOf("Origin" to mainUrl)
).forEach(callback) ).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 package com.lagradost.cloudstream3.animeproviders
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.mvvm.safeApiCall
@ -157,6 +156,8 @@ class KuronimeProvider : MainAPI() {
val token = data.substringAfter("var token = \"").substringBefore("\";") val token = data.substringAfter("var token = \"").substringBefore("\";")
val pat = data.substringAfter("var pat = \"").substringBefore("\";") val pat = data.substringAfter("var pat = \"").substringBefore("\";")
val link = "$doma$token$pat/index.m3u8" val link = "$doma$token$pat/index.m3u8"
val quality =
Regex("\\d{3,4}p").find(doc.select("title").text())?.groupValues?.get(0)
sourceCallback.invoke( sourceCallback.invoke(
ExtractorLink( ExtractorLink(
@ -164,7 +165,8 @@ class KuronimeProvider : MainAPI() {
this.name, this.name,
link, link,
referer = "https://animeku.org/", referer = "https://animeku.org/",
quality = Qualities.Unknown.value, quality = getQualityFromName(quality),
headers = mapOf("Origin" to "https://animeku.org"),
isM3u8 = true isM3u8 = true
) )
) )
@ -186,7 +188,7 @@ class KuronimeProvider : MainAPI() {
sources.apmap { sources.apmap {
safeApiCall { safeApiCall {
when { when {
it.contains("animeku.org") -> invokeKuroSource(it, callback) it.startsWith("https://animeku.org") -> invokeKuroSource(it, callback)
else -> loadExtractor(it, mainUrl, 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 package com.lagradost.cloudstream3.animeproviders
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.loadExtractor import com.lagradost.cloudstream3.utils.loadExtractor
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
@ -33,6 +31,7 @@ class NeonimeProvider : MainAPI() {
return when (t) { return when (t) {
"Ended" -> ShowStatus.Completed "Ended" -> ShowStatus.Completed
"OnGoing" -> ShowStatus.Ongoing "OnGoing" -> ShowStatus.Ongoing
"Ongoing" -> ShowStatus.Ongoing
"In Production" -> ShowStatus.Ongoing "In Production" -> ShowStatus.Ongoing
"Returning Series" -> ShowStatus.Ongoing "Returning Series" -> ShowStatus.Ongoing
else -> ShowStatus.Completed else -> ShowStatus.Completed

View file

@ -64,7 +64,7 @@ class NineAnimeProvider : MainAPI() {
} }
//Credits to https://github.com/jmir1 //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? { private fun getVrf(id: String): String? {
val reversed = ue(encode(id) + "0000000").slice(0..5).reversed() val reversed = ue(encode(id) + "0000000").slice(0..5).reversed()
@ -283,7 +283,8 @@ class NineAnimeProvider : MainAPI() {
jsonservers.vidstream, jsonservers.vidstream,
jsonservers.mcloud, jsonservers.mcloud,
jsonservers.mp4upload, jsonservers.mp4upload,
jsonservers.streamtape jsonservers.streamtape,
jsonservers.videovard
).mapNotNull { ).mapNotNull {
try { try {
val epserver = app.get("$mainUrl/ajax/anime/episode?id=$it").text 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 { override suspend fun getMainPage(): HomePageResponse {
val items = ArrayList<HomePageList>() val items = ArrayList<HomePageList>()
val soup = app.get(mainUrl, interceptor = ddosGuardKiller).document val soup = app.get(mainUrl, interceptor = ddosGuardKiller).document
println(soup)
for (section in soup.select("#content > section")) { for (section in soup.select("#content > section")) {
try { try {
if (section.attr("id") == "toplist-tabs") { 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.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.getQualityFromName
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
class DoodCxExtractor : DoodLaExtractor() { class DoodCxExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.cx" override var mainUrl = "https://dood.cx"
} }
class DoodShExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.sh"
}
class DoodPmExtractor : DoodLaExtractor() { class DoodPmExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.pm" 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 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 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 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( return listOf(
ExtractorLink( ExtractorLink(
trueUrl, trueUrl,
this.name, this.name,
trueUrl, trueUrl,
mainUrl, mainUrl,
Qualities.Unknown.value, getQualityFromName(quality),
false false
) )
) // links are valid in 8h ) // 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.USER_AGENT
import com.lagradost.cloudstream3.apmap import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.app 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.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
open class Mcloud : ExtractorApi() { open class Mcloud : ExtractorApi() {
override var name = "Mcloud" 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 "Referer" to "https://animekisa.in/", //Referer works for wco and animekisa, probably with others too
"Pragma" to "no-cache", "Pragma" to "no-cache",
"Cache-Control" 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>? { override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val link = url.replace("$mainUrl/e/","$mainUrl/info/") val id = url.substringAfter("e/").substringAfter("embed/").substringBefore("?")
val response = app.get(link, headers = headers).text 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>")) { if(response.startsWith("<!DOCTYPE html>")) {
// TODO decrypt html for link // TODO decrypt html for link
return emptyList() return emptyList()
} }
data class SourcesMcloud (
data class Sources ( @JsonProperty("file" ) val file : String
@JsonProperty("file") val file: String
) )
data class Media ( data class MediaMcloud (
@JsonProperty("sources") val sources: List<Sources> @JsonProperty("sources" ) val sources : ArrayList<SourcesMcloud> = arrayListOf()
)
data class DataMcloud (
@JsonProperty("media" ) val media : MediaMcloud? = MediaMcloud()
) )
data class JsonMcloud ( data class JsonMcloud (
@JsonProperty("success") val success: Boolean, @JsonProperty("status" ) val status : Int? = null,
@JsonProperty("media") val media: Media, @JsonProperty("data" ) val data : DataMcloud = DataMcloud()
) )
val mapped = parseJson<JsonMcloud>(response) val mapped = parseJson<JsonMcloud>(response)
val sources = mutableListOf<ExtractorLink>() val sources = mutableListOf<ExtractorLink>()
val checkfile = mapped.status == 200
if (mapped.success) if (checkfile)
mapped.media.sources.apmap { mapped.data.media?.sources?.apmap {
if (it.file.contains("m3u8")) { if (it.file.contains("m3u8")) {
M3u8Helper.generateM3u8( sources.addAll(
name, generateM3u8(
it.file, name,
url, it.file,
headers = app.get(url).headers.toMap() url,
).forEach { link -> headers = mapOf("Referer" to url)
sources.add(link) )
} )
} }
} }
return sources 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.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.apmap import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.app 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.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
@ -45,43 +47,103 @@ class VizcloudDigital : WcoStream() {
override var mainUrl = "https://vizcloud.digital" override var mainUrl = "https://vizcloud.digital"
} }
class VizcloudCloud : WcoStream() {
override var mainUrl = "https://vizcloud.cloud"
}
open class WcoStream : ExtractorApi() { open class WcoStream : ExtractorApi() {
override var name = "VidStream" // Cause works for animekisa and wco override var name = "VidStream" // Cause works for animekisa and wco
override var mainUrl = "https://vidstream.pro" override var mainUrl = "https://vidstream.pro"
override val requiresReferer = false 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> { override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val baseUrl = url.split("/e/")[0] 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( val (Id) = (Regex("/e/(.*?)?domain").find(url)?.destructured ?: Regex("""/e/(.*)""").find(
url url
)?.destructured) ?: return emptyList() )?.destructured) ?: return emptyList()
val (skey) = Regex("""skey\s=\s['"](.*?)['"];""").find(html)?.destructured // val (skey) = Regex("""skey\s=\s['"](.*?)['"];""").find(html)?.destructured
?: return emptyList() // ?: return emptyList()
val keys = getNewWcoKey()
val apiLink = "$baseUrl/info/$Id?domain=wcostream.cc&skey=$skey" 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" val referrer = "$baseUrl/e/$Id?domain=wcostream.cc"
data class Sources( data class SourcesWco (
@JsonProperty("file") val file: String, @JsonProperty("file" ) val file : String
@JsonProperty("label") val label: String?
) )
data class Media( data class MediaWco (
@JsonProperty("sources") val sources: List<Sources> @JsonProperty("sources" ) val sources : ArrayList<SourcesWco> = arrayListOf()
) )
data class WcoResponse( data class DataWco (
@JsonProperty("success") val success: Boolean, @JsonProperty("media" ) val media : MediaWco? = MediaWco()
@JsonProperty("media") val media: Media )
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 mapped = app.get(apiLink, headers = mapOf("Referer" to referrer)).parsed<WcoResponse>()
val sources = mutableListOf<ExtractorLink>() val sources = mutableListOf<ExtractorLink>()
val check = mapped.status == 200
if (mapped.success) { if (check) {
mapped.media.sources.forEach { mapped.data?.media?.sources?.forEach {
if (mainUrl == "https://vizcloud2.ru" || mainUrl == "https://vizcloud.online") { if (mainUrl == "https://vizcloud2.ru" || mainUrl == "https://vizcloud.online") {
if (it.file.contains("vizcloud2.ru") || it.file.contains("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 // 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.live",
"https://vizcloud.info", "https://vizcloud.info",
"https://mwvn.vizcloud.info", "https://mwvn.vizcloud.info",
"https://vizcloud.digital" "https://vizcloud.digital",
"https://vizcloud.cloud"
).contains(mainUrl) ).contains(mainUrl)
) { ) {
if (it.file.contains("m3u8")) { 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 package com.lagradost.cloudstream3.metaproviders
import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.aniListApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
import com.lagradost.cloudstream3.utils.SyncUtil import com.lagradost.cloudstream3.utils.SyncUtil
object SyncRedirector { object SyncRedirector {

View file

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

View file

@ -4,10 +4,12 @@ import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.LoadResponse.Companion.addActors import com.lagradost.cloudstream3.LoadResponse.Companion.addActors
import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.uwetrottmann.tmdb2.Tmdb import com.uwetrottmann.tmdb2.Tmdb
import com.uwetrottmann.tmdb2.entities.* import com.uwetrottmann.tmdb2.entities.*
import com.uwetrottmann.tmdb2.enumerations.AppendToResponseItem import com.uwetrottmann.tmdb2.enumerations.AppendToResponseItem
import com.uwetrottmann.tmdb2.enumerations.VideoType
import retrofit2.awaitResponse import retrofit2.awaitResponse
import java.util.* import java.util.*
@ -24,6 +26,8 @@ data class TmdbLink(
) )
open class TmdbProvider : MainAPI() { 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 // Use the LoadResponse from the metadata provider
open val useMetaLoadResponse = false open val useMetaLoadResponse = false
@ -142,6 +146,7 @@ open class TmdbProvider : MainAPI() {
tags = genres?.mapNotNull { it.name } tags = genres?.mapNotNull { it.name }
duration = episode_run_time?.average()?.toInt() duration = episode_run_time?.average()?.toInt()
rating = this@toLoadResponse.rating rating = this@toLoadResponse.rating
addTrailer(videos.toTrailers())
recommendations = (this@toLoadResponse.recommendations recommendations = (this@toLoadResponse.recommendations
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() } ?: 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 { private fun Movie.toLoadResponse(): MovieLoadResponse {
return newMovieLoadResponse( return newMovieLoadResponse(
this.title ?: this.original_title, getUrl(id, false), TvType.Movie, TmdbLink( this.title ?: this.original_title, getUrl(id, false), TvType.Movie, TmdbLink(
@ -170,6 +188,7 @@ open class TmdbProvider : MainAPI() {
tags = genres?.mapNotNull { it.name } tags = genres?.mapNotNull { it.name }
duration = runtime duration = runtime
rating = this@toLoadResponse.rating rating = this@toLoadResponse.rating
addTrailer(videos.toTrailers())
recommendations = (this@toLoadResponse.recommendations recommendations = (this@toLoadResponse.recommendations
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() } ?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
@ -259,7 +278,16 @@ open class TmdbProvider : MainAPI() {
return if (useMetaLoadResponse) { return if (useMetaLoadResponse) {
return if (isTvSeries) { 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() val response = body?.toLoadResponse()
if (response != null) { if (response != null) {
if (response.recommendations.isNullOrEmpty()) if (response.recommendations.isNullOrEmpty())
@ -278,7 +306,16 @@ open class TmdbProvider : MainAPI() {
response response
} else { } 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() val response = body?.toLoadResponse()
if (response != null) { if (response != null) {
if (response.recommendations.isNullOrEmpty()) if (response.recommendations.isNullOrEmpty())
@ -319,7 +356,7 @@ open class TmdbProvider : MainAPI() {
} }
override suspend fun search(query: String): List<SearchResponse>? { 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 { .body()?.results?.mapNotNull {
it.movie?.toSearchResponse() ?: it.tvShow?.toSearchResponse() 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 //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? { private fun getVrf(id: String): String? {
val reversed = ue(encode(id) + "0000000").slice(0..5).reversed() val reversed = ue(encode(id) + "0000000").slice(0..5).reversed()
@ -354,7 +354,8 @@ open class BflixProvider : MainAPI() {
jsonservers.vidstream, jsonservers.vidstream,
jsonservers.mcloud, jsonservers.mcloud,
jsonservers.mp4upload, jsonservers.mp4upload,
jsonservers.streamtape jsonservers.streamtape,
jsonservers.videovard,
).mapNotNull { ).mapNotNull {
val epserver = app.get("$mainUrl/ajax/episode/info?id=$it").text val epserver = app.get("$mainUrl/ajax/episode/info?id=$it").text
(if (epserver.contains("url")) { (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 hasChromecastSupport = true
override val hasDownloadSupport = true override val hasDownloadSupport = true
override val supportedTypes = setOf( override val supportedTypes = setOf(
TvType.TvSeries, TvType.AsianDrama,
TvType.Movie,
) )
override suspend fun getMainPage(): HomePageResponse { override suspend fun getMainPage(): HomePageResponse {

View file

@ -161,7 +161,6 @@ class EgyBestProvider : MainAPI() {
@JsonProperty("link") val link: String @JsonProperty("link") val link: String
) )
override suspend fun loadLinks( override suspend fun loadLinks(
data: String, data: String,
isCasting: Boolean, 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.ExtractorLink
import com.lagradost.cloudstream3.utils.httpsify import com.lagradost.cloudstream3.utils.httpsify
import com.lagradost.cloudstream3.utils.loadExtractor import com.lagradost.cloudstream3.utils.loadExtractor
import okhttp3.Interceptor
import org.jsoup.Jsoup import org.jsoup.Jsoup
class HDMovie5 : MainAPI() { class HDMovie5 : MainAPI() {
override var mainUrl = "https://hdmovie5.tv" override var mainUrl = "https://hdmovie5.mba"
override var name = "HDMovie" override var name = "HDMovie"
override val lang = "hi" override val lang = "hi"
@ -34,6 +33,7 @@ class HDMovie5 : MainAPI() {
MovieSearchResponse( MovieSearchResponse(
a.text(), a.text(),
a.attr("href"), a.attr("href"),
this.name, this.name,
TvType.Movie, TvType.Movie,
it.select("img").attr("src"), it.select("img").attr("src"),
@ -137,7 +137,8 @@ class HDMovie5 : MainAPI() {
callback: (ExtractorLink) -> Unit callback: (ExtractorLink) -> Unit
): Boolean { ): Boolean {
return data.split(",").apmapIndexed { index, it -> return data.split(",").apmapIndexed { index, it ->
val html = app.post( //println("loadLinks:::: $index $it")
val p = app.post(
"$mainUrl/wp-admin/admin-ajax.php", "$mainUrl/wp-admin/admin-ajax.php",
data = mapOf( data = mapOf(
"action" to "doo_player_ajax", "action" to "doo_player_ajax",
@ -145,10 +146,12 @@ class HDMovie5 : MainAPI() {
"nume" to "${index + 1}", "nume" to "${index + 1}",
"type" to "movie" "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 doc = Jsoup.parse(html)
val link = doc.select("iframe").attr("src") val link = doc.select("iframe").attr("src")
loadExtractor(httpsify(link), "$mainUrl/",callback) loadExtractor(httpsify(link), "$mainUrl/", callback)
}.contains(true) }.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 package com.lagradost.cloudstream3.movieproviders
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.httpsify
import com.lagradost.cloudstream3.utils.loadExtractor import com.lagradost.cloudstream3.utils.loadExtractor
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.util.* import java.util.*
class LayarKaca21Provider : MainAPI() { class LayarKacaProvider : MainAPI() {
override var mainUrl = "https://149.56.24.226/" override var mainUrl = "https://149.56.24.226"
override var name = "LayarKaca21" override var name = "LayarKaca"
override val hasMainPage = true override val hasMainPage = true
override val lang = "id" override val lang = "id"
override val hasDownloadSupport = true override val hasDownloadSupport = true
override val supportedTypes = setOf( override val supportedTypes = setOf(
TvType.Movie, TvType.Movie,
TvType.TvSeries, TvType.TvSeries,
TvType.AsianDrama
) )
override suspend fun getMainPage(): HomePageResponse { 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 package com.lagradost.cloudstream3.movieproviders
import com.lagradost.cloudstream3.* 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.utils.ExtractorLink
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.Qualities
import java.lang.Exception
class NginxProvider : MainAPI() { class NginxProvider : MainAPI() {
override var name = "Nginx" override var name = "Nginx"
@ -15,23 +10,40 @@ class NginxProvider : MainAPI() {
override val hasMainPage = true override val hasMainPage = true
override val supportedTypes = setOf(TvType.AnimeMovie, TvType.TvSeries, TvType.Movie) 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"
}
private fun getAuthHeader(): Map<String, String> {
fun getAuthHeader(storedCredentials: String?): Map<String, String> { val url = overrideUrl ?: throw ErrorLoadingException(ERROR_STRING)
if (storedCredentials == null) { mainUrl = url
return mapOf(Pair("Authorization", "Basic ")) // no Authorization headers 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 { 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 // 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 val metadataDocument = app.get(nfoUrl, authHeader).document // get the metadata nfo file
@ -44,27 +56,34 @@ class NginxProvider : MainAPI() {
if (isMovie) { if (isMovie) {
val poster = metadataDocument.selectFirst("thumb")!!.text() val poster = metadataDocument.selectFirst("thumb")!!.text()
val trailer = metadataDocument.select("trailer").mapNotNull { val trailer = metadataDocument.select("trailer").mapNotNull {
it?.text()?.replace( it?.text()?.replace(
"plugin://plugin.video.youtube/play/?video_id=", "plugin://plugin.video.youtube/play/?video_id=",
"https://www.youtube.com/watch?v=" "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 date = metadataDocument.selectFirst("year")?.text()?.toIntOrNull()
val ratingAverage = metadataDocument.selectFirst("value")?.text()?.toIntOrNull() val ratingAverage = metadataDocument.selectFirst("value")?.text()?.toIntOrNull()
val tagsList = metadataDocument.select("genre") val tagsList = metadataDocument.select("genre")
?.mapNotNull { // all the tags like action, thriller ... .mapNotNull { // all the tags like action, thriller ...
it?.text() it?.text()
} }
val dataList = mediaRootDocument.getElementsByAttributeValueContaining( // list of all urls of the webpage val dataList =
"href", mainRootDocument.getElementsByAttributeValueContaining( // list of all urls of the webpage
partialUrl "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( return newMovieLoadResponse(
title, title,
@ -81,7 +100,6 @@ class NginxProvider : MainAPI() {
} }
} else // a tv serie } else // a tv serie
{ {
val list = ArrayList<Pair<Int, String>>() val list = ArrayList<Pair<Int, String>>()
val mediaRootUrl = url.replace("tvshow.nfo", "") val mediaRootUrl = url.replace("tvshow.nfo", "")
val posterUrl = mediaRootUrl + "poster.jpg" val posterUrl = mediaRootUrl + "poster.jpg"
@ -91,7 +109,7 @@ class NginxProvider : MainAPI() {
val tagsList = metadataDocument.select("genre") 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() it?.text()
} }
@ -102,7 +120,7 @@ class NginxProvider : MainAPI() {
seasons.forEach { element -> seasons.forEach { element ->
val season = val season =
element.attr("href")?.replace("Season%20", "")?.replace("/", "")?.toIntOrNull() element.attr("href").replace("Season%20", "").replace("/", "").toIntOrNull()
val href = mediaRootUrl + element.attr("href") val href = mediaRootUrl + element.attr("href")
if (season != null && season > 0 && href.isNotBlank()) { if (season != null && season > 0 && href.isNotBlank()) {
list.add(Pair(season, href)) list.add(Pair(season, href))
@ -120,33 +138,40 @@ class NginxProvider : MainAPI() {
"href", "href",
".nfo" ".nfo"
) // get metadata ) // get metadata
episodes.forEach { episode -> episodes.forEach { episode ->
val nfoDocument = app.get(seasonString + episode.attr("href"), authHeader).document // get episode metadata file val nfoDocument = app.get(
val epNum = nfoDocument.selectFirst("episode")?.text()?.toIntOrNull() seasonString + episode.attr("href"),
val poster = authHeader
seasonString + episode.attr("href").replace(".nfo", "-thumb.jpg") ).document // get episode metadata file
val name = nfoDocument.selectFirst("title")!!.text() val epNum = nfoDocument.selectFirst("episode")?.text()?.toIntOrNull()
// val seasonInt = nfoDocument.selectFirst("season").text().toIntOrNull() val poster =
val date = nfoDocument.selectFirst("aired")?.text() seasonString + episode.attr("href").replace(".nfo", "-thumb.jpg")
val plot = nfoDocument.selectFirst("plot")?.text() 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( val dataList = seasonDocument.getElementsByAttributeValueContaining(
"href", "href",
episode.attr("href").replace(".nfo", "") 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 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( episodeList.add(
newEpisode(data) { newEpisode(data) {
this.name = name this.name = name
this.season = seasonInt this.season = seasonInt
this.episode = epNum this.episode = epNum
this.posterUrl = poster // will require headers too this.posterUrl = poster // will require headers too
this.description = plot this.description = plot
addDate(date) addDate(date)
} }
) )
} }
} }
return newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodeList) { return newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodeList) {
this.name = title this.name = title
@ -168,8 +193,9 @@ class NginxProvider : MainAPI() {
callback: (ExtractorLink) -> Unit callback: (ExtractorLink) -> Unit
): Boolean { ): Boolean {
// loadExtractor(data, null) { callback(it.copy(headers=authHeader)) } // 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 val authHeader =
callback.invoke ( getAuthHeader() // call again because it isn't reloaded if in main class and storedCredentials loads after
callback.invoke(
ExtractorLink( ExtractorLink(
name, name,
name, name,
@ -185,19 +211,23 @@ class NginxProvider : MainAPI() {
} }
override suspend fun getMainPage(): HomePageResponse { override suspend fun getMainPage(): HomePageResponse {
val authHeader = getAuthHeader(storedCredentials) // call again because it isn't reloaded if in main class and storedCredentials loads after val authHeader =
if (mainUrl == "NONE"){ getAuthHeader() // call again because it isn't reloaded if in main class and storedCredentials loads after
throw ErrorLoadingException("No nginx url specified in the settings: Nginx Settigns > Nginx server url, try again in a few seconds")
}
val document = app.get(mainUrl, authHeader).document val document = app.get(mainUrl, authHeader).document
val categories = document.select("a") val categories = document.select("a")
val returnList = categories.mapNotNull { 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 val categoryTitle = it.text() // get the category title like Movies or Series
if (categoryTitle != "../" && categoryTitle != "Music/") { // exclude parent dir and Music dir 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 contentLinks = categoryDocument.select("a")
val currentList = contentLinks.mapNotNull { head -> val currentList = contentLinks.mapNotNull { head ->
if (head.attr("href") != "../") { if (head.attr("href") != "../") {
@ -215,7 +245,6 @@ class NginxProvider : MainAPI() {
val nfoContent = val nfoContent =
app.get(nfoPath, authHeader).document // all the metadata app.get(nfoPath, authHeader).document // all the metadata
if (isMovieType) { if (isMovieType) {
val movieName = nfoContent.select("title").text() val movieName = nfoContent.select("title").text()
val posterUrl = mediaRootUrl + "poster.jpg" val posterUrl = mediaRootUrl + "poster.jpg"
@ -238,15 +267,11 @@ class NginxProvider : MainAPI() {
) { ) {
addPoster(posterUrl, authHeader) addPoster(posterUrl, authHeader)
} }
} }
} catch (e: Exception) { // can cause issues invisible errors } catch (e: Exception) { // can cause issues invisible errors
null null
//logError(e) // not working because it changes the return type of currentList to Any //logError(e) // not working because it changes the return type of currentList to Any
} }
} else null } else null
} }
if (currentList.isNotEmpty() && categoryTitle != "../") { // exclude upper dir if (currentList.isNotEmpty() && categoryTitle != "../") { // exclude upper dir

View file

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

View file

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

View file

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

View file

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

View file

@ -10,7 +10,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.loadExtractor import com.lagradost.cloudstream3.utils.loadExtractor
class WatchAsianProvider : MainAPI() { class WatchAsianProvider : MainAPI() {
override var mainUrl = "https://watchasian.sh" override var mainUrl = "https://watchasian.cx"
override var name = "WatchAsian" override var name = "WatchAsian"
override val hasQuickSearch = false override val hasQuickSearch = false
override val hasMainPage = true override val hasMainPage = true
@ -244,4 +244,4 @@ class WatchAsianProvider : MainAPI() {
fixUrlNull(it?.attr("data-video")) ?: return@mapNotNull null fixUrlNull(it?.attr("data-video")) ?: return@mapNotNull null
}?.toJson() ?: "" }?.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.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.syncproviders.providers.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 var accountIndex = defIndex
private var lastAccountIndex = defIndex
protected val accountId get() = "${idPrefix}_account_$accountIndex" protected val accountId get() = "${idPrefix}_account_$accountIndex"
private val accountActiveKey get() = "${idPrefix}_active" private val accountActiveKey get() = "${idPrefix}_active"
@ -35,8 +103,12 @@ abstract class AccountManager(private val defIndex: Int) : OAuth2API {
protected fun switchToNewAccount() { protected fun switchToNewAccount() {
val accounts = getAccounts() val accounts = getAccounts()
lastAccountIndex = accountIndex
accountIndex = (accounts?.maxOrNull() ?: 0) + 1 accountIndex = (accounts?.maxOrNull() ?: 0) + 1
} }
protected fun switchToOldAccount() {
accountIndex = lastAccountIndex
}
protected fun registerAccount() { protected fun registerAccount() {
setKey(accountActiveKey, accountIndex) 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 package com.lagradost.cloudstream3.syncproviders
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi interface OAuth2API : AuthAPI {
import com.lagradost.cloudstream3.syncproviders.providers.MALApi
import java.util.concurrent.TimeUnit
interface OAuth2API {
val key: String val key: String
val name: String
val redirectUrl: String val redirectUrl: String
// don't change this as all keys depend on it
val idPrefix: String
suspend fun handleRedirect(url: String) : Boolean suspend fun handleRedirect(url: String) : Boolean
fun authenticate() 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.* import com.lagradost.cloudstream3.*
interface SyncAPI : OAuth2API { interface SyncAPI : OAuth2API {
val icon: Int
val mainUrl: String 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.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.OAuth2API import com.lagradost.cloudstream3.syncproviders.AuthAPI
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.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.toJson
@ -32,11 +29,13 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
override val idPrefix = "anilist" override val idPrefix = "anilist"
override var mainUrl = "https://anilist.co" override var mainUrl = "https://anilist.co"
override val icon = R.drawable.ic_anilist_icon 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)?. // context.getUser(true)?.
getKey<AniListUser>(accountId, ANILIST_USER_KEY)?.let { user -> getKey<AniListUser>(accountId, ANILIST_USER_KEY)?.let { user ->
return OAuth2API.LoginInfo( return AuthAPI.LoginInfo(
profilePicture = user.picture, profilePicture = user.picture,
name = user.name, name = user.name,
accountIndex = accountIndex accountIndex = accountIndex

View file

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.syncproviders.providers package com.lagradost.cloudstream3.syncproviders.providers
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.OAuth2API import com.lagradost.cloudstream3.syncproviders.OAuth2API
//TODO dropbox sync //TODO dropbox sync
@ -8,6 +9,11 @@ class Dropbox : OAuth2API {
override var name = "Dropbox" override var name = "Dropbox"
override val key = "zlqsamadlwydvb2" override val key = "zlqsamadlwydvb2"
override val redirectUrl = "dropboxlogin" 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() { override fun authenticate() {
TODO("Not yet implemented") TODO("Not yet implemented")
@ -21,7 +27,7 @@ class Dropbox : OAuth2API {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override fun loginInfo(): OAuth2API.LoginInfo? { override fun loginInfo(): AuthAPI.LoginInfo? {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }

View file

@ -14,10 +14,7 @@ import com.lagradost.cloudstream3.ShowStatus
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.OAuth2API import com.lagradost.cloudstream3.syncproviders.AuthAPI
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.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
@ -37,15 +34,18 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
override val idPrefix = "mal" override val idPrefix = "mal"
override var mainUrl = "https://myanimelist.net" override var mainUrl = "https://myanimelist.net"
override val icon = R.drawable.mal_logo override val icon = R.drawable.mal_logo
override val requiresLogin = true
override val createAccountUrl = "$mainUrl/register.php"
override fun logOut() { override fun logOut() {
removeAccountKeys() removeAccountKeys()
} }
override fun loginInfo(): OAuth2API.LoginInfo? { override fun loginInfo(): AuthAPI.LoginInfo? {
//getMalUser(true)? //getMalUser(true)?
getKey<MalUser>(accountId, MAL_USER_KEY)?.let { user -> getKey<MalUser>(accountId, MAL_USER_KEY)?.let { user ->
return OAuth2API.LoginInfo( return AuthAPI.LoginInfo(
profilePicture = user.picture, profilePicture = user.picture,
name = user.name, name = user.name,
accountIndex = accountIndex 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.Resource
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.observe 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.noneApi
import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi
import com.lagradost.cloudstream3.ui.AutofitRecyclerView import com.lagradost.cloudstream3.ui.AutofitRecyclerView
@ -227,7 +227,7 @@ class HomeFragment : Fragment() {
listView?.choiceMode = AbsListView.CHOICE_MODE_SINGLE listView?.choiceMode = AbsListView.CHOICE_MODE_SINGLE
listView?.setOnItemClickListener { _, _, i, _ -> listView?.setOnItemClickListener { _, _, i, _ ->
if (!currentValidApis.isNullOrEmpty()) { if (currentValidApis.isNotEmpty()) {
currentApiName = currentValidApis[i].name currentApiName = currentValidApis[i].name
//to switch to apply simply remove this //to switch to apply simply remove this
currentApiName?.let(callback) currentApiName?.let(callback)
@ -882,7 +882,7 @@ class HomeFragment : Fragment() {
home_change_api_loading?.isVisible = false home_change_api_loading?.isVisible = false
} }
for (syncApi in OAuth2API.OAuth2Apis) { for (syncApi in OAuth2Apis) {
val login = syncApi.loginInfo() val login = syncApi.loginInfo()
val pic = login?.profilePicture val pic = login?.profilePicture
if (home_profile_picture?.setImage( if (home_profile_picture?.setImage(

View file

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

View file

@ -1,11 +1,17 @@
package com.lagradost.cloudstream3.ui.player package com.lagradost.cloudstream3.ui.player
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import android.util.SparseArray
import android.widget.FrameLayout 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.*
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider import com.google.android.exoplayer2.database.StandaloneDatabaseProvider
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource 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.LeastRecentlyUsedCacheEvictor
import com.google.android.exoplayer2.upstream.cache.SimpleCache import com.google.android.exoplayer2.upstream.cache.SimpleCache
import com.google.android.exoplayer2.util.MimeTypes import com.google.android.exoplayer2.util.MimeTypes
import com.google.android.exoplayer2.video.VideoSize
import com.lagradost.cloudstream3.APIHolder.getApiFromName import com.lagradost.cloudstream3.APIHolder.getApiFromName
import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.app 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.ui.subtitles.SaveCaptionStyle
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.ExtractorUri
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
import java.io.File import java.io.File
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
@ -153,7 +161,8 @@ class CS3IPlayer : IPlayer {
data: ExtractorUri?, data: ExtractorUri?,
startPosition: Long?, startPosition: Long?,
subtitles: Set<SubtitleData>, subtitles: Set<SubtitleData>,
subtitle: SubtitleData? subtitle: SubtitleData?,
autoPlay: Boolean?
) { ) {
Log.i(TAG, "loadPlayer") Log.i(TAG, "loadPlayer")
if (sameEpisode) { if (sameEpisode) {
@ -168,7 +177,7 @@ class CS3IPlayer : IPlayer {
} }
// we want autoplay because of TV and UX // we want autoplay because of TV and UX
isPlaying = true isPlaying = autoPlay ?: isPlaying
// release the current exoplayer and cache // release the current exoplayer and cache
releasePlayer() releasePlayer()
@ -322,6 +331,7 @@ class CS3IPlayer : IPlayer {
} }
companion object { companion object {
private var ytVideos: MutableMap<String, YtFile> = mutableMapOf()
private var simpleCache: SimpleCache? = null private var simpleCache: SimpleCache? = null
var requestSubtitleUpdate: (() -> Unit)? = null var requestSubtitleUpdate: (() -> Unit)? = null
@ -686,6 +696,14 @@ class CS3IPlayer : IPlayer {
isPlaying = exo.isPlaying isPlaying = exo.isPlaying
} }
when (playbackState) {
Player.STATE_READY -> {
onRenderFirst()
}
else -> {}
}
if (playWhenReady) { if (playWhenReady) {
when (playbackState) { when (playbackState) {
Player.STATE_READY -> { 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() }) // 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() { override fun onRenderedFirstFrame() {
updatedTime() 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() super.onRenderedFirstFrame()
onRenderFirst()
} }
}) })
} catch (e: Exception) { } 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) { private fun loadOfflinePlayer(context: Context, data: ExtractorUri) {
Log.i(TAG, "loadOfflinePlayer") Log.i(TAG, "loadOfflinePlayer")
try { try {
@ -815,10 +868,6 @@ class CS3IPlayer : IPlayer {
null null
} }
} }
SubtitleOrigin.OPEN_SUBTITLES -> {
// TODO
throw NotImplementedError()
}
SubtitleOrigin.EMBEDDED_IN_VIDEO -> { SubtitleOrigin.EMBEDDED_IN_VIDEO -> {
if (offlineSourceFactory != null) { if (offlineSourceFactory != null) {
activeSubtitles.add(sub) activeSubtitles.add(sub)
@ -833,9 +882,55 @@ class CS3IPlayer : IPlayer {
return Pair(subSources, activeSubtitles) 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) { private fun loadOnlinePlayer(context: Context, link: ExtractorLink) {
Log.i(TAG, "loadOnlinePlayer") Log.i(TAG, "loadOnlinePlayer $link")
try { 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 currentLink = link
if (ignoreSSL) { if (ignoreSSL) {

View file

@ -1,6 +1,8 @@
package com.lagradost.cloudstream3.ui.player package com.lagradost.cloudstream3.ui.player
import android.content.Context
import android.util.Log import android.util.Log
import androidx.preference.PreferenceManager
import com.google.android.exoplayer2.Format import com.google.android.exoplayer2.Format
import com.google.android.exoplayer2.text.SubtitleDecoder import com.google.android.exoplayer2.text.SubtitleDecoder
import com.google.android.exoplayer2.text.SubtitleDecoderFactory 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.ttml.TtmlDecoder
import com.google.android.exoplayer2.text.webvtt.WebvttDecoder import com.google.android.exoplayer2.text.webvtt.WebvttDecoder
import com.google.android.exoplayer2.util.MimeTypes import com.google.android.exoplayer2.util.MimeTypes
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import org.mozilla.universalchardet.UniversalDetector
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.charset.Charset
class CustomDecoder : SubtitleDecoder { class CustomDecoder : SubtitleDecoder {
companion object { 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 const val TAG = "CustomDecoder"
private var overrideEncoding: String? = null
var regexSubtitlesToRemoveCaptions = false var regexSubtitlesToRemoveCaptions = false
var regexSubtitlesToRemoveBloat = false
val bloatRegex = val bloatRegex =
listOf( listOf(
Regex( Regex(
@ -40,6 +60,8 @@ class CustomDecoder : SubtitleDecoder {
) )
val captionRegex = listOf(Regex("""(-\s?|)[\[({][\w\d\s]*?[])}]\s*""")) 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 { fun trimStr(string: String): String {
return string.trimStart().trim('\uFEFF', '\u200B').replace( return string.trimStart().trim('\uFEFF', '\u200B').replace(
Regex("[\u00A0\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u205F]"), 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() 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) { override fun queueInputBuffer(inputBuffer: SubtitleInputBuffer) {
Log.i(TAG, "queueInputBuffer") Log.i(TAG, "queueInputBuffer")
try { try {
if (realDecoder == null) { val inputString = getStr(inputBuffer)
inputBuffer.data?.let { data -> if (realDecoder == null && !inputString.isNullOrBlank()) {
// this way we read the subtitle file and decide what decoder to use instead of relying on mimetype var str: String = inputString
// this way we read the subtitle file and decide what decoder to use instead of relying on mimetype
val pos = data.position() Log.i(TAG, "Got data from queueInputBuffer")
data.position(0) //https://github.com/LagradOst/CloudStream-2/blob/ddd774ee66810137ff7bd65dae70bcf3ba2d2489/CloudStreamForms/CloudStreamForms/Script/MainChrome.cs#L388
val arr = ByteArray(minOf(data.remaining(), 100)) realDecoder = when {
data.get(arr) str.startsWith("WEBVTT", ignoreCase = true) -> WebvttDecoder()
data.position(pos) str.startsWith("<?xml version=\"", ignoreCase = true) -> TtmlDecoder()
(str.startsWith(
//https://emptycharacter.com/ "[Script Info]",
//https://www.fileformat.info/info/unicode/char/200b/index.htm ignoreCase = true
val str = trimStr(arr.decodeToString()) ) || str.startsWith("Title:", ignoreCase = true)) -> SsaDecoder()
Log.i(TAG, "Got data from queueInputBuffer") str.startsWith("1", ignoreCase = true) -> SubripDecoder()
Log.i(TAG, "first string is >>>$str<<<") else -> null
if (str.isNotEmpty()) { }
//https://github.com/LagradOst/CloudStream-2/blob/ddd774ee66810137ff7bd65dae70bcf3ba2d2489/CloudStreamForms/CloudStreamForms/Script/MainChrome.cs#L388 Log.i(
realDecoder = when { TAG,
str.startsWith("WEBVTT", ignoreCase = true) -> WebvttDecoder() "Decoder selected: $realDecoder"
str.startsWith("<?xml version=\"", ignoreCase = true) -> TtmlDecoder() )
(str.startsWith( realDecoder?.let { decoder ->
"[Script Info]", decoder.dequeueInputBuffer()?.let { buff ->
ignoreCase = true if (decoder::class.java != SsaDecoder::class.java) {
) || str.startsWith("Title:", ignoreCase = true)) -> SsaDecoder() if (regexSubtitlesToRemoveCaptions)
str.startsWith("1", ignoreCase = true) -> SubripDecoder() captionRegex.forEach { rgx ->
else -> null 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( Log.i(
TAG, 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 { } 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) realDecoder?.queueInputBuffer(inputBuffer)
} }
} catch (e: Exception) { } 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.CommonActivity.playerEventListener
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError 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.Qualities
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute 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.UIHelper.toPx
import com.lagradost.cloudstream3.utils.Vector2 import com.lagradost.cloudstream3.utils.Vector2
import kotlinx.android.synthetic.main.player_custom_layout.* 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.* import kotlin.math.*
const val MINIMUM_SEEK_TIME = 7000L // when swipe seeking 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 // All the UI Logic for the player
open class FullScreenPlayer : AbstractPlayerFragment() { open class FullScreenPlayer : AbstractPlayerFragment() {
protected open var lockRotation = true
protected open var isFullScreenPlayer = true
// state of player UI // state of player UI
protected var isShowing = false protected var isShowing = false
protected var isLocked = false protected var isLocked = false
@ -99,11 +125,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
// screenWidth and screenHeight does always // screenWidth and screenHeight does always
// refer to the screen while in landscape mode // refer to the screen while in landscape mode
private val screenWidth: Int protected val screenWidth: Int
get() { get() {
return max(displayMetrics.widthPixels, displayMetrics.heightPixels) return max(displayMetrics.widthPixels, displayMetrics.heightPixels)
} }
private val screenHeight: Int protected val screenHeight: Int
get() { get() {
return min(displayMetrics.widthPixels, displayMetrics.heightPixels) return min(displayMetrics.widthPixels, displayMetrics.heightPixels)
} }
@ -138,6 +164,14 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
throw NotImplementedError() 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*/ /** Returns false if the touch is on the status bar or navigation bar*/
private fun isValidTouch(rawX: Float, rawY: Float): Boolean { private fun isValidTouch(rawX: Float, rawY: Float): Boolean {
val statusHeight = statusBarHeight ?: 0 val statusHeight = statusBarHeight ?: 0
@ -150,7 +184,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
animateLayoutChanges() animateLayoutChanges()
} }
private fun animateLayoutChanges() { protected fun animateLayoutChanges() {
if (isShowing) { if (isShowing) {
updateUIVisibility() updateUIVisibility()
} else { } else {
@ -199,7 +233,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
player_ffwd_holder?.alpha = 1f player_ffwd_holder?.alpha = 1f
player_rew_holder?.alpha = 1f player_rew_holder?.alpha = 1f
// player_pause_play_holder?.alpha = 1f // player_pause_play_holder?.alpha = 1f
shadow_overlay?.isVisible = true
shadow_overlay?.startAnimation(fadeAnimation) shadow_overlay?.startAnimation(fadeAnimation)
player_ffwd_holder?.startAnimation(fadeAnimation) player_ffwd_holder?.startAnimation(fadeAnimation)
player_rew_holder?.startAnimation(fadeAnimation) player_rew_holder?.startAnimation(fadeAnimation)
@ -224,20 +258,22 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
player_subtitle_offset_btt?.isGone = player.getCurrentPreferredSubtitle() == null player_subtitle_offset_btt?.isGone = player.getCurrentPreferredSubtitle() == null
} }
override fun onResume() { protected fun enterFullscreen() {
activity?.hideSystemUI() if (isFullScreenPlayer) {
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE activity?.hideSystemUI()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && fullscreenNotch) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && fullscreenNotch) {
val params = activity?.window?.attributes val params = activity?.window?.attributes
params?.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES params?.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
activity?.window?.attributes = params activity?.window?.attributes = params
}
} }
if (lockRotation)
super.onResume() activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
} }
override fun onDestroy() { protected fun exitFullscreen() {
activity?.showSystemUI() activity?.showSystemUI()
//if (lockRotation)
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
// simply resets brightness and notch settings that might have been overridden // 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 WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
} }
activity?.window?.attributes = lp activity?.window?.attributes = lp
}
override fun onResume() {
enterFullscreen()
super.onResume()
}
override fun onDestroy() {
exitFullscreen()
super.onDestroy() super.onDestroy()
} }
@ -327,7 +372,8 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
} }
dialog.setOnDismissListener { dialog.setOnDismissListener {
activity?.hideSystemUI() if (isFullScreenPlayer)
activity?.hideSystemUI()
} }
applyButton.setOnClickListener { applyButton.setOnClickListener {
dialog.dismissSafe(activity) dialog.dismissSafe(activity)
@ -365,9 +411,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
act.getString(R.string.player_speed), act.getString(R.string.player_speed),
false, false,
{ {
activity?.hideSystemUI() if (isFullScreenPlayer)
activity?.hideSystemUI()
}) { index -> }) { index ->
activity?.hideSystemUI() if (isFullScreenPlayer)
activity?.hideSystemUI()
setPlayBackSpeed(speedsNumbers[index]) setPlayBackSpeed(speedsNumbers[index])
} }
} }
@ -446,9 +494,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
private fun onClickChange() { private fun onClickChange() {
isShowing = !isShowing isShowing = !isShowing
if (isShowing) { if (isShowing) {
player_intro_play?.isGone = true
autoHide() autoHide()
} }
activity?.hideSystemUI() if (isFullScreenPlayer)
activity?.hideSystemUI()
animateLayoutChanges() animateLayoutChanges()
player_pause_play?.requestFocus() player_pause_play?.requestFocus()
} }
@ -492,6 +542,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
player_lock_holder?.startAnimation(fadeAnimation) player_lock_holder?.startAnimation(fadeAnimation)
//player_go_back_holder?.startAnimation(fadeAnimation) //player_go_back_holder?.startAnimation(fadeAnimation)
shadow_overlay?.isVisible = true
shadow_overlay?.startAnimation(fadeAnimation) shadow_overlay?.startAnimation(fadeAnimation)
updateLockUI() updateLockUI()
@ -683,7 +734,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
if (event == null || view == null) return false if (event == null || view == null) return false
val currentTouch = Vector2(event.x, event.y) val currentTouch = Vector2(event.x, event.y)
val startTouch = currentTouchStart val startTouch = currentTouchStart
player_intro_play?.isGone = true
when (event.action) { when (event.action) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
// validates if the touch is inside of the player area // validates if the touch is inside of the player area
@ -708,7 +759,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
} }
} }
MotionEvent.ACTION_UP -> { MotionEvent.ACTION_UP -> {
if (isCurrentTouchValid && !isLocked) { if (isCurrentTouchValid && !isLocked && isFullScreenPlayer) {
// seek time // seek time
if (swipeHorizontalEnabled && currentTouchAction == TouchAction.Time) { if (swipeHorizontalEnabled && currentTouchAction == TouchAction.Time) {
val startTime = currentTouchStartPlayerTime val startTime = currentTouchStartPlayerTime
@ -737,7 +788,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
if (currentClickCount >= 1) { // have double clicked if (currentClickCount >= 1) { // have double clicked
currentDoubleTapIndex++ 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 { when {
currentTouch.x < screenWidth / 2 - (DOUBLE_TAB_PAUSE_PERCENTAGE * screenWidth) -> { currentTouch.x < screenWidth / 2 - (DOUBLE_TAB_PAUSE_PERCENTAGE * screenWidth) -> {
if (doubleTapEnabled) if (doubleTapEnabled)
@ -751,7 +802,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
player.handleEvent(CSPlayerEvent.PlayPauseToggle) player.handleEvent(CSPlayerEvent.PlayPauseToggle)
} }
} }
} else if (doubleTapEnabled) { } else if (doubleTapEnabled && isFullScreenPlayer) {
if (currentTouch.x < screenWidth / 2) { if (currentTouch.x < screenWidth / 2) {
rewind() rewind()
} else { } else {
@ -789,7 +840,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
} }
MotionEvent.ACTION_MOVE -> { MotionEvent.ACTION_MOVE -> {
// if current touch is valid // if current touch is valid
if (startTouch != null && isCurrentTouchValid && !isLocked) { if (startTouch != null && isCurrentTouchValid && !isLocked && isFullScreenPlayer) {
// action is unassigned and can therefore be assigned // action is unassigned and can therefore be assigned
if (currentTouchAction == null) { if (currentTouchAction == null) {
val diffFromStart = startTouch - currentTouch val diffFromStart = startTouch - currentTouch
@ -1013,6 +1064,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
// if nothing has loaded these buttons should not be visible // if nothing has loaded these buttons should not be visible
player_skip_episode?.isVisible = false player_skip_episode?.isVisible = false
player_skip_op?.isVisible = false player_skip_op?.isVisible = false
shadow_overlay?.isVisible = false
updateLockUI() updateLockUI()
updateUIVisibility() updateUIVisibility()
@ -1070,6 +1122,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
PlayerEventType.ShowMirrors -> { PlayerEventType.ShowMirrors -> {
showMirrorsDialogue() showMirrorsDialogue()
} }
PlayerEventType.SearchSubtitlesOnline -> {
if (subsProvidersIsActive) {
openOnlineSubPicker(view.context, null) {}
}
}
} }
} }
@ -1187,6 +1244,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
showMirrorsDialogue() 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 // 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 -> player_holder?.setOnTouchListener { callView, event ->
return@setOnTouchListener handleMotionEvent(callView, event) return@setOnTouchListener handleMotionEvent(callView, event)

View file

@ -1,7 +1,10 @@
package com.lagradost.cloudstream3.ui.player package com.lagradost.cloudstream3.ui.player
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
@ -15,7 +18,6 @@ import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.exoplayer2.util.MimeTypes import com.google.android.exoplayer2.util.MimeTypes
import com.google.android.material.button.MaterialButton
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull 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.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.observe 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.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType
import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.ui.result.SyncViewModel import com.lagradost.cloudstream3.ui.result.SyncViewModel
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment 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.*
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe 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.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage 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.fragment_player.*
import kotlinx.android.synthetic.main.player_custom_layout.* import kotlinx.android.synthetic.main.player_custom_layout.*
import kotlinx.android.synthetic.main.player_select_source_and_subs.*
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
class GeneratorPlayer : FullScreenPlayer() { class GeneratorPlayer : FullScreenPlayer() {
@ -50,8 +65,14 @@ class GeneratorPlayer : FullScreenPlayer() {
putSerializable("syncData", syncData) putSerializable("syncData", syncData)
} }
} }
val subsProviders
get() = subtitleProviders.filter { !it.requiresLogin || it.loginInfo() != null }
val subsProvidersIsActive
get() = subsProviders.isNotEmpty()
} }
private var titleRez = 3 private var titleRez = 3
private var limitTitle = 0 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() { private fun openSubPicker() {
try { try {
subsPathPicker.launch( 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 // Open file picker
private val subsPathPicker = private val subsPathPicker =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
@ -206,23 +416,7 @@ class GeneratorPlayer : FullScreenPlayer() {
name.toSubtitleMimeType() name.toSubtitleMimeType()
) )
setSubtitles(subtitleData) addAndSelectSubtitles(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
)
} }
} }
@ -230,6 +424,7 @@ class GeneratorPlayer : FullScreenPlayer() {
override fun showMirrorsDialogue() { override fun showMirrorsDialogue() {
try { try {
currentSelectedSubtitles = player.getCurrentPreferredSubtitle() currentSelectedSubtitles = player.getCurrentPreferredSubtitle()
//println("CURRENT SELECTED :$currentSelectedSubtitles of $currentSubs")
context?.let { ctx -> context?.let { ctx ->
val isPlaying = player.getIsPlaying() val isPlaying = player.getIsPlaying()
player.handleEvent(CSPlayerEvent.Pause) player.handleEvent(CSPlayerEvent.Pause)
@ -241,22 +436,46 @@ class GeneratorPlayer : FullScreenPlayer() {
val sourceDialog = sourceBuilder.create() val sourceDialog = sourceBuilder.create()
selectSourceDialog = sourceDialog selectSourceDialog = sourceDialog
sourceDialog.show() sourceDialog.show()
val providerList = val providerList = sourceDialog.sort_providers
sourceDialog.findViewById<ListView>(R.id.sort_providers)!! val subtitleList = sourceDialog.sort_subtitles
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 footer: TextView = val loadFromFileFooter: TextView =
layoutInflater.inflate(R.layout.sort_bottom_footer_add_choice, null) as 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() 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 sourceIndex = 0
var startSource = 0 var startSource = 0
@ -288,10 +507,7 @@ class GeneratorPlayer : FullScreenPlayer() {
} }
sourceDialog.setOnDismissListener { sourceDialog.setOnDismissListener {
if (isPlaying) { if (shouldDismiss) dismiss()
player.handleEvent(CSPlayerEvent.Play)
}
activity?.hideSystemUI()
selectSourceDialog = null selectSourceDialog = null
} }
@ -314,11 +530,60 @@ class GeneratorPlayer : FullScreenPlayer() {
subtitleList.setItemChecked(which, true) subtitleList.setItemChecked(which, true)
} }
cancelButton.setOnClickListener { sourceDialog.cancel_btt?.setOnClickListener {
sourceDialog.dismissSafe(activity) 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 var init = false
if (sourceIndex != startSource) { if (sourceIndex != startSource) {
init = true init = true
@ -477,14 +742,24 @@ class GeneratorPlayer : FullScreenPlayer() {
): SubtitleData? { ): SubtitleData? {
val langCode = preferredAutoSelectSubtitles ?: return null val langCode = preferredAutoSelectSubtitles ?: return null
val lang = SubtitleHelper.fromTwoLettersToLanguage(langCode) ?: return null val lang = SubtitleHelper.fromTwoLettersToLanguage(langCode) ?: return null
if (downloads) {
if (settings) return subtitles.firstOrNull { sub ->
subtitles.firstOrNull { sub -> (sub.origin == SubtitleOrigin.DOWNLOADED_FILE && sub.name == context?.getString(
sub.name.startsWith(lang) R.string.default_subtitles
|| sub.name.trim() == langCode ))
}?.let { sub ->
return sub
} }
}
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) { if (downloads) {
return subtitles.firstOrNull { sub -> return subtitles.firstOrNull { sub ->
(sub.origin == SubtitleOrigin.DOWNLOADED_FILE || sub.name == context?.getString( (sub.origin == SubtitleOrigin.DOWNLOADED_FILE || sub.name == context?.getString(
@ -492,10 +767,11 @@ class GeneratorPlayer : FullScreenPlayer() {
)) ))
} }
} }
return null return null
} }
private fun autoSelectFromSettings() { private fun autoSelectFromSettings(): Boolean {
// auto select subtitle based of settings // auto select subtitle based of settings
val langCode = preferredAutoSelectSubtitles val langCode = preferredAutoSelectSubtitles
@ -505,44 +781,46 @@ class GeneratorPlayer : FullScreenPlayer() {
if (setSubtitles(sub)) { if (setSubtitles(sub)) {
player.reloadPlayer(ctx) player.reloadPlayer(ctx)
player.handleEvent(CSPlayerEvent.Play) player.handleEvent(CSPlayerEvent.Play)
return true
} }
} }
} }
} }
return false
} }
private fun autoSelectFromDownloads() { private fun autoSelectFromDownloads(): Boolean {
if (player.getCurrentPreferredSubtitle() == null) { if (player.getCurrentPreferredSubtitle() == null) {
getAutoSelectSubtitle(currentSubs, settings = false, downloads = true)?.let { sub -> getAutoSelectSubtitle(currentSubs, settings = false, downloads = true)?.let { sub ->
context?.let { ctx -> context?.let { ctx ->
if (setSubtitles(sub)) { if (setSubtitles(sub)) {
player.reloadPlayer(ctx) player.reloadPlayer(ctx)
player.handleEvent(CSPlayerEvent.Play) player.handleEvent(CSPlayerEvent.Play)
return true
} }
} }
} }
} }
return false
} }
private fun autoSelectSubtitles() { private fun autoSelectSubtitles() {
normalSafeApiCall { normalSafeApiCall {
autoSelectFromSettings() if (!autoSelectFromSettings()) {
autoSelectFromDownloads() autoSelectFromDownloads()
}
} }
} }
@SuppressLint("SetTextI18n") private fun getPlayerVideoTitle(): String {
fun setTitle() {
var headerName: String? = null var headerName: String? = null
var subName: String? = null var subName: String? = null
var episode: Int? = null var episode: Int? = null
var season: Int? = null var season: Int? = null
var tvType: TvType? = null var tvType: TvType? = null
var isFiller: Boolean? = null
when (val meta = currentMeta) { when (val meta = currentMeta) {
is ResultEpisode -> { is ResultEpisode -> {
isFiller = meta.isFiller
headerName = meta.headerName headerName = meta.headerName
subName = meta.name subName = meta.name
episode = meta.episode episode = meta.episode
@ -559,7 +837,7 @@ class GeneratorPlayer : FullScreenPlayer() {
} }
//Generate video title //Generate video title
var playerVideoTitle = if (headerName != null) { val playerVideoTitle = if (headerName != null) {
(headerName + (headerName +
if (tvType.isEpisodeBased() && episode != null) if (tvType.isEpisodeBased() && episode != null)
if (season == null) if (season == null)
@ -570,6 +848,13 @@ class GeneratorPlayer : FullScreenPlayer() {
} else { } else {
"" ""
} }
return playerVideoTitle
}
@SuppressLint("SetTextI18n")
fun setTitle() {
var playerVideoTitle = getPlayerVideoTitle()
//Hide title, if set in setting //Hide title, if set in setting
if (limitTitle < 0) { if (limitTitle < 0) {
@ -582,6 +867,7 @@ class GeneratorPlayer : FullScreenPlayer() {
playerVideoTitle = playerVideoTitle.substring(0, limitTitle - 1) + "..." playerVideoTitle = playerVideoTitle.substring(0, limitTitle - 1) + "..."
} }
} }
val isFiller: Boolean? = (currentMeta as? ResultEpisode)?.isFiller
player_episode_filler_holder?.isVisible = isFiller ?: false player_episode_filler_holder?.isVisible = isFiller ?: false
player_video_title?.text = playerVideoTitle player_video_title?.text = playerVideoTitle
@ -645,6 +931,7 @@ class GeneratorPlayer : FullScreenPlayer() {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx) val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
titleRez = settingsManager.getInt(getString(R.string.prefer_limit_title_rez_key), 3) titleRez = settingsManager.getInt(getString(R.string.prefer_limit_title_rez_key), 3)
limitTitle = settingsManager.getInt(getString(R.string.prefer_limit_title_key), 0) limitTitle = settingsManager.getInt(getString(R.string.prefer_limit_title_key), 0)
updateForcedEncoding(ctx)
} }
unwrapBundle(savedInstanceState) unwrapBundle(savedInstanceState)

View file

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

View file

@ -1,14 +1,13 @@
package com.lagradost.cloudstream3.ui.player package com.lagradost.cloudstream3.ui.player
import android.content.Context
import android.net.Uri
import android.util.TypedValue import android.util.TypedValue
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import com.google.android.exoplayer2.ui.SubtitleView import com.google.android.exoplayer2.ui.SubtitleView
import com.google.android.exoplayer2.util.MimeTypes import com.google.android.exoplayer2.util.MimeTypes
import com.hippo.unifile.UniFile
import com.lagradost.cloudstream3.SubtitleFile 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.SaveCaptionStyle
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.fromSaveToStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.fromSaveToStyle
import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.UIHelper.toPx
@ -22,7 +21,6 @@ enum class SubtitleStatus {
enum class SubtitleOrigin { enum class SubtitleOrigin {
URL, URL,
DOWNLOADED_FILE, DOWNLOADED_FILE,
OPEN_SUBTITLES,
EMBEDDED_IN_VIDEO 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 { fun getSubtitleData(subtitleFile: SubtitleFile): SubtitleData {
return SubtitleData( return SubtitleData(
name = subtitleFile.lang, name = subtitleFile.lang,
@ -109,6 +85,8 @@ class PlayerSubtitleHelper {
} }
fun setSubStyle(style: SaveCaptionStyle) { fun setSubStyle(style: SaveCaptionStyle) {
regexSubtitlesToRemoveBloat = style.removeBloat
regexSubtitlesToRemoveCaptions = style.removeCaptions
subtitleView?.context?.let { ctx -> subtitleView?.context?.let { ctx ->
subStyle = style subStyle = style
subtitleView?.setStyle(ctx.fromSaveToStyle(style)) subtitleView?.setStyle(ctx.fromSaveToStyle(style))

View file

@ -15,7 +15,7 @@ class RepoLinkGenerator(
) : IGenerator { ) : IGenerator {
companion object { companion object {
const val TAG = "RepoLink" 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 override val hasCache = true
@ -71,7 +71,7 @@ class RepoLinkGenerator(
val (currentLinkCache, currentSubsCache) = if (clearCache) { val (currentLinkCache, currentSubsCache) = if (clearCache) {
Pair(mutableSetOf(), mutableSetOf()) Pair(mutableSetOf(), mutableSetOf())
} else { } 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() //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 return result
} }

View file

@ -24,12 +24,10 @@ import android.widget.*
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.graphics.drawable.toBitmap
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView import androidx.core.widget.NestedScrollView
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager 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.gms.cast.framework.CastState
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
import com.lagradost.cloudstream3.APIHolder.getApiFromName import com.lagradost.cloudstream3.APIHolder.getApiFromName
import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey 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.popupMenuNoIconsAndNoStringRes
import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.cloudstream3.utils.UIHelper.requestRW
import com.lagradost.cloudstream3.utils.UIHelper.setImage 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.getFileName
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
import kotlinx.android.synthetic.main.fragment_result.* import kotlinx.android.synthetic.main.fragment_result.*
@ -185,7 +181,7 @@ fun ResultEpisode.getWatchProgress(): Float {
return (getDisplayPosition() / 1000).toFloat() / (duration / 1000).toFloat() return (getDisplayPosition() / 1000).toFloat() / (duration / 1000).toFloat()
} }
class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegionsListener { class ResultFragment : ResultTrailerPlayer() {
companion object { companion object {
const val URL_BUNDLE = "url" const val URL_BUNDLE = "url"
const val API_NAME_BUNDLE = "apiName" 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)) 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>?) { private fun setActors(actors: List<ActorData>?) {
if (actors.isNullOrEmpty()) { if (actors.isNullOrEmpty()) {
result_cast_text?.isVisible = false result_cast_text?.isVisible = false
@ -779,7 +828,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
} else if (dy < -5) { } else if (dy < -5) {
result_bookmark_fab?.extend() result_bookmark_fab?.extend()
} }
result_poster_blur_holder?.translationY = -scrollY.toFloat() //result_poster_blur_holder?.translationY = -scrollY.toFloat()
}) })
result_back.setOnClickListener { result_back.setOnClickListener {
@ -1353,7 +1402,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
val newList = list.filter { it.isSynced && it.hasAccount } val newList = list.filter { it.isSynced && it.hasAccount }
result_mini_sync?.isVisible = newList.isNotEmpty() 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) { observe(syncModel.syncIds) {
@ -1508,7 +1557,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
when (startAction) { when (startAction) {
START_ACTION_RESUME_LATEST -> { START_ACTION_RESUME_LATEST -> {
for (ep in episodeList) { 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 if (ep.getWatchProgress() > 0.90f) { // watched too much
continue continue
} }
@ -1528,7 +1577,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
var found = false var found = false
for (ep in episodeList) { for (ep in episodeList) {
if (ep.id == startValue) { // watched too much 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)) handleAction(EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, ep))
found = true found = true
break break
@ -1537,7 +1586,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
if (!found) if (!found)
for (ep in episodeList) { for (ep in episodeList) {
if (ep.episode == resumeEpisode && ep.season == resumeSeason) { 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( handleAction(
EpisodeClickEvent( EpisodeClickEvent(
ACTION_PLAY_EPISODE_IN_PLAYER, ACTION_PLAY_EPISODE_IN_PLAYER,
@ -1729,6 +1778,8 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
setRecommendations(d.recommendations, null) setRecommendations(d.recommendations, null)
setActors(d.actors) setActors(d.actors)
setTrailers(d.trailers)
if (syncModel.addSyncs(d.syncData)) { if (syncModel.addSyncs(d.syncData)) {
syncModel.updateMetaAndUser() syncModel.updateMetaAndUser()
syncModel.updateSynced() syncModel.updateSynced()
@ -1741,7 +1792,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
val posterImageLink = d.posterUrl val posterImageLink = d.posterUrl
if (!posterImageLink.isNullOrEmpty()) { if (!posterImageLink.isNullOrEmpty()) {
result_poster?.setImage(posterImageLink, d.posterHeaders) 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 //Full screen view of Poster image
if (context?.isTrueTvSettings() == false) // Poster not clickable on tv if (context?.isTrueTvSettings() == false) // Poster not clickable on tv
result_poster_holder?.setOnClickListener { result_poster_holder?.setOnClickListener {
@ -1770,7 +1821,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
} else { } else {
result_poster?.setImageResource(R.drawable.default_cover) 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 result_poster_holder?.visibility = VISIBLE
@ -1788,7 +1839,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
} }
result_description.setOnClickListener { result_description.setOnClickListener {
val builder: AlertDialog.Builder = val builder: AlertDialog.Builder =
AlertDialog.Builder(requireContext()) AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom)
builder.setMessage(d.plot) builder.setMessage(d.plot)
.setTitle(if (d.type == TvType.Torrent) R.string.torrent_plot else R.string.result_plot) .setTitle(if (d.type == TvType.Torrent) R.string.torrent_plot else R.string.result_plot)
.show() .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 androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.apmap import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.aniListApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.utils.SyncUtil import com.lagradost.cloudstream3.utils.SyncUtil
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -20,7 +20,7 @@ data class CurrentSynced(
val idPrefix: String, val idPrefix: String,
val isSynced: Boolean, val isSynced: Boolean,
val hasAccount: Boolean, val hasAccount: Boolean,
val icon: Int, val icon: Int?,
) )
class SyncViewModel : ViewModel() { class SyncViewModel : ViewModel() {

View file

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

View file

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

View file

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

View file

@ -1,32 +1,54 @@
package com.lagradost.cloudstream3.ui.settings package com.lagradost.cloudstream3.ui.settings
import android.content.Context import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.widget.ImageView import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.annotation.UiThread
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager 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.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.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.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.setImage 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() { 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 = val builder =
AlertDialog.Builder(context ?: return, R.style.AlertDialogCustom) AlertDialog.Builder(context ?: return, R.style.AlertDialogCustom)
.setView(R.layout.account_managment) .setView(R.layout.account_managment)
val dialog = builder.show() val dialog = builder.show()
dialog.findViewById<ImageView>(R.id.account_profile_picture)?.setImage(info.profilePicture) dialog.account_main_profile_picture_holder?.isVisible =
dialog.findViewById<TextView>(R.id.account_logout)?.setOnClickListener { dialog.account_main_profile_picture?.setImage(info.profilePicture) == true
dialog.account_logout?.setOnClickListener {
api.logOut() api.logOut()
dialog.dismissSafe(activity) dialog.dismissSafe(activity)
} }
@ -34,13 +56,93 @@ class SettingsAccount : PreferenceFragmentCompat() {
(info.name ?: context?.getString(R.string.no_data))?.let { (info.name ?: context?.getString(R.string.no_data))?.let {
dialog.findViewById<TextView>(R.id.account_name)?.text = it 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) dialog.dismissSafe(activity)
showAccountSwitch(it.context, api) 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) { private fun showAccountSwitch(context: Context, api: AccountManager) {
val accounts = api.getAccounts() ?: return val accounts = api.getAccounts() ?: return
@ -48,17 +150,14 @@ class SettingsAccount : PreferenceFragmentCompat() {
AlertDialog.Builder(context, R.style.AlertDialogCustom).setView(R.layout.account_switch) AlertDialog.Builder(context, R.style.AlertDialogCustom).setView(R.layout.account_switch)
val dialog = builder.show() val dialog = builder.show()
dialog.findViewById<TextView>(R.id.account_add)?.setOnClickListener { dialog.account_add?.setOnClickListener {
try { addAccount(api)
api.authenticate() dialog?.dismissSafe(activity)
} catch (e: Exception) {
logError(e)
}
} }
val ogIndex = api.accountIndex val ogIndex = api.accountIndex
val items = ArrayList<OAuth2API.LoginInfo>() val items = ArrayList<AuthAPI.LoginInfo>()
for (index in accounts) { for (index in accounts) {
api.accountIndex = index api.accountIndex = index
@ -78,72 +177,30 @@ class SettingsAccount : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
hideKeyboard() hideKeyboard()
setPreferencesFromResource(R.xml.settings_credits_account, rootKey) setPreferencesFromResource(R.xml.settings_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
}
val syncApis = val syncApis =
listOf( listOf(
Pair(R.string.mal_key, OAuth2API.malApi), Pair( R.string.mal_key to malApi,
R.string.anilist_key, R.string.anilist_key to aniListApi,
OAuth2API.aniListApi R.string.opensubtitles_key to openSubtitlesApi,
) R.string.nginx_key to nginxApi,
) )
for ((key, api) in syncApis) { for ((key, api) in syncApis) {
getPref(key)?.apply { getPref(key)?.apply {
title = title =
getString(R.string.login_format).format(api.name, getString(R.string.account)) getString(R.string.login_format).format(api.name, getString(R.string.account))
setOnPreferenceClickListener { _ -> setOnPreferenceClickListener {
val info = api.loginInfo() val info = api.loginInfo()
if (info != null) { if (info != null) {
showLoginInfo(api, info) showLoginInfo(api, info)
} else { } else {
try { addAccount(api)
api.authenticate()
} catch (e: Exception) {
logError(e)
}
} }
return@setOnPreferenceClickListener true 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.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError 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.navigate
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import kotlinx.android.synthetic.main.main_settings.* import kotlinx.android.synthetic.main.main_settings.*
import kotlinx.android.synthetic.main.settings_title_top.*
import java.io.File import java.io.File
class SettingsFragment : Fragment() { 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 { fun getFolderSize(dir: File): Long {
var size: Long = 0 var size: Long = 0
dir.listFiles()?.let { dir.listFiles()?.let {
@ -75,9 +94,10 @@ class SettingsFragment : Fragment() {
private fun Context.isAutoTv(): Boolean { private fun Context.isAutoTv(): Boolean {
val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager? val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager?
// AFT = Fire TV // AFT = Fire TV
val model = Build.MODEL.lowercase()
return uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION || Build.MODEL.contains( return uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION || Build.MODEL.contains(
"AFT" "AFT"
) ) || model.contains("firestick") || model.contains("fire tv") || model.contains("chromecast")
} }
} }
@ -94,28 +114,39 @@ class SettingsFragment : Fragment() {
activity?.navigate(id, Bundle()) activity?.navigate(id, Bundle())
} }
settings_player?.setOnClickListener { val isTrueTv = context?.isTrueTvSettings() == true
navigate(R.id.action_navigation_settings_to_navigation_settings_player)
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 { listOf(
navigate(R.id.action_navigation_settings_to_navigation_settings_account) 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),
settings_ui?.setOnClickListener { Pair(settings_ui, R.id.action_navigation_settings_to_navigation_settings_ui),
navigate(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) ->
settings_lang?.setOnClickListener { view?.apply {
navigate(R.id.action_navigation_settings_to_navigation_settings_lang) setOnClickListener {
} navigate(navigationId)
}
settings_nginx?.setOnClickListener { if (isTrueTv) {
navigate(R.id.action_navigation_settings_to_navigation_settings_nginx) isFocusable = true
} isFocusableInTouchMode = true
}
settings_updates?.setOnClickListener { }
navigate(R.id.action_navigation_settings_to_navigation_settings_updates)
} }
} }
} }

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 package com.lagradost.cloudstream3.ui.settings
import android.os.Bundle import android.os.Bundle
import android.view.View
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
@ -10,6 +11,7 @@ import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref 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.HOMEPAGE_API
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
@ -44,6 +46,7 @@ class SettingsLang : PreferenceFragmentCompat() {
Triple("", "Italian", "it"), Triple("", "Italian", "it"),
Triple("", "Chinese", "zh"), Triple("", "Chinese", "zh"),
Triple("", "Indonesian", "id"), Triple("", "Indonesian", "id"),
Triple("", "Czech", "cs"),
).sortedBy { it.second } //ye, we go alphabetical, so ppl don't put their lang on top ).sortedBy { it.second } //ye, we go alphabetical, so ppl don't put their lang on top
private fun getCurrentLocale(): String { private fun getCurrentLocale(): String {
@ -54,6 +57,11 @@ class SettingsLang : PreferenceFragmentCompat() {
return conf?.locale?.language ?: "en" 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?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
hideKeyboard() hideKeyboard()
setPreferencesFromResource(R.xml.settings_media_lang, rootKey) setPreferencesFromResource(R.xml.settings_media_lang, rootKey)
@ -109,7 +117,7 @@ class SettingsLang : PreferenceFragmentCompat() {
return@setOnPreferenceClickListener true return@setOnPreferenceClickListener true
} }
getPref(R.string.locale_key)?.setOnPreferenceClickListener { pref -> getPref(R.string.locale_key)?.setOnPreferenceClickListener {
val tempLangs = languages.toMutableList() val tempLangs = languages.toMutableList()
//if (beneneCount > 100) { //if (beneneCount > 100) {
// tempLangs.add(Triple("\uD83E\uDD8D", "mmmm... monke", "mo")) // 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 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.Bundle
import android.os.Environment import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.hippo.unifile.UniFile
import com.lagradost.cloudstream3.AcraApplication
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError 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.getFolderSize
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref 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.ChromecastSubtitlesFragment
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard 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() { class SettingsPlayer : PreferenceFragmentCompat() {
// Open file picker override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
private val pathPicker = super.onViewCreated(view, savedInstanceState)
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> setUpToolbar(R.string.category_player)
// 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?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
hideKeyboard() hideKeyboard()
setPreferencesFromResource(R.xml.settings_player, rootKey) setPreferencesFromResource(R.xml.settings_player, rootKey)
@ -80,24 +45,6 @@ class SettingsPlayer : PreferenceFragmentCompat() {
} }
return@setOnPreferenceClickListener true 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 { getPref(R.string.prefer_limit_title_key)?.setOnPreferenceClickListener {
val prefNames = resources.getStringArray(R.array.limit_title_pref_names) val prefNames = resources.getStringArray(R.array.limit_title_pref_names)
@ -236,60 +183,6 @@ class SettingsPlayer : PreferenceFragmentCompat() {
return@setOnPreferenceClickListener true 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 package com.lagradost.cloudstream3.ui.settings
import android.os.Bundle import android.os.Bundle
import android.view.View
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref 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.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
class SettingsUI : PreferenceFragmentCompat() { 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?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
hideKeyboard() hideKeyboard()
setPreferencesFromResource(R.xml.settins_ui, rootKey) setPreferencesFromResource(R.xml.settins_ui, rootKey)

View file

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

View file

@ -57,6 +57,8 @@ data class SaveCaptionStyle(
@JsonProperty("elevation") var elevation: Int, @JsonProperty("elevation") var elevation: Int,
/**in sp**/ /**in sp**/
@JsonProperty("fixedTextSize") var fixedTextSize: Float?, @JsonProperty("fixedTextSize") var fixedTextSize: Float?,
@JsonProperty("removeCaptions") var removeCaptions: Boolean = false,
@JsonProperty("removeBloat") var removeBloat: Boolean = true,
) )
const val DEF_SUBS_ELEVATION = 20 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 { _ -> subs_font_size.setOnLongClickListener { _ ->
state.fixedTextSize = null state.fixedTextSize = null
//textView.context.updateState() // font size not changed //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.DeserializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.lagradost.cloudstream3.mvvm.logError
const val DOWNLOAD_HEADER_CACHE = "download_header_cache" 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 DOWNLOAD_EPISODE_CACHE = "download_episode_cache"
const val VIDEO_PLAYER_BRIGHTNESS = "video_player_alpha_key" const val VIDEO_PLAYER_BRIGHTNESS = "video_player_alpha_key"
const val HOMEPAGE_API = "home_api_used" 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 { object DataStore {
val mapper: JsonMapper = JsonMapper.builder().addModule(KotlinModule()) val mapper: JsonMapper = JsonMapper.builder().addModule(KotlinModule())
@ -34,17 +34,21 @@ object DataStore {
} }
fun <T> Context.setKeyRaw(path: String, value: T, isEditingAppSettings: Boolean = false) { fun <T> Context.setKeyRaw(path: String, value: T, isEditingAppSettings: Boolean = false) {
val editor: SharedPreferences.Editor = try {
if (isEditingAppSettings) getDefaultSharedPrefs().edit() else getSharedPrefs().edit() val editor: SharedPreferences.Editor =
when (value) { if (isEditingAppSettings) getDefaultSharedPrefs().edit() else getSharedPrefs().edit()
is Boolean -> editor.putBoolean(path, value) when (value) {
is Int -> editor.putInt(path, value) is Boolean -> editor.putBoolean(path, value)
is String -> editor.putString(path, value) is Int -> editor.putInt(path, value)
is Float -> editor.putFloat(path, value) is String -> editor.putString(path, value)
is Long -> editor.putLong(path, value) is Float -> editor.putFloat(path, value)
(value as? Set<String> != null) -> editor.putStringSet(path, value as Set<String>) 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 { fun Context.getDefaultSharedPrefs(): SharedPreferences {
@ -69,11 +73,15 @@ object DataStore {
} }
fun Context.removeKey(path: String) { fun Context.removeKey(path: String) {
val prefs = getSharedPrefs() try {
if (prefs.contains(path)) { val prefs = getSharedPrefs()
val editor: SharedPreferences.Editor = prefs.edit() if (prefs.contains(path)) {
editor.remove(path) val editor: SharedPreferences.Editor = prefs.edit()
editor.apply() editor.remove(path)
editor.apply()
}
} catch (e: Exception) {
logError(e)
} }
} }
@ -86,9 +94,13 @@ object DataStore {
} }
fun <T> Context.setKey(path: String, value: T) { fun <T> Context.setKey(path: String, value: T) {
val editor: SharedPreferences.Editor = getSharedPrefs().edit() try {
editor.putString(path, mapper.writeValueAsString(value)) val editor: SharedPreferences.Editor = getSharedPrefs().edit()
editor.apply() editor.putString(path, mapper.writeValueAsString(value))
editor.apply()
} catch (e: Exception) {
logError(e)
}
} }
fun <T> Context.setKey(folder: String, path: String, value: T) { 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 if (parentId == null) return
removeKey("$currentAccount/$RESULT_RESUME_WATCHING_OLD", parentId.toString()) removeKey("$currentAccount/$RESULT_RESUME_WATCHING_OLD", parentId.toString())
} }

View file

@ -61,6 +61,7 @@ enum class Qualities(var value: Int) {
0 -> "Auto" 0 -> "Auto"
Unknown.value -> "" Unknown.value -> ""
P2160.value -> "4K" P2160.value -> "4K"
null -> ""
else -> "${qual}p" else -> "${qual}p"
} }
} }
@ -117,6 +118,9 @@ val extractorApis: Array<ExtractorApi> = arrayOf(
VizcloudInfo(), VizcloudInfo(),
MwvnVizcloudInfo(), MwvnVizcloudInfo(),
VizcloudDigital(), VizcloudDigital(),
VizcloudCloud(),
VideoVard(),
VideovardSX(),
Mp4Upload(), Mp4Upload(),
StreamTape(), StreamTape(),
@ -168,6 +172,7 @@ val extractorApis: Array<ExtractorApi> = arrayOf(
DoodSoExtractor(), DoodSoExtractor(),
DoodLaExtractor(), DoodLaExtractor(),
DoodWsExtractor(), DoodWsExtractor(),
DoodShExtractor(),
AsianLoad(), AsianLoad(),
@ -177,6 +182,11 @@ val extractorApis: Array<ExtractorApi> = arrayOf(
ZplayerV2(), ZplayerV2(),
Upstream(), Upstream(),
Maxstream(),
Tantifilm(),
Userload(),
Supervideo(),
GuardareStream(),
// StreamSB.kt works // StreamSB.kt works
// SBPlay(), // SBPlay(),
@ -189,6 +199,7 @@ val extractorApis: Array<ExtractorApi> = arrayOf(
GMPlayer(), GMPlayer(),
Blogger(), Blogger(),
Solidfiles(),
Hxfile(), Hxfile(),
KotakAnimeid(), 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 * @param looseCheck will use .contains in addition to .equals
* */ * */
fun fromLanguageToTwoLetters(input: String, looseCheck: Boolean): String? { fun fromLanguageToTwoLetters(input: String, looseCheck: Boolean): String? {
languages.forEach { languages.forEach {
if (it.languageName.equals(input, ignoreCase = true) if (it.languageName.equals(input, ignoreCase = true)
|| it.nativeName.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" <vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24" android:viewportHeight="24"
app:tint="?attr/white"> android:tint="?attr/white">
<path <path
android:fillColor="@android:color/white" 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: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:fillType="evenOdd" />
</vector> </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"> android:layout_height="wrap_content">
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
android:id="@+id/account_main_profile_picture_holder"
app:cardCornerRadius="100dp" app:cardCornerRadius="100dp"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_width="35dp" android:layout_width="35dp"
android:layout_height="35dp"> android:layout_height="35dp">
<ImageView <ImageView
android:id="@+id/account_profile_picture" android:id="@+id/account_main_profile_picture"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />

View file

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