Merge branch 'Kohi-den:main' into main

This commit is contained in:
Dark25 2024-09-02 14:09:14 +01:00 committed by GitHub
commit 226fe17550
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 866 additions and 1383 deletions

View file

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 2 baseVersionCode = 3

View file

@ -8,6 +8,10 @@ import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.parseAs import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.add
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
@ -32,7 +36,7 @@ abstract class AniListAnimeHttpSource : AnimeHttpSource() {
query = ANIME_LIST_QUERY, query = ANIME_LIST_QUERY,
variables = AnimeListVariables( variables = AnimeListVariables(
page = page, page = page,
sort = AnimeListVariables.MediaSort.POPULARITY_DESC, sort = AnimeListVariables.MediaSort.TRENDING_DESC,
), ),
) )
} }
@ -58,20 +62,58 @@ abstract class AniListAnimeHttpSource : AnimeHttpSource() {
/* ===================================== Search Anime ===================================== */ /* ===================================== Search Anime ===================================== */
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
return buildAnimeListRequest( val params = AniListFilters.getSearchParameters(filters)
query = ANIME_LIST_QUERY,
variables = AnimeListVariables( val variablesObject = buildJsonObject {
page = page, put("page", page)
sort = AnimeListVariables.MediaSort.SEARCH_MATCH, put("perPage", 30)
search = query.ifBlank { null }, put("isAdult", false)
), put("type", "ANIME")
) put("sort", params.sort)
if (query.isNotBlank()) put("search", query)
if (params.genres.isNotEmpty()) {
putJsonArray("genres") {
params.genres.forEach { add(it) }
}
}
if (params.format.isNotEmpty()) {
putJsonArray("format") {
params.format.forEach { add(it) }
}
}
if (params.season.isBlank() && params.year.isNotBlank()) {
put("year", "${params.year}%")
}
if (params.season.isNotBlank() && params.year.isBlank()) {
throw Exception("Year cannot be blank if season is set")
}
if (params.season.isNotBlank() && params.year.isNotBlank()) {
put("season", params.season)
put("seasonYear", params.year)
}
if (params.status.isNotBlank()) {
put("status", params.status)
}
}
val variables = json.encodeToString(variablesObject)
return buildRequest(query = SORT_QUERY, variables = variables)
} }
override fun searchAnimeParse(response: Response): AnimesPage { override fun searchAnimeParse(response: Response): AnimesPage {
return parseAnimeListResponse(response) return parseAnimeListResponse(response)
} }
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AniListFilters.FILTER_LIST
/* ===================================== Anime Details ===================================== */ /* ===================================== Anime Details ===================================== */
override fun animeDetailsRequest(anime: SAnime): Request { override fun animeDetailsRequest(anime: SAnime): Request {
return buildRequest( return buildRequest(
@ -145,6 +187,9 @@ abstract class AniListAnimeHttpSource : AnimeHttpSource() {
status = when (media.status) { status = when (media.status) {
AniListMedia.Status.RELEASING -> SAnime.ONGOING AniListMedia.Status.RELEASING -> SAnime.ONGOING
AniListMedia.Status.FINISHED -> SAnime.COMPLETED AniListMedia.Status.FINISHED -> SAnime.COMPLETED
AniListMedia.Status.NOT_YET_RELEASED -> SAnime.LICENSED
AniListMedia.Status.CANCELLED -> SAnime.CANCELLED
AniListMedia.Status.HIATUS -> SAnime.ON_HIATUS
} }
thumbnail_url = media.coverImage.large thumbnail_url = media.coverImage.large
} }

View file

@ -0,0 +1,236 @@
package eu.kanade.tachiyomi.multisrc.anilist
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AniListFilters {
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, val pairs: Array<Pair<String, String>>) :
AnimeFilter.Group<AnimeFilter.CheckBox>(name, pairs.map { CheckBoxVal(it.first, false) })
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return (getFirst<R>() as QueryPartFilter).toQueryPart()
}
private inline fun <reified R> AnimeFilterList.getFirst(): R {
return first { it is R } as R
}
private inline fun <reified R> AnimeFilterList.parseCheckboxList(
options: Array<Pair<String, String>>,
): List<String> {
return (getFirst<R>() as CheckBoxFilterList).state
.filter { it.state }
.map { checkBox -> options.find { it.first == checkBox.name }!!.second }
.filter(String::isNotBlank)
}
private inline fun <reified R> AnimeFilterList.getSort(): String {
val state = (getFirst<R>() as AnimeFilter.Sort).state ?: return ""
val index = state.index
val suffix = if (state.ascending) "" else "_DESC"
return AniListFiltersData.SORT_LIST[index].second + suffix
}
class GenreFilter : CheckBoxFilterList("Genres", AniListFiltersData.GENRE_LIST)
class YearFilter : QueryPartFilter("Year", AniListFiltersData.YEAR_LIST)
class SeasonFilter : QueryPartFilter("Season", AniListFiltersData.SEASON_LIST)
class FormatFilter : CheckBoxFilterList("Format", AniListFiltersData.FORMAT_LIST)
class StatusFilter : QueryPartFilter("Airing Status", AniListFiltersData.STATUS_LIST)
class SortFilter : AnimeFilter.Sort(
"Sort",
AniListFiltersData.SORT_LIST.map { it.first }.toTypedArray(),
Selection(1, false),
)
val FILTER_LIST get() = AnimeFilterList(
GenreFilter(),
YearFilter(),
SeasonFilter(),
FormatFilter(),
StatusFilter(),
SortFilter(),
)
class FilterSearchParams(
val genres: List<String> = emptyList(),
val year: String = "",
val season: String = "",
val format: List<String> = emptyList(),
val status: String = "",
val sort: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.parseCheckboxList<GenreFilter>(AniListFiltersData.GENRE_LIST),
filters.asQueryPart<YearFilter>(),
filters.asQueryPart<SeasonFilter>(),
filters.parseCheckboxList<FormatFilter>(AniListFiltersData.FORMAT_LIST),
filters.asQueryPart<StatusFilter>(),
filters.getSort<SortFilter>(),
)
}
private object AniListFiltersData {
val GENRE_LIST = arrayOf(
Pair("Action", "Action"),
Pair("Adventure", "Adventure"),
Pair("Comedy", "Comedy"),
Pair("Drama", "Drama"),
Pair("Ecchi", "Ecchi"),
Pair("Fantasy", "Fantasy"),
Pair("Horror", "Horror"),
Pair("Mahou Shoujo", "Mahou Shoujo"),
Pair("Mecha", "Mecha"),
Pair("Music", "Music"),
Pair("Mystery", "Mystery"),
Pair("Psychological", "Psychological"),
Pair("Romance", "Romance"),
Pair("Sci-Fi", "Sci-Fi"),
Pair("Slice of Life", "Slice of Life"),
Pair("Sports", "Sports"),
Pair("Supernatural", "Supernatural"),
Pair("Thriller", "Thriller"),
)
val YEAR_LIST = arrayOf(
Pair("<Select>", ""),
Pair("2024", "2024"),
Pair("2023", "2023"),
Pair("2022", "2022"),
Pair("2021", "2021"),
Pair("2020", "2020"),
Pair("2019", "2019"),
Pair("2018", "2018"),
Pair("2017", "2017"),
Pair("2016", "2016"),
Pair("2015", "2015"),
Pair("2014", "2014"),
Pair("2013", "2013"),
Pair("2012", "2012"),
Pair("2011", "2011"),
Pair("2010", "2010"),
Pair("2009", "2009"),
Pair("2008", "2008"),
Pair("2007", "2007"),
Pair("2006", "2006"),
Pair("2005", "2005"),
Pair("2004", "2004"),
Pair("2003", "2003"),
Pair("2002", "2002"),
Pair("2001", "2001"),
Pair("2000", "2000"),
Pair("1999", "1999"),
Pair("1998", "1998"),
Pair("1997", "1997"),
Pair("1996", "1996"),
Pair("1995", "1995"),
Pair("1994", "1994"),
Pair("1993", "1993"),
Pair("1992", "1992"),
Pair("1991", "1991"),
Pair("1990", "1990"),
Pair("1989", "1989"),
Pair("1988", "1988"),
Pair("1987", "1987"),
Pair("1986", "1986"),
Pair("1985", "1985"),
Pair("1984", "1984"),
Pair("1983", "1983"),
Pair("1982", "1982"),
Pair("1981", "1981"),
Pair("1980", "1980"),
Pair("1979", "1979"),
Pair("1978", "1978"),
Pair("1977", "1977"),
Pair("1976", "1976"),
Pair("1975", "1975"),
Pair("1974", "1974"),
Pair("1973", "1973"),
Pair("1972", "1972"),
Pair("1971", "1971"),
Pair("1970", "1970"),
Pair("1969", "1969"),
Pair("1968", "1968"),
Pair("1967", "1967"),
Pair("1966", "1966"),
Pair("1965", "1965"),
Pair("1964", "1964"),
Pair("1963", "1963"),
Pair("1962", "1962"),
Pair("1961", "1961"),
Pair("1960", "1960"),
Pair("1959", "1959"),
Pair("1958", "1958"),
Pair("1957", "1957"),
Pair("1956", "1956"),
Pair("1955", "1955"),
Pair("1954", "1954"),
Pair("1953", "1953"),
Pair("1952", "1952"),
Pair("1951", "1951"),
Pair("1950", "1950"),
Pair("1949", "1949"),
Pair("1948", "1948"),
Pair("1947", "1947"),
Pair("1946", "1946"),
Pair("1945", "1945"),
Pair("1944", "1944"),
Pair("1943", "1943"),
Pair("1942", "1942"),
Pair("1941", "1941"),
Pair("1940", "1940"),
)
val SEASON_LIST = arrayOf(
Pair("<Select>", ""),
Pair("Winter", "WINTER"),
Pair("Spring", "SPRING"),
Pair("Summer", "SUMMER"),
Pair("Fall", "FALL"),
)
val FORMAT_LIST = arrayOf(
Pair("TV Show", "TV"),
Pair("Movie", "MOVIE"),
Pair("TV Short", "TV_SHORT"),
Pair("Special", "SPECIAL"),
Pair("OVA", "OVA"),
Pair("ONA", "ONA"),
Pair("Music", "MUSIC"),
)
val STATUS_LIST = arrayOf(
Pair("<Select>", ""),
Pair("Airing", "RELEASING"),
Pair("Finished", "FINISHED"),
Pair("Not Yet Aired", "NOT_YET_RELEASED"),
Pair("Cancelled", "CANCELLED"),
)
val SORT_LIST = arrayOf(
Pair("Title", "TITLE_ENGLISH"),
Pair("Popularity", "POPULARITY"),
Pair("Average Score", "SCORE"),
Pair("Trending", "TRENDING"),
Pair("Favorites", "FAVOURITES"),
Pair("Date Added", "ID"),
Pair("Release Date", "START_DATE"),
)
}
}

