Create DooPlay multisrc module (#1387)

This commit is contained in:
Claudemirovsky
2023-03-13 18:07:37 -03:00
committed by GitHub
parent 9e035ac1e1
commit b912cba295
61 changed files with 861 additions and 1736 deletions

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,97 @@
package eu.kanade.tachiyomi.animeextension.pt.animeshouse
import eu.kanade.tachiyomi.animeextension.pt.animeshouse.extractors.EdifierExtractor
import eu.kanade.tachiyomi.animeextension.pt.animeshouse.extractors.EmbedExtractor
import eu.kanade.tachiyomi.animeextension.pt.animeshouse.extractors.GenericExtractor
import eu.kanade.tachiyomi.animeextension.pt.animeshouse.extractors.JsUnpacker
import eu.kanade.tachiyomi.animeextension.pt.animeshouse.extractors.McpExtractor
import eu.kanade.tachiyomi.animeextension.pt.animeshouse.extractors.MpFourDooExtractor
import eu.kanade.tachiyomi.animeextension.pt.animeshouse.extractors.RedplayBypasser
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.multisrc.dooplay.DooPlay
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.FormBody
import okhttp3.Response
import org.jsoup.nodes.Element
class AnimesHouse : DooPlay(
"pt-BR",
"Animes House",
"https://animeshouse.net",
) {
override fun headersBuilder() = super.headersBuilder()
.add("Accept-Language", "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7")
// ============================== Popular ===============================
override fun popularAnimeSelector(): String = "div#featured-titles div.poster"
// =============================== Latest ===============================
override fun latestUpdatesNextPageSelector(): String = "div.resppages > a > span.icon-chevron-right"
// ============================ Video Links =============================
private fun getPlayerUrl(player: Element): String {
val body = FormBody.Builder()
.add("action", "doo_player_ajax")
.add("post", player.attr("data-post"))
.add("nume", player.attr("data-nume"))
.add("type", player.attr("data-type"))
.build()
return client.newCall(POST("$baseUrl/wp-admin/admin-ajax.php", headers, body))
.execute()
.use { it.asJsoup().selectFirst("iframe")!!.attr("src") }
.let {
if (it.startsWith("/redplay")) {
RedplayBypasser(client, headers).fromUrl(baseUrl + it)
} else {
it
}
}
}
override fun videoListParse(response: Response): List<Video> {
val players = response.use { it.asJsoup().select("ul#playeroptionsul li") }
return players.flatMap { player ->
runCatching {
val url = getPlayerUrl(player)
getPlayerVideos(url)
}.getOrDefault(emptyList<Video>())
}
}
private fun getPlayerVideos(url: String): List<Video> {
val iframeBody = client.newCall(GET(url, headers)).execute()
.use { it.body.string() }
val unpackedBody = JsUnpacker.unpack(iframeBody)
return when {
"embed.php?" in url ->
EmbedExtractor(headers).getVideoList(url, iframeBody)
"edifier" in url ->
EdifierExtractor(client, headers).getVideoList(url)
"mp4doo" in url ->
MpFourDooExtractor(client, headers).getVideoList(unpackedBody)
"clp-new" in url || "gcloud" in url ->
GenericExtractor(client, headers).getVideoList(url, unpackedBody)
"mcp_comm" in unpackedBody ->
McpExtractor(client, headers).getVideoList(unpackedBody)
else -> emptyList<Video>()
}
}
// ============================== Settings ==============================
override val PREF_QUALITY_ENTRIES = arrayOf(
"SD - 240p",
"SD - 360p",
"SD - 480p",
"HD - 720p",
"FULLHD - 1080p",
)
override val PREF_QUALITY_VALUES = arrayOf("240p", "360p", "480p", "720p", "1080p")
// ============================= Utilities ==============================
override val animeMenuSelector = "div.pag_episodes div.item a[href] i.icon-bars"
}

View File

@ -22,15 +22,16 @@ class GenericExtractor(
val playlistUrl = regex.find(js)!!.groupValues.get(1)
if ("m3u8.php" in playlistUrl) {
val req = client.newCall(GET(playlistUrl, headers)).execute()
val body = req.body.string()
val videos = REGEX_QUALITY.findAll(body).map {
val quality = "$player: " + it.groupValues.get(1) + "p"
val videoUrl = it.groupValues.get(2)
Video(videoUrl, quality, videoUrl, headers)
}.toList()
if (videos.size > 0) {
return videos
client.newCall(GET(playlistUrl, headers)).execute().use { req ->
val body = req.body.string()
val videos = REGEX_QUALITY.findAll(body).map {
val quality = "$player: " + it.groupValues.get(1) + "p"
val videoUrl = it.groupValues.get(2)
Video(videoUrl, quality, videoUrl, headers)
}.toList()
if (videos.size > 0) {
return videos
}
}
}

View File

@ -16,12 +16,15 @@ class McpExtractor(
fun getVideoList(js: String): List<Video> {
val epId = REGEX_EP_ID.find(js)!!.groupValues[1]
val req = client.newCall(GET("$API_URL/s_control.php?mid=$epId", headers))
val videoUrl = client.newCall(GET("$API_URL/s_control.php?mid=$epId", headers))
.execute()
val reqBody = req.body.string()
val videoUrl = REGEX_VIDEO_URL.find(reqBody)!!.groupValues
.get(1)
.replace("\\", "")
.use { req ->
val reqBody = req.body.string()
REGEX_VIDEO_URL.find(reqBody)!!.groupValues
.get(1)
.replace("\\", "")
}
return listOf(Video(videoUrl, "default_mcp", videoUrl, headers))
}
}

View File

@ -18,7 +18,7 @@ class MpFourDooExtractor(
.replace("fy..", "fy.v.")
return if (videoUrl.endsWith("playlist.m3u8")) {
val playlistBody = client.newCall(GET(videoUrl, headers)).execute()
.body.string()
.use { it.body.string() }
val separator = "#EXT-X-STREAM-INF:"
playlistBody.substringAfter(separator).split(separator).map {

View File

@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Response
class RedplayBypasser(
private val client: OkHttpClient,
@ -14,30 +15,35 @@ class RedplayBypasser(
) {
fun fromUrl(url: String): String {
val firstDoc = client.newCall(GET(url, headers)).execute().asJsoup()
val next = firstDoc.selectFirst("a")!!.attr("href")
var nextPage = client.newCall(GET(next, headers)).execute()
var iframeUrl = ""
var formUrl = next
while (iframeUrl == "") {
val nextDoc = nextPage.asJsoup(decodeAtob(nextPage.body.string()))
val iframe = nextDoc.selectFirst("iframe")
val next = client.newCall(GET(url, headers)).execute()
.use { it.asJsoup().selectFirst("a")!!.attr("href") }
val response = client.newCall(GET(next, headers)).execute()
return getIframeUrl(response, next)
}
private fun getIframeUrl(response: Response, formUrl: String): String {
return response.use { page ->
val document = page.asJsoup(decodeAtob(page.body.string()))
val iframe = document.selectFirst("iframe")
if (iframe != null) {
iframeUrl = iframe.attr("src")
iframe.attr("src")
} else {
val newHeaders = headers.newBuilder()
.set("Referer", formUrl)
.build()
val formBody = FormBody.Builder()
formUrl = nextDoc.selectFirst("form")!!.attr("action")
nextDoc.select("input[name]").forEach {
formBody.add(it.attr("name"), it.attr("value"))
}
nextPage = client.newCall(POST(formUrl, newHeaders, formBody.build()))
val formBody = FormBody.Builder().apply {
document.select("input[name]").forEach {
add(it.attr("name"), it.attr("value"))
}
}.build()
val nextForm = document.selectFirst("form")!!.attr("action")
val nextPage = client.newCall(POST(formUrl, newHeaders, formBody))
.execute()
getIframeUrl(nextPage, nextForm)
}
}
return iframeUrl
}
private fun decodeAtob(html: String): String {

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,62 @@
package eu.kanade.tachiyomi.animeextension.pt.cinevision
import eu.kanade.tachiyomi.animeextension.pt.cinevision.extractors.StreamlareExtractor
import eu.kanade.tachiyomi.animeextension.pt.cinevision.extractors.VidmolyExtractor
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.multisrc.dooplay.DooPlay
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Response
import org.jsoup.nodes.Element
import uy.kohesive.injekt.api.get
class CineVision : DooPlay(
"pt-BR",
"CineVision",
"https://cinevisionv6.net",
) {
// ============================== Popular ===============================
override fun popularAnimeSelector(): String = "article.w_item_b > a"
// =============================== Latest ===============================
override fun latestUpdatesNextPageSelector(): String = "div.resppages > a > span.fa-chevron-right"
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val players = document.select("ul#playeroptionsul li")
return players.flatMap(::getPlayerVideos)
}
private fun getPlayerVideos(player: Element): List<Video> {
val name = player.selectFirst("span.title")!!.text()
val url = getPlayerUrl(player)
return when {
"vidmoly.to" in url ->
VidmolyExtractor(client).getVideoList(url, name)
"streamlare.com" in url ->
StreamlareExtractor(client).videosFromUrl(url, name)
else -> emptyList<Video>()
}
}
private fun getPlayerUrl(player: Element): String {
val type = player.attr("data-type")
val id = player.attr("data-post")
val num = player.attr("data-nume")
return client.newCall(GET("$baseUrl/wp-json/dooplayer/v2/$id/$type/$num"))
.execute()
.use { response ->
response.body.string()
.substringAfter("\"embed_url\":\"")
.substringBefore("\",")
.replace("\\", "")
.let { "https:$it" }
}
}
// ============================= Utilities ==============================
override val animeMenuSelector = "div.pag_episodes div.item a[href] i.fa-bars"
override val genresListMessage = "Categoria"
}

View File

@ -13,7 +13,7 @@ class StreamlareExtractor(private val client: OkHttpClient) {
val body = "{\"id\":\"$id\"}".toRequestBody("application/json".toMediaType())
val playlist = client.newCall(
POST("https://sltube.org/api/video/stream/get", body = body),
).execute().body.string()
).execute().use { it.body.string() }
val separator = "\"label\":\""
return playlist.substringAfter(separator).split(separator).map {
val quality = "$name - " + it.substringAfter(separator).substringBefore("\",")

View File

@ -11,11 +11,11 @@ class VidmolyExtractor(private val client: OkHttpClient) {
fun getVideoList(url: String, lang: String): List<Video> {
val body = client.newCall(GET(url)).execute()
.body.string()
.use { it.body.string() }
val playlistUrl = REGEX_PLAYLIST.find(body)!!.groupValues.get(1)
val headers = Headers.headersOf("Referer", "https://vidmoly.to")
val playlistData = client.newCall(GET(playlistUrl, headers)).execute()
.body.string()
.use { it.body.string() }
val separator = "#EXT-X-STREAM-INF:"
return playlistData.substringAfter(separator).split(separator).map {

View File

@ -4,7 +4,7 @@
<application>
<activity
android:name=".pt.pifansubs.PFUrlActivity"
android:name="eu.kanade.tachiyomi.multisrc.dooplay.DooPlayUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
@ -15,9 +15,9 @@
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="pifansubs.com"
android:pathPattern="/tvshows/..*"
android:scheme="https" />
android:host="${SOURCEHOST}"
android:pathPattern="/.*/..*"
android:scheme="${SOURCESCHEME}" />
</intent-filter>
</activity>
</application>

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -0,0 +1,66 @@
package eu.kanade.tachiyomi.animeextension.en.pactedanime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.multisrc.dooplay.DooPlay
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Response
import org.jsoup.nodes.Element
class pactedanime : DooPlay(
"en",
"pactedanime",
"https://pactedanime.com",
) {
// ============================== Popular ===============================
override fun popularAnimeSelector(): String = latestUpdatesSelector()
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/trending/page/$page/")
override fun popularAnimeNextPageSelector() = latestUpdatesNextPageSelector()
// =============================== Latest ===============================
override fun latestUpdatesNextPageSelector(): String = "#nextpagination"
// ============================== Episodes ==============================
override fun episodeListParse(response: Response) = super.episodeListParse(response).reversed()
override fun getSeasonEpisodes(season: Element) = super.getSeasonEpisodes(season).reversed()
override fun episodeFromElement(element: Element, seasonName: String): SEpisode {
val episode = super.episodeFromElement(element, seasonName)
element.selectFirst("p:contains(Filler)")?.let {
episode.scanlator = "Filler Episode"
}
return episode
}
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val document = response.use { it.asJsoup() }
return document.select("div#playcontainer > div > div").mapNotNull {
if (!it.getElementsByTag("video").isEmpty()) {
runCatching {
val source = it.selectFirst("source")!!
val link = source.attr("src")
val quality = source.attr("label")
Video(link, quality, link)
}.getOrNull()
} else {
null
}
}
}
// ============================== Settings ==============================
override val PREF_QUALITY_VALUES = arrayOf("1080p", "720p", "480p", "360p", "240p")
override val PREF_QUALITY_ENTRIES = PREF_QUALITY_VALUES
// ============================= Utilities ==============================
override val animeMenuSelector = "div.pag_episodes div.item a[href] i.fa-bars"
}

View File

@ -0,0 +1,5 @@
dependencies {
implementation(project(':lib-streamsb-extractor'))
implementation("dev.datlag.jsunpacker:jsunpacker:1.0.1")
}

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -0,0 +1,75 @@
package eu.kanade.tachiyomi.animeextension.pt.pifansubs
import android.net.Uri
import eu.kanade.tachiyomi.animeextension.pt.pifansubs.extractors.AdoroDoramasExtractor
import eu.kanade.tachiyomi.animeextension.pt.pifansubs.extractors.GdrivePlayerExtractor
import eu.kanade.tachiyomi.animeextension.pt.pifansubs.extractors.JMVStreamExtractor
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor
import eu.kanade.tachiyomi.multisrc.dooplay.DooPlay
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
class PiFansubs : DooPlay(
"pt-BR",
"Pi Fansubs",
"https://pifansubs.org",
) {
override fun headersBuilder() = super.headersBuilder()
.add("Accept-Language", "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7")
override val PREF_QUALITY_VALUES = arrayOf("360p", "480p", "720p", "1080p")
override val PREF_QUALITY_ENTRIES = PREF_QUALITY_VALUES
// ============================== Popular ===============================
override fun popularAnimeSelector(): String = "div#featured-titles div.poster"
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val document = response.use { it.asJsoup() }
val players = document.select("div.source-box:not(#source-player-trailer) iframe")
return players.map(::getPlayerUrl).flatMap(::getPlayerVideos)
}
private fun getPlayerUrl(player: Element): String {
return player.attr("data-src").ifEmpty { player.attr("src") }.let {
when {
!it.startsWith("http") -> "https:" + it
else -> it
}
}
}
private fun getPlayerVideos(url: String): List<Video> {
val streamsbDomains = listOf("sbspeed", "sbanh", "streamsb", "sbfull", "sbbrisk")
return when {
"player.jmvstream" in url ->
JMVStreamExtractor(client).videosFromUrl(url)
"gdriveplayer." in url ->
GdrivePlayerExtractor(client).videosFromUrl(url)
streamsbDomains.any { it in url } ->
StreamSBExtractor(client).videosFromUrl(url, headers)
"adorodoramas.com" in url ->
AdoroDoramasExtractor(client).videosFromUrl(url)
"/jwplayer/?source" in url -> {
val videoUrl = Uri.parse(url).getQueryParameter("source")!!
listOf(Video(videoUrl, "JWPlayer", videoUrl, headers))
}
else -> emptyList<Video>()
}
}
// =========================== Anime Details ============================
override fun Document.getDescription(): String {
return select("$additionalInfoSelector p")
.eachText()
.joinToString("\n\n") + "\n"
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/episodes/page/$page", headers)
}

View File

@ -11,7 +11,7 @@ class AdoroDoramasExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String): List<Video> {
val body = client.newCall(GET(url)).execute()
.body.string()
.use { it.body.string() }
val unpacked = JsUnpacker.unpackAndCombine(body)
?.replace("\\", "")
?: return emptyList<Video>()
@ -19,7 +19,7 @@ class AdoroDoramasExtractor(private val client: OkHttpClient) {
return listStr.split("}").filter { it.isNotBlank() }.map {
val quality = it.substringAfter("label':'").substringBefore("'")
val videoUrl = it.substringAfter("file':'").substringBefore("'")
Video(url, "$PLAYER_NAME:$quality", videoUrl)
Video(url, "$PLAYER_NAME - $quality", videoUrl)
}
}
}

View File

@ -19,7 +19,7 @@ class GdrivePlayerExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String): List<Video> {
val body = client.newCall(GET(url.replace(".me", ".to"))).execute()
.body.string()
.use { it.body.string() }
val eval = JsUnpacker.unpackAndCombine(body)!!.replace("\\", "")
val json = Json.decodeFromString<JsonObject>(REGEX_DATAJSON.getFirst(eval))
val sojson = REGEX_SOJSON.getFirst(eval)

View File

@ -11,14 +11,18 @@ class JMVStreamExtractor(private val client: OkHttpClient) {
fun videosFromUrl(iframeUrl: String): List<Video> {
val iframeBody = client.newCall(GET(iframeUrl)).execute()
.body.string()
.use { it.body.string() }
val playlistUrl = REGEX_PLAYLIST.find(iframeBody)!!.groupValues.get(1)
val playlistData = client.newCall(GET(playlistUrl)).execute()
.body.string()
.use { it.body.string() }
val separator = "#EXT-X-STREAM-INF:"
return playlistData.substringAfter(separator).split(separator).map {
val quality = it.substringAfterLast("x") + "p"
val quality = it.substringAfter("RESOLUTION=")
.substringAfter("x")
.substringBefore("\n")
.substringBefore(",") + "p"
val path = it.substringAfter("\n").substringBefore("\n")
val url = playlistUrl.replace("playlist.m3u8", path)
Video(url, "$PLAYER_NAME - $quality", url)

View File

@ -0,0 +1,468 @@
package eu.kanade.tachiyomi.multisrc.dooplay
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.lang.Exception
import java.text.SimpleDateFormat
import java.util.Locale
/**
* Multisrc class for the DooPlay wordpress theme.
* This class takes some inspiration from Tachiyomi's Madara multisrc class.
*/
abstract class DooPlay(
override val lang: String,
override val name: String,
override val baseUrl: String,
) : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val supportsLatest = true
override val client: OkHttpClient = network.client
override fun headersBuilder() = super.headersBuilder().add("Referer", baseUrl)
protected open val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
companion object {
/**
* Useful for the URL intent handler.
*/
const val PREFIX_SEARCH = "path:"
}
protected open val PREF_QUALITY_DEFAULT = "720p"
protected open val PREF_QUALITY_KEY = "preferred_quality"
protected open val PREF_QUALITY_TITLE = when (lang) {
"pt-BR" -> "Qualidade preferida"
else -> "Preferred quality"
}
protected open val PREF_QUALITY_VALUES = arrayOf("480p", "720p")
protected open val PREF_QUALITY_ENTRIES = PREF_QUALITY_VALUES
protected open val VIDEO_SORT_PREF_KEY = PREF_QUALITY_KEY
protected open val VIDEO_SORT_PREF_DEFAULT = PREF_QUALITY_DEFAULT
// ============================== Popular ===============================
override fun popularAnimeSelector() = "article.w_item_a > a"
override fun popularAnimeRequest(page: Int) = GET(baseUrl)
override fun popularAnimeFromElement(element: Element): SAnime {
return SAnime.create().apply {
val img = element.selectFirst("img")!!
val url = element.selectFirst("a")?.attr("href") ?: element.attr("href")
setUrlWithoutDomain(url)
title = img.attr("alt")
thumbnail_url = img.getImageUrl()
}
}
override fun popularAnimeNextPageSelector(): String? = null
override fun popularAnimeParse(response: Response): AnimesPage {
fetchGenresList()
return super.popularAnimeParse(response)
}
// ============================== Episodes ==============================
override fun episodeListSelector() = "ul.episodios > li"
protected open val episodeNumberRegex by lazy { "(\\d+)$".toRegex() }
protected open val seasonListSelector = "div#seasons > div"
protected open val episodeDateSelector = ".date"
protected open val episodeMovieText = when (lang) {
"pt-BR" -> "Filme"
else -> "Movie"
}
protected open val episodeSeasonPrefix = when (lang) {
"pt-BR" -> "Temporada"
else -> "Season"
}
override fun episodeListParse(response: Response): List<SEpisode> {
val doc = response.use { getRealAnimeDoc(it.asJsoup()) }
val seasonList = doc.select(seasonListSelector)
return if (seasonList.size < 1) {
SEpisode.create().apply {
setUrlWithoutDomain(doc.location())
episode_number = 1F
name = episodeMovieText
}.let(::listOf)
} else {
seasonList.flatMap(::getSeasonEpisodes).reversed()
}
}
protected open fun getSeasonEpisodes(season: Element): List<SEpisode> {
val seasonName = season.selectFirst("span.se-t")!!.text()
return season.select(episodeListSelector()).map { element ->
episodeFromElement(element, seasonName)
}
}
override fun episodeFromElement(element: Element): SEpisode = throw Exception("not used")
protected open fun episodeFromElement(element: Element, seasonName: String): SEpisode {
return SEpisode.create().apply {
val epNum = element.selectFirst("div.numerando")!!.text()
.trim()
.let {
episodeNumberRegex.find(it)?.groupValues?.last() ?: "0"
}
val href = element.selectFirst("a[href]")!!
val episodeName = href.ownText()
episode_number = runCatching { epNum.toFloat() }.getOrDefault(0F)
date_upload = element.selectFirst(episodeDateSelector)
?.text()
?.toDate() ?: 0L
name = "$episodeSeasonPrefix $seasonName x $epNum - $episodeName"
setUrlWithoutDomain(href.attr("href"))
}
}
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> = throw Exception("not used")
override fun videoListSelector(): String = throw Exception("not used")
override fun videoFromElement(element: Element): Video = throw Exception("not used")
override fun videoUrlParse(document: Document): String = throw Exception("not used")
// =============================== Search ===============================
private fun searchAnimeByPathParse(response: Response): AnimesPage {
val details = animeDetailsParse(response)
return AnimesPage(listOf(details), false)
}
override fun searchAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val url = response.request.url.toString()
val animes = when {
"/?s=" in url -> { // Search by name.
document.select(searchAnimeSelector()).map { element ->
searchAnimeFromElement(element)
}
}
else -> { // Search by some kind of filter, like genres or popularity.
document.select(latestUpdatesSelector()).map { element ->
popularAnimeFromElement(element)
}
}
}
val hasNextPage = document.selectFirst(searchAnimeNextPageSelector()) != null
return AnimesPage(animes, hasNextPage)
}
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
return if (query.startsWith(PREFIX_SEARCH)) {
val path = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/$path", headers))
.asObservableSuccess()
.map { response ->
searchAnimeByPathParse(response)
}
} else {
super.fetchSearchAnime(page, query, filters)
}
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
return when {
query.isBlank() -> {
val genreUri = filters.asUriPart()
var url = "$baseUrl/$genreUri"
if (page > 1) url += "/page/$page"
GET(url, headers)
}
else -> GET("$baseUrl/page/$page/?s=$query", headers)
}
}
override fun searchAnimeFromElement(element: Element): SAnime {
return SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
val img = element.selectFirst("img")!!
title = img.attr("alt")
thumbnail_url = img.getImageUrl()
}
}
override fun searchAnimeNextPageSelector() = latestUpdatesNextPageSelector()
override fun searchAnimeSelector() = "div.result-item div.image a"
// =========================== Anime Details ============================
/**
* Selector for the element on the anime details page that have some
* additional info about the anime.
*
* @see [Element.getInfo]
*/
protected open val additionalInfoSelector = "div#info"
protected open val additionalInfoItems = when (lang) {
"pt-BR" -> listOf("Título", "Ano", "Temporadas", "Episódios")
else -> listOf("Original", "First", "Last", "Seasons", "Episodes")
}
protected open fun Document.getDescription(): String {
return selectFirst("$additionalInfoSelector p")
?.let { it.text() + "\n" }
?: ""
}
override fun animeDetailsParse(document: Document): SAnime {
val doc = getRealAnimeDoc(document)
val sheader = doc.selectFirst("div.sheader")!!
val anime = SAnime.create().apply {
setUrlWithoutDomain(doc.location())
sheader.selectFirst("div.poster > img")!!.let {
thumbnail_url = it.getImageUrl()
title = it.attr("alt").ifEmpty {
sheader.selectFirst("div.data > h1")!!.text()
}
}
genre = sheader.select("div.data > div.sgeneros > a")
.eachText()
.joinToString(", ")
doc.selectFirst(additionalInfoSelector)?.let { info ->
description = buildString {
append(doc.getDescription())
additionalInfoItems.forEach {
info.getInfo(it)?.let(::append)
}
}
}
}
return anime
}
// =============================== Latest ===============================
override fun latestUpdatesNextPageSelector() = "div.resppages > a > span.fa-chevron-right"
override fun latestUpdatesSelector() = "div.content article > div.poster"
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
protected open val latestUpdatesPath = when (lang) {
"pt-BR" -> "episodio"
else -> "episodes"
}
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/$latestUpdatesPath/page/$page", headers)
override fun latestUpdatesParse(response: Response): AnimesPage {
fetchGenresList()
return super.latestUpdatesParse(response)
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_VALUES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoQualityPref)
}
// ============================== Filters ===============================
/**
* Disable it if you don't want the genres to be fetched.
*/
protected open val fetchGenres = true
/**
* Automatically fetched genres from the source to be used in the filters.
*/
protected open lateinit var genresListFilter: AnimeFilter<*>
override fun getFilterList(): AnimeFilterList {
return if (this::genresListFilter.isInitialized) {
AnimeFilterList(
AnimeFilter.Header(genreFilterHeader),
genresListFilter,
)
} else if (fetchGenres) {
AnimeFilterList(AnimeFilter.Header(genresMissingWarning))
} else {
AnimeFilterList()
}
}
/**
* Fetch the genres from the source to be used in the filters.
*/
protected open fun fetchGenresList() {
if (!this::genresListFilter.isInitialized && fetchGenres) {
runCatching {
val filter = client.newCall(genresListRequest())
.execute()
.asJsoup()
.let(::genresListParse)
if ((filter as AnimeFilter.Select<*>).values.size > 0) {
genresListFilter = filter
}
}.onFailure { it.printStackTrace() }
}
}
/**
* The request to the page that have the genres list.
*/
protected open fun genresListRequest() = GET(baseUrl)
/**
* Get the genres from the document.
*/
protected open fun genresListParse(document: Document): AnimeFilter<*> {
val items = document.select(genresListSelector()).map {
val name = it.text()
val value = it.attr("href").substringAfter("$baseUrl/")
Pair(name, value)
}.toTypedArray()
return UriPartFilter(genresListMessage, items)
}
protected open val genreFilterHeader = when (lang) {
"pt-BR" -> "NOTA: Filtros serão ignorados se usar a pesquisa por nome!"
else -> "NOTE: Filters are going to be ignored if using search text!"
}
protected open val genresMissingWarning: String = when (lang) {
"pt-BR" -> "Aperte 'Redefinir' para tentar mostrar os gêneros"
else -> "Press 'Reset' to attempt to show the genres"
}
protected open val genresListMessage = when (lang) {
"pt-BR" -> "Gênero"
else -> "Genre"
}
protected open fun genresListSelector() = "li:contains(${genresListMessage}s) ul.sub-menu li > a"
open class UriPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
fun toUriPart() = vals[state].second
}
private fun AnimeFilterList.asUriPart(): String {
return this.filterIsInstance<UriPartFilter>().first().toUriPart()
}
// ============================= Utilities ==============================
/**
* The selector to the item in the menu (in episodes page) with the
* anime page url.
*/
protected open val animeMenuSelector = "div.pag_episodes div.item a[href] i.fa-bars"
/**
* If the document comes from a episode page, this function will get the
* real/expected document from the anime details page. else, it will return the
* original document.
*
* @return A document from a anime details page.
*/
protected open fun getRealAnimeDoc(document: Document): Document {
val menu = document.selectFirst(animeMenuSelector)
if (menu != null) {
val originalUrl = menu.parent()!!.attr("href")
val req = client.newCall(GET(originalUrl, headers)).execute()
return req.asJsoup()
} else {
return document
}
}
/**
* Tries to get additional info from an element at a anime details page,
* like how many seasons it have, the year it was aired, etc.
* Useful for anime description.
*/
protected open fun Element.getInfo(substring: String): String? {
val target = selectFirst("div.custom_fields:contains($substring)")
?: return null
val key = target.selectFirst("b")!!.text()
val value = target.selectFirst("span")!!.text()
return "\n$key: $value"
}
/**
* Tries to get the image url via various possible attributes.
* Taken from Tachiyomi's Madara multisrc.
*/
protected open fun Element.getImageUrl(): String? {
return when {
hasAttr("data-src") -> attr("abs:data-src")
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
hasAttr("srcset") -> attr("abs:srcset").substringBefore(" ")
else -> attr("abs:src")
}
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(VIDEO_SORT_PREF_KEY, VIDEO_SORT_PREF_DEFAULT)!!
return sortedWith(
compareBy { it.quality.lowercase().contains(quality.lowercase()) },
).reversed()
}
protected open val DATE_FORMATTER by lazy {
SimpleDateFormat("MMMM. dd, yyyy", Locale.ENGLISH)
}
private fun String.toDate(): Long {
return runCatching { DATE_FORMATTER.parse(trim())?.time }
.getOrNull() ?: 0L
}
}

View File

@ -0,0 +1,24 @@
package eu.kanade.tachiyomi.multisrc.dooplay
import generator.ThemeSourceData.SingleLang
import generator.ThemeSourceGenerator
class DooPlayGenerator : ThemeSourceGenerator {
override val themePkg = "dooplay"
override val themeClass = "Dooplay"
override val baseVersionCode = 1
override val sources = listOf(
SingleLang("Animes House", "https://animeshouse.net", "pt-BR", isNsfw = false, overrideVersionCode = 4),
SingleLang("CineVision", "https://cinevisionv6.net", "pt-BR", isNsfw = true, overrideVersionCode = 3),
SingleLang("pactedanime", "https://pactedanime.com", "en", isNsfw = false, overrideVersionCode = 4),
SingleLang("Pi Fansubs", "https://pifansubs.org", "pt-BR", isNsfw = true, overrideVersionCode = 11),
)
companion object {
@JvmStatic
fun main(args: Array<String>) = DooPlayGenerator().createAll()
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.animeextension.pt.pifansubs
package eu.kanade.tachiyomi.multisrc.dooplay
import android.app.Activity
import android.content.ActivityNotFoundException
@ -7,23 +7,20 @@ import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://pifansubs.com/tvshows/<slug> intents
* and redirects them to the main Aniyomi process.
*/
class PFUrlActivity : Activity() {
class DooPlayUrlActivity : Activity() {
private val TAG = "PFUrlActivity"
private val TAG = "DooPlayUrlActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val path = pathSegments[0]
val slug = pathSegments[1]
val searchQuery = PFConstants.PREFIX_SEARCH + slug
val searchQuery = "$path/$slug"
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", searchQuery)
putExtra("query", "${DooPlay.PREFIX_SEARCH}$searchQuery")
putExtra("filter", packageName)
}

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.animeextension" />

View File

@ -1,12 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'pactedanime'
pkgNameSuffix = 'en.pactedanime'
extClass = '.pactedanime'
extVersionCode = 4
libVersion = '13'
}
apply from: "$rootDir/common.gradle"

View File

@ -1,297 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.pactedanime
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.lang.Exception
class pactedanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "pactedanime"
override val baseUrl = "https://pactedanime.com"
override val lang = "en"
override val supportsLatest = false
override val client: OkHttpClient = network.cloudflareClient
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// Popular Anime
override fun popularAnimeSelector(): String = "div.content > div.items > article.item"
override fun popularAnimeRequest(page: Int): Request {
return GET("$baseUrl/trending/page/$page/")
}
override fun popularAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.select("div h3 a").attr("href").toHttpUrl().encodedPath)
anime.title = element.select("div h3 a").text()
anime.thumbnail_url = element.select("div.poster img").attr("src")
if (!anime.thumbnail_url.toString().startsWith("https://")) {
anime.thumbnail_url = element.select("div.poster img").attr("data-src")
}
return anime
}
override fun popularAnimeNextPageSelector(): String = "div.pagination span.current ~ a"
// Episodes
override fun episodeListSelector() = throw Exception("Not used")
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
val episodeList = mutableListOf<SEpisode>()
if (response.request.url.encodedPath.startsWith("/movies/")) {
val episode = SEpisode.create()
episode.name = document.select("div.data > h1").text()
episode.episode_number = 1F
episode.setUrlWithoutDomain(response.request.url.encodedPath)
episodeList.add(episode)
} else {
var counter = 1
for (season in document.select("div#seasons > div")) {
val seasonList = mutableListOf<SEpisode>()
for (ep in season.select("ul > li")) {
if (ep.childrenSize() > 0) {
val episode = SEpisode.create()
episode.name = "Season ${season.selectFirst("span.se-t")!!.text()} × ${ep.selectFirst("div.numerando")!!.ownText().substringAfter("E")} - ${ep.selectFirst("a[href]")!!.ownText()}"
episode.episode_number = counter.toFloat()
episode.setUrlWithoutDomain(ep.selectFirst("a[href]")!!.attr("href").toHttpUrl().encodedPath)
if (ep.selectFirst("p:contains(Filler)") != null) episode.scanlator = "Filler Episode"
seasonList.add(episode)
counter++
}
}
episodeList.addAll(seasonList.reversed())
}
}
return episodeList
}
override fun episodeFromElement(element: Element): SEpisode = throw Exception("Not used")
// Video urls
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val videoList = mutableListOf<Video>()
for (video in document.select("div#playcontainer > div > div")) {
if (!video.getElementsByTag("video").isEmpty()) {
videoList.add(
Video(
video.select("source").attr("src"),
video.select("source").attr("label"),
video.select("source").attr("src"),
),
)
}
}
return videoList.sort()
}
override fun videoListSelector() = throw Exception("Not used")
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", null)
if (quality != null) {
val newList = mutableListOf<Video>()
var preferred = 0
for (video in this) {
if (video.quality.contains(quality)) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
return newList
}
return this
}
override fun videoFromElement(element: Element) = throw Exception("Not used")
override fun videoUrlParse(document: Document) = throw Exception("Not used")
// search
override fun searchAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val animes = if (response.request.url.encodedPath.startsWith("/genre/")) {
document.select(searchGenreAnimeSelector()).map { element ->
searchGenreAnimeFromElement(element)
}
} else {
document.select(searchAnimeSelector()).map { element ->
searchAnimeFromElement(element)
}
}
val hasNextPage = searchAnimeNextPageSelector()?.let { selector ->
document.select(selector).first()
} != null
return AnimesPage(animes, hasNextPage)
}
override fun searchAnimeSelector(): String = "div.search-page > div.result-item"
override fun searchAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.select("div.details > div.title a").attr("href").toHttpUrl().encodedPath)
anime.title = element.select("div.details > div.title a").text()
anime.thumbnail_url = element.select("div.image img").attr("src")
if (!anime.thumbnail_url.toString().startsWith("https://")) {
anime.thumbnail_url = element.select("div.poster img").attr("data-src")
}
return anime
}
private fun searchGenreAnimeSelector(): String = "div.items > article.item"
private fun searchGenreAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.select("div.data h3 a").attr("href").toHttpUrl().encodedPath)
anime.title = element.select("div.data h3 a").text()
anime.thumbnail_url = element.select("div.poster img").attr("src")
if (!anime.thumbnail_url.toString().startsWith("https://")) {
anime.thumbnail_url = element.select("div.poster img").attr("data-src")
}
return anime
}
override fun searchAnimeNextPageSelector(): String = "div.pagination span.current ~ a"
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
return if (query.isNotBlank()) {
GET("$baseUrl/page/$page/?s=$query", headers)
} else {
val url = "$baseUrl/genre/".toHttpUrl().newBuilder()
filters.forEach { filter ->
when (filter) {
is GenreFilter -> url.addPathSegment(filter.toUriPart())
else -> {}
}
}
url.addPathSegment("page")
url.addPathSegment("$page")
GET(url.toString(), headers)
}
}
// Details
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
anime.title = document.select("div.sheader > div.data > h1").text()
anime.genre = document.select("div.sgeneros a").eachText().joinToString(separator = ", ")
anime.description = document.selectFirst("div#info p")!!.text()
return anime
}
// Latest
override fun latestUpdatesNextPageSelector(): String = throw Exception("Not used")
override fun latestUpdatesFromElement(element: Element): SAnime = throw Exception("Not used")
override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used")
override fun latestUpdatesSelector(): String = throw Exception("Not used")
// Filters
override fun getFilterList() = AnimeFilterList(
AnimeFilter.Header("NOTE: Ignored if using text search!"),
AnimeFilter.Separator(),
GenreFilter(getGenreList()),
)
private class GenreFilter(vals: Array<Pair<String, String>>) : UriPartFilter("Genres", vals)
private fun getGenreList() = arrayOf(
Pair("Action & Adventure", "action-adventure"),
Pair("Animation", "animation"),
Pair("Comedy", "comedy"),
Pair("Crime", "crime"),
Pair("Drama", "drama"),
Pair("Ecchi", "ecchi"),
Pair("Family", "family"),
Pair("Fantasy", "fantasy"),
Pair("Music", "music"),
Pair("Mystery", "mystery"),
Pair("Romance", "romance"),
Pair("Thriller", "thriller"),
Pair("Sci-Fi & Fantasy", "sci-fi-fantasy"),
Pair("War & Politics", "war-politics"),
)
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
// settings
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p", "240p")
entryValues = arrayOf("1080", "720", "480", "360", "240")
setDefaultValue("1080")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoQualityPref)
}
}

