chore: Remove some extensions

This commit is contained in:
jmir1 2024-06-14 00:18:42 +02:00
parent ad842e1645
commit cc6cffd700
No known key found for this signature in database
GPG Key ID: 7B3B624787A072BD
2090 changed files with 1 additions and 97819 deletions

View File

@ -7,7 +7,7 @@ shopt -s globstar nullglob extglob
APKS=( **/*".apk" )
# Fail if too little extensions seem to have been built
if [ "${#APKS[@]}" -le "50" ]; then
if [ "${#APKS[@]}" -le "1" ]; then
echo "Insufficient amount of APKs found. Please check the project configuration."
exit 1;
fi;

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

View File

@ -1,56 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.animeonsen
import eu.kanade.tachiyomi.network.POST
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
class AOAPIInterceptor(client: OkHttpClient) : Interceptor {
private val token: String
init {
token = try {
val body = """
{
"client_id": "f296be26-28b5-4358-b5a1-6259575e23b7",
"client_secret": "349038c4157d0480784753841217270c3c5b35f4281eaee029de21cb04084235",
"grant_type": "client_credentials"
}
""".trimIndent().toRequestBody("application/json".toMediaType())
val headers = Headers.headersOf("user-agent", AO_USER_AGENT)
val tokenResponse = client.newCall(
POST(
"https://auth.animeonsen.xyz/oauth/token",
headers,
body,
),
).execute().body.string()
val tokenObject = Json.decodeFromString<JsonObject>(tokenResponse)
tokenObject["access_token"]!!.jsonPrimitive.content
} catch (_: Throwable) {
""
}
}
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val newRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer $token")
.build()
return chain.proceed(newRequest)
}
}

View File

@ -1,192 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.animeonsen
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.all.animeonsen.dto.AnimeDetails
import eu.kanade.tachiyomi.animeextension.all.animeonsen.dto.AnimeListItem
import eu.kanade.tachiyomi.animeextension.all.animeonsen.dto.AnimeListResponse
import eu.kanade.tachiyomi.animeextension.all.animeonsen.dto.EpisodeDto
import eu.kanade.tachiyomi.animeextension.all.animeonsen.dto.SearchResponse
import eu.kanade.tachiyomi.animeextension.all.animeonsen.dto.VideoData
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.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
class AnimeOnsen : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "AnimeOnsen"
override val baseUrl = "https://animeonsen.xyz"
private val apiUrl = "https://api.animeonsen.xyz/v4"
override val lang = "all"
override val supportsLatest = false
override val client by lazy {
network.client.newBuilder()
.addInterceptor(AOAPIInterceptor(network.client))
.build()
}
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val json: Json by injectLazy()
override fun headersBuilder() = Headers.Builder().add("user-agent", AO_USER_AGENT)
// ============================== Popular ===============================
// The site doesn't have a popular anime tab, so we use the home page instead (latest anime).
override fun popularAnimeRequest(page: Int) =
GET("$apiUrl/content/index?start=${(page - 1) * 20}&limit=20")
override fun popularAnimeParse(response: Response): AnimesPage {
val responseJson = response.parseAs<AnimeListResponse>()
val animes = responseJson.content.map { it.toSAnime() }
// we can't (easily) serialize this thing because it returns a array with
// two types: a boolean and a integer.
val hasNextPage = responseJson.cursor.next.firstOrNull()?.jsonPrimitive?.boolean == true
return AnimesPage(animes, hasNextPage)
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException()
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) =
GET("$apiUrl/search/$query")
override fun searchAnimeParse(response: Response): AnimesPage {
val searchResult = response.parseAs<SearchResponse>().result
val results = searchResult.map { it.toSAnime() }
return AnimesPage(results, false)
}
// =========================== Anime Details ============================
override fun animeDetailsRequest(anime: SAnime) = GET("$apiUrl/content/${anime.url}/extensive")
override fun getAnimeUrl(anime: SAnime) = "$baseUrl/details/${anime.url}"
override fun animeDetailsParse(response: Response) = SAnime.create().apply {
val details = response.parseAs<AnimeDetails>()
url = details.content_id
title = details.content_title ?: details.content_title_en!!
status = parseStatus(details.mal_data?.status)
author = details.mal_data?.studios?.joinToString { it.name }
genre = details.mal_data?.genres?.joinToString { it.name }
description = details.mal_data?.synopsis
thumbnail_url = "$apiUrl/image/210x300/${details.content_id}"
}
// ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime) = GET("$apiUrl/content/${anime.url}/episodes")
override fun episodeListParse(response: Response): List<SEpisode> {
val contentId = response.request.url.toString().substringBeforeLast("/episodes")
.substringAfterLast("/")
val responseJson = response.parseAs<Map<String, EpisodeDto>>()
return responseJson.map { (epNum, item) ->
SEpisode.create().apply {
url = "$contentId/video/$epNum"
episode_number = epNum.toFloat()
name = "Episode $epNum: ${item.name}"
}
}.sortedByDescending { it.episode_number }
}
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val videoData = response.parseAs<VideoData>()
val videoUrl = videoData.uri.stream
val subtitleLangs = videoData.metadata.subtitles
val headers = headersBuilder().add("referer", baseUrl).build()
val subs = videoData.uri.subtitles.sortSubs().map { (langPrefix, subUrl) ->
val language = subtitleLangs[langPrefix]!!
Track(subUrl, language)
}
val video = Video(videoUrl, "Default (720p)", videoUrl, headers, subtitleTracks = subs)
return listOf(video)
}
override fun videoListRequest(episode: SEpisode) = GET("$apiUrl/content/${episode.url}")
override fun videoUrlParse(response: Response) = throw UnsupportedOperationException()
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_SUB_KEY
title = PREF_SUB_TITLE
entries = PREF_SUB_ENTRIES
entryValues = PREF_SUB_VALUES
setDefaultValue(PREF_SUB_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
}
// ============================= Utilities ==============================
private fun parseStatus(statusString: String?): Int {
return when (statusString?.trim()) {
"finished_airing" -> SAnime.COMPLETED
else -> SAnime.ONGOING
}
}
private fun AnimeListItem.toSAnime() = SAnime.create().apply {
url = content_id
title = content_title ?: content_title_en!!
thumbnail_url = "$apiUrl/image/210x300/$content_id"
}
private fun Map<String, String>.sortSubs(): List<Map.Entry<String, String>> {
val sub = preferences.getString(PREF_SUB_KEY, PREF_SUB_DEFAULT)!!
return entries.sortedWith(
compareBy { it.key.contains(sub) },
).reversed()
}
}
const val AO_USER_AGENT = "Aniyomi/app (mobile)"
private const val PREF_SUB_KEY = "preferred_subLang"
private const val PREF_SUB_TITLE = "Preferred sub language"
const val PREF_SUB_DEFAULT = "en-US"
private val PREF_SUB_ENTRIES = arrayOf(
"العربية", "Deutsch", "English", "Español (Spain)",
"Español (Latin)", "Français", "Italiano",
"Português (Brasil)", "Русский",
)
private val PREF_SUB_VALUES = arrayOf(
"ar-ME", "de-DE", "en-US", "es-ES",
"es-LA", "fr-FR", "it-IT",
"pt-BR", "ru-RU",
)

View File

@ -1,83 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.animeonsen.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.JsonTransformingSerializer
@Serializable
data class AnimeListResponse(
val content: List<AnimeListItem>,
val cursor: AnimeListCursor,
)
@Serializable
data class AnimeListItem(
val content_id: String,
val content_title: String? = null,
val content_title_en: String? = null,
)
@Serializable
data class AnimeListCursor(val next: JsonArray)
@Serializable
data class AnimeDetails(
val content_id: String,
val content_title: String?,
val content_title_en: String?,
@Serializable(with = MalSerializer::class)
val mal_data: MalData?,
)
@Serializable
data class EpisodeDto(
@SerialName("contentTitle_episode_en")
val name: String,
)
@Serializable
data class MalData(
val genres: List<Genre>?,
val status: String?,
val studios: List<Studio>?,
val synopsis: String?,
)
@Serializable
data class Genre(val name: String)
@Serializable
data class Studio(val name: String)
@Serializable
data class VideoData(
val metadata: MetaData,
val uri: StreamData,
)
@Serializable
data class MetaData(val subtitles: Map<String, String>)
@Serializable
data class StreamData(
val stream: String,
val subtitles: Map<String, String>,
)
@Serializable
data class SearchResponse(
val status: Int,
val result: List<AnimeListItem>,
)
object MalSerializer : JsonTransformingSerializer<MalData>(MalData.serializer()) {
override fun transformDeserialize(element: JsonElement): JsonElement =
when (element) {
is JsonPrimitive -> JsonObject(emptyMap())
else -> element
}
}

View File

@ -1,11 +0,0 @@
ext {
extName = 'AnimeWorld India'
extClass = '.AnimeWorldIndiaFactory'
extVersionCode = 12
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:playlist-utils"))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1,246 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.animeworldindia
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
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 kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
class AnimeWorldIndia(
final override val lang: String,
private val language: String,
) : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "AnimeWorld India"
override val baseUrl = "https://anime-world.in"
override val supportsLatest = true
private val json: Json by injectLazy()
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/advanced-search/page/$page/?s_lang=$lang&s_orderby=viewed")
override fun popularAnimeSelector() = searchAnimeSelector()
override fun popularAnimeFromElement(element: Element) = searchAnimeFromElement(element)
override fun popularAnimeNextPageSelector() = searchAnimeNextPageSelector()
// =============================== Latest ===============================
override fun latestUpdatesNextPageSelector() = searchAnimeNextPageSelector()
override fun latestUpdatesSelector() = searchAnimeSelector()
override fun latestUpdatesFromElement(element: Element) = searchAnimeFromElement(element)
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/advanced-search/page/$page/?s_lang=$lang&s_orderby=update")
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val searchParams = AnimeWorldIndiaFilters().getSearchParams(filters)
return GET("$baseUrl/advanced-search/page/$page/?s_keyword=$query&s_lang=$lang$searchParams")
}
override fun searchAnimeSelector() = "div.col-span-1"
override fun searchAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
thumbnail_url = element.selectFirst("img")!!.attr("abs:src")
title = element.selectFirst("div.font-medium.line-clamp-2.mb-3")!!.text()
}
override fun searchAnimeNextPageSelector() = "ul.page-numbers li:has(span.current) + li"
override fun getFilterList() = AnimeWorldIndiaFilters().filters
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
title = document.selectFirst("h2.text-4xl")!!.text()
genre = document.select("span.leading-6 a[class~=border-opacity-30]").joinToString { it.text() }
description = document.selectFirst("div[data-synopsis]")?.text()
author = document.selectFirst("span.leading-6 a[href*=\"producer\"]:first-child")?.text()
artist = document.selectFirst("span.leading-6 a[href*=\"studio\"]:first-child")?.text()
status = parseStatus(document)
}
private val selector = "ul li:has(div.w-1.h-1.bg-gray-500.rounded-full) + li"
private fun parseStatus(document: Document): Int {
return when (document.selectFirst("$selector a:not(:contains(Ep))")?.text()) {
null -> SAnime.UNKNOWN
"Movie" -> SAnime.COMPLETED
else -> {
val epParts = document.selectFirst("$selector a:not(:contains(TV))")
?.text()
?.drop(3)
?.split("/")
?.takeIf { it.size >= 2 }
?: return SAnime.UNKNOWN
if (epParts.first().trim().compareTo(epParts[1].trim()) == 0) {
SAnime.COMPLETED
} else {
SAnime.ONGOING
}
}
}
}
// ============================== Episodes ==============================
override fun episodeListSelector() = throw UnsupportedOperationException()
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException()
@Serializable
data class SeasonDto(val episodes: EpisodeTypeDto)
@Serializable
data class EpisodeTypeDto(val all: List<EpisodeDto>) {
@Serializable
data class EpisodeDto(val id: Int, val metadata: MetadataDto)
@Serializable
data class MetadataDto(
val number: String,
val title: String,
val released: String? = null,
)
}
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
val isMovie = document.selectFirst("nav li > a[href*=\"type/movies/\"]") != null
val seasonsJson = json.decodeFromString<List<SeasonDto>>(
document.html()
.substringAfter("var season_list = ")
.substringBefore("var season_label =")
.trim().dropLast(1),
)
var episodeNumberFallback = 1F
val isSingleSeason = seasonsJson.size == 1
return seasonsJson.flatMapIndexed { seasonNumber, season ->
val seasonName = if (isSingleSeason) "" else "Season ${seasonNumber + 1}"
season.episodes.all.reversed().map { episode ->
val episodeTitle = episode.metadata.title
val epNum = episode.metadata.number.toIntOrNull() ?: episodeNumberFallback.toInt()
val episodeName = when {
isMovie -> "Movie"
else -> buildString {
if (seasonName.isNotBlank()) append("$seasonName - ")
append("Episode $epNum")
if (episodeTitle.isNotBlank()) append(" - $episodeTitle")
}
}
SEpisode.create().apply {
name = episodeName
episode_number = when {
isSingleSeason -> epNum.toFloat()
else -> episodeNumberFallback
}
episodeNumberFallback++
setUrlWithoutDomain("$baseUrl/wp-json/kiranime/v1/episode?id=${episode.id}")
date_upload = episode.metadata.released?.toLongOrNull()?.times(1000) ?: 0L
}
}
}.sortedByDescending { it.episode_number }
}
// ============================ Video Links =============================
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
override fun videoListSelector() = throw UnsupportedOperationException()
@Serializable
private data class PlayerDto(
val type: String,
val url: String,
val language: String,
val server: String,
)
private val mystreamExtractor by lazy { MyStreamExtractor(client, headers) }
override fun videoListParse(response: Response): List<Video> {
val body = response.body.string()
val documentTrimmed = body
.substringAfterLast("\"players\":")
.substringBefore(",\"noplayer\":")
.trim()
val playersList = json.decodeFromString<List<PlayerDto>>(documentTrimmed)
.filter { it.type == "stream" && it.url.isNotBlank() }
.also { require(it.isNotEmpty()) { "No streams available!" } }
.filter { language.isEmpty() || it.language.equals(language) }
.also { require(it.isNotEmpty()) { "No videos for your language!" } }
return playersList.flatMap {
when (it.server) {
"Mystream" -> mystreamExtractor.videosFromUrl(it.url, it.language)
else -> emptyList()
}
}
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
}
// ============================ Preferences =============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_VALUES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
}
companion object {
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "1080"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p", "240p")
private val PREF_QUALITY_VALUES = arrayOf("1080", "720", "480", "360", "240")
}
}

View File

@ -1,18 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.animeworldindia
import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
class AnimeWorldIndiaFactory : AnimeSourceFactory {
override fun createSources() = listOf(
AnimeWorldIndia("all", ""),
AnimeWorldIndia("bn", "bengali"),
AnimeWorldIndia("en", "english"),
AnimeWorldIndia("hi", "hindi"),
AnimeWorldIndia("ja", "japanese"),
AnimeWorldIndia("ml", "malayalam"),
AnimeWorldIndia("mr", "marathi"),
AnimeWorldIndia("ta", "tamil"),
AnimeWorldIndia("te", "telugu"),
)
}

View File

@ -1,179 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.animeworldindia
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
class AnimeWorldIndiaFilters {
private data class StringQuery(val name: String, val query: String)
private class TypeList(types: Array<String>) : AnimeFilter.Select<String>("Type", types)
private val typesName = getTypeList().map {
it.name
}.toTypedArray()
private fun getTypeList() = listOf(
StringQuery("Any", "all"),
StringQuery("TV", "tv"),
StringQuery("Movie", "movie"),
)
private class StatusList(statuses: Array<String>) : AnimeFilter.Select<String>("Status", statuses)
private val statusesName = getStatusesList().map {
it.name
}.toTypedArray()
private fun getStatusesList() = listOf(
StringQuery("Any", "all"),
StringQuery("Currently Airing", "airing"),
StringQuery("Finished Airing", "completed"),
)
private class StyleList(styles: Array<String>) : AnimeFilter.Select<String>("Style", styles)
private val stylesName = getStyleList().map {
it.name
}.toTypedArray()
private fun getStyleList() = listOf(
StringQuery("Any", "all"),
StringQuery("Anime", "anime"),
StringQuery("Cartoon", "cartoon"),
)
private class YearList(years: Array<String>) : AnimeFilter.Select<String>("Year", years)
private val yearsName = getYearList().map {
it.name
}.toTypedArray()
private fun getYearList() = listOf(
StringQuery("Any", "all"),
StringQuery("2024", "2024"),
StringQuery("2023", "2023"),
StringQuery("2022", "2022"),
StringQuery("2021", "2021"),
StringQuery("2020", "2020"),
StringQuery("2019", "2019"),
StringQuery("2018", "2018"),
StringQuery("2017", "2017"),
StringQuery("2016", "2016"),
StringQuery("2015", "2015"),
StringQuery("2014", "2014"),
StringQuery("2013", "2013"),
StringQuery("2012", "2012"),
StringQuery("2011", "2011"),
StringQuery("2010", "2010"),
StringQuery("2009", "2009"),
StringQuery("2008", "2008"),
StringQuery("2007", "2007"),
StringQuery("2006", "2006"),
StringQuery("2005", "2005"),
StringQuery("2004", "2004"),
StringQuery("2003", "2003"),
StringQuery("2002", "2002"),
StringQuery("2001", "2001"),
StringQuery("2000", "2000"),
StringQuery("1999", "1999"),
StringQuery("1998", "1998"),
StringQuery("1997", "1997"),
StringQuery("1996", "1996"),
StringQuery("1995", "1995"),
StringQuery("1994", "1994"),
StringQuery("1993", "1993"),
StringQuery("1992", "1992"),
StringQuery("1991", "1991"),
StringQuery("1990", "1990"),
)
private class SortList(sorts: Array<String>) : AnimeFilter.Select<String>("Sort", sorts)
private val sortsName = getSortList().map {
it.name
}.toTypedArray()
private fun getSortList() = listOf(
StringQuery("Default", "default"),
StringQuery("Ascending", "title_a_z"),
StringQuery("Descending", "title_z_a"),
StringQuery("Updated", "update"),
StringQuery("Published", "date"),
StringQuery("Most Viewed", "viewed"),
StringQuery("Favourite", "favorite"),
)
internal class Genre(val id: String) : AnimeFilter.CheckBox(id)
private class GenreList(genres: List<Genre>) : AnimeFilter.Group<Genre>("Genres", genres)
private fun genresName() = listOf(
Genre("Action"),
Genre("Adult Cast"),
Genre("Adventure"),
Genre("Animation"),
Genre("Comedy"),
Genre("Detective"),
Genre("Drama"),
Genre("Ecchi"),
Genre("Family"),
Genre("Fantasy"),
Genre("Isekai"),
Genre("Kids"),
Genre("Martial Arts"),
Genre("Mecha"),
Genre("Military"),
Genre("Mystery"),
Genre("Otaku Culture"),
Genre("Reality"),
Genre("Romance"),
Genre("School"),
Genre("Sci-Fi"),
Genre("Seinen"),
Genre("Shounen"),
Genre("Slice of Life"),
Genre("Sports"),
Genre("Super Power"),
Genre("SuperHero"),
Genre("Supernatural"),
Genre("TV Movie"),
)
val filters: AnimeFilterList get() = AnimeFilterList(
TypeList(typesName),
StatusList(statusesName),
StyleList(stylesName),
YearList(yearsName),
SortList(sortsName),
GenreList(genresName()),
)
fun getSearchParams(filters: AnimeFilterList): String {
return "&" + filters.mapNotNull { filter ->
when (filter) {
is TypeList -> {
val type = getTypeList()[filter.state].query
"s_type=$type"
}
is StatusList -> {
val status = getStatusesList()[filter.state].query
"s_status=$status"
}
is StyleList -> {
val style = getStyleList()[filter.state].query
"s_sub_type=$style"
}
is YearList -> {
val year = getYearList()[filter.state].query
"s_year=$year"
}
is SortList -> {
val sort = getSortList()[filter.state].query
"s_orderby=$sort"
}
is GenreList -> {
"s_genre=" + filter.state.filter { it.state }
.joinToString("%2C") {
val genre = it.id.replace(" ", "-")
"$genre%2C"
}
}
else -> null
}
}.joinToString("&")
}
}

View File

@ -1,44 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.animeworldindia
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.OkHttpClient
class MyStreamExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
fun videosFromUrl(url: String, language: String): List<Video> {
val host = url.substringBefore("/watch")
return runCatching {
val response = client.newCall(GET(url, headers)).execute()
val body = response.body.string()
val streamCode = body
.substringAfter("sniff(") // Video function
.substringAfter(", \"") // our beloved ID
.substringBefore('"')
val streamUrl = "$host/m3u8/$streamCode/master.txt?s=1&cache=1"
val cookie = response.headers.firstOrNull {
it.first.startsWith("set-cookie", true) && it.second.startsWith("PHPSESSID", true)
}?.second?.substringBefore(";") ?: ""
val newHeaders = headers.newBuilder()
.set("cookie", cookie)
.set("accept", "*/*")
.build()
playlistUtils.extractFromHls(
streamUrl,
masterHeaders = newHeaders,
videoHeaders = newHeaders,
videoNameGen = { "[$language] MyStream: $it" },
)
}.getOrElse { emptyList<Video>() }
}
}