View file

@ -55,16 +55,76 @@ internal const val ANIME_DETAILS_QUERY = """
} }
} }
""" """
private fun String.toQuery() = this.trimIndent().replace("%", "$")
internal val SORT_QUERY = """
query (
${"$"}page: Int,
${"$"}perPage: Int,
${"$"}isAdult: Boolean,
${"$"}type: MediaType,
${"$"}sort: [MediaSort],
${"$"}status: MediaStatus,
${"$"}search: String,
${"$"}genres: [String],
${"$"}year: String,
${"$"}seasonYear: Int,
${"$"}season: MediaSeason,
${"$"}format: [MediaFormat]
) {
Page (page: ${"$"}page, perPage: ${"$"}perPage) {
pageInfo {
hasNextPage
}
media (
isAdult: ${"$"}isAdult,
type: ${"$"}type,
sort: ${"$"}sort,
status: ${"$"}status,
search: ${"$"}search,
genre_in: ${"$"}genres,
startDate_like: ${"$"}year,
seasonYear: ${"$"}seasonYear,
season: ${"$"}season,
format_in: ${"$"}format
) {
id
title {
romaji
english
native
}
coverImage {
extraLarge
large
medium
}
status
genres
studios {
nodes {
name
}
}
}
}
}
""".toQuery()
@Serializable @Serializable
internal data class AnimeListVariables( internal data class AnimeListVariables(
val page: Int, val page: Int,
val sort: MediaSort, val sort: MediaSort,
val search: String? = null, val search: String? = null,
val genre: String? = null,
val year: String? = null,
val status: String? = null,
val format: String? = null,
val season: String? = null,
val seasonYear: String? = null,
val isAdult: Boolean = false,
) { ) {
enum class MediaSort { enum class MediaSort {
POPULARITY_DESC, TRENDING_DESC,
SEARCH_MATCH,
START_DATE_DESC, START_DATE_DESC,
} }
} }

View file

@ -47,6 +47,9 @@ internal data class AniListMedia(
enum class Status { enum class Status {
RELEASING, RELEASING,
FINISHED, FINISHED,
NOT_YET_RELEASED,
CANCELLED,
HIATUS,
} }
@Serializable @Serializable

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Sudatchi' extName = 'Sudatchi'
extClass = '.Sudatchi' extClass = '.Sudatchi'
extVersionCode = 3 extVersionCode = 4
isNsfw = true isNsfw = true
} }

View file

@ -39,7 +39,7 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
override val baseUrl = "https://sudatchi.com" override val baseUrl = "https://sudatchi.com"
private val ipfsUrl = "https://ipfs.animeui.com" private val ipfsUrl = "https://gboesk298le91ct41kibaonc7o.ingress.akashprovid.com"
override val lang = "all" override val lang = "all"

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Torrentio Anime (Torrent / Debrid)' extName = 'Torrentio Anime (Torrent / Debrid)'
extClass = '.Torrentio' extClass = '.Torrentio'
extVersionCode = 7 extVersionCode = 8
containsNsfw = false containsNsfw = false
} }

View file

@ -29,9 +29,11 @@ import kotlinx.serialization.json.Json
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.json.JSONObject
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.net.URL
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@ -358,7 +360,9 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
// ============================== Episodes ============================== // ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime): Request { override fun episodeListRequest(anime: SAnime): Request {
return GET("https://anime-kitsu.strem.fun/meta/series/anilist%3A${anime.url}.json") val res = URL("https://api.ani.zip/mappings?anilist_id=${anime.url}").readText()
val kitsuId = JSONObject(res).getJSONObject("mappings").getInt("kitsu_id").toString()
return GET("https://anime-kitsu.strem.fun/meta/series/kitsu%3A$kitsuId.json")
} }
override fun episodeListParse(response: Response): List<SEpisode> { override fun episodeListParse(response: Response): List<SEpisode> {

View file

@ -2,7 +2,7 @@ ext {
extName = 'AniPlay' extName = 'AniPlay'
extClass = '.AniPlay' extClass = '.AniPlay'
themePkg = 'anilist' themePkg = 'anilist'
overrideVersionCode = 1 overrideVersionCode = 2
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.animeextension.en.aniplay
import android.app.Application import android.app.Application
import android.util.Base64 import android.util.Base64
import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
@ -17,6 +18,7 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.parallelFlatMapBlocking import eu.kanade.tachiyomi.util.parallelFlatMapBlocking
import eu.kanade.tachiyomi.util.parseAs import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
@ -25,6 +27,7 @@ import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.IOException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@ -83,26 +86,26 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
providers.forEach { provider -> providers.forEach { provider ->
provider.episodes.forEach { episode -> provider.episodes.forEach { episode ->
if (!episodes.containsKey(episode.number)) { val episodeNumber = episode.number.toString().toIntOrNull() ?: episode.number.toInt()
episodes[episode.number] = episode if (!episodes.containsKey(episodeNumber)) {
episodes[episodeNumber] = episode
} }
val existingEpisodeExtras = episodeExtras.getOrElse(episode.number) { emptyList() } val existingEpisodeExtras = episodeExtras.getOrElse(episodeNumber) { emptyList() }
val episodeExtra = EpisodeExtra( val episodeExtra = EpisodeExtra(
source = provider.providerId, source = provider.providerId,
episodeId = episode.id, episodeId = episode.id,
hasDub = episode.hasDub, hasDub = episode.hasDub,
) )
episodeExtras[episode.number] = existingEpisodeExtras + listOf(episodeExtra) episodeExtras[episodeNumber] = existingEpisodeExtras + listOf(episodeExtra)
} }
} }
return episodes.map { episodeMap -> return episodes.map { episodeMap ->
val episode = episodeMap.value val episode = episodeMap.value
val episodeNumber = episode.number val episodeNumber = episodeMap.key
val episodeExtra = episodeExtras.getValue(episodeNumber) val episodeExtra = episodeExtras.getValue(episodeNumber)
val episodeExtraString = json.encodeToString(episodeExtra) val episodeExtraString = json.encodeToString(episodeExtra)
.let { Base64.encode(it.toByteArray(), Base64.DEFAULT) } .let { Base64.encodeToString(it.toByteArray(), Base64.DEFAULT) }
.toString(Charsets.UTF_8)
val url = baseUrl.toHttpUrl().newBuilder() val url = baseUrl.toHttpUrl().newBuilder()
.addPathSegment("anime") .addPathSegment("anime")
@ -112,7 +115,7 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
.addQueryParameter("extras", episodeExtraString) .addQueryParameter("extras", episodeExtraString)
.build() .build()
val name = parseEpisodeName(episodeNumber, episode.title) val name = parseEpisodeName(episodeNumber.toString(), episode.title)
val uploadDate = parseDate(episode.createdAt) val uploadDate = parseDate(episode.createdAt)
val dub = when { val dub = when {
episodeExtra.any { it.hasDub } -> ", Dub" episodeExtra.any { it.hasDub } -> ", Dub"
@ -142,45 +145,57 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
val episodeNum = episodeUrl.queryParameter("ep") ?: return emptyList() val episodeNum = episodeUrl.queryParameter("ep") ?: return emptyList()
val extras = episodeUrl.queryParameter("extras") val extras = episodeUrl.queryParameter("extras")
?.let { ?.let {
Base64.decode(it, Base64.DEFAULT).toString(Charsets.UTF_8) try {
Base64.decode(it, Base64.DEFAULT).toString(Charsets.UTF_8)
} catch (e: IllegalArgumentException) {
Log.e("AniPlay", "Error decoding base64", e)
return emptyList()
}
}
?.let {
try {
json.decodeFromString<List<EpisodeExtra>>(it)
} catch (e: SerializationException) {
Log.e("AniPlay", "Error parsing JSON", e)
emptyList()
}
} }
?.let { json.decodeFromString<List<EpisodeExtra>>(it) }
?: emptyList() ?: emptyList()
val episodeDataList = extras.parallelFlatMapBlocking { extra -> val episodeDataList = extras.parallelFlatMapBlocking { extra ->
val languages = mutableListOf("sub") val languages = mutableListOf("sub").apply {
if (extra.hasDub) { if (extra.hasDub) add("dub")
languages.add("dub")
} }
val url = "$baseUrl/api/anime/source/$animeId" val url = "$baseUrl/api/anime/source/$animeId"
languages.map { language -> languages.map { language ->
val requestBody = json val requestBody = json.encodeToString(
.encodeToString( VideoSourceRequest(
VideoSourceRequest( source = extra.source,
source = extra.source, episodeId = extra.episodeId,
episodeId = extra.episodeId, episodeNum = episodeNum,
episodeNum = episodeNum, subType = language,
subType = language, ),
), ).toRequestBody("application/json".toMediaType())
try {
val response = client.newCall(POST(url = url, body = requestBody)).execute().parseAs<VideoSourceResponse>()
EpisodeData(
source = extra.source,
language = language,
response = response,
) )
.toRequestBody("application/json".toMediaType()) } catch (e: IOException) {
null // Return null to be filtered out
val response = client } catch (e: Exception) {
.newCall(POST(url = url, body = requestBody)) null // Return null to be filtered out
.execute() }
.parseAs<VideoSourceResponse>() }.filterNotNull() // Filter out null values due to errors
EpisodeData(
source = extra.source,
language = language,
response = response,
)
}
} }
val videos = episodeDataList.flatMap { episodeData -> val videos = episodeDataList.flatMap { episodeData ->
val defaultSource = episodeData.response.sources?.first { val defaultSource = episodeData.response.sources?.firstOrNull {
it.quality in listOf("default", "auto") it.quality in listOf("default", "auto")
} ?: return@flatMap emptyList() } ?: return@flatMap emptyList()
@ -316,14 +331,13 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
} }
/* =================================== AniPlay Utilities =================================== */ /* =================================== AniPlay Utilities =================================== */
private fun parseEpisodeName(number: String, title: String?): String {
private fun parseEpisodeName(number: Int, name: String): String { return if (title.isNullOrBlank()) {
return when { "Episode $number"
listOf("EP ", "EPISODE ").any(name::startsWith) -> "Episode $number" } else {
else -> "Episode $number: $name" "Episode $number: $title"
} }
} }
private fun getServerName(value: String): String { private fun getServerName(value: String): String {
val index = PREF_SERVER_ENTRY_VALUES.indexOf(value) val index = PREF_SERVER_ENTRY_VALUES.indexOf(value)
return PREF_SERVER_ENTRIES[index] return PREF_SERVER_ENTRIES[index]

View file

@ -12,7 +12,7 @@ data class EpisodeListResponse(
@Serializable @Serializable
data class Episode( data class Episode(
val id: String, val id: String,
val number: Int, val number: Float,
val title: String, val title: String,
val hasDub: Boolean, val hasDub: Boolean,
val isFiller: Boolean, val isFiller: Boolean,

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Animes Games' extName = 'Animes Games'
extClass = '.AnimesGames' extClass = '.AnimesGames'
extVersionCode = 3 extVersionCode = 4
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -61,7 +61,7 @@ class AnimesGames : ParsedAnimeHttpSource() {
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply { override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href")) setUrlWithoutDomain(element.attr("href"))
title = element.selectFirst("div.tituloEP")!!.text() title = element.selectFirst("div.tituloEP")!!.text()
thumbnail_url = element.selectFirst("img")?.attr("data-lazy-src") thumbnail_url = element.selectFirst("img")?.getImageUrl()
} }
override fun latestUpdatesNextPageSelector() = "ol.pagination > a:contains(>)" override fun latestUpdatesNextPageSelector() = "ol.pagination > a:contains(>)"
@ -144,7 +144,7 @@ class AnimesGames : ParsedAnimeHttpSource() {
override fun searchAnimeFromElement(element: Element) = SAnime.create().apply { override fun searchAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href")) setUrlWithoutDomain(element.attr("href"))
title = element.selectFirst("div.tituloAnime")!!.text() title = element.selectFirst("div.tituloAnime")!!.text()
thumbnail_url = element.selectFirst("img")!!.attr("src") thumbnail_url = element.selectFirst("img")!!.getImageUrl()
} }
override fun searchAnimeNextPageSelector(): String? { override fun searchAnimeNextPageSelector(): String? {
@ -159,7 +159,7 @@ class AnimesGames : ParsedAnimeHttpSource() {
title = content.selectFirst("section > h1")!!.text() title = content.selectFirst("section > h1")!!.text()
.removePrefix("Assistir ") .removePrefix("Assistir ")
.removeSuffix("Temporada Online") .removeSuffix("Temporada Online")
thumbnail_url = content.selectFirst("img")?.attr("data-lazy-src") thumbnail_url = content.selectFirst("img")?.getImageUrl()
description = content.select("section.sinopseEp p").eachText().joinToString("\n") description = content.select("section.sinopseEp p").eachText().joinToString("\n")
val infos = content.selectFirst("div.info > ol")!! val infos = content.selectFirst("div.info > ol")!!
@ -273,6 +273,19 @@ class AnimesGames : ParsedAnimeHttpSource() {
.getOrNull() ?: 0L .getOrNull() ?: 0L
} }
/**
* Tries to get the image url via various possible attributes.
* Taken from Tachiyomi's Madara multisrc.
*/
protected open fun Element.getImageUrl(): String? {
return when {
hasAttr("data-src") -> attr("abs:data-src")
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
hasAttr("srcset") -> attr("abs:srcset").substringBefore(" ")
else -> attr("abs:src")
}.substringBefore("?resize")
}
companion object { companion object {
const val PREFIX_SEARCH = "id:" const val PREFIX_SEARCH = "id:"

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,7 +1,7 @@
ext { ext {
extName = 'Anitube' extName = 'Anitube'
extClass = '.Anitube' extClass = '.Anitube'
extVersionCode = 18 extVersionCode = 19
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -4,6 +4,7 @@ import android.app.Application
import androidx.preference.EditTextPreference import androidx.preference.EditTextPreference
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.pt.anitube.extractors.AnitubeDownloadExtractor
import eu.kanade.tachiyomi.animeextension.pt.anitube.extractors.AnitubeExtractor import eu.kanade.tachiyomi.animeextension.pt.anitube.extractors.AnitubeExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
@ -15,6 +16,7 @@ import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
@ -189,9 +191,29 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
// ============================ Video Links ============================= // ============================ Video Links =============================
private val extractor by lazy { AnitubeExtractor(headers, client, preferences) } private val anitubeExtractor by lazy { AnitubeExtractor(headers, client, preferences) }
private val downloadExtractor by lazy { AnitubeDownloadExtractor(headers, client) }
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val links = mutableListOf(document.location())
document.selectFirst("div.abaItemDown > a")?.attr("href")?.let {
links.add(it)
}
val epName = document.selectFirst("meta[itemprop=name]")!!.attr("content")
return links.parallelCatchingFlatMapBlocking {
when {
it.contains("/download/") -> downloadExtractor.videosFromUrl(it, epName)
it.contains("file4go.net") -> downloadExtractor.videosFromUrl(it, epName)
else -> anitubeExtractor.getVideoList(document)
}
}
}
override fun videoListParse(response: Response) = extractor.getVideoList(response)
override fun videoListSelector() = throw UnsupportedOperationException() override fun videoListSelector() = throw UnsupportedOperationException()
override fun videoFromElement(element: Element) = throw UnsupportedOperationException() override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException() override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
@ -264,8 +286,11 @@ class Anitube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!! val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith( return sortedWith(
compareByDescending { it.quality.equals(quality) }, compareBy<Video>(
) { it.quality.startsWith(quality) },
{ PREF_QUALITY_ENTRIES.indexOf(it.quality.substringBefore(" ")) },
).thenByDescending { it.quality },
).reversed()
} }
private fun String.toDate(): Long { private fun String.toDate(): Long {

View file

@ -0,0 +1,98 @@
package eu.kanade.tachiyomi.animeextension.pt.anitube.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 AnitubeDownloadExtractor(
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

@ -6,11 +6,14 @@ import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelMapNotNullBlocking
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import org.jsoup.nodes.Document
import java.net.ProtocolException
class AnitubeExtractor( class AnitubeExtractor(
private val headers: Headers, private val headers: Headers,
@ -20,6 +23,32 @@ class AnitubeExtractor(
private val tag by lazy { javaClass.simpleName } 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( private fun getAdsUrl(
serverUrl: String, serverUrl: String,
thumbUrl: String, thumbUrl: String,
@ -28,15 +57,21 @@ class AnitubeExtractor(
): String { ): String {
val videoName = serverUrl.split('/').last() val videoName = serverUrl.split('/').last()
Log.d(tag, "Accessing the link $link") val finalLink =
val response = client.newCall(GET(link, headers = linkHeaders)).execute() 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 docLink = response.asJsoup()
val refresh = docLink.selectFirst("meta[http-equiv=refresh]")?.attr("content") val refresh = docLink.selectFirst("meta[http-equiv=refresh]")?.attr("content")
if (!refresh.isNullOrBlank()) { if (!refresh.isNullOrBlank()) {
val newLink = refresh.substringAfter("=") val newLink = refresh.substringAfter("=")
val newHeaders = linkHeaders.newBuilder().set("Referer", link).build() val newHeaders = linkHeaders.newBuilder().set("Referer", finalLink).build()
Log.d(tag, "Following link redirection to $newLink") Log.d(tag, "Following link redirection to $newLink")
return getAdsUrl(serverUrl, thumbUrl, newLink, newHeaders) return getAdsUrl(serverUrl, thumbUrl, newLink, newHeaders)
@ -47,30 +82,32 @@ class AnitubeExtractor(
Log.d(tag, "Final URL: $referer") Log.d(tag, "Final URL: $referer")
Log.d(tag, "Fetching ADS URL") Log.d(tag, "Fetching ADS URL")
val newHeaders = linkHeaders.newBuilder().set("Referer", referer).build() val newHeaders =
linkHeaders.newBuilder().set("Referer", "https://${referer.toHttpUrl().host}/").build()
try { try {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
val adsUrl = val body = client.newCall(
client.newCall( GET(
GET( "$SITE_URL?name=apphd/$videoName&img=$thumbUrl&pais=pais=BR&time=$now&url=$serverUrl",
"$SITE_URL/playerricas.php?name=apphd/$videoName&img=$thumbUrl&pais=pais=BR&time=$now&url=$serverUrl", headers = newHeaders,
headers = newHeaders, ),
), )
) .execute()
.execute() .body.string()
.body.string()
.let { val adsUrl = body.let {
Regex("""ADS_URL\s*=\s*['"]([^'"]+)['"]""") Regex("""ADS_URL\s*=\s*['"]([^'"]+)['"]""")
.find(it)?.groups?.get(1)?.value .find(it)?.groups?.get(1)?.value
?: "" ?: ""
} }
if (adsUrl.startsWith("http")) { if (adsUrl.startsWith("http")) {
Log.d(tag, "ADS URL: $adsUrl") Log.d(tag, "ADS URL: $adsUrl")
return adsUrl return adsUrl
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(tag, e.toString())
} }
// Try default url // Try default url
@ -84,15 +121,9 @@ class AnitubeExtractor(
if (authCode.isNotBlank()) { if (authCode.isNotBlank()) {
Log.d(tag, "AuthCode found in preferences") Log.d(tag, "AuthCode found in preferences")
val request = Request.Builder() val response = checkVideoExists("${serverUrl}$authCode")
.head()
.url("${serverUrl}$authCode")
.headers(headers)
.build()
val response = client.newCall(request).execute() if (response.exists || response.code == 500) {
if (response.isSuccessful || response.code == 500) {
Log.d(tag, "AuthCode is OK") Log.d(tag, "AuthCode is OK")
return authCode return authCode
} }
@ -112,7 +143,7 @@ class AnitubeExtractor(
.build() .build()
val newHeaders = headers.newBuilder() val newHeaders = headers.newBuilder()
.set("Referer", SITE_URL) .set("Referer", "https://${SITE_URL.toHttpUrl().host}/")
.add("Accept", "*/*") .add("Accept", "*/*")
.add("Cache-Control", "no-cache") .add("Cache-Control", "no-cache")
.add("Pragma", "no-cache") .add("Pragma", "no-cache")
@ -165,8 +196,7 @@ class AnitubeExtractor(
return authCode return authCode
} }
fun getVideoList(response: Response): List<Video> { fun getVideoList(doc: Document): List<Video> {
val doc = response.asJsoup()
val hasFHD = doc.selectFirst("div.abaItem:contains(FULLHD)") != null val hasFHD = doc.selectFirst("div.abaItem:contains(FULLHD)") != null
val serverUrl = doc.selectFirst("meta[itemprop=contentURL]")!! val serverUrl = doc.selectFirst("meta[itemprop=contentURL]")!!
.attr("content") .attr("content")
@ -188,16 +218,27 @@ class AnitubeExtractor(
val authCode = getAuthCode(serverUrl, thumbUrl, firstLink) val authCode = getAuthCode(serverUrl, thumbUrl, firstLink)
return qualities.mapIndexed { index, quality -> return qualities
val path = paths[index] .mapIndexed { index, quality ->
val url = serverUrl.replace(type, path) + authCode object {
Video(url, quality, url, headers = headers) var path = paths[index]
}.reversed() 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 { companion object {
private const val PREF_AUTHCODE_KEY = "authcode" private const val PREF_AUTHCODE_KEY = "authcode"
private const val ADS_URL = "https://ads.anitube.vip" private const val ADS_URL = "https://ads.anitube.vip"
private const val SITE_URL = "https://www.anitube.vip" private const val SITE_URL = "https://www.anitube.vip/playerricas.php"
} }
} }

View file

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

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,7 +1,7 @@
ext { ext {
extName = 'Hinata Soul' extName = 'Hinata Soul'
extClass = '.HinataSoul' extClass = '.HinataSoul'
extVersionCode = 8 extVersionCode = 9
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -4,6 +4,7 @@ import android.app.Application
import androidx.preference.EditTextPreference import androidx.preference.EditTextPreference
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen 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.animeextension.pt.hinatasoul.extractors.HinataSoulExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
@ -15,6 +16,7 @@ import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
@ -67,7 +69,11 @@ class HinataSoul : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun latestUpdatesNextPageSelector() = null override fun latestUpdatesNextPageSelector() = null
// =============================== Search =============================== // =============================== Search ===============================
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage { override suspend fun getSearchAnime(
page: Int,
query: String,
filters: AnimeFilterList,
): AnimesPage {
return if (query.startsWith(PREFIX_SEARCH)) { return if (query.startsWith(PREFIX_SEARCH)) {
val slug = query.removePrefix(PREFIX_SEARCH) val slug = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/animes/$slug")) client.newCall(GET("$baseUrl/animes/$slug"))
@ -156,16 +162,47 @@ class HinataSoul : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val title = element.attr("title") val title = element.attr("title")
setUrlWithoutDomain(element.attr("href")) setUrlWithoutDomain(element.attr("href"))
name = title name = title
episode_number = title.substringBeforeLast(" - FINAL").substringAfterLast(" ").toFloatOrNull() ?: 0F episode_number =
title.substringBeforeLast(" - FINAL").substringAfterLast(" ").toFloatOrNull() ?: 0F
date_upload = element.selectFirst("div.lancaster_episodio_info_data")!! date_upload = element.selectFirst("div.lancaster_episodio_info_data")!!
.text() .text()
.toDate() .toDate()
} }
// ============================ Video Links ============================= // ============================ Video Links =============================
private val extractor by lazy { HinataSoulExtractor(headers, client, preferences) } private val hinataExtractor by lazy { HinataSoulExtractor(headers, client, preferences) }
private val downloadExtractor by lazy { HinataSoulDownloadExtractor(headers, client) }
override fun videoListParse(response: Response) = extractor.getVideoList(response) 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 videoListSelector() = throw UnsupportedOperationException()
override fun videoFromElement(element: Element) = throw UnsupportedOperationException() override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
@ -246,7 +283,10 @@ class HinataSoul : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!! val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith( return sortedWith(
compareBy { it.quality.contains(quality) }, compareBy<Video>(
{ it.quality.startsWith(quality) },
{ PREF_QUALITY_VALUES.indexOf(it.quality.substringBefore(" ")) },
).thenByDescending { it.quality },
).reversed() ).reversed()
} }

View file

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

@ -6,11 +6,14 @@ import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelMapNotNullBlocking
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import org.jsoup.nodes.Document
import java.net.ProtocolException
class HinataSoulExtractor( class HinataSoulExtractor(
private val headers: Headers, private val headers: Headers,
@ -20,6 +23,32 @@ class HinataSoulExtractor(
private val tag by lazy { javaClass.simpleName } 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( private fun getAdsUrl(
serverUrl: String, serverUrl: String,
thumbUrl: String, thumbUrl: String,
@ -28,15 +57,21 @@ class HinataSoulExtractor(
): String { ): String {
val videoName = serverUrl.split('/').last() val videoName = serverUrl.split('/').last()
Log.d(tag, "Accessing the link $link") val finalLink =
val response = client.newCall(GET(link, headers = linkHeaders)).execute() 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 docLink = response.asJsoup()
val refresh = docLink.selectFirst("meta[http-equiv=refresh]")?.attr("content") val refresh = docLink.selectFirst("meta[http-equiv=refresh]")?.attr("content")
if (!refresh.isNullOrBlank()) { if (!refresh.isNullOrBlank()) {
val newLink = refresh.substringAfter("=") val newLink = refresh.substringAfter("=")
val newHeaders = linkHeaders.newBuilder().set("Referer", link).build() val newHeaders = linkHeaders.newBuilder().set("Referer", finalLink).build()
Log.d(tag, "Following link redirection to $newLink") Log.d(tag, "Following link redirection to $newLink")
return getAdsUrl(serverUrl, thumbUrl, newLink, newHeaders) return getAdsUrl(serverUrl, thumbUrl, newLink, newHeaders)
@ -47,30 +82,31 @@ class HinataSoulExtractor(
Log.d(tag, "Final URL: $referer") Log.d(tag, "Final URL: $referer")
Log.d(tag, "Fetching ADS URL") Log.d(tag, "Fetching ADS URL")
val newHeaders = linkHeaders.newBuilder().set("Referer", referer).build() val newHeaders = linkHeaders.newBuilder().set("Referer", "https://${referer.toHttpUrl().host}/").build()
try { try {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
val adsUrl = val body = client.newCall(
client.newCall( GET(
GET( "$SITE_URL?name=apphd/$videoName&img=$thumbUrl&pais=pais=BR&time=$now&url=$serverUrl",
"$SITE_URL/playerricas.php?name=apphd/$videoName&img=$thumbUrl&pais=pais=BR&time=$now&url=$serverUrl", headers = newHeaders,
headers = newHeaders, ),
), )
) .execute()
.execute() .body.string()
.body.string()
.let { val adsUrl = body.let {
Regex("""ADS_URL\s*=\s*['"]([^'"]+)['"]""") Regex("""ADS_URL\s*=\s*['"]([^'"]+)['"]""")
.find(it)?.groups?.get(1)?.value .find(it)?.groups?.get(1)?.value
?: "" ?: ""
} }
if (adsUrl.startsWith("http")) { if (adsUrl.startsWith("http")) {
Log.d(tag, "ADS URL: $adsUrl") Log.d(tag, "ADS URL: $adsUrl")
return adsUrl return adsUrl
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(tag, e.toString())
} }
// Try default url // Try default url
@ -84,15 +120,9 @@ class HinataSoulExtractor(
if (authCode.isNotBlank()) { if (authCode.isNotBlank()) {
Log.d(tag, "AuthCode found in preferences") Log.d(tag, "AuthCode found in preferences")
val request = Request.Builder() val response = checkVideoExists("${serverUrl}$authCode")
.head()
.url("${serverUrl}$authCode")
.headers(headers)
.build()
val response = client.newCall(request).execute() if (response.exists || response.code == 500) {
if (response.isSuccessful || response.code == 500) {
Log.d(tag, "AuthCode is OK") Log.d(tag, "AuthCode is OK")
return authCode return authCode
} }
@ -112,7 +142,7 @@ class HinataSoulExtractor(
.build() .build()
val newHeaders = headers.newBuilder() val newHeaders = headers.newBuilder()
.set("Referer", SITE_URL) .set("Referer", "https://${SITE_URL.toHttpUrl().host}/")
.add("Accept", "*/*") .add("Accept", "*/*")
.add("Cache-Control", "no-cache") .add("Cache-Control", "no-cache")
.add("Pragma", "no-cache") .add("Pragma", "no-cache")
@ -165,8 +195,7 @@ class HinataSoulExtractor(
return authCode return authCode
} }
fun getVideoList(response: Response): List<Video> { fun getVideoList(doc: Document): List<Video> {
val doc = response.asJsoup()
val hasFHD = doc.selectFirst("div.abaItem:contains(FULLHD)") != null val hasFHD = doc.selectFirst("div.abaItem:contains(FULLHD)") != null
val serverUrl = doc.selectFirst("meta[itemprop=contentURL]")!! val serverUrl = doc.selectFirst("meta[itemprop=contentURL]")!!
.attr("content") .attr("content")
@ -188,16 +217,27 @@ class HinataSoulExtractor(
val authCode = getAuthCode(serverUrl, thumbUrl, firstLink) val authCode = getAuthCode(serverUrl, thumbUrl, firstLink)
return qualities.mapIndexed { index, quality -> return qualities
val path = paths[index] .mapIndexed { index, quality ->
val url = serverUrl.replace(type, path) + authCode object {
Video(url, quality, url, headers = headers) var path = paths[index]
}.reversed() 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 { companion object {
private const val PREF_AUTHCODE_KEY = "authcode" private const val PREF_AUTHCODE_KEY = "authcode"
private const val ADS_URL = "https://ads.anitube.vip" private const val ADS_URL = "https://ads.anitube.vip"
private const val SITE_URL = "https://www.anitube.vip" private const val SITE_URL = "https://www.hinatasoul.com/luffy.php"
} }
} }

View file

@ -1,7 +0,0 @@
ext {
extName = 'Lista de Animes'
extClass = '.ListaDeAnimes'
extVersionCode = 2
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View file

@ -1,112 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.listadeanimes
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
class ListaDeAnimes : ParsedAnimeHttpSource() {
override val name = "Lista de Animes"
override val baseUrl = "https://www.listadeanimes.com"
override val lang = "pt-BR"
override val supportsLatest = false
override fun headersBuilder() = super.headersBuilder()
.add("Referer", baseUrl)
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/page/$page")
override fun popularAnimeSelector() = "article.post.excerpt > div.capa:not(:has(a[href=$baseUrl/anime-lista-online]))"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
val img = element.selectFirst("img")!!
title = titleCase(img.attr("title").substringBefore(" todos os episódios"))
thumbnail_url = img.attr("data-src")
}
override fun popularAnimeNextPageSelector() = "a.next.page-numbers"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
override fun latestUpdatesSelector(): String = throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element): SAnime = throw UnsupportedOperationException()
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) = GET("$baseUrl/page/$page?s=$query")
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
override fun searchAnimeSelector() = popularAnimeSelector()
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector() = popularAnimeNextPageSelector()
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
setUrlWithoutDomain(document.location())
val titleText = document.selectFirst("h1.title.single-title")!!.text()
title = titleCase(titleText.substringBefore(" todos os episódios"))
thumbnail_url = document.selectFirst("img.aligncenter.size-full")?.attr("src")
val infos = document.selectFirst("div#content.post-single-content > center")
val infosText = infos?.run {
html()
.replace("<br>", "\n")
.replace("<b>", "")
.replace("</b>", "")
}?.let { "\n\n$it" }.orEmpty()
val sinopse = document.selectFirst("div#content > *:contains(Sinopse)")?.nextElementSibling()
description = (sinopse?.text() ?: "Sem sinopse.") + infosText
genre = document.select("a[rel=tag]").joinToString { it.text() }
}
// ============================== Episodes ==============================
override fun episodeListSelector() = "div.videos > ul"
override fun episodeListParse(response: Response): List<SEpisode> {
val doc = response.asJsoup()
return doc.select("div.videos > ul > li:gt(0)")
.map(::episodeFromElement)
.reversed()
}
override fun episodeFromElement(element: Element): SEpisode {
return SEpisode.create().apply {
episode_number = runCatching {
element.selectFirst("string")!!
.text()
.substringAfter(" ")
.toFloat()
}.getOrDefault(0F)
name = element.text().substringAfter("")
url = element.attr("id")
}
}
// ============================ Video Links =============================
override suspend fun getVideoList(episode: SEpisode): List<Video> {
return listOf(Video(episode.url, episode.name, episode.url))
}
override fun videoListSelector() = throw UnsupportedOperationException()
override fun videoListRequest(episode: SEpisode) = throw UnsupportedOperationException()
override fun videoListParse(response: Response) = throw UnsupportedOperationException()
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
// ============================= Utilities ==============================
private fun titleCase(str: String): String {
return str.split(' ')
.map { it.replaceFirstChar(Char::uppercase) }
.joinToString(" ")
}
}

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'HentaiZM' extName = 'HentaiZM'
extClass = '.HentaiZM' extClass = '.HentaiZM'
extVersionCode = 4 extVersionCode = 5
isNsfw = true isNsfw = true
} }

View file

@ -30,7 +30,7 @@ class HentaiZM : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
override val name = "HentaiZM" override val name = "HentaiZM"
override val baseUrl = "https://www.hentaizm.pro" override val baseUrl = "https://www.hentaizm.cam"
override val lang = "tr" override val lang = "tr"

View file

@ -8,7 +8,7 @@ import android.util.Log
import kotlin.system.exitProcess import kotlin.system.exitProcess
/** /**
* Springboard that accepts https://www.hentaizm.life/hentai-detay/<item> intents * Springboard that accepts https://www.hentaizm.cam/hentai-detay/<item> intents
* and redirects them to the main Aniyomi process. * and redirects them to the main Aniyomi process.
*/ */
class HentaiZMUrlActivity : Activity() { class HentaiZMUrlActivity : Activity() {