fix(src/pt): Removed dead sources #681

Merged
WebDitto merged 1 commit from fix/pt/dead-sources into main 2025-02-16 16:26:18 -06:00
48 changed files with 0 additions and 2676 deletions

View file

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".pt.animestc.AnimesTCUrlActivity"
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="www.animestc.net"
android:pathPattern="/animes/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -1,11 +0,0 @@
ext {
extName = 'AnimesTC'
extClass = '.AnimesTC'
extVersionCode = 7
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:googledrive-extractor"))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,174 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.animestc
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object ATCFilters {
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 (first { it is R } as QueryPartFilter).toQueryPart()
}
class TypeFilter : QueryPartFilter("Tipo", ATCFiltersData.TYPES)
class YearFilter : QueryPartFilter("Ano", ATCFiltersData.YEARS)
class GenreFilter : QueryPartFilter("Gênero", ATCFiltersData.GENRES)
class StatusFilter : QueryPartFilter("Status", ATCFiltersData.STATUS)
val FILTER_LIST get() = AnimeFilterList(
TypeFilter(),
YearFilter(),
GenreFilter(),
StatusFilter(),
)
data class FilterSearchParams(
val type: String = "series",
val year: String = "",
val genre: String = "",
val status: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.asQueryPart<TypeFilter>(),
filters.asQueryPart<YearFilter>(),
filters.asQueryPart<GenreFilter>(),
filters.asQueryPart<StatusFilter>(),
)
}
private object ATCFiltersData {
val TYPES = arrayOf(
Pair("Anime", "series"),
Pair("Filme", "movie"),
Pair("OVA", "ova"),
)
val SELECT = Pair("Selecione", "")
val STATUS = arrayOf(
SELECT,
Pair("Cancelado", "canceled"),
Pair("Completo", "complete"),
Pair("Em Lançamento", "airing"),
Pair("Pausado", "onhold"),
)
val YEARS = arrayOf(SELECT) + (1997..2024).map {
Pair(it.toString(), it.toString())
}.toTypedArray()
val GENRES = arrayOf(
SELECT,
Pair("Ação", "acao"),
Pair("Action", "action"),
Pair("Adventure", "adventure"),
Pair("Artes Marciais", "artes-marciais"),
Pair("Artes Marcial", "artes-marcial"),
Pair("Aventura", "aventura"),
Pair("Beisebol", "beisebol"),
Pair("Boys Love", "boys-love"),
Pair("Comédia", "comedia"),
Pair("Comédia Romântica", "comedia-romantica"),
Pair("Comedy", "comedy"),
Pair("Crianças", "criancas"),
Pair("Culinária", "culinaria"),
Pair("Cyberpunk", "cyberpunk"),
Pair("Demônios", "demonios"),
Pair("Distopia", "distopia"),
Pair("Documentário", "documentario"),
Pair("Drama", "drama"),
Pair("Ecchi", "ecchi"),
Pair("Escola", "escola"),
Pair("Escolar", "escolar"),
Pair("Espaço", "espaco"),
Pair("Esporte", "esporte"),
Pair("Esportes", "esportes"),
Pair("Fantasia", "fantasia"),
Pair("Ficção Científica", "ficcao-cientifica"),
Pair("Futebol", "futebol"),
Pair("Game", "game"),
Pair("Girl battleships", "girl-battleships"),
Pair("Gourmet", "gourmet"),
Pair("Gundam", "gundam"),
Pair("Harém", "harem"),
Pair("Hentai", "hentai"),
Pair("Historia", "historia"),
Pair("Historial", "historial"),
Pair("Historical", "historical"),
Pair("Histórico", "historico"),
Pair("Horror", "horror"),
Pair("Humor Negro", "humor-negro"),
Pair("Ídolo", "idolo"),
Pair("Infantis", "infantis"),
Pair("Investigação", "investigacao"),
Pair("Isekai", "isekai"),
Pair("Jogo", "jogo"),
Pair("Jogos", "jogos"),
Pair("Josei", "josei"),
Pair("Kids", "kids"),
Pair("Luta", "luta"),
Pair("Maduro", "maduro"),
Pair("Máfia", "mafia"),
Pair("Magia", "magia"),
Pair("Mágica", "magica"),
Pair("Mecha", "mecha"),
Pair("Militar", "militar"),
Pair("Militares", "militares"),
Pair("Mistério", "misterio"),
Pair("Música", "musica"),
Pair("Musical", "musical"),
Pair("Não Informado!", "nao-informado"),
Pair("Paródia", "parodia"),
Pair("Piratas", "piratas"),
Pair("Polícia", "policia"),
Pair("Policial", "policial"),
Pair("Político", "politico"),
Pair("Pós-Apocalíptico", "pos-apocaliptico"),
Pair("Psico", "psico"),
Pair("Psicológico", "psicologico"),
Pair("Romance", "romance"),
Pair("Samurai", "samurai"),
Pair("Samurais", "samurais"),
Pair("Sátiro", "satiro"),
Pair("School Life", "school-life"),
Pair("SciFi", "scifi"),
Pair("Sci-Fi", "sci-fi"),
Pair("Seinen", "seinen"),
Pair("Shotacon", "shotacon"),
Pair("Shoujo", "shoujo"),
Pair("Shoujo Ai", "shoujo-ai"),
Pair("Shounem", "shounem"),
Pair("Shounen", "shounen"),
Pair("Shounen-ai", "shounen-ai"),
Pair("Slice of Life", "slice-of-life"),
Pair("Sobrenatural", "sobrenatural"),
Pair("Space", "space"),
Pair("Supernatural", "supernatural"),
Pair("Super Poder", "super-poder"),
Pair("Super-Poderes", "super-poderes"),
Pair("Suspense", "suspense"),
Pair("tear-studio", "tear-studio"),
Pair("Terror", "terror"),
Pair("Thriller", "thriller"),
Pair("Tragédia", "tragedia"),
Pair("Vampiro", "vampiro"),
Pair("Vampiros", "vampiros"),
Pair("Vida Escolar", "vida-escolar"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
Pair("Zombie", "zombie"),
)
}
}

View file