View File

@ -1,17 +0,0 @@
ext {
extName = 'AnimeXin'
extClass = '.AnimeXin'
themePkg = 'animestream'
baseUrl = 'https://animexin.vip'
overrideVersionCode = 8
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:dailymotion-extractor'))
implementation(project(':lib:okru-extractor'))
implementation(project(':lib:gdriveplayer-extractor'))
implementation(project(':lib:dood-extractor'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@ -1,91 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.animexin
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.VidstreamingExtractor
import eu.kanade.tachiyomi.animeextension.all.animexin.extractors.YouTubeExtractor
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.dailymotionextractor.DailymotionExtractor
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.gdriveplayerextractor.GdrivePlayerExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStream
class AnimeXin : AnimeStream(
"all",
"AnimeXin",
"https://animexin.vip",
) {
override val id = 4620219025406449669
// ============================ Video Links =============================
private val dailymotionExtractor by lazy { DailymotionExtractor(client, headers) }
private val doodExtractor by lazy { DoodExtractor(client) }
private val gdrivePlayerExtractor by lazy { GdrivePlayerExtractor(client) }
private val okruExtractor by lazy { OkruExtractor(client) }
private val vidstreamingExtractor by lazy { VidstreamingExtractor(client) }
private val youTubeExtractor by lazy { YouTubeExtractor(client) }
override fun getVideoList(url: String, name: String): List<Video> {
val prefix = "$name - "
return when {
url.contains("ok.ru") -> okruExtractor.videosFromUrl(url, prefix)
url.contains("dailymotion") -> dailymotionExtractor.videosFromUrl(url, prefix)
url.contains("https://dood") -> doodExtractor.videosFromUrl(url, name)
url.contains("gdriveplayer") -> {
val gdriveHeaders = headersBuilder()
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
.add("Referer", "$baseUrl/")
.build()
gdrivePlayerExtractor.videosFromUrl(url, name, gdriveHeaders)
}
url.contains("youtube.com") -> youTubeExtractor.videosFromUrl(url, prefix)
url.contains("vidstreaming") -> vidstreamingExtractor.videosFromUrl(url, prefix)
else -> emptyList()
}
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
super.setupPreferenceScreen(screen) // Quality preferences
ListPreference(screen.context).apply {
key = PREF_LANG_KEY
title = PREF_LANG_TITLE
entries = PREF_LANG_VALUES
entryValues = PREF_LANG_VALUES
setDefaultValue(PREF_LANG_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
}
// ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(prefQualityKey, prefQualityDefault)!!
val language = preferences.getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
return sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ it.quality.contains(language, true) },
),
).reversed()
}
companion object {
private const val PREF_LANG_KEY = "preferred_language"
private const val PREF_LANG_TITLE = "Preferred Video Language"
private const val PREF_LANG_DEFAULT = "All Sub"
private val PREF_LANG_VALUES = arrayOf(
"All Sub", "Arabic", "English", "German", "Indonesia", "Italian",
"Polish", "Portuguese", "Spanish", "Thai", "Turkish",
)
}
}

View File

@ -1,127 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.animexin.extractors
import android.util.Base64
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy
import java.lang.Exception
import java.util.Locale
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
@ExperimentalSerializationApi
class VidstreamingExtractor(private val client: OkHttpClient) {
private val json: Json by injectLazy()
fun videosFromUrl(serverUrl: String, prefix: String): List<Video> {
try {
val document = client.newCall(GET(serverUrl)).execute().asJsoup()
val iv = document.select("div.wrapper")
.attr("class").substringAfter("container-")
.filter { it.isDigit() }.toByteArray()
val secretKey = document.select("body[class]")
.attr("class").substringAfter("container-")
.filter { it.isDigit() }.toByteArray()
val decryptionKey = document.select("div.videocontent")
.attr("class").substringAfter("videocontent-")
.filter { it.isDigit() }.toByteArray()
val encryptAjaxParams = cryptoHandler(
document.select("script[data-value]")
.attr("data-value"),
iv,
secretKey,
false,
).substringAfter("&")
val httpUrl = serverUrl.toHttpUrl()
val host = "https://" + httpUrl.host + "/"
val id = httpUrl.queryParameter("id") ?: throw Exception("error getting id")
val encryptedId = cryptoHandler(id, iv, secretKey)
val token = httpUrl.queryParameter("token")
val qualitySuffix = if (token != null) " (Vid-mp4 - Gogostream)" else " (Vid-mp4 - Vidstreaming)"
val jsonResponse = client.newCall(
GET(
"${host}encrypt-ajax.php?id=$encryptedId&$encryptAjaxParams&alias=$id",
Headers.headersOf(
"X-Requested-With",
"XMLHttpRequest",
),
),
).execute().body.string()
val data = json.decodeFromString<JsonObject>(jsonResponse)["data"]!!.jsonPrimitive.content
val decryptedData = cryptoHandler(data, iv, decryptionKey, false)
val videoList = mutableListOf<Video>()
val autoList = mutableListOf<Video>()
val array = json.decodeFromString<JsonObject>(decryptedData)["source"]!!.jsonArray
if (array.size == 1 && array[0].jsonObject["type"]!!.jsonPrimitive.content == "hls") {
val fileURL = array[0].jsonObject["file"].toString().trim('"')
val masterPlaylist = client.newCall(GET(fileURL)).execute().body.string()
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:")
.split("#EXT-X-STREAM-INF:").forEach {
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",").substringBefore("\n") + "p"
var videoUrl = it.substringAfter("\n").substringBefore("\n")
if (!videoUrl.startsWith("http")) {
videoUrl = fileURL.substringBeforeLast("/") + "/$videoUrl"
}
videoList.add(Video(videoUrl, prefix + quality + qualitySuffix, videoUrl))
}
} else {
array.forEach {
val label = it.jsonObject["label"].toString().lowercase(Locale.ROOT)
.trim('"').replace(" ", "")
val fileURL = it.jsonObject["file"].toString().trim('"')
val videoHeaders = Headers.headersOf("Referer", serverUrl)
if (label == "auto") {
autoList.add(
Video(
fileURL,
label + qualitySuffix,
fileURL,
headers = videoHeaders,
),
)
} else {
videoList.add(Video(fileURL, label + qualitySuffix, fileURL, headers = videoHeaders))
}
}
}
return videoList.sortedByDescending {
it.quality.substringBefore(qualitySuffix).substringBefore("p").toIntOrNull() ?: -1
} + autoList
} catch (e: Exception) {
return emptyList()
}
}
private fun cryptoHandler(
string: String,
iv: ByteArray,
secretKeyString: ByteArray,
encrypt: Boolean = true,
): String {
val ivParameterSpec = IvParameterSpec(iv)
val secretKey = SecretKeySpec(secretKeyString, "AES")
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
return if (!encrypt) {
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec)
String(cipher.doFinal(Base64.decode(string, Base64.DEFAULT)))
} else {
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec)
Base64.encodeToString(cipher.doFinal(string.toByteArray()), Base64.NO_WRAP)
}
}
}

View File

@ -1,164 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.animexin.extractors
import android.annotation.SuppressLint
import android.util.Log
import eu.kanade.tachiyomi.animesource.model.Track
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 kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import uy.kohesive.injekt.injectLazy
import kotlin.math.abs
class YouTubeExtractor(private val client: OkHttpClient) {
private val json: Json by injectLazy()
fun videosFromUrl(url: String, prefix: String): List<Video> {
// Ported from https://github.com/dermasmid/scrapetube/blob/master/scrapetube/scrapetube.py
// TODO: Make code prettier
// GET KEY
val videoId = url.substringAfter("/embed/")
val document = client.newCall(GET(url.replace("/embed/", "/watch?v=")))
.execute()
.asJsoup()
val ytcfg = document.selectFirst("script:containsData(window.ytcfg=window.ytcfg)")
?.data() ?: run {
Log.e("YouTubeExtractor", "Failed while trying to fetch the api key >:(")
return emptyList()
}
val clientName = ytcfg.substringAfter("INNERTUBE_CONTEXT_CLIENT_NAME\":", "")
.substringBefore(",", "").ifEmpty { "5" }
val apiKey = ytcfg
.substringAfter("innertubeApiKey\":\"", "")
.substringBefore('"')
val playerUrl = "$YOUTUBE_URL/youtubei/v1/player?key=$apiKey&prettyPrint=false"
val body = """
{
"context":{
"client":{
"clientName":"IOS",
"clientVersion":"17.33.2",
"deviceModel": "iPhone14,3",
"userAgent": "com.google.ios.youtube/17.33.2 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)",
"hl": "en",
"timeZone": "UTC",
"utcOffsetMinutes": 0
}
},
"videoId":"$videoId",
"playbackContext":{
"contentPlaybackContext":{
"html5Preference":"HTML5_PREF_WANTS"
}
},
"contentCheckOk":true,
"racyCheckOk":true
}
""".trimIndent().toRequestBody("application/json".toMediaType())
val headers = Headers.Builder().apply {
add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
add("Origin", YOUTUBE_URL)
add("User-Agent", "com.google.ios.youtube/17.33.2 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)")
add("X-Youtube-Client-Name", clientName)
add("X-Youtube-Client-Version", "17.33.2")
}.build()
val ytResponse = client.newCall(POST(playerUrl, headers, body)).execute()
.let { json.decodeFromString<YoutubeResponse>(it.body.string()) }
val formats = ytResponse.streamingData.adaptiveFormats
// Get Audio
val audioTracks = formats.filter { it.mimeType.startsWith("audio/webm") }
.map { Track(it.url, it.audioQuality!! + " (${formatBits(it.averageBitrate!!)}ps)") }
// Get Subtitles
val subs = ytResponse.captions?.renderer?.captionTracks?.map {
Track(it.baseUrl, it.label)
} ?: emptyList()
// Get videos, finally
return formats.filter { it.mimeType.startsWith("video/mp4") }.map {
val codecs = it.mimeType.substringAfter("codecs=\"").substringBefore("\"")
Video(
it.url,
prefix + it.qualityLabel.orEmpty() + " ($codecs)",
it.url,
subtitleTracks = subs,
audioTracks = audioTracks,
)
}
}
@SuppressLint("DefaultLocale")
fun formatBits(size: Long): String {
var bits = abs(size)
if (bits < 1000) {
return "${bits}b"
}
val iterator = "kMGTPE".iterator()
var currentChar = iterator.next()
while (bits >= 999950 && iterator.hasNext()) {
bits /= 1000
currentChar = iterator.next()
}
return "%.0f%cb".format(bits / 1000.0, currentChar)
}
@Serializable
data class YoutubeResponse(
val streamingData: AdaptiveDto,
val captions: CaptionsDto? = null,
)
@Serializable
data class AdaptiveDto(val adaptiveFormats: List<TrackDto>)
@Serializable
data class TrackDto(
val mimeType: String,
val url: String,
val averageBitrate: Long? = null,
val qualityLabel: String? = null,
val audioQuality: String? = null,
)
@Serializable
data class CaptionsDto(
@SerialName("playerCaptionsTracklistRenderer")
val renderer: CaptionsRendererDto,
) {
@Serializable
data class CaptionsRendererDto(val captionTracks: List<CaptionItem>)
@Serializable
data class CaptionItem(val baseUrl: String, val name: NameDto) {
@Serializable
data class NameDto(val runs: List<GodDamnitYoutube>)
@Serializable
data class GodDamnitYoutube(val text: String)
val label by lazy { name.runs.first().text }
}
}
}
private const val YOUTUBE_URL = "https://www.youtube.com"

