Add extension: Marinmoe (#1164)

Closes https://github.com/jmir1/aniyomi-extensions/issues/1161
This commit is contained in:
Secozzi
2023-01-14 03:41:03 +01:00
committed by GitHub
parent 2311972098
commit a09dc0a8bf
25 changed files with 1856 additions and 749 deletions

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1,391 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.animixplay
import android.app.Application
import android.content.SharedPreferences
import android.util.Base64
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.en.animixplay.extractors.GogoCdnExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.OkHttpClient
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
import uy.kohesive.injekt.injectLazy
import java.lang.Exception
@ExperimentalSerializationApi
class Animixplay : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Animixplay"
override val baseUrl = "https://animixplay.to"
override val lang = "en"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
var nextPage = "99999999"
var hasNextPage = true
var latestNextDate = "3020-05-06 00:00:00"
var latestHasNextPage = true
override fun popularAnimeSelector(): String = throw Exception("not used")
override fun popularAnimeRequest(page: Int): Request {
val formBody = FormBody.Builder()
.add("genre", "any")
.add("minstr", nextPage)
.add("orderby", "popular")
.build()
return POST("https://animixplay.to/api/search", headers, body = formBody)
}
override fun popularAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val responseJson = json.decodeFromString<JsonObject>(document.select("body").text())
nextPage = responseJson["last"]!!.jsonPrimitive.content
hasNextPage = responseJson["more"]!!.jsonPrimitive.boolean
val animeList = responseJson["result"]!!.jsonArray
val animes = animeList.map { element ->
popularAnimeFromElement(element.jsonObject)
}
return AnimesPage(animes, hasNextPage)
}
override fun popularAnimeFromElement(element: Element) = throw Exception("not used")
private fun popularAnimeFromElement(animeJson: JsonObject): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(animeJson["url"]!!.jsonPrimitive.content.substringBefore("/ep"))
anime.thumbnail_url = animeJson["picture"]!!.jsonPrimitive.content
anime.title = animeJson["title"]!!.jsonPrimitive.content
return anime
}
override fun popularAnimeNextPageSelector(): String = throw Exception("not used")
override fun episodeListSelector() = throw Exception("not used")
override fun episodeListParse(response: Response): List<SEpisode> {
return if (response.request.url.toString().contains(".json")) {
val document = response.asJsoup()
val animeJson = json.decodeFromString<JsonObject>(document.select("body").text())
val malId = animeJson["mal_id"]!!.jsonPrimitive.int
episodesRequest(malId, document)
} else {
episodeFromResponse(response)
}
}
private fun episodesRequest(malId: Int, document: Document): List<SEpisode> {
// POST data
val body = FormBody.Builder()
.add("recomended", malId.toString())
.build()
val animeServersJson = json.decodeFromString<JsonObject>(
client.newCall(
POST(
"https://animixplay.to/api/search",
body = body,
headers = Headers.headersOf("Referer", document.location())
)
).execute().body!!.string()
)
val animeSubDubUrls = animeServersJson["data"]!!.jsonArray[0].jsonObject["items"]!!.jsonArray
val newList = mutableListOf<JsonElement>()
var preferred = 0
for (jsonObj in animeSubDubUrls) {
if (jsonObj.toString().contains("dub")) {
newList.add(preferred, jsonObj)
preferred++
} else {
newList.add(jsonObj)
}
}
newList.reverse()
val urlEndpoint = newList[0].jsonObject["url"]!!.jsonPrimitive.content
val episodesResponse = client.newCall(
GET(
baseUrl + urlEndpoint,
)
).execute()
return episodeFromResponse(episodesResponse)
}
private fun episodeFromResponse(response: Response): List<SEpisode> {
val document = response.asJsoup()
val episodeListJson = json.decodeFromString<JsonObject>(document.select("div#epslistplace").text())
val url = response.request.url.toString()
val episodeAvailable = episodeListJson["eptotal"]!!.jsonPrimitive.int
val episodeList = mutableListOf<SEpisode>()
for (i in 0 until episodeAvailable) {
episodeList.add(episodeFromJsonElement(url, i))
}
return episodeList.reversed()
}
override fun episodeFromElement(element: Element): SEpisode = throw Exception("not used")
private fun episodeFromJsonElement(url: String, number: Int): SEpisode {
val episode = SEpisode.create()
episode.setUrlWithoutDomain("$url/ep$number")
episode.episode_number = number.toFloat() + 1F
episode.name = "Episode ${number + 1}"
return episode
}
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val episodeListJson = json.decodeFromString<JsonObject>(document.select("div#epslistplace").text())
val epNo = response.request.url.toString().substringAfter("/ep")
val serverUrl = "https:" + episodeListJson[epNo]!!.jsonPrimitive.content
val serverPref = preferences.getString("preferred_server", "vrv")
return if (serverPref!!.contains("gogo")) {
GogoCdnExtractor(client, json).videosFromUrl(serverUrl)
} else {
vrvExtractor(serverUrl)
}
}
private fun vrvExtractor(url: String): List<Video> {
val id = url.split("?id=")[1].split("&")[0]
val reqUrl = baseUrl + "/api/live" + encodeBase64(id + "LTXs3GrU8we9O" + encodeBase64(id))
val redirectClient = client.newBuilder().followRedirects(true).build()
val redirectUrlEncodedString = redirectClient.newCall(
GET(
reqUrl,
headers
)
).execute().request.url.fragment!!.substringBefore("#")
val masterUrl = decodeBase64(redirectUrlEncodedString)
return if (masterUrl.contains("gogo")) {
parseCdnMasterPlaylist(masterUrl)
} else {
val masterPlaylist = client.newCall(GET(masterUrl, headers)).execute().body!!.string()
val videosList = mutableListOf<Video>()
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:").split("#EXT-X-STREAM-INF:").forEach {
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",") + "p"
val videoUrl = it.substringAfter("\n").substringBefore("\n")
videosList.add(Video(videoUrl, quality, videoUrl, headers = headers))
}
videosList
}
}
private fun parseCdnMasterPlaylist(url: String): List<Video> {
val videosList = mutableListOf<Video>()
val masterUrlPrefix = url.substringBefore("/ep")
val masterPlaylist = client.newCall(GET(url, headers)).execute().body!!.string()
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:").split("#EXT-X-STREAM-INF:").forEach {
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",") + "p"
val videoUrl = "$masterUrlPrefix/${it.substringAfter("\n").substringBefore("\r").substringBefore("\n")}"
videosList.add(Video(videoUrl, quality, videoUrl, headers = headers))
}
return videosList
}
private fun encodeBase64(string: String): String {
return Base64.encodeToString(string.toByteArray(), Base64.NO_PADDING)
}
private fun decodeBase64(string: String): String {
return Base64.decode(string, Base64.DEFAULT).decodeToString()
}
override fun videoListSelector() = throw Exception("not used")
override fun videoFromElement(element: Element) = throw Exception("not used")
override fun videoUrlParse(document: Document) = throw Exception("not used")
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", "1080")
if (quality != null) {
val newList = mutableListOf<Video>()
var preferred = 0
for (video in this) {
if (video.quality.contains(quality)) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
return newList
}
return this
}
override fun searchAnimeParse(response: Response): AnimesPage {
val responseJson = json.decodeFromString<JsonObject>(response.body!!.string())
val document = Jsoup.parse(responseJson["result"]!!.jsonPrimitive.content)
val animeList = document.select("li")
val animes = animeList.map {
searchAnimeFromElement(it)
}
return AnimesPage(animes, false)
}
override fun searchAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.select("div a").attr("href"))
anime.thumbnail_url = element.select("img").first().attr("src")
anime.title = element.select("p.name a").attr("title")
return anime
}
override fun searchAnimeNextPageSelector(): String = throw Exception("not used")
override fun searchAnimeSelector(): String = throw Exception("not used")
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val formData = FormBody.Builder()
.addEncoded("q2", query)
.addEncoded("root", "animixplay.to")
.addEncoded("origin", "1")
.build()
return when {
query.isNotBlank() -> POST("https://v1.ic5qwh28vwloyjo28qtg.workers.dev/", headers, formData)
else -> GET("$baseUrl/?tab=popular")
}
}
override fun animeDetailsParse(response: Response): SAnime {
val document = if (!response.request.url.toString().contains(".json")) {
getDocumentFromRequestUrl(response)
} else {
response.asJsoup()
}
return animeDetailsParse(document)
}
private fun getDocumentFromRequestUrl(response: Response): Document {
val scriptData = response.asJsoup().select("script:containsData(var malid )").toString()
val malId = scriptData.substringAfter("var malid = '").substringBefore("';")
val url = "https://animixplay.to/assets/mal/$malId.json"
return client.newCall(
GET(
url,
headers
)
).execute().asJsoup()
}
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
val animeJson = json.decodeFromString<JsonObject>(document.select("body").text())
anime.title = animeJson["title"]!!.jsonPrimitive.content
anime.genre =
animeJson["genres"]!!.jsonArray.joinToString { it.jsonObject["name"]!!.jsonPrimitive.content }
anime.description = animeJson["synopsis"]!!.jsonPrimitive.content
anime.status = parseStatus(animeJson["status"]!!.jsonPrimitive.content)
val studiosArray = animeJson["studios"]!!.jsonArray
if (studiosArray.isNotEmpty()) {
anime.author =
studiosArray[0].jsonObject["name"]!!.jsonPrimitive.content
}
return anime
}
private fun parseStatus(statusString: String): Int {
return when (statusString) {
"Currently Airing" -> SAnime.ONGOING
"Finished Airing" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
override fun latestUpdatesNextPageSelector(): String = throw Exception("not used")
override fun latestUpdatesFromElement(element: Element): SAnime = throw Exception("not used")
override fun latestUpdatesRequest(page: Int): Request {
val formBody = FormBody.Builder()
.add("genre", "any")
.add("minstr", latestNextDate)
.add("orderby", "latest")
.build()
return POST("$baseUrl/api/search", headers, body = formBody)
}
override fun latestUpdatesParse(response: Response): AnimesPage {
val document = response.asJsoup()
val responseJson = json.decodeFromString<JsonObject>(document.select("body").text())
latestNextDate = responseJson["last"]!!.jsonPrimitive.content
latestHasNextPage = responseJson["more"]!!.jsonPrimitive.boolean
val animeList = responseJson["result"]!!.jsonArray
val animes = animeList.map { element ->
popularAnimeFromElement(element.jsonObject)
}
return AnimesPage(animes, latestHasNextPage)
}
override fun latestUpdatesSelector(): String = throw Exception("not used")
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p")
entryValues = arrayOf("1080", "720", "480", "360")
setDefaultValue("1080")
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()
}
}
val videoServerPref = ListPreference(screen.context).apply {
key = "preferred_server"
title = "Preferred server"
entries = arrayOf("Vrv/Cdn", "Gogo")
entryValues = arrayOf("vrv", "gogo")
setDefaultValue("vrv")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoQualityPref)
screen.addPreference(videoServerPref)
}
}

