diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 95bf7ac4..cac2a506 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,6 +54,7 @@ jobs: ANICHI_SERVER: ${{ secrets.ANICHI_SERVER }} ANICHI_ENDPOINT: ${{ secrets.ANICHI_ENDPOINT }} ANICHI_APP: ${{ secrets.ANICHI_APP }} + PRIMEWIRE_KEY: ${{ secrets.PRIMEWIRE_KEY }} run: | cd $GITHUB_WORKSPACE/src echo SORA_API=$SORA_API >> local.properties @@ -68,6 +69,7 @@ jobs: echo ANICHI_SERVER=$ANICHI_SERVER >> local.properties echo ANICHI_ENDPOINT=$ANICHI_ENDPOINT >> local.properties echo ANICHI_APP=$ANICHI_APP >> local.properties + echo PRIMEWIRE_KEY=$PRIMEWIRE_KEY >> local.properties - name: Build Plugins run: | diff --git a/Anichi/build.gradle.kts b/Anichi/build.gradle.kts new file mode 100644 index 00000000..643809b0 --- /dev/null +++ b/Anichi/build.gradle.kts @@ -0,0 +1,42 @@ +import org.jetbrains.kotlin.konan.properties.Properties + +// use an integer for version numbers +version = 9 + +android { + defaultConfig { + val properties = Properties() + properties.load(project.rootProject.file("local.properties").inputStream()) + + buildConfigField("String", "ANICHI_API", "\"${properties.getProperty("ANICHI_API")}\"") + buildConfigField("String", "ANICHI_SERVER", "\"${properties.getProperty("ANICHI_SERVER")}\"") + buildConfigField("String", "ANICHI_ENDPOINT", "\"${properties.getProperty("ANICHI_ENDPOINT")}\"") + buildConfigField("String", "ANICHI_APP", "\"${properties.getProperty("ANICHI_APP")}\"") + + + } +} + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + authors = listOf("Hexated") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AnimeMovie", + "Anime", + "OVA", + ) + + iconUrl = "https://media.discordapp.net/attachments/1059306855865782282/1123970193274712096/Anichi.png" +} \ No newline at end of file diff --git a/Anichi/src/main/kotlin/com/hexated/Anichi.kt b/Anichi/src/main/kotlin/com/hexated/Anichi.kt new file mode 100644 index 00000000..bbad3ae7 --- /dev/null +++ b/Anichi/src/main/kotlin/com/hexated/Anichi.kt @@ -0,0 +1,263 @@ +package com.hexated + +import com.hexated.AnichiExtractors.invokeExternalSources +import com.hexated.AnichiExtractors.invokeInternalSources +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addActors +import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.syncproviders.SyncIdName +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.nicehttp.RequestBodyTypes +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.toRequestBody + +open class Anichi : MainAPI() { + override var name = "Anichi" + override val instantLinkLoading = true + override val hasQuickSearch = false + override val hasMainPage = true + + private fun getStatus(t: String): ShowStatus { + return when (t) { + "Finished" -> ShowStatus.Completed + "Releasing" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + + override val supportedSyncNames = setOf(SyncIdName.Anilist, SyncIdName.MyAnimeList) + override val supportedTypes = setOf(TvType.Anime, TvType.AnimeMovie) + + private val popularTitle = "Popular" + private val animeRecentTitle = "Latest Anime" + private val donghuaRecentTitle = "Latest Donghua" + private val movieTitle = "Movie" + + override val mainPage = mainPageOf( + """$apiUrl?variables={"search":{"sortBy":"Latest_Update","allowAdult":${settingsForProvider.enableAdult},"allowUnknown":false},"limit":26,"page":%d,"translationType":"sub","countryOrigin":"JP"}&extensions={"persistedQuery":{"version":1,"sha256Hash":"$mainHash"}}""" to animeRecentTitle, + """$apiUrl?variables={"search":{"sortBy":"Latest_Update","allowAdult":${settingsForProvider.enableAdult},"allowUnknown":false},"limit":26,"page":%d,"translationType":"sub","countryOrigin":"CN"}&extensions={"persistedQuery":{"version":1,"sha256Hash":"$mainHash"}}""" to donghuaRecentTitle, + """$apiUrl?variables={"type":"anime","size":30,"dateRange":1,"page":%d,"allowAdult":${settingsForProvider.enableAdult},"allowUnknown":false}&extensions={"persistedQuery":{"version":1,"sha256Hash":"$popularHash"}}""" to popularTitle, + """$apiUrl?variables={"search":{"slug":"movie-anime","format":"anime","tagType":"upcoming","name":"Trending Movies"}}&extensions={"persistedQuery":{"version":1,"sha256Hash":"$slugHash"}}""" to movieTitle + ) + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + + val url = request.data.format(page) + val res = app.get(url, headers = headers).parsedSafe()?.data + val query = res?.shows ?: res?.queryPopular ?: res?.queryListForTag + val card = if(request.name == popularTitle) query?.recommendations?.map { it.anyCard } else query?.edges + val home = card?.filter { + // filtering in case there is an anime with 0 episodes available on the site. + !(it?.availableEpisodes?.raw == 0 && it.availableEpisodes.sub == 0 && it.availableEpisodes.dub == 0) + }?.mapNotNull { media -> + media?.toSearchResponse() + } ?: emptyList() + return newHomePageResponse( + list = HomePageList( + name = request.name, + list = home, + ), + hasNext = request.name != movieTitle + ) + } + + private fun Edges.toSearchResponse(): AnimeSearchResponse? { + + return newAnimeSearchResponse( + name ?: englishName ?: nativeName ?: "", + Id ?: return null, + fix = false + ) { + this.posterUrl = thumbnail + this.year = airedStart?.year + this.otherName = englishName + addDub(availableEpisodes?.dub) + addSub(availableEpisodes?.sub) + } + } + + override suspend fun search(query: String): List? { + + val link = + """$apiUrl?variables={"search":{"allowAdult":false,"allowUnknown":false,"query":"$query"},"limit":26,"page":1,"translationType":"sub","countryOrigin":"ALL"}&extensions={"persistedQuery":{"version":1,"sha256Hash":"$mainHash"}}""" + val res = app.get( + link, + headers = headers + ).text.takeUnless { it.contains("PERSISTED_QUERY_NOT_FOUND") } + // Retries + ?: app.get( + link, + headers = headers + ).text.takeUnless { it.contains("PERSISTED_QUERY_NOT_FOUND") } + ?: return emptyList() + + val response = parseJson(res) + + val results = response.data?.shows?.edges?.filter { + // filtering in case there is an anime with 0 episodes available on the site. + !(it.availableEpisodes?.raw == 0 && it.availableEpisodes.sub == 0 && it.availableEpisodes.dub == 0) + } + + return results?.map { + newAnimeSearchResponse(it.name ?: "", "${it.Id}", fix = false) { + this.posterUrl = it.thumbnail + this.year = it.airedStart?.year + this.otherName = it.englishName + addDub(it.availableEpisodes?.dub) + addSub(it.availableEpisodes?.sub) + } + } + } + + override suspend fun getLoadUrl(name: SyncIdName, id: String): String? { + val syncId = id.split("/").last() + val malId = if (name == SyncIdName.MyAnimeList) { + syncId + } else { + aniToMal(syncId) + } + + val media = app.get("$jikanApi/anime/$malId").parsedSafe()?.data + val link = + """$apiUrl?variables={"search":{"allowAdult":false,"allowUnknown":false,"query":"${media?.title}"},"limit":26,"page":1,"translationType":"sub","countryOrigin":"ALL"}&extensions={"persistedQuery":{"version":1,"sha256Hash":"$mainHash"}}""" + val res = app.get( + link, + headers = headers + ).parsedSafe()?.data?.shows?.edges + return res?.find { + (it.name.equals(media?.title, true) || it.englishName.equals( + media?.title_english, + true + ) || it.nativeName.equals(media?.title_japanese, true)) && it.airedStart?.year == media?.year + }?.Id + } + + override suspend fun load(url: String): LoadResponse? { + + val id = url.substringAfterLast("/") + // lazy to format + val body = """ + { + "query": " query(\n ${'$'}_id: String!\n ) {\n show(\n _id: ${'$'}_id\n ) {\n _id\n name\n description\n thumbnail\n thumbnails\n lastEpisodeInfo\n lastEpisodeDate \n type\n genres\n score\n status\n season\n altNames \n averageScore\n rating\n episodeCount\n episodeDuration\n broadcastInterval\n banner\n airedEnd\n airedStart \n studios\n characters\n availableEpisodesDetail\n availableEpisodes\n prevideos\n nameOnlyString\n relatedShows\n relatedMangas\n musics\n isAdult\n \n tags\n countryOfOrigin\n\n pageStatus{\n _id\n notes\n pageId\n showId\n \n # ranks:[Object]\n views\n likesCount\n commentCount\n dislikesCount\n reviewCount\n userScoreCount\n userScoreTotalValue\n userScoreAverValue\n viewers{\n firstViewers{\n viewCount\n lastWatchedDate\n user{\n _id\n displayName\n picture\n # description\n hideMe\n # createdAt\n # badges\n brief\n }\n \n }\n recViewers{\n viewCount\n lastWatchedDate\n user{\n _id\n displayName\n picture\n # description\n hideMe\n # createdAt\n # badges\n brief\n }\n \n }\n }\n\n }\n }\n }", + "extensions": "{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"$detailHash\"}}", + "variables": "{\"_id\":\"$id\"}" + } + """.trimIndent().trim().toRequestBody(RequestBodyTypes.JSON.toMediaTypeOrNull()) + + val res = app.post(apiUrl, requestBody = body, headers = headers) + val showData = res.parsedSafe()?.data?.show ?: return null + + val title = showData.name + val description = showData.description + val poster = showData.thumbnail + + val trackers = getTracker( + title, + showData.altNames?.firstOrNull(), + showData.airedStart?.year, + showData.season?.quarter, + showData.type + ) + + val episodes = showData.availableEpisodesDetail.let { + if (it == null) return@let Pair(null, null) + if (showData.Id == null) return@let Pair(null, null) + Pair( + it.getEpisode("sub", showData.Id, trackers?.idMal), + it.getEpisode("dub", showData.Id, trackers?.idMal), + ) + } + + val characters = showData.characters?.map { + val role = when (it.role) { + "Main" -> ActorRole.Main + "Supporting" -> ActorRole.Supporting + "Background" -> ActorRole.Background + else -> null + } + val name = it.name?.full ?: it.name?.native ?: "" + val image = it.image?.large ?: it.image?.medium + Pair(Actor(name, image), role) + } + + return newAnimeLoadResponse(title ?: "", url, TvType.Anime) { + engName = showData.altNames?.firstOrNull() + posterUrl = trackers?.coverImage?.extraLarge ?: trackers?.coverImage?.large ?: poster + backgroundPosterUrl = trackers?.bannerImage ?: showData.banner + rating = showData.averageScore?.times(100) + tags = showData.genres + year = showData.airedStart?.year + duration = showData.episodeDuration?.div(60_000) + addTrailer(showData.prevideos.filter { it.isNotBlank() } + .map { "https://www.youtube.com/watch?v=$it" }) + addEpisodes(DubStatus.Subbed, episodes.first) + addEpisodes(DubStatus.Dubbed, episodes.second) + addActors(characters) + //this.recommendations = recommendations + + showStatus = getStatus(showData.status.toString()) + addMalId(trackers?.idMal) + addAniListId(trackers?.id) + plot = description?.replace(Regex("""<(.*?)>"""), "") + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val loadData = parseJson(data) + + argamap( + { + invokeInternalSources( + loadData.hash, + loadData.dubStatus, + loadData.episode, + subtitleCallback, + callback + ) + }, + { + invokeExternalSources( + loadData.idMal, + loadData.dubStatus, + loadData.episode, + subtitleCallback, + callback + ) + } + ) + + return true + } + + companion object { + const val apiUrl = BuildConfig.ANICHI_API + const val serverUrl = BuildConfig.ANICHI_SERVER + const val apiEndPoint = BuildConfig.ANICHI_ENDPOINT + + const val anilistApi = "https://graphql.anilist.co" + const val jikanApi = "https://api.jikan.moe/v4" + const val marinHost = "https://marin.moe" + + private const val mainHash = "e42a4466d984b2c0a2cecae5dd13aa68867f634b16ee0f17b380047d14482406" + private const val popularHash = "31a117653812a2547fd981632e8c99fa8bf8a75c4ef1a77a1567ef1741a7ab9c" + private const val slugHash = "bf603205eb2533ca21d0324a11f623854d62ed838a27e1b3fcfb712ab98b03f4" + private const val detailHash = "bb263f91e5bdd048c1c978f324613aeccdfe2cbc694a419466a31edb58c0cc0b" + const val serverHash = "5e7e17cdd0166af5a2d8f43133d9ce3ce9253d1fdb5160a0cfd515564f98d061" + + val headers = mapOf( + "app-version" to "android_c-247", + "from-app" to BuildConfig.ANICHI_APP, + "platformstr" to "android_c", + ) + } + +} \ No newline at end of file diff --git a/Anichi/src/main/kotlin/com/hexated/AnichiExtractors.kt b/Anichi/src/main/kotlin/com/hexated/AnichiExtractors.kt new file mode 100644 index 00000000..86d452ed --- /dev/null +++ b/Anichi/src/main/kotlin/com/hexated/AnichiExtractors.kt @@ -0,0 +1,218 @@ +package com.hexated + +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.apmap +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.argamap +import com.lagradost.cloudstream3.extractors.helper.GogoHelper +import com.lagradost.cloudstream3.mvvm.safeApiCall +import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.SubtitleHelper +import com.lagradost.cloudstream3.utils.httpsify +import com.lagradost.cloudstream3.utils.loadExtractor +import java.net.URI + +object AnichiExtractors : Anichi() { + + suspend fun invokeInternalSources( + hash: String, + dubStatus: String, + episode: String, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit, + ) { + val apiUrl = + """$apiUrl?variables={"showId":"$hash","translationType":"$dubStatus","episodeString":"$episode"}&extensions={"persistedQuery":{"version":1,"sha256Hash":"$serverHash"}}""" + val apiResponse = app.get(apiUrl, headers = headers).parsed() + + apiResponse.data?.episode?.sourceUrls?.apmap { source -> + safeApiCall { + val link = fixSourceUrls(source.sourceUrl ?: return@safeApiCall, source.sourceName) + ?: return@safeApiCall + if (URI(link).isAbsolute || link.startsWith("//")) { + val fixedLink = if (link.startsWith("//")) "https:$link" else link + val host = link.getHost() + + when { + fixedLink.contains(Regex("(?i)playtaku|gogo")) || source.sourceName == "Vid-mp4" -> { + invokeGogo(fixedLink, subtitleCallback, callback) + } + + embedIsBlacklisted(fixedLink) -> { + loadExtractor(fixedLink, subtitleCallback, callback) + } + + URI(fixedLink).path.contains(".m3u") -> { + getM3u8Qualities(fixedLink, serverUrl, host).forEach(callback) + } + + else -> { + callback( + ExtractorLink( + name, + host, + fixedLink, + serverUrl, + Qualities.P1080.value, + false + ) + ) + } + } + } else { + val fixedLink = link.fixUrlPath() + val links = app.get(fixedLink).parsedSafe()?.links + ?: emptyList() + links.forEach { server -> + val host = server.link.getHost() + when { + source.sourceName?.contains("Default") == true -> { + if (server.resolutionStr == "SUB" || server.resolutionStr == "Alt vo_SUB") { + getM3u8Qualities( + server.link, + "https://static.crunchyroll.com/", + host, + ).forEach(callback) + } + } + + server.hls != null && server.hls -> { + getM3u8Qualities( + server.link, + "$apiEndPoint/player?uri=" + (if (URI(server.link).host.isNotEmpty()) server.link else apiEndPoint + URI( + server.link + ).path), + host + ).forEach(callback) + } + + else -> { + callback( + ExtractorLink( + host, + host, + server.link, + "$apiEndPoint/player?uri=" + (if (URI(server.link).host.isNotEmpty()) server.link else apiEndPoint + URI( + server.link + ).path), + server.resolutionStr.removeSuffix("p").toIntOrNull() + ?: Qualities.P1080.value, + false, + isDash = server.resolutionStr == "Dash 1" + ) + ) + server.subtitles?.map { sub -> + subtitleCallback.invoke( + SubtitleFile( + SubtitleHelper.fromTwoLettersToLanguage(sub.lang ?: "") + ?: sub.lang ?: "", + httpsify(sub.src ?: return@map) + ) + ) + } + } + } + } + } + } + } + } + + suspend fun invokeExternalSources( + idMal: Int? = null, + dubStatus: String, + episode: String, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit, + ) { + val ids = app.get("https://api.malsync.moe/mal/anime/${idMal ?: return}") + .parsedSafe()?.sites + + if (dubStatus == "sub") invokeMarin(ids?.marin?.keys?.firstOrNull(), episode, callback) + + } + + private suspend fun invokeMarin( + id: String? = null, + episode: String, + callback: (ExtractorLink) -> Unit + ) { + val url = "$marinHost/anime/${id ?: return}/$episode" + val cookies = app.get( + "$marinHost/anime", + headers = mapOf( + "Cookie" to "__ddg1_=;__ddg2_=;" + ), + referer = "$marinHost/anime", + ).cookies.let { + decode(it["XSRF-TOKEN"].toString()) to decode(it["marin_session"].toString()) + } + + val json = app.get( + url, + headers = mapOf( + "Accept" to "text/html, application/xhtml+xml", + "Cookie" to "__ddg1=;__ddg2_=;XSRF-TOKEN=${cookies.first};marin_session=${cookies.second};", + "X-XSRF-TOKEN" to cookies.first + ), + referer = "$marinHost/anime/$id" + ).document.selectFirst("div#app")?.attr("data-page") + tryParseJson(json)?.props?.video?.data?.mirror?.map { video -> + callback.invoke( + ExtractorLink( + "Marin", + "Marin", + video.code?.file ?: return@map, + url, + video.code.height ?: Qualities.Unknown.value, + headers = mapOf( + "Accept" to "video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5", + "Accept-Language" to "en-US,en;q=0.5", + "Cookie" to "__ddg1=;__ddg2_=; XSRF-TOKEN=${cookies.first}; marin_session=${cookies.second};", + "Connection" to "keep-alive", + "Sec-Fetch-Dest" to "video", + "Sec-Fetch-Mode" to "cors", + "Sec-Fetch-Site" to "cross-site", + ) + ) + ) + } + } + + private suspend fun invokeGogo( + link: String, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val iframe = app.get(link) + val iframeDoc = iframe.document + argamap({ + iframeDoc.select(".list-server-items > .linkserver") + .forEach { element -> + val status = element.attr("data-status") ?: return@forEach + if (status != "1") return@forEach + val extractorData = element.attr("data-video") ?: return@forEach + loadExtractor(extractorData, iframe.url, subtitleCallback, callback) + } + }, { + val iv = "3134003223491201" + val secretKey = "37911490979715163134003223491201" + val secretDecryptKey = "54674138327930866480207815084989" + GogoHelper.extractVidstream( + iframe.url, + "Gogoanime", + callback, + iv, + secretKey, + secretDecryptKey, + isUsingAdaptiveKeys = false, + isUsingAdaptiveData = true, + iframeDocument = iframeDoc + ) + }) + } + +} \ No newline at end of file diff --git a/Anichi/src/main/kotlin/com/hexated/AnichiParser.kt b/Anichi/src/main/kotlin/com/hexated/AnichiParser.kt new file mode 100644 index 00000000..6cbe56ec --- /dev/null +++ b/Anichi/src/main/kotlin/com/hexated/AnichiParser.kt @@ -0,0 +1,292 @@ +package com.hexated + +import com.fasterxml.jackson.annotation.JsonProperty + +data class AnichiLoadData( + val hash: String, + val dubStatus: String, + val episode: String, + val idMal: Int? = null, +) + +data class JikanData( + @JsonProperty("title") val title: String? = null, + @JsonProperty("title_english") val title_english: String? = null, + @JsonProperty("title_japanese") val title_japanese: String? = null, + @JsonProperty("year") val year: Int? = null, + @JsonProperty("season") val season: String? = null, + @JsonProperty("type") val type: String? = null, +) + +data class JikanResponse( + @JsonProperty("data") val data: JikanData? = null, +) + +data class IdMal( + @JsonProperty("idMal") val idMal: String? = null, +) + +data class MediaAni( + @JsonProperty("Media") val media: IdMal? = null, +) + +data class DataAni( + @JsonProperty("data") val data: MediaAni? = null, +) + +data class CoverImage( + @JsonProperty("extraLarge") var extraLarge: String? = null, + @JsonProperty("large") var large: String? = null, +) + +data class AniMedia( + @JsonProperty("id") var id: Int? = null, + @JsonProperty("idMal") var idMal: Int? = null, + @JsonProperty("coverImage") var coverImage: CoverImage? = null, + @JsonProperty("bannerImage") var bannerImage: String? = null, +) + +data class AniPage( + @JsonProperty("media") var media: ArrayList = arrayListOf() +) + +data class AniData( + @JsonProperty("Page") var Page: AniPage? = AniPage() +) + +data class AniSearch( + @JsonProperty("data") var data: AniData? = AniData() +) + +data class AkIframe( + @JsonProperty("idUrl") val idUrl: String? = null, +) + +data class Stream( + @JsonProperty("format") val format: String? = null, + @JsonProperty("audio_lang") val audio_lang: String? = null, + @JsonProperty("hardsub_lang") val hardsub_lang: String? = null, + @JsonProperty("url") val url: String? = null, +) + +data class PortData( + @JsonProperty("streams") val streams: ArrayList? = arrayListOf(), +) + +data class Subtitles( + @JsonProperty("lang") val lang: String?, + @JsonProperty("label") val label: String?, + @JsonProperty("src") val src: String?, +) + +data class Links( + @JsonProperty("link") val link: String, + @JsonProperty("hls") val hls: Boolean?, + @JsonProperty("resolutionStr") val resolutionStr: String, + @JsonProperty("src") val src: String?, + @JsonProperty("portData") val portData: PortData? = null, + @JsonProperty("subtitles") val subtitles: ArrayList? = arrayListOf(), +) + +data class AnichiVideoApiResponse( + @JsonProperty("links") val links: List +) + +data class Data( + @JsonProperty("shows") val shows: Shows? = null, + @JsonProperty("queryListForTag") val queryListForTag: Shows? = null, + @JsonProperty("queryPopular") val queryPopular: Shows? = null, +) + +data class Shows( + @JsonProperty("edges") val edges: List? = arrayListOf(), + @JsonProperty("recommendations") val recommendations: List? = arrayListOf(), +) + +data class EdgesCard( + @JsonProperty("anyCard") val anyCard: Edges? = null, +) + +data class CharacterImage( + @JsonProperty("large") val large: String?, + @JsonProperty("medium") val medium: String? +) + +data class CharacterName( + @JsonProperty("full") val full: String?, + @JsonProperty("native") val native: String? +) + +data class Characters( + @JsonProperty("image") val image: CharacterImage?, + @JsonProperty("role") val role: String?, + @JsonProperty("name") val name: CharacterName?, +) + +data class Edges( + @JsonProperty("_id") val Id: String?, + @JsonProperty("name") val name: String?, + @JsonProperty("englishName") val englishName: String?, + @JsonProperty("nativeName") val nativeName: String?, + @JsonProperty("thumbnail") val thumbnail: String?, + @JsonProperty("type") val type: String?, + @JsonProperty("season") val season: Season?, + @JsonProperty("score") val score: Double?, + @JsonProperty("airedStart") val airedStart: AiredStart?, + @JsonProperty("availableEpisodes") val availableEpisodes: AvailableEpisodes?, + @JsonProperty("availableEpisodesDetail") val availableEpisodesDetail: AvailableEpisodesDetail?, + @JsonProperty("studios") val studios: List?, + @JsonProperty("genres") val genres: List?, + @JsonProperty("averageScore") val averageScore: Int?, + @JsonProperty("characters") val characters: List?, + @JsonProperty("altNames") val altNames: List?, + @JsonProperty("description") val description: String?, + @JsonProperty("status") val status: String?, + @JsonProperty("banner") val banner: String?, + @JsonProperty("episodeDuration") val episodeDuration: Int?, + @JsonProperty("prevideos") val prevideos: List = emptyList(), +) + +data class AvailableEpisodes( + @JsonProperty("sub") val sub: Int, + @JsonProperty("dub") val dub: Int, + @JsonProperty("raw") val raw: Int +) + +data class AiredStart( + @JsonProperty("year") val year: Int, + @JsonProperty("month") val month: Int, + @JsonProperty("date") val date: Int +) + +data class Season( + @JsonProperty("quarter") val quarter: String, + @JsonProperty("year") val year: Int +) + +data class AnichiQuery( + @JsonProperty("data") val data: Data? = null +) + +data class Detail( + @JsonProperty("data") val data: DetailShow +) + +data class DetailShow( + @JsonProperty("show") val show: Edges +) + +data class AvailableEpisodesDetail( + @JsonProperty("sub") val sub: List, + @JsonProperty("dub") val dub: List, + @JsonProperty("raw") val raw: List +) + +data class LinksQuery( + @JsonProperty("data") val data: LinkData? = LinkData() +) + +data class LinkData( + @JsonProperty("episode") val episode: Episode? = Episode() +) + +data class SourceUrls( + @JsonProperty("sourceUrl") val sourceUrl: String? = null, + @JsonProperty("priority") val priority: Int? = null, + @JsonProperty("sourceName") val sourceName: String? = null, + @JsonProperty("type") val type: String? = null, + @JsonProperty("className") val className: String? = null, + @JsonProperty("streamerId") val streamerId: String? = null +) + +data class Episode( + @JsonProperty("sourceUrls") val sourceUrls: ArrayList = arrayListOf(), +) + +data class Sub( + @JsonProperty("hour") val hour: Int? = null, + @JsonProperty("minute") val minute: Int? = null, + @JsonProperty("year") val year: Int? = null, + @JsonProperty("month") val month: Int? = null, + @JsonProperty("date") val date: Int? = null +) + +data class LastEpisodeDate( + @JsonProperty("dub") val dub: Sub? = Sub(), + @JsonProperty("sub") val sub: Sub? = Sub(), + @JsonProperty("raw") val raw: Sub? = Sub() +) + +data class AnyCard( + @JsonProperty("_id") val Id: String? = null, + @JsonProperty("name") val name: String? = null, + @JsonProperty("englishName") val englishName: String? = null, + @JsonProperty("nativeName") val nativeName: String? = null, + @JsonProperty("availableEpisodes") val availableEpisodes: AvailableEpisodes? = null, + @JsonProperty("score") val score: Double? = null, + @JsonProperty("lastEpisodeDate") val lastEpisodeDate: LastEpisodeDate? = LastEpisodeDate(), + @JsonProperty("thumbnail") val thumbnail: String? = null, + @JsonProperty("lastChapterDate") val lastChapterDate: String? = null, + @JsonProperty("availableChapters") val availableChapters: String? = null, + @JsonProperty("__typename") val _typename: String? = null +) + +data class PageStatus( + @JsonProperty("_id") val Id: String? = null, + @JsonProperty("views") val views: String? = null, + @JsonProperty("showId") val showId: String? = null, + @JsonProperty("rangeViews") val rangeViews: String? = null, + @JsonProperty("isManga") val isManga: Boolean? = null, + @JsonProperty("__typename") val _typename: String? = null +) + + +data class Recommendations( + @JsonProperty("anyCard") val anyCard: AnyCard? = null, + @JsonProperty("pageStatus") val pageStatus: PageStatus? = PageStatus(), + @JsonProperty("__typename") val _typename: String? = null +) + +data class QueryPopular( + @JsonProperty("total") val total: Int? = null, + @JsonProperty("recommendations") val recommendations: ArrayList = arrayListOf(), + @JsonProperty("__typename") val _typename: String? = null +) + +data class DataPopular( + @JsonProperty("queryPopular") val queryPopular: QueryPopular? = QueryPopular() +) + +data class MALSyncSites( + @JsonProperty("Zoro") val zoro: HashMap>? = hashMapOf(), + @JsonProperty("Marin") val marin: HashMap>? = hashMapOf(), +) + +data class MALSyncResponses( + @JsonProperty("Sites") val sites: MALSyncSites? = null, +) + +data class MarinCode( + @JsonProperty("file") val file: String? = null, + @JsonProperty("height") val height: Int? = null, +) + +data class MarinMirror( + @JsonProperty("code") val code: MarinCode? = null, +) + +data class MarinData( + @JsonProperty("mirror") val mirror: ArrayList? = arrayListOf(), +) + +data class MarinVideos( + @JsonProperty("data") val data: MarinData? = null, +) + +data class MarinProps( + @JsonProperty("video") val video: MarinVideos? = null, +) + +data class MarinResponses( + @JsonProperty("props") val props: MarinProps? = null, +) \ No newline at end of file diff --git a/Anichi/src/main/kotlin/com/hexated/AnichiUtils.kt b/Anichi/src/main/kotlin/com/hexated/AnichiUtils.kt new file mode 100644 index 00000000..42d3242f --- /dev/null +++ b/Anichi/src/main/kotlin/com/hexated/AnichiUtils.kt @@ -0,0 +1,147 @@ +package com.hexated + +import com.hexated.Anichi.Companion.anilistApi +import com.hexated.Anichi.Companion.apiEndPoint +import com.lagradost.cloudstream3.Episode +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.base64Decode +import com.lagradost.cloudstream3.fixTitle +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.M3u8Helper +import com.lagradost.nicehttp.RequestBodyTypes +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.toRequestBody +import java.net.URI +import java.net.URLDecoder + +suspend fun getTracker(name: String?, altName: String?, year: Int?, season: String?, type: String?): AniMedia? { + return fetchId(name, year, season, type).takeIf { it?.id != null } ?: fetchId( + altName, + year, + season, + type + ) +} + +suspend fun fetchId(title: String?, year: Int?, season: String?, type: String?): AniMedia? { + val query = """ + query ( + ${'$'}page: Int = 1 + ${'$'}search: String + ${'$'}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC] + ${'$'}type: MediaType + ${'$'}season: MediaSeason + ${'$'}year: String + ${'$'}format: [MediaFormat] + ) { + Page(page: ${'$'}page, perPage: 20) { + media( + search: ${'$'}search + sort: ${'$'}sort + type: ${'$'}type + season: ${'$'}season + startDate_like: ${'$'}year + format_in: ${'$'}format + ) { + id + idMal + coverImage { extraLarge large } + bannerImage + } + } + } + """.trimIndent().trim() + + val variables = mapOf( + "search" to title, + "sort" to "SEARCH_MATCH", + "type" to "ANIME", + "season" to if(type.equals("ona", true)) "" else season?.uppercase(), + "year" to "$year%", + "format" to listOf(type?.uppercase()) + ).filterValues { value -> value != null && value.toString().isNotEmpty() } + + val data = mapOf( + "query" to query, + "variables" to variables + ).toJson().toRequestBody(RequestBodyTypes.JSON.toMediaTypeOrNull()) + + return try { + app.post(anilistApi, requestBody = data) + .parsedSafe()?.data?.Page?.media?.firstOrNull() + } catch (t: Throwable) { + logError(t) + null + } + +} + +suspend fun aniToMal(id: String): String? { + return app.post( + anilistApi, data = mapOf( + "query" to "{Media(id:$id,type:ANIME){idMal}}", + ) + ).parsedSafe()?.data?.media?.idMal +} + +fun decode(input: String): String = URLDecoder.decode(input, "utf-8") + +private val embedBlackList = listOf( + "https://mp4upload.com/", + "https://streamsb.net/", + "https://dood.to/", + "https://videobin.co/", + "https://ok.ru", + "https://streamlare.com", + "https://filemoon", + "streaming.php", +) + +fun embedIsBlacklisted(url: String): Boolean { + return embedBlackList.any { url.contains(it) } +} + +fun AvailableEpisodesDetail.getEpisode( + lang: String, + id: String, + malId: Int?, +): List { + val meta = if (lang == "sub") this.sub else this.dub + return meta.map { eps -> + Episode( + AnichiLoadData(id, lang, eps, malId).toJson(), + episode = eps.toIntOrNull() + ) + }.reversed() +} + +suspend fun getM3u8Qualities( + m3u8Link: String, + referer: String, + qualityName: String, +): List { + return M3u8Helper.generateM3u8( + qualityName, + m3u8Link, + referer, + ) +} + +fun String.getHost(): String { + return fixTitle(URI(this).host.substringBeforeLast(".").substringAfterLast(".")) +} + +fun String.fixUrlPath() : String { + return if(this.contains(".json?")) apiEndPoint + this else apiEndPoint + URI(this).path + ".json?" + URI(this).query +} + +fun fixSourceUrls(url: String, source: String?) : String? { + return if(source == "Ak" || url.contains("/player/vitemb")) { + AppUtils.tryParseJson(base64Decode(url.substringAfter("=")))?.idUrl + } else { + url.replace(" ", "%20") + } +} \ No newline at end of file diff --git a/Animasu/build.gradle.kts b/Animasu/build.gradle.kts new file mode 100644 index 00000000..ce71e900 --- /dev/null +++ b/Animasu/build.gradle.kts @@ -0,0 +1,27 @@ +// use an integer for version numbers +version = 3 + + +cloudstream { + language = "id" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + authors = listOf("Hexated") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AnimeMovie", + "OVA", + "Anime", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=animasu.cc&sz=%size%" +} \ No newline at end of file diff --git a/Animasu/src/main/AndroidManifest.xml b/Animasu/src/main/AndroidManifest.xml new file mode 100644 index 00000000..874740e3 --- /dev/null +++ b/Animasu/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Animasu/src/main/kotlin/com/hexated/Animasu.kt b/Animasu/src/main/kotlin/com/hexated/Animasu.kt new file mode 100644 index 00000000..04efbd3a --- /dev/null +++ b/Animasu/src/main/kotlin/com/hexated/Animasu.kt @@ -0,0 +1,174 @@ +package com.hexated + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.utils.* +import org.jsoup.Jsoup +import org.jsoup.nodes.Element + +class Animasu : MainAPI() { + override var mainUrl = "https://animasu.uno" + override var name = "Animasu" + override val hasMainPage = true + override var lang = "id" + override val hasDownloadSupport = true + + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + companion object { + fun getType(t: String?): TvType { + if(t == null) return TvType.Anime + return when { + t.contains("Tv", true) -> TvType.Anime + t.contains("Movie", true) -> TvType.AnimeMovie + t.contains("OVA", true) || t.contains("Special", true) -> TvType.OVA + else -> TvType.Anime + } + } + + fun getStatus(t: String?): ShowStatus { + if(t == null) return ShowStatus.Completed + return when { + t.contains("Sedang Tayang", true) -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + } + + override val mainPage = mainPageOf( + "urutan=update" to "Baru diupdate", + "status=&tipe=&urutan=publikasi" to "Baru ditambahkan", + "status=&tipe=&urutan=populer" to "Terpopuler", + "status=&tipe=&urutan=rating" to "Rating Tertinggi", + "status=&tipe=Movie&urutan=update" to "Movie Terbaru", + "status=&tipe=Movie&urutan=populer" to "Movie Terpopuler", + ) + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + val document = app.get("$mainUrl/pencarian/?${request.data}&halaman=$page").document + val home = document.select("div.listupd div.bs").map { + it.toSearchResult() + } + return newHomePageResponse(request.name, home) + } + + private fun getProperAnimeLink(uri: String): String { + return if (uri.contains("/anime/")) { + uri + } else { + var title = uri.substringAfter("$mainUrl/") + title = when { + (title.contains("-episode")) && !(title.contains("-movie")) -> title.substringBefore( + "-episode" + ) + + (title.contains("-movie")) -> title.substringBefore("-movie") + else -> title + } + + "$mainUrl/anime/$title" + } + } + + private fun Element.toSearchResult(): AnimeSearchResponse { + val href = getProperAnimeLink(fixUrlNull(this.selectFirst("a")?.attr("href")).toString()) + val title = this.select("div.tt").text().trim() + val posterUrl = fixUrlNull(this.selectFirst("img")?.attr("src")) + val epNum = this.selectFirst("span.epx")?.text()?.filter { it.isDigit() }?.toIntOrNull() + return newAnimeSearchResponse(title, href, TvType.Anime) { + this.posterUrl = posterUrl + addSub(epNum) + } + + } + + override suspend fun search(query: String): List { + return app.get("$mainUrl/?s=$query").document.select("div.listupd div.bs").map { + it.toSearchResult() + } + } + + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document + + val title = document.selectFirst("div.infox h1")?.text().toString().replace("Sub Indo", "").trim() + val poster = document.selectFirst("div.bigcontent img")?.attr("src")?.replace("\n", "") + + val table = document.selectFirst("div.infox div.spe") + val type = getType(table?.selectFirst("span:contains(Jenis:)")?.ownText()) + val year = table?.selectFirst("span:contains(Rilis:)")?.ownText()?.substringAfterLast(",")?.trim()?.toIntOrNull() + val status = table?.selectFirst("span:contains(Status:) font")?.text() + val trailer = document.selectFirst("div.trailer iframe")?.attr("src") + val episodes = document.select("ul#daftarepisode > li").map { + val link = fixUrl(it.selectFirst("a")!!.attr("href")) + val name = it.selectFirst("a")?.text() ?: "" + val episode = Regex("Episode\\s?(\\d+)").find(name)?.groupValues?.getOrNull(0)?.toIntOrNull() + Episode(link, episode = episode) + }.reversed() + + val tracker = APIHolder.getTracker(listOf(title),TrackerType.getTypes(type),year,true) + + return newAnimeLoadResponse(title, url, type) { + posterUrl = tracker?.image ?: poster + backgroundPosterUrl = tracker?.cover + this.year = year + addEpisodes(DubStatus.Subbed, episodes) + showStatus = getStatus(status) + plot = document.select("div.sinopsis p").text() + this.tags = table?.select("span:contains(Genre:) a")?.map { it.text() } + addTrailer(trailer) + addMalId(tracker?.malId) + addAniListId(tracker?.aniId?.toIntOrNull()) + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val document = app.get(data).document + document.select(".mobius > .mirror > option").mapNotNull { + fixUrl(Jsoup.parse(base64Decode(it.attr("value"))).select("iframe").attr("src")) to it.text() + }.apmap { (iframe, quality) -> + loadFixedExtractor(iframe, quality, "$mainUrl/", subtitleCallback, callback) + } + return true + } + + private suspend fun loadFixedExtractor( + url: String, + quality: String?, + referer: String? = null, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + loadExtractor(url, referer, subtitleCallback) { link -> + callback.invoke( + ExtractorLink( + link.name, + link.name, + link.url, + link.referer, + if(link.type != ExtractorLinkType.M3U8) getIndexQuality(quality) else link.quality, + link.type, + link.headers, + link.extractorData + ) + ) + } + } + + private fun getIndexQuality(str: String?): Int { + return Regex("(\\d{3,4})[pP]").find(str ?: "")?.groupValues?.getOrNull(1)?.toIntOrNull() + ?: Qualities.Unknown.value + } + +} \ No newline at end of file diff --git a/Animasu/src/main/kotlin/com/hexated/AnimasuPlugin.kt b/Animasu/src/main/kotlin/com/hexated/AnimasuPlugin.kt new file mode 100644 index 00000000..800edd02 --- /dev/null +++ b/Animasu/src/main/kotlin/com/hexated/AnimasuPlugin.kt @@ -0,0 +1,18 @@ +package com.elostoratv +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +<<<<<<<< HEAD:ElOstora/src/main/kotlin/com/elostoratv/ElOstoraTVPlugin.kt +class ElOstoraTVPlugin: Plugin() { + override fun load(context: Context) { + registerMainAPI(ElOstoraTV()) +======== +class AnimasuPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(Animasu()) +>>>>>>>> bf89248a7d2a64a6e7dfa4049ac908b967e111a0:Animasu/src/main/kotlin/com/hexated/AnimasuPlugin.kt + } +} \ No newline at end of file diff --git a/AnimeIndoProvider/build.gradle.kts b/AnimeIndoProvider/build.gradle.kts new file mode 100644 index 00000000..f6e8b592 --- /dev/null +++ b/AnimeIndoProvider/build.gradle.kts @@ -0,0 +1,27 @@ +// use an integer for version numbers +version = 14 + + +cloudstream { + language = "id" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + authors = listOf("Hexated") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AnimeMovie", + "OVA", + "Anime", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=animeindo.fun&sz=%size%" +} \ No newline at end of file diff --git a/AnimeIndoProvider/src/main/kotlin/com/hexated/AnimeIndoProvider.kt b/AnimeIndoProvider/src/main/kotlin/com/hexated/AnimeIndoProvider.kt new file mode 100644 index 00000000..5f3d09bd --- /dev/null +++ b/AnimeIndoProvider/src/main/kotlin/com/hexated/AnimeIndoProvider.kt @@ -0,0 +1,179 @@ +package com.hexated + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.httpsify +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.Jsoup +import org.jsoup.nodes.Element + +class AnimeIndoProvider : MainAPI() { + override var mainUrl = "https://animeindo.quest" + override var name = "AnimeIndo" + override val hasMainPage = true + override var lang = "id" + + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + companion object { + fun getType(t: String): TvType { + return if (t.contains("OVA", true) || t.contains("Special")) TvType.OVA + else if (t.contains("Movie", true)) TvType.AnimeMovie + else TvType.Anime + } + + fun getStatus(t: String): ShowStatus { + return when (t) { + "Finished Airing" -> ShowStatus.Completed + "Currently Airing" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + + } + + override val mainPage = mainPageOf( + "episode-terbaru" to "Episode Terbaru", + "ongoing" to "Anime Ongoing", + "populer" to "Anime Populer", + "donghua-terbaru" to "Donghua Terbaru", + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val url = "$mainUrl/${request.data}/page/$page" + val document = app.get(url).document + val home = document.select("main#main div.animposx").mapNotNull { + it.toSearchResult() + } + return newHomePageResponse(request.name, home) + } + + private fun getProperAnimeLink(uri: String): String { + return if (uri.contains("/anime/")) { + uri + } else { + var title = uri.substringAfter("$mainUrl/") + title = when { + (title.contains("-episode")) && !(title.contains("-movie")) -> title.substringBefore( + "-episode" + ) + + (title.contains("-movie")) -> title.substringBefore("-movie") + else -> title + } + + "$mainUrl/anime/$title" + } + } + + private fun Element.toSearchResult(): AnimeSearchResponse { + val title = this.selectFirst("div.title, h2.entry-title, h4")?.text()?.trim() ?: "" + val href = getProperAnimeLink(this.selectFirst("a")!!.attr("href")) + val posterUrl = fixUrlNull(this.selectFirst("img")?.attr("src")) + val epNum = this.selectFirst("span.episode")?.ownText()?.replace(Regex("\\D"), "")?.trim() + ?.toIntOrNull() + return newAnimeSearchResponse(title, href, TvType.Anime) { + this.posterUrl = posterUrl + addSub(epNum) + } + + } + + override suspend fun search(query: String): List { + val anime = mutableListOf() + (1..2).forEach { page -> + val link = "$mainUrl/page/$page/?s=$query" + val document = app.get(link).document + val media = document.select(".site-main.relat > article").mapNotNull { + val title = it.selectFirst("div.title > h2")!!.ownText().trim() + val href = it.selectFirst("a")!!.attr("href") + val posterUrl = it.selectFirst("img")!!.attr("src").toString() + val type = getType(it.select("div.type").text().trim()) + newAnimeSearchResponse(title, href, type) { + this.posterUrl = posterUrl + } + } + if(media.isNotEmpty()) anime.addAll(media) + } + return anime + } + + override suspend fun load(url: String): LoadResponse? { + val document = app.get(url).document + + val title = document.selectFirst("h1.entry-title")?.text()?.replace("Subtitle Indonesia", "") + ?.trim() ?: return null + val poster = document.selectFirst("div.thumb > img[itemprop=image]")?.attr("src") + val tags = document.select("div.genxed > a").map { it.text() } + val type = getType(document.selectFirst("div.info-content > div.spe > span:contains(Type:)")?.ownText() + ?.trim()?.lowercase() ?: "tv") + val year = document.selectFirst("div.info-content > div.spe > span:contains(Released:)")?.ownText()?.let { + Regex("\\d,\\s(\\d*)").find(it)?.groupValues?.get(1)?.toIntOrNull() + } + val status = getStatus(document.selectFirst("div.info-content > div.spe > span:nth-child(1)")!!.ownText().trim()) + val description = document.select("div[itemprop=description] > p").text() + + val trailer = document.selectFirst("div.player-embed iframe")?.attr("src") + val episodes = document.select("div.lstepsiode.listeps ul li").mapNotNull { + val header = it.selectFirst("span.lchx > a") ?: return@mapNotNull null + val episode = header.text().trim().replace("Episode", "").trim().toIntOrNull() + val link = fixUrl(header.attr("href")) + Episode(link, episode = episode) + }.reversed() + + val recommendations = document.select("div.relat div.animposx").mapNotNull { + it.toSearchResult() + } + + val tracker = APIHolder.getTracker(listOf(title),TrackerType.getTypes(type),year,true) + + return newAnimeLoadResponse(title, url, type) { + engName = title + posterUrl = tracker?.image ?: poster + backgroundPosterUrl = tracker?.cover + this.year = year + addEpisodes(DubStatus.Subbed, episodes) + showStatus = status + plot = description + this.tags = tags + this.recommendations = recommendations + addTrailer(trailer) + addMalId(tracker?.malId) + addAniListId(tracker?.aniId?.toIntOrNull()) + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val document = app.get(data).document + document.select("div.itemleft > .mirror > option").mapNotNull { + fixUrl(Jsoup.parse(base64Decode(it.attr("value"))).select("iframe").attr("src")) + }.apmap { + if (it.startsWith(mainUrl)) { + app.get(it, referer = "$mainUrl/").document.select("iframe").attr("src") + } else { + it + } + }.apmap { + loadExtractor(httpsify(it), data, subtitleCallback, callback) + } + + return true + } + +} \ No newline at end of file diff --git a/AnimeSailProvider/build.gradle.kts b/AnimeSailProvider/build.gradle.kts new file mode 100644 index 00000000..cea32bdc --- /dev/null +++ b/AnimeSailProvider/build.gradle.kts @@ -0,0 +1,27 @@ +// use an integer for version numbers +version = 10 + + +cloudstream { + language = "id" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + authors = listOf("Hexated") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AnimeMovie", + "Anime", + "OVA", + ) + + iconUrl = "https://aghanim.xyz/wp-content/themes/animesail/assets/images/ico.png" +} \ No newline at end of file diff --git a/AnimeSailProvider/src/main/kotlin/com/hexated/AnimeSailProvider.kt b/AnimeSailProvider/src/main/kotlin/com/hexated/AnimeSailProvider.kt new file mode 100644 index 00000000..721121a2 --- /dev/null +++ b/AnimeSailProvider/src/main/kotlin/com/hexated/AnimeSailProvider.kt @@ -0,0 +1,230 @@ +package com.hexated + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId +import com.lagradost.cloudstream3.mvvm.safeApiCall +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.ExtractorLinkType +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.loadExtractor +import com.lagradost.nicehttp.NiceResponse +import org.jsoup.Jsoup +import org.jsoup.nodes.Element + +class AnimeSailProvider : MainAPI() { + override var mainUrl = "https://154.26.137.28" + override var name = "AnimeSail" + override val hasMainPage = true + override var lang = "id" + override val hasDownloadSupport = true + + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + companion object { + fun getType(t: String): TvType { + return if (t.contains("OVA", true) || t.contains("Special")) TvType.OVA + else if (t.contains("Movie", true)) TvType.AnimeMovie + else TvType.Anime + } + + fun getStatus(t: String): ShowStatus { + return when (t) { + "Completed" -> ShowStatus.Completed + "Ongoing" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + } + + private suspend fun request(url: String, ref: String? = null): NiceResponse { + return app.get( + url, + headers = mapOf("Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"), + cookies = mapOf("_as_ipin_ct" to "ID"), + referer = ref + ) + } + + override val mainPage = mainPageOf( + "$mainUrl/page/" to "Episode Terbaru", + "$mainUrl/movie-terbaru/page/" to "Movie Terbaru", + "$mainUrl/genres/donghua/page/" to "Donghua" + ) + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + val document = request(request.data + page).document + val home = document.select("article").map { + it.toSearchResult() + } + return newHomePageResponse(request.name, home) + } + + private fun getProperAnimeLink(uri: String): String { + return if (uri.contains("/anime/")) { + uri + } else { + var title = uri.substringAfter("$mainUrl/") + title = when { + (title.contains("-episode")) && !(title.contains("-movie")) -> title.substringBefore( + "-episode" + ) + (title.contains("-movie")) -> title.substringBefore("-movie") + else -> title + } + + "$mainUrl/anime/$title" + } + } + + private fun Element.toSearchResult(): AnimeSearchResponse { + val href = getProperAnimeLink(fixUrlNull(this.selectFirst("a")?.attr("href")).toString()) + val title = this.select(".tt > h2").text().trim() + val posterUrl = fixUrlNull(this.selectFirst("div.limit img")?.attr("src")) + val epNum = this.selectFirst(".tt > h2")?.text()?.let { + Regex("Episode\\s?(\\d+)").find(it)?.groupValues?.getOrNull(1)?.toIntOrNull() + } + return newAnimeSearchResponse(title, href, TvType.Anime) { + this.posterUrl = posterUrl + addSub(epNum) + } + + } + + override suspend fun search(query: String): List { + val link = "$mainUrl/?s=$query" + val document = request(link).document + + return document.select("div.listupd article").map { + it.toSearchResult() + } + } + + override suspend fun load(url: String): LoadResponse { + val document = request(url).document + + val title = document.selectFirst("h1.entry-title")?.text().toString() + .replace("Subtitle Indonesia", "").trim() + val poster = document.selectFirst("div.entry-content > img")?.attr("src") + val type = getType(document.select("tbody th:contains(Tipe)").next().text().lowercase()) + val year = document.select("tbody th:contains(Dirilis)").next().text().trim().toIntOrNull() + + val episodes = document.select("ul.daftar > li").map { + val link = fixUrl(it.select("a").attr("href")) + val name = it.select("a").text() + val episode = Regex("Episode\\s?(\\d+)").find(name)?.groupValues?.getOrNull(0)?.toIntOrNull() + Episode(link, episode = episode) + }.reversed() + + val tracker = APIHolder.getTracker(listOf(title),TrackerType.getTypes(type),year,true) + + return newAnimeLoadResponse(title, url, type) { + posterUrl = tracker?.image ?: poster + backgroundPosterUrl = tracker?.cover + this.year = year + addEpisodes(DubStatus.Subbed, episodes) + showStatus = + getStatus(document.select("tbody th:contains(Status)").next().text().trim()) + plot = document.selectFirst("div.entry-content > p")?.text() + this.tags = + document.select("tbody th:contains(Genre)").next().select("a").map { it.text() } + addMalId(tracker?.malId) + addAniListId(tracker?.aniId?.toIntOrNull()) + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val document = request(data).document + + document.select(".mobius > .mirror > option").apmap { + safeApiCall { + val iframe = fixUrl( + Jsoup.parse(base64Decode(it.attr("data-em"))).select("iframe").attr("src") + ?: throw ErrorLoadingException("No iframe found") + ) + val quality = getIndexQuality(it.text()) + when { + iframe.startsWith("$mainUrl/utils/player/arch/") || iframe.startsWith( + "$mainUrl/utils/player/race/" + ) -> request(iframe, ref = data).document.select("source").attr("src") + .let { link -> + val source = + when { + iframe.contains("/arch/") -> "Arch" + iframe.contains("/race/") -> "Race" + else -> this.name + } + callback.invoke( + ExtractorLink( + source = source, + name = source, + url = link, + referer = mainUrl, + quality = quality + ) + ) + } +// skip for now +// iframe.startsWith("$mainUrl/utils/player/fichan/") -> "" +// iframe.startsWith("$mainUrl/utils/player/blogger/") -> "" + iframe.startsWith("https://aghanim.xyz/tools/redirect/") -> { + val link = "https://rasa-cintaku-semakin-berantai.xyz/v/${ + iframe.substringAfter("id=").substringBefore("&token") + }" + loadFixedExtractor(link, quality, mainUrl, subtitleCallback, callback) + } + iframe.startsWith("$mainUrl/utils/player/framezilla/") || iframe.startsWith("https://uservideo.xyz") -> { + request(iframe, ref = data).document.select("iframe").attr("src") + .let { link -> + loadFixedExtractor(fixUrl(link), quality, mainUrl, subtitleCallback, callback) + } + } + else -> { + loadFixedExtractor(iframe, quality, mainUrl, subtitleCallback, callback) + } + } + } + } + + return true + } + + private suspend fun loadFixedExtractor( + url: String, + quality: Int?, + referer: String? = null, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + loadExtractor(url, referer, subtitleCallback) { link -> + callback.invoke( + ExtractorLink( + link.name, + link.name, + link.url, + link.referer, + if(link.type == ExtractorLinkType.M3U8) link.quality else quality ?: Qualities.Unknown.value, + link.type, + link.headers, + link.extractorData + ) + ) + } + } + + private fun getIndexQuality(str: String): Int { + return Regex("(\\d{3,4})[pP]").find(str)?.groupValues?.getOrNull(1)?.toIntOrNull() + ?: Qualities.Unknown.value + } + +} \ No newline at end of file diff --git a/Aniworld/build.gradle.kts b/Aniworld/build.gradle.kts new file mode 100644 index 00000000..86c61272 --- /dev/null +++ b/Aniworld/build.gradle.kts @@ -0,0 +1,27 @@ +// use an integer for version numbers +version = 6 + + +cloudstream { + language = "de" + // All of these properties are optional, you can safely remove them + + description = "Include: Serienstream" + authors = listOf("Hexated") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AnimeMovie", + "Anime", + "OVA", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=aniworld.to&sz=%size%" +} \ No newline at end of file diff --git a/Aniworld/src/main/kotlin/com/hexated/Aniworld.kt b/Aniworld/src/main/kotlin/com/hexated/Aniworld.kt new file mode 100644 index 00000000..f55ac59e --- /dev/null +++ b/Aniworld/src/main/kotlin/com/hexated/Aniworld.kt @@ -0,0 +1,232 @@ +package com.hexated + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addActors +import com.lagradost.cloudstream3.extractors.DoodLaExtractor +import com.lagradost.cloudstream3.extractors.Voe +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.M3u8Helper +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.net.URI + +open class Aniworld : MainAPI() { + override var mainUrl = "https://aniworld.to" + override var name = "Aniworld" + override val hasMainPage = true + override var lang = "de" + + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + companion object { + fun getType(t: String): TvType { + return when { + t.contains("Anime Ova") -> TvType.OVA + t.contains("Anime Movie") -> TvType.AnimeMovie + else -> TvType.Anime + } + } + + fun getStatus(t: String): ShowStatus { + return when { + t.contains("/complete", true) -> ShowStatus.Completed + t.contains("/running", true) -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + } + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + + val document = app.get(mainUrl).document + val item = arrayListOf() + document.select("div.carousel").map { ele -> + val header = ele.selectFirst("h2")?.text() ?: return@map + val home = ele.select("div.coverListItem").mapNotNull { + it.toSearchResult() + } + if (home.isNotEmpty()) item.add(HomePageList(header, home)) + } + return HomePageResponse(item) + } + + override suspend fun search(query: String): List { + val json = app.post( + "$mainUrl/ajax/search", + data = mapOf("keyword" to query), + referer = "$mainUrl/search", + headers = mapOf( + "x-requested-with" to "XMLHttpRequest" + ) + ) + return tryParseJson>(json.text)?.filter { + !it.link.contains("episode-") && it.link.contains( + "/stream" + ) + }?.map { + newAnimeSearchResponse( + it.title?.replace(Regex(""), "") ?: "", + fixUrl(it.link), + TvType.Anime + ) { + } + } ?: throw ErrorLoadingException() + + } + + override suspend fun load(url: String): LoadResponse? { + val document = app.get(url).document + + val title = document.selectFirst("div.series-title span")?.text() ?: return null + val poster = fixUrlNull(document.selectFirst("div.seriesCoverBox img")?.attr("data-src")) + val tags = document.select("div.genres li a").map { it.text() } + val year = document.selectFirst("span[itemprop=startDate] a")?.text()?.toIntOrNull() + val description = document.select("p.seri_des").text() + val actor = + document.select("li:contains(Schauspieler:) ul li a").map { it.select("span").text() } + + val episodes = mutableListOf() + document.select("div#stream > ul:first-child li").map { ele -> + val page = ele.selectFirst("a") + val epsDocument = app.get(fixUrl(page?.attr("href") ?: return@map)).document + epsDocument.select("div#stream > ul:nth-child(4) li").mapNotNull { eps -> + episodes.add( + Episode( + fixUrl(eps.selectFirst("a")?.attr("href") ?: return@mapNotNull null), + episode = eps.selectFirst("a")?.text()?.toIntOrNull(), + season = page.text().toIntOrNull() + ) + ) + } + } + + return newAnimeLoadResponse( + title, + url, + TvType.Anime + ) { + engName = title + posterUrl = poster + this.year = year + addEpisodes( + DubStatus.Subbed, + episodes + ) + addActors(actor) + plot = description + this.tags = tags + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val document = app.get(data).document + document.select("div.hosterSiteVideo ul li").map { + Triple( + it.attr("data-lang-key"), + it.attr("data-link-target"), + it.select("h4").text() + ) + }.filter { + it.third != "Vidoza" + }.apmap { + val redirectUrl = app.get(fixUrl(it.second)).url + val lang = it.first.getLanguage(document) + if (it.third == "VOE") { + invokeVoe(redirectUrl, lang, data, callback) + } else { + loadExtractor(redirectUrl, data, subtitleCallback) { link -> + val name = "${link.name} [${lang}]" + callback.invoke( + ExtractorLink( + name, + name, + link.url, + link.referer, + link.quality, + link.type, + link.headers, + link.extractorData + ) + ) + } + } + } + + return true + } + + private suspend fun invokeVoe( + url: String, + lang: String?, + referer: String, + callback: (ExtractorLink) -> Unit, + ) { + val name = "Voe [${lang}]" + val request = app.get(url, referer = referer) + val baseUrl = getBaseUrl(request.url) + val res = request.document + val script = res.select("script").find { it.data().contains("sources =") }?.data() + val link = + Regex("[\"']hls[\"']:\\s*[\"'](.*)[\"']").find(script ?: return)?.groupValues?.get(1) + + M3u8Helper.generateM3u8( + name, + link ?: return, + "$baseUrl/", + headers = mapOf("Origin" to "$baseUrl/") + ).forEach(callback) + } + + private fun Element.toSearchResult(): AnimeSearchResponse? { + val href = fixUrlNull(this.selectFirst("a")?.attr("href")) ?: return null + val title = this.selectFirst("h3")?.text() ?: return null + val posterUrl = fixUrlNull(this.selectFirst("img")?.attr("data-src")) + return newAnimeSearchResponse(title, href, TvType.Anime) { + this.posterUrl = posterUrl + } + } + + private fun String.getLanguage(document: Document): String? { + return document.selectFirst("div.changeLanguageBox img[data-lang-key=$this]")?.attr("title") + ?.removePrefix("mit")?.trim() + } + + private fun getBaseUrl(url: String): String { + return URI(url).let { + "${it.scheme}://${it.host}" + } + } + + private data class AnimeSearch( + @JsonProperty("link") val link: String, + @JsonProperty("title") val title: String? = null, + ) + +} + +class Dooood : DoodLaExtractor() { + override var mainUrl = "https://urochsunloath.com" +} + +class Simpulumlamerop : Voe() { + override var mainUrl = "https://simpulumlamerop.com" +} + +class Urochsunloath : Voe() { + override var mainUrl = "https://urochsunloath.com" +} \ No newline at end of file diff --git a/Aniworld/src/main/kotlin/com/hexated/AniworldPlugin.kt b/Aniworld/src/main/kotlin/com/hexated/AniworldPlugin.kt new file mode 100644 index 00000000..7eee506e --- /dev/null +++ b/Aniworld/src/main/kotlin/com/hexated/AniworldPlugin.kt @@ -0,0 +1,18 @@ + +package com.hexated + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class AniworldPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(Aniworld()) + registerMainAPI(Serienstream()) + registerExtractorAPI(Urochsunloath()) + registerExtractorAPI(Simpulumlamerop()) + registerExtractorAPI(Dooood()) + } +} \ No newline at end of file diff --git a/Aniworld/src/main/kotlin/com/hexated/Serienstream.kt b/Aniworld/src/main/kotlin/com/hexated/Serienstream.kt new file mode 100644 index 00000000..ff475aa6 --- /dev/null +++ b/Aniworld/src/main/kotlin/com/hexated/Serienstream.kt @@ -0,0 +1,17 @@ +package com.hexated + +import com.lagradost.cloudstream3.LoadResponse +import com.lagradost.cloudstream3.TvType + +class Serienstream : Aniworld() { + override var mainUrl = "https://s.to" + override var name = "Serienstream" + override val supportedTypes = setOf( + TvType.Movie, + TvType.TvSeries, + ) + + override suspend fun load(url: String): LoadResponse? { + return super.load(url).apply { this?.type = TvType.TvSeries } + } +} \ No newline at end of file diff --git a/DramaSerial/src/main/kotlin/com/hexated/DramaSerial.kt b/DramaSerial/src/main/kotlin/com/hexated/DramaSerial.kt new file mode 100644 index 00000000..98047d9f --- /dev/null +++ b/DramaSerial/src/main/kotlin/com/hexated/DramaSerial.kt @@ -0,0 +1,130 @@ +package com.hexated + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.extractors.Filesim +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.nodes.Element + +class DramaSerial : MainAPI() { + override var mainUrl = "https://dramaserial.sbs" + override var name = "DramaSerial" + override val hasMainPage = true + override var lang = "id" + override val hasDownloadSupport = true + override val supportedTypes = setOf(TvType.AsianDrama) + + override val mainPage = mainPageOf( + "$mainUrl/page/" to "Latest Movie", + "$mainUrl/Genre/ongoing/page/" to "Ongoing", + "$mainUrl/Genre/drama-serial-korea/page/" to "Drama Serial Korea", + "$mainUrl/Genre/drama-serial-jepang/page/" to "Drama Serial Jepang", + "$mainUrl/Genre/drama-serial-mandarin/page/" to "Drama Serial Mandarin", + "$mainUrl/Genre/drama-serial-filipina/page/" to "Drama Serial Filipina", + "$mainUrl/Genre/drama-serial-india/page/" to "Drama Serial India", + ) + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + val document = app.get(request.data + page).document + val home = document.select("main#main article").mapNotNull { + it.toSearchResult() + } + return newHomePageResponse(request.name, home) + } + + private fun Element.toSearchResult(): SearchResponse? { + val href = fixUrl(this.selectFirst("a")!!.attr("href")) + val title = this.selectFirst("h2.entry-title a")?.text()?.trim() ?: return null + val posterUrl = fixUrlNull(this.selectFirst("img")?.attr("src")) + val episode = + this.selectFirst("div.gmr-episode-item")?.text()?.filter { it.isDigit() }?.toIntOrNull() + + return newAnimeSearchResponse(title, href, TvType.AsianDrama) { + this.posterUrl = posterUrl + addSub(episode) + } + } + + override suspend fun search(query: String): List { + val link = "$mainUrl/?s=$query&post_type[]=post&post_type[]=tv" + val document = app.get(link).document + + return document.select("main#main article").mapNotNull { + it.toSearchResult() + } + } + + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document + + val title = document.selectFirst("h1.entry-title")!!.text().trim() + val poster = fixUrlNull(document.selectFirst("figure.pull-left img")?.attr("src")) + val tags = + document.select("div.gmr-movie-innermeta span:contains(Genre:) a").map { it.text() } + val year = + document.selectFirst("div.gmr-movie-innermeta span:contains(Year:) a")!!.text().trim() + .toIntOrNull() + val duration = + document.selectFirst("div.gmr-movie-innermeta span:contains(Duration:)")?.text() + ?.filter { it.isDigit() }?.toIntOrNull() + val description = document.select("div.entry-content.entry-content-single div.entry-content.entry-content-single").text().trim() + val type = if(document.select("div.page-links").isNullOrEmpty()) TvType.Movie else TvType.AsianDrama + + if (type == TvType.Movie) { + return newMovieLoadResponse(title, url, TvType.Movie, url) { + posterUrl = poster + this.year = year + plot = description + this.tags = tags + this.duration = duration + } + } else { + val episodes = document.select("div.page-links span.page-link-number").mapNotNull { eps -> + val episode = eps.text().filter { it.isDigit() }.toIntOrNull() + val link = if(episode == 1) { + url + } else { + eps.parent()?.attr("href") + } + Episode( + link ?: return@mapNotNull null, + episode = episode, + ) + } + return newTvSeriesLoadResponse(title, url, TvType.AsianDrama, episodes = episodes) { + posterUrl = poster + this.year = year + this.duration = duration + plot = description + this.tags = tags + } + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val document = app.get(data).document + + val iframe = document.select("div.gmr-server-wrap iframe").attr("src") + app.get(iframe, referer = "$mainUrl/").document.select("div#header-slider ul li").apmap { mLink -> + mLink.attr("onclick").substringAfter("frame('").substringBefore("')").let { iLink -> + val uLink = app.get(iLink, referer = iframe).document.select("script").find { it.data().contains("(document).ready") }?.data()?.substringAfter("replace(\"")?.substringBefore("\");") ?: return@apmap null + val link = app.get(uLink, referer = iLink).document.selectFirst("iframe")?.attr("src") ?: return@apmap null + loadExtractor(fixUrl(link), "https://juraganfilm.info/", subtitleCallback, callback) + } + } + + return true + + } + +} + +class Bk21 : Filesim() { + override val name = "Bk21" + override var mainUrl = "https://bk21.net" +} diff --git a/DramaSerial/src/main/kotlin/com/hexated/DramaSerialPlugin.kt b/DramaSerial/src/main/kotlin/com/hexated/DramaSerialPlugin.kt new file mode 100644 index 00000000..5d5de498 --- /dev/null +++ b/DramaSerial/src/main/kotlin/com/hexated/DramaSerialPlugin.kt @@ -0,0 +1,15 @@ + +package com.hexated + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class DramaSerialPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(DramaSerial()) + registerExtractorAPI(Bk21()) + } +} \ No newline at end of file diff --git a/DramaidProvider/build.gradle.kts b/DramaidProvider/build.gradle.kts new file mode 100644 index 00000000..476c5d00 --- /dev/null +++ b/DramaidProvider/build.gradle.kts @@ -0,0 +1,26 @@ +// use an integer for version numbers +version = 9 + + +cloudstream { + language = "id" + // All of these properties are optional, you can safely remove them + + description = "Include: Oppadrama" + authors = listOf("Hexated") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AsianDrama", + "Movie", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=dramaid.asia&sz=%size%" +} \ No newline at end of file diff --git a/DramaidProvider/src/main/kotlin/com/hexated/DramaidProvider.kt b/DramaidProvider/src/main/kotlin/com/hexated/DramaidProvider.kt new file mode 100644 index 00000000..17f9b7e1 --- /dev/null +++ b/DramaidProvider/src/main/kotlin/com/hexated/DramaidProvider.kt @@ -0,0 +1,212 @@ +package com.hexated + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.extractors.XStreamCdn +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.getQualityFromName +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.Jsoup +import org.jsoup.nodes.Element + +open class DramaidProvider : MainAPI() { + override var mainUrl = "https://dramaid.best" + override var name = "DramaId" + override val hasMainPage = true + override var lang = "id" + override val supportedTypes = setOf(TvType.AsianDrama) + + companion object { + fun getStatus(t: String): ShowStatus { + return when (t) { + "Completed" -> ShowStatus.Completed + "Ongoing" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + + fun getType(t: String?): TvType { + return when { + t?.contains("Movie", true) == true -> TvType.Movie + t?.contains("Anime", true) == true -> TvType.Anime + else -> TvType.AsianDrama + } + } + } + + override val mainPage = mainPageOf( + "&status=&type=&order=update" to "Drama Terbaru", + "&order=latest" to "Baru Ditambahkan", + "&status=&type=&order=popular" to "Drama Popular", + ) + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + val document = app.get("$mainUrl/series/?page=$page${request.data}").document + val home = document.select("article[itemscope=itemscope]").mapNotNull { + it.toSearchResult() + } + return newHomePageResponse(request.name, home) + } + + private fun getProperDramaLink(uri: String): String { + return if (uri.contains("-episode-")) { + "$mainUrl/series/" + Regex("$mainUrl/(.+)-ep.+").find(uri)?.groupValues?.get(1) + } else { + uri + } + } + + private fun Element.toSearchResult(): SearchResponse? { + val href = getProperDramaLink(this.selectFirst("a.tip")!!.attr("href")) + val title = this.selectFirst("h2[itemprop=headline]")?.text()?.trim() ?: return null + val posterUrl = fixUrlNull(this.select("img:last-child").attr("src")) + + return newTvSeriesSearchResponse(title, href, TvType.AsianDrama) { + this.posterUrl = posterUrl + } + } + + override suspend fun search(query: String): List { + val document = app.get("$mainUrl/?s=$query").document + return document.select("article[itemscope=itemscope]").mapNotNull { + it.toSearchResult() + } + } + + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document + + val title = document.selectFirst("h1.entry-title")?.text()?.trim() ?: "" + val poster = fixUrlNull(document.select("div.thumb img:last-child").attr("src")) + val tags = document.select(".genxed > a").map { it.text() } + val type = document.selectFirst(".info-content .spe span:contains(Tipe:)")?.ownText() + val year = Regex("\\d, ([0-9]*)").find( + document.selectFirst(".info-content > .spe > span > time")!!.text().trim() + )?.groupValues?.get(1).toString().toIntOrNull() + val status = getStatus( + document.select(".info-content > .spe > span:nth-child(1)") + .text().trim().replace("Status: ", "") + ) + val description = document.select(".entry-content > p").text().trim() + + val episodes = document.select(".eplister > ul > li").mapNotNull { + val name = it.selectFirst("a > .epl-title")?.text() + val link = fixUrl(it.selectFirst("a")?.attr("href") ?: return@mapNotNull null) + val epNum = it.selectFirst(".epl-num")?.text()?.toIntOrNull() + Episode( + link, + name, + episode = epNum + ) + }.reversed() + + val recommendations = + document.select(".listupd > article[itemscope=itemscope]").mapNotNull { rec -> + rec.toSearchResult() + } + + return newTvSeriesLoadResponse( + title, + url, + getType(type), + episodes = episodes + ) { + posterUrl = poster + this.year = year + showStatus = status + plot = description + this.tags = tags + this.recommendations = recommendations + } + + } + + private data class Sources( + @JsonProperty("file") val file: String, + @JsonProperty("label") val label: String, + @JsonProperty("type") val type: String, + @JsonProperty("default") val default: Boolean? + ) + + private data class Tracks( + @JsonProperty("file") val file: String, + @JsonProperty("label") val label: String, + @JsonProperty("kind") val type: String, + @JsonProperty("default") val default: Boolean? + ) + + private suspend fun invokeDriveSource( + url: String, + name: String, + subCallback: (SubtitleFile) -> Unit, + sourceCallback: (ExtractorLink) -> Unit + ) { + val server = app.get(url).document.selectFirst(".picasa")?.nextElementSibling()?.data() + + val source = "[${server!!.substringAfter("sources: [").substringBefore("],")}]".trimIndent() + val trackers = server.substringAfter("tracks:[").substringBefore("],") + .replace("//language", "") + .replace("file", "\"file\"") + .replace("label", "\"label\"") + .replace("kind", "\"kind\"").trimIndent() + + tryParseJson>(source)?.map { + sourceCallback( + ExtractorLink( + name, + "Drive", + fixUrl(it.file), + referer = "https://motonews.club/", + quality = getQualityFromName(it.label) + ) + ) + } + + tryParseJson(trackers)?.let { + subCallback.invoke( + SubtitleFile( + if (it.label.contains("Indonesia")) "${it.label}n" else it.label, + it.file + ) + ) + } + + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val document = app.get(data).document + val sources = document.select(".mobius > .mirror > option").mapNotNull { + fixUrl(Jsoup.parse(base64Decode(it.attr("value"))).select("iframe").attr("src")) + } + + sources.map { + it.replace("https://ndrama.xyz", "https://www.fembed.com") + }.apmap { + when { + it.contains("motonews") -> invokeDriveSource( + it, + this.name, + subtitleCallback, + callback + ) + + else -> loadExtractor(it, "$mainUrl/", subtitleCallback, callback) + } + } + + return true + } + +} + +class Vanfem : XStreamCdn() { + override val name: String = "Vanfem" + override val mainUrl: String = "https://vanfem.com" +} + diff --git a/DramaidProvider/src/main/kotlin/com/hexated/DramaidProviderPlugin.kt b/DramaidProvider/src/main/kotlin/com/hexated/DramaidProviderPlugin.kt new file mode 100644 index 00000000..536b3b2c --- /dev/null +++ b/DramaidProvider/src/main/kotlin/com/hexated/DramaidProviderPlugin.kt @@ -0,0 +1,17 @@ + +package com.hexated + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class DramaidProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(DramaidProvider()) + registerMainAPI(Oppadrama()) + registerExtractorAPI(Vanfem()) + registerExtractorAPI(Filelions()) + } +} \ No newline at end of file diff --git a/DramaidProvider/src/main/kotlin/com/hexated/Oppadrama.kt b/DramaidProvider/src/main/kotlin/com/hexated/Oppadrama.kt new file mode 100644 index 00000000..21a0aeb7 --- /dev/null +++ b/DramaidProvider/src/main/kotlin/com/hexated/Oppadrama.kt @@ -0,0 +1,13 @@ +package com.hexated + +import com.lagradost.cloudstream3.extractors.Filesim + +class Oppadrama : DramaidProvider() { + override var mainUrl = "http://185.217.95.34" + override var name = "Oppadrama" +} + +class Filelions : Filesim() { + override val name = "Filelions" + override var mainUrl = "https://filelions.live" +} \ No newline at end of file diff --git a/ElOstora/src/main/kotlin/com/elostoratv/ElOstoraTVPlugin.kt b/ElOstora/src/main/kotlin/com/elostoratv/ElOstoraTVPlugin.kt index c3c98323..800edd02 100644 --- a/ElOstora/src/main/kotlin/com/elostoratv/ElOstoraTVPlugin.kt +++ b/ElOstora/src/main/kotlin/com/elostoratv/ElOstoraTVPlugin.kt @@ -4,8 +4,15 @@ import com.lagradost.cloudstream3.plugins.Plugin import android.content.Context @CloudstreamPlugin +<<<<<<<< HEAD:ElOstora/src/main/kotlin/com/elostoratv/ElOstoraTVPlugin.kt class ElOstoraTVPlugin: Plugin() { override fun load(context: Context) { registerMainAPI(ElOstoraTV()) +======== +class AnimasuPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(Animasu()) +>>>>>>>> bf89248a7d2a64a6e7dfa4049ac908b967e111a0:Animasu/src/main/kotlin/com/hexated/AnimasuPlugin.kt } } \ No newline at end of file diff --git a/FilmPalast/build.gradle.kts b/FilmPalast/build.gradle.kts new file mode 100644 index 00000000..6f3a4bdc --- /dev/null +++ b/FilmPalast/build.gradle.kts @@ -0,0 +1,24 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + // All of these properties are optional, you can safely remove them + + description = "German Filmpalast provider" + authors = listOf("Bnyro") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + + // List of video source types. Users are able to filter for extensions in a given category. + // You can find a list of avaliable types here: + // https://recloudstream.github.io/cloudstream/html/app/com.lagradost.cloudstream3/-tv-type/index.html + tvTypes = listOf("TvSeries", "Movie") +} diff --git a/FilmPalast/src/main/AndroidManifest.xml b/FilmPalast/src/main/AndroidManifest.xml new file mode 100644 index 00000000..1863f02a --- /dev/null +++ b/FilmPalast/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/FilmPalast/src/main/kotlin/com/example/FilmpalastExtractors.kt b/FilmPalast/src/main/kotlin/com/example/FilmpalastExtractors.kt new file mode 100644 index 00000000..11611566 --- /dev/null +++ b/FilmPalast/src/main/kotlin/com/example/FilmpalastExtractors.kt @@ -0,0 +1,30 @@ +package com.example + +import com.lagradost.cloudstream3.extractors.Filesim +import com.lagradost.cloudstream3.extractors.StreamTape +import com.lagradost.cloudstream3.extractors.Streamhub +import com.lagradost.cloudstream3.extractors.Voe + +class StreamTapeTo : StreamTape() { + override var mainUrl = "https://streamtape.com" +} + +class StreamHubGg : Streamhub() { + override var name = "Streamhub Gg" + override var mainUrl = "https://streamhub.gg" +} + +class VoeSx: Voe() { + override val name = "Voe Sx" + override val mainUrl = "https://voe.sx" +} + +class MetaGnathTuggers : Voe() { + override val name = "Metagnathtuggers" + override val mainUrl = "https://metagnathtuggers.com" +} + +class FileLions : Filesim() { + override val name = "Filelions" + override var mainUrl = "https://filelions.to" +} diff --git a/FilmPalast/src/main/kotlin/com/example/FilmpalastPlugin.kt b/FilmPalast/src/main/kotlin/com/example/FilmpalastPlugin.kt new file mode 100644 index 00000000..023c0f59 --- /dev/null +++ b/FilmPalast/src/main/kotlin/com/example/FilmpalastPlugin.kt @@ -0,0 +1,18 @@ +package com.example + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class FilmpalastPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(FilmpalastProvider()) + registerExtractorAPI(StreamTapeTo()) + registerExtractorAPI(StreamHubGg()) + registerExtractorAPI(VoeSx()) + registerExtractorAPI(MetaGnathTuggers()) + registerExtractorAPI(FileLions()) + } +} \ No newline at end of file diff --git a/FilmPalast/src/main/kotlin/com/example/FilmpalastProvider.kt b/FilmPalast/src/main/kotlin/com/example/FilmpalastProvider.kt new file mode 100644 index 00000000..7c606967 --- /dev/null +++ b/FilmPalast/src/main/kotlin/com/example/FilmpalastProvider.kt @@ -0,0 +1,82 @@ +package com.example + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.nodes.Element +import org.jsoup.select.Elements + +class FilmpalastProvider : MainAPI() { + override var mainUrl = "https://filmpalast.to" + override var name = "Filmpalast" + override val supportedTypes = setOf(TvType.Movie, TvType.TvSeries) + + override var lang = "de" + override val hasMainPage = true + + private fun Element.toSearchResponse(): SearchResponse { + val title = select("cite a.rb").text() + val url = select("a.rb").attr("href") + val posterPath = select("img.cover-opacity").attr("src") + return newMovieSearchResponse(title, type = TvType.Movie, url = url).apply { + this.posterUrl = "$mainUrl$posterPath" + } + } + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + val movies = app.get("$mainUrl/movies/top").document + val movieResults = movies.select("#content .liste.rb").mapNotNull { + it.toSearchResponse() + } + val series = app.get("$mainUrl/serien/view").document + val seriesResults = series.select("#content .liste.rb").mapNotNull { + it.toSearchResponse() + } + val homePageLists = listOf(HomePageList("Movies", movieResults), HomePageList("Series", seriesResults)) + return newHomePageResponse(homePageLists, hasNext = false) + } + + override suspend fun search(query: String): List { + val document = app.get("$mainUrl/search/title/$query").document + return document.select("#content .glowliste").mapNotNull { + it.toSearchResponse() + } + } + + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document.select("#content") + + val title = document.select("h2.rb.bgDark").text() + val imagePath = document.select(".detail.rb img.cover2").attr("src") + val description = document.select("span[itemprop=description]").text() + val details = document.select("detail-content-list li") + val year = details.first()?.html()?.split("
")?.getOrNull(1)?.filter { it.isDigit() }?.toIntOrNull() + val duration = details.select("em").first()?.ownText()?.filter { it.isDigit() }?.toIntOrNull() + + val links = document.select(".currentStreamLinks a.iconPlay").mapNotNull { + it.attr("href") ?: it.attr("data-player-url") + } + return newMovieLoadResponse(title, url, TvType.Movie, links.toJson()).apply { + this.posterUrl = "$mainUrl$imagePath" + this.plot = description + this.duration = duration + this.year = year + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val links = parseJson>(data) + links.apmap { + val link = fixUrlNull(it) ?: return@apmap null + loadExtractor(link, "$mainUrl/", subtitleCallback, callback) + } + return links.isNotEmpty() + } +} diff --git a/Gomov/build.gradle.kts b/Gomov/build.gradle.kts new file mode 100644 index 00000000..cae88ee2 --- /dev/null +++ b/Gomov/build.gradle.kts @@ -0,0 +1,27 @@ +// use an integer for version numbers +version = 11 + + +cloudstream { + language = "id" + // All of these properties are optional, you can safely remove them + + description = "Includes: DutaMovie, Ngefilm, Nodrakorid, Multiplex" + authors = listOf("Hexated") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AsianDrama", + "TvSeries", + "Movie", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=gomov.bio&sz=%size%" +} \ No newline at end of file diff --git a/Gomov/src/main/AndroidManifest.xml b/Gomov/src/main/AndroidManifest.xml new file mode 100644 index 00000000..ec832ddb --- /dev/null +++ b/Gomov/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Gomov/src/main/kotlin/com/hexated/DutaMovie.kt b/Gomov/src/main/kotlin/com/hexated/DutaMovie.kt new file mode 100644 index 00000000..08bf2b87 --- /dev/null +++ b/Gomov/src/main/kotlin/com/hexated/DutaMovie.kt @@ -0,0 +1,37 @@ +package com.hexated + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.httpsify +import com.lagradost.cloudstream3.utils.loadExtractor + +open class DutaMovie : Gomov() { + override var mainUrl = "https://dutamovie21.live" + override var name = "DutaMovie" + + override val mainPage = mainPageOf( + "category/box-office/page/%d/" to "Box Office", + "category/serial-tv/page/%d/" to "Serial TV", + "category/animation/page/%d/" to "Animasi", + "country/korea/page/%d/" to "Serial TV Korea", + "country/indonesia/page/%d/" to "Serial TV Indonesia", + ) + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + app.get(data).document.select("ul.muvipro-player-tabs li a").apmap { + val iframe = app.get(fixUrl(it.attr("href"))).document.selectFirst("div.gmr-embed-responsive iframe") + ?.attr("src") + loadExtractor(httpsify(iframe ?: return@apmap ), "$mainUrl/", subtitleCallback, callback) + } + + return true + } + + +} \ No newline at end of file diff --git a/Gomov/src/main/kotlin/com/hexated/Extractors.kt b/Gomov/src/main/kotlin/com/hexated/Extractors.kt new file mode 100644 index 00000000..aa629966 --- /dev/null +++ b/Gomov/src/main/kotlin/com/hexated/Extractors.kt @@ -0,0 +1,37 @@ +package com.hexated + +import com.lagradost.cloudstream3.extractors.* + +class Doods : DoodLaExtractor() { + override var name = "Doods" + override var mainUrl = "https://doods.pro" +} + +class Dutamovie21 : StreamSB() { + override var name = "Dutamovie21" + override var mainUrl = "https://dutamovie21.xyz" +} + +class FilelionsTo : Filesim() { + override val name = "Filelions" + override var mainUrl = "https://filelions.to" +} + +class Lylxan : Filesim() { + override val name = "Lylxan" + override var mainUrl = "https://lylxan.com" +} + +class Embedwish : Filesim() { + override val name = "Embedwish" + override var mainUrl = "https://embedwish.com" +} + +class Likessb : StreamSB() { + override var name = "Likessb" + override var mainUrl = "https://likessb.com" +} + +class DbGdriveplayer : Gdriveplayer() { + override var mainUrl = "https://database.gdriveplayer.us" +} \ No newline at end of file diff --git a/Gomov/src/main/kotlin/com/hexated/Gomov.kt b/Gomov/src/main/kotlin/com/hexated/Gomov.kt new file mode 100644 index 00000000..6fc1892e --- /dev/null +++ b/Gomov/src/main/kotlin/com/hexated/Gomov.kt @@ -0,0 +1,182 @@ +package com.hexated + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addActors +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.httpsify +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.nodes.Element +import java.net.URI + +open class Gomov : MainAPI() { + override var mainUrl = "https://gomov.bio" + private var directUrl: String? = null + override var name = "Gomov" + override val hasMainPage = true + override var lang = "id" + override val supportedTypes = setOf( + TvType.Movie, + TvType.TvSeries, + TvType.AsianDrama + ) + + override val mainPage = mainPageOf( + "page/%d/?s&search=advanced&post_type=movie" to "Movies", + "category/western-series/page/%d/" to "Western Series", + "tv/page/%d/" to "Tv Shows", + "category/korean-series/page/%d/" to "Korean Series", + "category/chinese-series/page/%d/" to "Chinese Series", + "category/india-series/page/%d/" to "India Series", + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val data = request.data.format(page) + val document = app.get("$mainUrl/$data").document + val home = document.select("article.item").mapNotNull { + it.toSearchResult() + } + return newHomePageResponse(request.name, home) + } + + private fun Element.toSearchResult(): SearchResponse? { + val title = this.selectFirst("h2.entry-title > a")?.text()?.trim() ?: return null + val href = fixUrl(this.selectFirst("a")!!.attr("href")) + val posterUrl = fixUrlNull(this.selectFirst("a > img")?.attr("src"))?.fixImageQuality() + val quality = this.select("div.gmr-qual, div.gmr-quality-item > a").text().trim().replace("-", "") + return if (quality.isEmpty()) { + val episode = + Regex("Episode\\s?([0-9]+)").find(title)?.groupValues?.getOrNull(1)?.toIntOrNull() + ?: this.select("div.gmr-numbeps > span").text().toIntOrNull() + newAnimeSearchResponse(title, href, TvType.TvSeries) { + this.posterUrl = posterUrl + addSub(episode) + } + } else { + newMovieSearchResponse(title, href, TvType.Movie) { + this.posterUrl = posterUrl + addQuality(quality) + } + } + } + + private fun Element.toRecommendResult(): SearchResponse? { + val title = this.selectFirst("a > span.idmuvi-rp-title")?.text()?.trim() ?: return null + val href = this.selectFirst("a")!!.attr("href") + val posterUrl = fixUrlNull(this.selectFirst("a > img")?.attr("src").fixImageQuality()) + return newMovieSearchResponse(title, href, TvType.Movie) { + this.posterUrl = posterUrl + } + } + + override suspend fun search(query: String): List { + return app.get("$mainUrl/?s=$query&post_type[]=post&post_type[]=tv").document.select("article.item") + .mapNotNull { + it.toSearchResult() + } + } + + override suspend fun load(url: String): LoadResponse { + val fetch = app.get(url) + directUrl = getBaseUrl(fetch.url) + val document = fetch.document + + val title = + document.selectFirst("h1.entry-title")?.text()?.substringBefore("Season")?.substringBefore("Episode")?.trim() + .toString() + val poster = + fixUrlNull(document.selectFirst("figure.pull-left > img")?.attr("src"))?.fixImageQuality() + val tags = document.select("span.gmr-movie-genre:contains(Genre:) > a").map { it.text() } + + val year = + document.select("span.gmr-movie-genre:contains(Year:) > a").text().trim().toIntOrNull() + val tvType = if (url.contains("/tv/")) TvType.TvSeries else TvType.Movie + val description = document.selectFirst("div[itemprop=description] > p")?.text()?.trim() + val trailer = document.selectFirst("ul.gmr-player-nav li a.gmr-trailer-popup")?.attr("href") + val rating = + document.selectFirst("div.gmr-meta-rating > span[itemprop=ratingValue]")?.text() + ?.toRatingInt() + val actors = document.select("div.gmr-moviedata").last()?.select("span[itemprop=actors]") + ?.map { it.select("a").text() } + + val recommendations = document.select("div.idmuvi-rp ul li").mapNotNull { + it.toRecommendResult() + } + + return if (tvType == TvType.TvSeries) { + val episodes = document.select("div.vid-episodes a, div.gmr-listseries a").map { eps -> + val href = fixUrl(eps.attr("href")) + val name = eps.text() + val episode = name.split(" ").lastOrNull()?.filter { it.isDigit() }?.toIntOrNull() + val season = name.split(" ").firstOrNull()?.filter { it.isDigit() }?.toIntOrNull() + Episode( + href, + name, + season = if(name.contains(" ")) season else null, + episode = episode, + ) + }.filter { it.episode != null } + newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodes) { + this.posterUrl = poster + this.year = year + this.plot = description + this.tags = tags + this.rating = rating + addActors(actors) + this.recommendations = recommendations + addTrailer(trailer) + } + } else { + newMovieLoadResponse(title, url, TvType.Movie, url) { + this.posterUrl = poster + this.year = year + this.plot = description + this.tags = tags + this.rating = rating + addActors(actors) + this.recommendations = recommendations + addTrailer(trailer) + } + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val document = app.get(data).document + val id = document.selectFirst("div#muvipro_player_content_id")!!.attr("data-id") + + document.select("div.tab-content-ajax").apmap { + val server = app.post( + "$directUrl/wp-admin/admin-ajax.php", + data = mapOf("action" to "muvipro_player_content", "tab" to it.attr("id"), "post_id" to id) + ).document.select("iframe").attr("src") + + loadExtractor(httpsify(server), "$directUrl/", subtitleCallback, callback) + } + + return true + + } + + private fun String?.fixImageQuality(): String? { + if(this == null) return null + val regex = Regex("(-\\d*x\\d*)").find(this)?.groupValues + if(regex?.isEmpty() == true) return this + return this.replace(regex?.get(0) ?: return null, "") + } + + private fun getBaseUrl(url: String): String { + return URI(url).let { + "${it.scheme}://${it.host}" + } + } + +} \ No newline at end of file diff --git a/Gomov/src/main/kotlin/com/hexated/GomovPlugin.kt b/Gomov/src/main/kotlin/com/hexated/GomovPlugin.kt new file mode 100644 index 00000000..8ee692f2 --- /dev/null +++ b/Gomov/src/main/kotlin/com/hexated/GomovPlugin.kt @@ -0,0 +1,25 @@ + +package com.hexated + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class GomovPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(Gomov()) + registerMainAPI(DutaMovie()) + registerMainAPI(Ngefilm()) + registerMainAPI(Nodrakorid()) + registerMainAPI(Multiplex()) + registerExtractorAPI(FilelionsTo()) + registerExtractorAPI(Likessb()) + registerExtractorAPI(DbGdriveplayer()) + registerExtractorAPI(Dutamovie21()) + registerExtractorAPI(Embedwish()) + registerExtractorAPI(Doods()) + registerExtractorAPI(Lylxan()) + } +} \ No newline at end of file diff --git a/Gomov/src/main/kotlin/com/hexated/Multiplex.kt b/Gomov/src/main/kotlin/com/hexated/Multiplex.kt new file mode 100644 index 00000000..e45af7d2 --- /dev/null +++ b/Gomov/src/main/kotlin/com/hexated/Multiplex.kt @@ -0,0 +1,15 @@ +package com.hexated + +import com.lagradost.cloudstream3.mainPageOf + +class Multiplex : DutaMovie() { + override var mainUrl = "http://5.104.81.46" + override var name = "Multiplex" + + override val mainPage = mainPageOf( + "country/usa/page/%d/" to "Movie", + "west-series/page/%d/" to "West Series", + "nonton-drama-korea/page/%d/" to "Drama Korea", + ) + +} \ No newline at end of file diff --git a/Gomov/src/main/kotlin/com/hexated/Ngefilm.kt b/Gomov/src/main/kotlin/com/hexated/Ngefilm.kt new file mode 100644 index 00000000..51b17cf2 --- /dev/null +++ b/Gomov/src/main/kotlin/com/hexated/Ngefilm.kt @@ -0,0 +1,22 @@ +package com.hexated + +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.apmap +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.fixUrl +import com.lagradost.cloudstream3.mainPageOf +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.loadExtractor + +class Ngefilm : DutaMovie() { + override var mainUrl = "https://ngefilm21.lat" + override var name = "Ngefilm" + + override val mainPage = mainPageOf( + "/page/%d/?s&search=advanced&post_type=movie&index&orderby&genre&movieyear&country&quality=" to "Movies Terbaru", + "/page/%d/?s=&search=advanced&post_type=tv&index=&orderby=&genre=&movieyear=&country=&quality=" to "Series Terbaru", + "/page/%d/?s=&search=advanced&post_type=tv&index=&orderby=&genre=drakor&movieyear=&country=&quality=" to "Series Korea", + "/page/%d/?s=&search=advanced&post_type=tv&index=&orderby=&genre=&movieyear=&country=indonesia&quality=" to "Series Indonesia", + ) + +} \ No newline at end of file diff --git a/Gomov/src/main/kotlin/com/hexated/Nodrakorid.kt b/Gomov/src/main/kotlin/com/hexated/Nodrakorid.kt new file mode 100644 index 00000000..afc8e4da --- /dev/null +++ b/Gomov/src/main/kotlin/com/hexated/Nodrakorid.kt @@ -0,0 +1,154 @@ +package com.hexated + +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.net.URI + +class Nodrakorid : DutaMovie() { + override var mainUrl = "https://no-drak-or.xyz" + override var name = "Nodrakorid" + + override val mainPage = mainPageOf( + "genre/movie/page/%d/" to "Film Terbaru", + "genre/korean-movie/page/%d/" to "Film Korea", + "genre/drama/page/%d/" to "Drama Korea", + "genre/c-drama/c-drama-c-drama/page/%d/" to "Drama China", + "genre/thai-drama/page/%d/" to "Drama Thailand", + ) + + override suspend fun load(url: String): LoadResponse { + return super.load(url).apply { + when (this) { + is TvSeriesLoadResponse -> { + val doc = app.get(url).document + this.comingSoon = false + this.episodes = when { + doc.select("div.vid-episodes a, div.gmr-listseries a").isNotEmpty() -> this.episodes + doc.select("div#download").isEmpty() -> { + doc.select("div.entry-content p:contains(Episode)").distinctBy { + it.text() + }.mapNotNull { eps -> + val endSibling = eps.nextElementSiblings().select("p:contains(Episode)").firstOrNull() ?: eps.nextElementSiblings().select("div.content-moviedata").firstOrNull() + val siblings = eps.nextElementSiblingsUntil(endSibling).map { ele -> + ele.ownText().filter { it.isDigit() }.toIntOrNull() to ele.select("a") + .map { it.attr("href") to it.text() } + }.filter { it.first != null } + Episode(siblings.toJson(), episode = eps.text().toEpisode()) + } + } + else -> { + doc.select("div#download h3.title-download").mapNotNull { eps -> + val siblings = eps.nextElementSibling()?.select("li")?.map { ele -> + ele.text().filter { it.isDigit() }.toIntOrNull() to ele.select("a").map { + it.attr("href") to it.text().split(" ").first() + } + }?.filter { it.first != null } + Episode(siblings?.toJson() ?: return@mapNotNull null, episode = eps.text().toEpisode()) + } + } + } + } + } + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + return if (data.startsWith("[")) { + tryParseJson>(data)?.filter { it.first != 360 }?.map { + it.second.apmap { link -> + loadFixedExtractor( + fixEmbed(link.first, link.second), + it.first, + "$mainUrl/", + subtitleCallback, + callback + ) + } + } + true + } else { + super.loadLinks(data, isCasting, subtitleCallback, callback) + } + } + + private fun fixEmbed(url: String, server: String): String { + return when { + server.contains("streamsb", true) -> { + val host = getBaseUrl(url) + url.replace("$host/", "$host/e/") + } + + server.contains("hxfile", true) -> { + val host = getBaseUrl(url) + val id = url.substringAfterLast("/") + "$host/embed-$id.html" + } + + else -> url + } + } + + private fun String.toEpisode() : Int? { + return Regex("(?i)Episode\\s?([0-9]+)").find(this)?.groupValues?.getOrNull(1)?.toIntOrNull() + } + + private fun getBaseUrl(url: String): String { + return URI(url).let { + "${it.scheme}://${it.host}" + } + } + + private suspend fun loadFixedExtractor( + url: String, + quality: Int?, + referer: String? = null, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + loadExtractor(url, referer, subtitleCallback) { link -> + callback.invoke( + ExtractorLink( + link.name, + link.name, + link.url, + link.referer, + if(link.type == ExtractorLinkType.M3U8) link.quality else quality ?: Qualities.Unknown.value, + link.type, + link.headers, + link.extractorData + ) + ) + } + } + + private fun Element.nextElementSiblingsUntil(untilElement: Element?): List { + val siblings = mutableListOf() + var currentElement = this.nextElementSibling() + + while (currentElement != null && currentElement != untilElement) { + siblings.add(currentElement) + currentElement = currentElement.nextElementSibling() + } + + return siblings + } + + data class LinkData( + @JsonProperty("first") var first: Int? = null, + @JsonProperty("second") var second: ArrayList = arrayListOf() + ) + + data class Second( + @JsonProperty("first") var first: String, + @JsonProperty("second") var second: String + ) +} \ No newline at end of file diff --git a/Hdfilmcehennemi/build.gradle.kts b/Hdfilmcehennemi/build.gradle.kts new file mode 100644 index 00000000..3b75abc6 --- /dev/null +++ b/Hdfilmcehennemi/build.gradle.kts @@ -0,0 +1,26 @@ +// use an integer for version numbers +version = 11 + + +cloudstream { + language = "tr" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + authors = listOf("Hexated") + + /** + * 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=hdfilmcehennemi.live&sz=%size%" +} \ No newline at end of file diff --git a/Hdfilmcehennemi/src/main/kotlin/com/hexated/Hdfilmcehennemi.kt b/Hdfilmcehennemi/src/main/kotlin/com/hexated/Hdfilmcehennemi.kt new file mode 100644 index 00000000..cce3f58e --- /dev/null +++ b/Hdfilmcehennemi/src/main/kotlin/com/hexated/Hdfilmcehennemi.kt @@ -0,0 +1,245 @@ +package com.hexated + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addActors +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.mvvm.safeApiCall +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import org.jsoup.nodes.Element + +class Hdfilmcehennemi : MainAPI() { + override var mainUrl = "https://www.hdfilmcehennemi.life" + override var name = "hdfilmcehennemi" + override val hasMainPage = true + override var lang = "tr" + override val hasQuickSearch = true + override val hasDownloadSupport = true + override val supportedTypes = setOf( + TvType.Movie, + TvType.TvSeries, + ) + + override val mainPage = mainPageOf( + "$mainUrl/category/tavsiye-filmler-izle2/page/" to "Tavsiye Filmler Kategorisi", + "$mainUrl/yabancidiziizle-1/page/" to "Son Eklenen Yabancı Diziler", + "$mainUrl/imdb-7-puan-uzeri-filmler/page/" to "Imdb 7+ Filmler", + "$mainUrl/en-cok-yorumlananlar/page/" to "En Çok Yorumlananlar", + "$mainUrl/en-cok-begenilen-filmleri-izle/page/" to "En Çok Beğenilenler", + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val document = app.get(request.data + page).document + val home = document.select("div.card-body div.row div.col-6.col-sm-3.poster-container") + .mapNotNull { + it.toSearchResult() + } + return newHomePageResponse(request.name, home) + } + + private fun Element.toSearchResult(): SearchResponse? { + val title = this.selectFirst("h2.title")?.text() ?: return null + val href = fixUrlNull(this.selectFirst("a")?.attr("href")) ?: return null + val posterUrl = fixUrlNull(this.selectFirst("img")?.attr("data-src")) + return newMovieSearchResponse(title, href, TvType.Movie) { + this.posterUrl = posterUrl + } + + } + + private fun Media.toSearchResponse(): SearchResponse? { + return newMovieSearchResponse( + title ?: return null, + "$mainUrl/$slugPrefix$slug", + TvType.TvSeries, + ) { + this.posterUrl = "$mainUrl/uploads/poster/$poster" + } + } + + override suspend fun quickSearch(query: String): List = search(query) + + override suspend fun search(query: String): List { + return app.post( + "$mainUrl/search/", + data = mapOf("query" to query), + referer = "$mainUrl/", + headers = mapOf( + "Accept" to "application/json, text/javascript, */*; q=0.01", + "X-Requested-With" to "XMLHttpRequest" + ) + ).parsedSafe()?.result?.mapNotNull { media -> + media.toSearchResponse() + } ?: throw ErrorLoadingException("Invalid Json reponse") + } + + override suspend fun load(url: String): LoadResponse? { + val document = app.get(url).document + + val title = document.selectFirst("div.card-header > h1, div.card-header > h2")?.text() + ?.removeSuffix("Filminin Bilgileri")?.trim() + ?: return null + val poster = fixUrlNull(document.select("img.img-fluid").lastOrNull()?.attr("src")) + val tags = document.select("div.mb-0.lh-lg div:nth-child(5) a").map { it.text() } + val year = + document.selectFirst("div.mb-0.lh-lg div:nth-child(4) a")?.text()?.trim()?.toIntOrNull() + val tvType = if (document.select("nav#seasonsTabs").isNullOrEmpty() + ) TvType.Movie else TvType.TvSeries + val description = document.selectFirst("article.text-white > p")?.text()?.trim() + val rating = document.selectFirst("div.rating-votes div.rate span")?.text()?.toRatingInt() + val actors = document.select("div.mb-0.lh-lg div:last-child a.chip").map { + Actor(it.text(), it.select("img").attr("src")) + } + val recommendations = + document.select("div.swiper-wrapper div.poster.poster-pop").mapNotNull { + val recName = it.selectFirst("h2.title")?.text() ?: return@mapNotNull null + val recHref = + fixUrlNull(it.selectFirst("a")?.attr("href")) ?: return@mapNotNull null + val recPosterUrl = fixUrlNull(it.selectFirst("img")?.attr("data-src")) + newTvSeriesSearchResponse(recName, recHref, TvType.TvSeries) { + this.posterUrl = recPosterUrl + } + } + + return if (tvType == TvType.TvSeries) { + val trailer = + document.selectFirst("button.btn.btn-fragman.btn-danger")?.attr("data-trailer") + ?.let { + "https://www.youtube.com/embed/$it" + } + val episodes = document.select("div#seasonsTabs-tabContent div.card-list-item").map { + val href = it.select("a").attr("href") + val name = it.select("h3").text().trim() + val episode = it.select("h3").text().let { num -> + Regex("Sezon\\s?([0-9]+).").find(num)?.groupValues?.getOrNull(1)?.toIntOrNull() + } + val season = it.parents()[1].attr("id").substringAfter("-").toIntOrNull() + Episode( + href, + name, + season, + episode, + ) + } + newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodes) { + this.posterUrl = poster + this.year = year + this.plot = description + this.tags = tags + this.rating = rating + addActors(actors) + this.recommendations = recommendations + addTrailer(trailer) + } + } else { + val trailer = + document.selectFirst("nav.nav.card-nav.nav-slider a[data-bs-toggle=\"modal\"]") + ?.attr("data-trailer")?.let { + "https://www.youtube.com/embed/$it" + } + newMovieLoadResponse(title, url, TvType.Movie, url) { + this.posterUrl = poster + this.year = year + this.plot = description + this.tags = tags + this.rating = rating + addActors(actors) + this.recommendations = recommendations + addTrailer(trailer) + } + } + } + + private suspend fun invokeLocalSource( + source: String, + url: String, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val script = app.get( + url, + referer = "${mainUrl}/" + ).document.select("script") + .find { it.data().contains("sources:") }?.data() ?: return + val videoData = getAndUnpack(script).substringAfter("file_link=\"").substringBefore("\";") + val subData = script.substringAfter("tracks: [").substringBefore("]") + + callback.invoke( + ExtractorLink( + source, + source, + base64Decode(videoData), + "$mainUrl/", + Qualities.Unknown.value, + true + ) + ) + + tryParseJson>("[${subData}]") + ?.filter { it.kind == "captions" }?.map { + subtitleCallback.invoke( + SubtitleFile( + it.label.toString(), + fixUrl(it.file.toString()) + ) + ) + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + app.get(data).document.select("nav.nav.card-nav.nav-slider a.nav-link").map { + Pair(it.attr("href"), it.text()) + }.apmap { (url, source) -> + safeApiCall { + app.get(url).document.select("div.card-video > iframe").attr("data-src") + .let { url -> + if (url.startsWith(mainUrl)) { + invokeLocalSource(source, url, subtitleCallback, callback) + } else { + loadExtractor(url, "$mainUrl/", subtitleCallback) { link -> + callback.invoke( + ExtractorLink( + source, + source, + link.url, + link.referer, + link.quality, + link.type, + link.headers, + link.extractorData + ) + ) + } + } + } + } + } + return true + } + + private data class SubSource( + @JsonProperty("file") val file: String? = null, + @JsonProperty("label") val label: String? = null, + @JsonProperty("kind") val kind: String? = null, + ) + + data class Result( + @JsonProperty("result") val result: ArrayList? = arrayListOf(), + ) + + data class Media( + @JsonProperty("title") val title: String? = null, + @JsonProperty("poster") val poster: String? = null, + @JsonProperty("slug") val slug: String? = null, + @JsonProperty("slug_prefix") val slugPrefix: String? = null, + ) +} \ No newline at end of file diff --git a/IdlixProvider/build.gradle.kts b/IdlixProvider/build.gradle.kts new file mode 100644 index 00000000..63fef982 --- /dev/null +++ b/IdlixProvider/build.gradle.kts @@ -0,0 +1,28 @@ +// use an integer for version numbers +version = 15 + + +cloudstream { + language = "id" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + authors = listOf("Hexated") + + /** + * 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", + "Anime", + "AsianDrama", + ) + + iconUrl = "https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=http://idlixian.com&size=16" +} \ No newline at end of file diff --git a/IdlixProvider/src/main/kotlin/com/hexated/IdlixProvider.kt b/IdlixProvider/src/main/kotlin/com/hexated/IdlixProvider.kt new file mode 100644 index 00000000..d103bad9 --- /dev/null +++ b/IdlixProvider/src/main/kotlin/com/hexated/IdlixProvider.kt @@ -0,0 +1,317 @@ +package com.hexated + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addActors +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.mvvm.safeApiCall +import com.lagradost.cloudstream3.network.CloudflareKiller +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import okhttp3.Interceptor +import okhttp3.Response +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import java.net.URI + +class IdlixProvider : MainAPI() { + override var mainUrl = "https://tv.idlixplus.net" + private var directUrl = mainUrl + override var name = "Idlix" + override val hasMainPage = true + override var lang = "id" + override val hasDownloadSupport = true + private val cloudflareKiller by lazy { CloudflareKiller() } + private val interceptor by lazy { CloudflareInterceptor(cloudflareKiller) } + override val supportedTypes = setOf( + TvType.Movie, + TvType.TvSeries, + TvType.Anime, + TvType.AsianDrama + ) + + private val key = "\\x5a\\x6d\\x5a\\x6c\\x4e\\x7a\\x55\\x79\\x4d\\x54\\x56\\x6a\\x5a\\x47\\x52\\x69\\x5a\\x44\\x55\\x30\\x5a\\x6d\\x59\\x35\\x4f\\x57\\x45\\x33\\x4d\\x44\\x4a\\x69\\x4e\\x32\\x4a\\x6c\\x4f\\x54\\x42\\x6c\\x4e\\x7a\\x49\\x3d" + + override val mainPage = mainPageOf( + "$mainUrl/" to "Featured", + "$mainUrl/trending/page/?get=movies" to "Trending Movies", + "$mainUrl/trending/page/?get=tv" to "Trending TV Series", + "$mainUrl/movie/page/" to "Movie Terbaru", + "$mainUrl/tvseries/page/" to "TV Series Terbaru", + "$mainUrl/network/netflix/page/" to "Netflix", + "$mainUrl/genre/anime/page/" to "Anime", + "$mainUrl/genre/drama-korea/page/" to "Drama Korea", + ) + + private fun getBaseUrl(url: String): String { + return URI(url).let { + "${it.scheme}://${it.host}" + } + } + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val url = request.data.split("?") + val nonPaged = request.name == "Featured" && page <= 1 + val req = if (nonPaged) { + app.get(request.data, interceptor = interceptor) + } else { + app.get("${url.first()}$page/?${url.lastOrNull()}", interceptor = interceptor) + } + mainUrl = getBaseUrl(req.url) + val document = req.document + val home = (if (nonPaged) { + document.select("div.items.featured article") + } else { + document.select("div.items.full article, div#archive-content article") + }).mapNotNull { + it.toSearchResult() + } + return newHomePageResponse(request.name, home) + } + + private fun getProperLink(uri: String): String { + return when { + uri.contains("/episode/") -> { + var title = uri.substringAfter("$mainUrl/episode/") + title = Regex("(.+?)-season").find(title)?.groupValues?.get(1).toString() + "$mainUrl/tvseries/$title" + } + uri.contains("/season/") -> { + var title = uri.substringAfter("$mainUrl/season/") + title = Regex("(.+?)-season").find(title)?.groupValues?.get(1).toString() + "$mainUrl/tvseries/$title" + } + else -> { + uri + } + } + } + + private fun Element.toSearchResult(): SearchResponse { + val title = this.selectFirst("h3 > a")!!.text().replace(Regex("\\(\\d{4}\\)"), "").trim() + val href = getProperLink(this.selectFirst("h3 > a")!!.attr("href")) + val posterUrl = this.select("div.poster > img").attr("src").toString() + val quality = getQualityFromString(this.select("span.quality").text()) + return newMovieSearchResponse(title, href, TvType.Movie) { + this.posterUrl = posterUrl + this.quality = quality + posterHeaders = cloudflareKiller.getCookieHeaders(mainUrl).toMap() + } + + } + + override suspend fun search(query: String): List { + val req = app.get("$mainUrl/search/$query", interceptor = interceptor) + mainUrl = getBaseUrl(req.url) + val document = req.document + return document.select("div.result-item").map { + val title = + it.selectFirst("div.title > a")!!.text().replace(Regex("\\(\\d{4}\\)"), "").trim() + val href = getProperLink(it.selectFirst("div.title > a")!!.attr("href")) + val posterUrl = it.selectFirst("img")!!.attr("src").toString() + newMovieSearchResponse(title, href, TvType.TvSeries) { + this.posterUrl = posterUrl + posterHeaders = cloudflareKiller.getCookieHeaders(mainUrl).toMap() + } + } + } + + override suspend fun load(url: String): LoadResponse { + val request = app.get(url, interceptor = interceptor, referer = "$directUrl/") + directUrl = getBaseUrl(request.url) + val document = request.document + val title = + document.selectFirst("div.data > h1")?.text()?.replace(Regex("\\(\\d{4}\\)"), "") + ?.trim().toString() + val poster = document.select("div.poster > img").attr("src").toString() + val tags = document.select("div.sgeneros > a").map { it.text() } + + val year = Regex(",\\s?(\\d+)").find( + document.select("span.date").text().trim() + )?.groupValues?.get(1).toString().toIntOrNull() + val tvType = if (document.select("ul#section > li:nth-child(1)").text().contains("Episodes") + ) TvType.TvSeries else TvType.Movie + val description = document.select("div.wp-content > p").text().trim() + val trailer = document.selectFirst("div.embed iframe")?.attr("src") + val rating = + document.selectFirst("span.dt_rating_vgs")?.text()?.toRatingInt() + val actors = document.select("div.persons > div[itemprop=actor]").map { + Actor(it.select("meta[itemprop=name]").attr("content"), it.select("img").attr("src")) + } + + val recommendations = document.select("div.owl-item").map { + val recName = + it.selectFirst("a")!!.attr("href").toString().removeSuffix("/").split("/").last() + val recHref = it.selectFirst("a")!!.attr("href") + val recPosterUrl = it.selectFirst("img")?.attr("src").toString() + newTvSeriesSearchResponse(recName, recHref, TvType.TvSeries) { + this.posterUrl = recPosterUrl + posterHeaders = cloudflareKiller.getCookieHeaders(mainUrl).toMap() + } + } + + return if (tvType == TvType.TvSeries) { + val episodes = document.select("ul.episodios > li").map { + val href = it.select("a").attr("href") + val name = fixTitle(it.select("div.episodiotitle > a").text().trim()) + val image = it.select("div.imagen > img").attr("src") + val episode = it.select("div.numerando").text().replace(" ", "").split("-").last() + .toIntOrNull() + val season = it.select("div.numerando").text().replace(" ", "").split("-").first() + .toIntOrNull() + Episode( + href, + name, + season, + episode, + image + ) + } + newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodes) { + this.posterUrl = poster + this.year = year + this.plot = description + this.tags = tags + this.rating = rating + addActors(actors) + this.recommendations = recommendations + addTrailer(trailer) + posterHeaders = cloudflareKiller.getCookieHeaders(mainUrl).toMap() + } + } else { + newMovieLoadResponse(title, url, TvType.Movie, url) { + this.posterUrl = poster + this.year = year + this.plot = description + this.tags = tags + this.rating = rating + addActors(actors) + this.recommendations = recommendations + addTrailer(trailer) + posterHeaders = cloudflareKiller.getCookieHeaders(mainUrl).toMap() + } + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val document = app.get(data, interceptor = interceptor, referer = "$directUrl/").document + val id = document.select("meta#dooplay-ajax-counter").attr("data-postid") + val type = if (data.contains("/movie/")) "movie" else "tv" + + document.select("ul#playeroptionsul > li").map { + it.attr("data-nume") + }.apmap { nume -> + safeApiCall { + val source = app.post( + url = "$directUrl/wp-admin/admin-ajax.php", data = mapOf( + "action" to "doo_player_ajax", "post" to id, "nume" to nume, "type" to type + ), headers = mapOf("X-Requested-With" to "XMLHttpRequest"), referer = data, interceptor = interceptor + ).let { tryParseJson(it.text) } ?: return@safeApiCall + + val password = if(source.key?.startsWith("\\x") == true) source.key else key + var decrypted = AesHelper.cryptoAESHandler(source.embed_url, password.toByteArray(), false)?.fixBloat() ?: return@safeApiCall + + if (decrypted.startsWith("https://uservideo.xyz")) { + decrypted = app.get(decrypted).document.select("iframe").attr("src") + } + + getUrl(decrypted, "$directUrl/", subtitleCallback, callback) + + } + } + + return true + } + + class CloudflareInterceptor(private val cloudflareKiller: CloudflareKiller): Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val response = chain.proceed(request) + val doc = Jsoup.parse(response.peekBody(1024 * 1024).string()) + if (doc.select("title").text() == "Just a moment...") { + return cloudflareKiller.intercept(chain) + } + return response + } + } + + private fun String.fixBloat() : String { + return this.replace("\"", "").replace("\\", "") + } + + private suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val document = app.get(url, referer = referer).document + val hash = url.split("/").last().substringAfter("data=") + + val m3uLink = app.post( + url = "$mainUrl/player/index.php?data=$hash&do=getVideo", + data = mapOf("hash" to hash, "r" to "$referer"), + referer = referer, + headers = mapOf("X-Requested-With" to "XMLHttpRequest") + ).parsed().videoSource + + M3u8Helper.generateM3u8( + this.name, + m3uLink, + "$referer", + ).forEach(callback) + + + document.select("script").map { script -> + if (script.data().contains("eval(function(p,a,c,k,e,d)")) { + val subData = + getAndUnpack(script.data()).substringAfter("\"tracks\":[").substringBefore("],") + tryParseJson>("[$subData]")?.map { subtitle -> + subtitleCallback.invoke( + SubtitleFile( + getLanguage(subtitle.label ?: ""), + subtitle.file + ) + ) + } + } + } + } + + private fun getLanguage(str: String): String { + return when { + str.contains("indonesia", true) || str + .contains("bahasa", true) -> "Indonesian" + else -> str + } + } + + data class ResponseSource( + @JsonProperty("hls") val hls: Boolean, + @JsonProperty("videoSource") val videoSource: String, + @JsonProperty("securedLink") val securedLink: String?, + ) + + data class Tracks( + @JsonProperty("kind") val kind: String?, + @JsonProperty("file") val file: String, + @JsonProperty("label") val label: String?, + ) + + data class ResponseHash( + @JsonProperty("embed_url") val embed_url: String, + @JsonProperty("key") val key: String?, + @JsonProperty("type") val type: String?, + ) + + +} \ No newline at end of file diff --git a/IdlixProvider/src/main/kotlin/com/hexated/Utils.kt b/IdlixProvider/src/main/kotlin/com/hexated/Utils.kt new file mode 100644 index 00000000..96bb5a14 --- /dev/null +++ b/IdlixProvider/src/main/kotlin/com/hexated/Utils.kt @@ -0,0 +1,95 @@ +package com.hexated + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.ErrorLoadingException +import com.lagradost.cloudstream3.base64DecodeArray +import com.lagradost.cloudstream3.base64Encode +import com.lagradost.cloudstream3.utils.AppUtils +import java.security.DigestException +import java.security.MessageDigest +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +object AesHelper { + + fun cryptoAESHandler( + data: String, + pass: ByteArray, + encrypt: Boolean = true, + padding: String = "AES/CBC/PKCS5PADDING", + ): String? { + val parse = AppUtils.tryParseJson(data) ?: return null + val (key, iv) = generateKeyAndIv(pass, parse.s.hexToByteArray()) ?: throw ErrorLoadingException("failed to generate key") + val cipher = Cipher.getInstance(padding) + return if (!encrypt) { + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) + String(cipher.doFinal(base64DecodeArray(parse.ct))) + } else { + cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) + base64Encode(cipher.doFinal(parse.ct.toByteArray())) + } + } + + // https://stackoverflow.com/a/41434590/8166854 + private fun generateKeyAndIv( + password: ByteArray, + salt: ByteArray, + hashAlgorithm: String = "MD5", + keyLength: Int = 32, + ivLength: Int = 16, + iterations: Int = 1 + ): List? { + + val md = MessageDigest.getInstance(hashAlgorithm) + val digestLength = md.digestLength + val targetKeySize = keyLength + ivLength + val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength + val generatedData = ByteArray(requiredLength) + var generatedLength = 0 + + try { + md.reset() + + while (generatedLength < targetKeySize) { + if (generatedLength > 0) + md.update( + generatedData, + generatedLength - digestLength, + digestLength + ) + + md.update(password) + md.update(salt, 0, 8) + md.digest(generatedData, generatedLength, digestLength) + + for (i in 1 until iterations) { + md.update(generatedData, generatedLength, digestLength) + md.digest(generatedData, generatedLength, digestLength) + } + + generatedLength += digestLength + } + return listOf( + generatedData.copyOfRange(0, keyLength), + generatedData.copyOfRange(keyLength, targetKeySize) + ) + } catch (e: DigestException) { + return null + } + } + + private fun String.hexToByteArray(): ByteArray { + check(length % 2 == 0) { "Must have an even length" } + return chunked(2) + .map { it.toInt(16).toByte() } + .toByteArray() + } + + private data class AesData( + @JsonProperty("ct") val ct: String, + @JsonProperty("iv") val iv: String, + @JsonProperty("s") val s: String + ) + +} \ No newline at end of file diff --git a/Kissasian/build.gradle.kts b/Kissasian/build.gradle.kts new file mode 100644 index 00000000..03d38561 --- /dev/null +++ b/Kissasian/build.gradle.kts @@ -0,0 +1,23 @@ +// use an integer for version numbers +version = 3 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + description = "Include: KissasianMx" + authors = listOf("Hexated") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf("AsianDrama",) + + iconUrl = "https://www.google.com/s2/favicons?domain=kissasian.pe&sz=%size%" +} \ No newline at end of file diff --git a/Kissasian/src/main/kotlin/com/hexated/Kissasian.kt b/Kissasian/src/main/kotlin/com/hexated/Kissasian.kt new file mode 100644 index 00000000..0d70fda2 --- /dev/null +++ b/Kissasian/src/main/kotlin/com/hexated/Kissasian.kt @@ -0,0 +1,146 @@ +package com.hexated + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.extractors.Filesim +import com.lagradost.cloudstream3.extractors.helper.GogoHelper +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.httpsify +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.nodes.Element + +open class Kissasian : MainAPI() { + override var mainUrl = "https://kissasian.pe" + override var name = "Kissasian" + override val hasMainPage = true + override val hasDownloadSupport = true + override val supportedTypes = setOf(TvType.AsianDrama) + + companion object { + fun getStatus(t: String?): ShowStatus { + return when (t) { + "Completed" -> ShowStatus.Completed + "Ongoing" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + } + + override val mainPage = mainPageOf( + "drama-list/ongoing.html?page=" to "Drama Ongoing", + "drama-list/completed.html?page=" to "Drama Completed", + "genre/variety/?page=" to "Variety Show", + "genre/romance/?page=" to "Romance", + "genre/action/?page=" to "Action", + "genre/mystery/?page=" to "Mystery", + ) + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + val document = app.get("$mainUrl/${request.data}$page").document + val home = document.select("div.list-drama div.item").mapNotNull { + it.toSearchResult() + } + return newHomePageResponse(request.name, home) + } + + private fun Element.toSearchResult(): SearchResponse? { + val href = fixUrl(this.selectFirst("a")?.attr("href") ?: return null) + val title = this.selectFirst("span.title")?.text()?.trim() ?: return null + val posterUrl = fixUrlNull(this.selectFirst("img")?.attr("src")) + return newTvSeriesSearchResponse(title, href, TvType.AsianDrama) { + this.posterUrl = posterUrl + } + } + + override suspend fun search(query: String): List { + val document = app.get("$mainUrl/search.html?keyword=$query").document + return document.select("div.list-drama div.item").mapNotNull { + it.toSearchResult() + } + } + + open val contentInfoClass = "barContentInfo" + override suspend fun load(url: String): LoadResponse? { + val document = app.get(url).document + + val title = document.selectFirst("div.$contentInfoClass a")?.text()?.trim() ?: return null + val poster = fixUrlNull(document.select("div.$contentInfoClass img").last()?.attr("src")) + val tags = document.select("div.$contentInfoClass p:contains(Genres:) a").map { it.text().removePrefix(",").trim() } + + val year = document.selectFirst("div.$contentInfoClass p.type.Releasea")?.text()?.trim()?.toIntOrNull() + val status = getStatus(document.selectFirst("div.$contentInfoClass p:contains(Status:)")?.ownText()?.trim()) + val description = document.selectFirst("div.$contentInfoClass p.des, div.$contentInfoClass p:last-child")?.nextElementSiblings()?.select("p")?.text() + + val episodes = document.select("ul.listing li, table.listing td.episodeSub").map { + val name = it.selectFirst("a")?.attr("title") + val link = fixUrlNull(it.selectFirst("a")?.attr("href")) + val epNum = Regex("Episode\\s(\\d+)").find("$name")?.groupValues?.getOrNull(1)?.toIntOrNull() + newEpisode(link) { + this.name = name + this.episode = epNum + } + }.reversed() + + if (episodes.size == 1) { + return newMovieLoadResponse(title, url, TvType.Movie, episodes[0].data) { + posterUrl = poster + this.year = year + plot = description + this.tags = tags + } + } else { + return newTvSeriesLoadResponse(title, url, TvType.AsianDrama, episodes = episodes) { + posterUrl = poster + this.year = year + showStatus = status + plot = description + this.tags = tags + } + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val document = app.get(data).document + val server = document.selectFirst("select#selectServer option")?.attr("value") + val iframe = app.get(httpsify(server ?: return false)) + val iframeDoc = iframe.document + + argamap({ + iframeDoc.select(".list-server-items > .linkserver") + .forEach { element -> + val status = element.attr("data-status") ?: return@forEach + if (status != "1") return@forEach + val extractorData = element.attr("data-video") ?: return@forEach + loadExtractor(extractorData, iframe.url, subtitleCallback, callback) + } + }, { + val iv = "9262859232435825" + val secretKey = "93422192433952489752342908585752" + val secretDecryptKey = secretKey + GogoHelper.extractVidstream( + iframe.url, + this.name, + callback, + iv, + secretKey, + secretDecryptKey, + isUsingAdaptiveKeys = false, + isUsingAdaptiveData = true, + iframeDocument = iframeDoc + ) + }) + + return true + } + +} + +class Kswplayer : Filesim() { + override val name = "Kswplayer" + override var mainUrl = "https://kswplayer.info" +} diff --git a/Kissasian/src/main/kotlin/com/hexated/KissasianMx.kt b/Kissasian/src/main/kotlin/com/hexated/KissasianMx.kt new file mode 100644 index 00000000..ede3dd86 --- /dev/null +++ b/Kissasian/src/main/kotlin/com/hexated/KissasianMx.kt @@ -0,0 +1,58 @@ +package com.hexated + +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.apmap +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.fixUrl +import com.lagradost.cloudstream3.mainPageOf +import com.lagradost.cloudstream3.newTvSeriesSearchResponse +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.loadExtractor + +class KissasianMx : Kissasian() { + override var mainUrl = "https://kissasian.mx" + override var name = "KissasianMx" + override val contentInfoClass = "barContent" + override val mainPage = mainPageOf( + "Status/Ongoing?page=" to "Drama Ongoing", + "Status/Completed?page=" to "Drama Completed", + "Status/Completed?page=" to "Drama Completed", + "Genre/Romance?page=" to "Drama Romance", + "Genre/Reality-TV?page=" to "Reality-TV", + "Genre/Mystery?page=" to "Drama Mystery", + "Genre/Movie?page=" to "Movie", + ) + + override suspend fun search(query: String): List { + val document = app.post( + "$mainUrl/Search/SearchSuggest", data = mapOf( + "type" to "Drama", + "keyword" to query, + ), headers = mapOf("X-Requested-With" to "XMLHttpRequest") + ).document + return document.select("a").mapNotNull { + val href = fixUrl(it.attr("href")) + val title = it.text() + newTvSeriesSearchResponse(title, href, TvType.AsianDrama) + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val document = app.get(data).document + document.select("select#selectServer option").apmap { + val server = it.attr("value") + val iframe = app.get(fixUrl(server ?: return@apmap)).document.selectFirst("div#centerDivVideo iframe")?.attr("src") + loadExtractor(iframe ?: return@apmap, "$mainUrl/", subtitleCallback, callback) + } + + return true + } +} \ No newline at end of file diff --git a/Kissasian/src/main/kotlin/com/hexated/KissasianPlugin.kt b/Kissasian/src/main/kotlin/com/hexated/KissasianPlugin.kt new file mode 100644 index 00000000..0fd3e446 --- /dev/null +++ b/Kissasian/src/main/kotlin/com/hexated/KissasianPlugin.kt @@ -0,0 +1,16 @@ + +package com.hexated + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class KissasianPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(Kissasian()) + registerMainAPI(KissasianMx()) + registerExtractorAPI(Kswplayer()) + } +} \ No newline at end of file diff --git a/KuramanimeProvider/build.gradle.kts b/KuramanimeProvider/build.gradle.kts new file mode 100644 index 00000000..34c7e57e --- /dev/null +++ b/KuramanimeProvider/build.gradle.kts @@ -0,0 +1,27 @@ +// use an integer for version numbers +version = 15 + + +cloudstream { + language = "id" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + authors = listOf("Hexated") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AnimeMovie", + "Anime", + "OVA", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=kuramanime.com&sz=%size%" +} \ No newline at end of file diff --git a/KuramanimeProvider/src/main/kotlin/com/hexated/KuramanimeProvider.kt b/KuramanimeProvider/src/main/kotlin/com/hexated/KuramanimeProvider.kt new file mode 100644 index 00000000..63a94194 --- /dev/null +++ b/KuramanimeProvider/src/main/kotlin/com/hexated/KuramanimeProvider.kt @@ -0,0 +1,217 @@ +package com.hexated + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId +import com.lagradost.cloudstream3.mvvm.safeApiCall +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.Jsoup +import org.jsoup.nodes.Element + +class KuramanimeProvider : MainAPI() { + override var mainUrl = "https://kuramanime.pro" + override var name = "Kuramanime" + override val hasQuickSearch = false + override val hasMainPage = true + override var lang = "id" + override val hasDownloadSupport = true + + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + companion object { + fun getType(t: String, s: Int): TvType { + return if (t.contains("OVA", true) || t.contains("Special")) TvType.OVA + else if (t.contains("Movie", true) && s == 1) TvType.AnimeMovie + else TvType.Anime + } + + fun getStatus(t: String): ShowStatus { + return when (t) { + "Selesai Tayang" -> ShowStatus.Completed + "Sedang Tayang" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + } + + override val mainPage = mainPageOf( + "$mainUrl/anime/ongoing?order_by=updated&page=" to "Sedang Tayang", + "$mainUrl/anime/finished?order_by=updated&page=" to "Selesai Tayang", + "$mainUrl/properties/season/summer-2022?order_by=most_viewed&page=" to "Dilihat Terbanyak Musim Ini", + "$mainUrl/anime/movie?order_by=updated&page=" to "Film Layar Lebar", + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val document = app.get(request.data + page).document + + val home = document.select("div.col-lg-4.col-md-6.col-sm-6").mapNotNull { + it.toSearchResult() + } + + return newHomePageResponse(request.name, home) + } + + private fun getProperAnimeLink(uri: String): String { + return if (uri.contains("/episode")) { + Regex("(.*)/episode/.+").find(uri)?.groupValues?.get(1).toString() + "/" + } else { + uri + } + } + + private fun Element.toSearchResult(): AnimeSearchResponse? { + val href = getProperAnimeLink(fixUrl(this.selectFirst("a")!!.attr("href"))) + val title = this.selectFirst("h5 a")?.text() ?: return null + val posterUrl = fixUrl(this.select("div.product__item__pic.set-bg").attr("data-setbg")) + val episode = this.select("div.ep span").text().let { + Regex("Ep\\s(\\d+)\\s/").find(it)?.groupValues?.getOrNull(1)?.toIntOrNull() + } + + return newAnimeSearchResponse(title, href, TvType.Anime) { + this.posterUrl = posterUrl + addSub(episode) + } + + } + + override suspend fun search(query: String): List { + val link = "$mainUrl/anime?search=$query&order_by=latest" + val document = app.get(link).document + + return document.select("div#animeList div.product__item").mapNotNull { + it.toSearchResult() + } + } + + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document + + val title = document.selectFirst(".anime__details__title > h3")!!.text().trim() + val poster = document.selectFirst(".anime__details__pic")?.attr("data-setbg") + val tags = document.select("div.anime__details__widget > div > div:nth-child(2) > ul > li:nth-child(1)") + .text().trim().replace("Genre: ", "").split(", ") + + val year = Regex("\\D").replace( + document.select("div.anime__details__widget > div > div:nth-child(1) > ul > li:nth-child(5)") + .text().trim().replace("Musim: ", ""), "" + ).toIntOrNull() + val status = getStatus( + document.select("div.anime__details__widget > div > div:nth-child(1) > ul > li:nth-child(3)") + .text().trim().replace("Status: ", "") + ) + val description = document.select(".anime__details__text > p").text().trim() + + val episodes = mutableListOf() + + for (i in 1..6) { + val doc = app.get("$url?page=$i").document + val eps = Jsoup.parse(doc.select("#episodeLists").attr("data-content")).select("a.btn.btn-sm.btn-danger") + .mapNotNull { + val name = it.text().trim() + val episode = Regex("(\\d+[.,]?\\d*)").find(name)?.groupValues?.getOrNull(0) + ?.toIntOrNull() + val link = it.attr("href") + Episode(link, episode = episode) + } + if(eps.isEmpty()) break else episodes.addAll(eps) + } + + val type = getType(document.selectFirst("div.col-lg-6.col-md-6 ul li:contains(Tipe:) a")?.text()?.lowercase() ?: "tv", episodes.size) + val recommendations = document.select("div#randomList > a").mapNotNull { + val epHref = it.attr("href") + val epTitle = it.select("h5.sidebar-title-h5.px-2.py-2").text() + val epPoster = it.select(".product__sidebar__view__item.set-bg").attr("data-setbg") + newAnimeSearchResponse(epTitle, epHref, TvType.Anime) { + this.posterUrl = epPoster + addDubStatus(dubExist = false, subExist = true) + } + } + + val tracker = APIHolder.getTracker(listOf(title),TrackerType.getTypes(type),year,true) + + return newAnimeLoadResponse(title, url, type) { + engName = title + posterUrl = tracker?.image ?: poster + backgroundPosterUrl = tracker?.cover + this.year = year + addEpisodes(DubStatus.Subbed, episodes) + showStatus = status + plot = description + this.tags = tags + this.recommendations = recommendations + addMalId(tracker?.malId) + addAniListId(tracker?.aniId?.toIntOrNull()) + } + + } + + private suspend fun invokeLocalSource( + url: String, + server: String, + ref: String, + callback: (ExtractorLink) -> Unit + ) { + val document = app.get( + url, + referer = ref, + headers = mapOf("X-Requested-With" to "XMLHttpRequest") + ).document + document.select("video#player > source").map { + val link = fixUrl(it.attr("src")) + val quality = it.attr("size").toIntOrNull() + callback.invoke( + ExtractorLink( + fixTitle(server), + fixTitle(server), + link, + referer = "$mainUrl/", + quality = quality ?: Qualities.Unknown.value, + headers = mapOf( + "Accept" to "video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5", + "Range" to "bytes=0-", + "Sec-Fetch-Dest" to "video", + "Sec-Fetch-Mode" to "no-cors", + ) + ) + ) + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val res = app.get(data).document + res.select("select#changeServer option").apmap { source -> + safeApiCall { + val server = source.attr("value") + val link = "$data?activate_stream=1&stream_server=$server" + if (server == "kuramadrive" || server == "archive") { + invokeLocalSource(link, server, data, callback) + } else { + app.get( + link, + referer = data, + headers = mapOf("X-Requested-With" to "XMLHttpRequest") + ).document.select("div.iframe-container iframe").attr("src").let { videoUrl -> + loadExtractor(fixUrl(videoUrl), "$mainUrl/", subtitleCallback, callback) + } + } + } + } + + return true + } + +} \ No newline at end of file diff --git a/KuronimeProvider/build.gradle.kts b/KuronimeProvider/build.gradle.kts new file mode 100644 index 00000000..84d6863e --- /dev/null +++ b/KuronimeProvider/build.gradle.kts @@ -0,0 +1,27 @@ +// use an integer for version numbers +version = 17 + + +cloudstream { + language = "id" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + authors = listOf("Hexated") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AnimeMovie", + "Anime", + "OVA", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=45.12.2.2&sz=%size%" +} \ No newline at end of file diff --git a/KuronimeProvider/src/main/kotlin/com/hexated/KuronimeProvider.kt b/KuronimeProvider/src/main/kotlin/com/hexated/KuronimeProvider.kt new file mode 100644 index 00000000..70e75127 --- /dev/null +++ b/KuronimeProvider/src/main/kotlin/com/hexated/KuronimeProvider.kt @@ -0,0 +1,299 @@ +package com.hexated + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.extractors.helper.AesHelper +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.M3u8Helper +import com.lagradost.cloudstream3.utils.getQualityFromName +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.nodes.Element +import java.net.URI +import java.util.ArrayList + +class KuronimeProvider : MainAPI() { + override var mainUrl = "https://45.12.2.26" + private var animekuUrl = "https://animeku.org" + override var name = "Kuronime" + override val hasQuickSearch = true + override val hasMainPage = true + override var lang = "id" + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + companion object { + const val KEY = "3&!Z0M,VIZ;dZW==" + fun getType(t: String): TvType { + return if (t.contains("OVA", true) || t.contains("Special", true)) TvType.OVA + else if (t.contains("Movie", true)) TvType.AnimeMovie + else TvType.Anime + } + + fun getStatus(t: String): ShowStatus { + return when (t) { + "Completed" -> ShowStatus.Completed + "Ongoing" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + } + + override val mainPage = mainPageOf( + "$mainUrl/page/" to "New Episodes", + "$mainUrl/popular-anime/page/" to "Popular Anime", + "$mainUrl/movies/page/" to "Movies", +// "$mainUrl/genres/donghua/page/" to "Donghua", +// "$mainUrl/live-action/page/" to "Live Action", + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val req = app.get(request.data + page) + mainUrl = getBaseUrl(req.url) + val document = req.document + val home = document.select("article").map { + it.toSearchResult() + } + return newHomePageResponse(request.name, home) + } + + private fun getProperAnimeLink(uri: String): String { + return if (uri.contains("/anime/")) { + uri + } else { + var title = uri.substringAfter("$mainUrl/") + title = when { + (title.contains("-episode")) && !(title.contains("-movie")) -> Regex("nonton-(.+)-episode").find( + title + )?.groupValues?.get(1).toString() + + (title.contains("-movie")) -> Regex("nonton-(.+)-movie").find(title)?.groupValues?.get( + 1 + ).toString() + + else -> title + } + + "$mainUrl/anime/$title" + } + } + + private fun Element.toSearchResult(): AnimeSearchResponse { + val href = getProperAnimeLink(fixUrlNull(this.selectFirst("a")?.attr("href")).toString()) + val title = this.select(".bsuxtt, .tt > h4").text().trim() + val posterUrl = fixUrlNull( + this.selectFirst("div.view,div.bt")?.nextElementSibling()?.select("img") + ?.attr("data-src") + ) + val epNum = this.select(".ep").text().replace(Regex("\\D"), "").trim().toIntOrNull() + val tvType = getType(this.selectFirst(".bt > span")?.text().toString()) + return newAnimeSearchResponse(title, href, tvType) { + this.posterUrl = posterUrl + addSub(epNum) + } + + } + + override suspend fun quickSearch(query: String): List? = search(query) + + override suspend fun search(query: String): List? { + mainUrl = app.get(mainUrl).url + return app.post( + "$mainUrl/wp-admin/admin-ajax.php", data = mapOf( + "action" to "ajaxy_sf", + "sf_value" to query, + "search" to "false" + ), headers = mapOf("X-Requested-With" to "XMLHttpRequest") + ).parsedSafe()?.anime?.firstOrNull()?.all?.mapNotNull { + newAnimeSearchResponse( + it.postTitle ?: "", + it.postLink ?: return@mapNotNull null, + TvType.Anime + ) { + this.posterUrl = it.postImage + addSub(it.postLatest?.toIntOrNull()) + } + } + } + + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document + + val title = document.selectFirst(".entry-title")?.text().toString().trim() + val poster = document.selectFirst("div.l[itemprop=image] > img")?.attr("data-src") + val tags = document.select(".infodetail > ul > li:nth-child(2) > a").map { it.text() } + val type = + getType(document.selectFirst(".infodetail > ul > li:nth-child(7)")?.ownText()?.removePrefix(":") + ?.lowercase()?.trim() ?: "tv") + + val trailer = document.selectFirst("div.tply iframe")?.attr("data-src") + val year = Regex("\\d, (\\d*)").find( + document.select(".infodetail > ul > li:nth-child(5)").text() + )?.groupValues?.get(1)?.toIntOrNull() + val status = getStatus( + document.selectFirst(".infodetail > ul > li:nth-child(3)")!!.ownText() + .replace(Regex("\\W"), "") + ) + val description = document.select("span.const > p").text() + + val episodes = document.select("div.bixbox.bxcl > ul > li").mapNotNull { + val link = it.selectFirst("a")?.attr("href") ?: return@mapNotNull null + val name = it.selectFirst("a")?.text() ?: return@mapNotNull null + val episode = + Regex("(\\d+[.,]?\\d*)").find(name)?.groupValues?.getOrNull(0)?.toIntOrNull() + Episode(link, episode = episode) + }.reversed() + + val tracker = APIHolder.getTracker(listOf(title),TrackerType.getTypes(type),year,true) + + return newAnimeLoadResponse(title, url, type) { + engName = title + posterUrl = tracker?.image ?: poster + backgroundPosterUrl = tracker?.cover + this.year = year + addEpisodes(DubStatus.Subbed, episodes) + showStatus = status + plot = description + addTrailer(trailer) + this.tags = tags + addMalId(tracker?.malId) + addAniListId(tracker?.aniId?.toIntOrNull()) + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val document = app.get(data).document + val id = document.selectFirst("div#content script:containsData(is_singular)")?.data() + ?.substringAfter("\"")?.substringBefore("\";") + ?: throw ErrorLoadingException("No id found") + val servers = app.post( + "$animekuUrl/afi.php", data = mapOf( + "id" to id + ), referer = "$mainUrl/" + ).parsedSafe() + + argamap( + { + val decrypt = AesHelper.cryptoAESHandler( + base64Decode(servers?.src ?: return@argamap), + KEY.toByteArray(), + false, + "AES/CBC/NoPadding" + ) + val source = + tryParseJson(decrypt?.toJsonFormat())?.src?.replace("\\", "") + M3u8Helper.generateM3u8( + this.name, + source ?: return@argamap, + "$animekuUrl/", + headers = mapOf("Origin" to animekuUrl) + ).forEach(callback) + }, + { + val decrypt = AesHelper.cryptoAESHandler( + base64Decode(servers?.mirror ?: return@argamap), + KEY.toByteArray(), + false, + "AES/CBC/NoPadding" + ) + tryParseJson(decrypt)?.embed?.map { embed -> + embed.value.apmap { + loadFixedExtractor( + it.value, + embed.key.removePrefix("v"), + "$mainUrl/", + subtitleCallback, + callback + ) + } + } + + } + ) + + return true + } + + private fun String.toJsonFormat(): String { + return if (this.startsWith("\"")) this.substringAfter("\"").substringBeforeLast("\"") + .replace("\\\"", "\"") else this + } + + private suspend fun loadFixedExtractor( + url: String? = null, + quality: String? = null, + referer: String? = null, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + loadExtractor(url ?: return, referer, subtitleCallback) { link -> + callback.invoke( + ExtractorLink( + link.name, + link.name, + link.url, + link.referer, + getQualityFromName(quality), + link.type, + link.headers, + link.extractorData + ) + ) + } + } + + private fun getBaseUrl(url: String): String { + return URI(url).let { + "${it.scheme}://${it.host}" + } + } + + data class Mirrors( + @JsonProperty("embed") val embed: Map> = emptyMap(), + ) + + data class Sources( + @JsonProperty("src") var src: String? = null, + ) + + data class Servers( + @JsonProperty("src") var src: String? = null, + @JsonProperty("mirror") var mirror: String? = null, + ) + + data class All( + @JsonProperty("post_image") var postImage: String? = null, + @JsonProperty("post_image_html") var postImageHtml: String? = null, + @JsonProperty("ID") var ID: Int? = null, + @JsonProperty("post_title") var postTitle: String? = null, + @JsonProperty("post_genres") var postGenres: String? = null, + @JsonProperty("post_type") var postType: String? = null, + @JsonProperty("post_latest") var postLatest: String? = null, + @JsonProperty("post_sub") var postSub: String? = null, + @JsonProperty("post_link") var postLink: String? = null + ) + + data class Anime( + @JsonProperty("all") var all: ArrayList = arrayListOf(), + ) + + data class Search( + @JsonProperty("anime") var anime: ArrayList = arrayListOf() + ) + +} diff --git a/LayarKacaProvider/build.gradle.kts b/LayarKacaProvider/build.gradle.kts new file mode 100644 index 00000000..0be264f6 --- /dev/null +++ b/LayarKacaProvider/build.gradle.kts @@ -0,0 +1,27 @@ +// use an integer for version numbers +version = 16 + + +cloudstream { + language = "id" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + authors = listOf("Hexated") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AsianDrama", + "TvSeries", + "Movie", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=lk21official.org&sz=%size%" +} \ No newline at end of file diff --git a/LayarKacaProvider/src/main/kotlin/com/hexated/LayarKacaProvider.kt b/LayarKacaProvider/src/main/kotlin/com/hexated/LayarKacaProvider.kt new file mode 100644 index 00000000..cbccb55d --- /dev/null +++ b/LayarKacaProvider/src/main/kotlin/com/hexated/LayarKacaProvider.kt @@ -0,0 +1,202 @@ +package com.hexated + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addActors +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.extractors.Filesim +import com.lagradost.cloudstream3.utils.* +import org.jsoup.nodes.Element +import java.net.URLDecoder + +class LayarKacaProvider : MainAPI() { + override var mainUrl = "https://tv3.lk21official.pro" + private var seriesUrl = "https://tv1.nontondrama.click" + override var name = "LayarKaca" + override val hasMainPage = true + override var lang = "id" + override val supportedTypes = setOf( + TvType.Movie, + TvType.TvSeries, + TvType.AsianDrama + ) + + override val mainPage = mainPageOf( + "$mainUrl/populer/page/" to "Film Terplopuler", + "$mainUrl/rating/page/" to "Film Berdasarkan IMDb Rating", + "$mainUrl/most-commented/page/" to "Film Dengan Komentar Terbanyak", + "$seriesUrl/latest/page/" to "Series Terbaru", + "$seriesUrl/series/asian/page/" to "Film Asian Terbaru", + "$mainUrl/latest/page/" to "Film Upload Terbaru", + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val document = app.get(request.data + page).document + val home = document.select("article.mega-item").mapNotNull { + it.toSearchResult() + } + return newHomePageResponse(request.name, home) + } + + private suspend fun getProperLink(url: String): String? { + val res = app.get(url).document + return if (res.select("title").text().contains("- Nontondrama", true)) { + res.selectFirst("div#content a")?.attr("href") + } else { + url + } + } + + private fun Element.toSearchResult(): SearchResponse? { + val title = this.selectFirst("h1.grid-title > a")?.ownText()?.trim() ?: return null + val href = fixUrl(this.selectFirst("a")!!.attr("href")) + val posterUrl = fixUrlNull(this.selectFirst("img")?.attr("src")) + val type = + if (this.selectFirst("div.last-episode") == null) TvType.Movie else TvType.TvSeries + return if (type == TvType.TvSeries) { + val episode = this.selectFirst("div.last-episode span")?.text()?.filter { it.isDigit() } + ?.toIntOrNull() + newAnimeSearchResponse(title, href, TvType.TvSeries) { + this.posterUrl = posterUrl + addSub(episode) + } + } else { + val quality = this.select("div.quality").text().trim() + newMovieSearchResponse(title, href, TvType.Movie) { + this.posterUrl = posterUrl + addQuality(quality) + } + } + } + + override suspend fun search(query: String): List { + val document = app.get("$mainUrl/search.php?s=$query").document + return document.select("div.search-item").mapNotNull { + val title = it.selectFirst("a")?.attr("title") ?: "" + val href = fixUrl(it.selectFirst("a")?.attr("href") ?: return@mapNotNull null) + val posterUrl = fixUrlNull(it.selectFirst("img.img-thumbnail")?.attr("src")) + newTvSeriesSearchResponse(title, href, TvType.TvSeries) { + this.posterUrl = posterUrl + } + } + } + + override suspend fun load(url: String): LoadResponse? { + val fixUrl = getProperLink(url) + val document = app.get(fixUrl ?: return null).document + + val title = document.selectFirst("li.last > span[itemprop=name]")?.text()?.trim().toString() + val poster = fixUrl(document.select("img.img-thumbnail").attr("src").toString()) + val tags = document.select("div.content > div:nth-child(5) > h3 > a").map { it.text() } + + val year = Regex("\\d, (\\d+)").find( + document.select("div.content > div:nth-child(7) > h3").text().trim() + )?.groupValues?.get(1).toString().toIntOrNull() + val tvType = if (document.select("div.serial-wrapper") + .isNotEmpty() + ) TvType.TvSeries else TvType.Movie + val description = document.select("div.content > blockquote").text().trim() + val trailer = document.selectFirst("div.action-player li > a.fancybox")?.attr("href") + val rating = + document.selectFirst("div.content > div:nth-child(6) > h3")?.text()?.toRatingInt() + val actors = + document.select("div.col-xs-9.content > div:nth-child(3) > h3 > a").map { it.text() } + + val recommendations = document.select("div.row.item-media").map { + val recName = it.selectFirst("h3")?.text()?.trim().toString() + val recHref = it.selectFirst(".content-media > a")!!.attr("href") + val recPosterUrl = + fixUrl(it.selectFirst(".poster-media > a > img")?.attr("src").toString()) + newTvSeriesSearchResponse(recName, recHref, TvType.TvSeries) { + this.posterUrl = recPosterUrl + } + } + + return if (tvType == TvType.TvSeries) { + val episodes = document.select("div.episode-list > a:matches(\\d+)").map { + val href = fixUrl(it.attr("href")) + val episode = it.text().toIntOrNull() + val season = + it.attr("href").substringAfter("season-").substringBefore("-").toIntOrNull() + Episode( + href, + "Episode $episode", + season, + episode, + ) + }.reversed() + newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodes) { + this.posterUrl = poster + this.year = year + this.plot = description + this.tags = tags + this.rating = rating + addActors(actors) + this.recommendations = recommendations + addTrailer(trailer) + } + } else { + newMovieLoadResponse(title, url, TvType.Movie, url) { + this.posterUrl = poster + this.year = year + this.plot = description + this.tags = tags + this.rating = rating + addActors(actors) + this.recommendations = recommendations + addTrailer(trailer) + } + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val document = app.get(data).document + document.select("ul#loadProviders > li").map { + fixUrl(it.select("a").attr("href")) + }.apmap { + loadExtractor(it.getIframe(), "https://nganunganu.sbs", subtitleCallback, callback) + } + + return true + } + + private suspend fun String.getIframe() : String { + return app.get(this, referer = "$seriesUrl/").document.select("div.embed iframe").attr("src") + } + +} + +open class Emturbovid : ExtractorApi() { + override val name = "Emturbovid" + override val mainUrl = "https://emturbovid.com" + override val requiresReferer = true + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val response = app.get(url, referer = referer) + val m3u8 = Regex("[\"'](.*?master\\.m3u8.*?)[\"']").find(response.text)?.groupValues?.getOrNull(1) + M3u8Helper.generateM3u8( + name, + m3u8 ?: return, + mainUrl + ).forEach(callback) + } + +} + +class Furher : Filesim() { + override val name = "Furher" + override var mainUrl = "https://furher.in" +} diff --git a/LayarKacaProvider/src/main/kotlin/com/hexated/LayarKacaProviderPlugin.kt b/LayarKacaProvider/src/main/kotlin/com/hexated/LayarKacaProviderPlugin.kt new file mode 100644 index 00000000..9a73cdc1 --- /dev/null +++ b/LayarKacaProvider/src/main/kotlin/com/hexated/LayarKacaProviderPlugin.kt @@ -0,0 +1,16 @@ + +package com.hexated + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class LayarKacaProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(LayarKacaProvider()) + registerExtractorAPI(Emturbovid()) + registerExtractorAPI(Furher()) + } +} \ No newline at end of file diff --git a/Minioppai/build.gradle.kts b/Minioppai/build.gradle.kts new file mode 100644 index 00000000..ee49ee06 --- /dev/null +++ b/Minioppai/build.gradle.kts @@ -0,0 +1,25 @@ +// use an integer for version numbers +version = 8 + + +cloudstream { + language = "id" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + authors = listOf("Sora") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "NSFW", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=minioppai.org&sz=%size%" +} \ No newline at end of file diff --git a/Minioppai/src/main/kotlin/com/hexated/Minioppai.kt b/Minioppai/src/main/kotlin/com/hexated/Minioppai.kt new file mode 100644 index 00000000..2dc220a1 --- /dev/null +++ b/Minioppai/src/main/kotlin/com/hexated/Minioppai.kt @@ -0,0 +1,182 @@ +package com.hexated + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.network.CloudflareKiller +import com.lagradost.cloudstream3.utils.* +import okhttp3.Interceptor +import okhttp3.Response +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import java.net.URLDecoder + +class Minioppai : MainAPI() { + override var mainUrl = "https://minioppai.org" + override var name = "Minioppai" + override val hasMainPage = true + override var lang = "id" + override val hasDownloadSupport = true + override val hasQuickSearch = true + private val cloudflareKiller by lazy { CloudflareKiller() } + private val interceptor by lazy { CloudflareInterceptor(cloudflareKiller) } + override val supportedTypes = setOf( + TvType.NSFW, + ) + + class CloudflareInterceptor(private val cloudflareKiller: CloudflareKiller): Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val response = chain.proceed(request) + val doc = Jsoup.parse(response.peekBody(1024 * 1024).string()) + if (doc.select("title").text() == "Just a moment...") { + return cloudflareKiller.intercept(chain) + } + return response + } + } + + companion object { + fun getStatus(t: String?): ShowStatus { + return when (t) { + "Completed" -> ShowStatus.Completed + "Ongoing" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + } + + override val mainPage = mainPageOf( + "$mainUrl/watch" to "New Episode", + "$mainUrl/populars" to "Popular Hentai", + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val document = app.get("${request.data}/page/$page", interceptor = interceptor).document + val home = document.select("div.latest a").mapNotNull { + it.toSearchResult() + } + return newHomePageResponse( + list = HomePageList( + name = request.name, + list = home, + isHorizontalImages = request.name == "New Episode" + ), + hasNext = true + ) + } + + private fun getProperAnimeLink(uri: String): String { + return if (uri.contains("-episode-")) { + uri.substringBefore("-episode-") + } else { + uri + } + } + + private fun Element.toSearchResult(): AnimeSearchResponse? { + val title = this.selectFirst("h2.entry-title")?.text()?.trim() ?: return null + val href = getProperAnimeLink(this.attr("href")) + val posterUrl = fixUrlNull(this.selectFirst("img")?.attr("src")) + val epNum = this.selectFirst("i.dot")?.text()?.filter { it.isDigit() }?.toIntOrNull() + return newAnimeSearchResponse(title, href, TvType.NSFW) { + this.posterUrl = posterUrl + addSub(epNum) + posterHeaders = cloudflareKiller.getCookieHeaders(mainUrl).toMap() + } + + } + + override suspend fun quickSearch(query: String): List? = search(query) + + override suspend fun search(query: String): List? { + return app.post( + "$mainUrl/wp-admin/admin-ajax.php", data = mapOf( + "action" to "ts_ac_do_search", + "ts_ac_query" to query, + ), headers = mapOf("X-Requested-With" to "XMLHttpRequest"), + interceptor = interceptor + ).parsedSafe()?.post?.firstOrNull()?.all?.mapNotNull { item -> + newAnimeSearchResponse( + item.postTitle ?: "", + item.postLink ?: return@mapNotNull null, + TvType.NSFW + ) { + this.posterUrl = item.postImage + posterHeaders = cloudflareKiller.getCookieHeaders(mainUrl).toMap() + } + } + } + + override suspend fun load(url: String): LoadResponse? { + val document = app.get(url, interceptor = interceptor).document + + val title = document.selectFirst("h1.entry-title")?.text()?.trim() ?: return null + val poster = fixUrlNull(document.selectFirst("div.limage img")?.attr("src")) + val table = document.select("ul.data") + val tags = table.select("ul.data li:nth-child(1) a").map { it.text() } + val year = + document.selectFirst("ul.data time[itemprop=dateCreated]")?.text()?.substringBefore("-") + ?.toIntOrNull() + val status = getStatus(document.selectFirst("ul.data li:nth-child(2) span")?.text()?.trim()) + val description = document.select("div[itemprop=description] > p").text() + + val episodes = document.select("div.epsdlist ul li").mapNotNull { + val name = it.selectFirst("div.epl-num")?.text() + val link = fixUrlNull(it.selectFirst("a")?.attr("href")) ?: return@mapNotNull null + Episode(link, name = name) + }.reversed() + + return newAnimeLoadResponse(title, url, TvType.NSFW) { + engName = title + posterUrl = poster + this.year = year + addEpisodes(DubStatus.Subbed, episodes) + showStatus = status + plot = description + this.tags = tags + posterHeaders = cloudflareKiller.getCookieHeaders(mainUrl).toMap() + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val document = app.get(data, interceptor = interceptor).document + document.select("div.server ul.mirror li a").mapNotNull { + Jsoup.parse(base64Decode(it.attr("data-em"))).select("iframe").attr("src") + }.apmap { link -> + loadExtractor( + fixUrl(decode(link.substringAfter("data="))), + mainUrl, + subtitleCallback, + callback + ) + } + + return true + } + + private fun decode(input: String): String = URLDecoder.decode(input, "utf-8") + + data class SearchResponses( + @JsonProperty("post") var post: ArrayList = arrayListOf() + ) + + data class All( + @JsonProperty("ID") var ID: Int? = null, + @JsonProperty("post_image") var postImage: String? = null, + @JsonProperty("post_title") var postTitle: String? = null, + @JsonProperty("post_link") var postLink: String? = null, + ) + + data class Post( + @JsonProperty("all") var all: ArrayList = arrayListOf(), + ) + +} \ No newline at end of file diff --git a/Movierulzhd/build.gradle.kts b/Movierulzhd/build.gradle.kts new file mode 100644 index 00000000..2f1b59d2 --- /dev/null +++ b/Movierulzhd/build.gradle.kts @@ -0,0 +1,26 @@ +// use an integer for version numbers +version = 40 + + +cloudstream { + language = "hi" + // All of these properties are optional, you can safely remove them + + description = "Include: Hdmovie2" + authors = listOf("Hexated") + + /** + * 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=movierulzhd.best&sz=%size%" +} diff --git a/Movierulzhd/src/main/kotlin/com/hexated/Extractors.kt b/Movierulzhd/src/main/kotlin/com/hexated/Extractors.kt new file mode 100644 index 00000000..a96e9c7b --- /dev/null +++ b/Movierulzhd/src/main/kotlin/com/hexated/Extractors.kt @@ -0,0 +1,181 @@ +package com.hexated + +import com.fasterxml.jackson.annotation.JsonProperty +import com.hexated.RabbitStream.extractRabbitStream +import com.lagradost.cloudstream3.APIHolder +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.apmap +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.* +import kotlin.random.Random + +const val twoEmbedAPI = "https://www.2embed.to" + +class Sbnmp : Sbflix() { + override val name = "Sbnmp" + override var mainUrl = "https://sbnmp.bar" +} + +class Sbrulz : Sbflix() { + override val name = "Sbrulz" + override var mainUrl = "https://sbrulz.xyz" +} + +class Sbmiz : Sbflix() { + override val name = "Sbmiz" + override var mainUrl = "https://sbmiz.site" + } + +open class Sbflix : ExtractorApi() { + override val mainUrl = "https://sbflix.xyz" + override val name = "Sbflix" + override val requiresReferer = false + private val alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val regexID = + Regex("(embed-[a-zA-Z\\d]{0,8}[a-zA-Z\\d_-]+|/e/[a-zA-Z\\d]{0,8}[a-zA-Z\\d_-]+)") + val id = regexID.findAll(url).map { + it.value.replace(Regex("(embed-|/e/)"), "") + }.first() + val master = "$mainUrl/375664356a494546326c4b797c7c6e756577776778623171737/${encodeId(id)}" + val headers = mapOf( + "watchsb" to "sbstream", + ) + val mapped = app.get( + master.lowercase(), + headers = headers, + referer = url, + ).parsedSafe
() + callback.invoke( + ExtractorLink( + name, + name, + mapped?.streamData?.file ?: return, + url, + Qualities.P720.value, + isM3u8 = true, + headers = headers + ) + ) + + mapped.streamData.subs?.map {sub -> + subtitleCallback.invoke( + SubtitleFile( + sub.label.toString(), + sub.file ?: return@map null, + ) + ) + } + } + + private fun encodeId(id: String): String { + val code = "${createHashTable()}||$id||${createHashTable()}||streamsb" + return code.toCharArray().joinToString("") { char -> + char.code.toString(16) + } + } + + private fun createHashTable(): String { + return buildString { + repeat(12) { + append(alphabet[Random.nextInt(alphabet.length)]) + } + } + } + + data class Subs ( + @JsonProperty("file") val file: String? = null, + @JsonProperty("label") val label: String? = null, + ) + + data class StreamData ( + @JsonProperty("file") val file: String, + @JsonProperty("cdn_img") val cdnImg: String, + @JsonProperty("hash") val hash: String, + @JsonProperty("subs") val subs: ArrayList? = arrayListOf(), + @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, + ) + +} + +open class Akamaicdn : ExtractorApi() { + override val name = "Akamaicdn" + override val mainUrl = "https://akamaicdn.life" + override val requiresReferer = true + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val res = app.get(url, referer = referer).document + val mapper = res.select("script:containsData(sniff)").last()?.data()?.substringAfter("sniff(") + ?.substringBefore(");")?.split(",")?.map { it.replace("\"", "").trim() } ?: return + callback.invoke( + ExtractorLink( + this.name, + this.name, + "$mainUrl/m3u8/${mapper[1]}/${mapper[2]}/master.txt?s=1&cache=1", + url, + Qualities.Unknown.value, + isM3u8 = true, + ) + ) + } + +} + +suspend fun invokeTwoEmbed( + url: String? = null, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit +) { + val document = app.get(url ?: return).document + val captchaKey = + document.select("script[src*=https://www.google.com/recaptcha/api.js?render=]") + .attr("src").substringAfter("render=") + + document.select(".dropdown-menu a[data-id]").map { it.attr("data-id") }.apmap { serverID -> + val token = APIHolder.getCaptchaToken(url, captchaKey) + app.get( + "${twoEmbedAPI}/ajax/embed/play?id=$serverID&_token=$token", referer = url + ).parsedSafe()?.let { source -> + val link = source.link ?: return@let + if (link.contains("rabbitstream")) { + extractRabbitStream( + link, + subtitleCallback, + callback, + false, + decryptKey = RabbitStream.getKey() + ) { it } + } else { + loadExtractor( + link, twoEmbedAPI, subtitleCallback, callback + ) + } + } + } +} + +data class EmbedJson( + @JsonProperty("type") val type: String? = null, + @JsonProperty("link") val link: String? = null, + @JsonProperty("sources") val sources: List = arrayListOf(), + @JsonProperty("tracks") val tracks: List? = null, +) diff --git a/Movierulzhd/src/main/kotlin/com/hexated/Hdmovie2.kt b/Movierulzhd/src/main/kotlin/com/hexated/Hdmovie2.kt new file mode 100644 index 00000000..63831c0e --- /dev/null +++ b/Movierulzhd/src/main/kotlin/com/hexated/Hdmovie2.kt @@ -0,0 +1,90 @@ +package com.hexated + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.apmap +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mainPageOf +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.Jsoup + +class Hdmovie2 : Movierulzhd() { + + override var mainUrl = "https://hdmovie2.codes" + + override var name = "Hdmovie2" + + override val mainPage = mainPageOf( + "trending" to "Trending", + "movies" to "Movies", + "genre/tv-series" to "TV-Series", + "genre/netflix" to "Netflix", + "genre/zee5-tv-series" to "Zee5 TV Series", + ) + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + if (data.startsWith("{")) { + val loadData = tryParseJson(data) + val source = app.get( + url = "$directUrl/wp-json/dooplayer/v2/${loadData?.post}/${loadData?.type}/${loadData?.nume}", + referer = data, + headers = mapOf("X-Requested-With" to "XMLHttpRequest") + ).parsed().embed_url.getIframe() + if (!source.contains("youtube")) loadExtractor( + source, + "$directUrl/", + subtitleCallback, + callback + ) + } else { + var document = app.get(data).document + if (document.select("title").text() == "Just a moment...") { + document = app.get(data, interceptor = interceptor).document + } + val id = document.select("meta#dooplay-ajax-counter").attr("data-postid") + val type = if (data.contains("/movies/")) "movie" else "tv" + + document.select("ul#playeroptionsul > li").map { + it.attr("data-nume") + }.apmap { nume -> + val source = app.get( + url = "$directUrl/wp-json/dooplayer/v2/${id}/${type}/${nume}", + referer = data, + headers = mapOf("X-Requested-With" to "XMLHttpRequest") + ).parsed().embed_url.getIframe() + when { + !source.contains("youtube") -> loadExtractor( + source, + "$directUrl/", + subtitleCallback, + callback + ) + else -> return@apmap + } + } + } + return true + } + + private fun String.getIframe(): String { + return Jsoup.parse(this).select("iframe").attr("src") + } + + data class LinkData( + val type: String? = null, + val post: String? = null, + val nume: String? = null, + ) + + data class ResponseHash( + @JsonProperty("embed_url") val embed_url: String, + @JsonProperty("type") val type: String?, + ) +} diff --git a/Movierulzhd/src/main/kotlin/com/hexated/Movierulzhd.kt b/Movierulzhd/src/main/kotlin/com/hexated/Movierulzhd.kt new file mode 100644 index 00000000..9c5c93af --- /dev/null +++ b/Movierulzhd/src/main/kotlin/com/hexated/Movierulzhd.kt @@ -0,0 +1,283 @@ +package com.hexated + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addActors +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.network.CloudflareKiller +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import org.jsoup.nodes.Element +import java.net.URI + +open class Movierulzhd : MainAPI() { + + override var mainUrl = "https://movierulzvid.gold" + + var directUrl = "" + override var name = "Movierulzhd" + override val hasMainPage = true + override var lang = "hi" + override val hasDownloadSupport = true + override val supportedTypes = setOf( + TvType.Movie, + TvType.TvSeries, + ) + + override val mainPage = mainPageOf( + "trending" to "Trending", + "movies" to "Movies", + "tvshows" to "TV Shows", + "genre/netflix" to "Netflix", + "genre/amazon-prime" to "Amazon Prime", + "genre/Zee5" to "Zee5", + "seasons" to "Season", + "episodes" to "Episode", + ) + + val interceptor = CloudflareKiller() + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + var document = app.get("$mainUrl/${request.data}/page/$page").document + if (document.select("title").text() == "Just a moment...") { + document = app.get(request.data + page, interceptor = interceptor).document + } + val home = + document.select("div.items.normal article, div#archive-content article").mapNotNull { + it.toSearchResult() + } + return newHomePageResponse(request.name, home) + } + + private fun getProperLink(uri: String): String { + return when { + uri.contains("/episodes/") -> { + var title = uri.substringAfter("$mainUrl/episodes/") + title = Regex("(.+?)-season").find(title)?.groupValues?.get(1).toString() + "$mainUrl/tvshows/$title" + } + + uri.contains("/seasons/") -> { + var title = uri.substringAfter("$mainUrl/seasons/") + title = Regex("(.+?)-season").find(title)?.groupValues?.get(1).toString() + "$mainUrl/tvshows/$title" + } + + else -> { + uri + } + } + } + + private fun Element.toSearchResult(): SearchResponse? { + val title = this.selectFirst("h3 > a")?.text() ?: return null + val href = getProperLink(fixUrl(this.selectFirst("h3 > a")!!.attr("href"))) + val posterUrl = fixUrlNull(this.select("div.poster img").last()?.attr("src")) + val quality = getQualityFromString(this.select("span.quality").text()) + return newMovieSearchResponse(title, href, TvType.Movie) { + this.posterUrl = posterUrl + this.quality = quality + posterHeaders = interceptor.getCookieHeaders(mainUrl).toMap() + } + + } + + override suspend fun search(query: String): List { + val link = "$mainUrl/search/$query" + var document = app.get(link).document + if (document.select("title").text() == "Just a moment...") { + document = app.get(link, interceptor = interceptor).document + } + + return document.select("div.result-item").map { + val title = + it.selectFirst("div.title > a")!!.text().replace(Regex("\\(\\d{4}\\)"), "").trim() + val href = getProperLink(it.selectFirst("div.title > a")!!.attr("href")) + val posterUrl = it.selectFirst("img")!!.attr("src").toString() + newMovieSearchResponse(title, href, TvType.TvSeries) { + this.posterUrl = posterUrl + posterHeaders = interceptor.getCookieHeaders(mainUrl).toMap() + } + } + } + + override suspend fun load(url: String): LoadResponse { + val request = app.get(url) + var document = request.document + if (document.select("title").text() == "Just a moment...") { + document = app.get(url, interceptor = interceptor).document + } + directUrl = getBaseUrl(request.url) + val title = + document.selectFirst("div.data > h1")?.text()?.trim().toString() + val poster = fixUrlNull(document.select("div.poster img:last-child").attr("src")) + val tags = document.select("div.sgeneros > a").map { it.text() } + + val year = Regex(",\\s?(\\d+)").find( + document.select("span.date").text().trim() + )?.groupValues?.get(1).toString().toIntOrNull() + val tvType = if (document.select("ul#section > li:nth-child(1)").text() + .contains("Episodes") || document.select("ul#playeroptionsul li span.title") + .text().contains( + Regex("Episode\\s+\\d+|EP\\d+|PE\\d+") + ) + ) TvType.TvSeries else TvType.Movie + val description = document.select("div.wp-content > p").text().trim() + val trailer = document.selectFirst("div.embed iframe")?.attr("src") + val rating = + document.selectFirst("span.dt_rating_vgs")?.text()?.toRatingInt() + val actors = document.select("div.persons > div[itemprop=actor]").map { + Actor( + it.select("meta[itemprop=name]").attr("content"), + it.select("img:last-child").attr("src") + ) + } + + val recommendations = document.select("div.owl-item").map { + val recName = + it.selectFirst("a")!!.attr("href").toString().removeSuffix("/").split("/").last() + val recHref = it.selectFirst("a")!!.attr("href") + val recPosterUrl = it.selectFirst("img")?.attr("src").toString() + newTvSeriesSearchResponse(recName, recHref, TvType.TvSeries) { + this.posterUrl = recPosterUrl + posterHeaders = interceptor.getCookieHeaders(url).toMap() + } + } + + return if (tvType == TvType.TvSeries) { + val episodes = if (document.select("ul.episodios > li").isNotEmpty()) { + document.select("ul.episodios > li").map { + val href = it.select("a").attr("href") + val name = fixTitle(it.select("div.episodiotitle > a").text().trim()) + val image = it.select("div.imagen > img").attr("src") + val episode = + it.select("div.numerando").text().replace(" ", "").split("-").last() + .toIntOrNull() + val season = + it.select("div.numerando").text().replace(" ", "").split("-").first() + .toIntOrNull() + Episode( + href, + name, + season, + episode, + image + ) + } + } else { + document.select("ul#playeroptionsul > li").map { + val name = it.selectFirst("span.title")?.text() + val type = it.attr("data-type") + val post = it.attr("data-post") + val nume = it.attr("data-nume") + Episode( + LinkData(type, post, nume).toJson(), + name, + ) + } + } + newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodes) { + this.posterUrl = poster + this.year = year + this.plot = description + this.tags = tags + this.rating = rating + addActors(actors) + this.recommendations = recommendations + addTrailer(trailer) + posterHeaders = interceptor.getCookieHeaders(url).toMap() + } + } else { + newMovieLoadResponse(title, url, TvType.Movie, url) { + this.posterUrl = poster + this.year = year + this.plot = description + this.tags = tags + this.rating = rating + addActors(actors) + this.recommendations = recommendations + addTrailer(trailer) + posterHeaders = interceptor.getCookieHeaders(url).toMap() + } + } + } + + private fun getBaseUrl(url: String): String { + return URI(url).let { + "${it.scheme}://${it.host}" + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + if (data.startsWith("{")) { + val loadData = AppUtils.tryParseJson(data) + val source = app.post( + url = "$directUrl/wp-admin/admin-ajax.php", + data = mapOf( + "action" to "doo_player_ajax", + "post" to "${loadData?.post}", + "nume" to "${loadData?.nume}", + "type" to "${loadData?.type}" + ), + referer = data, + headers = mapOf("X-Requested-With" to "XMLHttpRequest") + ).parsed().embed_url + if (!source.contains("youtube")) loadExtractor(source, data, subtitleCallback, callback) + } else { + var document = app.get(data).document + if (document.select("title").text() == "Just a moment...") { + document = app.get(data, interceptor = interceptor).document + } + val id = document.select("meta#dooplay-ajax-counter").attr("data-postid") + val type = if (data.contains("/movies/")) "movie" else "tv" + + document.select("ul#playeroptionsul > li").map { + it.attr("data-nume") + }.apmap { nume -> + val source = app.post( + url = "$directUrl/wp-admin/admin-ajax.php", + data = mapOf( + "action" to "doo_player_ajax", + "post" to id, + "nume" to nume, + "type" to type + ), + referer = data, + headers = mapOf("X-Requested-With" to "XMLHttpRequest") + ).parsed().embed_url + + when { + !source.contains("youtube") -> loadExtractor( + source, + data, + subtitleCallback, + callback + ) + else -> return@apmap + } + } + } + return true + } + + data class LinkData( + val type: String? = null, + val post: String? = null, + val nume: String? = null, + ) + + data class ResponseHash( + @JsonProperty("embed_url") val embed_url: String, + @JsonProperty("type") val type: String?, + ) + +} diff --git a/Movierulzhd/src/main/kotlin/com/hexated/MovierulzhdPlugin.kt b/Movierulzhd/src/main/kotlin/com/hexated/MovierulzhdPlugin.kt new file mode 100644 index 00000000..a97f4cc9 --- /dev/null +++ b/Movierulzhd/src/main/kotlin/com/hexated/MovierulzhdPlugin.kt @@ -0,0 +1,20 @@ + +package com.hexated + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class MovierulzhdPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(Movierulzhd()) + registerMainAPI(Hdmovie2()) + registerExtractorAPI(Sbflix()) + registerExtractorAPI(Sbrulz()) + registerExtractorAPI(Sbmiz()) + registerExtractorAPI(Sbnmp()) + registerExtractorAPI(Akamaicdn()) + } +} \ No newline at end of file diff --git a/Nekopoi/build.gradle.kts b/Nekopoi/build.gradle.kts new file mode 100644 index 00000000..809c2b60 --- /dev/null +++ b/Nekopoi/build.gradle.kts @@ -0,0 +1,25 @@ +// use an integer for version numbers +version = 6 + + +cloudstream { + language = "id" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + authors = listOf("Sora") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "NSFW", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=nekopoi.care&sz=%size%" +} \ No newline at end of file diff --git a/Nekopoi/src/main/AndroidManifest.xml b/Nekopoi/src/main/AndroidManifest.xml new file mode 100644 index 00000000..c98063f8 --- /dev/null +++ b/Nekopoi/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Nekopoi/src/main/kotlin/com/hexated/Extractors.kt b/Nekopoi/src/main/kotlin/com/hexated/Extractors.kt new file mode 100644 index 00000000..47e90c61 --- /dev/null +++ b/Nekopoi/src/main/kotlin/com/hexated/Extractors.kt @@ -0,0 +1,32 @@ +package com.hexated + +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.Qualities + +open class ZippyShare : ExtractorApi() { + override val name = "ZippyShare" + override val mainUrl = "https://zippysha.re" + override val requiresReferer = true + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val res = app.get(url, referer = referer).document + val video = res.selectFirst("a#download-url")?.attr("href") + callback.invoke( + ExtractorLink( + this.name, + this.name, + video ?: return, + "$mainUrl/", + Qualities.Unknown.value + ) + ) + } +} \ No newline at end of file diff --git a/Nekopoi/src/main/kotlin/com/hexated/Nekopoi.kt b/Nekopoi/src/main/kotlin/com/hexated/Nekopoi.kt new file mode 100644 index 00000000..124595b6 --- /dev/null +++ b/Nekopoi/src/main/kotlin/com/hexated/Nekopoi.kt @@ -0,0 +1,292 @@ +package com.hexated + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.* +import com.lagradost.nicehttp.NiceResponse +import com.lagradost.nicehttp.Requests +import com.lagradost.nicehttp.Session +import kotlinx.coroutines.delay +import org.jsoup.nodes.Element +import java.net.URI + +class Nekopoi : MainAPI() { + override var mainUrl = "https://nekopoi.care" + override var name = "Nekopoi" + override val hasMainPage = true + override var lang = "id" + private val fetch by lazy { Session(app.baseClient) } + override val supportedTypes = setOf( + TvType.NSFW, + ) + + companion object { + val session = Session(Requests().baseClient) + val mirrorBlackList = arrayOf( + "MegaupNet", + "DropApk", + "Racaty", + "ZippyShare", + "VideobinCo", + "DropApk", + "SendCm", + "GoogleDrive", + ) + const val mirroredHost = "https://www.mirrored.to" + + fun getStatus(t: String?): ShowStatus { + return when (t) { + "Completed" -> ShowStatus.Completed + "Ongoing" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + + } + + override val mainPage = mainPageOf( + "$mainUrl/category/hentai/" to "Hentai", + "$mainUrl/category/jav/" to "Jav", + "$mainUrl/category/3d-hentai/" to "3D Hentai", + "$mainUrl/category/jav-cosplay/" to "Jav Cosplay", + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val document = fetch.get("${request.data}/page/$page").document + val home = document.select("div.result ul li").mapNotNull { + it.toSearchResult() + } + return newHomePageResponse( + list = HomePageList( + name = request.name, + list = home, + isHorizontalImages = true + ), + hasNext = true + ) + } + + private fun getProperAnimeLink(uri: String): String { + return if (uri.contains("-episode-")) { + val title = uri.substringAfter("$mainUrl/").substringBefore("-episode-") + .removePrefix("new-release-").removePrefix("uncensored-") + "$mainUrl/hentai/$title" + } else { + uri + } + } + + private fun Element.toSearchResult(): AnimeSearchResponse? { + val title = this.selectFirst("h2 a")?.text()?.trim() ?: return null + val href = getProperAnimeLink(this.selectFirst("a")?.attr("href") ?: return null) + val posterUrl = fixUrlNull(this.selectFirst("img")?.attr("src")) + val epNum = this.selectFirst("i.dot")?.text()?.filter { it.isDigit() }?.toIntOrNull() + return newAnimeSearchResponse(title, href, TvType.NSFW) { + this.posterUrl = posterUrl + addSub(epNum) + } + + } + + override suspend fun search(query: String): List { + return fetch.get("$mainUrl/search/$query").document.select("div.result ul li") + .mapNotNull { + it.toSearchResult() + } + } + + override suspend fun load(url: String): LoadResponse { + val document = fetch.get(url).document + + val title = document.selectFirst("span.desc b, div.eroinfo h1")?.text()?.trim() ?: "" + val poster = fixUrlNull(document.selectFirst("div.imgdesc img, div.thm img")?.attr("src")) + val table = document.select("div.listinfo ul, div.konten") + val tags = + table.select("li:contains(Genres) a").map { it.text() }.takeIf { it.isNotEmpty() } + ?: table.select("p:contains(Genre)").text().substringAfter(":").split(",") + .map { it.trim() } + val year = + document.selectFirst("li:contains(Tayang)")?.text()?.substringAfterLast(",") + ?.filter { it.isDigit() }?.toIntOrNull() + val status = getStatus( + document.selectFirst("li:contains(Status)")?.text()?.substringAfter(":")?.trim() + ) + val duration = document.selectFirst("li:contains(Durasi)")?.text()?.substringAfterLast(":") + ?.filter { it.isDigit() }?.toIntOrNull() + val description = document.selectFirst("span.desc p")?.text() + + val episodes = document.select("div.episodelist ul li").mapNotNull { + val name = it.selectFirst("a")?.text() + val link = fixUrlNull(it.selectFirst("a")?.attr("href")) ?: return@mapNotNull null + Episode(link, name = name) + }.takeIf { it.isNotEmpty() } ?: listOf(Episode(url, title)) + + return newAnimeLoadResponse(title, url, TvType.NSFW) { + engName = title + posterUrl = poster + this.year = year + this.duration = duration + addEpisodes(DubStatus.Subbed, episodes) + showStatus = status + plot = description + this.tags = tags + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val res = fetch.get(data).document + + argamap( + { + res.select("div#show-stream iframe").apmap { iframe -> + loadExtractor(iframe.attr("src"), "$mainUrl/", subtitleCallback, callback) + } + }, + { + res.select("div.boxdownload div.liner").map { ele -> + getIndexQuality( + ele.select("div.name").text() + ) to ele.selectFirst("a:contains(ouo)") + ?.attr("href") + }.filter { it.first != Qualities.P360.value }.map { + val bypassedAds = bypassMirrored(bypassOuo(it.second)) + bypassedAds.apmap ads@{ adsLink -> + loadExtractor( + fixEmbed(adsLink) ?: return@ads, + "$mainUrl/", + subtitleCallback, + ) { link -> + callback.invoke( + ExtractorLink( + link.name, + link.name, + link.url, + link.referer, + if (link.type == ExtractorLinkType.M3U8) link.quality else it.first, + link.type, + link.headers, + link.extractorData + ) + ) + } + } + } + } + ) + + return true + } + + private fun fixEmbed(url: String?): String? { + if (url == null) return null + val host = getBaseUrl(url) + return when { + url.contains("streamsb", true) -> url.replace("$host/", "$host/e/") + else -> url + } + } + + private fun getBaseUrl(url: String): String { + return URI(url).let { + "${it.scheme}://${it.host}" + } + } + + private suspend fun bypassOuo(url: String?): String? { + var res = session.get(url ?: return null) + run lit@{ + (1..2).forEach { _ -> + if (res.headers["location"] != null) return@lit + val document = res.document + val nextUrl = document.select("form").attr("action") + val data = document.select("form input").mapNotNull { + it.attr("name") to it.attr("value") + }.toMap().toMutableMap() + val captchaKey = + document.select("script[src*=https://www.google.com/recaptcha/api.js?render=]") + .attr("src").substringAfter("render=") + val token = APIHolder.getCaptchaToken(url, captchaKey) + data["x-token"] = token ?: "" + res = session.post( + nextUrl, + data = data, + headers = mapOf("content-type" to "application/x-www-form-urlencoded"), + allowRedirects = false + ) + } + } + + return res.headers["location"] + } + + private fun NiceResponse.selectMirror(): String? { + return this.document.selectFirst("script:containsData(#passcheck)")?.data() + ?.substringAfter("\"GET\", \"")?.substringBefore("\"") + } + + private suspend fun bypassMirrored(url: String?): List { + val request = session.get(url ?: return emptyList()) + delay(2000) + val mirrorUrl = request.selectMirror() ?: run { + val nextUrl = request.document.select("div.col-sm.centered.extra-top a").attr("href") + app.get(nextUrl).selectMirror() + } + return session.get( + fixUrl( + mirrorUrl ?: return emptyList(), + mirroredHost + ) + ).document.select("table.hoverable tbody tr") + .filter { mirror -> + !mirrorIsBlackList(mirror.selectFirst("img")?.attr("alt")) + }.apmap { + val fileLink = it.selectFirst("a")?.attr("href") + session.get( + fixUrl( + fileLink ?: return@apmap null, + mirroredHost + ) + ).document.selectFirst("div.code_wrap code")?.text() + } + } + + private fun mirrorIsBlackList(host: String?): Boolean { + return mirrorBlackList.any { it.equals(host, true) } + } + + private fun fixUrl(url: String, domain: String): String { + if (url.startsWith("http")) { + return url + } + if (url.isEmpty()) { + return "" + } + + val startsWithNoHttp = url.startsWith("//") + if (startsWithNoHttp) { + return "https:$url" + } else { + if (url.startsWith('/')) { + return domain + url + } + return "$domain/$url" + } + } + + private fun getIndexQuality(str: String?): Int { + return when (val quality = + Regex("""(?i)\[(\d+[pk])]""").find(str ?: "")?.groupValues?.getOrNull(1)?.lowercase()) { + "2k" -> Qualities.P1440.value + else -> getQualityFromName(quality) + } + } + +} \ No newline at end of file diff --git a/Nekopoi/src/main/kotlin/com/hexated/NekopoiPlugin.kt b/Nekopoi/src/main/kotlin/com/hexated/NekopoiPlugin.kt new file mode 100644 index 00000000..62dea5a4 --- /dev/null +++ b/Nekopoi/src/main/kotlin/com/hexated/NekopoiPlugin.kt @@ -0,0 +1,14 @@ +package com.hexated + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class NekopoiPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(Nekopoi()) + registerExtractorAPI(ZippyShare()) + } +} \ No newline at end of file diff --git a/NeonimeProvider/build.gradle.kts b/NeonimeProvider/build.gradle.kts new file mode 100644 index 00000000..bfa7a96c --- /dev/null +++ b/NeonimeProvider/build.gradle.kts @@ -0,0 +1,28 @@ +// use an integer for version numbers +version = 8 + + +cloudstream { + language = "id" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + authors = listOf("Hexated") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AnimeMovie", + "Anime", + "Movie", + "OVA", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=neonime.watch&sz=%size%" +} \ No newline at end of file diff --git a/NeonimeProvider/src/main/kotlin/com/hexated/NeonimeProvider.kt b/NeonimeProvider/src/main/kotlin/com/hexated/NeonimeProvider.kt new file mode 100644 index 00000000..f6e2d83c --- /dev/null +++ b/NeonimeProvider/src/main/kotlin/com/hexated/NeonimeProvider.kt @@ -0,0 +1,195 @@ +package com.hexated + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.nodes.Element +import java.net.URI + +class NeonimeProvider : MainAPI() { + override var mainUrl = "https://neonime.ink" + private var baseUrl = mainUrl + override var name = "Neonime" + override val hasQuickSearch = false + override val hasMainPage = true + override var lang = "id" + override val hasDownloadSupport = true + + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + companion object { + fun getType(t: String): TvType { + return if (t.contains("OVA", true) || t.contains("Special", true)) TvType.OVA + else if (t.contains("Movie", true)) TvType.AnimeMovie + else TvType.Anime + } + + fun getStatus(t: String): ShowStatus { + return when (t) { + "Ended" -> ShowStatus.Completed + "OnGoing" -> ShowStatus.Ongoing + "Ongoing" -> ShowStatus.Ongoing + "In Production" -> ShowStatus.Ongoing + "Returning Series" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + } + + override val mainPage = mainPageOf( + "$mainUrl/episode/page/" to "Episode Terbaru", + "$mainUrl/tvshows/page/" to "Anime Terbaru", + "$mainUrl/movies/page/" to "Movie", + ) + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + val req = app.get(request.data + page) + baseUrl = getBaseUrl(req.url) + val home = req.document.select("tbody tr,div.item").mapNotNull { + it.toSearchResult() + } + return newHomePageResponse(request.name, home) + } + + private fun getProperAnimeLink(uri: String): String { + return when { + uri.contains("/episode") -> { + val title = uri.substringAfter("$baseUrl/episode/").let { tt -> + val fixTitle = Regex("(.*)-\\d{1,2}x\\d+").find(tt)?.groupValues?.getOrNull(1).toString() + when { + !tt.contains("-season") && !tt.contains(Regex("-1x\\d+")) && !tt.contains("one-piece") -> "$fixTitle-season-${Regex("-(\\d{1,2})x\\d+").find(tt)?.groupValues?.getOrNull(1).toString()}" + tt.contains("-special") -> fixTitle.replace(Regex("-x\\d+"), "") + !fixTitle.contains("-subtitle-indonesia") -> "$fixTitle-subtitle-indonesia" + else -> fixTitle + } + } + +// title = when { +// title.contains("youkoso-jitsuryoku") && !title.contains("-season") -> title.replace("-e-", "-e-tv-") +// else -> title +// } + + "$baseUrl/tvshows/$title" + } + else -> uri + } + } + + private fun Element.toSearchResult(): AnimeSearchResponse? { + val title = this.selectFirst("td.bb a")?.ownText() ?: this.selectFirst("h2")?.text() ?: return null + val href = getProperAnimeLink(fixUrl(this.select("a").attr("href"))) + val posterUrl = fixUrl(this.select("img").attr("data-src")) + val epNum = this.selectFirst("td.bb span")?.text()?.let { eps -> + Regex("Episode\\s?(\\d+)").find(eps)?.groupValues?.getOrNull(1)?.toIntOrNull() + } + + return newAnimeSearchResponse(title, href, TvType.Anime) { + this.posterUrl = posterUrl + addSub(epNum) + } + + } + + override suspend fun search(query: String): List { + val document = app.get("$baseUrl/?s=$query").document + return document.select("div.item.episode-home").mapNotNull { + val title = it.selectFirst("div.judul-anime > span")!!.text() + val poster = it.select("img").attr("data-src").toString().trim() + val episodes = it.selectFirst("div.fixyear > h2.text-center")!! + .text().replace(Regex("\\D"), "").trim().toIntOrNull() + val tvType = getType(it.selectFirst("span.calidad2.episode")?.text().toString()) + val href = getProperAnimeLink(fixUrl(it.selectFirst("a")!!.attr("href"))) + + newAnimeSearchResponse(title, href, tvType) { + this.posterUrl = poster + addSub(episodes) + } + } + } + + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document + + if (url.contains("movie") || url.contains("live-action")) { + val mTitle = document.selectFirst(".sbox > .data > h1[itemprop = name]")?.text().toString().replace("Subtitle Indonesia", "").trim() + val mPoster = document.selectFirst(".sbox > .imagen > .fix > img[itemprop = image]")?.attr("data-src") + val mTrailer = document.selectFirst("div.youtube_id iframe")?.attr("data-wpfc-original-src")?.substringAfterLast("html#")?.let{ "https://www.youtube.com/embed/$it"} + val year = document.selectFirst("a[href*=release-year]")!!.text().toIntOrNull() + val tracker = APIHolder.getTracker(listOf(mTitle),TrackerType.getTypes(TvType.Movie),year,true) + return newMovieLoadResponse(name = mTitle, url = url, type = TvType.Movie, dataUrl = url) { + posterUrl = tracker?.image ?: mPoster + backgroundPosterUrl = tracker?.cover + this.year = year + plot = document.select("div[itemprop = description]").text().trim() + rating = document.select("span[itemprop = ratingValue]").text().toIntOrNull() + tags = document.select("p.meta_dd > a").map { it.text() } + addTrailer(mTrailer) + addMalId(tracker?.malId) + addAniListId(tracker?.aniId?.toIntOrNull()) + } + } + else { + val title = document.select("h1[itemprop = name]").text().replace("Subtitle Indonesia", "").trim() + val poster = document.selectFirst(".imagen > img")?.attr("data-src") + val trailer = document.selectFirst("div.youtube_id_tv iframe")?.attr("data-wpfc-original-src")?.substringAfterLast("html#")?.let{ "https://www.youtube.com/embed/$it"} + val year = document.select("#info a[href*=\"-year/\"]").text().toIntOrNull() + val episodes = document.select("ul.episodios > li").mapNotNull { + val link = fixUrl(it.selectFirst(".episodiotitle > a")!!.attr("href")) + val name = it.selectFirst(".episodiotitle > a")?.ownText().toString() + val episode = Regex("(\\d+[.,]?\\d*)").find(name)?.groupValues?.getOrNull(0)?.toIntOrNull() + Episode(link, episode = episode) + }.reversed() + val tracker = APIHolder.getTracker(listOf(title),TrackerType.getTypes(TvType.Anime),year,true) + return newAnimeLoadResponse(title, url, TvType.Anime) { + engName = title + posterUrl = tracker?.image ?: poster + backgroundPosterUrl = tracker?.cover + this.year = year + addEpisodes(DubStatus.Subbed, episodes) + showStatus = getStatus(document.select("div.metadatac > span").last()!!.text().trim()) + plot = document.select("div[itemprop = description] > p").text().trim() + tags = document.select("#info a[href*=\"-genre/\"]").map { it.text() } + addTrailer(trailer) + addMalId(tracker?.malId) + addAniListId(tracker?.aniId?.toIntOrNull()) + } + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val source = if(data.contains("movie") || data.contains("live-action")) { + app.get(data).document.select("#player2-1 > div[id*=div]").mapNotNull { + fixUrl(it.select("iframe").attr("data-src")) + } + } else { + app.get(data).document.select(".player2 > .embed2 > div[id*=player]").mapNotNull { + fixUrl(it.select("iframe").attr("data-src")) + } + } + + source.apmap { + loadExtractor(it, data, subtitleCallback, callback) + } + + return true + } + + private fun getBaseUrl(url: String): String { + return URI(url).let { + "${it.scheme}://${it.host}" + } + } + +} \ No newline at end of file diff --git a/Nimegami/build.gradle.kts b/Nimegami/build.gradle.kts new file mode 100644 index 00000000..a1e4ca98 --- /dev/null +++ b/Nimegami/build.gradle.kts @@ -0,0 +1,27 @@ +// use an integer for version numbers +version = 5 + + +cloudstream { + language = "id" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + authors = listOf("Hexated") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AnimeMovie", + "Anime", + "OVA", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=nimegami.id&sz=%size%" +} \ No newline at end of file diff --git a/Nimegami/src/main/AndroidManifest.xml b/Nimegami/src/main/AndroidManifest.xml new file mode 100644 index 00000000..7fa00a96 --- /dev/null +++ b/Nimegami/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Nimegami/src/main/kotlin/com/hexated/Extractors.kt b/Nimegami/src/main/kotlin/com/hexated/Extractors.kt new file mode 100644 index 00000000..a7f36f2e --- /dev/null +++ b/Nimegami/src/main/kotlin/com/hexated/Extractors.kt @@ -0,0 +1,55 @@ +package com.hexated + +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.Qualities + +open class Mitedrive : ExtractorApi() { + override val name = "Mitedrive" + override val mainUrl = "https://mitedrive.com" + override val requiresReferer = false + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val id = url.substringAfterLast("/") + val video = app.post( + "$mainUrl/api/generate", + referer = "$mainUrl/", + data = mapOf( + "short_url" to id + ) + ).parsedSafe()?.data?.url + + callback.invoke( + ExtractorLink( + this.name, + this.name, + video ?: return, + "$mainUrl/", + Qualities.Unknown.value, + headers = mapOf( + "Accept" to "video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5", + "Sec-Fetch-Dest" to "video", + "Sec-Fetch-Mode" to "no-cors", + ) + ) + ) + + } + + data class Data( + @JsonProperty("url") val url: String? = null, + ) + + data class Responses( + @JsonProperty("data") val data: Data? = null, + ) + +} \ No newline at end of file diff --git a/Nimegami/src/main/kotlin/com/hexated/Nimegami.kt b/Nimegami/src/main/kotlin/com/hexated/Nimegami.kt new file mode 100644 index 00000000..5fffdd9d --- /dev/null +++ b/Nimegami/src/main/kotlin/com/hexated/Nimegami.kt @@ -0,0 +1,208 @@ +package com.hexated + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import org.jsoup.nodes.Element +import org.jsoup.select.Elements +import java.net.URI + +class Nimegami : MainAPI() { + override var mainUrl = "https://nimegami.id" + override var name = "Nimegami" + override val hasMainPage = true + override var lang = "id" + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + companion object { + fun getType(t: String): TvType { + return when { + t.contains("Tv", true) -> TvType.Anime + t.contains("Movie", true) -> TvType.AnimeMovie + t.contains("OVA", true) || t.contains("Special", true) -> TvType.OVA + else -> TvType.Anime + } + } + + fun getStatus(t: String?): ShowStatus { + return when { + t?.contains("On-Going", true) == true -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + } + + override val mainPage = mainPageOf( + "" to "Updated Anime", + "/type/tv" to "Anime", + "/type/movie" to "Movie", + "/type/ona" to "ONA", + "/type/live-action" to "Live Action", + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val document = app.get("$mainUrl${request.data}/page/$page").document + val home = document.select("div.post-article article, div.archive article").mapNotNull { + it.toSearchResult() + } + return newHomePageResponse( + list = HomePageList( + name = request.name, + list = home, + isHorizontalImages = request.name != "Updated Anime" + ), + hasNext = true + ) + } + + private fun Element.toSearchResult(): AnimeSearchResponse? { + val href = fixUrl(this.selectFirst("a")!!.attr("href")) + val title = this.selectFirst("h2 a")?.text() ?: return null + val posterUrl = (this.selectFirst("noscript img") ?: this.selectFirst("img"))?.attr("src") + val episode = this.selectFirst("ul li:contains(Episode), div.eps-archive")?.ownText() + ?.filter { it.isDigit() }?.toIntOrNull() + + return newAnimeSearchResponse(title, href, TvType.Anime) { + this.posterUrl = posterUrl + addSub(episode) + } + + } + + override suspend fun search(query: String): List { + return app.get("$mainUrl/?s=$query&post_type=post").document.select("div.archive article") + .mapNotNull { + it.toSearchResult() + } + } + + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document + + val table = document.select("div#Info table tbody") + val title = table.getContent("Judul :").text() + val poster = document.selectFirst("div.coverthumbnail img")?.attr("src") + val bgPoster = document.selectFirst("div.thumbnail-a img")?.attr("src") + val tags = table.getContent("Kategori").select("a").map { it.text() } + + val year = table.getContent("Musim / Rilis").text().filter { it.isDigit() }.toIntOrNull() + val status = getStatus(document.selectFirst("h1[itemprop=headline]")?.text()) + val type = getType(table.getContent("Type").text()) + val description = document.select("div#Sinopsis p").text().trim() + val trailer = document.selectFirst("div#Trailer iframe")?.attr("src") + + val episodes = document.select("div.list_eps_stream li") + .mapNotNull { + val episode = Regex("Episode\\s?(\\d+)").find(it.text())?.groupValues?.getOrNull(0)?.toIntOrNull() + val link = it.attr("data") + Episode(link, episode = episode) + } + + val recommendations = document.select("div#randomList > a").mapNotNull { + val epHref = it.attr("href") + val epTitle = it.select("h5.sidebar-title-h5.px-2.py-2").text() + val epPoster = it.select(".product__sidebar__view__item.set-bg").attr("data-setbg") + + newAnimeSearchResponse(epTitle, epHref, TvType.Anime) { + this.posterUrl = epPoster + addDubStatus(dubExist = false, subExist = true) + } + } + + val tracker = APIHolder.getTracker(listOf(title),TrackerType.getTypes(type),year,true) + + return newAnimeLoadResponse(title, url, type) { + engName = title + posterUrl = tracker?.image ?: poster + backgroundPosterUrl = tracker?.cover ?: bgPoster + this.year = year + addEpisodes(DubStatus.Subbed, episodes) + showStatus = status + plot = description + this.tags = tags + this.recommendations = recommendations + addTrailer(trailer) + addMalId(tracker?.malId) + addAniListId(tracker?.aniId?.toIntOrNull()) + } + + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + tryParseJson>(base64Decode(data))?.map { sources -> + sources.url?.apmap { url -> + loadFixedExtractor(url.fixIframe(), sources.format, "$mainUrl/", subtitleCallback, callback) + } + } + + return true + } + + private suspend fun loadFixedExtractor( + url: String, + quality: String?, + referer: String? = null, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + loadExtractor(url, referer, subtitleCallback) { link -> + callback.invoke( + ExtractorLink( + link.name, + link.name, + link.url, + link.referer, + getQualityFromName(quality), + link.type, + link.headers, + link.extractorData + ) + ) + } + } + + private fun getBaseUrl(url: String): String { + return URI(url).let { + "${it.scheme}://${it.host}" + } + } + + private fun Elements.getContent(css: String) : Elements { + return this.select("tr:contains($css) td:last-child") + } + + private fun String.fixIframe() : String { + val url = base64Decode(this.substringAfter("url=").substringAfter("id=")) + val host = getBaseUrl(url) + return when { + url.contains("hxfile") -> { + val id = url.substringAfterLast("/") + "$host/embed-$id.html" + } + else -> fixUrl(url) + } + } + + data class Sources( + @JsonProperty("format") val format: String? = null, + @JsonProperty("url") val url: ArrayList? = arrayListOf(), + ) + +} diff --git a/Nimegami/src/main/kotlin/com/hexated/NimegamiPlugin.kt b/Nimegami/src/main/kotlin/com/hexated/NimegamiPlugin.kt new file mode 100644 index 00000000..8ab01378 --- /dev/null +++ b/Nimegami/src/main/kotlin/com/hexated/NimegamiPlugin.kt @@ -0,0 +1,15 @@ + +package com.hexated + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class NimegamiPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(Nimegami()) + registerExtractorAPI(Mitedrive()) + } +} \ No newline at end of file diff --git a/NontonAnimeIDProvider/build.gradle.kts b/NontonAnimeIDProvider/build.gradle.kts new file mode 100644 index 00000000..14a56948 --- /dev/null +++ b/NontonAnimeIDProvider/build.gradle.kts @@ -0,0 +1,27 @@ +// use an integer for version numbers +version = 17 + + +cloudstream { + language = "id" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + authors = listOf("Hexated") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AnimeMovie", + "Anime", + "OVA", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=nontonanimeid.site&sz=%size%" +} \ No newline at end of file diff --git a/NontonAnimeIDProvider/src/main/kotlin/com/hexated/NontonAnimeIDProvider.kt b/NontonAnimeIDProvider/src/main/kotlin/com/hexated/NontonAnimeIDProvider.kt new file mode 100644 index 00000000..6f068fba --- /dev/null +++ b/NontonAnimeIDProvider/src/main/kotlin/com/hexated/NontonAnimeIDProvider.kt @@ -0,0 +1,257 @@ +package com.hexated + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.extractors.Hxfile +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import java.net.URI + +class NontonAnimeIDProvider : MainAPI() { + override var mainUrl = "https://nontonanimeid.top" + override var name = "NontonAnimeID" + override val hasQuickSearch = false + override val hasMainPage = true + override var lang = "id" + override val hasDownloadSupport = true + + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + companion object { + fun getType(t: String): TvType { + return when { + t.contains("TV",true) -> TvType.Anime + t.contains("Movie",true) -> TvType.AnimeMovie + else -> TvType.OVA + } + } + + fun getStatus(t: String): ShowStatus { + return when (t) { + "Finished Airing" -> ShowStatus.Completed + "Currently Airing" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + } + + override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse { + val document = app.get(mainUrl).document + + val homePageList = ArrayList() + + document.select("section#postbaru").forEach { block -> + val header = block.selectFirst("h2")!!.text().trim() + val animes = block.select("article.animeseries").mapNotNull { + it.toSearchResult() + } + if (animes.isNotEmpty()) homePageList.add(HomePageList(header, animes)) + } + + document.select("aside#sidebar_right > div.side").forEach { block -> + val header = block.selectFirst("h3")!!.ownText().trim() + val animes = block.select("ul li.fullwdth").mapNotNull { + it.toSearchResultPopular() + } + if (animes.isNotEmpty()) homePageList.add(HomePageList(header, animes)) + } + + return HomePageResponse(homePageList) + } + + private fun Element.toSearchResult(): AnimeSearchResponse? { + val href = fixUrl(this.selectFirst("a")!!.attr("href")) + val title = this.selectFirst("h3.title")?.text() ?: return null + val posterUrl = fixUrl(this.select("img").attr("data-src")) + + return newAnimeSearchResponse(title, href, TvType.Anime) { + this.posterUrl = posterUrl + addDubStatus(dubExist = false, subExist = true) + } + + } + + private fun Element.toSearchResultPopular(): AnimeSearchResponse? { + val href = fixUrl(this.selectFirst("a")!!.attr("href")) + val title = this.selectFirst("h4")?.text()?.trim() ?: return null + val posterUrl = fixUrl(this.select("img").attr("data-src")) + + return newAnimeSearchResponse(title, href, TvType.Anime) { + this.posterUrl = posterUrl + addDubStatus(dubExist = false, subExist = true) + } + + } + + override suspend fun search(query: String): List { + val link = "$mainUrl/?s=$query" + val document = app.get(link).document + + return document.select(".result > ul > li").mapNotNull { + val title = it.selectFirst("h2")!!.text().trim() + val poster = it.selectFirst("img")!!.attr("src") + val tvType = getType( + it.selectFirst(".boxinfores > span.typeseries")!!.text().toString() + ) + val href = fixUrl(it.selectFirst("a")!!.attr("href")) + + newAnimeSearchResponse(title, href, tvType) { + this.posterUrl = poster + addDubStatus(dubExist = false, subExist = true) + } + } + } + + override suspend fun load(url: String): LoadResponse? { + val fixUrl = if (url.contains("/anime/")) { + url + } else { + app.get(url).document.selectFirst("div.nvs.nvsc a")?.attr("href") + } + + val req = app.get(fixUrl ?: return null) + mainUrl = getBaseUrl(req.url) + val document = req.document + + val title = document.selectFirst("h1.entry-title.cs")!!.text().removeSurrounding("Nonton Anime", "Sub Indo").trim() + val poster = document.selectFirst(".poster > img")?.attr("data-src") + val tags = document.select(".tagline > a").map { it.text() } + + val year = Regex("\\d, (\\d*)").find( + document.select(".bottomtitle > span:nth-child(5)").text() + )?.groupValues?.get(1)?.toIntOrNull() + val status = getStatus( + document.select("span.statusseries").text().trim() + ) + val type = getType(document.select("span.typeseries").text().trim().lowercase()) + val rating = document.select("span.nilaiseries").text().trim().toIntOrNull() + val description = document.select(".entry-content.seriesdesc > p").text().trim() + val trailer = document.selectFirst("a.trailerbutton")?.attr("href") + + val episodes = if (document.select("button.buttfilter").isNotEmpty()) { + val id = document.select("input[name=series_id]").attr("value") + val numEp = + document.selectFirst(".latestepisode > a")?.text()?.replace(Regex("\\D"), "") + .toString() + Jsoup.parse( + app.post( + url = "$mainUrl/wp-admin/admin-ajax.php", + data = mapOf( + "misha_number_of_results" to numEp, + "misha_order_by" to "date-DESC", + "action" to "mishafilter", + "series_id" to id + ) + ).parsed().content + ).select("li").map { + val episode = Regex("Episode\\s?(\\d+)").find( + it.selectFirst("a")?.text().toString() + )?.groupValues?.getOrNull(0) ?: it.selectFirst("a")?.text() + val link = fixUrl(it.selectFirst("a")!!.attr("href")) + Episode(link, episode = episode?.toIntOrNull()) + }.reversed() + } else { + document.select("ul.misha_posts_wrap2 > li").map { + val episode = Regex("Episode\\s?(\\d+)").find( + it.selectFirst("a")?.text().toString() + )?.groupValues?.getOrNull(0) ?: it.selectFirst("a")?.text() + val link = it.select("a").attr("href") + Episode(link, episode = episode?.toIntOrNull()) + }.reversed() + } + + val recommendations = document.select(".result > li").mapNotNull { + val epHref = it.selectFirst("a")!!.attr("href") + val epTitle = it.selectFirst("h3")!!.text() + val epPoster = it.select(".top > img").attr("data-src") + newAnimeSearchResponse(epTitle, epHref, TvType.Anime) { + this.posterUrl = epPoster + addDubStatus(dubExist = false, subExist = true) + } + } + + val tracker = APIHolder.getTracker(listOf(title),TrackerType.getTypes(type),year,true) + + return newAnimeLoadResponse(title, url, type) { + engName = title + posterUrl = tracker?.image ?: poster + backgroundPosterUrl = tracker?.cover + this.year = year + addEpisodes(DubStatus.Subbed, episodes) + showStatus = status + this.rating = rating + plot = description + addTrailer(trailer) + this.tags = tags + this.recommendations = recommendations + addMalId(tracker?.malId) + addAniListId(tracker?.aniId?.toIntOrNull()) + } + + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val document = app.get(data).document + val sources = ArrayList() + + document.select(".container1 > ul > li:not(.boxtab)").apmap { + val dataPost = it.attr("data-post") + val dataNume = it.attr("data-nume") + val dataType = it.attr("data-type") + + val iframe = app.post( + url = "$mainUrl/wp-admin/admin-ajax.php", + data = mapOf( + "action" to "player_ajax", + "post" to dataPost, + "nume" to dataNume, + "type" to dataType + ), + referer = data, + headers = mapOf("X-Requested-With" to "XMLHttpRequest") + ).document.select("iframe").attr("src") + + sources.add(fixUrl(iframe)) + } + + sources.apmap { + loadExtractor(it, "$mainUrl/", subtitleCallback, callback) + } + + return true + } + + private fun getBaseUrl(url: String): String { + return URI(url).let { + "${it.scheme}://${it.host}" + } + } + private data class EpResponse( + @JsonProperty("posts") val posts: String?, + @JsonProperty("max_page") val max_page: Int?, + @JsonProperty("found_posts") val found_posts: Int?, + @JsonProperty("content") val content: String + ) + +} + +class KotakAnimeid2 : Hxfile() { + override val name = "KotakAnimeid2" + override val mainUrl = "https://embed2.kotakanimeid.com" + override val requiresReferer = true +} diff --git a/OploverzProvider/build.gradle.kts b/OploverzProvider/build.gradle.kts new file mode 100644 index 00000000..e2ebbfc0 --- /dev/null +++ b/OploverzProvider/build.gradle.kts @@ -0,0 +1,27 @@ +// use an integer for version numbers +version = 24 + + +cloudstream { + language = "id" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + authors = listOf("Hexated") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AnimeMovie", + "Anime", + "OVA", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=oploverz.care&sz=%size%" +} diff --git a/OploverzProvider/src/main/kotlin/com/hexated/Extractors.kt b/OploverzProvider/src/main/kotlin/com/hexated/Extractors.kt new file mode 100644 index 00000000..c38c13b8 --- /dev/null +++ b/OploverzProvider/src/main/kotlin/com/hexated/Extractors.kt @@ -0,0 +1,45 @@ +package com.hexated + +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.* + +open class Qiwi : ExtractorApi() { + override val name = "Qiwi" + override val mainUrl = "https://qiwi.gg" + override val requiresReferer = true + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val document = app.get(url, referer = referer).document + val title = document.select("title").text() + val source = document.select("video source").attr("src") + + callback.invoke( + ExtractorLink( + this.name, + this.name, + source, + "$mainUrl/", + getIndexQuality(title), + headers = mapOf( + "Accept" to "video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5", + "Range" to "bytes=0-", + "Sec-Fetch-Dest" to "video", + "Sec-Fetch-Mode" to "no-cors", + ) + ) + ) + + } + + private fun getIndexQuality(str: String): Int { + return Regex("(\\d{3,4})[pP]").find(str)?.groupValues?.getOrNull(1)?.toIntOrNull() + ?: Qualities.Unknown.value + } + +} \ No newline at end of file diff --git a/OploverzProvider/src/main/kotlin/com/hexated/OploverzProvider.kt b/OploverzProvider/src/main/kotlin/com/hexated/OploverzProvider.kt new file mode 100644 index 00000000..8ed03196 --- /dev/null +++ b/OploverzProvider/src/main/kotlin/com/hexated/OploverzProvider.kt @@ -0,0 +1,228 @@ +package com.hexated + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId +import com.lagradost.cloudstream3.utils.* +import org.jsoup.nodes.Element + +class OploverzProvider : MainAPI() { + override var mainUrl = "https://oploverz.red" + override var name = "Oploverz" + override val hasMainPage = true + override var lang = "id" + override val hasDownloadSupport = true + + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + companion object { + const val acefile = "https://acefile.co" + fun getType(t: String): TvType { + return if (t.contains("OVA", true) || t.contains("Special")) TvType.OVA + else if (t.contains("Movie", true)) TvType.AnimeMovie + else TvType.Anime + } + + fun getStatus(t: String?): ShowStatus { + return when (t) { + "Finished Airing" -> ShowStatus.Completed + "Completed" -> ShowStatus.Completed + "Currently Airing" -> ShowStatus.Ongoing + "Ongoing" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + + } + + override val mainPage = mainPageOf( + "update" to "Latest Update", + "latest" to "Latest Added", + "popular" to "Popular Anime", + "rating" to "Top Rated", + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val document = app.get("$mainUrl/anime-list/page/$page/?title&order=${request.data}&status&type").document + val home = document.select("div.relat > article").mapNotNull { + it.toSearchResult() + } + return newHomePageResponse(request.name, home) + } + + private fun getProperAnimeLink(uri: String): String { + return if (uri.contains("/anime/")) { + uri + } else { + var title = uri.substringAfter("$mainUrl/") + title = when { + (title.contains("-episode")) && !(title.contains("-movie")) -> Regex("(.+)-episode").find( + title + )?.groupValues?.get(1).toString() + (title.contains("-movie")) -> Regex("(.+)-movie").find(title)?.groupValues?.get( + 1 + ).toString() + else -> title + } + "$mainUrl/anime/$title" + } + } + + private fun Element.toSearchResult(): AnimeSearchResponse? { + val title = this.selectFirst("div.title")?.text()?.trim() ?: return null + val href = getProperAnimeLink(this.selectFirst("a")!!.attr("href")) + val posterUrl = this.select("img[itemprop=image]").attr("src").toString() + val type = getType(this.select("div.type").text().trim()) + val epNum = + this.selectFirst("span.episode")?.ownText()?.replace(Regex("\\D"), "")?.trim() + ?.toIntOrNull() + return newAnimeSearchResponse(title, href, type) { + this.posterUrl = posterUrl + addSub(epNum) + } + + } + + override suspend fun search(query: String): List { + val anime = mutableListOf() + (1..2).forEach { page -> + val link = "$mainUrl/page/$page/?s=$query" + val document = app.get(link).document + val media = document.select(".site-main.relat > article").mapNotNull { + val title = it.selectFirst("div.title > h2")!!.ownText().trim() + val href = it.selectFirst("a")!!.attr("href") + val posterUrl = it.selectFirst("img")!!.attr("src").toString() + val type = getType(it.select("div.type").text().trim()) + newAnimeSearchResponse(title, href, type) { + this.posterUrl = posterUrl + } + } + if(media.isNotEmpty()) anime.addAll(media) + } + return anime + } + + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document + + val title = document.selectFirst("h1.entry-title")?.text() + ?.replace("Subtitle Indonesia", "")?.trim() ?: "" + val type = getType(document.selectFirst("div.alternati span.type")?.text() ?: "") + val year = document.selectFirst("div.alternati a")?.text()?.filter { it.isDigit() }?.toIntOrNull() + val episodes = document.select("div.lstepsiode.listeps ul li").mapNotNull { + val header = it.selectFirst("a") ?: return@mapNotNull null + val episode = header.text().trim().toIntOrNull() + val link = fixUrl(header.attr("href")) + Episode(link, episode = episode) + }.reversed() + + val tracker = APIHolder.getTracker(listOf(title),TrackerType.getTypes(type),year,true) + + return newAnimeLoadResponse(title, url, type) { + posterUrl = tracker?.image ?: document.selectFirst("div.thumb > img")?.attr("src") + backgroundPosterUrl = tracker?.cover + this.year = year + addEpisodes(DubStatus.Subbed, episodes) + showStatus = + getStatus( + document.selectFirst("div.alternati span:nth-child(2)")?.text()?.trim() + ) + plot = document.selectFirst("div.entry-content > p")?.text()?.trim() + this.tags = + document.select("div.genre-info a").map { it.text() } + addMalId(tracker?.malId) + addAniListId(tracker?.aniId?.toIntOrNull()) + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val document = app.get(data).document + + argamap( + { + document.select("div#server ul li div").apmap { + val dataPost = it.attr("data-post") + val dataNume = it.attr("data-nume") + val dataType = it.attr("data-type") + + val iframe = app.post( + url = "$mainUrl/wp-admin/admin-ajax.php", + data = mapOf( + "action" to "player_ajax", + "post" to dataPost, + "nume" to dataNume, + "type" to dataType + ), + referer = data, + headers = mapOf("X-Requested-With" to "XMLHttpRequest") + ).document.select("iframe").attr("src") + + loadExtractor(fixedIframe(iframe), "$mainUrl/", subtitleCallback, callback) + + } + }, + { + document.select("div#download tr").map { el -> + el.select("a").apmap { + loadFixedExtractor(fixedIframe(it.attr("href")), el.select("strong").text(), "$mainUrl/", subtitleCallback, callback) + } + } + } + ) + + return true + } + + private suspend fun loadFixedExtractor( + url: String, + name: String, + referer: String? = null, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + loadExtractor(url, referer, subtitleCallback) { link -> + callback.invoke( + ExtractorLink( + link.name, + link.name, + link.url, + link.referer, + name.fixQuality(), + link.type, + link.headers, + link.extractorData + ) + ) + } + } + + private fun String.fixQuality() : Int { + return when(this) { + "MP4HD" -> Qualities.P720.value + "FULLHD" -> Qualities.P1080.value + else -> Regex("(\\d{3,4})p").find(this)?.groupValues?.get(1)?.toIntOrNull() ?: Qualities.Unknown.value + } + } + + private fun fixedIframe(url: String): String { + val id = Regex("""(?:/f/|/file/)(\w+)""").find(url)?.groupValues?.getOrNull(1) + return when { + url.startsWith(acefile) -> "${acefile}/player/$id" + else -> fixUrl(url) + } + } + +} diff --git a/OploverzProvider/src/main/kotlin/com/hexated/OploverzProviderPlugin.kt b/OploverzProvider/src/main/kotlin/com/hexated/OploverzProviderPlugin.kt new file mode 100644 index 00000000..ba7c0b85 --- /dev/null +++ b/OploverzProvider/src/main/kotlin/com/hexated/OploverzProviderPlugin.kt @@ -0,0 +1,15 @@ + +package com.hexated + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class OploverzProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(OploverzProvider()) + registerExtractorAPI(Qiwi()) + } +} \ No newline at end of file diff --git a/OtakudesuProvider/build.gradle.kts b/OtakudesuProvider/build.gradle.kts new file mode 100644 index 00000000..fd37451e --- /dev/null +++ b/OtakudesuProvider/build.gradle.kts @@ -0,0 +1,27 @@ +// use an integer for version numbers +version = 15 + + +cloudstream { + language = "id" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + authors = listOf("Hexated") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AnimeMovie", + "Anime", + "OVA", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=otakudesu.watch&sz=%size%" +} \ No newline at end of file diff --git a/OtakudesuProvider/src/main/kotlin/com/hexated/OtakudesuProvider.kt b/OtakudesuProvider/src/main/kotlin/com/hexated/OtakudesuProvider.kt new file mode 100644 index 00000000..9632d72e --- /dev/null +++ b/OtakudesuProvider/src/main/kotlin/com/hexated/OtakudesuProvider.kt @@ -0,0 +1,284 @@ +package com.hexated + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId +import com.lagradost.cloudstream3.extractors.JWPlayer +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.Jsoup +import org.jsoup.nodes.Element + +class OtakudesuProvider : MainAPI() { + override var mainUrl = "https://otakudesu.lol" + override var name = "Otakudesu" + override val hasMainPage = true + override var lang = "id" + override val hasDownloadSupport = true + + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + companion object { + const val acefile = "https://acefile.co" + val mirrorBlackList = arrayOf( + "Mega", + "MegaUp", + "Otakufiles", + ) + fun getType(t: String): TvType { + return if (t.contains("OVA", true) || t.contains("Special")) TvType.OVA + else if (t.contains("Movie", true)) TvType.AnimeMovie + else TvType.Anime + } + + fun getStatus(t: String): ShowStatus { + return when (t) { + "Completed" -> ShowStatus.Completed + "Ongoing" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + } + + override val mainPage = mainPageOf( + "$mainUrl/ongoing-anime/page/" to "Anime Ongoing", + "$mainUrl/complete-anime/page/" to "Anime Completed" + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val document = app.get(request.data + page).document + val home = document.select("div.venz > ul > li").mapNotNull { + it.toSearchResult() + } + return newHomePageResponse(request.name, home) + } + + private fun Element.toSearchResult(): AnimeSearchResponse? { + val title = this.selectFirst("h2.jdlflm")?.text()?.trim() ?: return null + val href = this.selectFirst("a")!!.attr("href") + val posterUrl = this.select("div.thumbz > img").attr("src").toString() + val epNum = this.selectFirst("div.epz")?.ownText()?.replace(Regex("\\D"), "")?.trim() + ?.toIntOrNull() + return newAnimeSearchResponse(title, href, TvType.Anime) { + this.posterUrl = posterUrl + addSub(epNum) + } + + } + + override suspend fun search(query: String): List { + return app.get("$mainUrl/?s=$query&post_type=anime").document.select("ul.chivsrc > li") + .map { + val title = it.selectFirst("h2 > a")!!.ownText().trim() + val href = it.selectFirst("h2 > a")!!.attr("href") + val posterUrl = it.selectFirst("img")!!.attr("src").toString() + newAnimeSearchResponse(title, href, TvType.Anime) { + this.posterUrl = posterUrl + } + } + } + + + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document + + val title = document.selectFirst("div.infozingle > p:nth-child(1) > span")?.ownText() + ?.replace(":", "")?.trim().toString() + val poster = document.selectFirst("div.fotoanime > img")?.attr("src") + val tags = document.select("div.infozingle > p:nth-child(11) > span > a").map { it.text() } + val type = getType(document.selectFirst("div.infozingle > p:nth-child(5) > span")?.ownText() + ?.replace(":", "")?.trim() ?: "tv") + + val year = Regex("\\d, (\\d*)").find( + document.select("div.infozingle > p:nth-child(9) > span").text() + )?.groupValues?.get(1)?.toIntOrNull() + val status = getStatus( + document.selectFirst("div.infozingle > p:nth-child(6) > span")!!.ownText() + .replace(":", "") + .trim() + ) + val description = document.select("div.sinopc > p").text() + + val episodes = document.select("div.episodelist")[1].select("ul > li").mapNotNull { + val name = it.selectFirst("a")?.text() ?: return@mapNotNull null + val episode = Regex("Episode\\s?(\\d+)").find(name)?.groupValues?.getOrNull(0) + ?: it.selectFirst("a")?.text() + val link = fixUrl(it.selectFirst("a")!!.attr("href")) + Episode(link, episode = episode?.toIntOrNull()) + }.reversed() + + val recommendations = + document.select("div.isi-recommend-anime-series > div.isi-konten").map { + val recName = it.selectFirst("span.judul-anime > a")!!.text() + val recHref = it.selectFirst("a")!!.attr("href") + val recPosterUrl = it.selectFirst("a > img")?.attr("src").toString() + newAnimeSearchResponse(recName, recHref, TvType.Anime) { + this.posterUrl = recPosterUrl + } + } + + val tracker = APIHolder.getTracker(listOf(title),TrackerType.getTypes(type),year,true) + + return newAnimeLoadResponse(title, url, type) { + engName = title + posterUrl = tracker?.image ?: poster + backgroundPosterUrl = tracker?.cover + this.year = year + addEpisodes(DubStatus.Subbed, episodes) + showStatus = status + plot = description + this.tags = tags + this.recommendations = recommendations + addMalId(tracker?.malId) + addAniListId(tracker?.aniId?.toIntOrNull()) + } + } + + + data class ResponseSources( + @JsonProperty("id") val id: String, + @JsonProperty("i") val i: String, + @JsonProperty("q") val q: String, + ) + + data class ResponseData( + @JsonProperty("data") val data: String + ) + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val document = app.get(data).document + + argamap( + { + val scriptData = + document.select("script:containsData(action:)").lastOrNull()?.data() + val token = + scriptData?.substringAfter("{action:\"")?.substringBefore("\"}").toString() + + val nonce = + app.post("$mainUrl/wp-admin/admin-ajax.php", data = mapOf("action" to token)) + .parsed().data + val action = + scriptData?.substringAfter(",action:\"")?.substringBefore("\"}").toString() + + val mirrorData = document.select("div.mirrorstream > ul > li").mapNotNull { + base64Decode(it.select("a").attr("data-content")) + }.toString() + + tryParseJson>(mirrorData)?.apmap { res -> + val id = res.id + val i = res.i + val q = res.q + + val sources = Jsoup.parse( + base64Decode( + app.post( + "${mainUrl}/wp-admin/admin-ajax.php", data = mapOf( + "id" to id, + "i" to i, + "q" to q, + "nonce" to nonce, + "action" to action + ) + ).parsed().data + ) + ).select("iframe").attr("src") + + loadCustomExtractor(sources, data, subtitleCallback, callback, getQuality(q)) + + } + }, + { + document.select("div.download li").map { ele -> + val quality = getQuality(ele.select("strong").text()) + ele.select("a").map { + it.attr("href") to it.text() + }.filter { + !inBlacklist(it.first) && quality != Qualities.P360.value + }.apmap { + val link = app.get(it.first, referer = "$mainUrl/").url + loadCustomExtractor( + fixedIframe(link), + data, + subtitleCallback, + callback, + quality + ) + } + } + } + ) + + return true + } + + private suspend fun loadCustomExtractor( + url: String, + referer: String? = null, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit, + quality: Int = Qualities.Unknown.value, + ) { + loadExtractor(url, referer, subtitleCallback) { link -> + callback.invoke( + ExtractorLink( + link.name, + link.name, + link.url, + link.referer, + quality, + link.type, + link.headers, + link.extractorData + ) + ) + } + } + + private fun fixedIframe(url: String): String { + return when { + url.startsWith(acefile) -> { + val id = Regex("""(?:/f/|/file/)(\w+)""").find(url)?.groupValues?.getOrNull(1) + "${acefile}/player/$id" + } + + else -> fixUrl(url) + } + } + + private fun inBlacklist(host: String?): Boolean { + return mirrorBlackList.any { it.equals(host, true) } + } + + private fun getQuality(str: String?): Int { + return Regex("(\\d{3,4})[pP]").find(str ?: "")?.groupValues?.getOrNull(1)?.toIntOrNull() + ?: Qualities.Unknown.value + } + +} + +class Moedesu : JWPlayer() { + override val name = "Moedesu" + override val mainUrl = "https://desustream.me/moedesu/" +} + +class DesuBeta : JWPlayer() { + override val name = "DesuBeta" + override val mainUrl = "https://desustream.me/beta/" +} \ No newline at end of file diff --git a/OtakudesuProvider/src/main/kotlin/com/hexated/OtakudesuProviderPlugin.kt b/OtakudesuProvider/src/main/kotlin/com/hexated/OtakudesuProviderPlugin.kt new file mode 100644 index 00000000..462f0bee --- /dev/null +++ b/OtakudesuProvider/src/main/kotlin/com/hexated/OtakudesuProviderPlugin.kt @@ -0,0 +1,16 @@ + +package com.hexated + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class OtakudesuProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(OtakudesuProvider()) + registerExtractorAPI(Moedesu()) + registerExtractorAPI(DesuBeta()) + } +} \ No newline at end of file diff --git a/Phim1080/build.gradle.kts b/Phim1080/build.gradle.kts new file mode 100644 index 00000000..c0fd5859 --- /dev/null +++ b/Phim1080/build.gradle.kts @@ -0,0 +1,28 @@ +// use an integer for version numbers +version = 2 + + +cloudstream { + language = "vi" + // All of these properties are optional, you can safely remove them + + description = "Xem Phim Online Chất Lượng Cao" + authors = listOf("TuaSan") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AsianDrama", + "Anime", + "TvSeries", + "Movie", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=xem1080.com" +} diff --git a/Phim1080/src/main/AndroidManifest.xml b/Phim1080/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a3f95b4f --- /dev/null +++ b/Phim1080/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/Phim1080/src/main/kotlin/com/hexated/Phim1080Provider.kt b/Phim1080/src/main/kotlin/com/hexated/Phim1080Provider.kt new file mode 100644 index 00000000..4f25ba99 --- /dev/null +++ b/Phim1080/src/main/kotlin/com/hexated/Phim1080Provider.kt @@ -0,0 +1,267 @@ +package com.hexated + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.mvvm.safeApiCall +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.ExtractorLink +import org.jsoup.nodes.Element + +class Phim1080Provider : MainAPI() { + override var mainUrl = "https://phimnhanh2.com" + override var name = "Phim1080" + override val hasMainPage = true + override var lang = "vi" + override val hasDownloadSupport = true + override val supportedTypes = setOf( + TvType.Movie, + TvType.TvSeries, + TvType.Anime, + TvType.AsianDrama + ) + + private fun decodeString(e: String, t: Int): String { + var a = "" + for (i in 0 until e.length) { + val r = e[i].code + val o = r xor t + a += o.toChar() + } + return a + } + + override val mainPage = mainPageOf( + "$mainUrl/phim-de-cu?page=" to "Phim Đề Cử", + "$mainUrl/the-loai/hoat-hinh?page=" to "Phim Hoạt Hình", + "$mainUrl/phim-chieu-rap?page=" to "Phim Chiếu Rạp", + "$mainUrl/phim-bo?page=" to "Phim Bộ", + "$mainUrl/phim-le?page=" to "Phim Lẻ", + "$mainUrl/bang-xep-hang?page=" to "Bảng Xếp Hạng", + "$mainUrl/bo-suu-tap/disney-plus?page=" to "Disney+", + "$mainUrl/bo-suu-tap/netflix-original?page=" to "Netflix", + "$mainUrl/hom-nay-xem-gi?page=" to "Hôm Nay Xem Gì", + "$mainUrl/phim-sap-chieu?page=" to "Phim Sắp Chiếu", + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val document = app.get(request.data + page).document + val home = document.select("div.tray-item").mapNotNull { + it.toSearchResult() + } + return newHomePageResponse( + list = HomePageList( + name = request.name, + list = home, + ), + hasNext = true + ) + } + + private fun Element.toSearchResult(): SearchResponse { + val title = this.selectFirst("div.tray-item-title")?.text()?.trim().toString() + val href = fixUrl(this.selectFirst("a")!!.attr("href")) + val posterUrl = this.selectFirst("img")!!.attr("data-src") + val temp = this.select("div.tray-film-likes").text() + return if (temp.contains("/")) { + val episode = Regex("((\\d+)\\s)").find(temp)?.groupValues?.map { num -> + num.replace(Regex("\\s"), "") + }?.distinct()?.firstOrNull()?.toIntOrNull() + newAnimeSearchResponse(title, href, TvType.TvSeries) { + this.posterUrl = posterUrl + addSub(episode) + } + } else { + val quality = this.select("span.tray-item-quality").text().replace("FHD", "HD").trim() + newMovieSearchResponse(title, href, TvType.Movie) { + this.posterUrl = posterUrl + addQuality(quality) + } + } + } + + override suspend fun search(query: String): List { + val link = "$mainUrl/tim-kiem/$query" + val document = app.get(link).document + + return document.select("div.tray-item").map { + it.toSearchResult() + } + } + + override suspend fun load( url: String ): LoadResponse { + val document = app.get( + url = url, + referer = "$mainUrl/", + headers = mapOf( + "Sec-Ch-Ua-Mobile" to "?1", + "Sec-Ch-Ua-Platform" to "\"Android\"", + "User-Agent" to "Mozilla/5.0 (Linux; Android 10; SM-G981B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.162 Mobile Safari/537.36 Edg/114.0.0.0", + ) + ).document + val fId = document.select("div.container").attr("data-id") + val filmInfo = app.get( + "$mainUrl/api/v2/films/$fId", + referer = url, + headers = mapOf( + "Content-Type" to "application/json", + "X-Requested-With" to "XMLHttpRequest" + ) + ).parsedSafe() + val title = filmInfo?.name?.trim().toString() + val poster = filmInfo?.thumbnail + val background = filmInfo?.poster + val slug = filmInfo?.slug + val link = "$mainUrl/$slug" + val tags = document.select("div.film-content div.film-info-genre:nth-child(7) a").map { it.text() } + val year = filmInfo?.year + val tvType = if (document.select("div.episode-group-tab").isNotEmpty()) TvType.TvSeries else TvType.Movie + val description = document.select("div.film-info-description").text().trim() + val comingSoon = document.select("button.direction-trailer").isNotEmpty() + val trailerCode = filmInfo?.trailer?.original?.id + val trailer = "https://www.youtube.com/embed/$trailerCode" + val recommendations = document.select("section.tray.index.related div.tray-content.carousel div.tray-item").map { + it.toSearchResult() + } + + return if (tvType == TvType.TvSeries) { + val epsInfo = app.get( + "$mainUrl/api/v2/films/$fId/episodes?sort=name", + referer = link, + headers = mapOf( + "Content-Type" to "application/json", + "X-Requested-With" to "XMLHttpRequest", + ) + ).parsedSafe()?.eps?.map { ep -> + Episode( + data = fixUrl(ep.link.toString()), + name = ep.detailname, + episode = ep.episodeNumber, + ) + } ?: listOf() + newTvSeriesLoadResponse(title, url, TvType.TvSeries, epsInfo) { + this.posterUrl = poster + this.backgroundPosterUrl = background + this.year = year + this.plot = description + this.tags = tags + this.comingSoon = comingSoon + addTrailer(trailer) + this.recommendations = recommendations + } + } else { + newMovieLoadResponse(title, url, TvType.Movie, link) { + this.posterUrl = poster + this.backgroundPosterUrl = background + this.year = year + this.plot = description + this.tags = tags + this.comingSoon = comingSoon + addTrailer(trailer) + this.recommendations = recommendations + } + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val document = app.get(data).document + val fId = document.select("div.container").attr("data-id") + val epId = document.select("div.container").attr("data-episode-id") + val doc = app.get( + "$mainUrl/api/v2/films/$fId/episodes/$epId", + referer = data, + headers = mapOf( + "Content-Type" to "application/json", + "cookie" to "phimnhanh=%3D", + "X-Requested-With" to "XMLHttpRequest" + ) + ) + + val optEncode = if (doc.text.indexOf("\"opt\":\"") != -1) { + doc.text.substringAfter("\"opt\":\"").substringBefore("\"},") + } else { "" } + val opt = decodeString(optEncode as String, 69).replace("0uut$", "_").replace("index.m3u8", "3000k/hls/mixed.m3u8") + val hlsEncode = if (doc.text.indexOf(":{\"hls\":\"") != -1) { + doc.text.substringAfter(":{\"hls\":\"").substringBefore("\"},") + } else { "" } + val hls = decodeString(hlsEncode as String, 69) + val fb = if (doc.text.indexOf("\"fb\":[{\"src\":\"") != -1) { + doc.text.substringAfter("\"fb\":[{\"src\":\"").substringBefore("\",").replace("\\", "") + } else { "" } + + listOfNotNull( + if (hls.contains(".m3u8")) {Triple("$hls", "HS", true)} else null, + if (fb.contains(".mp4")) {Triple("$fb", "FB", false)} else null, + if (opt.contains(".m3u8")) {Triple("$opt", "OP", true)} else null, + ).apmap { (link, source, isM3u8) -> + safeApiCall { + callback.invoke( + ExtractorLink( + source, + source, + link, + referer = data, + quality = Qualities.Unknown.value, + isM3u8, + ) + ) + } + } + val subId = doc.parsedSafe()?.subtitle?.vi + val isSubIdEmpty = subId.isNullOrBlank() + if (!isSubIdEmpty) { + subtitleCallback.invoke( + SubtitleFile( + "Vietnamese", + "$mainUrl/subtitle/$subId.vtt" + ) + ) + } + return true + } + + data class filmInfo( + @JsonProperty("name") val name: String? = null, + @JsonProperty("poster") val poster: String? = null, + @JsonProperty("thumbnail") val thumbnail: String? = null, + @JsonProperty("slug") val slug: String? = null, + @JsonProperty("year") val year: Int? = null, + @JsonProperty("trailer") val trailer: TrailerInfo? = null, + ) + + data class TrailerInfo( + @JsonProperty("original") val original: TrailerKey? = null, + ) + + data class TrailerKey( + @JsonProperty("id") val id: String? = null, + ) + + data class MediaDetailEpisodes( + @JsonProperty("data") val eps: ArrayList? = arrayListOf(), + ) + + data class Episodes( + @JsonProperty("link") val link: String? = null, + @JsonProperty("detail_name") val detailname: String? = null, + @JsonProperty("name") val episodeNumber: Int? = null, + ) + + data class Media( + @JsonProperty("subtitle") val subtitle: SubInfo? = null, + ) + + data class SubInfo( + @JsonProperty("vi") val vi: String? = null, + ) + +} diff --git a/Phim1080/src/main/kotlin/com/hexated/Phim1080ProviderPlugin.kt b/Phim1080/src/main/kotlin/com/hexated/Phim1080ProviderPlugin.kt new file mode 100644 index 00000000..a521b6ac --- /dev/null +++ b/Phim1080/src/main/kotlin/com/hexated/Phim1080ProviderPlugin.kt @@ -0,0 +1,14 @@ + +package com.hexated + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class Phim1080ProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(Phim1080Provider()) + } +} diff --git a/PhimmoichillProvider/src/main/kotlin/com/hexated/PhimmoichillProvider.kt b/PhimmoichillProvider/src/main/kotlin/com/hexated/PhimmoichillProvider.kt new file mode 100644 index 00000000..aa7c4f5d --- /dev/null +++ b/PhimmoichillProvider/src/main/kotlin/com/hexated/PhimmoichillProvider.kt @@ -0,0 +1,197 @@ +package com.hexated + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addActors +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.mvvm.safeApiCall +import com.lagradost.cloudstream3.utils.* +import org.jsoup.nodes.Element +import java.net.URLDecoder + +class PhimmoichillProvider : MainAPI() { + override var mainUrl = "https://phimmoichillg.net" + override var name = "Phimmoichill" + override val hasMainPage = true + override var lang = "vi" + override val hasDownloadSupport = true + override val supportedTypes = setOf( + TvType.Movie, + TvType.TvSeries, + TvType.Anime, + TvType.AsianDrama + ) + + override val mainPage = mainPageOf( + "$mainUrl/genre/phim-chieu-rap/page-" to "Phim Chiếu Rạp", + "$mainUrl/list/phim-le/page-" to "Phim Lẻ", + "$mainUrl/list/phim-bo/page-" to "Phim Bộ", + "$mainUrl/genre/phim-hoat-hinh/page-" to "Phim Hoạt Hình", + "$mainUrl/genre/phim-anime/page-" to "Phim Anime", + "$mainUrl/country/phim-han-quoc/page-" to "Phim Hàn Quốc", + "$mainUrl/country/phim-trung-quoc/page-" to "Phim Trung Quốc", + "$mainUrl/country/phim-thai-lan/page-" to "Phim Thái Lan", + "$mainUrl/genre/phim-sap-chieu/page-" to "Phim Sắp Chiếu", + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val document = app.get(request.data + page).document + val home = document.select("li.item").mapNotNull { + it.toSearchResult() + } + return newHomePageResponse( + list = HomePageList( + name = request.name, + list = home, + isHorizontalImages = true + ), + hasNext = true + ) + } + + private fun decode(input: String): String? = URLDecoder.decode(input, "utf-8") + + private fun Element.toSearchResult(): SearchResponse { + val title = this.selectFirst("p,h3")?.text()?.trim().toString() + val href = fixUrl(this.selectFirst("a")!!.attr("href")) + val posterUrl = decode(this.selectFirst("img")!!.attr("src").substringAfter("url=")) + val temp = this.select("span.label").text() + return if (temp.contains(Regex("\\d"))) { + val episode = Regex("(\\((\\d+))|(\\s(\\d+))").find(temp)?.groupValues?.map { num -> + num.replace(Regex("\\(|\\s"), "") + }?.distinct()?.firstOrNull()?.toIntOrNull() + newAnimeSearchResponse(title, href, TvType.TvSeries) { + this.posterUrl = posterUrl + addSub(episode) + } + } else if (temp.contains(Regex("Trailer"))) { + newMovieSearchResponse(title, href, TvType.Movie) { + this.posterUrl = posterUrl + } + } else { + val quality = + temp.replace(Regex("(-.*)|(\\|.*)|(?i)(VietSub.*)|(?i)(Thuyết.*)"), "").trim() + newMovieSearchResponse(title, href, TvType.Movie) { + this.posterUrl = posterUrl + addQuality(quality) + } + } + } + + override suspend fun search(query: String): List { + val link = "$mainUrl/tim-kiem/$query" + val document = app.get(link).document + + return document.select("ul.list-film li").map { + it.toSearchResult() + } + } + + override suspend fun load( url: String ): LoadResponse { + val document = app.get(url).document + + val title = document.selectFirst("h1[itemprop=name]")?.text()?.trim().toString() + val link = document.select("ul.list-button li:last-child a").attr("href") + val poster = document.selectFirst("div.image img[itemprop=image]")?.attr("src") + val tags = document.select("ul.entry-meta.block-film li:nth-child(4) a").map { it.text()!!.substringAfter("Phim") } + val year = document.select("ul.entry-meta.block-film li:nth-child(2) a").text().trim() + .toIntOrNull() + val tvType = if (document.select("div.latest-episode").isNotEmpty() + ) TvType.TvSeries else TvType.Movie + val description = document.select("div#film-content").text().substringAfter("Full HD Vietsub Thuyết Minh").substringBefore("@phimmoi").trim() + val trailer = document.select("body script") + .find { it.data().contains("youtube.com") }?.data()?.substringAfterLast("file: \"")?.substringBefore("\",") + val rating = + document.select("ul.entry-meta.block-film li:nth-child(7) span").text().toRatingInt() + val actors = document.select("ul.entry-meta.block-film li:last-child a").map { it.text() } + val recommendations = document.select("ul#list-film-realted li.item").map { + it.toSearchResult().apply { + this.posterUrl = decode(it.selectFirst("img")!!.attr("data-src").substringAfter("url=")) + } + } + + return if (tvType == TvType.TvSeries) { + val docEpisodes = app.get(link).document + val episodes = docEpisodes.select("ul#list_episodes > li").map { + val href = it.select("a").attr("href") + val episode = + it.select("a").text().replace(Regex("[^0-9]"), "").trim().toIntOrNull() + val name = "Episode $episode" + Episode( + data = href, + name = name, + episode = episode, + ) + } + newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodes) { + this.posterUrl = poster + this.year = year + this.plot = description + this.tags = tags + this.rating = rating + addActors(actors) + this.recommendations = recommendations + addTrailer(trailer) + } + } else { + newMovieLoadResponse(title, url, TvType.Movie, link) { + this.posterUrl = poster + this.year = year + this.plot = description + this.tags = tags + this.rating = rating + addActors(actors) + this.recommendations = recommendations + addTrailer(trailer) + } + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val document = app.get(data).document + + val key = document.select("div#content script") + .find { it.data().contains("filmInfo.episodeID =") }?.data()?.let { script -> + val id = script.substringAfter("filmInfo.episodeID = parseInt('") + app.post( + url = "${this.mainUrl}/chillsplayer.php", + data = mapOf("qcao" to id, "sv" to "0"), + referer = data, + headers = mapOf( + "X-Requested-With" to "XMLHttpRequest", + "Content-Type" to "application/x-www-form-urlencoded; charset=UTF-8" + ) + ).text.substringAfterLast("iniPlayers(\"") + .substringBefore("\",") + } + + listOf( + Pair("https://so-trym.topphimmoi.org/raw/$key/index.m3u8", "PMFAST"), + Pair("https://dash.megacdn.xyz/raw/$key/index.m3u8", "PMHLS"), + Pair("https://so-trym.phimchill.net/dash/$key/index.m3u8", "PMPRO"), + Pair("https://dash.megacdn.xyz/dast/$key/index.m3u8", "PMBK") + ).apmap { (link, source) -> + safeApiCall { + callback.invoke( + ExtractorLink( + source, + source, + link, + referer = "$mainUrl/", + quality = Qualities.P1080.value, + isM3u8 = true, + ) + ) + } + } + return true + } + +} diff --git a/RebahinProvider/build.gradle.kts b/RebahinProvider/build.gradle.kts new file mode 100644 index 00000000..a1d65c12 --- /dev/null +++ b/RebahinProvider/build.gradle.kts @@ -0,0 +1,29 @@ +// use an integer for version numbers +version = 7 + + +cloudstream { + language = "id" + // All of these properties are optional, you can safely remove them + + description = "Includes: Cgvindo, Kitanonton" + authors = listOf("Hexated") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AsianDrama", + "Anime", + "TvSeries", + "Movie", + ) + + + iconUrl = "https://www.google.com/s2/favicons?domain=104.237.198.194&sz=%size%" +} \ No newline at end of file diff --git a/RebahinProvider/src/main/kotlin/com/hexated/Cgvindo.kt b/RebahinProvider/src/main/kotlin/com/hexated/Cgvindo.kt new file mode 100644 index 00000000..328f0a06 --- /dev/null +++ b/RebahinProvider/src/main/kotlin/com/hexated/Cgvindo.kt @@ -0,0 +1,9 @@ +package com.hexated + +import com.lagradost.cloudstream3.TvType + +class Cgvindo : RebahinProvider() { + override var mainUrl = "http://cgvindo.click" + override var name = "Cgvindo" + +} \ No newline at end of file diff --git a/RebahinProvider/src/main/kotlin/com/hexated/RebahinProvider.kt b/RebahinProvider/src/main/kotlin/com/hexated/RebahinProvider.kt new file mode 100644 index 00000000..fcb63f6e --- /dev/null +++ b/RebahinProvider/src/main/kotlin/com/hexated/RebahinProvider.kt @@ -0,0 +1,314 @@ +package com.hexated + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addActors +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.mvvm.safeApiCall +import com.lagradost.cloudstream3.network.WebViewResolver +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import org.jsoup.nodes.Element +import java.net.URI + +open class RebahinProvider : MainAPI() { + override var mainUrl = "http://179.43.163.50" + override var name = "Rebahin" + override val hasMainPage = true + override var lang = "id" + open var mainServer = "http://172.96.161.72" + override val supportedTypes = setOf( + TvType.Movie, + TvType.TvSeries, + TvType.Anime, + TvType.AsianDrama + ) + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + val urls = listOf( + Pair("Featured", "xtab1"), + Pair("Film Terbaru", "xtab2"), + Pair("Romance", "xtab3"), + Pair("Drama", "xtab4"), + Pair("Action", "xtab5"), + Pair("Scifi", "xtab6"), + Pair("Tv Series Terbaru", "stab1"), + Pair("Anime Series", "stab2"), + Pair("Drakor Series", "stab3"), + Pair("West Series", "stab4"), + Pair("China Series", "stab5"), + Pair("Japan Series", "stab6"), + ) + + val items = ArrayList() + + for ((header, tab) in urls) { + try { + val home = + app.get("$mainUrl/wp-content/themes/indoxxi/ajax-top-$tab.php").document.select( + "div.ml-item" + ).mapNotNull { + it.toSearchResult() + } + items.add(HomePageList(header, home)) + } catch (e: Exception) { + logError(e) + } + } + + if (items.size <= 0) throw ErrorLoadingException() + return HomePageResponse(items) + } + + fun Element.toSearchResult(): SearchResponse? { + val title = this.selectFirst("span.mli-info > h2")?.text() ?: return null + val href = this.selectFirst("a")!!.attr("href") + val type = + if (this.select("span.mli-quality").isNotEmpty()) TvType.Movie else TvType.TvSeries + return if (type == TvType.Movie) { + val posterUrl = fixUrlNull(this.select("img").attr("src")) + val quality = getQualityFromString(this.select("span.mli-quality").text().trim()) + newMovieSearchResponse(title, href, TvType.Movie) { + this.posterUrl = posterUrl + this.quality = quality + } + } else { + val posterUrl = + fixUrlNull( + this.select("img").attr("src") + .ifEmpty { this.select("img").attr("data-original") }) + val episode = + this.select("div.mli-eps > span").text().replace(Regex("[^0-9]"), "").toIntOrNull() + newAnimeSearchResponse(title, href, TvType.TvSeries) { + this.posterUrl = posterUrl + addSub(episode) + } + } + } + + override suspend fun search(query: String): List { + val link = "$mainUrl/?s=$query" + val document = app.get(link).document + + return document.select("div.ml-item").mapNotNull { + it.toSearchResult() + } + } + + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document + + val title = document.selectFirst("h3[itemprop=name]")!!.ownText().trim() + val poster = document.select(".mvic-desc > div.thumb.mvic-thumb").attr("style") + .substringAfter("url(").substringBeforeLast(")") + val tags = document.select("span[itemprop=genre]").map { it.text() } + + val year = Regex("([0-9]{4}?)-").find( + document.selectFirst(".mvici-right > p:nth-child(3)")!!.ownText().trim() + )?.groupValues?.get(1).toString().toIntOrNull() + val tvType = if (url.contains("/series/")) TvType.TvSeries else TvType.Movie + val description = document.select("span[itemprop=reviewBody] > p").text().trim() + val trailer = fixUrlNull(document.selectFirst("div.modal-body-trailer iframe")?.attr("src")) + val rating = document.selectFirst("span[itemprop=ratingValue]")?.text()?.toRatingInt() + val duration = document.selectFirst(".mvici-right > p:nth-child(1)")!! + .ownText().replace(Regex("[^0-9]"), "").toIntOrNull() + val actors = document.select("span[itemprop=actor] > a").map { it.select("span").text() } + + val baseLink = fixUrl(document.select("div#mv-info > a").attr("href").toString()) + + return if (tvType == TvType.TvSeries) { + val episodes = app.get(baseLink).document.select("div#list-eps > a").map { + Pair(it.text(), it.attr("data-iframe")) + }.groupBy { it.first }.map { eps -> + Episode( + data = eps.value.map { fixUrl(base64Decode(it.second)) }.toString(), + name = eps.key, + episode = eps.key.filter { it.isDigit() }.toIntOrNull() + ) + + } + newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodes) { + this.posterUrl = poster + this.year = year + this.plot = description + this.tags = tags + this.rating = rating + this.duration = duration + addActors(actors) + addTrailer(trailer) + } + } else { + val links = + app.get(baseLink).document.select("div#server-list div.server-wrapper div[id*=episode]") + .map { + fixUrl(base64Decode(it.attr("data-iframe"))) + }.toString() + newMovieLoadResponse(title, url, TvType.Movie, links) { + this.posterUrl = poster + this.year = year + this.plot = description + this.tags = tags + this.rating = rating + this.duration = duration + addActors(actors) + addTrailer(trailer) + } + } + } + + private fun getLanguage(str: String): String { + return when { + str.contains("indonesia", true) || str.contains("bahasa", true) -> "Indonesian" + else -> str + } + } + + private suspend fun invokeLokalSource( + url: String, + subCallback: (SubtitleFile) -> Unit, + sourceCallback: (ExtractorLink) -> Unit + ) { + val document = app.get( + url, + allowRedirects = false, + referer = mainUrl, + headers = mapOf("Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8") + ).document + + document.select("script").find { it.data().contains("config =") }?.data()?.let { script -> + Regex("\"file\":\\s?\"(.+.m3u8)\"").find(script)?.groupValues?.getOrNull(1) + ?.let { link -> + M3u8Helper.generateM3u8( + name, + link, + referer = "$mainServer/", + headers = mapOf("Accept" to "*/*", "Origin" to mainServer) + ).forEach(sourceCallback) + } + + val subData = + Regex("\"?tracks\"?:\\s\\n?\\[(.*)],").find(script)?.groupValues?.getOrNull(1) + ?: Regex("\"?tracks\"?:\\s\\n?\\[\\s*(?s:(.+)],\\n\\s*\"sources)").find(script)?.groupValues?.getOrNull( + 1 + ) + tryParseJson>("[$subData]")?.map { + subCallback.invoke( + SubtitleFile( + getLanguage(it.label ?: return@map null), + if (it.file?.contains(".srt") == true) it.file else return@map null + ) + ) + + } + } + } + + private suspend fun invokeKotakAjairSource( + url: String, + subCallback: (SubtitleFile) -> Unit, + sourceCallback: (ExtractorLink) -> Unit + ) { + val domainUrl = "https://kotakajair.xyz" + val id = url.trimEnd('/').split("/").last() + val sources = app.post( + url = "$domainUrl/api/source/$id", + data = mapOf("r" to mainUrl, "d" to URI(url).host) + ).parsed() + + sources.data?.map { + sourceCallback.invoke( + ExtractorLink( + name, + "KotakAjair", + fixUrl(it.file), + referer = url, + quality = getQualityFromName(it.label) + ) + ) + } + val userData = sources.player.poster_file.split("/")[2] + sources.captions?.map { + subCallback.invoke( + SubtitleFile( + getLanguage(it.language), + "$domainUrl/asset/userdata/$userData/caption/${it.hash}/${it.id}.srt" + ) + ) + } + + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + data.removeSurrounding("[", "]").split(",").map { it.trim() }.apmap { link -> + safeApiCall { + when { + link.startsWith(mainServer) -> invokeLokalSource( + link, + subtitleCallback, + callback + ) + link.startsWith("https://kotakajair.xyz") -> invokeKotakAjairSource( + link, + subtitleCallback, + callback + ) + else -> { + loadExtractor(link, "$mainUrl/", subtitleCallback, callback) + if (link.startsWith("https://sbfull.com")) { + val response = app.get( + link, interceptor = WebViewResolver( + Regex("""\.srt""") + ) + ) + subtitleCallback.invoke( + SubtitleFile( + "Indonesian", + response.url + ) + ) + } + } + } + } + } + + return true + } + + private data class Tracks( + @JsonProperty("file") val file: String? = null, + @JsonProperty("label") val label: String? = null, + @JsonProperty("kind") val kind: String? = null + ) + + private data class Captions( + @JsonProperty("id") val id: String, + @JsonProperty("hash") val hash: String, + @JsonProperty("language") val language: String, + ) + + private data class Data( + @JsonProperty("file") val file: String, + @JsonProperty("label") val label: String, + ) + + private data class Player( + @JsonProperty("poster_file") val poster_file: String, + ) + + private data class ResponseKotakAjair( + @JsonProperty("success") val success: Boolean, + @JsonProperty("player") val player: Player, + @JsonProperty("data") val data: List?, + @JsonProperty("captions") val captions: List? + ) + +} + diff --git a/RebahinProvider/src/main/kotlin/com/hexated/RebahinProviderPlugin.kt b/RebahinProvider/src/main/kotlin/com/hexated/RebahinProviderPlugin.kt new file mode 100644 index 00000000..e051cc41 --- /dev/null +++ b/RebahinProvider/src/main/kotlin/com/hexated/RebahinProviderPlugin.kt @@ -0,0 +1,15 @@ +package com.hexated + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class RebahinProviderPlugin : Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(RebahinProvider()) + registerMainAPI(Kitanonton()) + registerMainAPI(Cgvindo()) + } +} \ No newline at end of file diff --git a/Samehadaku/build.gradle.kts b/Samehadaku/build.gradle.kts new file mode 100644 index 00000000..f3101866 --- /dev/null +++ b/Samehadaku/build.gradle.kts @@ -0,0 +1,27 @@ +// use an integer for version numbers +version = 15 + + +cloudstream { + language = "id" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + authors = listOf("Hexated") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AnimeMovie", + "OVA", + "Anime", + ) + + iconUrl = "https://www.google.com/s2/favicons?domain=https://samehadaku.world&sz=%size%" +} \ No newline at end of file diff --git a/Samehadaku/src/main/kotlin/com/hexated/Samehadaku.kt b/Samehadaku/src/main/kotlin/com/hexated/Samehadaku.kt new file mode 100644 index 00000000..d97e5426 --- /dev/null +++ b/Samehadaku/src/main/kotlin/com/hexated/Samehadaku.kt @@ -0,0 +1,241 @@ +package com.hexated + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.nodes.Element + +class Samehadaku : MainAPI() { + override var mainUrl = "https://samehadaku.bond" + override var name = "Samehadaku" + override val hasMainPage = true + override var lang = "id" + override val hasDownloadSupport = true + + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + companion object { + const val acefile = "https://acefile.co" + + fun getType(t: String): TvType { + return if (t.contains("OVA", true) || t.contains("Special", true)) TvType.OVA + else if (t.contains("Movie", true)) TvType.AnimeMovie + else TvType.Anime + } + + fun getStatus(t: String): ShowStatus { + return when (t) { + "Completed" -> ShowStatus.Completed + "Ongoing" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + } + + override val mainPage = mainPageOf( + "$mainUrl/page/" to "Episode Terbaru", + "$mainUrl/" to "HomePage", + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val items = mutableListOf() + + if (request.name != "Episode Terbaru" && page <= 1) { + val doc = app.get(request.data).document + doc.select("div.widget_senction:not(:contains(Baca Komik))").forEach { block -> + val header = block.selectFirst("div.widget-title h3")?.ownText() ?: return@forEach + val home = block.select("div.animepost").mapNotNull { + it.toSearchResult() + } + if (home.isNotEmpty()) items.add(HomePageList(header, home)) + } + } + + if (request.name == "Episode Terbaru") { + val home = + app.get(request.data + page).document.selectFirst("div.post-show")?.select("ul li") + ?.mapNotNull { + it.toSearchResult() + } ?: throw ErrorLoadingException("No Media Found") + items.add(HomePageList(request.name, home, true)) + } + + return newHomePageResponse(items) + + } + + private fun Element.toSearchResult(): AnimeSearchResponse? { + val title = this.selectFirst("div.title, h2.entry-title a, div.lftinfo h2")?.text()?.trim() + ?: return null + val href = fixUrlNull(this.selectFirst("a")?.attr("href") ?: return null) + val posterUrl = fixUrlNull(this.select("img").attr("src")) + val epNum = this.selectFirst("div.dtla author")?.text()?.toIntOrNull() + return newAnimeSearchResponse(title, href ?: return null, TvType.Anime) { + this.posterUrl = posterUrl + addSub(epNum) + } + + } + + override suspend fun search(query: String): List { + val document = app.get("$mainUrl/?s=$query").document + return document.select("main#main div.animepost").mapNotNull { + it.toSearchResult() + } + } + + override suspend fun load(url: String): LoadResponse? { + val fixUrl = if (url.contains("/anime/")) { + url + } else { + app.get(url).document.selectFirst("div.nvs.nvsc a")?.attr("href") + } + + val document = app.get(fixUrl ?: return null).document + val title = document.selectFirst("h1.entry-title")?.text()?.removeBloat() ?: return null + val poster = document.selectFirst("div.thumb > img")?.attr("src") + val tags = document.select("div.genre-info > a").map { it.text() } + val year = document.selectFirst("div.spe > span:contains(Rilis)")?.ownText()?.let { + Regex("\\d,\\s(\\d*)").find(it)?.groupValues?.getOrNull(1)?.toIntOrNull() + } + val status = getStatus( + document.selectFirst("div.spe > span:contains(Status)")?.ownText() ?: return null + ) + val type = + getType(document.selectFirst("div.spe > span:contains(Type)")?.ownText()?.trim()?.lowercase() + ?: "tv") + val rating = document.selectFirst("span.ratingValue")?.text()?.trim()?.toRatingInt() + val description = document.select("div.desc p").text().trim() + val trailer = document.selectFirst("div.trailer-anime iframe")?.attr("src") + + val episodes = document.select("div.lstepsiode.listeps ul li").mapNotNull { + val header = it.selectFirst("span.lchx > a") ?: return@mapNotNull null + val episode = Regex("Episode\\s?(\\d+)").find(header.text())?.groupValues?.getOrNull(1) + ?.toIntOrNull() + val link = fixUrl(header.attr("href")) + Episode(link, episode = episode) + }.reversed() + + val recommendations = document.select("aside#sidebar ul li").mapNotNull { + it.toSearchResult() + } + + val tracker = APIHolder.getTracker(listOf(title),TrackerType.getTypes(type),year,true) + + return newAnimeLoadResponse(title, url, type) { + engName = title + posterUrl = tracker?.image ?: poster + backgroundPosterUrl = tracker?.cover + this.year = year + addEpisodes(DubStatus.Subbed, episodes) + showStatus = status + this.rating = rating + plot = description + addTrailer(trailer) + this.tags = tags + this.recommendations = recommendations + addMalId(tracker?.malId) + addAniListId(tracker?.aniId?.toIntOrNull()) + } + + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val document = app.get(data).document + + argamap( + { + document.select("div#server ul li div").apmap { + val dataPost = it.attr("data-post") + val dataNume = it.attr("data-nume") + val dataType = it.attr("data-type") + + val iframe = app.post( + url = "$mainUrl/wp-admin/admin-ajax.php", + data = mapOf( + "action" to "player_ajax", + "post" to dataPost, + "nume" to dataNume, + "type" to dataType + ), + referer = data, + headers = mapOf("X-Requested-With" to "XMLHttpRequest") + ).document.select("iframe").attr("src") + + loadFixedExtractor(fixedIframe(iframe), it.text(), "$mainUrl/", subtitleCallback, callback) + + } + }, + { + document.select("div#downloadb li").map { el -> + el.select("a").apmap { + loadFixedExtractor(fixedIframe(it.attr("href")), el.select("strong").text(), "$mainUrl/", subtitleCallback, callback) + } + } + } + ) + + return true + } + + private suspend fun loadFixedExtractor( + url: String, + name: String, + referer: String? = null, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + loadExtractor(url, referer, subtitleCallback) { link -> + callback.invoke( + ExtractorLink( + link.name, + link.name, + link.url, + link.referer, + name.fixQuality(), + link.type, + link.headers, + link.extractorData + ) + ) + } + } + + private fun String.fixQuality() : Int { + return when(this) { + "MP4HD" -> Qualities.P720.value + "FULLHD" -> Qualities.P1080.value + else -> this.filter { it.isDigit() }.toIntOrNull() ?: Qualities.Unknown.value + } + } + + private fun fixedIframe(url: String): String { + val id = Regex("""(?:/f/|/file/)(\w+)""").find(url)?.groupValues?.getOrNull(1) + return when { + url.startsWith(acefile) -> "${acefile}/player/$id" + else -> fixUrl(url) + } + } + + private fun String.removeBloat(): String { + return this.replace(Regex("(Nonton)|(Anime)|(Subtitle\\sIndonesia)"), "").trim() + } + +} \ No newline at end of file diff --git a/SoraStream/build.gradle.kts b/SoraStream/build.gradle.kts index f913dc3f..c1174d20 100644 --- a/SoraStream/build.gradle.kts +++ b/SoraStream/build.gradle.kts @@ -1,7 +1,7 @@ import org.jetbrains.kotlin.konan.properties.Properties // use an integer for version numbers -version = 144 +version = 167 android { defaultConfig { @@ -14,6 +14,7 @@ android { buildConfigField("String", "SORATED", "\"${properties.getProperty("SORATED")}\"") buildConfigField("String", "DUMP_API", "\"${properties.getProperty("DUMP_API")}\"") buildConfigField("String", "DUMP_KEY", "\"${properties.getProperty("DUMP_KEY")}\"") + buildConfigField("String", "PRIMEWIRE_KEY", "\"${properties.getProperty("PRIMEWIRE_KEY")}\"") buildConfigField("String", "CRUNCHYROLL_BASIC_TOKEN", "\"${properties.getProperty("CRUNCHYROLL_BASIC_TOKEN")}\"") buildConfigField("String", "CRUNCHYROLL_REFRESH_TOKEN", "\"${properties.getProperty("CRUNCHYROLL_REFRESH_TOKEN")}\"") diff --git a/SoraStream/src/main/kotlin/com/hexated/Extractors.kt b/SoraStream/src/main/kotlin/com/hexated/Extractors.kt new file mode 100644 index 00000000..b91348b1 --- /dev/null +++ b/SoraStream/src/main/kotlin/com/hexated/Extractors.kt @@ -0,0 +1,162 @@ +package com.hexated + +import com.lagradost.cloudstream3.extractors.Filesim +import com.lagradost.cloudstream3.extractors.GMPlayer +import com.lagradost.cloudstream3.extractors.StreamSB +import com.lagradost.cloudstream3.extractors.Voe +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.APIHolder.getCaptchaToken +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.* +import java.math.BigInteger +import java.security.MessageDigest + +open class Playm4u : ExtractorApi() { + override val name = "Playm4u" + override val mainUrl = "https://play9str.playm4u.xyz" + override val requiresReferer = true + private val password = "plhq@@@22" + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val document = app.get(url, referer = referer).document + val script = document.selectFirst("script:containsData(idfile =)")?.data() ?: return + val passScript = document.selectFirst("script:containsData(domain_ref =)")?.data() ?: return + + val pass = passScript.substringAfter("CryptoJS.MD5('").substringBefore("')") + val amount = passScript.substringAfter(".toString()), ").substringBefore("));").toInt() + + val idFile = "idfile".findIn(script) + val idUser = "idUser".findIn(script) + val domainApi = "DOMAIN_API".findIn(script) + val nameKeyV3 = "NameKeyV3".findIn(script) + val dataEnc = caesarShift( + mahoa( + "Win32|$idUser|$idFile|$referer", + md5(pass) + ), amount + ).toHex() + + val captchaKey = + document.select("script[src*=https://www.google.com/recaptcha/api.js?render=]") + .attr("src").substringAfter("render=") + val token = getCaptchaToken( + url, + captchaKey, + referer = referer + ) + + val source = app.post( + domainApi, data = mapOf( + "namekey" to nameKeyV3, + "token" to "$token", + "referrer" to "$referer", + "data" to "$dataEnc|${md5(dataEnc + password)}", + ), referer = "$mainUrl/" + ).parsedSafe() + + callback.invoke( + ExtractorLink( + this.name, + this.name, + source?.data ?: return, + "$mainUrl/", + Qualities.P1080.value, + INFER_TYPE + ) + ) + + subtitleCallback.invoke( + SubtitleFile( + source.sub?.substringBefore("|")?.toLanguage() ?: return, + source.sub.substringAfter("|"), + ) + ) + + } + + private fun caesarShift(str: String, amount: Int): String { + var output = "" + val adjustedAmount = if (amount < 0) amount + 26 else amount + for (element in str) { + var c = element + if (c.isLetter()) { + val code = c.code + c = when (code) { + in 65..90 -> ((code - 65 + adjustedAmount) % 26 + 65).toChar() + in 97..122 -> ((code - 97 + adjustedAmount) % 26 + 97).toChar() + else -> c + } + } + output += c + } + return output + } + + private fun mahoa(input: String, key: String): String { + val a = CryptoJS.encrypt(key, input) + return a.replace("U2FsdGVkX1", "") + .replace("/", "|a") + .replace("+", "|b") + .replace("=", "|c") + .replace("|", "-z") + } + + private fun md5(input: String): String { + val md = MessageDigest.getInstance("MD5") + return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0') + } + + private fun String.toHex(): String { + return this.toByteArray().joinToString("") { "%02x".format(it) } + } + + private fun String.findIn(data: String): String { + return "$this\\s*=\\s*[\"'](\\S+)[\"'];".toRegex().find(data)?.groupValues?.get(1) ?: "" + } + + private fun String.toLanguage() : String { + return if(this == "EN") "English" else this + } + + data class Source( + @JsonProperty("data") val data: String? = null, + @JsonProperty("sub") val sub: String? = null, + ) + +} + +class TravelR : GMPlayer() { + override val name = "TravelR" + override val mainUrl = "https://travel-russia.xyz" +} + +class Mwish : Filesim() { + override val name = "Mwish" + override var mainUrl = "https://mwish.pro" +} + +class Animefever : Filesim() { + override val name = "Animefever" + override var mainUrl = "https://animefever.fun" +} + +class Multimovies : Filesim() { + override val name = "Multimovies" + override var mainUrl = "https://multimovies.cloud" +} + +class MultimoviesSB : StreamSB() { + override var name = "Multimovies" + override var mainUrl = "https://multimovies.website" +} + +class Yipsu : Voe() { + override val name = "Yipsu" + override var mainUrl = "https://yip.su" +} \ No newline at end of file diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt b/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt index 88674b15..5f6e72f0 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraExtractor.kt @@ -5,18 +5,20 @@ import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.Session -import com.hexated.RabbitStream.extractRabbitStream +import com.lagradost.cloudstream3.extractors.Filesim +import com.lagradost.cloudstream3.extractors.GMPlayer +import com.lagradost.cloudstream3.extractors.StreamSB +import com.lagradost.cloudstream3.extractors.Voe +import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler import com.lagradost.cloudstream3.extractors.helper.GogoHelper import com.lagradost.cloudstream3.network.CloudflareKiller import com.lagradost.nicehttp.RequestBodyTypes import kotlinx.coroutines.delay import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.RequestBody.Companion.toRequestBody -import okio.ByteString.Companion.encode import org.jsoup.Jsoup import org.jsoup.nodes.Document -import com.lagradost.cloudstream3.utils.INFER_TYPE - +import org.jsoup.select.Elements val session = Session(Requests().baseClient) @@ -42,18 +44,19 @@ object SoraExtractor : SoraStream() { val media = app.get( "$gokuAPI/ajax/movie/search?keyword=$title", headers = headers ).document.select("div.item").find { ele -> - val url = ele.selectFirst("a")?.attr("href") + val url = ele.selectFirst("a.movie-link")?.attr("href") val titleMedia = ele.select("h3.movie-name").text() - val yearMedia = - ele.select("div.info-split > div:first-child").text().toIntOrNull() + val titleSlug = title.createSlug() + val yearMedia = ele.select("div.info-split > div:first-child").text().toIntOrNull() val lastSeasonMedia = ele.select("div.info-split > div:nth-child(2)").text().substringAfter("SS") .substringBefore("/").trim().toIntOrNull() - (titleMedia.equals(title, true) || titleMedia.createSlug().equals(title.createSlug())) && + (titleMedia.equals(title, true) || titleMedia.createSlug() + .equals(titleSlug) || url?.contains("$titleSlug-") == true) && (if (season == null) { - yearMedia == year && url?.contains("/watch-movie/") == true + yearMedia == year && url?.contains("/movie/") == true } else { - lastSeasonMedia == lastSeason && url?.contains("/watch-series/") == true + lastSeasonMedia == lastSeason && url?.contains("/series/") == true }) } ?: return @@ -73,7 +76,8 @@ object SoraExtractor : SoraStream() { "$gokuAPI/ajax/movie/seasons/${ media.selectFirst("a.btn-wl")?.attr("data-id") ?: return }", headers = headers - ).document.select("a.ss-item").find { it.ownText().equals("Season $season", true) }?.attr("data-id") + ).document.select("a.ss-item").find { it.ownText().equals("Season $season", true) } + ?.attr("data-id") val episodeId = app.get( "$gokuAPI/ajax/movie/season/episodes/${seasonId ?: return}", @@ -92,15 +96,13 @@ object SoraExtractor : SoraStream() { val iframe = app.get("$gokuAPI/ajax/movie/episode/server/sources/$id", headers = headers) .parsedSafe()?.data?.link ?: return@apmap - extractRabbitStream( + loadCustomExtractor( if (iframe.contains("rabbitstream")) "Vidcloud" else "Upcloud", iframe, "$gokuAPI/", subtitleCallback, callback, - false, - decryptKey = RabbitStream.getKey() - ) { it } + ) } } @@ -125,9 +127,9 @@ object SoraExtractor : SoraStream() { link.url, link.referer, if (link.name == "VidSrc") Qualities.P1080.value else link.quality, - link.headers, - link.extractorData, link.type, + link.headers, + link.extractorData ) ) } @@ -232,92 +234,6 @@ object SoraExtractor : SoraStream() { } } - suspend fun invokeHDMovieBox( - title: String? = null, - season: Int? = null, - episode: Int? = null, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit - ) { - val fixTitle = title.createSlug() - val url = "$hdMovieBoxAPI/watch/$fixTitle" - val doc = app.get(url).document - val id = if (season == null) { - doc.selectFirst("div.player div#not-loaded")?.attr("data-whatwehave") - } else { - doc.select("div.season-list-column div[data-season=$season] div.list div.item")[episode?.minus( - 1 - ) ?: 0].selectFirst("div.ui.checkbox")?.attr("data-episode") - } ?: return - - val iframeUrl = app.post( - "$hdMovieBoxAPI/ajax/service", data = mapOf( - "e_id" to id, - "v_lang" to "en", - "type" to "get_whatwehave", - ), headers = mapOf("X-Requested-With" to "XMLHttpRequest") - ).parsedSafe()?.apiIframe ?: return - - delay(1000) - val iframe = app.get(iframeUrl, referer = url).document.selectFirst("iframe") - ?.attr("src").let { httpsify(it ?: return) } - - if (iframe.startsWith("https://vidmoly.to")) { - loadExtractor(iframe, "$hdMovieBoxAPI/", subtitleCallback) { video -> - callback.invoke( - ExtractorLink( - video.name, - video.name, - video.url, - video.referer, - Qualities.P1080.value, - type = INFER_TYPE, - video.headers, - video.extractorData - ) - ) - } - } else { - val base = getBaseUrl(iframe) - val script = app.get( - httpsify(iframe), referer = "$hdMovieBoxAPI/" - ).document.selectFirst("script:containsData(var vhash =)")?.data() - ?.substringAfter("vhash, {")?.substringBefore("}, false") - - tryParseJson("{$script}").let { source -> - val disk = if (source?.videoDisk == null) { - "" - } else { - base64Encode(source.videoDisk.toString().toByteArray()) - } - val link = getBaseUrl(iframe) + source?.videoUrl?.replace( - "\\", "" - ) + "?s=${source?.videoServer}&d=$disk" - callback.invoke( - ExtractorLink( - "HDMovieBox", - "HDMovieBox", - link, - iframe, - Qualities.P1080.value, - isM3u8 = true, - ) - ) - - source?.tracks?.map { sub -> - subtitleCallback.invoke( - SubtitleFile( - sub.label ?: "", - fixUrl(sub.file ?: return@map null, base), - ) - ) - } - } - } - - - } - suspend fun invokeDreamfilm( title: String? = null, season: Int? = null, @@ -332,74 +248,19 @@ object SoraExtractor : SoraStream() { "$dreamfilmAPI/series/$fixTitle/season-$season/episode-$episode" } - val iframe = app.get(url).document.selectFirst("iframe.Moly")?.attr("data-src") - loadExtractor(iframe ?: return, "$dreamfilmAPI/", subtitleCallback) { link -> - callback.invoke( - ExtractorLink( - link.name, - link.name, - link.url, - link.referer, - Qualities.P1080.value, - type = INFER_TYPE, - link.headers, - link.extractorData - ) - ) - } - - } - - suspend fun invokeSeries9( - title: String? = null, - year: Int? = null, - season: Int? = null, - episode: Int? = null, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit - ) { - val fixTitle = title.createSlug() - val doc = if (season == null) { - val res = app.get("$series9API/film/$fixTitle/watching.html") - if (!res.isSuccessful) app.get("$series9API/film/$fixTitle-$year/watching.html").document else res.document - } else { - app.get("$series9API/film/$fixTitle-season-$season/watching.html").document - } - - val server = doc.select("div#list-eps div.le-server").map { ele -> - if (season == null) { - ele.select("a").attr("player-data") - } else { - ele.select("a[episode-data=$episode]").attr("player-data") - } - }.find { it.contains(Regex("movembed|membed")) } - - val iframe = app.get(httpsify(server ?: return)) - val iframeDoc = iframe.document - - argamap({ - iframeDoc.select(".list-server-items > .linkserver") - .forEach { element -> - val status = element.attr("data-status") ?: return@forEach - if (status != "1") return@forEach - val extractorData = element.attr("data-video") ?: return@forEach - loadExtractor(extractorData, iframe.url, subtitleCallback, callback) - } - }, { - val iv = "9225679083961858" - val secretKey = "25742532592138496744665879883281" - GogoHelper.extractVidstream( - iframe.url, - "Vidstream", + val doc = app.get(url).document + doc.select("div#videosen a").apmap { + val iframe = app.get(it.attr("href")).document.selectFirst("div.card-video iframe") + ?.attr("data-src") + loadCustomExtractor( + null, + iframe ?: return@apmap, + "$dreamfilmAPI/", + subtitleCallback, callback, - iv, - secretKey, - secretKey, - isUsingAdaptiveKeys = false, - isUsingAdaptiveData = true, - iframeDocument = iframeDoc + Qualities.P1080.value ) - }) + } } suspend fun invokeIdlix( @@ -416,30 +277,26 @@ object SoraExtractor : SoraStream() { } else { "$idlixAPI/episode/$fixTitle-season-$season-episode-$episode" } - - val res = app.get(url) - if (!res.isSuccessful) return - val referer = getBaseUrl(res.url) - val document = res.document - val id = document.select("meta#dooplay-ajax-counter").attr("data-postid") - val type = if (url.contains("/movie/")) "movie" else "tv" - - document.select("ul#playeroptionsul > li").map { - it.attr("data-nume") - }.apmap { nume -> - val source = app.post( - url = "$referer/wp-admin/admin-ajax.php", data = mapOf( - "action" to "doo_player_ajax", "post" to id, "nume" to nume, "type" to type - ), headers = mapOf("X-Requested-With" to "XMLHttpRequest"), referer = url - ).parsed().embed_url - - if (!source.contains("youtube")) { - loadExtractor(source, "$referer/", subtitleCallback, callback) - } - } + invokeWpmovies(url, subtitleCallback, callback, encrypt = true) } - suspend fun invokeUniqueStream( + suspend fun invokeMultimovies( + title: String? = null, + season: Int? = null, + episode: Int? = null, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val fixTitle = title.createSlug() + val url = if (season == null) { + "$multimoviesAPI/movies/$fixTitle" + } else { + "$multimoviesAPI/episodes/$fixTitle-${season}x${episode}" + } + invokeWpmovies(url, subtitleCallback, callback, true) + } + + suspend fun invokeNetmovies( title: String? = null, year: Int? = null, season: Int? = null, @@ -449,58 +306,81 @@ object SoraExtractor : SoraStream() { ) { val fixTitle = title.createSlug() val url = if (season == null) { - "$uniqueStreamAPI/movies/$fixTitle-$year" + "$netmoviesAPI/movies/$fixTitle-$year" } else { - "$uniqueStreamAPI/episodes/$fixTitle-season-$season-episode-$episode" + "$netmoviesAPI/episodes/$fixTitle-${season}x${episode}" } + invokeWpmovies(url, subtitleCallback, callback) + } - val res = app.get(url) - if (!res.isSuccessful) return - val baseApi = getBaseUrl(res.url) - + private suspend fun invokeWpmovies( + url: String? = null, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit, + fixIframe: Boolean = false, + encrypt: Boolean = false, + key: String? = null, + ) { + fun String.fixBloat() : String { + return this.replace("\"", "").replace("\\", "") + } + val res = app.get(url ?: return) + val headers = mapOf("X-Requested-With" to "XMLHttpRequest") + val referer = getBaseUrl(res.url) val document = res.document - val type = if (url.contains("/movies/")) "movie" else "tv" - document.select("ul#playeroptionsul > li").apmap { el -> - val id = el.attr("data-post") - val nume = el.attr("data-nume") - val source = app.post( - url = "$baseApi/wp-admin/admin-ajax.php", data = mapOf( + document.select("ul#playeroptionsul > li").map { + Triple( + it.attr("data-post"), + it.attr("data-nume"), + it.attr("data-type") + ) + }.apmap { (id, nume, type) -> + val json = app.post( + url = "$referer/wp-admin/admin-ajax.php", data = mapOf( "action" to "doo_player_ajax", "post" to id, "nume" to nume, "type" to type - ), headers = mapOf("X-Requested-With" to "XMLHttpRequest"), referer = url - ).parsed().embed_url.let { fixUrl(it) } - - when { - source.contains("uniquestream") -> { - val resDoc = app.get( - source, referer = "$baseApi/", headers = mapOf( - "Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8" - ) - ).document - val srcm3u8 = - resDoc.selectFirst("script:containsData(let url =)")?.data()?.let { - Regex("['|\"](.*?.m3u8)['|\"]").find(it)?.groupValues?.getOrNull(1) - } - callback.invoke( - ExtractorLink( - "UniqueStream", - "UniqueStream", - srcm3u8 ?: return@apmap null, - source, - Qualities.P1080.value, - true, - ) - ) - } - !source.contains("youtube") -> loadExtractor( - source, "$uniqueStreamAPI/", subtitleCallback, callback - ) - else -> { - // pass + ), headers = headers, referer = url + ) + val source = tryParseJson(json.text)?.let { + when { + encrypt -> cryptoAESHandler(it.embed_url,(it.key ?: return@apmap).toByteArray(), false)?.fixBloat() + fixIframe -> Jsoup.parse(it.embed_url).select("IFRAME").attr("SRC") + else -> it.embed_url } + } ?: return@apmap + if (!source.contains("youtube")) { + loadExtractor(source, "$referer/", subtitleCallback, callback) } } } + suspend fun invokeDoomovies( + title: String? = null, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val res = app.get("$doomoviesAPI/movies/${title.createSlug()}/") + val host = getBaseUrl(res.url) + val document = res.document + document.select("ul#playeroptionsul > li") + .filter { element -> element.select("span.flag img").attr("src").contains("/en.") } + .map { + Triple( + it.attr("data-post"), + it.attr("data-nume"), + it.attr("data-type") + ) + }.apmap { (id, nume, type) -> + val source = app.get( + "$host/wp-json/dooplayer/v2/${id}/${type}/${nume}", + headers = mapOf("X-Requested-With" to "XMLHttpRequest"), + referer = "$host/" + ).parsed().embed_url + if (!source.contains("youtube")) { + loadExtractor(source, "$host/", subtitleCallback, callback) + } + } + } + suspend fun invokeNoverse( title: String? = null, season: Int? = null, @@ -556,16 +436,8 @@ object SoraExtractor : SoraStream() { } else { "${filmxyAPI}/tv/$imdbId" } - val filmxyCookies = getFilmxyCookies(imdbId, season) - - val cookiesDoc = mapOf( - "G_ENABLED_IDPS" to "google", - "wordpress_logged_in_8bf9d5433ac88cc9a3a396d6b154cd01" to (filmxyCookies.wLog - ?: return), - "PHPSESSID" to (filmxyCookies.phpsessid ?: return) - ) - - val doc = session.get(url, cookies = cookiesDoc).document + val filmxyCookies = getFilmxyCookies(url) + val doc = app.get(url, cookies = filmxyCookies).document val script = doc.selectFirst("script:containsData(var isSingle)")?.data() ?: return val sourcesData = @@ -604,16 +476,9 @@ object SoraExtractor : SoraStream() { "&linkIDs%5B%5D=$it" }?.replace("\"", "") - val body = "action=get_vid_links$linkIDs&user_id=$userId&nonce=$userNonce".toRequestBody() - val cookiesJson = mapOf( - "G_ENABLED_IDPS" to "google", - "PHPSESSID" to "${filmxyCookies.phpsessid}", - "wordpress_logged_in_8bf9d5433ac88cc9a3a396d6b154cd01" to "${filmxyCookies.wLog}", - "wordpress_sec_8bf9d5433ac88cc9a3a396d6b154cd01" to "${filmxyCookies.wSec}" - ) val json = app.post( "$filmxyAPI/wp-admin/admin-ajax.php", - requestBody = body, + requestBody = "action=get_vid_links$linkIDs&user_id=$userId&nonce=$userNonce".toRequestBody(), referer = url, headers = mapOf( "Accept" to "*/*", @@ -622,7 +487,7 @@ object SoraExtractor : SoraStream() { "Origin" to filmxyAPI, "X-Requested-With" to "XMLHttpRequest", ), - cookies = cookiesJson + cookies = filmxyCookies ).text.let { tryParseJson>(it) } sources?.map { source -> @@ -653,6 +518,74 @@ object SoraExtractor : SoraStream() { } + suspend fun invokeDramaday( + title: String? = null, + year: Int? = null, + season: Int? = null, + episode: Int? = null, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + fun String.getQuality(): String? = + Regex("""\d{3,4}[pP]""").find(this)?.groupValues?.getOrNull(0) + + fun String.getTag(): String? = + Regex("""\d{3,4}[pP]\s*(.*)""").find(this)?.groupValues?.getOrNull(1) + + val slug = title.createSlug() + val epsSlug = getEpisodeSlug(season, episode) + val url = if (season == null) { + "$dramadayAPI/$slug-$year/" + } else { + "$dramadayAPI/$slug/" + } + val res = app.get(url).document + + val servers = if (season == null) { + val player = res.select("div.tabs__pane p a[href*=https://ouo]").attr("href") + val ouo = bypassOuo(player) + app.get(ouo ?: return).document.select("article p:matches(\\d{3,4}[pP]) + p:has(a)") + .flatMap { ele -> + val entry = ele.previousElementSibling()?.text() ?: "" + ele.select("a").map { + Triple(entry.getQuality(), entry.getTag(), it.attr("href")) + }.filter { + it.third.startsWith("https://pixeldrain.com") || it.third.startsWith("https://krakenfiles.com") + } + } + } else { + val data = res.select("tbody tr:has(td[data-order=${epsSlug.second}])") + val qualities = + data.select("td:nth-child(2)").attr("data-order").split("
").map { it } + val iframe = data.select("a[href*=https://ouo]").map { it.attr("href") } + qualities.zip(iframe).map { + Triple(it.first.getQuality(), it.first.getTag(), it.second) + } + } + + servers.filter { it.first == "720p" || it.first == "1080p" }.apmap { + val server = if (it.third.startsWith("https://ouo")) bypassOuo(it.third) else it.third + loadExtractor(server ?: return@apmap, "$dramadayAPI/", subtitleCallback) { link -> + callback.invoke( + ExtractorLink( + link.source, + "${link.name} ${it.second}", + link.url, + link.referer, + when { + link.type == ExtractorLinkType.M3U8 -> link.quality + else -> getQualityFromName(it.first) + }, + link.type, + link.headers, + link.extractorData + ) + ) + } + } + + } + suspend fun invokeKimcartoon( title: String? = null, season: Int? = null, @@ -710,59 +643,6 @@ object SoraExtractor : SoraStream() { } } - suspend fun invokeXmovies( - title: String? = null, - year: Int? = null, - season: Int? = null, - episode: Int? = null, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit - ) { - val fixTitle = title.createSlug() - val doc = if (season == null) { - val res = app.get("$xMovieAPI/movies/$fixTitle/watch") - if (res.url == "$xMovieAPI/") app.get("$xMovieAPI/movies/$fixTitle-$year/watch").document else res.document - } else { - app.get("$xMovieAPI/series/$fixTitle-season-$season-episode-$episode/watch").document - } - - val script = doc.selectFirst("script:containsData(const player =)")?.data() ?: return - val link = - Regex("[\"|']file[\"|']:\\s?[\"|'](http.*?.(mp4|m3u8))[\"|'],").find(script)?.groupValues?.getOrNull( - 1 - ) ?: return - - if (link.contains(".m3u8")) { - M3u8Helper.generateM3u8( - "Xmovie", - link, - "", - ).forEach(callback) - } else { - callback.invoke( - ExtractorLink( - "Xmovie", - "Xmovie", - link, - "", - Qualities.P720.value, - ) - ) - } - - Regex(""""file":\s+?"(\S+\.(vtt|srt))""").find(script)?.groupValues?.getOrNull(1) - ?.let { sub -> - subtitleCallback.invoke( - SubtitleFile( - "English", - sub, - ) - ) - } - - - } - suspend fun invokeFmovies( title: String? = null, year: Int? = null, @@ -829,6 +709,33 @@ object SoraExtractor : SoraStream() { } } + suspend fun invokeVidsrcto( + imdbId: String?, + season: Int?, + episode: Int?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val url = if(season == null) { + "$vidsrctoAPI/embed/movie/$imdbId" + } else { + "$vidsrctoAPI/embed/tv/$imdbId/$season/$episode" + } + + val id = app.get(url).document.selectFirst("ul.episodes li a")?.attr("data-id") ?: return + + val subtitles = app.get("$vidsrctoAPI/ajax/embed/episode/$id/subtitles").text + tryParseJson>(subtitles)?.map { + subtitleCallback.invoke( + SubtitleFile( + it.label ?: "", + it.file ?: return@map + ) + ) + } + + } + suspend fun invokeKisskh( title: String? = null, season: Int? = null, @@ -858,7 +765,10 @@ object SoraExtractor : SoraStream() { when { season == null -> slugTitle?.equals(slug) == true lastSeason == 1 -> slugTitle?.contains(slug) == true - else -> slugTitle?.contains(slug) == true && it.title?.contains("Season $season", true) == true + else -> slugTitle?.contains(slug) == true && it.title?.contains( + "Season $season", + true + ) == true } } data?.id to data?.title @@ -932,13 +842,10 @@ object SoraExtractor : SoraStream() { argamap( { - invokeZoro(aniId, episode, subtitleCallback, callback) + invokeAnimetosho(malId, season, episode, subtitleCallback, callback) }, { - invokeAnimeKaizoku(malId, epsTitle, season, episode, callback) - }, - { - invokeBiliBili(aniId, episode, subtitleCallback, callback) + invokeAniwatch(malId, episode, subtitleCallback, callback) }, { if (season != null) invokeCrunchyroll( @@ -954,80 +861,86 @@ object SoraExtractor : SoraStream() { ) } - private suspend fun invokeBiliBili( - aniId: Int? = null, + private suspend fun invokeAnimetosho( + malId: Int? = null, + season: Int? = null, episode: Int? = null, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val res = app.get( - "$biliBiliAPI/anime/episodes?id=${aniId ?: return}&source_id=bilibili", - referer = otakuzBaseUrl - ) - .parsedSafe()?.episodes?.find { - it.episodeNumber == episode - } ?: return - - val sources = - app.get( - "$biliBiliAPI/source?episode_id=${res.sourceEpisodeId}&source_media_id=${res.sourceMediaId}&source_id=${res.sourceId}", - referer = otakuzBaseUrl - ) - .parsedSafe() - - sources?.sources?.apmap { source -> - val quality = - app.get( - source.file ?: return@apmap null, - referer = otakuzBaseUrl - ).document.selectFirst("Representation") - ?.attr("height") - callback.invoke( - ExtractorLink( - "BiliBili", - "BiliBili", - source.file, - "", - quality?.toIntOrNull() ?: Qualities.Unknown.value, - isDash = true - ) - ) + fun Elements.getLinks(): List> { + return this.flatMap { ele -> + ele.select("div.links a:matches(KrakenFiles|GoFile)").map { + Triple( + it.attr("href"), + ele.select("div.size").text(), + getIndexQuality(ele.select("div.link a").text()) + ) + } + } } - sources?.subtitles?.map { sub -> - subtitleCallback.invoke( - SubtitleFile( - SubtitleHelper.fromTwoLettersToLanguage(sub.lang ?: "") ?: sub.language - ?: return@map null, - sub.file ?: return@map null + val (seasonSLug, episodeSlug) = getEpisodeSlug(season, episode) + val jikan = app.get("$jikanAPI/anime/$malId/full").parsedSafe()?.data + val aniId = jikan?.external?.find { it.name == "AniDB" }?.url?.substringAfterLast("=") + val res = app.get("$animetoshoAPI/series/${jikan?.title?.createSlug()}.$aniId?filter[0][t]=nyaa_class&filter[0][v]=trusted").document + + val servers = if (season == null) { + res.select("div.home_list_entry:has(div.links)").getLinks() + } else { + res.select("div.home_list_entry:has(div.link a:matches([\\.\\s]$episodeSlug[\\.\\s]|S${seasonSLug}E$episodeSlug))") + .getLinks() + } + + servers.filter { it.third in arrayOf(Qualities.P1080.value,Qualities.P720.value) }.apmap { + loadExtractor(it.first, "$animetoshoAPI/", subtitleCallback) { link -> + callback.invoke( + ExtractorLink( + link.source, + "${link.name} [${it.second}]", + link.url, + link.referer, + when { + link.type == ExtractorLinkType.M3U8 -> link.quality + else -> it.third + }, + link.type, + link.headers, + link.extractorData + ) ) - ) + } } } - private suspend fun invokeZoro( - aniId: Int? = null, + private suspend fun invokeAniwatch( + malId: Int? = null, episode: Int? = null, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val animeId = - app.get("https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/anilist/anime/${aniId ?: return}.json") - .parsedSafe()?.pages?.zoro?.keys?.map { it } val headers = mapOf( "X-Requested-With" to "XMLHttpRequest", ) + val animeId = app.get("$malsyncAPI/mal/anime/${malId ?: return}") + .parsedSafe()?.sites?.zoro?.keys?.map { it } animeId?.apmap { id -> - val episodeId = app.get("$zoroAPI/ajax/episode/list/${id ?: return@apmap}", headers = headers) - .parsedSafe()?.html?.let { + val episodeId = app.get( + "$aniwatchAPI/ajax/v2/episode/list/${id ?: return@apmap}", + headers = headers + ) + .parsedSafe()?.html?.let { Jsoup.parse(it) }?.select("div.ss-list a")?.find { it.attr("data-number") == "${episode ?: 1}" } ?.attr("data-id") val servers = - app.get("$zoroAPI/ajax/episode/servers?episodeId=${episodeId ?: return@apmap}", headers = headers) - .parsedSafe()?.html?.let { Jsoup.parse(it) } + app.get( + "$aniwatchAPI/ajax/v2/episode/servers?episodeId=${episodeId ?: return@apmap}", + headers = headers + ) + .parsedSafe()?.html?.let { Jsoup.parse(it) } ?.select("div.item.server-item")?.map { Triple( it.text(), @@ -1037,91 +950,20 @@ object SoraExtractor : SoraStream() { } servers?.apmap servers@{ server -> - val iframe = - app.get("$zoroAPI/ajax/episode/sources?id=${server.second ?: return@servers}", headers = headers) - .parsedSafe()?.link ?: return@servers - val audio = if (server.third == "sub") "Raw" else "English Dub" - if (server.first.contains(Regex("Vidstreaming|MegaCloud|Vidcloud"))) { - extractRabbitStream( - "${server.first} [$audio]", - iframe, - "$zoroAPI/", - subtitleCallback, - callback, - false, - decryptKey = RabbitStream.getZoroKey() - ) { it } - } else { - loadExtractor(iframe, "$zoroAPI/", subtitleCallback, callback) - } - - } - } - - - } - - private suspend fun invokeAnimeKaizoku( - malId: Int? = null, - epsTitle: String? = null, - season: Int? = null, - episode: Int? = null, - callback: (ExtractorLink) -> Unit - ) { - val search = app.get("$animeKaizokuAPI/?s=${malId ?: return}").document - val detailHref = - search.select("ul#posts-container li").map { it.selectFirst("a")?.attr("href") } - .find { - it?.contains("$malId") == true - }?.let { fixUrl(it, animeKaizokuAPI) } - - val detail = app.get(detailHref ?: return).document - val postId = - detail.selectFirst("link[rel=shortlink]")?.attr("href")?.substringAfter("?p=") ?: return - val script = detail.selectFirst("script:containsData(DDL)")?.data()?.splitData() ?: return - - val media = fetchingKaizoku(animeKaizokuAPI, postId, script, detailHref).document - val iframe = media.select("tbody td[colspan=2]").map { it.attr("onclick") to it.text() } - .filter { it.second.contains("1080p", true) } - - val eps = if (season == null) { - null - } else { - if (episode!! < 10) "0$episode" else episode - } - - iframe.apmap { (data, name) -> - val worker = - fetchingKaizoku(animeKaizokuAPI, postId, data.splitData(), detailHref).document - .select("tbody td") - .map { Triple(it.attr("onclick"), it.text(), it.nextElementSibling()?.text()) } - - val episodeData = worker.let { list -> - if (season == null) list.firstOrNull() else list.find { - it.second.contains( - Regex("($eps\\.)|(-\\s$eps)") - ) || it.second.contains("$epsTitle", true) - } - } ?: return@apmap null - - val ouo = fetchingKaizoku( - animeKaizokuAPI, - postId, - episodeData.first.splitData(), - detailHref - ).text.substringAfter("openInNewTab(\"") - .substringBefore("\")").let { base64Decode(it) } - - if (!ouo.startsWith("https://ouo")) return@apmap null - callback.invoke( - ExtractorLink( - "AnimeKaizoku", - "AnimeKaizoku [${episodeData.third}]", - bypassOuo(ouo) ?: return@apmap null, - "$animeKaizokuAPI/", - Qualities.P1080.value, + val iframe = app.get( + "$aniwatchAPI/ajax/v2/episode/sources?id=${server.second ?: return@servers}", + headers = headers ) - ) + .parsedSafe()?.link ?: return@servers + val audio = if (server.third == "sub") "Raw" else "English Dub" + loadCustomExtractor( + "${server.first} [$audio]", + iframe, + "$aniwatchAPI/", + subtitleCallback, + callback, + ) + } } } @@ -1226,42 +1068,50 @@ object SoraExtractor : SoraStream() { } }.filter { it.second?.contains(Regex("(https:)|(http:)")) == true } - val sources = mutableListOf>() - if (iframeList.any { - it.first.contains( - "2160p", - true - ) - }) { - sources.addAll(iframeList.filter { - it.first.contains( - "2160p", - true - ) - }) - sources.add(iframeList.first { - it.first.contains( - "1080p", - true - ) - }) - } else { - sources.addAll(iframeList.filter { it.first.contains("1080p", true) }) - } +// val sources = mutableListOf>() +// if (iframeList.any { +// it.first.contains( +// "2160p", +// true +// ) +// }) { +// sources.addAll(iframeList.filter { +// it.first.contains( +// "2160p", +// true +// ) +// }) +// sources.add(iframeList.first { +// it.first.contains( +// "1080p", +// true +// ) +// }) +// } else { +// sources.addAll(iframeList.filter { it.first.contains("1080p", true) }) +// } - sources.apmap { (quality, link) -> + iframeList.apmap { (quality, link) -> val driveLink = if (link?.contains("driveleech") == true) bypassDriveleech(link) else bypassTechmny( link ?: return@apmap ) val base = getBaseUrl(driveLink ?: return@apmap) - val resDoc = app.get(driveLink).document - val bitLink = resDoc.selectFirst("a.btn.btn-outline-success")?.attr("href") - val downloadLink = if (bitLink.isNullOrEmpty()) { - val backupIframe = resDoc.select("a.btn.btn-outline-warning").attr("href") - extractBackupUHD(backupIframe ?: return@apmap) - } else { - extractMirrorUHD(bitLink, base) + val driveReq = app.get(driveLink) + val driveRes = driveReq.document + val bitLink = driveRes.select("a.btn.btn-outline-success").attr("href") + val insLink = driveRes.select("a.btn.btn-danger:contains(Instant Download)").attr("href") + val downloadLink = when { + insLink.isNotEmpty() -> extractInstantUHD(insLink) + driveRes.select("button.btn.btn-success").text() + .contains("Direct Download", true) -> extractDirectUHD(driveLink, driveReq) + bitLink.isNullOrEmpty() -> { + val backupIframe = driveRes.select("a.btn.btn-outline-warning").attr("href") + extractBackupUHD(backupIframe ?: return@apmap) + } + else -> { + extractMirrorUHD(bitLink, base) + } } val tags = getUhdTags(quality) @@ -1323,9 +1173,11 @@ object SoraExtractor : SoraStream() { val gdBotLink = extractGdbot(link) extractGdflix(gdBotLink ?: return@apmap) } + link.contains("gdflix") -> { extractGdflix(link) } + else -> { return@apmap } @@ -1492,9 +1344,17 @@ object SoraExtractor : SoraStream() { val gdBotLink = extractGdbot(fdLink ?: return@apmap null) extractGdflix(gdBotLink ?: return@apmap null) } + type.contains("oiya") -> { - extractOiya(fdLink ?: return@apmap null, qualities) + val oiyaLink = extractOiya(fdLink ?: return@apmap null, qualities) + if (oiyaLink?.contains("gdtot") == true) { + val gdBotLink = extractGdbot(oiyaLink) + extractGdflix(gdBotLink ?: return@apmap null) + } else { + oiyaLink + } } + else -> { return@apmap null } @@ -1521,12 +1381,15 @@ object SoraExtractor : SoraStream() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val res = app.get("$m4uhdAPI/search/${title.createSlug()}.html").document - val scriptData = res.select("div.row div.item").map { + val (seasonSlug, episodeSlug) = getEpisodeSlug(season, episode) + val req = app.get("$m4uhdAPI/search/${title.createSlug()}.html") + val referer = getBaseUrl(req.url) + val res = req.document + val scriptData = res.select("div.row div.item").map { ele -> Triple( - it.selectFirst("img.imagecover")?.attr("title"), - it.selectFirst("div.jtip-top div:last-child")?.text(), - it.selectFirst("a")?.attr("href") + ele.select("div.tiptitle p").text(), + ele.select("div.jtip-top div:last-child").text().filter { it.isDigit() }, + ele.selectFirst("a")?.attr("href") ) } @@ -1534,15 +1397,13 @@ object SoraExtractor : SoraStream() { scriptData.firstOrNull() } else { scriptData.find { - it.first?.contains( - "Watch Free ${title?.replace(":", "")}", true - ) == true && (it.first?.contains("$year") == true || it.second?.contains( - "$year" - ) == true) + it.first.contains( + "$title", true + ) && it.second == "$year" } } - val link = fixUrl(script?.third ?: return, m4uhdAPI) + val link = fixUrl(script?.third ?: return, referer) val request = app.get(link) var cookiesSet = request.headers.filter { it.first == "set-cookie" } var xsrf = @@ -1558,11 +1419,11 @@ object SoraExtractor : SoraStream() { doc.select("div.le-server span").map { it.attr("data") } } else { val episodeData = - doc.selectFirst("div.col-lg-9.col-xl-9 p:matches((?i)S0?$season-E0?$episode$)") + doc.selectFirst("div.col-lg-9.col-xl-9 p:matches((?i)S$seasonSlug-E$episodeSlug)") ?: return val idepisode = episodeData.select("button").attr("idepisode") ?: return val requestEmbed = app.post( - "$m4uhdAPI/ajaxtv", data = mapOf( + "$referer/ajaxtv", data = mapOf( "idepisode" to idepisode, "_token" to "$token" ), referer = link, headers = mapOf( "X-Requested-With" to "XMLHttpRequest", @@ -1576,14 +1437,16 @@ object SoraExtractor : SoraStream() { cookiesSet.find { it.second.contains("XSRF-TOKEN") }?.second?.substringAfter("XSRF-TOKEN=") ?.substringBefore(";") session = - cookiesSet.find { it.second.contains("laravel_session") }?.second?.substringAfter("laravel_session=") + cookiesSet.find { it.second.contains("laravel_session") }?.second?.substringAfter( + "laravel_session=" + ) ?.substringBefore(";") requestEmbed.document.select("div.le-server span").map { it.attr("data") } } m4uData.apmap { data -> val iframe = app.post( - "$m4uhdAPI/ajax", + "$referer/ajax", data = mapOf( "m4u" to data, "_token" to "$token" ), @@ -1598,7 +1461,7 @@ object SoraExtractor : SoraStream() { ), ).document.select("iframe").attr("src") - loadExtractor(iframe, m4uhdAPI, subtitleCallback, callback) + loadExtractor(iframe, referer, subtitleCallback, callback) } } @@ -1644,7 +1507,7 @@ object SoraExtractor : SoraStream() { callback: (ExtractorLink) -> Unit ) { val id = getCrunchyrollId("${aniId ?: return}") - ?: getCrunchyrollIdFromMalSync("${malId ?: return}") ?: return + ?: getCrunchyrollIdFromMalSync("${malId ?: return}") val audioLocal = listOf( "ja-JP", "en-US", @@ -1935,81 +1798,6 @@ object SoraExtractor : SoraStream() { } - suspend fun invokeMovie123Net( - title: String? = null, - season: Int? = null, - episode: Int? = null, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit, - ) { - val server = "https://vidcloud9.org" - val fixTitle = title.createSlug() - val m = app.get("$movie123NetAPI/searching?q=$title&limit=40") - .parsedSafe()?.data?.find { - if (season == null) { - (it.t.equals(title, true) || it.t.createSlug() - .equals(fixTitle)) && it.t?.contains("season", true) == false - } else { - it.t?.equals( - "$title - Season $season", - true - ) == true || it.s?.contains("$fixTitle-season-$season-", true) == true - } - }?.s?.substringAfterLast("-") ?: return - - listOf( - "1", - "2" - ).apmap { serverNum -> - val media = app.post( - "$movie123NetAPI/datas", - requestBody = """{"m":$m,"e":${episode ?: 1},"s":$serverNum}""".toRequestBody( - RequestBodyTypes.JSON.toMediaTypeOrNull() - ) - ).parsedSafe()?.url ?: return@apmap null - - val serverUrl = "$server/watch?v=$media" - val token = - app.get(serverUrl).document.selectFirst("script:containsData(setRequestHeader)") - ?.data()?.let { - Regex("\\('0x1f2'\\),'(\\S+?)'\\)").find(it)?.groupValues?.getOrNull(1) - } ?: return@apmap null - - val videoUrl = app.post( - "$server/data", - requestBody = """{"doc":"$media"}""".toRequestBody( - RequestBodyTypes.JSON.toMediaTypeOrNull() - ), - headers = mapOf( - "x-csrf-token" to token - ), - ).parsedSafe()?.url ?: return@apmap null - - if (videoUrl.startsWith("https")) { - loadExtractor(videoUrl, movie123NetAPI, subtitleCallback, callback) - } else { - callback.invoke( - ExtractorLink( - "123Movies", - "123Movies", - fixUrl(base64Decode(videoUrl), server), - serverUrl, - Qualities.P720.value, - true - ) - ) - - subtitleCallback.invoke( - SubtitleFile( - "English", - "https://sub.vxdn.net/sub/$m-${episode ?: 1}.vtt" - ) - ) - } - } - - } - suspend fun invokeSmashyStream( imdbId: String? = null, season: Int? = null, @@ -2031,18 +1819,33 @@ object SoraExtractor : SoraStream() { it.attr("data-id") to it.text() }.apmap { when { - it.first.contains("/ffix") && !isAnime -> { + (it.second.equals("Player F", true) || it.second.equals( + "Player N", + true + )) && !isAnime -> { invokeSmashyFfix(it.second, it.first, url, callback) } + it.first.contains("/gtop") -> { invokeSmashyGtop(it.second, it.first, callback) } + it.first.contains("/dude_tv") -> { invokeSmashyDude(it.second, it.first, callback) } + it.first.contains("/rip") -> { invokeSmashyRip(it.second, it.first, subtitleCallback, callback) } + + it.first.contains("/im.php") && !isAnime -> { + invokeSmashyIm(it.second, it.first, subtitleCallback, callback) + } + + it.first.contains("/rw.php") && !isAnime -> { + invokeSmashyRw(it.second, it.first, subtitleCallback, callback) + } + else -> return@apmap } } @@ -2096,187 +1899,6 @@ object SoraExtractor : SoraStream() { } - suspend fun invokeBaymovies( - title: String? = null, - year: Int? = null, - season: Int? = null, - episode: Int? = null, - callback: (ExtractorLink) -> Unit, - ) { - val api = "https://thebayindexpublicgroupapi.zindex.eu.org" - val key = base64DecodeAPI("ZW0=c3Q=c3k=b28=YWQ=Ymg=") - val headers = mapOf( - "Referer" to "$baymoviesAPI/", - "Origin" to baymoviesAPI, - "cf_cache_token" to "UKsVpQqBMxB56gBfhYKbfCVkRIXMh42pk6G4DdkXXoVh7j4BjV" - ) - val query = getIndexQuery(title, year, season, episode) - val search = app.get( - "$api/0:search?q=$query&page_token=&page_index=0", - headers = headers - ).text - val media = searchIndex(title, season, episode, year, search) ?: return - - media.apmap { file -> - val expiry = (System.currentTimeMillis() + 345600000).toString() - val hmacSign = "${file.id}@$expiry".encode() - .hmacSha256(key.encode()).base64().replace("+", "-") - val encryptedId = - base64Encode(CryptoAES.encrypt(key, file.id ?: return@apmap null).toByteArray()) - val encryptedExpiry = base64Encode(CryptoAES.encrypt(key, expiry).toByteArray()) - val worker = getConfig().workers.randomOrNull() ?: return@apmap null - - val link = - "https://api.$worker.workers.dev/download.aspx?file=$encryptedId&expiry=$encryptedExpiry&mac=$hmacSign" - val size = file.size?.toDouble() ?: return@apmap null - val sizeFile = "%.2f GB".format(bytesToGigaBytes(size)) - val tags = Regex("\\d{3,4}[pP]\\.?(.*?)\\.(mkv|mp4)").find( - file.name ?: return@apmap null - )?.groupValues?.getOrNull(1)?.replace(".", " ")?.trim() - ?: "" - val quality = - Regex("(\\d{3,4})[pP]").find(file.name)?.groupValues?.getOrNull(1)?.toIntOrNull() - ?: Qualities.P1080.value - - callback.invoke( - ExtractorLink( - "Baymovies", - "Baymovies $tags [$sizeFile]", - link, - "$baymoviesAPI/", - quality, - ) - ) - - } - - - } - - suspend fun invokeBlackmovies( - apiUrl: String, - api: String, - title: String? = null, - year: Int? = null, - season: Int? = null, - episode: Int? = null, - callback: (ExtractorLink) -> Unit, - ) { - invokeIndex( - apiUrl, - api, - title, - year, - season, - episode, - callback, - ) - } - - suspend fun invokeRinzrymovies( - apiUrl: String, - api: String, - title: String? = null, - year: Int? = null, - season: Int? = null, - episode: Int? = null, - callback: (ExtractorLink) -> Unit, - ) { - invokeIndex( - apiUrl, - api, - title, - year, - season, - episode, - callback, - ) - } - - suspend fun invokeCodexmovies( - apiUrl: String, - api: String, - title: String? = null, - year: Int? = null, - season: Int? = null, - episode: Int? = null, - callback: (ExtractorLink) -> Unit, - password: String = "", - ) { - invokeIndex( - apiUrl, - api, - title, - year, - season, - episode, - callback, - password, - ) - } - - suspend fun invokeEdithxmovies( - apiUrl: String, - api: String, - title: String? = null, - year: Int? = null, - season: Int? = null, - episode: Int? = null, - callback: (ExtractorLink) -> Unit, - password: String = "", - ) { - invokeIndex( - apiUrl, - api, - title, - year, - season, - episode, - callback, - password, - ) - } - - suspend fun invokeJmdkhMovies( - apiUrl: String, - api: String, - title: String? = null, - year: Int? = null, - season: Int? = null, - episode: Int? = null, - callback: (ExtractorLink) -> Unit, - ) { - invokeIndex( - apiUrl, - api, - title, - year, - season, - episode, - callback, - ) - } - - suspend fun invokeRubyMovies( - apiUrl: String, - api: String, - title: String? = null, - year: Int? = null, - season: Int? = null, - episode: Int? = null, - callback: (ExtractorLink) -> Unit, - ) { - invokeIndex( - apiUrl, - api, - title, - year, - season, - episode, - callback, - ) - } - suspend fun invokeShinobiMovies( apiUrl: String, api: String, @@ -2297,26 +1919,6 @@ object SoraExtractor : SoraStream() { ) } - suspend fun invokeVitoenMovies( - apiUrl: String, - api: String, - title: String? = null, - year: Int? = null, - season: Int? = null, - episode: Int? = null, - callback: (ExtractorLink) -> Unit, - ) { - invokeIndex( - apiUrl, - api, - title, - year, - season, - episode, - callback, - ) - } - private suspend fun invokeIndex( apiUrl: String, api: String, @@ -2348,7 +1950,8 @@ object SoraExtractor : SoraStream() { "${apiUrl}search", data = data, headers = passHeaders, - referer = apiUrl + referer = apiUrl, + timeout = 120L ).text else app.post( "${apiUrl}search", data = data, @@ -2356,7 +1959,7 @@ object SoraExtractor : SoraStream() { ).text ) } else { - app.post("${apiUrl}search", requestBody = body, referer = apiUrl).text + app.post("${apiUrl}search", requestBody = body, referer = apiUrl, timeout = 120L).text } val media = if (api in untrimmedIndex) searchIndex( title, @@ -2379,20 +1982,22 @@ object SoraExtractor : SoraStream() { "${apiUrl}id2path", data = pathData, headers = passHeaders, - referer = apiUrl + referer = apiUrl, + timeout = 120L ) } else { app.post( - "${apiUrl}id2path", data = pathData, referer = apiUrl + "${apiUrl}id2path", data = pathData, referer = apiUrl, timeout = 120L ) } } else { - app.post("${apiUrl}id2path", requestBody = pathBody, referer = apiUrl) + app.post("${apiUrl}id2path", requestBody = pathBody, referer = apiUrl, timeout = 120L) }).text.let { path -> if (api in ddomainIndex) { val worker = app.get( "${fixUrl(path, apiUrl).encodeUrl()}?a=view", - referer = if (api in needRefererIndex) apiUrl else "" + referer = if (api in needRefererIndex) apiUrl else "", + timeout = 120L ).document.selectFirst("script:containsData(downloaddomain)")?.data() ?.substringAfter("\"downloaddomain\":\"")?.substringBefore("\",")?.let { "$it/0:" @@ -2403,9 +2008,6 @@ object SoraExtractor : SoraStream() { } }.encodeUrl() -// removed due to rate limit -// if (!app.get(path).isSuccessful) return@apmap null - val size = "%.2f GB".format(bytesToGigaBytes(file.size?.toDouble() ?: return@apmap null)) val quality = getIndexQuality(file.name) @@ -2425,48 +2027,6 @@ object SoraExtractor : SoraStream() { } - suspend fun invokeTgarMovies( - title: String? = null, - year: Int? = null, - season: Int? = null, - episode: Int? = null, - callback: (ExtractorLink) -> Unit, - ) { - val query = getIndexQuery(title, year, season, episode) - - val files = app.get( - "https://api.tgarchive.superfastsearch.zindex.eu.org/search?name=${encode(query)}&page=1", - referer = tgarMovieAPI, - timeout = 600L - ).parsedSafe()?.documents?.filter { media -> - matchingIndex( - media.name, - media.mime_type, - title, - year, - season, - episode, - true - ) && media.name?.contains("XXX") == false - } - - files?.map { file -> - val size = "%.2f GB".format(bytesToGigaBytes(file.size ?: return@map null)) - val quality = getIndexQuality(file.name) - val tags = getIndexQualityTags(file.name) - callback.invoke( - ExtractorLink( - "TgarMovies", - "TgarMovies $tags [$size]", - "https://api.southkoreacdn.workers.dev/telegram/${file._id}", - "$tgarMovieAPI/", - quality, - ) - ) - } - - } - suspend fun invokeGdbotMovies( title: String? = null, year: Int? = null, @@ -2532,15 +2092,10 @@ object SoraExtractor : SoraStream() { "$dahmerMoviesAPI/tvs/${title?.replace(":", " -")}/Season $season/" } - val request = app.get(url) + val request = app.get(url, timeout = 120L) if (!request.isSuccessful) return - - val paths = request.document.select("tr.file").map { - Triple( - it.select("a").text(), - it.select("a").attr("href"), - it.select("size").text(), - ) + val paths = request.document.select("a").map { + it.text() to it.attr("href") }.filter { if (season == null) { it.first.contains(Regex("(?i)(1080p|2160p)")) @@ -2553,11 +2108,10 @@ object SoraExtractor : SoraStream() { paths.map { val quality = getIndexQuality(it.first) val tags = getIndexQualityTags(it.first) - val size = "%.2f GB".format(bytesToGigaBytes(it.third.toDouble())) callback.invoke( ExtractorLink( "DahmerMovies", - "DahmerMovies $tags [$size]", + "DahmerMovies $tags", (url + it.second).encodeUrl(), "", quality, @@ -2568,23 +2122,53 @@ object SoraExtractor : SoraStream() { } - suspend fun invokeGomovies( + suspend fun invoke2embed( + imdbId: String?, + season: Int?, + episode: Int?, + callback: (ExtractorLink) -> Unit + ) { + val server = "https://stream.2embed.cc" + val url = if(season == null) { + "$twoEmbedAPI/embed/$imdbId" + } else { + "$twoEmbedAPI/embedtv/$imdbId&s=$season&e=$episode" + } + + val iframesrc = app.get(url).document.selectFirst("iframe#iframesrc")?.attr("src") + val framesrc = app.get(fixUrl(iframesrc ?: return, twoEmbedAPI)).document.selectFirst("iframe#framesrc")?.attr("src") + val video = app.get(fixUrl(framesrc ?: return, "$server/e/")).text.let { + Regex("file:\\s*\"(.*?m3u8.*?)\"").find(it)?.groupValues?.getOrNull(1) + } + + M3u8Helper.generateM3u8( + "2embed", + video ?: return, + "$server/", + ).forEach(callback) + + } + + suspend fun invokePrimewire( title: String? = null, year: Int? = null, season: Int? = null, episode: Int? = null, callback: (ExtractorLink) -> Unit, ) { - val (seasonSlug, episodeSlug) = getEpisodeSlug(season, episode) + fun String.decrypt(key: String) : List? { + return tryParseJson>(base64Decode(this).decodePrimewireXor(key)) + } + val slug = getEpisodeSlug(season, episode) val query = if (season == null) { title } else { "$title Season $season" } - val doc = app.get("$gomoviesAPI/search/$query").document + val doc = app.get("$primewireAPI/search/$query").document - val media = doc.select("div._gory div.g_yFsxmKnYLvpKDTrdbizeYMWy").map { + val media = doc.select("div.RvnMfoxhgm").map { Triple( it.attr("data-filmName"), it.attr("data-year"), @@ -2613,23 +2197,26 @@ object SoraExtractor : SoraStream() { app.get( fixUrl( media.third, - gomoviesAPI + primewireAPI ) - ).document.selectFirst("div#g_MXOzFGouZrOAUioXjpddqkZK a:contains(Episode $episodeSlug)") + ).document.selectFirst("div#vvqUtffkId a:contains(Episode ${slug.second})") ?.attr("href") } ?: return - val res = app.get(fixUrl(iframe, gomoviesAPI), verify = false) - val match = "var url = '(/user/servers/.*?\\?ep=.*?)';".toRegex().find(res.text) - val serverUrl = match?.groupValues?.get(1) ?: return - val cookies = res.okhttpResponse.headers.getGomoviesCookies() + val res = app.get(fixUrl(iframe, primewireAPI), verify = false) + val serverUrl = "var url = '(/user/servers/.*?\\?ep=.*?)';".toRegex().find(res.text)?.groupValues?.get(1) ?: return + val cookies = res.okhttpResponse.headers.getPrimewireCookies() val url = res.document.select("meta[property=og:url]").attr("content") val headers = mapOf("X-Requested-With" to "XMLHttpRequest") val qualities = intArrayOf(2160, 1440, 1080, 720, 480, 360) - app.get( - "$gomoviesAPI$serverUrl", + val serverRes = app.get( + "$primewireAPI$serverUrl", cookies = cookies, referer = url, headers = headers - ).document.select("ul li").amap { el -> + ) + val unpack = getAndUnpack(serverRes.text) + val key = unpack.substringAfter("(key=").substringBefore(")") + val key2 = unpack.substringAfter("<\"").substringBefore("\".") + serverRes.document.select("ul li").amap { el -> val server = el.attr("data-value") val encryptedData = app.get( "$url?server=$server&_=${System.currentTimeMillis()}", @@ -2637,16 +2224,15 @@ object SoraExtractor : SoraStream() { referer = url, headers = headers ).text - val json = base64Decode(encryptedData).decryptGomoviesJson() - val links = tryParseJson>(json) ?: return@amap + val links = encryptedData.decrypt(key) ?: encryptedData.decrypt(key2) ?: return@amap links.forEach { video -> qualities.filter { it <= video.max.toInt() }.forEach { callback( ExtractorLink( - "Gomovies", - "Gomovies", + "Primewire", + "Primewire", video.src.split("360", limit = 3).joinToString(it.toString()), - "$gomoviesAPI/", + "$primewireAPI/", it, ) ) @@ -2656,12 +2242,32 @@ object SoraExtractor : SoraStream() { } + suspend fun invokeOmega( + tmdbId: Int? = null, + callback: (ExtractorLink) -> Unit, + ) { + app.get("$omegaAPI/v3/movie/sources/$tmdbId") + .parsedSafe()?.sources?.filter { it.label != "2" }?.map { sources -> + sources.sources?.map source@{ source -> + callback.invoke( + ExtractorLink( + "Omega ${sources.label}", + "Omega ${sources.label}", + source.url ?: return@source, + "", + getQualityFromName(source.quality), + INFER_TYPE + ) + ) + } + } + } + suspend fun invokeAsk4Movies( title: String? = null, year: Int? = null, season: Int? = null, episode: Int? = null, - subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit, ) { val query = if (season == null) { @@ -2694,7 +2300,16 @@ object SoraExtractor : SoraStream() { epsDoc.select("ul.group-links-list li:nth-child($episode) a").attr("data-embed-src") } - loadExtractor(iframe, ask4MoviesAPI, subtitleCallback, callback) + val iframeDoc = app.get(iframe, referer = "$ask4MoviesAPI/").text + val script = + Regex("""eval\(function\(p,a,c,k,e,.*\)\)""").findAll(iframeDoc).lastOrNull()?.value + val unpacked = getAndUnpack(script ?: return) + val m3u8 = Regex("file:\\s*\"(.*?m3u8.*?)\"").find(unpacked)?.groupValues?.getOrNull(1) + M3u8Helper.generateM3u8( + "Ask4movie", + m3u8 ?: return, + mainUrl + ).forEach(callback) } @@ -2719,7 +2334,6 @@ object SoraExtractor : SoraStream() { var res = app.get(url) if (res.code == 403) return if (!res.isSuccessful) res = searchWatchOnline(title, season, imdbId, tmdbId) ?: return - val doc = res.document val episodeId = if (season == null) { doc.selectFirst("div.movie__buttons-items a")?.attr("data-watch-list-media-id") @@ -2728,35 +2342,31 @@ object SoraExtractor : SoraStream() { it.select("div.episodes__number").text().equals("Episode $episode", true) }?.attr("data-id-episode") } ?: return - - val videoUrl = if (season == null) { - "$watchOnlineAPI/api/v1/security/movie-access?id_movie=$episodeId" - } else { - "$watchOnlineAPI/api/v1/security/episode-access?id=$episodeId" - } - - val json = app.get(videoUrl, referer = url).parsedSafe() - - json?.streams?.mapKeys { source -> - callback.invoke( - ExtractorLink( - "WatchOnline", - "WatchOnline", - source.value, - "$watchOnlineAPI/", - getQualityFromName(source.key), - true - ) - ) - } - argamap( { - invokeMonster( - res.url.substringAfterLast("/"), episodeId, season, callback - ) + invokeMonster(res.url.substringAfterLast("/"), episodeId, season, callback) }, { + val videoUrl = if (season == null) { + "$watchOnlineAPI/api/v1/security/movie-access?id_movie=$episodeId" + } else { + "$watchOnlineAPI/api/v1/security/episode-access?id=$episodeId" + } + + val json = app.get(videoUrl, referer = url).parsedSafe() + + json?.streams?.mapKeys { source -> + callback.invoke( + ExtractorLink( + "WatchOnline", + "WatchOnline", + source.value, + "$watchOnlineAPI/", + getQualityFromName(source.key), + true + ) + ) + } val subtitles = json?.subtitles as ArrayList> subtitles.map { sub -> subtitleCallback.invoke( @@ -2766,7 +2376,8 @@ object SoraExtractor : SoraStream() { ) ) } - }) + } + ) } @@ -2776,19 +2387,17 @@ object SoraExtractor : SoraStream() { season: Int? = null, callback: (ExtractorLink) -> Unit, ) { - val monsterMainUrl = "https://mobirs.monster" - val playSlug = if (season == null) { - "movies/play/$urlSlug" + val monsterMainUrl = "https://lookmovie.foundation" + val viewSlug = if (season == null) { + "movies/view/$urlSlug" } else { - "shows/play/$urlSlug" + "shows/view/$urlSlug" } - val sid = "9k9iupt5sebbnfajrc6ti3ht7l" - val sec = "1974bc4a902c4d69fcbab261dcec69094a9b8164" - val url = - "$monsterMainUrl/$playSlug?mid=1&sid=$sid&sec=$sec&t=${System.currentTimeMillis()}" - val res = app.get(url).document - val script = res.selectFirst("script:containsData(window['show_storage'])")?.data() - val hash = Regex("hash:\\s*['\"](\\S+)['\"],").find(script ?: return)?.groupValues?.get(1) + val streamUrl = app.get("$monsterMainUrl/$viewSlug").document.select("a.round-button:first-child").attr("href") + val res = app.get(streamUrl).document + val script = res.selectFirst("script:containsData(hash:)")?.data() + val hash = + Regex("hash:\\s*['\"](\\S+)['\"],").find(script ?: return)?.groupValues?.get(1) val expires = Regex("expires:\\s*(\\d+),").find(script)?.groupValues?.get(1) val videoUrl = if (season == null) { @@ -2797,7 +2406,7 @@ object SoraExtractor : SoraStream() { "$monsterMainUrl/api/v1/security/episode-access?id_episode=$episodeId&hash=$hash&expires=$expires" } - app.get(videoUrl, referer = url) + app.get(videoUrl, referer = streamUrl) .parsedSafe()?.streams?.mapKeys { source -> callback.invoke( ExtractorLink( @@ -2825,157 +2434,16 @@ object SoraExtractor : SoraStream() { "$nineTvAPI/tv/$tmdbId-$season-$episode" } - val iframe = app.get(url, referer = "https://pressplay.top/").document.selectFirst("iframe")?.attr("src") ?: return + val iframe = app.get(url, referer = "https://pressplay.top/").document.selectFirst("iframe") + ?.attr("src") ?: return loadExtractor(iframe, "$nineTvAPI/", subtitleCallback, callback) } - suspend fun invokePutlocker( - title: String? = null, - year: Int? = null, - season: Int? = null, - episode: Int? = null, - callback: (ExtractorLink) -> Unit, - ) { - val query = if (season == null) { - title - } else { - "$title - season $season" - } - - val res = app.get("$putlockerAPI/movie/search/$query").document - val scripData = res.select("div.movies-list div.ml-item").map { - it.selectFirst("h2")?.text() to it.selectFirst("a")?.attr("href") - } - val script = if (scripData.size == 1) { - scripData.first() - } else { - scripData.find { - if (season == null) { - it.first.equals(title, true) || (it.first?.contains( - "$title", true - ) == true && it.first?.contains("$year") == true) - } else { - it.first?.contains("$title", true) == true && it.first?.contains( - "Season $season", true - ) == true - } - } - } - - val id = fixUrl(script?.second ?: return).split("-").lastOrNull()?.removeSuffix("/") - val iframe = app.get("$putlockerAPI/ajax/movie_episodes/$id") - .parsedSafe()?.html?.let { Jsoup.parse(it) }?.let { server -> - if (season == null) { - server.select("div.les-content a").map { - it.attr("data-id") to it.attr("data-server") - } - } else { - server.select("div.les-content a").map { it } - .filter { it.text().contains("Episode $episode", true) }.map { - it.attr("data-id") to it.attr("data-server") - } - } - } - - iframe?.apmap { - delay(3000) - val embedUrl = app.get("$putlockerAPI/ajax/movie_embed/${it.first}") - .parsedSafe()?.src ?: return@apmap null - val sources = extractPutlockerSources(embedUrl)?.parsedSafe() - - argamap( - { - sources?.callback(embedUrl, "Server ${it.second}", callback) - }, - { - if (!sources?.backupLink.isNullOrBlank()) { - extractPutlockerSources(sources?.backupLink)?.parsedSafe() - ?.callback( - embedUrl, "Backup ${it.second}", callback - ) - } else { - return@argamap - } - }, - ) - - } - - } - - suspend fun invokeShivamhw( - title: String? = null, - year: Int? = null, - season: Int? = null, - episode: Int? = null, - callback: (ExtractorLink) -> Unit, - ) { - val (seasonSlug, episodeSlug) = getEpisodeSlug(season, episode) - val url = if (season == null) { - "$shivamhwAPI/search?search_box=$title&release_year=$year" - } else { - "$shivamhwAPI/api/series_search?search_box=$title&sess_nm=$seasonSlug&epi_nm=$episodeSlug" - } - - val res = app.get(url) - - val media = if (season == null) { - res.document.select("table.rwd-table tr").map { - Triple( - it.select("td[data-th=File Name]").text(), - it.select("td[data-th=Size]").text(), - it.selectFirst("div.download_button.pls_wait > a")?.attr("href") - ) - } - } else { - tryParseJson>(res.text)?.map { - Triple( - it.name, - it.size, - it.stream_link, - ) - } - } - - media?.filter { - matchingIndex( - it.first, - null, - title, - year, - season, - episode, - false - ) - }?.sortedByDescending { - it.second.getFileSize() - }?.apmap { source -> - val quality = getIndexQuality(source.first) - val tags = getIndexQualityTags(source.first) - val video = source.third - if (!app.get( - video ?: return@apmap, - referer = "$shivamhwAPI/" - ).isSuccessful - ) return@apmap - callback.invoke( - ExtractorLink( - "Shivamhw", - "Shivamhw $tags [${source.second}]", - video, - "$shivamhwAPI/", - quality, - ) - ) - } - } - suspend fun invokeCryMovies( imdbId: String? = null, title: String? = null, year: Int? = null, - season: Int? = null, episode: Int? = null, callback: (ExtractorLink) -> Unit ) { @@ -2986,7 +2454,7 @@ object SoraExtractor : SoraStream() { null, title, year, - season, + null, episode, false ) @@ -2996,7 +2464,11 @@ object SoraExtractor : SoraStream() { val size = getIndexSize(stream.title) val headers = stream.behaviorHints?.proxyHeaders?.request ?: mapOf() - if(!app.get(stream.url ?: return@apmap, headers = headers).isSuccessful) return@apmap + if (!app.get( + stream.url ?: return@apmap, + headers = headers + ).isSuccessful + ) return@apmap callback.invoke( ExtractorLink( @@ -3019,8 +2491,9 @@ object SoraExtractor : SoraStream() { callback: (ExtractorLink) -> Unit ) { val referer = "https://2now.tv/" - val (seasonSlug, episodeSlug) = getEpisodeSlug(season, episode) - val url = if(season == null) "$nowTvAPI/$tmdbId.mp4" else "$nowTvAPI/tv/$tmdbId/s${season}e${episodeSlug}.mp4" + val slug = getEpisodeSlug(season, episode) + val url = + if (season == null) "$nowTvAPI/$tmdbId.mp4" else "$nowTvAPI/tv/$tmdbId/s${season}e${slug.second}.mp4" if (!app.get(url, referer = referer).isSuccessful) return callback.invoke( ExtractorLink( @@ -3062,18 +2535,39 @@ object SoraExtractor : SoraStream() { season: Int? = null, episode: Int? = null, callback: (ExtractorLink) -> Unit, + ) { + invokeHindi(navyAPI, navyAPI, imdbId, season, episode, callback) + } + + suspend fun invokeMoment( + imdbId: String? = null, + season: Int? = null, + episode: Int? = null, + callback: (ExtractorLink) -> Unit, + ) { + invokeHindi(momentAPI, "https://hdmovies4u.green", imdbId, season, episode, callback) + } + + private suspend fun invokeHindi( + host: String? = null, + referer: String? = null, + imdbId: String? = null, + season: Int? = null, + episode: Int? = null, + callback: (ExtractorLink) -> Unit, ) { val res = app.get( - "$navyAPI/play/$imdbId", - referer = "$navyAPI/" + "$host/play/$imdbId", + referer = "$referer/" ).document.selectFirst("script:containsData(player =)")?.data()?.substringAfter("{") ?.substringBefore(";")?.substringBefore(")") val json = tryParseJson("{${res ?: return}") val headers = mapOf( "X-CSRF-TOKEN" to "${json?.key}" ) + val serverRes = app.get( - fixUrl(json?.file ?: return, navyAPI), headers = headers, referer = "$navyAPI/" + fixUrl(json?.file ?: return, navyAPI), headers = headers, referer = "$referer/" ).text.replace(Regex(""",\s*\[]"""), "") val server = tryParseJson>(serverRes).let { server -> if (season == null) { @@ -3088,15 +2582,15 @@ object SoraExtractor : SoraStream() { } val path = app.post( - "${navyAPI}/playlist/${server ?: return}.txt", + "${host}/playlist/${server ?: return}.txt", headers = headers, - referer = "$navyAPI/" + referer = "$referer/" ).text M3u8Helper.generateM3u8( - "Navy", + if (host == navyAPI) "Navy" else "Moment", path, - "${navyAPI}/" + "${referer}/" ).forEach(callback) } @@ -3137,7 +2631,10 @@ object SoraExtractor : SoraStream() { ) ).parsedSafe()?.value - val script = app.get(server ?: return, referer = "$emoviesAPI/").document.selectFirst("script:containsData(sources:)")?.data() ?: return + val script = app.get( + server ?: return, + referer = "$emoviesAPI/" + ).document.selectFirst("script:containsData(sources:)")?.data() ?: return val sources = Regex("sources:\\s*\\[(.*)],").find(script)?.groupValues?.get(1)?.let { tryParseJson>("[$it]") } @@ -3213,6 +2710,84 @@ object SoraExtractor : SoraStream() { } + suspend fun invokeSusflix( + tmdbId: Int? = null, + season: Int? = null, + episode: Int? = null, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit, + ) { + val url = if(season == null) { + "$susflixAPI/view/movie/$tmdbId" + } else { + "$susflixAPI/view/tv/$tmdbId/$season/$episode" + } + + val res = app.get(url,cookies = mapOf( + "session" to "eyJfZnJlc2giOmZhbHNlLCJwaG9uZV9udW1iZXIiOiJzdXNoZXg5OCJ9.ZO6CsA.XUs6Y5gna8ExAUX55-myMi1QpYU" + )).text.substringAfter("response = {").substringBefore("};").replace("\'", "\"") + + val sources = tryParseJson("{$res}") + sources?.qualities?.map { source -> + callback.invoke( + ExtractorLink( + "Susflix", + "Susflix", + source.path ?: return@map, + "$susflixAPI/", + getQualityFromName(source.quality) + ) + ) + } + + sources?.srtfiles?.map { sub -> + subtitleCallback.invoke( + SubtitleFile( + sub.caption ?: return@map, + sub.url ?: return@map, + ) + ) + } + + } + + suspend fun invokeJump1( + tmdbId: Int? = null, + tvdbId: Int? = null, + title: String? = null, + year: Int? = null, + season: Int? = null, + episode: Int? = null, + callback: (ExtractorLink) -> Unit, + ) { + val referer = "https://jump1.net/" + val res = if(season == null) { + val body = """{"filters":[{"type":"slug","args":{"slugs":["${title.createSlug()}-$year"]}}],"sort":"addedRecent","skip":0,"limit":100}""".toRequestBody(RequestBodyTypes.JSON.toMediaTypeOrNull()) + app.post("$jump1API/api/movies", requestBody = body, referer = referer) + } else { + app.get("$jump1API/api/shows/$tvdbId/seasons", referer = referer) + }.text + + val source = if(season == null) { + tryParseJson(res)?.movies?.find { it.id == tmdbId }?.videoId + } else { + val jumpSeason = tryParseJson>(res)?.find { it.seasonNumber == season }?.id + val seasonRes = app.get("$jump1API/api/shows/seasons/${jumpSeason ?: return}/episodes", referer = referer) + tryParseJson>(seasonRes.text)?.find { it.episodeNumber == episode }?.videoId + } + + callback.invoke( + ExtractorLink( + "Jump1", + "Jump1", + "$jump1API/hls/${source ?: return}/master.m3u8?ts=${APIHolder.unixTimeMS}", + referer, + Qualities.P1080.value, + true + ) + ) + } + } diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraParser.kt b/SoraStream/src/main/kotlin/com/hexated/SoraParser.kt index 9646b846..fe32debf 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraParser.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraParser.kt @@ -9,12 +9,6 @@ data class FDMovieIFrame( val type: String, ) -data class BaymoviesConfig( - val country: String, - val downloadTime: String, - val workers: List -) - data class AniIds( var id: Int? = null, var idMal: Int? = null @@ -37,26 +31,7 @@ data class AniSearch( @JsonProperty("data") var data: AniData? = AniData() ) -data class Tmdb2Anilist( - @JsonProperty("tmdb_id") val tmdb_id: String? = null, - @JsonProperty("anilist_id") val anilist_id: String? = null, - @JsonProperty("mal_id") val mal_id: String? = null, -) - -data class Movie123Media( - @JsonProperty("url") val url: String? = null, -) - -data class Movie123Data( - @JsonProperty("t") val t: String? = null, - @JsonProperty("s") val s: String? = null, -) - -data class Movie123Search( - @JsonProperty("data") val data: ArrayList? = arrayListOf(), -) - -data class GomoviesSources( +data class PrimewireSources( @JsonProperty("src") val src: String, @JsonProperty("file") val file: String? = null, @JsonProperty("label") val label: Int? = null, @@ -72,25 +47,10 @@ data class MoviesbayValues( @JsonProperty("values") val values: List>? = arrayListOf(), ) -data class HdMovieBoxTracks( - @JsonProperty("label") val label: String? = null, - @JsonProperty("file") val file: String? = null, -) - -data class HdMovieBoxSource( - @JsonProperty("videoUrl") val videoUrl: String? = null, - @JsonProperty("videoServer") val videoServer: String? = null, - @JsonProperty("videoDisk") val videoDisk: Any? = null, - @JsonProperty("tracks") val tracks: ArrayList? = arrayListOf(), -) - -data class HdMovieBoxIframe( - @JsonProperty("api_iframe") val apiIframe: String? = null, -) - data class ResponseHash( @JsonProperty("embed_url") val embed_url: String, - @JsonProperty("type") val type: String?, + @JsonProperty("key") val key: String? = null, + @JsonProperty("type") val type: String? = null, ) data class KisskhSources( @@ -98,6 +58,20 @@ data class KisskhSources( @JsonProperty("ThirdParty") val thirdParty: String?, ) +data class OmegaSource( + @JsonProperty("quality") val quality: String? = null, + @JsonProperty("url") val url: String? = null, +) + +data class OmegaSources( + @JsonProperty("label") val label: String? = null, + @JsonProperty("sources") val sources: ArrayList? = arrayListOf(), +) + +data class OmegaResponse( + @JsonProperty("sources") val sources: ArrayList? = arrayListOf(), +) + data class KisskhSubtitle( @JsonProperty("src") val src: String?, @JsonProperty("label") val label: String?, @@ -112,11 +86,41 @@ data class KisskhDetail( @JsonProperty("episodes") val episodes: ArrayList? = arrayListOf(), ) +data class SusflixSrtfiles( + @JsonProperty("caption") val caption: String? = null, + @JsonProperty("url") val url: String? = null, +) + +data class SusflixQualities( + @JsonProperty("path") val path: String? = null, + @JsonProperty("quality") val quality: String? = null, +) + +data class SusflixSources( + @JsonProperty("Qualities") val qualities: ArrayList? = arrayListOf(), + @JsonProperty("Srtfiles") val srtfiles: ArrayList? = arrayListOf(), +) + data class KisskhResults( @JsonProperty("id") val id: Int?, @JsonProperty("title") val title: String?, ) +data class Jump1Episodes( + @JsonProperty("id") val id: Any? = null, + @JsonProperty("episodeNumber") val episodeNumber: Int? = null, + @JsonProperty("videoId") val videoId: String? = null, +) + +data class Jump1Season( + @JsonProperty("seasonNumber") val seasonNumber: Int? = null, + @JsonProperty("id") val id: String? = null, +) + +data class Jump1Movies( + @JsonProperty("movies") val movies: ArrayList? = arrayListOf(), +) + data class EpisodesFwatayako( @JsonProperty("id") val id: String? = null, @JsonProperty("file") val file: String? = null, @@ -202,53 +206,18 @@ data class IndexSearch( @JsonProperty("data") val data: IndexData? = null, ) -data class TgarMedia( - @JsonProperty("_id") val _id: Int? = null, +data class JikanExternal( @JsonProperty("name") val name: String? = null, - @JsonProperty("size") val size: Double? = null, - @JsonProperty("file_unique_id") val file_unique_id: String? = null, - @JsonProperty("mime_type") val mime_type: String? = null, + @JsonProperty("url") val url: String? = null, ) -data class TgarData( - @JsonProperty("documents") val documents: ArrayList? = arrayListOf(), +data class JikanData( + @JsonProperty("title") val title: String? = null, + @JsonProperty("external") val external: ArrayList? = arrayListOf(), ) -data class SorastreamResponse( - @JsonProperty("data") val data: SorastreamVideos? = null, -) - -data class SorastreamVideos( - @JsonProperty("mediaUrl") val mediaUrl: String? = null, - @JsonProperty("currentDefinition") val currentDefinition: String? = null, -) - -data class BiliBiliEpisodes( - @JsonProperty("id") val id: Int? = null, - @JsonProperty("sourceId") val sourceId: String? = null, - @JsonProperty("sourceEpisodeId") val sourceEpisodeId: String? = null, - @JsonProperty("sourceMediaId") val sourceMediaId: String? = null, - @JsonProperty("episodeNumber") val episodeNumber: Int? = null, -) - -data class BiliBiliDetails( - @JsonProperty("episodes") val episodes: ArrayList? = arrayListOf(), -) - -data class BiliBiliSubtitles( - @JsonProperty("file") val file: String? = null, - @JsonProperty("lang") val lang: String? = null, - @JsonProperty("language") val language: String? = null, -) - -data class BiliBiliSources( - @JsonProperty("file") val file: String? = null, - @JsonProperty("type") val type: String? = null, -) - -data class BiliBiliSourcesResponse( - @JsonProperty("sources") val sources: ArrayList? = arrayListOf(), - @JsonProperty("subtitles") val subtitles: ArrayList? = arrayListOf(), +data class JikanResponse( + @JsonProperty("data") val data: JikanData? = null, ) data class WatchOnlineItems( @@ -266,33 +235,6 @@ data class WatchOnlineResponse( @JsonProperty("subtitles") val subtitles: Any? = null, ) -data class PutlockerEpisodes( - @JsonProperty("html") val html: String? = null, -) - -data class PutlockerEmbed( - @JsonProperty("src") val src: String? = null, -) - -data class PutlockerSources( - @JsonProperty("file") val file: String, - @JsonProperty("label") val label: String? = null, - @JsonProperty("type") val type: String? = null, -) - -data class PutlockerResponses( - @JsonProperty("sources") val sources: ArrayList? = arrayListOf(), - @JsonProperty("backupLink") val backupLink: String? = null, -) - -data class ShivamhwSources( - @JsonProperty("id") val id: String? = null, - @JsonProperty("stream_link") val stream_link: String? = null, - @JsonProperty("process_link") val process_link: String? = null, - @JsonProperty("name") val name: String, - @JsonProperty("size") val size: String, -) - data class CryMoviesProxyHeaders( @JsonProperty("request") val request: Map?, ) @@ -336,16 +278,12 @@ data class VizcloudSources( @JsonProperty("file") val file: String? = null, ) -data class VizcloudMedia( +data class VizcloudResult( @JsonProperty("sources") val sources: ArrayList? = arrayListOf(), ) -data class VizcloudData( - @JsonProperty("media") val media: VizcloudMedia? = null, -) - data class VizcloudResponses( - @JsonProperty("data") val data: VizcloudData? = null, + @JsonProperty("result") val result: VizcloudResult? = null, ) data class AnilistExternalLinks( @@ -405,15 +343,15 @@ data class CrunchyrollSourcesResponses( @JsonProperty("meta") val meta: CrunchyrollMeta? = null, ) -data class MALSyncPages( +data class MALSyncSites( @JsonProperty("Zoro") val zoro: HashMap>? = hashMapOf(), ) data class MALSyncResponses( - @JsonProperty("Pages") val pages: MALSyncPages? = null, + @JsonProperty("Sites") val sites: MALSyncSites? = null, ) -data class ZoroResponses( +data class AniwatchResponses( @JsonProperty("html") val html: String? = null, @JsonProperty("link") val link: String? = null, ) diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt b/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt index f0916e0d..0e01c239 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraStream.kt @@ -1,62 +1,58 @@ package com.hexated import com.fasterxml.jackson.annotation.JsonProperty +import com.hexated.SoraExtractor.invoke2embed import com.hexated.SoraExtractor.invokeAnimes import com.hexated.SoraExtractor.invokeAsk4Movies -import com.hexated.SoraExtractor.invokeBlackmovies import com.hexated.SoraExtractor.invokeBollyMaza -import com.hexated.SoraExtractor.invokeCodexmovies import com.hexated.SoraExtractor.invokeCryMovies import com.hexated.SoraExtractor.invokeDbgo import com.hexated.SoraExtractor.invokeFilmxy -import com.hexated.SoraExtractor.invokeHDMovieBox import com.hexated.SoraExtractor.invokeIdlix import com.hexated.SoraExtractor.invokeKimcartoon import com.hexated.SoraExtractor.invokeMovieHab -import com.hexated.SoraExtractor.invokeNoverse -import com.hexated.SoraExtractor.invokeSeries9 import com.hexated.SoraExtractor.invokeVidSrc -import com.hexated.SoraExtractor.invokeXmovies import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer import com.lagradost.cloudstream3.metaproviders.TmdbProvider import com.hexated.SoraExtractor.invokeDahmerMovies +import com.hexated.SoraExtractor.invokeDoomovies +import com.hexated.SoraExtractor.invokeDramaday import com.hexated.SoraExtractor.invokeDreamfilm -import com.hexated.SoraExtractor.invokeEdithxmovies import com.hexated.SoraExtractor.invokeFDMovies import com.hexated.SoraExtractor.invokeFlixon -import com.hexated.SoraExtractor.invokeFmovies import com.hexated.SoraExtractor.invokeFwatayako import com.hexated.SoraExtractor.invokeGMovies -import com.hexated.SoraExtractor.invokeGdbotMovies import com.hexated.SoraExtractor.invokeGoku -import com.hexated.SoraExtractor.invokeGomovies -import com.hexated.SoraExtractor.invokeJmdkhMovies import com.hexated.SoraExtractor.invokeKisskh import com.hexated.SoraExtractor.invokeLing import com.hexated.SoraExtractor.invokeM4uhd -import com.hexated.SoraExtractor.invokeMovie123Net import com.hexated.SoraExtractor.invokeMoviesbay import com.hexated.SoraExtractor.invokeMoviezAdd import com.hexated.SoraExtractor.invokeNavy import com.hexated.SoraExtractor.invokeNinetv import com.hexated.SoraExtractor.invokeNowTv -import com.hexated.SoraExtractor.invokePutlocker import com.hexated.SoraExtractor.invokeRStream import com.hexated.SoraExtractor.invokeRidomovies -import com.hexated.SoraExtractor.invokeRubyMovies import com.hexated.SoraExtractor.invokeShinobiMovies -import com.hexated.SoraExtractor.invokeShivamhw import com.hexated.SoraExtractor.invokeSmashyStream import com.hexated.SoraExtractor.invokeDumpStream import com.hexated.SoraExtractor.invokeEmovies import com.hexated.SoraExtractor.invokeFourCartoon +import com.hexated.SoraExtractor.invokeJump1 +import com.hexated.SoraExtractor.invokeMoment +import com.hexated.SoraExtractor.invokeMultimovies +import com.hexated.SoraExtractor.invokeNetmovies +import com.hexated.SoraExtractor.invokeOmega import com.hexated.SoraExtractor.invokePobmovies +import com.hexated.SoraExtractor.invokePrimewire import com.hexated.SoraExtractor.invokeTvMovies import com.hexated.SoraExtractor.invokeUhdmovies -import com.hexated.SoraExtractor.invokeVitoenMovies +import com.hexated.SoraExtractor.invokeVidsrcto import com.hexated.SoraExtractor.invokeWatchOnline import com.hexated.SoraExtractor.invokeWatchsomuch +import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId +import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId import com.lagradost.cloudstream3.extractors.VidSrcExtractor import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson @@ -83,48 +79,41 @@ open class SoraStream : TmdbProvider() { const val anilistAPI = "https://graphql.anilist.co" const val malsyncAPI = "https://api.malsync.moe" const val consumetHelper = "https://api.consumet.org/anime/9anime/helper" + const val jikanAPI = "https://api.jikan.moe/v4" val apiKey = base64DecodeAPI("ZTM=NTg=MjM=MjM=ODc=MzI=OGQ=MmE=Nzk=Nzk=ZjI=NTA=NDY=NDA=MzA=YjA=") // PLEASE DON'T STEAL /** ALL SOURCES */ + const val twoEmbedAPI = "https://www.2embed.cc" const val vidSrcAPI = "https://v2.vidsrc.me" const val dbgoAPI = "https://dbgo.fun" const val movieHabAPI = "https://moviehab.com" - const val hdMovieBoxAPI = "https://hdmoviebox.net" const val dreamfilmAPI = "https://dreamfilmsw.net" - const val series9API = "https://series9.sh" - const val idlixAPI = "https://idlixian.com" + const val idlixAPI = "https://tv.idlixplus.net" const val noverseAPI = "https://www.nollyverse.com" - const val uniqueStreamAPI = "https://uniquestream.net" const val filmxyAPI = "https://www.filmxy.vip" const val kimcartoonAPI = "https://kimcartoon.li" - const val xMovieAPI = "https://xemovies.to" - const val zoroAPI = "https://kaido.to" + const val aniwatchAPI = "https://aniwatch.to" const val crunchyrollAPI = "https://beta-api.crunchyroll.com" const val kissKhAPI = "https://kisskh.co" const val lingAPI = "https://ling-online.net" - const val uhdmoviesAPI = "https://uhdmovies.life" + const val uhdmoviesAPI = "https://uhdmovies.wiki" const val fwatayakoAPI = "https://5100.svetacdn.in" const val gMoviesAPI = "https://gdrivemovies.xyz" const val fdMoviesAPI = "https://freedrivemovie.lol" - const val m4uhdAPI = "https://m4uhd.tv" + const val m4uhdAPI = "https://ww2.m4ufree.com" const val tvMoviesAPI = "https://www.tvseriesnmovies.com" const val moviezAddAPI = "https://ww2.moviezaddiction.click" const val bollyMazaAPI = "https://m.bollymaza.click" const val moviesbayAPI = "https://moviesbay.live" const val rStreamAPI = "https://remotestre.am" const val flixonAPI = "https://flixon.lol" - const val animeKaizokuAPI = "https://animekaizoku.com" - const val movie123NetAPI = "https://ww8.0123movie.net" const val smashyStreamAPI = "https://embed.smashystream.com" const val watchSomuchAPI = "https://watchsomuch.tv" // sub only - val gomoviesAPI = base64DecodeAPI("bQ==Y28=ZS4=aW4=bmw=LW8=ZXM=dmk=bW8=Z28=Ly8=czo=dHA=aHQ=") - const val ask4MoviesAPI = "https://ask4movie.net" - const val biliBiliAPI = "https://api-vn.otakuz.live/server" + const val ask4MoviesAPI = "https://ask4movie.nl" const val watchOnlineAPI = "https://watchonline.ag" - const val nineTvAPI = "https://api.9animetv.live" - const val putlockerAPI = "https://ww7.putlocker.vip" + const val nineTvAPI = "https://moviesapi.club" const val fmoviesAPI = "https://fmovies.to" const val nowTvAPI = "https://myfilestorage.xyz" const val gokuAPI = "https://goku.sx" @@ -133,32 +122,24 @@ open class SoraStream : TmdbProvider() { const val emoviesAPI = "https://emovies.si" const val pobmoviesAPI = "https://pobmovies.cam" const val fourCartoonAPI = "https://4cartoon.net" + const val multimoviesAPI = "https://multi-movies.xyz" + const val netmoviesAPI = "https://web.netmovies.to" + const val momentAPI = "https://moment-explanation-i-244.site" + const val doomoviesAPI = "https://doomovies.net" + const val primewireAPI = "https://real-primewire.club" + const val vidsrctoAPI = "https://vidsrc.to" + const val dramadayAPI = "https://dramaday.me" + const val animetoshoAPI = "https://animetosho.org" + const val susflixAPI = "https://susflix.tv" + const val jump1API = "https://ca.jump1.net" + const val omegaAPI = "https://prod.omega.themoviearchive.site" // INDEX SITE - const val blackMoviesAPI = "https://dl.blacklistedbois.workers.dev/0:" - const val codexMoviesAPI = "https://packs.codexcloudx.tech/0:" - const val edithxMoviesAPI = "https://index.edithx.ga/0:" const val dahmerMoviesAPI = "https://edytjedhgmdhm.abfhaqrhbnf.workers.dev" - const val jmdkhMovieAPI = "https://tg.jmdkh.eu.org/0:" - const val rubyMovieAPI = "https://upload.rubyshare111.workers.dev/0:" const val shinobiMovieAPI = "https://home.shinobicloud.cf/0:" - const val vitoenMovieAPI = "https://openmatte.vitoencodes.workers.dev/0:" - const val shivamhwAPI = "https://foogle.shivamhw.me" val cryMoviesAPI = base64DecodeAPI("ZXY=LmQ=cnM=a2U=b3I=Lnc=ZXI=ZGQ=bGE=cy0=b2I=YWM=Lmo=YWw=aW4=LWY=cm4=Ym8=cmU=Ly8=czo=dHA=aHQ=") - // DEAD SITE - const val rinzryMoviesAPI = "https://rinzry.stream/0:" - const val chillmovies0API = "https://chill.aicirou.workers.dev/0:" - const val chillmovies1API = "https://chill.aicirou.workers.dev/1:" - const val gamMoviesAPI = "https://drive.gamick.workers.dev/0:" - const val jsMoviesAPI = "https://jsupload.jnsbot.workers.dev/0:" - const val xtremeMoviesAPI = "https://kartik19.xtrememirror0.workers.dev/0:" - const val tgarMovieAPI = "https://tgarchive.eu.org" - const val baymoviesAPI = "https://opengatewayindex.pages.dev" - const val papaonMovies1API = "https://m.papaonwork.workers.dev/0:" - const val papaonMovies2API = "https://m.papaonwork.workers.dev/1:" - fun getType(t: String?): TvType { return when (t) { "movie" -> TvType.Movie @@ -267,7 +248,9 @@ open class SoraStream : TmdbProvider() { val year = releaseDate?.split("-")?.first()?.toIntOrNull() val rating = res.vote_average.toString().toRatingInt() val genres = res.genres?.mapNotNull { it.name } - val isAnime = genres?.contains("Animation") == true && (res.original_language == "zh" || res.original_language == "ja") + val isAnime = + genres?.contains("Animation") == true && (res.original_language == "zh" || res.original_language == "ja") + val isAsian = !isAnime && (res.original_language == "zh" || res.original_language == "ko") val keywords = res.keywords?.results?.mapNotNull { it.name }.orEmpty() .ifEmpty { res.keywords?.keywords?.mapNotNull { it.name } } @@ -295,6 +278,7 @@ open class SoraStream : TmdbProvider() { LinkData( data.id, res.external_ids?.imdb_id, + res.external_ids?.tvdb_id, data.type, eps.seasonNumber, eps.episodeNumber, @@ -308,8 +292,9 @@ open class SoraStream : TmdbProvider() { jpTitle = res.alternative_titles?.results?.find { it.iso_3166_1 == "JP" }?.title, date = season.airDate, airedDate = res.releaseDate ?: res.firstAirDate, + isAsian = isAsian, ).toJson(), - name = eps.name + if(isUpcoming(eps.airDate)) " - [UPCOMING]" else "", + name = eps.name + if (isUpcoming(eps.airDate)) " - [UPCOMING]" else "", season = eps.seasonNumber, episode = eps.episodeNumber, posterUrl = getImageUrl(eps.stillPath), @@ -336,6 +321,8 @@ open class SoraStream : TmdbProvider() { this.recommendations = recommendations this.actors = actors addTrailer(trailer) + addTMDbId(data.id.toString()) + addImdbId(res.external_ids?.imdb_id) } } else { newMovieLoadResponse( @@ -345,6 +332,7 @@ open class SoraStream : TmdbProvider() { LinkData( data.id, res.external_ids?.imdb_id, + res.external_ids?.tvdb_id, data.type, title = title, year = year, @@ -352,6 +340,7 @@ open class SoraStream : TmdbProvider() { isAnime = isAnime, jpTitle = res.alternative_titles?.results?.find { it.iso_3166_1 == "JP" }?.title, airedDate = res.releaseDate ?: res.firstAirDate, + isAsian = isAsian, ).toJson(), ) { this.posterUrl = poster @@ -365,6 +354,8 @@ open class SoraStream : TmdbProvider() { this.recommendations = recommendations this.actors = actors addTrailer(trailer) + addTMDbId(data.id.toString()) + addImdbId(res.external_ids?.imdb_id) } } } @@ -395,7 +386,15 @@ open class SoraStream : TmdbProvider() { ) }, { - invokeGoku(res.title, res.year, res.season, res.lastSeason, res.episode, subtitleCallback, callback) + invokeGoku( + res.title, + res.year, + res.season, + res.lastSeason, + res.episode, + subtitleCallback, + callback + ) }, { invokeVidSrc(res.id, res.season, res.episode, subtitleCallback, callback) @@ -406,15 +405,6 @@ open class SoraStream : TmdbProvider() { { invokeMovieHab(res.imdbId, res.season, res.episode, subtitleCallback, callback) }, -// { -// invokeDatabaseGdrive( -// res.imdbId, -// res.season, -// res.episode, -// subtitleCallback, -// callback -// ) -// }, { if (res.isAnime) invokeAnimes( res.title, @@ -427,25 +417,6 @@ open class SoraStream : TmdbProvider() { callback ) }, -// { -// if (res.season != null && res.isAnime) invokeCrunchyroll( -// res.title, -// res.epsTitle, -// res.season, -// res.episode, -// subtitleCallback, -// callback -// ) -// }, - { - if (!res.isAnime) invokeHDMovieBox( - res.title, - res.season, - res.episode, - subtitleCallback, - callback - ) - }, { if (!res.isAnime) invokeDreamfilm( res.title, @@ -455,16 +426,6 @@ open class SoraStream : TmdbProvider() { callback ) }, - { - invokeSeries9( - res.title, - res.year, - res.season, - res.episode, - subtitleCallback, - callback - ) - }, { invokeIdlix( res.title, @@ -475,18 +436,8 @@ open class SoraStream : TmdbProvider() { callback ) }, - { - invokeNoverse(res.title, res.season, res.episode, callback) - }, // { -// invokeUniqueStream( -// res.title, -// res.year, -// res.season, -// res.episode, -// subtitleCallback, -// callback -// ) +// invokeNoverse(res.title, res.season, res.episode, callback) // }, { if (!res.isAnime) invokeFilmxy( @@ -498,22 +449,8 @@ open class SoraStream : TmdbProvider() { ) }, { - if(!res.isAnime) invokeKimcartoon(res.title, res.season, res.episode, subtitleCallback, callback) - }, -// { -// invokeXmovies( -// res.title, -// res.year, -// res.season, -// res.episode, -// subtitleCallback, -// callback -// ) -// }, - { - if (!res.isAnime) invokeFmovies( + if (!res.isAnime) invokeKimcartoon( res.title, - res.airedYear ?: res.year, res.season, res.episode, subtitleCallback, @@ -521,7 +458,24 @@ open class SoraStream : TmdbProvider() { ) }, { - invokeKisskh(res.title, res.season, res.episode, res.isAnime, res.lastSeason, subtitleCallback, callback) + if (!res.isAnime) invokeVidsrcto( + res.imdbId, + res.season, + res.episode, + subtitleCallback, + callback + ) + }, + { + invokeKisskh( + res.title, + res.season, + res.episode, + res.isAnime, + res.lastSeason, + subtitleCallback, + callback + ) }, { invokeLing( @@ -564,18 +518,15 @@ open class SoraStream : TmdbProvider() { ) }, { - invokeM4uhd( + if (!res.isAnime) invokeM4uhd( res.title, - res.year, + res.airedYear ?: res.year, res.season, res.episode, subtitleCallback, callback ) }, - { - invokePutlocker(res.title, res.year, res.season, res.episode, callback) - }, { invokeTvMovies(res.title, res.season, res.episode, callback) }, @@ -614,9 +565,6 @@ open class SoraStream : TmdbProvider() { { invokeFlixon(res.id, res.imdbId, res.season, res.episode, callback) }, - { - invokeMovie123Net(res.title, res.season, res.episode, subtitleCallback, callback) - }, { invokeSmashyStream( res.imdbId, @@ -644,52 +592,6 @@ open class SoraStream : TmdbProvider() { callback ) }, - { - if (!res.isAnime) invokeBlackmovies( - blackMoviesAPI, - "BlackMovies", - res.title, - res.year, - res.season, - res.episode, - callback - ) - }, -// { -// invokeRinzrymovies( -// rinzryMoviesAPI, -// "RinzryMovies", -// res.title, -// res.year, -// res.season, -// res.episode, -// callback, -// ) -// }, - { - if (!res.isAnime) invokeCodexmovies( - codexMoviesAPI, - "CodexMovies", - res.title, - res.year, - res.season, - res.episode, - callback, - "Basic Y29kZXg6Y29kZXhjbG91ZA==" - ) - }, - { - if (!res.isAnime) invokeEdithxmovies( - edithxMoviesAPI, - "EdithxMovies", - res.title, - res.year, - res.season, - res.episode, - callback, - "Basic ZWRpdGg6amFydmlz" - ) - }, { invokeDahmerMovies( res.title, @@ -700,31 +602,17 @@ open class SoraStream : TmdbProvider() { ) }, { - invokeGomovies(res.title, res.year, res.season, res.episode, callback) + invokePrimewire(res.title, res.year, res.season, res.episode, callback) }, // { -// if (!res.isAnime) invokeTgarMovies(res.title, res.year, res.season, res.episode, callback) +// if (!res.isAnime) invokeGdbotMovies( +// res.title, +// res.year, +// res.season, +// res.episode, +// callback +// ) // }, - { - if (!res.isAnime) invokeGdbotMovies( - res.title, - res.year, - res.season, - res.episode, - callback - ) - }, - { - if (!res.isAnime) invokeJmdkhMovies( - jmdkhMovieAPI, - "JmdkhMovies", - res.title, - res.year, - res.season, - res.episode, - callback - ) - }, { if (!res.isAnime) invokeShinobiMovies( shinobiMovieAPI, @@ -736,35 +624,12 @@ open class SoraStream : TmdbProvider() { callback ) }, - { - if (!res.isAnime) invokeRubyMovies( - rubyMovieAPI, - "RubyMovies", - res.title, - res.year, - res.season, - res.episode, - callback - ) - }, - { - if (!res.isAnime) invokeVitoenMovies( - vitoenMovieAPI, - "VitoenMovies", - res.title, - res.year, - res.season, - res.episode, - callback - ) - }, { if (!res.isAnime) invokeAsk4Movies( res.title, res.year, res.season, res.episode, - subtitleCallback, callback ) }, @@ -780,21 +645,11 @@ open class SoraStream : TmdbProvider() { callback ) }, - { - if (!res.isAnime) invokeShivamhw( - res.title, - res.year, - res.season, - res.episode, - callback - ) - }, { if (!res.isAnime && res.season == null) invokeCryMovies( res.imdbId, res.title, res.year, - res.season, res.episode, callback ) @@ -809,14 +664,87 @@ open class SoraStream : TmdbProvider() { invokeNavy(res.imdbId, res.season, res.episode, callback) }, { - if (!res.isAnime) invokeEmovies(res.title, res.year, res.season, res.episode, subtitleCallback, callback) + invokeMoment(res.imdbId, res.season, res.episode, callback) }, { - if(!res.isAnime && res.season == null) invokePobmovies(res.title, res.year, callback) + if (!res.isAnime) invokeEmovies( + res.title, + res.year, + res.season, + res.episode, + subtitleCallback, + callback + ) }, { - if(!res.isAnime) invokeFourCartoon(res.title, res.year, res.season, res.episode, callback) - } + if (!res.isAnime && res.season == null) invokePobmovies( + res.title, + res.year, + callback + ) + }, + { + if (!res.isAnime) invokeFourCartoon( + res.title, + res.year, + res.season, + res.episode, + callback + ) + }, + { + invokeMultimovies(res.title, res.season, res.episode, subtitleCallback, callback) + }, + { + invokeNetmovies( + res.title, + res.year, + res.season, + res.episode, + subtitleCallback, + callback + ) + }, + { + if (!res.isAnime && res.season == null) invokeDoomovies( + res.title, + subtitleCallback, + callback + ) + }, + { + if (res.isAsian) invokeDramaday( + res.title, + res.year, + res.season, + res.episode, + subtitleCallback, + callback + ) + }, + { + if (!res.isAnime) invoke2embed(res.imdbId, res.season, res.episode, callback) + }, +// { +// invokeSusflix(res.id,res.season,res.episode,subtitleCallback,callback) +// }, + { + if (!res.isAnime) invokeJump1( + res.id, + res.tvdbId, + res.title, + res.year, + res.season, + res.episode, + callback + ) + }, + { + if (!res.isAnime && res.season == null) invokeOmega( + res.id, + callback + ) + }, ) return true @@ -825,6 +753,7 @@ open class SoraStream : TmdbProvider() { data class LinkData( val id: Int? = null, val imdbId: String? = null, + val tvdbId: Int? = null, val type: String? = null, val season: Int? = null, val episode: Int? = null, @@ -840,6 +769,7 @@ open class SoraStream : TmdbProvider() { val jpTitle: String? = null, val date: String? = null, val airedDate: String? = null, + val isAsian: Boolean = false, ) data class Data( @@ -928,7 +858,7 @@ open class SoraStream : TmdbProvider() { data class ExternalIds( @JsonProperty("imdb_id") val imdb_id: String? = null, - @JsonProperty("tvdb_id") val tvdb_id: String? = null, + @JsonProperty("tvdb_id") val tvdb_id: Int? = null, ) data class Credits( @@ -971,13 +901,6 @@ open class SoraStream : TmdbProvider() { @JsonProperty("alternative_titles") val alternative_titles: ResultsAltTitles? = null, ) - data class EmbedJson( - @JsonProperty("type") val type: String? = null, - @JsonProperty("link") val link: String? = null, - @JsonProperty("sources") val sources: List = arrayListOf(), - @JsonProperty("tracks") val tracks: List? = null, - ) - data class MovieHabData( @JsonProperty("link") val link: String? = null, @JsonProperty("token") val token: String? = null, diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraStreamLite.kt b/SoraStream/src/main/kotlin/com/hexated/SoraStreamLite.kt index dec99931..d0f9146e 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraStreamLite.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraStreamLite.kt @@ -1,38 +1,41 @@ package com.hexated +import com.hexated.SoraExtractor.invoke2embed import com.hexated.SoraExtractor.invokeAnimes import com.hexated.SoraExtractor.invokeAsk4Movies import com.hexated.SoraExtractor.invokeDbgo +import com.hexated.SoraExtractor.invokeDoomovies +import com.hexated.SoraExtractor.invokeDramaday import com.hexated.SoraExtractor.invokeDreamfilm import com.hexated.SoraExtractor.invokeFilmxy import com.hexated.SoraExtractor.invokeFlixon -import com.hexated.SoraExtractor.invokeFmovies import com.hexated.SoraExtractor.invokeFwatayako import com.hexated.SoraExtractor.invokeGoku -import com.hexated.SoraExtractor.invokeGomovies -import com.hexated.SoraExtractor.invokeHDMovieBox import com.hexated.SoraExtractor.invokeIdlix import com.hexated.SoraExtractor.invokeKimcartoon import com.hexated.SoraExtractor.invokeKisskh import com.hexated.SoraExtractor.invokeLing import com.hexated.SoraExtractor.invokeM4uhd -import com.hexated.SoraExtractor.invokeMovie123Net import com.hexated.SoraExtractor.invokeMovieHab import com.hexated.SoraExtractor.invokeNavy import com.hexated.SoraExtractor.invokeNinetv import com.hexated.SoraExtractor.invokeNowTv -import com.hexated.SoraExtractor.invokePutlocker import com.hexated.SoraExtractor.invokeRStream import com.hexated.SoraExtractor.invokeRidomovies -import com.hexated.SoraExtractor.invokeSeries9 import com.hexated.SoraExtractor.invokeSmashyStream import com.hexated.SoraExtractor.invokeDumpStream import com.hexated.SoraExtractor.invokeEmovies import com.hexated.SoraExtractor.invokeFourCartoon +import com.hexated.SoraExtractor.invokeJump1 +import com.hexated.SoraExtractor.invokeMoment +import com.hexated.SoraExtractor.invokeMultimovies +import com.hexated.SoraExtractor.invokeNetmovies +import com.hexated.SoraExtractor.invokeOmega +import com.hexated.SoraExtractor.invokePrimewire import com.hexated.SoraExtractor.invokeVidSrc +import com.hexated.SoraExtractor.invokeVidsrcto import com.hexated.SoraExtractor.invokeWatchOnline import com.hexated.SoraExtractor.invokeWatchsomuch -import com.hexated.SoraExtractor.invokeXmovies import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.argamap import com.lagradost.cloudstream3.utils.AppUtils @@ -52,14 +55,17 @@ class SoraStreamLite : SoraStream() { argamap( { - invokePutlocker( - res.title, - res.year, - res.season, - res.episode, - callback - ) + if(!res.isAnime) invokeJump1(res.id,res.tvdbId,res.title,res.year,res.season,res.episode,callback) }, +// { +// invokeSusflix( +// res.id, +// res.season, +// res.episode, +// subtitleCallback, +// callback +// ) +// }, { invokeWatchsomuch( res.imdbId, @@ -88,7 +94,15 @@ class SoraStreamLite : SoraStream() { ) }, { - invokeGoku(res.title, res.year, res.season, res.lastSeason, res.episode, subtitleCallback, callback) + invokeGoku( + res.title, + res.year, + res.season, + res.lastSeason, + res.episode, + subtitleCallback, + callback + ) }, { invokeVidSrc(res.id, res.season, res.episode, subtitleCallback, callback) @@ -96,15 +110,6 @@ class SoraStreamLite : SoraStream() { { invokeDbgo(res.imdbId, res.season, res.episode, subtitleCallback, callback) }, - { - invokeMovie123Net( - res.title, - res.season, - res.episode, - subtitleCallback, - callback - ) - }, { invokeMovieHab(res.imdbId, res.season, res.episode, subtitleCallback, callback) }, @@ -120,25 +125,6 @@ class SoraStreamLite : SoraStream() { callback ) }, -// { -// if (res.season != null && res.isAnime) invokeCrunchyroll( -// res.title, -// res.epsTitle, -// res.season, -// res.episode, -// subtitleCallback, -// callback -// ) -// }, - { - if (!res.isAnime) invokeHDMovieBox( - res.title, - res.season, - res.episode, - subtitleCallback, - callback - ) - }, { if (!res.isAnime) invokeDreamfilm( res.title, @@ -148,16 +134,6 @@ class SoraStreamLite : SoraStream() { callback ) }, - { - invokeSeries9( - res.title, - res.year, - res.season, - res.episode, - subtitleCallback, - callback - ) - }, { invokeIdlix( res.title, @@ -188,7 +164,13 @@ class SoraStreamLite : SoraStream() { ) }, { - if(!res.isAnime) invokeKimcartoon(res.title, res.season, res.episode, subtitleCallback, callback) + if (!res.isAnime) invokeKimcartoon( + res.title, + res.season, + res.episode, + subtitleCallback, + callback + ) }, { invokeSmashyStream( @@ -211,9 +193,8 @@ class SoraStreamLite : SoraStream() { // ) // }, { - if (!res.isAnime) invokeFmovies( - res.title, - res.airedYear ?: res.year, + if (!res.isAnime) invokeVidsrcto( + res.imdbId, res.season, res.episode, subtitleCallback, @@ -221,7 +202,15 @@ class SoraStreamLite : SoraStream() { ) }, { - invokeKisskh(res.title, res.season, res.episode, res.isAnime, res.lastSeason, subtitleCallback, callback) + invokeKisskh( + res.title, + res.season, + res.episode, + res.isAnime, + res.lastSeason, + subtitleCallback, + callback + ) }, { invokeLing( @@ -237,15 +226,21 @@ class SoraStreamLite : SoraStream() { invokeFwatayako(res.imdbId, res.season, res.episode, callback) }, { - invokeM4uhd( + if(!res.isAnime) invokeM4uhd( res.title, - res.year, + res.airedYear ?: res.year, res.season, res.episode, subtitleCallback, callback ) }, + { + if (!res.isAnime && res.season == null) invokeOmega( + res.id, + callback + ) + }, { invokeRStream(res.id, res.season, res.episode, callback) }, @@ -253,7 +248,7 @@ class SoraStreamLite : SoraStream() { invokeFlixon(res.id, res.imdbId, res.season, res.episode, callback) }, { - invokeGomovies(res.title, res.year, res.season, res.episode, callback) + invokePrimewire(res.title, res.year, res.season, res.episode, callback) }, { if (!res.isAnime) invokeAsk4Movies( @@ -261,7 +256,6 @@ class SoraStreamLite : SoraStream() { res.year, res.season, res.episode, - subtitleCallback, callback ) }, @@ -301,14 +295,55 @@ class SoraStreamLite : SoraStream() { ) }, { - if(!res.isAnime) invokeFourCartoon( + if (!res.isAnime) invokeFourCartoon( res.title, res.year, res.season, res.episode, callback ) - } + }, + { + invokeMultimovies(res.title, res.season, res.episode, subtitleCallback, callback) + }, + { + invokeNetmovies( + res.title, + res.year, + res.season, + res.episode, + subtitleCallback, + callback + ) + }, + { + invokeMoment(res.imdbId, res.season, res.episode, callback) + }, + { + if (!res.isAnime && res.season == null) invokeDoomovies( + res.title, + subtitleCallback, + callback + ) + }, + { + if(res.isAsian) invokeDramaday( + res.title, + res.year, + res.season, + res.episode, + subtitleCallback, + callback + ) + }, + { + if(!res.isAnime) invoke2embed( + res.imdbId, + res.season, + res.episode, + callback + ) + }, ) return true diff --git a/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt b/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt index 524470c6..f9d02a50 100644 --- a/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt +++ b/SoraStream/src/main/kotlin/com/hexated/SoraUtils.kt @@ -1,29 +1,24 @@ package com.hexated import android.util.Base64 -import com.fasterxml.jackson.annotation.JsonProperty import com.hexated.DumpUtils.queryApi import com.hexated.SoraStream.Companion.anilistAPI import com.hexated.SoraStream.Companion.base64DecodeAPI -import com.hexated.SoraStream.Companion.baymoviesAPI import com.hexated.SoraStream.Companion.consumetHelper import com.hexated.SoraStream.Companion.crunchyrollAPI import com.hexated.SoraStream.Companion.filmxyAPI import com.hexated.SoraStream.Companion.fmoviesAPI import com.hexated.SoraStream.Companion.gdbot import com.hexated.SoraStream.Companion.malsyncAPI -import com.hexated.SoraStream.Companion.putlockerAPI import com.hexated.SoraStream.Companion.smashyStreamAPI import com.hexated.SoraStream.Companion.tvMoviesAPI import com.hexated.SoraStream.Companion.watchOnlineAPI import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getCaptchaToken import com.lagradost.cloudstream3.APIHolder.unixTimeMS -import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson -import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.nicehttp.NiceResponse import com.lagradost.nicehttp.RequestBodyTypes import com.lagradost.nicehttp.requestCreator @@ -36,7 +31,6 @@ import okhttp3.RequestBody.Companion.toRequestBody import org.jsoup.nodes.Document import java.math.BigInteger import java.net.* -import java.nio.charset.StandardCharsets import java.security.* import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.X509EncodedKeySpec @@ -50,13 +44,6 @@ import kotlin.math.min val bflixChipperKey = base64DecodeAPI("Yjc=ejM=TzA=YTk=WHE=WnU=bXU=RFo=") const val bflixKey = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" -const val otakuzBaseUrl = "https://otakuz.live/" -val soraHeaders = mapOf( - "lang" to "en", - "versioncode" to "33", - "clienttype" to "android_Official", - "deviceid" to getDeviceId(), -) val encodedIndex = arrayOf( "GamMovies", "JSMovies", @@ -104,12 +91,6 @@ val mimeType = arrayOf( "video/x-msvideo" ) -data class FilmxyCookies( - val phpsessid: String? = null, - val wLog: String? = null, - val wSec: String? = null, -) - fun String.filterIframe( seasonNum: Int? = null, lastSeason: Int? = null, @@ -182,6 +163,46 @@ suspend fun extractMirrorUHD(url: String, ref: String): String? { ) } +suspend fun extractInstantUHD(url: String): String? { + val host = getBaseUrl(url) + val body = FormBody.Builder() + .addEncoded("keys", url.substringAfter("url=")) + .build() + return app.post( + "$host/api", requestBody = body, headers = mapOf( + "x-token" to URI(url).host + ), referer = "$host/" + ).parsedSafe>()?.get("url") +} + +suspend fun extractDirectUHD(url: String, niceResponse: NiceResponse): String? { + val document = niceResponse.document + val script = document.selectFirst("script:containsData(cf_token)")?.data() ?: return null + val actionToken = script.substringAfter("\"key\", \"").substringBefore("\");") + val cfToken = script.substringAfter("cf_token = \"").substringBefore("\";") + val body = FormBody.Builder() + .addEncoded("action", "direct") + .addEncoded("key", actionToken) + .addEncoded("action_token", cfToken) + .build() + val cookies = mapOf("PHPSESSID" to "${niceResponse.cookies["PHPSESSID"]}") + val direct = app.post( + url, + requestBody = body, + cookies = cookies, + referer = url, + headers = mapOf( + "x-token" to "driveleech.org" + ) + ).parsedSafe>()?.get("url") + + return app.get( + direct ?: return null, cookies = cookies, + referer = url + ).text.substringAfter("worker_url = '").substringBefore("';") + +} + suspend fun extractBackupUHD(url: String): String? { val resumeDoc = app.get(url) @@ -417,7 +438,7 @@ suspend fun invokeVizcloud( ) { val id = Regex("(?:/embed[-/]|/e/)([^?/]*)").find(url)?.groupValues?.getOrNull(1) app.get("$consumetHelper?query=${id ?: return}&action=vizcloud") - .parsedSafe()?.data?.media?.sources?.map { + .parsedSafe()?.result?.sources?.map { M3u8Helper.generateM3u8( "Vizcloud", it.file ?: return@map, @@ -442,14 +463,8 @@ suspend fun invokeSmashyFfix( ref: String, callback: (ExtractorLink) -> Unit, ) { - val script = - app.get(url, referer = ref).document.selectFirst("script:containsData(player =)")?.data() - ?: return - - val source = - Regex("['\"]?file['\"]?:\\s*\"([^\"]+)").find(script)?.groupValues?.get( - 1 - ) ?: return + val res = app.get(url, referer = ref).text + val source = Regex("['\"]?file['\"]?:\\s*\"([^\"]+)").find(res)?.groupValues?.get(1) ?: return source.split(",").map { links -> val quality = Regex("\\[(\\d+)]").find(links)?.groupValues?.getOrNull(1)?.trim() @@ -458,7 +473,7 @@ suspend fun invokeSmashyFfix( ExtractorLink( "Smashy [$name]", "Smashy [$name]", - link, + decode(link).replace("\\/", "/"), smashyStreamAPI, quality?.toIntOrNull() ?: return@map, isM3u8 = link.contains(".m3u8"), @@ -563,6 +578,65 @@ suspend fun invokeSmashyRip( } +suspend fun invokeSmashyIm( + name: String, + url: String, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit, +) { + val script = + app.get(url).document.selectFirst("script:containsData(player =)")?.data() ?: return + + val sources = + Regex("['\"]?file['\"]?:\\s*\"([^\"]+)").find(script)?.groupValues?.get(1) ?: return + val subtitles = + Regex("['\"]?subtitle['\"]?:\\s*\"([^\"]+)").find(script)?.groupValues?.get(1) ?: return + + M3u8Helper.generateM3u8( + "Smashy [$name]", + sources, + "" + ).forEach(callback) + + subtitles.split(",").map { sub -> + val lang = Regex("\\[(.*?)]").find(sub)?.groupValues?.getOrNull(1)?.trim() + val trimmedSubLink = sub.removePrefix("[$lang]").trim().substringAfter("?url=") + subtitleCallback.invoke( + SubtitleFile( + lang.takeIf { !it.isNullOrEmpty() } ?: return@map, + trimmedSubLink + ) + ) + } + +} + +suspend fun invokeSmashyRw( + name: String, + url: String, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit, +) { + val res = app.get(url).document + val video = res.selectFirst("media-player")?.attr("src") + + M3u8Helper.generateM3u8( + "Smashy [$name]", + video ?: return, + "" + ).forEach(callback) + + res.select("track").map { track -> + subtitleCallback.invoke( + SubtitleFile( + track.attr("label"), + track.attr("src"), + ) + ) + } + +} + suspend fun getDumpIdAndType(title: String?, year: Int?, season: Int?): Pair { val res = tryParseJson( queryApi( @@ -841,31 +915,29 @@ suspend fun getTvMoviesServer(url: String, season: Int?, episode: Int?): Pair? = null +suspend fun getFilmxyCookies(url: String) = filmxyCookies ?: fetchFilmxyCookies(url).also { filmxyCookies = it } +suspend fun fetchFilmxyCookies(url: String): Map { - val url = if (season == null) { - "${filmxyAPI}/movie/$imdbId" - } else { - "${filmxyAPI}/tv/$imdbId" - } - val cookieUrl = "${filmxyAPI}/wp-admin/admin-ajax.php" - - val res = session.get( + val defaultCookies = mutableMapOf("G_ENABLED_IDPS" to "google", "true_checker" to "1", "XID" to "1") + session.get( url, headers = mapOf( "Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8" ), + cookies = defaultCookies, ) - - if (!res.isSuccessful) return FilmxyCookies() + val phpsessid = session.baseClient.cookieJar.loadForRequest(url.toHttpUrl()) + .first { it.name == "PHPSESSID" }.value + defaultCookies["PHPSESSID"] = phpsessid val userNonce = - res.document.select("script").find { it.data().contains("var userNonce") }?.data()?.let { - Regex("var\\suserNonce.*?\"(\\S+?)\";").find(it)?.groupValues?.get(1) - } + app.get("$filmxyAPI/login/?redirect_to=$filmxyAPI/", cookies = defaultCookies).document.select("script") + .find { it.data().contains("var userNonce") }?.data()?.let { + Regex("var\\suserNonce.*?\"(\\S+?)\";").find(it)?.groupValues?.get(1) + } - var phpsessid = session.baseClient.cookieJar.loadForRequest(url.toHttpUrl()) - .first { it.name == "PHPSESSID" }.value + val cookieUrl = "${filmxyAPI}/wp-admin/admin-ajax.php" session.post( cookieUrl, @@ -874,18 +946,14 @@ suspend fun getFilmxyCookies(imdbId: String? = null, season: Int? = null): Filmx "nonce" to "$userNonce", ), headers = mapOf( - "Cookie" to "PHPSESSID=$phpsessid; G_ENABLED_IDPS=google", "X-Requested-With" to "XMLHttpRequest", - ) + ), + cookies = defaultCookies ) - val cookieJar = session.baseClient.cookieJar.loadForRequest(cookieUrl.toHttpUrl()) - phpsessid = cookieJar.first { it.name == "PHPSESSID" }.value - val wLog = - cookieJar.first { it.name == "wordpress_logged_in_8bf9d5433ac88cc9a3a396d6b154cd01" }.value - val wSec = cookieJar.first { it.name == "wordpress_sec_8bf9d5433ac88cc9a3a396d6b154cd01" }.value + .associate { it.name to it.value }.toMutableMap() - return FilmxyCookies(phpsessid, wLog, wSec) + return cookieJar.plus(defaultCookies) } fun Document.findTvMoviesIframe(): String? { @@ -922,7 +990,7 @@ suspend fun searchWatchOnline( } //modified code from https://github.com/jmir1/aniyomi-extensions/blob/master/src/all/kamyroll/src/eu/kanade/tachiyomi/animeextension/all/kamyroll/AccessTokenInterceptor.kt -fun getCrunchyrollToken(): Map { +suspend fun getCrunchyrollToken(): Map { val client = app.baseClient.newBuilder() .proxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress("cr-unblocker.us.to", 1080))) .build() @@ -942,7 +1010,7 @@ fun getCrunchyrollToken(): Map { "Authorization" to "Basic ${BuildConfig.CRUNCHYROLL_BASIC_TOKEN}" ), data = mapOf( - "refresh_token" to BuildConfig.CRUNCHYROLL_REFRESH_TOKEN, + "refresh_token" to app.get(BuildConfig.CRUNCHYROLL_REFRESH_TOKEN).text, "grant_type" to "refresh_token", "scope" to "offline_access" ) @@ -997,52 +1065,6 @@ suspend fun getCrunchyrollIdFromMalSync(aniId: String?): String? { ?: regex.find("$crunchyroll")?.groupValues?.getOrNull(1) } -suspend fun extractPutlockerSources(url: String?): NiceResponse? { - val embedHost = url?.substringBefore("/embed-player") - val player = app.get( - url ?: return null, - referer = "${putlockerAPI}/" - ).document.select("div#player") - - val text = "\"${player.attr("data-id")}\"" - val password = player.attr("data-hash") - val cipher = CryptoAES.plEncrypt(password, text) - - return app.get( - "$embedHost/ajax/getSources/", params = mapOf( - "id" to cipher.cipherText, - "h" to cipher.password, - "a" to cipher.iv, - "t" to cipher.salt, - ), referer = url - ) -} - -suspend fun PutlockerResponses?.callback( - referer: String, - server: String, - callback: (ExtractorLink) -> Unit -) { - val ref = getBaseUrl(referer) - this?.sources?.map { source -> - val request = app.get(source.file, referer = ref) - callback.invoke( - ExtractorLink( - "Putlocker [$server]", - "Putlocker [$server]", - if (!request.isSuccessful) return@map null else source.file, - ref, - if (source.file.contains("m3u8")) getPutlockerQuality(request.text) else source.label?.replace( - Regex("[Pp]"), - "" - )?.trim()?.toIntOrNull() - ?: Qualities.P720.value, - source.file.contains("m3u8") - ) - ) - } -} - suspend fun convertTmdbToAnimeId( title: String?, date: String?, @@ -1117,6 +1139,33 @@ suspend fun tmdbToAnimeId(title: String?, year: Int?, season: String?, type: TvT } +suspend fun loadCustomExtractor( + name: String? = null, + url: String, + referer: String? = null, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit, + quality: Int? = null, +) { + loadExtractor(url, referer, subtitleCallback) { link -> + callback.invoke( + ExtractorLink( + name ?: link.source, + name ?: link.name, + link.url, + link.referer, + when { + link.type == ExtractorLinkType.M3U8 -> link.quality + else -> quality ?: link.quality + }, + link.type, + link.headers, + link.extractorData + ) + ) + } +} + fun getSeason(month: Int?): String? { val seasons = arrayOf( "Winter", "Winter", "Spring", "Spring", "Spring", "Summer", @@ -1134,7 +1183,6 @@ fun getPutlockerQuality(quality: String): Int { } } - fun getEpisodeSlug( season: Int? = null, episode: Int? = null, @@ -1216,29 +1264,12 @@ fun matchingIndex( ) == true && ((mediaMimeType in mimeType) || mediaName.contains(Regex("\\.mkv|\\.mp4|\\.avi"))) } -suspend fun getConfig(): BaymoviesConfig { - val regex = """const country = "(.*?)"; -const downloadtime = "(.*?)"; -var arrayofworkers = (.*)""".toRegex() - val js = app.get( - "https://geolocation.zindex.eu.org/api.js", - referer = "$baymoviesAPI/", - ).text - val match = regex.find(js) ?: throw ErrorLoadingException() - val country = match.groupValues[1] - val downloadTime = match.groupValues[2] - val workers = tryParseJson>(match.groupValues[3]) - ?: throw ErrorLoadingException() - - return BaymoviesConfig(country, downloadTime, workers) -} - fun decodeIndexJson(json: String): String { val slug = json.reversed().substring(24) return base64Decode(slug.substring(0, slug.length - 20)) } -fun String.decryptGomoviesJson(key: String = "123"): String { +fun String.decodePrimewireXor(key: String = BuildConfig.PRIMEWIRE_KEY): String { val sb = StringBuilder() var i = 0 while (i < this.length) { @@ -1252,7 +1283,7 @@ fun String.decryptGomoviesJson(key: String = "123"): String { return sb.toString() } -fun Headers.getGomoviesCookies(cookieKey: String = "set-cookie"): Map { +fun Headers.getPrimewireCookies(cookieKey: String = "set-cookie"): Map { val cookieList = this.filter { it.first.equals(cookieKey, ignoreCase = true) }.mapNotNull { it.second.split(";").firstOrNull() @@ -1588,453 +1619,6 @@ private enum class Symbol(val decimalValue: Int) { } } -// code found on https://stackoverflow.com/a/63701411 - -/** - * Conforming with CryptoJS AES method - */ -// see https://gist.github.com/thackerronak/554c985c3001b16810af5fc0eb5c358f -@Suppress("unused", "FunctionName", "SameParameterValue") -object CryptoAES { - - private const val KEY_SIZE = 256 - private const val IV_SIZE = 128 - private const val HASH_CIPHER = "AES/CBC/PKCS5Padding" - private const val AES = "AES" - private const val KDF_DIGEST = "MD5" - - // Seriously crypto-js, what's wrong with you? - private const val APPEND = "Salted__" - - /** - * Encrypt - * @param password passphrase - * @param plainText plain string - */ - fun encrypt(password: String, plainText: String): String { - val saltBytes = generateSalt(8) - val key = ByteArray(KEY_SIZE / 8) - val iv = ByteArray(IV_SIZE / 8) - EvpKDF(password.toByteArray(), KEY_SIZE, IV_SIZE, saltBytes, key, iv) - val keyS = SecretKeySpec(key, AES) - val cipher = Cipher.getInstance(HASH_CIPHER) - val ivSpec = IvParameterSpec(iv) - cipher.init(Cipher.ENCRYPT_MODE, keyS, ivSpec) - val cipherText = cipher.doFinal(plainText.toByteArray()) - // Thanks kientux for this: https://gist.github.com/kientux/bb48259c6f2133e628ad - // Create CryptoJS-like encrypted! - val sBytes = APPEND.toByteArray() - val b = ByteArray(sBytes.size + saltBytes.size + cipherText.size) - System.arraycopy(sBytes, 0, b, 0, sBytes.size) - System.arraycopy(saltBytes, 0, b, sBytes.size, saltBytes.size) - System.arraycopy(cipherText, 0, b, sBytes.size + saltBytes.size, cipherText.size) - val bEncode = Base64.encode(b, Base64.NO_WRAP) - return String(bEncode) - } - - fun plEncrypt(password: String, plainText: String): EncryptResult { - val saltBytes = generateSalt(8) - val key = ByteArray(KEY_SIZE / 8) - val iv = ByteArray(IV_SIZE / 8) - EvpKDF(password.toByteArray(), KEY_SIZE, IV_SIZE, saltBytes, key, iv) - val keyS = SecretKeySpec(key, AES) - val cipher = Cipher.getInstance(HASH_CIPHER) - val ivSpec = IvParameterSpec(iv) - cipher.init(Cipher.ENCRYPT_MODE, keyS, ivSpec) - val cipherText = cipher.doFinal(plainText.toByteArray()) - val bEncode = Base64.encode(cipherText, Base64.NO_WRAP) - return EncryptResult( - String(bEncode).toHex(), - password.toHex(), - saltBytes.toHex(), - iv.toHex() - ) - } - - /** - * Decrypt - * Thanks Artjom B. for this: http://stackoverflow.com/a/29152379/4405051 - * @param password passphrase - * @param cipherText encrypted string - */ - fun decrypt(password: String, cipherText: String): String { - val ctBytes = Base64.decode(cipherText.toByteArray(), Base64.NO_WRAP) - val saltBytes = Arrays.copyOfRange(ctBytes, 8, 16) - val cipherTextBytes = Arrays.copyOfRange(ctBytes, 16, ctBytes.size) - val key = ByteArray(KEY_SIZE / 8) - val iv = ByteArray(IV_SIZE / 8) - EvpKDF(password.toByteArray(), KEY_SIZE, IV_SIZE, saltBytes, key, iv) - val cipher = Cipher.getInstance(HASH_CIPHER) - val keyS = SecretKeySpec(key, AES) - cipher.init(Cipher.DECRYPT_MODE, keyS, IvParameterSpec(iv)) - val plainText = cipher.doFinal(cipherTextBytes) - return String(plainText) - } - - private fun EvpKDF( - password: ByteArray, - keySize: Int, - ivSize: Int, - salt: ByteArray, - resultKey: ByteArray, - resultIv: ByteArray - ): ByteArray { - return EvpKDF(password, keySize, ivSize, salt, 1, KDF_DIGEST, resultKey, resultIv) - } - - @Suppress("NAME_SHADOWING") - private fun EvpKDF( - password: ByteArray, - keySize: Int, - ivSize: Int, - salt: ByteArray, - iterations: Int, - hashAlgorithm: String, - resultKey: ByteArray, - resultIv: ByteArray - ): ByteArray { - val keySize = keySize / 32 - val ivSize = ivSize / 32 - val targetKeySize = keySize + ivSize - val derivedBytes = ByteArray(targetKeySize * 4) - var numberOfDerivedWords = 0 - var block: ByteArray? = null - val hash = MessageDigest.getInstance(hashAlgorithm) - while (numberOfDerivedWords < targetKeySize) { - if (block != null) { - hash.update(block) - } - hash.update(password) - block = hash.digest(salt) - hash.reset() - // Iterations - for (i in 1 until iterations) { - block = hash.digest(block!!) - hash.reset() - } - System.arraycopy( - block!!, 0, derivedBytes, numberOfDerivedWords * 4, - min(block.size, (targetKeySize - numberOfDerivedWords) * 4) - ) - numberOfDerivedWords += block.size / 4 - } - System.arraycopy(derivedBytes, 0, resultKey, 0, keySize * 4) - System.arraycopy(derivedBytes, keySize * 4, resultIv, 0, ivSize * 4) - return derivedBytes // key + iv - } - - private fun generateSalt(length: Int): ByteArray { - return ByteArray(length).apply { - SecureRandom().nextBytes(this) - } - } - - private fun ByteArray.toHex(): String = - joinToString(separator = "") { eachByte -> "%02x".format(eachByte) } - - private fun String.toHex(): String = toByteArray().toHex() - - data class EncryptResult( - val cipherText: String, - val password: String, - val salt: String, - val iv: String - ) - -} - -object RabbitStream { - - suspend fun MainAPI.extractRabbitStream( - server: String, - url: String, - ref: String, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit, - useSidAuthentication: Boolean, - /** Used for extractorLink name, input: Source name */ - extractorData: String? = null, - decryptKey: String? = null, - nameTransformer: (String) -> String, - ) = suspendSafeApiCall { - // https://rapid-cloud.ru/embed-6/dcPOVRE57YOT?z= -> https://rapid-cloud.ru/embed-6 - val mainIframeUrl = - url.substringBeforeLast("/") - val mainIframeId = url.substringAfterLast("/") - .substringBefore("?") // https://rapid-cloud.ru/embed-6/dcPOVRE57YOT?z= -> dcPOVRE57YOT - var sid: String? = null - if (useSidAuthentication && extractorData != null) { - negotiateNewSid(extractorData)?.also { pollingData -> - app.post( - "$extractorData&t=${generateTimeStamp()}&sid=${pollingData.sid}", - requestBody = "40".toRequestBody(), - timeout = 60 - ) - val text = app.get( - "$extractorData&t=${generateTimeStamp()}&sid=${pollingData.sid}", - timeout = 60 - ).text.replaceBefore("{", "") - - sid = AppUtils.parseJson(text).sid - ioSafe { app.get("$extractorData&t=${generateTimeStamp()}&sid=${pollingData.sid}") } - } - } - val mainIframeAjax = mainIframeUrl.let { - if(it.contains("/embed-2/e-1")) it.replace( - "/embed-2/e-1", - "/embed-2/ajax/e-1" - ) else it.replace( - "/embed", - "/ajax/embed" - ) - } - val getSourcesUrl = "$mainIframeAjax/getSources?id=$mainIframeId${sid?.let { "$&sId=$it" } ?: ""}" - val response = app.get( - getSourcesUrl, - referer = mainUrl, - headers = mapOf( - "X-Requested-With" to "XMLHttpRequest", - "Accept" to "*/*", - "Accept-Language" to "en-US,en;q=0.5", - "Connection" to "keep-alive", - "TE" to "trailers" - ) - ) - - val sourceObject = if (decryptKey != null) { - val encryptedMap = response.parsedSafe() - val sources = encryptedMap?.sources - if (sources == null || encryptedMap.encrypted == false) { - response.parsedSafe() - } else { - val decrypted = - decryptMapped>(sources, decryptKey) - SourceObject( - sources = decrypted, - tracks = encryptedMap.tracks - ) - } - } else { - response.parsedSafe() - } ?: return@suspendSafeApiCall - - sourceObject.tracks?.forEach { track -> - track?.toSubtitleFile()?.let { subtitleFile -> - subtitleCallback.invoke(subtitleFile) - } - } - - val list = listOf( - sourceObject.sources to "source 1", - sourceObject.sources1 to "source 2", - sourceObject.sources2 to "source 3", - sourceObject.sourcesBackup to "source backup" - ) - - list.forEach { subList -> - subList.first?.forEach { source -> - source?.toExtractorLink( - server, - ref, - extractorData, - ) - ?.forEach { - // Sets Zoro SID used for video loading -// (this as? ZoroProvider)?.sid?.set(it.url.hashCode(), sid) - callback(it) - } - } - } - } - - private suspend fun Sources.toExtractorLink( - name: String, - referer: String, - extractorData: String? = null, - ): List? { - return this.file?.let { file -> - //println("FILE::: $file") - val isM3u8 = URI(this.file).path.endsWith(".m3u8") || this.type.equals( - "hls", - ignoreCase = true - ) - return if (isM3u8) { - suspendSafeApiCall { - M3u8Helper().m3u8Generation( - M3u8Helper.M3u8Stream( - this.file, - null, - mapOf("Referer" to "https://mzzcloud.life/") - ), false - ) - .map { stream -> - ExtractorLink( - name, - name, - stream.streamUrl, - referer, - getQualityFromName(stream.quality?.toString()), - true, - extractorData = extractorData - ) - } - }.takeIf { !it.isNullOrEmpty() } ?: listOf( - // Fallback if m3u8 extractor fails - ExtractorLink( - name, - name, - this.file, - referer, - getQualityFromName(this.label), - isM3u8, - extractorData = extractorData - ) - ) - } else { - listOf( - ExtractorLink( - name, - name, - file, - referer, - getQualityFromName(this.label), - false, - extractorData = extractorData - ) - ) - } - } - } - - private fun Tracks.toSubtitleFile(): SubtitleFile? { - return this.file?.let { - SubtitleFile( - this.label ?: "Unknown", - it - ) - } - } - - /** - * Generates a session - * 1 Get request. - * */ - private suspend fun negotiateNewSid(baseUrl: String): PollingData? { - // Tries multiple times - for (i in 1..5) { - val jsonText = - app.get("$baseUrl&t=${generateTimeStamp()}").text.replaceBefore( - "{", - "" - ) -// println("Negotiated sid $jsonText") - AppUtils.parseJson(jsonText)?.let { return it } - delay(1000L * i) - } - return null - } - - private fun generateTimeStamp(): String { - val chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_" - var code = "" - var time = APIHolder.unixTimeMS - while (time > 0) { - code += chars[(time % (chars.length)).toInt()] - time /= chars.length - } - return code.reversed() - } - - suspend fun getKey(): String { - return app.get("https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt") - .text - } - - suspend fun getZoroKey(): String { - return app.get("https://raw.githubusercontent.com/enimax-anime/key/e0/key.txt").text - } - - private inline fun decryptMapped(input: String, key: String): T? { - return tryParseJson(decrypt(input, key)) - } - - private fun decrypt(input: String, key: String): String { - return decryptSourceUrl( - generateKey( - base64DecodeArray(input).copyOfRange(8, 16), - key.toByteArray() - ), input - ) - } - - private fun generateKey(salt: ByteArray, secret: ByteArray): ByteArray { - var key = md5(secret + salt) - var currentKey = key - while (currentKey.size < 48) { - key = md5(key + secret + salt) - currentKey += key - } - return currentKey - } - - private fun md5(input: ByteArray): ByteArray { - return MessageDigest.getInstance("MD5").digest(input) - } - - private fun decryptSourceUrl(decryptionKey: ByteArray, sourceUrl: String): String { - val cipherData = base64DecodeArray(sourceUrl) - val encrypted = cipherData.copyOfRange(16, cipherData.size) - val aesCBC = Cipher.getInstance("AES/CBC/PKCS5Padding") - - Objects.requireNonNull(aesCBC).init( - Cipher.DECRYPT_MODE, SecretKeySpec( - decryptionKey.copyOfRange(0, 32), - "AES" - ), - IvParameterSpec(decryptionKey.copyOfRange(32, decryptionKey.size)) - ) - val decryptedData = aesCBC!!.doFinal(encrypted) - return String(decryptedData, StandardCharsets.UTF_8) - } - - data class PollingData( - @JsonProperty("sid") val sid: String? = null, - @JsonProperty("upgrades") val upgrades: ArrayList = arrayListOf(), - @JsonProperty("pingInterval") val pingInterval: Int? = null, - @JsonProperty("pingTimeout") val pingTimeout: Int? = null - ) - - data class Tracks( - @JsonProperty("file") val file: String?, - @JsonProperty("label") val label: String?, - @JsonProperty("kind") val kind: String? - ) - - data class Sources( - @JsonProperty("file") val file: String?, - @JsonProperty("type") val type: String?, - @JsonProperty("label") val label: String? - ) - - data class SourceObject( - @JsonProperty("sources") val sources: List? = null, - @JsonProperty("sources_1") val sources1: List? = null, - @JsonProperty("sources_2") val sources2: List? = null, - @JsonProperty("sourcesBackup") val sourcesBackup: List? = null, - @JsonProperty("tracks") val tracks: List? = null - ) - - data class SourceObjectEncrypted( - @JsonProperty("sources") val sources: String?, - @JsonProperty("encrypted") val encrypted: Boolean?, - @JsonProperty("sources_1") val sources1: String?, - @JsonProperty("sources_2") val sources2: String?, - @JsonProperty("sourcesBackup") val sourcesBackup: String?, - @JsonProperty("tracks") val tracks: List? - ) - -} - object DumpUtils { private val deviceId = getDeviceId() @@ -2151,4 +1735,127 @@ object RSAEncryptionHelper { exception.printStackTrace() null } +} + +// code found on https://stackoverflow.com/a/63701411 + +/** + * Conforming with CryptoJS AES method + */ +// see https://gist.github.com/thackerronak/554c985c3001b16810af5fc0eb5c358f +@Suppress("unused", "FunctionName", "SameParameterValue") +object CryptoJS { + + private const val KEY_SIZE = 256 + private const val IV_SIZE = 128 + private const val HASH_CIPHER = "AES/CBC/PKCS7Padding" + private const val AES = "AES" + private const val KDF_DIGEST = "MD5" + + // Seriously crypto-js, what's wrong with you? + private const val APPEND = "Salted__" + + /** + * Encrypt + * @param password passphrase + * @param plainText plain string + */ + fun encrypt(password: String, plainText: String): String { + val saltBytes = generateSalt(8) + val key = ByteArray(KEY_SIZE / 8) + val iv = ByteArray(IV_SIZE / 8) + EvpKDF(password.toByteArray(), KEY_SIZE, IV_SIZE, saltBytes, key, iv) + val keyS = SecretKeySpec(key, AES) + val cipher = Cipher.getInstance(HASH_CIPHER) + val ivSpec = IvParameterSpec(iv) + cipher.init(Cipher.ENCRYPT_MODE, keyS, ivSpec) + val cipherText = cipher.doFinal(plainText.toByteArray()) + // Thanks kientux for this: https://gist.github.com/kientux/bb48259c6f2133e628ad + // Create CryptoJS-like encrypted! + val sBytes = APPEND.toByteArray() + val b = ByteArray(sBytes.size + saltBytes.size + cipherText.size) + System.arraycopy(sBytes, 0, b, 0, sBytes.size) + System.arraycopy(saltBytes, 0, b, sBytes.size, saltBytes.size) + System.arraycopy(cipherText, 0, b, sBytes.size + saltBytes.size, cipherText.size) + val bEncode = Base64.encode(b, Base64.NO_WRAP) + return String(bEncode) + } + + /** + * Decrypt + * Thanks Artjom B. for this: http://stackoverflow.com/a/29152379/4405051 + * @param password passphrase + * @param cipherText encrypted string + */ + fun decrypt(password: String, cipherText: String): String { + val ctBytes = Base64.decode(cipherText.toByteArray(), Base64.NO_WRAP) + val saltBytes = Arrays.copyOfRange(ctBytes, 8, 16) + val cipherTextBytes = Arrays.copyOfRange(ctBytes, 16, ctBytes.size) + val key = ByteArray(KEY_SIZE / 8) + val iv = ByteArray(IV_SIZE / 8) + EvpKDF(password.toByteArray(), KEY_SIZE, IV_SIZE, saltBytes, key, iv) + val cipher = Cipher.getInstance(HASH_CIPHER) + val keyS = SecretKeySpec(key, AES) + cipher.init(Cipher.DECRYPT_MODE, keyS, IvParameterSpec(iv)) + val plainText = cipher.doFinal(cipherTextBytes) + return String(plainText) + } + + private fun EvpKDF( + password: ByteArray, + keySize: Int, + ivSize: Int, + salt: ByteArray, + resultKey: ByteArray, + resultIv: ByteArray + ): ByteArray { + return EvpKDF(password, keySize, ivSize, salt, 1, KDF_DIGEST, resultKey, resultIv) + } + + @Suppress("NAME_SHADOWING") + private fun EvpKDF( + password: ByteArray, + keySize: Int, + ivSize: Int, + salt: ByteArray, + iterations: Int, + hashAlgorithm: String, + resultKey: ByteArray, + resultIv: ByteArray + ): ByteArray { + val keySize = keySize / 32 + val ivSize = ivSize / 32 + val targetKeySize = keySize + ivSize + val derivedBytes = ByteArray(targetKeySize * 4) + var numberOfDerivedWords = 0 + var block: ByteArray? = null + val hash = MessageDigest.getInstance(hashAlgorithm) + while (numberOfDerivedWords < targetKeySize) { + if (block != null) { + hash.update(block) + } + hash.update(password) + block = hash.digest(salt) + hash.reset() + // Iterations + for (i in 1 until iterations) { + block = hash.digest(block!!) + hash.reset() + } + System.arraycopy( + block!!, 0, derivedBytes, numberOfDerivedWords * 4, + min(block.size, (targetKeySize - numberOfDerivedWords) * 4) + ) + numberOfDerivedWords += block.size / 4 + } + System.arraycopy(derivedBytes, 0, resultKey, 0, keySize * 4) + System.arraycopy(derivedBytes, keySize * 4, resultIv, 0, ivSize * 4) + return derivedBytes // key + iv + } + + private fun generateSalt(length: Int): ByteArray { + return ByteArray(length).apply { + SecureRandom().nextBytes(this) + } + } } \ No newline at end of file diff --git a/StremioX/build.gradle.kts b/StremioX/build.gradle.kts new file mode 100644 index 00000000..3366ed71 --- /dev/null +++ b/StremioX/build.gradle.kts @@ -0,0 +1,26 @@ +// use an integer for version numbers +version = 10 + + +cloudstream { + language = "en" + // All of these properties are optional, you can safely remove them + + description = "[!] Requires Setup \n- StremioX allows you to use stream addons \n- StremioC allows you to use catalog addons" + authors = listOf("Hexated") + + /** + * 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://raw.githubusercontent.com/hexated/cloudstream-extensions-hexated/master/StremioX/icon.png" +} \ No newline at end of file diff --git a/StremioX/src/main/kotlin/com/hexated/StremioC.kt b/StremioX/src/main/kotlin/com/hexated/StremioC.kt new file mode 100644 index 00000000..4d972d31 --- /dev/null +++ b/StremioX/src/main/kotlin/com/hexated/StremioC.kt @@ -0,0 +1,364 @@ +package com.hexated + +import com.fasterxml.jackson.annotation.JsonProperty +import com.hexated.SubsExtractors.invokeOpenSubs +import com.hexated.SubsExtractors.invokeWatchsomuch +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addActors +import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import java.net.URI + +private const val TRACKER_LIST_URL = + "https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt" + +class StremioC : MainAPI() { + override var mainUrl = "https://stremio.github.io/stremio-static-addon-example" + override var name = "StremioC" + override val supportedTypes = setOf(TvType.Others) + override val hasMainPage = true + private val cinemataUrl = "https://v3-cinemeta.strem.io" + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse? { + mainUrl = mainUrl.fixSourceUrl() + val res = tryParseJson(request("${mainUrl}/manifest.json").body.string()) ?: return null + val lists = mutableListOf() + res.catalogs.apmap { catalog -> + catalog.toHomePageList(this).let { + if (it.list.isNotEmpty()) lists.add(it) + } + } + return HomePageResponse( + lists, + false + ) + } + + override suspend fun search(query: String): List? { + mainUrl = mainUrl.fixSourceUrl() + val res = tryParseJson(request("${mainUrl}/manifest.json").body.string()) ?: return null + val list = mutableListOf() + res.catalogs.apmap { catalog -> + list.addAll(catalog.search(query, this)) + } + return list.distinct() + } + + override suspend fun load(url: String): LoadResponse { + val res = parseJson(url) + mainUrl = + if ((res.type == "movie" || res.type == "series") && isImdborTmdb(res.id)) cinemataUrl else mainUrl + val json = app.get("${mainUrl}/meta/${res.type}/${res.id}.json") + .parsedSafe()?.meta ?: throw RuntimeException(url) + return json.toLoadResponse(this, res.id) + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val loadData = parseJson(data) + val request = request("${mainUrl}/stream/${loadData.type}/${loadData.id}.json") + if (request.code.isSuccessful()) { + val res = tryParseJson(request.body.string()) ?: return false + res.streams.forEach { stream -> + stream.runCallback(subtitleCallback, callback) + } + } else { + argamap( + { + invokeStremioX(loadData.type, loadData.id, subtitleCallback, callback) + }, + { + invokeWatchsomuch( + loadData.imdbId, + loadData.season, + loadData.episode, + subtitleCallback + ) + }, + { + invokeOpenSubs( + loadData.imdbId, + loadData.season, + loadData.episode, + subtitleCallback + ) + }, + ) + } + + return true + } + + private suspend fun invokeStremioX( + type: String?, + id: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val sites = + AcraApplication.getKey>(USER_PROVIDER_API)?.toMutableList() + ?: mutableListOf() + sites.filter { it.parentJavaClass == "StremioX" }.apmap { site -> + val request = request("${site.url.fixSourceUrl()}/stream/${type}/${id}.json").body.string() + val res = + tryParseJson(request) + ?: return@apmap + res.streams.forEach { stream -> + stream.runCallback(subtitleCallback, callback) + } + } + } + + data class LoadData( + val type: String? = null, + val id: String? = null, + val season: Int? = null, + val episode: Int? = null, + val imdbId: String? = null, + ) + + data class CustomSite( + @JsonProperty("parentJavaClass") val parentJavaClass: String, + @JsonProperty("name") val name: String, + @JsonProperty("url") val url: String, + @JsonProperty("lang") val lang: String, + ) + + // check if id is imdb/tmdb cause stremio addons like torrentio works base on imdbId + private fun isImdborTmdb(url: String?): Boolean { + return imdbUrlToIdNullable(url) != null || url?.startsWith("tmdb:") == true + } + + private data class Manifest(val catalogs: List) + private data class Catalog( + var name: String?, + val id: String, + val type: String?, + val types: MutableList = mutableListOf() + ) { + init { + if (type != null) types.add(type) + } + + suspend fun search(query: String, provider: StremioC): List { + val entries = mutableListOf() + types.forEach { type -> + val json = request("${provider.mainUrl}/catalog/${type}/${id}/search=${query}.json").body.string() + val res = + tryParseJson(json) + ?: return@forEach + res.metas?.forEach { entry -> + entries.add(entry.toSearchResponse(provider)) + } + } + return entries + } + + suspend fun toHomePageList(provider: StremioC): HomePageList { + val entries = mutableListOf() + types.forEach { type -> + val json = request("${provider.mainUrl}/catalog/${type}/${id}.json").body.string() + val res = + tryParseJson(json) + ?: return@forEach + res.metas?.forEach { entry -> + entries.add(entry.toSearchResponse(provider)) + } + } + return HomePageList( + "$type - ${name ?: id}", + entries + ) + } + } + + private data class CatalogResponse(val metas: List?, val meta: CatalogEntry?) + + private data class Trailer( + val source: String?, + val type: String? + ) + private data class CatalogEntry( + @JsonProperty("name") val name: String, + @JsonProperty("id") val id: String, + @JsonProperty("poster") val poster: String?, + @JsonProperty("background") val background: String?, + @JsonProperty("description") val description: String?, + @JsonProperty("imdbRating") val imdbRating: String?, + @JsonProperty("type") val type: String?, + @JsonProperty("videos") val videos: List