chore: Remove some extensions
2
.github/scripts/sign-apks.sh
vendored
@ -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;
|
||||
|
@ -1,7 +0,0 @@
|
||||
ext {
|
||||
extName = 'AnimeOnsen'
|
||||
extClass = '.AnimeOnsen'
|
||||
extVersionCode = 7
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 164 KiB |
@ -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)
|
||||
}
|
||||
}
|
@ -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",
|
||||
)
|
@ -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
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
ext {
|
||||
extName = 'AnimeWorld India'
|
||||
extClass = '.AnimeWorldIndiaFactory'
|
||||
extVersionCode = 12
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib:playlist-utils"))
|
||||
}
|
Before Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 22 KiB |
@ -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")
|
||||
}
|
||||
}
|
@ -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"),
|
||||
)
|
||||
}
|
@ -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("&")
|
||||
}
|
||||
}
|
@ -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>() }
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 21 KiB |
@ -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",
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
@ -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"))
|
||||
}
|
Before Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 207 KiB |
@ -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",
|
||||
)
|
||||
}
|
||||
}
|
@ -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 },
|
||||
)
|
||||
}
|
||||
}
|
@ -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'))
|
||||
}
|
Before Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 47 KiB |
@ -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"),
|
||||
),
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
@ -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>
|
@ -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'))
|
||||
}
|
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 7.5 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 51 KiB |
@ -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()
|
||||
}
|
||||
}
|
@ -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/"),
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
@ -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*\'([^\']+)""")
|
||||
}
|
||||
}
|
@ -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" })
|
||||
}
|
||||
}
|
@ -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"))
|
||||
}
|
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 13 KiB |
@ -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()
|
||||
}
|
||||
}
|
@ -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'))
|
||||
}
|
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 64 KiB |
@ -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"
|
||||
}
|
||||
}
|
@ -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!!"),
|
||||
)
|
@ -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>
|
@ -1,12 +0,0 @@
|
||||
ext {
|
||||
extName = 'Sudatchi'
|
||||
extClass = '.Sudatchi'
|
||||
extVersionCode = 3
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib:playlist-utils"))
|
||||
}
|
Before Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 17 KiB |
@ -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"),
|
||||
)
|
||||
}
|
||||
}
|
@ -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()),
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
@ -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>
|
@ -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"))
|
||||
}
|
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 13 KiB |
@ -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")
|
||||
}
|
||||
}
|
@ -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"),
|
||||
)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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>
|
@ -1,8 +0,0 @@
|
||||
ext {
|
||||
extName = 'Torrentio (Torrent / Debrid)'
|
||||
extClass = '.Torrentio'
|
||||
extVersionCode = 1
|
||||
containsNsfw = false
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 1.7 KiB |