refactor: Convert ar/animetitans to multisrc/animestream (#1777)

This commit is contained in:
Claudemirovsky
2023-06-24 06:01:03 +00:00
committed by GitHub
parent 2bbd7d9358
commit 7cf524d70e
19 changed files with 140 additions and 812 deletions

View File

@ -0,0 +1,6 @@
dependencies {
implementation(project(':lib-mp4upload-extractor'))
implementation(project(':lib-gdriveplayer-extractor'))
implementation(project(':lib-vidbom-extractor'))
implementation(project(':lib-streamsb-extractor'))
}

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -0,0 +1,89 @@
package eu.kanade.tachiyomi.animeextension.ar.animetitans
import eu.kanade.tachiyomi.animeextension.ar.animetitans.extractors.AnimeTitansExtractor
import eu.kanade.tachiyomi.animeextension.ar.animetitans.extractors.SharedExtractor
import eu.kanade.tachiyomi.animeextension.ar.animetitans.extractors.VidYardExtractor
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.gdriveplayerextractor.GdrivePlayerExtractor
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor
import eu.kanade.tachiyomi.lib.vidbomextractor.VidBomExtractor
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStream
import java.text.SimpleDateFormat
import java.util.Locale
class AnimeTitans : AnimeStream(
"ar",
"AnimeTitans",
"https://animetitans.com",
) {
override val dateFormatter by lazy {
SimpleDateFormat("MMMM d, yyyy", Locale("ar"))
}
// =========================== Anime Details ============================
override val animeArtistText = "الاستديو"
override val animeStatusText = "الحالة"
override val animeAuthorText = "المخرج"
override val animeAltNamePrefix = " :أسماء أخرى"
override fun parseStatus(statusString: String?): Int {
return when (statusString?.trim()?.lowercase()) {
"مكتمل" -> SAnime.COMPLETED
"مستمر", "publishing" -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
}
// ============================== Episodes ==============================
override val episodePrefix = "الحلقة"
// ============================ Video Links =============================
override val prefQualityValues = arrayOf("1080p", "720p", "480p", "360p", "Mp4Upload", "4shared")
override val prefQualityEntries = prefQualityValues
override val prefQualityDefault = "1080p"
override val videoSortPrefDefault = prefQualityDefault
override fun getVideoList(url: String, name: String): List<Video> {
val streamSbDomains = listOf(
"sbembed.com", "sbembed1.com", "sbplay.org", "sbvideo.net",
"streamsb.net", "sbplay.one", "cloudemb.com", "playersb.com",
"tubesb.com", "sbplay1.com", "embedsb.com", "watchsb.com",
"sbplay2.com", "japopav.tv", "viewsb.com", "sbfast", "sbfull.com",
"javplaya.com", "ssbstream.net", "p1ayerjavseen.com", "sbthe.com",
"vidmovie.xyz", "sbspeed.com", "streamsss.net", "sblanh.com",
)
val vidbomDomains = listOf(
"vidbom.com", "vidbem.com", "vidbm.com", "vedpom.com",
"vedbom.com", "vedbom.org", "vadbom.com", "vidbam.org",
"myviid.com", "myviid.net", "myvid.com", "vidshare.com",
"vedsharr.com", "vedshar.com", "vedshare.com", "vadshar.com",
"vidshar.org",
)
return when {
baseUrl in url ->
AnimeTitansExtractor(client).videosFromUrl(url, headers, baseUrl)
streamSbDomains.any(url::contains) ->
StreamSBExtractor(client).videosFromUrl(url, headers)
vidbomDomains.any(url::contains) ->
VidBomExtractor(client).videosFromUrl(url)
"vidyard" in url ->
VidYardExtractor(client).videosFromUrl(url, headers)
"mp4upload" in url ->
Mp4uploadExtractor(client).videosFromUrl(url, headers)
"4shared" in url ->
SharedExtractor(client).videoFromUrl(url, name)
?.let(::listOf)
?: emptyList()
"drive.google" in url -> {
val gdriveUrl = "https://gdriveplayer.to/embed2.php?link=$url"
GdrivePlayerExtractor(client).videosFromUrl(gdriveUrl, name, headers)
}
else -> emptyList()
}
}
}

View File

@ -0,0 +1,26 @@
package eu.kanade.tachiyomi.animeextension.ar.animetitans.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.OkHttpClient
class AnimeTitansExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String, headers: Headers, baseUrl: String): List<Video> {
val newHeaders = headers.newBuilder().add("Referer", baseUrl).build()
val callPlayer = client.newCall(GET(url)).execute().use { it.asJsoup() }
val masterUrl = callPlayer.data().substringAfter("source: \"").substringBefore("\",")
val domain = masterUrl.substringBefore("/videowl") // .replace("https", "http")
val masterPlaylist = client.newCall(GET(masterUrl, newHeaders)).execute()
.use { it.body.string() }
val separator = "#EXT-X-STREAM-INF:"
return masterPlaylist.substringAfter(separator).split(separator).map {
val resolution = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore("\n") + "p"
val quality = "AnimeTitans: $resolution"
val videoUrl = domain + it.substringAfter("\n").substringBefore("\n")
Video(videoUrl, quality, videoUrl, headers = newHeaders)
}
}
}