@ -1,294 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.animestc
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.pt.animestc.dto.AnimeDto
import eu.kanade.tachiyomi.animeextension.pt.animestc.dto.EpisodeDto
import eu.kanade.tachiyomi.animeextension.pt.animestc.dto.ResponseDto
import eu.kanade.tachiyomi.animeextension.pt.animestc.dto.VideoDto
import eu.kanade.tachiyomi.animeextension.pt.animestc.extractors.LinkBypasser
import eu.kanade.tachiyomi.animeextension.pt.animestc.extractors.SendcmExtractor
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.AnimeHttpSource
import eu.kanade.tachiyomi.lib.googledriveextractor.GoogleDriveExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class AnimesTC : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "AnimesTC"
override val baseUrl = "https://api2.animestc.com"
override val lang = "pt-BR"
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$HOST_URL/")
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val json: Json by injectLazy()
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/series?order=id&direction=asc&page=1&top=true", headers)
override fun popularAnimeParse(response: Response): AnimesPage {
val data = response.parseAs<List<AnimeDto>>()
val animes = data.map(::searchAnimeFromObject)
return AnimesPage(animes, false)
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET(HOST_URL, headers)
override fun latestUpdatesParse(response: Response): AnimesPage {
val doc = response.asJsoup()
val animes = doc.select("div > article.episode").map {
SAnime.create().apply {
val ahref = it.selectFirst("h3 > a.episode-info-title-orange")!!
title = ahref.text()
val slug = ahref.attr("href").substringAfterLast("/")
setUrlWithoutDomain("/series?slug=$slug")
thumbnail_url = it.selectFirst("img.episode-image")?.attr("abs:data-src")
}
}
.filter { it.thumbnail_url?.contains("/_nuxt/img/") == false }
.distinctBy { it.url }
return AnimesPage(animes, false)
}
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = ATCFilters.getSearchParameters(filters)
val url = "$baseUrl/series?order=title&direction=asc&page=$page".toHttpUrl()
.newBuilder()
.addQueryParameter("type", params.type)
.addQueryParameter("search", query)
.addQueryParameter("year", params.year)
.addQueryParameter("releaseStatus", params.status)
.addQueryParameter("tag", params.genre)
.build()
return GET(url, headers)
}
override fun searchAnimeParse(response: Response): AnimesPage {
val data = response.parseAs<ResponseDto<AnimeDto>>()
val animes = data.items.map(::searchAnimeFromObject)
val hasNextPage = data.lastPage > data.page
return AnimesPage(animes, hasNextPage)
}
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val slug = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/series?slug=$slug"))
.awaitSuccess()
.use(::searchAnimeBySlugParse)
} else {
return super.getSearchAnime(page, query, filters)
}
}
override fun getFilterList(): AnimeFilterList = ATCFilters.FILTER_LIST
private fun searchAnimeFromObject(anime: AnimeDto) = SAnime.create().apply {
thumbnail_url = anime.cover.url
title = anime.title
setUrlWithoutDomain("/series/${anime.id}")
}
private fun searchAnimeBySlugParse(response: Response): AnimesPage {
val details = animeDetailsParse(response).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}
// =========================== Anime Details ============================
override fun animeDetailsParse(response: Response) = SAnime.create().apply {
val anime = response.getAnimeDto()
setUrlWithoutDomain("/series/${anime.id}")
title = anime.title
status = anime.status
thumbnail_url = anime.cover.url
artist = anime.producer
genre = anime.genres
description = buildString {
append(anime.synopsis + "\n")
anime.classification?.also { append("\nClassificação: ", it, " anos") }
anime.year?.also { append("\nAno de lançamento: ", it) }
}
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val id = response.getAnimeDto().id
return getEpisodeList(id)
}
private fun episodeListRequest(animeId: Int, page: Int) =
GET("$baseUrl/episodes?order=id&direction=desc&page=$page&seriesId=$animeId&specialOrder=true")
private fun getEpisodeList(animeId: Int, page: Int = 1): List<SEpisode> {
val response = client.newCall(episodeListRequest(animeId, page)).execute()
val parsed = response.parseAs<ResponseDto<EpisodeDto>>()
val episodes = parsed.items.map(::episodeFromObject)
if (parsed.page < parsed.lastPage) {
return episodes + getEpisodeList(animeId, page + 1)
} else {
return episodes
}
}
private fun episodeFromObject(episode: EpisodeDto) = SEpisode.create().apply {
name = episode.title
setUrlWithoutDomain("/episodes?slug=${episode.slug}")
episode_number = episode.number.toFloat()
date_upload = episode.created_at.toDate()
}
// ============================ Video Links =============================
private val sendcmExtractor by lazy { SendcmExtractor(client) }
private val gdriveExtractor by lazy { GoogleDriveExtractor(client, headers) }
private val linkBypasser by lazy { LinkBypasser(client, json) }
private val supportedPlayers = listOf("send", "drive")
override fun videoListParse(response: Response): List<Video> {
val videoDto = response.parseAs<ResponseDto<VideoDto>>().items.first()
val links = videoDto.links
val allLinks = listOf(links.low, links.medium, links.high).flatten()
.filter { it.name in supportedPlayers }
val online = links.online?.run {
filterNot { "mega" in it }.map {
Video(it, "Player ATC", it, headers)
}
}.orEmpty()
val videoId = videoDto.id
return online + allLinks.parallelCatchingFlatMapBlocking { extractVideosFromLink(it, videoId) }
}
private fun extractVideosFromLink(video: VideoDto.VideoLink, videoId: Int): List<Video> {
val playerUrl = linkBypasser.bypass(video, videoId)
?: return emptyList()
val quality = when (video.quality) {
"low" -> "SD"
"medium" -> "HD"
"high" -> "FULLHD"
else -> "SD"
}
return when (video.name) {
"send" -> sendcmExtractor.videosFromUrl(playerUrl, quality)
"drive" -> {
val id = GDRIVE_REGEX.find(playerUrl)?.groupValues?.get(0) ?: return emptyList()
gdriveExtractor.videosFromUrl(id, "GDrive - $quality")
}
else -> emptyList()
}
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_ENTRIES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_PLAYER_KEY
title = PREF_PLAYER_TITLE
entries = PREF_PLAYER_VALUES
entryValues = PREF_PLAYER_VALUES
setDefaultValue(PREF_PLAYER_DEFAULT)
summary = "%s"
}.also(screen::addPreference)
}
// ============================= Utilities ==============================
private fun Response.getAnimeDto(): AnimeDto {
val jsonString = body.string()
return try {
jsonString.parseAs<AnimeDto>()
} catch (e: Exception) {
// URL intent handler moment
jsonString.parseAs<ResponseDto<AnimeDto>>().items.first()
}
}
private fun String.toDate(): Long {
return try {
DATE_FORMATTER.parse(this)?.time
} catch (_: Throwable) { null } ?: 0L
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val player = preferences.getString(PREF_PLAYER_KEY, PREF_PLAYER_DEFAULT)!!
return sortedWith(
compareBy(
{ it.quality.contains(player) },
{ it.quality.contains("- $quality") },
),
).reversed()
}
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH)
}
const val PREFIX_SEARCH = "slug:"
private const val HOST_URL = "https://www.animestc.net"
private const val PREF_QUALITY_KEY = "pref_quality"
private const val PREF_QUALITY_TITLE = "Qualidade preferida"
private const val PREF_QUALITY_DEFAULT = "HD"
private val PREF_QUALITY_ENTRIES = arrayOf("SD", "HD", "FULLHD")
private const val PREF_PLAYER_KEY = "pref_player"
private const val PREF_PLAYER_TITLE = "Player preferido"
private const val PREF_PLAYER_DEFAULT = "Sendcm"
private val PREF_PLAYER_VALUES = arrayOf("Sendcm", "GDrive", "Player ATC")
private val GDRIVE_REGEX = Regex("[\\w-]{28,}")
}
}

View file