View File

@ -1,16 +0,0 @@
ext {
extName = 'ChineseAnime'
extClass = '.ChineseAnime'
themePkg = 'animestream'
baseUrl = 'https://www.chineseanime.vip'
overrideVersionCode = 8
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:dailymotion-extractor"))
implementation(project(":lib:streamwish-extractor"))
implementation(project(":lib:streamvid-extractor"))
implementation(project(":lib:playlist-utils"))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

View File

@ -1,88 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.chineseanime
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.all.chineseanime.extractors.VatchusExtractor
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.dailymotionextractor.DailymotionExtractor
import eu.kanade.tachiyomi.lib.streamvidextractor.StreamVidExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStream
class ChineseAnime : AnimeStream(
"all",
"ChineseAnime",
"https://www.chineseanime.vip",
) {
// =============================== Search ===============================
override fun searchAnimeNextPageSelector() = "div.mrgn > a.r"
// =========================== Anime Details ============================
override val animeDescriptionSelector = ".entry-content"
// ============================== Filters ===============================
override val filtersSelector = "div.filter > ul"
// ============================ Video Links =============================
private val dailymotionExtractor by lazy { DailymotionExtractor(client, headers) }
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
private val streamvidExtractor by lazy { StreamVidExtractor(client) }
private val vatchusExtractor by lazy { VatchusExtractor(client, headers) }
override fun getVideoList(url: String, name: String): List<Video> {
val prefix = "$name - "
return when {
url.contains("dailymotion") -> dailymotionExtractor.videosFromUrl(url, prefix)
url.contains("embedwish") -> streamwishExtractor.videosFromUrl(url, prefix)
url.contains("vatchus") -> vatchusExtractor.videosFromUrl(url, prefix)
url.contains("donghua.xyz/v/") -> streamvidExtractor.videosFromUrl(url, prefix, true)
else -> emptyList()
}
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
super.setupPreferenceScreen(screen) // Quality preferences
val videoLangPref = ListPreference(screen.context).apply {
key = PREF_LANG_KEY
title = PREF_LANG_TITLE
entries = PREF_LANG_VALUES
entryValues = PREF_LANG_VALUES
setDefaultValue(PREF_LANG_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoLangPref)
}
// ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(prefQualityKey, prefQualityDefault)!!
val language = preferences.getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
return sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ it.quality.contains(language, true) },
),
).reversed()
}
companion object {
private const val PREF_LANG_KEY = "preferred_language"
private const val PREF_LANG_TITLE = "Preferred Video Language"
private const val PREF_LANG_DEFAULT = "All Sub"
private val PREF_LANG_VALUES = arrayOf(
"All Sub", "Arabic", "English", "Indonesia", "Persian", "Malay",
"Polish", "Portuguese", "Spanish", "Thai", "Vietnamese",
)
}
}

View File

@ -1,56 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.chineseanime.extractors
import android.util.Base64
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.OkHttpClient
class VatchusExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
fun videosFromUrl(url: String, prefix: String): List<Video> {
val doc = client.newCall(GET(url, headers)).execute()
.asJsoup()
val script = doc.selectFirst("script:containsData(document.write)")
?.data()
?: return emptyList()
val numberList = script.substringAfter(" = [").substringBefore("];")
.replace("\"", "")
.split(",")
.map(String::trim)
.filter(String::isNotBlank)
.map { String(Base64.decode(it, Base64.DEFAULT)) }
.mapNotNull { it.filter(Char::isDigit).toIntOrNull() }
val offset = numberList.first() - 60
val decodedData = numberList.joinToString("") {
Char(it - offset).toString()
}.trim()
val playlistUrl = decodedData.substringAfter("file:'").substringBefore("'")
val subs = decodedData.substringAfter("tracks:[").substringBefore("]")
.split("{")
.drop(1)
.filter { it.contains(""""kind":"captions"""") }
.mapNotNull {
val trackUrl = it.substringAfter("file\":\"").substringBefore('"')
.takeIf { link -> link.startsWith("http") }
?: return@mapNotNull null
val language = it.substringAfter("label\":\"").substringBefore('"')
Track(trackUrl, language)
}
return playlistUtils.extractFromHls(
playlistUrl,
url,
subtitleList = subs,
videoNameGen = { prefix + it },
)
}
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

View File

@ -1,255 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.hikari
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import okhttp3.HttpUrl
import java.util.Calendar
interface UriFilter {
fun addToUri(url: HttpUrl.Builder)
}
sealed class UriPartFilter(
name: String,
private val param: String,
private val vals: Array<Pair<String, String>>,
defaultValue: String? = null,
) : AnimeFilter.Select<String>(
name,
vals.map { it.first }.toTypedArray(),
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
),
UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
builder.addQueryParameter(param, vals[state].second)
}
}
class UriMultiSelectOption(name: String, val value: String) : AnimeFilter.CheckBox(name)
sealed class UriMultiSelectFilter(
name: String,
private val param: String,
private val vals: Array<Pair<String, String>>,
) : AnimeFilter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
val checked = state.filter { it.state }
builder.addQueryParameter(param, checked.joinToString(",") { it.value })
}
}
class TypeFilter : UriPartFilter(
"Type",
"type",
arrayOf(
Pair("All", ""),
Pair("TV", "1"),
Pair("Movie", "2"),
Pair("OVA", "3"),
Pair("ONA", "4"),
Pair("Special", "5"),
),
)
class CountryFilter : UriPartFilter(
"Country",
"country",
arrayOf(
Pair("All", ""),
Pair("Japanese", "1"),
Pair("Chinese", "2"),
),
)
class StatusFilter : UriPartFilter(
"Status",
"stats",
arrayOf(
Pair("All", ""),
Pair("Currently Airing", "1"),
Pair("Finished Airing", "2"),
Pair("Not yet Aired", "3"),
),
)
class RatingFilter : UriPartFilter(
"Rating",
"rate",
arrayOf(
Pair("All", ""),
Pair("G", "1"),
Pair("PG", "2"),
Pair("PG-13", "3"),
Pair("R-17+", "4"),
Pair("R+", "5"),
Pair("Rx", "6"),
),
)
class SourceFilter : UriPartFilter(
"Source",
"source",
arrayOf(
Pair("All", ""),
Pair("LightNovel", "1"),
Pair("Manga", "2"),
Pair("Original", "3"),
),
)
class SeasonFilter : UriPartFilter(
"Season",
"season",
arrayOf(
Pair("All", ""),
Pair("Spring", "1"),
Pair("Summer", "2"),
Pair("Fall", "3"),
Pair("Winter", "4"),
),
)
class LanguageFilter : UriPartFilter(
"Language",
"language",
arrayOf(
Pair("All", ""),
Pair("Raw", "1"),
Pair("Sub", "2"),
Pair("Dub", "3"),
Pair("Turk", "4"),
),
)
class SortFilter : UriPartFilter(
"Sort",
"sort",
arrayOf(
Pair("Default", "default"),
Pair("Recently Added", "recently_added"),
Pair("Recently Updated", "recently_updated"),
Pair("Score", "score"),
Pair("Name A-Z", "name_az"),
Pair("Released Date", "released_date"),
Pair("Most Watched", "most_watched"),
),
)
class YearFilter(name: String, param: String) : UriPartFilter(
name,
param,
YEARS,
) {
companion object {
private val NEXT_YEAR by lazy {
Calendar.getInstance()[Calendar.YEAR] + 1
}
private val YEARS = Array(NEXT_YEAR - 1917) { year ->
if (year == 0) {
Pair("Any", "")
} else {
(NEXT_YEAR - year).toString().let { Pair(it, it) }
}
}
}
}
class MonthFilter(name: String, param: String) : UriPartFilter(
name,
param,
MONTHS,
) {
companion object {
private val MONTHS = Array(13) { months ->
if (months == 0) {
Pair("Any", "")
} else {
val monthStr = "%02d".format(months)
Pair(monthStr, monthStr)
}
}
}
}
class DayFilter(name: String, param: String) : UriPartFilter(
name,
param,
DAYS,
) {
companion object {
private val DAYS = Array(32) { day ->
if (day == 0) {
Pair("Any", "")
} else {
val dayStr = "%02d".format(day)
Pair(dayStr, dayStr)
}
}
}
}
class AiringDateFilter(
private val values: List<UriPartFilter> = PARTS,
) : AnimeFilter.Group<UriPartFilter>("Airing Date", values), UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
values.forEach {
it.addToUri(builder)
}
}
companion object {
private val PARTS = listOf(
YearFilter("Year", "aired_year"),
MonthFilter("Month", "aired_month"),
DayFilter("Day", "aired_day"),
)
}
}
class GenreFilter : UriMultiSelectFilter(
"Genre",
"genres",
arrayOf(
Pair("Action", "Action"),
Pair("Adventure", "Adventure"),
Pair("Cars", "Cars"),
Pair("Comedy", "Comedy"),
Pair("Dementia", "Dementia"),
Pair("Demons", "Demons"),
Pair("Drama", "Drama"),
Pair("Ecchi", "Ecchi"),
Pair("Fantasy", "Fantasy"),
Pair("Game", "Game"),
Pair("Harem", "Harem"),
Pair("Historical", "Historical"),
Pair("Horror", "Horror"),
Pair("Isekai", "Isekai"),
Pair("Josei", "Josei"),
Pair("Kids", "Kids"),
Pair("Magic", "Magic"),
Pair("Martial Arts", "Martial Arts"),
Pair("Mecha", "Mecha"),
Pair("Military", "Military"),
Pair("Music", "Music"),
Pair("Mystery", "Mystery"),
Pair("Parody", "Parody"),
Pair("Police", "Police"),
Pair("Psychological", "Psychological"),
Pair("Romance", "Romance"),
Pair("Samurai", "Samurai"),
Pair("School", "School"),
Pair("Sci-Fi", "Sci-Fi"),
Pair("Seinen", "Seinen"),
Pair("Shoujo", "Shoujo"),
Pair("Shoujo Ai", "Shoujo Ai"),
Pair("Shounen", "Shounen"),
Pair("Shounen Ai", "Shounen Ai"),
Pair("Slice of Life", "Slice of Life"),
Pair("Space", "Space"),
Pair("Sports", "Sports"),
Pair("Super Power", "Super Power"),
Pair("Supernatural", "Supernatural"),
Pair("Thriller", "Thriller"),
Pair("Vampire", "Vampire"),
),
)

View File

