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..d60efcf --- /dev/null +++ b/VostfreeProvider/src/main/kotlin/com/lagradost/MytvExtractor.kt @@ -0,0 +1,40 @@ +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..98a1778 --- /dev/null +++ b/VostfreeProvider/src/main/kotlin/com/lagradost/SibnetExtractor.kt @@ -0,0 +1,33 @@ +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..b485430 --- /dev/null +++ b/VostfreeProvider/src/main/kotlin/com/lagradost/VostfreeProvider.kt @@ -0,0 +1,385 @@ +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 + + +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.Movie) // ici on ne supporte que les films + override val supportedTypes = setOf( TvType.Anime,TvType.AnimeMovie,TvType.OVA) // animes, animesfilms et series + // 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/?s=$query" + val link = "$mainUrl/index.php?story=$query&do=search&subaction=search" + // L'url pour chercher un anime de dragon sera donc: 'https://vostfree.cx/index.php?story=dragon&do=search&subaction=search' + // le $ dans une string permet d'insérer une variable + val document = app.get(link).document // app.get() permet de télécharger la page html avec une requete HTTP (get) + // on convertit le html en un document + return document.select("div.search-result") // on séléctione tous les éléments 'enfant' du type articles + .apmap { div -> // apmap crée une liste des éléments (ici newMovieSearchResponse et newAnimeSearchResponse) + //val posterContainer = div.selectFirst("> span.image ") // selectione le premier élément correspondant à ces critères + val type = div?.selectFirst("div.genre")?.text()?.replace("\t", "")?.replace("\n", "") + // replace enlève tous les '\t' et '\n' du titre + val mediaPoster = mainUrl + div?.selectFirst("span.image > img")?.attr("src") // 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() + + when (type) { + "FILM" -> ( + newMovieSearchResponse( // réponse du film qui sera ajoutée à la liste apmap qui sera ensuite return + title, + href, + TvType.AnimeMovie, + false + ) { + this.posterUrl = mediaPoster + // this.rating = rating + } + ) + null -> ( + newAnimeSearchResponse( + title, + href, + TvType.Anime, + false + ) { + this.posterUrl = mediaPoster + // this.rating = rating + } + + + ) + else -> { + throw ErrorLoadingException("invalid media type") // le type n'est pas reconnu ==> affiche une erreur + } + } + } + } + + private data class EmbedUrlClass( + @JsonProperty("embed_url") val url: String?, + ) + + /** + * 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 meta = document.selectFirst("div#dle-content > div.watch-top > div.image-bg > div.image-bg-content > div.slide-block ") + var poster : String? = "" + var title = "" + + var description : String? = ""// first() selectione le premier élément de la liste + +///////////////////////////////////////////////// + val isSaison = document.select("div.new_player_series_count > a") + var saison00 = -1 + var i = 0 + var noSeason =true + var enterInIseason = false + isSaison.forEach { + var url1=it.attr("href") + while(noSeason){ + it.select("[alt=Saison 0$i]").forEach{ // enter in the block when it match the season + noSeason=false + } + it.select("[alt=Saison $i]").forEach{ // enter in the block when it match the season + noSeason=false + } + i++ + } + i=i-1 + saison00 = i + i = 0 // reinit i et noSeason for the next page + noSeason =true + var document1 = app.get(url1).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 meta1 = document1.selectFirst("div#dle-content > div.watch-top > div.image-bg > div.image-bg-content > div.slide-block ") + poster = mainUrl + meta1?.select(" div.slide-poster > img")?.attr("src") // récupere le texte de l'attribut 'data-src' + title = meta1?.select("div.slide-middle > h1")?.text() ?: throw ErrorLoadingException("Invalid title") + title =title.replace("Saison","").replace("saison","").replace("SAISON","") + title =title.replace("Season","").replace("season","").replace("SEASON","") + description= meta1.select("div.slide-middle > div.slide-desc").first()?.text() // first() selectione le premier élément de la liste + + val listEpisode =document.select(" select.new_player_selector > option").forEach { + + if( it.text()!="Film"){ + val link = + EpisodeData( + url1, + it.text().replace("Episode ",""), + ).toJson() + episodes.add( Episode( + link, + episode = it.text().replace("Episode ","").toInt(), + season=saison00, + name = "Saison ${saison00.toString()}" + it.text(), + //description= description, + posterUrl = poster + ) ) + }else{ + + mediaType=TvType.AnimeMovie + } + } + enterInIseason = true + } + poster = mainUrl + meta?.select(" div.slide-poster > img")?.attr("src") // récupere le texte de l'attribut 'data-src' + title = meta?.select("div.slide-middle > h1")?.text() ?: throw ErrorLoadingException("Invalid title") + + + description = meta.select("div.slide-middle > div.slide-desc").first()?.text() // first() selectione le premier élément de la liste + //var saison0 = document.select("div.new_player_series_count > a")?.text()?.replace("Saison 0","")?.replace("Saison ","")?.toInt() + var season : Int? + if(enterInIseason){val seasontext = meta.select("ul.slide-top > li:last-child > b:last-child").text() + title =title.replace("Saison","").replace("saison","").replace("SAISON","") + title =title.replace("Season","").replace("season","").replace("SEASON","") + var index = seasontext?.indexOf('0') + var no = seasontext + while (index == 0) {no = seasontext?.drop(1).toString() + index = no?.indexOf('0')} + season = no.toInt() + }else{ + season = null} + val listEpisode =document.select(" select.new_player_selector > option").forEach { + + if( it.text()!="Film"){ + val link = + EpisodeData( + url, + it.text().replace("Episode ",""), + ).toJson() + episodes.add( Episode( + link, + episode = it.text().replace("Episode ","").toInt(), + season=season, + name = "Saison ${season.toString()}" + it.text(), + //description= description, + posterUrl = poster + + ) )}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 + } + } else // an anime + { + return newAnimeLoadResponse( + title, + url, + mediaType, + ) { + this.posterUrl = poster + this.plot = description + 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)//?:throw ErrorLoadingException("Invalid url") + val url = if(parsedInfo?.url !=null){parsedInfo.url}else{data} + val noEpisode = if(parsedInfo?.episodeNumber !=null){parsedInfo.episodeNumber}else{"1"} // if is not a movie then take the episode number else 1 + + val document = app.get(url).document + document.select("div.new_player_bottom").apmap { player_bottom -> // séléctione tous les players + + // supprimer les zéro de 0015 pour obtenir l'episode 15 + var index = noEpisode?.indexOf('0') + var no = noEpisode + while (index == 0) {no = noEpisode?.drop(1).toString() + index = no?.indexOf('0')} + + var cssQuery = " div#buttons_$no" // no numéro épisode + val buttonsNepisode = player_bottom?.select(cssQuery)?:throw ErrorLoadingException("Non player") //séléctione tous les players pour l'episode NoEpisode + buttonsNepisode.select("> div").forEach { + val player = it.attr("id")?.toString()?: throw ErrorLoadingException("Player No found") //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" + //for(i in playerName.indices){ + var codePlayload = + document.selectFirst("div#content_$player")?.text().toString() // resultat : "325544" ou "https:..." peut être lorsque playerName = VIP ou Upvid DStream + var playerUrl ="" + when (playerName) { + "VIP" -> playerUrl = codePlayload + "Upvid" -> playerUrl = codePlayload // extractor à faire + "Uqload" -> // film_iframe + playerUrl = "https://uqload.com/embed-$codePlayload.html" // OK + "Dstream" -> playerUrl = codePlayload + "Streamsb" -> playerUrl = codePlayload // OK + "Vudeo" -> playerUrl = codePlayload // OK + "Mytv" -> // film_iframe + playerUrl = "https://www.myvi.tv/embed/$codePlayload" //OK + "Sibnet" -> // film_iframe + playerUrl = "https://video.sibnet.ru/shell.php?videoid=$codePlayload" // OK + + "NinjaS" -> // film_iframe + playerUrl = codePlayload + "Stream" -> // == myTv extractor + security + playerUrl = "https://myvi.ru/player/embed/html/$codePlayload" //OK + + + + } + + if (playerUrl != "") + 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 + } + + + override suspend fun getMainPage( + page: Int, + request : MainPageRequest + ): HomePageResponse { + val list = ArrayList() + val animeDubStatus = ArrayList(15) + + animeDubStatus.add("/animes-vf/") + animeDubStatus.add("/animes-vostfr/") + animeDubStatus.add("/films-vf-vostfr/") + animeDubStatus.add("/genre/Action/") + animeDubStatus.add("/genre/Aventure/") + animeDubStatus.add("/genre/Comédie/") + animeDubStatus.add("/genre/Tranche+de+vie/") + animeDubStatus.add("/genre/Drame/") + animeDubStatus.add("/genre/Fantasy/") + animeDubStatus.add("/genre/Surnaturel/") + animeDubStatus.add("/genre/Mystère/") + animeDubStatus.add("/genre/Shonen/") + animeDubStatus.add("/genre/Psychologique/") + animeDubStatus.add("/genre/Romance/") + animeDubStatus.add("/genre/Sci-Fi/") + + + + animeDubStatus.forEach { + val documentZero = app.get("$mainUrl$it").document + val numberOFpage=documentZero.select("div.navigation > div.pages > a").text() + var lastpage = if(numberOFpage !="")numberOFpage.substringAfterLast(" ").toString().toInt() else 1 + val openPageMax = 2 // prendre uniquement les n pages + lastpage =if(lastpage>openPageMax){openPageMax}else{ lastpage} + var categoryTitle ="" + for(i in 1 .. lastpage) { + var page = "/page/$i" + val document = app.get("$mainUrl$it$page").document + + val movies = document.select("div#content > div#dle-content > div.movie-poster") + + if (it.contains("genre", true)) { + categoryTitle = it.replace("/genre/", "").replace("/", " (page $i)").replace("+", " ") + categoryTitle = "Catégorie : $categoryTitle" + } else { + categoryTitle = + document.select("div#left-movies-block > ul#left-movies-tabs > li").text().replace("Animes VF", " Animes EN FRANÇAIS (page $i)").replace("Animes VOSTFR", " Animes VOSTFR (page $i)") + .replace("Films VF et VOSTFR", "FILMS EN FRANÇAIS OU EN VOSTFR (page $i)").replace("Liste","").replace("La liste des","") + } + + + + val returnList = movies.mapNotNull { article -> + // map est la même chose que apmap (mais apmap est plus rapide) + // ici si un élément est null, il sera automatiquement enlevé de la liste + val poster = article.select("span.image") + val posterUrl = mainUrl + poster.select("> img").attr("src") + val subdub =article.select("div.quality").text() + + val genre= article.select("div.genre").text() + + val title = article.select("div.info > div.title").text() + val link = article.select("div.play > a").attr("href") + if (genre == "FILM") { + newMovieSearchResponse( + title, + link, + TvType.AnimeMovie, + false, + ) { + this.posterUrl = posterUrl + //this.quality = quality + } + + } else // a tv serie + { + newAnimeSearchResponse( + title, + link, + TvType.Anime, + false, + ) { + this.posterUrl = posterUrl + if (subdub=="VF") DubStatus.Dubbed else DubStatus.Subbed + //this.quality = quality + } } + + } + if (returnList.isEmpty()) throw ErrorLoadingException() + list.add(HomePageList(categoryTitle, returnList)) + + } + } + if (list.isEmpty()) throw ErrorLoadingException() + return HomePageResponse( + list) + } + + +} \ 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..ceeaab1 --- /dev/null +++ b/VostfreeProvider/src/main/kotlin/com/lagradost/VudeoExtractor.kt @@ -0,0 +1,31 @@ +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 + } +}