@ -1,41 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.animestc
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://wwww.animestc.net/animes/<item> intents
* and redirects them to the main Aniyomi process.
*/
class AnimesTCUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val item = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${AnimesTC.PREFIX_SEARCH}$item")
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,78 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.animestc.dto
import eu.kanade.tachiyomi.animesource.model.SAnime
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ResponseDto<T>(
@SerialName("data")
val items: List<T>,
val lastPage: Int,
val page: Int,
)
@Serializable
data class AnimeDto(
val classification: String?,
val cover: CoverDto,
val id: Int,
val producer: String?,
val releaseStatus: String,
val synopsis: String,
val tags: List<TagDto>,
val title: String,
val year: Int?,
) {
val status by lazy {
when (releaseStatus) {
"complete" -> SAnime.COMPLETED
"airing" -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
}
val genres by lazy { tags.joinToString(", ") { it.name } }
@Serializable
data class TagDto(val name: String)
}
@Serializable
data class EpisodeDto(
@SerialName("seriesId")
val animeId: Int,
val cover: CoverDto?,
val created_at: String,
val number: String,
val slug: String,
val title: String,
)
@Serializable
data class VideoDto(
val id: Int,
val links: VideoLinksDto,
) {
@Serializable
data class VideoLinksDto(
val low: List<VideoLink> = emptyList(),
val medium: List<VideoLink> = emptyList(),
val high: List<VideoLink> = emptyList(),
val online: List<String>? = null,
)
@Serializable
data class VideoLink(
val index: Int,
val name: String,
val quality: String,
)
}
@Serializable
data class CoverDto(
val originalName: String,
) {
val url by lazy { "https://stc.animestc.com/$originalName" }
}

View file

@ -1,42 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.animestc.extractors
import android.util.Base64
import eu.kanade.tachiyomi.animeextension.pt.animestc.dto.VideoDto.VideoLink
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
class LinkBypasser(
private val client: OkHttpClient,
private val json: Json,
) {
fun bypass(video: VideoLink, episodeId: Int): String? {
val joined = "$episodeId/${video.quality}/${video.index}"
val encoded = Base64.encodeToString(joined.toByteArray(), Base64.NO_WRAP)
val url = "$PROTECTOR_URL/link/$encoded"
val res = client.newCall(GET(url)).execute()
if (res.code != 200) {
return null
}
// Sadly we MUST wait 6s or we are going to get a HTTP 500
Thread.sleep(6000L)
val id = res.asJsoup().selectFirst("meta#link-id")!!.attr("value")
val apiCall = client.newCall(GET("$PROTECTOR_URL/api/link/$id")).execute()
if (apiCall.code != 200) {
return null
}
val apiBody = apiCall.body.string()
return json.decodeFromString<LinkDto>(apiBody).link
}
@Serializable
data class LinkDto(val link: String)
companion object {
private const val PROTECTOR_URL = "https://protetor.animestc.xyz"
}
}

View file

@ -1,20 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.animestc.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.OkHttpClient
class SendcmExtractor(private val client: OkHttpClient) {
private val playerName = "Sendcm"
fun videosFromUrl(url: String, quality: String): List<Video> {
val doc = client.newCall(GET(url)).execute().asJsoup()
val videoUrl = doc.selectFirst("video#vjsplayer > source")?.attr("src")
return videoUrl?.let {
val headers = Headers.headersOf("Referer", url)
listOf(Video(it, "$playerName - $quality", it, headers = headers))
}.orEmpty()
}
}

View file

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".pt.animeszone.AZUrlActivity"
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="animeszone.net"
android:pathPattern="/..*/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -1,12 +0,0 @@
ext {
extName = 'AnimesZone'
extClass = '.AnimesZone'
extVersionCode = 12
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:dood-extractor'))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View file

@ -1,42 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.animeszone
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://animeszone.net/<type>/<slug> intents
* and redirects them to the main Aniyomi process.
*/
class AZUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val searchQuery = "${pathSegments[0]}/${pathSegments[1]}"
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${AnimesZone.PREFIX_SEARCH}$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,439 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.animeszone
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import app.cash.quickjs.QuickJs
import eu.kanade.tachiyomi.animeextension.pt.animeszone.extractors.BloggerJWPlayerExtractor
import eu.kanade.tachiyomi.animeextension.pt.animeszone.extractors.PlaylistExtractor
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.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
class AnimesZone : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "AnimesZone"
override val baseUrl = "https://animeszone.net"
override val lang = "pt-BR"
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
.add("Origin", baseUrl)
private val json: Json by injectLazy()
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/tendencia/")
override fun popularAnimeSelector(): String = "div.items > div.seriesList"
override fun popularAnimeNextPageSelector(): String? = null
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a[href]")!!.attr("abs:href"))
thumbnail_url = element.selectFirst("div.cover-image")?.run {
attr("style").substringAfter("url('").substringBefore("'")
}
title = element.selectFirst("span.series-title")!!.text()
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request {
return if (page == 1) {
GET("$baseUrl/animes-legendados/")
} else {
GET("$baseUrl/animes-legendados/page/$page/")
}
}
override fun latestUpdatesSelector(): String = "main#list-animes ul.post-lst > li"
override fun latestUpdatesNextPageSelector(): String = "div.paginadorplay > a.active + a"
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("div.aniItem > a[href]")!!.attr("abs:href"))
thumbnail_url = element.selectFirst("div.aniItemImg img[src]")?.attr("abs:src")
title = element.selectFirst("h2.aniTitulo")!!.text()
}
// =============================== Search ===============================
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
return if (query.startsWith(PREFIX_SEARCH)) {
val path = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/$path"))
.awaitSuccess()
.use(::searchAnimeByIdParse)
} else {
super.getSearchAnime(page, query, filters)
}
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = AnimesZoneFilters.getSearchParameters(filters)
val cleanQuery = query.replace(" ", "%20")
val queryQuery = if (query.isBlank()) {
""
} else {
"&_pesquisa=$cleanQuery"
}
val url = "$baseUrl/?s=${params.genre}${params.year}${params.version}${params.studio}${params.type}${params.adult}$queryQuery"
val httpParams = url.substringAfter("$baseUrl/?").split("&").joinToString(",") {
val (key, value) = it.split("=", limit = 2)
"\"$key\":\"$value\""
}
val softRefresh = if (page == 1) 0 else 1
val jsonBody = """
{
"action":"facetwp_refresh",
"data":{
"facets":{
"generos":[
${queryToJsonList(params.genre)}
],
"versao":[
${queryToJsonList(params.year)}
],
"tipo":[
${queryToJsonList(params.version)}
],
"estudio":[
${queryToJsonList(params.studio)}
],
"tipototal":[
${queryToJsonList(params.type)}
],
"adulto":[
${queryToJsonList(params.adult)}
],
"pesquisa":"$query",
"paginar":[
]
},
"frozen_facets":{
},
"http_params":{
"get":{
$httpParams
},
"uri":"",
"url_vars":[
]
},
"template":"wp",
"extras":{
"sort":"default"
},
"soft_refresh":$softRefresh,
"is_bfcache":1,
"first_load":0,
"paged":$page
}
}
""".trimIndent().toRequestBody("application/json".toMediaType())
return POST(url, headers, jsonBody)
}
override fun searchAnimeParse(response: Response): AnimesPage {
val parsed = response.parseAs<PostResponse>()
val document = Jsoup.parse(parsed.template)
val animes = document.select(searchAnimeSelector()).map(::searchAnimeFromElement)
val hasNextPage = parsed.settings.pager.run { page < total_pages }
return AnimesPage(animes, hasNextPage)
}
override fun searchAnimeSelector(): String = "div.aniItem"
override fun searchAnimeNextPageSelector(): String? = null
override fun searchAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a[href]")!!.attr("href"))
thumbnail_url = element.selectFirst("img[src]")?.attr("abs:src")
title = element.selectFirst("div.aniInfos")?.text() ?: "Anime"
}
// ============================== FILTERS ===============================
override fun getFilterList(): AnimeFilterList = AnimesZoneFilters.FILTER_LIST
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
title = document.selectFirst("div.container > div div.bottom-div > h4")?.ownText()
?: document.selectFirst("div#info > h1")?.text()
?: "Anime"
thumbnail_url = document.selectFirst("div.container > div > img[src]")?.attr("abs:src")
description = document.selectFirst("section#sinopse p:matches(.)")?.text()
?: document.selectFirst("div.content.right > dialog > p:matches(.)")?.text()
genre = document.select("div.card-body table > tbody > tr:has(>td:contains(Genres)) td > a").joinToString { it.text() }
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
val singleVideo = document.selectFirst("div.anime__video__player")
// First check if single episode
return if (singleVideo != null) {
SEpisode.create().apply {
name = document.selectFirst("div#info h1")?.text() ?: "Episódio"
episode_number = 1F
setUrlWithoutDomain(document.location())
}.let(::listOf)
} else {
buildList {
document.select(episodeListSelector()).forEach { ep ->
val name = ep.selectFirst("h2.aniTitulo")?.text()?.trim()
// Check if it's multi-season
var nextPageUrl = when {
name != null && name.startsWith("temporada ", true) -> ep.selectFirst("a[href]")!!.attr("href")
else -> {
add(episodeFromElement(ep, size + 1))
document.nextPageUrl()
}
}
while (nextPageUrl != null) {
val seasonDocument = client.newCall(GET(nextPageUrl)).execute()
.asJsoup()
seasonDocument.select(episodeListSelector()).forEach { seasonEp ->
add(episodeFromElement(seasonEp, size + 1, name))
}
nextPageUrl = seasonDocument.nextPageUrl()
}
}
reverse()
}
}
}
private fun Document.nextPageUrl() = selectFirst("div.paginadorplay > a:contains(Próxima Pagina)")?.absUrl("href")
override fun episodeListSelector(): String = "main.site-main ul.post-lst > li"
private fun episodeFromElement(element: Element, counter: Int, info: String? = null): SEpisode {
val epTitle = element.selectFirst("div.title")?.text() ?: element.text()
val epNumber = element.selectFirst("span.epiTipo")
return SEpisode.create().apply {
name = "Ep. ${epNumber?.text()?.trim() ?: counter} - ${epTitle.replace(EPISODE_REGEX, "")}"
.replace(" - - ", " - ")
episode_number = epNumber?.run {
text().trim().toFloatOrNull()
} ?: counter.toFloat()
scanlator = info
setUrlWithoutDomain(element.selectFirst("article > a")!!.attr("href"))
}
}
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException()
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val videoList = document.select("div#playeroptions li[data-post]").flatMap { vid ->
val jsonHeaders = headersBuilder()
.add("Accept", "application/json, text/javascript, */*; q=0.01")
.add("X-Requested-With", "XMLHttpRequest")
.build()
val post = vid.attr("data-post")
val type = vid.attr("data-type")
val nume = vid.attr("data-nume")
val apires = client.newCall(
GET("$baseUrl/wp-json/dooplayer/v2/$post/$type/$nume", jsonHeaders),
).execute()
val url = apires.parseAs<VideoResponse>().embed_url
when {
url.startsWith("https://dood") -> DoodExtractor(client).videosFromUrl(url, vid.text().trim())
"https://gojopoolt" in url -> {
client.newCall(GET(url, headers)).execute()
.asJsoup()
.selectFirst("script:containsData(sources:)")
?.data()
?.let(BloggerJWPlayerExtractor::videosFromScript)
.orEmpty()
}
url.startsWith(baseUrl) -> videosFromInternalUrl(url)
"blogger.com" in url -> extractBloggerVideos(url, vid.text().trim())
else -> emptyList()
}
}
return videoList
}
private fun videosFromInternalUrl(url: String): List<Video> {
val videoDocument = client.newCall(GET(url, headers)).execute()
.asJsoup()
val script = videoDocument.selectFirst("script:containsData(decodeURIComponent)")?.data()
?.let(::getDecrypted)
?: videoDocument.selectFirst("script:containsData(sources:)")?.data()
?: return emptyList()
return when {
"/bloggerjwplayer" in url || "jwplayer-2" in url || "/ctaplayer" in url -> {
BloggerJWPlayerExtractor.videosFromScript(script)
}
"/m3u8" in url -> PlaylistExtractor.videosFromScript(script)
else -> emptyList()
}
}
private fun extractBloggerVideos(url: String, name: String): List<Video> {
return client.newCall(GET(url, headers)).execute()
.body.string()
.takeIf { !it.contains("errorContainer") }
.let { it ?: return emptyList() }
.substringAfter("\"streams\":[")
.substringBefore("]")
.split("},")
.map {
val videoUrl = it.substringAfter("{\"play_url\":\"").substringBefore('"')
val format = it.substringAfter("\"format_id\":").substringBefore("}")
val quality = when (format) {
"18" -> "360p"
"22" -> "720p"
else -> "Unknown"
}
Video(videoUrl, "$quality - $name", videoUrl, headers = headers)
}
}
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException()
override fun videoListSelector(): String = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException()
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
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()
}
}.also(screen::addPreference)
}
// ============================= Utilities ==============================
private fun getDecrypted(script: String): String? {
val patchedScript = script.trim().split("\n").first()
.replace("eval(function", "function a")
.replace("decodeURIComponent(escape(r))}(", "r};a(")
.substringBeforeLast(")")
return QuickJs.create().use {
it.evaluate(patchedScript)?.toString()
}
}
private fun queryToJsonList(input: String): String {
if (input.isEmpty()) return ""
return input.substringAfter("=").split("%2C").joinToString(",") {
"\"$it\""
}
}
@Serializable
data class VideoResponse(val embed_url: String)
@Serializable
data class PostResponse(
val template: String,
val settings: Settings,
) {
@Serializable
data class Settings(val pager: Pager) {
@Serializable
data class Pager(
val page: Int,
val total_pages: Int,
)
}
}
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 = "id:"
private val EPISODE_REGEX by lazy { Regex("""Episódio ?\d+\.?\d* ?""") }
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Qualidade preferida"
private const val PREF_QUALITY_DEFAULT = "720"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p")
private val PREF_QUALITY_VALUES = arrayOf("1080", "720", "480", "360")
}
}

