add 9anime & update kotlin

This commit is contained in:
jmir1 2022-02-28 10:30:57 +01:00
parent 5a6766c531
commit ea9bea76f8
15 changed files with 887 additions and 424 deletions

View File

@ -1,6 +1,6 @@
buildscript {
ext.kotlin_version = '1.4.32'
ext.coroutines_version = '1.4.3'
ext.kotlin_version = '1.6.10'
ext.coroutines_version = '1.6.0'
repositories {
mavenCentral()
google()
@ -10,7 +10,7 @@ buildscript {
classpath 'com.android.tools.build:gradle:4.2.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath 'org.jmailen.gradle:kotlinter-gradle:3.3.0'
classpath 'org.jmailen.gradle:kotlinter-gradle:3.6.0'
}
}

View File

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

View File

@ -1,274 +1,274 @@
package eu.kanade.tachiyomi.animeextension.en.gogoanime
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.en.gogoanime.extractors.DoodExtractor
import eu.kanade.tachiyomi.animeextension.en.gogoanime.extractors.GogoCdnExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
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 uy.kohesive.injekt.injectLazy
import java.lang.Exception
@ExperimentalSerializationApi
class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Gogoanime"
override val baseUrl = "https://gogoanime.fi"
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)
}
override fun popularAnimeSelector(): String = "div.img a"
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/popular.html?page=$page")
override fun popularAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.attr("href"))
anime.thumbnail_url = element.select("img").first().attr("src")
anime.title = element.attr("title")
return anime
}
override fun popularAnimeNextPageSelector(): String = "ul.pagination-list li:last-child:not(.selected)"
override fun episodeListSelector() = "ul#episode_page li a"
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
val totalEpisodes = document.select(episodeListSelector()).last().attr("ep_end")
val id = document.select("input#movie_id").attr("value")
return episodesRequest(totalEpisodes, id)
}
private fun episodesRequest(totalEpisodes: String, id: String): List<SEpisode> {
val request = GET("https://ajax.gogo-load.com/ajax/load-list-episode?ep_start=0&ep_end=$totalEpisodes&id=$id", headers)
val epResponse = client.newCall(request).execute()
val document = epResponse.asJsoup()
return document.select("a").map { episodeFromElement(it) }
}
override fun episodeFromElement(element: Element): SEpisode {
val episode = SEpisode.create()
episode.setUrlWithoutDomain(baseUrl + element.attr("href").substringAfter(" "))
val ep = element.selectFirst("div.name").ownText().substringAfter(" ")
episode.episode_number = ep.toFloat()
episode.name = "Episode $ep"
episode.date_upload = System.currentTimeMillis()
return episode
}
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val serverUrl = "https:" + document.select("div.anime_muti_link > ul > li.anime > a")
.attr("data-video")
val doodUrl = document.select("div.anime_muti_link > ul > li.doodstream > a")
.attr("data-video")
val gogoVideos = GogoCdnExtractor(client, json).videosFromUrl(serverUrl)
return gogoVideos.ifEmpty {
DoodExtractor(client).videosFromUrl(doodUrl)
}
}
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 searchAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.attr("href"))
anime.thumbnail_url = element.select("img").first().attr("src")
anime.title = element.attr("title")
return anime
}
override fun searchAnimeNextPageSelector(): String = "ul.pagination-list li:last-child:not(.selected)"
override fun searchAnimeSelector(): String = "div.img a"
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val filterList = if (filters.isEmpty()) getFilterList() else filters
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
return when {
query.isNotBlank() -> GET("$baseUrl/search.html?keyword=$query&page=$page", headers)
genreFilter.state != 0 -> GET("$baseUrl/genre/${genreFilter.toUriPart()}?page=$page")
else -> GET("$baseUrl/popular.html?page=$page")
}
}
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
anime.title = document.select("div.anime_info_body_bg h1").text()
anime.genre = document.select("p.type:eq(5) a").joinToString("") { it.text() }
anime.description = document.select("p.type:eq(4)").first().ownText()
anime.status = parseStatus(document.select("p.type:eq(7) a").text())
// add alternative name to anime description
val altName = "Other name(s): "
document.select("p.type:eq(8)").firstOrNull()?.ownText()?.let {
if (it.isBlank().not()) {
anime.description = when {
anime.description.isNullOrBlank() -> altName + it
else -> anime.description + "\n\n$altName" + it
}
}
}
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-list li:last-child:not(.selected)"
override fun latestUpdatesFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(baseUrl + element.attr("href"))
val style = element.select("div.thumbnail-popular").attr("style")
anime.thumbnail_url = style.substringAfter("background: url('").substringBefore("');")
anime.title = element.attr("title")
return anime
}
override fun latestUpdatesRequest(page: Int): Request =
GET("https://ajax.gogo-load.com/ajax/page-recent-release-ongoing.html?page=$page&type=1", headers)
override fun latestUpdatesSelector(): String = "div.added_series_body.popular li a:has(div)"
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)
}
// Filters
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("Text search ignores filters"),
GenreFilter()
)
private class GenreFilter : UriPartFilter(
"Genres",
arrayOf(
Pair("<select>", ""),
Pair("Action", "action"),
Pair("Adventure", "adventure"),
Pair("Cars", "cars"),
Pair("Comedy", "comedy"),
Pair("Crime", "crime"),
Pair("Dementia", "dementia"),
Pair("Demons", "demons"),
Pair("Drama", "drama"),
Pair("Dub", "dub"),
Pair("Ecchi", "ecchi"),
Pair("Family", "family"),
Pair("Fantasy", "fantasy"),
Pair("Game", "game"),
Pair("Harem", "harem"),
Pair("Historical", "historical"),
Pair("Horror", "horror"),
Pair("Josei", "josei"),
Pair("Kids", "kids"),
Pair("Magic", "magic"),
Pair("Martial Arts", "martial-arts"),
Pair("Mature", "mature"),
Pair("Mecha", "mecha"),
Pair("Military", "military"),
Pair("Music", "music"),
Pair("Mystery", "mystery"),
Pair("Parody", "parody"),
Pair("Police", "police"),
Pair("Psychological", "psychological"),
Pair("Romance", "romance"),
Pair("Samurai", "samurai"),
Pair("School", "school"),
Pair("Sci-Fi", "sci-fi"),
Pair("Seinen", "seinen"),
Pair("Shoujo", "shoujo"),
Pair("Shoujo Ai", "shoujo-ai"),
Pair("Shounen", "shounen"),
Pair("Shounen Ai", "shounen-ai"),
Pair("Slice of Life", "slice-of-life"),
Pair("Space", "space"),
Pair("Sports", "sports"),
Pair("Super Power", "super-power"),
Pair("Supernatural", "supernatural"),
Pair("Thriller", "thriller"),
Pair("Vampire", "vampire"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri")
)
)
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
}
package eu.kanade.tachiyomi.animeextension.en.gogoanime
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.en.gogoanime.extractors.DoodExtractor
import eu.kanade.tachiyomi.animeextension.en.gogoanime.extractors.GogoCdnExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
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 uy.kohesive.injekt.injectLazy
import java.lang.Exception
@ExperimentalSerializationApi
class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Gogoanime"
override val baseUrl = "https://gogoanime.fi"
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)
}
override fun popularAnimeSelector(): String = "div.img a"
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/popular.html?page=$page")
override fun popularAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.attr("href"))
anime.thumbnail_url = element.select("img").first().attr("src")
anime.title = element.attr("title")
return anime
}
override fun popularAnimeNextPageSelector(): String = "ul.pagination-list li:last-child:not(.selected)"
override fun episodeListSelector() = "ul#episode_page li a"
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
val totalEpisodes = document.select(episodeListSelector()).last().attr("ep_end")
val id = document.select("input#movie_id").attr("value")
return episodesRequest(totalEpisodes, id)
}
private fun episodesRequest(totalEpisodes: String, id: String): List<SEpisode> {
val request = GET("https://ajax.gogo-load.com/ajax/load-list-episode?ep_start=0&ep_end=$totalEpisodes&id=$id", headers)
val epResponse = client.newCall(request).execute()
val document = epResponse.asJsoup()
return document.select("a").map { episodeFromElement(it) }
}
override fun episodeFromElement(element: Element): SEpisode {
val episode = SEpisode.create()
episode.setUrlWithoutDomain(baseUrl + element.attr("href").substringAfter(" "))
val ep = element.selectFirst("div.name").ownText().substringAfter(" ")
episode.episode_number = ep.toFloat()
episode.name = "Episode $ep"
episode.date_upload = System.currentTimeMillis()
return episode
}
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val serverUrl = "https:" + document.select("div.anime_muti_link > ul > li.anime > a")
.attr("data-video")
val doodUrl = document.select("div.anime_muti_link > ul > li.doodstream > a")
.attr("data-video")
val gogoVideos = GogoCdnExtractor(client, json).videosFromUrl(serverUrl)
return gogoVideos.ifEmpty {
DoodExtractor(client).videosFromUrl(doodUrl)
}
}
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 searchAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.attr("href"))
anime.thumbnail_url = element.select("img").first().attr("src")
anime.title = element.attr("title")
return anime
}
override fun searchAnimeNextPageSelector(): String = "ul.pagination-list li:last-child:not(.selected)"
override fun searchAnimeSelector(): String = "div.img a"
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val filterList = if (filters.isEmpty()) getFilterList() else filters
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
return when {
query.isNotBlank() -> GET("$baseUrl/search.html?keyword=$query&page=$page", headers)
genreFilter.state != 0 -> GET("$baseUrl/genre/${genreFilter.toUriPart()}?page=$page")
else -> GET("$baseUrl/popular.html?page=$page")
}
}
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
anime.title = document.select("div.anime_info_body_bg h1").text()
anime.genre = document.select("p.type:eq(5) a").joinToString("") { it.text() }
anime.description = document.select("p.type:eq(4)").first().ownText()
anime.status = parseStatus(document.select("p.type:eq(7) a").text())
// add alternative name to anime description
val altName = "Other name(s): "
document.select("p.type:eq(8)").firstOrNull()?.ownText()?.let {
if (it.isBlank().not()) {
anime.description = when {
anime.description.isNullOrBlank() -> altName + it
else -> anime.description + "\n\n$altName" + it
}
}
}
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-list li:last-child:not(.selected)"
override fun latestUpdatesFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(baseUrl + element.attr("href"))
val style = element.select("div.thumbnail-popular").attr("style")
anime.thumbnail_url = style.substringAfter("background: url('").substringBefore("');")
anime.title = element.attr("title")
return anime
}
override fun latestUpdatesRequest(page: Int): Request =
GET("https://ajax.gogo-load.com/ajax/page-recent-release-ongoing.html?page=$page&type=1", headers)
override fun latestUpdatesSelector(): String = "div.added_series_body.popular li a:has(div)"
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)
}
// Filters
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("Text search ignores filters"),
GenreFilter()
)
private class GenreFilter : UriPartFilter(
"Genres",
arrayOf(
Pair("<select>", ""),
Pair("Action", "action"),
Pair("Adventure", "adventure"),
Pair("Cars", "cars"),
Pair("Comedy", "comedy"),
Pair("Crime", "crime"),
Pair("Dementia", "dementia"),
Pair("Demons", "demons"),
Pair("Drama", "drama"),
Pair("Dub", "dub"),
Pair("Ecchi", "ecchi"),
Pair("Family", "family"),
Pair("Fantasy", "fantasy"),
Pair("Game", "game"),
Pair("Harem", "harem"),
Pair("Historical", "historical"),
Pair("Horror", "horror"),
Pair("Josei", "josei"),
Pair("Kids", "kids"),
Pair("Magic", "magic"),
Pair("Martial Arts", "martial-arts"),
Pair("Mature", "mature"),
Pair("Mecha", "mecha"),
Pair("Military", "military"),
Pair("Music", "music"),
Pair("Mystery", "mystery"),
Pair("Parody", "parody"),
Pair("Police", "police"),
Pair("Psychological", "psychological"),
Pair("Romance", "romance"),
Pair("Samurai", "samurai"),
Pair("School", "school"),
Pair("Sci-Fi", "sci-fi"),
Pair("Seinen", "seinen"),
Pair("Shoujo", "shoujo"),
Pair("Shoujo Ai", "shoujo-ai"),
Pair("Shounen", "shounen"),
Pair("Shounen Ai", "shounen-ai"),
Pair("Slice of Life", "slice-of-life"),
Pair("Space", "space"),
Pair("Sports", "sports"),
Pair("Super Power", "super-power"),
Pair("Supernatural", "supernatural"),
Pair("Thriller", "thriller"),
Pair("Vampire", "vampire"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri")
)
)
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
}

