New source: Animes House (#764)

This commit is contained in:
Claudemirovsky
2022-08-14 17:12:59 -03:00
committed by GitHub
parent 289eb4e823
commit a165e03e30
17 changed files with 640 additions and 0 deletions

View File

@ -0,0 +1,24 @@
<?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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.animeextension.pt.animeshouse
object AHConstants {
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 MSG_ERR_BODY = "Erro ao obter dados do episódio."
const val PREFERRED_QUALITY = "preferred_quality"
const val DEFAULT_QUALITY = "720p"
const val PREFIX_SEARCH = "slug:"
val QUALITY_LIST = arrayOf("240p", "360p", "480p", "720p", "1080p")
}

View File

@ -0,0 +1,65 @@
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

@ -0,0 +1,42 @@
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 = AHConstants.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

@ -0,0 +1,317 @@
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.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", AHConstants.ACCEPT_LANGUAGE)
.add("User-Agent", AHConstants.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("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"))
return episode
}
// ============================ 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")
}
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 ->
val url = getPlayerUrl(player)
videoList.addAll(getPlayerVideos(url))
}
return videoList
}
private fun getPlayerVideos(url: String): List<Video> {
val iframeBody = client.newCall(GET(url, headers)).execute()
.body?.string() ?: throw Exception(AHConstants.MSG_ERR_BODY)
val unpackedBody = JsUnpacker.unpack(iframeBody)
return when {
"embed.php?" in url ->
EmbedExtractor(headers).getVideoList(url, iframeBody)
"edifier" in url ->
EdifierExtractor(client, headers).getVideoList(url, iframeBody)
"mp4doo" in url ->
MpFourDooExtractor(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 url = response.request.url.toString()
val document = response.asJsoup()
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(AHConstants.PREFIX_SEARCH)) {
val slug = query.removePrefix(AHConstants.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 {
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")
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() }
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.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 = AHConstants.PREFERRED_QUALITY
title = "Qualidade preferida"
entries = AHConstants.QUALITY_LIST
entryValues = AHConstants.QUALITY_LIST
setDefaultValue(AHConstants.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.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(AHConstants.PREFERRED_QUALITY, AHConstants.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
}
}

View File

@ -0,0 +1,26 @@
package eu.kanade.tachiyomi.animeextension.pt.animeshouse.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.POST
import okhttp3.Headers
import okhttp3.OkHttpClient
class EdifierExtractor(
private val client: OkHttpClient,
private val headers: Headers
) {
private val REGEX_EDIFIER = Regex(""""file":"(.*?)","label":"(\S+?)"""")
private val PLAYER_NAME = "EDIFIER"
fun getVideoList(url: String, iframeBody: String): List<Video> {
val apiUrl = url.replace("/v/", "/api/source/")
val req = client.newCall(POST(apiUrl)).execute()
val body = req.body?.string().orEmpty()
return REGEX_EDIFIER.findAll(body).map {
val videoUrl = it.groupValues.get(1).replace("\\", "")
val quality = "$PLAYER_NAME: " + it.groupValues.get(2)
Video(videoUrl, quality, videoUrl, headers)
}.toList()
}
}

View File

@ -0,0 +1,19 @@
package eu.kanade.tachiyomi.animeextension.pt.animeshouse.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import okhttp3.Headers
class EmbedExtractor(private val headers: Headers) {
private val REGEX_EMBED_PLAYER = Regex("""file: "(\S+)",\s+"label":"(\w+)"""")
private val PLAYER_NAME = "EmbedPlayer"
fun getVideoList(url: String, iframeBody: String): List<Video> {
val hostUrl = url.substringBefore("/embed")
return REGEX_EMBED_PLAYER.findAll(iframeBody).map {
val newUrl = "$hostUrl/${it.groupValues[1]}"
val quality = "$PLAYER_NAME: " + it.groupValues[2]
Video(newUrl, quality, newUrl, headers)
}.toList()
}
}

View File

@ -0,0 +1,40 @@
package eu.kanade.tachiyomi.animeextension.pt.animeshouse.extractors
import android.util.Log
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient
class GenericExtractor(
private val client: OkHttpClient,
private val headers: Headers
) {
private val REGEX_CLP_PLAYER = Regex("player\\('(\\S+)',")
private val REGEX_GCLOUD_PLAYER = "file\":\"(\\S+)\"".toRegex()
private val REGEX_QUALITY = Regex("(?<=RESOLUTION=)\\d+x(\\d+).*?\n(https.*)")
fun getVideoList(url: String, js: String): List<Video> {
val (player, regex) = when {
"gcloud" in url -> Pair("GCLOUD", REGEX_GCLOUD_PLAYER)
else -> Pair("CLP", REGEX_CLP_PLAYER)
}
Log.e(player, url)
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().orEmpty()
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
}
return listOf(Video(playlistUrl, player, playlistUrl, headers))
}
}

View File

@ -0,0 +1,39 @@
package eu.kanade.tachiyomi.animeextension.pt.animeshouse.extractors
object JsUnpacker {
private val REGEX_REPLACE = "\\b\\w+\\b".toRegex()
private val REGEX_EVAL = """\}\('(.*)',(\d+),(\d+),'(.*)'\.split""".toRegex()
private fun hasPacker(js: String): Boolean = REGEX_EVAL.containsMatchIn(js)
private fun getPackerArgs(js: String): List<String> = REGEX_EVAL.findAll(js)
.last().groupValues
private fun convert(base: Int, num: Int): String {
val firstPart = if (num < base) "" else (num / base).toString()
val calc = num % base
if (calc > 35)
return firstPart + (calc + 29).toChar().toString()
return firstPart + calc.toString(36)
}
fun unpack(js: String): String {
if (!hasPacker(js)) return js
val args = getPackerArgs(js)
val origJS = args[1]
val base = args[2].toInt()
val count = args[3].toInt()
val origList = args[4].split("|")
val replaceMap = (0..(count - 1)).map {
val key = convert(base, it)
key to try { origList[it] } catch (e: Exception) { key }
}.toMap()
val result = origJS.replace(REGEX_REPLACE) {
replaceMap.get(it.value) ?: it.value
}.replace("\\", "")
return unpack(result)
}
}

View File

@ -0,0 +1,28 @@
package eu.kanade.tachiyomi.animeextension.pt.animeshouse.extractors
import eu.kanade.tachiyomi.animeextension.pt.animeshouse.AHConstants
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient
class McpExtractor(
private val client: OkHttpClient,
private val headers: Headers
) {
private val REGEX_EP_ID = Regex("ss,\"(\\d+)\"")
private val REGEX_VIDEO_URL = Regex("h\":\"(\\S+?)\"")
private val API_URL = "https://clp-new.animeshouse.net/ah-clp-new"
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))
.execute()
val reqBody = req.body?.string() ?: throw Exception(AHConstants.MSG_ERR_BODY)
val videoUrl = REGEX_VIDEO_URL.find(reqBody)!!.groupValues
.get(1)
.replace("\\", "")
return listOf(Video(videoUrl, "default_mcp", videoUrl, headers))
}
}

View File

@ -0,0 +1,17 @@
package eu.kanade.tachiyomi.animeextension.pt.animeshouse.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import okhttp3.Headers
class MpFourDooExtractor(private val headers: Headers) {
private val REGEX_MPDOO = Regex("file\":\"(.*?)\"")
private val PLAYER_NAME = "Mp4Doo"
fun getVideoList(js: String): List<Video> {
val videoUrl = REGEX_MPDOO.find(js)!!.groupValues
.get(1)
.replace("fy..", "fy.v.")
return listOf(Video(videoUrl, PLAYER_NAME, videoUrl, headers))
}
}