Merge remote-tracking branch 'origin/master'

This commit is contained in:
LagradOst 2022-04-13 19:29:38 +02:00
commit e5515b575c
22 changed files with 1010 additions and 437 deletions

75
.github/site-list.py vendored
View file

@ -1,63 +1,54 @@
#!/usr/bin/python3 #!/usr/bin/python3
from glob import glob from glob import glob
from re import findall, compile, sub, DOTALL from re import findall, compile, DOTALL
from json import dump, load
from typing import List, Dict from typing import List, Dict
# Globals # Globals
URL_REGEX = compile( URL_REGEX = compile(
"override\sva[lr]\smainUrl[^\"']+[\"'](https?://[a-zA-Z0-9\.-]+)[\"']") "override\sva[lr]\smainUrl[^\"']+[\"'](https?://[a-zA-Z0-9\.-]+)[\"']")
NAME_REGEX = compile("class (.+?) ?: \w+\(\)\s\{") NAME_REGEX = compile("([A-Za-z0-9]+)(?:.kt)$")
START_MARKER = "<!--SITE LIST START-->" JSON_PATH = "docs/providers.json"
END_MARKER = "<!--SITE LIST END-->"
GLOB = "app/src/main/java/com/lagradost/cloudstream3/*providers/*Provider.kt" GLOB = "app/src/main/java/com/lagradost/cloudstream3/*providers/*Provider.kt"
MAIN_API = "app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt"
API_REGEX = compile(
"val\s*allProviders.*?{\s.*?arrayListOf\(([\W\w]*?)\)\s*\n*\s*}", DOTALL)
sites: Dict[str, str] = {} old_sites: Dict[str, Dict] = load(open(JSON_PATH, "r", encoding="utf-8"))
enabled_sites: List[str] = [] sites: Dict[str, Dict] = {}
with open(MAIN_API, "r", encoding="utf-8") as f:
apis = findall(API_REGEX, f.read())
for api_list in apis:
for api in api_list.split("\n"):
if not api.strip() or api.strip().startswith("/"):
continue
enabled_sites.append(api.strip().split("(")[0])
# parse all *Provider.kt files
for path in glob(GLOB): for path in glob(GLOB):
with open(path, "r", encoding='utf-8') as file: with open(path, "r", encoding='utf-8') as file:
try: try:
site_text: str = file.read() site_text: str = file.read()
name: List[str] = findall(NAME_REGEX, site_text) name: str = findall(NAME_REGEX, path)[0]
provider_text: str = findall(URL_REGEX, site_text) provider_url: str = [*findall(URL_REGEX, site_text), ""][0]
if name: if name in old_sites.keys(): # if already in previous list use old status and name
if name[0] not in enabled_sites: sites[name] = {
continue "name": old_sites[name]['name'],
sites[name[0]] = provider_text[0] "url": provider_url if provider_url else old_sites[name]['url'],
"status": old_sites[name]['status']
}
else: # if not in previous list add with new data
display_name = name
if display_name.endswith("Provider"):
display_name = display_name[:-len("Provider")]
sites[name] = {
"name": display_name,
"url": provider_url if provider_url else "",
"status": 1
}
except Exception as ex: except Exception as ex:
print("{0}: {1}".format(path, ex)) print("{0}: {1}".format(path, ex))
# add sites from old_sites that are missing in new list
for name in old_sites.keys():
if name not in sites.keys():
sites[name] = {
"name": old_sites[name]['name'],
"url": old_sites[name]['url'],
"status": old_sites[name]['status']
}
with open("README.md", "r+", encoding='utf-8') as readme: dump(sites, open(JSON_PATH, "w+", encoding="utf-8"), indent=4, sort_keys=True)
raw = readme.read()
if START_MARKER not in raw or END_MARKER not in raw:
raise RuntimeError("Missing start and end markers")
readme.seek(0)
readme.write(raw.split(START_MARKER)[0])
readme.write(START_MARKER+"\n")
for site in enabled_sites:
if site in sites:
readme.write(
"- [{0}]({1}) \n".format(sub("^https?://(?:www\.)?", "", sites[site]), sites[site]))
readme.write(END_MARKER)
readme.write(raw.split(END_MARKER)[-1])
readme.truncate()

View file

@ -4,10 +4,10 @@ on:
push: push:
branches: [ master ] branches: [ master ]
paths: paths:
- 'README.md'
- 'app/src/main/java/com/lagradost/cloudstream3/*providers/*Provider.kt' - 'app/src/main/java/com/lagradost/cloudstream3/*providers/*Provider.kt'
- '.github/workflows/site_list.yml' - '.github/workflows/site_list.yml'
- '.github/site-list.py' - '.github/site-list.py'
- 'docs/providers.json'
- 'app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt' - 'app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt'
concurrency: concurrency:
@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Edit README.md - name: Edit providers.json
run: | run: |
python3 .github/site-list.py python3 .github/site-list.py
- name: Commit to the repo - name: Commit to the repo

View file