View File

@ -1,39 +1,39 @@
package eu.kanade.tachiyomi.animeextension.en.gogoanime.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient
class DoodExtractor(private val client: OkHttpClient) {
fun videosFromUrl(serverUrl: String): List<Video> {
val response = client.newCall(GET(serverUrl)).execute()
val doodTld = serverUrl.substringAfter("https://dood.").substringBefore("/")
val content = response.body!!.string()
if (!content.contains("'/pass_md5/")) return emptyList()
val md5 = content.substringAfter("'/pass_md5/").substringBefore("',")
val token = md5.substringAfterLast("/")
val randomString = getRandomString()
val expiry = System.currentTimeMillis()
val videoUrlStart = client.newCall(
GET(
"https://dood.$doodTld/pass_md5/$md5",
Headers.headersOf("referer", serverUrl)
)
).execute().body!!.string()
val videoUrl = "$videoUrlStart$randomString?token=$token&expiry=$expiry"
return listOf(Video(serverUrl, "Doodstream mirror", videoUrl, null, doodHeaders(doodTld)))
}
private fun getRandomString(length: Int = 10): String {
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
return (1..length)
.map { allowedChars.random() }
.joinToString("")
}
private fun doodHeaders(tld: String) = Headers.Builder().apply {
add("User-Agent", "Aniyomi")
add("Referer", "https://dood.$tld/")
}.build()
}
package eu.kanade.tachiyomi.animeextension.en.gogoanime.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient
class DoodExtractor(private val client: OkHttpClient) {
fun videosFromUrl(serverUrl: String): List<Video> {
val response = client.newCall(GET(serverUrl)).execute()
val doodTld = serverUrl.substringAfter("https://dood.").substringBefore("/")
val content = response.body!!.string()
if (!content.contains("'/pass_md5/")) return emptyList()
val md5 = content.substringAfter("'/pass_md5/").substringBefore("',")
val token = md5.substringAfterLast("/")
val randomString = getRandomString()
val expiry = System.currentTimeMillis()
val videoUrlStart = client.newCall(
GET(
"https://dood.$doodTld/pass_md5/$md5",
Headers.headersOf("referer", serverUrl)
)
).execute().body!!.string()
val videoUrl = "$videoUrlStart$randomString?token=$token&expiry=$expiry"
return listOf(Video(serverUrl, "Doodstream mirror", videoUrl, null, doodHeaders(doodTld)))
}
private fun getRandomString(length: Int = 10): String {
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
return (1..length)
.map { allowedChars.random() }
.joinToString("")
}
private fun doodHeaders(tld: String) = Headers.Builder().apply {
add("User-Agent", "Aniyomi")
add("Referer", "https://dood.$tld/")
}.build()
}

