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