View file

@ -1,279 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.animeszone
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AnimesZoneFilters {
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
}
open class CheckBoxFilterList(name: String, values: List<CheckBox>) : AnimeFilter.Group<AnimeFilter.CheckBox>(name, values)
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
private inline fun <reified R> AnimeFilterList.asQueryPart(name: String): String {
return (first { it is R } as QueryPartFilter).toQueryPart()
.takeUnless(String::isEmpty)
?.let { "&$name=$it" }
.orEmpty()
}
private inline fun <reified R> AnimeFilterList.parseCheckbox(
options: Array<Pair<String, String>>,
name: String,
): String {
return (first { it is R } as CheckBoxFilterList).state
.asSequence()
.filter { it.state }
.map { checkbox -> options.find { it.first == checkbox.name }!!.second }
.filter(String::isNotBlank)
.joinToString("%2C") { "&$name=$it" }
}
class GenreFilter : CheckBoxFilterList(
"Selecionar Gêneros",
AnimesZoneFiltersData.GENRE.map { CheckBoxVal(it.first, false) },
)
class YearFilter : QueryPartFilter("Ano", AnimesZoneFiltersData.YEAR)
class VersionFilter : QueryPartFilter("Versão", AnimesZoneFiltersData.VERSION)
class StudioFilter : CheckBoxFilterList(
"Estudio",
AnimesZoneFiltersData.STUDIO.map { CheckBoxVal(it.first, false) },
)
class TypeFilter : QueryPartFilter("Tipo", AnimesZoneFiltersData.TYPE)
class AdultFilter : QueryPartFilter("Adulto", AnimesZoneFiltersData.ADULT)
val FILTER_LIST get() = AnimeFilterList(
GenreFilter(),
StudioFilter(),
YearFilter(),
VersionFilter(),
TypeFilter(),
AdultFilter(),
)
data class FilterSearchParams(
val genre: String = "",
val year: String = "",
val version: String = "",
val studio: String = "",
val type: String = "",
val adult: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.parseCheckbox<GenreFilter>(AnimesZoneFiltersData.GENRE, "_generos"),
filters.asQueryPart<YearFilter>("_versao"),
filters.asQueryPart<VersionFilter>("_tipo"),
filters.parseCheckbox<StudioFilter>(AnimesZoneFiltersData.STUDIO, "_estudio"),
filters.asQueryPart<TypeFilter>("_tipototal"),
filters.asQueryPart<AdultFilter>("_adulto"),
)
}
private object AnimesZoneFiltersData {
val ANY = Pair("Selecione", "")
val GENRE = arrayOf(
Pair("Comédia", "comedia"),
Pair("Ação", "acao"),
Pair("Fantasia", "fantasia"),
Pair("Romance", "romance"),
Pair("Drama", "drama"),
Pair("Escolar", "escolar"),
Pair("Aventura", "aventura"),
Pair("Shounen", "shounen"),
Pair("Sobrenatural", "sobrenatural"),
Pair("Slice-of-life", "slice-of-life"),
Pair("Sci-Fi", "sci-fi"),
Pair("Ecchi", "ecchi"),
Pair("Mistério", "misterio"),
Pair("Seinen", "seinen"),
Pair("Magia", "magia"),
Pair("Animação", "animacao"),
Pair("Harem", "harem"),
Pair("Psicológico", "psicologico"),
Pair("Super Poder", "super-poder"),
Pair("Violência", "violencia"),
Pair("Sci-Fi & Fantasy", "sci-fi-fantasy"),
Pair("Histórico", "historico"),
Pair("Isekai", "isekai"),
Pair("Mecha", "mecha"),
Pair("Demónio", "demonio"),
Pair("Terror", "terror"),
Pair("Esportes", "esportes"),
Pair("Militar", "militar"),
Pair("Artes Marciais", "artes-marciais"),
Pair("Jogo", "jogo"),
Pair("Vampiro", "vampiro"),
Pair("Musical", "musical"),
Pair("Suspense", "suspense"),
Pair("Paródia", "parodia"),
Pair("Shoujo", "shoujo"),
Pair("Nudez", "nudez"),
Pair("Supernatural", "supernatural"),
Pair("Espacial", "espacial"),
Pair("Shoujo-ai", "shoujo-ai"),
Pair("Crime", "crime"),
Pair("Policial", "policial"),
Pair("Ficção Científica", "ficcao-cientifica"),
Pair("Samurai", "samurai"),
Pair("Josei", "josei"),
Pair("Action & Adventure", "action-adventure"),
Pair("Amizade", "amizade"),
Pair("Horror", "horror"),
Pair("Família", "familia"),
Pair("Música", "musica"),
Pair("Insanidade", "insanidade"),
Pair("Obsceno", "obsceno"),
Pair("Shounen-ai", "shounen-ai"),
Pair("Carros", "carros"),
Pair("Gore", "gore"),
Pair("War & Politics", "war-politics"),
Pair("Yaoi", "yaoi"),
Pair("Cinema TV", "cinema-tv"),
Pair("Gourmet", "gourmet"),
Pair("Infantil", "infantil"),
Pair("Vida Escolar", "vida-escolar"),
)
val YEAR = arrayOf(ANY) + (1986..2024).map {
Pair(it.toString(), it.toString())
}.reversed().toTypedArray()
val VERSION = arrayOf(
ANY,
Pair("Legendados", "legendada"),
Pair("Dublado", "series02"),
Pair("Seção de Hentais", "series03"),
)
val STUDIO = arrayOf(
Pair("J.C.Staff", "j-c-staff"),
Pair("Shueisha", "shueisha"),
Pair("Aniplex", "aniplex"),
Pair("BONES", "bones"),
Pair("Kadokawa", "kadokawa"),
Pair("TOHO Animation", "toho-animation"),
Pair("Pony Canyon", "pony-canyon"),
Pair("A-1 Pictures", "a-1-pictures"),
Pair("DENTSU", "dentsu"),
Pair("Kodansha", "kodansha"),
Pair("Production I.G", "production-i-g"),
Pair("CloverWorks", "cloverworks"),
Pair("Madhouse", "madhouse"),
Pair("Bit grooove promotion", "bit-grooove-promotion"),
Pair("MAPPA", "mappa"),
Pair("SILVER LINK.", "silver-link"),
Pair("Wit Studio", "wit-studio"),
Pair("Magic Capsule", "magic-capsule"),
Pair("OLM", "olm"),
Pair("Lantis", "lantis"),
Pair("Movic", "movic"),
Pair("SATELIGHT", "satelight"),
Pair("Shogakukan-Shueisha Productions", "shogakukan-shueisha-productions"),
Pair("Square Enix", "square-enix"),
Pair("STUDIO MAUSU", "studio-mausu"),
Pair("Yomiuri Telecasting Corporation", "yomiuri-telecasting-corporation"),
Pair("Bandai Namco Arts", "bandai-namco-arts"),
Pair("David Production", "david-production"),
Pair("EGG FIRM", "egg-firm"),
Pair("Lerche", "lerche"),
Pair("Liden Films", "liden-films"),
Pair("Sony Music Entertainment", "sony-music-entertainment-japan"),
Pair("Studio Deen", "studio-deen"),
Pair("TMS Entertainment", "tms-entertainment"),
Pair("Toho", "toho"),
Pair("Crunchyroll", "crunchyroll"),
Pair("dugout", "dugout"),
Pair("ENGI", "engi"),
Pair("MBS", "mbs"),
Pair("P.A.Works", "p-a-works"),
Pair("Tezuka Productions", "tezuka-productions"),
Pair("TV Tokyo", "tv-tokyo"),
Pair("Warner Bros. Japan", "warner-bros-japan"),
Pair("White Fox", "white-fox"),
Pair("avex pictures", "avex-pictures"),
Pair("Bibury Animation Studios", "bibury-animation-studios"),
Pair("Brain's Base", "brains-base"),
Pair("DMM music", "dmm-music"),
Pair("DMM pictures", "dmm-pictures"),
Pair("feel.", "feel"),
Pair("Hakuhodo DY Music & Pictures", "hakuhodo-dy-music-pictures"),
Pair("Lidenfilms", "lidenfilms"),
Pair("MAHO FILM", "maho-film"),
Pair("NHK Enterprises", "nhk-enterprises"),
Pair("Passione", "passione"),
Pair("Pierrot", "pierrot"),
Pair("Pine Jam", "pine-jam"),
Pair("Pink Pineapple", "pink-pineapple"),
Pair("project No.9", "project-no-9"),
Pair("Seven", "seven"),
Pair("SHAFT", "shaft"),
Pair("TNK", "tnk"),
Pair("Zero-G", "zero-g"),
Pair("Asahi Production", "asahi-production"),
Pair("asread", "asread"),
Pair("AT-X", "at-x"),
Pair("Bandai Namco Pictures", "bandai-namco-pictures"),
Pair("BS Fuji", "bs-fuji"),
Pair("C2C", "c2c"),
Pair("Children's Playground Entertainment", "childrens-playground-entertainment"),
Pair("diomedéa", "diomedea"),
Pair("Doga Kobo", "doga-kobo"),
Pair("Geno Studio", "geno-studio"),
Pair("Good Smile Company", "good-smile-company"),
Pair("Graphinica", "graphinica"),
Pair("Hakusensha", "hakusensha"),
Pair("HALF H·P STUDIO", "f279ee47217fbae84c07eb11181f4997"),
Pair("King Records", "king-records"),
Pair("Kyoto Animation", "kyoto-animation"),
Pair("Nippon BS Broadcasting", "nippon-bs-broadcasting"),
Pair("Nippon Columbia", "nippon-columbia"),
Pair("Nitroplus", "nitroplus"),
Pair("Shogakukan", "shogakukan"),
Pair("Sotsu", "sotsu"),
Pair("Sound Team・Don Juan", "45e6f4604baaebfbebf4f43139db8d68"),
Pair("Studio Gokumi", "studio-gokumi"),
Pair("Suiseisha", "suiseisha"),
Pair("SUNRISE", "sunrise"),
Pair("SynergySP", "synergysp"),
Pair("Techno Sound", "techno-sound"),
Pair("THE KLOCKWORX", "the-klockworx"),
Pair("Toei Animation", "toei-animation"),
Pair("TOY'S FACTORY", "toys-factory"),
Pair("Twin Engine", "twin-engine"),
Pair("ufotable", "ufotable"),
Pair("ABC Animation", "abc-animation"),
Pair("Ajiado", "ajiado"),
Pair("APDREAM", "apdream"),
Pair("Ashi Productions", "ashi-productions"),
)
val TYPE = arrayOf(
ANY,
Pair("TV Shows", "tvshows"),
Pair("Filmes", "movies"),
)
val ADULT = arrayOf(
ANY,
Pair("Hentais", "dublada"),
Pair("Seção de Hentais", "series03"),
)
}
}