View File

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi.animeextension">
<application>
<activity
android:name=".pt.animeshouse.AHUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="animeshouse.net"
android:pathPattern="/anime/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,12 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Animes House'
pkgNameSuffix = 'pt.animeshouse'
extClass = '.AnimesHouse'
extVersionCode = 4
libVersion = '13'
}
apply from: "$rootDir/common.gradle"

View File

@ -1,65 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.animeshouse
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AHFilters {
open class QueryPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
fun toQueryPart() = vals[state].second
}
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return this.filterIsInstance<R>().joinToString("") {
(it as QueryPartFilter).toQueryPart()
}
}
class GenreFilter : QueryPartFilter("Gênero", AHFiltersData.genres)
val filterList = AnimeFilterList(
AnimeFilter.Header(AHFiltersData.IGNORE_SEARCH_MSG),
GenreFilter(),
)
data class FilterSearchParams(
val genre: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
return FilterSearchParams(filters.asQueryPart<GenreFilter>())
}
private object AHFiltersData {
const val IGNORE_SEARCH_MSG = "NOTA: O filtro por gênero será IGNORADO ao usar a pesquisa por nome."
val genres = arrayOf(
Pair("Ação", "acao"),
Pair("Aventura", "aventura"),
Pair("Artes Marciais", "artes-marciais"),
Pair("Drama", "drama"),
Pair("Ecchi", "ecchi"),
Pair("Escolar", "escolar"),
Pair("Esporte", "esporte"),
Pair("Fantasia", "fantasia"),
Pair("Ficção Científica", "ficcao-cientifica"),
Pair("Harém", "harem"),
Pair("Mecha", "mecha"),
Pair("Mistério", "misterio"),
Pair("Psicológico", "psicologico"),
Pair("Romance", "romance"),
Pair("Seinen", "seinen"),
Pair("Shounen", "shounen"),
Pair("Slice Of Life", "slice-of-life"),
Pair("Sobrenatural", "sobrenatural"),
Pair("Superpoderes", "superpoderes"),
)
}
}