View File

@ -1,96 +1,96 @@
package eu.kanade.tachiyomi.animeextension.en.gogoanime.extractors
import android.util.Base64
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import 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 serverResponse = client.newCall(GET(serverUrl)).execute().asJsoup()
val encrypted = serverResponse.select("script[data-name='crypto']").attr("data-value")
val iv =
serverResponse.select("script[data-name='ts']").attr("data-value").toByteArray()
val id =
serverUrl.toHttpUrl().queryParameter("id") ?: throw Exception("error decrypting")
val secretKey = cryptoHandler(encrypted, iv, iv + iv, false)
val encryptedId =
cryptoHandler(id, "0000000000000000".toByteArray(), secretKey.toByteArray())
val jsonResponse = client.newCall(
GET(
"http://gogoplay.io/encrypt-ajax.php?id=$encryptedId&time=00000000000000000000",
Headers.headersOf("X-Requested-With", "XMLHttpRequest")
)
).execute().body!!.string()
val videoList = mutableListOf<Video>()
val autoList = mutableListOf<Video>()
val array = json.decodeFromString<JsonObject>(jsonResponse)["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, null))
}
} 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,
null,
videoHeaders
)
)
else videoList.add(Video(fileURL, label, fileURL, null, 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)
}
}
}
package eu.kanade.tachiyomi.animeextension.en.gogoanime.extractors
import android.util.Base64
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import 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 serverResponse = client.newCall(GET(serverUrl)).execute().asJsoup()
val encrypted = serverResponse.select("script[data-name='crypto']").attr("data-value")
val iv =
serverResponse.select("script[data-name='ts']").attr("data-value").toByteArray()
val id =
serverUrl.toHttpUrl().queryParameter("id") ?: throw Exception("error decrypting")
val secretKey = cryptoHandler(encrypted, iv, iv + iv, false)
val encryptedId =
cryptoHandler(id, "0000000000000000".toByteArray(), secretKey.toByteArray())
val jsonResponse = client.newCall(
GET(
"http://gogoplay.io/encrypt-ajax.php?id=$encryptedId&time=00000000000000000000",
Headers.headersOf("X-Requested-With", "XMLHttpRequest")
)
).execute().body!!.string()
val videoList = mutableListOf<Video>()
val autoList = mutableListOf<Video>()
val array = json.decodeFromString<JsonObject>(jsonResponse)["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, null))
}
} 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,
null,
videoHeaders
)
)
else videoList.add(Video(fileURL, label, fileURL, null, 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)
}
}
}

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -0,0 +1,88 @@
package eu.kanade.tachiyomi.animeextension.en.nineanime;
public class JSONUtil {
public static String escape(String input) {
StringBuilder output = new StringBuilder();
for(int i=0; i<input.length(); i++) {
char ch = input.charAt(i);
int chx = (int) ch;
// let's not put any nulls in our strings
assert(chx != 0);
if(ch == '\n') {
output.append("\\n");
} else if(ch == '\t') {
output.append("\\t");
} else if(ch == '\r') {
output.append("\\r");
} else if(ch == '\\') {
output.append("\\\\");
} else if(ch == '"') {
output.append("\\\"");
} else if(ch == '\b') {
output.append("\\b");
} else if(ch == '\f') {
output.append("\\f");
} else if(chx >= 0x10000) {
assert false : "Java stores as u16, so it should never give us a character that's bigger than 2 bytes. It literally can't.";
} else if(chx > 127) {
output.append(String.format("\\u%04x", chx));
} else {
output.append(ch);
}
}
return output.toString();
}
public static String unescape(String input) {
StringBuilder builder = new StringBuilder();
int i = 0;
while (i < input.length()) {
char delimiter = input.charAt(i); i++; // consume letter or backslash
if(delimiter == '\\' && i < input.length()) {
// consume first after backslash
char ch = input.charAt(i); i++;
if(ch == '\\' || ch == '/' || ch == '"' || ch == '\'') {
builder.append(ch);
}
else if(ch == 'n') builder.append('\n');
else if(ch == 'r') builder.append('\r');
else if(ch == 't') builder.append('\t');
else if(ch == 'b') builder.append('\b');
else if(ch == 'f') builder.append('\f');
else if(ch == 'u') {
StringBuilder hex = new StringBuilder();
// expect 4 digits
if (i+4 > input.length()) {
throw new RuntimeException("Not enough unicode digits! ");
}
for (char x : input.substring(i, i + 4).toCharArray()) {
if(!Character.isLetterOrDigit(x)) {
throw new RuntimeException("Bad character in unicode escape.");
}
hex.append(Character.toLowerCase(x));
}
i+=4; // consume those four digits.
int code = Integer.parseInt(hex.toString(), 16);
builder.append((char) code);
} else {
throw new RuntimeException("Illegal escape sequence: \\"+ch);
}
} else { // it's not a backslash, or it's the last character.
builder.append(delimiter);
}
}
return builder.toString();
}
}