View file

@ -1,27 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.animeszone.extractors
import eu.kanade.tachiyomi.animesource.model.Video
object BloggerJWPlayerExtractor {
fun videosFromScript(script: String): List<Video> {
val sources = script.substringAfter("sources: [").substringBefore("],")
return sources.split("{").drop(1).mapNotNull {
val label = it.substringAfter("label")
.substringAfter(':')
.substringAfter('"')
.substringBefore('"')
val videoUrl = it.substringAfter("file")
.substringAfter(':')
.substringAfter('"')
.substringBefore('"')
.replace("\\", "")
if (videoUrl.isEmpty()) {
null
} else {
Video(videoUrl, "BloggerJWPlayer - $label", videoUrl)
}
}
}
}

View file

@ -1,18 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.animeszone.extractors
import eu.kanade.tachiyomi.animesource.model.Video
object PlaylistExtractor {
fun videosFromScript(script: String): List<Video> {
val sources = script.substringAfter("sources: [").substringBefore("],")
return sources.split("file:\"").drop(1).mapNotNull { source ->
val url = source.substringBefore("\"").ifEmpty { return@mapNotNull null }
val label = source.substringAfter("label:\"").substringBefore("\"")
.replace("FHD", "1080p")
.replace("HD", "720p")
.replace("SD", "480p")
Video(url, "Playlist - $label", url)
}
}
}

View file

@ -1,16 +0,0 @@
ext {
extName = 'GoAnimes'
extClass = '.GoAnimes'
themePkg = 'dooplay'
baseUrl = 'https://goanimes.net'
overrideVersionCode = 17
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:playlist-utils"))
implementation(project(":lib:blogger-extractor"))
implementation(libs.jsunpacker)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

View file

@ -1,185 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.goanimes
import eu.kanade.tachiyomi.animeextension.pt.goanimes.extractors.BloggerJWPlayerExtractor
import eu.kanade.tachiyomi.animeextension.pt.goanimes.extractors.GoAnimesExtractor
import eu.kanade.tachiyomi.animeextension.pt.goanimes.extractors.JsDecoder
import eu.kanade.tachiyomi.animeextension.pt.goanimes.extractors.LinkfunBypasser
import eu.kanade.tachiyomi.animeextension.pt.goanimes.extractors.PlaylistExtractor
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.bloggerextractor.BloggerExtractor
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.multisrc.dooplay.DooPlay
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import okhttp3.Response
import org.jsoup.nodes.Element
class GoAnimes : DooPlay(
"pt-BR",
"GoAnimes",
"https://goanimes.net",
) {
// ============================== Popular ===============================
override fun popularAnimeSelector() = "div#featured-titles article.item.tvshows > div.poster"
// =============================== Latest ===============================
override val latestUpdatesPath = "lancamentos"
// ============================== Episodes ==============================
override val seasonListSelector = "div#seasons > *"
override fun getSeasonEpisodes(season: Element): List<SEpisode> {
// All episodes are listed under a single page
season.selectFirst(episodeListSelector())?.let {
return getSeasonEpisodesRecursive(season)
}
// Episodes are listed at another page
val url = season.attr("href")
return client.newCall(GET(url, headers))
.execute()
.asJsoup()
.let(::getSeasonEpisodes)
}
private val episodeListNextPageSelector = "div.pagination span.current + a:not(.arrow_pag)"
private fun getSeasonEpisodesRecursive(season: Element): List<SEpisode> {
var doc = season.root()
return buildList {
do {
if (isNotEmpty()) {
doc.selectFirst(episodeListNextPageSelector)?.let {
val url = it.attr("abs:href")
doc = client.newCall(GET(url, headers)).execute()
.asJsoup()
}
}
addAll(super.getSeasonEpisodes(doc))
} while (doc.selectFirst(episodeListNextPageSelector) != null)
reversed()
}
}
// ============================ Video Links =============================
override val prefQualityValues = arrayOf("240p", "360p", "480p", "720p", "1080p")
override val prefQualityEntries = prefQualityValues
private val goanimesExtractor by lazy { GoAnimesExtractor(client, headers) }
private val bloggerExtractor by lazy { BloggerExtractor(client) }
private val linkfunBypasser by lazy { LinkfunBypasser(client) }
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val players = document.select("ul#playeroptionsul li")
return players.parallelCatchingFlatMapBlocking(::getPlayerVideos)
}
private suspend fun getPlayerVideos(player: Element): List<Video> {
val name = player.selectFirst("span.title")!!.text()
.replace("FULLHD", "1080p")
.replace("HD", "720p")
.replace("SD", "480p")
val url = getPlayerUrl(player)
return when {
"https://gojopoolt" in url -> {
val headers = headers.newBuilder()
.set("referer", url)
.build()
val script = client.newCall(GET(url, headers)).await()
.body.string()
.let { JsDecoder.decodeScript(it, false).ifBlank { it } }
script.substringAfter("sources: [")
.substringBefore(']')
.split('{')
.drop(1)
.mapNotNull {
val videoUrl = it.substringAfter("file: ")
.substringBefore(", ")
.trim('"', '\'', ' ')
.ifBlank { return@mapNotNull null }
val resolution = it.substringAfter("label: ", "")
.substringAfter('"')
.substringBefore('"')
.ifBlank { name.split('-').last().trim() }
val partialName = name.split('-').first().trim()
return when {
videoUrl.contains(".m3u8") -> {
playlistUtils.extractFromHls(
videoUrl,
url,
videoNameGen = {
"$partialName - ${it.replace("Video", resolution)}"
},
)
}
else -> listOf(Video(videoUrl, "$partialName - $resolution", videoUrl, headers))
}
}
}
listOf("/bloggerjwplayer", "/m3u8", "/multivideo").any { it in url } -> {
val script = client.newCall(GET(url)).await()
.body.string()
.let { JsDecoder.decodeScript(it, true).ifBlank { JsDecoder.decodeScript(it, false).ifBlank { it } } }
when {
"/bloggerjwplayer" in url ->
BloggerJWPlayerExtractor.videosFromScript(script)
"/m3u8" in url ->
PlaylistExtractor.videosFromScript(script)
"/multivideo" in url ->
script.substringAfter("attr")
.substringAfter(" \"")
.substringBefore('"')
.let { goanimesExtractor.videosFromUrl(it, name) }
else -> emptyList<Video>()
}
}
"www.blogger.com" in url -> bloggerExtractor.videosFromUrl(url, headers)
else -> goanimesExtractor.videosFromUrl(url, name)
}
}
private suspend fun getPlayerUrl(player: Element): String {
val type = player.attr("data-type")
val id = player.attr("data-post")
val num = player.attr("data-nume")
val url = client.newCall(GET("$baseUrl/wp-json/dooplayer/v2/$id/$type/$num"))
.await()
.body.string()
.substringAfter("\"embed_url\":\"")
.substringBefore("\",")
.replace("\\", "")
return when {
"/protetorlinks/" in url -> {
val link = client.newCall(GET(url)).await()
.asJsoup()
.selectFirst("a[href]")!!.attr("href")
client.newCall(GET(link)).await()
.use(linkfunBypasser::getIframeUrl)
}
else -> url
}
}
// ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(videoSortPrefKey, videoSortPrefDefault)!!
return sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
),
).reversed()
}
}