View File

@ -1,42 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.animeshouse
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://animeshouse.net/anime/<slug> intents
* and redirects them to the main Aniyomi process.
*/
class AHUrlActivity : Activity() {
private val TAG = "AHUrlActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val slug = pathSegments[1]
val searchQuery = AnimesHouse.PREFIX_SEARCH + slug
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", searchQuery)
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(TAG, e.toString())
}
} else {
Log.e(TAG, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View File

@ -1,325 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.animeshouse
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.pt.animeshouse.extractors.EdifierExtractor
import eu.kanade.tachiyomi.animeextension.pt.animeshouse.extractors.EmbedExtractor
import eu.kanade.tachiyomi.animeextension.pt.animeshouse.extractors.GenericExtractor
import eu.kanade.tachiyomi.animeextension.pt.animeshouse.extractors.JsUnpacker
import eu.kanade.tachiyomi.animeextension.pt.animeshouse.extractors.McpExtractor
import eu.kanade.tachiyomi.animeextension.pt.animeshouse.extractors.MpFourDooExtractor
import eu.kanade.tachiyomi.animeextension.pt.animeshouse.extractors.RedplayBypasser
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.lang.Exception
class AnimesHouse : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Animes House"
override val baseUrl = "https://animeshouse.net"
override val lang = "pt-BR"
override val supportsLatest = true
override val client: OkHttpClient = network.client
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Referer", baseUrl)
.add("Accept-Language", ACCEPT_LANGUAGE)
.add("User-Agent", USER_AGENT)
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeSelector(): String = "div#featured-titles div.poster"
override fun popularAnimeRequest(page: Int): Request = GET(baseUrl)
override fun popularAnimeFromElement(element: Element): SAnime {
return SAnime.create().apply {
val img = element.selectFirst("img")!!
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
title = img.attr("alt")
thumbnail_url = img.attr("src")
}
}
override fun popularAnimeNextPageSelector() = null
// ============================== Episodes ==============================
override fun episodeListSelector(): String = "ul.episodios > li"
override fun episodeListParse(response: Response): List<SEpisode> {
val doc = getRealDoc(response.asJsoup())
val epList = doc.select(episodeListSelector())
return if (epList.size < 1) {
SEpisode.create().apply {
setUrlWithoutDomain(doc.location())
episode_number = 1F
name = "Filme"
}.let(::listOf)
} else {
epList.reversed().map { episodeFromElement(it) }
}
}
override fun episodeFromElement(element: Element): SEpisode {
return SEpisode.create().apply {
val origName = element.selectFirst("div.numerando")!!.text()
episode_number = origName.substringAfter("- ").toFloat() + if ("Dub" in origName) 0.5F else 0F
name = "Temp " + origName.replace(" - ", ": Ep ")
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
}
}
// ============================ Video Links =============================
private fun getPlayerUrl(player: Element): String {
val body = FormBody.Builder()
.add("action", "doo_player_ajax")
.add("post", player.attr("data-post"))
.add("nume", player.attr("data-nume"))
.add("type", player.attr("data-type"))
.build()
val doc = client.newCall(
POST("$baseUrl/wp-admin/admin-ajax.php", headers, body),
)
.execute()
.asJsoup()
val iframe = doc.selectFirst("iframe")!!
return iframe.attr("src").let {
if (it.startsWith("/redplay")) {
RedplayBypasser(client, headers).fromUrl(baseUrl + it)
} else {
it
}
}
}
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val players = document.select("ul#playeroptionsul li")
return players.flatMap { player ->
val url = getPlayerUrl(player)
runCatching { getPlayerVideos(url) }.getOrDefault(emptyList<Video>())
}
}
private fun getPlayerVideos(url: String): List<Video> {
val iframeBody = client.newCall(GET(url, headers)).execute()
.body.string()
val unpackedBody = JsUnpacker.unpack(iframeBody)
return when {
"embed.php?" in url ->
EmbedExtractor(headers).getVideoList(url, iframeBody)
"edifier" in url ->
EdifierExtractor(client, headers).getVideoList(url)
"mp4doo" in url ->
MpFourDooExtractor(client, headers).getVideoList(unpackedBody)
"clp-new" in url || "gcloud" in url ->
GenericExtractor(client, headers).getVideoList(url, unpackedBody)
"mcp_comm" in unpackedBody ->
McpExtractor(client, headers).getVideoList(unpackedBody)
else -> emptyList<Video>()
}
}
override fun videoListSelector() = throw Exception("not used")
override fun videoFromElement(element: Element) = throw Exception("not used")
override fun videoUrlParse(document: Document) = throw Exception("not used")
// =============================== Search ===============================
private fun searchAnimeBySlugParse(response: Response, slug: String): AnimesPage {
val details = animeDetailsParse(response)
details.url = "/anime/$slug"
return AnimesPage(listOf(details), false)
}
override fun searchAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val url = document.location()
val animes = when {
"/generos/" in url -> {
document.select(latestUpdatesSelector()).map { element ->
popularAnimeFromElement(element)
}
}
else -> {
document.select(searchAnimeSelector()).map { element ->
searchAnimeFromElement(element)
}
}
}
val hasNextPage = document.selectFirst(searchAnimeNextPageSelector()) != null
return AnimesPage(animes, hasNextPage)
}
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
return if (query.startsWith(PREFIX_SEARCH)) {
val slug = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/anime/$slug", headers))
.asObservableSuccess()
.map { response ->
searchAnimeBySlugParse(response, slug)
}
} else {
val params = AHFilters.getSearchParameters(filters)
client.newCall(searchAnimeRequest(page, query, params))
.asObservableSuccess()
.map { response ->
searchAnimeParse(response)
}
}
}
override fun getFilterList(): AnimeFilterList = AHFilters.filterList
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) = throw Exception("not used")
private fun searchAnimeRequest(page: Int, query: String, filters: AHFilters.FilterSearchParams): Request {
return when {
query.isBlank() -> {
val genre = filters.genre
var url = "$baseUrl/generos/$genre"
if (page > 1) url += "/page/$page"
GET(url, headers)
}
else -> GET("$baseUrl/page/$page/?s=$query", headers)
}
}
override fun searchAnimeFromElement(element: Element): SAnime {
return SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
title = element.text()
}
}
override fun searchAnimeNextPageSelector(): String = latestUpdatesNextPageSelector()
override fun searchAnimeSelector(): String = "div.result-item div.details div.title a"
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
val doc = getRealDoc(document)
val sheader = doc.selectFirst("div.sheader")!!
val anime = SAnime.create()
anime.thumbnail_url = sheader.selectFirst("div.poster > img")!!.attr("src")
anime.title = sheader.selectFirst("div.data > h1")!!.text()
anime.genre = sheader.select("div.data > div.sgeneros > a")
.joinToString(", ") { it.text() }
doc.selectFirst("div#info")?.let { info ->
var description = info.selectFirst("p")?.let { it.text() + "\n" } ?: ""
info.getInfo("Título")?.let { description += "$it" }
info.getInfo("Ano")?.let { description += "$it" }
info.getInfo("Temporadas")?.let { description += "$it" }
info.getInfo("Episódios")?.let { description += "$it" }
anime.description = description
}
return anime
}
// =============================== Latest ===============================
override fun latestUpdatesNextPageSelector(): String = "div.resppages > a > span.icon-chevron-right"
override fun latestUpdatesSelector(): String = "div.content article > div.poster"
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/episodio/page/$page", headers)
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_VALUES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoQualityPref)
}
// ============================= Utilities ==============================
private val animeMenuSelector = "div.pag_episodes div.item a[href] i.icon-bars"
private fun getRealDoc(document: Document): Document {
val menu = document.selectFirst(animeMenuSelector)
if (menu != null) {
val originalUrl = menu.parent()!!.attr("href")
val req = client.newCall(GET(originalUrl, headers)).execute()
return req.asJsoup()
} else {
return document
}
}
private fun Element.getInfo(substring: String): String? {
val target = this.selectFirst("div.custom_fields:contains($substring)")
?: return null
val key = target.selectFirst("b")!!.text()
val value = target.selectFirst("span")!!.text()
return "\n$key: $value"
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
}
companion object {
const val PREFIX_SEARCH = "slug:"
private const val ACCEPT_LANGUAGE = "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7"
private const val USER_AGENT = "Mozilla/5.0 (Linux; Android 10; SM-A307GT Build/QP1A.190711.020;) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/103.0.5060.71 Mobile Safari/537.36"
private const val PREF_QUALITY_DEFAULT = "720p"
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Qualidade preferida"
private val PREF_QUALITY_ENTRIES = arrayOf(
"SD - 240p",
"SD - 360p",
"SD - 480p",
"HD - 720p",
"FULLHD - 1080p",
)
private val PREF_QUALITY_VALUES = arrayOf("240p", "360p", "480p", "720p", "1080p")
}
}