View File

@ -8,12 +8,8 @@ import okhttp3.OkHttpClient
class SharedExtractor(private val client: OkHttpClient) { class SharedExtractor(private val client: OkHttpClient) {
fun videoFromUrl(url: String, quality: String): Video? { fun videoFromUrl(url: String, quality: String): Video? {
val document = client.newCall(GET(url)).execute().asJsoup() val document = client.newCall(GET(url)).execute().asJsoup()
val check = document.select("div.error4shared").text() return document.selectFirst("source")?.let {
val videoUrl = document.select("source").attr("src") Video(it.attr("src"), quality, it.attr("src"))
return if (check.contains("This file is not available any more")) {
Video(url, "no 1video", "https")
} else {
Video(url, quality, videoUrl)
} }
} }
} }

View File

@ -6,17 +6,23 @@ import okhttp3.Headers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
class VidYardExtractor(private val client: OkHttpClient) { class VidYardExtractor(private val client: OkHttpClient) {
companion object {
private const val VIDYARD_URL = "https://play.vidyard.com"
}
fun videosFromUrl(url: String, headers: Headers): List<Video> { fun videosFromUrl(url: String, headers: Headers): List<Video> {
val callPlayer = client.newCall(GET(url)).execute().body.string() val newHeaders = headers.newBuilder().add("Referer", VIDYARD_URL).build()
val id = url.substringAfter("com/").substringBefore("?")
val playerUrl = "$VIDYARD_URL/player/" + id + ".json"
val callPlayer = client.newCall(GET(playerUrl, newHeaders)).execute()
.use { it.body.string() }
val data = callPlayer.substringAfter("hls\":[").substringBefore("]") val data = callPlayer.substringAfter("hls\":[").substringBefore("]")
val sources = data.split("profile\":\"").drop(1) val sources = data.split("profile\":\"").drop(1)
val videoList = mutableListOf<Video>()
for (source in sources) { return sources.map { source ->
val src = source.substringAfter("url\":\"").substringBefore("\"") val src = source.substringAfter("url\":\"").substringBefore("\"")
val quality = source.substringBefore("\"") val quality = source.substringBefore("\"")
val video = Video(src, quality, src, headers = headers) Video(src, quality, src, headers = newHeaders)
videoList.add(video) }
}
return videoList
} }
} }

View File

