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
				
			
		| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			@ -32,7 +32,6 @@
 | 
			
		|||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="match_parent"
 | 
			
		||||
            android:layout_rowWeight="1"
 | 
			
		||||
            android:autofillHints="Autofill Hint"
 | 
			
		||||
            android:inputType="text"
 | 
			
		||||
            tools:ignore="LabelFor" />
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										140
									
								
								app/src/main/res/layout/dialog_online_subtitles.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								app/src/main/res/layout/dialog_online_subtitles.xml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,140 @@
 | 
			
		|||
<?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:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="match_parent"
 | 
			
		||||
        android:background="@null"
 | 
			
		||||
        android:orientation="vertical">
 | 
			
		||||
 | 
			
		||||
    <LinearLayout
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="match_parent"
 | 
			
		||||
            android:layout_marginBottom="60dp"
 | 
			
		||||
            android:baselineAligned="false"
 | 
			
		||||
            android:orientation="horizontal">
 | 
			
		||||
 | 
			
		||||
        <LinearLayout
 | 
			
		||||
                android:id="@+id/sort_subtitles_holder"
 | 
			
		||||
                android:layout_width="0dp"
 | 
			
		||||
                android:layout_height="match_parent"
 | 
			
		||||
                android:layout_weight="50"
 | 
			
		||||
                android:orientation="vertical">
 | 
			
		||||
 | 
			
		||||
            <!--   android:id="@+id/subs_settings"                 android:foreground="?android:attr/selectableItemBackgroundBorderless"
 | 
			
		||||