@ -1,332 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.hikari
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.vidhideextractor.VidHideExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.Serializable
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class Hikari : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
override val name = "Hikari"
override val baseUrl = "https://watch.hikaritv.xyz"
override val lang = "all"
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder().apply {
add("Origin", baseUrl)
add("Referer", "$baseUrl/")
}
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request {
val url = "$baseUrl/ajax/getfilter?type=&country=&stats=&rate=&source=&season=&language=&aired_year=&aired_month=&aired_day=&sort=score&genres=&page=$page"
val headers = headersBuilder().set("Referer", "$baseUrl/filter").build()
return GET(url, headers)
}
override fun popularAnimeParse(response: Response): AnimesPage {
val parsed = response.parseAs<HtmlResponseDto>()
val hasNextPage = response.request.url.queryParameter("page")!!.toInt() < parsed.page!!.totalPages
val animeList = parsed.toHtml(baseUrl).select(popularAnimeSelector())
.map(::popularAnimeFromElement)
return AnimesPage(animeList, hasNextPage)
}
override fun popularAnimeSelector(): String = ".flw-item"
override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a[data-id]")!!.attr("abs:href"))
thumbnail_url = element.selectFirst("img")!!.attr("abs:src")
title = element.selectFirst(".film-name")!!.text()
}
override fun popularAnimeNextPageSelector(): String? = null
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request {
val url = "$baseUrl/ajax/getfilter?type=&country=&stats=&rate=&source=&season=&language=&aired_year=&aired_month=&aired_day=&sort=recently_updated&genres=&page=$page"
val headers = headersBuilder().set("Referer", "$baseUrl/filter").build()
return GET(url, headers)
}
override fun latestUpdatesParse(response: Response): AnimesPage =
popularAnimeParse(response)
override fun latestUpdatesSelector(): String =
throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element): SAnime =
throw UnsupportedOperationException()
override fun latestUpdatesNextPageSelector(): String =
throw UnsupportedOperationException()
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder().apply {
if (query.isNotEmpty()) {
addPathSegment("search")
addQueryParameter("keyword", query)
addQueryParameter("page", page.toString())
} else {
addPathSegment("ajax")
addPathSegment("getfilter")
filters.filterIsInstance<UriFilter>().forEach {
it.addToUri(this)
}
addQueryParameter("page", page.toString())
}
}.build()
val headers = headersBuilder().apply {
if (query.isNotEmpty()) {
set("Referer", url.toString().substringBeforeLast("&page"))
} else {
set("Referer", "$baseUrl/filter")
}
}.build()
return GET(url, headers)
}
override fun searchAnimeParse(response: Response): AnimesPage {
return if (response.request.url.encodedPath.startsWith("/search")) {
super.searchAnimeParse(response)
} else {
popularAnimeParse(response)
}
}
override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector(): String = "ul.pagination > li.active + li"
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("Note: text search ignores filters"),
AnimeFilter.Separator(),
TypeFilter(),
CountryFilter(),
StatusFilter(),
RatingFilter(),
SourceFilter(),
SeasonFilter(),
LanguageFilter(),
SortFilter(),
AiringDateFilter(),
GenreFilter(),
)
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime = SAnime.create().apply {
with(document.selectFirst("#ani_detail")!!) {
title = selectFirst(".film-name")!!.text()
thumbnail_url = selectFirst(".film-poster img")!!.attr("abs:src")
description = selectFirst(".film-description > .text")?.text()
genre = select(".item-list:has(span:contains(Genres)) > a").joinToString { it.text() }
author = select(".item:has(span:contains(Studio)) > a").joinToString { it.text() }
status = selectFirst(".item:has(span:contains(Status)) > .name").parseStatus()
}
}
private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
"currently airing" -> SAnime.ONGOING
"finished" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
// ============================== Episodes ==============================
private val specialCharRegex = Regex("""(?![\-_])\W{1,}""")
override fun episodeListRequest(anime: SAnime): Request {
val animeId = anime.url.split("/")[2]
val sanitized = anime.title.replace(" ", "_")
val refererUrl = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("watch")
addQueryParameter("anime", specialCharRegex.replace(sanitized, ""))
addQueryParameter("uid", animeId)
addQueryParameter("eps", "1")
}.build()
val headers = headersBuilder()
.set("Referer", refererUrl.toString())
.build()
return GET("$baseUrl/ajax/episodelist/$animeId", headers)
}
override fun episodeListParse(response: Response): List<SEpisode> {
return response.parseAs<HtmlResponseDto>().toHtml(baseUrl)
.select(episodeListSelector())
.map(::episodeFromElement)
.reversed()
}
override fun episodeListSelector() = "a[class~=ep-item]"
override fun episodeFromElement(element: Element): SEpisode {
val ep = element.selectFirst(".ssli-order")!!.text()
return SEpisode.create().apply {
setUrlWithoutDomain(element.attr("abs:href"))
episode_number = ep.toFloat()
name = "Ep. $ep - ${element.selectFirst(".ep-name")?.text() ?: ""}"
}
}
// ============================ Video Links =============================
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
private val vidHideExtractor by lazy { VidHideExtractor(client, headers) }
private val embedRegex = Regex("""getEmbed\(\s*(\d+)\s*,\s*(\d+)\s*,\s*'(\d+)'""")
override fun videoListRequest(episode: SEpisode): Request {
val url = (baseUrl + episode.url).toHttpUrl()
val animeId = url.queryParameter("uid")!!
val episodeNum = url.queryParameter("eps")!!
val headers = headersBuilder()
.set("Referer", baseUrl + episode.url)
.build()
return GET("$baseUrl/ajax/embedserver/$animeId/$episodeNum", headers)
}
override fun videoListParse(response: Response): List<Video> {
val html = response.parseAs<HtmlResponseDto>().toHtml(baseUrl)
val headers = headersBuilder()
.set("Referer", response.request.url.toString())
.build()
val embedUrls = html.select(videoListSelector()).flatMap {
val name = it.text()
val onClick = it.selectFirst("a")!!.attr("onclick")
val match = embedRegex.find(onClick)!!.groupValues
val url = "$baseUrl/ajax/embed/${match[1]}/${match[2]}/${match[3]}"
val iframeList = client.newCall(
GET(url, headers),
).execute().parseAs<List<String>>()
iframeList.map {
Pair(Jsoup.parseBodyFragment(it).selectFirst("iframe")!!.attr("src"), name)
}
}
return embedUrls.parallelCatchingFlatMapBlocking {
getVideosFromEmbed(it.first, it.second)
}
}
private fun getVideosFromEmbed(embedUrl: String, name: String): List<Video> = when {
name.contains("vidhide", true) -> vidHideExtractor.videosFromUrl(embedUrl)
embedUrl.contains("filemoon", true) -> {
filemoonExtractor.videosFromUrl(embedUrl, prefix = "$name - ", headers = headers)
}
else -> emptyList()
}
override fun videoListSelector() = ".server-item:has(a[onclick~=getEmbed])"
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ QUALITY_REGEX.find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
),
).reversed()
}
override fun videoFromElement(element: Element): Video =
throw UnsupportedOperationException()
override fun videoUrlParse(document: Document): String =
throw UnsupportedOperationException()
// ============================= Utilities ==============================
@Serializable
class HtmlResponseDto(
val html: String,
val page: PageDto? = null,
) {
fun toHtml(baseUrl: String): Document = Jsoup.parseBodyFragment(html, baseUrl)
@Serializable
class PageDto(
val totalPages: Int,
)
}
companion object {
private val QUALITY_REGEX = Regex("""(\d+)p""")
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_DEFAULT = "1080"
private val PREF_QUALITY_VALUES = arrayOf("1080", "720", "480", "360")
private val PREF_QUALITY_ENTRIES = PREF_QUALITY_VALUES.map {
"${it}p"
}.toTypedArray()
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = "Preferred quality"
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_VALUES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
}
}

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".all.javguru.JavGuruUrlActivity"
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="jav.guru"
android:pathPattern="/.*/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,18 +0,0 @@
ext {
extName = 'Jav Guru'
extClass = '.JavGuru'
extVersionCode = 15
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:streamwish-extractor'))
implementation(project(':lib:streamtape-extractor'))
implementation(project(':lib:dood-extractor'))
implementation(project(':lib:mixdrop-extractor'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
implementation(project(':lib:playlist-utils'))
implementation(project(':lib:javcoverfetcher'))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

View File

@ -1,381 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.javguru
import android.app.Application
import android.util.Base64
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.all.javguru.extractors.EmTurboExtractor
import eu.kanade.tachiyomi.animeextension.all.javguru.extractors.MaxStreamExtractor
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.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.javcoverfetcher.JavCoverFetcher
import eu.kanade.tachiyomi.lib.javcoverfetcher.JavCoverFetcher.fetchHDCovers
import eu.kanade.tachiyomi.lib.mixdropextractor.MixDropExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import okhttp3.Call
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Request
import okhttp3.Response
import org.jsoup.select.Elements
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.math.min
class JavGuru : AnimeHttpSource(), ConfigurableAnimeSource {
override val name = "Jav Guru"
override val baseUrl = "https://jav.guru"
override val lang = "all"
override val supportsLatest = true
private val noRedirectClient = client.newBuilder()
.followRedirects(false)
.build()
private val preference by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private lateinit var popularElements: Elements
override suspend fun getPopularAnime(page: Int): AnimesPage {
return if (page == 1) {
client.newCall(popularAnimeRequest(page))
.awaitSuccess()
.use(::popularAnimeParse)
} else {
cachedPopularAnimeParse(page)
}
}
override fun popularAnimeRequest(page: Int) =
GET("$baseUrl/most-watched-rank/", headers)
override fun popularAnimeParse(response: Response): AnimesPage {
popularElements = response.asJsoup().select(".tabcontent li")
return cachedPopularAnimeParse(1)
}
private fun cachedPopularAnimeParse(page: Int): AnimesPage {
val end = min(page * 20, popularElements.size)
val entries = popularElements.subList((page - 1) * 20, end).map { element ->
SAnime.create().apply {
element.select("a").let { a ->
getIDFromUrl(a)?.let { url = it }
?: setUrlWithoutDomain(a.attr("href"))
title = a.text()
thumbnail_url = a.select("img").attr("abs:src")
}
}
}
return AnimesPage(entries, end < popularElements.size)
}
override fun latestUpdatesRequest(page: Int): Request {
val url = baseUrl + if (page > 1) "/page/$page/" else ""
return GET(url, headers)
}
override fun latestUpdatesParse(response: Response): AnimesPage {
val document = response.asJsoup()
val entries = document.select("div.site-content div.inside-article:not(:contains(nothing))").map { element ->
SAnime.create().apply {
element.select("a").let { a ->
getIDFromUrl(a)?.let { url = it }
?: setUrlWithoutDomain(a.attr("href"))
}
thumbnail_url = element.select("img").attr("abs:src")
title = element.select("h2 > a").text()
}
}
val page = document.location()
.pageNumberFromUrlOrNull() ?: 1
val lastPage = document.select("div.wp-pagenavi a")
.last()
?.attr("href")
.pageNumberFromUrlOrNull() ?: 1
return AnimesPage(entries, page < lastPage)
}
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
if (query.startsWith(PREFIX_ID)) {
val id = query.substringAfter(PREFIX_ID)
if (id.toIntOrNull() == null) {
return AnimesPage(emptyList(), false)
}
val url = "/$id/"
val tempAnime = SAnime.create().apply { this.url = url }
return getAnimeDetails(tempAnime).let {
val anime = it.apply { this.url = url }
AnimesPage(listOf(anime), false)
}
} else if (query.isNotEmpty()) {
return client.newCall(searchAnimeRequest(page, query, filters))
.awaitSuccess()
.use(::searchAnimeParse)
} else {
filters.forEach { filter ->
when (filter) {
is TagFilter,
is CategoryFilter,
-> {
if (filter.state != 0) {
val url = "$baseUrl${filter.toUrlPart()}" + if (page > 1) "page/$page/" else ""
val request = GET(url, headers)
return client.newCall(request)
.awaitSuccess()
.use(::searchAnimeParse)
}
}
is ActressFilter,
is ActorFilter,
is StudioFilter,
is MakerFilter,
-> {
if ((filter.state as String).isNotEmpty()) {
val url = "$baseUrl${filter.toUrlPart()}" + if (page > 1) "page/$page/" else ""
val request = GET(url, headers)
return client.newCall(request)
.awaitIgnoreCode(404)
.use(::searchAnimeParse)
}
}
else -> { }
}
}
}
throw Exception("Select at least one Filter")
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder().apply {
if (page > 1) addPathSegments("page/$page/")
addQueryParameter("s", query)
}.build().toString()
return GET(url, headers)
}
override fun getFilterList() = getFilters()
override fun searchAnimeParse(response: Response) = latestUpdatesParse(response)
override fun animeDetailsParse(response: Response): SAnime {
val document = response.asJsoup()
val javId = document.selectFirst(".infoleft li:contains(code)")?.ownText()
val siteCover = document.select(".large-screenshot img").attr("abs:src")
return SAnime.create().apply {
title = document.select(".titl").text()
genre = document.select(".infoleft a[rel*=tag]").joinToString { it.text() }
author = document.selectFirst(".infoleft li:contains(studio) a")?.text()
artist = document.selectFirst(".infoleft li:contains(label) a")?.text()
status = SAnime.COMPLETED
description = buildString {
document.selectFirst(".infoleft li:contains(code)")?.text()?.let { append("$it\n") }
document.selectFirst(".infoleft li:contains(director)")?.text()?.let { append("$it\n") }
document.selectFirst(".infoleft li:contains(studio)")?.text()?.let { append("$it\n") }
document.selectFirst(".infoleft li:contains(label)")?.text()?.let { append("$it\n") }
document.selectFirst(".infoleft li:contains(actor)")?.text()?.let { append("$it\n") }
document.selectFirst(".infoleft li:contains(actress)")?.text()?.let { append("$it\n") }
}
thumbnail_url = if (preference.fetchHDCovers) {
javId?.let { JavCoverFetcher.getCoverById(it) } ?: siteCover
} else {
siteCover
}
}
}
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
return listOf(
SEpisode.create().apply {
url = anime.url
name = "Episode"
},
)
}
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val iframeData = document.selectFirst("script:containsData(iframe_url)")?.html()
?: return emptyList()
val iframeUrls = IFRAME_B64_REGEX.findAll(iframeData)
.map { it.groupValues[1] }
.map { Base64.decode(it, Base64.DEFAULT).let(::String) }
.toList()
return iframeUrls
.mapNotNull(::resolveHosterUrl)
.parallelCatchingFlatMapBlocking(::getVideos)
}
private fun resolveHosterUrl(iframeUrl: String): String? {
val iframeResponse = client.newCall(GET(iframeUrl, headers)).execute()
if (iframeResponse.isSuccessful.not()) {
iframeResponse.close()
return null
}
val iframeDocument = iframeResponse.asJsoup()
val script = iframeDocument.selectFirst("script:containsData(start_player)")
?.html() ?: return null
val olid = IFRAME_OLID_REGEX.find(script)?.groupValues?.get(1)?.reversed()
?: return null
val olidUrl = IFRAME_OLID_URL.find(script)?.groupValues?.get(1)
?.substringBeforeLast("=")?.let { "$it=$olid" }
?: return null
val newHeaders = headersBuilder()
.set("Referer", iframeUrl)
.build()
val redirectUrl = noRedirectClient.newCall(GET(olidUrl, newHeaders))
.execute().use { it.header("location") }
?: return null
if (redirectUrl.toHttpUrlOrNull() == null) {
return null
}
return redirectUrl
}
private val streamWishExtractor by lazy {
val swHeaders = headersBuilder()
.set("Referer", "$baseUrl/")
.build()
StreamWishExtractor(client, swHeaders)
}
private val streamTapeExtractor by lazy { StreamTapeExtractor(client) }
private val doodExtractor by lazy { DoodExtractor(client) }
private val mixDropExtractor by lazy { MixDropExtractor(client) }
private val maxStreamExtractor by lazy { MaxStreamExtractor(client, headers) }
private val emTurboExtractor by lazy { EmTurboExtractor(client, headers) }
private fun getVideos(hosterUrl: String): List<Video> {
return when {
listOf("javplaya", "javclan").any { it in hosterUrl } -> {
streamWishExtractor.videosFromUrl(hosterUrl)
}
hosterUrl.contains("streamtape") -> {
streamTapeExtractor.videoFromUrl(hosterUrl).let(::listOfNotNull)
}
listOf("dood", "ds2play").any { it in hosterUrl } -> {
doodExtractor.videosFromUrl(hosterUrl)
}
listOf("mixdrop", "mixdroop").any { it in hosterUrl } -> {
mixDropExtractor.videoFromUrl(hosterUrl)
}
hosterUrl.contains("maxstream") -> {
maxStreamExtractor.videoFromUrl(hosterUrl)
}
hosterUrl.contains("emturbovid") -> {
emTurboExtractor.getVideos(hosterUrl)
}
else -> emptyList()
}
}
override fun List<Video>.sort(): List<Video> {
val quality = preference.getString(PREF_QUALITY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
}
private fun getIDFromUrl(element: Elements): String? {
return element.attr("abs:href")
.toHttpUrlOrNull()
?.pathSegments
?.firstOrNull()
?.toIntOrNull()
?.toString()
?.let { "/$it/" }
}
private fun String?.pageNumberFromUrlOrNull() =
this
?.substringBeforeLast("/")
?.toHttpUrlOrNull()
?.pathSegments
?.last()
?.toIntOrNull()
private suspend fun Call.awaitIgnoreCode(code: Int): Response {
return await().also { response ->
if (!response.isSuccessful && response.code != code) {
response.close()
throw Exception("HTTP error ${response.code}")
}
}
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY
title = PREF_QUALITY_TITLE
entries = arrayOf("1080p", "720p", "480p", "360p")
entryValues = arrayOf("1080", "720", "480", "360")
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
}.also(screen::addPreference)
JavCoverFetcher.addPreferenceToScreen(screen)
}
companion object {
const val PREFIX_ID = "id:"
private val IFRAME_B64_REGEX = Regex(""""iframe_url":"([^"]+)"""")
private val IFRAME_OLID_REGEX = Regex("""var OLID = '([^']+)'""")
private val IFRAME_OLID_URL = Regex("""src="([^"]+)"""")
private const val PREF_QUALITY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "720"
}
override fun episodeListParse(response: Response): List<SEpisode> {
throw UnsupportedOperationException()
}
}

View File

@ -1,335 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.javguru
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
fun getFilters() = AnimeFilterList(
AnimeFilter.Header("Only One Filter Works at a time!!"),
AnimeFilter.Header("Ignored With Text Search!!"),
TagFilter(),
CategoryFilter(),
AnimeFilter.Separator(),
ActressFilter(),
ActorFilter(),
StudioFilter(),
MakerFilter(),
)
class UriPartFilter(val name: String, val urlPart: String)
abstract class UriPartFilters(name: String, private val tags: List<UriPartFilter>) :
AnimeFilter.Select<String>(name, tags.map { it.name }.toTypedArray()) {
fun toUrlPart() = tags[state].urlPart
}
class TagFilter : UriPartFilters("Tags", TAGS)
class CategoryFilter : UriPartFilters("Categories", CATEGORIES)
abstract class TextFilter(name: String, private val urlSubDirectory: String) : AnimeFilter.Text(name) {
fun toUrlPart() = state.trim()
.lowercase()
.replace(SPECIAL_CHAR_REGEX, "-")
.replace(TRAILING_HIPHEN_REGEX, "")
.let { "/$urlSubDirectory/$it/" }
companion object {
private val SPECIAL_CHAR_REGEX = "[^a-z0-9]+".toRegex()
private val TRAILING_HIPHEN_REGEX = "-+$".toRegex()
}
}
class ActressFilter : TextFilter("Actress", "actress")
class ActorFilter : TextFilter("Actor", "actor")
class StudioFilter : TextFilter("Studio", "studio")
class MakerFilter : TextFilter("Maker", "maker")
fun <T> AnimeFilter<T>.toUrlPart(): String? {
return when (this) {
is TagFilter -> this.toUrlPart()
is CategoryFilter -> this.toUrlPart()
is ActressFilter -> this.toUrlPart()
is ActorFilter -> this.toUrlPart()
is StudioFilter -> this.toUrlPart()
is MakerFilter -> this.toUrlPart()
else -> null
}
}
val TAGS = listOf(
UriPartFilter("", "/"),
UriPartFilter("Solowork", "/tag/solowork/"),
UriPartFilter("Creampie", "/tag/creampie/"),
UriPartFilter("Big tits", "/tag/big-tits/"),
UriPartFilter("Beautiful Girl", "/tag/beautiful-girl/"),
UriPartFilter("Married Woman", "/tag/married-woman/"),
UriPartFilter("Amateur", "/tag/amateur/"),
UriPartFilter("Digital Mosaic", "/tag/digital-mosaic/"),
UriPartFilter("Slut", "/tag/slut/"),
UriPartFilter("Mature Woman", "/tag/mature-woman/"),
UriPartFilter("Cuckold", "/tag/cuckold/"),
UriPartFilter("3P", "/tag/3p/"),
UriPartFilter("Slender", "/tag/slender/"),
UriPartFilter("Blow", "/tag/blow/"),
UriPartFilter("Squirting", "/tag/squirting/"),
UriPartFilter("Drama", "/tag/drama/"),
UriPartFilter("Nasty", "/tag/nasty/"),
UriPartFilter("Hardcore", "/tag/hardcore/"),
UriPartFilter("School Girls", "/tag/school-girls/"),
UriPartFilter("4P", "/tag/4p/"),
UriPartFilter("Titty fuck", "/tag/titty-fuck/"),
UriPartFilter("Cowgirl", "/tag/cowgirl/"),
UriPartFilter("Incest", "/tag/incest/"),
UriPartFilter("Facials", "/tag/facials/"),
UriPartFilter("breasts", "/tag/breasts/"),
UriPartFilter("abuse", "/tag/abuse/"),
UriPartFilter("Risky Mosaic", "/tag/risky-mosaic/"),
UriPartFilter("Debut Production", "/tag/debut-production/"),
UriPartFilter("Older sister", "/tag/older-sister/"),
UriPartFilter("Huge Butt", "/tag/huge-butt/"),
UriPartFilter("4HR+", "/tag/4hr/"),
UriPartFilter("Affair", "/tag/affair/"),
UriPartFilter("Kiss", "/tag/kiss/"),
UriPartFilter("Deep Throating", "/tag/deep-throating/"),
UriPartFilter("Documentary", "/tag/documentary/"),
UriPartFilter("Mini", "/tag/mini/"),
UriPartFilter("Entertainer", "/tag/entertainer/"),
UriPartFilter("Dirty Words", "/tag/dirty-words/"),
UriPartFilter("Cosplay", "/tag/cosplay/"),
UriPartFilter("POV", "/tag/pov/"),
UriPartFilter("Shaved", "/tag/shaved/"),
UriPartFilter("butt", "/tag/butt/"),
UriPartFilter("OL", "/tag/ol/"),
UriPartFilter("Tits", "/tag/tits/"),
UriPartFilter("Promiscuity", "/tag/promiscuity/"),
UriPartFilter("Restraint", "/tag/restraint/"),
UriPartFilter("Gal", "/tag/gal/"),
UriPartFilter("planning", "/tag/planning/"),
UriPartFilter("Subjectivity", "/tag/subjectivity/"),
UriPartFilter("Handjob", "/tag/handjob/"),
UriPartFilter("Uniform", "/tag/uniform/"),
UriPartFilter("Sister", "/tag/sister/"),
UriPartFilter("Humiliation", "/tag/humiliation/"),
UriPartFilter("Prostitutes", "/tag/prostitutes/"),
UriPartFilter("School Uniform", "/tag/school-uniform/"),
UriPartFilter("Rape", "/tag/rape/"),
UriPartFilter("Lesbian", "/tag/lesbian/"),
UriPartFilter("Anal", "/tag/anal/"),
UriPartFilter("Image video", "/tag/image-video/"),
UriPartFilter("Pantyhose", "/tag/pantyhose/"),
UriPartFilter("Other fetish", "/tag/other-fetish/"),
UriPartFilter("Female College Student", "/tag/female-college-student/"),
UriPartFilter("Female teacher", "/tag/female-teacher/"),
UriPartFilter("Bukkake", "/tag/bukkake/"),
UriPartFilter("Training", "/tag/training/"),
UriPartFilter("Cum", "/tag/cum/"),
UriPartFilter("Masturbation", "/tag/masturbation/"),
UriPartFilter("Sweat", "/tag/sweat/"),
UriPartFilter("Omnibus", "/tag/omnibus/"),
UriPartFilter("Best", "/tag/best/"),
UriPartFilter("Lotion", "/tag/lotion/"),
UriPartFilter("Girl", "/tag/girl/"),
UriPartFilter("Submissive Men", "/tag/submissive-men/"),
UriPartFilter("Outdoors", "/tag/outdoors/"),
UriPartFilter("Beauty Shop", "/tag/beauty-shop/"),
UriPartFilter("Busty fetish", "/tag/busty-fetish/"),
UriPartFilter("Toy", "/tag/toy/"),
UriPartFilter("Urination", "/tag/urination/"),
UriPartFilter("huge cock", "/tag/huge-cock/"),
UriPartFilter("Gangbang", "/tag/gangbang/"),
UriPartFilter("Massage", "/tag/massage/"),
UriPartFilter("Tall", "/tag/tall/"),
UriPartFilter("Hot Spring", "/tag/hot-spring/"),
UriPartFilter("virgin man", "/tag/virgin-man/"),
UriPartFilter("Various Professions", "/tag/various-professions/"),
UriPartFilter("Bride", "/tag/bride/"),
UriPartFilter("Leg Fetish", "/tag/leg-fetish/"),
UriPartFilter("Young wife", "/tag/young-wife/"),
UriPartFilter("Maid", "/tag/maid/"),
UriPartFilter("BBW", "/tag/bbw/"),
UriPartFilter("SM", "/tag/sm/"),
UriPartFilter("Restraints", "/tag/restraints/"),
UriPartFilter("Lesbian Kiss", "/tag/lesbian-kiss/"),
UriPartFilter("Voyeur", "/tag/voyeur/"),
UriPartFilter("Mother", "/tag/mother/"),
UriPartFilter("Evil", "/tag/evil/"),
UriPartFilter("Underwear", "/tag/underwear/"),
UriPartFilter("Nurse", "/tag/nurse/"),
UriPartFilter("Glasses", "/tag/glasses/"),
UriPartFilter("Lingerie", "/tag/lingerie/"),
UriPartFilter("Drug", "/tag/drug/"),
UriPartFilter("Nampa", "/tag/nampa/"),
UriPartFilter("School Swimsuit", "/tag/school-swimsuit/"),
UriPartFilter("Stepmother", "/tag/stepmother/"),
UriPartFilter("Sailor suit", "/tag/sailor-suit/"),
UriPartFilter("Prank", "/tag/prank/"),
UriPartFilter("Cunnilingus", "/tag/cunnilingus/"),
UriPartFilter("Electric Massager", "/tag/electric-massager/"),
UriPartFilter("Molester", "/tag/molester/"),
UriPartFilter("Black Actor", "/tag/black-actor/"),
UriPartFilter("Ultra-Huge Tits", "/tag/ultra-huge-tits/"),
UriPartFilter("Original Collaboration", "/tag/original-collaboration/"),
UriPartFilter("Confinement", "/tag/confinement/"),
UriPartFilter("Shotacon", "/tag/shotacon/"),
UriPartFilter("Footjob", "/tag/footjob/"),
UriPartFilter("Female Boss", "/tag/female-boss/"),
UriPartFilter("Female investigator", "/tag/female-investigator/"),
UriPartFilter("Swimsuit", "/tag/swimsuit/"),
UriPartFilter("Bloomers", "/tag/bloomers/"),
UriPartFilter("Facesitting", "/tag/facesitting/"),
UriPartFilter("Kimono", "/tag/kimono/"),
UriPartFilter("Mourning", "/tag/mourning/"),
UriPartFilter("White Actress", "/tag/white-actress/"),
UriPartFilter("Acme · Orgasm", "/tag/acme-%c2%b7-orgasm/"),
UriPartFilter("Sun tan", "/tag/sun-tan/"),
UriPartFilter("Finger Fuck", "/tag/finger-fuck/"),
UriPartFilter("Transsexual", "/tag/transsexual/"),
UriPartFilter("Blu-ray", "/tag/blu-ray/"),
UriPartFilter("VR", "/tag/vr/"),
UriPartFilter("Cross Dressing", "/tag/cross-dressing/"),
UriPartFilter("Soapland", "/tag/soapland/"),
UriPartFilter("Fan Appreciation", "/tag/fan-appreciation/"),
UriPartFilter("AV Actress", "/tag/av-actress/"),
UriPartFilter("School Stuff", "/tag/school-stuff/"),
UriPartFilter("Love", "/tag/love/"),
UriPartFilter("Close Up", "/tag/close-up/"),
UriPartFilter("Submissive Woman", "/tag/submissive-woman/"),
UriPartFilter("Mini Skirt", "/tag/mini-skirt/"),
UriPartFilter("Impromptu Sex", "/tag/impromptu-sex/"),
UriPartFilter("Vibe", "/tag/vibe/"),
UriPartFilter("Bitch", "/tag/bitch/"),
UriPartFilter("Enema", "/tag/enema/"),
UriPartFilter("Hypnosis", "/tag/hypnosis/"),
UriPartFilter("Childhood Friend", "/tag/childhood-friend/"),
UriPartFilter("Erotic Wear", "/tag/erotic-wear/"),
UriPartFilter("Tutor", "/tag/tutor/"),
UriPartFilter("Male Squirting", "/tag/male-squirting/"),
UriPartFilter("Bath", "/tag/bath/"),
UriPartFilter("Conceived", "/tag/conceived/"),
UriPartFilter("Stewardess", "/tag/stewardess/"),
UriPartFilter("Sport", "/tag/sport/"),
UriPartFilter("Bunny Girl", "/tag/bunny-girl/"),
UriPartFilter("Piss Drinking", "/tag/piss-drinking/"),
UriPartFilter("Shibari", "/tag/shibari/"),
UriPartFilter("Couple", "/tag/couple/"),
UriPartFilter("Anchorwoman", "/tag/anchorwoman/"),
UriPartFilter("Delusion", "/tag/delusion/"),
UriPartFilter("69", "/tag/69/"),
UriPartFilter("Secretary", "/tag/secretary/"),
UriPartFilter("Idol", "/tag/idol/"),
UriPartFilter("Elder Male", "/tag/elder-male/"),
UriPartFilter("Cervix", "/tag/cervix/"),
UriPartFilter("Leotard", "/tag/leotard/"),
UriPartFilter("Miss", "/tag/miss/"),
UriPartFilter("Back", "/tag/back/"),
UriPartFilter("blog", "/tag/blog/"),
UriPartFilter("virgin", "/tag/virgin/"),
UriPartFilter("Female Doctor", "/tag/female-doctor/"),
UriPartFilter("No Bra", "/tag/no-bra/"),
UriPartFilter("Tsundere", "/tag/tsundere/"),
UriPartFilter("Race Queen", "/tag/race-queen/"),
UriPartFilter("Multiple Story", "/tag/multiple-story/"),
UriPartFilter("Widow", "/tag/widow/"),
UriPartFilter("Actress Best", "/tag/actress-best/"),
UriPartFilter("Bondage", "/tag/bondage/"),
UriPartFilter("Muscle", "/tag/muscle/"),
UriPartFilter("User Submission", "/tag/user-submission/"),
UriPartFilter("Breast Milk", "/tag/breast-milk/"),
UriPartFilter("Sexy", "/tag/sexy/"),
UriPartFilter("Travel", "/tag/travel/"),
UriPartFilter("Knee Socks", "/tag/knee-socks/"),
UriPartFilter("Date", "/tag/date/"),
UriPartFilter("For Women", "/tag/for-women/"),
UriPartFilter("Premature Ejaculation", "/tag/premature-ejaculation/"),
UriPartFilter("Hi-Def", "/tag/hi-def/"),
UriPartFilter("Time Stop", "/tag/time-stop/"),
UriPartFilter("Subordinates / Colleagues", "/tag/subordinates-colleagues/"),
UriPartFilter("Adopted Daughter", "/tag/adopted-daughter/"),
UriPartFilter("Instructor", "/tag/instructor/"),
UriPartFilter("Catgirl", "/tag/catgirl/"),
UriPartFilter("Body Conscious", "/tag/body-conscious/"),
UriPartFilter("Fighting Action", "/tag/fighting-action/"),
UriPartFilter("Featured Actress", "/tag/featured-actress/"),
UriPartFilter("Hostess", "/tag/hostess/"),
UriPartFilter("Dead Drunk", "/tag/dead-drunk/"),
UriPartFilter("Landlady", "/tag/landlady/"),
UriPartFilter("Business Attire", "/tag/business-attire/"),
UriPartFilter("Dildo", "/tag/dildo/"),
UriPartFilter("Reversed Role", "/tag/reversed-role/"),
UriPartFilter("Foreign Objects", "/tag/foreign-objects/"),
UriPartFilter("Athlete", "/tag/athlete/"),
UriPartFilter("Aunt", "/tag/aunt/"),
UriPartFilter("Model", "/tag/model/"),
UriPartFilter("Big Breasts", "/tag/big-breasts/"),
UriPartFilter("Oversea Import", "/tag/oversea-import/"),
UriPartFilter("Drinking Party", "/tag/drinking-party/"),
UriPartFilter("Booth Girl", "/tag/booth-girl/"),
UriPartFilter("Car Sex", "/tag/car-sex/"),
UriPartFilter("Blowjob", "/tag/blowjob/"),
UriPartFilter("Other Asian", "/tag/other-asian/"),
UriPartFilter("Special Effects", "/tag/special-effects/"),
UriPartFilter("Spanking", "/tag/spanking/"),
UriPartFilter("Club Activities / Manager", "/tag/club-activities-manager/"),
UriPartFilter("Naked Apron", "/tag/naked-apron/"),
UriPartFilter("Fantasy", "/tag/fantasy/"),
UriPartFilter("Female Warrior", "/tag/female-warrior/"),
UriPartFilter("Anime Characters", "/tag/anime-characters/"),
UriPartFilter("Sex Conversion / Feminized", "/tag/sex-conversion-feminized/"),
UriPartFilter("Flexible", "/tag/flexible/"),
UriPartFilter("Schoolgirl", "/tag/schoolgirl/"),
UriPartFilter("Long Boots", "/tag/long-boots/"),
UriPartFilter("No Undies", "/tag/no-undies/"),
UriPartFilter("Immediate Oral", "/tag/immediate-oral/"),
UriPartFilter("Hospital / Clinic", "/tag/hospital-clinic/"),
UriPartFilter("Dance", "/tag/dance/"),
UriPartFilter("Breast Peeker", "/tag/breast-peeker/"),
UriPartFilter("Waitress", "/tag/waitress/"),
UriPartFilter("Futanari", "/tag/futanari/"),
UriPartFilter("Rolling Back Eyes / Fainting", "/tag/rolling-back-eyes-fainting/"),
UriPartFilter("Hotel", "/tag/hotel/"),
UriPartFilter("Exposure", "/tag/exposure/"),
UriPartFilter("Torture", "/tag/torture/"),
UriPartFilter("Office Lady", "/tag/office-lady/"),
UriPartFilter("Masturbation Support", "/tag/masturbation-support/"),
UriPartFilter("facial", "/tag/facial/"),
UriPartFilter("Egg Vibrator", "/tag/egg-vibrator/"),
UriPartFilter("Fisting", "/tag/fisting/"),
UriPartFilter("Vomit", "/tag/vomit/"),
UriPartFilter("Orgy", "/tag/orgy/"),
UriPartFilter("Cruel Expression", "/tag/cruel-expression/"),
UriPartFilter("Doll", "/tag/doll/"),
UriPartFilter("Loose Socks", "/tag/loose-socks/"),
UriPartFilter("Best of 2021", "/tag/best-of-2021/"),
UriPartFilter("Reserved Role", "/tag/reserved-role/"),
UriPartFilter("Best of 2019", "/tag/best-of-2019/"),
UriPartFilter("Mother-in-law", "/tag/mother-in-law/"),
UriPartFilter("Gay", "/tag/gay/"),
UriPartFilter("Swingers", "/tag/swingers/"),
UriPartFilter("Best of 2020", "/tag/best-of-2020/"),
UriPartFilter("Mistress", "/tag/mistress/"),
UriPartFilter("Shame", "/tag/shame/"),
UriPartFilter("Yukata", "/tag/yukata/"),
UriPartFilter("Best of 2017", "/tag/best-of-2017/"),
UriPartFilter("Best of 2018", "/tag/best-of-2018/"),
UriPartFilter("Nose Hook", "/tag/nose-hook/"),
)
val CATEGORIES = listOf(
UriPartFilter("", "/"),
UriPartFilter("1080p", "/category/1080p/"),
UriPartFilter("4K", "/category/4k/"),
UriPartFilter("Amateur", "/category/amateur/"),
UriPartFilter("Blog", "/category/blog/"),
UriPartFilter("Decensored", "/category/decensored/"),
UriPartFilter("English subbed JAV", "/category/english-subbed/"),
UriPartFilter("FC2", "/category/fc2/"),
UriPartFilter("HD", "/category/hd/"),
UriPartFilter("Idol", "/category/idol/"),
UriPartFilter("JAV", "/category/jav/"),
UriPartFilter("LEGACY", "/category/legacy/"),
UriPartFilter("UNCENSORED", "/category/jav-uncensored/"),
UriPartFilter("VR AV", "/category/vr-av/"),
)

View File

@ -1,34 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.javguru
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
class JavGuruUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val id = pathSegments[0]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${JavGuru.PREFIX_ID}$id")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("JavGuruUrlActivity", e.toString())
}
} else {
Log.e("JavGuruUrlActivity", "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View File

@ -1,36 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.javguru.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
class EmTurboExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val playlistExtractor by lazy { PlaylistUtils(client, headers) }
fun getVideos(url: String): List<Video> {
val document = client.newCall(GET(url, headers)).execute().asJsoup()
val script = document.selectFirst("script:containsData(urlplay)")
?.data()
?: return emptyList()
val urlPlay = URLPLAY.find(script)?.groupValues?.get(1)
?: return emptyList()
if (urlPlay.toHttpUrlOrNull() == null) {
return emptyList()
}
return playlistExtractor.extractFromHls(urlPlay, url, videoNameGen = { quality -> "EmTurboVid: $quality" })
.distinctBy { it.url } // they have the same stream repeated twice in the playlist file
}
companion object {
private val URLPLAY = Regex("""urlPlay\s*=\s*\'([^\']+)""")
}
}

View File

@ -1,32 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.javguru.extractors
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 eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
class MaxStreamExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val playListUtils by lazy { PlaylistUtils(client, headers) }
fun videoFromUrl(url: String): List<Video> {
val document = client.newCall(GET(url, headers)).execute().asJsoup()
val script = document.selectFirst("script:containsData(function(p,a,c,k,e,d))")
?.data()
?.let(JsUnpacker::unpackAndCombine)
?: return emptyList()
val videoUrl = script.substringAfter("file:\"").substringBefore("\"")
if (videoUrl.toHttpUrlOrNull() == null) {
return emptyList()
}
return playListUtils.extractFromHls(videoUrl, url, videoNameGen = { quality -> "MaxStream: $quality" })
}
}