@ -131,8 +131,11 @@ abstract class AnimeStream(
protected open val animeAdditionalInfoSelector = "div.spe > span, li:has(b)" protected open val animeAdditionalInfoSelector = "div.spe > span, li:has(b)"
protected open val animeStatusText = "Status" protected open val animeStatusText = "Status"
protected open val animeArtistText = "tudio"
protected open val animeAuthorText = "Fansub" protected open val animeAuthorText = "Fansub"
protected open val animeArtistText = when (lang) {
"pt-BR" -> "Estudio"
else -> "Studio"
}
protected open val animeAltNamePrefix = when (lang) { protected open val animeAltNamePrefix = when (lang) {
"pt-BR" -> "Nome(s) alternativo(s): " "pt-BR" -> "Nome(s) alternativo(s): "

View File

@ -16,6 +16,7 @@ class AnimeStreamGenerator : ThemeSourceGenerator {
SingleLang("RineCloud", "https://rine.cloud", "pt-BR", isNsfw = false), SingleLang("RineCloud", "https://rine.cloud", "pt-BR", isNsfw = false),
SingleLang("Animenosub", "https://animenosub.com", "en", isNsfw = true), SingleLang("Animenosub", "https://animenosub.com", "en", isNsfw = true),
SingleLang("AnimeKhor", "https://animekhor.xyz", "en", isNsfw = false), SingleLang("AnimeKhor", "https://animekhor.xyz", "en", isNsfw = false),
SingleLang("AnimeTitans", "https://animetitans.com", "ar", isNsfw = false, overrideVersionCode = 11),
) )
companion object { companion object {

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@ -1,19 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'AnimeTitans'
pkgNameSuffix = 'ar.animetitans'
extClass = '.AnimeTitans'
extVersionCode = 13
libVersion = '13'
}
dependencies {
implementation(project(':lib-mp4upload-extractor'))
implementation(project(':lib-streamsb-extractor'))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
}
apply from: "$rootDir/common.gradle"

View File

@ -1,576 +0,0 @@
package eu.kanade.tachiyomi.animeextension.ar.animetitans
import android.app.Application
import android.content.SharedPreferences
import android.util.Log
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.ar.animetitans.extractors.AnimeTitansExtractor
import eu.kanade.tachiyomi.animeextension.ar.animetitans.extractors.GdrivePlayerExtractor
import eu.kanade.tachiyomi.animeextension.ar.animetitans.extractors.SharedExtractor
import eu.kanade.tachiyomi.animeextension.ar.animetitans.extractors.VidBomExtractor
import eu.kanade.tachiyomi.animeextension.ar.animetitans.extractors.VidYardExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.lang.Exception
import java.text.SimpleDateFormat
import java.util.Base64
import java.util.Locale
class AnimeTitans : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "AnimeTitans"
override val baseUrl = "https://animetitans.com"
override val lang = "ar"
override val supportsLatest = true
private val animeUrlDirectory: String = "/anime"
private val dateFormat: SimpleDateFormat = SimpleDateFormat("MMM d, yyy", Locale.US)
override val client: OkHttpClient = network.cloudflareClient
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// Popular (Search with popular order and nothing else)
override fun popularAnimeRequest(page: Int) = searchAnimeRequest(page, "", AnimeFilterList(OrderByFilter("popular")))
override fun popularAnimeParse(response: Response) = searchAnimeParse(response)
// Latest (Search with update order and nothing else)
override fun latestUpdatesRequest(page: Int) = searchAnimeRequest(page, "", AnimeFilterList(OrderByFilter("update")))
override fun latestUpdatesParse(response: Response) = searchAnimeParse(response)
// Episodes
override fun episodeListSelector() = "ul li[data-index]"
private fun parseEpisodeDate(date: String): Long {
return SimpleDateFormat("MMM d, yyy", Locale("ar")).parse(date)?.time ?: 0L
}
override fun episodeFromElement(element: Element): SEpisode {
val episode = SEpisode.create()
val epNum = getNumberFromEpsString(element.select(".epl-num").text())
val urlElements = element.select("a")
episode.setUrlWithoutDomain(urlElements.attr("href"))
episode.name = element.select(".epl-title").text().ifBlank { urlElements.first()!!.text() }
episode.episode_number = when {
(epNum.isNotEmpty()) -> epNum.toFloat()
else -> 1F
}
episode.date_upload = element.selectFirst(".epl-date")?.text()?.let { parseEpisodeDate(it) } ?: 0L
return episode
}
private fun getNumberFromEpsString(epsStr: String): String {
return epsStr.filter { it.isDigit() }
}
// Video urls
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
return videosFromElement(document)
}
override fun videoListSelector() = "select.mirror option"
private fun videosFromElement(document: Document): List<Video> {
val videoList = mutableListOf<Video>()
val elements = document.select(videoListSelector())
for (element in elements) {
val location = element.ownerDocument()!!.location()
val videoEncode = element.attr("value")
val qualityy = element.text()
val decoder: Base64.Decoder = Base64.getDecoder()
val decoded = String(decoder.decode(videoEncode))
val embedUrl = decoded.substringAfter("src=\"").substringBefore("\"")
Log.i("embedUrl", "$embedUrl")
when {
embedUrl.contains("vidyard")
-> {
val headers = headers.newBuilder()
.set("Referer", "https://play.vidyard.com")
.set("Accept-Encoding", "gzip, deflate, br")
.set("Accept-Language", "en-US,en;q=0.5")
.set("TE", "trailers")
.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0")
.build()
val id = embedUrl.substringAfter("com/").substringBefore("?")
val vidUrl = "https://play.vidyard.com/player/" + id + ".json"
val videos = VidYardExtractor(client).videosFromUrl(vidUrl, headers)
videoList.addAll(videos)
}
embedUrl.contains("animetitans.net")
-> {
val headers = headers.newBuilder()
.set("Referer", "https://animetitans.net/")
.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36")
.set("Accept", "*/*")
.set("Accept-Language", "en-US,en;q=0.5")
.set("Accept-Encoding", "gzip, deflate, br")
.build()
val videos = AnimeTitansExtractor(client).videosFromUrl(embedUrl, headers)
videoList.addAll(videos)
}
embedUrl.contains("sbembed.com") || embedUrl.contains("sbembed1.com") || embedUrl.contains("sbplay.org") ||
embedUrl.contains("sbvideo.net") || embedUrl.contains("streamsb.net") || embedUrl.contains("sbplay.one") ||
embedUrl.contains("cloudemb.com") || embedUrl.contains("playersb.com") || embedUrl.contains("tubesb.com") ||
embedUrl.contains("sbplay1.com") || embedUrl.contains("embedsb.com") || embedUrl.contains("watchsb.com") ||
embedUrl.contains("sbplay2.com") || embedUrl.contains("japopav.tv") || embedUrl.contains("viewsb.com") ||
embedUrl.contains("sbfast") || embedUrl.contains("sbfull.com") || embedUrl.contains("javplaya.com") ||
embedUrl.contains("ssbstream.net") || embedUrl.contains("p1ayerjavseen.com") || embedUrl.contains("sbthe.com") ||
embedUrl.contains("vidmovie.xyz") || embedUrl.contains("sbspeed.com") || embedUrl.contains("streamsss.net") ||
embedUrl.contains("sblanh.com")
-> {
val videos = StreamSBExtractor(client).videosFromUrl(embedUrl, headers)
videoList.addAll(videos)
}
embedUrl.contains("drive.google")
-> {
val embedUrlG = "https://gdriveplayer.to/embed2.php?link=" + embedUrl
val videos = GdrivePlayerExtractor(client).videosFromUrl(embedUrlG)
videoList.addAll(videos)
}
embedUrl.contains("animetitans.net")
-> {
val headers = headers.newBuilder()
.set("Referer", "$baseUrl/")
.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36")
.set("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
.set("Accept-Language", "en-US,en;q=0.5")
.set("Accept-Encoding", "gzip, deflate, br")
.build()
val videos = AnimeTitansExtractor(client).videosFromUrl(embedUrl, headers)
videoList.addAll(videos)
}
embedUrl.contains("4shared") -> {
val video = SharedExtractor(client).videoFromUrl(embedUrl, qualityy)
if (video != null) {
videoList.add(video)
}
}
embedUrl.contains("mp4upload") -> {
val videos = Mp4uploadExtractor(client).videosFromUrl(embedUrl, headers)
videoList.addAll(videos)
}
embedUrl.contains("vidbom.com") ||
embedUrl.contains("vidbem.com") || embedUrl.contains("vidbm.com") || embedUrl.contains("vedpom.com") ||
embedUrl.contains("vedbom.com") || embedUrl.contains("vedbom.org") || embedUrl.contains("vadbom.com") ||
embedUrl.contains("vidbam.org") || embedUrl.contains("myviid.com") || embedUrl.contains("myviid.net") ||
embedUrl.contains("myvid.com") || embedUrl.contains("vidshare.com") || embedUrl.contains("vedsharr.com") ||
embedUrl.contains("vedshar.com") || embedUrl.contains("vedshare.com") || embedUrl.contains("vadshar.com") || embedUrl.contains("vidshar.org")
-> {
val videos = VidBomExtractor(client).videosFromUrl(embedUrl)
videoList.addAll(videos)
}
}
}
return videoList
}
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", 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
}
// Search
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
if (query.startsWith(URL_SEARCH_PREFIX).not()) return super.fetchSearchAnime(page, query, filters)
val animePath = try {
animePathFromUrl(query.substringAfter(URL_SEARCH_PREFIX))
?: return Observable.just(AnimesPage(emptyList(), false))
} catch (e: Exception) {
return Observable.error(e)
}
return fetchAnimeDetails(
SAnime.create()
.apply { this.url = "$animeUrlDirectory/$animePath" },
)
.map {
// Isn't set in returned Anime
it.url = "$animeUrlDirectory/$id"
AnimesPage(listOf(it), false)
}
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder()
if (query.isNotEmpty()) {
url.addPathSegments("page/$page/").addQueryParameter("s", query)
} else {
url.addPathSegment(animeUrlDirectory.substring(1)).addPathSegments("page/$page/")
filters.forEach { filter ->
when (filter) {
is AuthorFilter -> {
url.addQueryParameter("author", filter.state)
}
is YearFilter -> {
url.addQueryParameter("yearx", filter.state)
}
is StatusFilter -> {
url.addQueryParameter("status", filter.selectedValue())
}
is TypeFilter -> {
url.addQueryParameter("type", filter.selectedValue())
}
is OrderByFilter -> {
url.addQueryParameter("order", filter.selectedValue())
}
is GenreListFilter -> {
filter.state
.filter { it.state != AnimeFilter.TriState.STATE_IGNORE }
.forEach {
val value = if (it.state == AnimeFilter.TriState.STATE_EXCLUDE) "-${it.value}" else it.value
url.addQueryParameter("genre[]", value)
}
}
is SeasonListFilter -> {
filter.state
.filter { it.state != AnimeFilter.TriState.STATE_IGNORE }
.forEach {
val value = if (it.state == AnimeFilter.TriState.STATE_EXCLUDE) "-${it.value}" else it.value
url.addQueryParameter("season[]", value)
}
}
is StudioListFilter -> {
filter.state
.filter { it.state != AnimeFilter.TriState.STATE_IGNORE }
.forEach {
val value = if (it.state == AnimeFilter.TriState.STATE_EXCLUDE) "-${it.value}" else it.value
url.addQueryParameter("studio[]", value)
}
}
else -> { /* Do Nothing */ }
}
}
}
return GET(url.toString())
}
override fun searchAnimeParse(response: Response): AnimesPage {
if (genrelist == null) {
genrelist = parseGenres(response.asJsoup(response.peekBody(Long.MAX_VALUE).string()))
seasonlist = parseSeasons(response.asJsoup(response.peekBody(Long.MAX_VALUE).string()))
studiolist = parseStudios(response.asJsoup(response.peekBody(Long.MAX_VALUE).string()))
}
return super.searchAnimeParse(response)
}
override fun searchAnimeSelector() = ".utao .uta .imgu, .listupd .bs .bsx, .listo .bs .bsx"
override fun searchAnimeFromElement(element: Element) = SAnime.create().apply {
thumbnail_url = element.select("img").attr("src")
title = element.select("a").attr("title")
setUrlWithoutDomain(element.select("a").attr("href"))
}
override fun searchAnimeNextPageSelector() = "div.pagination .next, div.hpage .r"
// Anime details
open val seriesDetailsSelector = "div.bigcontent, div.animefull, div.main-info, div.postbody"
open val seriesTitleSelector = "h1.entry-title"
open val seriesArtistSelector = "span:contains(الاستديو) a"
open val seriesAuthorSelector = "span:contains(المخرج) a"
open val seriesDescriptionSelector = ".entry-content[itemprop=description]"
open val seriesAltNameSelector = ".alter"
open val seriesGenreSelector = ".genxed a"
open val seriesTypeSelector = "span:contains(النوع)"
open val seriesStatusSelector = "span:contains(الحالة)"
open val seriesThumbnailSelector = ".thumb img"
open val altNamePrefix = " :أسماء أخرى"
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
document.selectFirst(seriesDetailsSelector)?.let { seriesDetails ->
title = seriesDetails.selectFirst(seriesTitleSelector)?.text().orEmpty()
artist = seriesDetails.selectFirst(seriesArtistSelector)?.ownText().removeEmptyPlaceholder()
author = seriesDetails.selectFirst(seriesAuthorSelector)?.ownText().removeEmptyPlaceholder()
description = seriesDetails.select(seriesDescriptionSelector).joinToString("\n") { it.text() }
// Add alternative name to Anime description
val altName = seriesDetails.selectFirst(seriesAltNameSelector)?.ownText().takeIf { it.isNullOrBlank().not() }
altName?.let {
description = "$description\n\n$altName$altNamePrefix".trim()
}
val genres = seriesDetails.select(seriesGenreSelector).map { it.text() }.toMutableList()
// Add series type (Anime/manhwa/manhua/other) to genre
seriesDetails.selectFirst(seriesTypeSelector)?.ownText().takeIf { it.isNullOrBlank().not() }?.let { genres.add(it) }
genre = genres.map { genre ->
genre.lowercase(Locale.forLanguageTag(lang)).replaceFirstChar { char ->
if (char.isLowerCase()) {
char.titlecase(Locale.forLanguageTag(lang))
} else {
char.toString()
}
}
}
.joinToString { it.trim() }
status = seriesDetails.selectFirst(seriesStatusSelector)?.text().parseStatus()
thumbnail_url = seriesDetails.select(seriesThumbnailSelector).attr("src")
}
}
private fun String?.removeEmptyPlaceholder(): String? {
return if (this.isNullOrBlank() || this == "-" || this == "N/A") null else this
}
open fun String?.parseStatus(): Int = when {
this == null -> SAnime.UNKNOWN
listOf("مستمر", "publishing").any { this.contains(it, ignoreCase = true) } -> SAnime.ONGOING
this.contains("مكتمل", ignoreCase = true) -> SAnime.COMPLETED
// this.contains("مؤجل", ignoreCase = true) -> SAnime.HIATUS
else -> SAnime.UNKNOWN
}
// Filters
private class AuthorFilter : AnimeFilter.Text("Author")
private class YearFilter : AnimeFilter.Text("Year")
open class SelectFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
defaultValue: String? = null,
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
) {
fun selectedValue() = vals[state].second
}
protected class StatusFilter : SelectFilter(
"Status",
arrayOf(
Pair("All", ""),
Pair("Ongoing", "ongoing"),
Pair("Completed", "completed"),
Pair("Hiatus", "hiatus"),
Pair("UpComing", "upcoming"),
),
)
protected class TypeFilter : SelectFilter(
"Type",
arrayOf(
Pair("All", ""),
Pair("Anime", "tv"),
Pair("Movie", "movie"),
Pair("OVA", "ova"),
Pair("ONA", "ona"),
Pair("Special", "special"),
),
)
protected class OrderByFilter(defaultOrder: String? = null) : SelectFilter(
"Sort By",
arrayOf(
Pair("Default", ""),
Pair("A-Z", "title"),
Pair("Z-A", "titlereverse"),
Pair("Latest Update", "update"),
Pair("Latest Added", "latest"),
Pair("Popular", "popular"),
),
defaultOrder,
)
protected class Genre(name: String, val value: String) : AnimeFilter.TriState(name)
protected class GenreListFilter(genres: List<Genre>) : AnimeFilter.Group<Genre>("Genre", genres)
private var genrelist: List<Genre>? = null
protected open fun getGenreList(): List<Genre> {
// Filters are fetched immediately once an extension loads
// We're only able to get filters after a loading the Anime directory,
// and resetting the filters is the only thing that seems to reinflate the view
return genrelist ?: listOf(Genre("Press reset to attempt to fetch genres", ""))
}
protected class Season(name: String, val value: String) : AnimeFilter.TriState(name)
protected class SeasonListFilter(seasons: List<Season>) : AnimeFilter.Group<Season>("Season", seasons)
private var seasonlist: List<Season>? = null
protected open fun getSeasonList(): List<Season> {
return seasonlist ?: listOf(Season("Press reset to attempt to fetch Seasons", ""))
}
protected class Studio(name: String, val value: String) : AnimeFilter.TriState(name)
protected class StudioListFilter(studios: List<Studio>) : AnimeFilter.Group<Studio>("Studio", studios)
private var studiolist: List<Studio>? = null
protected open fun getStudioList(): List<Studio> {
return studiolist ?: listOf(Studio("Press reset to attempt to fetch Studios", ""))
}
override fun getFilterList(): AnimeFilterList {
val filters = mutableListOf<AnimeFilter<*>>(
AnimeFilter.Separator(),
AuthorFilter(),
YearFilter(),
StatusFilter(),
TypeFilter(),
OrderByFilter(),
AnimeFilter.Header("Genre exclusion is not available for all sources"),
GenreListFilter(getGenreList()),
SeasonListFilter(getSeasonList()),
StudioListFilter(getStudioList()),
)
return AnimeFilterList(filters)
}
// Helpers
/**
* Given some string which represents an http urlString, returns path for a Anime
* which can be used to fetch its details at "$baseUrl$AnimeUrlDirectory/$AnimePath"
*
* @param urlString: String
*
* @returns Path of a Anime, or null if none could be found
*/
private fun animePathFromUrl(urlString: String): String? {
val baseAnimeUrl = "$baseUrl$animeUrlDirectory".toHttpUrl()
val url = urlString.toHttpUrlOrNull() ?: return null
val isAnimeUrl = (baseAnimeUrl.host == url.host && pathLengthIs(url, 2) && url.pathSegments[0] == baseAnimeUrl.pathSegments[0])
if (isAnimeUrl) return url.pathSegments[1]
val potentiallyChapterUrl = pathLengthIs(url, 1)
if (potentiallyChapterUrl) {
val response = client.newCall(GET(urlString, headers)).execute()
if (response.isSuccessful.not()) {
response.close()
throw IllegalStateException("HTTP error ${response.code}")
} else if (response.isSuccessful) {
val links = response.asJsoup().select("a[itemprop=item]")
// near the top of page: home > Anime > current chapter
if (links.size == 3) {
return links[1].attr("href").toHttpUrlOrNull()?.encodedPath
}
}
}
return null
}
private fun pathLengthIs(url: HttpUrl, n: Int, strict: Boolean = false): Boolean {
return url.pathSegments.size == n && url.pathSegments[n - 1].isNotEmpty() ||
(!strict && url.pathSegments.size == n + 1 && url.pathSegments[n].isEmpty())
}
private fun parseGenres(document: Document): List<Genre>? {
return document.selectFirst("div.filter:contains(التصنيف) ul.scrollz")?.select("li")?.map { li ->
Genre(
li.selectFirst("label")!!.text(),
li.selectFirst("input[type=checkbox]")!!.attr("value"),
)
}
}
private fun parseSeasons(document: Document): List<Season>? {
return document.selectFirst("div.filter:contains(الموسم) ul.scrollz")?.select("li")?.map { li ->
Season(
li.selectFirst("label")!!.text(),
li.selectFirst("input[type=checkbox]")!!.attr("value"),
)
}
}
private fun parseStudios(document: Document): List<Studio>? {
return document.selectFirst("div.filter:contains(الاستديو) ul.scrollz")?.select("li")?.map { li ->
Studio(
li.selectFirst("label")!!.text(),
li.selectFirst("input[type=checkbox]")!!.attr("value"),
)
}
}
// Unused
override fun popularAnimeSelector(): String = throw UnsupportedOperationException("Not used")
override fun popularAnimeFromElement(element: Element): SAnime = throw UnsupportedOperationException("Not used")
override fun popularAnimeNextPageSelector(): String? = throw UnsupportedOperationException("Not used")
override fun latestUpdatesSelector(): String = throw UnsupportedOperationException("Not used")
override fun latestUpdatesFromElement(element: Element): SAnime = throw UnsupportedOperationException("Not used")
override fun latestUpdatesNextPageSelector(): String? = throw UnsupportedOperationException("Not used")
companion object {
const val URL_SEARCH_PREFIX = "url:"
// More info: https://issuetracker.google.com/issues/36970498
@Suppress("RegExpRedundantEscape")
private val ANIME_PAGE_ID_REGEX = "post_id\\s*:\\s*(\\d+)\\}".toRegex()
private val CHAPTER_PAGE_ID_REGEX = "chapter_id\\s*=\\s*(\\d+);".toRegex()
val JSON_IMAGE_LIST_REGEX = "\"images\"\\s*:\\s*(\\[.*?])".toRegex()
}
// Preferences
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p", "Doodstream", "4shared")
entryValues = arrayOf("1080", "720", "480", "360", "Doodstream", "4shared")
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)
}
}