View File

@ -1,97 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.animixplay.extractors
import android.util.Base64
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
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 java.lang.Exception
import java.util.Locale
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
@ExperimentalSerializationApi
class GogoCdnExtractor(private val client: OkHttpClient, private val json: Json) {
fun videosFromUrl(serverUrl: String): List<Video> {
try {
val id = serverUrl.toHttpUrl().queryParameter("id") ?: throw Exception("error getting id")
val iv = "31323835363732333833393339383532".decodeHex()
val secretKey = "3235373136353338353232393338333936313634363632323738383333323838".decodeHex()
val encryptedId = try { cryptoHandler(id, iv, secretKey) } catch (e: Exception) { e.message ?: "" }
val jsonResponse = client.newCall(
GET(
"https://gogoplay4.com/encrypt-ajax.php?id=$encryptedId",
Headers.headersOf("X-Requested-With", "XMLHttpRequest")
)
).execute().body!!.string()
val data = json.decodeFromString<JsonObject>(jsonResponse)["data"]!!.jsonPrimitive.content
val decryptedData = cryptoHandler(data, iv, secretKey, 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:").reversed().forEach {
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore("\n") + "p"
val videoUrl = fileURL.substringBeforeLast("/") + "/" + it.substringAfter("\n").substringBefore("\n")
videoList.add(Video(videoUrl, quality, videoUrl))
}
} else array.forEach {
val label = it.jsonObject["label"].toString().toLowerCase(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,
fileURL,
headers = videoHeaders
)
)
else videoList.add(Video(fileURL, label, fileURL, headers = videoHeaders))
}
return videoList.reversed() + 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)
}
}
private fun String.decodeHex(): ByteArray {
check(length % 2 == 0) { "Must have an even length" }
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
}

