mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
86aed5b830
158 changed files with 7487 additions and 1874 deletions
.github/workflows
.idea
app
build.gradle
src/main
java/com/lagradost/cloudstream3
CommonActivity.ktMainAPI.ktMainActivity.kt
animeproviders
AnimeWorldProvider.ktAnimefenixProvider.ktAnimeflvIOProvider.ktGogoanimeProvider.ktGomunimeProvider.ktJKAnimeProvider.ktKuronimeProvider.ktMundoDonghuaProvider.ktNeonimeProvider.ktNineAnimeProvider.ktTenshiProvider.kt
extractors
DoodExtractor.ktGuardareStream.ktMaxstream.ktMcloud.ktSolidfiles.ktSupervideo.ktTantifilm.ktUserload.ktVideoVard.ktWcoStream.kt
helper
metaproviders
movieproviders
AltadefinizioneProvider.ktBflixProvider.ktCineblogProvider.ktDoramasYTProvider.ktEgyBestProvider.ktElifilmsProvider.ktEstrenosDoramasProvider.ktHDMovie5.ktHDTodayProvider.ktLayarKaca21Provider.ktLookMovieProvider.ktNginxProvider.ktOlgplyProvider.ktRebahinProvider.ktStreamingcommunityProvider.ktTantiFilmProvider.ktWatchAsianProvider.kt
subtitles
syncproviders
ui
home
player
AbstractPlayerFragment.ktCS3IPlayer.ktCustomSubtitleDecoderFactory.ktFullScreenPlayer.ktGeneratorPlayer.ktIPlayer.ktPlayerSubtitleHelper.ktRepoLinkGenerator.kt
result
search
settings
AccountAdapter.ktSettingsAccount.ktSettingsFragment.ktSettingsGeneral.ktSettingsLang.ktSettingsNginx.ktSettingsPlayer.ktSettingsUI.ktSettingsUpdates.kt
subtitles
utils
res
1
.github/workflows/prerelease.yml
vendored
1
.github/workflows/prerelease.yml
vendored
|
@ -6,6 +6,7 @@ on:
|
|||
paths-ignore:
|
||||
- '*.md'
|
||||
- '*.json'
|
||||
- '**/wcokey.txt'
|
||||
|
||||
concurrency:
|
||||
group: "pre-release"
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
|
|
|
@ -35,8 +35,8 @@ android {
|
|||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
|
||||
versionCode 47
|
||||
versionName "2.10.25"
|
||||
versionCode 48
|
||||
versionName "2.10.28"
|
||||
|
||||
resValue "string", "app_version",
|
||||
"${defaultConfig.versionName}${versionNameSuffix ?: ""}"
|
||||
|
@ -93,12 +93,12 @@ dependencies {
|
|||
testImplementation 'org.json:json:20180813'
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'androidx.core:core-ktx:1.7.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
implementation 'androidx.core:core-ktx:1.8.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.2'
|
||||
implementation 'com.google.android.material:material:1.5.0' // dont change this to 1.6.0 it looks ugly af
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.0-beta01'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.5.0-beta01'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.0-rc01'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.5.0-rc01'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
|
@ -171,4 +171,10 @@ dependencies {
|
|||
implementation 'com.facebook.shimmer:shimmer:0.5.0'
|
||||
|
||||
implementation "androidx.tvprovider:tvprovider:1.0.0"
|
||||
|
||||
// used for subtitle decoding https://github.com/albfernandez/juniversalchardet
|
||||
implementation 'com.github.albfernandez:juniversalchardet:2.4.0'
|
||||
|
||||
// play yt
|
||||
implementation 'com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT'
|
||||
}
|
|
@ -33,7 +33,6 @@ object CommonActivity {
|
|||
var canShowPipMode: Boolean = false
|
||||
var isInPIPMode: Boolean = false
|
||||
|
||||
val backEvent = Event<Boolean>()
|
||||
val onColorSelectedEvent = Event<Pair<Int, Int>>()
|
||||
val onDialogDismissedEvent = Event<Int>()
|
||||
|
||||
|
@ -282,6 +281,10 @@ object CommonActivity {
|
|||
KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_NUMPAD_9 -> {
|
||||
PlayerEventType.ShowMirrors
|
||||
}
|
||||
// OpenSubtitles shortcut
|
||||
KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8 -> {
|
||||
PlayerEventType.SearchSubtitlesOnline
|
||||
}
|
||||
KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3 -> {
|
||||
PlayerEventType.ShowSpeed
|
||||
}
|
||||
|
|
|
@ -14,8 +14,8 @@ import com.lagradost.cloudstream3.animeproviders.*
|
|||
import com.lagradost.cloudstream3.metaproviders.CrossTmdbProvider
|
||||
import com.lagradost.cloudstream3.movieproviders.*
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.aniListApi
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
|
||||
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
|
@ -42,6 +42,8 @@ object APIHolder {
|
|||
val allProviders by lazy {
|
||||
arrayListOf(
|
||||
// Movie providers
|
||||
ElifilmsProvider(),
|
||||
EstrenosDoramasProvider(),
|
||||
PelisplusProvider(),
|
||||
PelisplusHDProvider(),
|
||||
PeliSmartProvider(),
|
||||
|
@ -53,7 +55,6 @@ object APIHolder {
|
|||
PelisflixProvider(),
|
||||
SeriesflixProvider(),
|
||||
IHaveNoTvProvider(), // Documentaries provider
|
||||
LookMovieProvider(), // RECAPTCHA (Please allow up to 5 seconds...)
|
||||
VMoveeProvider(),
|
||||
AllMoviesForYouProvider(),
|
||||
VidEmbedProvider(),
|
||||
|
@ -76,7 +77,7 @@ object APIHolder {
|
|||
TwoEmbedProvider(),
|
||||
DramaSeeProvider(),
|
||||
WatchAsianProvider(),
|
||||
DramaidProvider(),
|
||||
DramaidProvider(),
|
||||
KdramaHoodProvider(),
|
||||
AkwamProvider(),
|
||||
MyCimaProvider(),
|
||||
|
@ -87,9 +88,12 @@ object APIHolder {
|
|||
TheFlixToProvider(),
|
||||
StreamingcommunityProvider(),
|
||||
TantifilmProvider(),
|
||||
CineblogProvider(),
|
||||
AltadefinizioneProvider(),
|
||||
HDMovie5(),
|
||||
RebahinProvider(),
|
||||
LayarKaca21Provider(),
|
||||
LayarKacaProvider(),
|
||||
HDTodayProvider(),
|
||||
|
||||
// Metadata providers
|
||||
//TmdbProvider(),
|
||||
|
@ -104,6 +108,9 @@ object APIHolder {
|
|||
//ShiroProvider(), // v2 fucked me
|
||||
AnimeFlickProvider(),
|
||||
AnimeflvnetProvider(),
|
||||
AnimefenixProvider(),
|
||||
AnimeflvIOProvider(),
|
||||
JKAnimeProvider(),
|
||||
TenshiProvider(),
|
||||
WcoProvider(),
|
||||
AnimePaheProvider(),
|
||||
|
@ -113,19 +120,27 @@ object APIHolder {
|
|||
ZoroProvider(),
|
||||
DubbedAnimeProvider(),
|
||||
MonoschinosProvider(),
|
||||
MundoDonghuaProvider(),
|
||||
KawaiifuProvider(), // disabled due to cloudflare
|
||||
NeonimeProvider(),
|
||||
NeonimeProvider(),
|
||||
KuramanimeProvider(),
|
||||
OploverzProvider(),
|
||||
GomunimeProvider(),
|
||||
NontonAnimeIDProvider(),
|
||||
KuronimeProvider(),
|
||||
//MultiAnimeProvider(),
|
||||
NginxProvider(),
|
||||
NginxProvider(),
|
||||
OlgplyProvider(),
|
||||
)
|
||||
}
|
||||
|
||||
fun initAll() {
|
||||
for (api in allProviders) {
|
||||
api.init()
|
||||
}
|
||||
apiMap = null
|
||||
}
|
||||
|
||||
var apis: List<MainAPI> = arrayListOf()
|
||||
private var apiMap: Map<String, Int>? = null
|
||||
|
||||
|
@ -141,7 +156,6 @@ object APIHolder {
|
|||
fun getApiFromNameNull(apiName: String?): MainAPI? {
|
||||
if (apiName == null) return null
|
||||
initMap()
|
||||
|
||||
return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
|
||||
}
|
||||
|
||||
|
@ -154,12 +168,12 @@ object APIHolder {
|
|||
return null
|
||||
}
|
||||
|
||||
fun getLoadResponseIdFromUrl(url : String, apiName: String) : Int {
|
||||
fun getLoadResponseIdFromUrl(url: String, apiName: String): Int {
|
||||
return url.replace(getApiFromName(apiName).mainUrl, "").replace("/", "").hashCode()
|
||||
}
|
||||
|
||||
fun LoadResponse.getId(): Int {
|
||||
return getLoadResponseIdFromUrl(url,apiName)
|
||||
return getLoadResponseIdFromUrl(url, apiName)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -336,18 +350,19 @@ abstract class MainAPI {
|
|||
var overrideData: HashMap<String, ProvidersInfoJson>? = null
|
||||
}
|
||||
|
||||
fun overrideWithNewData(data: ProvidersInfoJson) {
|
||||
this.name = data.name
|
||||
this.mainUrl = data.url
|
||||
this.storedCredentials = data.credentials
|
||||
}
|
||||
|
||||
init {
|
||||
fun init() {
|
||||
overrideData?.get(this.javaClass.simpleName)?.let { data ->
|
||||
overrideWithNewData(data)
|
||||
}
|
||||
}
|
||||
|
||||
fun overrideWithNewData(data: ProvidersInfoJson) {
|
||||
this.name = data.name
|
||||
if (data.url.isNotBlank() && data.url != "NONE")
|
||||
this.mainUrl = data.url
|
||||
this.storedCredentials = data.credentials
|
||||
}
|
||||
|
||||
open var name = "NONE"
|
||||
open var mainUrl = "NONE"
|
||||
open var storedCredentials: String? = null
|
||||
|
@ -463,12 +478,6 @@ fun base64Encode(array: ByteArray): String {
|
|||
|
||||
class ErrorLoadingException(message: String? = null) : Exception(message)
|
||||
|
||||
fun parseRating(ratingString: String?): Int? {
|
||||
if (ratingString == null) return null
|
||||
val floatRating = ratingString.toFloatOrNull() ?: return null
|
||||
return (floatRating * 10).toInt()
|
||||
}
|
||||
|
||||
fun MainAPI.fixUrlNull(url: String?): String? {
|
||||
if (url.isNullOrEmpty()) {
|
||||
return null
|
||||
|
@ -763,12 +772,12 @@ fun AnimeSearchResponse.addDubStatus(isDub: Boolean, episodes: Int? = null) {
|
|||
}
|
||||
|
||||
fun AnimeSearchResponse.addDub(episodes: Int?) {
|
||||
if(episodes == null || episodes <= 0) return
|
||||
if (episodes == null || episodes <= 0) return
|
||||
addDubStatus(DubStatus.Dubbed, episodes)
|
||||
}
|
||||
|
||||
fun AnimeSearchResponse.addSub(episodes: Int?) {
|
||||
if(episodes == null || episodes <= 0) return
|
||||
if (episodes == null || episodes <= 0) return
|
||||
addDubStatus(DubStatus.Subbed, episodes)
|
||||
}
|
||||
|
||||
|
@ -840,7 +849,7 @@ interface LoadResponse {
|
|||
var posterUrl: String?
|
||||
var year: Int?
|
||||
var plot: String?
|
||||
var rating: Int? // 1-1000
|
||||
var rating: Int? // 0-10000
|
||||
var tags: List<String>?
|
||||
var duration: Int? // in minutes
|
||||
var trailers: List<String>?
|
||||
|
@ -898,6 +907,17 @@ interface LoadResponse {
|
|||
}
|
||||
}
|
||||
|
||||
fun LoadResponse.addTrailer(trailerUrls: List<String>?) {
|
||||
if(trailerUrls == null) return
|
||||
if (this.trailers == null) {
|
||||
this.trailers = trailerUrls
|
||||
} else {
|
||||
val update = this.trailers?.toMutableList()
|
||||
update?.addAll(trailerUrls)
|
||||
this.trailers = update
|
||||
}
|
||||
}
|
||||
|
||||
fun LoadResponse.addImdbId(id: String?) {
|
||||
// TODO add imdb sync
|
||||
}
|
||||
|
@ -919,7 +939,7 @@ interface LoadResponse {
|
|||
}
|
||||
|
||||
fun LoadResponse.addRating(value: Int?) {
|
||||
if (value ?: return < 0 || value > 1000) {
|
||||
if ((value ?: return) < 0 || value > 10000) {
|
||||
return
|
||||
}
|
||||
this.rating = value
|
||||
|
|
|
@ -27,20 +27,20 @@ import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
|
|||
import com.lagradost.cloudstream3.APIHolder.allProviders
|
||||
import com.lagradost.cloudstream3.APIHolder.apis
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
|
||||
import com.lagradost.cloudstream3.CommonActivity.backEvent
|
||||
import com.lagradost.cloudstream3.APIHolder.initAll
|
||||
import com.lagradost.cloudstream3.CommonActivity.loadThemes
|
||||
import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
|
||||
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
|
||||
import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.CommonActivity.updateLocale
|
||||
import com.lagradost.cloudstream3.movieproviders.NginxProvider
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.network.initClient
|
||||
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.OAuth2Apis
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.OAuth2accountApis
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.appString
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths
|
||||
import com.lagradost.cloudstream3.ui.APIRepository
|
||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
|
||||
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
||||
|
@ -59,6 +59,7 @@ import com.lagradost.cloudstream3.utils.DataStore.removeKey
|
|||
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.setViewPos
|
||||
import com.lagradost.cloudstream3.utils.IOnBackPressed
|
||||
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
|
||||
|
@ -131,12 +132,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
R.id.navigation_download_child,
|
||||
R.id.navigation_subtitles,
|
||||
R.id.navigation_chrome_subtitles,
|
||||
R.id.navigation_settings_nginx,
|
||||
R.id.navigation_settings_player,
|
||||
R.id.navigation_settings_updates,
|
||||
R.id.navigation_settings_ui,
|
||||
R.id.navigation_settings_account,
|
||||
R.id.navigation_settings_lang,
|
||||
R.id.navigation_settings_general,
|
||||
).contains(destination.id)
|
||||
|
||||
val landscape = when (resources.configuration.orientation) {
|
||||
|
@ -234,15 +235,23 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
onUserLeaveHint(this)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
private fun backPressed() {
|
||||
this.window?.navigationBarColor =
|
||||
this.colorFromAttribute(R.attr.primaryGrayBackground)
|
||||
this.updateLocale()
|
||||
backEvent.invoke(true)
|
||||
super.onBackPressed()
|
||||
this.updateLocale()
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
((supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment?)?.childFragmentManager?.primaryNavigationFragment as? IOnBackPressed)?.onBackPressed()
|
||||
?.let { runNormal ->
|
||||
if (runNormal) backPressed()
|
||||
} ?: run {
|
||||
backPressed()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (VLC_REQUEST_CODE == requestCode) {
|
||||
if (resultCode == RESULT_OK && data != null) {
|
||||
|
@ -354,12 +363,67 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
}
|
||||
}
|
||||
|
||||
fun test() {
|
||||
//val youtubeLink = "https://www.youtube.com/watch?v=TxB48MEAmZw"
|
||||
|
||||
|
||||
/*
|
||||
runBlocking {
|
||||
|
||||
val query = """
|
||||
query {
|
||||
searchShows(search: "spider", limit: 10) {
|
||||
id
|
||||
name
|
||||
originalName
|
||||
}
|
||||
}
|
||||
"""
|
||||
val data =
|
||||
mapOf(
|
||||
"query" to query,
|
||||
//"variables" to
|
||||
// mapOf(
|
||||
// "name" to name,
|
||||
// ).toJson()
|
||||
)
|
||||
val txt = app.post(
|
||||
"http://api.anime-skip.com/graphql",
|
||||
headers = mapOf(
|
||||
"X-Client-ID" to "",
|
||||
"Content-Type" to "application/json",
|
||||
"Accept" to "application/json",
|
||||
),
|
||||
json = data
|
||||
)
|
||||
println("TEXT: $txt")
|
||||
}*/
|
||||
/*runBlocking {
|
||||
//https://test.api.anime-skip.com/graphiql
|
||||
val txt = app.get(
|
||||
"https://api.anime-skip.com/status",
|
||||
headers = mapOf("X-Client-ID" to "")
|
||||
)
|
||||
println("TEXT: $txt")
|
||||
}*/
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// init accounts
|
||||
for (api in OAuth2accountApis) {
|
||||
for (api in accountManagers) {
|
||||
api.init()
|
||||
}
|
||||
|
||||
ioSafe {
|
||||
inAppAuths.apmap { api ->
|
||||
try {
|
||||
api.initialize()
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SearchResultBuilder.updateCache(this)
|
||||
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
@ -379,68 +443,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
false
|
||||
}
|
||||
|
||||
fun addNginxToJson(data: java.util.HashMap<String, ProvidersInfoJson>): java.util.HashMap<String, ProvidersInfoJson>? {
|
||||
try {
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val nginxUrl =
|
||||
settingsManager.getString(getString(R.string.nginx_url_key), "nginx_url_key")
|
||||
.toString()
|
||||
val nginxCredentials =
|
||||
settingsManager.getString(
|
||||
getString(R.string.nginx_credentials),
|
||||
"nginx_credentials"
|
||||
)
|
||||
.toString()
|
||||
val StoredNginxProvider = NginxProvider()
|
||||
if (nginxUrl == "nginx_url_key" || nginxUrl == "") { // if key is default value, or empty:
|
||||
data[StoredNginxProvider.javaClass.simpleName] = ProvidersInfoJson(
|
||||
url = nginxUrl,
|
||||
name = StoredNginxProvider.name,
|
||||
status = PROVIDER_STATUS_DOWN, // the provider will not be display
|
||||
credentials = nginxCredentials
|
||||
)
|
||||
} else { // valid url
|
||||
data[StoredNginxProvider.javaClass.simpleName] = ProvidersInfoJson(
|
||||
url = nginxUrl,
|
||||
name = StoredNginxProvider.name,
|
||||
status = PROVIDER_STATUS_OK,
|
||||
credentials = nginxCredentials
|
||||
)
|
||||
}
|
||||
|
||||
return data
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
fun createNginxJson(): ProvidersInfoJson? { //java.util.HashMap<String, ProvidersInfoJson>
|
||||
return try {
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val nginxUrl =
|
||||
settingsManager.getString(getString(R.string.nginx_url_key), "nginx_url_key")
|
||||
.toString()
|
||||
val nginxCredentials = settingsManager.getString(
|
||||
getString(R.string.nginx_credentials),
|
||||
"nginx_credentials"
|
||||
).toString()
|
||||
if (nginxUrl == "nginx_url_key" || nginxUrl == "") { // if key is default value or empty:
|
||||
null // don't overwrite anything
|
||||
} else {
|
||||
ProvidersInfoJson(
|
||||
url = nginxUrl,
|
||||
name = NginxProvider().name,
|
||||
status = PROVIDER_STATUS_OK,
|
||||
credentials = nginxCredentials
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// this pulls the latest data so ppl don't have to update to simply change provider url
|
||||
if (downloadFromGithub) {
|
||||
try {
|
||||
|
@ -460,11 +462,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
setKey(PROVIDER_STATUS_KEY, txt)
|
||||
MainAPI.overrideData = newCache // update all new providers
|
||||
|
||||
val newUpdatedCache =
|
||||
newCache?.let { addNginxToJson(it) ?: it }
|
||||
|
||||
initAll()
|
||||
for (api in apis) { // update current providers
|
||||
newUpdatedCache?.get(api.javaClass.simpleName)
|
||||
newCache?.get(api.javaClass.simpleName)
|
||||
?.let { data ->
|
||||
api.overrideWithNewData(data)
|
||||
}
|
||||
|
@ -482,14 +482,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
newCache
|
||||
}?.let { providersJsonMap ->
|
||||
MainAPI.overrideData = providersJsonMap
|
||||
val providersJsonMapUpdated = addNginxToJson(providersJsonMap)
|
||||
?: providersJsonMap // if return null, use unchanged one
|
||||
initAll()
|
||||
val acceptableProviders =
|
||||
providersJsonMapUpdated.filter { it.value.status == PROVIDER_STATUS_OK || it.value.status == PROVIDER_STATUS_SLOW }
|
||||
providersJsonMap.filter { it.value.status == PROVIDER_STATUS_OK || it.value.status == PROVIDER_STATUS_SLOW }
|
||||
.map { it.key }.toSet()
|
||||
|
||||
val restrictedApis =
|
||||
if (hasBenene) providersJsonMapUpdated.filter { it.value.status == PROVIDER_STATUS_BETA_ONLY }
|
||||
if (hasBenene) providersJsonMap.filter { it.value.status == PROVIDER_STATUS_BETA_ONLY }
|
||||
.map { it.key }.toSet() else emptySet()
|
||||
|
||||
apis = allProviders.filter { api ->
|
||||
|
@ -506,23 +505,14 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
initAll()
|
||||
apis = allProviders
|
||||
e.printStackTrace()
|
||||
logError(e)
|
||||
}
|
||||
} else {
|
||||
initAll()
|
||||
apis = allProviders
|
||||
try {
|
||||
val nginxProviderName = NginxProvider().name
|
||||
val nginxProviderIndex = apis.indexOf(APIHolder.getApiFromName(nginxProviderName))
|
||||
val createdJsonProvider = createNginxJson()
|
||||
if (createdJsonProvider != null) {
|
||||
apis[nginxProviderIndex].overrideWithNewData(createdJsonProvider) // people will have access to it if they disable metadata check (they are not filtered)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
loadThemes(this)
|
||||
|
@ -585,7 +575,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
}
|
||||
|
||||
loadCache()
|
||||
|
||||
test()
|
||||
/*nav_view.setOnNavigationItemSelectedListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.navigation_home -> {
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.animeproviders
|
|||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addDuration
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addRating
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||
|
@ -119,16 +120,6 @@ class AnimeWorldProvider : MainAPI() {
|
|||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
fun String.parseDuration(): Int? {
|
||||
val arr = this.split(" e ")
|
||||
return if (arr.size == 1)
|
||||
arr[0].split(' ')[0].toIntOrNull()
|
||||
else
|
||||
arr[1].split(' ')[0].toIntOrNull()?.let {
|
||||
arr[0].removeSuffix("h").toIntOrNull()?.times(60)!!.plus(it)
|
||||
}
|
||||
}
|
||||
|
||||
val document = request(url).document
|
||||
|
||||
val widget = document.select("div.widget.info")
|
||||
|
@ -140,7 +131,7 @@ class AnimeWorldProvider : MainAPI() {
|
|||
|
||||
val type: TvType = getType(widget.select("dd").first()?.text())
|
||||
val genres = widget.select(".meta").select("a[href*=\"/genre/\"]").map { it.text() }
|
||||
val rating = widget.select("#average-vote")?.text()
|
||||
val rating = widget.select("#average-vote").text()
|
||||
|
||||
val trailerUrl = document.select(".trailer[data-url]").attr("data-url")
|
||||
val malId = document.select("#mal-button").attr("href")
|
||||
|
@ -151,7 +142,7 @@ class AnimeWorldProvider : MainAPI() {
|
|||
var dub = false
|
||||
var year: Int? = null
|
||||
var status: ShowStatus? = null
|
||||
var duration: Int? = null
|
||||
var duration: String? = null
|
||||
|
||||
for (meta in document.select(".meta dt, .meta dd")) {
|
||||
val text = meta.text()
|
||||
|
@ -162,7 +153,7 @@ class AnimeWorldProvider : MainAPI() {
|
|||
else if (status == null && text.contains("Stato"))
|
||||
status = getStatus(meta.nextElementSibling()?.text())
|
||||
else if (status == null && text.contains("Durata"))
|
||||
duration = meta.nextElementSibling()?.text()?.parseDuration()
|
||||
duration = meta.nextElementSibling()?.text()
|
||||
}
|
||||
|
||||
val servers = document.select(".widget.servers")
|
||||
|
@ -183,7 +174,7 @@ class AnimeWorldProvider : MainAPI() {
|
|||
return newAnimeLoadResponse(title, url, type) {
|
||||
engName = title
|
||||
japName = otherTitle
|
||||
posterUrl = poster
|
||||
addPoster(poster)
|
||||
this.year = year
|
||||
addEpisodes(if (dub) DubStatus.Dubbed else DubStatus.Subbed, episodes)
|
||||
showStatus = status
|
||||
|
@ -192,7 +183,7 @@ class AnimeWorldProvider : MainAPI() {
|
|||
addMalId(malId)
|
||||
addAniListId(anlId)
|
||||
addRating(rating)
|
||||
this.duration = duration
|
||||
addDuration(duration)
|
||||
addTrailer(trailerUrl)
|
||||
this.recommendations = recommendations
|
||||
this.comingSoon = comingSoon
|
||||
|
|
|
@ -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=.*&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=.*&code(.*)&")
|
||||
val test = sourcesRegex.findAll(script).toList()
|
||||
test.apmap {
|
||||
val codestream = it.value
|
||||
val links = when {
|
||||
codestream.contains("player=2&") -> "https://embedsito.com/v/"+cleanStreamID(codestream)
|
||||
codestream.contains("player=3&") -> "https://www.mp4upload.com/embed-"+cleanStreamID(codestream)+".html"
|
||||
codestream.contains("player=6&") -> "https://www.yourupload.com/embed/"+cleanStreamID(codestream)
|
||||
codestream.contains("player=12&") -> "http://ok.ru/videoembed/"+cleanStreamID(codestream)
|
||||
codestream.contains("player=4&") -> "https://sendvid.com/"+cleanStreamID(codestream)
|
||||
codestream.contains("player=9&") -> "AmaNormal https://www.animefenix.com/stream/amz.php?v="+cleanStreamID(codestream)
|
||||
codestream.contains("player=11&") -> "AmazonES https://www.animefenix.com/stream/amz.php?v="+cleanStreamID(codestream)
|
||||
codestream.contains("player=22&") -> "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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -183,7 +183,7 @@ class GogoanimeProvider : MainAPI() {
|
|||
}
|
||||
}
|
||||
|
||||
override var mainUrl = "https://gogoanime.film"
|
||||
override var mainUrl = "https://gogoanime.sk"
|
||||
override var name = "GogoAnime"
|
||||
override val hasQuickSearch = false
|
||||
override val hasMainPage = true
|
||||
|
|
|
@ -3,20 +3,11 @@ package com.lagradost.cloudstream3.animeproviders
|
|||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Element
|
||||
import java.util.*
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.lagradost.cloudstream3.movieproviders.SflixProvider
|
||||
import com.lagradost.cloudstream3.movieproviders.SflixProvider.Companion.extractRabbitStream
|
||||
import com.lagradost.cloudstream3.movieproviders.SflixProvider.Companion.toExtractorLink
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.nicehttp.Requests.Companion.await
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.Interceptor
|
||||
import java.net.URI
|
||||
|
||||
class GomunimeProvider : MainAPI() {
|
||||
override var mainUrl = "https://185.231.223.76"
|
||||
|
@ -210,7 +201,8 @@ class GomunimeProvider : MainAPI() {
|
|||
M3u8Helper.generateM3u8(
|
||||
this.name,
|
||||
link,
|
||||
mainUrl,
|
||||
"$mainUrl/",
|
||||
headers = mapOf("Origin" to mainUrl)
|
||||
).forEach(callback)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package com.lagradost.cloudstream3.animeproviders
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||
|
@ -157,6 +156,8 @@ class KuronimeProvider : MainAPI() {
|
|||
val token = data.substringAfter("var token = \"").substringBefore("\";")
|
||||
val pat = data.substringAfter("var pat = \"").substringBefore("\";")
|
||||
val link = "$doma$token$pat/index.m3u8"
|
||||
val quality =
|
||||
Regex("\\d{3,4}p").find(doc.select("title").text())?.groupValues?.get(0)
|
||||
|
||||
sourceCallback.invoke(
|
||||
ExtractorLink(
|
||||
|
@ -164,7 +165,8 @@ class KuronimeProvider : MainAPI() {
|
|||
this.name,
|
||||
link,
|
||||
referer = "https://animeku.org/",
|
||||
quality = Qualities.Unknown.value,
|
||||
quality = getQualityFromName(quality),
|
||||
headers = mapOf("Origin" to "https://animeku.org"),
|
||||
isM3u8 = true
|
||||
)
|
||||
)
|
||||
|
@ -186,7 +188,7 @@ class KuronimeProvider : MainAPI() {
|
|||
sources.apmap {
|
||||
safeApiCall {
|
||||
when {
|
||||
it.contains("animeku.org") -> invokeKuroSource(it, callback)
|
||||
it.startsWith("https://animeku.org") -> invokeKuroSource(it, callback)
|
||||
else -> loadExtractor(it, mainUrl, callback)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1,8 +1,6 @@
|
|||
package com.lagradost.cloudstream3.animeproviders
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||
import org.jsoup.nodes.Element
|
||||
|
@ -33,6 +31,7 @@ class NeonimeProvider : MainAPI() {
|
|||
return when (t) {
|
||||
"Ended" -> ShowStatus.Completed
|
||||
"OnGoing" -> ShowStatus.Ongoing
|
||||
"Ongoing" -> ShowStatus.Ongoing
|
||||
"In Production" -> ShowStatus.Ongoing
|
||||
"Returning Series" -> ShowStatus.Ongoing
|
||||
else -> ShowStatus.Completed
|
||||
|
|
|
@ -64,7 +64,7 @@ class NineAnimeProvider : MainAPI() {
|
|||
}
|
||||
|
||||
//Credits to https://github.com/jmir1
|
||||
private val key = "0wMrYU+ixjJ4QdzgfN2HlyIVAt3sBOZnCT9Lm7uFDovkb/EaKpRWhqXS5168ePcG"
|
||||
private val key = "c/aUAorINHBLxWTy3uRiPt8J+vjsOheFG1E0q2X9CYwDZlnmd4Kb5M6gSVzfk7pQ" //key credits to @Modder4869
|
||||
|
||||
private fun getVrf(id: String): String? {
|
||||
val reversed = ue(encode(id) + "0000000").slice(0..5).reversed()
|
||||
|
@ -283,7 +283,8 @@ class NineAnimeProvider : MainAPI() {
|
|||
jsonservers.vidstream,
|
||||
jsonservers.mcloud,
|
||||
jsonservers.mp4upload,
|
||||
jsonservers.streamtape
|
||||
jsonservers.streamtape,
|
||||
jsonservers.videovard
|
||||
).mapNotNull {
|
||||
try {
|
||||
val epserver = app.get("$mainUrl/ajax/anime/episode?id=$it").text
|
||||
|
|
|
@ -47,7 +47,6 @@ class TenshiProvider : MainAPI() {
|
|||
override suspend fun getMainPage(): HomePageResponse {
|
||||
val items = ArrayList<HomePageList>()
|
||||
val soup = app.get(mainUrl, interceptor = ddosGuardKiller).document
|
||||
println(soup)
|
||||
for (section in soup.select("#content > section")) {
|
||||
try {
|
||||
if (section.attr("id") == "toplist-tabs") {
|
||||
|
|
|
@ -4,12 +4,17 @@ import com.lagradost.cloudstream3.app
|
|||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
class DoodCxExtractor : DoodLaExtractor() {
|
||||
override var mainUrl = "https://dood.cx"
|
||||
}
|
||||
|
||||
class DoodShExtractor : DoodLaExtractor() {
|
||||
override var mainUrl = "https://dood.sh"
|
||||
}
|
||||
|
||||
class DoodPmExtractor : DoodLaExtractor() {
|
||||
override var mainUrl = "https://dood.pm"
|
||||
}
|
||||
|
@ -40,13 +45,14 @@ open class DoodLaExtractor : ExtractorApi() {
|
|||
val response0 = app.get(url).text // html of DoodStream page to look for /pass_md5/...
|
||||
val md5 =mainUrl+(Regex("/pass_md5/[^']*").find(response0)?.value ?: return null) // get https://dood.ws/pass_md5/...
|
||||
val trueUrl = app.get(md5, referer = url).text + "zUEJeL3mUN?token=" + md5.substringAfterLast("/") //direct link to extract (zUEJeL3mUN is random)
|
||||
val quality = Regex("\\d{3,4}p").find(response0.substringAfter("<title>").substringBefore("</title>"))?.groupValues?.get(0)
|
||||
return listOf(
|
||||
ExtractorLink(
|
||||
trueUrl,
|
||||
this.name,
|
||||
trueUrl,
|
||||
mainUrl,
|
||||
Qualities.Unknown.value,
|
||||
getQualityFromName(quality),
|
||||
false
|
||||
)
|
||||
) // links are valid in 8h
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -4,10 +4,15 @@ import com.fasterxml.jackson.annotation.JsonProperty
|
|||
import com.lagradost.cloudstream3.USER_AGENT
|
||||
import com.lagradost.cloudstream3.apmap
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.extractors.WcoStream.Companion.cipher
|
||||
import com.lagradost.cloudstream3.extractors.WcoStream.Companion.encrypt
|
||||
import com.lagradost.cloudstream3.extractors.WcoStream.Companion.keytwo
|
||||
import com.lagradost.cloudstream3.extractors.helper.WcoHelper.Companion.getNewWcoKey
|
||||
import com.lagradost.cloudstream3.extractors.helper.WcoHelper.Companion.getWcoKey
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
|
||||
|
||||
open class Mcloud : ExtractorApi() {
|
||||
override var name = "Mcloud"
|
||||
|
@ -27,42 +32,49 @@ open class Mcloud : ExtractorApi() {
|
|||
"Referer" to "https://animekisa.in/", //Referer works for wco and animekisa, probably with others too
|
||||
"Pragma" to "no-cache",
|
||||
"Cache-Control" to "no-cache",)
|
||||
private val key = "LCbu3iYC7ln24K7P" // key credits @Modder4869
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||
val link = url.replace("$mainUrl/e/","$mainUrl/info/")
|
||||
val response = app.get(link, headers = headers).text
|
||||
|
||||
val id = url.substringAfter("e/").substringAfter("embed/").substringBefore("?")
|
||||
val keys = getNewWcoKey()
|
||||
keytwo = keys?.encryptKey ?: return null
|
||||
val encryptedid = encrypt(cipher(keys.cipherkey!!, encrypt(id))).replace("/", "_").replace("=","")
|
||||
val link = "$mainUrl/mediainfo/$encryptedid?key=${keys.mainKey}"
|
||||
val response = app.get(link, referer = "https://animekisa.in/").text
|
||||
if(response.startsWith("<!DOCTYPE html>")) {
|
||||
// TODO decrypt html for link
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
data class Sources (
|
||||
@JsonProperty("file") val file: String
|
||||
data class SourcesMcloud (
|
||||
@JsonProperty("file" ) val file : String
|
||||
)
|
||||
|
||||
data class Media (
|
||||
@JsonProperty("sources") val sources: List<Sources>
|
||||
data class MediaMcloud (
|
||||
@JsonProperty("sources" ) val sources : ArrayList<SourcesMcloud> = arrayListOf()
|
||||
)
|
||||
|
||||
data class DataMcloud (
|
||||
@JsonProperty("media" ) val media : MediaMcloud? = MediaMcloud()
|
||||
)
|
||||
|
||||
data class JsonMcloud (
|
||||
@JsonProperty("success") val success: Boolean,
|
||||
@JsonProperty("media") val media: Media,
|
||||
@JsonProperty("status" ) val status : Int? = null,
|
||||
@JsonProperty("data" ) val data : DataMcloud = DataMcloud()
|
||||
)
|
||||
|
||||
val mapped = parseJson<JsonMcloud>(response)
|
||||
val sources = mutableListOf<ExtractorLink>()
|
||||
|
||||
if (mapped.success)
|
||||
mapped.media.sources.apmap {
|
||||
val checkfile = mapped.status == 200
|
||||
if (checkfile)
|
||||
mapped.data.media?.sources?.apmap {
|
||||
if (it.file.contains("m3u8")) {
|
||||
M3u8Helper.generateM3u8(
|
||||
name,
|
||||
it.file,
|
||||
url,
|
||||
headers = app.get(url).headers.toMap()
|
||||
).forEach { link ->
|
||||
sources.add(link)
|
||||
}
|
||||
sources.addAll(
|
||||
generateM3u8(
|
||||
name,
|
||||
it.file,
|
||||
url,
|
||||
headers = mapOf("Referer" to url)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
return sources
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -3,6 +3,8 @@ package com.lagradost.cloudstream3.extractors
|
|||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.apmap
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.extractors.helper.WcoHelper.Companion.getNewWcoKey
|
||||
import com.lagradost.cloudstream3.extractors.helper.WcoHelper.Companion.getWcoKey
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
|
||||
|
@ -45,43 +47,103 @@ class VizcloudDigital : WcoStream() {
|
|||
override var mainUrl = "https://vizcloud.digital"
|
||||
}
|
||||
|
||||
class VizcloudCloud : WcoStream() {
|
||||
override var mainUrl = "https://vizcloud.cloud"
|
||||
}
|
||||
|
||||
open class WcoStream : ExtractorApi() {
|
||||
override var name = "VidStream" // Cause works for animekisa and wco
|
||||
override var mainUrl = "https://vidstream.pro"
|
||||
override val requiresReferer = false
|
||||
|
||||
companion object {
|
||||
var keytwo = ""
|
||||
fun encrypt(input: String): String {
|
||||
if (input.any { it.code >= 256 }) throw Exception("illegal characters!")
|
||||
var output = ""
|
||||
for (i in input.indices step 3) {
|
||||
val a = intArrayOf(-1, -1, -1, -1)
|
||||
a[0] = input[i].code shr 2
|
||||
a[1] = (3 and input[i].code) shl 4
|
||||
if (input.length > i + 1) {
|
||||
a[1] = a[1] or (input[i + 1].code shr 4)
|
||||
a[2] = (15 and input[i + 1].code) shl 2
|
||||
}
|
||||
if (input.length > i + 2) {
|
||||
a[2] = a[2] or (input[i + 2].code shr 6)
|
||||
a[3] = 63 and input[i + 2].code
|
||||
}
|
||||
for (n in a) {
|
||||
if (n == -1) output += "="
|
||||
else {
|
||||
if (n in 0..63) output += keytwo[n]
|
||||
}
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
fun cipher(inputOne: String, inputTwo: String): String {
|
||||
val arr = IntArray(256) { it }
|
||||
var output = ""
|
||||
var u = 0
|
||||
var r: Int
|
||||
for (a in arr.indices) {
|
||||
u = (u + arr[a] + inputOne[a % inputOne.length].code) % 256
|
||||
r = arr[a]
|
||||
arr[a] = arr[u]
|
||||
arr[u] = r
|
||||
}
|
||||
u = 0
|
||||
var c = 0
|
||||
for (f in inputTwo.indices) {
|
||||
c = (c + f) % 256
|
||||
u = (u + arr[c]) % 256
|
||||
r = arr[c]
|
||||
arr[c] = arr[u]
|
||||
arr[u] = r
|
||||
output += (inputTwo[f].code xor arr[(arr[c] + arr[u]) % 256]).toChar()
|
||||
}
|
||||
return output
|
||||
}
|
||||
}
|
||||
private val key = "LCbu3iYC7ln24K7P" // key credits @Modder4869
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||
val baseUrl = url.split("/e/")[0]
|
||||
|
||||
val html = app.get(url, headers = mapOf("Referer" to "https://wcostream.cc/")).text
|
||||
val (Id) = (Regex("/e/(.*?)?domain").find(url)?.destructured ?: Regex("""/e/(.*)""").find(
|
||||
url
|
||||
)?.destructured) ?: return emptyList()
|
||||
val (skey) = Regex("""skey\s=\s['"](.*?)['"];""").find(html)?.destructured
|
||||
?: return emptyList()
|
||||
|
||||
val apiLink = "$baseUrl/info/$Id?domain=wcostream.cc&skey=$skey"
|
||||
// val (skey) = Regex("""skey\s=\s['"](.*?)['"];""").find(html)?.destructured
|
||||
// ?: return emptyList()
|
||||
val keys = getNewWcoKey()
|
||||
keytwo = keys?.encryptKey ?: return emptyList()
|
||||
val encryptedID = encrypt(cipher(keys.cipherkey!!, encrypt(Id))).replace("/", "_").replace("=","")
|
||||
val apiLink = "$baseUrl/mediainfo/$encryptedID?key=${keys.mainKey}"
|
||||
val referrer = "$baseUrl/e/$Id?domain=wcostream.cc"
|
||||
|
||||
data class Sources(
|
||||
@JsonProperty("file") val file: String,
|
||||
@JsonProperty("label") val label: String?
|
||||
data class SourcesWco (
|
||||
@JsonProperty("file" ) val file : String
|
||||
)
|
||||
|
||||
data class Media(
|
||||
@JsonProperty("sources") val sources: List<Sources>
|
||||
data class MediaWco (
|
||||
@JsonProperty("sources" ) val sources : ArrayList<SourcesWco> = arrayListOf()
|
||||
)
|
||||
|
||||
data class WcoResponse(
|
||||
@JsonProperty("success") val success: Boolean,
|
||||
@JsonProperty("media") val media: Media
|
||||
data class DataWco (
|
||||
@JsonProperty("media" ) val media : MediaWco? = MediaWco()
|
||||
)
|
||||
|
||||
data class WcoResponse (
|
||||
@JsonProperty("status" ) val status : Int? = null,
|
||||
@JsonProperty("data" ) val data : DataWco? = DataWco()
|
||||
)
|
||||
|
||||
val mapped = app.get(apiLink, headers = mapOf("Referer" to referrer)).parsed<WcoResponse>()
|
||||
val sources = mutableListOf<ExtractorLink>()
|
||||
|
||||
if (mapped.success) {
|
||||
mapped.media.sources.forEach {
|
||||
val check = mapped.status == 200
|
||||
if (check) {
|
||||
mapped.data?.media?.sources?.forEach {
|
||||
if (mainUrl == "https://vizcloud2.ru" || mainUrl == "https://vizcloud.online") {
|
||||
if (it.file.contains("vizcloud2.ru") || it.file.contains("vizcloud.online")) {
|
||||
// Had to do this thing 'cause "list.m3u8#.mp4" gives 404 error so no quality is added
|
||||
|
@ -128,7 +190,8 @@ open class WcoStream : ExtractorApi() {
|
|||
"https://vizcloud.live",
|
||||
"https://vizcloud.info",
|
||||
"https://mwvn.vizcloud.info",
|
||||
"https://vizcloud.digital"
|
||||
"https://vizcloud.digital",
|
||||
"https://vizcloud.cloud"
|
||||
).contains(mainUrl)
|
||||
) {
|
||||
if (it.file.contains("m3u8")) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
package com.lagradost.cloudstream3.metaproviders
|
||||
|
||||
import com.lagradost.cloudstream3.ErrorLoadingException
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.aniListApi
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
|
||||
import com.lagradost.cloudstream3.utils.SyncUtil
|
||||
|
||||
object SyncRedirector {
|
||||
|
|
|
@ -3,7 +3,7 @@ package com.lagradost.cloudstream3.metaproviders
|
|||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.MALApi
|
||||
|
@ -15,7 +15,7 @@ class MultiAnimeProvider : MainAPI() {
|
|||
override val lang = "en"
|
||||
override val usesWebView = true
|
||||
override val supportedTypes = setOf(TvType.Anime)
|
||||
private val syncApi: SyncAPI = OAuth2API.aniListApi
|
||||
private val syncApi: SyncAPI = aniListApi
|
||||
|
||||
private val syncUtilType by lazy {
|
||||
when (syncApi) {
|
||||
|
|
|
@ -4,10 +4,12 @@ import com.fasterxml.jackson.annotation.JsonProperty
|
|||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addActors
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
import com.uwetrottmann.tmdb2.Tmdb
|
||||
import com.uwetrottmann.tmdb2.entities.*
|
||||
import com.uwetrottmann.tmdb2.enumerations.AppendToResponseItem
|
||||
import com.uwetrottmann.tmdb2.enumerations.VideoType
|
||||
import retrofit2.awaitResponse
|
||||
import java.util.*
|
||||
|
||||
|
@ -24,6 +26,8 @@ data class TmdbLink(
|
|||
)
|
||||
|
||||
open class TmdbProvider : MainAPI() {
|
||||
// This should always be false, but might as well make it easier for forks
|
||||
open val includeAdult = false
|
||||
|
||||
// Use the LoadResponse from the metadata provider
|
||||
open val useMetaLoadResponse = false
|
||||
|
@ -142,6 +146,7 @@ open class TmdbProvider : MainAPI() {
|
|||
tags = genres?.mapNotNull { it.name }
|
||||
duration = episode_run_time?.average()?.toInt()
|
||||
rating = this@toLoadResponse.rating
|
||||
addTrailer(videos.toTrailers())
|
||||
|
||||
recommendations = (this@toLoadResponse.recommendations
|
||||
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
|
||||
|
@ -149,6 +154,19 @@ open class TmdbProvider : MainAPI() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun Videos?.toTrailers(): List<String>? {
|
||||
return this?.results?.filter { it.type != VideoType.OPENING_CREDITS && it.type != VideoType.FEATURETTE }
|
||||
?.sortedBy { it.type?.ordinal ?: 10000 }
|
||||
?.mapNotNull {
|
||||
when (it.site?.trim()?.lowercase()) {
|
||||
"youtube" -> { // TODO FILL SITES
|
||||
"https://www.youtube.com/watch?v=${it.key}"
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Movie.toLoadResponse(): MovieLoadResponse {
|
||||
return newMovieLoadResponse(
|
||||
this.title ?: this.original_title, getUrl(id, false), TvType.Movie, TmdbLink(
|
||||
|
@ -170,6 +188,7 @@ open class TmdbProvider : MainAPI() {
|
|||
tags = genres?.mapNotNull { it.name }
|
||||
duration = runtime
|
||||
rating = this@toLoadResponse.rating
|
||||
addTrailer(videos.toTrailers())
|
||||
|
||||
recommendations = (this@toLoadResponse.recommendations
|
||||
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
|
||||
|
@ -259,7 +278,16 @@ open class TmdbProvider : MainAPI() {
|
|||
|
||||
return if (useMetaLoadResponse) {
|
||||
return if (isTvSeries) {
|
||||
val body = tmdb.tvService().tv(id, "en-US", AppendToResponse(AppendToResponseItem.EXTERNAL_IDS)).awaitResponse().body()
|
||||
val body = tmdb.tvService()
|
||||
.tv(
|
||||
id,
|
||||
"en-US",
|
||||
AppendToResponse(
|
||||
AppendToResponseItem.EXTERNAL_IDS,
|
||||
AppendToResponseItem.VIDEOS
|
||||
)
|
||||
)
|
||||
.awaitResponse().body()
|
||||
val response = body?.toLoadResponse()
|
||||
if (response != null) {
|
||||
if (response.recommendations.isNullOrEmpty())
|
||||
|
@ -278,7 +306,16 @@ open class TmdbProvider : MainAPI() {
|
|||
|
||||
response
|
||||
} else {
|
||||
val body = tmdb.moviesService().summary(id, "en-US", AppendToResponse(AppendToResponseItem.EXTERNAL_IDS)).awaitResponse().body()
|
||||
val body = tmdb.moviesService()
|
||||
.summary(
|
||||
id,
|
||||
"en-US",
|
||||
AppendToResponse(
|
||||
AppendToResponseItem.EXTERNAL_IDS,
|
||||
AppendToResponseItem.VIDEOS
|
||||
)
|
||||
)
|
||||
.awaitResponse().body()
|
||||
val response = body?.toLoadResponse()
|
||||
if (response != null) {
|
||||
if (response.recommendations.isNullOrEmpty())
|
||||
|
@ -319,7 +356,7 @@ open class TmdbProvider : MainAPI() {
|
|||
}
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse>? {
|
||||
return tmdb.searchService().multi(query, 1, "en-Us", "US", true).awaitResponse()
|
||||
return tmdb.searchService().multi(query, 1, "en-Us", "US", includeAdult).awaitResponse()
|
||||
.body()?.results?.mapNotNull {
|
||||
it.movie?.toSearchResponse() ?: it.tvShow?.toSearchResponse()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -57,7 +57,7 @@ open class BflixProvider : MainAPI() {
|
|||
}
|
||||
|
||||
//Credits to https://github.com/jmir1
|
||||
val key = "eST4kCjadnvlAm5b1BOGyLJzrE90Q6oKgRfhV+M8NDYtcxW3IP/qp2i7XHuwZFUs"
|
||||
private val key = "5uLKesbh0nkrpPq9VwMC6+tQBdomjJ4HNl/fWOSiREvAYagT8yIG7zx2D13UZFXc" //key credits to @Modder4869
|
||||
|
||||
private fun getVrf(id: String): String? {
|
||||
val reversed = ue(encode(id) + "0000000").slice(0..5).reversed()
|
||||
|
@ -354,7 +354,8 @@ open class BflixProvider : MainAPI() {
|
|||
jsonservers.vidstream,
|
||||
jsonservers.mcloud,
|
||||
jsonservers.mp4upload,
|
||||
jsonservers.streamtape
|
||||
jsonservers.streamtape,
|
||||
jsonservers.videovard,
|
||||
).mapNotNull {
|
||||
val epserver = app.get("$mainUrl/ajax/episode/info?id=$it").text
|
||||
(if (epserver.contains("url")) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -28,8 +28,7 @@ class DoramasYTProvider : MainAPI() {
|
|||
override val hasChromecastSupport = true
|
||||
override val hasDownloadSupport = true
|
||||
override val supportedTypes = setOf(
|
||||
TvType.TvSeries,
|
||||
TvType.Movie,
|
||||
TvType.AsianDrama,
|
||||
)
|
||||
|
||||
override suspend fun getMainPage(): HomePageResponse {
|
||||
|
|
|
@ -161,7 +161,6 @@ class EgyBestProvider : MainAPI() {
|
|||
@JsonProperty("link") val link: String
|
||||
)
|
||||
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -5,11 +5,10 @@ import com.lagradost.cloudstream3.*
|
|||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.httpsify
|
||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||
import okhttp3.Interceptor
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
class HDMovie5 : MainAPI() {
|
||||
override var mainUrl = "https://hdmovie5.tv"
|
||||
override var mainUrl = "https://hdmovie5.mba"
|
||||
override var name = "HDMovie"
|
||||
override val lang = "hi"
|
||||
|
||||
|
@ -34,6 +33,7 @@ class HDMovie5 : MainAPI() {
|
|||
MovieSearchResponse(
|
||||
a.text(),
|
||||
a.attr("href"),
|
||||
|
||||
this.name,
|
||||
TvType.Movie,
|
||||
it.select("img").attr("src"),
|
||||
|
@ -137,7 +137,8 @@ class HDMovie5 : MainAPI() {
|
|||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
return data.split(",").apmapIndexed { index, it ->
|
||||
val html = app.post(
|
||||
//println("loadLinks:::: $index $it")
|
||||
val p = app.post(
|
||||
"$mainUrl/wp-admin/admin-ajax.php",
|
||||
data = mapOf(
|
||||
"action" to "doo_player_ajax",
|
||||
|
@ -145,10 +146,12 @@ class HDMovie5 : MainAPI() {
|
|||
"nume" to "${index + 1}",
|
||||
"type" to "movie"
|
||||
)
|
||||
).parsed<PlayerAjaxResponse>().embedURL ?: return@apmapIndexed false
|
||||
)
|
||||
// println("TEXT::::: ${p.text}")
|
||||
val html = p.parsedSafe<PlayerAjaxResponse>()?.embedURL ?: return@apmapIndexed false
|
||||
val doc = Jsoup.parse(html)
|
||||
val link = doc.select("iframe").attr("src")
|
||||
loadExtractor(httpsify(link), "$mainUrl/",callback)
|
||||
loadExtractor(httpsify(link), "$mainUrl/", callback)
|
||||
}.contains(true)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package com.lagradost.cloudstream3.movieproviders
|
||||
|
||||
class HDTodayProvider : SflixProvider() {
|
||||
override var mainUrl = "https://hdtoday.cc"
|
||||
override var name = "HDToday"
|
||||
}
|
|
@ -1,23 +1,22 @@
|
|||
package com.lagradost.cloudstream3.movieproviders
|
||||
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.httpsify
|
||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Element
|
||||
import java.util.*
|
||||
|
||||
class LayarKaca21Provider : MainAPI() {
|
||||
override var mainUrl = "https://149.56.24.226/"
|
||||
override var name = "LayarKaca21"
|
||||
class LayarKacaProvider : MainAPI() {
|
||||
override var mainUrl = "https://149.56.24.226"
|
||||
override var name = "LayarKaca"
|
||||
override val hasMainPage = true
|
||||
override val lang = "id"
|
||||
override val hasDownloadSupport = true
|
||||
override val supportedTypes = setOf(
|
||||
TvType.Movie,
|
||||
TvType.TvSeries,
|
||||
TvType.AsianDrama
|
||||
)
|
||||
|
||||
override suspend fun getMainPage(): HomePageResponse {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +1,8 @@
|
|||
package com.lagradost.cloudstream3.movieproviders
|
||||
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addDuration
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addRating
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.TvType
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import java.lang.Exception
|
||||
|
||||
class NginxProvider : MainAPI() {
|
||||
override var name = "Nginx"
|
||||
|
@ -15,23 +10,40 @@ class NginxProvider : MainAPI() {
|
|||
override val hasMainPage = true
|
||||
override val supportedTypes = setOf(TvType.AnimeMovie, TvType.TvSeries, TvType.Movie)
|
||||
|
||||
companion object {
|
||||
var loginCredentials: String? = null
|
||||
var overrideUrl: String? = null
|
||||
const val ERROR_STRING = "No nginx url specified in the settings"
|
||||
}
|
||||
|
||||
|
||||
fun getAuthHeader(storedCredentials: String?): Map<String, String> {
|
||||
if (storedCredentials == null) {
|
||||
return mapOf(Pair("Authorization", "Basic ")) // no Authorization headers
|
||||
private fun getAuthHeader(): Map<String, String> {
|
||||
val url = overrideUrl ?: throw ErrorLoadingException(ERROR_STRING)
|
||||
mainUrl = url
|
||||
println("OVERRIDING URL TO $overrideUrl")
|
||||
if (mainUrl == "NONE" || mainUrl.isBlank()) {
|
||||
throw ErrorLoadingException(ERROR_STRING)
|
||||
}
|
||||
val basicAuthToken = base64Encode(storedCredentials.toByteArray()) // will this be loaded when not using the provider ??? can increase load
|
||||
return mapOf(Pair("Authorization", "Basic $basicAuthToken"))
|
||||
|
||||
val localCredentials = loginCredentials
|
||||
if (localCredentials == null || localCredentials.trim() == ":") {
|
||||
return mapOf("Authorization" to "Basic ") // no Authorization headers
|
||||
}
|
||||
|
||||
val basicAuthToken =
|
||||
base64Encode(localCredentials.toByteArray()) // will this be loaded when not using the provider ??? can increase load
|
||||
|
||||
return mapOf("Authorization" to "Basic $basicAuthToken")
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
val authHeader = getAuthHeader(storedCredentials) // call again because it isn't reloaded if in main class and storedCredentials loads after
|
||||
val authHeader =
|
||||
getAuthHeader() // call again because it isn't reloaded if in main class and storedCredentials loads after
|
||||
// url can be tvshow.nfo for series or mediaRootUrl for movies
|
||||
|
||||
val mediaRootDocument = app.get(url, authHeader).document
|
||||
val mainRootDocument = app.get(url, authHeader).document
|
||||
|
||||
val nfoUrl = url + mediaRootDocument.getElementsByAttributeValueContaining("href", ".nfo").attr("href") // metadata url file
|
||||
val nfoUrl = url + mainRootDocument.getElementsByAttributeValueContaining("href", ".nfo")
|
||||
.attr("href") // metadata url file
|
||||
|
||||
val metadataDocument = app.get(nfoUrl, authHeader).document // get the metadata nfo file
|
||||
|
||||
|
@ -44,27 +56,34 @@ class NginxProvider : MainAPI() {
|
|||
if (isMovie) {
|
||||
val poster = metadataDocument.selectFirst("thumb")!!.text()
|
||||
val trailer = metadataDocument.select("trailer").mapNotNull {
|
||||
it?.text()?.replace(
|
||||
"plugin://plugin.video.youtube/play/?video_id=",
|
||||
"https://www.youtube.com/watch?v="
|
||||
)
|
||||
it?.text()?.replace(
|
||||
"plugin://plugin.video.youtube/play/?video_id=",
|
||||
"https://www.youtube.com/watch?v="
|
||||
)
|
||||
}
|
||||
val partialUrl = mediaRootDocument.getElementsByAttributeValueContaining("href", ".nfo").attr("href").replace(".nfo", ".")
|
||||
val partialUrl =
|
||||
mainRootDocument.getElementsByAttributeValueContaining("href", ".nfo").attr("href")
|
||||
.replace(".nfo", ".")
|
||||
val date = metadataDocument.selectFirst("year")?.text()?.toIntOrNull()
|
||||
val ratingAverage = metadataDocument.selectFirst("value")?.text()?.toIntOrNull()
|
||||
val tagsList = metadataDocument.select("genre")
|
||||
?.mapNotNull { // all the tags like action, thriller ...
|
||||
.mapNotNull { // all the tags like action, thriller ...
|
||||
it?.text()
|
||||
|
||||
}
|
||||
|
||||
|
||||
val dataList = mediaRootDocument.getElementsByAttributeValueContaining( // list of all urls of the webpage
|
||||
"href",
|
||||
partialUrl
|
||||
)
|
||||
val dataList =
|
||||
mainRootDocument.getElementsByAttributeValueContaining( // list of all urls of the webpage
|
||||
"href",
|
||||
partialUrl
|
||||
)
|
||||
|
||||
val data = url + dataList.firstNotNullOf { item -> item.takeIf { (!it.attr("href").contains(".nfo") && !it.attr("href").contains(".jpg"))} }.attr("href").toString() // exclude poster and nfo (metadata) file
|
||||
val data = url + dataList.firstNotNullOf { item ->
|
||||
item.takeIf {
|
||||
(!it.attr("href").contains(".nfo") && !it.attr("href").contains(".jpg"))
|
||||
}
|
||||
}.attr("href").toString() // exclude poster and nfo (metadata) file
|
||||
|
||||
return newMovieLoadResponse(
|
||||
title,
|
||||
|
@ -81,7 +100,6 @@ class NginxProvider : MainAPI() {
|
|||
}
|
||||
} else // a tv serie
|
||||
{
|
||||
|
||||
val list = ArrayList<Pair<Int, String>>()
|
||||
val mediaRootUrl = url.replace("tvshow.nfo", "")
|
||||
val posterUrl = mediaRootUrl + "poster.jpg"
|
||||
|
@ -91,7 +109,7 @@ class NginxProvider : MainAPI() {
|
|||
|
||||
|
||||
val tagsList = metadataDocument.select("genre")
|
||||
?.mapNotNull { // all the tags like action, thriller ...; unused variable
|
||||
.mapNotNull { // all the tags like action, thriller ...; unused variable
|
||||
it?.text()
|
||||
}
|
||||
|
||||
|
@ -102,7 +120,7 @@ class NginxProvider : MainAPI() {
|
|||
|
||||
seasons.forEach { element ->
|
||||
val season =
|
||||
element.attr("href")?.replace("Season%20", "")?.replace("/", "")?.toIntOrNull()
|
||||
element.attr("href").replace("Season%20", "").replace("/", "").toIntOrNull()
|
||||
val href = mediaRootUrl + element.attr("href")
|
||||
if (season != null && season > 0 && href.isNotBlank()) {
|
||||
list.add(Pair(season, href))
|
||||
|
@ -120,33 +138,40 @@ class NginxProvider : MainAPI() {
|
|||
"href",
|
||||
".nfo"
|
||||
) // get metadata
|
||||
episodes.forEach { episode ->
|
||||
val nfoDocument = app.get(seasonString + episode.attr("href"), authHeader).document // get episode metadata file
|
||||
val epNum = nfoDocument.selectFirst("episode")?.text()?.toIntOrNull()
|
||||
val poster =
|
||||
seasonString + episode.attr("href").replace(".nfo", "-thumb.jpg")
|
||||
val name = nfoDocument.selectFirst("title")!!.text()
|
||||
// val seasonInt = nfoDocument.selectFirst("season").text().toIntOrNull()
|
||||
val date = nfoDocument.selectFirst("aired")?.text()
|
||||
val plot = nfoDocument.selectFirst("plot")?.text()
|
||||
episodes.forEach { episode ->
|
||||
val nfoDocument = app.get(
|
||||
seasonString + episode.attr("href"),
|
||||
authHeader
|
||||
).document // get episode metadata file
|
||||
val epNum = nfoDocument.selectFirst("episode")?.text()?.toIntOrNull()
|
||||
val poster =
|
||||
seasonString + episode.attr("href").replace(".nfo", "-thumb.jpg")
|
||||
val name = nfoDocument.selectFirst("title")!!.text()
|
||||
// val seasonInt = nfoDocument.selectFirst("season").text().toIntOrNull()
|
||||
val date = nfoDocument.selectFirst("aired")?.text()
|
||||
val plot = nfoDocument.selectFirst("plot")?.text()
|
||||
|
||||
val dataList = seasonDocument.getElementsByAttributeValueContaining(
|
||||
"href",
|
||||
episode.attr("href").replace(".nfo", "")
|
||||
)
|
||||
val data = seasonString + dataList.firstNotNullOf { item -> item.takeIf { (!it.attr("href").contains(".nfo") && !it.attr("href").contains(".jpg"))} }.attr("href").toString() // exclude poster and nfo (metadata) file
|
||||
val dataList = seasonDocument.getElementsByAttributeValueContaining(
|
||||
"href",
|
||||
episode.attr("href").replace(".nfo", "")
|
||||
)
|
||||
val data = seasonString + dataList.firstNotNullOf { item ->
|
||||
item.takeIf {
|
||||
(!it.attr("href").contains(".nfo") && !it.attr("href").contains(".jpg"))
|
||||
}
|
||||
}.attr("href").toString() // exclude poster and nfo (metadata) file
|
||||
|
||||
episodeList.add(
|
||||
newEpisode(data) {
|
||||
this.name = name
|
||||
this.season = seasonInt
|
||||
this.episode = epNum
|
||||
this.posterUrl = poster // will require headers too
|
||||
this.description = plot
|
||||
addDate(date)
|
||||
}
|
||||
)
|
||||
}
|
||||
episodeList.add(
|
||||
newEpisode(data) {
|
||||
this.name = name
|
||||
this.season = seasonInt
|
||||
this.episode = epNum
|
||||
this.posterUrl = poster // will require headers too
|
||||
this.description = plot
|
||||
addDate(date)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
return newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodeList) {
|
||||
this.name = title
|
||||
|
@ -168,8 +193,9 @@ class NginxProvider : MainAPI() {
|
|||
callback: (ExtractorLink) -> Unit
|
||||
): Boolean {
|
||||
// loadExtractor(data, null) { callback(it.copy(headers=authHeader)) }
|
||||
val authHeader = getAuthHeader(storedCredentials) // call again because it isn't reloaded if in main class and storedCredentials loads after
|
||||
callback.invoke (
|
||||
val authHeader =
|
||||
getAuthHeader() // call again because it isn't reloaded if in main class and storedCredentials loads after
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
|
@ -185,19 +211,23 @@ class NginxProvider : MainAPI() {
|
|||
}
|
||||
|
||||
|
||||
|
||||
override suspend fun getMainPage(): HomePageResponse {
|
||||
val authHeader = getAuthHeader(storedCredentials) // call again because it isn't reloaded if in main class and storedCredentials loads after
|
||||
if (mainUrl == "NONE"){
|
||||
throw ErrorLoadingException("No nginx url specified in the settings: Nginx Settigns > Nginx server url, try again in a few seconds")
|
||||
}
|
||||
val authHeader =
|
||||
getAuthHeader() // call again because it isn't reloaded if in main class and storedCredentials loads after
|
||||
|
||||
val document = app.get(mainUrl, authHeader).document
|
||||
val categories = document.select("a")
|
||||
val returnList = categories.mapNotNull {
|
||||
val categoryPath = mainUrl + it.attr("href") ?: return@mapNotNull null // get the url of the category; like http://192.168.1.10/media/Movies/
|
||||
val categoryTitle = it.text() // get the category title like Movies or Series
|
||||
if (categoryTitle != "../" && categoryTitle != "Music/") { // exclude parent dir and Music dir
|
||||
val categoryDocument = app.get(categoryPath, authHeader).document // queries the page http://192.168.1.10/media/Movies/
|
||||
val href = it?.attr("href")
|
||||
val categoryPath = fixUrlNull(href?.trim())
|
||||
?: return@mapNotNull null // get the url of the category; like http://192.168.1.10/media/Movies/
|
||||
|
||||
val categoryDocument = app.get(
|
||||
categoryPath,
|
||||
authHeader
|
||||
).document // queries the page http://192.168.1.10/media/Movies/
|
||||
val contentLinks = categoryDocument.select("a")
|
||||
val currentList = contentLinks.mapNotNull { head ->
|
||||
if (head.attr("href") != "../") {
|
||||
|
@ -215,7 +245,6 @@ class NginxProvider : MainAPI() {
|
|||
val nfoContent =
|
||||
app.get(nfoPath, authHeader).document // all the metadata
|
||||
|
||||
|
||||
if (isMovieType) {
|
||||
val movieName = nfoContent.select("title").text()
|
||||
val posterUrl = mediaRootUrl + "poster.jpg"
|
||||
|
@ -238,15 +267,11 @@ class NginxProvider : MainAPI() {
|
|||
) {
|
||||
addPoster(posterUrl, authHeader)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
} catch (e: Exception) { // can cause issues invisible errors
|
||||
null
|
||||
//logError(e) // not working because it changes the return type of currentList to Any
|
||||
}
|
||||
|
||||
|
||||
} else null
|
||||
}
|
||||
if (currentList.isNotEmpty() && categoryTitle != "../") { // exclude upper dir
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.lagradost.cloudstream3.movieproviders
|
||||
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.TvType
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.metaproviders.TmdbLink
|
||||
import com.lagradost.cloudstream3.metaproviders.TmdbProvider
|
||||
|
@ -14,6 +15,7 @@ class OlgplyProvider : TmdbProvider() {
|
|||
override var name = "Olgply"
|
||||
override val instantLinkLoading = true
|
||||
override val useMetaLoadResponse = true
|
||||
override val supportedTypes = setOf(TvType.TvSeries, TvType.Movie)
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
|
|
|
@ -20,6 +20,8 @@ class RebahinProvider : MainAPI() {
|
|||
override val supportedTypes = setOf(
|
||||
TvType.Movie,
|
||||
TvType.TvSeries,
|
||||
TvType.Anime,
|
||||
TvType.AsianDrama
|
||||
)
|
||||
|
||||
override suspend fun getMainPage(): HomePageResponse {
|
||||
|
@ -168,6 +170,7 @@ class RebahinProvider : MainAPI() {
|
|||
private suspend fun invokeLokalSource(
|
||||
url: String,
|
||||
name: String,
|
||||
ref: String,
|
||||
subCallback: (SubtitleFile) -> Unit,
|
||||
sourceCallback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
|
@ -182,11 +185,21 @@ class RebahinProvider : MainAPI() {
|
|||
if (script.data().contains("sources: [")) {
|
||||
val source = tryParseJson<ResponseLocal>(
|
||||
script.data().substringAfter("sources: [").substringBefore("],"))
|
||||
M3u8Helper.generateM3u8(
|
||||
name,
|
||||
source!!.file,
|
||||
"http://172.96.161.72",
|
||||
).forEach(sourceCallback)
|
||||
val m3uData = app.get(source!!.file, referer = ref).text
|
||||
val quality = Regex("\\d{3,4}\\.m3u8").findAll(m3uData).map { it.value }.toList()
|
||||
|
||||
quality.forEach {
|
||||
sourceCallback.invoke(
|
||||
ExtractorLink(
|
||||
source = name,
|
||||
name = name,
|
||||
url = source.file.replace("video.m3u8", it),
|
||||
referer = ref,
|
||||
quality = getQualityFromName("${it.replace(".m3u8", "")}p"),
|
||||
isM3u8 = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val trackJson = script.data().substringAfter("tracks: [").substringBefore("],")
|
||||
val track = tryParseJson<List<Tracks>>("[$trackJson]")
|
||||
|
@ -291,6 +304,7 @@ class RebahinProvider : MainAPI() {
|
|||
it.startsWith("http://172.96.161.72") -> invokeLokalSource(
|
||||
it,
|
||||
this.name,
|
||||
"http://172.96.161.72/",
|
||||
subtitleCallback,
|
||||
callback
|
||||
)
|
||||
|
|
|
@ -52,24 +52,26 @@ data class Image(
|
|||
@JsonProperty("url") val url: String,
|
||||
@JsonProperty("type") val type: String,
|
||||
@JsonProperty("sc_url") val scURL: String,
|
||||
@JsonProperty("proxy") val proxy: Proxy,
|
||||
@JsonProperty("server") val server: Proxy
|
||||
// @JsonProperty("proxy") val proxy: Proxy,
|
||||
// @JsonProperty("server") val server: Proxy
|
||||
)
|
||||
|
||||
data class Proxy(
|
||||
@JsonProperty("id") val id: Long,
|
||||
@JsonProperty("type") val type: String,
|
||||
@JsonProperty("ip") val ip: String,
|
||||
@JsonProperty("number") val number: Long,
|
||||
@JsonProperty("storage") val storage: Long,
|
||||
@JsonProperty("max_storage") val maxStorage: Long,
|
||||
@JsonProperty("max_conversions") val maxConversions: Any? = null,
|
||||
@JsonProperty("max_publications") val maxPublications: Any? = null,
|
||||
@JsonProperty("created_at") val createdAt: String,
|
||||
@JsonProperty("updated_at") val updatedAt: String,
|
||||
@JsonProperty("upload_bandwidth") val uploadBandwidth: Any? = null,
|
||||
@JsonProperty("upload_bandwidth_limit") val uploadBandwidthLimit: Any? = null
|
||||
)
|
||||
// Proxy is not used and crashes otherwise
|
||||
|
||||
//data class Proxy(
|
||||
// @JsonProperty("id") val id: Long,
|
||||
// @JsonProperty("type") val type: String,
|
||||
// @JsonProperty("ip") val ip: String,
|
||||
// @JsonProperty("number") val number: Long,
|
||||
// @JsonProperty("storage") val storage: Long,
|
||||
// @JsonProperty("max_storage") val maxStorage: Long,
|
||||
// @JsonProperty("max_conversions") val maxConversions: Any? = null,
|
||||
// @JsonProperty("max_publications") val maxPublications: Any? = null,
|
||||
// @JsonProperty("created_at") val createdAt: String,
|
||||
// @JsonProperty("updated_at") val updatedAt: String,
|
||||
// @JsonProperty("upload_bandwidth") val uploadBandwidth: Any? = null,
|
||||
// @JsonProperty("upload_bandwidth_limit") val uploadBandwidthLimit: Any? = null
|
||||
//)
|
||||
|
||||
data class Season(
|
||||
@JsonProperty("id") val id: Long,
|
||||
|
@ -126,7 +128,7 @@ data class TrailerElement(
|
|||
|
||||
class StreamingcommunityProvider : MainAPI() {
|
||||
override val lang = "it"
|
||||
override var mainUrl = "https://streamingcommunity.top"
|
||||
override var mainUrl = "https://streamingcommunity.press"
|
||||
override var name = "Streamingcommunity"
|
||||
override val hasMainPage = true
|
||||
override val hasChromecastSupport = true
|
||||
|
|
|
@ -56,7 +56,7 @@ class TantifilmProvider : MainAPI() {
|
|||
return doc.select("div.film.film-2").map {
|
||||
val href = it.selectFirst("a")!!.attr("href")
|
||||
val poster = it.selectFirst("img")!!.attr("src")
|
||||
val name = it.selectFirst("a")!!.text().substringBefore("(")
|
||||
val name = it.selectFirst("a > p")!!.text().substringBeforeLast("(")
|
||||
MovieSearchResponse(
|
||||
name,
|
||||
href,
|
||||
|
@ -95,7 +95,7 @@ class TantifilmProvider : MainAPI() {
|
|||
val recomm = document.select("div.mediaWrap.mediaWrapAlt.recomended_videos").map {
|
||||
val href = it.selectFirst("a")!!.attr("href")
|
||||
val poster = it.selectFirst("img")!!.attr("src")
|
||||
val name = it.selectFirst("a")!!.attr("title").substringBeforeLast("(")
|
||||
val name = it.selectFirst("a > p")!!.text().substringBeforeLast("(")
|
||||
MovieSearchResponse(
|
||||
name,
|
||||
href,
|
||||
|
|
|
@ -10,7 +10,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
|
|||
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||
|
||||
class WatchAsianProvider : MainAPI() {
|
||||
override var mainUrl = "https://watchasian.sh"
|
||||
override var mainUrl = "https://watchasian.cx"
|
||||
override var name = "WatchAsian"
|
||||
override val hasQuickSearch = false
|
||||
override val hasMainPage = true
|
||||
|
@ -244,4 +244,4 @@ class WatchAsianProvider : MainAPI() {
|
|||
fixUrlNull(it?.attr("data-video")) ?: return@mapNotNull null
|
||||
}?.toJson() ?: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -3,9 +3,77 @@ package com.lagradost.cloudstream3.syncproviders
|
|||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.MALApi
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.NginxApi
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.OpenSubtitlesApi
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
abstract class AccountManager(private val defIndex: Int) : AuthAPI {
|
||||
companion object {
|
||||
val malApi = MALApi(0)
|
||||
val aniListApi = AniListApi(0)
|
||||
val openSubtitlesApi = OpenSubtitlesApi(0)
|
||||
val nginxApi = NginxApi(0)
|
||||
|
||||
// used to login via app intent
|
||||
val OAuth2Apis
|
||||
get() = listOf<OAuth2API>(
|
||||
malApi, aniListApi
|
||||
)
|
||||
|
||||
// this needs init with context and can be accessed in settings
|
||||
val accountManagers
|
||||
get() = listOf(
|
||||
malApi, aniListApi, openSubtitlesApi, nginxApi
|
||||
)
|
||||
|
||||
// used for active syncing
|
||||
val SyncApis
|
||||
get() = listOf(
|
||||
SyncRepo(malApi), SyncRepo(aniListApi)
|
||||
)
|
||||
|
||||
val inAppAuths
|
||||
get() = listOf(openSubtitlesApi, nginxApi)
|
||||
|
||||
val subtitleProviders
|
||||
get() = listOf(
|
||||
openSubtitlesApi
|
||||
)
|
||||
|
||||
const val appString = "cloudstreamapp"
|
||||
|
||||
val unixTime: Long
|
||||
get() = System.currentTimeMillis() / 1000L
|
||||
val unixTimeMs: Long
|
||||
get() = System.currentTimeMillis()
|
||||
|
||||
const val maxStale = 60 * 10
|
||||
|
||||
fun secondsToReadable(seconds: Int, completedValue: String): String {
|
||||
var secondsLong = seconds.toLong()
|
||||
val days = TimeUnit.SECONDS
|
||||
.toDays(secondsLong)
|
||||
secondsLong -= TimeUnit.DAYS.toSeconds(days)
|
||||
|
||||
val hours = TimeUnit.SECONDS
|
||||
.toHours(secondsLong)
|
||||
secondsLong -= TimeUnit.HOURS.toSeconds(hours)
|
||||
|
||||
val minutes = TimeUnit.SECONDS
|
||||
.toMinutes(secondsLong)
|
||||
secondsLong -= TimeUnit.MINUTES.toSeconds(minutes)
|
||||
if (minutes < 0) {
|
||||
return completedValue
|
||||
}
|
||||
//println("$days $hours $minutes")
|
||||
return "${if (days != 0L) "$days" + "d " else ""}${if (hours != 0L) "$hours" + "h " else ""}${minutes}m"
|
||||
}
|
||||
}
|
||||
|
||||
abstract class AccountManager(private val defIndex: Int) : OAuth2API {
|
||||
var accountIndex = defIndex
|
||||
private var lastAccountIndex = defIndex
|
||||
protected val accountId get() = "${idPrefix}_account_$accountIndex"
|
||||
private val accountActiveKey get() = "${idPrefix}_active"
|
||||
|
||||
|
@ -35,8 +103,12 @@ abstract class AccountManager(private val defIndex: Int) : OAuth2API {
|
|||
|
||||
protected fun switchToNewAccount() {
|
||||
val accounts = getAccounts()
|
||||
lastAccountIndex = accountIndex
|
||||
accountIndex = (accounts?.maxOrNull() ?: 0) + 1
|
||||
}
|
||||
protected fun switchToOldAccount() {
|
||||
accountIndex = lastAccountIndex
|
||||
}
|
||||
|
||||
protected fun registerAccount() {
|
||||
setKey(accountActiveKey, accountIndex)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -1,77 +1,9 @@
|
|||
package com.lagradost.cloudstream3.syncproviders
|
||||
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.MALApi
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
interface OAuth2API {
|
||||
interface OAuth2API : AuthAPI {
|
||||
val key: String
|
||||
val name: String
|
||||
val redirectUrl: String
|
||||
|
||||
// don't change this as all keys depend on it
|
||||
val idPrefix: String
|
||||
|
||||
suspend fun handleRedirect(url: String) : Boolean
|
||||
fun authenticate()
|
||||
|
||||
fun loginInfo(): LoginInfo?
|
||||
fun logOut()
|
||||
|
||||
class LoginInfo(
|
||||
val profilePicture: String?,
|
||||
val name: String?,
|
||||
|
||||
val accountIndex: Int,
|
||||
)
|
||||
|
||||
companion object {
|
||||
val malApi = MALApi(0)
|
||||
val aniListApi = AniListApi(0)
|
||||
|
||||
// used to login via app intent
|
||||
val OAuth2Apis
|
||||
get() = listOf<OAuth2API>(
|
||||
malApi, aniListApi
|
||||
)
|
||||
|
||||
// this needs init with context and can be accessed in settings
|
||||
val OAuth2accountApis
|
||||
get() = listOf<AccountManager>(
|
||||
malApi, aniListApi
|
||||
)
|
||||
|
||||
// used for active syncing
|
||||
val SyncApis
|
||||
get() = listOf(
|
||||
SyncRepo(malApi), SyncRepo(aniListApi)
|
||||
)
|
||||
|
||||
const val appString = "cloudstreamapp"
|
||||
|
||||
val unixTime: Long
|
||||
get() = System.currentTimeMillis() / 1000L
|
||||
|
||||
const val maxStale = 60 * 10
|
||||
|
||||
fun secondsToReadable(seconds: Int, completedValue: String): String {
|
||||
var secondsLong = seconds.toLong()
|
||||
val days = TimeUnit.SECONDS
|
||||
.toDays(secondsLong)
|
||||
secondsLong -= TimeUnit.DAYS.toSeconds(days)
|
||||
|
||||
val hours = TimeUnit.SECONDS
|
||||
.toHours(secondsLong)
|
||||
secondsLong -= TimeUnit.HOURS.toSeconds(hours)
|
||||
|
||||
val minutes = TimeUnit.SECONDS
|
||||
.toMinutes(secondsLong)
|
||||
secondsLong -= TimeUnit.MINUTES.toSeconds(minutes)
|
||||
if (minutes < 0) {
|
||||
return completedValue
|
||||
}
|
||||
//println("$days $hours $minutes")
|
||||
return "${if (days != 0L) "$days" + "d " else ""}${if (hours != 0L) "$hours" + "h " else ""}${minutes}m"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.syncproviders
|
|||
import com.lagradost.cloudstream3.*
|
||||
|
||||
interface SyncAPI : OAuth2API {
|
||||
val icon: Int
|
||||
val mainUrl: String
|
||||
|
||||
/**
|
||||
|
|
|
@ -12,10 +12,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
|
|||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.appString
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.maxStale
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.unixTime
|
||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
|
@ -32,11 +29,13 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
override val idPrefix = "anilist"
|
||||
override var mainUrl = "https://anilist.co"
|
||||
override val icon = R.drawable.ic_anilist_icon
|
||||
override val requiresLogin = true
|
||||
override val createAccountUrl = "$mainUrl/signup"
|
||||
|
||||
override fun loginInfo(): OAuth2API.LoginInfo? {
|
||||
override fun loginInfo(): AuthAPI.LoginInfo? {
|
||||
// context.getUser(true)?.
|
||||
getKey<AniListUser>(accountId, ANILIST_USER_KEY)?.let { user ->
|
||||
return OAuth2API.LoginInfo(
|
||||
return AuthAPI.LoginInfo(
|
||||
profilePicture = user.picture,
|
||||
name = user.name,
|
||||
accountIndex = accountIndex
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.lagradost.cloudstream3.syncproviders.providers
|
||||
|
||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API
|
||||
|
||||
//TODO dropbox sync
|
||||
|
@ -8,6 +9,11 @@ class Dropbox : OAuth2API {
|
|||
override var name = "Dropbox"
|
||||
override val key = "zlqsamadlwydvb2"
|
||||
override val redirectUrl = "dropboxlogin"
|
||||
override val requiresLogin = true
|
||||
override val createAccountUrl: String? = null
|
||||
|
||||
override val icon: Int
|
||||
get() = TODO("Not yet implemented")
|
||||
|
||||
override fun authenticate() {
|
||||
TODO("Not yet implemented")
|
||||
|
@ -21,7 +27,7 @@ class Dropbox : OAuth2API {
|
|||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun loginInfo(): OAuth2API.LoginInfo? {
|
||||
override fun loginInfo(): AuthAPI.LoginInfo? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
|
@ -14,10 +14,7 @@ import com.lagradost.cloudstream3.ShowStatus
|
|||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.appString
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.secondsToReadable
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.unixTime
|
||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
|
||||
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
|
||||
|
@ -37,15 +34,18 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
override val idPrefix = "mal"
|
||||
override var mainUrl = "https://myanimelist.net"
|
||||
override val icon = R.drawable.mal_logo
|
||||
override val requiresLogin = true
|
||||
|
||||
override val createAccountUrl = "$mainUrl/register.php"
|
||||
|
||||
override fun logOut() {
|
||||
removeAccountKeys()
|
||||
}
|
||||
|
||||
override fun loginInfo(): OAuth2API.LoginInfo? {
|
||||
override fun loginInfo(): AuthAPI.LoginInfo? {
|
||||
//getMalUser(true)?
|
||||
getKey<MalUser>(accountId, MAL_USER_KEY)?.let { user ->
|
||||
return OAuth2API.LoginInfo(
|
||||
return AuthAPI.LoginInfo(
|
||||
profilePicture = user.picture,
|
||||
name = user.name,
|
||||
accountIndex = accountIndex
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -33,7 +33,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
|||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
|
||||
import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi
|
||||
import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi
|
||||
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
|
||||
|
@ -227,7 +227,7 @@ class HomeFragment : Fragment() {
|
|||
listView?.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||
|
||||
listView?.setOnItemClickListener { _, _, i, _ ->
|
||||
if (!currentValidApis.isNullOrEmpty()) {
|
||||
if (currentValidApis.isNotEmpty()) {
|
||||
currentApiName = currentValidApis[i].name
|
||||
//to switch to apply simply remove this
|
||||
currentApiName?.let(callback)
|
||||
|
@ -882,7 +882,7 @@ class HomeFragment : Fragment() {
|
|||
home_change_api_loading?.isVisible = false
|
||||
}
|
||||
|
||||
for (syncApi in OAuth2API.OAuth2Apis) {
|
||||
for (syncApi in OAuth2Apis) {
|
||||
val login = syncApi.loginInfo()
|
||||
val pic = login?.profilePicture
|
||||
if (home_profile_picture?.setImage(
|
||||
|
|
|
@ -68,6 +68,8 @@ abstract class AbstractPlayerFragment(
|
|||
var subStyle: SaveCaptionStyle? = null
|
||||
var subView: SubtitleView? = null
|
||||
var isBuffering = true
|
||||
protected open var hasPipModeSupport = true
|
||||
|
||||
|
||||
@LayoutRes
|
||||
protected var layout: Int = R.layout.fragment_player
|
||||
|
@ -154,7 +156,7 @@ abstract class AbstractPlayerFragment(
|
|||
}
|
||||
}
|
||||
|
||||
canEnterPipMode = isPlayingRightNow
|
||||
canEnterPipMode = isPlayingRightNow && hasPipModeSupport
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isInPIPMode) {
|
||||
activity?.let { act ->
|
||||
PlayerPipHelper.updatePIPModeActions(act, isPlayingRightNow)
|
||||
|
@ -213,7 +215,13 @@ abstract class AbstractPlayerFragment(
|
|||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
private fun playerError(exception: Exception) {
|
||||
private fun requestAudioFocus() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
activity?.requestLocalAudioFocus(AppUtils.getFocusRequest())
|
||||
}
|
||||
}
|
||||
|
||||
open fun playerError(exception: Exception) {
|
||||
val ctx = context ?: return
|
||||
when (exception) {
|
||||
is PlaybackException -> {
|
||||
|
@ -267,12 +275,6 @@ abstract class AbstractPlayerFragment(
|
|||
}
|
||||
}
|
||||
|
||||
private fun requestAudioFocus() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
activity?.requestLocalAudioFocus(AppUtils.getFocusRequest())
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSubStyleChanged(style: SaveCaptionStyle) {
|
||||
if (player is CS3IPlayer) {
|
||||
player.updateSubtitleStyle(style)
|
||||
|
@ -394,6 +396,7 @@ abstract class AbstractPlayerFragment(
|
|||
override fun onDestroy() {
|
||||
playerEventListener = null
|
||||
keyEventListener = null
|
||||
canEnterPipMode = false
|
||||
SubtitlesFragment.applyStyleEvent -= ::onSubStyleChanged
|
||||
|
||||
keepScreenOn(false)
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
package com.lagradost.cloudstream3.ui.player
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.util.SparseArray
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.util.forEach
|
||||
import at.huber.youtubeExtractor.VideoMeta
|
||||
import at.huber.youtubeExtractor.YouTubeExtractor
|
||||
import at.huber.youtubeExtractor.YtFile
|
||||
import com.google.android.exoplayer2.*
|
||||
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider
|
||||
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource
|
||||
|
@ -23,6 +29,7 @@ import com.google.android.exoplayer2.upstream.cache.CacheDataSource
|
|||
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
|
||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache
|
||||
import com.google.android.exoplayer2.util.MimeTypes
|
||||
import com.google.android.exoplayer2.video.VideoSize
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiFromName
|
||||
import com.lagradost.cloudstream3.USER_AGENT
|
||||
import com.lagradost.cloudstream3.app
|
||||
|
@ -31,6 +38,7 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
|||
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.ExtractorUri
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
|
||||
import java.io.File
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
|
@ -153,7 +161,8 @@ class CS3IPlayer : IPlayer {
|
|||
data: ExtractorUri?,
|
||||
startPosition: Long?,
|
||||
subtitles: Set<SubtitleData>,
|
||||
subtitle: SubtitleData?
|
||||
subtitle: SubtitleData?,
|
||||
autoPlay: Boolean?
|
||||
) {
|
||||
Log.i(TAG, "loadPlayer")
|
||||
if (sameEpisode) {
|
||||
|
@ -168,7 +177,7 @@ class CS3IPlayer : IPlayer {
|
|||
}
|
||||
|
||||
// we want autoplay because of TV and UX
|
||||
isPlaying = true
|
||||
isPlaying = autoPlay ?: isPlaying
|
||||
|
||||
// release the current exoplayer and cache
|
||||
releasePlayer()
|
||||
|
@ -322,6 +331,7 @@ class CS3IPlayer : IPlayer {
|
|||
}
|
||||
|
||||
companion object {
|
||||
private var ytVideos: MutableMap<String, YtFile> = mutableMapOf()
|
||||
private var simpleCache: SimpleCache? = null
|
||||
|
||||
var requestSubtitleUpdate: (() -> Unit)? = null
|
||||
|
@ -686,6 +696,14 @@ class CS3IPlayer : IPlayer {
|
|||
isPlaying = exo.isPlaying
|
||||
}
|
||||
|
||||
when (playbackState) {
|
||||
Player.STATE_READY -> {
|
||||
onRenderFirst()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
|
||||
if (playWhenReady) {
|
||||
when (playbackState) {
|
||||
Player.STATE_READY -> {
|
||||
|
@ -715,45 +733,41 @@ class CS3IPlayer : IPlayer {
|
|||
// super.onCues(cues.map { cue -> cue.buildUpon().setText("Hello world").setSize(Cue.DIMEN_UNSET).build() })
|
||||
//}
|
||||
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
super.onIsPlayingChanged(isPlaying)
|
||||
if (isPlaying) {
|
||||
onRenderFirst()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
super.onPlaybackStateChanged(playbackState)
|
||||
when (playbackState) {
|
||||
Player.STATE_READY -> {
|
||||
requestAutoFocus?.invoke()
|
||||
}
|
||||
Player.STATE_ENDED -> {
|
||||
handleEvent(CSPlayerEvent.NextEpisode)
|
||||
}
|
||||
Player.STATE_BUFFERING -> {
|
||||
updatedTime()
|
||||
}
|
||||
Player.STATE_IDLE -> {
|
||||
// IDLE
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
override fun onVideoSizeChanged(videoSize: VideoSize) {
|
||||
super.onVideoSizeChanged(videoSize)
|
||||
playerDimensionsLoaded?.invoke(Pair(videoSize.width, videoSize.height))
|
||||
}
|
||||
|
||||
override fun onRenderedFirstFrame() {
|
||||
updatedTime()
|
||||
|
||||
if (!hasUsedFirstRender) { // this insures that we only call this once per player load
|
||||
Log.i(TAG, "Rendered first frame")
|
||||
|
||||
val invalid = exoPlayer?.duration?.let { duration ->
|
||||
// Only errors short playback when not playing downloaded files
|
||||
duration < 20_000L && currentDownloadedFile == null
|
||||
} ?: false
|
||||
if (invalid) {
|
||||
releasePlayer(saveTime = false)
|
||||
playerError?.invoke(InvalidFileException("Too short playback"))
|
||||
return
|
||||
}
|
||||
|
||||
setPreferredSubtitles(currentSubtitles)
|
||||
hasUsedFirstRender = true
|
||||
val format = exoPlayer?.videoFormat
|
||||
val width = format?.width
|
||||
val height = format?.height
|
||||
if (height != null && width != null) {
|
||||
playerDimensionsLoaded?.invoke(Pair(width, height))
|
||||
updatedTime()
|
||||
exoPlayer?.apply {
|
||||
requestedListeningPercentages?.forEach { percentage ->
|
||||
createMessage { _, _ ->
|
||||
updatedTime()
|
||||
}
|
||||
.setLooper(Looper.getMainLooper())
|
||||
.setPosition( /* positionMs= */contentDuration * percentage / 100)
|
||||
// .setPayload(customPayloadData)
|
||||
.setDeleteAfterDelivery(false)
|
||||
.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
super.onRenderedFirstFrame()
|
||||
onRenderFirst()
|
||||
}
|
||||
})
|
||||
} catch (e: Exception) {
|
||||
|
@ -762,6 +776,45 @@ class CS3IPlayer : IPlayer {
|
|||
}
|
||||
}
|
||||
|
||||
fun onRenderFirst() {
|
||||
if (!hasUsedFirstRender) { // this insures that we only call this once per player load
|
||||
Log.i(TAG, "Rendered first frame")
|
||||
|
||||
val invalid = exoPlayer?.duration?.let { duration ->
|
||||
// Only errors short playback when not playing downloaded files
|
||||
duration < 20_000L && currentDownloadedFile == null
|
||||
} ?: false
|
||||
|
||||
if (invalid) {
|
||||
releasePlayer(saveTime = false)
|
||||
playerError?.invoke(InvalidFileException("Too short playback"))
|
||||
return
|
||||
}
|
||||
|
||||
setPreferredSubtitles(currentSubtitles)
|
||||
hasUsedFirstRender = true
|
||||
val format = exoPlayer?.videoFormat
|
||||
val width = format?.width
|
||||
val height = format?.height
|
||||
if (height != null && width != null) {
|
||||
playerDimensionsLoaded?.invoke(Pair(width, height))
|
||||
updatedTime()
|
||||
exoPlayer?.apply {
|
||||
requestedListeningPercentages?.forEach { percentage ->
|
||||
createMessage { _, _ ->
|
||||
updatedTime()
|
||||
}
|
||||
.setLooper(Looper.getMainLooper())
|
||||
.setPosition( /* positionMs= */contentDuration * percentage / 100)
|
||||
// .setPayload(customPayloadData)
|
||||
.setDeleteAfterDelivery(false)
|
||||
.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadOfflinePlayer(context: Context, data: ExtractorUri) {
|
||||
Log.i(TAG, "loadOfflinePlayer")
|
||||
try {
|
||||
|
@ -815,10 +868,6 @@ class CS3IPlayer : IPlayer {
|
|||
null
|
||||
}
|
||||
}
|
||||
SubtitleOrigin.OPEN_SUBTITLES -> {
|
||||
// TODO
|
||||
throw NotImplementedError()
|
||||
}
|
||||
SubtitleOrigin.EMBEDDED_IN_VIDEO -> {
|
||||
if (offlineSourceFactory != null) {
|
||||
activeSubtitles.add(sub)
|
||||
|
@ -833,9 +882,55 @@ class CS3IPlayer : IPlayer {
|
|||
return Pair(subSources, activeSubtitles)
|
||||
}
|
||||
|
||||
|
||||
fun loadYtFile(context: Context, yt: YtFile) {
|
||||
loadOnlinePlayer(
|
||||
context,
|
||||
ExtractorLink(
|
||||
"YouTube",
|
||||
"",
|
||||
yt.url,
|
||||
"",
|
||||
yt.format?.height ?: Qualities.Unknown.value
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadOnlinePlayer(context: Context, link: ExtractorLink) {
|
||||
Log.i(TAG, "loadOnlinePlayer")
|
||||
Log.i(TAG, "loadOnlinePlayer $link")
|
||||
try {
|
||||
if (link.url.contains("youtube.com")) {
|
||||
val ytLink = link.url.replace("/embed/", "/watch?v=")
|
||||
ytVideos[ytLink]?.let {
|
||||
loadYtFile(context, it)
|
||||
return
|
||||
}
|
||||
val ytExtractor =
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
object : YouTubeExtractor(context) {
|
||||
override fun onExtractionComplete(
|
||||
ytFiles: SparseArray<YtFile>?,
|
||||
videoMeta: VideoMeta?
|
||||
) {
|
||||
var yt: YtFile? = null
|
||||
ytFiles?.forEach { _, value ->
|
||||
if ((yt?.format?.height ?: 0) < (value.format?.height
|
||||
?: -1) && (value.format?.audioBitrate ?: -1) > 0
|
||||
) {
|
||||
yt = value
|
||||
}
|
||||
}
|
||||
yt?.let { ytf ->
|
||||
ytVideos[ytLink] = ytf
|
||||
loadYtFile(context, ytf)
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.i(TAG, "YouTube extraction on $ytLink")
|
||||
ytExtractor.extract(ytLink)
|
||||
return
|
||||
}
|
||||
|
||||
currentLink = link
|
||||
|
||||
if (ignoreSSL) {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package com.lagradost.cloudstream3.ui.player
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.exoplayer2.Format
|
||||
import com.google.android.exoplayer2.text.SubtitleDecoder
|
||||
import com.google.android.exoplayer2.text.SubtitleDecoderFactory
|
||||
|
@ -11,14 +13,32 @@ import com.google.android.exoplayer2.text.subrip.SubripDecoder
|
|||
import com.google.android.exoplayer2.text.ttml.TtmlDecoder
|
||||
import com.google.android.exoplayer2.text.webvtt.WebvttDecoder
|
||||
import com.google.android.exoplayer2.util.MimeTypes
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import org.mozilla.universalchardet.UniversalDetector
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
import java.nio.charset.Charset
|
||||
|
||||
class CustomDecoder : SubtitleDecoder {
|
||||
companion object {
|
||||
fun updateForcedEncoding(context: Context) {
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val value = settingsManager.getString(
|
||||
context.getString(R.string.subtitles_encoding_key),
|
||||
null
|
||||
)
|
||||
overrideEncoding = if (value.isNullOrBlank()) {
|
||||
null
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
private const val UTF_8 = "UTF-8"
|
||||
private const val TAG = "CustomDecoder"
|
||||
private var overrideEncoding: String? = null
|
||||
var regexSubtitlesToRemoveCaptions = false
|
||||
var regexSubtitlesToRemoveBloat = false
|
||||
val bloatRegex =
|
||||
listOf(
|
||||
Regex(
|
||||
|
@ -40,6 +60,8 @@ class CustomDecoder : SubtitleDecoder {
|
|||
)
|
||||
val captionRegex = listOf(Regex("""(-\s?|)[\[({][\w\d\s]*?[])}]\s*"""))
|
||||
|
||||
//https://emptycharacter.com/
|
||||
//https://www.fileformat.info/info/unicode/char/200b/index.htm
|
||||
fun trimStr(string: String): String {
|
||||
return string.trimStart().trim('\uFEFF', '\u200B').replace(
|
||||
Regex("[\u00A0\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u205F]"),
|
||||
|
@ -59,73 +81,118 @@ class CustomDecoder : SubtitleDecoder {
|
|||
return realDecoder?.dequeueInputBuffer() ?: SubtitleInputBuffer()
|
||||
}
|
||||
|
||||
private fun getStr(byteArray: ByteArray): Pair<String, Charset> {
|
||||
val encoding = try {
|
||||
val encoding = overrideEncoding ?: run {
|
||||
val detector = UniversalDetector()
|
||||
|
||||
detector.handleData(byteArray, 0, byteArray.size)
|
||||
detector.dataEnd()
|
||||
|
||||
detector.detectedCharset // "windows-1256"
|
||||
}
|
||||
|
||||
Log.i(
|
||||
TAG,
|
||||
"Detected encoding with charset $encoding and override = $overrideEncoding"
|
||||
)
|
||||
encoding ?: UTF_8
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to detect encoding throwing error")
|
||||
logError(e)
|
||||
UTF_8
|
||||
}
|
||||
|
||||
return try {
|
||||
val set = charset(encoding)
|
||||
Pair(String(byteArray, set), set)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to parse using encoding $encoding")
|
||||
logError(e)
|
||||
Pair(byteArray.decodeToString(), charset(UTF_8))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStr(input: SubtitleInputBuffer): String? {
|
||||
try {
|
||||
val data = input.data ?: return null
|
||||
data.position(0)
|
||||
val fullDataArr = ByteArray(data.remaining())
|
||||
data.get(fullDataArr)
|
||||
return trimStr(getStr(fullDataArr).first)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to parse text returning plain data")
|
||||
logError(e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
override fun queueInputBuffer(inputBuffer: SubtitleInputBuffer) {
|
||||
Log.i(TAG, "queueInputBuffer")
|
||||
try {
|
||||
if (realDecoder == null) {
|
||||
inputBuffer.data?.let { data ->
|
||||
// this way we read the subtitle file and decide what decoder to use instead of relying on mimetype
|
||||
|
||||
val pos = data.position()
|
||||
data.position(0)
|
||||
val arr = ByteArray(minOf(data.remaining(), 100))
|
||||
data.get(arr)
|
||||
data.position(pos)
|
||||
|
||||
//https://emptycharacter.com/
|
||||
//https://www.fileformat.info/info/unicode/char/200b/index.htm
|
||||
val str = trimStr(arr.decodeToString())
|
||||
Log.i(TAG, "Got data from queueInputBuffer")
|
||||
Log.i(TAG, "first string is >>>$str<<<")
|
||||
if (str.isNotEmpty()) {
|
||||
//https://github.com/LagradOst/CloudStream-2/blob/ddd774ee66810137ff7bd65dae70bcf3ba2d2489/CloudStreamForms/CloudStreamForms/Script/MainChrome.cs#L388
|
||||
realDecoder = when {
|
||||
str.startsWith("WEBVTT", ignoreCase = true) -> WebvttDecoder()
|
||||
str.startsWith("<?xml version=\"", ignoreCase = true) -> TtmlDecoder()
|
||||
(str.startsWith(
|
||||
"[Script Info]",
|
||||
ignoreCase = true
|
||||
) || str.startsWith("Title:", ignoreCase = true)) -> SsaDecoder()
|
||||
str.startsWith("1", ignoreCase = true) -> SubripDecoder()
|
||||
else -> null
|
||||
val inputString = getStr(inputBuffer)
|
||||
if (realDecoder == null && !inputString.isNullOrBlank()) {
|
||||
var str: String = inputString
|
||||
// this way we read the subtitle file and decide what decoder to use instead of relying on mimetype
|
||||
Log.i(TAG, "Got data from queueInputBuffer")
|
||||
//https://github.com/LagradOst/CloudStream-2/blob/ddd774ee66810137ff7bd65dae70bcf3ba2d2489/CloudStreamForms/CloudStreamForms/Script/MainChrome.cs#L388
|
||||
realDecoder = when {
|
||||
str.startsWith("WEBVTT", ignoreCase = true) -> WebvttDecoder()
|
||||
str.startsWith("<?xml version=\"", ignoreCase = true) -> TtmlDecoder()
|
||||
(str.startsWith(
|
||||
"[Script Info]",
|
||||
ignoreCase = true
|
||||
) || str.startsWith("Title:", ignoreCase = true)) -> SsaDecoder()
|
||||
str.startsWith("1", ignoreCase = true) -> SubripDecoder()
|
||||
else -> null
|
||||
}
|
||||
Log.i(
|
||||
TAG,
|
||||
"Decoder selected: $realDecoder"
|
||||
)
|
||||
realDecoder?.let { decoder ->
|
||||
decoder.dequeueInputBuffer()?.let { buff ->
|
||||
if (decoder::class.java != SsaDecoder::class.java) {
|
||||
if (regexSubtitlesToRemoveCaptions)
|
||||
captionRegex.forEach { rgx ->
|
||||
str = str.replace(rgx, "\n")
|
||||
}
|
||||
if (regexSubtitlesToRemoveBloat)
|
||||
bloatRegex.forEach { rgx ->
|
||||
str = str.replace(rgx, "\n")
|
||||
}
|
||||
}
|
||||
buff.data = ByteBuffer.wrap(str.toByteArray(charset(UTF_8)))
|
||||
|
||||
decoder.queueInputBuffer(buff)
|
||||
Log.i(
|
||||
TAG,
|
||||
"Decoder selected: $realDecoder"
|
||||
"Decoder queueInputBuffer successfully"
|
||||
)
|
||||
val decoder = realDecoder
|
||||
if (decoder != null) {
|
||||
decoder.dequeueInputBuffer()?.let { buff ->
|
||||
if (regexSubtitlesToRemoveCaptions && decoder::class.java != SsaDecoder::class.java) {
|
||||
try {
|
||||
data.position(0)
|
||||
val fullDataArr = ByteArray(data.remaining())
|
||||
data.get(fullDataArr)
|
||||
var fullStr = trimStr(fullDataArr.decodeToString())
|
||||
|
||||
bloatRegex.forEach { rgx ->
|
||||
fullStr = fullStr.replace(rgx, "\n")
|
||||
}
|
||||
captionRegex.forEach { rgx ->
|
||||
fullStr = fullStr.replace(rgx, "\n")
|
||||
}
|
||||
fullStr.replace(Regex("(\r\n|\r|\n){2,}"), "\n")
|
||||
|
||||
buff.data = ByteBuffer.wrap(fullStr.toByteArray())
|
||||
} catch (e: Exception) {
|
||||
data.position(pos)
|
||||
buff.data = data
|
||||
}
|
||||
} else {
|
||||
buff.data = data
|
||||
}
|
||||
decoder.queueInputBuffer(buff)
|
||||
}
|
||||
CS3IPlayer.requestSubtitleUpdate?.invoke()
|
||||
}
|
||||
}
|
||||
CS3IPlayer.requestSubtitleUpdate?.invoke()
|
||||
}
|
||||
} else {
|
||||
Log.i(
|
||||
TAG,
|
||||
"Decoder else queueInputBuffer successfully"
|
||||
)
|
||||
|
||||
if (!inputString.isNullOrBlank()) {
|
||||
var str: String = inputString
|
||||
if (realDecoder!!::class.java != SsaDecoder::class.java) {
|
||||
if (regexSubtitlesToRemoveCaptions)
|
||||
captionRegex.forEach { rgx ->
|
||||
str = str.replace(rgx, "\n")
|
||||
}
|
||||
if (regexSubtitlesToRemoveBloat)
|
||||
bloatRegex.forEach { rgx ->
|
||||
str = str.replace(rgx, "\n")
|
||||
}
|
||||
}
|
||||
inputBuffer.data = ByteBuffer.wrap(str.toByteArray(charset(UTF_8)))
|
||||
}
|
||||
|
||||
realDecoder?.queueInputBuffer(inputBuffer)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
|
|
@ -38,6 +38,7 @@ import com.lagradost.cloudstream3.CommonActivity.keyEventListener
|
|||
import com.lagradost.cloudstream3.CommonActivity.playerEventListener
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer.Companion.subsProvidersIsActive
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||
|
@ -50,6 +51,28 @@ import com.lagradost.cloudstream3.utils.UIHelper.showSystemUI
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||
import com.lagradost.cloudstream3.utils.Vector2
|
||||
import kotlinx.android.synthetic.main.player_custom_layout.*
|
||||
import kotlinx.android.synthetic.main.player_custom_layout.bottom_player_bar
|
||||
import kotlinx.android.synthetic.main.player_custom_layout.exo_ffwd
|
||||
import kotlinx.android.synthetic.main.player_custom_layout.exo_ffwd_text
|
||||
import kotlinx.android.synthetic.main.player_custom_layout.exo_progress
|
||||
import kotlinx.android.synthetic.main.player_custom_layout.exo_rew
|
||||
import kotlinx.android.synthetic.main.player_custom_layout.exo_rew_text
|
||||
import kotlinx.android.synthetic.main.player_custom_layout.player_center_menu
|
||||
import kotlinx.android.synthetic.main.player_custom_layout.player_ffwd_holder
|
||||
import kotlinx.android.synthetic.main.player_custom_layout.player_holder
|
||||
import kotlinx.android.synthetic.main.player_custom_layout.player_pause_play
|
||||
import kotlinx.android.synthetic.main.player_custom_layout.player_pause_play_holder
|
||||
import kotlinx.android.synthetic.main.player_custom_layout.player_progressbar_left
|
||||
import kotlinx.android.synthetic.main.player_custom_layout.player_progressbar_left_holder
|
||||
import kotlinx.android.synthetic.main.player_custom_layout.player_progressbar_left_icon
|
||||
import kotlinx.android.synthetic.main.player_custom_layout.player_progressbar_right
|
||||
import kotlinx.android.synthetic.main.player_custom_layout.player_progressbar_right_holder
|
||||
import kotlinx.android.synthetic.main.player_custom_layout.player_progressbar_right_icon
|
||||
import kotlinx.android.synthetic.main.player_custom_layout.player_rew_holder
|
||||
import kotlinx.android.synthetic.main.player_custom_layout.player_time_text
|
||||
import kotlinx.android.synthetic.main.player_custom_layout.player_video_bar
|
||||
import kotlinx.android.synthetic.main.player_custom_layout.shadow_overlay
|
||||
import kotlinx.android.synthetic.main.trailer_custom_layout.*
|
||||
import kotlin.math.*
|
||||
|
||||
const val MINIMUM_SEEK_TIME = 7000L // when swipe seeking
|
||||
|
@ -63,6 +86,9 @@ const val DOUBLE_TAB_PAUSE_PERCENTAGE = 0.15 // in both directions
|
|||
|
||||
// All the UI Logic for the player
|
||||
open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||
protected open var lockRotation = true
|
||||
protected open var isFullScreenPlayer = true
|
||||
|
||||
// state of player UI
|
||||
protected var isShowing = false
|
||||
protected var isLocked = false
|
||||
|
@ -99,11 +125,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
|
||||
// screenWidth and screenHeight does always
|
||||
// refer to the screen while in landscape mode
|
||||
private val screenWidth: Int
|
||||
protected val screenWidth: Int
|
||||
get() {
|
||||
return max(displayMetrics.widthPixels, displayMetrics.heightPixels)
|
||||
}
|
||||
private val screenHeight: Int
|
||||
protected val screenHeight: Int
|
||||
get() {
|
||||
return min(displayMetrics.widthPixels, displayMetrics.heightPixels)
|
||||
}
|
||||
|
@ -138,6 +164,14 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
open fun openOnlineSubPicker(
|
||||
context: Context,
|
||||
imdbId: Long?,
|
||||
dismissCallback: (() -> Unit)
|
||||
) {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
/** Returns false if the touch is on the status bar or navigation bar*/
|
||||
private fun isValidTouch(rawX: Float, rawY: Float): Boolean {
|
||||
val statusHeight = statusBarHeight ?: 0
|
||||
|
@ -150,7 +184,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
animateLayoutChanges()
|
||||
}
|
||||
|
||||
private fun animateLayoutChanges() {
|
||||
protected fun animateLayoutChanges() {
|
||||
if (isShowing) {
|
||||
updateUIVisibility()
|
||||
} else {
|
||||
|
@ -199,7 +233,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
player_ffwd_holder?.alpha = 1f
|
||||
player_rew_holder?.alpha = 1f
|
||||
// player_pause_play_holder?.alpha = 1f
|
||||
|
||||
shadow_overlay?.isVisible = true
|
||||
shadow_overlay?.startAnimation(fadeAnimation)
|
||||
player_ffwd_holder?.startAnimation(fadeAnimation)
|
||||
player_rew_holder?.startAnimation(fadeAnimation)
|
||||
|
@ -224,20 +258,22 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
player_subtitle_offset_btt?.isGone = player.getCurrentPreferredSubtitle() == null
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
activity?.hideSystemUI()
|
||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && fullscreenNotch) {
|
||||
val params = activity?.window?.attributes
|
||||
params?.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
activity?.window?.attributes = params
|
||||
protected fun enterFullscreen() {
|
||||
if (isFullScreenPlayer) {
|
||||
activity?.hideSystemUI()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && fullscreenNotch) {
|
||||
val params = activity?.window?.attributes
|
||||
params?.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
activity?.window?.attributes = params
|
||||
}
|
||||
}
|
||||
|
||||
super.onResume()
|
||||
if (lockRotation)
|
||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
protected fun exitFullscreen() {
|
||||
activity?.showSystemUI()
|
||||
//if (lockRotation)
|
||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
|
||||
|
||||
// simply resets brightness and notch settings that might have been overridden
|
||||
|
@ -248,6 +284,15 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
|
||||
}
|
||||
activity?.window?.attributes = lp
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
enterFullscreen()
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
exitFullscreen()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
@ -327,7 +372,8 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
}
|
||||
|
||||
dialog.setOnDismissListener {
|
||||
activity?.hideSystemUI()
|
||||
if (isFullScreenPlayer)
|
||||
activity?.hideSystemUI()
|
||||
}
|
||||
applyButton.setOnClickListener {
|
||||
dialog.dismissSafe(activity)
|
||||
|
@ -365,9 +411,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
act.getString(R.string.player_speed),
|
||||
false,
|
||||
{
|
||||
activity?.hideSystemUI()
|
||||
if (isFullScreenPlayer)
|
||||
activity?.hideSystemUI()
|
||||
}) { index ->
|
||||
activity?.hideSystemUI()
|
||||
if (isFullScreenPlayer)
|
||||
activity?.hideSystemUI()
|
||||
setPlayBackSpeed(speedsNumbers[index])
|
||||
}
|
||||
}
|
||||
|
@ -446,9 +494,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
private fun onClickChange() {
|
||||
isShowing = !isShowing
|
||||
if (isShowing) {
|
||||
player_intro_play?.isGone = true
|
||||
autoHide()
|
||||
}
|
||||
activity?.hideSystemUI()
|
||||
if (isFullScreenPlayer)
|
||||
activity?.hideSystemUI()
|
||||
animateLayoutChanges()
|
||||
player_pause_play?.requestFocus()
|
||||
}
|
||||
|
@ -492,6 +542,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
player_lock_holder?.startAnimation(fadeAnimation)
|
||||
//player_go_back_holder?.startAnimation(fadeAnimation)
|
||||
|
||||
shadow_overlay?.isVisible = true
|
||||
shadow_overlay?.startAnimation(fadeAnimation)
|
||||
|
||||
updateLockUI()
|
||||
|
@ -683,7 +734,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
if (event == null || view == null) return false
|
||||
val currentTouch = Vector2(event.x, event.y)
|
||||
val startTouch = currentTouchStart
|
||||
|
||||
player_intro_play?.isGone = true
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
// validates if the touch is inside of the player area
|
||||
|
@ -708,7 +759,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (isCurrentTouchValid && !isLocked) {
|
||||
if (isCurrentTouchValid && !isLocked && isFullScreenPlayer) {
|
||||
// seek time
|
||||
if (swipeHorizontalEnabled && currentTouchAction == TouchAction.Time) {
|
||||
val startTime = currentTouchStartPlayerTime
|
||||
|
@ -737,7 +788,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
|
||||
if (currentClickCount >= 1) { // have double clicked
|
||||
currentDoubleTapIndex++
|
||||
if (doubleTapPauseEnabled) { // you can pause if your tap is in the middle of the screen
|
||||
if (doubleTapPauseEnabled && isFullScreenPlayer) { // you can pause if your tap is in the middle of the screen
|
||||
when {
|
||||
currentTouch.x < screenWidth / 2 - (DOUBLE_TAB_PAUSE_PERCENTAGE * screenWidth) -> {
|
||||
if (doubleTapEnabled)
|
||||
|
@ -751,7 +802,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
player.handleEvent(CSPlayerEvent.PlayPauseToggle)
|
||||
}
|
||||
}
|
||||
} else if (doubleTapEnabled) {
|
||||
} else if (doubleTapEnabled && isFullScreenPlayer) {
|
||||
if (currentTouch.x < screenWidth / 2) {
|
||||
rewind()
|
||||
} else {
|
||||
|
@ -789,7 +840,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
// if current touch is valid
|
||||
if (startTouch != null && isCurrentTouchValid && !isLocked) {
|
||||
if (startTouch != null && isCurrentTouchValid && !isLocked && isFullScreenPlayer) {
|
||||
// action is unassigned and can therefore be assigned
|
||||
if (currentTouchAction == null) {
|
||||
val diffFromStart = startTouch - currentTouch
|
||||
|
@ -1013,6 +1064,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
// if nothing has loaded these buttons should not be visible
|
||||
player_skip_episode?.isVisible = false
|
||||
player_skip_op?.isVisible = false
|
||||
shadow_overlay?.isVisible = false
|
||||
|
||||
updateLockUI()
|
||||
updateUIVisibility()
|
||||
|
@ -1070,6 +1122,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
PlayerEventType.ShowMirrors -> {
|
||||
showMirrorsDialogue()
|
||||
}
|
||||
PlayerEventType.SearchSubtitlesOnline -> {
|
||||
if (subsProvidersIsActive) {
|
||||
openOnlineSubPicker(view.context, null) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1187,6 +1244,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
showMirrorsDialogue()
|
||||
}
|
||||
|
||||
player_intro_play?.setOnClickListener {
|
||||
player_intro_play?.isGone = true
|
||||
player.handleEvent(CSPlayerEvent.Play)
|
||||
}
|
||||
|
||||
// it is !not! a bug that you cant touch the right side, it does not register inputs on navbar or status bar
|
||||
player_holder?.setOnTouchListener { callView, event ->
|
||||
return@setOnTouchListener handleMotionEvent(callView, event)
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
package com.lagradost.cloudstream3.ui.player
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
|
@ -15,7 +18,6 @@ import androidx.core.view.isVisible
|
|||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.exoplayer2.util.MimeTypes
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||
|
@ -24,19 +26,32 @@ import com.lagradost.cloudstream3.mvvm.Resource
|
|||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subtitleProviders
|
||||
import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.updateForcedEncoding
|
||||
import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType
|
||||
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
||||
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
||||
import com.lagradost.cloudstream3.ui.result.SyncViewModel
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
|
||||
import com.lagradost.cloudstream3.utils.SubtitleHelper.languages
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
|
||||
import kotlinx.android.synthetic.main.dialog_online_subtitles.*
|
||||
import kotlinx.android.synthetic.main.dialog_online_subtitles.apply_btt
|
||||
import kotlinx.android.synthetic.main.dialog_online_subtitles.cancel_btt
|
||||
import kotlinx.android.synthetic.main.fragment_player.*
|
||||
import kotlinx.android.synthetic.main.player_custom_layout.*
|
||||
import kotlinx.android.synthetic.main.player_select_source_and_subs.*
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
class GeneratorPlayer : FullScreenPlayer() {
|
||||
|
@ -50,8 +65,14 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
putSerializable("syncData", syncData)
|
||||
}
|
||||
}
|
||||
|
||||
val subsProviders
|
||||
get() = subtitleProviders.filter { !it.requiresLogin || it.loginInfo() != null }
|
||||
val subsProvidersIsActive
|
||||
get() = subsProviders.isNotEmpty()
|
||||
}
|
||||
|
||||
|
||||
private var titleRez = 3
|
||||
private var limitTitle = 0
|
||||
|
||||
|
@ -161,6 +182,174 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
}
|
||||
|
||||
data class TempMetaData(
|
||||
var episode: Int? = null,
|
||||
var season: Int? = null,
|
||||
var name: String? = null,
|
||||
)
|
||||
|
||||
private fun getMetaData(): TempMetaData {
|
||||
val meta = TempMetaData()
|
||||
|
||||
when (val newMeta = currentMeta) {
|
||||
is ResultEpisode -> {
|
||||
if (!newMeta.tvType.isMovieType()) {
|
||||
meta.episode = newMeta.episode
|
||||
meta.season = newMeta.season
|
||||
}
|
||||
meta.name = newMeta.headerName
|
||||
}
|
||||
is ExtractorUri -> {
|
||||
if (newMeta.tvType?.isMovieType() == false) {
|
||||
meta.episode = newMeta.episode
|
||||
meta.season = newMeta.season
|
||||
}
|
||||
meta.name = newMeta.headerName
|
||||
}
|
||||
}
|
||||
return meta
|
||||
}
|
||||
|
||||
override fun openOnlineSubPicker(
|
||||
context: Context,
|
||||
imdbId: Long?,
|
||||
dismissCallback: (() -> Unit)
|
||||
) {
|
||||
val providers = subsProviders
|
||||
|
||||
val dialog = Dialog(context, R.style.AlertDialogCustomBlack)
|
||||
dialog.setContentView(R.layout.dialog_online_subtitles)
|
||||
|
||||
val arrayAdapter =
|
||||
ArrayAdapter<String>(dialog.context, R.layout.sort_bottom_single_choice)
|
||||
|
||||
dialog.show()
|
||||
|
||||
dialog.cancel_btt.setOnClickListener {
|
||||
dialog.dismissSafe()
|
||||
}
|
||||
|
||||
dialog.subtitle_adapter.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||
dialog.subtitle_adapter.adapter = arrayAdapter
|
||||
val adapter = dialog.subtitle_adapter.adapter as? ArrayAdapter<String>
|
||||
|
||||
var currentSubtitles: List<AbstractSubtitleEntities.SubtitleEntity> = emptyList()
|
||||
var currentSubtitle: AbstractSubtitleEntities.SubtitleEntity? = null
|
||||
|
||||
dialog.subtitle_adapter.setOnItemClickListener { _, _, position, _ ->
|
||||
currentSubtitle = currentSubtitles.getOrNull(position) ?: return@setOnItemClickListener
|
||||
}
|
||||
|
||||
var currentLanguageTwoLetters: String = getAutoSelectLanguageISO639_1()
|
||||
|
||||
fun getName(entry: AbstractSubtitleEntities.SubtitleEntity): String {
|
||||
return if (entry.lang.isBlank()) {
|
||||
entry.name
|
||||
} else {
|
||||
val language = fromTwoLettersToLanguage(entry.lang.trim()) ?: entry.lang
|
||||
return "$language ${entry.name}"
|
||||
}
|
||||
}
|
||||
|
||||
fun setSubtitlesList(list: List<AbstractSubtitleEntities.SubtitleEntity>) {
|
||||
currentSubtitles = list
|
||||
adapter?.clear()
|
||||
adapter?.addAll(currentSubtitles.map { getName(it) })
|
||||
}
|
||||
|
||||
val currentTempMeta = getMetaData()
|
||||
// bruh idk why it is not correct
|
||||
val color = ColorStateList.valueOf(context.colorFromAttribute(R.attr.colorAccent))
|
||||
dialog.search_loading_bar.progressTintList = color
|
||||
dialog.search_loading_bar.indeterminateTintList = color
|
||||
|
||||
dialog.subtitles_search.setOnQueryTextListener(object :
|
||||
androidx.appcompat.widget.SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
dialog.search_loading_bar?.show()
|
||||
ioSafe {
|
||||
val search = AbstractSubtitleEntities.SubtitleSearch(
|
||||
query = query ?: return@ioSafe,
|
||||
imdb = imdbId,
|
||||
epNumber = currentTempMeta.episode,
|
||||
seasonNumber = currentTempMeta.season,
|
||||
lang = currentLanguageTwoLetters.ifBlank { null }
|
||||
)
|
||||
val results = providers.apmap {
|
||||
try {
|
||||
it.search(search)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}.filterNotNull()
|
||||
val max = results.map { it.size }.maxOrNull() ?: return@ioSafe
|
||||
|
||||
// very ugly
|
||||
val items = ArrayList<AbstractSubtitleEntities.SubtitleEntity>()
|
||||
val arrays = results.size
|
||||
for (index in 0 until max) {
|
||||
for (i in 0 until arrays) {
|
||||
items.add(results[i].getOrNull(index) ?: continue)
|
||||
}
|
||||
}
|
||||
|
||||
// ugly ik
|
||||
activity?.runOnUiThread {
|
||||
setSubtitlesList(items)
|
||||
dialog.search_loading_bar?.hide()
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
dialog.search_filter.setOnClickListener {
|
||||
val lang639_1 = languages.map { it.ISO_639_1 }
|
||||
activity?.showDialog(
|
||||
languages.map { it.languageName },
|
||||
lang639_1.indexOf(currentLanguageTwoLetters),
|
||||
context.getString(R.string.subs_subtitle_languages),
|
||||
true,
|
||||
{ }
|
||||
) { index ->
|
||||
currentLanguageTwoLetters = lang639_1[index]
|
||||
dialog.subtitles_search.setQuery(dialog.subtitles_search.query, true)
|
||||
}
|
||||
}
|
||||
|
||||
dialog.apply_btt.setOnClickListener {
|
||||
currentSubtitle?.let { currentSubtitle ->
|
||||
providers.firstOrNull { it.idPrefix == currentSubtitle.idPrefix }?.let { api ->
|
||||
ioSafe {
|
||||
val url = api.load(currentSubtitle) ?: return@ioSafe
|
||||
val subtitle = SubtitleData(
|
||||
name = getName(currentSubtitle),
|
||||
url = url,
|
||||
origin = SubtitleOrigin.URL,
|
||||
mimeType = url.toSubtitleMimeType()
|
||||
)
|
||||
runOnMainThread {
|
||||
addAndSelectSubtitles(subtitle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dialog.dismissSafe()
|
||||
}
|
||||
|
||||
dialog.setOnDismissListener {
|
||||
dismissCallback.invoke()
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
dialog.subtitles_search.setQuery(currentTempMeta.name, true)
|
||||
}
|
||||
|
||||
private fun openSubPicker() {
|
||||
try {
|
||||
subsPathPicker.launch(
|
||||
|
@ -181,6 +370,27 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun addAndSelectSubtitles(subtitleData: SubtitleData) {
|
||||
val ctx = context ?: return
|
||||
setSubtitles(subtitleData)
|
||||
|
||||
// this is used instead of observe, because observe is too slow
|
||||
val subs = currentSubs.toMutableSet()
|
||||
subs.add(subtitleData)
|
||||
player.setActiveSubtitles(subs)
|
||||
player.reloadPlayer(ctx)
|
||||
|
||||
viewModel.addSubtitles(setOf(subtitleData))
|
||||
|
||||
selectSourceDialog?.dismissSafe()
|
||||
|
||||
showToast(
|
||||
activity,
|
||||
String.format(ctx.getString(R.string.player_loaded_subtitles), subtitleData.name),
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
}
|
||||
|
||||
// Open file picker
|
||||
private val subsPathPicker =
|
||||
registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
|
||||
|
@ -206,23 +416,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
name.toSubtitleMimeType()
|
||||
)
|
||||
|
||||
setSubtitles(subtitleData)
|
||||
|
||||
// this is used instead of observe, because observe is too slow
|
||||
val subs = currentSubs.toMutableSet()
|
||||
subs.add(subtitleData)
|
||||
player.setActiveSubtitles(subs)
|
||||
player.reloadPlayer(ctx)
|
||||
|
||||
viewModel.addSubtitles(setOf(subtitleData))
|
||||
|
||||
selectSourceDialog?.dismissSafe()
|
||||
|
||||
showToast(
|
||||
activity,
|
||||
String.format(ctx.getString(R.string.player_loaded_subtitles), name),
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
addAndSelectSubtitles(subtitleData)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -230,6 +424,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
override fun showMirrorsDialogue() {
|
||||
try {
|
||||
currentSelectedSubtitles = player.getCurrentPreferredSubtitle()
|
||||
//println("CURRENT SELECTED :$currentSelectedSubtitles of $currentSubs")
|
||||
context?.let { ctx ->
|
||||
val isPlaying = player.getIsPlaying()
|
||||
player.handleEvent(CSPlayerEvent.Pause)
|
||||
|
@ -241,22 +436,46 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
val sourceDialog = sourceBuilder.create()
|
||||
selectSourceDialog = sourceDialog
|
||||
sourceDialog.show()
|
||||
val providerList =
|
||||
sourceDialog.findViewById<ListView>(R.id.sort_providers)!!
|
||||
val subtitleList =
|
||||
sourceDialog.findViewById<ListView>(R.id.sort_subtitles)!!
|
||||
val applyButton =
|
||||
sourceDialog.findViewById<MaterialButton>(R.id.apply_btt)!!
|
||||
val cancelButton =
|
||||
sourceDialog.findViewById<MaterialButton>(R.id.cancel_btt)!!
|
||||
val providerList = sourceDialog.sort_providers
|
||||
val subtitleList = sourceDialog.sort_subtitles
|
||||
|
||||
val footer: TextView =
|
||||
val loadFromFileFooter: TextView =
|
||||
layoutInflater.inflate(R.layout.sort_bottom_footer_add_choice, null) as TextView
|
||||
footer.text = ctx.getString(R.string.player_load_subtitles)
|
||||
footer.setOnClickListener {
|
||||
|
||||
loadFromFileFooter.text = ctx.getString(R.string.player_load_subtitles)
|
||||
loadFromFileFooter.setOnClickListener {
|
||||
openSubPicker()
|
||||
}
|
||||
subtitleList.addFooterView(footer)
|
||||
subtitleList.addFooterView(loadFromFileFooter)
|
||||
|
||||
var shouldDismiss = true
|
||||
|
||||
fun dismiss() {
|
||||
if (isPlaying) {
|
||||
player.handleEvent(CSPlayerEvent.Play)
|
||||
}
|
||||
activity?.hideSystemUI()
|
||||
}
|
||||
|
||||
if (subsProvidersIsActive) {
|
||||
val loadFromOpenSubsFooter: TextView =
|
||||
layoutInflater.inflate(
|
||||
R.layout.sort_bottom_footer_add_choice,
|
||||
null
|
||||
) as TextView
|
||||
|
||||
loadFromOpenSubsFooter.text =
|
||||
ctx.getString(R.string.player_load_subtitles_online)
|
||||
|
||||
loadFromOpenSubsFooter.setOnClickListener {
|
||||
shouldDismiss = false
|
||||
sourceDialog.dismissSafe(activity)
|
||||
openOnlineSubPicker(it.context, null) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
subtitleList.addFooterView(loadFromOpenSubsFooter)
|
||||
}
|
||||
|
||||
var sourceIndex = 0
|
||||
var startSource = 0
|
||||
|
@ -288,10 +507,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
sourceDialog.setOnDismissListener {
|
||||
if (isPlaying) {
|
||||
player.handleEvent(CSPlayerEvent.Play)
|
||||
}
|
||||
activity?.hideSystemUI()
|
||||
if (shouldDismiss) dismiss()
|
||||
selectSourceDialog = null
|
||||
}
|
||||
|
||||
|
@ -314,11 +530,60 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
subtitleList.setItemChecked(which, true)
|
||||
}
|
||||
|
||||
cancelButton.setOnClickListener {
|
||||
sourceDialog.cancel_btt?.setOnClickListener {
|
||||
sourceDialog.dismissSafe(activity)
|
||||
}
|
||||
|
||||
applyButton.setOnClickListener {
|
||||
sourceDialog.subtitles_encoding_format?.apply {
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
|
||||
val prefNames = ctx.resources.getStringArray(R.array.subtitles_encoding_list)
|
||||
val prefValues = ctx.resources.getStringArray(R.array.subtitles_encoding_values)
|
||||
|
||||
val value = settingsManager.getString(
|
||||
ctx.getString(R.string.subtitles_encoding_key),
|
||||
null
|
||||
)
|
||||
val index = prefValues.indexOf(value)
|
||||
text = prefNames[if (index == -1) 0 else index]
|
||||
}
|
||||
|
||||
sourceDialog.subtitles_click_settings?.setOnClickListener {
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
|
||||
val prefNames = ctx.resources.getStringArray(R.array.subtitles_encoding_list)
|
||||
val prefValues = ctx.resources.getStringArray(R.array.subtitles_encoding_values)
|
||||
|
||||
val currentPrefMedia =
|
||||
settingsManager.getString(
|
||||
getString(R.string.subtitles_encoding_key),
|
||||
null
|
||||
)
|
||||
|
||||
shouldDismiss = false
|
||||
sourceDialog.dismissSafe(activity)
|
||||
|
||||
val index = prefValues.indexOf(currentPrefMedia)
|
||||
activity?.showDialog(
|
||||
prefNames.toList(),
|
||||
if (index == -1) 0 else index,
|
||||
ctx.getString(R.string.subtitles_encoding),
|
||||
true,
|
||||
{}) {
|
||||
settingsManager.edit()
|
||||
.putString(
|
||||
ctx.getString(R.string.subtitles_encoding_key),
|
||||
prefValues[it]
|
||||
)
|
||||
.apply()
|
||||
|
||||
updateForcedEncoding(ctx)
|
||||
dismiss()
|
||||
player.seekTime(-1) // to update subtitles, a dirty trick
|
||||
}
|
||||
}
|
||||
|
||||
sourceDialog.apply_btt?.setOnClickListener {
|
||||
var init = false
|
||||
if (sourceIndex != startSource) {
|
||||
init = true
|
||||
|
@ -477,14 +742,24 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
): SubtitleData? {
|
||||
val langCode = preferredAutoSelectSubtitles ?: return null
|
||||
val lang = SubtitleHelper.fromTwoLettersToLanguage(langCode) ?: return null
|
||||
|
||||
if (settings)
|
||||
subtitles.firstOrNull { sub ->
|
||||
sub.name.startsWith(lang)
|
||||
|| sub.name.trim() == langCode
|
||||
}?.let { sub ->
|
||||
return sub
|
||||
if (downloads) {
|
||||
return subtitles.firstOrNull { sub ->
|
||||
(sub.origin == SubtitleOrigin.DOWNLOADED_FILE && sub.name == context?.getString(
|
||||
R.string.default_subtitles
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
sortSubs(subtitles).firstOrNull { sub ->
|
||||
val t = sub.name.replace(Regex("[^A-Za-z]"), " ").trim()
|
||||
(settings || (downloads && sub.origin == SubtitleOrigin.DOWNLOADED_FILE)) && t == lang || t.startsWith(
|
||||
"$lang "
|
||||
) || t == langCode
|
||||
}?.let { sub ->
|
||||
return sub
|
||||
}
|
||||
|
||||
// post check in case both did not catch anything
|
||||
if (downloads) {
|
||||
return subtitles.firstOrNull { sub ->
|
||||
(sub.origin == SubtitleOrigin.DOWNLOADED_FILE || sub.name == context?.getString(
|
||||
|
@ -492,10 +767,11 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
))
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun autoSelectFromSettings() {
|
||||
private fun autoSelectFromSettings(): Boolean {
|
||||
// auto select subtitle based of settings
|
||||
val langCode = preferredAutoSelectSubtitles
|
||||
|
||||
|
@ -505,44 +781,46 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
if (setSubtitles(sub)) {
|
||||
player.reloadPlayer(ctx)
|
||||
player.handleEvent(CSPlayerEvent.Play)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun autoSelectFromDownloads() {
|
||||
private fun autoSelectFromDownloads(): Boolean {
|
||||
if (player.getCurrentPreferredSubtitle() == null) {
|
||||
getAutoSelectSubtitle(currentSubs, settings = false, downloads = true)?.let { sub ->
|
||||
context?.let { ctx ->
|
||||
if (setSubtitles(sub)) {
|
||||
player.reloadPlayer(ctx)
|
||||
player.handleEvent(CSPlayerEvent.Play)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun autoSelectSubtitles() {
|
||||
normalSafeApiCall {
|
||||
autoSelectFromSettings()
|
||||
autoSelectFromDownloads()
|
||||
if (!autoSelectFromSettings()) {
|
||||
autoSelectFromDownloads()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun setTitle() {
|
||||
private fun getPlayerVideoTitle(): String {
|
||||
var headerName: String? = null
|
||||
var subName: String? = null
|
||||
var episode: Int? = null
|
||||
var season: Int? = null
|
||||
var tvType: TvType? = null
|
||||
|
||||
var isFiller: Boolean? = null
|
||||
when (val meta = currentMeta) {
|
||||
is ResultEpisode -> {
|
||||
isFiller = meta.isFiller
|
||||
headerName = meta.headerName
|
||||
subName = meta.name
|
||||
episode = meta.episode
|
||||
|
@ -559,7 +837,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
//Generate video title
|
||||
var playerVideoTitle = if (headerName != null) {
|
||||
val playerVideoTitle = if (headerName != null) {
|
||||
(headerName +
|
||||
if (tvType.isEpisodeBased() && episode != null)
|
||||
if (season == null)
|
||||
|
@ -570,6 +848,13 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
} else {
|
||||
""
|
||||
}
|
||||
return playerVideoTitle
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun setTitle() {
|
||||
var playerVideoTitle = getPlayerVideoTitle()
|
||||
|
||||
//Hide title, if set in setting
|
||||
if (limitTitle < 0) {
|
||||
|
@ -582,6 +867,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
playerVideoTitle = playerVideoTitle.substring(0, limitTitle - 1) + "..."
|
||||
}
|
||||
}
|
||||
val isFiller: Boolean? = (currentMeta as? ResultEpisode)?.isFiller
|
||||
|
||||
player_episode_filler_holder?.isVisible = isFiller ?: false
|
||||
player_video_title?.text = playerVideoTitle
|
||||
|
@ -645,6 +931,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
titleRez = settingsManager.getInt(getString(R.string.prefer_limit_title_rez_key), 3)
|
||||
limitTitle = settingsManager.getInt(getString(R.string.prefer_limit_title_key), 0)
|
||||
updateForcedEncoding(ctx)
|
||||
}
|
||||
|
||||
unwrapBundle(savedInstanceState)
|
||||
|
|
|
@ -22,6 +22,7 @@ enum class PlayerEventType(val value: Int) {
|
|||
ShowSpeed(11),
|
||||
ShowMirrors(12),
|
||||
Resize(13),
|
||||
SearchSubtitlesOnline(14),
|
||||
}
|
||||
|
||||
enum class CSPlayerEvent(val value: Int) {
|
||||
|
@ -97,6 +98,7 @@ interface IPlayer {
|
|||
startPosition: Long? = null,
|
||||
subtitles : Set<SubtitleData>,
|
||||
subtitle : SubtitleData?,
|
||||
autoPlay : Boolean? = true
|
||||
)
|
||||
|
||||
fun reloadPlayer(context: Context)
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
package com.lagradost.cloudstream3.ui.player
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.TypedValue
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import com.google.android.exoplayer2.ui.SubtitleView
|
||||
import com.google.android.exoplayer2.util.MimeTypes
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveBloat
|
||||
import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveCaptions
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.fromSaveToStyle
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||
|
@ -22,7 +21,6 @@ enum class SubtitleStatus {
|
|||
enum class SubtitleOrigin {
|
||||
URL,
|
||||
DOWNLOADED_FILE,
|
||||
OPEN_SUBTITLES,
|
||||
EMBEDDED_IN_VIDEO
|
||||
}
|
||||
|
||||
|
@ -66,28 +64,6 @@ class PlayerSubtitleHelper {
|
|||
}
|
||||
}
|
||||
|
||||
private fun getSubtitleMimeType(context: Context, url: String, origin: SubtitleOrigin): String {
|
||||
return when (origin) {
|
||||
// The url can look like .../document/4294 when the name is EnglishSDH.srt
|
||||
SubtitleOrigin.DOWNLOADED_FILE -> {
|
||||
UniFile.fromUri(
|
||||
context,
|
||||
Uri.parse(url)
|
||||
).name?.toSubtitleMimeType() ?: MimeTypes.APPLICATION_SUBRIP
|
||||
}
|
||||
SubtitleOrigin.URL -> {
|
||||
return url.toSubtitleMimeType()
|
||||
}
|
||||
SubtitleOrigin.OPEN_SUBTITLES -> {
|
||||
// TODO
|
||||
throw NotImplementedError()
|
||||
}
|
||||
SubtitleOrigin.EMBEDDED_IN_VIDEO -> {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getSubtitleData(subtitleFile: SubtitleFile): SubtitleData {
|
||||
return SubtitleData(
|
||||
name = subtitleFile.lang,
|
||||
|
@ -109,6 +85,8 @@ class PlayerSubtitleHelper {
|
|||
}
|
||||
|
||||
fun setSubStyle(style: SaveCaptionStyle) {
|
||||
regexSubtitlesToRemoveBloat = style.removeBloat
|
||||
regexSubtitlesToRemoveCaptions = style.removeCaptions
|
||||
subtitleView?.context?.let { ctx ->
|
||||
subStyle = style
|
||||
subtitleView?.setStyle(ctx.fromSaveToStyle(style))
|
||||
|
|
|
@ -15,7 +15,7 @@ class RepoLinkGenerator(
|
|||
) : IGenerator {
|
||||
companion object {
|
||||
const val TAG = "RepoLink"
|
||||
val cache: HashMap<Int, Pair<MutableSet<ExtractorLink>, MutableSet<SubtitleData>>> = hashMapOf()
|
||||
val cache: HashMap<Pair<String, Int>, Pair<MutableSet<ExtractorLink>, MutableSet<SubtitleData>>> = hashMapOf()
|
||||
}
|
||||
|
||||
override val hasCache = true
|
||||
|
@ -71,7 +71,7 @@ class RepoLinkGenerator(
|
|||
val (currentLinkCache, currentSubsCache) = if (clearCache) {
|
||||
Pair(mutableSetOf(), mutableSetOf())
|
||||
} else {
|
||||
cache[current.id] ?: Pair(mutableSetOf(), mutableSetOf())
|
||||
cache[Pair(current.apiName, current.id)] ?: Pair(mutableSetOf(), mutableSetOf())
|
||||
}
|
||||
|
||||
//val currentLinkCache = if (clearCache) mutableSetOf() else linkCache[index].toMutableSet()
|
||||
|
@ -137,7 +137,7 @@ class RepoLinkGenerator(
|
|||
}
|
||||
}
|
||||
)
|
||||
cache[current.id] = Pair(currentLinkCache, currentSubsCache)
|
||||
cache[Pair(current.apiName, current.id)] = Pair(currentLinkCache, currentSubsCache)
|
||||
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -24,12 +24,10 @@ import android.widget.*
|
|||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
|
@ -41,7 +39,6 @@ import com.google.android.gms.cast.framework.CastContext
|
|||
import com.google.android.gms.cast.framework.CastState
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiFromName
|
||||
import com.lagradost.cloudstream3.APIHolder.getId
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
|
@ -87,7 +84,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImageBlur
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getFileName
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
|
||||
import kotlinx.android.synthetic.main.fragment_result.*
|
||||
|
@ -185,7 +181,7 @@ fun ResultEpisode.getWatchProgress(): Float {
|
|||
return (getDisplayPosition() / 1000).toFloat() / (duration / 1000).toFloat()
|
||||
}
|
||||
|
||||
class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegionsListener {
|
||||
class ResultFragment : ResultTrailerPlayer() {
|
||||
companion object {
|
||||
const val URL_BUNDLE = "url"
|
||||
const val API_NAME_BUNDLE = "apiName"
|
||||
|
@ -603,6 +599,59 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
|
|||
setFormatText(result_meta_rating, R.string.rating_format, rating?.div(1000f))
|
||||
}
|
||||
|
||||
var currentTrailers: List<String> = emptyList()
|
||||
var currentTrailerIndex = 0
|
||||
|
||||
override fun nextMirror() {
|
||||
currentTrailerIndex++
|
||||
loadTrailer()
|
||||
}
|
||||
|
||||
override fun playerError(exception: Exception) {
|
||||
if (player.getIsPlaying()) // because we dont want random toasts in player
|
||||
super.playerError(exception)
|
||||
}
|
||||
|
||||
private fun loadTrailer(index: Int? = null) {
|
||||
currentTrailers.getOrNull(index ?: currentTrailerIndex)?.let { trailer ->
|
||||
//if(trailer.contains("youtube.com")) { // wont load in exo
|
||||
// nextMirror()
|
||||
// return
|
||||
//}
|
||||
context?.let { ctx ->
|
||||
player.onPause()
|
||||
player.loadPlayer(
|
||||
ctx,
|
||||
false,
|
||||
ExtractorLink(
|
||||
"",
|
||||
"Trailer",
|
||||
trailer,
|
||||
"",
|
||||
Qualities.Unknown.value
|
||||
),
|
||||
null,
|
||||
startPosition = 0L,
|
||||
subtitles = emptySet(),
|
||||
subtitle = null,
|
||||
autoPlay = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setTrailers(trailers: List<String>?) {
|
||||
context?.let { ctx ->
|
||||
if (ctx.isTvSettings()) return
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
val showTrailers =
|
||||
settingsManager.getBoolean(ctx.getString(R.string.show_trailers_key), true)
|
||||
if (!showTrailers) return
|
||||
currentTrailers = trailers ?: emptyList()
|
||||
loadTrailer()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setActors(actors: List<ActorData>?) {
|
||||
if (actors.isNullOrEmpty()) {
|
||||
result_cast_text?.isVisible = false
|
||||
|
@ -779,7 +828,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
|
|||
} else if (dy < -5) {
|
||||
result_bookmark_fab?.extend()
|
||||
}
|
||||
result_poster_blur_holder?.translationY = -scrollY.toFloat()
|
||||
//result_poster_blur_holder?.translationY = -scrollY.toFloat()
|
||||
})
|
||||
|
||||
result_back.setOnClickListener {
|
||||
|
@ -1353,7 +1402,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
|
|||
val newList = list.filter { it.isSynced && it.hasAccount }
|
||||
|
||||
result_mini_sync?.isVisible = newList.isNotEmpty()
|
||||
(result_mini_sync?.adapter as? ImageAdapter?)?.updateList(newList.map { it.icon })
|
||||
(result_mini_sync?.adapter as? ImageAdapter?)?.updateList(newList.mapNotNull { it.icon })
|
||||
}
|
||||
|
||||
observe(syncModel.syncIds) {
|
||||
|
@ -1508,7 +1557,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
|
|||
when (startAction) {
|
||||
START_ACTION_RESUME_LATEST -> {
|
||||
for (ep in episodeList) {
|
||||
println("WATCH STATUS::: S${ep.season} E ${ep.episode} - ${ep.getWatchProgress()}")
|
||||
//println("WATCH STATUS::: S${ep.season} E ${ep.episode} - ${ep.getWatchProgress()}")
|
||||
if (ep.getWatchProgress() > 0.90f) { // watched too much
|
||||
continue
|
||||
}
|
||||
|
@ -1528,7 +1577,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
|
|||
var found = false
|
||||
for (ep in episodeList) {
|
||||
if (ep.id == startValue) { // watched too much
|
||||
println("WATCH STATUS::: START_ACTION_LOAD_EP S${ep.season} E ${ep.episode} - ${ep.getWatchProgress()}")
|
||||
//println("WATCH STATUS::: START_ACTION_LOAD_EP S${ep.season} E ${ep.episode} - ${ep.getWatchProgress()}")
|
||||
handleAction(EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, ep))
|
||||
found = true
|
||||
break
|
||||
|
@ -1537,7 +1586,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
|
|||
if (!found)
|
||||
for (ep in episodeList) {
|
||||
if (ep.episode == resumeEpisode && ep.season == resumeSeason) {
|
||||
println("WATCH STATUS::: START_ACTION_LOAD_EP S${ep.season} E ${ep.episode} - ${ep.getWatchProgress()}")
|
||||
//println("WATCH STATUS::: START_ACTION_LOAD_EP S${ep.season} E ${ep.episode} - ${ep.getWatchProgress()}")
|
||||
handleAction(
|
||||
EpisodeClickEvent(
|
||||
ACTION_PLAY_EPISODE_IN_PLAYER,
|
||||
|
@ -1729,6 +1778,8 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
|
|||
setRecommendations(d.recommendations, null)
|
||||
setActors(d.actors)
|
||||
|
||||
setTrailers(d.trailers)
|
||||
|
||||
if (syncModel.addSyncs(d.syncData)) {
|
||||
syncModel.updateMetaAndUser()
|
||||
syncModel.updateSynced()
|
||||
|
@ -1741,7 +1792,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
|
|||
val posterImageLink = d.posterUrl
|
||||
if (!posterImageLink.isNullOrEmpty()) {
|
||||
result_poster?.setImage(posterImageLink, d.posterHeaders)
|
||||
result_poster_blur?.setImageBlur(posterImageLink, 10, 3, d.posterHeaders)
|
||||
//result_poster_blur?.setImageBlur(posterImageLink, 10, 3, d.posterHeaders)
|
||||
//Full screen view of Poster image
|
||||
if (context?.isTrueTvSettings() == false) // Poster not clickable on tv
|
||||
result_poster_holder?.setOnClickListener {
|
||||
|
@ -1770,7 +1821,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
|
|||
|
||||
} else {
|
||||
result_poster?.setImageResource(R.drawable.default_cover)
|
||||
result_poster_blur?.setImageResource(R.drawable.default_cover)
|
||||
//result_poster_blur?.setImageResource(R.drawable.default_cover)
|
||||
}
|
||||
|
||||
result_poster_holder?.visibility = VISIBLE
|
||||
|
@ -1788,7 +1839,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
|
|||
}
|
||||
result_description.setOnClickListener {
|
||||
val builder: AlertDialog.Builder =
|
||||
AlertDialog.Builder(requireContext())
|
||||
AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom)
|
||||
builder.setMessage(d.plot)
|
||||
.setTitle(if (d.type == TvType.Torrent) R.string.torrent_plot else R.string.result_plot)
|
||||
.show()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,9 +7,9 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.lagradost.cloudstream3.apmap
|
||||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.aniListApi
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||
import com.lagradost.cloudstream3.utils.SyncUtil
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -20,7 +20,7 @@ data class CurrentSynced(
|
|||
val idPrefix: String,
|
||||
val isSynced: Boolean,
|
||||
val hasAccount: Boolean,
|
||||
val icon: Int,
|
||||
val icon: Int?,
|
||||
)
|
||||
|
||||
class SyncViewModel : ViewModel() {
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.ui.search
|
|||
import android.app.Activity
|
||||
import android.widget.Toast
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_PLAY_FILE
|
||||
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
|
||||
import com.lagradost.cloudstream3.ui.download.DownloadClickEvent
|
||||
|
@ -20,25 +21,30 @@ object SearchHelper {
|
|||
}
|
||||
SEARCH_ACTION_PLAY_FILE -> {
|
||||
if (card is DataStoreHelper.ResumeWatchingResult) {
|
||||
if (card.isFromDownload) {
|
||||
handleDownloadClick(
|
||||
activity, card.name, DownloadClickEvent(
|
||||
DOWNLOAD_ACTION_PLAY_FILE,
|
||||
VideoDownloadHelper.DownloadEpisodeCached(
|
||||
card.name,
|
||||
card.posterUrl,
|
||||
card.episode ?: 0,
|
||||
card.season,
|
||||
card.id!!,
|
||||
card.parentId ?: return,
|
||||
null,
|
||||
null,
|
||||
System.currentTimeMillis()
|
||||
val id = card.id
|
||||
if(id == null) {
|
||||
showToast(activity, R.string.error_invalid_id, Toast.LENGTH_SHORT)
|
||||
} else {
|
||||
if (card.isFromDownload) {
|
||||
handleDownloadClick(
|
||||
activity, card.name, DownloadClickEvent(
|
||||
DOWNLOAD_ACTION_PLAY_FILE,
|
||||
VideoDownloadHelper.DownloadEpisodeCached(
|
||||
card.name,
|
||||
card.posterUrl,
|
||||
card.episode ?: 0,
|
||||
card.season,
|
||||
id,
|
||||
card.parentId ?: return,
|
||||
null,
|
||||
null,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
activity.loadSearchResult(card, START_ACTION_LOAD_EP, card.id)
|
||||
} else {
|
||||
activity.loadSearchResult(card, START_ACTION_LOAD_EP, id)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
handleSearchClickCallback(
|
||||
|
|
|
@ -3,10 +3,10 @@ package com.lagradost.cloudstream3.ui.search
|
|||
import com.lagradost.cloudstream3.SearchQuality
|
||||
import com.lagradost.cloudstream3.SearchResponse
|
||||
import com.lagradost.cloudstream3.TvType
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
|
||||
|
||||
class SyncSearchViewModel {
|
||||
private val repos = OAuth2API.SyncApis
|
||||
private val repos = SyncApis
|
||||
|
||||
data class SyncSearchResultSearchResponse(
|
||||
override val name: String,
|
||||
|
@ -18,5 +18,4 @@ class SyncSearchViewModel {
|
|||
override var quality: SearchQuality? = null,
|
||||
override var posterHeaders: Map<String, String>? = null,
|
||||
) : SearchResponse
|
||||
|
||||
}
|
|
@ -8,13 +8,13 @@ import android.widget.TextView
|
|||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API
|
||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
|
||||
class AccountClickCallback(val action: Int, val view : View, val card: OAuth2API.LoginInfo)
|
||||
class AccountClickCallback(val action: Int, val view: View, val card: AuthAPI.LoginInfo)
|
||||
|
||||
class AccountAdapter(
|
||||
val cardList: List<OAuth2API.LoginInfo>,
|
||||
val cardList: List<AuthAPI.LoginInfo>,
|
||||
val layout: Int = R.layout.account_single,
|
||||
private val clickCallback: (AccountClickCallback) -> Unit
|
||||
) :
|
||||
|
@ -48,15 +48,13 @@ class AccountAdapter(
|
|||
private val pfp: ImageView = itemView.findViewById(R.id.account_profile_picture)!!
|
||||
private val accountName: TextView = itemView.findViewById(R.id.account_name)!!
|
||||
|
||||
fun bind(card: OAuth2API.LoginInfo) {
|
||||
fun bind(card: AuthAPI.LoginInfo) {
|
||||
// just in case name is null account index will show, should never happened
|
||||
accountName.text = card.name ?: "%s %d".format(accountName.context.getString(R.string.account), card.accountIndex)
|
||||
if(card.profilePicture.isNullOrEmpty()) {
|
||||
pfp.isVisible = false
|
||||
} else {
|
||||
pfp.isVisible = true
|
||||
pfp.setImage(card.profilePicture)
|
||||
}
|
||||
accountName.text = card.name ?: "%s %d".format(
|
||||
accountName.context.getString(R.string.account),
|
||||
card.accountIndex
|
||||
)
|
||||
pfp.isVisible = pfp.setImage(card.profilePicture)
|
||||
|
||||
itemView.setOnClickListener {
|
||||
clickCallback.invoke(AccountClickCallback(0, itemView, card))
|
||||
|
|
|
@ -1,32 +1,54 @@
|
|||
package com.lagradost.cloudstream3.ui.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.widget.ImageView
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.nginxApi
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.openSubtitlesApi
|
||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.beneneCount
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
import kotlinx.android.synthetic.main.account_managment.*
|
||||
import kotlinx.android.synthetic.main.account_switch.*
|
||||
import kotlinx.android.synthetic.main.add_account_input.*
|
||||
|
||||
class SettingsAccount : PreferenceFragmentCompat() {
|
||||
private fun showLoginInfo(api: AccountManager, info: OAuth2API.LoginInfo) {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setUpToolbar(R.string.category_account)
|
||||
}
|
||||
|
||||
private fun showLoginInfo(api: AccountManager, info: AuthAPI.LoginInfo) {
|
||||
val builder =
|
||||
AlertDialog.Builder(context ?: return, R.style.AlertDialogCustom)
|
||||
.setView(R.layout.account_managment)
|
||||
val dialog = builder.show()
|
||||
|
||||
dialog.findViewById<ImageView>(R.id.account_profile_picture)?.setImage(info.profilePicture)
|
||||
dialog.findViewById<TextView>(R.id.account_logout)?.setOnClickListener {
|
||||
dialog.account_main_profile_picture_holder?.isVisible =
|
||||
dialog.account_main_profile_picture?.setImage(info.profilePicture) == true
|
||||
|
||||
dialog.account_logout?.setOnClickListener {
|
||||
api.logOut()
|
||||
dialog.dismissSafe(activity)
|
||||
}
|
||||
|
@ -34,13 +56,93 @@ class SettingsAccount : PreferenceFragmentCompat() {
|
|||
(info.name ?: context?.getString(R.string.no_data))?.let {
|
||||
dialog.findViewById<TextView>(R.id.account_name)?.text = it
|
||||
}
|
||||
dialog.findViewById<TextView>(R.id.account_site)?.text = api.name
|
||||
dialog.findViewById<TextView>(R.id.account_switch_account)?.setOnClickListener {
|
||||
|
||||
dialog.account_site?.text = api.name
|
||||
dialog.account_switch_account?.setOnClickListener {
|
||||
dialog.dismissSafe(activity)
|
||||
showAccountSwitch(it.context, api)
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private fun addAccount(api: AccountManager) {
|
||||
try {
|
||||
when (api) {
|
||||
is OAuth2API -> {
|
||||
api.authenticate()
|
||||
}
|
||||
is InAppAuthAPI -> {
|
||||
val builder =
|
||||
AlertDialog.Builder(context ?: return, R.style.AlertDialogCustom)
|
||||
.setView(R.layout.add_account_input)
|
||||
val dialog = builder.show()
|
||||
dialog.login_email_input?.isVisible = api.requiresEmail
|
||||
dialog.login_password_input?.isVisible = api.requiresPassword
|
||||
dialog.login_server_input?.isVisible = api.requiresServer
|
||||
dialog.login_username_input?.isVisible = api.requiresUsername
|
||||
dialog.create_account?.isGone = api.createAccountUrl.isNullOrBlank()
|
||||
dialog.create_account?.setOnClickListener {
|
||||
val i = Intent(Intent.ACTION_VIEW)
|
||||
i.data = Uri.parse(api.createAccountUrl)
|
||||
try {
|
||||
startActivity(i)
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
dialog.text1?.text = api.name
|
||||
|
||||
if (api.storesPasswordInPlainText) {
|
||||
api.getLatestLoginData()?.let { data ->
|
||||
dialog.login_email_input?.setText(data.email ?: "")
|
||||
dialog.login_server_input?.setText(data.server ?: "")
|
||||
dialog.login_username_input?.setText(data.username ?: "")
|
||||
dialog.login_password_input?.setText(data.password ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
dialog.apply_btt?.setOnClickListener {
|
||||
val loginData = InAppAuthAPI.LoginData(
|
||||
username = if (api.requiresUsername) dialog.login_username_input?.text?.toString() else null,
|
||||
password = if (api.requiresPassword) dialog.login_password_input?.text?.toString() else null,
|
||||
email = if (api.requiresEmail) dialog.login_email_input?.text?.toString() else null,
|
||||
server = if (api.requiresServer) dialog.login_server_input?.text?.toString() else null,
|
||||
)
|
||||
ioSafe {
|
||||
val isSuccessful = try {
|
||||
api.login(loginData)
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
false
|
||||
}
|
||||
activity?.runOnUiThread {
|
||||
try {
|
||||
showToast(
|
||||
activity,
|
||||
getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format(
|
||||
api.name
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logError(e) // format might fail
|
||||
}
|
||||
}
|
||||
}
|
||||
dialog.dismissSafe(activity)
|
||||
}
|
||||
dialog.cancel_btt?.setOnClickListener {
|
||||
dialog.dismissSafe(activity)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
throw NotImplementedError("You are trying to add an account that has an unknown login method")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showAccountSwitch(context: Context, api: AccountManager) {
|
||||
val accounts = api.getAccounts() ?: return
|
||||
|
||||
|
@ -48,17 +150,14 @@ class SettingsAccount : PreferenceFragmentCompat() {
|
|||
AlertDialog.Builder(context, R.style.AlertDialogCustom).setView(R.layout.account_switch)
|
||||
val dialog = builder.show()
|
||||
|
||||
dialog.findViewById<TextView>(R.id.account_add)?.setOnClickListener {
|
||||
try {
|
||||
api.authenticate()
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
dialog.account_add?.setOnClickListener {
|
||||
addAccount(api)
|
||||
dialog?.dismissSafe(activity)
|
||||
}
|
||||
|
||||
val ogIndex = api.accountIndex
|
||||
|
||||
val items = ArrayList<OAuth2API.LoginInfo>()
|
||||
val items = ArrayList<AuthAPI.LoginInfo>()
|
||||
|
||||
for (index in accounts) {
|
||||
api.accountIndex = index
|
||||
|
@ -78,72 +177,30 @@ class SettingsAccount : PreferenceFragmentCompat() {
|
|||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
hideKeyboard()
|
||||
setPreferencesFromResource(R.xml.settings_credits_account, rootKey)
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
|
||||
getPref(R.string.legal_notice_key)?.setOnPreferenceClickListener {
|
||||
val builder: AlertDialog.Builder = AlertDialog.Builder(it.context)
|
||||
builder.setTitle(R.string.legal_notice)
|
||||
builder.setMessage(R.string.legal_notice_text)
|
||||
builder.show()
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
setPreferencesFromResource(R.xml.settings_account, rootKey)
|
||||
|
||||
val syncApis =
|
||||
listOf(
|
||||
Pair(R.string.mal_key, OAuth2API.malApi), Pair(
|
||||
R.string.anilist_key,
|
||||
OAuth2API.aniListApi
|
||||
)
|
||||
R.string.mal_key to malApi,
|
||||
R.string.anilist_key to aniListApi,
|
||||
R.string.opensubtitles_key to openSubtitlesApi,
|
||||
R.string.nginx_key to nginxApi,
|
||||
)
|
||||
|
||||
for ((key, api) in syncApis) {
|
||||
getPref(key)?.apply {
|
||||
title =
|
||||
getString(R.string.login_format).format(api.name, getString(R.string.account))
|
||||
setOnPreferenceClickListener { _ ->
|
||||
setOnPreferenceClickListener {
|
||||
val info = api.loginInfo()
|
||||
if (info != null) {
|
||||
showLoginInfo(api, info)
|
||||
} else {
|
||||
try {
|
||||
api.authenticate()
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
addAccount(api)
|
||||
}
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
try {
|
||||
beneneCount = settingsManager.getInt(getString(R.string.benene_count), 0)
|
||||
getPref(R.string.benene_count)?.let { pref ->
|
||||
pref.summary =
|
||||
if (beneneCount <= 0) getString(R.string.benene_count_text_none) else getString(
|
||||
R.string.benene_count_text
|
||||
).format(
|
||||
beneneCount
|
||||
)
|
||||
|
||||
pref.setOnPreferenceClickListener {
|
||||
try {
|
||||
beneneCount++
|
||||
settingsManager.edit().putInt(getString(R.string.benene_count), beneneCount)
|
||||
.apply()
|
||||
it.summary = getString(R.string.benene_count_text).format(beneneCount)
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -8,14 +8,21 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
|
||||
import com.lagradost.cloudstream3.ui.home.HomeFragment
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
import kotlinx.android.synthetic.main.main_settings.*
|
||||
import kotlinx.android.synthetic.main.settings_title_top.*
|
||||
import java.io.File
|
||||
|
||||
class SettingsFragment : Fragment() {
|
||||
|
@ -33,6 +40,18 @@ class SettingsFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
fun PreferenceFragmentCompat?.setUpToolbar(@StringRes title: Int) {
|
||||
if (this == null) return
|
||||
settings_toolbar?.apply {
|
||||
setTitle(title)
|
||||
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
|
||||
setNavigationOnClickListener {
|
||||
activity?.onBackPressed()
|
||||
}
|
||||
}
|
||||
context.fixPaddingStatusbar(settings_toolbar)
|
||||
}
|
||||
|
||||
fun getFolderSize(dir: File): Long {
|
||||
var size: Long = 0
|
||||
dir.listFiles()?.let {
|
||||
|
@ -75,9 +94,10 @@ class SettingsFragment : Fragment() {
|
|||
private fun Context.isAutoTv(): Boolean {
|
||||
val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager?
|
||||
// AFT = Fire TV
|
||||
val model = Build.MODEL.lowercase()
|
||||
return uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION || Build.MODEL.contains(
|
||||
"AFT"
|
||||
)
|
||||
) || model.contains("firestick") || model.contains("fire tv") || model.contains("chromecast")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,28 +114,39 @@ class SettingsFragment : Fragment() {
|
|||
activity?.navigate(id, Bundle())
|
||||
}
|
||||
|
||||
settings_player?.setOnClickListener {
|
||||
navigate(R.id.action_navigation_settings_to_navigation_settings_player)
|
||||
val isTrueTv = context?.isTrueTvSettings() == true
|
||||
|
||||
for (syncApi in accountManagers) {
|
||||
val login = syncApi.loginInfo()
|
||||
val pic = login?.profilePicture ?: continue
|
||||
if (settings_profile_pic?.setImage(
|
||||
pic,
|
||||
errorImageDrawable = HomeFragment.errorProfilePic
|
||||
) == true
|
||||
) {
|
||||
settings_profile_text?.text = login.name
|
||||
settings_profile?.isVisible = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
settings_credits?.setOnClickListener {
|
||||
navigate(R.id.action_navigation_settings_to_navigation_settings_account)
|
||||
}
|
||||
|
||||
settings_ui?.setOnClickListener {
|
||||
navigate(R.id.action_navigation_settings_to_navigation_settings_ui)
|
||||
}
|
||||
|
||||
settings_lang?.setOnClickListener {
|
||||
navigate(R.id.action_navigation_settings_to_navigation_settings_lang)
|
||||
}
|
||||
|
||||
settings_nginx?.setOnClickListener {
|
||||
navigate(R.id.action_navigation_settings_to_navigation_settings_nginx)
|
||||
}
|
||||
|
||||
settings_updates?.setOnClickListener {
|
||||
navigate(R.id.action_navigation_settings_to_navigation_settings_updates)
|
||||
listOf(
|
||||
Pair(settings_general, R.id.action_navigation_settings_to_navigation_settings_general),
|
||||
Pair(settings_player, R.id.action_navigation_settings_to_navigation_settings_player),
|
||||
Pair(settings_credits, R.id.action_navigation_settings_to_navigation_settings_account),
|
||||
Pair(settings_ui, R.id.action_navigation_settings_to_navigation_settings_ui),
|
||||
Pair(settings_lang, R.id.action_navigation_settings_to_navigation_settings_lang),
|
||||
Pair(settings_updates, R.id.action_navigation_settings_to_navigation_settings_updates),
|
||||
).forEach { (view, navigationId) ->
|
||||
view?.apply {
|
||||
setOnClickListener {
|
||||
navigate(navigationId)
|
||||
}
|
||||
if (isTrueTv) {
|
||||
isFocusable = true
|
||||
isFocusableInTouchMode = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package com.lagradost.cloudstream3.ui.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.lagradost.cloudstream3.*
|
||||
|
@ -10,6 +11,7 @@ import com.lagradost.cloudstream3.mvvm.logError
|
|||
import com.lagradost.cloudstream3.network.initClient
|
||||
import com.lagradost.cloudstream3.ui.APIRepository
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
||||
import com.lagradost.cloudstream3.utils.HOMEPAGE_API
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||
|
@ -44,6 +46,7 @@ class SettingsLang : PreferenceFragmentCompat() {
|
|||
Triple("", "Italian", "it"),
|
||||
Triple("", "Chinese", "zh"),
|
||||
Triple("", "Indonesian", "id"),
|
||||
Triple("", "Czech", "cs"),
|
||||
).sortedBy { it.second } //ye, we go alphabetical, so ppl don't put their lang on top
|
||||
|
||||
private fun getCurrentLocale(): String {
|
||||
|
@ -54,6 +57,11 @@ class SettingsLang : PreferenceFragmentCompat() {
|
|||
return conf?.locale?.language ?: "en"
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setUpToolbar(R.string.category_preferred_media_and_lang)
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
hideKeyboard()
|
||||
setPreferencesFromResource(R.xml.settings_media_lang, rootKey)
|
||||
|
@ -109,7 +117,7 @@ class SettingsLang : PreferenceFragmentCompat() {
|
|||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
getPref(R.string.locale_key)?.setOnPreferenceClickListener { pref ->
|
||||
getPref(R.string.locale_key)?.setOnPreferenceClickListener {
|
||||
val tempLangs = languages.toMutableList()
|
||||
//if (beneneCount > 100) {
|
||||
// tempLangs.add(Triple("\uD83E\uDD8D", "mmmm... monke", "mo"))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,61 +1,26 @@
|
|||
package com.lagradost.cloudstream3.ui.settings
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import android.view.View
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.lagradost.cloudstream3.AcraApplication
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.network.initClient
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getFolderSize
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
||||
import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath
|
||||
import java.io.File
|
||||
|
||||
class SettingsPlayer : PreferenceFragmentCompat() {
|
||||
// Open file picker
|
||||
private val pathPicker =
|
||||
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
|
||||
// It lies, it can be null if file manager quits.
|
||||
if (uri == null) return@registerForActivityResult
|
||||
val context = context ?: AcraApplication.context ?: return@registerForActivityResult
|
||||
// RW perms for the path
|
||||
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
|
||||
context.contentResolver.takePersistableUriPermission(uri, flags)
|
||||
|
||||
val file = UniFile.fromUri(context, uri)
|
||||
println("Selected URI path: $uri - Full path: ${file.filePath}")
|
||||
|
||||
// Stores the real URI using download_path_key
|
||||
// Important that the URI is stored instead of filepath due to permissions.
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.edit().putString(getString(R.string.download_path_key), uri.toString()).apply()
|
||||
|
||||
// From URI -> File path
|
||||
// File path here is purely for cosmetic purposes in settings
|
||||
(file.filePath ?: uri.toString()).let {
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.edit().putString(getString(R.string.download_path_pref), it).apply()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setUpToolbar(R.string.category_player)
|
||||
}
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
hideKeyboard()
|
||||
setPreferencesFromResource(R.xml.settings_player, rootKey)
|
||||
|
@ -80,24 +45,6 @@ class SettingsPlayer : PreferenceFragmentCompat() {
|
|||
}
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
getPref(R.string.dns_key)?.setOnPreferenceClickListener {
|
||||
val prefNames = resources.getStringArray(R.array.dns_pref)
|
||||
val prefValues = resources.getIntArray(R.array.dns_pref_values)
|
||||
|
||||
val currentDns =
|
||||
settingsManager.getInt(getString(R.string.dns_pref), 0)
|
||||
|
||||
activity?.showBottomDialog(
|
||||
prefNames.toList(),
|
||||
prefValues.indexOf(currentDns),
|
||||
getString(R.string.dns_pref),
|
||||
true,
|
||||
{}) {
|
||||
settingsManager.edit().putInt(getString(R.string.dns_pref), prefValues[it]).apply()
|
||||
(context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) }
|
||||
}
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
getPref(R.string.prefer_limit_title_key)?.setOnPreferenceClickListener {
|
||||
val prefNames = resources.getStringArray(R.array.limit_title_pref_names)
|
||||
|
@ -236,60 +183,6 @@ class SettingsPlayer : PreferenceFragmentCompat() {
|
|||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
}
|
||||
fun getDownloadDirs(): List<String> {
|
||||
return normalSafeApiCall {
|
||||
val defaultDir = VideoDownloadManager.getDownloadDir()?.filePath
|
||||
|
||||
// app_name_download_path = Cloudstream and does not change depending on release.
|
||||
// DOES NOT WORK ON SCOPED STORAGE.
|
||||
val secondaryDir =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) null else Environment.getExternalStorageDirectory().absolutePath +
|
||||
File.separator + resources.getString(R.string.app_name_download_path)
|
||||
val first = listOf(defaultDir, secondaryDir)
|
||||
(try {
|
||||
val currentDir = context?.getBasePath()?.let { it.first?.filePath ?: it.second }
|
||||
|
||||
(first +
|
||||
requireContext().getExternalFilesDirs("").mapNotNull { it.path } +
|
||||
currentDir)
|
||||
} catch (e: Exception) {
|
||||
first
|
||||
}).filterNotNull().distinct()
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
getPref(R.string.download_path_key)?.setOnPreferenceClickListener {
|
||||
val dirs = getDownloadDirs()
|
||||
|
||||
val currentDir =
|
||||
settingsManager.getString(getString(R.string.download_path_pref), null)
|
||||
?: VideoDownloadManager.getDownloadDir().toString()
|
||||
|
||||
activity?.showBottomDialog(
|
||||
dirs + listOf("Custom"),
|
||||
dirs.indexOf(currentDir),
|
||||
getString(R.string.download_path_pref),
|
||||
true,
|
||||
{}) {
|
||||
// Last = custom
|
||||
if (it == dirs.size) {
|
||||
try {
|
||||
pathPicker.launch(Uri.EMPTY)
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
} else {
|
||||
// Sets both visual and actual paths.
|
||||
// key = used path
|
||||
// pref = visual path
|
||||
settingsManager.edit()
|
||||
.putString(getString(R.string.download_path_key), dirs[it]).apply()
|
||||
settingsManager.edit()
|
||||
.putString(getString(R.string.download_path_pref), dirs[it]).apply()
|
||||
}
|
||||
}
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,18 +1,24 @@
|
|||
package com.lagradost.cloudstream3.ui.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||
|
||||
class SettingsUI : PreferenceFragmentCompat() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setUpToolbar(R.string.category_ui)
|
||||
}
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
hideKeyboard()
|
||||
setPreferencesFromResource(R.xml.settins_ui, rootKey)
|
||||
|
|
|
@ -4,14 +4,15 @@ import android.content.ClipData
|
|||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.lagradost.cloudstream3.CommonActivity
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
||||
import com.lagradost.cloudstream3.utils.BackupUtils.backup
|
||||
import com.lagradost.cloudstream3.utils.BackupUtils.restorePrompt
|
||||
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
|
||||
|
@ -26,11 +27,14 @@ import java.io.OutputStream
|
|||
import kotlin.concurrent.thread
|
||||
|
||||
class SettingsUpdates : PreferenceFragmentCompat() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setUpToolbar(R.string.category_updates)
|
||||
}
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
hideKeyboard()
|
||||
setPreferencesFromResource(R.xml.settings_updates, rootKey)
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
|
||||
//val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
|
||||
getPref(R.string.backup_key)?.setOnPreferenceClickListener {
|
||||
activity?.backup()
|
||||
|
|
|
@ -57,6 +57,8 @@ data class SaveCaptionStyle(
|
|||
@JsonProperty("elevation") var elevation: Int,
|
||||
/**in sp**/
|
||||
@JsonProperty("fixedTextSize") var fixedTextSize: Float?,
|
||||
@JsonProperty("removeCaptions") var removeCaptions: Boolean = false,
|
||||
@JsonProperty("removeBloat") var removeBloat: Boolean = true,
|
||||
)
|
||||
|
||||
const val DEF_SUBS_ELEVATION = 20
|
||||
|
@ -397,6 +399,15 @@ class SubtitlesFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
subtitles_remove_bloat?.isChecked = state.removeBloat
|
||||
subtitles_remove_bloat?.setOnCheckedChangeListener { _, b ->
|
||||
state.removeBloat = b
|
||||
}
|
||||
subtitles_remove_captions?.isChecked = state.removeCaptions
|
||||
subtitles_remove_captions?.setOnCheckedChangeListener { _, b ->
|
||||
state.removeCaptions = b
|
||||
}
|
||||
|
||||
subs_font_size.setOnLongClickListener { _ ->
|
||||
state.fixedTextSize = null
|
||||
//textView.context.updateState() // font size not changed
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.preference.PreferenceManager
|
|||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper
|
||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
|
||||
const val DOWNLOAD_HEADER_CACHE = "download_header_cache"
|
||||
|
||||
|
@ -13,9 +14,8 @@ const val DOWNLOAD_HEADER_CACHE = "download_header_cache"
|
|||
const val DOWNLOAD_EPISODE_CACHE = "download_episode_cache"
|
||||
const val VIDEO_PLAYER_BRIGHTNESS = "video_player_alpha_key"
|
||||
const val HOMEPAGE_API = "home_api_used"
|
||||
const val SEARCH_PROVIDER_TOGGLE = "settings_providers_toggle"
|
||||
|
||||
const val PREFERENCES_NAME: String = "rebuild_preference"
|
||||
const val PREFERENCES_NAME = "rebuild_preference"
|
||||
|
||||
object DataStore {
|
||||
val mapper: JsonMapper = JsonMapper.builder().addModule(KotlinModule())
|
||||
|
@ -34,17 +34,21 @@ object DataStore {
|
|||
}
|
||||
|
||||
fun <T> Context.setKeyRaw(path: String, value: T, isEditingAppSettings: Boolean = false) {
|
||||
val editor: SharedPreferences.Editor =
|
||||
if (isEditingAppSettings) getDefaultSharedPrefs().edit() else getSharedPrefs().edit()
|
||||
when (value) {
|
||||
is Boolean -> editor.putBoolean(path, value)
|
||||
is Int -> editor.putInt(path, value)
|
||||
is String -> editor.putString(path, value)
|
||||
is Float -> editor.putFloat(path, value)
|
||||
is Long -> editor.putLong(path, value)
|
||||
(value as? Set<String> != null) -> editor.putStringSet(path, value as Set<String>)
|
||||
try {
|
||||
val editor: SharedPreferences.Editor =
|
||||
if (isEditingAppSettings) getDefaultSharedPrefs().edit() else getSharedPrefs().edit()
|
||||
when (value) {
|
||||
is Boolean -> editor.putBoolean(path, value)
|
||||
is Int -> editor.putInt(path, value)
|
||||
is String -> editor.putString(path, value)
|
||||
is Float -> editor.putFloat(path, value)
|
||||
is Long -> editor.putLong(path, value)
|
||||
(value as? Set<String> != null) -> editor.putStringSet(path, value as Set<String>)
|
||||
}
|
||||
editor.apply()
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
fun Context.getDefaultSharedPrefs(): SharedPreferences {
|
||||
|
@ -69,11 +73,15 @@ object DataStore {
|
|||
}
|
||||
|
||||
fun Context.removeKey(path: String) {
|
||||
val prefs = getSharedPrefs()
|
||||
if (prefs.contains(path)) {
|
||||
val editor: SharedPreferences.Editor = prefs.edit()
|
||||
editor.remove(path)
|
||||
editor.apply()
|
||||
try {
|
||||
val prefs = getSharedPrefs()
|
||||
if (prefs.contains(path)) {
|
||||
val editor: SharedPreferences.Editor = prefs.edit()
|
||||
editor.remove(path)
|
||||
editor.apply()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -86,9 +94,13 @@ object DataStore {
|
|||
}
|
||||
|
||||
fun <T> Context.setKey(path: String, value: T) {
|
||||
val editor: SharedPreferences.Editor = getSharedPrefs().edit()
|
||||
editor.putString(path, mapper.writeValueAsString(value))
|
||||
editor.apply()
|
||||
try {
|
||||
val editor: SharedPreferences.Editor = getSharedPrefs().edit()
|
||||
editor.putString(path, mapper.writeValueAsString(value))
|
||||
editor.apply()
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Context.setKey(folder: String, path: String, value: T) {
|
||||
|
|
|
@ -132,7 +132,7 @@ object DataStoreHelper {
|
|||
)
|
||||
}
|
||||
|
||||
fun removeLastWatchedOld(parentId: Int?) {
|
||||
private fun removeLastWatchedOld(parentId: Int?) {
|
||||
if (parentId == null) return
|
||||
removeKey("$currentAccount/$RESULT_RESUME_WATCHING_OLD", parentId.toString())
|
||||
}
|
||||
|
|
|
@ -61,6 +61,7 @@ enum class Qualities(var value: Int) {
|
|||
0 -> "Auto"
|
||||
Unknown.value -> ""
|
||||
P2160.value -> "4K"
|
||||
null -> ""
|
||||
else -> "${qual}p"
|
||||
}
|
||||
}
|
||||
|
@ -117,6 +118,9 @@ val extractorApis: Array<ExtractorApi> = arrayOf(
|
|||
VizcloudInfo(),
|
||||
MwvnVizcloudInfo(),
|
||||
VizcloudDigital(),
|
||||
VizcloudCloud(),
|
||||
VideoVard(),
|
||||
VideovardSX(),
|
||||
Mp4Upload(),
|
||||
StreamTape(),
|
||||
|
||||
|
@ -168,6 +172,7 @@ val extractorApis: Array<ExtractorApi> = arrayOf(
|
|||
DoodSoExtractor(),
|
||||
DoodLaExtractor(),
|
||||
DoodWsExtractor(),
|
||||
DoodShExtractor(),
|
||||
|
||||
AsianLoad(),
|
||||
|
||||
|
@ -177,6 +182,11 @@ val extractorApis: Array<ExtractorApi> = arrayOf(
|
|||
ZplayerV2(),
|
||||
Upstream(),
|
||||
|
||||
Maxstream(),
|
||||
Tantifilm(),
|
||||
Userload(),
|
||||
Supervideo(),
|
||||
GuardareStream(),
|
||||
|
||||
// StreamSB.kt works
|
||||
// SBPlay(),
|
||||
|
@ -189,6 +199,7 @@ val extractorApis: Array<ExtractorApi> = arrayOf(
|
|||
GMPlayer(),
|
||||
|
||||
Blogger(),
|
||||
Solidfiles(),
|
||||
|
||||
Hxfile(),
|
||||
KotakAnimeid(),
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package com.lagradost.cloudstream3.utils
|
||||
|
||||
interface IOnBackPressed {
|
||||
fun onBackPressed(): Boolean
|
||||
}
|
|
@ -45,7 +45,6 @@ object SubtitleHelper {
|
|||
* @param looseCheck will use .contains in addition to .equals
|
||||
* */
|
||||
fun fromLanguageToTwoLetters(input: String, looseCheck: Boolean): String? {
|
||||
|
||||
languages.forEach {
|
||||
if (it.languageName.equals(input, ignoreCase = true)
|
||||
|| it.nativeName.equals(input, ignoreCase = true)
|
||||
|
|
10
app/src/main/res/drawable/baseline_fullscreen_24.xml
Normal file
10
app/src/main/res/drawable/baseline_fullscreen_24.xml
Normal 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>
|
10
app/src/main/res/drawable/baseline_fullscreen_exit_24.xml
Normal file
10
app/src/main/res/drawable/baseline_fullscreen_exit_24.xml
Normal 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>
|
|
@ -1,12 +1,11 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
app:tint="?attr/white">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3,3v8h8L11,3L3,3zM9,9L5,9L5,5h4v4zM3,13v8h8v-8L3,13zM9,19L5,19v-4h4v4zM13,3v8h8L21,3h-8zM19,9h-4L15,5h4v4zM13,13v8h8v-8h-8zM19,19h-4v-4h4v4z"
|
||||
android:fillType="evenOdd"/>
|
||||
android:tint="?attr/white">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3,3v8h8L11,3L3,3zM9,9L5,9L5,5h4v4zM3,13v8h8v-8L3,13zM9,19L5,19v-4h4v4zM13,3v8h8L21,3h-8zM19,9h-4L15,5h4v4zM13,13v8h8v-8h-8zM19,19h-4v-4h4v4z"
|
||||
android:fillType="evenOdd" />
|
||||
</vector>
|
||||
|
|
10
app/src/main/res/drawable/baseline_theaters_24.xml
Normal file
10
app/src/main/res/drawable/baseline_theaters_24.xml
Normal 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>
|
13
app/src/main/res/drawable/nginx.xml
Normal file
13
app/src/main/res/drawable/nginx.xml
Normal 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>
|
18
app/src/main/res/drawable/nginx_question.xml
Normal file
18
app/src/main/res/drawable/nginx_question.xml
Normal 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>
|
32
app/src/main/res/drawable/open_subtitles_icon.xml
Normal file
32
app/src/main/res/drawable/open_subtitles_icon.xml
Normal 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>
|
10
app/src/main/res/drawable/question_mark_24.xml
Normal file
10
app/src/main/res/drawable/question_mark_24.xml
Normal 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>
|
|
@ -16,13 +16,14 @@
|
|||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/account_main_profile_picture_holder"
|
||||
app:cardCornerRadius="100dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_width="35dp"
|
||||
android:layout_height="35dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/account_profile_picture"
|
||||
android:id="@+id/account_main_profile_picture"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
android:layout_width="match_parent">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/account_profile_picture_holder"
|
||||
android:layout_marginStart="10dp"
|
||||
app:cardCornerRadius="100dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
|
|
131
app/src/main/res/layout/add_account_input.xml
Normal file
131
app/src/main/res/layout/add_account_input.xml
Normal 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
Loading…
Reference in a new issue