View file

@ -1,18 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.goanimes.extractors
import eu.kanade.tachiyomi.animesource.model.Video
object BloggerJWPlayerExtractor {
fun videosFromScript(script: String): List<Video> {
val sources = script.substringAfter("sources: [").substringBefore("],")
return sources.split("{").drop(1).map {
val label = it.substringAfter("label").substringAfter(":\"").substringBefore('"')
val videoUrl = it.substringAfter("file")
.substringAfter(":\"")
.substringBefore('"')
.replace("\\", "")
Video(videoUrl, "BloggerJWPlayer - $label", videoUrl)
}
}
}

View file

@ -1,81 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.goanimes.extractors
import android.util.Base64
import dev.datlag.jsunpacker.JsUnpacker
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
class GoAnimesExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
fun videosFromUrl(url: String, name: String): List<Video> {
val body = client.newCall(GET(url, headers)).execute()
.body.string()
val decodedBody = JsUnpacker.unpackAndCombine(body)
?: JsDecoder.decodeScript(body, false).takeIf(String::isNotEmpty)
?: JsDecoder.decodeScript(body, true).takeIf(String::isNotEmpty)
?: body
val partialName = name.split('-').first().trim()
val resolution = name.split('-').last().trim()
return when {
"/proxy/v.php" in url -> {
val playlistUrl = JsUnpacker.unpackAndCombine(body)
?.substringAfterLast("player(\\'", "")
?.substringBefore("\\'", "")
?.takeIf(String::isNotEmpty)
?: return emptyList()
playlistUtils.extractFromHls(
playlistUrl,
url,
videoNameGen = { "$partialName - ${it.replace("Video", resolution)}" },
)
}
"/proxy/api3/" in url -> {
val playlistUrl = body.substringAfter("sources:", "")
.substringAfter("file:", "")
.substringAfter("'", "")
.substringBefore("'", "")
.takeIf(String::isNotEmpty)
?: return emptyList()
val fixedUrl = if (playlistUrl.contains("/aHR0")) {
val encoded = playlistUrl.substringAfterLast("/").substringBefore(".")
String(Base64.decode(encoded, Base64.DEFAULT))
} else {
playlistUrl
}
val referer = url.toHttpUrl().queryParameter("url") ?: url
playlistUtils.extractFromHls(
fixedUrl,
referer,
videoNameGen = { "$partialName - ${it.replace("Video", resolution)}" },
)
}
"jwplayer" in decodedBody && "sources:" in decodedBody -> {
val videos = PlaylistExtractor.videosFromScript(decodedBody, partialName)
if ("label:" !in decodedBody && videos.size === 1) {
return playlistUtils.extractFromHls(
videos[0].url,
url,
videoNameGen = { "$partialName - ${it.replace("Video", resolution)}" },
)
}
videos
}
else -> emptyList()
}
}
}
private const val PLAYER_NAME = "GoAnimes"

View file

@ -1,48 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.goanimes.extractors
import android.util.Base64
import kotlin.math.pow
object JsDecoder {
private fun convertToNum(thing: String, limit: Float): Int {
return thing.split("")
.reversed()
.map { it.toIntOrNull() ?: 0 }
.reduceIndexed { index: Int, acc, num ->
acc + (num * limit.pow(index - 1)).toInt()
}
}
fun decodeScript(encodedString: String, magicStr: String, offset: Int, limit: Int): String {
val regex = "\\w".toRegex()
return encodedString
.split(magicStr[limit])
.dropLast(1)
.map { str ->
val replaced = regex.replace(str) { magicStr.indexOf(it.value).toString() }
val charInt = convertToNum(replaced, limit.toFloat()) - offset
Char(charInt)
}.joinToString("")
}
fun decodeScript(html: String, isB64: Boolean = true): String {
val script = if (isB64) {
html.substringAfter(";base64,")
.substringBefore('"')
.let { String(Base64.decode(it, Base64.DEFAULT)) }
} else {
html
}
val regex = """\}\("(\w+)",.*?"(\w+)",(\d+),(\d+),.*?\)""".toRegex()
return regex.find(script)
?.run {
decodeScript(
groupValues[1], // encoded data
groupValues[2], // magic string
groupValues[3].toIntOrNull() ?: 0, // offset
groupValues[4].toIntOrNull() ?: 0, // limit
)
} ?: ""
}
}

View file