View File

@ -1,15 +0,0 @@
ext {
extName = 'LMAnime'
extClass = '.LMAnime'
themePkg = 'animestream'
baseUrl = 'https://lmanime.com'
overrideVersionCode = 6
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:dailymotion-extractor"))
implementation(project(":lib:mp4upload-extractor"))
implementation(project(":lib:streamwish-extractor"))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,117 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.lmanime
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.dailymotionextractor.DailymotionExtractor
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStream
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import okhttp3.Response
class LMAnime : AnimeStream(
"all",
"LMAnime",
"https://lmanime.com",
) {
// ============================ Video Links =============================
override val prefQualityValues = arrayOf("144p", "288p", "480p", "720p", "1080p")
override val prefQualityEntries = prefQualityValues
override fun videoListParse(response: Response): List<Video> {
val items = response.asJsoup().select(videoListSelector())
val allowed = preferences.getStringSet(PREF_ALLOWED_LANGS_KEY, PREF_ALLOWED_LANGS_DEFAULT)!!
return items
.filter { element ->
val text = element.text()
allowed.any { it in text }
}.parallelCatchingFlatMapBlocking {
val language = it.text().substringBefore(" ")
val url = getHosterUrl(it)
getVideoList(url, language)
}
}
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
private val dailyExtractor by lazy { DailymotionExtractor(client, headers) }
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
override fun getVideoList(url: String, name: String): List<Video> {
val prefix = "($name) - "
return when {
"dailymotion" in url -> dailyExtractor.videosFromUrl(url, "Dailymotion ($name)")
"mp4upload" in url -> mp4uploadExtractor.videosFromUrl(url, headers, "$prefix")
"filelions" in url -> streamwishExtractor.videosFromUrl(url, "StreamWish ($name)")
else -> emptyList()
}
}
// ============================== Settings ==============================
@Suppress("UNCHECKED_CAST")
override fun setupPreferenceScreen(screen: PreferenceScreen) {
super.setupPreferenceScreen(screen) // Quality preferences
ListPreference(screen.context).apply {
key = PREF_LANG_KEY
title = PREF_LANG_TITLE
entries = PREF_LANG_ENTRIES
entryValues = PREF_LANG_ENTRIES
setDefaultValue(PREF_LANG_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
MultiSelectListPreference(screen.context).apply {
key = PREF_ALLOWED_LANGS_KEY
title = PREF_ALLOWED_LANGS_TITLE
entries = PREF_ALLOWED_LANGS_ENTRIES
entryValues = PREF_ALLOWED_LANGS_ENTRIES
setDefaultValue(PREF_ALLOWED_LANGS_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}.also(screen::addPreference)
}
// ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(prefQualityKey, prefQualityDefault)!!
val lang = preferences.getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
return sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ it.quality.contains(lang, true) },
),
).reversed()
}
companion object {
private const val PREF_LANG_KEY = "pref_language"
private const val PREF_LANG_TITLE = "Preferred language"
private const val PREF_LANG_DEFAULT = "English"
private val PREF_LANG_ENTRIES = arrayOf(
"English",
"Español",
"Indonesian",
"Portugués",
"Türkçe",
"العَرَبِيَّة",
"ไทย",
)
private const val PREF_ALLOWED_LANGS_KEY = "pref_allowed_languages"
private const val PREF_ALLOWED_LANGS_TITLE = "Allowed languages to fetch videos"
private val PREF_ALLOWED_LANGS_ENTRIES = PREF_LANG_ENTRIES
private val PREF_ALLOWED_LANGS_DEFAULT = PREF_ALLOWED_LANGS_ENTRIES.toSet()
}
}

