diff --git a/FrenchStreamProvider/build.gradle.kts b/FrenchStreamProvider/build.gradle.kts index 83658ab..b86bc10 100644 --- a/FrenchStreamProvider/build.gradle.kts +++ b/FrenchStreamProvider/build.gradle.kts @@ -6,8 +6,8 @@ cloudstream { language = "fr" // All of these properties are optional, you can safely remove them - // description = "Lorem Ipsum" - // authors = listOf("Cloudburst") + description = "FRENCH STREAM en plus d'être un site efficace et plaisant dispose d'un contenu visuel diversifié" + authors = listOf("Sarlay", "Eddy976") /** * Status int as the following: @@ -18,10 +18,9 @@ cloudstream { * */ status = 1 // will be 3 if unspecified tvTypes = listOf( - "AnimeMovie", "TvSeries", "Movie", ) - iconUrl = "https://www.google.com/s2/favicons?domain=french-stream.re&sz=%size%" + iconUrl = "https://www.google.com/s2/favicons?domain=french-stream.ac&sz=%size%" } \ No newline at end of file diff --git a/FrenchStreamProvider/src/main/kotlin/com/lagradost/FrenchStreamProvider.kt b/FrenchStreamProvider/src/main/kotlin/com/lagradost/FrenchStreamProvider.kt index 4d6720c..05aef23 100644 --- a/FrenchStreamProvider/src/main/kotlin/com/lagradost/FrenchStreamProvider.kt +++ b/FrenchStreamProvider/src/main/kotlin/com/lagradost/FrenchStreamProvider.kt @@ -1,54 +1,31 @@ package com.lagradost + import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.LoadResponse.Companion.addRating import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.extractorApis +import org.jsoup.nodes.Element class FrenchStreamProvider : MainAPI() { - override var mainUrl = "https://french-stream.re" - override var name = "French Stream" + override var mainUrl = "https://french-stream.cx" //re ou ac ou city + override var name = "FrenchStream" override val hasQuickSearch = false override val hasMainPage = true override var lang = "fr" - override val supportedTypes = setOf(TvType.AnimeMovie, TvType.TvSeries, TvType.Movie) - + override val supportedTypes = setOf(TvType.Movie, TvType.TvSeries) override suspend fun search(query: String): List { - val link = "$mainUrl/?do=search&subaction=search&story=$query" - val soup = app.post(link).document + val link = "$mainUrl/?do=search&subaction=search&story=$query" // search' + val document = + app.post(link).document // app.get() permet de télécharger la page html avec une requete HTTP (get) + val results = document.select("div#dle-content > > div.short") - return soup.select("div.short-in.nl").map { li -> - val href = fixUrl(li.selectFirst("a.short-poster")!!.attr("href")) - val poster = li.selectFirst("img")?.attr("src") - val title = li.selectFirst("> a.short-poster")!!.text().toString().replace(". ", "") - val year = li.selectFirst(".date")?.text()?.split("-")?.get(0)?.toIntOrNull() - if (title.contains( - "saison", - ignoreCase = true - ) - ) { // if saison in title ==> it's a TV serie - TvSeriesSearchResponse( - title, - href, - this.name, - TvType.TvSeries, - poster, - year, - (title.split("Eps ", " ")[1]).split(" ")[0].toIntOrNull() - ) - } else { // it's a movie - MovieSearchResponse( - title, - href, - this.name, - TvType.Movie, - poster, - year, - ) + val allresultshome = + results.apmap { article -> // avec mapnotnull si un élément est null, il sera automatiquement enlevé de la liste + article.toSearchResponse() } - } + return allresultshome } override suspend fun load(url: String): LoadResponse { @@ -58,45 +35,45 @@ class FrenchStreamProvider : MainAPI() { val isMovie = !title.contains("saison", ignoreCase = true) val description = soup.selectFirst("div.fdesc")!!.text().toString() - .split("streaming", ignoreCase = true)[1].replace(" : ", "") - var poster = fixUrlNull(soup.selectFirst("div.fposter > img")?.attr("src")) + .split("streaming", ignoreCase = true)[1].replace(":", "") + var poster = soup.selectFirst("div.fposter > img")?.attr("src") val listEpisode = soup.select("div.elink") + val tags = soup.select("ul.flist-col > li").getOrNull(1) + //val rating = soup.select("span[id^=vote-num-id]")?.getOrNull(1)?.text()?.toInt() if (isMovie) { - val tags = soup.select("ul.flist-col > li").getOrNull(1) + val yearRegex = Regex("""ate de sortie\: (\d*)""") + val year = yearRegex.find(soup.text())?.groupValues?.get(1) val tagsList = tags?.select("a") ?.mapNotNull { // all the tags like action, thriller ...; unused variable it?.text() } return newMovieLoadResponse(title, url, TvType.Movie, url) { this.posterUrl = poster - addRating(soup.select("div.fr-count > div").text()) - this.year = soup.select("ul.flist-col > li").getOrNull(2)?.text()?.toIntOrNull() + this.year = year?.toIntOrNull() this.tags = tagsList this.plot = description - addTrailer(soup.selectFirst("div.fleft > span > a")?.attr("href")) + //this.rating = rating + addTrailer(soup.selectFirst("button#myBtn > a")?.attr("href")) } } else // a tv serie { - //println(listEpisode) - //println("listeEpisode:") + val episodeList = if (" val epNum = a.text().split("Episode")[1].trim().toIntOrNull() val epTitle = if (a.text().contains("Episode")) { val type = if ("honey" in a.attr("id")) { "VF" } else { - "VOSTFR" + "Vostfr" } - "Episode " + epNum?.toString() + " en " + type + "Episode " + type } else { a.text() } @@ -112,17 +89,24 @@ class FrenchStreamProvider : MainAPI() { null // episode date ) } - return TvSeriesLoadResponse( + + // val tagsList = tags?.text()?.replace("Genre :","") + val yearRegex = Regex("""Titre .* \/ (\d*)""") + val year = yearRegex.find(soup.text())?.groupValues?.get(1) + return newTvSeriesLoadResponse( title, url, - this.name, TvType.TvSeries, episodes, - poster, - null, - description, - ShowStatus.Ongoing, - ) + ) { + this.posterUrl = poster + this.plot = description + this.year = year?.toInt() + //this.rating = rating + //this.showStatus = ShowStatus.Ongoing + //this.tags = tagsList + addTrailer(soup.selectFirst("button#myBtn > a")?.attr("href")) + } } } @@ -219,10 +203,27 @@ class FrenchStreamProvider : MainAPI() { servers.apmap { for (extractor in extractorApis) { - if (it.first.contains(extractor.name, ignoreCase = true)) { - // val name = it.first - // print("true for $name") - extractor.getSafeUrl(it.second, it.second, subtitleCallback, callback) + var playerName = it.first + + if (playerName.contains("Stream.B")) { + playerName = it.first.replace("Stream.B", "StreamSB") + } + if (it.second.contains("streamlare")) { + playerName = "Streamlare" + } + if (playerName.contains(extractor.name, ignoreCase = true)) { + val header = app.get( + "https" + it.second.split("https").get(1), + allowRedirects = false + ).headers + val urlplayer = it.second + var playerUrl = when (!urlplayer.isNullOrEmpty()) { + urlplayer.contains("opsktp.com") -> header.get("location") + .toString() // case where there is redirection to opsktp + + else -> it.second + } + extractor.getSafeUrl(playerUrl, playerUrl, subtitleCallback, callback) break } } @@ -232,42 +233,71 @@ class FrenchStreamProvider : MainAPI() { } - override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse? { - val document = app.get(mainUrl).document - val docs = document.select("div.sect") - val returnList = docs.mapNotNull { - val epList = it.selectFirst("> div.sect-c.floats.clearfix") ?: return@mapNotNull null - val title = - it.selectFirst("> div.sect-t.fx-row.icon-r > div.st-left > a.st-capt")!!.text() - val list = epList.select("> div.short") - val isMovieType = title.contains("Films") // if truen type is Movie - val currentList = list.map { head -> - val hrefItem = head.selectFirst("> div.short-in.nl > a") - val href = fixUrl(hrefItem!!.attr("href")) - val img = hrefItem.selectFirst("> img") - val posterUrl = img!!.attr("src") - val name = img.attr("> div.short-title").toString() - return@map if (isMovieType) MovieSearchResponse( - name, - href, - this.name, - TvType.Movie, - posterUrl, - null - ) else TvSeriesSearchResponse( - name, - href, - this.name, - TvType.TvSeries, - posterUrl, - null, null - ) - } - if (currentList.isNotEmpty()) { - HomePageList(title, currentList) - } else null + private fun Element.toSearchResponse(): SearchResponse { + + val posterUrl = fixUrl(select("a.short-poster > img").attr("src")) + val qualityExtracted = select("span.film-ripz > a").text() + val type = select("span.mli-eps").text() + val title = select("div.short-title").text() + val link = select("a.short-poster").attr("href").replace("wvw.", "") //wvw is an issue + var quality = when (!qualityExtracted.isNullOrBlank()) { + qualityExtracted.contains("HDLight") -> getQualityFromString("HD") + qualityExtracted.contains("Bdrip") -> getQualityFromString("BlueRay") + qualityExtracted.contains("DVD") -> getQualityFromString("DVD") + qualityExtracted.contains("CAM") -> getQualityFromString("Cam") + + else -> null + } + + if (type.contains("Eps", false)) { + return MovieSearchResponse( + name = title, + url = link, + apiName = title, + type = TvType.Movie, + posterUrl = posterUrl, + quality = quality + + ) + + + } else // an Serie + { + + return TvSeriesSearchResponse( + name = title, + url = link, + apiName = title, + type = TvType.TvSeries, + posterUrl = posterUrl, + quality = quality, + // + ) + } - if (returnList.isEmpty()) return null - return HomePageResponse(returnList) } + + override val mainPage = mainPageOf( + Pair("$mainUrl/xfsearch/version-film/page/", "Derniers films"), + Pair("$mainUrl/xfsearch/version-serie/page/", "Derniers séries"), + Pair("$mainUrl/film/arts-martiaux/page/", "Films za m'ringué (Arts martiaux)"), + Pair("$mainUrl/film/action/page/", "Films Actions"), + Pair("$mainUrl/film/romance/page/", "Films za malomo (Romance)"), + Pair("$mainUrl/serie/aventure-serie/page/", "Série aventure"), + Pair("$mainUrl/film/documentaire/page/", "Documentaire") + + ) + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + val url = request.data + page + val document = app.get(url).document + val movies = document.select("div#dle-content > div.short") + + val home = + movies.map { article -> // avec mapnotnull si un élément est null, il sera automatiquement enlevé de la liste + article.toSearchResponse() + } + return newHomePageResponse(request.name, home) + } + } diff --git a/FrenchStreamProvider/src/main/kotlin/com/lagradost/FrenchStreamProviderPlugin.kt b/FrenchStreamProvider/src/main/kotlin/com/lagradost/FrenchStreamProviderPlugin.kt index f111fc9..ac71bb4 100644 --- a/FrenchStreamProvider/src/main/kotlin/com/lagradost/FrenchStreamProviderPlugin.kt +++ b/FrenchStreamProvider/src/main/kotlin/com/lagradost/FrenchStreamProviderPlugin.kt @@ -10,5 +10,6 @@ class FrenchStreamProviderPlugin: Plugin() { override fun load(context: Context) { // All providers should be added in this manner. Please don't edit the providers list directly. registerMainAPI(FrenchStreamProvider()) + registerExtractorAPI(VidoExtractor()) } } \ No newline at end of file diff --git a/FrenchStreamProvider/src/main/kotlin/com/lagradost/VidoExtractor.kt b/FrenchStreamProvider/src/main/kotlin/com/lagradost/VidoExtractor.kt new file mode 100644 index 0000000..32f5517 --- /dev/null +++ b/FrenchStreamProvider/src/main/kotlin/com/lagradost/VidoExtractor.kt @@ -0,0 +1,47 @@ +package com.lagradost + +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.getAndUnpack + +class VidoExtractor : ExtractorApi() { + override var name = "Vido" + override var mainUrl = "https://vido.lol" + private val srcRegex = Regex("""layer\(\{sources\:\["(.*)"\]""") + override val requiresReferer = true + + override suspend fun getUrl(url: String, referer: String?): List? { + val methode = if (url.contains("embed")) { + app.get(url) // french stream + } else { + val code = url.substringAfterLast("/") + val data = mapOf( + "op" to "embed", + "file_code" to code, + "&auto" to "1" + + ) + app.post("https://vido.lol/dl", referer = url, data = data) // wiflix + } + with(methode) { + getAndUnpack(this.text).let { unpackedText -> + //val quality = unpackedText.lowercase().substringAfter(" height=").substringBefore(" ").toIntOrNull() + srcRegex.find(unpackedText)?.groupValues?.get(1)?.let { link -> + return listOf( + ExtractorLink( + name, + name, + link, + url, + Qualities.Unknown.value, + true, + ) + ) + } + } + } + return null + } +} \ No newline at end of file diff --git a/NekosamaProvider/build.gradle.kts b/NekosamaProvider/build.gradle.kts new file mode 100644 index 0000000..1e0cc3f --- /dev/null +++ b/NekosamaProvider/build.gradle.kts @@ -0,0 +1,26 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "fr" + // All of these properties are optional, you can safely remove them + + description = " Ce site fait son entrée dans la catégorie des meilleurs sites animes Français. Il est très fiable car quasiment tous ses liens vidéos marchent. Il propose des animes en « VF » version française et en « VOSTFR » version originale Sous-titrée en Français." + authors = listOf("Eddy") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "Anime", + "AnimeMovie", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=neko-sama.fr&sz=%size%" +} \ No newline at end of file diff --git a/NekosamaProvider/src/main/AndroidManifest.xml b/NekosamaProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/NekosamaProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/NekosamaProvider/src/main/kotlin/com/lagradost/NekosamaProvider.kt b/NekosamaProvider/src/main/kotlin/com/lagradost/NekosamaProvider.kt new file mode 100644 index 0000000..8905e3e --- /dev/null +++ b/NekosamaProvider/src/main/kotlin/com/lagradost/NekosamaProvider.kt @@ -0,0 +1,443 @@ +package com.lagradost + + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppUtils.toJson + +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import org.jsoup.nodes.Element + +import me.xdrop.fuzzywuzzy.FuzzySearch +import java.util.* +import kotlin.collections.ArrayList + +class NekosamaProvider : MainAPI() { + override var mainUrl = "https://neko-sama.fr" + override var name = "Neko-sama" + override val hasQuickSearch = false // recherche rapide (optionel, pas vraimet utile) + override val hasMainPage = true // page d'accueil (optionel mais encoragé) + override var lang = "fr" // fournisseur est en francais + override val supportedTypes = + setOf(TvType.Anime, TvType.AnimeMovie, TvType.OVA) // animes, animesfilms + + private val nCharQuery = 10 // take the lenght of the query + nCharQuery + private val resultsSearchNbr = 50 // take only n results from search function + + + data class EpisodeData( + @JsonProperty("id") val id: Int, + @JsonProperty("title") val title: String?, + @JsonProperty("title_english") val title_english: String?, + @JsonProperty("title_romanji") val title_romanji: String?, + @JsonProperty("title_french") val title_french: String?, + @JsonProperty("others") val others: String?, + @JsonProperty("type") val type: String?, + @JsonProperty("status") val status: String?, + @JsonProperty("popularity") val popularity: Int?, + @JsonProperty("url") val url: String, + @JsonProperty("genre") val genre: Genre?, + @JsonProperty("url_image") val url_image: String?, + @JsonProperty("score") val score: String?, + @JsonProperty("start_date_year") val start_date_year: String?, + @JsonProperty("nb_eps") val nb_eps: String?, + + ) + + data class Genre( + @JsonProperty("0") val action: String?, + @JsonProperty("1") val adventure: String?, + @JsonProperty("2") val drama: String?, + @JsonProperty("3") val fantasy: String?, + @JsonProperty("4") val military: String?, + @JsonProperty("5") val shounen: String?, + ) + + // Looking for the best title matching from parsed Episode data + private fun EpisodeData.titleObtainedBysortByQuery(query: String?): String? { + + if (query == null) { + // No shorting so return the first title + var title = this.title + + return title + } else { + + + val titles = listOf(title, title_french, title_english, title_romanji).filterNotNull() + // Sorted by the best title matching + val titlesSorted = titles.sortedBy { it -> + -FuzzySearch.ratio( + it?.take(query.length + nCharQuery), + query + ) + } + return titlesSorted.elementAt(0) + + + } + } + + private fun List.sortByQuery(query: String?): List { + return if (query == null) { + // Return list to base state if no query + this.sortedBy { it.title } + } else { + + this.sortedBy { + val bestTitleMatching = it.titleObtainedBysortByQuery(query) + -FuzzySearch.ratio( + bestTitleMatching?.take(query.length + nCharQuery) ?: bestTitleMatching, + query + ) + } + } + } + + /** This function is done because there is two database (vf and vostfr). So it allows to sort the combined database **/ + private fun List.sortByname(query: String?): List { + return if (query == null) { + // Return list to base state if no query + this.sortedBy { it.name } + } else { + + this.sortedBy { + val name = it.name + -FuzzySearch.ratio(name.take(query.length + nCharQuery), query) + } + } + } + + /** + Cherche le site pour un titre spécifique + + La recherche retourne une SearchResponse, qui peut être des classes suivants: AnimeSearchResponse, MovieSearchResponse, TorrentSearchResponse, TvSeriesSearchResponse + Chaque classes nécessite des données différentes, mais a en commun le nom, le poster et l'url + **/ + override suspend fun search(query: String): List { + + var listofResults = ArrayList() + + listOf( + "$mainUrl/animes-search-vf.json" to "(VF) ", + "$mainUrl/animes-search-vostfr.json" to "(Vostfr) " + ).apmap {(url, version) -> + val dubStatus = when (!version.isNullOrBlank()) { + version.contains("VF") -> DubStatus.Dubbed + version.contains("Vostfr") -> DubStatus.Subbed + else -> null + } + val reponse = app.get(url).text + val ParsedData = tryParseJson>(reponse) + + ParsedData?.sortByQuery(query)?.take(resultsSearchNbr)?.forEach { it -> + val type = it.type + val mediaPoster = it.url_image + val href = fixUrl(it.url) + val bestTitleMatching = it.titleObtainedBysortByQuery(query) + val title = version + bestTitleMatching + + when (type) { + "m0v1e", "special" -> ( + listofResults.add(newMovieSearchResponse( // réponse du film qui sera ajoutée à la liste apmap qui sera ensuite return + title, + href, + TvType.AnimeMovie, + false + ) { + this.posterUrl = mediaPoster + } + )) + null, "tv", "ova", "" -> ( + listofResults.add(newAnimeSearchResponse( + title, + href, + TvType.Anime, + false + ) { + this.posterUrl = mediaPoster + this.dubStatus = EnumSet.of(dubStatus) + } + + )) + else -> { + + throw ErrorLoadingException("invalid media type") // le type n'est pas reconnu ==> affiche une erreur + } + } + } ?: throw ErrorLoadingException("ParsedData failed") + } + return listofResults.sortByname(query) + .take(resultsSearchNbr) // Do that to short the vf and vostfr anime together + + } + + /** + * charge la page d'informations, il ya toutes les donées, les épisodes, le résumé etc ... + * Il faut retourner soit: AnimeLoadResponse, MovieLoadResponse, TorrentLoadResponse, TvSeriesLoadResponse. + */ + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document // + // url est le lien retourné par la fonction search (la variable href) ou la fonction getMainPage + + val episodes = ArrayList() + var mediaType = TvType.Anime + val script = + document.select("div#main > script:first-of-type") + + val srcAllInfoEpisode = + Regex("""min\"\,\"([^\}]*)\}""") + val results = srcAllInfoEpisode.findAll(script.toString()) + //srcAllInfoEpisode.find(script.toString())?.groupValues?.get(1)? + ////////////////////////////////////// + var title = "" //document.select("div.offset-md-4 >:not(small)").text() + var dataUrl = "" + var link_video = "" + ///////////////////////////////////// + results.forEach { infoEpisode -> + val episodeScript = infoEpisode.groupValues[1] + val srcScriptEpisode = + Regex("""episode\"\:\"Ep\. ([0-9]*)\"""") + val episodeNum = srcScriptEpisode.find(episodeScript)?.groupValues?.get(1)?.toInt() + val srcScriptTitle = Regex("""title\"\:\"([^\"]*)\"\,\"url\"\:\"\\\/anime""") + var titleE = srcScriptTitle.find(episodeScript)?.groupValues?.get(1) + if (titleE != null) title = titleE + val srcScriptlink = + Regex("""\"url\"\:\"([^\"]*)\"""") // remove\ + val link = srcScriptlink.find(episodeScript)?.groupValues?.get(1) + + if (link != null) link_video = fixUrl(link.replace("\\", "")) + + val srcScriptposter = + Regex("""\"url_image\"\:\"([^\"]*)\"""") // remove\ + val poster = srcScriptposter.find(episodeScript)?.groupValues?.get(1) + var link_poster = "" + if (poster != null) link_poster = poster.replace("\\", "") + dataUrl = link_video + + + episodes.add( + Episode( + link_video, + episode = episodeNum, + name = title, + posterUrl = link_poster + + ) + ) + + } + val regexYear = Regex("""Diffusion [a-zA-Z]* (\d*)""") + val infosList = + document.selectFirst("div#anime-info-list")?.text() + val isinfosList = !infosList.isNullOrBlank() + var year:Int?=null + if (isinfosList) { + if (infosList!!.contains("movie")) mediaType = TvType.AnimeMovie + year =regexYear.find(infosList)!!.groupValues.get(1).toInt() + } + + val description = document.selectFirst("div.synopsis > p")?.text() + val poster = document.select("div.cover > img").attr("src") + + if (mediaType == TvType.AnimeMovie) { + return newMovieLoadResponse( + title, + url, + mediaType, + dataUrl + ) { // retourne les informations du film + this.posterUrl = poster + this.plot = description + this.year = year + } + } else // an anime + { + val status = when (isinfosList) { + infosList!!.contains("En cours") -> ShowStatus.Ongoing // En cours + infosList!!.contains("Terminé") -> ShowStatus.Completed + else -> null + } + return newAnimeLoadResponse( + title, + url, + mediaType, + ) { + this.posterUrl = poster + this.plot = description + addEpisodes( + DubStatus.Dubbed, + episodes + ) + this.showStatus = status + this.year = year + + } + } + } + + + /** récupere les liens .mp4 ou m3u8 directement à partir du paramètre data généré avec la fonction load()**/ + override suspend fun loadLinks( + data: String, // fournit par load() + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit, + ): Boolean { + val url = data + val document = app.get(url).document + val script = document.select("""[type^="text"]""")[1] + val srcAllvideolinks = + Regex("""\'(https:\/\/[^']*)""") + + val results = srcAllvideolinks.findAll(script.toString()) + + results.forEach { infoEpisode -> + + var playerUrl = infoEpisode.groupValues[1] + + if (!playerUrl.isNullOrBlank()) + loadExtractor( + httpsify(playerUrl), + playerUrl, + subtitleCallback + ) { link -> + callback.invoke( + ExtractorLink( + link.source, + link.name + "", + link.url, + link.referer, + getQualityFromName("HD"), + link.isM3u8, + link.headers, + link.extractorData + ) + ) + } + } + + return true + } + + private fun Element.toSearchResponse(): SearchResponse { + val poster = select("div.cover > a > div.ma-lazy-wrapper") + var posterUrl = poster.select("img:last-child").attr("src") + if (posterUrl == "#") posterUrl = poster.select("img:last-child").attr("data-src") + val type = select("div.info > p.year").text() + val title = select("div.info > a.title > div.limit").text() + val link = fixUrl(select("div.cover > a").attr("href")) + if (type.contains("Film")) { + return newMovieSearchResponse( + title, + link, + TvType.AnimeMovie, + false, + ) { + this.posterUrl = posterUrl + } + + } else // an Anime + { + return newAnimeSearchResponse( + title, + link, + TvType.Anime, + false, + ) { + this.posterUrl = posterUrl + } + } + } + + data class LastEpisodeData( + @JsonProperty("time") val time: String?, + @JsonProperty("timestamp") val timestamp: Int?, + @JsonProperty("episode") val episode: String?, + @JsonProperty("icons") val icons: String?, + @JsonProperty("title") val title: String?, + @JsonProperty("lang") val lang: String?, + @JsonProperty("url") val url: String?, + @JsonProperty("anime_url") val anime_url: String?, + @JsonProperty("url_image") val url_image: String?, + @JsonProperty("url_bg") val url_bg: String, + ) + + private fun LastEpisodeData.tomainHome(): SearchResponse { + + var posterUrl = this.url_image?.replace("""\""", "") + val link = this.anime_url?.replace("""\""", "")?.let { fixUrl(it) } + ?: throw error("Error parsing") + val title = this.title ?: throw error("Error parsing") + val type = this.episode ?: "" + var lang = this.lang + val dubStatus = if (lang?.contains("vf") == true) { + DubStatus.Dubbed + } else { + DubStatus.Subbed + } + + if (type.contains("Ep")) { + return newAnimeSearchResponse( + title.take(15).replace("\n", "") + "\n" + type.replace("Ep", "Episode"), + link, + TvType.Anime, + false, + ) { + this.posterUrl = posterUrl + this.dubStatus = EnumSet.of(dubStatus) + + } + + } else // a movie + { + return newMovieSearchResponse( + title, + link, + TvType.AnimeMovie, + false, + ) { + this.posterUrl = posterUrl + } + } + } + + override val mainPage = mainPageOf( + Pair("$mainUrl", "Nouveaux épisodes"), + Pair("$mainUrl/anime-vf/", "Animes et Films en version français"), + Pair("$mainUrl/anime/", "Animes et Films sous-titrés en français"), + ) + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + val categoryName = request.name + var cssSelector = "" + if (categoryName.contains("Nouveaux") && page <= 1) { + cssSelector = "div#main >script"//"div.js-last-episode-container > div.col-lg-3" + } + val url: String + url = if (page == 1) { + request.data + } else { + request.data + page + } + val document = app.get(url).document + + val regexLastEpisode = Regex("""lastEpisodes = (.*)\;""") + val home = when (!categoryName.isNullOrBlank()) { + request.name.contains("Animes") -> document.select("div#regular-list-animes > div.anime") + .mapNotNull { article -> article.toSearchResponse() } + else -> + tryParseJson>( + document.selectFirst( + cssSelector + )?.let { + regexLastEpisode.find( + it.toString() + )?.groupValues?.get(1) + } + )!!.map { episode -> episode.tomainHome() } + } + return newHomePageResponse(request.name, home) + } +} \ No newline at end of file diff --git a/NekosamaProvider/src/main/kotlin/com/lagradost/NekosamaProviderPlugin.kt b/NekosamaProvider/src/main/kotlin/com/lagradost/NekosamaProviderPlugin.kt new file mode 100644 index 0000000..a94d27a --- /dev/null +++ b/NekosamaProvider/src/main/kotlin/com/lagradost/NekosamaProviderPlugin.kt @@ -0,0 +1,17 @@ + +package com.lagradost + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class NekosamaPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(NekosamaProvider()) + registerExtractorAPI(PstreamExtractor()) + + + } +} \ No newline at end of file diff --git a/NekosamaProvider/src/main/kotlin/com/lagradost/PstreamExtractor.kt b/NekosamaProvider/src/main/kotlin/com/lagradost/PstreamExtractor.kt new file mode 100644 index 0000000..ad93b03 --- /dev/null +++ b/NekosamaProvider/src/main/kotlin/com/lagradost/PstreamExtractor.kt @@ -0,0 +1,53 @@ +package com.lagradost + +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.app + +import okio.ByteString.Companion.decodeBase64 + +open class PstreamExtractor : ExtractorApi() { + override val name: String = "Pstream" + override val mainUrl: String = "https://www.pstream.net" + override val requiresReferer = true + + override suspend fun getUrl(url: String, referer: String?): List? { + val refer = url + val headers = mapOf( + "Accept" to "*/*", + "Accept-Language" to "en-US,en;q=0.5", + ) + val document = app.get(url, headers = headers).document + + val scriptsourceUrl = + document.select("""script[src^="https://www.pstream.net/u/player-script?"]""") + .attr("src")//** Get the url where the scritp function is **/ + + val Scripdocument = + app.get(scriptsourceUrl, headers = headers).document//** Open the scritp function **/ + + val base64CodeRegex = + Regex("""e\.parseJSON\(atob\(t\)\.slice\(2\)\)\}\(\"(.*)\=\="\)\,n\=\"""") //** Search the code64 **/ + val code64 = base64CodeRegex.find(Scripdocument.toString())?.groupValues?.get(1) + + val decoded = code64?.decodeBase64()?.utf8() //** decode the code64 **/ + + val regexLink = Regex("""\"(https:\\\/\\\/[^"]*)""") //** Extract the m3u8 link **/ + val m3u8found = regexLink.find(decoded.toString())?.groupValues?.get(1) + var m3u8 = m3u8found.toString().replace("""\""", "") + + return listOf( + ExtractorLink( + name, + name, + m3u8, + refer, // voir si site demande le referer à mettre ici + Qualities.Unknown.value, + true, + headers = headers + + ) + ) + + } +} + diff --git a/README.md b/README.md index a377ccc..f207cae 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Cloudstream Non-English Plugin Repository +# Cloudstream Non-English Plugin Repository All available repositories: https://recloudstream.github.io/repos/ diff --git a/VostfreeProvider/build.gradle.kts b/VostfreeProvider/build.gradle.kts new file mode 100644 index 0000000..9e936a5 --- /dev/null +++ b/VostfreeProvider/build.gradle.kts @@ -0,0 +1,26 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "fr" + // All of these properties are optional, you can safely remove them + + description = " Ce site est certainement l’un des meilleurs sites permettant de regarder des animes en ligne et gratuitement. Il vous propose la version « VF » version française et la « VOSTFR » version originale Sous-titrée en Français." + authors = listOf("Eddy") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "Anime", + "AnimeMovie", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=vostfree.cx&sz=%size%" +} \ No newline at end of file diff --git a/VostfreeProvider/src/main/AndroidManifest.xml b/VostfreeProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/VostfreeProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/VostfreeProvider/src/main/kotlin/com/lagradost/MytvExtractor.kt b/VostfreeProvider/src/main/kotlin/com/lagradost/MytvExtractor.kt new file mode 100644 index 0000000..66c5d21 --- /dev/null +++ b/VostfreeProvider/src/main/kotlin/com/lagradost/MytvExtractor.kt @@ -0,0 +1,42 @@ +package com.lagradost +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.app +import org.jsoup.Jsoup + + +open class MytvExtractor : ExtractorApi() { + override val name: String = "Mytv" + override val mainUrl: String = "https://www.myvi.tv/" + private val srcRegex = + Regex("""PlayerLoader\.CreatePlayer\(\"v\=(.*)\\u0026tp""") // would be possible to use the parse and find src attribute + override val requiresReferer = false + + + override suspend fun getUrl(url: String, referer: String?): List? { + val cleaned_url = url + val html = app.get(cleaned_url) + with(html) { // raised error ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED (3003) is due to the response: "error_nofile" + srcRegex.find(this.text)?.groupValues?.get(1)?.let { link -> + var lien = link + lien = lien.replace("%2f", "/").replace("%3a", ":").replace("%3f", "?") + .replace("%3d", "=").replace("%26", "&") + + //val html = app.get(url).text + //val document = Jsoup.parse(html) + //val link1 = document.select("script") + return listOf( + ExtractorLink( + name, + name, + lien, + cleaned_url, // voir si site demande le referer à mettre ici + Qualities.Unknown.value, + ) + ) + } + } + + return null + + } +} diff --git a/VostfreeProvider/src/main/kotlin/com/lagradost/SibnetExtractor.kt b/VostfreeProvider/src/main/kotlin/com/lagradost/SibnetExtractor.kt new file mode 100644 index 0000000..46bdccb --- /dev/null +++ b/VostfreeProvider/src/main/kotlin/com/lagradost/SibnetExtractor.kt @@ -0,0 +1,35 @@ + +package com.lagradost +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.app +import org.jsoup.Jsoup + + +open class SibnetExtractor : ExtractorApi() { + override val name: String = "Sibnet" + override val mainUrl: String = "https://video.sibnet.ru" + private val srcRegex = + Regex("""player\.src\(\[\{src: \"(.*?)\"""") // would be possible to use the parse and find src attribute + override val requiresReferer = true + + + override suspend fun getUrl(url: String, referer: String?): List? { + val cleaned_url = url + val html = app.get(cleaned_url) + with(html) { // raised error ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED (3003) is due to the response: "error_nofile" + srcRegex.find(this.text)?.groupValues?.get(1)?.let { link -> + return listOf( + ExtractorLink( + name, + name, + mainUrl + link, + cleaned_url, // voir si site demande le referer à mettre ici + Qualities.Unknown.value, + ) + ) + } + } + + return null + } +} diff --git a/VostfreeProvider/src/main/kotlin/com/lagradost/VostfreeProvider.kt b/VostfreeProvider/src/main/kotlin/com/lagradost/VostfreeProvider.kt new file mode 100644 index 0000000..b41e261 --- /dev/null +++ b/VostfreeProvider/src/main/kotlin/com/lagradost/VostfreeProvider.kt @@ -0,0 +1,368 @@ +package com.lagradost + + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import org.jsoup.nodes.Element +import java.util.* +import kotlin.collections.ArrayList + + +class VostfreeProvider : MainAPI() { + // VostFreeProvider() est ajouté à la liste allProviders dans MainAPI.kt + override var mainUrl = "https://vostfree.cx" + override var name = "Vostfree" + override val hasQuickSearch = false // recherche rapide (optionel, pas vraimet utile) + override val hasMainPage = true // page d'accueil (optionel mais encoragé) + override var lang = "fr" // fournisseur est en francais + override val supportedTypes = + setOf(TvType.Anime, TvType.AnimeMovie, TvType.OVA) // animes, animesfilms + // liste des types: https://recloudstream.github.io/dokka/app/com.lagradost.cloudstream3/-tv-type/index.html + + /** + Cherche le site pour un titre spécifique + + La recherche retourne une SearchResponse, qui peut être des classes suivants: AnimeSearchResponse, MovieSearchResponse, TorrentSearchResponse, TvSeriesSearchResponse + Chaque classes nécessite des données différentes, mais a en commun le nom, le poster et l'url + **/ + override suspend fun search(query: String): List { + val link = + "$mainUrl/index.php?do=search&subaction=search&story=$query&submit=Submit+Query" // L'url pour chercher un anime de dragon sera donc: 'https://vostfree.cx/index.php?story=dragon&do=search&subaction=search' + var mediaType = TvType.Anime + val document = + app.post(link).document // app.get() permet de télécharger la page html avec une requete HTTP (get) + return document.select("div.search-result") // on séléctione tous les éléments 'enfant' du type articles + .mapNotNull { div -> // map crée une liste des éléments (ici newMovieSearchResponse et newAnimeSearchResponse) + val type = + div?.selectFirst("div.genre") + ?.text() // replace enlève tous les '\t' et '\n' du titre + val mediaPoster = + div?.selectFirst("span.image > img")?.attr("src") + ?.let { fixUrl(it) } // récupère le texte de l'attribut src de l'élément + val href = div?.selectFirst("div.info > div.title > a")?.attr("href") + ?: throw ErrorLoadingException("invalid link") // renvoie une erreur si il n'y a pas de lien vers le média + val title = div.selectFirst("> div.info > div.title > a")?.text().toString() + val version = div.selectFirst("> div.info > ul > li")?.text().toString() + if (type == "OAV") mediaType = TvType.OVA + when (type) { + "FILM" -> ( + newMovieSearchResponse( // réponse du film qui sera ajoutée à la liste map qui sera ensuite return + title, + href, + TvType.AnimeMovie, + false + ) { + this.posterUrl = mediaPoster + // this.rating = rating + } + ) + null, "OAV" -> ( + newAnimeSearchResponse( + title, + href, + mediaType, + false + ) { + this.posterUrl = mediaPoster + this.dubStatus = + if (version.contains("VF")) EnumSet.of(DubStatus.Dubbed) else EnumSet.of( + DubStatus.Subbed + ) + // this.rating = rating + } + + + ) + else -> { + throw ErrorLoadingException("invalid media type") // le type n'est pas reconnu ==> affiche une erreur + } + } + } + } + + /** + * charge la page d'informations, il ya toutes les donées, les épisodes, le résumé etc ... + * Il faut retourner soit: AnimeLoadResponse, MovieLoadResponse, TorrentLoadResponse, TvSeriesLoadResponse. + */ + data class EpisodeData( + @JsonProperty("url") val url: String, + @JsonProperty("episodeNumber") val episodeNumber: String, + ) + + + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document // récupere le texte sur la page (requète http) + // url est le lien retourné par la fonction search (la variable href) ou la fonction getMainPage + var mediaType = TvType.Anime + val episodes = ArrayList() + val urlSaison = ArrayList() + val meta = + document.selectFirst("div#dle-content > div.watch-top > div.image-bg > div.image-bg-content > div.slide-block ") + val description = meta?.select("div.slide-middle > div.slide-desc")?.first() + ?.text() // first() selectione le premier élément de la liste + var title = meta?.select("div.slide-middle > h1")?.text() + ?: "Invalid title" + title = title.replace("Saison", "").replace("saison", "").replace("SAISON", "") + .replace("Season", "").replace("season", "").replace("SEASON", "") + val poster = fixUrl( + meta?.select(" div.slide-poster > img") + ?.attr("src")!! + )// récupere le texte de l'attribut 'data-src' + var year = document.select("div.slide-info > p > b > a")?.text()?.toInt() + + urlSaison.add(url) + + + var seasonNumber: Int? = null + val otherSaisonFound = document.select("div.new_player_series_count > a") + otherSaisonFound.forEach { + urlSaison.add(it.attr("href")) + } + + urlSaison.apmap { urlseason -> + val document = + app.get(urlseason).document // récupere le texte sur la page (requète http) + + val meta = + document.selectFirst("div#dle-content > div.watch-top > div.image-bg > div.image-bg-content > div.slide-block ") + val poster_saison = mainUrl + meta?.select(" div.slide-poster > img") + ?.attr("src") + val seasontext = meta?.select("ul.slide-top > li:last-child > b:last-child")?.text() + var indication: String? = null + + if (!seasontext.isNullOrBlank() && !seasontext.contains("""([a-zA-Z])""".toRegex())) { + seasonNumber = seasontext.toInt() + + if (seasonNumber!! < 1) { // seem a an OVA has 0 as season number + seasonNumber = 1000 + indication = "Vous regardez un OVA" + } + } + + document.select(" select.new_player_selector > option").forEach { + val typeOftheAnime = it.text() + + if (typeOftheAnime != "Film") { + mediaType = TvType.Anime + val link = + EpisodeData( + urlseason, + typeOftheAnime.replace("Episode ", ""), + ).toJson() + episodes.add( + Episode( + link, + episode = typeOftheAnime.replace("Episode ", "").toInt(), + season = seasonNumber, + name = typeOftheAnime, + description = indication, + posterUrl = poster_saison + ) + ) + } else { + + mediaType = TvType.AnimeMovie + } + } + } + + if (mediaType == TvType.AnimeMovie) { + return newMovieLoadResponse( + title, + url, + mediaType, + url + ) { // retourne les informations du film + this.posterUrl = poster + this.plot = description + this.year = year + } + } else // an anime + { + return newAnimeLoadResponse( + title, + url, + mediaType, + ) { + this.posterUrl = poster + this.plot = description + this.year = year + addEpisodes( + if (title.contains("VF")) DubStatus.Dubbed else DubStatus.Subbed, + episodes + ) + + } + } + } + + + // récupere les liens .mp4 ou m3u8 directement à partir du paramètre data généré avec la fonction load() + override suspend fun loadLinks( + data: String, // fournit par load() + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit, + ): Boolean { + val parsedInfo = tryParseJson(data) + val url = parsedInfo?.url ?: data + + val noMovie = "1" + val numeroEpisode = parsedInfo?.episodeNumber + ?: noMovie // if is not a movie then take the episode number else for movie it is 1 + + val document = app.get(url).document + document.select("div.new_player_bottom") + .forEach { player_bottom -> // séléctione tous les players + + // supprimer les zéro de 0015 pour obtenir l'episode 15 + var index = numeroEpisode.indexOf('0') + var numero = numeroEpisode + while (index == 0) { + numero = numeroEpisode.drop(1) + index = numero.indexOf('0') + } + + val cssQuery = " div#buttons_$numero" // numero épisode + val buttonsNepisode = player_bottom?.select(cssQuery) + ?: throw ErrorLoadingException("Non player") //séléctione tous les players pour l'episode NoEpisode + buttonsNepisode.select("> div").apmap { + val player = it.attr("id") + .toString() //prend tous les players resultat : "player_2140" et "player_6521" + val playerName = it.select("div#$player") + .text() // prend le nom du player ex : "Uqload" et "Sibnet" + val codePlayload = + document.selectFirst("div#content_$player")?.text() + .toString() // result : "325544" ou "https:..." + var playerUrl = when (playerName) { + "VIP", "Upvid", "Dstream", "Streamsb", "Vudeo", "NinjaS", "Upstream" -> codePlayload // case https + "Uqload" -> "https://uqload.com/embed-$codePlayload.html" + "Mytv" -> "https://www.myvi.tv/embed/$codePlayload" + "Sibnet" -> "https://video.sibnet.ru/shell.php?videoid=$codePlayload" + "Stream" -> "https://myvi.ru/player/embed/html/$codePlayload" + else -> return@apmap + } + + + loadExtractor( + httpsify(playerUrl), + playerUrl, + subtitleCallback + ) { link -> // charge un extracteur d'extraire le lien direct .mp4 + callback.invoke( + ExtractorLink( // ici je modifie le callback pour ajouter des informations, normalement ce n'est pas nécessaire + link.source, + link.name + "", + link.url, + link.referer, + getQualityFromName("HD"), + link.isM3u8, + link.headers, + link.extractorData + ) + ) + } + // } + + } + + } + return true + } + + private fun Element.toSearchResponse(): SearchResponse { + val poster = select("span.image") + val posterUrl = fixUrl(poster.select("> img").attr("src")) + val subdub = select("div.quality").text() + val genre = select("div.genre").text() + val title = select("div.info > div.title").text() + val link = select("div.play > a").attr("href") + if (genre == "FILM") { + return newMovieSearchResponse( + title, + link, + TvType.AnimeMovie, + false, + ) { + this.posterUrl = posterUrl +//this.quality = quality + } + + } else // an Anime + { + return newAnimeSearchResponse( + title, + link, + TvType.Anime, + false, + ) { + this.posterUrl = posterUrl + this.dubStatus = + if (subdub == "VF") EnumSet.of(DubStatus.Dubbed) else EnumSet.of(DubStatus.Subbed) + } + } + } + + private fun Element.toSearchResponse1(): SearchResponse { + val poster = select("span.image") + val posterUrl = fixUrl(poster.select("> img").attr("src")) + val subdub = select("div.quality").text() + //val genre = select("div.info > ul.additional > li").text() + val title = select("div.info > div.title").text() + val link = select(" div.info > div.title > a").attr("href") + + return newAnimeSearchResponse( + title, + link, + TvType.Anime, + false, + ) { + this.posterUrl = posterUrl + this.dubStatus = + if (subdub == "VF") EnumSet.of(DubStatus.Dubbed) else EnumSet.of(DubStatus.Subbed) + } + + } + + override val mainPage = mainPageOf( + Pair("$mainUrl/last-episode.html/page/", "Nouveaux épisodes en Vostfr"), + Pair( + "$mainUrl/animes-vostfr-recement-ajoutees.html/page/", + "Animes Vostfr récemment ajoutés" + ), + Pair("$mainUrl/last-episode-vf.html/page/", "Nouveaux épisodes en français"), + Pair("$mainUrl/last-anime-vf.html/page/", "Animes VF récemment ajoutés"), + Pair("$mainUrl/animes-vf/page/", "Animes en version français"), + Pair("$mainUrl/animes-vostfr/page/", "Animes sous-titrés en français"), + Pair("$mainUrl/films-vf-vostfr/page/", "Films en Fr et Vostfr") + ) + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + val categoryName = request.name + var cssSelector = "" + if (categoryName.contains("récemment")) { + cssSelector = "div#content > div.movie-poster" + } else { + cssSelector = "div#content > div#dle-content > div.movie-poster" + } + val url = request.data + page + val document = app.get(url).document + + val home = + when (!categoryName.isNullOrBlank()) { + request.name.contains("Nouveaux") -> document.select("div#content > div.last-episode") + .mapNotNull { article -> article.toSearchResponse1() } + else -> + document.select(cssSelector) + .mapNotNull { article -> // avec mapnotnull si un élément est null, il sera automatiquement enlevé de la liste + article.toSearchResponse() + } + } + return newHomePageResponse(request.name, home) + } + + +} \ No newline at end of file diff --git a/VostfreeProvider/src/main/kotlin/com/lagradost/VostfreeProviderPlugin.kt b/VostfreeProvider/src/main/kotlin/com/lagradost/VostfreeProviderPlugin.kt new file mode 100644 index 0000000..8083b33 --- /dev/null +++ b/VostfreeProvider/src/main/kotlin/com/lagradost/VostfreeProviderPlugin.kt @@ -0,0 +1,18 @@ + +package com.lagradost + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class VostfreePlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(VostfreeProvider()) + registerExtractorAPI(VudeoExtractor()) + registerExtractorAPI(SibnetExtractor()) + registerExtractorAPI(MytvExtractor()) + + } +} \ No newline at end of file diff --git a/VostfreeProvider/src/main/kotlin/com/lagradost/VudeoExtractor.kt b/VostfreeProvider/src/main/kotlin/com/lagradost/VudeoExtractor.kt new file mode 100644 index 0000000..a0d4e54 --- /dev/null +++ b/VostfreeProvider/src/main/kotlin/com/lagradost/VudeoExtractor.kt @@ -0,0 +1,34 @@ + +package com.lagradost +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.app + + +open class VudeoExtractor : ExtractorApi() { + override val name: String = "Vudeo" + override val mainUrl: String = "https://vudeo.io/" + private val srcRegex = + Regex("""sources\: \[\"(.*)\"""") // would be possible to use the parse and find src attribute + override val requiresReferer = false + + + override suspend fun getUrl(url: String, referer: String?): List? { + val cleaned_url = url + with(app.get(cleaned_url)) { // raised error ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED (3003) is due to the response: "error_nofile" + srcRegex.find(this.text)?.groupValues?.get(1)?.let { link -> + return listOf( + ExtractorLink( + name, + name, + link, + cleaned_url, // voir si site demande le referer à mettre ici + Qualities.Unknown.value, + ) + ) + } + } + return null + } +} + + diff --git a/WiflixProvider/build.gradle.kts b/WiflixProvider/build.gradle.kts new file mode 100644 index 0000000..096b9ae --- /dev/null +++ b/WiflixProvider/build.gradle.kts @@ -0,0 +1,26 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "fr" + // All of these properties are optional, you can safely remove them + + description = "WIFLIX, le site grâce auquel vous allez pouvoir regarder vos films et séries préférées" + authors = listOf("Eddy") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "TvSeries", + "Movie", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=wiflix.zone&sz=%size%" +} \ No newline at end of file diff --git a/WiflixProvider/src/main/AndroidManifest.xml b/WiflixProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/WiflixProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/WiflixProvider/src/main/kotlin/com/lagradost/DoodStreamExtractor.kt b/WiflixProvider/src/main/kotlin/com/lagradost/DoodStreamExtractor.kt new file mode 100644 index 0000000..06b0277 --- /dev/null +++ b/WiflixProvider/src/main/kotlin/com/lagradost/DoodStreamExtractor.kt @@ -0,0 +1,35 @@ +package com.lagradost +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.getQualityFromName + + + +open class DoodStreamExtractor : ExtractorApi() { + override var name = "DoodStream" + override var mainUrl = "https://doodstream.com" + override val requiresReferer = false + + override fun getExtractorUrl(id: String): String { + return "$mainUrl/d/$id" + } + + override suspend fun getUrl(url: String, referer: String?): List? { + val response0 = app.get(url).text // html of DoodStream page to look for /pass_md5/... + val md5 =mainUrl+(Regex("/pass_md5/[^']*").find(response0)?.value ?: return null) // get https://dood.ws/pass_md5/... + val trueUrl = app.get(md5, referer = url).text + "zUEJeL3mUN?token=" + md5.substringAfterLast("/") //direct link to extract (zUEJeL3mUN is random) + val quality = Regex("\\d{3,4}p").find(response0.substringAfter("").substringBefore(""))?.groupValues?.get(0) + return listOf( + ExtractorLink( + trueUrl, + this.name, + trueUrl, + mainUrl, + getQualityFromName(quality), + false + ) + ) // links are valid in 8h + + } +} \ No newline at end of file diff --git a/WiflixProvider/src/main/kotlin/com/lagradost/StreamSBPlusExtractor.kt b/WiflixProvider/src/main/kotlin/com/lagradost/StreamSBPlusExtractor.kt new file mode 100644 index 0000000..6db5221 --- /dev/null +++ b/WiflixProvider/src/main/kotlin/com/lagradost/StreamSBPlusExtractor.kt @@ -0,0 +1,81 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.M3u8Helper + + +// This is a modified version of https://github.com/jmir1/aniyomi-extensions/blob/master/src/en/genoanime/src/eu/kanade/tachiyomi/animeextension/en/genoanime/extractors/StreamSBExtractor.kt +// The following code is under the Apache License 2.0 https://github.com/jmir1/aniyomi-extensions/blob/master/LICENSE +open class StreamSBPlusExtractor : ExtractorApi() { + override var name = "StreamSB" + override var mainUrl = "https://sbspeed.com" + override val requiresReferer = false + + private val hexArray = "0123456789ABCDEF".toCharArray() + + private fun bytesToHex(bytes: ByteArray): String { + val hexChars = CharArray(bytes.size * 2) + for (j in bytes.indices) { + val v = bytes[j].toInt() and 0xFF + + hexChars[j * 2] = hexArray[v ushr 4] + hexChars[j * 2 + 1] = hexArray[v and 0x0F] + } + return String(hexChars) + } + + data class Subs ( + @JsonProperty("file") val file: String, + @JsonProperty("label") val label: String, + ) + + data class StreamData ( + @JsonProperty("file") val file: String, + @JsonProperty("cdn_img") val cdnImg: String, + @JsonProperty("hash") val hash: String, + @JsonProperty("subs") val subs: List?, + @JsonProperty("length") val length: String, + @JsonProperty("id") val id: String, + @JsonProperty("title") val title: String, + @JsonProperty("backup") val backup: String, + ) + + data class Main ( + @JsonProperty("stream_data") val streamData: StreamData, + @JsonProperty("status_code") val statusCode: Int, + ) + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val regexID = + Regex("(embed-[a-zA-Z0-9]{0,8}[a-zA-Z0-9_-]+|/e/[a-zA-Z0-9]{0,8}[a-zA-Z0-9_-]+)") + val id = regexID.findAll(url).map { + it.value.replace(Regex("(embed-|/e/)"), "") + }.first() +// val master = "$mainUrl/sources48/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362" + val master = "$mainUrl/sources48/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/" + val headers = mapOf( + "watchsb" to "sbstream", + ) + val mapped = app.get( + master.lowercase(), + headers = headers, + referer = url, + ).parsedSafe
() + // val urlmain = mapped.streamData.file.substringBefore("/hls/") + M3u8Helper.generateM3u8( + name, + mapped?.streamData?.file ?: return, + url, + headers = headers + ).forEach(callback) + } +} \ No newline at end of file diff --git a/WiflixProvider/src/main/kotlin/com/lagradost/WiflixProvider.kt b/WiflixProvider/src/main/kotlin/com/lagradost/WiflixProvider.kt new file mode 100644 index 0000000..77a55d7 --- /dev/null +++ b/WiflixProvider/src/main/kotlin/com/lagradost/WiflixProvider.kt @@ -0,0 +1,307 @@ +package com.lagradost + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import org.jsoup.nodes.Element +import org.jsoup.select.Elements +import kotlin.collections.ArrayList + +class WiflixProvider : MainAPI() { + + + override var mainUrl = "https://wiflix.zone" + override var name = "Wiflix" + override val hasQuickSearch = false // recherche rapide (optionel, pas vraimet utile) + override val hasMainPage = true // page d'accueil (optionel mais encoragé) + override var lang = "fr" // fournisseur est en francais + override val supportedTypes = + setOf(TvType.Movie, TvType.TvSeries) // series, films + // liste des types: https://recloudstream.github.io/dokka/app/com.lagradost.cloudstream3/-tv-type/index.html + + /** + Cherche le site pour un titre spécifique + + La recherche retourne une SearchResponse, qui peut être des classes suivants: AnimeSearchResponse, MovieSearchResponse, TorrentSearchResponse, TvSeriesSearchResponse + Chaque classes nécessite des données différentes, mais a en commun le nom, le poster et l'url + **/ + override suspend fun search(query: String): List { + val link = + "$mainUrl/index.php?do=search&subaction=search&search_start=0&full_search=1&result_from=1&story=$query&titleonly=3&searchuser=&replyless=0&replylimit=0&searchdate=0&beforeafter=after&sortby=date&resorder=desc&showposts=0&catlist%5B%5D=0" // search' + val document = + app.post(link).document // app.get() permet de télécharger la page html avec une requete HTTP (get) + val results = document.select("div#dle-content > div.clearfix") + + val allresultshome = + results.mapNotNull { article -> // avec mapnotnull si un élément est null, il sera automatiquement enlevé de la liste + article.toSearchResponse() + } + return allresultshome + } + + /** + * charge la page d'informations, il ya toutes les donées, les épisodes, le résumé etc ... + * Il faut retourner soit: AnimeLoadResponse, MovieLoadResponse, TorrentLoadResponse, TvSeriesLoadResponse. + */ + data class EpisodeData( + @JsonProperty("url") val url: String, + @JsonProperty("episodeNumber") val episodeNumber: String, + ) + + private fun Elements.takeEpisode(url: String, duborSub: String?): ArrayList { + + val episodes = ArrayList() + this.select("ul.eplist > li").forEach { + + val strEpisode = it.text() + val strEpisodeN = strEpisode.replace("Episode ", "") + val link = + EpisodeData( + url, + strEpisodeN, + ).toJson() + + + episodes.add( + Episode( + link, + name = duborSub, + episode = strEpisodeN.toInt(), + ) + ) + } + + return episodes + } + + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document // + // url est le lien retourné par la fonction search (la variable href) ou la fonction getMainPage + + var episodes = ArrayList() + var mediaType: TvType + val episodeFrfound = + document.select("div.blocfr") + + val episodeVostfrfound = + document.select("div.blocvostfr") + val title = + document.select("h1[itemprop]").text() + val posterUrl = + document.select("img#posterimg").attr("src") + val yearRegex = Regex("""ate de sortie\: (\d*)""") + val year = yearRegex.find(document.text())?.groupValues?.get(1) + + + val tags = document.select("[itemprop=genre] > a") + .map { it.text() } // séléctione tous les tags et les ajoutes à une liste + + if (episodeFrfound.text().contains("Episode")) { + mediaType = TvType.TvSeries + val duborSub = "Episode en VF" + episodes = episodeFrfound.takeEpisode(url, duborSub) + } else if (episodeVostfrfound.text().contains("Episode")) { + mediaType = TvType.TvSeries + val duborSub = "Episode sous-titré" + episodes = episodeVostfrfound.takeEpisode(url, duborSub) + } else { + + mediaType = TvType.Movie + } + /////////////////////////////////////////// + /////////////////////////////////////////// + var type_rec: TvType + val recommendations = + document.select("div.clearfixme > div > div")?.mapNotNull { element -> + val recTitle = + element.select("a").text() ?: return@mapNotNull null + val image = element.select("a >img")?.attr("src") + val recUrl = element.select("a").attr("href") + type_rec = TvType.TvSeries + if (recUrl.contains("film")) type_rec = TvType.Movie + + if (type_rec == TvType.TvSeries) { + TvSeriesSearchResponse( + recTitle, + recUrl, + this.name, + TvType.TvSeries, + image?.let { fixUrl(it) }, + + ) + } else + MovieSearchResponse( + recTitle, + recUrl, + this.name, + TvType.Movie, + image?.let { fixUrl(it) }, + + ) + + } + + var comingSoon = url.contains("films-prochainement") + + + if (mediaType == TvType.Movie) { + val description = document.selectFirst("div.screenshots-full")?.text() + ?.replace("(.* .ynopsis)".toRegex(), "") + return newMovieLoadResponse( + name = title, + url = url, + type = TvType.Movie, + dataUrl = url + + ) { + this.posterUrl = fixUrl(posterUrl) + this.plot = description + this.recommendations = recommendations + this.year = year?.toIntOrNull() + this.comingSoon = comingSoon + this.tags = tags + } + } else { + val description = document.selectFirst("span[itemprop=description]")?.text() + return newTvSeriesLoadResponse( + title, + url, + mediaType, + episodes + ) { + this.posterUrl = fixUrl(posterUrl) + this.plot = description + this.recommendations = recommendations + this.year = year?.toIntOrNull() + this.comingSoon = comingSoon + this.tags = tags + + } + } + } + + + // récupere les liens .mp4 ou m3u8 directement à partir du paramètre data généré avec la fonction load() + override suspend fun loadLinks( + data: String, // fournit par load() + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit, + ): Boolean { + val parsedInfo = + tryParseJson(data) + val url = parsedInfo?.url ?: data + + val numeroEpisode = parsedInfo?.episodeNumber ?: null + + val document = app.get(url).document + val episodeFrfound = + document.select("div.blocfr") + val episodeVostfrfound = + document.select("div.blocvostfr") + + val cssCodeForPlayer = if (episodeFrfound.text().contains("Episode")) { + "div.ep${numeroEpisode}vf > a" + } else if (episodeVostfrfound.text().contains("Episode")) { + "div.ep${numeroEpisode}vs > a" + } else { + "div.linkstab > a" + } + + + document.select("$cssCodeForPlayer").apmap { player -> // séléctione tous les players + var playerUrl = "https"+player.attr("href").replace("(.*)https".toRegex(), "") + if (!playerUrl.isNullOrBlank()) + if (playerUrl.contains("dood")) { + playerUrl = playerUrl.replace("doodstream.com", "dood.wf") + } + loadExtractor( + httpsify(playerUrl), + playerUrl, + subtitleCallback + ) { link -> + callback.invoke( + ExtractorLink( // ici je modifie le callback pour ajouter des informations, normalement ce n'est pas nécessaire + link.source, + link.name + "", + link.url, + link.referer, + getQualityFromName("HD"), + link.isM3u8, + link.headers, + link.extractorData + ) + ) + } + } + + + return true + } + + private fun Element.toSearchResponse(): SearchResponse { + + val posterUrl = fixUrl(select("div.img-box > img").attr("src")) + val qualityExtracted = select("div.nbloc1-2 >span").text() + val type = select("div.nbloc3").text() + val title = select("a.nowrap").text() + val link = select("a.nowrap").attr("href") + var quality = when (!qualityExtracted.isNullOrBlank()) { + qualityExtracted.contains("HDLight") -> getQualityFromString("HD") + qualityExtracted.contains("Bdrip") -> getQualityFromString("BlueRay") + qualityExtracted.contains("DVD") -> getQualityFromString("DVD") + qualityExtracted.contains("CAM") -> getQualityFromString("Cam") + + else -> null + } + if (type.contains("Film")) { + return MovieSearchResponse( + name = title, + url = link, + apiName = title, + type = TvType.Movie, + posterUrl = posterUrl, + quality = quality + + ) + + + } else // an Serie + { + + return TvSeriesSearchResponse( + name = title, + url = link, + apiName = title, + type = TvType.TvSeries, + posterUrl = posterUrl, + quality = quality, + // + ) + + } + } + + override val mainPage = mainPageOf( + Pair("$mainUrl/films-prochainement/page/", "Film Prochainement en Streaming"), + Pair("$mainUrl/film-en-streaming/page/", "Top Films cette année"), + Pair("$mainUrl/serie-en-streaming/page/", "Top Séries cette année"), + Pair("$mainUrl/saison-complete/page/", "Les saisons complètes"), + Pair("$mainUrl/film-ancien/page/", "Film zahalé (ancien)") + ) + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + val url = request.data + page + val document = app.get(url).document + val movies = document.select("div#dle-content > div.clearfix") + + val home = + movies.mapNotNull { article -> // avec mapnotnull si un élément est null, il sera automatiquement enlevé de la liste + article.toSearchResponse() + } + return newHomePageResponse(request.name, home) + } + +} diff --git a/WiflixProvider/src/main/kotlin/com/lagradost/WiflixProviderPlugin.kt b/WiflixProvider/src/main/kotlin/com/lagradost/WiflixProviderPlugin.kt new file mode 100644 index 0000000..7e90a52 --- /dev/null +++ b/WiflixProvider/src/main/kotlin/com/lagradost/WiflixProviderPlugin.kt @@ -0,0 +1,18 @@ + +package com.lagradost + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class WiflixPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(WiflixProvider()) + registerExtractorAPI(DoodStreamExtractor()) + registerExtractorAPI(StreamSBPlusExtractor()) + + + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 7d6e19e..122837f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -84,6 +84,8 @@ subprojects { //run JS implementation("org.mozilla:rhino:1.7.14") + // Library/extensions searching with Levenshtein distance + implementation ("me.xdrop:fuzzywuzzy:1.4.0") } }