mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
b619ee9bf8
81 changed files with 1299 additions and 466 deletions
|
@ -36,7 +36,7 @@ android {
|
|||
targetSdkVersion 30
|
||||
|
||||
versionCode 48
|
||||
versionName "2.10.28"
|
||||
versionName "2.10.29"
|
||||
|
||||
resValue "string", "app_version",
|
||||
"${defaultConfig.versionName}${versionNameSuffix ?: ""}"
|
||||
|
@ -74,6 +74,8 @@ android {
|
|||
}
|
||||
}
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
@ -143,7 +145,7 @@ dependencies {
|
|||
implementation 'com.jaredrummler:colorpicker:1.1.0'
|
||||
|
||||
//run JS
|
||||
implementation 'org.mozilla:rhino:1.7R4'
|
||||
implementation 'org.mozilla:rhino:1.7.14'
|
||||
|
||||
// TorrentStream
|
||||
//implementation 'com.github.TorrentStream:TorrentStream-Android:2.7.0'
|
||||
|
@ -175,6 +177,11 @@ dependencies {
|
|||
// used for subtitle decoding https://github.com/albfernandez/juniversalchardet
|
||||
implementation 'com.github.albfernandez:juniversalchardet:2.4.0'
|
||||
|
||||
// play yt
|
||||
implementation 'com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT'
|
||||
// slow af yt
|
||||
//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'
|
||||
|
||||
}
|
|
@ -155,11 +155,11 @@
|
|||
<service
|
||||
android:name=".services.VideoDownloadService"
|
||||
android:enabled="true"
|
||||
android:exported="false"></service>
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.ControllerActivity"></activity>
|
||||
android:name=".ui.ControllerActivity" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonProperty
|
|||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper
|
||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId
|
||||
import com.lagradost.cloudstream3.animeproviders.*
|
||||
import com.lagradost.cloudstream3.metaproviders.CrossTmdbProvider
|
||||
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.malApi
|
||||
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.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||
import okhttp3.Interceptor
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
@ -39,7 +42,7 @@ object APIHolder {
|
|||
|
||||
private const val defProvider = 0
|
||||
|
||||
val allProviders by lazy {
|
||||
val allProviders =
|
||||
arrayListOf(
|
||||
// Movie providers
|
||||
ElifilmsProvider(),
|
||||
|
@ -132,7 +135,7 @@ object APIHolder {
|
|||
NginxProvider(),
|
||||
OlgplyProvider(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
fun initAll() {
|
||||
for (api in allProviders) {
|
||||
|
@ -142,7 +145,7 @@ object APIHolder {
|
|||
}
|
||||
|
||||
var apis: List<MainAPI> = arrayListOf()
|
||||
private var apiMap: Map<String, Int>? = null
|
||||
var apiMap: Map<String, Int>? = null
|
||||
|
||||
private fun initMap() {
|
||||
if (apiMap == null)
|
||||
|
@ -297,6 +300,16 @@ object APIHolder {
|
|||
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> {
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val currentPrefMedia =
|
||||
|
@ -357,6 +370,7 @@ abstract class MainAPI {
|
|||
}
|
||||
|
||||
fun overrideWithNewData(data: ProvidersInfoJson) {
|
||||
if (!canBeOverridden) return
|
||||
this.name = data.name
|
||||
if (data.url.isNotBlank() && data.url != "NONE")
|
||||
this.mainUrl = data.url
|
||||
|
@ -366,10 +380,11 @@ abstract class MainAPI {
|
|||
open var name = "NONE"
|
||||
open var mainUrl = "NONE"
|
||||
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 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*/
|
||||
open val instantLinkLoading = false
|
||||
|
@ -852,7 +867,7 @@ interface LoadResponse {
|
|||
var rating: Int? // 0-10000
|
||||
var tags: List<String>?
|
||||
var duration: Int? // in minutes
|
||||
var trailers: List<String>?
|
||||
var trailers: List<ExtractorLink>?
|
||||
var recommendations: List<SearchResponse>?
|
||||
var actors: List<ActorData>?
|
||||
var comingSoon: Boolean
|
||||
|
@ -862,6 +877,7 @@ interface LoadResponse {
|
|||
companion object {
|
||||
private val malIdPrefix = malApi.idPrefix
|
||||
private val aniListIdPrefix = aniListApi.idPrefix
|
||||
var isTrailersEnabled = true
|
||||
|
||||
@JvmName("addActorNames")
|
||||
fun LoadResponse.addActors(actors: List<String>?) {
|
||||
|
@ -883,6 +899,14 @@ interface LoadResponse {
|
|||
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?) {
|
||||
this.syncData[malIdPrefix] = (id ?: return).toString()
|
||||
}
|
||||
|
@ -895,27 +919,38 @@ interface LoadResponse {
|
|||
addImdbId(imdbUrlToIdNullable(url))
|
||||
}
|
||||
|
||||
/**better to set trailers directly instead of calling this multiple times*/
|
||||
fun LoadResponse.addTrailer(trailerUrl: String?) {
|
||||
if (trailerUrl == null) return
|
||||
/**better to call addTrailer with mutible trailers directly instead of calling this multiple times*/
|
||||
suspend fun LoadResponse.addTrailer(trailerUrl: String?, referer: String? = null) {
|
||||
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) {
|
||||
this.trailers = listOf(trailerUrl)
|
||||
this.trailers = newTrailers
|
||||
} else {
|
||||
val update = this.trailers?.toMutableList()
|
||||
update?.add(trailerUrl)
|
||||
val update = this.trailers?.toMutableList() ?: mutableListOf()
|
||||
update.addAll(newTrailers)
|
||||
this.trailers = update
|
||||
}
|
||||
}
|
||||
|
||||
fun LoadResponse.addTrailer(trailerUrls: List<String>?) {
|
||||
if(trailerUrls == null) return
|
||||
if (this.trailers == null) {
|
||||
this.trailers = trailerUrls
|
||||
} else {
|
||||
val update = this.trailers?.toMutableList()
|
||||
update?.addAll(trailerUrls)
|
||||
this.trailers = update
|
||||
}
|
||||
suspend fun LoadResponse.addTrailer(trailerUrls: List<String>?, referer: String? = null) {
|
||||
if (!isTrailersEnabled || trailerUrls == null) return
|
||||
val newTrailers = trailerUrls.apmap { trailerUrl ->
|
||||
try {
|
||||
loadExtractor(trailerUrl, referer)
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
emptyList()
|
||||
}
|
||||
}.flatten().distinct()
|
||||
addTrailer(newTrailers)
|
||||
}
|
||||
|
||||
fun LoadResponse.addImdbId(id: String?) {
|
||||
|
@ -995,7 +1030,7 @@ data class TorrentLoadResponse(
|
|||
override var rating: Int? = null,
|
||||
override var tags: List<String>? = 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 actors: List<ActorData>? = null,
|
||||
override var comingSoon: Boolean = false,
|
||||
|
@ -1023,7 +1058,7 @@ data class AnimeLoadResponse(
|
|||
|
||||
override var rating: 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 actors: List<ActorData>? = null,
|
||||
override var comingSoon: Boolean = false,
|
||||
|
@ -1036,12 +1071,12 @@ fun AnimeLoadResponse.addEpisodes(status: DubStatus, episodes: List<Episode>?) {
|
|||
this.episodes[status] = episodes
|
||||
}
|
||||
|
||||
fun MainAPI.newAnimeLoadResponse(
|
||||
suspend fun MainAPI.newAnimeLoadResponse(
|
||||
name: String,
|
||||
url: String,
|
||||
type: TvType,
|
||||
comingSoonIfNone: Boolean = true,
|
||||
initializer: AnimeLoadResponse.() -> Unit = { },
|
||||
initializer: suspend AnimeLoadResponse.() -> Unit = { },
|
||||
): AnimeLoadResponse {
|
||||
val builder = AnimeLoadResponse(name = name, url = url, apiName = this.name, type = type)
|
||||
builder.initializer()
|
||||
|
@ -1070,7 +1105,7 @@ data class MovieLoadResponse(
|
|||
override var rating: Int? = null,
|
||||
override var tags: List<String>? = 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 actors: List<ActorData>? = null,
|
||||
override var comingSoon: Boolean = false,
|
||||
|
@ -1078,12 +1113,12 @@ data class MovieLoadResponse(
|
|||
override var posterHeaders: Map<String, String>? = null,
|
||||
) : LoadResponse
|
||||
|
||||
fun <T> MainAPI.newMovieLoadResponse(
|
||||
suspend fun <T> MainAPI.newMovieLoadResponse(
|
||||
name: String,
|
||||
url: String,
|
||||
type: TvType,
|
||||
data: T?,
|
||||
initializer: MovieLoadResponse.() -> Unit = { }
|
||||
initializer: suspend MovieLoadResponse.() -> Unit = { }
|
||||
): MovieLoadResponse {
|
||||
// just in case
|
||||
if (data is String) return newMovieLoadResponse(
|
||||
|
@ -1106,12 +1141,12 @@ fun <T> MainAPI.newMovieLoadResponse(
|
|||
return builder
|
||||
}
|
||||
|
||||
fun MainAPI.newMovieLoadResponse(
|
||||
suspend fun MainAPI.newMovieLoadResponse(
|
||||
name: String,
|
||||
url: String,
|
||||
type: TvType,
|
||||
dataUrl: String,
|
||||
initializer: MovieLoadResponse.() -> Unit = { }
|
||||
initializer: suspend MovieLoadResponse.() -> Unit = { }
|
||||
): MovieLoadResponse {
|
||||
val builder = MovieLoadResponse(
|
||||
name = name,
|
||||
|
@ -1191,7 +1226,7 @@ data class TvSeriesLoadResponse(
|
|||
override var rating: Int? = null,
|
||||
override var tags: List<String>? = 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 actors: List<ActorData>? = null,
|
||||
override var comingSoon: Boolean = false,
|
||||
|
@ -1199,12 +1234,12 @@ data class TvSeriesLoadResponse(
|
|||
override var posterHeaders: Map<String, String>? = null,
|
||||
) : LoadResponse
|
||||
|
||||
fun MainAPI.newTvSeriesLoadResponse(
|
||||
suspend fun MainAPI.newTvSeriesLoadResponse(
|
||||
name: String,
|
||||
url: String,
|
||||
type: TvType,
|
||||
episodes: List<Episode>,
|
||||
initializer: TvSeriesLoadResponse.() -> Unit = { }
|
||||
initializer: suspend TvSeriesLoadResponse.() -> Unit = { }
|
||||
): TvSeriesLoadResponse {
|
||||
val builder = TvSeriesLoadResponse(
|
||||
name = name,
|
||||
|
|
|
@ -28,6 +28,7 @@ import com.lagradost.cloudstream3.APIHolder.allProviders
|
|||
import com.lagradost.cloudstream3.APIHolder.apis
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
|
||||
import com.lagradost.cloudstream3.APIHolder.initAll
|
||||
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
|
||||
import com.lagradost.cloudstream3.CommonActivity.loadThemes
|
||||
import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
|
||||
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.settings.SettingsFragment.Companion.isEmulatorSettings
|
||||
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.loadCache
|
||||
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.navigate
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
|
||||
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
|
||||
import com.lagradost.nicehttp.Requests
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import kotlinx.android.synthetic.main.fragment_result_swipe.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
import java.io.File
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
|
@ -364,9 +368,23 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
||||
|
@ -515,6 +533,26 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
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)
|
||||
updateLocale()
|
||||
app.initClient(this)
|
||||
|
@ -576,6 +614,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
|
||||
loadCache()
|
||||
test()
|
||||
NewPipe.init(DownloaderTestImpl.getInstance())
|
||||
updateHasTrailers()
|
||||
/*nav_view.setOnNavigationItemSelectedListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.navigation_home -> {
|
||||
|
|
|
@ -16,7 +16,7 @@ import org.jsoup.nodes.Element
|
|||
class AnimeWorldProvider : MainAPI() {
|
||||
override var mainUrl = "https://www.animeworld.tv"
|
||||
override var name = "AnimeWorld"
|
||||
override val lang = "it"
|
||||
override var lang = "it"
|
||||
override val hasMainPage = true
|
||||
|
||||
override val supportedTypes = setOf(
|
||||
|
|
|
@ -13,7 +13,7 @@ class AnimefenixProvider:MainAPI() {
|
|||
|
||||
override var mainUrl = "https://animefenix.com"
|
||||
override var name = "Animefenix"
|
||||
override val lang = "es"
|
||||
override var lang = "es"
|
||||
override val hasMainPage = true
|
||||
override val hasChromecastSupport = true
|
||||
override val hasDownloadSupport = true
|
||||
|
|
|
@ -11,7 +11,7 @@ import kotlin.collections.ArrayList
|
|||
class AnimeflvIOProvider:MainAPI() {
|
||||
override var mainUrl = "https://animeflv.io" //Also scrapes from animeid.to
|
||||
override var name = "Animeflv.io"
|
||||
override val lang = "es"
|
||||
override var lang = "es"
|
||||
override val hasMainPage = true
|
||||
override val hasChromecastSupport = true
|
||||
override val hasDownloadSupport = true
|
||||
|
|
|
@ -24,7 +24,7 @@ class AnimeflvnetProvider : MainAPI() {
|
|||
|
||||
override var mainUrl = "https://www3.animeflv.net"
|
||||
override var name = "Animeflv.net"
|
||||
override val lang = "es"
|
||||
override var lang = "es"
|
||||
override val hasMainPage = true
|
||||
override val hasChromecastSupport = true
|
||||
override val hasDownloadSupport = true
|
||||
|
|
|
@ -11,7 +11,7 @@ import org.jsoup.nodes.Element
|
|||
class DreamSubProvider : MainAPI() {
|
||||
override var mainUrl = "https://dreamsub.me"
|
||||
override var name = "DreamSub"
|
||||
override val lang = "it"
|
||||
override var lang = "it"
|
||||
override val hasMainPage = true
|
||||
|
||||
override val supportedTypes = setOf(
|
||||
|
|
|
@ -14,7 +14,7 @@ class GomunimeProvider : MainAPI() {
|
|||
override var name = "Gomunime"
|
||||
override val hasQuickSearch = false
|
||||
override val hasMainPage = true
|
||||
override val lang = "id"
|
||||
override var lang = "id"
|
||||
override val hasDownloadSupport = true
|
||||
|
||||
override val supportedTypes = setOf(
|
||||
|
|
|
@ -22,7 +22,7 @@ class JKAnimeProvider : MainAPI() {
|
|||
|
||||
override var mainUrl = "https://jkanime.net"
|
||||
override var name = "JKAnime"
|
||||
override val lang = "es"
|
||||
override var lang = "es"
|
||||
override val hasMainPage = true
|
||||
override val hasChromecastSupport = true
|
||||
override val hasDownloadSupport = true
|
||||
|
|
|
@ -12,7 +12,7 @@ class KuramanimeProvider : MainAPI() {
|
|||
override var name = "Kuramanime"
|
||||
override val hasQuickSearch = false
|
||||
override val hasMainPage = true
|
||||
override val lang = "id"
|
||||
override var lang = "id"
|
||||
override val hasDownloadSupport = true
|
||||
|
||||
override val supportedTypes = setOf(
|
||||
|
|
|
@ -3,17 +3,18 @@ package com.lagradost.cloudstream3.animeproviders
|
|||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||
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.nodes.Element
|
||||
import java.util.ArrayList
|
||||
|
||||
class KuronimeProvider : MainAPI() {
|
||||
override var mainUrl = "https://185.231.223.254"
|
||||
override var name = "Kuronime"
|
||||
override val hasQuickSearch = false
|
||||
override val hasMainPage = true
|
||||
override val lang = "id"
|
||||
override var lang = "id"
|
||||
override val hasDownloadSupport = true
|
||||
|
||||
override val supportedTypes = setOf(
|
||||
|
@ -139,7 +140,6 @@ class KuronimeProvider : MainAPI() {
|
|||
plot = description
|
||||
addTrailer(trailer)
|
||||
this.tags = tags
|
||||
trailers = listOf(trailer)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ class MonoschinosProvider : MainAPI() {
|
|||
|
||||
override var mainUrl = "https://monoschinos2.com"
|
||||
override var name = "Monoschinos"
|
||||
override val lang = "es"
|
||||
override var lang = "es"
|
||||
override val hasMainPage = true
|
||||
override val hasChromecastSupport = true
|
||||
override val hasDownloadSupport = true
|
||||
|
|
|
@ -15,7 +15,7 @@ class MundoDonghuaProvider : MainAPI() {
|
|||
|
||||
override var mainUrl = "https://www.mundodonghua.com"
|
||||
override var name = "MundoDonghua"
|
||||
override val lang = "es"
|
||||
override var lang = "es"
|
||||
override val hasMainPage = true
|
||||
override val hasChromecastSupport = true
|
||||
override val hasDownloadSupport = true
|
||||
|
|
|
@ -11,7 +11,7 @@ class NeonimeProvider : MainAPI() {
|
|||
override var name = "Neonime"
|
||||
override val hasQuickSearch = false
|
||||
override val hasMainPage = true
|
||||
override val lang = "id"
|
||||
override var lang = "id"
|
||||
override val hasDownloadSupport = true
|
||||
|
||||
override val supportedTypes = setOf(
|
||||
|
|
|
@ -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_dub&page=1",
|
||||
"Recently Updated (DUB)(DUB)"
|
||||
"Recently Updated (DUB)"
|
||||
),
|
||||
Pair(
|
||||
"$mainUrl/ajax/home/widget?name=updated_chinese&page=1",
|
||||
|
@ -64,7 +64,8 @@ class NineAnimeProvider : MainAPI() {
|
|||
}
|
||||
|
||||
//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? {
|
||||
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 {
|
||||
val title = it.selectFirst("a.name")!!.text()
|
||||
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
|
||||
val image = it.selectFirst("a.poster img")!!.attr("src")
|
||||
AnimeSearchResponse(
|
||||
|
@ -199,7 +203,8 @@ class NineAnimeProvider : MainAPI() {
|
|||
override suspend fun load(url: String): LoadResponse? {
|
||||
val validUrl = url.replace("https://9anime.to", mainUrl)
|
||||
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 poster = doc.selectFirst("aside.main div.thumb div img")!!.attr("src")
|
||||
val title = doc.selectFirst(".info .title")!!.text()
|
||||
|
|
|
@ -15,7 +15,7 @@ class NontonAnimeIDProvider : MainAPI() {
|
|||
override var name = "NontonAnimeID"
|
||||
override val hasQuickSearch = false
|
||||
override val hasMainPage = true
|
||||
override val lang = "id"
|
||||
override var lang = "id"
|
||||
override val hasDownloadSupport = true
|
||||
|
||||
override val supportedTypes = setOf(
|
||||
|
|
|
@ -13,7 +13,7 @@ class OploverzProvider : MainAPI() {
|
|||
override var name = "Oploverz"
|
||||
override val hasQuickSearch = false
|
||||
override val hasMainPage = true
|
||||
override val lang = "id"
|
||||
override var lang = "id"
|
||||
override val hasDownloadSupport = true
|
||||
|
||||
override val supportedTypes = setOf(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
|
|||
class CrossTmdbProvider : TmdbProvider() {
|
||||
override var name = "MultiMovie"
|
||||
override val apiName = "MultiMovie"
|
||||
override val lang = "en"
|
||||
override var lang = "en"
|
||||
override val useMetaLoadResponse = true
|
||||
override val usesWebView = true
|
||||
override val supportedTypes = setOf(TvType.Movie)
|
||||
|
|
|
@ -12,7 +12,7 @@ import com.lagradost.cloudstream3.utils.SyncUtil
|
|||
// wont be implemented
|
||||
class MultiAnimeProvider : MainAPI() {
|
||||
override var name = "MultiAnime"
|
||||
override val lang = "en"
|
||||
override var lang = "en"
|
||||
override val usesWebView = true
|
||||
override val supportedTypes = setOf(TvType.Anime)
|
||||
private val syncApi: SyncAPI = aniListApi
|
||||
|
@ -61,7 +61,7 @@ class MultiAnimeProvider : MainAPI() {
|
|||
plot = res.synopsis
|
||||
tags = res.genres
|
||||
rating = res.publicScore
|
||||
addTrailer(res.trailerUrl)
|
||||
addTrailer(res.trailers)
|
||||
addAniListId(res.id.toIntOrNull())
|
||||
recommendations = res.recommendations
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
?.mapNotNull { season ->
|
||||
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(
|
||||
this.title ?: this.original_title, getUrl(id, false), TvType.Movie, TmdbLink(
|
||||
this.imdb_id,
|
||||
|
|
|
@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.Qualities
|
|||
import org.jsoup.nodes.Element
|
||||
|
||||
class AkwamProvider : MainAPI() {
|
||||
override val lang = "ar"
|
||||
override var lang = "ar"
|
||||
override var mainUrl = "https://akwam.to"
|
||||
override var name = "Akwam"
|
||||
override val usesWebView = false
|
||||
|
|
|
@ -5,9 +5,11 @@ import com.lagradost.cloudstream3.*
|
|||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||
|
||||
|
||||
class AltadefinizioneProvider : MainAPI() {
|
||||
override val lang = "it"
|
||||
override var lang = "it"
|
||||
override var mainUrl = "https://altadefinizione.hair"
|
||||
override var name = "Altadefinizione"
|
||||
override val hasMainPage = true
|
||||
|
@ -111,7 +113,10 @@ class AltadefinizioneProvider : MainAPI() {
|
|||
}
|
||||
|
||||
val tags: List<String> = document.select("#details > li:nth-child(1) > a").map { it.text() }
|
||||
return newMovieLoadResponse(
|
||||
|
||||
val trailerurl = document.selectFirst("#showtrailer > div > div > iframe")!!.attr("src")
|
||||
|
||||
return newMovieLoadResponse(
|
||||
title,
|
||||
url,
|
||||
TvType.Movie,
|
||||
|
@ -125,6 +130,7 @@ class AltadefinizioneProvider : MainAPI() {
|
|||
this.duration = null
|
||||
this.actors = actors
|
||||
this.tags = tags
|
||||
addTrailer(trailerurl)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
|
|||
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||
|
||||
class CineblogProvider : MainAPI() {
|
||||
override val lang = "it"
|
||||
override var lang = "it"
|
||||
override var mainUrl = "https://cb01.rip"
|
||||
override var name = "CineBlog"
|
||||
override val hasMainPage = true
|
||||
|
|
|
@ -9,7 +9,7 @@ import com.lagradost.cloudstream3.utils.loadExtractor
|
|||
class CinecalidadProvider:MainAPI() {
|
||||
override var mainUrl = "https://cinecalidad.lol"
|
||||
override var name = "Cinecalidad"
|
||||
override val lang = "es"
|
||||
override var lang = "es"
|
||||
override val hasMainPage = true
|
||||
override val hasChromecastSupport = true
|
||||
override val hasDownloadSupport = true
|
||||
|
|
|
@ -10,7 +10,7 @@ import com.lagradost.cloudstream3.utils.loadExtractor
|
|||
class CuevanaProvider : MainAPI() {
|
||||
override var mainUrl = "https://cuevana3.me"
|
||||
override var name = "Cuevana"
|
||||
override val lang = "es"
|
||||
override var lang = "es"
|
||||
override val hasMainPage = true
|
||||
override val hasChromecastSupport = true
|
||||
override val hasDownloadSupport = true
|
||||
|
|
|
@ -23,7 +23,7 @@ class DoramasYTProvider : MainAPI() {
|
|||
|
||||
override var mainUrl = "https://doramasyt.com"
|
||||
override var name = "DoramasYT"
|
||||
override val lang = "es"
|
||||
override var lang = "es"
|
||||
override val hasMainPage = true
|
||||
override val hasChromecastSupport = true
|
||||
override val hasDownloadSupport = true
|
||||
|
|
|
@ -15,7 +15,7 @@ class DramaidProvider : MainAPI() {
|
|||
override var name = "DramaId"
|
||||
override val hasQuickSearch = false
|
||||
override val hasMainPage = true
|
||||
override val lang = "id"
|
||||
override var lang = "id"
|
||||
override val hasDownloadSupport = true
|
||||
override val hasChromecastSupport = false
|
||||
override val supportedTypes = setOf(TvType.AsianDrama)
|
||||
|
|
|
@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
|
|||
import org.jsoup.nodes.Element
|
||||
|
||||
class EgyBestProvider : MainAPI() {
|
||||
override val lang = "ar"
|
||||
override var lang = "ar"
|
||||
override var mainUrl = "https://www.egy.best"
|
||||
override var name = "EgyBest"
|
||||
override val usesWebView = false
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
package com.lagradost.cloudstream3.movieproviders
|
||||
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import kotlin.collections.ArrayList
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||
|
||||
class ElifilmsProvider:MainAPI() {
|
||||
class ElifilmsProvider : MainAPI() {
|
||||
override var mainUrl: String = "https://elifilms.net"
|
||||
override var name: String = "Elifilms"
|
||||
override val lang = "es"
|
||||
override var lang = "es"
|
||||
override val hasMainPage = true
|
||||
override val hasChromecastSupport = true
|
||||
override val hasDownloadSupport = true
|
||||
override val supportedTypes = setOf(
|
||||
TvType.Movie,
|
||||
)
|
||||
|
||||
override suspend fun getMainPage(): HomePageResponse {
|
||||
val items = ArrayList<HomePageList>()
|
||||
val newest = app.get(mainUrl).document.selectFirst("a.fav_link.premiera")?.attr("href")
|
||||
|
@ -42,6 +43,7 @@ class ElifilmsProvider:MainAPI() {
|
|||
if (items.size <= 0) throw ErrorLoadingException()
|
||||
return HomePageResponse(items)
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse> {
|
||||
val url = "$mainUrl/?s=$query"
|
||||
val doc = app.get(url).document
|
||||
|
@ -52,6 +54,7 @@ class ElifilmsProvider:MainAPI() {
|
|||
(MovieSearchResponse(name, href, this.name, TvType.Movie, poster, null))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
val document = app.get(url, timeout = 120).document
|
||||
val title = document.selectFirst(".post_title h1")?.text() ?: ""
|
||||
|
@ -73,6 +76,7 @@ class ElifilmsProvider:MainAPI() {
|
|||
tags
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun loadLinks(
|
||||
data: String,
|
||||
isCasting: Boolean,
|
||||
|
|
|
@ -8,7 +8,7 @@ import com.lagradost.cloudstream3.utils.loadExtractor
|
|||
class EntrepeliculasyseriesProvider:MainAPI() {
|
||||
override var mainUrl = "https://entrepeliculasyseries.nu"
|
||||
override var name = "EntrePeliculasySeries"
|
||||
override val lang = "es"
|
||||
override var lang = "es"
|
||||
override val hasMainPage = true
|
||||
override val hasChromecastSupport = true
|
||||
override val hasDownloadSupport = true
|
||||
|
|
|
@ -20,7 +20,7 @@ class EstrenosDoramasProvider : MainAPI() {
|
|||
|
||||
override var mainUrl = "https://www23.estrenosdoramas.net"
|
||||
override var name = "EstrenosDoramas"
|
||||
override val lang = "es"
|
||||
override var lang = "es"
|
||||
override val hasMainPage = true
|
||||
override val hasChromecastSupport = true
|
||||
override val hasDownloadSupport = true
|
||||
|
|
|
@ -5,7 +5,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
|
|||
import org.jsoup.nodes.Element
|
||||
|
||||
class FaselHDProvider : MainAPI() {
|
||||
override val lang = "ar"
|
||||
override var lang = "ar"
|
||||
override var mainUrl = "https://faselhd.io"
|
||||
override var name = "FaselHD"
|
||||
override val usesWebView = false
|
||||
|
|
|
@ -11,7 +11,7 @@ import org.jsoup.select.Elements
|
|||
class FilmanProvider : MainAPI() {
|
||||
override var mainUrl = "https://filman.cc"
|
||||
override var name = "filman.cc"
|
||||
override val lang = "pl"
|
||||
override var lang = "pl"
|
||||
override val hasMainPage = true
|
||||
override val supportedTypes = setOf(
|
||||
TvType.Movie,
|
||||
|
|
|
@ -12,7 +12,7 @@ class FrenchStreamProvider : MainAPI() {
|
|||
override var name = "French Stream"
|
||||
override val hasQuickSearch = false
|
||||
override val hasMainPage = true
|
||||
override val lang = "fr"
|
||||
override var lang = "fr"
|
||||
override val supportedTypes = setOf(TvType.AnimeMovie, TvType.TvSeries, TvType.Movie)
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse> {
|
||||
|
|
|
@ -8,9 +8,9 @@ import com.lagradost.cloudstream3.utils.loadExtractor
|
|||
import org.jsoup.Jsoup
|
||||
|
||||
class HDMovie5 : MainAPI() {
|
||||
override var mainUrl = "https://hdmovie5.mba"
|
||||
override var mainUrl = "https://hdmovie2.tv"
|
||||
override var name = "HDMovie"
|
||||
override val lang = "hi"
|
||||
override var lang = "hi"
|
||||
|
||||
override val hasQuickSearch = true
|
||||
override val hasMainPage = true
|
||||
|
|
|
@ -11,7 +11,7 @@ class LayarKacaProvider : MainAPI() {
|
|||
override var mainUrl = "https://149.56.24.226"
|
||||
override var name = "LayarKaca"
|
||||
override val hasMainPage = true
|
||||
override val lang = "id"
|
||||
override var lang = "id"
|
||||
override val hasDownloadSupport = true
|
||||
override val supportedTypes = setOf(
|
||||
TvType.Movie,
|
||||
|
|
|
@ -8,7 +8,7 @@ import org.jsoup.Jsoup
|
|||
import org.jsoup.nodes.Element
|
||||
|
||||
class MyCimaProvider : MainAPI() {
|
||||
override val lang = "ar"
|
||||
override var lang = "ar"
|
||||
override var mainUrl = "https://mycima.tv"
|
||||
override var name = "MyCima"
|
||||
override val usesWebView = false
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.lagradost.cloudstream3.movieproviders
|
||||
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
|
||||
|
@ -95,7 +96,7 @@ class NginxProvider : MainAPI() {
|
|||
this.plot = description
|
||||
this.rating = ratingAverage
|
||||
this.tags = tagsList
|
||||
this.trailers = trailer
|
||||
addTrailer(trailer)
|
||||
addPoster(poster, authHeader)
|
||||
}
|
||||
} else // a tv serie
|
||||
|
|
|
@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.loadExtractor
|
|||
class PeliSmartProvider: MainAPI() {
|
||||
override var mainUrl = "https://pelismart.com"
|
||||
override var name = "PeliSmart"
|
||||
override val lang = "es"
|
||||
override var lang = "es"
|
||||
override val hasMainPage = true
|
||||
override val hasChromecastSupport = true
|
||||
override val hasDownloadSupport = true
|
||||
|
|
|
@ -9,7 +9,7 @@ import com.lagradost.cloudstream3.utils.loadExtractor
|
|||
class PelisflixProvider : MainAPI() {
|
||||
override var mainUrl = "https://pelisflix.li"
|
||||
override var name = "Pelisflix"
|
||||
override val lang = "es"
|
||||
override var lang = "es"
|
||||
override val hasMainPage = true
|
||||
override val hasChromecastSupport = true
|
||||
override val hasDownloadSupport = true
|
||||
|
|
|
@ -8,7 +8,7 @@ import org.jsoup.nodes.Element
|
|||
class PelisplusHDProvider:MainAPI() {
|
||||
override var mainUrl = "https://pelisplushd.net"
|
||||
override var name = "PelisplusHD"
|
||||
override val lang = "es"
|
||||
override var lang = "es"
|
||||
override val hasMainPage = true
|
||||
override val hasChromecastSupport = true
|
||||
override val hasDownloadSupport = true
|
||||
|
|
|
@ -12,7 +12,7 @@ import org.jsoup.Jsoup
|
|||
*/
|
||||
|
||||
open class PelisplusProviderTemplate : MainAPI() {
|
||||
override val lang = "es"
|
||||
override var lang = "es"
|
||||
open val homePageUrlList = listOf<String>()
|
||||
|
||||
// // mainUrl is good to have as a holder for the url to make future changes easier.
|
||||
|
|
|
@ -9,7 +9,7 @@ import com.lagradost.cloudstream3.utils.loadExtractor
|
|||
class PinoyHDXyzProvider : MainAPI() {
|
||||
override var name = "Pinoy-HD"
|
||||
override var mainUrl = "https://www.pinoy-hd.xyz"
|
||||
override val lang = "tl"
|
||||
override var lang = "tl"
|
||||
override val supportedTypes = setOf(TvType.AsianDrama)
|
||||
override val hasDownloadSupport = true
|
||||
override val hasMainPage = true
|
||||
|
|
|
@ -10,7 +10,7 @@ import com.lagradost.cloudstream3.utils.loadExtractor
|
|||
class PinoyMoviePediaProvider : MainAPI() {
|
||||
override var name = "Pinoy Moviepedia"
|
||||
override var mainUrl = "https://pinoymoviepedia.ru"
|
||||
override val lang = "tl"
|
||||
override var lang = "tl"
|
||||
override val supportedTypes = setOf(TvType.AsianDrama)
|
||||
override val hasDownloadSupport = true
|
||||
override val hasMainPage = true
|
||||
|
|
|
@ -15,7 +15,7 @@ import org.jsoup.select.Elements
|
|||
class PinoyMoviesEsProvider : MainAPI() {
|
||||
override var name = "Pinoy Movies"
|
||||
override var mainUrl = "https://pinoymovies.es"
|
||||
override val lang = "tl"
|
||||
override var lang = "tl"
|
||||
override val supportedTypes = setOf(TvType.AsianDrama)
|
||||
override val hasDownloadSupport = false
|
||||
override val hasMainPage = true
|
||||
|
|
|
@ -15,7 +15,7 @@ class RebahinProvider : MainAPI() {
|
|||
override var mainUrl = "http://167.88.14.149"
|
||||
override var name = "Rebahin"
|
||||
override val hasMainPage = true
|
||||
override val lang = "id"
|
||||
override var lang = "id"
|
||||
override val hasDownloadSupport = true
|
||||
override val supportedTypes = setOf(
|
||||
TvType.Movie,
|
||||
|
|
|
@ -9,7 +9,7 @@ import com.lagradost.cloudstream3.utils.loadExtractor
|
|||
class SeriesflixProvider : MainAPI() {
|
||||
override var mainUrl = "https://seriesflix.video"
|
||||
override var name = "Seriesflix"
|
||||
override val lang = "es"
|
||||
override var lang = "es"
|
||||
override val hasMainPage = true
|
||||
override val hasChromecastSupport = true
|
||||
override val hasDownloadSupport = true
|
||||
|
|
|
@ -127,7 +127,7 @@ data class TrailerElement(
|
|||
|
||||
|
||||
class StreamingcommunityProvider : MainAPI() {
|
||||
override val lang = "it"
|
||||
override var lang = "it"
|
||||
override var mainUrl = "https://streamingcommunity.press"
|
||||
override var name = "Streamingcommunity"
|
||||
override val hasMainPage = true
|
||||
|
@ -284,7 +284,7 @@ class StreamingcommunityProvider : MainAPI() {
|
|||
val trailerinfojs = document.select("slider-trailer").attr("videos")
|
||||
val trailerinfo = parseJson<List<TrailerElement>>(trailerinfojs)
|
||||
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 {
|
||||
null
|
||||
}
|
||||
|
|
|
@ -4,9 +4,11 @@ import com.lagradost.cloudstream3.*
|
|||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||
|
||||
|
||||
class TantifilmProvider : MainAPI() {
|
||||
override val lang = "it"
|
||||
override var lang = "it"
|
||||
override var mainUrl = "https://www.tantifilm.rodeo"
|
||||
override var name = "Tantifilm"
|
||||
override val hasMainPage = true
|
||||
|
@ -69,7 +71,6 @@ class TantifilmProvider : MainAPI() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
val document = app.get(url).document
|
||||
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) {
|
||||
val list = ArrayList<Pair<Int, String>>()
|
||||
|
@ -142,22 +143,18 @@ class TantifilmProvider : MainAPI() {
|
|||
}
|
||||
}
|
||||
}
|
||||
return TvSeriesLoadResponse(
|
||||
return newTvSeriesLoadResponse(
|
||||
title,
|
||||
url,
|
||||
this.name,
|
||||
type,
|
||||
episodeList,
|
||||
fixUrlNull(poster),
|
||||
year.toIntOrNull(),
|
||||
descipt[0],
|
||||
null,
|
||||
rating,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
recomm
|
||||
)
|
||||
episodeList) {
|
||||
this.posterUrl= fixUrlNull(poster)
|
||||
this.year = year.toIntOrNull()
|
||||
this.plot= descipt[0]
|
||||
this.rating= rating
|
||||
this.recommendations = recomm
|
||||
addTrailer(trailerurl)
|
||||
}
|
||||
} else {
|
||||
val url2 = document.selectFirst("iframe")!!.attr("src")
|
||||
val actorpagelink =
|
||||
|
@ -217,6 +214,7 @@ class TantifilmProvider : MainAPI() {
|
|||
this.tags = tags
|
||||
this.duration = duratio
|
||||
this.actors = actors
|
||||
addTrailer(trailerurl)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import org.jsoup.Jsoup
|
|||
class VfFilmProvider : MainAPI() {
|
||||
override var mainUrl = "https://vf-film.me"
|
||||
override var name = "vf-film.me"
|
||||
override val lang = "fr"
|
||||
override var lang = "fr"
|
||||
override val hasQuickSearch = false
|
||||
override val hasMainPage = false
|
||||
override val hasChromecastSupport = false
|
||||
|
|
|
@ -9,7 +9,7 @@ import org.jsoup.Jsoup
|
|||
class VfSerieProvider : MainAPI() {
|
||||
override var mainUrl = "https://vf-serie.org"
|
||||
override var name = "vf-serie.org"
|
||||
override val lang = "fr"
|
||||
override var lang = "fr"
|
||||
|
||||
override val hasQuickSearch = false
|
||||
override val hasMainPage = false
|
||||
|
|
|
@ -67,7 +67,7 @@ interface SyncAPI : OAuth2API {
|
|||
var studio: List<String>? = null,
|
||||
var genres: List<String>? = null,
|
||||
var synonyms: List<String>? = null,
|
||||
var trailerUrl: String? = null,
|
||||
var trailers: List<String>? = null,
|
||||
var isAdult : Boolean? = null,
|
||||
var posterUrl: String? = null,
|
||||
var backgroundPosterUrl : String? = null,
|
||||
|
|
|
@ -142,6 +142,10 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
getUrlFromId(recMedia.id),
|
||||
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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -33,6 +33,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
override val redirectUrl = "mallogin"
|
||||
override val idPrefix = "mal"
|
||||
override var mainUrl = "https://myanimelist.net"
|
||||
val apiUrl = "https://api.myanimelist.net"
|
||||
override val icon = R.drawable.mal_logo
|
||||
override val requiresLogin = true
|
||||
|
||||
|
@ -62,7 +63,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
}
|
||||
|
||||
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 res = app.get(
|
||||
url, headers = mapOf(
|
||||
|
@ -179,7 +180,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
name = node?.title ?: return null,
|
||||
apiName = this.name,
|
||||
syncId = node.id.toString(),
|
||||
url = "https://myanimelist.net/anime/${node.id}",
|
||||
url = "$mainUrl/anime/${node.id}",
|
||||
posterUrl = node.main_picture?.large
|
||||
)
|
||||
}
|
||||
|
@ -187,7 +188,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
override suspend fun getResult(id: String): SyncAPI.SyncResult? {
|
||||
val internalId = id.toIntOrNull() ?: return null
|
||||
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(
|
||||
url, headers = mapOf(
|
||||
"Authorization" to "Bearer " + (getAuth() ?: return null)
|
||||
|
@ -195,7 +196,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
).text
|
||||
return mapper.readValue<MalAnime>(res).let { malAnime ->
|
||||
SyncAPI.SyncResult(
|
||||
id = malAnime.id?.toString()!!,
|
||||
id = internalId.toString(),
|
||||
totalEpisodes = malAnime.numEpisodes,
|
||||
title = malAnime.title,
|
||||
publicScore = malAnime.mean?.toFloat()?.times(1000)?.toInt(),
|
||||
|
@ -203,13 +204,14 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
synopsis = malAnime.synopsis,
|
||||
airStatus = when (malAnime.status) {
|
||||
"finished_airing" -> ShowStatus.Completed
|
||||
"airing" -> ShowStatus.Ongoing
|
||||
"currently_airing" -> ShowStatus.Ongoing
|
||||
//"not_yet_aired"
|
||||
else -> null
|
||||
},
|
||||
nextAiring = null,
|
||||
studio = malAnime.studios?.mapNotNull { it.name },
|
||||
genres = malAnime.genres?.map { it.name },
|
||||
trailerUrl = null,
|
||||
trailers = null,
|
||||
startDate = parseDate(malAnime.startDate),
|
||||
endDate = parseDate(malAnime.endDate),
|
||||
recommendations = malAnime.recommendations?.mapNotNull { rec ->
|
||||
|
@ -260,7 +262,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
val currentCode = sanitizer["code"]!!
|
||||
|
||||
val res = app.post(
|
||||
"https://myanimelist.net/v1/oauth2/token",
|
||||
"$mainUrl/v1/oauth2/token",
|
||||
data = mapOf(
|
||||
"client_id" to key,
|
||||
"code" to currentCode,
|
||||
|
@ -292,7 +294,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
.replace("/", "_").replace("\n", "")
|
||||
val codeChallenge = codeVerifier
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -318,7 +320,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
private suspend fun refreshToken() {
|
||||
try {
|
||||
val res = app.post(
|
||||
"https://myanimelist.net/v1/oauth2/token",
|
||||
"$mainUrl/v1/oauth2/token",
|
||||
data = mapOf(
|
||||
"client_id" to key,
|
||||
"grant_type" to "refresh_token",
|
||||
|
@ -451,7 +453,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
// Very lackluster docs
|
||||
// https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_animelist_get
|
||||
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(
|
||||
url, headers = mapOf(
|
||||
"Authorization" to "Bearer $auth",
|
||||
|
@ -463,7 +465,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
private suspend fun getDataAboutMalId(id: Int): SmallMalAnime? {
|
||||
// https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get
|
||||
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(
|
||||
url, headers = mapOf(
|
||||
"Authorization" to "Bearer " + (getAuth() ?: return null)
|
||||
|
@ -481,7 +483,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
checkMalToken()
|
||||
while (!isDone) {
|
||||
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(
|
||||
"Authorization" to "Bearer " + (getAuth() ?: return)
|
||||
), cacheTime = 0
|
||||
|
@ -532,10 +534,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
}
|
||||
|
||||
private suspend fun checkMalToken() {
|
||||
if (unixTime > getKey(
|
||||
if (unixTime > (getKey(
|
||||
accountId,
|
||||
MAL_UNIXTIME_KEY
|
||||
) ?: 0L
|
||||
) ?: 0L)
|
||||
) {
|
||||
refreshToken()
|
||||
}
|
||||
|
@ -544,7 +546,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
private suspend fun getMalUser(setSettings: Boolean = true): MalUser? {
|
||||
checkMalToken()
|
||||
val res = app.get(
|
||||
"https://api.myanimelist.net/v2/users/@me",
|
||||
"$apiUrl/v2/users/@me",
|
||||
headers = mapOf(
|
||||
"Authorization" to "Bearer " + (getAuth() ?: return null)
|
||||
), cacheTime = 0
|
||||
|
@ -620,7 +622,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
).filter { it.value != null } as Map<String, String>
|
||||
|
||||
return app.put(
|
||||
"https://api.myanimelist.net/v2/anime/$id/my_list_status",
|
||||
"$apiUrl/v2/anime/$id/my_list_status",
|
||||
headers = mapOf(
|
||||
"Authorization" to "Bearer " + (getAuth() ?: return null)
|
||||
),
|
||||
|
|
|
@ -13,12 +13,12 @@ class APIRepository(val api: MainAPI) {
|
|||
val noneApi = object : MainAPI() {
|
||||
override var name = "None"
|
||||
override val supportedTypes = emptySet<TvType>()
|
||||
override val lang = ""
|
||||
override var lang = ""
|
||||
}
|
||||
val randomApi = object : MainAPI() {
|
||||
override var name = "Random"
|
||||
override val supportedTypes = emptySet<TvType>()
|
||||
override val lang = ""
|
||||
override var lang = ""
|
||||
}
|
||||
|
||||
fun isInvalidData(data: String): Boolean {
|
||||
|
|
|
@ -1,17 +1,11 @@
|
|||
package com.lagradost.cloudstream3.ui.player
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.util.SparseArray
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.util.forEach
|
||||
import at.huber.youtubeExtractor.VideoMeta
|
||||
import at.huber.youtubeExtractor.YouTubeExtractor
|
||||
import at.huber.youtubeExtractor.YtFile
|
||||
import com.google.android.exoplayer2.*
|
||||
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider
|
||||
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource
|
||||
|
@ -38,7 +32,6 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
|||
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.ExtractorUri
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
|
||||
import java.io.File
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
|
@ -334,7 +327,6 @@ class CS3IPlayer : IPlayer {
|
|||
}
|
||||
|
||||
companion object {
|
||||
private var ytVideos: MutableMap<String, YtFile> = mutableMapOf()
|
||||
private var simpleCache: SimpleCache? = null
|
||||
|
||||
var requestSubtitleUpdate: (() -> Unit)? = null
|
||||
|
@ -534,7 +526,6 @@ class CS3IPlayer : IPlayer {
|
|||
)
|
||||
setHandleAudioBecomingNoisy(true)
|
||||
setPlaybackSpeed(playBackSpeed)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -711,7 +702,7 @@ class CS3IPlayer : IPlayer {
|
|||
if (playWhenReady) {
|
||||
when (playbackState) {
|
||||
Player.STATE_READY -> {
|
||||
requestAutoFocus?.invoke()
|
||||
|
||||
}
|
||||
Player.STATE_ENDED -> {
|
||||
handleEvent(CSPlayerEvent.NextEpisode)
|
||||
|
@ -740,6 +731,7 @@ class CS3IPlayer : IPlayer {
|
|||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
super.onIsPlayingChanged(isPlaying)
|
||||
if (isPlaying) {
|
||||
requestAutoFocus?.invoke()
|
||||
onRenderFirst()
|
||||
}
|
||||
}
|
||||
|
@ -886,55 +878,9 @@ class CS3IPlayer : IPlayer {
|
|||
return Pair(subSources, activeSubtitles)
|
||||
}
|
||||
|
||||
|
||||
fun loadYtFile(context: Context, yt: YtFile) {
|
||||
loadOnlinePlayer(
|
||||
context,
|
||||
ExtractorLink(
|
||||
"YouTube",
|
||||
"",
|
||||
yt.url,
|
||||
"",
|
||||
yt.format?.height ?: Qualities.Unknown.value
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadOnlinePlayer(context: Context, link: ExtractorLink) {
|
||||
Log.i(TAG, "loadOnlinePlayer $link")
|
||||
try {
|
||||
if (link.url.contains("youtube.com")) {
|
||||
val ytLink = link.url.replace("/embed/", "/watch?v=")
|
||||
ytVideos[ytLink]?.let {
|
||||
loadYtFile(context, it)
|
||||
return
|
||||
}
|
||||
val ytExtractor =
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
object : YouTubeExtractor(context) {
|
||||
override fun onExtractionComplete(
|
||||
ytFiles: SparseArray<YtFile>?,
|
||||
videoMeta: VideoMeta?
|
||||
) {
|
||||
var yt: YtFile? = null
|
||||
ytFiles?.forEach { _, value ->
|
||||
if ((yt?.format?.height ?: 0) < (value.format?.height
|
||||
?: -1) && (value.format?.audioBitrate ?: -1) > 0
|
||||
) {
|
||||
yt = value
|
||||
}
|
||||
}
|
||||
yt?.let { ytf ->
|
||||
ytVideos[ytLink] = ytf
|
||||
loadYtFile(context, ytf)
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.i(TAG, "YouTube extraction on $ytLink")
|
||||
ytExtractor.extract(ytLink)
|
||||
return
|
||||
}
|
||||
|
||||
currentLink = link
|
||||
|
||||
if (ignoreSSL) {
|
||||
|
|
|
@ -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) {
|
||||
player_ffwd_holder?.alpha = 1f
|
||||
player_rew_holder?.alpha = 1f
|
||||
|
@ -251,6 +260,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
}
|
||||
|
||||
bottom_player_bar?.startAnimation(fadeAnimation)
|
||||
player_open_source?.startAnimation(fadeAnimation)
|
||||
player_top_holder?.startAnimation(fadeAnimation)
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import android.view.ViewGroup
|
|||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.ContentLoadingProgressBar
|
||||
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.VideoDownloadManager
|
||||
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_large.view.*
|
||||
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_SHOW_TOAST = 12
|
||||
const val ACTION_SHOW_DESCRIPTION = 15
|
||||
|
||||
const val ACTION_DOWNLOAD_EPISODE_SUBTITLE = 13
|
||||
const val ACTION_DOWNLOAD_EPISODE_SUBTITLE_MIRROR = 14
|
||||
|
@ -93,10 +94,10 @@ class EpisodeAdapter(
|
|||
@LayoutRes
|
||||
private var layout: Int = 0
|
||||
fun updateLayout() {
|
||||
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
|
||||
else R.layout.result_episode
|
||||
// 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
|
||||
// else R.layout.result_episode
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
|
@ -105,7 +106,7 @@ class EpisodeAdapter(
|
|||
else R.layout.result_episode*/
|
||||
|
||||
return EpisodeCardViewHolder(
|
||||
LayoutInflater.from(parent.context).inflate(layout, parent, false),
|
||||
LayoutInflater.from(parent.context).inflate(R.layout.result_episode_both, parent, false),
|
||||
hasDownloadSupport,
|
||||
clickCallback,
|
||||
downloadClickCallback
|
||||
|
@ -134,27 +135,39 @@ class EpisodeAdapter(
|
|||
) : RecyclerView.ViewHolder(itemView), DownloadButtonViewHolder {
|
||||
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
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun bind(card: ResultEpisode) {
|
||||
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
|
||||
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
|
||||
|
||||
val displayPos = card.getDisplayPosition()
|
||||
|
@ -171,16 +184,20 @@ class EpisodeAdapter(
|
|||
}
|
||||
|
||||
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 {
|
||||
episodeRating?.text = ""
|
||||
}
|
||||
|
||||
if (card.description != null) {
|
||||
episodeDescript?.visibility = View.VISIBLE
|
||||
episodeDescript?.text = card.description
|
||||
} else {
|
||||
episodeDescript?.visibility = View.GONE
|
||||
episodeRating?.isGone = episodeRating?.text.isNullOrBlank()
|
||||
|
||||
episodeDescript?.apply {
|
||||
text = card.description ?: ""
|
||||
isGone = text.isNullOrBlank()
|
||||
setOnClickListener {
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_DESCRIPTION, card))
|
||||
}
|
||||
}
|
||||
|
||||
episodePoster?.setOnClickListener {
|
||||
|
@ -192,34 +209,42 @@ class EpisodeAdapter(
|
|||
return@setOnLongClickListener true
|
||||
}
|
||||
|
||||
episodeHolder.setOnClickListener {
|
||||
parentView.setOnClickListener {
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
|
||||
}
|
||||
|
||||
if (episodeHolder.context.isTrueTvSettings()) {
|
||||
episodeHolder.isFocusable = true
|
||||
episodeHolder.isFocusableInTouchMode = true
|
||||
episodeHolder.touchscreenBlocksFocus = false
|
||||
if (parentView.context.isTrueTvSettings()) {
|
||||
parentView.isFocusable = true
|
||||
parentView.isFocusableInTouchMode = true
|
||||
parentView.touchscreenBlocksFocus = false
|
||||
}
|
||||
|
||||
episodeHolder.setOnLongClickListener {
|
||||
parentView.setOnLongClickListener {
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card))
|
||||
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
|
||||
episodeDownloadImage.isVisible = hasDownloadSupport
|
||||
episodeDownloadBar.isVisible = hasDownloadSupport
|
||||
episodeDownloadImage?.isVisible = hasDownloadSupport
|
||||
episodeDownloadBar?.isVisible = hasDownloadSupport
|
||||
reattachDownloadButton()
|
||||
}
|
||||
|
||||
override fun reattachDownloadButton() {
|
||||
downloadButton.dispose()
|
||||
val card = localCard
|
||||
if (hasDownloadSupport && card != null) {
|
||||
val downloadInfo = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(itemView.context, card.id)
|
||||
val downloadInfo = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(
|
||||
itemView.context,
|
||||
card.id
|
||||
)
|
||||
|
||||
downloadButton.setUpButton(
|
||||
downloadInfo?.fileLength, downloadInfo?.totalBytes, episodeDownloadBar, episodeDownloadImage, null,
|
||||
downloadInfo?.fileLength,
|
||||
downloadInfo?.totalBytes,
|
||||
episodeDownloadBar ?: return,
|
||||
episodeDownloadImage ?: return,
|
||||
null,
|
||||
VideoDownloadHelper.DownloadEpisodeCached(
|
||||
card.name,
|
||||
card.poster,
|
||||
|
|
|
@ -41,6 +41,7 @@ import com.google.android.material.button.MaterialButton
|
|||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiFromName
|
||||
import com.lagradost.cloudstream3.APIHolder.getId
|
||||
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.CommonActivity.getCastSession
|
||||
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.DownloadButtonSetup.handleDownloadClick
|
||||
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.RepoLinkGenerator
|
||||
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 kotlinx.android.synthetic.main.fragment_result.*
|
||||
import kotlinx.android.synthetic.main.fragment_result_swipe.*
|
||||
import kotlinx.android.synthetic.main.fragment_trailer.*
|
||||
import kotlinx.android.synthetic.main.result_recommendations.*
|
||||
import kotlinx.android.synthetic.main.result_sync.*
|
||||
import kotlinx.android.synthetic.main.trailer_custom_layout.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
@ -599,7 +603,7 @@ class ResultFragment : ResultTrailerPlayer() {
|
|||
setFormatText(result_meta_rating, R.string.rating_format, rating?.div(1000f))
|
||||
}
|
||||
|
||||
var currentTrailers: List<String> = emptyList()
|
||||
var currentTrailers: List<ExtractorLink> = emptyList()
|
||||
var currentTrailerIndex = 0
|
||||
|
||||
override fun nextMirror() {
|
||||
|
@ -608,48 +612,43 @@ class ResultFragment : ResultTrailerPlayer() {
|
|||
}
|
||||
|
||||
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)
|
||||
} else {
|
||||
nextMirror()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadTrailer(index: Int? = null) {
|
||||
currentTrailers.getOrNull(index ?: currentTrailerIndex)?.let { trailer ->
|
||||
//if(trailer.contains("youtube.com")) { // wont load in exo
|
||||
// nextMirror()
|
||||
// return
|
||||
//}
|
||||
context?.let { ctx ->
|
||||
player.onPause()
|
||||
player.loadPlayer(
|
||||
ctx,
|
||||
false,
|
||||
ExtractorLink(
|
||||
"",
|
||||
"Trailer",
|
||||
val isSuccess =
|
||||
currentTrailers.getOrNull(index ?: currentTrailerIndex)?.let { trailer ->
|
||||
context?.let { ctx ->
|
||||
player.onPause()
|
||||
player.loadPlayer(
|
||||
ctx,
|
||||
false,
|
||||
trailer,
|
||||
"",
|
||||
Qualities.Unknown.value
|
||||
),
|
||||
null,
|
||||
startPosition = 0L,
|
||||
subtitles = emptySet(),
|
||||
subtitle = null,
|
||||
autoPlay = false
|
||||
)
|
||||
null,
|
||||
startPosition = 0L,
|
||||
subtitles = emptySet(),
|
||||
subtitle = null,
|
||||
autoPlay = false
|
||||
)
|
||||
true
|
||||
} ?: run {
|
||||
false
|
||||
}
|
||||
} ?: run {
|
||||
false
|
||||
}
|
||||
}
|
||||
result_trailer_loading?.isVisible = isSuccess
|
||||
}
|
||||
|
||||
private fun setTrailers(trailers: List<String>?) {
|
||||
context?.let { ctx ->
|
||||
if (ctx.isTvSettings()) return
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
val showTrailers =
|
||||
settingsManager.getBoolean(ctx.getString(R.string.show_trailers_key), true)
|
||||
if (!showTrailers) return
|
||||
currentTrailers = trailers ?: emptyList()
|
||||
loadTrailer()
|
||||
}
|
||||
private fun setTrailers(trailers: List<ExtractorLink>?) {
|
||||
context?.updateHasTrailers()
|
||||
if (!LoadResponse.isTrailersEnabled) return
|
||||
currentTrailers = trailers?.sortedBy { -it.quality } ?: emptyList()
|
||||
loadTrailer()
|
||||
}
|
||||
|
||||
private fun setActors(actors: List<ActorData>?) {
|
||||
|
@ -758,6 +757,12 @@ class ResultFragment : ResultTrailerPlayer() {
|
|||
result_overlapping_panels?.setStartPanelLockState(OverlappingPanelsLayout.LockState.CLOSE)
|
||||
result_overlapping_panels?.setEndPanelLockState(OverlappingPanelsLayout.LockState.CLOSE)
|
||||
|
||||
player_open_source?.setOnClickListener {
|
||||
currentTrailers.getOrNull(currentTrailerIndex)?.let {
|
||||
context?.openBrowser(it.url)
|
||||
}
|
||||
}
|
||||
|
||||
updateUIListener = ::updateUI
|
||||
|
||||
val restart = arguments?.getBoolean(RESTART_BUNDLE) ?: false
|
||||
|
@ -767,6 +772,7 @@ class ResultFragment : ResultTrailerPlayer() {
|
|||
|
||||
activity?.window?.decorView?.clearFocus()
|
||||
hideKeyboard()
|
||||
context?.updateHasTrailers()
|
||||
activity?.loadCache()
|
||||
|
||||
activity?.fixPaddingStatusbar(result_top_bar)
|
||||
|
@ -828,6 +834,12 @@ class ResultFragment : ResultTrailerPlayer() {
|
|||
} else if (dy < -5) {
|
||||
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()
|
||||
})
|
||||
|
||||
|
@ -975,6 +987,7 @@ class ResultFragment : ResultTrailerPlayer() {
|
|||
}
|
||||
ACTION_CHROME_CAST_EPISODE -> requireLinks(true)
|
||||
ACTION_CHROME_CAST_MIRROR -> requireLinks(true)
|
||||
ACTION_SHOW_DESCRIPTION -> true
|
||||
else -> requireLinks(false)
|
||||
}
|
||||
if (!isLoaded) return@main // CANT LOAD
|
||||
|
@ -984,6 +997,14 @@ class ResultFragment : ResultTrailerPlayer() {
|
|||
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 -> {
|
||||
context?.let { ctx ->
|
||||
if (ctx.isConnectedToChromecast()) {
|
||||
|
@ -1431,7 +1452,7 @@ class ResultFragment : ResultTrailerPlayer() {
|
|||
val d = meta.value
|
||||
result_sync_episodes?.progress = currentSyncProgress * 1000
|
||||
setSyncMaxEpisodes(d.totalEpisodes)
|
||||
viewModel.setMeta(d)
|
||||
viewModel.setMeta(d, syncdata)
|
||||
}
|
||||
is Resource.Loading -> {
|
||||
result_sync_max_episodes?.text =
|
||||
|
|
|
@ -53,6 +53,7 @@ open class ResultTrailerPlayer : com.lagradost.cloudstream3.ui.player.FullScreen
|
|||
screenHeight
|
||||
}
|
||||
|
||||
result_trailer_loading?.isVisible = false
|
||||
player_background?.apply {
|
||||
isVisible = true
|
||||
layoutParams =
|
||||
|
|
|
@ -13,17 +13,22 @@ import com.lagradost.cloudstream3.APIHolder.getId
|
|||
import com.lagradost.cloudstream3.AcraApplication.Companion.context
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
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.NineAnimeProvider
|
||||
import com.lagradost.cloudstream3.metaproviders.SyncRedirector
|
||||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||
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.WatchType
|
||||
import com.lagradost.cloudstream3.ui.player.IGenerator
|
||||
import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator
|
||||
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.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
|
||||
|
@ -70,7 +75,6 @@ class ResultViewModel : ViewModel() {
|
|||
val dubStatus: LiveData<DubStatus> get() = _dubStatus
|
||||
private val _dubStatus: MutableLiveData<DubStatus> = MutableLiveData()
|
||||
|
||||
private val page: MutableLiveData<LoadResponse> = MutableLiveData()
|
||||
val id: MutableLiveData<Int> = MutableLiveData()
|
||||
val selectedSeason: MutableLiveData<Int> = MutableLiveData(-2)
|
||||
val seasonSelections: MutableLiveData<List<Int?>> = MutableLiveData()
|
||||
|
@ -88,11 +92,12 @@ class ResultViewModel : ViewModel() {
|
|||
fun updateWatchStatus(status: WatchType) = viewModelScope.launch {
|
||||
val currentId = id.value ?: return@launch
|
||||
_watchStatus.postValue(status)
|
||||
val resultPage = page.value
|
||||
val resultPage = _resultResponse.value
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
setResultWatchState(currentId, status.internalId)
|
||||
if (resultPage != null) {
|
||||
if (resultPage != null && resultPage is Resource.Success) {
|
||||
val resultPageData = resultPage.value
|
||||
val current = getBookmarkedData(currentId)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
setBookmarkedData(
|
||||
|
@ -101,12 +106,12 @@ class ResultViewModel : ViewModel() {
|
|||
currentId,
|
||||
current?.bookmarkedTime ?: currentTime,
|
||||
currentTime,
|
||||
resultPage.name,
|
||||
resultPage.url,
|
||||
resultPage.apiName,
|
||||
resultPage.type,
|
||||
resultPage.posterUrl,
|
||||
resultPage.year
|
||||
resultPageData.name,
|
||||
resultPageData.url,
|
||||
resultPageData.apiName,
|
||||
resultPageData.type,
|
||||
resultPageData.posterUrl,
|
||||
resultPageData.year
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -118,20 +123,29 @@ class ResultViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
var lastMeta: SyncAPI.SyncResult? = null
|
||||
private fun applyMeta(resp: LoadResponse, meta: SyncAPI.SyncResult?): LoadResponse {
|
||||
if (meta == null) return resp
|
||||
lastMeta = meta
|
||||
return resp.apply {
|
||||
var lastSync: Map<String, String>? = null
|
||||
|
||||
private suspend fun applyMeta(
|
||||
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")
|
||||
|
||||
duration = duration ?: meta.duration
|
||||
rating = rating ?: meta.publicScore
|
||||
tags = tags ?: meta.genres
|
||||
plot = if (plot.isNullOrBlank()) meta.synopsis else plot
|
||||
addTrailer(meta.trailerUrl)
|
||||
posterUrl = posterUrl ?: meta.posterUrl ?: meta.backgroundPosterUrl
|
||||
actors = actors ?: meta.actors
|
||||
|
||||
for ((k, v) in syncs ?: emptyMap()) {
|
||||
syncData[k] = v
|
||||
}
|
||||
|
||||
val realRecommendations = ArrayList<SearchResponse>()
|
||||
val apiNames = listOf(GogoanimeProvider().name, NineAnimeProvider().name)
|
||||
meta.recommendations?.forEach { rec ->
|
||||
|
@ -142,15 +156,55 @@ class ResultViewModel : ViewModel() {
|
|||
|
||||
recommendations = recommendations?.union(realRecommendations)?.toList()
|
||||
?: 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) {
|
||||
Log.i(TAG, "setMeta")
|
||||
(result.value as? Resource.Success<LoadResponse>?)?.value?.let { resp ->
|
||||
_resultResponse.postValue(Resource.Success(applyMeta(resp, meta)))
|
||||
fun setMeta(meta: SyncAPI.SyncResult, syncs: Map<String, String>?) =
|
||||
viewModelScope.launch {
|
||||
Log.i(TAG, "setMeta")
|
||||
lastMeta = meta
|
||||
lastSync = syncs
|
||||
val (value, updateEpisodes) = ioWork {
|
||||
(result.value as? Resource.Success<LoadResponse>?)?.value?.let { resp ->
|
||||
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) {
|
||||
val currentId = localId ?: id.value ?: return
|
||||
|
@ -310,6 +364,159 @@ class ResultViewModel : ViewModel() {
|
|||
return name
|
||||
}
|
||||
|
||||
var lastShowFillers = false
|
||||
private suspend fun updateEpisodes(loadResponse: LoadResponse, showFillers: Boolean) {
|
||||
Log.i(TAG, "updateEpisodes")
|
||||
try {
|
||||
lastShowFillers = showFillers
|
||||
val mainId = loadResponse.getId()
|
||||
|
||||
when (loadResponse) {
|
||||
is AnimeLoadResponse -> {
|
||||
if (loadResponse.episodes.isEmpty()) {
|
||||
_dubSubEpisodes.postValue(emptyMap())
|
||||
return
|
||||
}
|
||||
|
||||
// val status = getDub(mainId)
|
||||
val statuses = loadResponse.episodes.map { it.key }
|
||||
|
||||
// Extremely bruh to have to take in context here, but I'm not sure how to do this in a better way :(
|
||||
val preferDub = context?.getApiDubstatusSettings()
|
||||
?.contains(DubStatus.Dubbed) == true
|
||||
|
||||
// 3 statements because there can be only dub even if you do not prefer it.
|
||||
val dubStatus =
|
||||
if (preferDub && statuses.contains(DubStatus.Dubbed)) DubStatus.Dubbed
|
||||
else if (!preferDub && statuses.contains(DubStatus.Subbed)) DubStatus.Subbed
|
||||
else statuses.first()
|
||||
|
||||
val fillerEpisodes =
|
||||
if (showFillers) safeApiCall { getFillerEpisodes(loadResponse.name) } else null
|
||||
|
||||
val existingEpisodes = HashSet<Int>()
|
||||
val res = loadResponse.episodes.map { ep ->
|
||||
val episodes = ArrayList<ResultEpisode>()
|
||||
val idIndex = ep.key.id
|
||||
for ((index, i) in ep.value.withIndex()) {
|
||||
val episode = i.episode ?: (index + 1)
|
||||
val id = mainId + episode + idIndex * 1000000
|
||||
if (!existingEpisodes.contains(episode)) {
|
||||
existingEpisodes.add(id)
|
||||
episodes.add(buildResultEpisode(
|
||||
loadResponse.name,
|
||||
filterName(i.name),
|
||||
i.posterUrl,
|
||||
episode,
|
||||
i.season,
|
||||
i.data,
|
||||
loadResponse.apiName,
|
||||
id,
|
||||
index,
|
||||
i.rating,
|
||||
i.description,
|
||||
if (fillerEpisodes is Resource.Success) fillerEpisodes.value?.let {
|
||||
it.contains(episode) && it[episode] == true
|
||||
} ?: false else false,
|
||||
loadResponse.type,
|
||||
mainId
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
Pair(ep.key, episodes)
|
||||
}.toMap()
|
||||
|
||||
// These posts needs to be in this order as to make the preferDub in ResultFragment work
|
||||
_dubSubEpisodes.postValue(res)
|
||||
res[dubStatus]?.let { episodes ->
|
||||
updateEpisodes(mainId, episodes, -1)
|
||||
}
|
||||
_dubStatus.postValue(dubStatus)
|
||||
_dubSubSelections.postValue(loadResponse.episodes.keys)
|
||||
}
|
||||
|
||||
is TvSeriesLoadResponse -> {
|
||||
val episodes = ArrayList<ResultEpisode>()
|
||||
val existingEpisodes = HashSet<Int>()
|
||||
for ((index, episode) in loadResponse.episodes.sortedBy {
|
||||
(it.season?.times(10000) ?: 0) + (it.episode ?: 0)
|
||||
}.withIndex()) {
|
||||
val episodeIndex = episode.episode ?: (index + 1)
|
||||
val id =
|
||||
mainId + (episode.season?.times(100000) ?: 0) + episodeIndex + 1
|
||||
if (!existingEpisodes.contains(id)) {
|
||||
existingEpisodes.add(id)
|
||||
episodes.add(
|
||||
buildResultEpisode(
|
||||
loadResponse.name,
|
||||
filterName(episode.name),
|
||||
episode.posterUrl,
|
||||
episodeIndex,
|
||||
episode.season,
|
||||
episode.data,
|
||||
loadResponse.apiName,
|
||||
id,
|
||||
index,
|
||||
episode.rating,
|
||||
episode.description,
|
||||
null,
|
||||
loadResponse.type,
|
||||
mainId
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
updateEpisodes(mainId, episodes, -1)
|
||||
}
|
||||
is MovieLoadResponse -> {
|
||||
buildResultEpisode(
|
||||
loadResponse.name,
|
||||
loadResponse.name,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
loadResponse.dataUrl,
|
||||
loadResponse.apiName,
|
||||
(mainId), // HAS SAME ID
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
loadResponse.type,
|
||||
mainId
|
||||
).let {
|
||||
updateEpisodes(mainId, listOf(it), -1)
|
||||
}
|
||||
}
|
||||
is TorrentLoadResponse -> {
|
||||
updateEpisodes(
|
||||
mainId, listOf(
|
||||
buildResultEpisode(
|
||||
loadResponse.name,
|
||||
loadResponse.name,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
loadResponse.torrent ?: loadResponse.magnet ?: "",
|
||||
loadResponse.apiName,
|
||||
(mainId), // HAS SAME ID
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
loadResponse.type,
|
||||
mainId
|
||||
)
|
||||
), -1
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun load(url: String, apiName: String, showFillers: Boolean) = viewModelScope.launch {
|
||||
_publicEpisodes.postValue(Resource.Loading())
|
||||
_resultResponse.postValue(Resource.Loading(url))
|
||||
|
@ -356,8 +563,10 @@ class ResultViewModel : ViewModel() {
|
|||
|
||||
when (data) {
|
||||
is Resource.Success -> {
|
||||
val loadResponse = applyMeta(data.value, lastMeta)
|
||||
page.postValue(loadResponse)
|
||||
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)
|
||||
|
@ -375,148 +584,7 @@ class ResultViewModel : ViewModel() {
|
|||
System.currentTimeMillis(),
|
||||
)
|
||||
)
|
||||
|
||||
when (loadResponse) {
|
||||
is AnimeLoadResponse -> {
|
||||
if (loadResponse.episodes.isEmpty()) {
|
||||
_dubSubEpisodes.postValue(emptyMap())
|
||||
return@launch
|
||||
}
|
||||
|
||||
// val status = getDub(mainId)
|
||||
val statuses = loadResponse.episodes.map { it.key }
|
||||
|
||||
// Extremely bruh to have to take in context here, but I'm not sure how to do this in a better way :(
|
||||
val preferDub = context?.getApiDubstatusSettings()
|
||||
?.contains(DubStatus.Dubbed) == true
|
||||
|
||||
// 3 statements because there can be only dub even if you do not prefer it.
|
||||
val dubStatus =
|
||||
if (preferDub && statuses.contains(DubStatus.Dubbed)) DubStatus.Dubbed
|
||||
else if (!preferDub && statuses.contains(DubStatus.Subbed)) DubStatus.Subbed
|
||||
else statuses.first()
|
||||
|
||||
val fillerEpisodes =
|
||||
if (showFillers) safeApiCall { getFillerEpisodes(loadResponse.name) } else null
|
||||
|
||||
val existingEpisodes = HashSet<Int>()
|
||||
val res = loadResponse.episodes.map { ep ->
|
||||
val episodes = ArrayList<ResultEpisode>()
|
||||
val idIndex = ep.key.id
|
||||
for ((index, i) in ep.value.withIndex()) {
|
||||
val episode = i.episode ?: (index + 1)
|
||||
val id = mainId + episode + idIndex * 1000000
|
||||
if (!existingEpisodes.contains(episode)) {
|
||||
existingEpisodes.add(id)
|
||||
episodes.add(buildResultEpisode(
|
||||
loadResponse.name,
|
||||
filterName(i.name),
|
||||
i.posterUrl,
|
||||
episode,
|
||||
i.season,
|
||||
i.data,
|
||||
apiName,
|
||||
id,
|
||||
index,
|
||||
i.rating,
|
||||
i.description,
|
||||
if (fillerEpisodes is Resource.Success) fillerEpisodes.value?.let {
|
||||
it.contains(episode) && it[episode] == true
|
||||
} ?: false else false,
|
||||
loadResponse.type,
|
||||
mainId
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
Pair(ep.key, episodes)
|
||||
}.toMap()
|
||||
|
||||
// These posts needs to be in this order as to make the preferDub in ResultFragment work
|
||||
_dubSubEpisodes.postValue(res)
|
||||
res[dubStatus]?.let { episodes ->
|
||||
updateEpisodes(mainId, episodes, -1)
|
||||
}
|
||||
_dubStatus.postValue(dubStatus)
|
||||
_dubSubSelections.postValue(loadResponse.episodes.keys)
|
||||
}
|
||||
|
||||
is TvSeriesLoadResponse -> {
|
||||
val episodes = ArrayList<ResultEpisode>()
|
||||
val existingEpisodes = HashSet<Int>()
|
||||
for ((index, episode) in loadResponse.episodes.sortedBy {
|
||||
(it.season?.times(10000) ?: 0) + (it.episode ?: 0)
|
||||
}.withIndex()) {
|
||||
val episodeIndex = episode.episode ?: (index + 1)
|
||||
val id =
|
||||
mainId + (episode.season?.times(100000) ?: 0) + episodeIndex + 1
|
||||
if (!existingEpisodes.contains(id)) {
|
||||
existingEpisodes.add(id)
|
||||
episodes.add(
|
||||
buildResultEpisode(
|
||||
loadResponse.name,
|
||||
filterName(episode.name),
|
||||
episode.posterUrl,
|
||||
episodeIndex,
|
||||
episode.season,
|
||||
episode.data,
|
||||
apiName,
|
||||
id,
|
||||
index,
|
||||
episode.rating,
|
||||
episode.description,
|
||||
null,
|
||||
loadResponse.type,
|
||||
mainId
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
updateEpisodes(mainId, episodes, -1)
|
||||
}
|
||||
is MovieLoadResponse -> {
|
||||
buildResultEpisode(
|
||||
loadResponse.name,
|
||||
loadResponse.name,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
loadResponse.dataUrl,
|
||||
loadResponse.apiName,
|
||||
(mainId), // HAS SAME ID
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
loadResponse.type,
|
||||
mainId
|
||||
).let {
|
||||
updateEpisodes(mainId, listOf(it), -1)
|
||||
}
|
||||
}
|
||||
is TorrentLoadResponse -> {
|
||||
updateEpisodes(
|
||||
mainId, listOf(
|
||||
buildResultEpisode(
|
||||
loadResponse.name,
|
||||
loadResponse.name,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
loadResponse.torrent ?: loadResponse.magnet ?: "",
|
||||
loadResponse.apiName,
|
||||
(mainId), // HAS SAME ID
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
loadResponse.type,
|
||||
mainId
|
||||
)
|
||||
), -1
|
||||
)
|
||||
}
|
||||
}
|
||||
updateEpisodes(loadResponse, showFillers)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
|
|
|
@ -6,12 +6,18 @@ import android.os.Build
|
|||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.lagradost.cloudstream3.APIHolder.allProviders
|
||||
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.app
|
||||
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.setUpToolbar
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
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.getBasePath
|
||||
import kotlinx.android.synthetic.main.add_remove_sites.*
|
||||
import kotlinx.android.synthetic.main.add_site_input.*
|
||||
import java.io.File
|
||||
|
||||
class SettingsGeneral : PreferenceFragmentCompat() {
|
||||
|
@ -31,6 +43,17 @@ class SettingsGeneral : PreferenceFragmentCompat() {
|
|||
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
|
||||
private val pathPicker =
|
||||
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
|
||||
|
@ -64,6 +87,94 @@ class SettingsGeneral : PreferenceFragmentCompat() {
|
|||
setPreferencesFromResource(R.xml.settins_general, rootKey)
|
||||
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 {
|
||||
val builder: AlertDialog.Builder =
|
||||
AlertDialog.Builder(it.context, R.style.AlertDialogCustom)
|
||||
|
@ -72,7 +183,7 @@ class SettingsGeneral : PreferenceFragmentCompat() {
|
|||
builder.show()
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
|
||||
getPref(R.string.dns_key)?.setOnPreferenceClickListener {
|
||||
val prefNames = resources.getStringArray(R.array.dns_pref)
|
||||
val prefValues = resources.getIntArray(R.array.dns_pref_values)
|
||||
|
@ -177,6 +288,5 @@ class SettingsGeneral : PreferenceFragmentCompat() {
|
|||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -3,28 +3,31 @@ package com.lagradost.cloudstream3.utils
|
|||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
object Coroutines {
|
||||
fun main(work: suspend (() -> Unit)) : Job {
|
||||
fun main(work: suspend (() -> Unit)): Job {
|
||||
return CoroutineScope(Dispatchers.Main).launch {
|
||||
work()
|
||||
}
|
||||
}
|
||||
|
||||
fun ioSafe(work: suspend (() -> Unit)) : Job {
|
||||
fun ioSafe(work: suspend (() -> Unit)): Job {
|
||||
return CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
work()
|
||||
} catch (e : Exception) {
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> ioWork(work: suspend (() -> T)): T {
|
||||
return withContext(Dispatchers.IO) {
|
||||
work()
|
||||
}
|
||||
}
|
||||
|
||||
fun runOnMainThread(work: (() -> Unit)) {
|
||||
val mainHandler = Handler(Looper.getMainLooper())
|
||||
mainHandler.post {
|
||||
|
|
|
@ -14,6 +14,7 @@ const val DOWNLOAD_HEADER_CACHE = "download_header_cache"
|
|||
const val DOWNLOAD_EPISODE_CACHE = "download_episode_cache"
|
||||
const val VIDEO_PLAYER_BRIGHTNESS = "video_player_alpha_key"
|
||||
const val HOMEPAGE_API = "home_api_used"
|
||||
const val USER_PROVIDER_API = "user_custom_sites"
|
||||
|
||||
const val PREFERENCES_NAME = "rebuild_preference"
|
||||
|
||||
|
|
|
@ -106,6 +106,19 @@ suspend fun loadExtractor(
|
|||
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(
|
||||
//AllProvider(),
|
||||
WcoStream(),
|
||||
|
@ -205,6 +218,9 @@ val extractorApis: Array<ExtractorApi> = arrayOf(
|
|||
KotakAnimeid(),
|
||||
Neonime8n(),
|
||||
Neonime7n(),
|
||||
|
||||
YoutubeExtractor(),
|
||||
YoutubeShortLinkExtractor(),
|
||||
)
|
||||
|
||||
fun getExtractorApiFromName(name: String): ExtractorApi {
|
||||
|
|
|
@ -11,9 +11,11 @@ object FillerEpisodeCheck {
|
|||
private const val MAIN_URL = "https://www.animefillerlist.com"
|
||||
|
||||
var list: HashMap<String, String>? = null
|
||||
var cache: HashMap<String, HashMap<Int, Boolean>> = hashMapOf()
|
||||
|
||||
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 {
|
||||
|
@ -61,6 +63,9 @@ object FillerEpisodeCheck {
|
|||
|
||||
suspend fun getFillerEpisodes(query: String): HashMap<Int, Boolean>? {
|
||||
try {
|
||||
cache[query]?.let {
|
||||
return it
|
||||
}
|
||||
if (!getFillerList()) return null
|
||||
val localList = list ?: return null
|
||||
|
||||
|
@ -75,9 +80,15 @@ object FillerEpisodeCheck {
|
|||
"(\\d+)" // year
|
||||
)
|
||||
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
|
||||
val href = localList[realQuery]?.replace(MAIN_URL, "") ?: return null // JUST IN CASE
|
||||
val result = app.get("$MAIN_URL$href").text
|
||||
|
@ -90,6 +101,7 @@ object FillerEpisodeCheck {
|
|||
hashMap[episodeNumber] = type
|
||||
}
|
||||
}
|
||||
cache[query] = hashMap
|
||||
return hashMap
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
|
|
|
@ -14,23 +14,33 @@ import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
|
||||
object SingleSelectionHelper {
|
||||
fun Activity.showOptionSelectStringRes(
|
||||
fun Activity?.showOptionSelectStringRes(
|
||||
view: View?,
|
||||
poster: String?,
|
||||
options: List<Int>,
|
||||
tvOptions: List<Int> = listOf(),
|
||||
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?,
|
||||
poster: String?,
|
||||
options: List<String>,
|
||||
tvOptions: List<String>,
|
||||
callback: (Pair<Boolean, Int>) -> Unit
|
||||
) {
|
||||
if(this == null) return
|
||||
|
||||
if (this.isTvSettings()) {
|
||||
val builder =
|
||||
AlertDialog.Builder(this, R.style.AlertDialogCustom)
|
||||
|
@ -41,12 +51,13 @@ object SingleSelectionHelper {
|
|||
|
||||
dialog.findViewById<ListView>(R.id.listview1)?.let { listView ->
|
||||
listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||
listView.adapter = ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice_color).apply {
|
||||
addAll(tvOptions)
|
||||
}
|
||||
listView.adapter =
|
||||
ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice_color).apply {
|
||||
addAll(tvOptions)
|
||||
}
|
||||
|
||||
listView.setOnItemClickListener { _, _, i, _ ->
|
||||
callback.invoke(Pair(true,i))
|
||||
callback.invoke(Pair(true, i))
|
||||
dialog.dismissSafe(this)
|
||||
}
|
||||
}
|
||||
|
@ -62,12 +73,12 @@ object SingleSelectionHelper {
|
|||
s
|
||||
)
|
||||
}) {
|
||||
callback(Pair(false,this.itemId))
|
||||
callback(Pair(false, this.itemId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Activity.showDialog(
|
||||
fun Activity?.showDialog(
|
||||
dialog: Dialog,
|
||||
items: List<String>,
|
||||
selectedIndex: List<Int>,
|
||||
|
@ -77,6 +88,8 @@ object SingleSelectionHelper {
|
|||
callback: (List<Int>) -> Unit,
|
||||
dismissCallback: () -> Unit
|
||||
) {
|
||||
if(this == null) return
|
||||
|
||||
val realShowApply = showApply || isMultiSelect
|
||||
val listView = dialog.findViewById<ListView>(R.id.listview1)!!
|
||||
val textView = dialog.findViewById<TextView>(R.id.text1)!!
|
||||
|
@ -145,8 +158,7 @@ object SingleSelectionHelper {
|
|||
}
|
||||
|
||||
|
||||
|
||||
private fun Activity.showInputDialog(
|
||||
private fun Activity?.showInputDialog(
|
||||
dialog: Dialog,
|
||||
value: String,
|
||||
name: String,
|
||||
|
@ -154,6 +166,8 @@ object SingleSelectionHelper {
|
|||
callback: (String) -> Unit,
|
||||
dismissCallback: () -> Unit
|
||||
) {
|
||||
if(this == null) return
|
||||
|
||||
val inputView = dialog.findViewById<EditText>(R.id.nginx_text_input)!!
|
||||
val textView = dialog.findViewById<TextView>(R.id.text1)!!
|
||||
val applyButton = dialog.findViewById<TextView>(R.id.apply_btt)!!
|
||||
|
@ -184,13 +198,15 @@ object SingleSelectionHelper {
|
|||
|
||||
}
|
||||
|
||||
fun Activity.showMultiDialog(
|
||||
fun Activity?.showMultiDialog(
|
||||
items: List<String>,
|
||||
selectedIndex: List<Int>,
|
||||
name: String,
|
||||
dismissCallback: () -> Unit,
|
||||
callback: (List<Int>) -> Unit,
|
||||
) {
|
||||
if(this == null) return
|
||||
|
||||
val builder =
|
||||
AlertDialog.Builder(this, R.style.AlertDialogCustom)
|
||||
.setView(R.layout.bottom_selection_dialog)
|
||||
|
@ -200,7 +216,7 @@ object SingleSelectionHelper {
|
|||
showDialog(dialog, items, selectedIndex, name, true, true, callback, dismissCallback)
|
||||
}
|
||||
|
||||
fun Activity.showDialog(
|
||||
fun Activity?.showDialog(
|
||||
items: List<String>,
|
||||
selectedIndex: Int,
|
||||
name: String,
|
||||
|
@ -208,6 +224,8 @@ object SingleSelectionHelper {
|
|||
dismissCallback: () -> Unit,
|
||||
callback: (Int) -> Unit,
|
||||
) {
|
||||
if(this == null) return
|
||||
|
||||
val builder =
|
||||
AlertDialog.Builder(this, R.style.AlertDialogCustom)
|
||||
.setView(R.layout.bottom_selection_dialog)
|
||||
|
@ -227,14 +245,15 @@ object SingleSelectionHelper {
|
|||
}
|
||||
|
||||
/** Only for a low amount of items */
|
||||
fun Activity.showBottomDialog(
|
||||
fun Activity?.showBottomDialog(
|
||||
items: List<String>,
|
||||
selectedIndex: Int,
|
||||
name: String,
|
||||
showApply: Boolean,
|
||||
dismissCallback: () -> Unit,
|
||||
dismissCallback: () -> Unit,
|
||||
callback: (Int) -> Unit,
|
||||
) {
|
||||
if (this == null) return
|
||||
val builder =
|
||||
BottomSheetDialog(this)
|
||||
builder.setContentView(R.layout.bottom_selection_dialog)
|
||||
|
@ -252,12 +271,12 @@ object SingleSelectionHelper {
|
|||
)
|
||||
}
|
||||
|
||||
fun Activity.showNginxTextInputDialog(
|
||||
name: String,
|
||||
value: String,
|
||||
textInputType: Int?,
|
||||
dismissCallback: () -> Unit,
|
||||
callback: (String) -> Unit,
|
||||
fun Activity.showNginxTextInputDialog(
|
||||
name: String,
|
||||
value: String,
|
||||
textInputType: Int?,
|
||||
dismissCallback: () -> Unit,
|
||||
callback: (String) -> Unit,
|
||||
) {
|
||||
val builder = BottomSheetDialog(this) // probably the stuff at the bottom
|
||||
builder.setContentView(R.layout.bottom_input_dialog) // input layout
|
||||
|
|
|
@ -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: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"/>
|
||||
|
|
19
app/src/main/res/layout/add_remove_sites.xml
Normal file
19
app/src/main/res/layout/add_remove_sites.xml
Normal 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>
|
117
app/src/main/res/layout/add_site_input.xml
Normal file
117
app/src/main/res/layout/add_site_input.xml
Normal 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>
|
|
@ -138,6 +138,34 @@
|
|||
android:background="?attr/primaryBlackBackground"
|
||||
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
|
||||
android:id="@+id/result_smallscreen_holder"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
8
app/src/main/res/layout/result_episode_both.xml
Normal file
8
app/src/main/res/layout/result_episode_both.xml
Normal 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>
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
android:nextFocusLeft="@id/episode_poster"
|
||||
android:nextFocusRight="@id/result_episode_download"
|
||||
android:id="@+id/episode_holder"
|
||||
android:id="@+id/episode_holder_large"
|
||||
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -142,11 +142,13 @@
|
|||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:maxLines="4"
|
||||
android:ellipsize="end"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:id="@+id/episode_descript"
|
||||
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_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
|
|
|
@ -58,7 +58,6 @@
|
|||
android:background="@color/black_overlay" />
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/player_video_holder"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -273,6 +272,17 @@
|
|||
android:layout_width="0dp" />
|
||||
</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
|
||||
android:id="@+id/bottom_player_bar"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -343,9 +353,9 @@
|
|||
app:tint="@color/white"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:src="@drawable/baseline_fullscreen_24"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp" />
|
||||
android:layout_marginEnd="20dp"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp" />
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
<string name="bottom_title_key" translatable="false">bottom_title_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="override_site_key" translatable="false">override_site_key</string>
|
||||
|
||||
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
|
||||
<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_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="nginx_url_pref">Nginx server url</string>
|
||||
|
@ -440,6 +445,9 @@
|
|||
<string name="example_username">MyCoolUsername</string>
|
||||
<string name="example_email">hello@world.com</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>
|
||||
|
@ -529,6 +537,8 @@
|
|||
<string name="title">Title</string>
|
||||
<string name="resolution">Resolution</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_bloat">Remove bloat from subtitles</string>
|
||||
<string name="extras">Extras</string>
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
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
|
||||
android:key="@string/dns_key"
|
||||
|
|
Loading…
Reference in a new issue