View File

@ -1,14 +0,0 @@
ext {
extName = 'MissAV'
extClass = '.MissAV'
extVersionCode = 11
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:unpacker'))
implementation(project(':lib:playlist-utils'))
implementation(project(':lib:javcoverfetcher'))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

View File

@ -1,191 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.missav
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
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.javcoverfetcher.JavCoverFetcher
import eu.kanade.tachiyomi.lib.javcoverfetcher.JavCoverFetcher.fetchHDCovers
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.lib.unpacker.Unpacker
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MissAV : AnimeHttpSource(), ConfigurableAnimeSource {
override val name = "MissAV"
override val lang = "all"
override val baseUrl = "https://missav.com"
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
private val playlistExtractor by lazy {
PlaylistUtils(client, headers)
}
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun popularAnimeRequest(page: Int) =
GET("$baseUrl/en/today-hot?page=$page", headers)
override fun popularAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val entries = document.select("div.thumbnail").map { element ->
SAnime.create().apply {
element.select("a.text-secondary").also {
setUrlWithoutDomain(it.attr("href"))
title = it.text()
}
thumbnail_url = element.selectFirst("img")?.attr("abs:data-src")
}
}
val hasNextPage = document.selectFirst("a[rel=next]") != null
return AnimesPage(entries, hasNextPage)
}
override fun latestUpdatesRequest(page: Int) =
GET("$baseUrl/en/new?page=$page", headers)
override fun latestUpdatesParse(response: Response) = popularAnimeParse(response)
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder().apply {
val genre = filters.firstInstanceOrNull<GenreList>()?.selected
if (query.isNotEmpty()) {
addEncodedPathSegments("en/search")
addPathSegment(query.trim())
} else if (genre != null) {
addEncodedPathSegments(genre)
} else {
addEncodedPathSegments("en/new")
}
filters.firstInstanceOrNull<SortFilter>()?.selected?.let {
addQueryParameter("sort", it)
}
addQueryParameter("page", page.toString())
}.build().toString()
return GET(url, headers)
}
override fun getFilterList() = getFilters()
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
override fun animeDetailsParse(response: Response): SAnime {
val document = response.asJsoup()
val jpTitle = document.select("div.text-secondary span:contains(title) + span").text()
val siteCover = document.selectFirst("video.player")?.attr("abs:data-poster")
return SAnime.create().apply {
title = document.selectFirst("h1.text-base")!!.text()
genre = document.getInfo("/genres/")
author = listOfNotNull(
document.getInfo("/directors/"),
document.getInfo("/makers/"),
).joinToString()
artist = document.getInfo("/actresses/")
status = SAnime.COMPLETED
description = buildString {
document.selectFirst("div.mb-1")?.text()?.also { append("$it\n") }
document.getInfo("/labels/")?.also { append("\nLabel: $it") }
document.getInfo("/series/")?.also { append("\nSeries: $it") }
document.select("div.text-secondary:not(:has(a)):has(span)")
.eachText()
.forEach { append("\n$it") }
}
thumbnail_url = if (preferences.fetchHDCovers) {
JavCoverFetcher.getCoverByTitle(jpTitle) ?: siteCover
} else {
siteCover
}
}
}
private fun Element.getInfo(urlPart: String) =
select("div.text-secondary > a[href*=$urlPart]")
.eachText()
.joinToString()
.takeIf(String::isNotBlank)
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
return listOf(
SEpisode.create().apply {
url = anime.url
name = "Episode"
},
)
}
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val playlists = document.selectFirst("script:containsData(function(p,a,c,k,e,d))")
?.data()
?.let(Unpacker::unpack)?.ifEmpty { null }
?: return emptyList()
val masterPlaylist = playlists.substringAfter("source=\"").substringBefore("\";")
return playlistExtractor.extractFromHls(masterPlaylist, referer = "$baseUrl/")
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY
title = PREF_QUALITY_TITLE
entries = arrayOf("720p", "480p", "360p")
entryValues = arrayOf("720", "480", "360")
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
}.also(screen::addPreference)
JavCoverFetcher.addPreferenceToScreen(screen)
}
override fun episodeListParse(response: Response): List<SEpisode> {
throw UnsupportedOperationException()
}
private inline fun <reified T> List<*>.firstInstanceOrNull(): T? =
filterIsInstance<T>().firstOrNull()
companion object {
private const val PREF_QUALITY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "720"
}
}

View File

