diff --git a/UASerialsProProvider/build.gradle.kts b/UASerialsProProvider/build.gradle.kts new file mode 100644 index 0000000..b920253 --- /dev/null +++ b/UASerialsProProvider/build.gradle.kts @@ -0,0 +1,25 @@ +// use an integer for version numbers +version = 1 + +dependencies { + implementation("com.google.code.gson:gson:2.9.0") +} + +cloudstream { + language = "uk" + // All of these properties are optional, you can safely remove them + + description = "UASerials.pro — твої улюблені серіали українською мовою онлайн." + authors = listOf("CakesTwix") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + + iconUrl = "https://www.google.com/s2/favicons?domain=uaserials.pro&sz=%size%" +} diff --git a/UASerialsProProvider/src/main/AndroidManifest.xml b/UASerialsProProvider/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29aec9d --- /dev/null +++ b/UASerialsProProvider/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/UASerialsProProvider/src/main/kotlin/com/lagradost/UASerialsProProvider.kt b/UASerialsProProvider/src/main/kotlin/com/lagradost/UASerialsProProvider.kt new file mode 100644 index 0000000..61f6584 --- /dev/null +++ b/UASerialsProProvider/src/main/kotlin/com/lagradost/UASerialsProProvider.kt @@ -0,0 +1,297 @@ +package com.lagradost + +import com.google.gson.Gson +import com.google.gson.annotations.SerializedName +import com.google.gson.reflect.TypeToken +import com.lagradost.cloudstream3.AnimeSearchResponse +import com.lagradost.cloudstream3.DubStatus +import com.lagradost.cloudstream3.Episode +import com.lagradost.cloudstream3.HomePageResponse +import com.lagradost.cloudstream3.LoadResponse +import com.lagradost.cloudstream3.MainAPI +import com.lagradost.cloudstream3.MainPageRequest +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.addDubStatus +import com.lagradost.cloudstream3.addEpisodes +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.base64DecodeArray +import com.lagradost.cloudstream3.base64Encode +import com.lagradost.cloudstream3.mainPageOf +import com.lagradost.cloudstream3.newAnimeLoadResponse +import com.lagradost.cloudstream3.newAnimeSearchResponse +import com.lagradost.cloudstream3.newHomePageResponse +import com.lagradost.cloudstream3.newMovieLoadResponse +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.M3u8Helper +import com.lagradost.models.AESPlayerDecodedModel +import com.lagradost.models.DecodedJSON +import org.jsoup.nodes.Element +import javax.crypto.Cipher +import javax.crypto.SecretKeyFactory +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.PBEKeySpec +import javax.crypto.spec.SecretKeySpec + +class UASerialsProProvider : MainAPI() { + + // Basic Info + override var mainUrl = "https://uaserials.pro" + override var name = "UASerialsPro" + override val hasMainPage = true + override var lang = "uk" + override val hasDownloadSupport = true + override val supportedTypes = setOf( + TvType.TvSeries, + TvType.Cartoon, + TvType.Movie, + TvType.Anime + ) + + // Sections + override val mainPage = mainPageOf( + "$mainUrl/series/page/" to "Серіали", + "$mainUrl/cartoons/page/" to "Мультсеріали", + "$mainUrl/fcartoon/page/" to "Мультфільми", + "$mainUrl/anime/page/" to "Аніме", + "$mainUrl/films/page/" to "Фільми" + ) + + // Main Page + private val animeSelector = ".short-item" + private val titleSelector = "div.th-title.truncate" + private val engTitleSelector = "div.th-title-oname.truncate" + private val hrefSelector = ".short-item.width-16 .short-img" + private val posterSelector = ".img-fit img" + + // Load info + // private val titleLoadSelector = ".page__subcol-main h1" + // private val genresSelector = "li span:contains(Жанр:) a" + private val yearSelector = "a[href*=https://uaserials.pro/year/]" + // private val playerSelector = "iframe" + private val descriptionSelector = ".full-text" + // private val ratingSelector = ".pmovie__subrating img" + + private val listAESModel = object : TypeToken>() { }.type + private val listDecodedJSONModel = object : TypeToken>() { }.type + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val document = app.get(request.data + page).document + + val home = document.select(animeSelector).map { + it.toSearchResponse() + } + return newHomePageResponse(request.name, home) + } + + private fun Element.toSearchResponse(): AnimeSearchResponse { + val title = this.selectFirst(titleSelector)?.text()?.trim().toString() + val engTitle = this.selectFirst(engTitleSelector)?.text()?.trim().toString() + val href = this.selectFirst(hrefSelector)?.attr("href").toString() + val posterUrl = this.selectFirst(posterSelector)?.attr("data-src") + + return newAnimeSearchResponse(title, href, TvType.Anime) { + this.otherName = engTitle + this.posterUrl = posterUrl + addDubStatus(isDub = true) + } + + } + + override suspend fun search(query: String): List { + val document = app.post( + url = mainUrl, + data = mapOf( + "do" to "search", + "subaction" to "search", + "story" to query.replace(" ", "+") + ) + ).document + + return document.select(animeSelector).map { + it.toSearchResponse() + } + } + + // Detailed information + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document + // Parse info + + val title = document.select("span.oname_ua").text().trim().toString() + val engTitle = document.select(".pmovie__original-title").text() + val poster = document.selectFirst("div.fimg.img-wide img")?.attr("src") + val tags = mutableListOf() + val actors = mutableListOf() + val year = document.select(yearSelector).text().substringAfter(": ").substringBefore("-").toIntOrNull() + + document.select(".short-list li").forEach { menu -> + with(menu){ + when{ + this.select("span").text() == "Жанр:" -> menu.select("a").map { tags.add(it.text()) } + this.select("span").text() == "Актори:" -> menu.select("#text").text().split(", ").map { actors.add(it) } + } + } + } + + val tvType = with(tags){ + when{ + contains("Серіал") -> TvType.TvSeries + contains("Мультсеріал") -> TvType.Cartoon + contains("Фільм") -> TvType.Movie + contains("Мультфільм") -> TvType.Movie + contains("Аніме") -> TvType.Anime + else -> TvType.TvSeries + } + } + val description = document.selectFirst(descriptionSelector)?.text()?.trim() + // val rating = document.select(ratingSelector).next().text().toRatingInt() + + val recommendations = document.select(animeSelector).map { + it.toSearchResponse() + } + + val decryptData = cryptojsAESHandler( + Gson().fromJson( + document.select("div.fplayer player-control").attr("data-tag1"), + AesData::class.java, + ), + "297796CCB81D2551", + false + ).replace("\\", "").substringBeforeLast("]") + "]" + + val seriesJson = Gson().fromJson>(decryptData, listDecodedJSONModel) + val movieJson = Gson().fromJson>(decryptData, listAESModel) + + // Return to app + // Parse Episodes as Series + return if (seriesJson[0].seasons != null) { + val episodes = mutableListOf() + + seriesJson[0].seasons.forEachIndexed { seasonsIndex, season -> + season.episodes.forEachIndexed { episodesIndex, episode -> + episodes.add( + Episode( + "$seasonsIndex, $seasonsIndex, $url", + episode.title, + seasonsIndex + 1, + episodesIndex + 1, + ) + ) + } + } + + newAnimeLoadResponse(title, url, tvType) { + this.posterUrl = poster + this.engName = engTitle + this.year = year + this.plot = description + this.tags = tags + this.recommendations = recommendations + addEpisodes(DubStatus.Dubbed, episodes) + } + } else { // Parse as Movie. + newMovieLoadResponse(title, url, TvType.Movie, "$title, ${movieJson[0].url}") { + this.posterUrl = poster + this.name = engTitle + this.year = year + this.plot = description + this.tags = tags + this.recommendations = recommendations + } + } + } + + // It works when I click to view the series + override suspend fun loadLinks( + data: String, // (Serial) [Season Index, Episode Name, url] | (Film) [Title, Player Url] + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val dataList = data.split(", ") + // Movie + if(dataList.size == 2){ + val m3u8Url = app.get(dataList[1]).document.select("script").html() + .substringAfterLast("file:\"") + .substringBefore("\",") + M3u8Helper.generateM3u8( + source = dataList[0], + streamUrl = m3u8Url, + referer = "https://tortuga.wtf/" + ).forEach(callback) + return true + } + + // Serials + val document = app.get(dataList[2]).document + val decryptData = cryptojsAESHandler( + Gson().fromJson( + document.select("div.fplayer player-control").attr("data-tag1"), + AesData::class.java, + ), + "297796CCB81D2551", + false + ).replace("\\", "").substringBeforeLast("]") + "]" + + Gson().fromJson>(decryptData, listDecodedJSONModel)[0] + .seasons[dataList[0].toInt()].episodes[dataList[1].toInt()].sounds.forEach { episode -> + val m3u8Url = app.get(episode.url).document.select("script").html() + .substringAfterLast("file:\"") + .substringBefore("\",") + M3u8Helper.generateM3u8( + source = episode.title, + streamUrl = m3u8Url, + referer = "https://tortuga.wtf/" + ).forEach(callback) + } + return true + } + + // THANKS to Hexated! + // https://github.com/hexated/cloudstream-extensions-hexated/blob/master/OnetwothreeTv/src/main/kotlin/com/hexated/OnetwothreeTv.kt + private fun cryptojsAESHandler( + data: AesData, + pass: String, + encrypt: Boolean = true + ): String { + val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512") + val spec = PBEKeySpec(pass.toCharArray(), data.s.decodeHex(), 999, 256) + val key = factory.generateSecret(spec) + val cipher = Cipher.getInstance("AES/CBC/NoPadding") + return if (!encrypt) { + cipher.init( + Cipher.DECRYPT_MODE, + SecretKeySpec(key.encoded, "AES"), + IvParameterSpec(data.iv.decodeHex()) + ) + String(cipher.doFinal(base64DecodeArray(data.ct))) + } else { + cipher.init( + Cipher.ENCRYPT_MODE, + SecretKeySpec(key.encoded, "AES"), + IvParameterSpec(data.iv.decodeHex()) + ) + base64Encode(cipher.doFinal(data.ct.toByteArray())) + + } + } + + private fun String.decodeHex(): ByteArray { + check(length % 2 == 0) { "Must have an even length" } + return chunked(2) + .map { it.toInt(16).toByte() } + .toByteArray() + } + + data class AesData( + @SerializedName("ciphertext") val ct: String, + @SerializedName("salt") val s: String, + @SerializedName("iv") val iv: String, + @SerializedName("iterations") val iterations: Int = 999, + ) +} \ No newline at end of file diff --git a/UASerialsProProvider/src/main/kotlin/com/lagradost/UASerialsProProviderPlugin.kt b/UASerialsProProvider/src/main/kotlin/com/lagradost/UASerialsProProviderPlugin.kt new file mode 100644 index 0000000..dea5a74 --- /dev/null +++ b/UASerialsProProvider/src/main/kotlin/com/lagradost/UASerialsProProviderPlugin.kt @@ -0,0 +1,13 @@ +package com.lagradost + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class UASerialsProProviderPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(UASerialsProProvider()) + } +} \ No newline at end of file diff --git a/UASerialsProProvider/src/main/kotlin/com/lagradost/models/DecodedJSON.kt b/UASerialsProProvider/src/main/kotlin/com/lagradost/models/DecodedJSON.kt new file mode 100644 index 0000000..ac375d9 --- /dev/null +++ b/UASerialsProProvider/src/main/kotlin/com/lagradost/models/DecodedJSON.kt @@ -0,0 +1,35 @@ +package com.lagradost.models + +import com.google.gson.annotations.SerializedName + + +data class DecodedJSON ( + + @SerializedName("tabName") val tabName : String, + @SerializedName("seasons") val seasons : List +) + +data class Seasons ( + + @SerializedName("title") val title : String, + @SerializedName("episodes") val episodes : List +) + +data class Episodes ( + + @SerializedName("title") val title : String, + @SerializedName("sounds") val sounds : List +) + +data class Sounds ( + + @SerializedName("title") val title : String, + @SerializedName("url") val url : String +) + +data class AESPlayerDecodedModel ( + + @SerializedName("tabName") val tabName : String, + @SerializedName("url") val url : String +) + diff --git a/UASerialsProProvider/src/main/kotlin/com/lagradost/models/PlayerJson.kt b/UASerialsProProvider/src/main/kotlin/com/lagradost/models/PlayerJson.kt new file mode 100644 index 0000000..b0cbb53 --- /dev/null +++ b/UASerialsProProvider/src/main/kotlin/com/lagradost/models/PlayerJson.kt @@ -0,0 +1,24 @@ +package com.lagradost.models + +import com.google.gson.annotations.SerializedName + +data class PlayerJson ( + + val title : String, + val folder : List +) + +data class Season ( + + val title : String, + val folder : List +) + +data class Episode ( + + val title : String, + val file : String, + val id : String, + val poster : String, + val subtitle : String, +)