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 { buildscript {
ext.kotlin_version = '1.4.32' ext.kotlin_version = '1.6.10'
ext.coroutines_version = '1.4.3' ext.coroutines_version = '1.6.0'
repositories { repositories {
mavenCentral() mavenCentral()
google() google()
@ -10,7 +10,7 @@ buildscript {
classpath 'com.android.tools.build:gradle:4.2.2' classpath 'com.android.tools.build:gradle:4.2.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$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: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
ext { ext {
extName = 'Gogoanime' extName = 'Gogoanime'
pkgNameSuffix = 'en.gogoanime' pkgNameSuffix = 'en.gogoanime'
extClass = '.GogoAnime' extClass = '.GogoAnime'
extVersionCode = 29 extVersionCode = 29
libVersion = '12' libVersion = '12'
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

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

View File

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

View File

@ -1,96 +1,96 @@
package eu.kanade.tachiyomi.animeextension.en.gogoanime.extractors package eu.kanade.tachiyomi.animeextension.en.gogoanime.extractors
import android.util.Base64 import android.util.Base64
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import java.lang.Exception import java.lang.Exception
import java.util.Locale import java.util.Locale
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
@ExperimentalSerializationApi @ExperimentalSerializationApi
class GogoCdnExtractor(private val client: OkHttpClient, private val json: Json) { class GogoCdnExtractor(private val client: OkHttpClient, private val json: Json) {
fun videosFromUrl(serverUrl: String): List<Video> { fun videosFromUrl(serverUrl: String): List<Video> {
try { try {
val serverResponse = client.newCall(GET(serverUrl)).execute().asJsoup() val serverResponse = client.newCall(GET(serverUrl)).execute().asJsoup()
val encrypted = serverResponse.select("script[data-name='crypto']").attr("data-value") val encrypted = serverResponse.select("script[data-name='crypto']").attr("data-value")
val iv = val iv =
serverResponse.select("script[data-name='ts']").attr("data-value").toByteArray() serverResponse.select("script[data-name='ts']").attr("data-value").toByteArray()
val id = val id =
serverUrl.toHttpUrl().queryParameter("id") ?: throw Exception("error decrypting") serverUrl.toHttpUrl().queryParameter("id") ?: throw Exception("error decrypting")
val secretKey = cryptoHandler(encrypted, iv, iv + iv, false) val secretKey = cryptoHandler(encrypted, iv, iv + iv, false)
val encryptedId = val encryptedId =
cryptoHandler(id, "0000000000000000".toByteArray(), secretKey.toByteArray()) cryptoHandler(id, "0000000000000000".toByteArray(), secretKey.toByteArray())
val jsonResponse = client.newCall( val jsonResponse = client.newCall(
GET( GET(
"http://gogoplay.io/encrypt-ajax.php?id=$encryptedId&time=00000000000000000000", "http://gogoplay.io/encrypt-ajax.php?id=$encryptedId&time=00000000000000000000",
Headers.headersOf("X-Requested-With", "XMLHttpRequest") Headers.headersOf("X-Requested-With", "XMLHttpRequest")
) )
).execute().body!!.string() ).execute().body!!.string()
val videoList = mutableListOf<Video>() val videoList = mutableListOf<Video>()
val autoList = mutableListOf<Video>() val autoList = mutableListOf<Video>()
val array = json.decodeFromString<JsonObject>(jsonResponse)["source"]!!.jsonArray val array = json.decodeFromString<JsonObject>(jsonResponse)["source"]!!.jsonArray
if (array.size == 1 && array[0].jsonObject["type"]!!.jsonPrimitive.content == "hls") { if (array.size == 1 && array[0].jsonObject["type"]!!.jsonPrimitive.content == "hls") {
val fileURL = array[0].jsonObject["file"].toString().trim('"') val fileURL = array[0].jsonObject["file"].toString().trim('"')
val masterPlaylist = client.newCall(GET(fileURL)).execute().body!!.string() val masterPlaylist = client.newCall(GET(fileURL)).execute().body!!.string()
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:") masterPlaylist.substringAfter("#EXT-X-STREAM-INF:")
.split("#EXT-X-STREAM-INF:").reversed().forEach { .split("#EXT-X-STREAM-INF:").reversed().forEach {
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore("\n") + "p" val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore("\n") + "p"
val videoUrl = fileURL.substringBeforeLast("/") + "/" + it.substringAfter("\n").substringBefore("\n") val videoUrl = fileURL.substringBeforeLast("/") + "/" + it.substringAfter("\n").substringBefore("\n")
videoList.add(Video(videoUrl, quality, videoUrl, null)) videoList.add(Video(videoUrl, quality, videoUrl, null))
} }
} else array.forEach { } else array.forEach {
val label = it.jsonObject["label"].toString().toLowerCase(Locale.ROOT) val label = it.jsonObject["label"].toString().toLowerCase(Locale.ROOT)
.trim('"').replace(" ", "") .trim('"').replace(" ", "")
val fileURL = it.jsonObject["file"].toString().trim('"') val fileURL = it.jsonObject["file"].toString().trim('"')
val videoHeaders = Headers.headersOf("Referer", serverUrl) val videoHeaders = Headers.headersOf("Referer", serverUrl)
if (label == "auto") autoList.add( if (label == "auto") autoList.add(
Video( Video(
fileURL, fileURL,
label, label,
fileURL, fileURL,
null, null,
videoHeaders videoHeaders
) )
) )
else videoList.add(Video(fileURL, label, fileURL, null, videoHeaders)) else videoList.add(Video(fileURL, label, fileURL, null, videoHeaders))
} }
return videoList.reversed() + autoList return videoList.reversed() + autoList
} catch (e: Exception) { } catch (e: Exception) {
return emptyList() return emptyList()
} }
} }
private fun cryptoHandler( private fun cryptoHandler(
string: String, string: String,
iv: ByteArray, iv: ByteArray,
secretKeyString: ByteArray, secretKeyString: ByteArray,
encrypt: Boolean = true encrypt: Boolean = true
): String { ): String {
val ivParameterSpec = IvParameterSpec(iv) val ivParameterSpec = IvParameterSpec(iv)
val secretKey = SecretKeySpec(secretKeyString, "AES") val secretKey = SecretKeySpec(secretKeyString, "AES")
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
return if (!encrypt) { return if (!encrypt) {
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec) cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec)
String(cipher.doFinal(Base64.decode(string, Base64.DEFAULT))) String(cipher.doFinal(Base64.decode(string, Base64.DEFAULT)))
} else { } else {
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec) cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec)
Base64.encodeToString(cipher.doFinal(string.toByteArray()), Base64.NO_WRAP) 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"