@ -58,53 +58,5 @@ It merely scrapes 3rd-party websites that are publicly accessable via any regula
***Sites used:*** ***Sites used:***
https://lagradost.github.io/CloudStream-3/
<!-- Do not remove those two comments --> Look [here](https://lagradost.github.io/CloudStream-3/) for a comprehensive list
<!--SITE LIST START-->
- [pelisplus.icu](https://pelisplus.icu)
- [pelismart.com](https://pelismart.com)
- [melomovie.com](https://melomovie.com)
- [doramasyt.com](https://doramasyt.com)
- [cuevana3.me](https://cuevana3.me)
- [pelisflix.li](https://pelisflix.li)
- [seriesflix.video](https://seriesflix.video)
- [ihavenotv.com](https://ihavenotv.com)
- [lookmovie.io](https://lookmovie.io)
- [vmovee.watch](https://www.vmovee.watch)
- [allmoviesforyou.net](https://allmoviesforyou.net)
- [vidembed.cc](https://vidembed.cc)
- [vf-film.me](https://vf-film.me)
- [vf-serie.org](https://vf-serie.org)
- [asianembed.io](https://asianembed.io)
- [asiaflix.app](https://asiaflix.app)
- [fmovies.to](https://fmovies.to)
- [filman.cc](https://filman.cc)
- [dopebox.to](https://dopebox.to)
- [pinoymoviepedia.ru](https://pinoymoviepedia.ru)
- [pinoy-hd.xyz](https://www.pinoy-hd.xyz)
- [pinoymovies.es](https://pinoymovies.es)
- [trailers.to](https://trailers.to)
- [2embed.ru](https://www.2embed.ru)
- [dramasee.net](https://dramasee.net)
- [watchasian.sh](https://watchasian.sh)
- [kdramahood.com](https://kdramahood.com)
- [akwam.to](https://akwam.to)
- [mycima.tv](https://mycima.tv)
- [egy.best](https://egy.best)
- [hdm.to](https://hdm.to)
- [theflix.to](https://theflix.to)
- [v2.apimdb.net](https://v2.apimdb.net)
- [wcostream.com](https://www.wcostream.com)
- [gogoanime.film](https://gogoanime.film)
- [allanime.site](https://allanime.site)
- [animekisa.in](https://animekisa.in)
- [animeflick.net](https://animeflick.net)
- [tenshi.moe](https://tenshi.moe)
- [wcostream.cc](https://wcostream.cc)
- [9anime.id](https://9anime.id)
- [animeworld.tv](https://www.animeworld.tv)
- [zoro.to](https://zoro.to)
- [bestdubbedanime.com](https://bestdubbedanime.com)
- [monoschinos2.com](https://monoschinos2.com)
- [kawaiifu.com](https://kawaiifu.com)
<!--SITE LIST END-->

View file

@ -19,6 +19,7 @@ import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi
import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import okhttp3.Headers
import okhttp3.Interceptor import okhttp3.Interceptor
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -107,6 +108,7 @@ object APIHolder {
MonoschinosProvider(), MonoschinosProvider(),
KawaiifuProvider(), // disabled due to cloudflare KawaiifuProvider(), // disabled due to cloudflare
//MultiAnimeProvider(), //MultiAnimeProvider(),
NginxProvider(),
) )
} }
@ -307,6 +309,7 @@ const val PROVIDER_STATUS_DOWN = 0
data class ProvidersInfoJson( data class ProvidersInfoJson(
@JsonProperty("name") var name: String, @JsonProperty("name") var name: String,
@JsonProperty("url") var url: String, @JsonProperty("url") var url: String,
@JsonProperty("credentials") var credentials: String? = null,
@JsonProperty("status") var status: Int, @JsonProperty("status") var status: Int,
) )
@ -319,6 +322,7 @@ abstract class MainAPI {
fun overrideWithNewData(data: ProvidersInfoJson) { fun overrideWithNewData(data: ProvidersInfoJson) {
this.name = data.name this.name = data.name
this.mainUrl = data.url this.mainUrl = data.url
this.storedCredentials = data.credentials
} }
init { init {
@ -329,6 +333,7 @@ abstract class MainAPI {
open var name = "NONE" open var name = "NONE"
open var mainUrl = "NONE" open var mainUrl = "NONE"
open var storedCredentials: String? = null
//open val uniqueId : Int by lazy { this.name.hashCode() } // in case of duplicate providers you can have a shared id //open val uniqueId : Int by lazy { this.name.hashCode() } // in case of duplicate providers you can have a shared id
@ -591,17 +596,22 @@ fun getQualityFromString(string: String?): SearchQuality? {
"cam" -> SearchQuality.Cam "cam" -> SearchQuality.Cam
"camrip" -> SearchQuality.CamRip "camrip" -> SearchQuality.CamRip
"hdcam" -> SearchQuality.HdCam "hdcam" -> SearchQuality.HdCam
"hdtc" -> SearchQuality.HdCam
"hdts" -> SearchQuality.HdCam
"highquality" -> SearchQuality.HQ "highquality" -> SearchQuality.HQ
"hq" -> SearchQuality.HQ "hq" -> SearchQuality.HQ
"highdefinition" -> SearchQuality.HD "highdefinition" -> SearchQuality.HD
"hdrip" -> SearchQuality.HD "hdrip" -> SearchQuality.HD
"hd" -> SearchQuality.HD "hd" -> SearchQuality.HD
"hdtv" -> SearchQuality.HD
"rip" -> SearchQuality.CamRip "rip" -> SearchQuality.CamRip
"telecine" -> SearchQuality.Telecine "telecine" -> SearchQuality.Telecine
"tc" -> SearchQuality.Telecine "tc" -> SearchQuality.Telecine
"telesync" -> SearchQuality.Telesync "telesync" -> SearchQuality.Telesync
"ts" -> SearchQuality.Telesync "ts" -> SearchQuality.Telesync
"dvd" -> SearchQuality.DVD "dvd" -> SearchQuality.DVD
"dvdrip" -> SearchQuality.DVD
"dvdscr" -> SearchQuality.DVD
"blueray" -> SearchQuality.BlueRay "blueray" -> SearchQuality.BlueRay
"bluray" -> SearchQuality.BlueRay "bluray" -> SearchQuality.BlueRay
"br" -> SearchQuality.BlueRay "br" -> SearchQuality.BlueRay
@ -613,6 +623,7 @@ fun getQualityFromString(string: String?): SearchQuality? {
"wp" -> SearchQuality.WorkPrint "wp" -> SearchQuality.WorkPrint
"workprint" -> SearchQuality.WorkPrint "workprint" -> SearchQuality.WorkPrint
"webrip" -> SearchQuality.WebRip "webrip" -> SearchQuality.WebRip
"webdl" -> SearchQuality.WebRip
"web" -> SearchQuality.WebRip "web" -> SearchQuality.WebRip
"hdr" -> SearchQuality.HDR "hdr" -> SearchQuality.HDR
"sdr" -> SearchQuality.SDR "sdr" -> SearchQuality.SDR

View file

@ -64,12 +64,14 @@ import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.cloudstream3.utils.UIHelper.requestRW
import com.lagradost.cloudstream3.movieproviders.NginxProvider
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.fragment_result_swipe.* import kotlinx.android.synthetic.main.fragment_result_swipe.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import kotlin.collections.HashMap
import kotlin.concurrent.thread import kotlin.concurrent.thread
@ -360,6 +362,57 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
e.printStackTrace() e.printStackTrace()
false false
} }
fun addNginxToJson(data: java.util.HashMap<String, ProvidersInfoJson>): java.util.HashMap<String, ProvidersInfoJson>? {
try {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val nginxUrl =
settingsManager.getString(getString(R.string.nginx_url_key), "nginx_url_key").toString()
val nginxCredentials =
settingsManager.getString(getString(R.string.nginx_credentials), "nginx_credentials")
.toString()
val StoredNginxProvider = NginxProvider()
if (nginxUrl == "nginx_url_key" || nginxUrl == "") { // if key is default value, or empty:
data[StoredNginxProvider.javaClass.simpleName] = ProvidersInfoJson(
url = nginxUrl,
name = StoredNginxProvider.name,
status = PROVIDER_STATUS_DOWN, // the provider will not be display
credentials = nginxCredentials
)
} else { // valid url
data[StoredNginxProvider.javaClass.simpleName] = ProvidersInfoJson(
url = nginxUrl,
name = StoredNginxProvider.name,
status = PROVIDER_STATUS_OK,
credentials = nginxCredentials
)
}
return data
} catch (e: Exception) {
logError(e)
return data
}
}
fun createNginxJson() : ProvidersInfoJson? { //java.util.HashMap<String, ProvidersInfoJson>
return try {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val nginxUrl = settingsManager.getString(getString(R.string.nginx_url_key), "nginx_url_key").toString()
val nginxCredentials = settingsManager.getString(getString(R.string.nginx_credentials), "nginx_credentials").toString()
if (nginxUrl == "nginx_url_key" || nginxUrl == "") { // if key is default value or empty:
null // don't overwrite anything
} else {
ProvidersInfoJson(
url = nginxUrl,
name = NginxProvider().name,
status = PROVIDER_STATUS_OK,
credentials = nginxCredentials
)
}
} catch (e: Exception) {
logError(e)
null
}
}
// this pulls the latest data so ppl don't have to update to simply change provider url // this pulls the latest data so ppl don't have to update to simply change provider url
if (downloadFromGithub) { if (downloadFromGithub) {
@ -379,8 +432,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
tryParseJson<HashMap<String, ProvidersInfoJson>>(txt) tryParseJson<HashMap<String, ProvidersInfoJson>>(txt)
setKey(PROVIDER_STATUS_KEY, txt) setKey(PROVIDER_STATUS_KEY, txt)
MainAPI.overrideData = newCache // update all new providers MainAPI.overrideData = newCache // update all new providers
for (api in apis) { // update current providers
newCache?.get(api.javaClass.simpleName)?.let { data -> val newUpdatedCache = newCache?.let { addNginxToJson(it) ?: it }
for (api in apis) { // update current providers
newUpdatedCache?.get(api.javaClass.simpleName)?.let { data ->
api.overrideWithNewData(data) api.overrideWithNewData(data)
} }
} }
@ -397,12 +453,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
newCache newCache
}?.let { providersJsonMap -> }?.let { providersJsonMap ->
MainAPI.overrideData = providersJsonMap MainAPI.overrideData = providersJsonMap
val providersJsonMapUpdated = addNginxToJson(providersJsonMap)?: providersJsonMap // if return null, use unchanged one
val acceptableProviders = val acceptableProviders =
providersJsonMap.filter { it.value.status == PROVIDER_STATUS_OK || it.value.status == PROVIDER_STATUS_SLOW } providersJsonMapUpdated.filter { it.value.status == PROVIDER_STATUS_OK || it.value.status == PROVIDER_STATUS_SLOW }
.map { it.key }.toSet() .map { it.key }.toSet()
val restrictedApis = val restrictedApis =
if (hasBenene) providersJsonMap.filter { it.value.status == PROVIDER_STATUS_BETA_ONLY } if (hasBenene) providersJsonMapUpdated.filter { it.value.status == PROVIDER_STATUS_BETA_ONLY }
.map { it.key }.toSet() else emptySet() .map { it.key }.toSet() else emptySet()
apis = allProviders.filter { api -> apis = allProviders.filter { api ->
@ -425,6 +482,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
} else { } else {
apis = allProviders apis = allProviders
try {
val nginxProviderName = NginxProvider().name
val nginxProviderIndex = apis.indexOf(APIHolder.getApiFromName(nginxProviderName))
val createdJsonProvider = createNginxJson()
if (createdJsonProvider != null) {
apis[nginxProviderIndex].overrideWithNewData(createdJsonProvider) // people will have access to it if they disable metadata check (they are not filtered)
}
} catch (e: Exception) {
logError(e)
}
} }
loadThemes(this) loadThemes(this)

View file

@ -30,6 +30,14 @@ class VizcloudLive : WcoStream() {
override var mainUrl = "https://vizcloud.live" override var mainUrl = "https://vizcloud.live"
} }
class VizcloudInfo : WcoStream() {
override var mainUrl = "https://vizcloud.info"
}
class MwvnVizcloudInfo : WcoStream() {
override var mainUrl = "https://mwvn.vizcloud.info"
}
open class WcoStream : ExtractorApi() { open class WcoStream : ExtractorApi() {
override var name = "VidStream" //Cause works for animekisa and wco override var name = "VidStream" //Cause works for animekisa and wco
override var mainUrl = "https://vidstream.pro" override var mainUrl = "https://vidstream.pro"
@ -103,8 +111,9 @@ open class WcoStream : ExtractorApi() {
} }
} }
if (mainUrl == "https://vidstream.pro" || mainUrl == "https://vidstreamz.online" || mainUrl == "https://vizcloud2.online" if (mainUrl == "https://vidstream.pro" || mainUrl == "https://vidstreamz.online" || mainUrl == "https://vizcloud2.online"
|| mainUrl == "https://vizcloud.xyz" || mainUrl == "https://vizcloud.live") { || mainUrl == "https://vizcloud.xyz" || mainUrl == "https://vizcloud.live" || mainUrl == "https://vizcloud.info"
if (it.file.contains("m3u8")) { || mainUrl == "https://mwvn.vizcloud.info") {
if (it.file.contains("m3u8")) {
hlsHelper.m3u8Generation(M3u8Helper.M3u8Stream(it.file.replace("#.mp4",""), null, hlsHelper.m3u8Generation(M3u8Helper.M3u8Stream(it.file.replace("#.mp4",""), null,
headers = mapOf("Referer" to url)), true) headers = mapOf("Referer" to url)), true)
.forEach { stream -> .forEach { stream ->

View file

@ -8,7 +8,7 @@ import org.jsoup.nodes.Element
class EgyBestProvider : MainAPI() { class EgyBestProvider : MainAPI() {
override val lang = "ar" override val lang = "ar"
override var mainUrl = "https://egy.best" override var mainUrl = "https://www.egy.best"
override var name = "EgyBest" override var name = "EgyBest"
override val usesWebView = false override val usesWebView = false
override val hasMainPage = true override val hasMainPage = true
@ -26,6 +26,7 @@ class EgyBestProvider : MainAPI() {
val isMovie = Regex(".*/movie/.*|.*/masrahiya/.*").matches(url) val isMovie = Regex(".*/movie/.*|.*/masrahiya/.*").matches(url)
val tvType = if (isMovie) TvType.Movie else TvType.TvSeries val tvType = if (isMovie) TvType.Movie else TvType.TvSeries
title = if (year !== null) title else title.split(" (")[0].trim() title = if (year !== null) title else title.split(" (")[0].trim()
val quality = select("span.ribbon span").text().replace("-", "")
// If you need to differentiate use the url. // If you need to differentiate use the url.
return MovieSearchResponse( return MovieSearchResponse(
title, title,
@ -35,18 +36,22 @@ class EgyBestProvider : MainAPI() {
posterUrl, posterUrl,
year, year,
null, null,
quality = getQualityFromString(quality)
) )
} }
override suspend fun getMainPage(): HomePageResponse { override suspend fun getMainPage(): HomePageResponse {
// url, title // url, title
val doc = app.get(mainUrl).document val doc = app.get(mainUrl).document
val pages = doc.select("#mainLoad div.mbox").apmap { val pages = arrayListOf<HomePageList>()
doc.select("#mainLoad div.mbox").apmap {
val name = it.select(".bdb.pda > strong").text() val name = it.select(".bdb.pda > strong").text()
val list = it.select(".movie").mapNotNull { element -> if (it.select(".movie").first().attr("href").contains("season-(.....)|ep-(.....)".toRegex())) return@apmap
element.toSearchResponse() val list = arrayListOf<SearchResponse>()
it.select(".movie").map { element ->
list.add(element.toSearchResponse()!!)
} }
HomePageList(name, list) pages.add(HomePageList(name, list))
} }
return HomePageResponse(pages) return HomePageResponse(pages)
} }
@ -72,7 +77,7 @@ class EgyBestProvider : MainAPI() {
val isMovie = Regex(".*/movie/.*|.*/masrahiya/.*").matches(url) val isMovie = Regex(".*/movie/.*|.*/masrahiya/.*").matches(url)
val posterUrl = doc.select("div.movie_img a img")?.attr("src") val posterUrl = doc.select("div.movie_img a img")?.attr("src")
val year = doc.select("div.movie_title h1 a")?.text()?.toIntOrNull() val year = doc.select("div.movie_title h1 a")?.text()?.toIntOrNull()
val title = doc.select("div.movie_title h1 span[itemprop=\"name\"]").text() val title = doc.select("div.movie_title h1 span").text()
val synopsis = doc.select("div.mbox").firstOrNull { val synopsis = doc.select("div.mbox").firstOrNull {
it.text().contains("القصة") it.text().contains("القصة")

View file

@ -0,0 +1,268 @@
package com.lagradost.cloudstream3.movieproviders
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.LoadResponse.Companion.addRating
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.Qualities
import java.lang.Exception
class NginxProvider : MainAPI() {
override var name = "Nginx"
override val hasQuickSearch = false
override val hasMainPage = true
override val supportedTypes = setOf(TvType.AnimeMovie, TvType.TvSeries, TvType.Movie)
fun getAuthHeader(storedCredentials: String?): Map<String, String> {
if (storedCredentials == null) {
return mapOf(Pair("Authorization", "Basic ")) // no Authorization headers
}
val basicAuthToken = base64Encode(storedCredentials.toByteArray()) // will this be loaded when not using the provider ??? can increase load
return mapOf(Pair("Authorization", "Basic $basicAuthToken"))
}
override suspend fun load(url: String): LoadResponse {
val authHeader = getAuthHeader(storedCredentials) // call again because it isn't reloaded if in main class and storedCredentials loads after
// url can be tvshow.nfo for series or mediaRootUrl for movies
val mediaRootDocument = app.get(url, authHeader).document
val nfoUrl = url + mediaRootDocument.getElementsByAttributeValueContaining("href", ".nfo").attr("href") // metadata url file
val metadataDocument = app.get(nfoUrl, authHeader).document // get the metadata nfo file
val isMovie = !nfoUrl.contains("tvshow.nfo")
val title = metadataDocument.selectFirst("title").text()
val description = metadataDocument.selectFirst("plot").text()
if (isMovie) {
val poster = metadataDocument.selectFirst("thumb").text()
val trailer = metadataDocument.select("trailer")?.mapNotNull {
it?.text()?.replace(
"plugin://plugin.video.youtube/play/?video_id=",
"https://www.youtube.com/watch?v="
)
}
val partialUrl = mediaRootDocument.getElementsByAttributeValueContaining("href", ".nfo").attr("href").replace(".nfo", ".")
val date = metadataDocument.selectFirst("year")?.text()?.toIntOrNull()
val ratingAverage = metadataDocument.selectFirst("value")?.text()?.toIntOrNull()
val tagsList = metadataDocument.select("genre")
?.mapNotNull { // all the tags like action, thriller ...
it?.text()
}
val dataList = mediaRootDocument.getElementsByAttributeValueContaining( // list of all urls of the webpage
"href",
partialUrl
)
val data = url + dataList.firstNotNullOf { item -> item.takeIf { (!it.attr("href").contains(".nfo") && !it.attr("href").contains(".jpg"))} }.attr("href").toString() // exclude poster and nfo (metadata) file
return MovieLoadResponse(
title,
data,
this.name,
TvType.Movie,
data,
poster,
date,
description,
ratingAverage,
tagsList,
null,
trailer,
null,
null,
)
} else // a tv serie
{
val list = ArrayList<Pair<Int, String>>()
val mediaRootUrl = url.replace("tvshow.nfo", "")
val posterUrl = mediaRootUrl + "poster.jpg"
val mediaRootDocument = app.get(mediaRootUrl, authHeader).document
val seasons =
mediaRootDocument.getElementsByAttributeValueContaining("href", "Season%20")
val tagsList = metadataDocument.select("genre")
?.mapNotNull { // all the tags like action, thriller ...; unused variable
it?.text()
}
//val actorsList = document.select("actor")
// ?.mapNotNull { // all the tags like action, thriller ...; unused variable
// it?.text()
// }
seasons.forEach { element ->
val season =
element.attr("href")?.replace("Season%20", "")?.replace("/", "")?.toIntOrNull()
val href = mediaRootUrl + element.attr("href")
if (season != null && season > 0 && href.isNotBlank()) {
list.add(Pair(season, href))
}
}
if (list.isEmpty()) throw ErrorLoadingException("No Seasons Found")
val episodeList = ArrayList<Episode>()
list.apmap { (seasonInt, seasonString) ->
val seasonDocument = app.get(seasonString, authHeader).document
val episodes = seasonDocument.getElementsByAttributeValueContaining(
"href",
".nfo"
) // get metadata
episodes.forEach { episode ->
val nfoDocument = app.get(seasonString + episode.attr("href"), authHeader).document // get episode metadata file
val epNum = nfoDocument.selectFirst("episode")?.text()?.toIntOrNull()
val poster =
seasonString + episode.attr("href").replace(".nfo", "-thumb.jpg")
val name = nfoDocument.selectFirst("title").text()
// val seasonInt = nfoDocument.selectFirst("season").text().toIntOrNull()
val date = nfoDocument.selectFirst("aired")?.text()
val plot = nfoDocument.selectFirst("plot")?.text()
val dataList = seasonDocument.getElementsByAttributeValueContaining(
"href",
episode.attr("href").replace(".nfo", "")
)
val data = seasonString + dataList.firstNotNullOf { item -> item.takeIf { (!it.attr("href").contains(".nfo") && !it.attr("href").contains(".jpg"))} }.attr("href").toString() // exclude poster and nfo (metadata) file
episodeList.add(
newEpisode(data) {
this.name = name
this.season = seasonInt
this.episode = epNum
this.posterUrl = poster
addDate(date)
this.description = plot
}
)
}
}
return newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodeList) {
this.name = title
this.url = url
this.posterUrl = posterUrl
this.episodes = episodeList
this.plot = description
this.tags = tagsList
}
}
}
override suspend fun loadLinks(
data: String,
isCasting: Boolean,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
// loadExtractor(data, null) { callback(it.copy(headers=authHeader)) }
val authHeader = getAuthHeader(storedCredentials) // call again because it isn't reloaded if in main class and storedCredentials loads after
callback.invoke (
ExtractorLink(
name,
name,
data,
data, // referer not needed
Qualities.Unknown.value,
false,
authHeader,
)
)
return true
}
override suspend fun getMainPage(): HomePageResponse? {
val authHeader = getAuthHeader(storedCredentials) // call again because it isn't reloaded if in main class and storedCredentials loads after
if (mainUrl == "NONE"){
throw ErrorLoadingException("No nginx url specified in the settings: Nginx Settigns > Nginx server url, try again in a few seconds")
}
val document = app.get(mainUrl, authHeader).document
val categories = document.select("a")
val returnList = categories.mapNotNull {
val categoryPath = mainUrl + it.attr("href") ?: return@mapNotNull null // get the url of the category; like http://192.168.1.10/media/Movies/
val categoryTitle = it.text() // get the category title like Movies or Series
if (categoryTitle != "../" && categoryTitle != "Music/") { // exclude parent dir and Music dir
val categoryDocument = app.get(categoryPath, authHeader).document // queries the page http://192.168.1.10/media/Movies/
val contentLinks = categoryDocument.select("a")
val currentList = contentLinks.mapNotNull { head ->
if (head.attr("href") != "../") {
try {
val mediaRootUrl =
categoryPath + head.attr("href")// like http://192.168.1.10/media/Series/Chernobyl/
val mediaDocument = app.get(mediaRootUrl, authHeader).document
val nfoFilename = mediaDocument.getElementsByAttributeValueContaining(
"href",
".nfo"
)[0].attr("href")
val isMovieType = nfoFilename != "tvshow.nfo"
val nfoPath =
mediaRootUrl + nfoFilename // must exist or will raise errors, only the first one is taken
val nfoContent =
app.get(nfoPath, authHeader).document // all the metadata
if (isMovieType) {
val movieName = nfoContent.select("title").text()
val posterUrl = mediaRootUrl + "poster.jpg"
return@mapNotNull MovieSearchResponse(
movieName,
mediaRootUrl,
this.name,
TvType.Movie,
posterUrl,
null,
)
} else { // tv serie
val serieName = nfoContent.select("title").text()
val posterUrl = mediaRootUrl + "poster.jpg"
TvSeriesSearchResponse(
serieName,
nfoPath,
this.name,
TvType.TvSeries,
posterUrl,
null,
null,
)
}
} catch (e: Exception) { // can cause issues invisible errors
null
//logError(e) // not working because it changes the return type of currentList to Any
}
} else null
}
if (currentList.isNotEmpty() && categoryTitle != "../") { // exclude upper dir
HomePageList(categoryTitle, currentList)
} else null
} else null // the path is ../ which is parent directory
}
// if (returnList.isEmpty()) return null // maybe doing nothing idk
return HomePageResponse(returnList)
}
}

View file

@ -663,7 +663,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("name") val name: String, @JsonProperty("name") val name: String,
@JsonProperty("location") val location: String, @JsonProperty("location") val location: String,
@JsonProperty("joined_at") val joined_at: String, @JsonProperty("joined_at") val joined_at: String,
@JsonProperty("picture") val picture: String, @JsonProperty("picture") val picture: String?,
) )
data class MalMainPicture( data class MalMainPicture(

View file

@ -545,10 +545,14 @@ class GeneratorPlayer : FullScreenPlayer() {
tvType = meta.tvType tvType = meta.tvType
} }
} }
//Get limit of characters on Video Title
player_episode_filler_holder?.isVisible = isFiller ?: false var limitTitle = 0
context?.let {
player_video_title?.text = if (headerName != null) { val settingsManager = PreferenceManager.getDefaultSharedPreferences(it)
limitTitle = settingsManager.getInt(getString(R.string.prefer_limit_title_key), 0)
}
//Generate video title
var playerVideoTitle = if (headerName != null) {
(headerName + (headerName +
if (tvType.isEpisodeBased() && episode != null) if (tvType.isEpisodeBased() && episode != null)
if (season == null) if (season == null)
@ -559,6 +563,15 @@ class GeneratorPlayer : FullScreenPlayer() {
} else { } else {
"" ""
} }
//Truncate video title if it exceeds limit
val differenceInLength = playerVideoTitle.length - limitTitle
val margin = 3 //If the difference is smaller than or equal to this value, ignore it
if (limitTitle > 0 && differenceInLength > margin) {
playerVideoTitle = playerVideoTitle.substring(0, limitTitle-1) + "..."
}
player_episode_filler_holder?.isVisible = isFiller ?: false
player_video_title?.text = playerVideoTitle
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")

View file

@ -47,6 +47,7 @@ import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showNginxTextInputDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog
import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.SubtitleHelper
import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso
@ -485,6 +486,32 @@ class SettingsFragment : PreferenceFragmentCompat() {
return@setOnPreferenceClickListener true return@setOnPreferenceClickListener true
} }
getPref(R.string.nginx_url_key)?.setOnPreferenceClickListener {
activity?.showNginxTextInputDialog(
settingsManager.getString(getString(R.string.nginx_url_pref), "Nginx server url").toString(),
settingsManager.getString(getString(R.string.nginx_url_key), "").toString(), // key: the actual you use rn
android.text.InputType.TYPE_TEXT_VARIATION_URI, // uri
{}) {
settingsManager.edit()
.putString(getString(R.string.nginx_url_key), it).apply() // change the stored url in nginx_url_key to it
}
return@setOnPreferenceClickListener true
}
getPref(R.string.nginx_credentials)?.setOnPreferenceClickListener {
activity?.showNginxTextInputDialog(
settingsManager.getString(getString(R.string.nginx_credentials_title), "Nginx Credentials").toString(),
settingsManager.getString(getString(R.string.nginx_credentials), "").toString(), // key: the actual you use rn
android.text.InputType.TYPE_TEXT_VARIATION_URI,
{}) {
settingsManager.edit()
.putString(getString(R.string.nginx_credentials), it).apply() // change the stored url in nginx_url_key to it
}
return@setOnPreferenceClickListener true
}
getPref(R.string.prefer_media_type_key)?.setOnPreferenceClickListener { getPref(R.string.prefer_media_type_key)?.setOnPreferenceClickListener {
val prefNames = resources.getStringArray(R.array.media_type_pref) val prefNames = resources.getStringArray(R.array.media_type_pref)
val prefValues = resources.getIntArray(R.array.media_type_pref_values) val prefValues = resources.getIntArray(R.array.media_type_pref_values)
@ -678,6 +705,24 @@ class SettingsFragment : PreferenceFragmentCompat() {
return@setOnPreferenceClickListener true return@setOnPreferenceClickListener true
} }
getPref(R.string.prefer_limit_title_key)?.setOnPreferenceClickListener {
val prefNames = resources.getStringArray(R.array.limit_title_pref_names)
val prefValues = resources.getIntArray(R.array.limit_title_pref_values)
val current = settingsManager.getInt(getString(R.string.prefer_limit_title_key), 0)
activity?.showBottomDialog(
prefNames.toList(),
prefValues.indexOf(current),
getString(R.string.limit_title),
true,
{}) {
settingsManager.edit()
.putInt(getString(R.string.prefer_limit_title_key), prefValues[it])
.apply()
}
return@setOnPreferenceClickListener true
}
getPref(R.string.dns_key)?.setOnPreferenceClickListener { getPref(R.string.dns_key)?.setOnPreferenceClickListener {
val prefNames = resources.getStringArray(R.array.dns_pref) val prefNames = resources.getStringArray(R.array.dns_pref)
val prefValues = resources.getIntArray(R.array.dns_pref_values) val prefValues = resources.getIntArray(R.array.dns_pref_values)

View file

@ -100,6 +100,8 @@ val extractorApis: Array<ExtractorApi> = arrayOf(
VizcloudOnline(), VizcloudOnline(),
VizcloudXyz(), VizcloudXyz(),
VizcloudLive(), VizcloudLive(),
VizcloudInfo(),
MwvnVizcloudInfo(),
Mp4Upload(), Mp4Upload(),
StreamTape(), StreamTape(),
MixDrop(), MixDrop(),

View file

@ -144,6 +144,46 @@ object SingleSelectionHelper {
} }
} }
private fun Activity.showInputDialog(
dialog: Dialog,
value: String,
name: String,
textInputType: Int?,
callback: (String) -> Unit,
dismissCallback: () -> Unit
) {
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)!!
val cancelButton = dialog.findViewById<TextView>(R.id.cancel_btt)!!
val applyHolder = dialog.findViewById<LinearLayout>(R.id.apply_btt_holder)!!
applyHolder.isVisible = true
textView.text = name
if (textInputType != null) {
inputView.inputType = textInputType // 16 for website url input type
}
inputView.setText(value, TextView.BufferType.EDITABLE)
applyButton.setOnClickListener {
callback.invoke(inputView.text.toString()) // try to save the setting, using callback
dialog.dismissSafe(this)
}
cancelButton.setOnClickListener { // just dismiss
dialog.dismissSafe(this)
}
dialog.setOnDismissListener {
dismissCallback.invoke()
}
}
fun Activity.showMultiDialog( fun Activity.showMultiDialog(
items: List<String>, items: List<String>,
selectedIndex: List<Int>, selectedIndex: List<Int>,
@ -192,7 +232,7 @@ object SingleSelectionHelper {
selectedIndex: Int, selectedIndex: Int,
name: String, name: String,
showApply: Boolean, showApply: Boolean,
dismissCallback: () -> Unit, dismissCallback: () -> Unit,
callback: (Int) -> Unit, callback: (Int) -> Unit,
) { ) {
val builder = val builder =
@ -211,4 +251,25 @@ object SingleSelectionHelper {
dismissCallback dismissCallback
) )
} }
fun Activity.showNginxTextInputDialog(
name: String,
value: String,
textInputType: Int?,
dismissCallback: () -> Unit,
callback: (String) -> Unit,
) {
val builder = BottomSheetDialog(this) // probably the stuff at the bottom
builder.setContentView(R.layout.bottom_input_dialog) // input layout
builder.show()
showInputDialog(
builder,
value,
name,
textInputType, // type is a uri
callback,
dismissCallback
)
}
} }

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/white">
<path
android:fillColor="@android:color/white"
android:pathData="M5,17v2h14v-2L5,17zM9.5,12.8h5l0.9,2.2h2.1L12.75,4h-1.5L6.5,15h2.1l0.9,-2.2zM12,5.98L13.87,11h-3.74L12,5.98z"/>
</vector>

View file

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:nextFocusDown="@id/nginx_text_input"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_rowWeight="1"
android:layout_marginTop="20dp"
android:layout_marginBottom="10dp"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:textColor="?attr/textColor"
android:textSize="20sp"
android:textStyle="bold"
tools:text="Test" />
<EditText
android:id="@+id/nginx_text_input"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusLeft="@id/apply_btt"
android:layout_marginBottom="60dp"
android:layout_marginHorizontal="10dp"
android:paddingTop="10dp"
android:requiresFadingEdge="vertical"
tools:text="nginx.com"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_rowWeight="1"
android:autofillHints="Autofill Hint"
android:inputType="text"
tools:ignore="LabelFor" />
<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/sort_apply"
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

@ -32,7 +32,7 @@
</array> </array>
<array name="dns_pref"> <array name="dns_pref">
<item>None</item> <item>@string/none</item>
<item>Google</item> <item>Google</item>
<item>Cloudflare</item> <item>Cloudflare</item>
<!-- <item>OpenDns</item>--> <!-- <item>OpenDns</item>-->
@ -59,6 +59,21 @@
<item>3</item> <item>3</item>
</array> </array>
<array name="limit_title_pref_names">
<item>@string/none</item>
<item>16 characters</item>
<item>32 characters</item>
<item>64 characters</item>
<item>128 characters</item>
</array>
<array name="limit_title_pref_values">
<item>0</item>
<item>16</item>
<item>32</item>
<item>64</item>
<item>128</item>
</array>
<array name="video_buffer_length_names"> <array name="video_buffer_length_names">
<item>@string/automatic</item> <item>@string/automatic</item>
<item>1min</item> <item>1min</item>

View file

@ -13,6 +13,7 @@
<string name="subtitle_settings_key" translatable="false">subtitle_settings_key</string> <string name="subtitle_settings_key" translatable="false">subtitle_settings_key</string>
<string name="subtitle_settings_chromecast_key" translatable="false">subtitle_settings_chromecast_key</string> <string name="subtitle_settings_chromecast_key" translatable="false">subtitle_settings_chromecast_key</string>
<string name="quality_pref_key" translatable="false">quality_pref_key</string> <string name="quality_pref_key" translatable="false">quality_pref_key</string>
<string name="prefer_limit_title_key" translatable="false">prefer_limit_title_key</string>
<string name="video_buffer_size_key" translatable="false">video_buffer_size_key</string> <string name="video_buffer_size_key" translatable="false">video_buffer_size_key</string>
<string name="video_buffer_length_key" translatable="false">video_buffer_length_key</string> <string name="video_buffer_length_key" translatable="false">video_buffer_length_key</string>
<string name="video_buffer_clear_key" translatable="false">video_buffer_clear_key</string> <string name="video_buffer_clear_key" translatable="false">video_buffer_clear_key</string>
@ -31,6 +32,9 @@
<string name="provider_lang_key" translatable="false">provider_lang_key</string> <string name="provider_lang_key" translatable="false">provider_lang_key</string>
<string name="dns_key" translatable="false">dns_key</string> <string name="dns_key" translatable="false">dns_key</string>
<string name="download_path_key" translatable="false">download_path_key</string> <string name="download_path_key" translatable="false">download_path_key</string>
<string name="nginx_url_key" translatable="false">nginx_url_key</string>
<string name="nginx_credentials" translatable="false">nginx_credentials</string>
<string name="nginx_info" translatable="false">nginx_info</string>
<string name="app_name_download_path" translatable="false">Cloudstream</string> <string name="app_name_download_path" translatable="false">Cloudstream</string>
<string name="app_layout_key" translatable="false">app_layout_key</string> <string name="app_layout_key" translatable="false">app_layout_key</string>
<string name="primary_color_key" translatable="false">primary_color_key</string> <string name="primary_color_key" translatable="false">primary_color_key</string>
@ -227,6 +231,12 @@
<string name="backup_failed_error_format">Error backing up %s</string> <string name="backup_failed_error_format">Error backing up %s</string>
<string name="search">Search</string> <string name="search">Search</string>
<string name="nginx_category">Nginx Settings</string>
<string name="nginx_credentials_title">Nginx Credential</string>
<string name="nginx_credentials_summary">You have to use the following format mycoolusername:mysecurepassword123</string>
<string name="nginx_info_title">What is Nginx ?</string>
<string name="nginx_info_summary">Nginx is a software that can be used to display files from a server that you own. Click to see a Nginx setup guide</string>
<string name="settings_info">Info</string> <string name="settings_info">Info</string>
<string name="advanced_search">Advanced Search</string> <string name="advanced_search">Advanced Search</string>
<string name="advanced_search_des">Gives you the search results separated by provider</string> <string name="advanced_search_des">Gives you the search results separated by provider</string>
@ -339,6 +349,7 @@
<string name="dont_show_again">Don\'t show again</string> <string name="dont_show_again">Don\'t show again</string>
<string name="update">Update</string> <string name="update">Update</string>
<string name="watch_quality_pref">Preferred watch quality</string> <string name="watch_quality_pref">Preferred watch quality</string>
<string name="limit_title">Limit title characters on player</string>
<string name="video_buffer_size_settings">Video buffer size</string> <string name="video_buffer_size_settings">Video buffer size</string>
<string name="video_buffer_length_settings">Video buffer length</string> <string name="video_buffer_length_settings">Video buffer length</string>
<string name="video_buffer_disk_settings">Video cache on disk</string> <string name="video_buffer_disk_settings">Video cache on disk</string>
@ -352,6 +363,8 @@
<string name="download_path_pref">Download path</string> <string name="download_path_pref">Download path</string>
<string name="nginx_url_pref">Nginx server url</string>
<string name="display_subbed_dubbed_settings">Display Dubbed/Subbed Anime</string> <string name="display_subbed_dubbed_settings">Display Dubbed/Subbed Anime</string>
<string name="resize_fit">Fit to screen</string> <string name="resize_fit">Fit to screen</string>

View file

@ -20,6 +20,10 @@
android:key="@string/quality_pref_key" android:key="@string/quality_pref_key"
android:title="@string/watch_quality_pref" android:title="@string/watch_quality_pref"
android:icon="@drawable/ic_baseline_hd_24" /> android:icon="@drawable/ic_baseline_hd_24" />
<Preference
android:key="@string/prefer_limit_title_key"
android:title="@string/limit_title"
android:icon="@drawable/ic_baseline_text_format_24" />
<SwitchPreference <SwitchPreference
android:icon="@drawable/ic_baseline_picture_in_picture_alt_24" android:icon="@drawable/ic_baseline_picture_in_picture_alt_24"
@ -162,6 +166,31 @@
app:defaultValue="true" /> app:defaultValue="true" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory
android:key="@string/nginx_category"
android:title="@string/nginx_category"
app:isPreferenceVisible="true">
<Preference
android:key="@string/nginx_url_key"
android:title="@string/nginx_url_pref"
android:icon="@drawable/ic_baseline_play_arrow_24" />
<Preference
android:key="@string/nginx_credentials"
android:title="@string/nginx_credentials_title"
android:icon="@drawable/video_locked"
android:summary="@string/nginx_credentials_summary"/>
<Preference
android:key="@string/nginx_info"
android:title="@string/nginx_info_title"
android:icon="@drawable/ic_baseline_play_arrow_24"
android:summary="@string/nginx_info_summary" >
<intent
android:action="android.intent.action.VIEW"
android:data="https://www.sarlays.com/use-nginx-with-cloudstream/" />
</Preference>
</PreferenceCategory>
<PreferenceCategory <PreferenceCategory
android:key="info" android:key="info"
android:title="@string/settings_info" android:title="@string/settings_info"

View file

@ -7,131 +7,17 @@
<meta http-equiv="X-UA-Compatible" content="ie=edge"> <meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto"> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto">
<title>CloudStream-3 Supported Sites</title> <title>CloudStream-3 Supported Sites</title>
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js'></script> <link rel="stylesheet" href="style.css">
<style>
body {
font-family: "Roboto", sans-serif;
background-color: #FFF;
}
.whiteText {
color : #FFF;
}
.button {
color : #000;
text-decoration: none;
}
.redButton {
}
.blueButton {
}
.greenButton {
}
.yellowButton {
}
.row {
padding: 0px 10px;
white-space: nowrap;
}
table {
border-spacing: 0.5rem;
}
.yellowButton::before {
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='m12 24c6.6274 0 12-5.3726 12-12 0-6.62744-5.3726-12-12-12-6.62744 0-12 5.37256-12 12 0 6.6274 5.37256 12 12 12zm0-17.5c.4141 0 .75.33582.75.75v4.5c0 .4142-.3359.75-.75.75s-.75-.3358-.75-.75v-4.5c0-.41418.3359-.75.75-.75zm.8242 9.5658c.0635-.0919.1118-.195.1416-.3054.0132-.0482.0225-.0979.0283-.1487.0039-.0366.0059-.074.0059-.1117 0-.5522-.4478-1-1-1s-1 .4478-1 1 .4478 1 1 1c.3423 0 .644-.172.8242-.4342z' fill='%23dbab09'/%3E%3C/svg%3e");
}
.blueButton::before {
filter: sepia(100%) saturate(300%) brightness(70%) hue-rotate(180deg);
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='m12 24c6.6274 0 12-5.3726 12-12 0-6.62744-5.3726-12-12-12-6.62744 0-12 5.37256-12 12 0 6.6274 5.37256 12 12 12zm0-17.5c.4141 0 .75.33582.75.75v4.5c0 .4142-.3359.75-.75.75s-.75-.3358-.75-.75v-4.5c0-.41418.3359-.75.75-.75zm.8242 9.5658c.0635-.0919.1118-.195.1416-.3054.0132-.0482.0225-.0979.0283-.1487.0039-.0366.0059-.074.0059-.1117 0-.5522-.4478-1-1-1s-1 .4478-1 1 .4478 1 1 1c.3423 0 .644-.172.8242-.4342z' fill='%23dbab09'/%3E%3C/svg%3e");
}
.redButton::before {
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='m12 24c6.6274 0 12-5.3726 12-12 0-6.62744-5.3726-12-12-12-6.62744 0-12 5.37256-12 12 0 6.6274 5.37256 12 12 12zm0-17.5c.4141 0 .75.33582.75.75v4.5c0 .4142-.3359.75-.75.75s-.75-.3358-.75-.75v-4.5c0-.41418.3359-.75.75-.75zm.8242 9.5658c.0635-.0919.1118-.195.1416-.3054.0132-.0482.0225-.0979.0283-.1487.0039-.0366.0059-.074.0059-.1117 0-.5522-.4478-1-1-1s-1 .4478-1 1 .4478 1 1 1c.3423 0 .644-.172.8242-.4342z' fill='%23d73a49'/%3E%3C/svg%3e");
}
.greenButton::before{
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg width='15' height='15' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='m1 12c0-6.07513 4.92487-11 11-11 6.0751 0 11 4.92487 11 11 0 6.0751-4.9249 11-11 11-6.07513 0-11-4.9249-11-11zm16.2803-2.71967c.2929-.29289.2929-.76777 0-1.06066s-.7677-.29289-1.0606 0l-5.9697 5.96963-2.46967-2.4696c-.29289-.2929-.76777-.2929-1.06066 0s-.29289.7677 0 1.0606l3 3c.29293.2929.76773.2929 1.06063 0z' fill='%2328a745'/%3E%3C/svg%3e");ontent: '';
}
.indicator::before {
display: inline-block;
width: 24px;
height: 24px;
content: "";
vertical-align: text-bottom;
background-size: 100% 100%;
background-repeat: no-repeat;
background-position: center center;
margin-right:10px;
}
</style>
</head> </head>
<body> <body>
<div> <div>
<h1>Site supported:</h1> <h1>Sites supported (<span id="count">0</span>):</h1>
<table> <table>
<tbody id="siteList"></tbody> <tbody id="siteList"></tbody>
</table> </table>
</div> </div>
<script> <script src="script.js" type="text/javascript"></script>
var status = document.getElementById("status");
var mainContainer = document.getElementById("siteList");
$(document).ready(function () {
$.getJSON("providers.json", function (data) {
status.innerHTML = "Parsing...";
for (var key in data) {
status.innerHTML = "Reading..." + key;
if (data.hasOwnProperty(key)) {
var value = data[key];
if(value.url == "NONE") { continue; }
var _status = value.status
var node = document.createElement("tr");
node.classList.add("row");
var _a = document.createElement("a");
_a.setAttribute('href', value.url);
_a.innerHTML = value.name
var _statusText = "Unknown";
var _buttonText = "yellow";
switch (_status) {
case 0:
_statusText = "Unavailable";
_buttonText = "red";
break;
case 1:
_statusText = "Available";
_buttonText = "green";
break;
case 2:
_statusText = "Slow";
_buttonText = "yellow";
break;
case 3:
_statusText = "Beta";
_buttonText = "blue";
break;
}
_a.classList.add(_buttonText+"Button");
_a.classList.add("indicator");
_a.classList.add("button");
node.appendChild(_a);
mainContainer.appendChild(node);
}
}
}).fail(function () {
console.log("An error has occurred.");
});
});
</script>
</body> </body>
</html> </html>

View file

@ -1,287 +1,322 @@
{ {
"AkwamProvider": { "AkwamProvider": {
"name": "Akwam", "name": "Akwam",
"url": "https://akwam.to", "status": 1,
"status": 1 "url": "https://akwam.to"
}, },
"AllAnimeProvider": { "AllAnimeProvider": {
"name": "AllAnime", "name": "AllAnime",
"url": "https://allanime.site", "status": 1,
"status": 1 "url": "https://allanime.site"
}, },
"AllMoviesForYouProvider": { "AllMoviesForYouProvider": {
"name": "AllMoviesForYou", "name": "AllMoviesForYou",
"url": "https://allmoviesforyou.net", "status": 1,
"status": 1 "url": "https://allmoviesforyou.net"
}, },
"AnimeFlickProvider": { "AnimeFlickProvider": {
"name": "AnimeFlick", "name": "AnimeFlick",
"url": "https://animeflick.net", "status": 1,
"status": 1 "url": "https://animeflick.net"
}, },
"AnimePaheProvider": { "AnimePaheProvider": {
"name": "AnimePahe", "name": "AnimePahe",
"url": "https://animepahe.com", "status": 0,
"status": 0 "url": "https://animepahe.com"
}, },
"AnimeWorldProvider": { "AnimeWorldProvider": {
"name": "AnimeWorld", "name": "AnimeWorld",
"url": "https://www.animeworld.tv", "status": 1,
"status": 1 "url": "https://www.animeworld.tv"
},
"AnimeflvProvider": {
"name": "Animeflv",
"status": 1,
"url": "https://www3.animeflv.net"
}, },
"AnimeflvnetProvider": { "AnimeflvnetProvider": {
"name": "Animeflv.net", "name": "Animeflv.net",
"url": "https://www3.animeflv.net", "status": 1,
"status": 1 "url": "https://www3.animeflv.net"
}, },
"AnimekisaProvider": { "AnimekisaProvider": {
"name": "Animekisa", "name": "Animekisa",
"url": "https://animekisa.in", "status": 1,
"status": 1 "url": "https://animekisa.in"
}, },
"AsianLoadProvider": { "ApiMDBProvider": {
"name": "AsianLoad", "name": "ApiMDB",
"url": "https://asianembed.io", "status": 1,
"status": 1 "url": "https://v2.apimdb.net"
}, },
"AsiaFlixProvider": { "AsiaFlixProvider": {
"name": "AsiaFlix", "name": "AsiaFlix",
"url": "https://asiaflix.app", "status": 3,
"status": 3 "url": "https://asiaflix.app"
},
"AsianLoadProvider": {
"name": "AsianLoad",
"status": 1,
"url": "https://asianembed.io"
}, },
"BflixProvider": { "BflixProvider": {
"name": "Bflix", "name": "Bflix",
"url": "https://bflix.to", "status": 1,
"status": 1 "url": "https://fmovies.to"
},
"FmoviesToProvider": {
"name": "Fmovies.to",
"url": "https://fmovies.to",
"status": 1
},
"SflixProProvider": {
"name": "Sflix.pro",
"url": "https://sflix.pro",
"status": 1
}, },
"CinecalidadProvider": { "CinecalidadProvider": {
"name": "Cinecalidad", "name": "Cinecalidad",
"url": "https://cinecalidad.lol", "status": 1,
"status": 1 "url": "https://cinecalidad.lol"
}, },
"CrossTmdbProvider": { "CrossTmdbProvider": {
"name": "MultiMovie", "name": "MultiMovie",
"url": "NONE", "status": 1,
"status": 1 "url": "NONE"
}, },
"CuevanaProvider": { "CuevanaProvider": {
"name": "Cuevana", "name": "Cuevana",
"url": "https://cuevana3.me", "status": 1,
"status": 1 "url": "https://cuevana3.me"
},
"DoramasYTProvider": {
"name": "DoramasYT",
"url": "https://doramasyt.com",
"status": 1
},
"DramaSeeProvider": {
"name": "DramaSee",
"url": "https://dramasee.net",
"status": 1
},
"DubbedAnimeProvider": {
"name": "DubbedAnime",
"url": "https://bestdubbedanime.com",
"status": 1
},
"EgyBestProvider": {
"name": "EgyBest",
"url": "https://egy.best",
"status": 1
},
"EntrepeliculasyseriesProvider": {
"name": "EntrePeliculasySeries",
"url": "https://entrepeliculasyseries.nu",
"status": 1
},
"FilmanProvider": {
"name": "filman.cc",
"url": "https://filman.cc",
"status": 1
},
"FrenchStreamProvider": {
"name": "French Stream",
"url": "https://french-stream.re",
"status": 1
},
"GogoanimeProvider": {
"name": "GogoAnime",
"url": "https://gogoanime.film",
"status": 1
},
"KawaiifuProvider": {
"name": "Kawaiifu",
"url": "https://kawaiifu.com",
"status": 0
},
"HDMProvider": {
"name": "HD Movies",
"url": "https://hdm.to",
"status": 0
},
"IHaveNoTvProvider": {
"name": "I Have No TV",
"url": "https://ihavenotv.com",
"status": 1
},
"KdramaHoodProvider": {
"name": "KDramaHood",
"url": "https://kdramahood.com",
"status": 1
},
"LookMovieProvider": {
"name": "LookMovie",
"url": "https://lookmovie.io",
"status": 0
},
"MeloMovieProvider": {
"name": "MeloMovie",
"url": "https://melomovie.com",
"status": 0
},
"MonoschinosProvider": {
"name": "Monoschinos",
"url": "https://monoschinos2.com",
"status": 1
},
"MyCimaProvider": {
"name": "MyCima",
"url": "https://mycima.tv",
"status": 1
},
"NineAnimeProvider": {
"name": "9Anime",
"url": "https://9anime.id",
"status": 1
},
"PeliSmartProvider": {
"name": "PeliSmart",
"url": "https://pelismart.com",
"status": 1
},
"PelisflixProvider": {
"name": "Pelisflix",
"url": "https://pelisflix.li",
"status": 1
},
"PelisplusHDProvider": {
"name": "PelisplusHD",
"url": "https://pelisplushd.net",
"status": 1
},
"PelisplusProvider": {
"name": "Pelisplus",
"url": "https://pelisplus.icu",
"status": 1
},
"PinoyHDXyzProvider": {
"name": "Pinoy-HD",
"url": "https://www.pinoy-hd.xyz",
"status": 1
},
"PinoyMoviePediaProvider": {
"name": "Pinoy Moviepedia",
"url": "https://pinoymoviepedia.ru",
"status": 1
},
"PinoyMoviesEsProvider": {
"name": "Pinoy Movies",
"url": "https://pinoymovies.es",
"status": 1
},
"SflixProvider": {
"name": "Sflix.to",
"url": "https://sflix.to",
"status": 1
}, },
"DopeboxProvider": { "DopeboxProvider": {
"name": "Dopebox", "name": "Dopebox",
"url": "https://dopebox.to", "status": 1,
"status": 1 "url": "https://dopebox.to"
}, },
"SolarmovieProvider": { "DoramasYTProvider": {
"name": "Solarmovie", "name": "DoramasYT",
"url": "https://solarmovie.pe", "status": 1,
"status": 1 "url": "https://doramasyt.com"
},
"DramaSeeProvider": {
"name": "DramaSee",
"status": 1,
"url": "https://dramasee.net"
},
"DubbedAnimeProvider": {
"name": "DubbedAnime",
"status": 1,
"url": "https://bestdubbedanime.com"
},
"EgyBestProvider": {
"name": "EgyBest",
"status": 1,
"url": "https://www.egy.best"
},
"EntrePeliculasySeriesProvider": {
"name": "EntrePeliculasySeries",
"status": 1,
"url": "https://entrepeliculasyseries.nu"
},
"EntrepeliculasyseriesProvider": {
"name": "EntrePeliculasySeries",
"status": 1,
"url": "https://entrepeliculasyseries.nu"
},
"FilmanProvider": {
"name": "filman.cc",
"status": 1,
"url": "https://filman.cc"
},
"FmoviesToProvider": {
"name": "Fmovies.to",
"status": 1,
"url": "https://fmovies.to"
},
"FrenchStreamProvider": {
"name": "French Stream",
"status": 1,
"url": "https://french-stream.re"
},
"GogoanimeProvider": {
"name": "GogoAnime",
"status": 1,
"url": "https://gogoanime.film"
},
"HDMProvider": {
"name": "HD Movies",
"status": 0,
"url": "https://hdm.to"
},
"IHaveNoTvProvider": {
"name": "I Have No TV",
"status": 1,
"url": "https://ihavenotv.com"
},
"KawaiifuProvider": {
"name": "Kawaiifu",
"status": 0,
"url": "https://kawaiifu.com"
},
"KdramaHoodProvider": {
"name": "KDramaHood",
"status": 1,
"url": "https://kdramahood.com"
},
"LookMovieProvider": {
"name": "LookMovie",
"status": 0,
"url": "https://lookmovie.io"
},
"MeloMovieProvider": {
"name": "MeloMovie",
"status": 0,
"url": "https://melomovie.com"
},
"MonoschinosProvider": {
"name": "Monoschinos",
"status": 1,
"url": "https://monoschinos2.com"
},
"MultiAnimeProvider": {
"name": "MultiAnime",
"status": 1,
"url": ""
},
"MyCimaProvider": {
"name": "MyCima",
"status": 1,
"url": "https://mycima.tv"
},
"NginxProvider": {
"name": "Nginx",
"status": 1,
"url": ""
},
"NineAnimeProvider": {
"name": "9Anime",
"status": 1,
"url": "https://9anime.id"
},
"NyaaProvider": {
"name": "Nyaa",
"status": 1,
"url": "https://nyaa.si"
},
"PeliSmartProvider": {
"name": "PeliSmart",
"status": 1,
"url": "https://pelismart.com"
},
"PelisflixProvider": {
"name": "Pelisflix",
"status": 1,
"url": "https://pelisflix.li"
},
"PelisplusHDProvider": {
"name": "PelisplusHD",
"status": 1,
"url": "https://pelisplushd.net"
},
"PelisplusProvider": {
"name": "Pelisplus",
"status": 1,
"url": "https://pelisplus.icu"
},
"PinoyHDXyzProvider": {
"name": "Pinoy-HD",
"status": 1,
"url": "https://www.pinoy-hd.xyz"
},
"PinoyMoviePediaProvider": {
"name": "Pinoy Moviepedia",
"status": 1,
"url": "https://pinoymoviepedia.ru"
},
"PinoyMoviesEsProvider": {
"name": "Pinoy Movies",
"status": 1,
"url": "https://pinoymovies.es"
}, },
"SeriesflixProvider": { "SeriesflixProvider": {
"name": "Seriesflix", "name": "Seriesflix",
"url": "https://seriesflix.video", "status": 1,
"status": 1 "url": "https://seriesflix.video"
},
"SflixProProvider": {
"name": "Sflix.pro",
"status": 1,
"url": "https://sflix.pro"
},
"SflixProvider": {
"name": "Sflix.to",
"status": 1,
"url": "https://dopebox.to"
}, },
"SoaptwoDayProvider": { "SoaptwoDayProvider": {
"name": "Soap2Day", "name": "Soap2Day",
"url": "https://secretlink.xyz", "status": 1,
"status": 1 "url": "https://secretlink.xyz"
},
"SolarmovieProvider": {
"name": "Solarmovie",
"status": 1,
"url": "https://solarmovie.pe"
}, },
"TenshiProvider": { "TenshiProvider": {
"name": "Tenshi.moe", "name": "Tenshi.moe",
"url": "https://tenshi.moe", "status": 1,
"status": 1 "url": "https://tenshi.moe"
},
"TrailersTwoProvider": {
"name": "Trailers.to",
"url": "https://trailers.to",
"status": 1
}, },
"TheFlixToProvider": { "TheFlixToProvider": {
"name": "TheFlix.to", "name": "TheFlix.to",
"url": "https://theflix.to", "status": 0,
"status": 0 "url": "https://theflix.to"
},
"TmdbProvider": {
"name": "Tmdb",
"status": 1,
"url": ""
},
"TrailersTwoProvider": {
"name": "Trailers.to",
"status": 1,
"url": "https://trailers.to"
}, },
"TwoEmbedProvider": { "TwoEmbedProvider": {
"name": "2Embed", "name": "2Embed",
"url": "https://www.2embed.ru", "status": 1,
"status": 1 "url": "https://www.2embed.ru"
}, },
"VMoveeProvider": { "VMoveeProvider": {
"name": "VMovee", "name": "VMovee",
"url": "https://www.vmovee.watch", "status": 1,
"status": 1 "url": "https://www.vmovee.watch"
}, },
"VfFilmProvider": { "VfFilmProvider": {
"name": "vf-film.me", "name": "vf-film.me",
"url": "https://vf-film.me", "status": 1,
"status": 1 "url": "https://vf-film.me"
}, },
"VfSerieProvider": { "VfSerieProvider": {
"name": "vf-serie.org", "name": "vf-serie.org",
"url": "https://vf-serie.org", "status": 1,
"status": 1 "url": "https://vf-serie.org"
}, },
"VidEmbedProvider": { "VidEmbedProvider": {
"name": "VidEmbed", "name": "VidEmbed",
"url": "https://vidembed.cc", "status": 1,
"status": 1 "url": "https://vidembed.cc"
}, },
"WatchAsianProvider": { "WatchAsianProvider": {
"name": "WatchAsian", "name": "WatchAsian",
"url": "https://watchasian.sh", "status": 1,
"status": 1 "url": "https://watchasian.sh"
}, },
"WatchCartoonOnlineProvider": { "WatchCartoonOnlineProvider": {
"name": "WatchCartoonOnline", "name": "WatchCartoonOnline",
"url": "https://www.wcostream.com", "status": 1,
"status": 1 "url": "https://www.wcostream.com"
}, },
"WcoProvider": { "WcoProvider": {
"name": "WCO Stream", "name": "WCO Stream",
"url": "https://wcostream.cc", "status": 1,
"status": 1 "url": "https://wcostream.cc"
}, },
"ZoroProvider": { "ZoroProvider": {
"name": "Zoro", "name": "Zoro",
"url": "https://zoro.to", "status": 1,
"status": 1 "url": "https://zoro.to"
} }
} }

49
docs/script.js Normal file
View file

@ -0,0 +1,49 @@
const count = document.getElementById("count")
const mainContainer = document.getElementById("siteList");
fetch("providers.json" + "?v=" + Date.now())
.then(r => r.json())
.then(function (data) {
count.innerHTML = Object.keys(data).length;
for (var key in data) {
if (data.hasOwnProperty(key)) {
var value = data[key];
if (value.url == "NONE") { continue; }
var _status = value.status
var node = document.createElement("tr");
node.classList.add("row");
var _a = document.createElement("a");
_a.setAttribute('href', value.url);
_a.innerHTML = value.name
var _statusText = "Unknown";
var _buttonText = "yellow";
switch (_status) {
case 0:
_statusText = "Unavailable";
_buttonText = "red";
break;
case 1:
_statusText = "Available";
_buttonText = "green";
break;
case 2:
_statusText = "Slow";
_buttonText = "yellow";
break;
case 3:
_statusText = "Beta";
_buttonText = "blue";
break;
}
_a.classList.add(_buttonText + "Button");
_a.classList.add("indicator");
_a.classList.add("button");
node.appendChild(_a);
mainContainer.appendChild(node);
}
}
})

49
docs/style.css Normal file
View file

@ -0,0 +1,49 @@
body {
font-family: "Roboto", sans-serif;
background-color: #FFF;
}
.whiteText {
color : #FFF;
}
.button {
color : #000;
text-decoration: none;
}
.row {
padding: 0px 10px;
white-space: nowrap;
}
table {
border-spacing: 0.5rem;
}
.yellowButton::before {
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='m12 24c6.6274 0 12-5.3726 12-12 0-6.62744-5.3726-12-12-12-6.62744 0-12 5.37256-12 12 0 6.6274 5.37256 12 12 12zm0-17.5c.4141 0 .75.33582.75.75v4.5c0 .4142-.3359.75-.75.75s-.75-.3358-.75-.75v-4.5c0-.41418.3359-.75.75-.75zm.8242 9.5658c.0635-.0919.1118-.195.1416-.3054.0132-.0482.0225-.0979.0283-.1487.0039-.0366.0059-.074.0059-.1117 0-.5522-.4478-1-1-1s-1 .4478-1 1 .4478 1 1 1c.3423 0 .644-.172.8242-.4342z' fill='%23dbab09'/%3E%3C/svg%3e");
}
.blueButton::before {
filter: sepia(100%) saturate(300%) brightness(70%) hue-rotate(180deg);
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='m12 24c6.6274 0 12-5.3726 12-12 0-6.62744-5.3726-12-12-12-6.62744 0-12 5.37256-12 12 0 6.6274 5.37256 12 12 12zm0-17.5c.4141 0 .75.33582.75.75v4.5c0 .4142-.3359.75-.75.75s-.75-.3358-.75-.75v-4.5c0-.41418.3359-.75.75-.75zm.8242 9.5658c.0635-.0919.1118-.195.1416-.3054.0132-.0482.0225-.0979.0283-.1487.0039-.0366.0059-.074.0059-.1117 0-.5522-.4478-1-1-1s-1 .4478-1 1 .4478 1 1 1c.3423 0 .644-.172.8242-.4342z' fill='%23dbab09'/%3E%3C/svg%3e");
}
.redButton::before {
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='m12 24c6.6274 0 12-5.3726 12-12 0-6.62744-5.3726-12-12-12-6.62744 0-12 5.37256-12 12 0 6.6274 5.37256 12 12 12zm0-17.5c.4141 0 .75.33582.75.75v4.5c0 .4142-.3359.75-.75.75s-.75-.3358-.75-.75v-4.5c0-.41418.3359-.75.75-.75zm.8242 9.5658c.0635-.0919.1118-.195.1416-.3054.0132-.0482.0225-.0979.0283-.1487.0039-.0366.0059-.074.0059-.1117 0-.5522-.4478-1-1-1s-1 .4478-1 1 .4478 1 1 1c.3423 0 .644-.172.8242-.4342z' fill='%23d73a49'/%3E%3C/svg%3e");
}
.greenButton::before{
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg width='15' height='15' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='m1 12c0-6.07513 4.92487-11 11-11 6.0751 0 11 4.92487 11 11 0 6.0751-4.9249 11-11 11-6.07513 0-11-4.9249-11-11zm16.2803-2.71967c.2929-.29289.2929-.76777 0-1.06066s-.7677-.29289-1.0606 0l-5.9697 5.96963-2.46967-2.4696c-.29289-.2929-.76777-.2929-1.06066 0s-.29289.7677 0 1.0606l3 3c.29293.2929.76773.2929 1.06063 0z' fill='%2328a745'/%3E%3C/svg%3e");ontent: '';
}
.indicator::before {
display: inline-block;
width: 24px;
height: 24px;
content: "";
vertical-align: text-bottom;
background-size: 100% 100%;
background-repeat: no-repeat;
background-position: center center;
margin-right:10px;
}