View File

@ -0,0 +1,361 @@
package eu.kanade.tachiyomi.animeextension.en.nineanime
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.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.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
@ExperimentalSerializationApi
class NineAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "9anime"
override val baseUrl by lazy { preferences.getString("preferred_domain", "https://9anime.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)
}
override fun headersBuilder(): Headers.Builder {
return Headers.Builder().add("Referer", baseUrl)
}
override fun popularAnimeSelector(): String = "li"
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/ajax/home/widget?name=trending&page=$page")
override fun popularAnimeParse(response: Response): AnimesPage {
val responseObject = json.decodeFromString<JsonObject>(response.body!!.string())
val document = Jsoup.parse(JSONUtil.unescape(responseObject["html"]!!.jsonPrimitive.content))
val animes = document.select(popularAnimeSelector()).map { element ->
popularAnimeFromElement(element)
}
val hasNextPage = popularAnimeNextPageSelector().let { selector ->
document.select(selector).first()
} != null
return AnimesPage(animes, hasNextPage)
}
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.select("a.name").attr("href").substringBefore("?"))
thumbnail_url = element.select("a.poster img").attr("src")
title = element.select("a.name").text()
}
override fun popularAnimeNextPageSelector(): String = "li"
override fun episodeListRequest(anime: SAnime): Request {
val animeId = anime.url.substringAfterLast(".")
val vrf = encode(getVrf(animeId))
return GET("$baseUrl/ajax/anime/servers?id=$animeId&vrf=$vrf")
}
override fun episodeListParse(response: Response): List<SEpisode> {
val responseObject = json.decodeFromString<JsonObject>(response.body!!.string())
val document = Jsoup.parse(JSONUtil.unescape(responseObject["html"]!!.jsonPrimitive.content))
val animeId = response.request.url.queryParameter("id")!!
val vrf = encode(response.request.url.queryParameter("vrf")!!)
return document.select(episodeListSelector()).map { episodeFromElement(it, animeId, vrf) }
}
override fun episodeListSelector() = "ul.episodes li a"
private fun episodeFromElement(element: Element, animeId: String, vrf: String): SEpisode {
val episode = SEpisode.create()
val epNum = element.attr("data-base")
episode.setUrlWithoutDomain("$baseUrl/ajax/anime/servers?id=$animeId&vrf=$vrf&episode=$epNum")
episode.episode_number = epNum.toFloat()
episode.name = "Episode $epNum"
episode.date_upload = System.currentTimeMillis()
return episode
}
override fun episodeFromElement(element: Element) = throw Exception("not used")
override fun videoListParse(response: Response): List<Video> {
val responseObject = json.decodeFromString<JsonObject>(response.body!!.string())
val document = Jsoup.parse(JSONUtil.unescape(responseObject["html"]!!.jsonPrimitive.content))
val epNum = response.request.url.queryParameter("episode")
val sources = document.select("ul.episodes li a[data-base=$epNum]").attr("data-sources")
val sourceId = json.decodeFromString<JsonObject>(sources)["41"]!!.jsonPrimitive.content
fun getEpisodeBody(): String? {
val res = network.client
.newCall(GET("$baseUrl/ajax/anime/episode?id=$sourceId"))
.execute()
return if (res.code == 200) res.body!!.string() else null
}
// sometimes I have to retry the request for some reason (???)
val episodeBody = getEpisodeBody() ?: getEpisodeBody()!!
val encryptedSourceUrl = json.decodeFromString<JsonObject>(episodeBody)["url"]!!.jsonPrimitive.content
val embedLink = getLink(encryptedSourceUrl)
val referer = Headers.headersOf("Referer", "$baseUrl/")
val embed = client.newCall(GET(embedLink, referer)).execute().asJsoup()
val skey = embed.selectFirst("script:containsData(window.skey = )")
.data().substringAfter("window.skey = \'").substringBefore("\'")
val sourceObject = json.decodeFromString<JsonObject>(
client.newCall(GET(embedLink.replace("/embed/", "/info/") + "?skey=$skey", referer))
.execute().body!!.string()
)
val masterUrl = sourceObject["media"]!!.jsonObject["sources"]!!.jsonArray
.first().jsonObject["file"]!!.jsonPrimitive.content
val masterPlaylist = client.newCall(GET(masterUrl)).execute().body!!.string()
return masterPlaylist.substringAfter("#EXT-X-STREAM-INF:")
.split("#EXT-X-STREAM-INF:").map {
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore("\n") + "p"
val videoUrl = masterUrl.substringBeforeLast("/") + "/" + it.substringAfter("\n").substringBefore("\n")
Video(videoUrl, quality, videoUrl, null)
}
}
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 searchAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(baseUrl + element.select("a.name").attr("href"))
anime.thumbnail_url = element.select("a.poster img").attr("src")
anime.title = element.select("a.name").text()
return anime
}
override fun searchAnimeNextPageSelector(): String = "a.btn-primary.next:not(.disabled)"
override fun searchAnimeSelector(): String = "ul.anime-list li"
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val vrf = encode(getVrf(query))
return GET("$baseUrl/search?keyword=${encode(query)}&vrf=$vrf&page=$page")
}
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
anime.title = document.select("h1.title").text()
anime.genre = document.select("div:contains(Genre) > span > a[title]").joinToString { it.text() }
anime.description = document.select("p[itemprop=description]").text()
anime.status = parseStatus(document.select("div:contains(Status) > span").text())
// add alternative name to anime description
val altName = "Other name(s): "
document.select("div.alias").firstOrNull()?.ownText()?.let {
if (it.isBlank().not()) {
anime.description = when {
anime.description.isNullOrBlank() -> altName + it
else -> anime.description + "\n\n$altName" + it
}
}
}
return anime
}
private fun parseStatus(statusString: String): Int {
return when (statusString) {
"Airing" -> SAnime.ONGOING
"Completed" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
override fun latestUpdatesNextPageSelector(): String = throw Exception("not used")
override fun latestUpdatesFromElement(element: Element) = throw Exception("not used")
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/ajax/home/widget?name=updated_all&page=$page")
override fun latestUpdatesSelector(): String = throw Exception("not used")
override fun latestUpdatesParse(response: Response) = popularAnimeParse(response)
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val domainPref = ListPreference(screen.context).apply {
key = "preferred_domain"
title = "Preferred domain (requires app restart)"
entries = arrayOf("9anime.to", "9anime.id", "9anime.club", "9anime.center")
entryValues = arrayOf("https://9anime.to", "https://9anime.id", "https://9anime.club", "https://9anime.center")
setDefaultValue("https://9anime.to")
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 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(domainPref)
screen.addPreference(videoQualityPref)
}
private fun getVrf(id: String): String {
val reversed = ue(encode(id) + "0000000").slice(0..5).reversed()
return reversed + ue(je(reversed, encode(id))).replace("""=+$""".toRegex(), "")
}
private fun getLink(url: String): String {
val i = url.slice(0..5)
val n = url.slice(6..url.lastIndex)
return decode(je(i, ze(n)))
}
private fun ue(input: String): String {
if (input.any { it.code >= 256 }) throw Exception("illegal characters!")
var output = ""
for (i in input.indices step 3) {
val a = intArrayOf(-1, -1, -1, -1)
a[0] = input[i].code shr 2
a[1] = (3 and input[i].code) shl 4
if (input.length > i + 1) {
a[1] = a[1] or (input[i + 1].code shr 4)
a[2] = (15 and input[i + 1].code) shl 2
}
if (input.length > i + 2) {
a[2] = a[2] or (input[i + 2].code shr 6)
a[3] = 63 and input[i + 2].code
}
for (n in a) {
if (n == -1) output += "="
else {
if (n in 0..63) output += key[n]
}
}
}
return output
}
private fun je(inputOne: String, inputTwo: String): String {
val arr = IntArray(256) { it }
var output = ""
var u = 0
var r: Int
for (a in arr.indices) {
u = (u + arr[a] + inputOne[a % inputOne.length].code) % 256
r = arr[a]
arr[a] = arr[u]
arr[u] = r
}
u = 0
var c = 0
for (f in inputTwo.indices) {
c = (c + f) % 256
u = (u + arr[c]) % 256
r = arr[c]
arr[c] = arr[u]
arr[u] = r
output += (inputTwo[f].code xor arr[(arr[c] + arr[u]) % 256]).toChar()
}
return output
}
private fun ze(input: String): String {
val t = if (input.replace("""[\t\n\f\r]""".toRegex(), "").length % 4 == 0) {
input.replace("""==?$""".toRegex(), "")
} else input
if (t.length % 4 == 1 || t.contains("""[^+/0-9A-Za-z]""".toRegex())) throw Exception("bad input")
var i: Int
var r = ""
var e = 0
var u = 0
for (o in t.indices) {
e = e shl 6
i = key.indexOf(t[o])
e = e or i
u += 6
if (24 == u) {
r += ((16711680 and e) shr 16).toChar()
r += ((65280 and e) shr 8).toChar()
r += (255 and e).toChar()
e = 0
u = 0
}
}
return if (12 == u) {
e = e shr 4
r + e.toChar()
} else {
if (18 == u) {
e = e shr 2
r += ((65280 and e) shr 8).toChar()
r += (255 and e).toChar()
}
r
}
}
private fun encode(input: String): String = java.net.URLEncoder.encode(input, "utf-8").replace("+", "%20")
private fun decode(input: String): String = java.net.URLDecoder.decode(input, "utf-8")
}
private const val key = "0wMrYU+ixjJ4QdzgfN2HlyIVAt3sBOZnCT9Lm7uFDovkb/EaKpRWhqXS5168ePcG"