@ -1,346 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.missav
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
abstract class SelectFilter(
name: String,
private val options: List<Pair<String, String>>,
) : AnimeFilter.Select<String>(
name,
options.map { it.first }.toTypedArray(),
) {
val selected get() = options[state].second.takeUnless { state == 0 }
}
class SortFilter : SelectFilter(
"Sort by",
SORT,
) {
companion object {
val SORT = listOf(
Pair("Release date", "released_at"),
Pair("Recent update", "published_at"),
Pair("Today views", "today_views"),
Pair("Weekly views", "weekly_views"),
Pair("Monthly views", "monthly_views"),
Pair("Total views", "views"),
)
}
}
class GenreList : SelectFilter(
"Genres",
GENRES,
) {
companion object {
val GENRES = listOf(
Pair("", ""),
Pair("Uncensored Leak", "en/uncensored-leak"),
Pair("Hd", "en/genres/Hd"),
Pair("Exclusive", "en/genres/Exclusive"),
Pair("Creampie", "en/genres/Creampie"),
Pair("Big Breasts", "en/genres/Big%20Breasts"),
Pair("Individual", "en/genres/Individual"),
Pair("Wife", "en/genres/Wife"),
Pair("Mature Woman", "en/genres/Mature%20Woman"),
Pair("Ordinary Person", "en/genres/Ordinary%20Person"),
Pair("Pretty Girl", "en/genres/Pretty%20Girl"),
Pair("Ride", "en/genres/Ride"),
Pair("Oral Sex", "en/genres/Oral%20Sex"),
Pair("Orgy", "en/genres/Orgy"),
Pair("Slim Pixelated", "en/genres/Slim%20Pixelated"),
Pair("4 Hours Or More", "en/genres/4%20Hours%20Or%20More"),
Pair("Slut", "en/genres/Slut"),
Pair("Collection", "en/genres/Collection"),
Pair("High School Girl", "en/genres/High%20School%20Girl"),
Pair("Squirting", "en/genres/Squirting"),
Pair("Fetish", "en/genres/Fetish"),
Pair("Selfie", "en/genres/Selfie"),
Pair("Tit Job", "en/genres/Tit%20Job"),
Pair("Planning", "en/genres/Planning"),
Pair("Incest", "en/genres/Incest"),
Pair("Hit On Girls", "en/genres/Hit%20On%20Girls"),
Pair("Sneak Shots", "en/genres/Sneak%20Shots"),
Pair("Slim", "en/genres/Slim"),
Pair("Bukkake", "en/genres/Bukkake"),
Pair("Beautiful Breasts", "en/genres/Beautiful%20Breasts"),
Pair("Masturbate", "en/genres/Masturbate"),
Pair("Masturbation", "en/genres/Masturbation"),
Pair("Restraint", "en/genres/Restraint"),
Pair("Promiscuous", "en/genres/Promiscuous"),
Pair("Lesbian", "en/genres/Lesbian"),
Pair("Ntr", "en/genres/Ntr"),
Pair("Sister", "en/genres/Sister"),
Pair("Plot", "en/genres/Plot"),
Pair("Cosplay", "en/genres/Cosplay"),
Pair("Humiliation", "en/genres/Humiliation"),
Pair("Documentary", "en/genres/Documentary"),
Pair("Hot Girl", "en/genres/Hot%20Girl"),
Pair("Ol", "en/genres/Ol"),
Pair("Uniform", "en/genres/Uniform"),
Pair("Fingering", "en/genres/Fingering"),
Pair("Vibrator", "en/genres/Vibrator"),
Pair("Adultery", "en/genres/Adultery"),
Pair("Cunnilingus", "en/genres/Cunnilingus"),
Pair("Delusion", "en/genres/Delusion"),
Pair("Female College Student", "en/genres/Female%20College%20Student"),
Pair("Sm", "en/genres/Sm"),
Pair("Shame", "en/genres/Shame"),
Pair("Anus", "en/genres/Anus"),
Pair("Uniform", "en/genres/Uniform"),
Pair("Petite", "en/genres/Petite"),
Pair("Shaving", "en/genres/Shaving"),
Pair("Subjective Perspective", "en/genres/Subjective%20Perspective"),
Pair("Prostitute", "en/genres/Prostitute"),
Pair("Various Occupations", "en/genres/Various%20Occupations"),
Pair("Mother", "en/genres/Mother"),
Pair("Vibrator", "en/genres/Vibrator"),
Pair("Toy", "en/genres/Toy"),
Pair("Promiscuity", "en/genres/Promiscuity"),
Pair("Outdoor Exposure", "en/genres/Outdoor%20Exposure"),
Pair("Butt Fetish", "en/genres/Butt%20Fetish"),
Pair("Pantyhose", "en/genres/Pantyhose"),
Pair("Debut", "en/genres/Debut"),
Pair("Urinate", "en/genres/Urinate"),
Pair("Dirty Talk", "en/genres/Dirty%20Talk"),
Pair("Massage", "en/genres/Massage"),
Pair("Underwear", "en/genres/Underwear"),
Pair("Big Ass", "en/genres/Big%20Ass"),
Pair("Forced Blowjob", "en/genres/Forced%20Blowjob"),
Pair("Sailor Suit", "en/genres/Sailor%20Suit"),
Pair("Swimsuit", "en/genres/Swimsuit"),
Pair("Delivery Only", "en/genres/Delivery%20Only"),
Pair("Female Teacher", "en/genres/Female%20Teacher"),
Pair("Kimono", "en/genres/Kimono"),
Pair("Swallow Sperm", "en/genres/Swallow%20Sperm"),
Pair("69", "en/genres/69"),
Pair("Small Breasts", "en/genres/Small%20Breasts"),
Pair("Elder Sister", "en/genres/Elder%20Sister"),
Pair("Young Wife", "en/genres/Young%20Wife"),
Pair("Nurse", "en/genres/Nurse"),
Pair("Massage Oil", "en/genres/Massage%20Oil"),
Pair("Group Bukkake", "en/genres/Group%20Bukkake"),
Pair("Tied Up", "en/genres/Tied%20Up"),
Pair("Fat Girl", "en/genres/Fat%20Girl"),
Pair("Rejuvenation Massage", "en/genres/Rejuvenation%20Massage"),
Pair("Short Skirt", "en/genres/Short%20Skirt"),
Pair("Ultra Slim Pixelated", "en/genres/Ultra%20Slim%20Pixelated"),
Pair("Contribution", "en/genres/Contribution"),
Pair("Nice Ass", "en/genres/Nice%20Ass"),
Pair("Foot Fetish", "en/genres/Foot%20Fetish"),
Pair("Full Hd (Fhd)", "en/genres/Full%20Hd%20%28Fhd%29"),
Pair("Glasses Girl", "en/genres/Glasses%20Girl"),
Pair("Kiss", "en/genres/Kiss"),
Pair("4K", "en/genres/4K"),
Pair("Close Up", "en/genres/Close%20Up"),
Pair("Big Breasts", "en/genres/Big%20Breasts"),
Pair("Tied Up", "en/genres/Tied%20Up"),
Pair("Big Breast Fetish", "en/genres/Big%20Breast%20Fetish"),
Pair("Swimsuit", "en/genres/Swimsuit"),
Pair("Sportswear", "en/genres/Sportswear"),
Pair("Virgin", "en/genres/Virgin"),
Pair("Vibrating Egg", "en/genres/Vibrating%20Egg"),
Pair("Aphrodisiac", "en/genres/Aphrodisiac"),
Pair("Lesbian Kiss", "en/genres/Lesbian%20Kiss"),
Pair("Mini Skirt", "en/genres/Mini%20Skirt"),
Pair("White Skin", "en/genres/White%20Skin"),
Pair("M Male", "en/genres/M%20Male"),
Pair("Couple", "en/genres/Couple"),
Pair("Hot Spring", "en/genres/Hot%20Spring"),
Pair("Maid", "en/genres/Maid"),
Pair("Face Ride", "en/genres/Face%20Ride"),
Pair("Imprisonment", "en/genres/Imprisonment"),
Pair("Footjob", "en/genres/Footjob"),
Pair("Fighting", "en/genres/Fighting"),
Pair("Tall Lady", "en/genres/Tall%20Lady"),
Pair("Female Warrior", "en/genres/Female%20Warrior"),
Pair("Artist", "en/genres/Artist"),
Pair("Science Fiction", "en/genres/Science%20Fiction"),
Pair("Mischief", "en/genres/Mischief"),
Pair("Actress Collection", "en/genres/Actress%20Collection"),
Pair("Married Woman", "en/genres/Married%20Woman"),
Pair("Sweating", "en/genres/Sweating"),
Pair("Black Male Actor", "en/genres/Black%20Male%20Actor"),
Pair("Stepmother", "en/genres/Stepmother"),
Pair("Petite", "en/genres/Petite"),
Pair("Beautiful Legs", "en/genres/Beautiful%20Legs"),
Pair("Private Teacher", "en/genres/Private%20Teacher"),
Pair("Big Pennis", "en/genres/Big%20Pennis"),
Pair("Super Breasts", "en/genres/Super%20Breasts"),
Pair("Advertising Idol", "en/genres/Advertising%20Idol"),
Pair("Torture", "en/genres/Torture"),
Pair("Emmanuel", "en/genres/Emmanuel"),
Pair("Anal Sex", "en/genres/Anal%20Sex"),
Pair("Black Hair", "en/genres/Black%20Hair"),
Pair("Beautiful Breasts", "en/genres/Beautiful%20Breasts"),
Pair("Erotic Photo", "en/genres/Erotic%20Photo"),
Pair("Widow", "en/genres/Widow"),
Pair("Gym Suit", "en/genres/Gym%20Suit"),
Pair("Cruel", "en/genres/Cruel"),
Pair("Sexy", "en/genres/Sexy"),
Pair("Car Sex", "en/genres/Car%20Sex"),
Pair("Multiple Stories", "en/genres/Multiple%20Stories"),
Pair("Campus Story", "en/genres/Campus%20Story"),
Pair("3P, 4P", "en/genres/3P,%204P"),
Pair("Transgender", "en/genres/Transgender"),
Pair("Slim", "en/genres/Slim"),
Pair("Female Doctor", "en/genres/Female%20Doctor"),
Pair("In Love", "en/genres/In%20Love"),
Pair("Fighter", "en/genres/Fighter"),
Pair("Fantasy", "en/genres/Fantasy"),
Pair("Pure", "en/genres/Pure"),
Pair("Virgin", "en/genres/Virgin"),
Pair("Instant Sex", "en/genres/Instant%20Sex"),
Pair("Missy", "en/genres/Missy"),
Pair("Enema", "en/genresenema"),
Pair("Dance", "en/genres/Dance"),
Pair("Feminine", "en/genres/Feminine"),
Pair("Best, Omnibus", "en/genres/Best,%20Omnibus"),
Pair("Whites", "en/genres/Whites"),
Pair("Flight Attendant", "en/genres/Flight%20Attendant"),
Pair("Harem", "en/genres/Harem"),
Pair("Foreign Actress", "en/genres/Foreign%20Actress"),
Pair("Physical Education", "en/genres/Physical%20Education"),
Pair("Bronze", "en/genres/Bronze"),
Pair("Female Investigator", "en/genres/Female%20Investigator"),
Pair("Transsexuals", "en/genres/Transsexuals"),
Pair("Model", "en/genres/Model"),
Pair("Baby Face", "en/genres/Baby%20Face"),
Pair("Doggy Style", "en/genres/Doggy%20Style"),
Pair("Shaving", "en/genres/Shaving"),
Pair("Bitch", "en/genres/Bitch"),
Pair("Bloomers", "en/genres/Bloomers"),
Pair("One Piece Dress", "en/genres/One%20Piece%20Dress"),
Pair("Knee Socks", "en/genres/Knee%20Socks"),
Pair("Thanks Offering", "en/genres/Thanks%20Offering"),
Pair("Cute Little Boy", "en/genres/Cute%20Little%20Boy"),
Pair("Delivery-Only Amateur", "en/genres/Delivery-Only%20Amateur"),
Pair("Other", "en/genres/Other"),
Pair("Bubble Bath", "en/genres/Bubble%20Bath"),
Pair("Tickle", "en/genres/Tickle"),
Pair("High School Girl", "en/genres/High%20School%20Girl"),
Pair("Sister", "en/genres/Sister"),
Pair("Extreme Orgasm", "en/genres/Extreme%20Orgasm"),
Pair("Breast Milk", "en/genres/Breast%20Milk"),
Pair("M Female", "en/genres/M%20Female"),
Pair("Pregnant Woman", "en/genres/Pregnant%20Woman"),
Pair("Indie", "en/genres/Indie"),
Pair("Homosexual", "en/genres/Homosexual"),
Pair("Vr", "en/genres/Vr"),
Pair("Drink Urine", "en/genres/Drink%20Urine"),
Pair("Racing Girl", "en/genres/Racing%20Girl"),
Pair("Femdom Slave", "en/genres/Femdom%20Slave"),
Pair("Heaven Tv", "en/genres/Heaven%20Tv"),
Pair("Secretary", "en/genres/Secretary"),
Pair("Insult", "en/genres/Insult"),
Pair("Hot Girl", "en/genres/Hot%20Girl"),
Pair("Small Breasts", "en/genres/Small%20Breasts"),
Pair("Rape", "en/genres/Rape"),
Pair("Thirty", "en/genres/Thirty"),
Pair("Lolita", "en/genres/Lolita"),
Pair("Female Boss", "en/genres/Female%20Boss"),
Pair("Foreign Object Penetration", "en/genres/Foreign%20Object%20Penetration"),
Pair("Hit On Boys", "en/genres/Hit%20On%20Boys"),
Pair("Stool", "en/genres/Stool"),
Pair("Hysteroscope", "en/genres/Hysteroscope"),
Pair("Young Wife", "en/genres/Young%20Wife"),
Pair("Defecation", "en/genres/Defecation"),
Pair("Gang Rape", "en/genres/Gang%20Rape"),
Pair("Anchorwoman", "en/genres/Anchorwoman"),
Pair("High Quality Vr", "en/genres/High%20Quality%20Vr"),
Pair("Similar", "en/genres/Similar"),
Pair("Transsexuals", "en/genres/Transsexuals"),
Pair("Catwoman", "en/genres/Catwoman"),
Pair("Bathtub", "en/genres/Bathtub"),
Pair("Dildo", "en/genres/Dildo"),
Pair("Limited Time", "en/genres/Limited%20Time"),
Pair("Fist", "en/genres/Fist"),
Pair("Dating", "en/genres/Dating"),
Pair("Cuckold", "en/genres/Cuckold"),
Pair("Original", "en/genres/Original"),
Pair("Lecturer", "en/genres/Lecturer"),
Pair("Esthetic Massage", "en/genres/Esthetic%20Massage"),
Pair("Childhood", "en/genres/Childhood"),
Pair("Uterus", "en/genres/Uterus"),
Pair("Pregnant", "en/genres/Pregnant"),
Pair("Entertainer", "en/genresentertainer"),
Pair("Long Hair", "en/genres/Long%20Hair"),
Pair("Petite", "en/genres/Petite"),
Pair("First Shot", "en/genres/First%20Shot"),
Pair("Muscle", "en/genres/Muscle"),
Pair("Outdoors", "en/genres/Outdoors"),
Pair("Naked Apron", "en/genres/Naked%20Apron"),
Pair("Male Squirting", "en/genres/Male%20Squirting"),
Pair("Hotel Owner", "en/genres/Hotel%20Owner"),
Pair("Molester", "en/genres/Molester"),
Pair("Artist", "en/genres/Artist"),
Pair("Bunny Girl", "en/genres/Bunny%20Girl"),
Pair("Travel", "en/genres/Travel"),
Pair("Racing Girl", "en/genres/Racing%20Girl"),
Pair("Asian Actress", "en/genres/Asian%20Actress"),
Pair("Tentacle", "en/genres/Tentacle"),
Pair("Proud Pussy", "en/genres/Proud%20Pussy"),
Pair("Subordinate Or Colleague", "en/genres/Subordinate%20Or%20Colleague"),
Pair("With Bonus Video Only For Mgs", "en/genres/With%20Bonus%20Video%20Only%20For%20Mgs"),
Pair("Business Clothing", "en/genres/Business%20Clothing"),
Pair("Premature Ejaculation", "en/genres/Premature%20Ejaculation"),
Pair("Friend", "en/genres/Friend"),
Pair("Shame And Humiliation", "en/genres/Shame%20And%20Humiliation"),
Pair("Big Breasts", "en/genres/Big%20Breasts"),
Pair("Short Hair", "en/genres/Short%20Hair"),
Pair("Dildo", "en/genres/Dildo"),
Pair("Limited Time", "en/genres/Limited%20Time"),
Pair("Fist", "en/genres/Fist"),
Pair("Dating", "en/genres/Dating"),
Pair("Cuckold", "en/genres/Cuckold"),
Pair("Original", "en/genres/Original"),
Pair("Lecturer", "en/genres/Lecturer"),
Pair("Esthetic Massage", "en/genres/Esthetic%20Massage"),
Pair("Childhood", "en/genres/Childhood"),
Pair("Uterus", "en/genres/Uterus"),
Pair("Pregnant", "en/genres/Pregnant"),
Pair("Entertainer", "en/genresentertainer"),
Pair("Long Hair", "en/genres/Long%20Hair"),
Pair("Petite", "en/genres/Petite"),
Pair("First Shot", "en/genres/First%20Shot"),
Pair("Muscle", "en/genres/Muscle"),
Pair("Outdoors", "en/genres/Outdoors"),
Pair("Naked Apron", "en/genres/Naked%20Apron"),
Pair("Male Squirting", "en/genres/Male%20Squirting"),
Pair("Hotel Owner", "en/genres/Hotel%20Owner"),
Pair("Molester", "en/genres/Molester"),
Pair("Artist", "en/genres/Artist"),
Pair("Bunny Girl", "en/genres/Bunny%20Girl"),
Pair("Travel", "en/genres/Travel"),
Pair("Racing Girl", "en/genres/Racing%20Girl"),
Pair("Asian Actress", "en/genres/Asian%20Actress"),
Pair("Tentacle", "en/genres/Tentacle"),
Pair("Proud Pussy", "en/genres/Proud%20Pussy"),
Pair("Subordinate Or Colleague", "en/genres/Subordinate%20Or%20Colleague"),
Pair("With Bonus Video Only For Mgs", "en/genres/With%20Bonus%20Video%20Only%20For%20Mgs"),
Pair("Business Clothing", "en/genres/Business%20Clothing"),
Pair("Premature Ejaculation", "en/genres/Premature%20Ejaculation"),
Pair("Friend", "en/genres/Friend"),
Pair("Shame And Humiliation", "en/genres/Shame%20And%20Humiliation"),
Pair("Big Breasts", "en/genres/Big%20Breasts"),
Pair("Short Hair", "en/genres/Short%20Hair"),
Pair("Waitress", "en/genres/Waitress"),
Pair("Clinic", "en/genres/Clinic"),
Pair("Exposure", "en/genres/Exposure"),
Pair("Kimono / Yukata", "en/genres/Kimono%20/%20Yukata"),
Pair("Lewd Nasty Lady", "en/genres/Lewd%20Nasty%20Lady"),
Pair("Bubble Socks", "en/genres/Bubble%20Socks"),
Pair("Fantasy", "en/genres/Fantasy"),
Pair("Idol", "en/genres/Idol"),
Pair("Time Stops", "en/genres/Time%20Stops"),
)
}
}
fun getFilters() = AnimeFilterList(
SortFilter(),
GenreList(),
AnimeFilter.Separator(),
AnimeFilter.Header("Genre filters ignored with text search!!"),
)

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@ -1,315 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.sudatchi
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.AnimeDto
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.AnimePageDto
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.DirectoryDto
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.EpisodePageDto
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.HomePageDto
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.PropsDto
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.SubtitleDto
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.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
override val name = "Sudatchi"
override val baseUrl = "https://sudatchi.com"
private val ipfsUrl = "https://ipfs.animeui.com"
override val lang = "all"
override val supportsLatest = true
private val codeRegex by lazy { Regex("""\((.*)\)""") }
private val json: Json by injectLazy()
private val sudatchiFilters: SudatchiFilters by lazy { SudatchiFilters(baseUrl, client) }
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET(baseUrl, headers)
private fun Int.parseStatus() = when (this) {
1 -> SAnime.UNKNOWN // Not Yet Released
2 -> SAnime.ONGOING
3 -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
private fun AnimeDto.toSAnime(titleLang: String) = SAnime.create().apply {
url = "/anime/$slug"
title = when (titleLang) {
"romaji" -> titleRomanji
"japanese" -> titleJapanese
else -> titleEnglish
} ?: arrayOf(titleEnglish, titleRomanji, titleJapanese, "").firstNotNullOf { it }
description = synopsis
status = statusId.parseStatus()
thumbnail_url = when {
imgUrl.startsWith("/") -> "$baseUrl$imgUrl"
else -> "$ipfsUrl/ipfs/$imgUrl"
}
genre = animeGenres?.joinToString { it.genre.name }
}
override fun popularAnimeParse(response: Response): AnimesPage {
sudatchiFilters.fetchFilters()
val titleLang = preferences.title
val document = response.asJsoup()
val data = document.parseAs<HomePageDto>().animeSpotlight
return AnimesPage(data.map { it.toSAnime(titleLang) }, false)
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/api/directory?page=$page&genres=&status=2,3", headers)
override fun latestUpdatesParse(response: Response): AnimesPage {
sudatchiFilters.fetchFilters()
val titleLang = preferences.title
return response.parseAs<DirectoryDto>().let {
AnimesPage(it.animes.map { it.toSAnime(titleLang) }, it.page != it.pages)
}
}
// =============================== Search ===============================
override fun getFilterList() = sudatchiFilters.getFilterList()
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val id = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/anime/$id", headers))
.awaitSuccess()
.use(::searchAnimeByIdParse)
} else {
super.getSearchAnime(page, query, filters)
}
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val url = "$baseUrl/api/directory".toHttpUrl().newBuilder()
url.addQueryParameter("page", page.toString())
url.addQueryParameter("title", query)
filters.filterIsInstance<SudatchiFilters.QueryParameterFilter>().forEach {
val (name, value) = it.toQueryParameter()
if (value != null) url.addQueryParameter(name, value)
}
return GET(url.build(), headers)
}
override fun searchAnimeParse(response: Response) = latestUpdatesParse(response)
// =========================== Anime Details ============================
override fun getAnimeUrl(anime: SAnime) = "$baseUrl${anime.url}"
override fun animeDetailsParse(response: Response): SAnime {
val document = response.asJsoup()
val data = document.parseAs<AnimePageDto>().animeData
return data.toSAnime(preferences.title)
}
// ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime) = animeDetailsRequest(anime)
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
val anime = document.parseAs<AnimePageDto>().animeData
return anime.episodes.map {
SEpisode.create().apply {
name = it.title
episode_number = it.number.toFloat()
url = "/watch/${anime.slug}/${it.number}"
}
}.reversed()
}
// ============================ Video Links =============================
override fun videoListRequest(episode: SEpisode) = GET("$baseUrl${episode.url}", headers)
private val playlistUtils: PlaylistUtils by lazy { PlaylistUtils(client, headers) }
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val data = document.parseAs<EpisodePageDto>().episodeData
val subtitles = json.decodeFromString<List<SubtitleDto>>(data.subtitlesJson)
// val videoUrl = client.newCall(GET("$baseUrl/api/streams?episodeId=${data.episode.id}", headers)).execute().parseAs<StreamsDto>().url
// keeping it in case the simpler solution breaks, can be hardcoded to this for now :
val videoUrl = "$baseUrl/videos/m3u8/episode-${data.episode.id}.m3u8"
return playlistUtils.extractFromHls(
videoUrl,
videoNameGen = { "Sudatchi (Private IPFS Gateway) - $it" },
subtitleList = subtitles.map {
Track("$ipfsUrl${it.url}", "${it.subtitlesName.name} (${it.subtitlesName.language})")
}.sort(),
)
}
@JvmName("trackSort")
private fun List<Track>.sort(): List<Track> {
val subtitles = preferences.subtitles
return sortedWith(
compareBy(
{ codeRegex.find(it.lang)!!.groupValues[1] != subtitles },
{ codeRegex.find(it.lang)!!.groupValues[1] != PREF_SUBTITLES_DEFAULT },
{ it.lang },
),
)
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.quality
return sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
}
// ============================ Preferences =============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES.map { it.first }.toTypedArray()
entryValues = PREF_QUALITY_ENTRIES.map { it.second }.toTypedArray()
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, new ->
val index = findIndexOfValue(new as String)
preferences.edit().putString(key, entryValues[index] as String).commit()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_SUBTITLES_KEY
title = PREF_SUBTITLES_TITLE
entries = PREF_SUBTITLES_ENTRIES.map { it.first }.toTypedArray()
entryValues = PREF_SUBTITLES_ENTRIES.map { it.second }.toTypedArray()
setDefaultValue(PREF_SUBTITLES_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, new ->
val index = findIndexOfValue(new as String)
preferences.edit().putString(key, entryValues[index] as String).commit()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_TITLE_KEY
title = PREF_TITLE_TITLE
entries = PREF_TITLE_ENTRIES.map { it.first }.toTypedArray()
entryValues = PREF_TITLE_ENTRIES.map { it.second }.toTypedArray()
setDefaultValue(PREF_TITLE_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, new ->
val index = findIndexOfValue(new as String)
preferences.edit().putString(key, entryValues[index] as String).commit()
}
}.also(screen::addPreference)
}
// ============================= Utilities ==============================
private inline fun <reified T> Document.parseAs(): T {
val nextData = this.selectFirst("script#__NEXT_DATA__")!!.data()
return json.decodeFromString<PropsDto<T>>(nextData).props.pageProps
}
private val SharedPreferences.quality get() = getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
private val SharedPreferences.subtitles get() = getString(PREF_SUBTITLES_KEY, PREF_SUBTITLES_DEFAULT)!!
private val SharedPreferences.title get() = getString(PREF_TITLE_KEY, PREF_TITLE_DEFAULT)!!
companion object {
const val PREFIX_SEARCH = "id:"
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "1080"
private val PREF_QUALITY_ENTRIES = arrayOf(
Pair("1080p", "1080"),
Pair("720p", "720"),
Pair("480p", "480"),
)
private const val PREF_SUBTITLES_KEY = "preferred_subtitles"
private const val PREF_SUBTITLES_TITLE = "Preferred subtitles"
private const val PREF_SUBTITLES_DEFAULT = "eng"
private val PREF_SUBTITLES_ENTRIES = arrayOf(
Pair("Arabic (Saudi Arabia)", "ara"),
Pair("Brazilian Portuguese", "por"),
Pair("Chinese", "chi"),
Pair("Croatian", "hrv"),
Pair("Czech", "cze"),
Pair("Danish", "dan"),
Pair("Dutch", "dut"),
Pair("English", "eng"),
Pair("European Spanish", "spa-es"),
Pair("Filipino", "fil"),
Pair("Finnish", "fin"),
Pair("French", "fra"),
Pair("German", "deu"),
Pair("Greek", "gre"),
Pair("Hebrew", "heb"),
Pair("Hindi", "hin"),
Pair("Hungarian", "hun"),
Pair("Indonesian", "ind"),
Pair("Italian", "ita"),
Pair("Japanese", "jpn"),
Pair("Korean", "kor"),
Pair("Latin American Spanish", "spa-419"),
Pair("Malay", "may"),
Pair("Norwegian Bokmål", "nob"),
Pair("Polish", "pol"),
Pair("Romanian", "rum"),
Pair("Russian", "rus"),
Pair("Swedish", "swe"),
Pair("Thai", "tha"),
Pair("Turkish", "tur"),
Pair("Ukrainian", "ukr"),
Pair("Vietnamese", "vie"),
)
private const val PREF_TITLE_KEY = "preferred_title"
private const val PREF_TITLE_TITLE = "Preferred title"
private const val PREF_TITLE_DEFAULT = "english"
private val PREF_TITLE_ENTRIES = arrayOf(
Pair("English", "english"),
Pair("Romaji", "romaji"),
Pair("Japanese", "japanese"),
)
}
}