View File

@ -1,23 +0,0 @@
package eu.kanade.tachiyomi.animeextension.ar.animetitans.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.OkHttpClient
class AnimeTitansExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String, headers: Headers): List<Video> {
val callPlayer = client.newCall(GET(url)).execute().asJsoup()
val masterUrl = callPlayer.data().substringAfter("source: \"").substringBefore("\",")
val domain = masterUrl.substringBefore("/videowl") // .replace("https", "http")
val masterPlaylist = client.newCall(GET(masterUrl)).execute().body.string()
val videoList = mutableListOf<Video>()
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:").split("#EXT-X-STREAM-INF:").forEach {
val quality = "AnimeTitans: " + it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore("\n") + "p"
val videoUrl = "$domain" + it.substringAfter("\n").substringBefore("\n")
videoList.add(Video(videoUrl, quality, videoUrl, headers = headers))
}
return videoList
}
}

View File

@ -1,125 +0,0 @@
package eu.kanade.tachiyomi.animeextension.ar.animetitans.extractors
import android.util.Base64
import dev.datlag.jsunpacker.JsUnpacker
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.OkHttpClient
import java.security.DigestException
import java.security.MessageDigest
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
class GdrivePlayerExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String): List<Video> {
val body = client.newCall(GET(url.replace(".me", ".to"))).execute()
.body.string()
val eval = JsUnpacker.unpackAndCombine(body)!!.replace("\\", "")
val json = Json.decodeFromString<JsonObject>(REGEX_DATAJSON.getFirst(eval))
val sojson = REGEX_SOJSON.getFirst(eval)
.split(Regex("\\D+"))
.joinToString("") {
Char(it.toInt()).toString()
}
val password = REGEX_PASSWORD.getFirst(sojson).toByteArray()
val decrypted = decryptAES(password, json)!!
val secondEval = JsUnpacker.unpackAndCombine(decrypted)!!.replace("\\", "")
return REGEX_VIDEOURL.findAll(secondEval)
.distinctBy { it.groupValues[2] } // remove duplicates by quality
.map {
val qualityStr = it.groupValues[2]
val quality = "$PLAYER_NAME - ${qualityStr}p"
val videoUrl = "https:" + it.groupValues[1] + "&res=$qualityStr"
Video(videoUrl, quality, videoUrl)
}.toList()
}
private fun decryptAES(password: ByteArray, json: JsonObject): String? {
val salt = json["s"]!!.jsonPrimitive.content
val encodedCiphetext = json["ct"]!!.jsonPrimitive.content
val ciphertext = Base64.decode(encodedCiphetext, Base64.DEFAULT)
val (key, iv) = generateKeyAndIv(password, salt.decodeHex())
?: return null
val keySpec = SecretKeySpec(key, "AES")
val ivSpec = IvParameterSpec(iv)
val cipher = Cipher.getInstance("AES/CBC/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec)
val decryptedData = String(cipher.doFinal(ciphertext))
return decryptedData
}
// https://stackoverflow.com/a/41434590/8166854
private fun generateKeyAndIv(
password: ByteArray,
salt: ByteArray,
hashAlgorithm: String = "MD5",
keyLength: Int = 32,
ivLength: Int = 16,
iterations: Int = 1,
): List<ByteArray>? {
val md = MessageDigest.getInstance(hashAlgorithm)
val digestLength = md.getDigestLength()
val targetKeySize = keyLength + ivLength
val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength
var generatedData = ByteArray(requiredLength)
var generatedLength = 0
try {
md.reset()
while (generatedLength < targetKeySize) {
if (generatedLength > 0) {
md.update(
generatedData,
generatedLength - digestLength,
digestLength,
)
}
md.update(password)
md.update(salt, 0, 8)
md.digest(generatedData, generatedLength, digestLength)
for (i in 1 until iterations) {
md.update(generatedData, generatedLength, digestLength)
md.digest(generatedData, generatedLength, digestLength)
}
generatedLength += digestLength
}
val result = listOf(
generatedData.copyOfRange(0, keyLength),
generatedData.copyOfRange(keyLength, targetKeySize),
)
return result
} catch (e: DigestException) {
return null
}
}
private fun Regex.getFirst(item: String): String {
return find(item)?.groups?.elementAt(1)?.value!!
}
// Stolen from AnimixPlay(EN) / GogoCdnExtractor
private fun String.decodeHex(): ByteArray {
check(length % 2 == 0) { "Must have an even length" }
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
companion object {
private const val PLAYER_NAME = "GDRIVE"
private val REGEX_DATAJSON = Regex("data='(\\S+?)'")
private val REGEX_PASSWORD = Regex("var pass = \"(\\S+?)\"")
private val REGEX_SOJSON = Regex("null,['|\"](\\w+)['|\"]")
private val REGEX_VIDEOURL = Regex("file\":\"(\\S+?)\".*?res=(\\d+)")
}
}

View File

@ -1,54 +0,0 @@
package eu.kanade.tachiyomi.animeextension.ar.animetitans.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.OkHttpClient
class VidBomExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String): List<Video> {
val doc = client.newCall(GET(url)).execute().asJsoup()
val script = doc.selectFirst("script:containsData(sources)")!!
val data = script.data().substringAfter("sources: [").substringBefore("],")
val sources = data.split("file:\"").drop(1)
val videoList = mutableListOf<Video>()
for (source in sources) {
val src = source.substringBefore("\"")
val quality = "Vidbom:" + source.substringAfter("label:\"").substringBefore("\"") // .substringAfter("format: '")
val video = Video(src, quality, src)
videoList.add(video)
}
return videoList
/*Log.i("looool", "$js")
val json = JSONObject(js)
Log.i("looool", "$json")
val videoList = mutableListOf<Video>()
val jsonArray = json.getJSONArray("sources")
for (i in 0 until jsonArray.length()) {
val `object` = jsonArray.getJSONObject(i)
val videoUrl = `object`.getString("file")
Log.i("looool", videoUrl)
val quality = "Vidbom:" + `object`.getString("label")
videoList.add(Video(videoUrl, quality, videoUrl))
}
return videoList*/
/*if (jas.contains("sources")) {
val js = script.data()
val json = JSONObject(js)
val videoList = mutableListOf<Video>()
val jsonArray = json.getJSONArray("sources")
for (i in 0 until jsonArray.length()) {
val `object` = jsonArray.getJSONObject(i)
val videoUrl = `object`.getString("file")
Log.i("lol", videoUrl)
val quality = "Vidbom:" + `object`.getString("label")
videoList.add(Video(videoUrl, quality, videoUrl))
}
return videoList
} else {
val videoList = mutableListOf<Video>()
videoList.add(Video(url, "no 2video", null))
return videoList
}*/
}
}