@ -1,54 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.goanimes.extractors
import android.util.Base64
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Response
class LinkfunBypasser(private val client: OkHttpClient) {
fun getIframeUrl(page: Response): String {
val docString = page.body.string()
val document = if (docString.startsWith("<script")) {
page.asJsoup(decodeAtob(docString))
} else { page.asJsoup(docString) }
val newHeaders = Headers.headersOf("Referer", document.location())
val iframe = document.selectFirst("iframe[src]")
return if (iframe != null) {
iframe.attr("src")
} else {
val formBody = FormBody.Builder().apply {
document.select("input[name]").forEach {
add(it.attr("name"), it.attr("value"))
}
}.build()
val formUrl = document.selectFirst("form")!!.attr("action")
client.newCall(POST(formUrl, newHeaders, formBody))
.execute()
.let(::getIframeUrl)
}
}
companion object {
fun decodeAtob(html: String): String {
val atobContent = html.substringAfter("atob(\"").substringBefore("\"));")
val hexAtob = atobContent.replace("\\x", "").decodeHex()
val decoded = Base64.decode(hexAtob, Base64.DEFAULT)
return String(decoded)
}
// Stolen from AnimixPlay(EN) / GogoCdnExtractor
private fun String.decodeHex(): ByteArray {
check(length % 2 == 0) { "Must have an even length" }
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
}
}

View file

@ -1,30 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.goanimes.extractors
import eu.kanade.tachiyomi.animesource.model.Video
object PlaylistExtractor {
fun videosFromScript(script: String, prefix: String = "Playlist"): List<Video> {
val sources = script.substringAfter("sources: [").substringBefore("],")
return sources.split("{").drop(1).mapNotNull { source ->
val url = source.substringAfter("file:")
.substringAfter('"', "")
.substringBefore('"', "")
.takeIf(String::isNotEmpty)
?: source.substringAfter("file:")
.substringAfter("'", "")
.substringBefore("'", "")
.takeIf(String::isNotEmpty)
if (url.isNullOrBlank()) {
return@mapNotNull null
}
val label = source.substringAfter("label:").substringAfter('"').substringBefore('"')
.replace("FHD", "1080p")
.replace("HD", "720p")
.replace("SD", "480p")
Video(url, "$prefix - $label", url)
}
}
}

View file

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".pt.hinatasoul.HSUrlActivity"
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="www.hinatasoul.com"
android:pathPattern="/animes/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -1,7 +0,0 @@
ext {
extName = 'Hinata Soul'
extClass = '.HinataSoul'
extVersionCode = 9
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,42 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.hinatasoul
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://www.hinatasoul.com/animes/<slug> intents
* and redirects them to the main Aniyomi process.
*/
class HSUrlActivity : Activity() {
private val tag = "HSUrlActivity"
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 = HinataSoul.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,308 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.hinatasoul
import android.app.Application
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.pt.hinatasoul.extractors.HinataSoulDownloadExtractor
import eu.kanade.tachiyomi.animeextension.pt.hinatasoul.extractors.HinataSoulExtractor
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.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
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.text.SimpleDateFormat
import java.util.Locale
class HinataSoul : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Hinata Soul"
override val baseUrl = "https://www.hinatasoul.com"
override val lang = "pt-BR"
override val supportsLatest = true
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun headersBuilder() = super.headersBuilder().add("Referer", baseUrl)
// ============================== Popular ===============================
override fun popularAnimeSelector() = "div.FsssItem:contains(Mais Vistos) > a"
override fun popularAnimeRequest(page: Int) = GET(baseUrl)
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
title = element.text()
}
override fun popularAnimeNextPageSelector() = null
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET(baseUrl)
override fun latestUpdatesSelector() =
"div.tituloContainer:contains(lançamento) + div.epiContainer a"
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
val img = element.selectFirst("img")!!
thumbnail_url = img.attr("src")
title = img.attr("alt")
}
override fun latestUpdatesNextPageSelector() = null
// =============================== Search ===============================
override suspend fun getSearchAnime(
page: Int,
query: String,
filters: AnimeFilterList,
): AnimesPage {
return if (query.startsWith(PREFIX_SEARCH)) {
val slug = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/animes/$slug"))
.awaitSuccess()
.use(::searchAnimeBySlugParse)
} else {
super.getSearchAnime(page, query, filters)
}
}
private fun searchAnimeBySlugParse(response: Response): AnimesPage {
val details = animeDetailsParse(response).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) =
GET("$baseUrl/busca?busca=$query&page=$page")
override fun searchAnimeSelector() = episodeListSelector()
override fun searchAnimeNextPageSelector() = null
override fun searchAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
thumbnail_url = element.selectFirst("img")?.attr("src")
title = element.selectFirst("div.ultimosAnimesHomeItemInfosNome")!!.text()
}
override fun searchAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val animes = document.select(searchAnimeSelector()).map(::searchAnimeFromElement)
val hasNext = hasNextPage(document)
return AnimesPage(animes, hasNext)
}
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
val doc = getRealDoc(document)
setUrlWithoutDomain(doc.location())
val infos = doc.selectFirst("div.aniInfosSingle")!!
val img = infos.selectFirst("img")!!
thumbnail_url = img.attr("src")
title = img.attr("alt")
genre = infos.select("div.aniInfosSingleGeneros > span")
.eachText()
.joinToString()
author = infos.getInfo("AUTOR")
artist = infos.getInfo("ESTÚDIO")
status = parseStatus(infos.selectFirst("div.anime_status")!!)
description = buildString {
append(infos.selectFirst("div.aniInfosSingleSinopse > p")!!.text() + "\n")
infos.getInfo("Título")?.also { append("\nTítulos Alternativos: $it") }
infos.selectFirst("div.aniInfosSingleNumsItem:contains(Ano)")?.also {
append("\nAno: ${it.ownText()}")
}
infos.getInfo("Temporada")?.also { append("\nTemporada: $it") }
}
}
// ============================== Episodes ==============================
override fun episodeListSelector() = "div.aniContainer a"
override fun episodeListParse(response: Response): List<SEpisode> {
var doc = getRealDoc(response.asJsoup())
val totalEpisodes = buildList {
do {
if (isNotEmpty()) {
val url = doc.selectFirst("div.mwidth > a:containsOwn(»)")!!.absUrl("href")
doc = client.newCall(GET(url, headers)).execute().asJsoup()
}
doc.select(episodeListSelector())
.map(::episodeFromElement)
.also(::addAll)
} while (hasNextPage(doc))
reverse()
}
return totalEpisodes
}
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
val title = element.attr("title")
setUrlWithoutDomain(element.attr("href"))
name = title
episode_number =
title.substringBeforeLast(" - FINAL").substringAfterLast(" ").toFloatOrNull() ?: 0F
date_upload = element.selectFirst("div.lancaster_episodio_info_data")!!
.text()
.toDate()
}
// ============================ Video Links =============================
private val hinataExtractor by lazy { HinataSoulExtractor(headers, client, preferences) }
private val downloadExtractor by lazy { HinataSoulDownloadExtractor(headers, client) }
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val links = mutableListOf(document.location())
val downloadsLinks = document.select("div.reportaBox .reportContent > a")
downloadsLinks.forEach {
it.attr("href")?.let {
links.add(it)
}
}
val epName = document.selectFirst("meta[itemprop=name]")!!.attr("content")
return links.parallelCatchingFlatMapBlocking { url ->
when {
url.contains("file4go.net") -> {
val quality =
downloadsLinks.first { it.attr("href") == url }
.textNodes().first().toString()
.trim().replace(" ", "")
downloadExtractor.videosFromUrl(url, epName, quality)
}
else -> hinataExtractor.getVideoList(document)
}
}
}
override fun videoListSelector() = throw UnsupportedOperationException()
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_VALUES
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()
}
}.also(screen::addPreference)
// Auth Code
EditTextPreference(screen.context).apply {
key = PREF_AUTHCODE_KEY
title = "Auth Code"
setDefaultValue(PREF_AUTHCODE_DEFAULT)
summary = PREF_AUTHCODE_SUMMARY
setOnPreferenceChangeListener { _, newValue ->
runCatching {
val value = (newValue as String).trim().ifBlank { PREF_AUTHCODE_DEFAULT }
preferences.edit().putString(key, value).commit()
}.getOrDefault(false)
}
}.also(screen::addPreference)
}
// ============================= Utilities ==============================
private fun parseStatus(element: Element): Int {
return when {
element.hasClass("completed") -> SAnime.COMPLETED
element.hasClass("airing") -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
}
private fun hasNextPage(doc: Document): Boolean {
val currentUrl = doc.location()
val nextUrl = doc.selectFirst("a:contains(»)")!!.attr("href")
val endings = listOf("/1", "page=1")
return endings.none(nextUrl::endsWith) && currentUrl != nextUrl
}
private val animeMenuSelector = "div.controlesBoxItem > a:has(i.iconLista)"
private fun getRealDoc(document: Document): Document {
if (!document.location().contains("/videos/")) {
return document
}
return document.selectFirst(animeMenuSelector)?.let {
client.newCall(GET(it.attr("href"), headers)).execute()
.asJsoup()
} ?: document
}
private fun Element.getInfo(key: String): String? {
return selectFirst("div.aniInfosSingleInfoItem:contains($key) span")
?.text()
?.trim()
}
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(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy<Video>(
{ it.quality.startsWith(quality) },
{ PREF_QUALITY_VALUES.indexOf(it.quality.substringBefore(" ")) },
).thenByDescending { it.quality },
).reversed()
}
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("dd/MM/yyyy à's' HH:mm", Locale.ENGLISH)
}
const val PREFIX_SEARCH = "slug:"
private const val PREF_AUTHCODE_KEY = "authcode"
private const val PREF_AUTHCODE_SUMMARY = "Código de Autenticação"
private const val PREF_AUTHCODE_DEFAULT = ""
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Qualidade preferida"
private const val PREF_QUALITY_DEFAULT = "FULLHD"
private val PREF_QUALITY_VALUES = arrayOf("SD", "HD", "FULLHD")
}
}

View file