-->
 | 
			
		||||
            <LinearLayout
 | 
			
		||||
                    android:layout_width="match_parent"
 | 
			
		||||
                    android:layout_height="wrap_content"
 | 
			
		||||
                    android:orientation="horizontal"
 | 
			
		||||
                    tools:ignore="UseCompoundDrawables">
 | 
			
		||||
 | 
			
		||||
                <FrameLayout
 | 
			
		||||
                        android:layout_width="match_parent"
 | 
			
		||||
                        android:layout_height="40dp"
 | 
			
		||||
                        android:layout_margin="10dp"
 | 
			
		||||
                        android:background="@drawable/search_background"
 | 
			
		||||
                        android:visibility="visible">
 | 
			
		||||
 | 
			
		||||
                    <FrameLayout
 | 
			
		||||
                            android:layout_width="match_parent"
 | 
			
		||||
                            android:layout_height="30dp"
 | 
			
		||||
                            android:layout_gravity="center_vertical"
 | 
			
		||||
                            android:layout_marginEnd="30dp">
 | 
			
		||||
 | 
			
		||||
                        <androidx.appcompat.widget.SearchView
 | 
			
		||||
                                android:id="@+id/subtitles_search"
 | 
			
		||||
                                app:iconifiedByDefault="false"
 | 
			
		||||
                                app:queryBackground="@color/transparent"
 | 
			
		||||
                                app:queryHint="@string/search_hint"
 | 
			
		||||
 | 
			
		||||
                                app:searchIcon="@drawable/search_icon"
 | 
			
		||||
                                android:layout_width="match_parent"
 | 
			
		||||
 | 
			
		||||
                                android:layout_height="match_parent"
 | 
			
		||||
                                android:layout_gravity="center_vertical"
 | 
			
		||||
 | 
			
		||||
                                android:imeOptions="actionSearch"
 | 
			
		||||
                                android:inputType="text"
 | 
			
		||||
                                android:paddingStart="-10dp"
 | 
			
		||||
                                tools:ignore="RtlSymmetry">
 | 
			
		||||
 | 
			
		||||
                            <androidx.core.widget.ContentLoadingProgressBar
 | 
			
		||||
                                    android:id="@+id/search_loading_bar"
 | 
			
		||||
                                    style="@style/Widget.AppCompat.ProgressBar"
 | 
			
		||||
                                    android:layout_width="20dp"
 | 
			
		||||
                                    android:layout_height="20dp"
 | 
			
		||||
                                    android:layout_gravity="center"
 | 
			
		||||
                                    android:layout_marginStart="-70dp"
 | 
			
		||||
                                    android:foregroundTint="@color/white"
 | 
			
		||||
                                    android:visibility="visible"
 | 
			
		||||
                                    tools:visibility="visible"
 | 
			
		||||
                                    android:progressTint="@color/white">
 | 
			
		||||
 | 
			
		||||
                            </androidx.core.widget.ContentLoadingProgressBar>
 | 
			
		||||
                            <!--app:queryHint="@string/search_hint"
 | 
			
		||||
                             android:background="@color/grayBackground" @color/itemBackground
 | 
			
		||||
                                        app:searchHintIcon="@drawable/search_white"
 | 
			
		||||
                                        -->
 | 
			
		||||
                        </androidx.appcompat.widget.SearchView>
 | 
			
		||||
                    </FrameLayout>
 | 
			
		||||
 | 
			
		||||
                    <ImageView
 | 
			
		||||
                            android:id="@+id/search_filter"
 | 
			
		||||
                            app:tint="?attr/textColor"
 | 
			
		||||
                            android:layout_width="25dp"
 | 
			
		||||
                            android:layout_height="25dp"
 | 
			
		||||
 | 
			
		||||
                            android:layout_gravity="end|center_vertical"
 | 
			
		||||
                            android:layout_margin="10dp"
 | 
			
		||||
                            android:background="?selectableItemBackgroundBorderless"
 | 
			
		||||
                            android:contentDescription="@string/change_providers_img_des"
 | 
			
		||||
                            android:nextFocusLeft="@id/main_search"
 | 
			
		||||
                            android:nextFocusRight="@id/main_search"
 | 
			
		||||
                            android:nextFocusUp="@id/nav_rail_view"
 | 
			
		||||
                            android:nextFocusDown="@id/search_autofit_results"
 | 
			
		||||
                            android:src="@drawable/ic_baseline_tune_24" />
 | 
			
		||||
                </FrameLayout>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            </LinearLayout>
 | 
			
		||||
 | 
			
		||||
            <ListView
 | 
			
		||||
                    android:id="@+id/subtitle_adapter"
 | 
			
		||||
                    android:layout_width="match_parent"
 | 
			
		||||
                    android:layout_height="match_parent"
 | 
			
		||||
 | 
			
		||||
                    android:layout_rowWeight="1"
 | 
			
		||||
                    android:background="?attr/primaryBlackBackground"
 | 
			
		||||
                    android:nextFocusLeft="@id/sort_providers"
 | 
			
		||||
                    android:nextFocusRight="@id/cancel_btt"
 | 
			
		||||
                    android:requiresFadingEdge="vertical"
 | 
			
		||||
                    tools:listfooter="@layout/sort_bottom_footer_add_choice"
 | 
			
		||||
                    tools:listitem="@layout/sort_bottom_single_choice" />
 | 
			
		||||
        </LinearLayout>
 | 
			
		||||
    </LinearLayout>
 | 
			
		||||
 | 
			
		||||
    <LinearLayout
 | 
			
		||||
            android:id="@+id/apply_btt_holder"
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="60dp"
 | 
			
		||||
            android:layout_gravity="bottom"
 | 
			
		||||
            android:layout_marginTop="-60dp"
 | 
			
		||||
            android:gravity="bottom|end"
 | 
			
		||||
            android:orientation="horizontal">
 | 
			
		||||
 | 
			
		||||
        <com.google.android.material.button.MaterialButton
 | 
			
		||||
                android:id="@+id/apply_btt"
 | 
			
		||||
                style="@style/WhiteButton"
 | 
			
		||||
                android:layout_width="wrap_content"
 | 
			
		||||
                android:layout_gravity="center_vertical|end"
 | 
			
		||||
                android:text="@string/sort_apply" />
 | 
			
		||||
 | 
			
		||||
        <com.google.android.material.button.MaterialButton
 | 
			
		||||
                android:id="@+id/cancel_btt"
 | 
			
		||||
                style="@style/BlackButton"
 | 
			
		||||
                android:layout_width="wrap_content"
 | 
			
		||||
                android:layout_gravity="center_vertical|end"
 | 
			
		||||
                android:text="@string/sort_cancel" />
 | 
			
		||||
    </LinearLayout>
 | 
			
		||||
</LinearLayout>
 | 
			
		||||
| 
						 | 
				
			
			@ -105,12 +105,10 @@
 | 
			
		|||
 | 
			
		||||
                    android:id="@+id/download_child_episode_download"
 | 
			
		||||
                    android:visibility="visible"
 | 
			
		||||
                    android:layout_marginEnd="10dp"
 | 
			
		||||
                    android:layout_marginStart="10dp"
 | 
			
		||||
                    android:layout_height="wrap_content"
 | 
			
		||||
                    android:layout_gravity="center_vertical"
 | 
			
		||||
                    android:padding="2dp"
 | 
			
		||||
                    android:layout_width="30dp"
 | 
			
		||||
                    android:padding="10dp"
 | 
			
		||||
                    android:layout_width="50dp"
 | 
			
		||||
                    android:background="?selectableItemBackgroundBorderless"
 | 
			
		||||
                    android:src="@drawable/ic_baseline_play_arrow_24"
 | 
			
		||||
                    app:tint="?attr/textColor"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue