Merge remote-tracking branch 'origin/master'

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

View file

@ -36,7 +36,7 @@ android {
targetSdkVersion 30
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'
}

View file

@ -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"

View file

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

View file

@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.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,

View file

@ -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 -> {

View file

@ -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(

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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(

View file

@ -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(

View file

@ -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

View file

@ -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(

View file

@ -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)
}
}

View file

@ -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

View file

@ -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

View file

@ -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(

View file

@ -34,7 +34,7 @@ class NineAnimeProvider : MainAPI() {
Pair("$mainUrl/ajax/home/widget?name=updated_sub&page=1", "Recently Updated (SUB)"),
Pair(
"$mainUrl/ajax/home/widget?name=updated_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()

View file

@ -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(

View file

@ -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(

View file

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

View file

@ -12,7 +12,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
class CrossTmdbProvider : TmdbProvider() {
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)

View file

@ -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
}

View file

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

View file

@ -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

View file

@ -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,6 +113,9 @@ class AltadefinizioneProvider : MainAPI() {
}
val tags: List<String> = document.select("#details > li:nth-child(1) > a").map { it.text() }
val trailerurl = document.selectFirst("#showtrailer > div > div > iframe")!!.attr("src")
return newMovieLoadResponse(
title,
url,
@ -125,6 +130,7 @@ class AltadefinizioneProvider : MainAPI() {
this.duration = null
this.actors = actors
this.tags = tags
addTrailer(trailerurl)
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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() {
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,

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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> {

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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
)

View file

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

View file

@ -33,6 +33,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
override val redirectUrl = "mallogin"
override val 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)
),

View file

@ -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 {

View file

@ -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) {

View file

@ -229,6 +229,15 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
}
}
val playerSourceMove = if (isShowing) 0f else -50.toPx.toFloat()
player_open_source?.let {
ObjectAnimator.ofFloat(it, "translationY", playerSourceMove).apply {
duration = 200
start()
}
}
if (!isLocked) {
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)
}

View file

@ -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,

View file

@ -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,49 +612,44 @@ 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) {
val isSuccess =
currentTrailers.getOrNull(index ?: currentTrailerIndex)?.let { trailer ->
//if(trailer.contains("youtube.com")) { // wont load in exo
// nextMirror()
// return
//}
context?.let { ctx ->
player.onPause()
player.loadPlayer(
ctx,
false,
ExtractorLink(
"",
"Trailer",
trailer,
"",
Qualities.Unknown.value
),
null,
startPosition = 0L,
subtitles = emptySet(),
subtitle = null,
autoPlay = false
)
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()
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>?) {
if (actors.isNullOrEmpty()) {
@ -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 =

View file

@ -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 =

View file

@ -13,17 +13,22 @@ import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.AcraApplication.Companion.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,14 +156,54 @@ 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) {
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 ->
_resultResponse.postValue(Resource.Success(applyMeta(resp, meta)))
return@ioWork applyMeta(resp, meta, syncs)
}
return@ioWork null to null
}
_resultResponse.postValue(Resource.Success(value ?: return@launch))
if (updateEpisodes ?: return@launch) updateEpisodes(value, lastShowFillers)
}
private fun loadWatchStatus(localId: Int? = null) {
@ -310,77 +364,18 @@ class ResultViewModel : ViewModel() {
return name
}
fun load(url: String, apiName: String, showFillers: Boolean) = viewModelScope.launch {
_publicEpisodes.postValue(Resource.Loading())
_resultResponse.postValue(Resource.Loading(url))
val api = getApiFromNameNull(apiName) ?: getApiFromUrlNull(url)
if (api == null) {
_resultResponse.postValue(
Resource.Failure(
false,
null,
null,
"This provider does not exist"
)
)
return@launch
}
val validUrlResource = safeApiCall {
SyncRedirector.redirect(
url,
api.mainUrl.replace(NineAnimeProvider().mainUrl, "9anime")
.replace(GogoanimeProvider().mainUrl, "gogoanime")
)
}
if (validUrlResource !is Resource.Success) {
if (validUrlResource is Resource.Failure) {
_resultResponse.postValue(validUrlResource)
}
return@launch
}
val validUrl = validUrlResource.value
_resultResponse.postValue(Resource.Loading(validUrl))
_apiName.postValue(apiName)
repo = APIRepository(api)
val data = repo?.load(validUrl) ?: return@launch
_resultResponse.postValue(data)
when (data) {
is Resource.Success -> {
val loadResponse = applyMeta(data.value, lastMeta)
page.postValue(loadResponse)
var lastShowFillers = false
private suspend fun updateEpisodes(loadResponse: LoadResponse, showFillers: Boolean) {
Log.i(TAG, "updateEpisodes")
try {
lastShowFillers = showFillers
val mainId = loadResponse.getId()
id.postValue(mainId)
loadWatchStatus(mainId)
setKey(
DOWNLOAD_HEADER_CACHE,
mainId.toString(),
VideoDownloadHelper.DownloadHeaderCached(
apiName,
validUrl,
loadResponse.type,
loadResponse.name,
loadResponse.posterUrl,
mainId,
System.currentTimeMillis(),
)
)
when (loadResponse) {
is AnimeLoadResponse -> {
if (loadResponse.episodes.isEmpty()) {
_dubSubEpisodes.postValue(emptyMap())
return@launch
return
}
// val status = getDub(mainId)
@ -415,7 +410,7 @@ class ResultViewModel : ViewModel() {
episode,
i.season,
i.data,
apiName,
loadResponse.apiName,
id,
index,
i.rating,
@ -460,7 +455,7 @@ class ResultViewModel : ViewModel() {
episodeIndex,
episode.season,
episode.data,
apiName,
loadResponse.apiName,
id,
index,
episode.rating,
@ -517,6 +512,79 @@ class ResultViewModel : ViewModel() {
)
}
}
} catch (e: Exception) {
logError(e)
}
}
fun load(url: String, apiName: String, showFillers: Boolean) = viewModelScope.launch {
_publicEpisodes.postValue(Resource.Loading())
_resultResponse.postValue(Resource.Loading(url))
val api = getApiFromNameNull(apiName) ?: getApiFromUrlNull(url)
if (api == null) {
_resultResponse.postValue(
Resource.Failure(
false,
null,
null,
"This provider does not exist"
)
)
return@launch
}
val validUrlResource = safeApiCall {
SyncRedirector.redirect(
url,
api.mainUrl.replace(NineAnimeProvider().mainUrl, "9anime")
.replace(GogoanimeProvider().mainUrl, "gogoanime")
)
}
if (validUrlResource !is Resource.Success) {
if (validUrlResource is Resource.Failure) {
_resultResponse.postValue(validUrlResource)
}
return@launch
}
val validUrl = validUrlResource.value
_resultResponse.postValue(Resource.Loading(validUrl))
_apiName.postValue(apiName)
repo = APIRepository(api)
val data = repo?.load(validUrl) ?: return@launch
_resultResponse.postValue(data)
when (data) {
is Resource.Success -> {
val loadResponse = if (lastMeta != null || lastSync != null) ioWork {
applyMeta(data.value, lastMeta, lastSync).first
} else data.value
_resultResponse.postValue(Resource.Success(loadResponse))
val mainId = loadResponse.getId()
id.postValue(mainId)
loadWatchStatus(mainId)
setKey(
DOWNLOAD_HEADER_CACHE,
mainId.toString(),
VideoDownloadHelper.DownloadHeaderCached(
apiName,
validUrl,
loadResponse.type,
loadResponse.name,
loadResponse.posterUrl,
mainId,
System.currentTimeMillis(),
)
)
updateEpisodes(loadResponse, showFillers)
}
else -> Unit
}

View file

@ -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)
@ -177,6 +288,5 @@ class SettingsGeneral : PreferenceFragmentCompat() {
} catch (e: Exception) {
e.printStackTrace()
}
}
}

View file

@ -3,10 +3,7 @@ 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 {
@ -25,6 +22,12 @@ object Coroutines {
}
}
suspend fun <T> ioWork(work: suspend (() -> T)): T {
return withContext(Dispatchers.IO) {
work()
}
}
fun runOnMainThread(work: (() -> Unit)) {
val mainHandler = Handler(Looper.getMainLooper())
mainHandler.post {

View file

@ -14,6 +14,7 @@ const val DOWNLOAD_HEADER_CACHE = "download_header_cache"
const val DOWNLOAD_EPISODE_CACHE = "download_episode_cache"
const val 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"

View file

@ -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 {

View file

@ -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()

View file

@ -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,7 +51,8 @@ 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 {
listView.adapter =
ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice_color).apply {
addAll(tvOptions)
}
@ -67,7 +78,7 @@ object SingleSelectionHelper {
}
}
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,7 +245,7 @@ object SingleSelectionHelper {
}
/** Only for a low amount of items */
fun Activity.showBottomDialog(
fun Activity?.showBottomDialog(
items: List<String>,
selectedIndex: Int,
name: String,
@ -235,6 +253,7 @@ object SingleSelectionHelper {
dismissCallback: () -> Unit,
callback: (Int) -> Unit,
) {
if (this == null) return
val builder =
BottomSheetDialog(this)
builder.setContentView(R.layout.bottom_selection_dialog)

View file

@ -1,4 +1,4 @@
<vector android:height="24dp" android:tint="#FFFFFF"
<vector android:height="24dp" android:tint="?attr/white"
android:viewportHeight="24" android:viewportWidth="24"
android: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"/>

View file

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

View file

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_marginTop="20dp"
android:layout_marginBottom="10dp"
android:id="@+id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_rowWeight="1"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:textColor="?attr/textColor"
android:textSize="20sp"
android:textStyle="bold"
android:text="@string/add_site_pref" />
<TextView
android:layout_marginBottom="10dp"
android:id="@+id/text2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_rowWeight="1"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:textColor="?attr/grayTextColor"
android:textSize="15sp"
tools:text="Gogoanime" />
<LinearLayout
android:orientation="vertical"
android:layout_marginBottom="60dp"
android:layout_marginHorizontal="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:textColorHint="?attr/grayTextColor"
android:hint="@string/example_site_name"
android:autofillHints="username"
android:id="@+id/site_name_input"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusLeft="@id/apply_btt"
android:nextFocusDown="@id/site_url_input"
android:requiresFadingEdge="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
tools:ignore="LabelFor" />
<EditText
android:textColorHint="?attr/grayTextColor"
android:hint="@string/example_site_url"
android:id="@+id/site_url_input"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusLeft="@id/apply_btt"
android:nextFocusUp="@id/site_name_input"
android:nextFocusDown="@id/site_lang_input"
android:requiresFadingEdge="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
tools:ignore="LabelFor" />
<EditText
android:textColorHint="?attr/grayTextColor"
android:hint="@string/example_lang_name"
android:autofillHints="username"
android:id="@+id/site_lang_input"
android:nextFocusUp="@id/site_url_input"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusLeft="@id/apply_btt"
android:nextFocusDown="@id/apply_btt"
android:requiresFadingEdge="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
tools:ignore="LabelFor" />
</LinearLayout>
<LinearLayout
android:id="@+id/apply_btt_holder"
android:orientation="horizontal"
android:layout_gravity="bottom"
android:gravity="bottom|end"
android:layout_marginTop="-60dp"
android:layout_width="match_parent"
android:layout_height="60dp">
<com.google.android.material.button.MaterialButton
style="@style/WhiteButton"
android:layout_gravity="center_vertical|end"
android:text="@string/add_site_pref"
android:id="@+id/apply_btt"
android:layout_width="wrap_content" />
<com.google.android.material.button.MaterialButton
style="@style/BlackButton"
android:layout_gravity="center_vertical|end"
android:text="@string/sort_cancel"
android:id="@+id/cancel_btt"
android:layout_width="wrap_content" />
</LinearLayout>
</LinearLayout>

View file

@ -138,6 +138,34 @@
android:background="?attr/primaryBlackBackground"
android: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"

View file

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

View file

@ -5,7 +5,7 @@
android:nextFocusLeft="@id/episode_poster"
android: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>

View file

@ -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>

View file

@ -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>

View file

@ -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"