Add extension: Marinmoe (#1164)
Closes https://github.com/jmir1/aniyomi-extensions/issues/1161
@ -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"
|
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 10 KiB |
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
13
src/en/marinmoe/build.gradle
Normal 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"
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
@ -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(""", "\"")
|
||||
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(""", "\"")
|
||||
|
||||
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(""", "\"")
|
||||
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(""", "\"")
|
||||
|
||||
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(""", "\"")
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
@ -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"
|
||||
)
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="eu.kanade.tachiyomi.animeextension" />
|
@ -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"
|
@ -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)
|
||||
}
|
||||
}
|