View File

@ -0,0 +1,13 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'marin.moe'
pkgNameSuffix = 'en.marinmoe'
extClass = '.MarinMoe'
extVersionCode = 1
libVersion = '13'
}
apply from: "$rootDir/common.gradle"

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

View File

@ -0,0 +1,125 @@
package eu.kanade.tachiyomi.animeextension.en.marinmoe
import kotlinx.serialization.Serializable
@Serializable
data class ResponseData(
val props: PropData
) {
@Serializable
data class PropData(
val anime_list: AnimeList
) {
@Serializable
data class AnimeList(
val data: List<Anime>,
val meta: MetaData
) {
@Serializable
data class Anime(
val title: String,
val slug: String,
val cover: String
)
@Serializable
data class MetaData(
val current_page: Int,
val last_page: Int
)
}
}
}
@Serializable
data class AnimeDetails(
val props: DetailsData
) {
@Serializable
data class DetailsData(
val anime: AnimeDetailsData,
val episode_list: EpisodesData
) {
@Serializable
data class AnimeDetailsData(
val title: String,
val cover: String,
val type: InfoType,
val status: InfoType,
val content_rating: InfoType,
val release_date: String,
val description: String,
val genre_list: List<InfoData>,
val production_list: List<InfoData>
) {
@Serializable
data class InfoType(
val id: Int,
val name: String
)
@Serializable
data class InfoData(
val name: String,
)
}
@Serializable
data class EpisodesData(
val data: List<EpisodeData>,
val links: LinksData
) {
@Serializable
data class EpisodeData(
val title: String,
val sort: Float,
val slug: String,
val release_date: String,
)
@Serializable
data class LinksData(
val next: String? = null
)
}
}
}
@Serializable
data class EpisodeData(
val props: PropData
) {
@Serializable
data class PropData(
val video_list: VideoList
) {
@Serializable
data class VideoList(
val data: List<Video>
) {
@Serializable
data class Video(
val title: String,
val sort: Float,
val audio: TrackInfo,
val mirror: List<Track>
) {
@Serializable
data class TrackInfo(
val code: String,
)
@Serializable
data class Track(
val resolution: String,
val code: TrackCode
) {
@Serializable
data class TrackCode(
val file: String
)
}
}
}
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.animeextension.en.tenshimoe
package eu.kanade.tachiyomi.animeextension.en.marinmoe
import android.webkit.CookieManager
import eu.kanade.tachiyomi.network.GET

View File

@ -0,0 +1,332 @@
package eu.kanade.tachiyomi.animeextension.en.marinmoe
import android.annotation.SuppressLint
import android.app.Application
import android.content.SharedPreferences
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.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.lang.Exception
import java.text.SimpleDateFormat
import java.util.Date
import kotlin.collections.ArrayList
class MarinMoe : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "marin.moe"
override val baseUrl = "https://marin.moe"
override val lang = "en"
override val supportsLatest = true
private val json: Json by injectLazy()
private val ddgInterceptor = DdosGuardInterceptor(network.client)
override val client: OkHttpClient = network.client
.newBuilder()
.addInterceptor(ddgInterceptor)
.build()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeParse(response: Response): AnimesPage {
return parseAnime(response)
}
override fun popularAnimeRequest(page: Int): Request {
return GET("$baseUrl/anime?sort=vwk-d&page=$page")
}
// =============================== Latest ===============================
override fun latestUpdatesParse(response: Response): AnimesPage {
return parseAnime(response)
}
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/anime?sort=rel-d&page=$page")
// =============================== Search ===============================
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
val params = MarinMoeFilters.getSearchParameters(filters)
return client.newCall(searchAnimeRequest(page, query, params))
.asObservableSuccess()
.map { response ->
searchAnimeParse(response)
}
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("Not used")
private fun searchAnimeRequest(page: Int, query: String, filters: MarinMoeFilters.FilterSearchParams): Request {
var url = "$baseUrl/anime".toHttpUrlOrNull()!!.newBuilder()
.addQueryParameter("sort", filters.sort)
.addQueryParameter("search", query)
.addQueryParameter("page", page.toString())
.build().toString()
if (filters.type.isNotBlank()) url += "&${filters.type}"
if (filters.status.isNotBlank()) url += "&${filters.status}"
if (filters.contentRating.isNotBlank()) url += "&${filters.contentRating}"
if (filters.genre.isNotBlank()) url += "&${filters.genre}"
if (filters.group.isNotBlank()) url += "&filter[group][0][id]=${filters.group}&filter[group][0][opr]=include"
if (filters.studio.isNotBlank()) url += "&filter[production][0][id]=${filters.studio}&filter[production][0][opr]=include"
return GET(url, headers = headers)
}
override fun searchAnimeParse(response: Response): AnimesPage {
return parseAnime(response)
}
override fun getFilterList(): AnimeFilterList = MarinMoeFilters.filterList
// =========================== Anime Details ============================
override fun animeDetailsParse(response: Response): SAnime {
val document = response.asJsoup()
val anime = SAnime.create()
val dataPage = document.select("div#app").attr("data-page").replace("&quot;", "\"")
val details = json.decodeFromString<AnimeDetails>(dataPage).props.anime
anime.thumbnail_url = details.cover
anime.title = details.title
anime.genre = details.genre_list.joinToString(", ") { it.name }
anime.author = details.production_list.joinToString(", ") { it.name }
anime.status = parseStatus(details.status.name)
var description = Jsoup.parse(
details.description.replace("<br />", "br2n")
).text().replace("br2n", "\n") + "\n"
description += "\nContent Rating: ${details.content_rating.name}"
description += "\nRelease Date: ${details.release_date}"
description += "\nType: ${details.type.name}"
anime.description = description
return anime
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val episodes = mutableListOf<SEpisode>()
val document = response.asJsoup()
val dataPage = document.select("div#app").attr("data-page").replace("&quot;", "\"")
val dataJson = json.decodeFromString<AnimeDetails>(dataPage)
dataJson.props.episode_list.data.forEach {
val episode = SEpisode.create()
episode.name = "Episode ${it.slug} ${it.title}"
episode.episode_number = it.sort
episode.url = "${response.request.url}/${it.slug}"
val parsedDate = parseDate(it.release_date)
if (parsedDate.time != -1L) episode.date_upload = parsedDate.time
episodes.add(episode)
}
var next = dataJson.props.episode_list.links.next
while (next != null) {
val nextDocument = client.newCall(GET(next, headers = headers)).execute().asJsoup()
val nextDataPage = nextDocument.select("div#app").attr("data-page").replace("&quot;", "\"")
val nextDataJson = json.decodeFromString<AnimeDetails>(nextDataPage)
nextDataJson.props.episode_list.data.forEach {
val episode = SEpisode.create()
episode.name = "Episode ${it.slug} ${it.title}"
episode.episode_number = it.sort
episode.url = "${response.request.url}/${it.slug}"
val parsedDate = parseDate(it.release_date)
if (parsedDate.time != -1L) episode.date_upload = parsedDate.time
episodes.add(episode)
}
next = nextDataJson.props.episode_list.links.next
}
return episodes.sortedBy { it.episode_number }.reversed()
}
// ============================ Video Links =============================
override fun videoListRequest(episode: SEpisode): Request {
return GET(episode.url, headers = headers)
}
override fun videoListParse(response: Response): List<Video> {
val videoList = mutableListOf<Pair<Video, Float>>()
val document = response.asJsoup()
val dataPage = document.select("div#app").attr("data-page").replace("&quot;", "\"")
val videos = json.decodeFromString<EpisodeData>(dataPage).props.video_list.data
for (src in videos) {
for (link in src.mirror) {
videoList.add(
Pair(
Video(
link.code.file,
"${src.title} ${link.resolution} (${if (src.audio.code == "jp") "Sub" else "Dub"})",
link.code.file,
headers = headers
),
src.sort
)
)
}
}
return prioritySort(videoList)
}
// ============================= Utilities ==============================
private fun prioritySort(pList: List<Pair<Video, Float>>): List<Video> {
val prefGroup = preferences.getString("preferred_group", "site_default")!!
val quality = preferences.getString("preferred_quality", "1080")!!
val subOrDub = preferences.getString("preferred_sub", "sub")!!
return pList.sortedWith(
compareBy(
{ it.first.quality.lowercase().contains(subOrDub) },
{ it.first.quality.contains(quality) },
{ if (prefGroup == "site_default") -it.second else it.first.quality.contains(prefGroup) },
)
).reversed().map { t -> t.first }
}
private fun parseAnime(response: Response): AnimesPage {
val document = response.asJsoup()
val dataPage = document.select("div#app").attr("data-page").replace("&quot;", "\"")
val dataJson = json.decodeFromString<ResponseData>(dataPage)
val animes = dataJson.props.anime_list.data.map { ani ->
SAnime.create().apply {
title = ani.title
thumbnail_url = ani.cover
url = "/anime/${ani.slug}"
}
}
val hasNextPage = dataJson.props.anime_list.meta.current_page < dataJson.props.anime_list.meta.last_page
return AnimesPage(animes, hasNextPage)
}
private fun parseStatus(statusString: String): Int {
return when (statusString) {
"Ongoing" -> SAnime.ONGOING
"Completed" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
@SuppressLint("SimpleDateFormat")
private fun parseDate(date: String): Date {
val knownPatterns: MutableList<SimpleDateFormat> = ArrayList()
knownPatterns.add(SimpleDateFormat("dd'th of 'MMM, yyyy"))
knownPatterns.add(SimpleDateFormat("dd'nd of 'MMM, yyyy"))
knownPatterns.add(SimpleDateFormat("dd'st of 'MMM, yyyy"))
knownPatterns.add(SimpleDateFormat("dd'rd of 'MMM, yyyy"))
for (pattern in knownPatterns) {
try {
// Take a try
return Date(pattern.parse(date)!!.time)
} catch (e: Throwable) {
// Loop on
}
}
return Date(-1L)
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p")
entryValues = arrayOf("1080", "720", "480", "360")
setDefaultValue("1080")
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()
}
}
val groupPref = ListPreference(screen.context).apply {
key = "preferred_group"
title = "Preferred group"
entries = MarinMoeConstants.groupEntries
entryValues = MarinMoeConstants.groupEntryValues
setDefaultValue("site_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()
}
}
val subPref = ListPreference(screen.context).apply {
key = "preferred_sub"
title = "Prefer subs or dubs?"
entries = arrayOf("Subs", "Dubs")
entryValues = arrayOf("sub", "dub")
setDefaultValue("sub")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoQualityPref)
screen.addPreference(groupPref)
screen.addPreference(subPref)
}
}

View File

@ -0,0 +1,89 @@
package eu.kanade.tachiyomi.animeextension.en.marinmoe
object MarinMoeConstants {
val groupEntries = arrayOf(
"Site Default", "-__-`", "-.-", "Abstinence", "AC_", "ADZ Subs", "Afternoon Naps Empire", "Almighty", "Alt",
"Anime Noobs Fansubbers", "Anime-4ever", "Anime-Conan", "Anime-Destiny", "Anime-Empire", "Anime-FanRips",
"Anime-Fury", "Anime-Jiyuu", "Anime-Supreme", "anime4life.", "AnimeBytes Remuxes", "AnimeOne", "AnimeSenshi Subs",
"AnimeYuki", "Anonymous", "Anonymous Russian Rippers", "AOmundson", "ARC", "Aria", "Arid", "aRMX", "aro",
"Asakura", "asc3nsi0n", "Asteroid", "AtlasSubbed", "Ayashii", "AZ Fansubs", "AZ Fansubs & Baka-Gaijin", "B2E",
"Baaro", "Baka-Gaijin & Detective Conan Files", "BakaBT Remuxes", "Bakahorizonsama", "BananaBoat", "Beatrice-Raws",
"Black Organization", "BluDragon", "BlurayDesuYo", "bradosia", "Bunny Hat Raw", "C137", "Cait-Sidhe",
"Cartoon-World", "Catar", "CBT", "Chihiro-Fansubs", "Chosensilver Remux", "Chotab", "CitadelofTheRaven", "Clara",
"Cman21", "Coalgirls", "Cold Fusion", "Cold Fusion & Baaro", "coldhell", "ColorMeSubbed", "Commie", "Commsei",
"cornbreadman", "Crow", "Cry", "CsS", "CUNNY", "D.O.M.O.", "DameDesuYo", "DarkDream", "darkfire68",
"Darkonius & wisperer", "Datte13", "deanzel", "Detective Conan Translation Project",
"Detective Conan Translation Project & AZ Fansubs", "Detective Conan Translation Project & Kienai-Niji",
"Detective Conan Translation Project & NazoNoKami", "Detective Conan Translation Project & The Moonlighters",
"Deus EX Anima", "DigitalPanic", "DigitalSubs", "Dknight", "DmonHiro", "Doki Fansubs",
"Doki Fansubs & Chihiro Fansubs", "DorianHD", "DorianHD & FoxesDen", "Doutei Fansubs", "Drag", "drawn-reality",
"Dual Audio Empire", "EightBit", "Elysium Subs", "EMBER", "Enceladus", "EncoderAnon", "Encodergasm", "entameSubs",
"Erai-raws", "Exiled-Destiny", "ExoDus", "Explorer", "eXterminator", "Fansubber-ES", "FFFpeeps",
"FFFpeeps & Interrobang", "FFFpeeps & Kantai-Subs", "FFFpeeps & Vivid Subs", "Final8", "Flamwenco", "FLUX",
"Frostii & AnimeONE", "Funnuraba Fansubs", "Galator", "Gekkostate", "GHOST", "ghostosenpai", "Glenn", "Glue",
"Godim", "Godless-Orphan", "Golosubs", "Golumpa", "good", "Good Job! Media", "Good Job! Media & Kaleido-subs",
"GotWoot Fansubs", "Grim Ripper", "Grim-F", "gskumar", "GST", "GUDish", "HachiRokuNiSanKyu", "Hari-No-Ito",
"Hark0n", "Harunatsu Fansubs", "hchcsen", "High Quality Releases", "Holomux", "HorribleSubs", "hose", "Hotaru",
"iAHD", "If you don`t download this I will cry", "IK-User", "iKaos", "Inka-Subs", "InoBestGirl", "Inverti", "ITH",
"Iznjie Biznjie", "jackie", "joseole99", "JPSDR", "Judgment", "Jumonji-Giri", "JustAnotherRemux", "JySzE",
"KaizouFansubs", "Kaleido-subs", "Kaleido-subs & Flax Subs", "Kaleido-subs & Scyrous", "Kametsu", "Kantai-Subs",
"karios", "Karma Rips", "Kawaii", "Kawaiika-Raws", "Kaze no Koe Fansubs", "kBaraka", "KH", "Kienai",
"Kienai-Niji & AZ FAnsubs", "Kira_SEV", "Kira-Fansub", "Kiriya", "KiteSeekers & Kira-Fansub",
"KiteSeekers & Licca Fansubs", "KOSMOS", "Koten Gars", "KOTEX", "kuchikirukia", "Kulot", "Kyozoku", "Laidback",
"Last Resort Encodes", "Late Night Snack", "Lazy Lily Subs", "Legion", "Lisata598", "Live-eviL", "LostYears",
"Lulu", "MaskedCape", "Mazui", "MeruMeruSubs", "MetalicBox", "Metaljerk", "Mew", "Migoto Fansubs", "Moodkiller",
"Moodkiller & Scyrous", "Moozzi2", "motbob", "mottoj-anime", "MoyaiSubs", "Mysteria", "NanDesuKa?", "NazoNoKami",
"NazoNoKami & KaizouFansubs", "NC-Raws", "neko-raws", "NekomimiTeikoku", "neo1024", "Nep Blanc", "Netaro",
"NetflixSucks", "NeutralHatred", "nielsen145", "nImEHuntetD", "Nineball", "Nofunloli", "Noms", "NovaWorks",
"Nuke Fansubs", "Nyanpasu Subs", "Okay-Subs", "Orphan", "Orphan & macros74", "owowo", "oZanderr", "OZC Anime",
"Patjantess", "Pineapple Salad", "PiPoPaHd", "pls", "Pog42", "Polished Subs", "pooookie", "Puto", "pyroneko",
"Quetzal", "Quickie", "Raizel", "Rakuda", "RASETSU", "Real-Anime Xtreme", "ReDone-Subs", "ReinForce",
"Reito`s Rockin Rips", "RemuxBobP", "Retrofit", "rickyhorror", "Rising Sun", "RMX", "Robonation", "Rom & Rem",
"Saizen Fansubs", "Saizen-Fansubs & Ignition-One", "SakuraCircle", "SallySubs", "samaritan", "SchwingBoner",
"Scriptum", "Scyrous", "Sea of Serenity Subs", "Serenae", "ShadyCrab", "Shimatta", "Shindou-Anime",
"Shining Fansubs", "ShiraRaminz", "Shirase", "Shirσ", "Shisukon", "Smoke", "smoon", "SmugCat", "SmugMug", "SNSbu",
"Solar Fansubs", "solaufein", "SSP-Corp", "Static-Subs & AnimeYuki", "Static-Subs & Eclipse Productions",
"SubsPlease", "Suiri Otaku", "Sushi Fansubs", "sxales", "Tamaya", "Tap24", "The Moonlighters", "The0x539",
"Thighs", "Tipota Encodes", "tlacatlc6", "ToTheGlory Anime", "Tsundere Rips", "TV-Nihon", "Underwater",
"Unlimited Translation Works", "Unlimited Translation Works & Mazui", "unwanteddubfan", "Ureshii", "Vanilla",
"Virus123", "Vivid Subs", "Vodes", "Waku", "Wasurenai Fansubs", "Wasurenai Fansubs & Anime-Himitsu", "WAVE",
"WhoBeDaPlaya", "WhyNot?", "WIP", "Wyse", "Xanth", "xCryptic", "xKrptonicz", "Xonline", "xPearse", "xxon", "YA",
"Yameii", "Yami-Fansubs", "YamiWheeler", "Yellow-Flash", "Yousoro", "YURI", "Z4ST1N", "zangafan", "ZetaRebel",
"Zurako Subs"
)
val groupEntryValues = arrayOf(
"site_default", "-__-`", "-.-", "Abstinence", "AC", "ADZ", "ANE", "Almighty", "Alt", "anfs", "a4e", "AConan", "A-Destiny", "A-E",
"A-FanRips", "A-Fury", "Ani-Jiyuu", "a-S", "anime4life.", "AB-RMX", "AonE", "Asenshi", "AnY", "None", "ARR",
"AOmundson", "ARC", "Aria", "Arid", "None", "aro", "Asakura", "ASC", "Asteroid", "AtlasSubbed", "Ayashii", "AZFS",
"AZFS-BG", "B2E", "Baaro", "BG-DCF", "BBT-RMX", "BHS", "BananaBoat", "Beatrice-Raws", "B-Org", "BluDragon", "None",
"ASO", "Bunny Hat Raw", "C137", "Cait-Sidhe", "C-W", "CTR", "CBT", "Chihiro", "CsRmX", "Chotab", "COR", "Clara",
"Cman", "Coalgirls", "ColdFusion", "CF&B", "None", "CMS", "Commie", "Commsei", "CBM", "Crow", "Cry", "None",
"CUNNY", "DOMO", "DameDesuYo", "DarkDream", "df68", "DarkWispers", "Datte13", "deanzel", "DCTP", "DCTP-AZFS",
"DCTP-Kienai", "DCTP-Neg", "DCTP-M-L", "DEXA", "dp", "Digital", "DKnt", "None", "Doki", "Doki-Chihiro", "DHD",
"DHD-FD", "Doutei", "Drag", "None", "DAE", "EightBit", "Elysium", "EMBER", "Enceladus", "EncAnon", "EG",
"entameSubs", "Erai-raws", "E-D", "eXo", "Exp", "eXterminator", "EUR", "FFF", "FBI", "FFF-Kantai", "FFF-Vivid",
"Final8", "Flamwenco", "FLUX", "Frostii_AonE", "Funnuraba", "Galator", "GS", "GHOST", "GHS", "Glenn", "Glue",
"Godim", "None", "Golo", "Golumpa", "None", "GJM", "GJM-Kaleido", "GotWoot", "GrimRipper", "grimf", "GSK_kun",
"GST", "GUDish", "HachiRoku", "HnI", "Hark0n", "Harunatsu", "hchcsen", "HQR", "Holomux", "None", "hose", "None",
"iAHD", "DownloadThisOrIWillCry", "IK", "iKaos", "Inka-Subs", "InoBestGirl", "Inverti", "ITH", "Iznjie Biznjie",
"jackie", "joseole99", "JPSDR", "Judgment", "J-G", "JAR", "JySzE", "Kaizou", "Kaleido-subs", "Kaleido-Flax",
"Kaleido-SCY", "Kametsu", "Kantai", "karios", "None", "None", "Kawaiika-Raws", "KnKF", "kBaraka", "KH", "Kienai",
"Kienai-AZFS", "SEV", "Kira-Fansub", "Kiriya", "KiteSeekers-Kira", "KiteSeekers-Licca", "KOSMOS", "Koten_Gars",
"None", "kuchikirukia", "Kulot", "Kyo", "None", "LRE", "None", "Lazy Lily", "Legion", "Lisata598", "L-E",
"LostYears", "None", "MC", "Mazui", "MeruMeruSubs", "Metal", "Metaljerk", "Mew", "Migoto", "MK", "MK-SCY",
"Moozzi2", "MTBB", "None", "MoyaiSubs", "Mysteria", "NanDesuKa", "Neg", "Neg & Kaizou", "NC-Raws", "neko-raws",
"NMTK", "neo1024", "Nep_Blanc", "Netaro", "NetflixSucks", "NH", "nielsen145", "nImEHuntetD", "9ball", "Nofunloli",
"Noms", "NovaWorks", "Nuke", "Nyanpasu", "Okay-Subs", "Orphan", "Orphan-M74", "OWO", "OZR", "OZC", "Patjantess",
"Pineapple Salad", "PiPoPaHd", "pls", "Pog42", "polished", "Pookie", "Puto", "None", "Quetzal", "Quickie",
"Raizel", "Rakuda", "RASETSU", "RaX", "ReDone", "ReinForce", "3xR", "BobP", "Retrofit", "RH", "Rising Sun", "None",
"Rn", "Rom & Rem", "Saizen", "SZN & I-O", "None", "None", "sam", "SchwingBoner", "Scriptum", "SCY", "SOSSubs",
"Serenae", "ShadyCrab", "Shimatta", "S-A", "Shi-Fa", "ShiraRaminz", "Shirase", "Shirσ", "Shisukon", "None",
"smoon", "SmugCat", "SmugMug", "None", "Solar", "SOLA", "SSP-Corp", "SS-AnY", "SS-Eclipse", "SubsPlease",
"SuiriOtaku", "Sushi", "sxales", "Tamaya", "Tap24", "M-L", "The0x539", "Thighs", "tipota", "None", "TTGA",
"Tsundere", "T-N", "Underwater", "UTW", "UTW-Mazui", "UDF", "Ureshii", "Vanilla", "Virus123", "Vivid", "Vodes",
"Waku", "Wasurenai", "Wasurenai-Himitsu", "WAVE", "WBDP", "WhyNot", "WIP", "WSE", "Xanth", "CyC", "KRP", "Xonline",
"xPearse", "xxon", "YA", "Yameii", "Yami", "None", "Yellow-Flash", "Yousoro", "YURI", "Z4ST1N", "zang",
"ZetaRebel", "Zurako"
)
}

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.animeextension" />

View File

@ -1,12 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'tenshi.moe'
pkgNameSuffix = 'en.tenshimoe'
extClass = '.TenshiMoe'
extVersionCode = 25
libVersion = '13'
}
apply from: "$rootDir/common.gradle"