@ -1,98 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.hinatasoul.extractors
import android.util.Log
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelMapNotNullBlocking
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.OkHttpClient
class HinataSoulDownloadExtractor(
private val headers: Headers,
private val client: OkHttpClient,
) {
private val qualities = listOf("SD", "HD", "FULLHD")
private val tag by lazy { javaClass.simpleName }
private fun videosFromFile4Go(url: String, quality: String): Video? {
Log.d(tag, "Checking download for $url")
val docDownload = client.newCall(GET(url)).execute().asJsoup()
val form =
docDownload.selectFirst("button.download")?.closest("form")
if (form == null) {
Log.d(tag, "Download form not found for $url")
return null
}
val body = FormBody.Builder().apply {
form.select("input[name]").forEach {
add(it.attr("name"), it.attr("value"))
}
}.build()
val postUrl = form.attr("action")
val postHeaders = headers.newBuilder()
.set("Referer", url)
.build()
val docFinal =
client.newCall(POST(postUrl, headers = postHeaders, body = body))
.execute().asJsoup()
val videoUrl = docFinal.selectFirst("a.novobotao.download")?.attr("href")
if (videoUrl == null) {
Log.d(tag, "Download link not found for $url")
return null
}
return Video(videoUrl, "$quality - File4Go", videoUrl)
}
private fun videosFromDownloadPage(url: String, epName: String): List<Video> {
Log.d(tag, "Extracting videos links for URL: $url")
val docDownload = client.newCall(GET(url)).execute().asJsoup()
val row = docDownload.select("table.downloadpag_episodios tr").firstOrNull {
it.text().contains(epName)
}
if (row == null) {
Log.d(tag, "Episode $epName not found in download page")
return emptyList()
}
val links = row.select("td").mapIndexedNotNull { index, el ->
val link = el.selectFirst("a") ?: return@mapIndexedNotNull null
object {
var quality = qualities.get(index - 1)
var url = link.attr("href")
}
}
Log.d(tag, "Found ${links.size} links for $epName")
return links.parallelMapNotNullBlocking {
if (!it.url.contains("file4go.net")) {
return@parallelMapNotNullBlocking null
}
videosFromFile4Go(it.url, it.quality)
}.reversed()
}
fun videosFromUrl(url: String, epName: String, quality: String = "Default"): List<Video> {
if (url.contains("file4go.net")) {
return listOfNotNull(videosFromFile4Go(url, quality))
}
return videosFromDownloadPage(url, epName)
}
}

View file

@ -1,243 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.hinatasoul.extractors
import android.content.SharedPreferences
import android.util.Log
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelMapNotNullBlocking
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import org.jsoup.nodes.Document
import java.net.ProtocolException
class HinataSoulExtractor(
private val headers: Headers,
private val client: OkHttpClient,
private val preferences: SharedPreferences,
) {
private val tag by lazy { javaClass.simpleName }
private data class VideoExists(
val exists: Boolean,
val code: Int,
)
private fun checkVideoExists(url: String): VideoExists {
try {
val request = Request.Builder()
.head()
.url(url)
.headers(headers)
.build()
val response = client.newCall(request).execute()
return VideoExists(response.isSuccessful, response.code)
} catch (e: ProtocolException) {
// There are a bug in the response that sometimes that the content is without headers
if (e.message?.contains("Unexpected status line") == true) {
return VideoExists(true, 200)
}
}
return VideoExists(false, 404)
}
private fun getAdsUrl(
serverUrl: String,
thumbUrl: String,
link: String,
linkHeaders: Headers,
): String {
val videoName = serverUrl.split('/').last()
val finalLink =
if (link.startsWith("//")) {
"https:$link"
} else {
link
}
Log.d(tag, "Accessing the link $finalLink")
val response = client.newCall(GET(finalLink, headers = linkHeaders)).execute()
val docLink = response.asJsoup()
val refresh = docLink.selectFirst("meta[http-equiv=refresh]")?.attr("content")
if (!refresh.isNullOrBlank()) {
val newLink = refresh.substringAfter("=")
val newHeaders = linkHeaders.newBuilder().set("Referer", finalLink).build()
Log.d(tag, "Following link redirection to $newLink")
return getAdsUrl(serverUrl, thumbUrl, newLink, newHeaders)
}
val referer: String = docLink.location() ?: link
Log.d(tag, "Final URL: $referer")
Log.d(tag, "Fetching ADS URL")
val newHeaders = linkHeaders.newBuilder().set("Referer", "https://${referer.toHttpUrl().host}/").build()
try {
val now = System.currentTimeMillis()
val body = client.newCall(
GET(
"$SITE_URL?name=apphd/$videoName&img=$thumbUrl&pais=pais=BR&time=$now&url=$serverUrl",
headers = newHeaders,
),
)
.execute()
.body.string()
val adsUrl = body.let {
Regex("""ADS_URL\s*=\s*['"]([^'"]+)['"]""")
.find(it)?.groups?.get(1)?.value
?: ""
}
if (adsUrl.startsWith("http")) {
Log.d(tag, "ADS URL: $adsUrl")
return adsUrl
}
} catch (e: Exception) {
Log.e(tag, e.toString())
}
// Try default url
Log.e(tag, "Failed to get the ADS URL, trying the default")
return "https://www.popads.net/js/adblock.js"
}
private fun getAuthCode(serverUrl: String, thumbUrl: String, link: String): String {
var authCode = preferences.getString(PREF_AUTHCODE_KEY, "")!!
if (authCode.isNotBlank()) {
Log.d(tag, "AuthCode found in preferences")
val response = checkVideoExists("${serverUrl}$authCode")
if (response.exists || response.code == 500) {
Log.d(tag, "AuthCode is OK")
return authCode
}
Log.d(tag, "AuthCode is invalid")
}
Log.d(tag, "Fetching new authCode")
val adsUrl = getAdsUrl(serverUrl, thumbUrl, link, headers)
val adsContent = client.newCall(GET(adsUrl)).execute().body.string()
val body = FormBody.Builder()
.add("category", "client")
.add("type", "premium")
.add("ad", adsContent)
.build()
val newHeaders = headers.newBuilder()
.set("Referer", "https://${SITE_URL.toHttpUrl().host}/")
.add("Accept", "*/*")
.add("Cache-Control", "no-cache")
.add("Pragma", "no-cache")
.add("Connection", "keep-alive")
.add("Sec-Fetch-Dest", "empty")
.add("Sec-Fetch-Mode", "cors")
.add("Sec-Fetch-Site", "same-site")
.build()
val publicidade =
client.newCall(POST("$ADS_URL/", headers = newHeaders, body = body))
.execute()
.body.string()
.substringAfter("\"publicidade\"")
.substringAfter('"')
.substringBefore('"')
if (publicidade.isBlank()) {
Log.e(
tag,
"Failed to fetch \"publicidade\" code, the current response: $publicidade",
)
throw Exception("Por favor, abra o vídeo uma vez no navegador para liberar o IP")
}
authCode =
client.newCall(
GET(
"$ADS_URL/?token=$publicidade",
headers = newHeaders,
),
)
.execute()
.body.string()
.substringAfter("\"publicidade\"")
.substringAfter('"')
.substringBefore('"')
if (authCode.startsWith("?")) {
Log.d(tag, "Auth code fetched successfully")
preferences.edit().putString(PREF_AUTHCODE_KEY, authCode).commit()
} else {
Log.e(
tag,
"Failed to fetch auth code, the current response: $authCode",
)
}
return authCode
}
fun getVideoList(doc: Document): List<Video> {
val hasFHD = doc.selectFirst("div.abaItem:contains(FULLHD)") != null
val serverUrl = doc.selectFirst("meta[itemprop=contentURL]")!!
.attr("content")
.replace("cdn1", "cdn3")
val thumbUrl = doc.selectFirst("meta[itemprop=thumbnailUrl]")!!
.attr("content")
val type = serverUrl.split("/").get(3)
val qualities = listOfNotNull("SD", "HD", if (hasFHD) "FULLHD" else null)
val paths = listOf("appsd", "apphd").let {
if (type.endsWith("2")) {
it.map { path -> path + "2" }
} else {
it
}
} + listOf("appfullhd")
val firstLink =
doc.selectFirst("div.video_container > a, div.playerContainer > a")!!.attr("href")
val authCode = getAuthCode(serverUrl, thumbUrl, firstLink)
return qualities
.mapIndexed { index, quality ->
object {
var path = paths[index]
var url = serverUrl.replace(type, path) + authCode
var quality = "$quality - Anitube"
}
}
.parallelMapNotNullBlocking {
if (!checkVideoExists(it.url).exists) {
Log.d(tag, "Video not exists: ${it.url.substringBefore("?")}")
return@parallelMapNotNullBlocking null
}
Video(it.url, it.quality, it.url, headers = headers)
}
.reversed()
}
companion object {
private const val PREF_AUTHCODE_KEY = "authcode"
private const val ADS_URL = "https://ads.anitube.vip"
private const val SITE_URL = "https://www.hinatasoul.com/luffy.php"
}
}