View File

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi.animeextension">
<application>
<activity
android:name=".pt.cinevision.CVUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="cinevisionv5.online"
android:pathPattern="/serie/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,13 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'CineVision'
pkgNameSuffix = 'pt.cinevision'
extClass = '.CineVision'
extVersionCode = 3
libVersion = '13'
containsNsfw = true
}
apply from: "$rootDir/common.gradle"

View File

@ -1,63 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.cinevision
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object CVFilters {
open class QueryPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
fun toQueryPart() = vals[state].second
}
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return this.filterIsInstance<R>().joinToString("") {
(it as QueryPartFilter).toQueryPart()
}
}
class GenreFilter : QueryPartFilter("Gênero", CVFiltersData.genres)
val filterList = AnimeFilterList(
AnimeFilter.Header(CVFiltersData.IGNORE_SEARCH_MSG),
GenreFilter(),
)
data class FilterSearchParams(
val genre: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
return FilterSearchParams(filters.asQueryPart<GenreFilter>())
}
private object CVFiltersData {
const val IGNORE_SEARCH_MSG = "NOTA: O filtro por gênero será IGNORADO ao usar a pesquisa por nome."
val genres = arrayOf(
Pair("Ação", "acao-online-1"),
Pair("Animação", "animacao-online"),
Pair("Aventura", "aventura-online"),
Pair("Comédia", "comedia-online"),
Pair("Crime", "crime-online"),
Pair("Documentário", "documentario-online"),
Pair("Drama", "drama-online"),
Pair("Família", "familia-online"),
Pair("Fantasia", "fantasia-online"),
Pair("Faroeste", "faroeste-online"),
Pair("Ficção científica", "ficcao-cientifica-online"),
Pair("Guerra", "guerra-online"),
Pair("Mistério", "misterio-online"),
Pair("Música", "musica-online"),
Pair("Romance", "romance-online"),
Pair("Terror", "terror-online"),
Pair("Thriller", "thriller-online"),
)
}
}