View File

@ -1,234 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.tenshimoe
import android.annotation.SuppressLint
import android.app.Application
import android.content.SharedPreferences
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 okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
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 java.lang.Exception
import java.lang.Float.parseFloat
import java.text.SimpleDateFormat
import java.util.Date
import kotlin.collections.ArrayList
class TenshiMoe : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "tenshi.moe"
override val baseUrl = "https://tenshi.moe"
override val lang = "en"
override val supportsLatest = true
private val ddgInterceptor = DdosGuardInterceptor(network.client)
override val client: OkHttpClient = network.client
.newBuilder()
.addInterceptor(ddgInterceptor)
.build()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun popularAnimeSelector(): String = "ul.anime-loop.loop li a"
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/anime?s=vdy-d&page=$page")
override fun popularAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.attr("href") + "?s=srt-d")
anime.title = element.select("div span").not(".badge").text()
return anime
}
override fun popularAnimeNextPageSelector(): String = "ul.pagination li.page-item a[rel=next]"
override fun episodeListSelector() = "ul.episode-loop li a"
private fun episodeNextPageSelector() = popularAnimeNextPageSelector()
override fun episodeListParse(response: Response): List<SEpisode> {
val episodes = mutableListOf<SEpisode>()
fun addEpisodes(document: Document) {
document.select(episodeListSelector()).map { episodes.add(episodeFromElement(it)) }
document.select(episodeNextPageSelector()).firstOrNull()
?.let { addEpisodes(client.newCall(GET(it.attr("href"), headers)).execute().asJsoup()) }
}
addEpisodes(response.asJsoup())
return episodes
}
override fun episodeFromElement(element: Element): SEpisode {
val episode = SEpisode.create()
episode.setUrlWithoutDomain(element.attr("href"))
val episodeNumberString = element.select("div.episode-number").text().removePrefix("Episode ")
var numeric = true
try {
parseFloat(episodeNumberString)
} catch (e: NumberFormatException) {
numeric = false
}
episode.episode_number = if (numeric) episodeNumberString.toFloat() else element.parent().className().removePrefix("episode").toFloat()
episode.name = element.select("div.episode-number").text() + ": " + element.select("div.episode-label").text() + element.select("div.episode-title").text()
val date: String = element.select("div.date").text()
val parsedDate = parseDate(date)
if (parsedDate.time != -1L) episode.date_upload = parsedDate.time
return episode
}
@SuppressLint("SimpleDateFormat")
private fun parseDate(date: String): Date {
val knownPatterns: MutableList<SimpleDateFormat> = ArrayList()
knownPatterns.add(SimpleDateFormat("dd'th of 'MMM, yyyy"))
knownPatterns.add(SimpleDateFormat("dd'nd of 'MMM, yyyy"))
knownPatterns.add(SimpleDateFormat("dd'st of 'MMM, yyyy"))
knownPatterns.add(SimpleDateFormat("dd'rd of 'MMM, yyyy"))
for (pattern in knownPatterns) {
try {
// Take a try
return Date(pattern.parse(date)!!.time)
} catch (e: Throwable) {
// Loop on
}
}
return Date(-1L)
}
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val iframe = document.selectFirst("iframe").attr("src")
val referer = response.request.url.toString()
val refererHeaders = Headers.headersOf("referer", referer)
val iframeResponse = client.newCall(GET(iframe, refererHeaders))
.execute().asJsoup()
return videosFromElement(iframeResponse.selectFirst(videoListSelector()))
}
override fun videoListSelector() = "script:containsData(source)"
private fun videosFromElement(element: Element): List<Video> {
val data = element.data().substringAfter("sources: [").substringBefore("],")
val sources = data.split("src: '").drop(1)
val videoList = mutableListOf<Video>()
for (source in sources) {
val src = source.substringBefore("'")
val size = source.substringAfter("size: ").substringBefore(",")
val cookie = ddgInterceptor.getNewCookie(src.toHttpUrl())?.value ?: ""
val video = Video(
src,
size + "p",
src,
headers = Headers.headersOf("cookie", "__ddg2_=$cookie"),
)
videoList.add(video)
}
return videoList
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", null)
if (quality != null) {
val newList = mutableListOf<Video>()
var preferred = 0
for (video in this) {
if (video.quality.contains(quality)) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
return newList
}
return this
}
override fun videoFromElement(element: Element) = throw Exception("not used")
override fun videoUrlParse(document: Document) = throw Exception("not used")
override fun searchAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.attr("href") + "?s=srt-d")
anime.title = element.select("div span.thumb-title, div span.text-primary").text()
return anime
}
override fun searchAnimeNextPageSelector(): String = "ul.pagination li.page-item a[rel=next]"
override fun searchAnimeSelector(): String = "ul.anime-loop.loop li a"
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = GET("$baseUrl/anime?q=$query")
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
anime.thumbnail_url = document.select("img.cover-image.img-thumbnail").first().attr("src")
anime.title = document.select("li.breadcrumb-item.active").text()
anime.genre = document.select("li.genre span.value").joinToString(", ") { it.text() }
anime.description = document.select("div.card-body").text()
anime.author = document.select("li.production span.value").joinToString(", ") { it.text() }
anime.status = parseStatus(document.select("li.status span.value").text())
return anime
}
private fun parseStatus(statusString: String): Int {
return when (statusString) {
"Ongoing" -> SAnime.ONGOING
"Completed" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
override fun latestUpdatesNextPageSelector(): String = "ul.pagination li.page-item a[rel=next]"
override fun latestUpdatesFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.attr("href") + "?s=srt-d")
anime.title = element.select("div span").not(".badge").text()
return anime
}
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/anime?s=rel-d&page=$page")
override fun latestUpdatesSelector(): String = "ul.anime-loop.loop li a"
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p")
entryValues = arrayOf("1080", "720", "480", "360")
setDefaultValue("1080")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoQualityPref)
}
}