View File

@ -1,72 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.sudatchi
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.DirectoryFiltersDto
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.FilterItemDto
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.FilterYearDto
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.parseAs
import okhttp3.OkHttpClient
class SudatchiFilters(
private val baseUrl: String,
private val client: OkHttpClient,
) {
private var error = false
private lateinit var filterList: AnimeFilterList
interface QueryParameterFilter { fun toQueryParameter(): Pair<String, String?> }
private class Checkbox(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
private class CheckboxList(name: String, private val paramName: String, private val pairs: List<Pair<String, String>>) :
AnimeFilter.Group<AnimeFilter.CheckBox>(name, pairs.map { Checkbox(it.first) }), QueryParameterFilter {
override fun toQueryParameter() = Pair(
paramName,
state.asSequence()
.filter { it.state }
.map { checkbox -> pairs.find { it.first == checkbox.name }!!.second }
.filter(String::isNotBlank)
.joinToString(","),
)
}
fun getFilterList(): AnimeFilterList {
return if (error) {
AnimeFilterList(AnimeFilter.Header("Error fetching the filters."))
} else if (this::filterList.isInitialized) {
filterList
} else {
AnimeFilterList(AnimeFilter.Header("Use 'Reset' to load the filters."))
}
}
fun fetchFilters() {
if (!this::filterList.isInitialized) {
runCatching {
error = false
filterList = client.newCall(GET("$baseUrl/api/directory"))
.execute()
.parseAs<DirectoryFiltersDto>()
.let(::filtersParse)
}.onFailure { error = true }
}
}
private fun List<FilterItemDto>.toPairList() = map { Pair(it.name, it.id.toString()) }
@JvmName("toPairList2")
private fun List<FilterYearDto>.toPairList() = map { Pair(it.year.toString(), it.year.toString()) }
private fun filtersParse(directoryFiltersDto: DirectoryFiltersDto): AnimeFilterList {
return AnimeFilterList(
CheckboxList("Genres", "genres", directoryFiltersDto.genres.toPairList()),
CheckboxList("Years", "years", directoryFiltersDto.years.toPairList()),
CheckboxList("Types", "types", directoryFiltersDto.types.toPairList()),
CheckboxList("Status", "status", directoryFiltersDto.status.toPairList()),
)
}
}

View File

@ -1,41 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.sudatchi
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://sudatchi.com/anime/<item> intents
* and redirects them to the main Aniyomi process.
*/
class SudatchiUrlActivity : 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", "${Sudatchi.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,107 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.sudatchi.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GenreDto(val name: String)
@Serializable
data class AnimeGenreRelationDto(
@SerialName("Genre")
val genre: GenreDto,
)
@Serializable
data class EpisodeDto(
val title: String,
val id: Int,
val number: Int,
)
@Serializable
data class AnimeDto(
val titleRomanji: String?,
val titleEnglish: String?,
val titleJapanese: String?,
val synopsis: String,
val slug: String,
val statusId: Int,
val imgUrl: String,
@SerialName("AnimeGenres")
val animeGenres: List<AnimeGenreRelationDto>?,
@SerialName("Episodes")
val episodes: List<EpisodeDto> = emptyList(),
)
@Serializable
data class HomePageDto(
@SerialName("AnimeSpotlight")
val animeSpotlight: List<AnimeDto>,
)
@Serializable
data class AnimePageDto(
val animeData: AnimeDto,
)
@Serializable
data class EpisodeDataDto(
val episode: EpisodeDto,
val subtitlesJson: String,
)
@Serializable
data class EpisodePageDto(
val episodeData: EpisodeDataDto,
)
@Serializable
data class PagePropsDto<T>(val pageProps: T)
@Serializable
data class PropsDto<T>(val props: PagePropsDto<T>)
@Serializable
data class DirectoryDto(
val animes: List<AnimeDto>,
val page: Int,
val pages: Int,
)
@Serializable
data class SubtitleLangDto(
val name: String,
val language: String,
)
@Serializable
data class SubtitleDto(
val url: String,
@SerialName("SubtitlesName")
val subtitlesName: SubtitleLangDto,
)
@Serializable
data class FilterItemDto(
val id: Int,
val name: String,
)
@Serializable
data class FilterYearDto(
val year: Int,
)
@Serializable
data class DirectoryFiltersDto(
val genres: List<FilterItemDto>,
val years: List<FilterYearDto>,
val types: List<FilterItemDto>,
val status: List<FilterItemDto>,
)
@Serializable
data class StreamsDto(
val url: String,
)

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".all.supjav.SupJavUrlActivity"
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="supjav.com"
android:pathPattern="/..*\\.html"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,15 +0,0 @@
ext {
extName = 'SupJav'
extClass = '.SupJavFactory'
extVersionCode = 9
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:streamtape-extractor"))
implementation(project(":lib:streamwish-extractor"))
implementation(project(":lib:voe-extractor"))
implementation(project(":lib:playlist-utils"))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,248 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.supjav
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.select.Elements
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class SupJav(override val lang: String = "en") : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "SupJav"
override val baseUrl = "https://supjav.com"
override val supportsLatest = false
override fun headersBuilder() = super.headersBuilder()
.set("Referer", "$baseUrl/")
.set("Origin", baseUrl)
private val langPath = when (lang) {
"en" -> ""
else -> "/$lang"
}
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl$langPath/popular/page/$page", headers)
override fun popularAnimeSelector() = "div.posts > div.post > a"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
element.selectFirst("img")!!.run {
title = attr("alt")
thumbnail_url = absUrl("data-original").ifBlank { absUrl("src") }
}
}
override fun popularAnimeNextPageSelector() = "div.pagination li.active:not(:nth-last-child(2))"
// =============================== 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(): String? {
throw UnsupportedOperationException()
}
// =============================== Search ===============================
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val id = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/$id"))
.awaitSuccess()
.use(::searchAnimeByIdParse)
} else {
super.getSearchAnime(page, query, filters)
}
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup()).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) =
GET("$baseUrl$langPath/?s=$query")
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 {
val content = document.selectFirst("div.content > div.post-meta")!!
title = content.selectFirst("h2")!!.text()
thumbnail_url = content.selectFirst("img")?.absUrl("src")
content.selectFirst("div.cats")?.run {
author = select("p:contains(Maker :) > a").textsOrNull()
artist = select("p:contains(Cast :) > a").textsOrNull()
}
genre = content.select("div.tags > a").textsOrNull()
status = SAnime.COMPLETED
}
private fun Elements.textsOrNull() = eachText().joinToString().takeUnless(String::isEmpty)
// ============================== Episodes ==============================
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
val episode = SEpisode.create().apply {
name = "JAV"
episode_number = 1F
url = anime.url
}
return listOf(episode)
}
override fun episodeListSelector(): String {
throw UnsupportedOperationException()
}
override fun episodeFromElement(element: Element): SEpisode {
throw UnsupportedOperationException()
}
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
val players = doc.select("div.btnst > a").toList()
.filter { it.text() in SUPPORTED_PLAYERS }
.map { it.text() to it.attr("data-link").reversed() }
return players.parallelCatchingFlatMapBlocking(::videosFromPlayer)
}
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
private val voeExtractor by lazy { VoeExtractor(client) }
private val protectorHeaders by lazy {
super.headersBuilder().set("referer", "$PROTECTOR_URL/").build()
}
private val noRedirectClient by lazy {
client.newBuilder().followRedirects(false).build()
}
private fun videosFromPlayer(player: Pair<String, String>): List<Video> {
val (hoster, id) = player
val url = noRedirectClient.newCall(GET("$PROTECTOR_URL/supjav.php?c=$id", protectorHeaders)).execute()
.use { it.headers["location"] }
?: return emptyList()
return when (hoster) {
"ST" -> streamtapeExtractor.videosFromUrl(url)
"VOE" -> voeExtractor.videosFromUrl(url)
"FST" -> streamwishExtractor.videosFromUrl(url)
"TV" -> {
val body = client.newCall(GET(url)).execute().body.string()
val playlistUrl = body.substringAfter("var urlPlay = '", "")
.substringBefore("';")
.takeUnless(String::isEmpty)
?: return emptyList()
playlistUtils.extractFromHls(playlistUrl, url, videoNameGen = { "TV - $it" })
.distinctBy { it.videoUrl }
}
else -> emptyList()
}
}
override fun videoListSelector(): String {
throw UnsupportedOperationException()
}
override fun videoFromElement(element: Element): Video {
throw UnsupportedOperationException()
}
override fun videoUrlParse(document: Document): String {
throw UnsupportedOperationException()
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_ENTRIES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
}
// ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
}
companion object {
const val PREFIX_SEARCH = "id:"
private const val PROTECTOR_URL = "https://lk1.supremejav.com/supjav.php"
private val SUPPORTED_PLAYERS = setOf("TV", "FST", "VOE", "ST")
private const val PREF_QUALITY_KEY = "pref_quality"
private const val PREF_QUALITY_TITLE = "Preferred video quality"
private const val PREF_QUALITY_DEFAULT = "720p"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p")
}
}

View File

@ -1,11 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.supjav
import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
class SupJavFactory : AnimeSourceFactory {
override fun createSources() = listOf(
SupJav("en"),
SupJav("ja"),
SupJav("zh"),
)
}

View File

@ -1,41 +0,0 @@
package eu.kanade.tachiyomi.animeextension.all.supjav
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://supjav.com/<language>/<item> intents
* and redirects them to the main Aniyomi process.
*/
class SupJavUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments ?: return
if (pathSegments.isNotEmpty()) {
val path = pathSegments.joinToString("/")
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${SupJav.PREFIX_SEARCH}$path")
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,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".all.torrentio.TorrentioUrlActivity"
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="torrentio.strem.fun"
android:pathPattern="/anime/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,8 +0,0 @@
ext {
extName = 'Torrentio (Torrent / Debrid)'
extClass = '.Torrentio'
extVersionCode = 1
containsNsfw = false
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Some files were not shown because too many files have changed in this diff Show More