View File

@ -1,42 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.cinevision
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://cinevisionv5.online.com/serie/<slug> intents
* and redirects them to the main Aniyomi process.
*/
class CVUrlActivity : Activity() {
private val TAG = "CVUrlActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val slug = pathSegments[1]
val searchQuery = CineVision.PREFIX_SEARCH + slug
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", searchQuery)
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(TAG, e.toString())
}
} else {
Log.e(TAG, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View File

@ -1,342 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.cinevision
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.pt.cinevision.extractors.StreamlareExtractor
import eu.kanade.tachiyomi.animeextension.pt.cinevision.extractors.VidmolyExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.lang.Exception
import java.text.SimpleDateFormat
import java.util.Locale
class CineVision : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "CineVision"
override val baseUrl = "https://cinevision.app"
override val lang = "pt-BR"
override val supportsLatest = true
override val client: OkHttpClient = network.client
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeSelector(): String = "article.w_item_b > a"
override fun popularAnimeRequest(page: Int): Request = GET(baseUrl, headers)
override fun popularAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
val img = element.selectFirst("img")!!
val url = element.selectFirst("a")?.attr("href") ?: element.attr("href")
anime.setUrlWithoutDomain(url)
anime.title = img.attr("alt")
anime.thumbnail_url = img.attr("data-src")
return anime
}
override fun popularAnimeNextPageSelector() = throw Exception("not used")
override fun popularAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val animes = document.select(popularAnimeSelector()).map { element ->
popularAnimeFromElement(element)
}
return AnimesPage(animes, false)
}
// ============================== Episodes ==============================
override fun episodeListSelector(): String = "ul.episodios > li"
override fun episodeListParse(response: Response): List<SEpisode> {
val doc = getRealDoc(response.asJsoup())
val epList = doc.select(episodeListSelector())
if (epList.size < 1) {
val episode = SEpisode.create()
episode.setUrlWithoutDomain(response.request.url.toString())
episode.episode_number = 1F
episode.name = "Filme"
return listOf(episode)
}
return epList.reversed().map { episodeFromElement(it) }
}
override fun episodeFromElement(element: Element): SEpisode {
val episode = SEpisode.create()
val origName = element.selectFirst("div.numerando")!!.text()
episode.episode_number = origName.substring(origName.indexOf("-") + 1)
.toFloat() + if ("Dub" in origName) 0.5F else 0F
episode.name = "Temp " + origName.replace(" - ", ": Ep ")
episode.setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
episode.date_upload = element.selectFirst("span.date")?.text()?.toDate() ?: 0L
return episode
}
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val players = document.select("ul#playeroptionsul li")
val videoList = mutableListOf<Video>()
players.forEach { player ->
videoList.addAll(getPlayerVideos(player))
}
return videoList
}
private fun getPlayerVideos(player: Element): List<Video> {
val type = player.attr("data-type")
val id = player.attr("data-post")
val num = player.attr("data-nume")
val name = player.selectFirst("span.title")!!.text()
val json = Json.decodeFromString<JsonObject>(
client.newCall(GET("$baseUrl/wp-json/dooplayer/v2/$id/$type/$num"))
.execute().body.string(),
)
val url = "https:" + json["embed_url"]!!.jsonPrimitive.content
return when {
"vidmoly.to" in url ->
VidmolyExtractor(client).getVideoList(url, name)
"streamlare.com" in url ->
StreamlareExtractor(client).videosFromUrl(url, name)
else -> emptyList<Video>()
}
}
override fun videoListSelector() = throw Exception("not used")
override fun videoFromElement(element: Element) = throw Exception("not used")
override fun videoUrlParse(document: Document) = throw Exception("not used")
// =============================== Search ===============================
private fun searchAnimeBySlugParse(response: Response, slug: String): AnimesPage {
val details = animeDetailsParse(response)
details.url = "/serie/$slug"
return AnimesPage(listOf(details), false)
}
override fun searchAnimeParse(response: Response): AnimesPage {
val url = response.request.url.toString()
val document = response.asJsoup()
val animes = when {
"/categorias/" in url -> {
document.select(latestUpdatesSelector()).map { element ->
popularAnimeFromElement(element)
}
}
else -> {
document.select(searchAnimeSelector()).map { element ->
searchAnimeFromElement(element)
}
}
}
val hasNextPage = document.selectFirst(searchAnimeNextPageSelector()) != null
return AnimesPage(animes, hasNextPage)
}
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
return if (query.startsWith(PREFIX_SEARCH)) {
val slug = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/serie/$slug", headers))
.asObservableSuccess()
.map { response ->
searchAnimeBySlugParse(response, slug)
}
} else {
val params = CVFilters.getSearchParameters(filters)
client.newCall(searchAnimeRequest(page, query, params))
.asObservableSuccess()
.map { response ->
searchAnimeParse(response)
}
}
}
override fun getFilterList(): AnimeFilterList = CVFilters.filterList
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) = throw Exception("not used")
private fun searchAnimeRequest(page: Int, query: String, filters: CVFilters.FilterSearchParams): Request {
return when {
query.isBlank() -> {
val genre = filters.genre
var url = "$baseUrl/categorias/assistir-filmes-e-series-de-$genre"
if (page > 1) url += "/page/$page"
GET(url, headers)
}
else -> GET("$baseUrl/page/$page/?s=$query", headers)
}
}
override fun searchAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.attr("href"))
val img = element.selectFirst("img")!!
anime.title = img.attr("alt")
anime.thumbnail_url = img.attr("data-src").replace("/p/w92", "/p/w185")
return anime
}
override fun searchAnimeNextPageSelector(): String = latestUpdatesNextPageSelector()
override fun searchAnimeSelector(): String = "div.result-item div.image a"
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
val doc = getRealDoc(document)
val sheader = doc.selectFirst("div.sheader")!!
val img = sheader.selectFirst("div.poster > img")!!
anime.thumbnail_url = img.attr("abs:data-src")
anime.title = sheader.selectFirst("div.data > h1")!!.text()
anime.genre = sheader.select("div.data > div.sgeneros > a")
.joinToString(", ") { it.text() }
val info = doc.selectFirst("div#info")!!
var description = info.selectFirst("p")!!.text() + "\n"
info.getInfo("Título")?.let { description += "$it" }
info.getInfo("Ano")?.let { description += "$it" }
info.getInfo("Temporadas")?.let { description += "$it" }
info.getInfo("Episódios")?.let { description += "$it" }
anime.description = description
return anime
}
// =============================== Latest ===============================
override fun latestUpdatesNextPageSelector(): String = "div.resppages > a > span.fa-chevron-right"
override fun latestUpdatesSelector(): String = "div#contenedor article > div.poster"
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/episodio/page/$page/", headers)
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = PREFERRED_QUALITY
title = "Qualidade preferida"
entries = QUALITY_LIST
entryValues = QUALITY_LIST
setDefaultValue(DEFAULT_QUALITY)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val languagePref = ListPreference(screen.context).apply {
key = PREFERRED_LANGUAGE
title = "Tipo/Língua preferida"
entries = LANGUAGE_LIST
entryValues = LANGUAGE_LIST
setDefaultValue(DEFAULT_LANGUAGE)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoQualityPref)
screen.addPreference(languagePref)
}
// ============================= Utilities ==============================
private val animeMenuSelector = "div.pag_episodes div.item a[href] > i.fa-bars"
private fun getRealDoc(document: Document): Document {
val menu = document.selectFirst(animeMenuSelector)
if (menu != null) {
val originalUrl = menu.parent()!!.attr("href")
val req = client.newCall(GET(originalUrl, headers)).execute()
return req.asJsoup()
} else {
return document
}
}
private fun Element.getInfo(substring: String): String? {
val target = this.selectFirst("div.custom_fields:contains($substring)")
?: return null
val key = target.selectFirst("b")!!.text()
val value = target.selectFirst("span")!!.text()
return "\n$key: $value"
}
private fun String.toDate(): Long {
return runCatching { DATE_FORMATTER.parse(trim())?.time }
.getOrNull() ?: 0L
}
private fun List<Video>.sortIfContains(item: String): List<Video> {
val newList = mutableListOf<Video>()
var preferred = 0
for (video in this) {
if (item in video.quality) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
return newList
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREFERRED_QUALITY, DEFAULT_QUALITY)!!
val language = preferences.getString(PREFERRED_LANGUAGE, DEFAULT_LANGUAGE)!!
val newList = sortIfContains(language).sortIfContains(quality)
return newList
}
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("MMMM. dd, yyyy", Locale.ENGLISH)
}
const val PREFIX_SEARCH = "slug:"
private const val PREFERRED_QUALITY = "preferred_quality"
private const val DEFAULT_QUALITY = "720p"
private val QUALITY_LIST = arrayOf("480p", "720p")
private const val PREFERRED_LANGUAGE = "preferred_language"
private const val DEFAULT_LANGUAGE = "Legendado"
private val LANGUAGE_LIST = arrayOf("Legendado", "Dublado")
}
}

