Merge remote-tracking branch 'origin/master'

This commit is contained in:
Blatzar 2022-06-18 17:18:40 +02:00
commit b619ee9bf8
81 changed files with 1299 additions and 466 deletions

View file

@ -36,7 +36,7 @@ android {
targetSdkVersion 30 targetSdkVersion 30
versionCode 48 versionCode 48
versionName "2.10.28" versionName "2.10.29"
resValue "string", "app_version", resValue "string", "app_version",
"${defaultConfig.versionName}${versionNameSuffix ?: ""}" "${defaultConfig.versionName}${versionNameSuffix ?: ""}"
@ -74,6 +74,8 @@ android {
} }
} }
compileOptions { compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
@ -143,7 +145,7 @@ dependencies {
implementation 'com.jaredrummler:colorpicker:1.1.0' implementation 'com.jaredrummler:colorpicker:1.1.0'
//run JS //run JS
implementation 'org.mozilla:rhino:1.7R4' implementation 'org.mozilla:rhino:1.7.14'
// TorrentStream // TorrentStream
//implementation 'com.github.TorrentStream:TorrentStream-Android:2.7.0' //implementation 'com.github.TorrentStream:TorrentStream-Android:2.7.0'
@ -175,6 +177,11 @@ dependencies {
// used for subtitle decoding https://github.com/albfernandez/juniversalchardet // used for subtitle decoding https://github.com/albfernandez/juniversalchardet
implementation 'com.github.albfernandez:juniversalchardet:2.4.0' implementation 'com.github.albfernandez:juniversalchardet:2.4.0'
// play yt // slow af yt
implementation 'com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT' //implementation 'com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT'
// newpipe yt
implementation 'com.github.TeamNewPipe:NewPipeExtractor:master-SNAPSHOT'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
} }

View file

@ -155,11 +155,11 @@
<service <service
android:name=".services.VideoDownloadService" android:name=".services.VideoDownloadService"
android:enabled="true" android:enabled="true"
android:exported="false"></service> android:exported="false" />
<activity <activity
android:exported="false" android:exported="false"
android:name=".ui.ControllerActivity"></activity> android:name=".ui.ControllerActivity" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"

View file

@ -0,0 +1,83 @@
package com.lagradost.cloudstream3
import okhttp3.OkHttpClient
import okhttp3.RequestBody
import org.schabi.newpipe.extractor.downloader.Downloader
import org.schabi.newpipe.extractor.downloader.Request
import org.schabi.newpipe.extractor.downloader.Response
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import java.util.concurrent.TimeUnit
class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Downloader() {
private val client: OkHttpClient
override fun execute(request: Request): Response {
val httpMethod: String = request.httpMethod()
val url: String = request.url()
val headers: Map<String, List<String>> = request.headers()
val dataToSend: ByteArray? = request.dataToSend()
var requestBody: RequestBody? = null
if (dataToSend != null) {
requestBody = RequestBody.create(null, dataToSend)
}
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
.method(httpMethod, requestBody).url(url)
.addHeader("User-Agent", USER_AGENT)
for ((headerName, headerValueList) in headers) {
if (headerValueList.size > 1) {
requestBuilder.removeHeader(headerName)
for (headerValue in headerValueList) {
requestBuilder.addHeader(headerName, headerValue)
}
} else if (headerValueList.size == 1) {
requestBuilder.header(headerName, headerValueList[0])
}
}
val response = client.newCall(requestBuilder.build()).execute()
if (response.code == 429) {
response.close()
throw ReCaptchaException("reCaptcha Challenge requested", url)
}
val body = response.body
var responseBodyToReturn: String? = null
if (body != null) {
responseBodyToReturn = body.string()
}
val latestUrl = response.request.url.toString()
return Response(
response.code, response.message, response.headers.toMultimap(),
responseBodyToReturn, latestUrl
)
}
companion object {
private const val USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"
private var instance: DownloaderTestImpl? = null
/**
* It's recommended to call exactly once in the entire lifetime of the application.
*
* @param builder if null, default builder will be used
* @return a new instance of [DownloaderTestImpl]
*/
fun init(builder: OkHttpClient.Builder?): DownloaderTestImpl? {
instance = DownloaderTestImpl(
builder ?: OkHttpClient.Builder()
)
return instance
}
fun getInstance(): DownloaderTestImpl? {
if (instance == null) {
init(null)
}
return instance
}
}
init {
client = builder.readTimeout(30, TimeUnit.SECONDS).build()
}
}

View file

@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonProperty
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.LoadResponse.Companion.addMalId
import com.lagradost.cloudstream3.animeproviders.* 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.*
@ -17,8 +18,10 @@ import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.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.ui.settings.SettingsFragment.Companion.isTvSettings
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
import com.lagradost.cloudstream3.utils.loadExtractor
import okhttp3.Interceptor import okhttp3.Interceptor
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -39,7 +42,7 @@ object APIHolder {
private const val defProvider = 0 private const val defProvider = 0
val allProviders by lazy { val allProviders =
arrayListOf( arrayListOf(
// Movie providers // Movie providers
ElifilmsProvider(), ElifilmsProvider(),
@ -132,7 +135,7 @@ object APIHolder {
NginxProvider(), NginxProvider(),
OlgplyProvider(), OlgplyProvider(),
) )
}
fun initAll() { fun initAll() {
for (api in allProviders) { for (api in allProviders) {
@ -142,7 +145,7 @@ object APIHolder {
} }
var apis: List<MainAPI> = arrayListOf() var apis: List<MainAPI> = arrayListOf()
private var apiMap: Map<String, Int>? = null var apiMap: Map<String, Int>? = null
private fun initMap() { private fun initMap() {
if (apiMap == null) if (apiMap == null)
@ -297,6 +300,16 @@ object APIHolder {
return realSet return realSet
} }
fun Context.updateHasTrailers() {
LoadResponse.isTrailersEnabled = getHasTrailers()
}
private fun Context.getHasTrailers(): Boolean {
if (this.isTvSettings()) return false
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
return settingsManager.getBoolean(this.getString(R.string.show_trailers_key), true)
}
fun Context.filterProviderByPreferredMedia(hasHomePageIsRequired: Boolean = true): List<MainAPI> { fun Context.filterProviderByPreferredMedia(hasHomePageIsRequired: Boolean = true): List<MainAPI> {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val currentPrefMedia = val currentPrefMedia =
@ -357,6 +370,7 @@ abstract class MainAPI {
} }
fun overrideWithNewData(data: ProvidersInfoJson) { fun overrideWithNewData(data: ProvidersInfoJson) {
if (!canBeOverridden) return
this.name = data.name this.name = data.name
if (data.url.isNotBlank() && data.url != "NONE") if (data.url.isNotBlank() && data.url != "NONE")
this.mainUrl = data.url this.mainUrl = data.url
@ -366,10 +380,11 @@ abstract class MainAPI {
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
open var canBeOverridden: Boolean = true
//open val uniqueId : Int by lazy { this.name.hashCode() } // in case of duplicate providers you can have a shared id //open val uniqueId : Int by lazy { this.name.hashCode() } // in case of duplicate providers you can have a shared id
open val lang = "en" // ISO_639_1 check SubtitleHelper open var lang = "en" // ISO_639_1 check SubtitleHelper
/**If link is stored in the "data" string, so links can be instantly loaded*/ /**If link is stored in the "data" string, so links can be instantly loaded*/
open val instantLinkLoading = false open val instantLinkLoading = false
@ -852,7 +867,7 @@ interface LoadResponse {
var rating: Int? // 0-10000 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<ExtractorLink>?
var recommendations: List<SearchResponse>? var recommendations: List<SearchResponse>?
var actors: List<ActorData>? var actors: List<ActorData>?
var comingSoon: Boolean var comingSoon: Boolean
@ -862,6 +877,7 @@ interface LoadResponse {
companion object { companion object {
private val malIdPrefix = malApi.idPrefix private val malIdPrefix = malApi.idPrefix
private val aniListIdPrefix = aniListApi.idPrefix private val aniListIdPrefix = aniListApi.idPrefix
var isTrailersEnabled = true
@JvmName("addActorNames") @JvmName("addActorNames")
fun LoadResponse.addActors(actors: List<String>?) { fun LoadResponse.addActors(actors: List<String>?) {
@ -883,6 +899,14 @@ interface LoadResponse {
this.actors = actors?.map { actor -> ActorData(actor) } this.actors = actors?.map { actor -> ActorData(actor) }
} }
fun LoadResponse.getMalId() : String? {
return this.syncData[malIdPrefix]
}
fun LoadResponse.getAniListId() : String? {
return this.syncData[aniListIdPrefix]
}
fun LoadResponse.addMalId(id: Int?) { fun LoadResponse.addMalId(id: Int?) {
this.syncData[malIdPrefix] = (id ?: return).toString() this.syncData[malIdPrefix] = (id ?: return).toString()
} }
@ -895,27 +919,38 @@ interface LoadResponse {
addImdbId(imdbUrlToIdNullable(url)) addImdbId(imdbUrlToIdNullable(url))
} }
/**better to set trailers directly instead of calling this multiple times*/ /**better to call addTrailer with mutible trailers directly instead of calling this multiple times*/
fun LoadResponse.addTrailer(trailerUrl: String?) { suspend fun LoadResponse.addTrailer(trailerUrl: String?, referer: String? = null) {
if (trailerUrl == null) return if (!isTrailersEnabled || trailerUrl == null) return
try {
val newTrailers = loadExtractor(trailerUrl, referer)
addTrailer(newTrailers)
} catch (e: Exception) {
logError(e)
}
}
fun LoadResponse.addTrailer(newTrailers: List<ExtractorLink>) {
if (this.trailers == null) { if (this.trailers == null) {
this.trailers = listOf(trailerUrl) this.trailers = newTrailers
} else { } else {
val update = this.trailers?.toMutableList() val update = this.trailers?.toMutableList() ?: mutableListOf()
update?.add(trailerUrl) update.addAll(newTrailers)
this.trailers = update this.trailers = update
} }
} }
fun LoadResponse.addTrailer(trailerUrls: List<String>?) { suspend fun LoadResponse.addTrailer(trailerUrls: List<String>?, referer: String? = null) {
if(trailerUrls == null) return if (!isTrailersEnabled || trailerUrls == null) return
if (this.trailers == null) { val newTrailers = trailerUrls.apmap { trailerUrl ->
this.trailers = trailerUrls try {
} else { loadExtractor(trailerUrl, referer)
val update = this.trailers?.toMutableList() } catch (e: Exception) {
update?.addAll(trailerUrls) logError(e)
this.trailers = update emptyList()
} }
}.flatten().distinct()
addTrailer(newTrailers)
} }
fun LoadResponse.addImdbId(id: String?) { fun LoadResponse.addImdbId(id: String?) {
@ -995,7 +1030,7 @@ data class TorrentLoadResponse(
override var rating: Int? = null, override var rating: Int? = null,
override var tags: List<String>? = null, override var tags: List<String>? = null,
override var duration: Int? = null, override var duration: Int? = null,
override var trailers: List<String>? = null, override var trailers: List<ExtractorLink>? = null,
override var recommendations: List<SearchResponse>? = null, override var recommendations: List<SearchResponse>? = null,
override var actors: List<ActorData>? = null, override var actors: List<ActorData>? = null,
override var comingSoon: Boolean = false, override var comingSoon: Boolean = false,
@ -1023,7 +1058,7 @@ data class AnimeLoadResponse(
override var rating: Int? = null, override var rating: Int? = null,
override var duration: Int? = null, override var duration: Int? = null,
override var trailers: List<String>? = null, override var trailers: List<ExtractorLink>? = null,
override var recommendations: List<SearchResponse>? = null, override var recommendations: List<SearchResponse>? = null,
override var actors: List<ActorData>? = null, override var actors: List<ActorData>? = null,
override var comingSoon: Boolean = false, override var comingSoon: Boolean = false,
@ -1036,12 +1071,12 @@ fun AnimeLoadResponse.addEpisodes(status: DubStatus, episodes: List<Episode>?) {
this.episodes[status] = episodes this.episodes[status] = episodes
} }
fun MainAPI.newAnimeLoadResponse( suspend fun MainAPI.newAnimeLoadResponse(
name: String, name: String,
url: String, url: String,
type: TvType, type: TvType,
comingSoonIfNone: Boolean = true, comingSoonIfNone: Boolean = true,
initializer: AnimeLoadResponse.() -> Unit = { }, initializer: suspend AnimeLoadResponse.() -> Unit = { },
): AnimeLoadResponse { ): AnimeLoadResponse {
val builder = AnimeLoadResponse(name = name, url = url, apiName = this.name, type = type) val builder = AnimeLoadResponse(name = name, url = url, apiName = this.name, type = type)
builder.initializer() builder.initializer()
@ -1070,7 +1105,7 @@ data class MovieLoadResponse(
override var rating: Int? = null, override var rating: Int? = null,
override var tags: List<String>? = null, override var tags: List<String>? = null,
override var duration: Int? = null, override var duration: Int? = null,
override var trailers: List<String>? = null, override var trailers: List<ExtractorLink>? = null,
override var recommendations: List<SearchResponse>? = null, override var recommendations: List<SearchResponse>? = null,
override var actors: List<ActorData>? = null, override var actors: List<ActorData>? = null,
override var comingSoon: Boolean = false, override var comingSoon: Boolean = false,
@ -1078,12 +1113,12 @@ data class MovieLoadResponse(
override var posterHeaders: Map<String, String>? = null, override var posterHeaders: Map<String, String>? = null,
) : LoadResponse ) : LoadResponse
fun <T> MainAPI.newMovieLoadResponse( suspend fun <T> MainAPI.newMovieLoadResponse(
name: String, name: String,
url: String, url: String,
type: TvType, type: TvType,
data: T?, data: T?,
initializer: MovieLoadResponse.() -> Unit = { } initializer: suspend MovieLoadResponse.() -> Unit = { }
): MovieLoadResponse { ): MovieLoadResponse {
// just in case // just in case
if (data is String) return newMovieLoadResponse( if (data is String) return newMovieLoadResponse(
@ -1106,12 +1141,12 @@ fun <T> MainAPI.newMovieLoadResponse(
return builder return builder
} }
fun MainAPI.newMovieLoadResponse( suspend fun MainAPI.newMovieLoadResponse(
name: String, name: String,
url: String, url: String,
type: TvType, type: TvType,
dataUrl: String, dataUrl: String,
initializer: MovieLoadResponse.() -> Unit = { } initializer: suspend MovieLoadResponse.() -> Unit = { }
): MovieLoadResponse { ): MovieLoadResponse {
val builder = MovieLoadResponse( val builder = MovieLoadResponse(
name = name, name = name,
@ -1191,7 +1226,7 @@ data class TvSeriesLoadResponse(
override var rating: Int? = null, override var rating: Int? = null,
override var tags: List<String>? = null, override var tags: List<String>? = null,
override var duration: Int? = null, override var duration: Int? = null,
override var trailers: List<String>? = null, override var trailers: List<ExtractorLink>? = null,
override var recommendations: List<SearchResponse>? = null, override var recommendations: List<SearchResponse>? = null,
override var actors: List<ActorData>? = null, override var actors: List<ActorData>? = null,
override var comingSoon: Boolean = false, override var comingSoon: Boolean = false,
@ -1199,12 +1234,12 @@ data class TvSeriesLoadResponse(
override var posterHeaders: Map<String, String>? = null, override var posterHeaders: Map<String, String>? = null,
) : LoadResponse ) : LoadResponse
fun MainAPI.newTvSeriesLoadResponse( suspend fun MainAPI.newTvSeriesLoadResponse(
name: String, name: String,
url: String, url: String,
type: TvType, type: TvType,
episodes: List<Episode>, episodes: List<Episode>,
initializer: TvSeriesLoadResponse.() -> Unit = { } initializer: suspend TvSeriesLoadResponse.() -> Unit = { }
): TvSeriesLoadResponse { ): TvSeriesLoadResponse {
val builder = TvSeriesLoadResponse( val builder = TvSeriesLoadResponse(
name = name, name = name,

View file

@ -28,6 +28,7 @@ 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.APIHolder.initAll import com.lagradost.cloudstream3.APIHolder.initAll
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
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
@ -47,6 +48,7 @@ import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsGeneral
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
import com.lagradost.cloudstream3.utils.AppUtils.loadCache import com.lagradost.cloudstream3.utils.AppUtils.loadCache
import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.AppUtils.loadResult
@ -68,12 +70,14 @@ import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.cloudstream3.utils.UIHelper.requestRW
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.Requests
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.fragment_result_swipe.* import kotlinx.android.synthetic.main.fragment_result_swipe.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.schabi.newpipe.extractor.NewPipe
import java.io.File import java.io.File
import kotlin.concurrent.thread import kotlin.concurrent.thread
@ -364,9 +368,23 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
fun test() { fun test() {
//val youtubeLink = "https://www.youtube.com/watch?v=TxB48MEAmZw" /*thread {
val youtubeLink = "https://www.youtube.com/watch?v=Zxem9rqJ5S0"
val url = YoutubeStreamLinkHandlerFactory.getInstance().fromUrl(youtubeLink)
println("ID:::: ${url.id}")
NewPipe.init(DownloaderTestImpl.getInstance())
val service = ServiceList.YouTube
val s = object : YoutubeStreamExtractor(
service,
url
) {
}
s.fetchPage()
val streams = s.videoStreams
println("STREAMS: ${streams.map { "url = "+ it.url + " extra= " + it.height + "|" + it.isVideoOnly + "\n" }}")
}*/
/* /*
runBlocking { runBlocking {
@ -515,6 +533,26 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
apis = allProviders apis = allProviders
} }
try {
getKey<Array<SettingsGeneral.CustomSite>>(USER_PROVIDER_API)?.let { list ->
list.forEach { custom ->
allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass }
?.let {
allProviders.add(it.javaClass.newInstance().apply {
name = custom.name
lang = custom.lang
mainUrl = custom.url.trimEnd('/')
canBeOverridden = false
})
}
}
}
apis = allProviders
APIHolder.apiMap = null
} catch (e: Exception) {
logError(e)
}
loadThemes(this) loadThemes(this)
updateLocale() updateLocale()
app.initClient(this) app.initClient(this)
@ -576,6 +614,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
loadCache() loadCache()
test() test()
NewPipe.init(DownloaderTestImpl.getInstance())
updateHasTrailers()
/*nav_view.setOnNavigationItemSelectedListener { item -> /*nav_view.setOnNavigationItemSelectedListener { item ->
when (item.itemId) { when (item.itemId) {
R.id.navigation_home -> { R.id.navigation_home -> {

View file

@ -16,7 +16,7 @@ import org.jsoup.nodes.Element
class AnimeWorldProvider : MainAPI() { class AnimeWorldProvider : MainAPI() {
override var mainUrl = "https://www.animeworld.tv" override var mainUrl = "https://www.animeworld.tv"
override var name = "AnimeWorld" override var name = "AnimeWorld"
override val lang = "it" override var lang = "it"
override val hasMainPage = true override val hasMainPage = true
override val supportedTypes = setOf( override val supportedTypes = setOf(

View file

@ -13,7 +13,7 @@ class AnimefenixProvider:MainAPI() {
override var mainUrl = "https://animefenix.com" override var mainUrl = "https://animefenix.com"
override var name = "Animefenix" override var name = "Animefenix"
override val lang = "es" override var lang = "es"
override val hasMainPage = true override val hasMainPage = true
override val hasChromecastSupport = true override val hasChromecastSupport = true
override val hasDownloadSupport = true override val hasDownloadSupport = true

View file

@ -11,7 +11,7 @@ import kotlin.collections.ArrayList
class AnimeflvIOProvider:MainAPI() { class AnimeflvIOProvider:MainAPI() {
override var mainUrl = "https://animeflv.io" //Also scrapes from animeid.to override var mainUrl = "https://animeflv.io" //Also scrapes from animeid.to
override var name = "Animeflv.io" override var name = "Animeflv.io"
override val lang = "es" override var lang = "es"
override val hasMainPage = true override val hasMainPage = true
override val hasChromecastSupport = true override val hasChromecastSupport = true
override val hasDownloadSupport = true override val hasDownloadSupport = true

View file

@ -24,7 +24,7 @@ class AnimeflvnetProvider : MainAPI() {
override var mainUrl = "https://www3.animeflv.net" override var mainUrl = "https://www3.animeflv.net"
override var name = "Animeflv.net" override var name = "Animeflv.net"
override val lang = "es" override var lang = "es"
override val hasMainPage = true override val hasMainPage = true
override val hasChromecastSupport = true override val hasChromecastSupport = true
override val hasDownloadSupport = true override val hasDownloadSupport = true

View file

@ -11,7 +11,7 @@ import org.jsoup.nodes.Element
class DreamSubProvider : MainAPI() { class DreamSubProvider : MainAPI() {
override var mainUrl = "https://dreamsub.me" override var mainUrl = "https://dreamsub.me"
override var name = "DreamSub" override var name = "DreamSub"
override val lang = "it" override var lang = "it"
override val hasMainPage = true override val hasMainPage = true
override val supportedTypes = setOf( override val supportedTypes = setOf(

View file

@ -14,7 +14,7 @@ class GomunimeProvider : MainAPI() {
override var name = "Gomunime" override var name = "Gomunime"
override val hasQuickSearch = false override val hasQuickSearch = false
override val hasMainPage = true override val hasMainPage = true
override val lang = "id" override var lang = "id"
override val hasDownloadSupport = true override val hasDownloadSupport = true
override val supportedTypes = setOf( override val supportedTypes = setOf(

View file

@ -22,7 +22,7 @@ class JKAnimeProvider : MainAPI() {
override var mainUrl = "https://jkanime.net" override var mainUrl = "https://jkanime.net"
override var name = "JKAnime" override var name = "JKAnime"
override val lang = "es" override var lang = "es"
override val hasMainPage = true override val hasMainPage = true
override val hasChromecastSupport = true override val hasChromecastSupport = true
override val hasDownloadSupport = true override val hasDownloadSupport = true

View file

@ -12,7 +12,7 @@ class KuramanimeProvider : MainAPI() {
override var name = "Kuramanime" override var name = "Kuramanime"
override val hasQuickSearch = false override val hasQuickSearch = false
override val hasMainPage = true override val hasMainPage = true
override val lang = "id" override var lang = "id"
override val hasDownloadSupport = true override val hasDownloadSupport = true
override val supportedTypes = setOf( override val supportedTypes = setOf(

View file

@ -3,17 +3,18 @@ package com.lagradost.cloudstream3.animeproviders
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
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName
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.ArrayList
class KuronimeProvider : MainAPI() { class KuronimeProvider : MainAPI() {
override var mainUrl = "https://185.231.223.254" override var mainUrl = "https://185.231.223.254"
override var name = "Kuronime" override var name = "Kuronime"
override val hasQuickSearch = false override val hasQuickSearch = false
override val hasMainPage = true override val hasMainPage = true
override val lang = "id" override var lang = "id"
override val hasDownloadSupport = true override val hasDownloadSupport = true
override val supportedTypes = setOf( override val supportedTypes = setOf(
@ -139,7 +140,6 @@ class KuronimeProvider : MainAPI() {
plot = description plot = description
addTrailer(trailer) addTrailer(trailer)
this.tags = tags this.tags = tags
trailers = listOf(trailer)
} }
} }

View file

@ -24,7 +24,7 @@ class MonoschinosProvider : MainAPI() {
override var mainUrl = "https://monoschinos2.com" override var mainUrl = "https://monoschinos2.com"
override var name = "Monoschinos" override var name = "Monoschinos"
override val lang = "es" override var lang = "es"
override val hasMainPage = true override val hasMainPage = true
override val hasChromecastSupport = true override val hasChromecastSupport = true
override val hasDownloadSupport = true override val hasDownloadSupport = true

View file

@ -15,7 +15,7 @@ class MundoDonghuaProvider : MainAPI() {
override var mainUrl = "https://www.mundodonghua.com" override var mainUrl = "https://www.mundodonghua.com"
override var name = "MundoDonghua" override var name = "MundoDonghua"
override val lang = "es" override var lang = "es"
override val hasMainPage = true override val hasMainPage = true
override val hasChromecastSupport = true override val hasChromecastSupport = true
override val hasDownloadSupport = true override val hasDownloadSupport = true

View file

@ -11,7 +11,7 @@ class NeonimeProvider : MainAPI() {
override var name = "Neonime" override var name = "Neonime"
override val hasQuickSearch = false override val hasQuickSearch = false
override val hasMainPage = true override val hasMainPage = true
override val lang = "id" override var lang = "id"
override val hasDownloadSupport = true override val hasDownloadSupport = true
override val supportedTypes = setOf( override val supportedTypes = setOf(

View file

@ -34,7 +34,7 @@ class NineAnimeProvider : MainAPI() {
Pair("$mainUrl/ajax/home/widget?name=updated_sub&page=1", "Recently Updated (SUB)"), Pair("$mainUrl/ajax/home/widget?name=updated_sub&page=1", "Recently Updated (SUB)"),
Pair( Pair(
"$mainUrl/ajax/home/widget?name=updated_dub&page=1", "$mainUrl/ajax/home/widget?name=updated_dub&page=1",
"Recently Updated (DUB)(DUB)" "Recently Updated (DUB)"
), ),
Pair( Pair(
"$mainUrl/ajax/home/widget?name=updated_chinese&page=1", "$mainUrl/ajax/home/widget?name=updated_chinese&page=1",
@ -64,7 +64,8 @@ class NineAnimeProvider : MainAPI() {
} }
//Credits to https://github.com/jmir1 //Credits to https://github.com/jmir1
private val key = "c/aUAorINHBLxWTy3uRiPt8J+vjsOheFG1E0q2X9CYwDZlnmd4Kb5M6gSVzfk7pQ" //key credits to @Modder4869 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()
@ -175,7 +176,10 @@ class NineAnimeProvider : MainAPI() {
return app.get(url).document.select("ul.anime-list li").mapNotNull { return app.get(url).document.select("ul.anime-list li").mapNotNull {
val title = it.selectFirst("a.name")!!.text() val title = it.selectFirst("a.name")!!.text()
val href = val href =
fixUrlNull(it.selectFirst("a")!!.attr("href"))?.replace(Regex("(\\?ep=(\\d+)\$)"), "") fixUrlNull(it.selectFirst("a")!!.attr("href"))?.replace(
Regex("(\\?ep=(\\d+)\$)"),
""
)
?: return@mapNotNull null ?: return@mapNotNull null
val image = it.selectFirst("a.poster img")!!.attr("src") val image = it.selectFirst("a.poster img")!!.attr("src")
AnimeSearchResponse( AnimeSearchResponse(
@ -199,7 +203,8 @@ class NineAnimeProvider : MainAPI() {
override suspend fun load(url: String): LoadResponse? { override suspend fun load(url: String): LoadResponse? {
val validUrl = url.replace("https://9anime.to", mainUrl) val validUrl = url.replace("https://9anime.to", mainUrl)
val doc = app.get(validUrl).document val doc = app.get(validUrl).document
val animeid = doc.selectFirst("div.player-wrapper.watchpage")!!.attr("data-id") ?: return null val animeid =
doc.selectFirst("div.player-wrapper.watchpage")!!.attr("data-id") ?: return null
val animeidencoded = encode(getVrf(animeid) ?: return null) val animeidencoded = encode(getVrf(animeid) ?: return null)
val poster = doc.selectFirst("aside.main div.thumb div img")!!.attr("src") val poster = doc.selectFirst("aside.main div.thumb div img")!!.attr("src")
val title = doc.selectFirst(".info .title")!!.text() val title = doc.selectFirst(".info .title")!!.text()

View file

@ -15,7 +15,7 @@ class NontonAnimeIDProvider : MainAPI() {
override var name = "NontonAnimeID" override var name = "NontonAnimeID"
override val hasQuickSearch = false override val hasQuickSearch = false
override val hasMainPage = true override val hasMainPage = true
override val lang = "id" override var lang = "id"
override val hasDownloadSupport = true override val hasDownloadSupport = true
override val supportedTypes = setOf( override val supportedTypes = setOf(

View file

@ -13,7 +13,7 @@ class OploverzProvider : MainAPI() {
override var name = "Oploverz" override var name = "Oploverz"
override val hasQuickSearch = false override val hasQuickSearch = false
override val hasMainPage = true override val hasMainPage = true
override val lang = "id" override var lang = "id"
override val hasDownloadSupport = true override val hasDownloadSupport = true
override val supportedTypes = setOf( override val supportedTypes = setOf(

View file

@ -0,0 +1,76 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeStreamLinkHandlerFactory
import org.schabi.newpipe.extractor.stream.VideoStream
class YoutubeShortLinkExtractor : YoutubeExtractor() {
override val mainUrl = "https://youtu.be"
override fun getExtractorUrl(id: String): String {
return "$mainUrl/$id"
}
}
open class YoutubeExtractor : ExtractorApi() {
override val mainUrl = "https://www.youtube.com"
override val requiresReferer = false
override val name = "YouTube"
companion object {
private var ytVideos: MutableMap<String, List<VideoStream>> = mutableMapOf()
}
override fun getExtractorUrl(id: String): String {
return "$mainUrl/watch?v=$id"
}
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val streams = safeApiCall {
val streams = ytVideos[url] ?: let {
val link =
YoutubeStreamLinkHandlerFactory.getInstance().fromUrl(url)
val s = object : YoutubeStreamExtractor(
ServiceList.YouTube,
link
) {
}
s.fetchPage()
val streams = s.videoStreams ?: return@let emptyList()
ytVideos[url] = streams
streams
}
if (streams.isEmpty()) {
throw ErrorLoadingException("No Youtube streams")
}
streams
//streams.sortedBy { it.height }
// .firstOrNull { !it.isVideoOnly && it.height > 0 }
// ?: throw ErrorLoadingException("No valid Youtube stream")
}
if (streams is Resource.Success) {
return streams.value.mapNotNull {
if (it.isVideoOnly || it.height <= 0) return@mapNotNull null
ExtractorLink(
this.name,
this.name,
it.url ?: return@mapNotNull null,
"",
it.height
)
}
} else {
return null
}
}
}

View file

@ -12,7 +12,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
class CrossTmdbProvider : TmdbProvider() { class CrossTmdbProvider : TmdbProvider() {
override var name = "MultiMovie" override var name = "MultiMovie"
override val apiName = "MultiMovie" override val apiName = "MultiMovie"
override val lang = "en" override var lang = "en"
override val useMetaLoadResponse = true override val useMetaLoadResponse = true
override val usesWebView = true override val usesWebView = true
override val supportedTypes = setOf(TvType.Movie) override val supportedTypes = setOf(TvType.Movie)

View file

@ -12,7 +12,7 @@ import com.lagradost.cloudstream3.utils.SyncUtil
// wont be implemented // wont be implemented
class MultiAnimeProvider : MainAPI() { class MultiAnimeProvider : MainAPI() {
override var name = "MultiAnime" override var name = "MultiAnime"
override val lang = "en" override var 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 = aniListApi private val syncApi: SyncAPI = aniListApi
@ -61,7 +61,7 @@ class MultiAnimeProvider : MainAPI() {
plot = res.synopsis plot = res.synopsis
tags = res.genres tags = res.genres
rating = res.publicScore rating = res.publicScore
addTrailer(res.trailerUrl) addTrailer(res.trailers)
addAniListId(res.id.toIntOrNull()) addAniListId(res.id.toIntOrNull())
recommendations = res.recommendations recommendations = res.recommendations
} }

View file

@ -95,7 +95,7 @@ open class TmdbProvider : MainAPI() {
} }
} }
private fun TvShow.toLoadResponse(): TvSeriesLoadResponse { private suspend fun TvShow.toLoadResponse(): TvSeriesLoadResponse {
val episodes = this.seasons?.filter { !disableSeasonZero || (it.season_number ?: 0) != 0 } val episodes = this.seasons?.filter { !disableSeasonZero || (it.season_number ?: 0) != 0 }
?.mapNotNull { season -> ?.mapNotNull { season ->
season.episodes?.map { episode -> season.episodes?.map { episode ->
@ -167,7 +167,7 @@ open class TmdbProvider : MainAPI() {
} }
} }
private fun Movie.toLoadResponse(): MovieLoadResponse { private suspend 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(
this.imdb_id, this.imdb_id,

View file

@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.Qualities
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
class AkwamProvider : MainAPI() { class AkwamProvider : MainAPI() {
override val lang = "ar" override var lang = "ar"
override var mainUrl = "https://akwam.to" override var mainUrl = "https://akwam.to"
override var name = "Akwam" override var name = "Akwam"
override val usesWebView = false override val usesWebView = false

View file

@ -5,9 +5,11 @@ import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
class AltadefinizioneProvider : MainAPI() { class AltadefinizioneProvider : MainAPI() {
override val lang = "it" override var lang = "it"
override var mainUrl = "https://altadefinizione.hair" override var mainUrl = "https://altadefinizione.hair"
override var name = "Altadefinizione" override var name = "Altadefinizione"
override val hasMainPage = true override val hasMainPage = true
@ -111,6 +113,9 @@ class AltadefinizioneProvider : MainAPI() {
} }
val tags: List<String> = document.select("#details > li:nth-child(1) > a").map { it.text() } val tags: List<String> = document.select("#details > li:nth-child(1) > a").map { it.text() }
val trailerurl = document.selectFirst("#showtrailer > div > div > iframe")!!.attr("src")
return newMovieLoadResponse( return newMovieLoadResponse(
title, title,
url, url,
@ -125,6 +130,7 @@ class AltadefinizioneProvider : MainAPI() {
this.duration = null this.duration = null
this.actors = actors this.actors = actors
this.tags = tags this.tags = tags
addTrailer(trailerurl)
} }
} }

View file

@ -6,7 +6,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.loadExtractor import com.lagradost.cloudstream3.utils.loadExtractor
class CineblogProvider : MainAPI() { class CineblogProvider : MainAPI() {
override val lang = "it" override var lang = "it"
override var mainUrl = "https://cb01.rip" override var mainUrl = "https://cb01.rip"
override var name = "CineBlog" override var name = "CineBlog"
override val hasMainPage = true override val hasMainPage = true

View file

@ -9,7 +9,7 @@ import com.lagradost.cloudstream3.utils.loadExtractor
class CinecalidadProvider:MainAPI() { class CinecalidadProvider:MainAPI() {
override var mainUrl = "https://cinecalidad.lol" override var mainUrl = "https://cinecalidad.lol"
override var name = "Cinecalidad" override var name = "Cinecalidad"
override val lang = "es" override var lang = "es"
override val hasMainPage = true override val hasMainPage = true
override val hasChromecastSupport = true override val hasChromecastSupport = true
override val hasDownloadSupport = true override val hasDownloadSupport = true

View file

@ -10,7 +10,7 @@ import com.lagradost.cloudstream3.utils.loadExtractor
class CuevanaProvider : MainAPI() { class CuevanaProvider : MainAPI() {
override var mainUrl = "https://cuevana3.me" override var mainUrl = "https://cuevana3.me"
override var name = "Cuevana" override var name = "Cuevana"
override val lang = "es" override var lang = "es"
override val hasMainPage = true override val hasMainPage = true
override val hasChromecastSupport = true override val hasChromecastSupport = true
override val hasDownloadSupport = true override val hasDownloadSupport = true

View file

@ -23,7 +23,7 @@ class DoramasYTProvider : MainAPI() {
override var mainUrl = "https://doramasyt.com" override var mainUrl = "https://doramasyt.com"
override var name = "DoramasYT" override var name = "DoramasYT"
override val lang = "es" override var lang = "es"
override val hasMainPage = true override val hasMainPage = true
override val hasChromecastSupport = true override val hasChromecastSupport = true
override val hasDownloadSupport = true override val hasDownloadSupport = true

View file

@ -15,7 +15,7 @@ class DramaidProvider : MainAPI() {
override var name = "DramaId" override var name = "DramaId"
override val hasQuickSearch = false override val hasQuickSearch = false
override val hasMainPage = true override val hasMainPage = true
override val lang = "id" override var lang = "id"
override val hasDownloadSupport = true override val hasDownloadSupport = true
override val hasChromecastSupport = false override val hasChromecastSupport = false
override val supportedTypes = setOf(TvType.AsianDrama) override val supportedTypes = setOf(TvType.AsianDrama)

View file

@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
class EgyBestProvider : MainAPI() { class EgyBestProvider : MainAPI() {
override val lang = "ar" override var lang = "ar"
override var mainUrl = "https://www.egy.best" override var mainUrl = "https://www.egy.best"
override var name = "EgyBest" override var name = "EgyBest"
override val usesWebView = false override val usesWebView = false

View file

@ -1,19 +1,20 @@
package com.lagradost.cloudstream3.movieproviders package com.lagradost.cloudstream3.movieproviders
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.ExtractorLink
import kotlin.collections.ArrayList import com.lagradost.cloudstream3.utils.loadExtractor
class ElifilmsProvider : MainAPI() { class ElifilmsProvider : MainAPI() {
override var mainUrl: String = "https://elifilms.net" override var mainUrl: String = "https://elifilms.net"
override var name: String = "Elifilms" override var name: String = "Elifilms"
override val lang = "es" override var lang = "es"
override val hasMainPage = true override val hasMainPage = true
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.Movie, TvType.Movie,
) )
override suspend fun getMainPage(): HomePageResponse { override suspend fun getMainPage(): HomePageResponse {
val items = ArrayList<HomePageList>() val items = ArrayList<HomePageList>()
val newest = app.get(mainUrl).document.selectFirst("a.fav_link.premiera")?.attr("href") val newest = app.get(mainUrl).document.selectFirst("a.fav_link.premiera")?.attr("href")
@ -42,6 +43,7 @@ class ElifilmsProvider:MainAPI() {
if (items.size <= 0) throw ErrorLoadingException() if (items.size <= 0) throw ErrorLoadingException()
return HomePageResponse(items) return HomePageResponse(items)
} }
override suspend fun search(query: String): List<SearchResponse> { override suspend fun search(query: String): List<SearchResponse> {
val url = "$mainUrl/?s=$query" val url = "$mainUrl/?s=$query"
val doc = app.get(url).document val doc = app.get(url).document
@ -52,6 +54,7 @@ class ElifilmsProvider:MainAPI() {
(MovieSearchResponse(name, href, this.name, TvType.Movie, poster, null)) (MovieSearchResponse(name, href, this.name, TvType.Movie, poster, null))
} }
} }
override suspend fun load(url: String): LoadResponse { override suspend fun load(url: String): LoadResponse {
val document = app.get(url, timeout = 120).document val document = app.get(url, timeout = 120).document
val title = document.selectFirst(".post_title h1")?.text() ?: "" val title = document.selectFirst(".post_title h1")?.text() ?: ""
@ -73,6 +76,7 @@ class ElifilmsProvider:MainAPI() {
tags tags
) )
} }
override suspend fun loadLinks( override suspend fun loadLinks(
data: String, data: String,
isCasting: Boolean, isCasting: Boolean,

View file

@ -8,7 +8,7 @@ import com.lagradost.cloudstream3.utils.loadExtractor
class EntrepeliculasyseriesProvider:MainAPI() { class EntrepeliculasyseriesProvider:MainAPI() {
override var mainUrl = "https://entrepeliculasyseries.nu" override var mainUrl = "https://entrepeliculasyseries.nu"
override var name = "EntrePeliculasySeries" override var name = "EntrePeliculasySeries"
override val lang = "es" override var lang = "es"
override val hasMainPage = true override val hasMainPage = true
override val hasChromecastSupport = true override val hasChromecastSupport = true
override val hasDownloadSupport = true override val hasDownloadSupport = true

View file

@ -20,7 +20,7 @@ class EstrenosDoramasProvider : MainAPI() {
override var mainUrl = "https://www23.estrenosdoramas.net" override var mainUrl = "https://www23.estrenosdoramas.net"
override var name = "EstrenosDoramas" override var name = "EstrenosDoramas"
override val lang = "es" override var lang = "es"
override val hasMainPage = true override val hasMainPage = true
override val hasChromecastSupport = true override val hasChromecastSupport = true
override val hasDownloadSupport = true override val hasDownloadSupport = true

View file

@ -5,7 +5,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
class FaselHDProvider : MainAPI() { class FaselHDProvider : MainAPI() {
override val lang = "ar" override var lang = "ar"
override var mainUrl = "https://faselhd.io" override var mainUrl = "https://faselhd.io"
override var name = "FaselHD" override var name = "FaselHD"
override val usesWebView = false override val usesWebView = false

View file

@ -11,7 +11,7 @@ import org.jsoup.select.Elements
class FilmanProvider : MainAPI() { class FilmanProvider : MainAPI() {
override var mainUrl = "https://filman.cc" override var mainUrl = "https://filman.cc"
override var name = "filman.cc" override var name = "filman.cc"
override val lang = "pl" override var lang = "pl"
override val hasMainPage = true override val hasMainPage = true
override val supportedTypes = setOf( override val supportedTypes = setOf(
TvType.Movie, TvType.Movie,

View file

@ -12,7 +12,7 @@ class FrenchStreamProvider : MainAPI() {
override var name = "French Stream" override var name = "French Stream"
override val hasQuickSearch = false override val hasQuickSearch = false
override val hasMainPage = true override val hasMainPage = true
override val lang = "fr" override var lang = "fr"
override val supportedTypes = setOf(TvType.AnimeMovie, TvType.TvSeries, TvType.Movie) override val supportedTypes = setOf(TvType.AnimeMovie, TvType.TvSeries, TvType.Movie)
override suspend fun search(query: String): List<SearchResponse> { override suspend fun search(query: String): List<SearchResponse> {

View file

@ -8,9 +8,9 @@ import com.lagradost.cloudstream3.utils.loadExtractor
import org.jsoup.Jsoup import org.jsoup.Jsoup
class HDMovie5 : MainAPI() { class HDMovie5 : MainAPI() {
override var mainUrl = "https://hdmovie5.mba" override var mainUrl = "https://hdmovie2.tv"
override var name = "HDMovie" override var name = "HDMovie"
override val lang = "hi" override var lang = "hi"
override val hasQuickSearch = true override val hasQuickSearch = true
override val hasMainPage = true override val hasMainPage = true

View file

@ -11,7 +11,7 @@ class LayarKacaProvider : MainAPI() {
override var mainUrl = "https://149.56.24.226" override var mainUrl = "https://149.56.24.226"
override var name = "LayarKaca" override var name = "LayarKaca"
override val hasMainPage = true override val hasMainPage = true
override val lang = "id" override var lang = "id"
override val hasDownloadSupport = true override val hasDownloadSupport = true
override val supportedTypes = setOf( override val supportedTypes = setOf(
TvType.Movie, TvType.Movie,

View file

@ -8,7 +8,7 @@ import org.jsoup.Jsoup
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
class MyCimaProvider : MainAPI() { class MyCimaProvider : MainAPI() {
override val lang = "ar" override var lang = "ar"
override var mainUrl = "https://mycima.tv" override var mainUrl = "https://mycima.tv"
override var name = "MyCima" override var name = "MyCima"
override val usesWebView = false override val usesWebView = false

View file

@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.movieproviders package com.lagradost.cloudstream3.movieproviders
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.Qualities
@ -95,7 +96,7 @@ class NginxProvider : MainAPI() {
this.plot = description this.plot = description
this.rating = ratingAverage this.rating = ratingAverage
this.tags = tagsList this.tags = tagsList
this.trailers = trailer addTrailer(trailer)
addPoster(poster, authHeader) addPoster(poster, authHeader)
} }
} else // a tv serie } else // a tv serie

View file

@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.loadExtractor
class PeliSmartProvider: MainAPI() { class PeliSmartProvider: MainAPI() {
override var mainUrl = "https://pelismart.com" override var mainUrl = "https://pelismart.com"
override var name = "PeliSmart" override var name = "PeliSmart"
override val lang = "es" override var lang = "es"
override val hasMainPage = true override val hasMainPage = true
override val hasChromecastSupport = true override val hasChromecastSupport = true
override val hasDownloadSupport = true override val hasDownloadSupport = true

View file

@ -9,7 +9,7 @@ import com.lagradost.cloudstream3.utils.loadExtractor
class PelisflixProvider : MainAPI() { class PelisflixProvider : MainAPI() {
override var mainUrl = "https://pelisflix.li" override var mainUrl = "https://pelisflix.li"
override var name = "Pelisflix" override var name = "Pelisflix"
override val lang = "es" override var lang = "es"
override val hasMainPage = true override val hasMainPage = true
override val hasChromecastSupport = true override val hasChromecastSupport = true
override val hasDownloadSupport = true override val hasDownloadSupport = true

View file

@ -8,7 +8,7 @@ import org.jsoup.nodes.Element
class PelisplusHDProvider:MainAPI() { class PelisplusHDProvider:MainAPI() {
override var mainUrl = "https://pelisplushd.net" override var mainUrl = "https://pelisplushd.net"
override var name = "PelisplusHD" override var name = "PelisplusHD"
override val lang = "es" override var lang = "es"
override val hasMainPage = true override val hasMainPage = true
override val hasChromecastSupport = true override val hasChromecastSupport = true
override val hasDownloadSupport = true override val hasDownloadSupport = true

View file

@ -12,7 +12,7 @@ import org.jsoup.Jsoup
*/ */
open class PelisplusProviderTemplate : MainAPI() { open class PelisplusProviderTemplate : MainAPI() {
override val lang = "es" override var lang = "es"
open val homePageUrlList = listOf<String>() open val homePageUrlList = listOf<String>()
// // mainUrl is good to have as a holder for the url to make future changes easier. // // mainUrl is good to have as a holder for the url to make future changes easier.

View file

@ -9,7 +9,7 @@ import com.lagradost.cloudstream3.utils.loadExtractor
class PinoyHDXyzProvider : MainAPI() { class PinoyHDXyzProvider : MainAPI() {
override var name = "Pinoy-HD" override var name = "Pinoy-HD"
override var mainUrl = "https://www.pinoy-hd.xyz" override var mainUrl = "https://www.pinoy-hd.xyz"
override val lang = "tl" override var lang = "tl"
override val supportedTypes = setOf(TvType.AsianDrama) override val supportedTypes = setOf(TvType.AsianDrama)
override val hasDownloadSupport = true override val hasDownloadSupport = true
override val hasMainPage = true override val hasMainPage = true

View file

@ -10,7 +10,7 @@ import com.lagradost.cloudstream3.utils.loadExtractor
class PinoyMoviePediaProvider : MainAPI() { class PinoyMoviePediaProvider : MainAPI() {
override var name = "Pinoy Moviepedia" override var name = "Pinoy Moviepedia"
override var mainUrl = "https://pinoymoviepedia.ru" override var mainUrl = "https://pinoymoviepedia.ru"
override val lang = "tl" override var lang = "tl"
override val supportedTypes = setOf(TvType.AsianDrama) override val supportedTypes = setOf(TvType.AsianDrama)
override val hasDownloadSupport = true override val hasDownloadSupport = true
override val hasMainPage = true override val hasMainPage = true

View file

@ -15,7 +15,7 @@ import org.jsoup.select.Elements
class PinoyMoviesEsProvider : MainAPI() { class PinoyMoviesEsProvider : MainAPI() {
override var name = "Pinoy Movies" override var name = "Pinoy Movies"
override var mainUrl = "https://pinoymovies.es" override var mainUrl = "https://pinoymovies.es"
override val lang = "tl" override var lang = "tl"
override val supportedTypes = setOf(TvType.AsianDrama) override val supportedTypes = setOf(TvType.AsianDrama)
override val hasDownloadSupport = false override val hasDownloadSupport = false
override val hasMainPage = true override val hasMainPage = true

View file

@ -15,7 +15,7 @@ class RebahinProvider : MainAPI() {
override var mainUrl = "http://167.88.14.149" override var mainUrl = "http://167.88.14.149"
override var name = "Rebahin" override var name = "Rebahin"
override val hasMainPage = true override val hasMainPage = true
override val lang = "id" override var lang = "id"
override val hasDownloadSupport = true override val hasDownloadSupport = true
override val supportedTypes = setOf( override val supportedTypes = setOf(
TvType.Movie, TvType.Movie,

View file

@ -9,7 +9,7 @@ import com.lagradost.cloudstream3.utils.loadExtractor
class SeriesflixProvider : MainAPI() { class SeriesflixProvider : MainAPI() {
override var mainUrl = "https://seriesflix.video" override var mainUrl = "https://seriesflix.video"
override var name = "Seriesflix" override var name = "Seriesflix"
override val lang = "es" override var lang = "es"
override val hasMainPage = true override val hasMainPage = true
override val hasChromecastSupport = true override val hasChromecastSupport = true
override val hasDownloadSupport = true override val hasDownloadSupport = true

View file

@ -127,7 +127,7 @@ data class TrailerElement(
class StreamingcommunityProvider : MainAPI() { class StreamingcommunityProvider : MainAPI() {
override val lang = "it" override var lang = "it"
override var mainUrl = "https://streamingcommunity.press" override var mainUrl = "https://streamingcommunity.press"
override var name = "Streamingcommunity" override var name = "Streamingcommunity"
override val hasMainPage = true override val hasMainPage = true
@ -284,7 +284,7 @@ class StreamingcommunityProvider : MainAPI() {
val trailerinfojs = document.select("slider-trailer").attr("videos") val trailerinfojs = document.select("slider-trailer").attr("videos")
val trailerinfo = parseJson<List<TrailerElement>>(trailerinfojs) val trailerinfo = parseJson<List<TrailerElement>>(trailerinfojs)
val trailerurl: String? = if (trailerinfo.isNotEmpty()) { val trailerurl: String? = if (trailerinfo.isNotEmpty()) {
"https://www.youtube.com/watch?v=${trailerinfo[0].id}" "https://www.youtube.com/watch?v=${trailerinfo[0].url}"
} else { } else {
null null
} }

View file

@ -4,9 +4,11 @@ import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
class TantifilmProvider : MainAPI() { class TantifilmProvider : MainAPI() {
override val lang = "it" override var lang = "it"
override var mainUrl = "https://www.tantifilm.rodeo" override var mainUrl = "https://www.tantifilm.rodeo"
override var name = "Tantifilm" override var name = "Tantifilm"
override val hasMainPage = true override val hasMainPage = true
@ -69,7 +71,6 @@ class TantifilmProvider : MainAPI() {
} }
} }
override suspend fun load(url: String): LoadResponse { override suspend fun load(url: String): LoadResponse {
val document = app.get(url).document val document = app.get(url).document
val type = if (document.selectFirst("div.category-film")!!.text().contains("Serie") val type = if (document.selectFirst("div.category-film")!!.text().contains("Serie")
@ -107,7 +108,7 @@ class TantifilmProvider : MainAPI() {
} }
val trailerurl = document.selectFirst("#trailer_mob > iframe")!!.attr("src")
if (type == TvType.TvSeries) { if (type == TvType.TvSeries) {
val list = ArrayList<Pair<Int, String>>() val list = ArrayList<Pair<Int, String>>()
@ -142,22 +143,18 @@ class TantifilmProvider : MainAPI() {
} }
} }
} }
return TvSeriesLoadResponse( return newTvSeriesLoadResponse(
title, title,
url, url,
this.name,
type, type,
episodeList, episodeList) {
fixUrlNull(poster), this.posterUrl= fixUrlNull(poster)
year.toIntOrNull(), this.year = year.toIntOrNull()
descipt[0], this.plot= descipt[0]
null, this.rating= rating
rating, this.recommendations = recomm
null, addTrailer(trailerurl)
null, }
null,
recomm
)
} else { } else {
val url2 = document.selectFirst("iframe")!!.attr("src") val url2 = document.selectFirst("iframe")!!.attr("src")
val actorpagelink = val actorpagelink =
@ -217,6 +214,7 @@ class TantifilmProvider : MainAPI() {
this.tags = tags this.tags = tags
this.duration = duratio this.duration = duratio
this.actors = actors this.actors = actors
addTrailer(trailerurl)
} }
} }

View file

@ -9,7 +9,7 @@ import org.jsoup.Jsoup
class VfFilmProvider : MainAPI() { class VfFilmProvider : MainAPI() {
override var mainUrl = "https://vf-film.me" override var mainUrl = "https://vf-film.me"
override var name = "vf-film.me" override var name = "vf-film.me"
override val lang = "fr" override var lang = "fr"
override val hasQuickSearch = false override val hasQuickSearch = false
override val hasMainPage = false override val hasMainPage = false
override val hasChromecastSupport = false override val hasChromecastSupport = false

View file

@ -9,7 +9,7 @@ import org.jsoup.Jsoup
class VfSerieProvider : MainAPI() { class VfSerieProvider : MainAPI() {
override var mainUrl = "https://vf-serie.org" override var mainUrl = "https://vf-serie.org"
override var name = "vf-serie.org" override var name = "vf-serie.org"
override val lang = "fr" override var lang = "fr"
override val hasQuickSearch = false override val hasQuickSearch = false
override val hasMainPage = false override val hasMainPage = false

View file

@ -67,7 +67,7 @@ interface SyncAPI : OAuth2API {
var studio: List<String>? = null, var studio: List<String>? = null,
var genres: List<String>? = null, var genres: List<String>? = null,
var synonyms: List<String>? = null, var synonyms: List<String>? = null,
var trailerUrl: String? = null, var trailers: List<String>? = null,
var isAdult : Boolean? = null, var isAdult : Boolean? = null,
var posterUrl: String? = null, var posterUrl: String? = null,
var backgroundPosterUrl : String? = null, var backgroundPosterUrl : String? = null,

View file

@ -142,6 +142,10 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
getUrlFromId(recMedia.id), getUrlFromId(recMedia.id),
recMedia.coverImage?.large ?: recMedia.coverImage?.medium recMedia.coverImage?.large ?: recMedia.coverImage?.medium
) )
},
trailers = when (season.trailer?.site?.lowercase()?.trim()) {
"youtube" -> listOf("https://www.youtube.com/watch?v=${season.trailer.id}")
else -> null
} }
//TODO REST //TODO REST
) )

View file

@ -0,0 +1,141 @@
package com.lagradost.cloudstream3.syncproviders.providers
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError
// modified code from from https://github.com/saikou-app/saikou/blob/main/app/src/main/java/ani/saikou/others/Kitsu.kt
// GNU General Public License v3.0 https://github.com/saikou-app/saikou/blob/main/LICENSE.md
object Kitsu {
private suspend fun getKitsuData(query: String): KitsuResponse {
val headers = mapOf(
"Content-Type" to "application/json",
"Accept" to "application/json",
"Connection" to "keep-alive",
"DNT" to "1",
"Origin" to "https://kitsu.io"
)
return app.post(
"https://kitsu.io/api/graphql",
headers = headers,
data = mapOf("query" to query)
).parsed()
}
private val cache: MutableMap<Pair<String, String>, Map<Int, KitsuResponse.Node>> =
mutableMapOf()
suspend fun getEpisodesDetails(
malId: String?,
anilistId: String?
): Map<Int, KitsuResponse.Node>? {
if (anilistId != null) {
try {
val map = getKitsuEpisodesDetails(anilistId, "ANILIST_ANIME")
if (!map.isNullOrEmpty()) return map
} catch (e: Exception) {
logError(e)
}
}
if (malId != null) {
try {
val map = getKitsuEpisodesDetails(malId, "MYANIMELIST_ANIME")
if (!map.isNullOrEmpty()) return map
} catch (e: Exception) {
logError(e)
}
}
return null
}
@Throws
suspend fun getKitsuEpisodesDetails(id: String, site: String): Map<Int, KitsuResponse.Node>? {
require(id.isNotBlank()) {
"Black id"
}
require(site.isNotBlank()) {
"invalid site"
}
if (cache.containsKey(id to site)) {
return cache[id to site]
}
val query =
"""
query {
lookupMapping(externalId: $id, externalSite: $site) {
__typename
... on Anime {
id
episodes(first: 2000) {
nodes {
number
titles {
canonical
}
description
thumbnail {
original {
url
}
}
}
}
}
}
}"""
val result = getKitsuData(query)
val map = (result.data?.lookupMapping?.episodes?.nodes ?: return null).mapNotNull { ep ->
val num = ep?.num ?: return@mapNotNull null
num to ep
}.toMap()
if (map.isNotEmpty()) {
cache[id to site] = map
}
return map
}
data class KitsuResponse(
val data: Data? = null
) {
data class Data(
val lookupMapping: LookupMapping? = null
)
data class LookupMapping(
val id: String? = null,
val episodes: Episodes? = null
)
data class Episodes(
val nodes: List<Node?>? = null
)
data class Node(
@JsonProperty("number")
val num: Int? = null,
val titles: Titles? = null,
val description: Description? = null,
val thumbnail: Thumbnail? = null
)
data class Description(
val en: String? = null
)
data class Thumbnail(
val original: Original? = null
)
data class Original(
val url: String? = null
)
data class Titles(
val canonical: String? = null
)
}
}

View file

@ -33,6 +33,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
override val redirectUrl = "mallogin" override val redirectUrl = "mallogin"
override val idPrefix = "mal" override val idPrefix = "mal"
override var mainUrl = "https://myanimelist.net" override var mainUrl = "https://myanimelist.net"
val apiUrl = "https://api.myanimelist.net"
override val icon = R.drawable.mal_logo override val icon = R.drawable.mal_logo
override val requiresLogin = true override val requiresLogin = true
@ -62,7 +63,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
} }
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult> { override suspend fun search(name: String): List<SyncAPI.SyncSearchResult> {
val url = "https://api.myanimelist.net/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT" val url = "$apiUrl/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT"
val auth = getAuth() ?: return emptyList() val auth = getAuth() ?: return emptyList()
val res = app.get( val res = app.get(
url, headers = mapOf( url, headers = mapOf(
@ -179,7 +180,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
name = node?.title ?: return null, name = node?.title ?: return null,
apiName = this.name, apiName = this.name,
syncId = node.id.toString(), syncId = node.id.toString(),
url = "https://myanimelist.net/anime/${node.id}", url = "$mainUrl/anime/${node.id}",
posterUrl = node.main_picture?.large posterUrl = node.main_picture?.large
) )
} }
@ -187,7 +188,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun getResult(id: String): SyncAPI.SyncResult? { override suspend fun getResult(id: String): SyncAPI.SyncResult? {
val internalId = id.toIntOrNull() ?: return null val internalId = id.toIntOrNull() ?: return null
val url = val url =
"https://api.myanimelist.net/v2/anime/$internalId?fields=id,title,main_picture,alternative_titles,start_date,end_date,synopsis,mean,rank,popularity,num_list_users,num_scoring_users,nsfw,created_at,updated_at,media_type,status,genres,my_list_status,num_episodes,start_season,broadcast,source,average_episode_duration,rating,pictures,background,related_anime,related_manga,recommendations,studios,statistics" "$apiUrl/v2/anime/$internalId?fields=id,title,main_picture,alternative_titles,start_date,end_date,synopsis,mean,rank,popularity,num_list_users,num_scoring_users,nsfw,created_at,updated_at,media_type,status,genres,my_list_status,num_episodes,start_season,broadcast,source,average_episode_duration,rating,pictures,background,related_anime,related_manga,recommendations,studios,statistics"
val res = app.get( val res = app.get(
url, headers = mapOf( url, headers = mapOf(
"Authorization" to "Bearer " + (getAuth() ?: return null) "Authorization" to "Bearer " + (getAuth() ?: return null)
@ -195,7 +196,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
).text ).text
return mapper.readValue<MalAnime>(res).let { malAnime -> return mapper.readValue<MalAnime>(res).let { malAnime ->
SyncAPI.SyncResult( SyncAPI.SyncResult(
id = malAnime.id?.toString()!!, id = internalId.toString(),
totalEpisodes = malAnime.numEpisodes, totalEpisodes = malAnime.numEpisodes,
title = malAnime.title, title = malAnime.title,
publicScore = malAnime.mean?.toFloat()?.times(1000)?.toInt(), publicScore = malAnime.mean?.toFloat()?.times(1000)?.toInt(),
@ -203,13 +204,14 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
synopsis = malAnime.synopsis, synopsis = malAnime.synopsis,
airStatus = when (malAnime.status) { airStatus = when (malAnime.status) {
"finished_airing" -> ShowStatus.Completed "finished_airing" -> ShowStatus.Completed
"airing" -> ShowStatus.Ongoing "currently_airing" -> ShowStatus.Ongoing
//"not_yet_aired"
else -> null else -> null
}, },
nextAiring = null, nextAiring = null,
studio = malAnime.studios?.mapNotNull { it.name }, studio = malAnime.studios?.mapNotNull { it.name },
genres = malAnime.genres?.map { it.name }, genres = malAnime.genres?.map { it.name },
trailerUrl = null, trailers = null,
startDate = parseDate(malAnime.startDate), startDate = parseDate(malAnime.startDate),
endDate = parseDate(malAnime.endDate), endDate = parseDate(malAnime.endDate),
recommendations = malAnime.recommendations?.mapNotNull { rec -> recommendations = malAnime.recommendations?.mapNotNull { rec ->
@ -260,7 +262,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
val currentCode = sanitizer["code"]!! val currentCode = sanitizer["code"]!!
val res = app.post( val res = app.post(
"https://myanimelist.net/v1/oauth2/token", "$mainUrl/v1/oauth2/token",
data = mapOf( data = mapOf(
"client_id" to key, "client_id" to key,
"code" to currentCode, "code" to currentCode,
@ -292,7 +294,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
.replace("/", "_").replace("\n", "") .replace("/", "_").replace("\n", "")
val codeChallenge = codeVerifier val codeChallenge = codeVerifier
val request = val request =
"https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id=$key&code_challenge=$codeChallenge&state=RequestID$requestId" "$mainUrl/v1/oauth2/authorize?response_type=code&client_id=$key&code_challenge=$codeChallenge&state=RequestID$requestId"
openBrowser(request) openBrowser(request)
} }
@ -318,7 +320,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
private suspend fun refreshToken() { private suspend fun refreshToken() {
try { try {
val res = app.post( val res = app.post(
"https://myanimelist.net/v1/oauth2/token", "$mainUrl/v1/oauth2/token",
data = mapOf( data = mapOf(
"client_id" to key, "client_id" to key,
"grant_type" to "refresh_token", "grant_type" to "refresh_token",
@ -451,7 +453,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
// Very lackluster docs // Very lackluster docs
// https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_animelist_get // https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_animelist_get
val url = val url =
"https://api.myanimelist.net/v2/users/$user/animelist?fields=list_status,num_episodes,media_type,status,start_date,end_date,synopsis,alternative_titles,mean,genres,rank,num_list_users,nsfw,average_episode_duration,num_favorites,popularity,num_scoring_users,start_season,favorites_info,broadcast,created_at,updated_at&nsfw=1&limit=100&offset=$offset" "$apiUrl/v2/users/$user/animelist?fields=list_status,num_episodes,media_type,status,start_date,end_date,synopsis,alternative_titles,mean,genres,rank,num_list_users,nsfw,average_episode_duration,num_favorites,popularity,num_scoring_users,start_season,favorites_info,broadcast,created_at,updated_at&nsfw=1&limit=100&offset=$offset"
val res = app.get( val res = app.get(
url, headers = mapOf( url, headers = mapOf(
"Authorization" to "Bearer $auth", "Authorization" to "Bearer $auth",
@ -463,7 +465,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
private suspend fun getDataAboutMalId(id: Int): SmallMalAnime? { private suspend fun getDataAboutMalId(id: Int): SmallMalAnime? {
// https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get // https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get
val url = val url =
"https://api.myanimelist.net/v2/anime/$id?fields=id,title,num_episodes,my_list_status" "$apiUrl/v2/anime/$id?fields=id,title,num_episodes,my_list_status"
val res = app.get( val res = app.get(
url, headers = mapOf( url, headers = mapOf(
"Authorization" to "Bearer " + (getAuth() ?: return null) "Authorization" to "Bearer " + (getAuth() ?: return null)
@ -481,7 +483,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
checkMalToken() checkMalToken()
while (!isDone) { while (!isDone) {
val res = app.get( val res = app.get(
"https://api.myanimelist.net/v2/users/$user/animelist?fields=list_status&limit=1000&offset=${index * 1000}", "$apiUrl/v2/users/$user/animelist?fields=list_status&limit=1000&offset=${index * 1000}",
headers = mapOf( headers = mapOf(
"Authorization" to "Bearer " + (getAuth() ?: return) "Authorization" to "Bearer " + (getAuth() ?: return)
), cacheTime = 0 ), cacheTime = 0
@ -532,10 +534,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
} }
private suspend fun checkMalToken() { private suspend fun checkMalToken() {
if (unixTime > getKey( if (unixTime > (getKey(
accountId, accountId,
MAL_UNIXTIME_KEY MAL_UNIXTIME_KEY
) ?: 0L ) ?: 0L)
) { ) {
refreshToken() refreshToken()
} }
@ -544,7 +546,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
private suspend fun getMalUser(setSettings: Boolean = true): MalUser? { private suspend fun getMalUser(setSettings: Boolean = true): MalUser? {
checkMalToken() checkMalToken()
val res = app.get( val res = app.get(
"https://api.myanimelist.net/v2/users/@me", "$apiUrl/v2/users/@me",
headers = mapOf( headers = mapOf(
"Authorization" to "Bearer " + (getAuth() ?: return null) "Authorization" to "Bearer " + (getAuth() ?: return null)
), cacheTime = 0 ), cacheTime = 0
@ -620,7 +622,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
).filter { it.value != null } as Map<String, String> ).filter { it.value != null } as Map<String, String>
return app.put( return app.put(
"https://api.myanimelist.net/v2/anime/$id/my_list_status", "$apiUrl/v2/anime/$id/my_list_status",
headers = mapOf( headers = mapOf(
"Authorization" to "Bearer " + (getAuth() ?: return null) "Authorization" to "Bearer " + (getAuth() ?: return null)
), ),

View file

@ -13,12 +13,12 @@ class APIRepository(val api: MainAPI) {
val noneApi = object : MainAPI() { val noneApi = object : MainAPI() {
override var name = "None" override var name = "None"
override val supportedTypes = emptySet<TvType>() override val supportedTypes = emptySet<TvType>()
override val lang = "" override var lang = ""
} }
val randomApi = object : MainAPI() { val randomApi = object : MainAPI() {
override var name = "Random" override var name = "Random"
override val supportedTypes = emptySet<TvType>() override val supportedTypes = emptySet<TvType>()
override val lang = "" override var lang = ""
} }
fun isInvalidData(data: String): Boolean { fun isInvalidData(data: String): Boolean {

View file

@ -1,17 +1,11 @@
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
@ -38,7 +32,6 @@ 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
@ -334,7 +327,6 @@ 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
@ -534,7 +526,6 @@ class CS3IPlayer : IPlayer {
) )
setHandleAudioBecomingNoisy(true) setHandleAudioBecomingNoisy(true)
setPlaybackSpeed(playBackSpeed) setPlaybackSpeed(playBackSpeed)
} }
} }
} }
@ -711,7 +702,7 @@ class CS3IPlayer : IPlayer {
if (playWhenReady) { if (playWhenReady) {
when (playbackState) { when (playbackState) {
Player.STATE_READY -> { Player.STATE_READY -> {
requestAutoFocus?.invoke()
} }
Player.STATE_ENDED -> { Player.STATE_ENDED -> {
handleEvent(CSPlayerEvent.NextEpisode) handleEvent(CSPlayerEvent.NextEpisode)
@ -740,6 +731,7 @@ class CS3IPlayer : IPlayer {
override fun onIsPlayingChanged(isPlaying: Boolean) { override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying) super.onIsPlayingChanged(isPlaying)
if (isPlaying) { if (isPlaying) {
requestAutoFocus?.invoke()
onRenderFirst() onRenderFirst()
} }
} }
@ -886,55 +878,9 @@ 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 $link") Log.i(TAG, "loadOnlinePlayer $link")
try { try {
if (link.url.contains("youtube.com")) {
val ytLink = link.url.replace("/embed/", "/watch?v=")
ytVideos[ytLink]?.let {
loadYtFile(context, it)
return
}
val ytExtractor =
@SuppressLint("StaticFieldLeak")
object : YouTubeExtractor(context) {
override fun onExtractionComplete(
ytFiles: SparseArray<YtFile>?,
videoMeta: VideoMeta?
) {
var yt: YtFile? = null
ytFiles?.forEach { _, value ->
if ((yt?.format?.height ?: 0) < (value.format?.height
?: -1) && (value.format?.audioBitrate ?: -1) > 0
) {
yt = value
}
}
yt?.let { ytf ->
ytVideos[ytLink] = ytf
loadYtFile(context, ytf)
}
}
}
Log.i(TAG, "YouTube extraction on $ytLink")
ytExtractor.extract(ytLink)
return
}
currentLink = link currentLink = link
if (ignoreSSL) { if (ignoreSSL) {

View file

@ -229,6 +229,15 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
} }
} }
val playerSourceMove = if (isShowing) 0f else -50.toPx.toFloat()
player_open_source?.let {
ObjectAnimator.ofFloat(it, "translationY", playerSourceMove).apply {
duration = 200
start()
}
}
if (!isLocked) { if (!isLocked) {
player_ffwd_holder?.alpha = 1f player_ffwd_holder?.alpha = 1f
player_rew_holder?.alpha = 1f player_rew_holder?.alpha = 1f
@ -251,6 +260,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
} }
bottom_player_bar?.startAnimation(fadeAnimation) bottom_player_bar?.startAnimation(fadeAnimation)
player_open_source?.startAnimation(fadeAnimation)
player_top_holder?.startAnimation(fadeAnimation) player_top_holder?.startAnimation(fadeAnimation)
} }

View file

@ -7,6 +7,7 @@ import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.ContentLoadingProgressBar import androidx.core.widget.ContentLoadingProgressBar
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -21,7 +22,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.VideoDownloadManager
import kotlinx.android.synthetic.main.result_episode.view.* import kotlinx.android.synthetic.main.result_episode.view.*
import kotlinx.android.synthetic.main.result_episode.view.episode_holder
import kotlinx.android.synthetic.main.result_episode.view.episode_text import kotlinx.android.synthetic.main.result_episode.view.episode_text
import kotlinx.android.synthetic.main.result_episode_large.view.* import kotlinx.android.synthetic.main.result_episode_large.view.*
import kotlinx.android.synthetic.main.result_episode_large.view.episode_filler import kotlinx.android.synthetic.main.result_episode_large.view.episode_filler
@ -47,6 +47,7 @@ const val ACTION_SHOW_OPTIONS = 10
const val ACTION_CLICK_DEFAULT = 11 const val ACTION_CLICK_DEFAULT = 11
const val ACTION_SHOW_TOAST = 12 const val ACTION_SHOW_TOAST = 12
const val ACTION_SHOW_DESCRIPTION = 15
const val ACTION_DOWNLOAD_EPISODE_SUBTITLE = 13 const val ACTION_DOWNLOAD_EPISODE_SUBTITLE = 13
const val ACTION_DOWNLOAD_EPISODE_SUBTITLE_MIRROR = 14 const val ACTION_DOWNLOAD_EPISODE_SUBTITLE_MIRROR = 14
@ -93,10 +94,10 @@ class EpisodeAdapter(
@LayoutRes @LayoutRes
private var layout: Int = 0 private var layout: Int = 0
fun updateLayout() { fun updateLayout() {
layout = // layout =
if (cardList.filter { it.poster != null }.size >= cardList.size / 2f) // If over half has posters then use the large layout // if (cardList.filter { it.poster != null }.size >= cardList.size / 2f) // If over half has posters then use the large layout
R.layout.result_episode_large // R.layout.result_episode_large
else R.layout.result_episode // else R.layout.result_episode
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
@ -105,7 +106,7 @@ class EpisodeAdapter(
else R.layout.result_episode*/ else R.layout.result_episode*/
return EpisodeCardViewHolder( return EpisodeCardViewHolder(
LayoutInflater.from(parent.context).inflate(layout, parent, false), LayoutInflater.from(parent.context).inflate(R.layout.result_episode_both, parent, false),
hasDownloadSupport, hasDownloadSupport,
clickCallback, clickCallback,
downloadClickCallback downloadClickCallback
@ -134,27 +135,39 @@ class EpisodeAdapter(
) : RecyclerView.ViewHolder(itemView), DownloadButtonViewHolder { ) : RecyclerView.ViewHolder(itemView), DownloadButtonViewHolder {
override var downloadButton = EasyDownloadButton() override var downloadButton = EasyDownloadButton()
private val episodeText: TextView = itemView.episode_text
private val episodeFiller: MaterialButton? = itemView.episode_filler
private val episodeRating: TextView? = itemView.episode_rating
private val episodeDescript: TextView? = itemView.episode_descript
private val episodeProgress: ContentLoadingProgressBar? = itemView.episode_progress
private val episodePoster: ImageView? = itemView.episode_poster
private val episodeDownloadBar: ContentLoadingProgressBar = itemView.result_episode_progress_downloaded
private val episodeDownloadImage: ImageView = itemView.result_episode_download
private val episodeHolder = itemView.episode_holder
var episodeDownloadBar: ContentLoadingProgressBar? = null
var episodeDownloadImage: ImageView? = null
var localCard: ResultEpisode? = null var localCard: ResultEpisode? = null
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun bind(card: ResultEpisode) { fun bind(card: ResultEpisode) {
localCard = card localCard = card
val name = if (card.name == null) "${episodeText.context.getString(R.string.episode)} ${card.episode}" else "${card.episode}. ${card.name}" val (parentView,otherView) = if(card.poster == null) {
itemView.episode_holder to itemView.episode_holder_large
} else {
itemView.episode_holder_large to itemView.episode_holder
}
parentView.isVisible = true
otherView.isVisible = false
val episodeText: TextView = parentView.episode_text
val episodeFiller: MaterialButton? = parentView.episode_filler
val episodeRating: TextView? = parentView.episode_rating
val episodeDescript: TextView? = parentView.episode_descript
val episodeProgress: ContentLoadingProgressBar? = parentView.episode_progress
val episodePoster: ImageView? = parentView.episode_poster
episodeDownloadBar =
parentView.result_episode_progress_downloaded
episodeDownloadImage = parentView.result_episode_download
val name =
if (card.name == null) "${episodeText.context.getString(R.string.episode)} ${card.episode}" else "${card.episode}. ${card.name}"
episodeFiller?.isVisible = card.isFiller == true episodeFiller?.isVisible = card.isFiller == true
episodeText.text = name//if(card.isFiller == true) episodeText.context.getString(R.string.filler).format(name) else name episodeText.text =
name//if(card.isFiller == true) episodeText.context.getString(R.string.filler).format(name) else name
episodeText.isSelected = true // is needed for text repeating episodeText.isSelected = true // is needed for text repeating
val displayPos = card.getDisplayPosition() val displayPos = card.getDisplayPosition()
@ -171,16 +184,20 @@ class EpisodeAdapter(
} }
if (card.rating != null) { if (card.rating != null) {
episodeRating?.text = episodeRating?.context?.getString(R.string.rated_format)?.format(card.rating.toFloat() / 10f) episodeRating?.text = episodeRating?.context?.getString(R.string.rated_format)
?.format(card.rating.toFloat() / 10f)
} else { } else {
episodeRating?.text = "" episodeRating?.text = ""
} }
if (card.description != null) { episodeRating?.isGone = episodeRating?.text.isNullOrBlank()
episodeDescript?.visibility = View.VISIBLE
episodeDescript?.text = card.description episodeDescript?.apply {
} else { text = card.description ?: ""
episodeDescript?.visibility = View.GONE isGone = text.isNullOrBlank()
setOnClickListener {
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_DESCRIPTION, card))
}
} }
episodePoster?.setOnClickListener { episodePoster?.setOnClickListener {
@ -192,34 +209,42 @@ class EpisodeAdapter(
return@setOnLongClickListener true return@setOnLongClickListener true
} }
episodeHolder.setOnClickListener { parentView.setOnClickListener {
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
} }
if (episodeHolder.context.isTrueTvSettings()) { if (parentView.context.isTrueTvSettings()) {
episodeHolder.isFocusable = true parentView.isFocusable = true
episodeHolder.isFocusableInTouchMode = true parentView.isFocusableInTouchMode = true
episodeHolder.touchscreenBlocksFocus = false parentView.touchscreenBlocksFocus = false
} }
episodeHolder.setOnLongClickListener { parentView.setOnLongClickListener {
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card)) clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card))
return@setOnLongClickListener true return@setOnLongClickListener true
} }
episodeDownloadImage.isVisible = hasDownloadSupport episodeDownloadImage?.isVisible = hasDownloadSupport
episodeDownloadBar.isVisible = hasDownloadSupport episodeDownloadBar?.isVisible = hasDownloadSupport
reattachDownloadButton()
} }
override fun reattachDownloadButton() { override fun reattachDownloadButton() {
downloadButton.dispose() downloadButton.dispose()
val card = localCard val card = localCard
if (hasDownloadSupport && card != null) { if (hasDownloadSupport && card != null) {
val downloadInfo = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(itemView.context, card.id) val downloadInfo = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(
itemView.context,
card.id
)
downloadButton.setUpButton( downloadButton.setUpButton(
downloadInfo?.fileLength, downloadInfo?.totalBytes, episodeDownloadBar, episodeDownloadImage, null, downloadInfo?.fileLength,
downloadInfo?.totalBytes,
episodeDownloadBar ?: return,
episodeDownloadImage ?: return,
null,
VideoDownloadHelper.DownloadEpisodeCached( VideoDownloadHelper.DownloadEpisodeCached(
card.name, card.name,
card.poster, card.poster,

View file

@ -41,6 +41,7 @@ import com.google.android.material.button.MaterialButton
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
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.APIHolder.updateHasTrailers
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.getCastSession import com.lagradost.cloudstream3.CommonActivity.getCastSession
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
@ -50,6 +51,7 @@ import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
import com.lagradost.cloudstream3.ui.download.EasyDownloadButton import com.lagradost.cloudstream3.ui.download.EasyDownloadButton
import com.lagradost.cloudstream3.ui.player.CSPlayerEvent
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator
import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.player.SubtitleData
@ -88,8 +90,10 @@ 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.*
import kotlinx.android.synthetic.main.fragment_result_swipe.* import kotlinx.android.synthetic.main.fragment_result_swipe.*
import kotlinx.android.synthetic.main.fragment_trailer.*
import kotlinx.android.synthetic.main.result_recommendations.* import kotlinx.android.synthetic.main.result_recommendations.*
import kotlinx.android.synthetic.main.result_sync.* import kotlinx.android.synthetic.main.result_sync.*
import kotlinx.android.synthetic.main.trailer_custom_layout.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -599,7 +603,7 @@ class ResultFragment : ResultTrailerPlayer() {
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 currentTrailers: List<ExtractorLink> = emptyList()
var currentTrailerIndex = 0 var currentTrailerIndex = 0
override fun nextMirror() { override fun nextMirror() {
@ -608,49 +612,44 @@ class ResultFragment : ResultTrailerPlayer() {
} }
override fun playerError(exception: Exception) { override fun playerError(exception: Exception) {
if (player.getIsPlaying()) // because we dont want random toasts in player if (player.getIsPlaying()) { // because we dont want random toasts in player
super.playerError(exception) super.playerError(exception)
} else {
nextMirror()
}
} }
private fun loadTrailer(index: Int? = null) { private fun loadTrailer(index: Int? = null) {
val isSuccess =
currentTrailers.getOrNull(index ?: currentTrailerIndex)?.let { trailer -> currentTrailers.getOrNull(index ?: currentTrailerIndex)?.let { trailer ->
//if(trailer.contains("youtube.com")) { // wont load in exo
// nextMirror()
// return
//}
context?.let { ctx -> context?.let { ctx ->
player.onPause() player.onPause()
player.loadPlayer( player.loadPlayer(
ctx, ctx,
false, false,
ExtractorLink(
"",
"Trailer",
trailer, trailer,
"",
Qualities.Unknown.value
),
null, null,
startPosition = 0L, startPosition = 0L,
subtitles = emptySet(), subtitles = emptySet(),
subtitle = null, subtitle = null,
autoPlay = false autoPlay = false
) )
true
} ?: run {
false
} }
} ?: run {
false
} }
result_trailer_loading?.isVisible = isSuccess
} }
private fun setTrailers(trailers: List<String>?) { private fun setTrailers(trailers: List<ExtractorLink>?) {
context?.let { ctx -> context?.updateHasTrailers()
if (ctx.isTvSettings()) return if (!LoadResponse.isTrailersEnabled) return
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx) currentTrailers = trailers?.sortedBy { -it.quality } ?: emptyList()
val showTrailers =
settingsManager.getBoolean(ctx.getString(R.string.show_trailers_key), true)
if (!showTrailers) return
currentTrailers = trailers ?: emptyList()
loadTrailer() loadTrailer()
} }
}
private fun setActors(actors: List<ActorData>?) { private fun setActors(actors: List<ActorData>?) {
if (actors.isNullOrEmpty()) { if (actors.isNullOrEmpty()) {
@ -758,6 +757,12 @@ class ResultFragment : ResultTrailerPlayer() {
result_overlapping_panels?.setStartPanelLockState(OverlappingPanelsLayout.LockState.CLOSE) result_overlapping_panels?.setStartPanelLockState(OverlappingPanelsLayout.LockState.CLOSE)
result_overlapping_panels?.setEndPanelLockState(OverlappingPanelsLayout.LockState.CLOSE) result_overlapping_panels?.setEndPanelLockState(OverlappingPanelsLayout.LockState.CLOSE)
player_open_source?.setOnClickListener {
currentTrailers.getOrNull(currentTrailerIndex)?.let {
context?.openBrowser(it.url)
}
}
updateUIListener = ::updateUI updateUIListener = ::updateUI
val restart = arguments?.getBoolean(RESTART_BUNDLE) ?: false val restart = arguments?.getBoolean(RESTART_BUNDLE) ?: false
@ -767,6 +772,7 @@ class ResultFragment : ResultTrailerPlayer() {
activity?.window?.decorView?.clearFocus() activity?.window?.decorView?.clearFocus()
hideKeyboard() hideKeyboard()
context?.updateHasTrailers()
activity?.loadCache() activity?.loadCache()
activity?.fixPaddingStatusbar(result_top_bar) activity?.fixPaddingStatusbar(result_top_bar)
@ -828,6 +834,12 @@ class ResultFragment : ResultTrailerPlayer() {
} else if (dy < -5) { } else if (dy < -5) {
result_bookmark_fab?.extend() result_bookmark_fab?.extend()
} }
if (!isFullScreenPlayer && player.getIsPlaying()) {
if (scrollY > (player_background?.height ?: scrollY)) {
player.handleEvent(CSPlayerEvent.Pause)
}
}
//result_poster_blur_holder?.translationY = -scrollY.toFloat() //result_poster_blur_holder?.translationY = -scrollY.toFloat()
}) })
@ -975,6 +987,7 @@ class ResultFragment : ResultTrailerPlayer() {
} }
ACTION_CHROME_CAST_EPISODE -> requireLinks(true) ACTION_CHROME_CAST_EPISODE -> requireLinks(true)
ACTION_CHROME_CAST_MIRROR -> requireLinks(true) ACTION_CHROME_CAST_MIRROR -> requireLinks(true)
ACTION_SHOW_DESCRIPTION -> true
else -> requireLinks(false) else -> requireLinks(false)
} }
if (!isLoaded) return@main // CANT LOAD if (!isLoaded) return@main // CANT LOAD
@ -984,6 +997,14 @@ class ResultFragment : ResultTrailerPlayer() {
showToast(activity, R.string.play_episode_toast, Toast.LENGTH_SHORT) showToast(activity, R.string.play_episode_toast, Toast.LENGTH_SHORT)
} }
ACTION_SHOW_DESCRIPTION -> {
val builder: AlertDialog.Builder =
AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom)
builder.setMessage(episodeClick.data.description ?: return@main)
.setTitle(R.string.torrent_plot)
.show()
}
ACTION_CLICK_DEFAULT -> { ACTION_CLICK_DEFAULT -> {
context?.let { ctx -> context?.let { ctx ->
if (ctx.isConnectedToChromecast()) { if (ctx.isConnectedToChromecast()) {
@ -1431,7 +1452,7 @@ class ResultFragment : ResultTrailerPlayer() {
val d = meta.value val d = meta.value
result_sync_episodes?.progress = currentSyncProgress * 1000 result_sync_episodes?.progress = currentSyncProgress * 1000
setSyncMaxEpisodes(d.totalEpisodes) setSyncMaxEpisodes(d.totalEpisodes)
viewModel.setMeta(d) viewModel.setMeta(d, syncdata)
} }
is Resource.Loading -> { is Resource.Loading -> {
result_sync_max_episodes?.text = result_sync_max_episodes?.text =

View file

@ -53,6 +53,7 @@ open class ResultTrailerPlayer : com.lagradost.cloudstream3.ui.player.FullScreen
screenHeight screenHeight
} }
result_trailer_loading?.isVisible = false
player_background?.apply { player_background?.apply {
isVisible = true isVisible = true
layoutParams = layoutParams =

View file

@ -13,17 +13,22 @@ import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId
import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId
import com.lagradost.cloudstream3.animeproviders.GogoanimeProvider import com.lagradost.cloudstream3.animeproviders.GogoanimeProvider
import com.lagradost.cloudstream3.animeproviders.NineAnimeProvider import com.lagradost.cloudstream3.animeproviders.NineAnimeProvider
import com.lagradost.cloudstream3.metaproviders.SyncRedirector import com.lagradost.cloudstream3.metaproviders.SyncRedirector
import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.providers.Kitsu.getEpisodesDetails
import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.player.IGenerator import com.lagradost.cloudstream3.ui.player.IGenerator
import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator
import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
@ -70,7 +75,6 @@ class ResultViewModel : ViewModel() {
val dubStatus: LiveData<DubStatus> get() = _dubStatus val dubStatus: LiveData<DubStatus> get() = _dubStatus
private val _dubStatus: MutableLiveData<DubStatus> = MutableLiveData() private val _dubStatus: MutableLiveData<DubStatus> = MutableLiveData()
private val page: MutableLiveData<LoadResponse> = MutableLiveData()
val id: MutableLiveData<Int> = MutableLiveData() val id: MutableLiveData<Int> = MutableLiveData()
val selectedSeason: MutableLiveData<Int> = MutableLiveData(-2) val selectedSeason: MutableLiveData<Int> = MutableLiveData(-2)
val seasonSelections: MutableLiveData<List<Int?>> = MutableLiveData() val seasonSelections: MutableLiveData<List<Int?>> = MutableLiveData()
@ -88,11 +92,12 @@ class ResultViewModel : ViewModel() {
fun updateWatchStatus(status: WatchType) = viewModelScope.launch { fun updateWatchStatus(status: WatchType) = viewModelScope.launch {
val currentId = id.value ?: return@launch val currentId = id.value ?: return@launch
_watchStatus.postValue(status) _watchStatus.postValue(status)
val resultPage = page.value val resultPage = _resultResponse.value
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
setResultWatchState(currentId, status.internalId) setResultWatchState(currentId, status.internalId)
if (resultPage != null) { if (resultPage != null && resultPage is Resource.Success) {
val resultPageData = resultPage.value
val current = getBookmarkedData(currentId) val current = getBookmarkedData(currentId)
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
setBookmarkedData( setBookmarkedData(
@ -101,12 +106,12 @@ class ResultViewModel : ViewModel() {
currentId, currentId,
current?.bookmarkedTime ?: currentTime, current?.bookmarkedTime ?: currentTime,
currentTime, currentTime,
resultPage.name, resultPageData.name,
resultPage.url, resultPageData.url,
resultPage.apiName, resultPageData.apiName,
resultPage.type, resultPageData.type,
resultPage.posterUrl, resultPageData.posterUrl,
resultPage.year resultPageData.year
) )
) )
} }
@ -118,20 +123,29 @@ class ResultViewModel : ViewModel() {
} }
var lastMeta: SyncAPI.SyncResult? = null var lastMeta: SyncAPI.SyncResult? = null
private fun applyMeta(resp: LoadResponse, meta: SyncAPI.SyncResult?): LoadResponse { var lastSync: Map<String, String>? = null
if (meta == null) return resp
lastMeta = meta private suspend fun applyMeta(
return resp.apply { resp: LoadResponse,
meta: SyncAPI.SyncResult?,
syncs: Map<String, String>? = null
): Pair<LoadResponse, Boolean> {
if (meta == null) return resp to false
var updateEpisodes = false
val out = resp.apply {
Log.i(TAG, "applyMeta") Log.i(TAG, "applyMeta")
duration = duration ?: meta.duration duration = duration ?: meta.duration
rating = rating ?: meta.publicScore rating = rating ?: meta.publicScore
tags = tags ?: meta.genres tags = tags ?: meta.genres
plot = if (plot.isNullOrBlank()) meta.synopsis else plot plot = if (plot.isNullOrBlank()) meta.synopsis else plot
addTrailer(meta.trailerUrl)
posterUrl = posterUrl ?: meta.posterUrl ?: meta.backgroundPosterUrl posterUrl = posterUrl ?: meta.posterUrl ?: meta.backgroundPosterUrl
actors = actors ?: meta.actors actors = actors ?: meta.actors
for ((k, v) in syncs ?: emptyMap()) {
syncData[k] = v
}
val realRecommendations = ArrayList<SearchResponse>() val realRecommendations = ArrayList<SearchResponse>()
val apiNames = listOf(GogoanimeProvider().name, NineAnimeProvider().name) val apiNames = listOf(GogoanimeProvider().name, NineAnimeProvider().name)
meta.recommendations?.forEach { rec -> meta.recommendations?.forEach { rec ->
@ -142,14 +156,54 @@ class ResultViewModel : ViewModel() {
recommendations = recommendations?.union(realRecommendations)?.toList() recommendations = recommendations?.union(realRecommendations)?.toList()
?: realRecommendations ?: realRecommendations
argamap({
addTrailer(meta.trailers)
}, {
if (this !is AnimeLoadResponse) return@argamap
val map = getEpisodesDetails(getMalId(), getAniListId())
if (map.isNullOrEmpty()) return@argamap
updateEpisodes = DubStatus.values().map { dubStatus ->
val current =
this.episodes[dubStatus]?.sortedBy { it.episode ?: 0 }?.toMutableList()
if (current.isNullOrEmpty()) return@map false
val episodes = current.mapIndexed { index, ep -> ep.episode ?: (index + 1) }
var updateCount = 0
map.forEach { (episode, node) ->
episodes.binarySearch(episode).let { index ->
current.getOrNull(index)?.let { currentEp ->
current[index] = currentEp.apply {
updateCount++
this.description = this.description ?: node.description?.en
this.name = this.name ?: node.titles?.canonical
this.episode = this.episode ?: node.num ?: episodes[index]
this.posterUrl = this.posterUrl ?: node.thumbnail?.original?.url
} }
} }
}
}
this.episodes[dubStatus] = current
updateCount > 0
}.any { it }
})
}
return out to updateEpisodes
}
fun setMeta(meta: SyncAPI.SyncResult) { fun setMeta(meta: SyncAPI.SyncResult, syncs: Map<String, String>?) =
viewModelScope.launch {
Log.i(TAG, "setMeta") Log.i(TAG, "setMeta")
lastMeta = meta
lastSync = syncs
val (value, updateEpisodes) = ioWork {
(result.value as? Resource.Success<LoadResponse>?)?.value?.let { resp -> (result.value as? Resource.Success<LoadResponse>?)?.value?.let { resp ->
_resultResponse.postValue(Resource.Success(applyMeta(resp, meta))) return@ioWork applyMeta(resp, meta, syncs)
} }
return@ioWork null to null
}
_resultResponse.postValue(Resource.Success(value ?: return@launch))
if (updateEpisodes ?: return@launch) updateEpisodes(value, lastShowFillers)
} }
private fun loadWatchStatus(localId: Int? = null) { private fun loadWatchStatus(localId: Int? = null) {
@ -310,77 +364,18 @@ class ResultViewModel : ViewModel() {
return name return name
} }
fun load(url: String, apiName: String, showFillers: Boolean) = viewModelScope.launch { var lastShowFillers = false
_publicEpisodes.postValue(Resource.Loading()) private suspend fun updateEpisodes(loadResponse: LoadResponse, showFillers: Boolean) {
_resultResponse.postValue(Resource.Loading(url)) Log.i(TAG, "updateEpisodes")
try {
val api = getApiFromNameNull(apiName) ?: getApiFromUrlNull(url) lastShowFillers = showFillers
if (api == null) {
_resultResponse.postValue(
Resource.Failure(
false,
null,
null,
"This provider does not exist"
)
)
return@launch
}
val validUrlResource = safeApiCall {
SyncRedirector.redirect(
url,
api.mainUrl.replace(NineAnimeProvider().mainUrl, "9anime")
.replace(GogoanimeProvider().mainUrl, "gogoanime")
)
}
if (validUrlResource !is Resource.Success) {
if (validUrlResource is Resource.Failure) {
_resultResponse.postValue(validUrlResource)
}
return@launch
}
val validUrl = validUrlResource.value
_resultResponse.postValue(Resource.Loading(validUrl))
_apiName.postValue(apiName)
repo = APIRepository(api)
val data = repo?.load(validUrl) ?: return@launch
_resultResponse.postValue(data)
when (data) {
is Resource.Success -> {
val loadResponse = applyMeta(data.value, lastMeta)
page.postValue(loadResponse)
val mainId = loadResponse.getId() val mainId = loadResponse.getId()
id.postValue(mainId)
loadWatchStatus(mainId)
setKey(
DOWNLOAD_HEADER_CACHE,
mainId.toString(),
VideoDownloadHelper.DownloadHeaderCached(
apiName,
validUrl,
loadResponse.type,
loadResponse.name,
loadResponse.posterUrl,
mainId,
System.currentTimeMillis(),
)
)
when (loadResponse) { when (loadResponse) {
is AnimeLoadResponse -> { is AnimeLoadResponse -> {
if (loadResponse.episodes.isEmpty()) { if (loadResponse.episodes.isEmpty()) {
_dubSubEpisodes.postValue(emptyMap()) _dubSubEpisodes.postValue(emptyMap())
return@launch return
} }
// val status = getDub(mainId) // val status = getDub(mainId)
@ -415,7 +410,7 @@ class ResultViewModel : ViewModel() {
episode, episode,
i.season, i.season,
i.data, i.data,
apiName, loadResponse.apiName,
id, id,
index, index,
i.rating, i.rating,
@ -460,7 +455,7 @@ class ResultViewModel : ViewModel() {
episodeIndex, episodeIndex,
episode.season, episode.season,
episode.data, episode.data,
apiName, loadResponse.apiName,
id, id,
index, index,
episode.rating, episode.rating,
@ -517,6 +512,79 @@ class ResultViewModel : ViewModel() {
) )
} }
} }
} catch (e: Exception) {
logError(e)
}
}
fun load(url: String, apiName: String, showFillers: Boolean) = viewModelScope.launch {
_publicEpisodes.postValue(Resource.Loading())
_resultResponse.postValue(Resource.Loading(url))
val api = getApiFromNameNull(apiName) ?: getApiFromUrlNull(url)
if (api == null) {
_resultResponse.postValue(
Resource.Failure(
false,
null,
null,
"This provider does not exist"
)
)
return@launch
}
val validUrlResource = safeApiCall {
SyncRedirector.redirect(
url,
api.mainUrl.replace(NineAnimeProvider().mainUrl, "9anime")
.replace(GogoanimeProvider().mainUrl, "gogoanime")
)
}
if (validUrlResource !is Resource.Success) {
if (validUrlResource is Resource.Failure) {
_resultResponse.postValue(validUrlResource)
}
return@launch
}
val validUrl = validUrlResource.value
_resultResponse.postValue(Resource.Loading(validUrl))
_apiName.postValue(apiName)
repo = APIRepository(api)
val data = repo?.load(validUrl) ?: return@launch
_resultResponse.postValue(data)
when (data) {
is Resource.Success -> {
val loadResponse = if (lastMeta != null || lastSync != null) ioWork {
applyMeta(data.value, lastMeta, lastSync).first
} else data.value
_resultResponse.postValue(Resource.Success(loadResponse))
val mainId = loadResponse.getId()
id.postValue(mainId)
loadWatchStatus(mainId)
setKey(
DOWNLOAD_HEADER_CACHE,
mainId.toString(),
VideoDownloadHelper.DownloadHeaderCached(
apiName,
validUrl,
loadResponse.type,
loadResponse.name,
loadResponse.posterUrl,
mainId,
System.currentTimeMillis(),
)
)
updateEpisodes(loadResponse, showFillers)
} }
else -> Unit else -> Unit
} }

View file

@ -6,12 +6,18 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.view.View import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.fasterxml.jackson.annotation.JsonProperty
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import com.lagradost.cloudstream3.APIHolder.allProviders
import com.lagradost.cloudstream3.AcraApplication import com.lagradost.cloudstream3.AcraApplication
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
@ -20,9 +26,15 @@ import com.lagradost.cloudstream3.network.initClient
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.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.showMultiDialog
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.USER_PROVIDER_API
import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.VideoDownloadManager
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath
import kotlinx.android.synthetic.main.add_remove_sites.*
import kotlinx.android.synthetic.main.add_site_input.*
import java.io.File import java.io.File
class SettingsGeneral : PreferenceFragmentCompat() { class SettingsGeneral : PreferenceFragmentCompat() {
@ -31,6 +43,17 @@ class SettingsGeneral : PreferenceFragmentCompat() {
setUpToolbar(R.string.category_general) setUpToolbar(R.string.category_general)
} }
data class CustomSite(
@JsonProperty("parentJavaClass") // javaClass.simpleName
val parentJavaClass: String,
@JsonProperty("name")
val name: String,
@JsonProperty("url")
val url: String,
@JsonProperty("lang")
val lang: String,
)
// Open file picker // Open file picker
private val pathPicker = private val pathPicker =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
@ -64,6 +87,94 @@ class SettingsGeneral : PreferenceFragmentCompat() {
setPreferencesFromResource(R.xml.settins_general, rootKey) setPreferencesFromResource(R.xml.settins_general, rootKey)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()) val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
fun getCurrent(): MutableList<CustomSite> {
return getKey<Array<CustomSite>>(USER_PROVIDER_API)?.toMutableList()
?: mutableListOf()
}
fun showAdd() {
val providers = allProviders.distinctBy { it.javaClass }.sortedBy { it.name }
activity?.showDialog(
providers.map { "${it.name} (${it.mainUrl})" },
-1,
context?.getString(R.string.add_site_pref) ?: return,
true,
{}) { selection ->
val provider = providers.getOrNull(selection) ?: return@showDialog
val builder =
AlertDialog.Builder(context ?: return@showDialog, R.style.AlertDialogCustom)
.setView(R.layout.add_site_input)
val dialog = builder.create()
dialog.show()
dialog.text2?.text = provider.name
dialog.apply_btt?.setOnClickListener {
val name = dialog.site_name_input?.text?.toString()
val url = dialog.site_url_input?.text?.toString()
val lang = dialog.site_lang_input?.text?.toString()
val realLang = if (lang.isNullOrBlank()) provider.lang else lang
if (url.isNullOrBlank() || name.isNullOrBlank() || realLang.length != 2) {
showToast(activity, R.string.error_invalid_data, Toast.LENGTH_SHORT)
return@setOnClickListener
}
val current = getCurrent()
val newSite = CustomSite(provider.javaClass.simpleName, name, url, realLang)
current.add(newSite)
setKey(USER_PROVIDER_API, current.toTypedArray())
dialog.dismissSafe(activity)
}
dialog.cancel_btt?.setOnClickListener {
dialog.dismissSafe(activity)
}
}
}
fun showDelete() {
val current = getCurrent()
activity?.showMultiDialog(
current.map { it.name },
listOf(),
context?.getString(R.string.remove_site_pref) ?: return,
{}) { indexes ->
current.removeAll(indexes.map { current[it] })
setKey(USER_PROVIDER_API, current.toTypedArray())
}
}
fun showAddOrDelete() {
val builder =
AlertDialog.Builder(context ?: return, R.style.AlertDialogCustom)
.setView(R.layout.add_remove_sites)
val dialog = builder.create()
dialog.show()
dialog.add_site?.setOnClickListener {
showAdd()
dialog.dismissSafe(activity)
}
dialog.remove_site?.setOnClickListener {
showDelete()
dialog.dismissSafe(activity)
}
}
getPref(R.string.override_site_key)?.setOnPreferenceClickListener { _ ->
if (getCurrent().isEmpty()) {
showAdd()
} else {
showAddOrDelete()
}
return@setOnPreferenceClickListener true
}
getPref(R.string.legal_notice_key)?.setOnPreferenceClickListener { getPref(R.string.legal_notice_key)?.setOnPreferenceClickListener {
val builder: AlertDialog.Builder = val builder: AlertDialog.Builder =
AlertDialog.Builder(it.context, R.style.AlertDialogCustom) AlertDialog.Builder(it.context, R.style.AlertDialogCustom)
@ -177,6 +288,5 @@ class SettingsGeneral : PreferenceFragmentCompat() {
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
} }
} }

View file

@ -3,10 +3,7 @@ package com.lagradost.cloudstream3.utils
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
object Coroutines { object Coroutines {
fun main(work: suspend (() -> Unit)): Job { fun main(work: suspend (() -> Unit)): Job {
@ -25,6 +22,12 @@ object Coroutines {
} }
} }
suspend fun <T> ioWork(work: suspend (() -> T)): T {
return withContext(Dispatchers.IO) {
work()
}
}
fun runOnMainThread(work: (() -> Unit)) { fun runOnMainThread(work: (() -> Unit)) {
val mainHandler = Handler(Looper.getMainLooper()) val mainHandler = Handler(Looper.getMainLooper())
mainHandler.post { mainHandler.post {

View file

@ -14,6 +14,7 @@ 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 USER_PROVIDER_API = "user_custom_sites"
const val PREFERENCES_NAME = "rebuild_preference" const val PREFERENCES_NAME = "rebuild_preference"

View file

@ -106,6 +106,19 @@ suspend fun loadExtractor(
return false return false
} }
suspend fun loadExtractor(
url: String,
referer: String? = null,
): List<ExtractorLink> {
for (extractor in extractorApis) {
if (url.startsWith(extractor.mainUrl)) {
return extractor.getSafeUrl(url, referer) ?: emptyList()
}
}
return emptyList()
}
val extractorApis: Array<ExtractorApi> = arrayOf( val extractorApis: Array<ExtractorApi> = arrayOf(
//AllProvider(), //AllProvider(),
WcoStream(), WcoStream(),
@ -205,6 +218,9 @@ val extractorApis: Array<ExtractorApi> = arrayOf(
KotakAnimeid(), KotakAnimeid(),
Neonime8n(), Neonime8n(),
Neonime7n(), Neonime7n(),
YoutubeExtractor(),
YoutubeShortLinkExtractor(),
) )
fun getExtractorApiFromName(name: String): ExtractorApi { fun getExtractorApiFromName(name: String): ExtractorApi {

View file

@ -11,9 +11,11 @@ object FillerEpisodeCheck {
private const val MAIN_URL = "https://www.animefillerlist.com" private const val MAIN_URL = "https://www.animefillerlist.com"
var list: HashMap<String, String>? = null var list: HashMap<String, String>? = null
var cache: HashMap<String, HashMap<Int, Boolean>> = hashMapOf()
private fun fixName(name: String): String { private fun fixName(name: String): String {
return name.lowercase(Locale.ROOT)/*.replace(" ", "")*/.replace("-", " ").replace("[^a-zA-Z0-9 ]".toRegex(), "") return name.lowercase(Locale.ROOT)/*.replace(" ", "")*/.replace("-", " ")
.replace("[^a-zA-Z0-9 ]".toRegex(), "")
} }
private suspend fun getFillerList(): Boolean { private suspend fun getFillerList(): Boolean {
@ -61,6 +63,9 @@ object FillerEpisodeCheck {
suspend fun getFillerEpisodes(query: String): HashMap<Int, Boolean>? { suspend fun getFillerEpisodes(query: String): HashMap<Int, Boolean>? {
try { try {
cache[query]?.let {
return it
}
if (!getFillerList()) return null if (!getFillerList()) return null
val localList = list ?: return null val localList = list ?: return null
@ -75,9 +80,15 @@ object FillerEpisodeCheck {
"(\\d+)" // year "(\\d+)" // year
) )
val blackListRegex = val blackListRegex =
Regex(""" (${blackList.joinToString(separator = "|").replace("(", "\\(").replace(")", "\\)")})""") Regex(
""" (${
blackList.joinToString(separator = "|").replace("(", "\\(")
.replace(")", "\\)")
})"""
)
val realQuery = fixName(query.replace(blackListRegex, "")).replace("shippuuden", "shippuden") val realQuery =
fixName(query.replace(blackListRegex, "")).replace("shippuuden", "shippuden")
if (!localList.containsKey(realQuery)) return null if (!localList.containsKey(realQuery)) return null
val href = localList[realQuery]?.replace(MAIN_URL, "") ?: return null // JUST IN CASE val href = localList[realQuery]?.replace(MAIN_URL, "") ?: return null // JUST IN CASE
val result = app.get("$MAIN_URL$href").text val result = app.get("$MAIN_URL$href").text
@ -90,6 +101,7 @@ object FillerEpisodeCheck {
hashMap[episodeNumber] = type hashMap[episodeNumber] = type
} }
} }
cache[query] = hashMap
return hashMap return hashMap
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()

View file

@ -14,23 +14,33 @@ import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.setImage
object SingleSelectionHelper { object SingleSelectionHelper {
fun Activity.showOptionSelectStringRes( fun Activity?.showOptionSelectStringRes(
view: View?, view: View?,
poster: String?, poster: String?,
options: List<Int>, options: List<Int>,
tvOptions: List<Int> = listOf(), tvOptions: List<Int> = listOf(),
callback: (Pair<Boolean, Int>) -> Unit callback: (Pair<Boolean, Int>) -> Unit
) { ) {
this.showOptionSelect(view, poster, options.map { this.getString(it) },tvOptions.map { this.getString(it) }, callback) if(this == null) return
this.showOptionSelect(
view,
poster,
options.map { this.getString(it) },
tvOptions.map { this.getString(it) },
callback
)
} }
private fun Activity.showOptionSelect( private fun Activity?.showOptionSelect(
view: View?, view: View?,
poster: String?, poster: String?,
options: List<String>, options: List<String>,
tvOptions: List<String>, tvOptions: List<String>,
callback: (Pair<Boolean, Int>) -> Unit callback: (Pair<Boolean, Int>) -> Unit
) { ) {
if(this == null) return
if (this.isTvSettings()) { if (this.isTvSettings()) {
val builder = val builder =
AlertDialog.Builder(this, R.style.AlertDialogCustom) AlertDialog.Builder(this, R.style.AlertDialogCustom)
@ -41,7 +51,8 @@ object SingleSelectionHelper {
dialog.findViewById<ListView>(R.id.listview1)?.let { listView -> dialog.findViewById<ListView>(R.id.listview1)?.let { listView ->
listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE
listView.adapter = ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice_color).apply { listView.adapter =
ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice_color).apply {
addAll(tvOptions) addAll(tvOptions)
} }
@ -67,7 +78,7 @@ object SingleSelectionHelper {
} }
} }
fun Activity.showDialog( fun Activity?.showDialog(
dialog: Dialog, dialog: Dialog,
items: List<String>, items: List<String>,
selectedIndex: List<Int>, selectedIndex: List<Int>,
@ -77,6 +88,8 @@ object SingleSelectionHelper {
callback: (List<Int>) -> Unit, callback: (List<Int>) -> Unit,
dismissCallback: () -> Unit dismissCallback: () -> Unit
) { ) {
if(this == null) return
val realShowApply = showApply || isMultiSelect val realShowApply = showApply || isMultiSelect
val listView = dialog.findViewById<ListView>(R.id.listview1)!! val listView = dialog.findViewById<ListView>(R.id.listview1)!!
val textView = dialog.findViewById<TextView>(R.id.text1)!! val textView = dialog.findViewById<TextView>(R.id.text1)!!
@ -145,8 +158,7 @@ object SingleSelectionHelper {
} }
private fun Activity?.showInputDialog(
private fun Activity.showInputDialog(
dialog: Dialog, dialog: Dialog,
value: String, value: String,
name: String, name: String,
@ -154,6 +166,8 @@ object SingleSelectionHelper {
callback: (String) -> Unit, callback: (String) -> Unit,
dismissCallback: () -> Unit dismissCallback: () -> Unit
) { ) {
if(this == null) return
val inputView = dialog.findViewById<EditText>(R.id.nginx_text_input)!! val inputView = dialog.findViewById<EditText>(R.id.nginx_text_input)!!
val textView = dialog.findViewById<TextView>(R.id.text1)!! val textView = dialog.findViewById<TextView>(R.id.text1)!!
val applyButton = dialog.findViewById<TextView>(R.id.apply_btt)!! val applyButton = dialog.findViewById<TextView>(R.id.apply_btt)!!
@ -184,13 +198,15 @@ object SingleSelectionHelper {
} }
fun Activity.showMultiDialog( fun Activity?.showMultiDialog(
items: List<String>, items: List<String>,
selectedIndex: List<Int>, selectedIndex: List<Int>,
name: String, name: String,
dismissCallback: () -> Unit, dismissCallback: () -> Unit,
callback: (List<Int>) -> Unit, callback: (List<Int>) -> Unit,
) { ) {
if(this == null) return
val builder = val builder =
AlertDialog.Builder(this, R.style.AlertDialogCustom) AlertDialog.Builder(this, R.style.AlertDialogCustom)
.setView(R.layout.bottom_selection_dialog) .setView(R.layout.bottom_selection_dialog)
@ -200,7 +216,7 @@ object SingleSelectionHelper {
showDialog(dialog, items, selectedIndex, name, true, true, callback, dismissCallback) showDialog(dialog, items, selectedIndex, name, true, true, callback, dismissCallback)
} }
fun Activity.showDialog( fun Activity?.showDialog(
items: List<String>, items: List<String>,
selectedIndex: Int, selectedIndex: Int,
name: String, name: String,
@ -208,6 +224,8 @@ object SingleSelectionHelper {
dismissCallback: () -> Unit, dismissCallback: () -> Unit,
callback: (Int) -> Unit, callback: (Int) -> Unit,
) { ) {
if(this == null) return
val builder = val builder =
AlertDialog.Builder(this, R.style.AlertDialogCustom) AlertDialog.Builder(this, R.style.AlertDialogCustom)
.setView(R.layout.bottom_selection_dialog) .setView(R.layout.bottom_selection_dialog)
@ -227,7 +245,7 @@ object SingleSelectionHelper {
} }
/** Only for a low amount of items */ /** Only for a low amount of items */
fun Activity.showBottomDialog( fun Activity?.showBottomDialog(
items: List<String>, items: List<String>,
selectedIndex: Int, selectedIndex: Int,
name: String, name: String,
@ -235,6 +253,7 @@ object SingleSelectionHelper {
dismissCallback: () -> Unit, dismissCallback: () -> Unit,
callback: (Int) -> Unit, callback: (Int) -> Unit,
) { ) {
if (this == null) return
val builder = val builder =
BottomSheetDialog(this) BottomSheetDialog(this)
builder.setContentView(R.layout.bottom_selection_dialog) builder.setContentView(R.layout.bottom_selection_dialog)

View file

@ -1,4 +1,4 @@
<vector android:height="24dp" android:tint="#FFFFFF" <vector android:height="24dp" android:tint="?attr/white"
android:viewportHeight="24" android:viewportWidth="24" android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/> <path android:fillColor="@android:color/white" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/add_site"
android:text="@string/add_site_pref"
style="@style/SettingsItem">
<requestFocus />
</TextView>
<TextView
android:id="@+id/remove_site"
android:text="@string/remove_site_pref"
style="@style/SettingsItem" />
</LinearLayout>

View file

@ -0,0 +1,117 @@
<?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">
<TextView
android:layout_marginTop="20dp"
android:layout_marginBottom="10dp"
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"
android:text="@string/add_site_pref" />
<TextView
android:layout_marginBottom="10dp"
android:id="@+id/text2"
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/grayTextColor"
android:textSize="15sp"
tools:text="Gogoanime" />
<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_site_name"
android:autofillHints="username"
android:id="@+id/site_name_input"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusLeft="@id/apply_btt"
android:nextFocusDown="@id/site_url_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:hint="@string/example_site_url"
android:id="@+id/site_url_input"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusLeft="@id/apply_btt"
android:nextFocusUp="@id/site_name_input"
android:nextFocusDown="@id/site_lang_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_lang_name"
android:autofillHints="username"
android:id="@+id/site_lang_input"
android:nextFocusUp="@id/site_url_input"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusLeft="@id/apply_btt"
android:nextFocusDown="@id/apply_btt"
android:requiresFadingEdge="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
tools:ignore="LabelFor" />
</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/add_site_pref"
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>

View file

@ -138,6 +138,34 @@
android:background="?attr/primaryBlackBackground" android:background="?attr/primaryBlackBackground"
android:orientation="vertical"> android:orientation="vertical">
<com.facebook.shimmer.ShimmerFrameLayout
android:visibility="gone"
android:id="@+id/result_trailer_loading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
app:shimmer_auto_start="true"
app:shimmer_base_alpha="0.2"
app:shimmer_duration="@integer/loading_time"
app:shimmer_highlight_alpha="0.3"
tools:visibility="visible">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/result_padding"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:background="@color/grayShimmer"
app:cardCornerRadius="@dimen/loading_radius"
android:layout_width="match_parent"
android:layout_height="150dp"
android:foreground="@drawable/outline_drawable" />
</LinearLayout>
</com.facebook.shimmer.ShimmerFrameLayout>
<FrameLayout <FrameLayout
android:id="@+id/result_smallscreen_holder" android:id="@+id/result_smallscreen_holder"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include android:visibility="gone" layout="@layout/result_episode" />
<include android:visibility="gone" layout="@layout/result_episode_large" />
</FrameLayout>

View file

@ -5,7 +5,7 @@
android:nextFocusLeft="@id/episode_poster" android:nextFocusLeft="@id/episode_poster"
android:nextFocusRight="@id/result_episode_download" android:nextFocusRight="@id/result_episode_download"
android:id="@+id/episode_holder" android:id="@+id/episode_holder_large"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -142,11 +142,13 @@
</LinearLayout> </LinearLayout>
<TextView <TextView
android:maxLines="4"
android:ellipsize="end"
android:paddingTop="10dp" android:paddingTop="10dp"
android:paddingBottom="10dp" android:paddingBottom="10dp"
android:id="@+id/episode_descript" android:id="@+id/episode_descript"
android:textColor="?attr/grayTextColor" android:textColor="?attr/grayTextColor"
tools:text="Jon and Daenerys arrive in Winterfell and are met with skepticism. Sam learns about the fate of his family. Cersei gives Euron the reward he aims for. Theon follows his heart. " tools:text="Jon and Daenerys arrive in Winterfell and are met with skepticism. Sam learns about the fate of his family. Cersei gives Euron the reward he aims for. Theon follows his heart. Jon and Daenerys arrive in Winterfell and are met with skepticism. Sam learns about the fate of his family. Cersei gives Euron the reward he aims for. Theon follows his heart."
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
</LinearLayout> </LinearLayout>

View file

@ -58,7 +58,6 @@
android:background="@color/black_overlay" /> android:background="@color/black_overlay" />
</FrameLayout> </FrameLayout>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/player_video_holder" android:id="@+id/player_video_holder"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -273,6 +272,17 @@
android:layout_width="0dp" /> android:layout_width="0dp" />
</LinearLayout> </LinearLayout>
<ImageView
android:background="?android:attr/selectableItemBackgroundBorderless"
android:id="@+id/player_open_source"
app:tint="@color/white"
android:src="@drawable/ic_baseline_public_24"
android:layout_margin="20dp"
android:layout_width="24dp"
android:layout_height="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout <LinearLayout
android:id="@+id/bottom_player_bar" android:id="@+id/bottom_player_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -343,9 +353,9 @@
app:tint="@color/white" app:tint="@color/white"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:src="@drawable/baseline_fullscreen_24" android:src="@drawable/baseline_fullscreen_24"
android:layout_marginEnd="10dp" android:layout_marginEnd="20dp"
android:layout_width="24dp" android:layout_width="30dp"
android:layout_height="24dp" /> android:layout_height="30dp" />
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -50,6 +50,7 @@
<string name="bottom_title_key" translatable="false">bottom_title_key</string> <string name="bottom_title_key" translatable="false">bottom_title_key</string>
<string name="poster_ui_key" translatable="false">poster_ui_key</string> <string name="poster_ui_key" translatable="false">poster_ui_key</string>
<string name="subtitles_encoding_key" translatable="false">subtitles_encoding_key</string> <string name="subtitles_encoding_key" translatable="false">subtitles_encoding_key</string>
<string name="override_site_key" translatable="false">override_site_key</string>
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG --> <!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
<string name="extra_info_format" translatable="false" formatted="true">%d %s | %sMB</string> <string name="extra_info_format" translatable="false" formatted="true">%d %s | %sMB</string>
@ -382,6 +383,10 @@
<string name="dns_pref">DNS over HTTPS</string> <string name="dns_pref">DNS over HTTPS</string>
<string name="dns_pref_summary">Useful for bypassing ISP blocks</string> <string name="dns_pref_summary">Useful for bypassing ISP blocks</string>
<string name="add_site_pref">Clone site</string>
<string name="remove_site_pref">Remove site</string>
<string name="add_site_summary">Add a clone of an existing site, with a different url</string>
<string name="download_path_pref">Download path</string> <string name="download_path_pref">Download path</string>
<string name="nginx_url_pref">Nginx server url</string> <string name="nginx_url_pref">Nginx server url</string>
@ -440,6 +445,9 @@
<string name="example_username">MyCoolUsername</string> <string name="example_username">MyCoolUsername</string>
<string name="example_email">hello@world.com</string> <string name="example_email">hello@world.com</string>
<string name="example_ip">127.0.0.1</string> <string name="example_ip">127.0.0.1</string>
<string name="example_site_name">MyCoolSite</string>
<string name="example_site_url">example.com</string>
<string name="example_lang_name">Language code (en)</string>
<!-- <!--
<string name="mal_account_settings" translatable="false">MAL</string> <string name="mal_account_settings" translatable="false">MAL</string>
@ -529,6 +537,8 @@
<string name="title">Title</string> <string name="title">Title</string>
<string name="resolution">Resolution</string> <string name="resolution">Resolution</string>
<string name="error_invalid_id">Invalid id</string> <string name="error_invalid_id">Invalid id</string>
<string name="error_invalid_data">Invalid data</string>
<string name="error">Error</string>
<string name="subtitles_remove_captions">Remove closed captions from subtitles</string> <string name="subtitles_remove_captions">Remove closed captions from subtitles</string>
<string name="subtitles_remove_bloat">Remove bloat from subtitles</string> <string name="subtitles_remove_bloat">Remove bloat from subtitles</string>
<string name="extras">Extras</string> <string name="extras">Extras</string>

View file

@ -1,6 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
android:key="@string/override_site_key"
android:title="@string/add_site_pref"
android:summary="@string/add_site_summary"
android:icon="@drawable/ic_baseline_add_24" />
<Preference <Preference
android:key="@string/dns_key" android:key="@string/dns_key"