View File

@ -1,18 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Pi Fansubs'
pkgNameSuffix = 'pt.pifansubs'
extClass = '.PiFansubs'
extVersionCode = 11
libVersion = '13'
containsNsfw = true
}
dependencies {
implementation(project(':lib-streamsb-extractor'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
}
apply from: "$rootDir/common.gradle"

View File

@ -1,10 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.pifansubs
object PFConstants {
const val ACCEPT_LANGUAGE = "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7"
const val USER_AGENT = "Mozilla/5.0 (Linux; Android 10; SM-A307GT Build/QP1A.190711.020;) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/103.0.5060.71 Mobile Safari/537.36"
const val PREFIX_SEARCH = "slug:"
const val PREFERRED_QUALITY = "preferred_quality"
const val DEFAULT_QUALITY = "720p"
val QUALITY_LIST = arrayOf("360p", "720p")
}

View File

@ -1,73 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.pifansubs
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object PFFilters {
open class QueryPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
fun toQueryPart() = vals[state].second
}
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return this.filterIsInstance<R>().joinToString("") {
(it as QueryPartFilter).toQueryPart()
}
}
class GenreFilter : QueryPartFilter("Gênero", PFFiltersData.genres)
val filterList = AnimeFilterList(
AnimeFilter.Header(PFFiltersData.IGNORE_SEARCH_MSG),
GenreFilter(),
)
data class FilterSearchParams(
val genre: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
return FilterSearchParams(filters.asQueryPart<GenreFilter>())
}
private object PFFiltersData {
const val IGNORE_SEARCH_MSG = "NOTA: O filtro por gênero será IGNORADO ao usar a pesquisa por nome."
val genres = arrayOf(
Pair("+18", "18"),
Pair("Adulto", "adulto"),
Pair("Animação", "animacao"),
Pair("Anime", "anime"),
Pair("Aventura", "aventura"),
Pair("Ação e Aventura", "adventure"),
Pair("Ação", "acao"),
Pair("Comédia", "comedia"),
Pair("Crime", "crime"),
Pair("Drama", "drama"),
Pair("Escolar", "escolar"),
Pair("Família", "familia"),
Pair("Fantasia", "fantasia"),
Pair("Ficção científica", "cientifica"),
Pair("Guerra", "guerra"),
Pair("Histórico", "historia"),
Pair("Mistério", "misterio"),
Pair("Musical", "musica"),
Pair("Programa de Tv", "news"),
Pair("Reality", "reality"),
Pair("Romance", "romance"),
Pair("Sci-Fi & Fantasy", "fantasy"),
Pair("Shounen-Ai", "ai"),
Pair("Sobrenatural", "sobrenatural"),
Pair("Terror", "terror"),
Pair("Thriller", "thriller"),
Pair("Yaoi", "yaoi"),
)
}
}

View File

@ -1,319 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.pifansubs
import android.app.Application
import android.content.SharedPreferences
import android.net.Uri
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.pt.pifansubs.extractors.AdoroDoramasExtractor
import eu.kanade.tachiyomi.animeextension.pt.pifansubs.extractors.GdrivePlayerExtractor
import eu.kanade.tachiyomi.animeextension.pt.pifansubs.extractors.JMVStreamExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.lang.Exception
import java.text.SimpleDateFormat
import java.util.Locale
class PiFansubs : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Pi Fansubs"
override val baseUrl = "https://pifansubs.org"
override val lang = "pt-BR"
override val supportsLatest = true
override val client: OkHttpClient = network.client
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Referer", baseUrl)
.add("Accept-Language", PFConstants.ACCEPT_LANGUAGE)
.add("User-Agent", PFConstants.USER_AGENT)
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeSelector(): String = "div#featured-titles div.poster"
override fun popularAnimeRequest(page: Int): Request = GET(baseUrl, headers)
override fun popularAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
val img = element.selectFirst("img")!!
anime.setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
anime.title = img.attr("alt")
anime.thumbnail_url = img.attr("abs:data-src")
return anime
}
override fun popularAnimeNextPageSelector() = throw Exception("not used")
override fun popularAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val animes = document.select(popularAnimeSelector()).map { element ->
popularAnimeFromElement(element)
}
return AnimesPage(animes, false)
}
// ============================== Episodes ==============================
override fun episodeListSelector(): String = "ul.episodios > li"
override fun episodeListParse(response: Response): List<SEpisode> {
val doc = getRealDoc(response.asJsoup())
val epList = doc.select(episodeListSelector())
if (epList.size < 1) {
val episode = SEpisode.create()
episode.setUrlWithoutDomain(response.request.url.toString())
episode.episode_number = 1F
episode.name = "Filme"
return listOf(episode)
}
return epList.reversed().map { episodeFromElement(it) }
}
override fun episodeFromElement(element: Element): SEpisode {
val episode = SEpisode.create()
val origName = element.selectFirst("div.numerando")!!.text()
episode.episode_number = origName.substring(origName.indexOf("-") + 1)
.toFloat() + if ("Dub" in origName) 0.5F else 0F
episode.name = "Temp " + origName.replace(" - ", ": Ep ")
episode.setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
episode.date_upload = element.selectFirst("span.date")?.text()?.toDate() ?: 0L
return episode
}
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val players = document.select("div.source-box:not(#source-player-trailer) iframe")
return players.flatMap { player ->
val url = player.attr("data-src").ifEmpty { player.attr("src") }.let {
if (!it.startsWith("http")) {
"https:" + it
} else {
it
}
}
getPlayerVideos(url)
}
}
private fun getPlayerVideos(url: String): List<Video> {
val streamsbDomains = listOf("sbspeed", "sbanh", "streamsb", "sbfull")
return when {
"player.jmvstream" in url ->
JMVStreamExtractor(client).videosFromUrl(url)
"gdriveplayer." in url ->
GdrivePlayerExtractor(client).videosFromUrl(url)
streamsbDomains.any { it in url } ->
StreamSBExtractor(client).videosFromUrl(url, headers)
"adorodoramas.com" in url ->
AdoroDoramasExtractor(client).videosFromUrl(url)
"/jwplayer/?source" in url -> {
val videoUrl = Uri.parse(url).getQueryParameter("source")!!
listOf(Video(videoUrl, "JWPlayer", videoUrl))
}
else -> emptyList<Video>()
}
}
override fun videoListSelector() = throw Exception("not used")
override fun videoFromElement(element: Element) = throw Exception("not used")
override fun videoUrlParse(document: Document) = throw Exception("not used")
// =============================== Search ===============================
private fun searchAnimeBySlugParse(response: Response, slug: String): AnimesPage {
val details = animeDetailsParse(response)
details.url = "/tvshows/$slug"
return AnimesPage(listOf(details), false)
}
override fun searchAnimeParse(response: Response): AnimesPage {
val url = response.request.url.toString()
val document = response.asJsoup()
val animes = when {
"/genre/" in url -> {
document.select(latestUpdatesSelector()).map { element ->
popularAnimeFromElement(element)
}
}
else -> {
document.select(searchAnimeSelector()).map { element ->
searchAnimeFromElement(element)
}
}
}
val hasNextPage = document.selectFirst(searchAnimeNextPageSelector()) != null
return AnimesPage(animes, hasNextPage)
}
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
return if (query.startsWith(PFConstants.PREFIX_SEARCH)) {
val slug = query.removePrefix(PFConstants.PREFIX_SEARCH)
client.newCall(GET("$baseUrl/tvshows/$slug", headers))
.asObservableSuccess()
.map { response ->
searchAnimeBySlugParse(response, slug)
}
} else {
val params = PFFilters.getSearchParameters(filters)
client.newCall(searchAnimeRequest(page, query, params))
.asObservableSuccess()
.map { response ->
searchAnimeParse(response)
}
}
}
override fun getFilterList(): AnimeFilterList = PFFilters.filterList
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) = throw Exception("not used")
private fun searchAnimeRequest(page: Int, query: String, filters: PFFilters.FilterSearchParams): Request {
return when {
query.isBlank() -> {
val genre = filters.genre
var url = "$baseUrl/genre/$genre"
if (page > 1) url += "/page/$page"
GET(url, headers)
}
else -> GET("$baseUrl/page/$page/?s=$query", headers)
}
}
override fun searchAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.attr("href"))
anime.title = element.text()
return anime
}
override fun searchAnimeNextPageSelector(): String = latestUpdatesNextPageSelector()
override fun searchAnimeSelector(): String = "div.result-item div.details div.title a"
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
val doc = getRealDoc(document)
val sheader = doc.selectFirst("div.sheader")!!
val img = sheader.selectFirst("div.poster > img")!!
anime.thumbnail_url = img.attr("data-src")
anime.title = sheader.selectFirst("div.data > h1")!!.text()
anime.genre = sheader.select("div.data > div.sgeneros > a")
.joinToString(", ") { it.text() }
val info = doc.selectFirst("div#info")!!
var description = info.select("p").joinToString("\n\n") + "\n"
info.getInfo("Título")?.let { description += "$it" }
info.getInfo("Ano")?.let { description += "$it" }
info.getInfo("Temporadas")?.let { description += "$it" }
info.getInfo("Episódios")?.let { description += "$it" }
anime.description = description
return anime
}
// =============================== Latest ===============================
override fun latestUpdatesNextPageSelector(): String = "div.resppages > a > span.fa-chevron-right"
override fun latestUpdatesSelector(): String = "div.content article > div.poster"
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/episodes/page/$page", headers)
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = PFConstants.PREFERRED_QUALITY
title = "Qualidade preferida"
entries = PFConstants.QUALITY_LIST
entryValues = PFConstants.QUALITY_LIST
setDefaultValue(PFConstants.DEFAULT_QUALITY)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoQualityPref)
}
// ============================= Utilities ==============================
private val animeMenuSelector = "div.pag_episodes div.item a[href] i.fa-bars"
private fun getRealDoc(document: Document): Document {
val menu = document.selectFirst(animeMenuSelector)
if (menu != null) {
val originalUrl = menu.parent()!!.attr("href")
val req = client.newCall(GET(originalUrl, headers)).execute()
return req.asJsoup()
} else {
return document
}
}
private fun Element.getInfo(substring: String): String? {
val target = this.selectFirst("div.custom_fields:contains($substring)")
?: return null
return runCatching {
val key = target.selectFirst("b")!!.text()
val value = target.selectFirst("span")!!.text()
"\n$key: $value"
}.getOrNull()
}
private fun String.toDate(): Long {
return runCatching { DATE_FORMATTER.parse(trim())?.time }
.getOrNull() ?: 0L
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PFConstants.PREFERRED_QUALITY, PFConstants.DEFAULT_QUALITY)!!
val newList = mutableListOf<Video>()
var preferred = 0
for (video in this) {
if (quality in video.quality) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
return newList
}
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("MMMM. dd, yyyy", Locale.ENGLISH)
}
}
}