Initial commit
7
src/ar/akwam/build.gradle
Normal file
|
@ -0,0 +1,7 @@
|
|||
ext {
|
||||
extName = 'Akwam'
|
||||
extClass = '.Akwam'
|
||||
extVersionCode = 9
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/ar/akwam/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
src/ar/akwam/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
src/ar/akwam/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
src/ar/akwam/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.9 KiB |
BIN
src/ar/akwam/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/ar/akwam/res/play_store_512.png
Normal file
After Width: | Height: | Size: 32 KiB |
|
@ -0,0 +1,355 @@
|
|||
package eu.kanade.tachiyomi.animeextension.ar.akwam
|
||||
|
||||
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.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 okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
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
|
||||
|
||||
class Akwam : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val name = "أكوام"
|
||||
|
||||
override val baseUrl = "https://akw-cdn1.link"
|
||||
|
||||
override val lang = "ar"
|
||||
|
||||
override val supportsLatest = false
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
// Popular
|
||||
|
||||
override fun popularAnimeSelector(): String = "div.entry-box-1 div.entry-image a.box"
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/movies?page=$page")
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.thumbnail_url = element.select("picture img").attr("data-src")
|
||||
anime.setUrlWithoutDomain(element.attr("href"))
|
||||
anime.title = element.select("picture img").attr("alt")
|
||||
return anime
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String = "ul.pagination li.page-item a[rel=next]"
|
||||
|
||||
// episodes
|
||||
override fun episodeListSelector() = "div.bg-primary2 h2 a"
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val episodes = mutableListOf<SEpisode>()
|
||||
fun addEpisodes(document: Document) {
|
||||
if (document.select(episodeListSelector()).isNullOrEmpty()) {
|
||||
// add movie
|
||||
document.select("input#reportInputUrl").map { episodes.add(episodeFromElement(it)) }
|
||||
} else {
|
||||
document.select(episodeListSelector()).map { episodes.add(episodesFromElement(it)) }
|
||||
}
|
||||
}
|
||||
addEpisodes(response.asJsoup())
|
||||
return episodes
|
||||
}
|
||||
|
||||
override fun episodeFromElement(element: Element): SEpisode {
|
||||
val episode = SEpisode.create()
|
||||
episode.setUrlWithoutDomain(element.attr("value"))
|
||||
episode.name = "مشاهدة"
|
||||
return episode
|
||||
}
|
||||
|
||||
private fun episodesFromElement(element: Element): SEpisode {
|
||||
val episode = SEpisode.create()
|
||||
val epNum = getNumberFromEpsString(element.text())
|
||||
episode.setUrlWithoutDomain(element.attr("href"))
|
||||
episode.name = element.text()
|
||||
episode.episode_number = when {
|
||||
epNum.isNotEmpty() -> epNum.toFloatOrNull() ?: 1F
|
||||
else -> 1F
|
||||
}
|
||||
return episode
|
||||
}
|
||||
|
||||
private fun getNumberFromEpsString(epsStr: String): String {
|
||||
return epsStr.filter { it.isDigit() }
|
||||
}
|
||||
|
||||
// Video links
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
val iframe = "https://akw-cdn1.link/watch" + document.select("a.link-show").attr("href").substringAfter("watch") + "/" + document.ownerDocument()!!.select("input#page_id").attr("value")
|
||||
val referer = response.request.url.toString()
|
||||
val refererHeaders = Headers.headersOf("referer", referer)
|
||||
val iframeResponse = client.newCall(GET(iframe, refererHeaders))
|
||||
.execute().asJsoup()
|
||||
return iframeResponse.select(videoListSelector()).map { videoFromElement(it) }
|
||||
}
|
||||
|
||||
override fun videoListSelector() = "source"
|
||||
|
||||
override fun videoFromElement(element: Element): Video {
|
||||
return Video(element.attr("src").replace("https", "http"), element.attr("size") + "p", element.attr("src").replace("https", "http"))
|
||||
}
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString("preferred_quality", null)
|
||||
if (quality != null) {
|
||||
val newList = mutableListOf<Video>()
|
||||
var preferred = 0
|
||||
for (video in this) {
|
||||
if (video.quality.contains(quality)) {
|
||||
newList.add(preferred, video)
|
||||
preferred++
|
||||
} else {
|
||||
newList.add(video)
|
||||
}
|
||||
}
|
||||
return newList
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
// Search
|
||||
|
||||
override fun searchAnimeFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.thumbnail_url = element.select("picture img").attr("data-src")
|
||||
anime.setUrlWithoutDomain(element.attr("href"))
|
||||
anime.title = element.select("picture img").attr("alt")
|
||||
return anime
|
||||
}
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String = "ul.pagination li.page-item a[rel=next]"
|
||||
|
||||
override fun searchAnimeSelector(): String = "div.widget div.widget-body div.col-lg-auto div.entry-box div.entry-image a.box"
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val url = if (query.isNotBlank()) {
|
||||
val url = "$baseUrl/search?q=$query&page=$page".toHttpUrlOrNull()!!.newBuilder()
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is SectionFilter -> url.addQueryParameter("section", filter.toUriPart())
|
||||
is RatingFilter -> url.addQueryParameter("rating", filter.toUriPart())
|
||||
is FormatFilter -> url.addQueryParameter("formats", filter.toUriPart())
|
||||
is QualityFilter -> url.addQueryParameter("quality", filter.toUriPart())
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
url.toString()
|
||||
} else {
|
||||
val url = "$baseUrl/search?page=$page".toHttpUrlOrNull()!!.newBuilder()
|
||||
var type = "movies"
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is TypeFilter -> type = filter.toUriPart().toString()
|
||||
is SectionSFilter -> url.addQueryParameter("section", filter.toUriPart())
|
||||
is CategorySFilter -> url.addQueryParameter("category", filter.toUriPart())
|
||||
is RatingSFilter -> url.addQueryParameter("rating", filter.toUriPart())
|
||||
// is LanguageSFilter -> url.addQueryParameter("quality", filter.toUriPart())
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
url.toString().replace("search", type)
|
||||
}
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
// Anime Details
|
||||
|
||||
override fun animeDetailsParse(document: Document): SAnime {
|
||||
val anime = SAnime.create()
|
||||
// anime.thumbnail_url = document.select("div.container div div a picture > img.img-fluid").attr("data-src")
|
||||
anime.title = document.select("picture > img.img-fluid").attr("alt")
|
||||
anime.genre = document.select("div.font-size-16.d-flex.align-items-center.mt-3 a.badge, span.badge-info, span:contains(جودة الفيلم), span:contains(انتاج)").joinToString(", ") { it.text().replace("جودة الفيلم : ", "") }
|
||||
anime.author = document.select("span:contains(انتاج)").text().replace("انتاج : ", "")
|
||||
anime.description = document.select("div.widget:contains(قصة )").text()
|
||||
anime.status = SAnime.COMPLETED
|
||||
return anime
|
||||
}
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesSelector(): String = throw UnsupportedOperationException()
|
||||
|
||||
// Filters
|
||||
|
||||
override fun getFilterList() = AnimeFilterList(
|
||||
AnimeFilter.Header("فلترات البحث"),
|
||||
AnimeFilter.Separator(),
|
||||
SectionFilter(getSectionFilter()),
|
||||
RatingFilter(getRatingFilter()),
|
||||
FormatFilter(getFormatFilter()),
|
||||
QualityFilter(getQualityFilter()),
|
||||
AnimeFilter.Header("تصفح الموقع (تعمل فقط لو كان البحث فارغ)"),
|
||||
AnimeFilter.Separator(),
|
||||
TypeFilter(getTypeFilter()),
|
||||
SectionSFilter(getSectionSFilter()),
|
||||
CategorySFilter(getCategorySFilter()),
|
||||
RatingSFilter(getRatingSFilter()),
|
||||
)
|
||||
|
||||
private class SectionFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("الأقسام", vals)
|
||||
private class RatingFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("التقيم", vals)
|
||||
private class FormatFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("الجودة", vals)
|
||||
private class QualityFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("الدقة", vals)
|
||||
private class TypeFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("النوع", vals)
|
||||
private class SectionSFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("القسم", vals)
|
||||
private class CategorySFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("التصنيف", vals)
|
||||
private class RatingSFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("التقييم", vals)
|
||||
private fun getTypeFilter(): Array<Pair<String?, String>> = arrayOf(
|
||||
Pair("movies", "افلام"),
|
||||
Pair("series", "مسلسلات"),
|
||||
)
|
||||
private fun getSectionFilter(): Array<Pair<String?, String>> = arrayOf(
|
||||
Pair("0", "الكل"),
|
||||
Pair("movie", "افلام"),
|
||||
Pair("series", "مسلسلات"),
|
||||
Pair("show", "تلفزيون"),
|
||||
)
|
||||
|
||||
private fun getRatingFilter(): Array<Pair<String?, String>> = arrayOf(
|
||||
Pair("0", "التقييم"),
|
||||
Pair("1", "1+"),
|
||||
Pair("2", "2+"),
|
||||
Pair("3", "3+"),
|
||||
Pair("4", "4+"),
|
||||
Pair("5", "5+"),
|
||||
Pair("6", "6+"),
|
||||
Pair("7", "7+"),
|
||||
Pair("8", "8+"),
|
||||
Pair("9", "9+"),
|
||||
)
|
||||
|
||||
private fun getFormatFilter(): Array<Pair<String?, String>> = arrayOf(
|
||||
Pair("0", "الكل"),
|
||||
Pair("BluRay", "BluRay"),
|
||||
Pair("WebRip", "WebRip"),
|
||||
Pair("BRRIP", "BRRIP"),
|
||||
Pair("DVDrip", "DVDrip"),
|
||||
Pair("DVDSCR", "DVDSCR"),
|
||||
Pair("HD", "HD"),
|
||||
Pair("HDTS", "HDTS"),
|
||||
Pair("HDTV", "HDTV"),
|
||||
Pair("CAM", "CAM"),
|
||||
Pair("WEB-DL", "WEB-DL"),
|
||||
Pair("HDTC", "HDTC"),
|
||||
Pair("BDRIP", "BDRIP"),
|
||||
Pair("HDRIP", "HDRIP"),
|
||||
Pair("HC+HDRIP", "HC HDRIP"),
|
||||
)
|
||||
|
||||
private fun getQualityFilter(): Array<Pair<String?, String>> = arrayOf(
|
||||
Pair("0", "الدقة"),
|
||||
Pair("240p", "240p"),
|
||||
Pair("360p", "360p"),
|
||||
Pair("480p", "480p"),
|
||||
Pair("720p", "720p"),
|
||||
Pair("1080p", "1080p"),
|
||||
Pair("3D", "3D"),
|
||||
Pair("4K", "4K"),
|
||||
)
|
||||
|
||||
private fun getSectionSFilter(): Array<Pair<String?, String>> = arrayOf(
|
||||
Pair("0", "القسم"),
|
||||
Pair("29", "عربي"),
|
||||
Pair("30", "اجنبي"),
|
||||
Pair("31", "هندي"),
|
||||
Pair("32", "تركي"),
|
||||
Pair("33", "اسيوي"),
|
||||
)
|
||||
|
||||
private fun getCategorySFilter(): Array<Pair<String?, String>> = arrayOf(
|
||||
Pair("0", "التصنيف"),
|
||||
Pair("87", "رمضان"),
|
||||
Pair("30", "انمي"),
|
||||
Pair("18", "اكشن"),
|
||||
Pair("71", "مدبلج"),
|
||||
Pair("72", "NETFLIX"),
|
||||
Pair("20", "كوميدي"),
|
||||
Pair("35", "اثارة"),
|
||||
Pair("34", "غموض"),
|
||||
Pair("33", "عائلي"),
|
||||
Pair("88", "اطفال"),
|
||||
Pair("25", "حربي"),
|
||||
Pair("32", "رياضي"),
|
||||
Pair("89", "قصير"),
|
||||
Pair("43", "فانتازيا"),
|
||||
Pair("24", "خيال علمي"),
|
||||
Pair("31", "موسيقى"),
|
||||
Pair("29", "سيرة ذاتية"),
|
||||
Pair("28", "وثائقي"),
|
||||
Pair("27", "رومانسي"),
|
||||
Pair("26", "تاريخي"),
|
||||
Pair("23", "دراما"),
|
||||
Pair("22", "رعب"),
|
||||
Pair("21", "جريمة"),
|
||||
Pair("19", "مغامرة"),
|
||||
Pair("91", "غربي"),
|
||||
)
|
||||
|
||||
private fun getRatingSFilter(): Array<Pair<String?, String>> = arrayOf(
|
||||
Pair("0", "التقييم"),
|
||||
Pair("1", "1+"),
|
||||
Pair("2", "2+"),
|
||||
Pair("3", "3+"),
|
||||
Pair("4", "4+"),
|
||||
Pair("5", "5+"),
|
||||
Pair("6", "6+"),
|
||||
Pair("7", "7+"),
|
||||
Pair("8", "8+"),
|
||||
Pair("9", "9+"),
|
||||
)
|
||||
|
||||
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String?, String>>) :
|
||||
AnimeFilter.Select<String>(displayName, vals.map { it.second }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].first
|
||||
}
|
||||
|
||||
// preferred quality settings
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val videoQualityPref = ListPreference(screen.context).apply {
|
||||
key = "preferred_quality"
|
||||
title = "Preferred quality"
|
||||
entries = arrayOf("1080p", "720p", "480p", "360p", "240p")
|
||||
entryValues = arrayOf("1080", "720", "480", "360", "240")
|
||||
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)
|
||||
}
|
||||
}
|
19
src/ar/anime4up/build.gradle
Normal file
|
@ -0,0 +1,19 @@
|
|||
ext {
|
||||
extName = 'Anime4up'
|
||||
extClass = '.Anime4Up'
|
||||
extVersionCode = 55
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:dood-extractor'))
|
||||
implementation(project(':lib:gdriveplayer-extractor'))
|
||||
implementation(project(':lib:mp4upload-extractor'))
|
||||
implementation(project(':lib:okru-extractor'))
|
||||
implementation(project(':lib:streamwish-extractor'))
|
||||
implementation(project(':lib:uqload-extractor'))
|
||||
implementation(project(':lib:vidbom-extractor'))
|
||||
implementation(project(':lib:voe-extractor'))
|
||||
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
|
||||
}
|
BIN
src/ar/anime4up/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
src/ar/anime4up/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src/ar/anime4up/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
src/ar/anime4up/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
src/ar/anime4up/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/ar/anime4up/res/play_store_512.png
Normal file
After Width: | Height: | Size: 71 KiB |
|
@ -0,0 +1,233 @@
|
|||
package eu.kanade.tachiyomi.animeextension.ar.anime4up
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Base64
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animeextension.ar.anime4up.extractors.SharedExtractor
|
||||
import eu.kanade.tachiyomi.animeextension.ar.anime4up.extractors.VidYardExtractor
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
|
||||
import eu.kanade.tachiyomi.lib.gdriveplayerextractor.GdrivePlayerExtractor
|
||||
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
|
||||
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
||||
import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor
|
||||
import eu.kanade.tachiyomi.lib.vidbomextractor.VidBomExtractor
|
||||
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
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
|
||||
|
||||
class Anime4Up : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val name = "Anime4Up"
|
||||
|
||||
override val baseUrl = "https://anime4up.cam"
|
||||
|
||||
override val lang = "ar"
|
||||
|
||||
override val supportsLatest = false
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder().add("Referer", "$baseUrl/")
|
||||
|
||||
// ============================== Popular ===============================
|
||||
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/anime-list-3/page/$page/")
|
||||
|
||||
override fun popularAnimeSelector() = "div.anime-list-content div.anime-card-poster > div.hover"
|
||||
|
||||
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
|
||||
element.selectFirst("img")!!.run {
|
||||
thumbnail_url = absUrl("src")
|
||||
title = attr("alt")
|
||||
}
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector() = "ul.pagination > li > a.next"
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
||||
override fun latestUpdatesSelector() = throw UnsupportedOperationException()
|
||||
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun getFilterList() = Anime4UpFilters.FILTER_LIST
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
if (query.isNotBlank()) {
|
||||
return GET("$baseUrl/?search_param=animes&s=$query", headers)
|
||||
}
|
||||
|
||||
return with(Anime4UpFilters.getSearchParameters(filters)) {
|
||||
val url = when {
|
||||
genre.isNotBlank() -> "$baseUrl/anime-genre/$genre"
|
||||
type.isNotBlank() -> "$baseUrl/anime-type/$type"
|
||||
status.isNotBlank() -> "$baseUrl/anime-status/$status"
|
||||
else -> throw Exception("اختر فلتر")
|
||||
}
|
||||
GET(url, headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
|
||||
override fun searchAnimeNextPageSelector() = popularAnimeNextPageSelector()
|
||||
override fun searchAnimeSelector() = popularAnimeSelector()
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
|
||||
val doc = document // Shortcut
|
||||
|
||||
thumbnail_url = doc.selectFirst("img.thumbnail")!!.attr("src")
|
||||
title = doc.selectFirst("h1.anime-details-title")!!.text()
|
||||
// Genres + useful info
|
||||
genre = doc.select("ul.anime-genres > li > a, div.anime-info > a").eachText().joinToString()
|
||||
|
||||
description = buildString {
|
||||
// Additional info
|
||||
doc.select("div.anime-info").eachText().forEach {
|
||||
append("$it\n")
|
||||
}
|
||||
// Description
|
||||
doc.selectFirst("p.anime-story")?.text()?.also {
|
||||
append("\n$it")
|
||||
}
|
||||
}
|
||||
|
||||
doc.selectFirst("div.anime-info:contains(حالة الأنمي)")?.text()?.also {
|
||||
status = when {
|
||||
it.contains("يعرض الان", true) -> SAnime.ONGOING
|
||||
it.contains("مكتمل", true) -> SAnime.COMPLETED
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
override fun episodeListParse(response: Response) = super.episodeListParse(response).reversed()
|
||||
|
||||
override fun episodeListSelector() = "div.ehover6 > div.episodes-card-title > h3 > a, ul.all-episodes-list li > a"
|
||||
|
||||
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
name = element.text()
|
||||
episode_number = name.substringAfterLast(" ").toFloatOrNull() ?: 0F
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
@Serializable
|
||||
data class Qualities(
|
||||
val fhd: Map<String, String> = emptyMap(),
|
||||
val hd: Map<String, String> = emptyMap(),
|
||||
val sd: Map<String, String> = emptyMap(),
|
||||
)
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val base64 = response.asJsoup().selectFirst("input[name=wl]")
|
||||
?.attr("value")
|
||||
?.let { String(Base64.decode(it, Base64.DEFAULT)) }
|
||||
?: return emptyList()
|
||||
|
||||
val parsedData = json.decodeFromString<Qualities>(base64)
|
||||
val streamLinks = with(parsedData) { fhd + hd + sd }
|
||||
|
||||
return streamLinks.values.distinct().parallelCatchingFlatMapBlocking(::extractVideos)
|
||||
}
|
||||
|
||||
private val uqloadExtractor by lazy { UqloadExtractor(client) }
|
||||
private val doodExtractor by lazy { DoodExtractor(client) }
|
||||
private val gdriveplayerExtractor by lazy { GdrivePlayerExtractor(client) }
|
||||
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
|
||||
private val okruExtractor by lazy { OkruExtractor(client) }
|
||||
private val sharedExtractor by lazy { SharedExtractor(client) }
|
||||
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
|
||||
private val vidbomExtractor by lazy { VidBomExtractor(client) }
|
||||
private val vidyardExtractor by lazy { VidYardExtractor(client, headers) }
|
||||
private val voeExtractor by lazy { VoeExtractor(client) }
|
||||
|
||||
private fun extractVideos(url: String): List<Video> {
|
||||
return when {
|
||||
url.contains("drive.google") -> {
|
||||
val embedUrlG = "https://gdriveplayer.to/embed2.php?link=$url"
|
||||
gdriveplayerExtractor.videosFromUrl(embedUrlG, "GdrivePlayer", headers)
|
||||
}
|
||||
url.contains("vidyard") -> vidyardExtractor.videosFromUrl(url)
|
||||
url.contains("ok.ru") -> okruExtractor.videosFromUrl(url)
|
||||
url.contains("mp4upload") -> mp4uploadExtractor.videosFromUrl(url, headers)
|
||||
url.contains("uqload") -> uqloadExtractor.videosFromUrl(url)
|
||||
url.contains("voe") -> voeExtractor.videosFromUrl(url)
|
||||
url.contains("shared") -> sharedExtractor.videosFromUrl(url)?.let(::listOf)
|
||||
DOOD_REGEX.containsMatchIn(url) -> doodExtractor.videosFromUrl(url, "Dood mirror")
|
||||
VIDBOM_REGEX.containsMatchIn(url) -> vidbomExtractor.videosFromUrl(url)
|
||||
STREAMWISH_REGEX.containsMatchIn(url) -> streamwishExtractor.videosFromUrl(url) { "Mirror: $it" }
|
||||
else -> null
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
override fun videoListSelector() = throw UnsupportedOperationException()
|
||||
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
// ============================== Settings ==============================
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = PREF_QUALITY_TITLE
|
||||
entries = PREF_QUALITY_ENTRIES
|
||||
entryValues = PREF_QUALITY_VALUES
|
||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
|
||||
return sortedWith(
|
||||
compareBy { it.quality.contains(quality) },
|
||||
).reversed()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val VIDBOM_REGEX = Regex("(?:v[aie]d[bp][aoe]?m|myvii?d|segavid|v[aei]{1,2}dshar[er]?)\\.(?:com|net|org|xyz)(?::\\d+)?/(?:embed[/-])?([A-Za-z0-9]+)")
|
||||
private val DOOD_REGEX = Regex("(do*d(?:stream)?\\.(?:com?|watch|to|s[ho]|cx|la|w[sf]|pm|re|yt|stream))/[de]/([0-9a-zA-Z]+)")
|
||||
private val STREAMWISH_REGEX = Regex("((?:streamwish|anime7u|animezd|ajmidyad|khadhnayad|yadmalik|hayaatieadhab)\\.(?:com|to|sbs))/(?:e/|v/|f/)?([0-9a-zA-Z]+)")
|
||||
|
||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||
private const val PREF_QUALITY_TITLE = "Preferred quality"
|
||||
private const val PREF_QUALITY_DEFAULT = "1080p"
|
||||
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p", "dood")
|
||||
private val PREF_QUALITY_VALUES = PREF_QUALITY_ENTRIES
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
package eu.kanade.tachiyomi.animeextension.ar.anime4up
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
|
||||
object Anime4UpFilters {
|
||||
open class QueryPartFilter(
|
||||
displayName: String,
|
||||
val vals: Array<Pair<String, String>>,
|
||||
) : AnimeFilter.Select<String>(
|
||||
displayName,
|
||||
vals.map { it.first }.toTypedArray(),
|
||||
) {
|
||||
fun toQueryPart() = vals[state].second
|
||||
}
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
|
||||
return (first { it is R } as QueryPartFilter).toQueryPart()
|
||||
}
|
||||
|
||||
internal class GenreFilter : QueryPartFilter("تصنيف الأنمي", Anime4UpFiltersData.GENRES)
|
||||
internal class TypeFilter : QueryPartFilter("نوع الأنمي", Anime4UpFiltersData.TYPES)
|
||||
internal class StatusFilter : QueryPartFilter("حالة الأنمي", Anime4UpFiltersData.STATUS)
|
||||
|
||||
val FILTER_LIST get() = AnimeFilterList(
|
||||
AnimeFilter.Header("الفلترات مش هتشتغل لو بتبحث او وهي فاضيه"),
|
||||
GenreFilter(),
|
||||
TypeFilter(),
|
||||
StatusFilter(),
|
||||
)
|
||||
|
||||
data class FilterSearchParams(
|
||||
val genre: String = "",
|
||||
val type: String = "",
|
||||
val status: String = "",
|
||||
)
|
||||
|
||||
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
|
||||
if (filters.isEmpty()) return FilterSearchParams()
|
||||
|
||||
return FilterSearchParams(
|
||||
filters.asQueryPart<GenreFilter>(),
|
||||
filters.asQueryPart<TypeFilter>(),
|
||||
filters.asQueryPart<StatusFilter>(),
|
||||
)
|
||||
}
|
||||
|
||||
private object Anime4UpFiltersData {
|
||||
private val ANY = Pair("أختر", "")
|
||||
|
||||
val GENRES = arrayOf(
|
||||
ANY,
|
||||
Pair("أطفال", "%d8%a3%d8%b7%d9%81%d8%a7%d9%84"),
|
||||
Pair("أكشن", "%d8%a3%d9%83%d8%b4%d9%86/"),
|
||||
Pair("إيتشي", "%d8%a5%d9%8a%d8%aa%d8%b4%d9%8a/"),
|
||||
Pair("اثارة", "%d8%a7%d8%ab%d8%a7%d8%b1%d8%a9/"),
|
||||
Pair("العاب", "%d8%a7%d9%84%d8%b9%d8%a7%d8%a8/"),
|
||||
Pair("بوليسي", "%d8%a8%d9%88%d9%84%d9%8a%d8%b3%d9%8a/"),
|
||||
Pair("تاريخي", "%d8%aa%d8%a7%d8%b1%d9%8a%d8%ae%d9%8a/"),
|
||||
Pair("جنون", "%d8%ac%d9%86%d9%88%d9%86/"),
|
||||
Pair("جوسي", "%d8%ac%d9%88%d8%b3%d9%8a/"),
|
||||
Pair("حربي", "%d8%ad%d8%b1%d8%a8%d9%8a/"),
|
||||
Pair("حريم", "%d8%ad%d8%b1%d9%8a%d9%85/"),
|
||||
Pair("خارق للعادة", "%d8%ae%d8%a7%d8%b1%d9%82-%d9%84%d9%84%d8%b9%d8%a7%d8%af%d8%a9/"),
|
||||
Pair("خيال علمي", "%d8%ae%d9%8a%d8%a7%d9%84-%d8%b9%d9%84%d9%85%d9%8a/"),
|
||||
Pair("دراما", "%d8%af%d8%b1%d8%a7%d9%85%d8%a7/"),
|
||||
Pair("رعب", "%d8%b1%d8%b9%d8%a8/"),
|
||||
Pair("رومانسي", "%d8%b1%d9%88%d9%85%d8%a7%d9%86%d8%b3%d9%8a/"),
|
||||
Pair("رياضي", "%d8%b1%d9%8a%d8%a7%d8%b6%d9%8a/"),
|
||||
Pair("ساموراي", "%d8%b3%d8%a7%d9%85%d9%88%d8%b1%d8%a7%d9%8a/"),
|
||||
Pair("سحر", "%d8%b3%d8%ad%d8%b1/"),
|
||||
Pair("سينين", "%d8%b3%d9%8a%d9%86%d9%8a%d9%86/"),
|
||||
Pair("شريحة من الحياة", "%d8%b4%d8%b1%d9%8a%d8%ad%d8%a9-%d9%85%d9%86-%d8%a7%d9%84%d8%ad%d9%8a%d8%a7%d8%a9/"),
|
||||
Pair("شوجو", "%d8%b4%d9%88%d8%ac%d9%88/"),
|
||||
Pair("شوجو اَي", "%d8%b4%d9%88%d8%ac%d9%88-%d8%a7%d9%8e%d9%8a/"),
|
||||
Pair("شونين", "%d8%b4%d9%88%d9%86%d9%8a%d9%86/"),
|
||||
Pair("شونين اي", "%d8%b4%d9%88%d9%86%d9%8a%d9%86-%d8%a7%d9%8a/"),
|
||||
Pair("شياطين", "%d8%b4%d9%8a%d8%a7%d8%b7%d9%8a%d9%86/"),
|
||||
Pair("غموض", "%d8%ba%d9%85%d9%88%d8%b6/"),
|
||||
Pair("فضائي", "%d9%81%d8%b6%d8%a7%d8%a6%d9%8a/"),
|
||||
Pair("فنتازيا", "%d9%81%d9%86%d8%aa%d8%a7%d8%b2%d9%8a%d8%a7/"),
|
||||
Pair("فنون قتالية", "%d9%81%d9%86%d9%88%d9%86-%d9%82%d8%aa%d8%a7%d9%84%d9%8a%d8%a9/"),
|
||||
Pair("قوى خارقة", "%d9%82%d9%88%d9%89-%d8%ae%d8%a7%d8%b1%d9%82%d8%a9/"),
|
||||
Pair("كوميدي", "%d9%83%d9%88%d9%85%d9%8a%d8%af%d9%8a/"),
|
||||
Pair("محاكاة ساخرة", "%d9%85%d8%ad%d8%a7%d9%83%d8%a7%d8%a9-%d8%b3%d8%a7%d8%ae%d8%b1%d8%a9/"),
|
||||
Pair("مدرسي", "%d9%85%d8%af%d8%b1%d8%b3%d9%8a/"),
|
||||
Pair("مصاصي دماء", "%d9%85%d8%b5%d8%a7%d8%b5%d9%8a-%d8%af%d9%85%d8%a7%d8%a1/"),
|
||||
Pair("مغامرات", "%d9%85%d8%ba%d8%a7%d9%85%d8%b1%d8%a7%d8%aa/"),
|
||||
Pair("موسيقي", "%d9%85%d9%88%d8%b3%d9%8a%d9%82%d9%8a/"),
|
||||
Pair("ميكا", "%d9%85%d9%8a%d9%83%d8%a7/"),
|
||||
Pair("نفسي", "%d9%86%d9%81%d8%b3%d9%8a/"),
|
||||
)
|
||||
|
||||
val TYPES = arrayOf(
|
||||
ANY,
|
||||
Pair("Movie", "movie-3"),
|
||||
Pair("ONA", "ona1"),
|
||||
Pair("OVA", "ova1"),
|
||||
Pair("Special", "special1"),
|
||||
Pair("TV", "tv2"),
|
||||
)
|
||||
|
||||
val STATUS = arrayOf(
|
||||
ANY,
|
||||
Pair("لم يعرض بعد", "%d9%84%d9%85-%d9%8a%d8%b9%d8%b1%d8%b6-%d8%a8%d8%b9%d8%af"),
|
||||
Pair("مكتمل", "complete"),
|
||||
Pair("يعرض الان", "%d9%8a%d8%b9%d8%b1%d8%b6-%d8%a7%d9%84%d8%a7%d9%86-1"),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package eu.kanade.tachiyomi.animeextension.ar.anime4up.extractors
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class SharedExtractor(private val client: OkHttpClient) {
|
||||
fun videosFromUrl(url: String, quality: String = "mirror"): Video? {
|
||||
val document = client.newCall(GET(url)).execute().asJsoup()
|
||||
return document.selectFirst("source")?.let {
|
||||
Video(it.attr("src"), "4Shared: $quality", it.attr("src"))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package eu.kanade.tachiyomi.animeextension.ar.anime4up.extractors
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class VidYardExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||
private val newHeaders by lazy {
|
||||
headers.newBuilder().set("Referer", VIDYARD_URL).build()
|
||||
}
|
||||
|
||||
fun videosFromUrl(url: String): List<Video> {
|
||||
val id = url.substringAfter("com/").substringBefore("?")
|
||||
val playerUrl = "$VIDYARD_URL/player/$id.json"
|
||||
val callPlayer = client.newCall(GET(playerUrl, newHeaders)).execute()
|
||||
.body.string()
|
||||
|
||||
val data = callPlayer.substringAfter("hls\":[").substringBefore("]")
|
||||
val sources = data.split("profile\":\"").drop(1)
|
||||
|
||||
return sources.map { source ->
|
||||
val src = source.substringAfter("url\":\"").substringBefore("\"")
|
||||
val quality = source.substringBefore("\"")
|
||||
Video(src, quality, src, headers = newHeaders)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val VIDYARD_URL = "https://play.vidyard.com"
|
12
src/ar/animeblkom/build.gradle
Normal file
|
@ -0,0 +1,12 @@
|
|||
ext {
|
||||
extName = 'Anime Blkom'
|
||||
extClass = '.AnimeBlkom'
|
||||
extVersionCode = 17
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:mp4upload-extractor'))
|
||||
implementation(project(":lib:okru-extractor"))
|
||||
}
|
BIN
src/ar/animeblkom/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
src/ar/animeblkom/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
src/ar/animeblkom/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4 KiB |
BIN
src/ar/animeblkom/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.1 KiB |
BIN
src/ar/animeblkom/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/ar/animeblkom/res/web_hi_res_512.png
Normal file
After Width: | Height: | Size: 55 KiB |
|
@ -0,0 +1,252 @@
|
|||
package eu.kanade.tachiyomi.animeextension.ar.animeblkom
|
||||
|
||||
import android.app.Application
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
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.lib.mp4uploadextractor.Mp4uploadExtractor
|
||||
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.lang.Exception
|
||||
|
||||
class AnimeBlkom : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val name = "أنمي بالكوم"
|
||||
|
||||
override val baseUrl by lazy { preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!! }
|
||||
|
||||
override val lang = "ar"
|
||||
|
||||
override val supportsLatest = false
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("referer", baseUrl)
|
||||
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
// ============================== Popular ===============================
|
||||
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/animes-list/?sort_by=rate&page=$page", headers)
|
||||
|
||||
override fun popularAnimeSelector() = "div.contents div.poster > a"
|
||||
|
||||
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
|
||||
val img = element.selectFirst("img")!!
|
||||
thumbnail_url = img.attr("abs:data-original")
|
||||
title = img.attr("alt").removeSuffix(" poster")
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector() = "ul.pagination li.page-item a[rel=next]"
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesSelector(): String = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = throw UnsupportedOperationException()
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
|
||||
|
||||
override fun searchAnimeNextPageSelector() = popularAnimeNextPageSelector()
|
||||
|
||||
override fun searchAnimeSelector() = popularAnimeSelector()
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val url = if (query.isNotBlank()) {
|
||||
"$baseUrl/search?query=$query&page=$page"
|
||||
} else {
|
||||
filters
|
||||
.filterIsInstance<TypeList>()
|
||||
.firstOrNull()
|
||||
?.takeIf { it.state > 0 }
|
||||
?.let { filter ->
|
||||
val genreN = getTypeList()[filter.state].query
|
||||
"$baseUrl/$genreN?page=$page"
|
||||
}
|
||||
?: throw Exception("اختر فلتر")
|
||||
}
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
|
||||
thumbnail_url = document.selectFirst("div.poster img")!!.attr("abs:data-original")
|
||||
title = document.selectFirst("div.name span h1")!!.text()
|
||||
genre = document.select("p.genres a").joinToString { it.text() }
|
||||
description = document.selectFirst("div.story p, div.story")?.text()
|
||||
author = document.selectFirst("div:contains(الاستديو) span > a")?.text()
|
||||
status = document.selectFirst("div.info-table div:contains(حالة الأنمي) span.info")?.text()?.let {
|
||||
when {
|
||||
it.contains("مستمر") -> SAnime.ONGOING
|
||||
it.contains("مكتمل") -> SAnime.COMPLETED
|
||||
else -> null
|
||||
}
|
||||
} ?: SAnime.UNKNOWN
|
||||
artist = document.selectFirst("div:contains(المخرج) > span.info")?.text()
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val document = response.asJsoup()
|
||||
if (document.selectFirst(episodeListSelector()) == null) {
|
||||
return oneEpisodeParse(document)
|
||||
}
|
||||
return document.select(episodeListSelector()).map(::episodeFromElement).reversed()
|
||||
}
|
||||
|
||||
private fun oneEpisodeParse(document: Document): List<SEpisode> {
|
||||
return SEpisode.create().apply {
|
||||
setUrlWithoutDomain(document.location())
|
||||
episode_number = 1F
|
||||
name = document.selectFirst("div.name.col-xs-12 span h1")!!.text()
|
||||
}.let(::listOf)
|
||||
}
|
||||
|
||||
override fun episodeListSelector() = "ul.episodes-links li a"
|
||||
|
||||
override fun episodeFromElement(element: Element): SEpisode {
|
||||
return SEpisode.create().apply {
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
|
||||
val eptitle = element.selectFirst("span:nth-child(3)")!!.text()
|
||||
val epNum = eptitle.filter { it.isDigit() }
|
||||
episode_number = when {
|
||||
epNum.isNotEmpty() -> epNum.toFloatOrNull() ?: 1F
|
||||
else -> 1F
|
||||
}
|
||||
name = eptitle + " :" + element.selectFirst("span:nth-child(1)")!!.text()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
return document.select("span.server a").flatMap {
|
||||
runCatching { extractVideos(it) }.getOrElse { emptyList() }
|
||||
}
|
||||
}
|
||||
|
||||
private val okruExtractor by lazy { OkruExtractor(client) }
|
||||
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
|
||||
|
||||
private fun extractVideos(element: Element): List<Video> {
|
||||
val url = element.attr("data-src").replace("http://", "https://")
|
||||
return when {
|
||||
".vid4up" in url || "Blkom" in element.text() -> {
|
||||
val videoDoc = client.newCall(GET(url, headers)).execute()
|
||||
.asJsoup()
|
||||
videoDoc.select(videoListSelector()).map(::videoFromElement)
|
||||
}
|
||||
"ok.ru" in url -> okruExtractor.videosFromUrl(url)
|
||||
"mp4upload" in url -> mp4uploadExtractor.videosFromUrl(url, headers)
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
override fun videoListSelector() = "source"
|
||||
|
||||
override fun videoFromElement(element: Element): Video {
|
||||
val videoUrl = element.attr("src")
|
||||
return Video(videoUrl, "Blkom - " + element.attr("label"), videoUrl, headers)
|
||||
}
|
||||
|
||||
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
// ============================== Filters ===============================
|
||||
override fun getFilterList() = AnimeFilterList(
|
||||
AnimeFilter.Header("الفلترات مش هتشتغل لو بتبحث او وهي فاضيه"),
|
||||
TypeList(typesName),
|
||||
)
|
||||
|
||||
private class TypeList(types: Array<String>) : AnimeFilter.Select<String>("نوع الأنمي", types)
|
||||
private data class Type(val name: String, val query: String)
|
||||
private val typesName = getTypeList().map {
|
||||
it.name
|
||||
}.toTypedArray()
|
||||
|
||||
private fun getTypeList() = listOf(
|
||||
Type("اختر", ""),
|
||||
Type("قائمة الأنمي", "anime-list"),
|
||||
Type(" قائمة المسلسلات ", "series-list"),
|
||||
Type(" قائمة الأفلام ", "movie-list"),
|
||||
Type(" قائمة الأوفا ", "ova-list"),
|
||||
Type(" قائمة الأونا ", "ona-list"),
|
||||
Type(" قائمة الحلقات خاصة ", "special-list"),
|
||||
)
|
||||
|
||||
// ============================== Settings ==============================
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = PREF_QUALITY_TITLE
|
||||
entries = PREF_QUALITY_ENTRIES
|
||||
entryValues = PREF_QUALITY_ENTRIES
|
||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_DOMAIN_KEY
|
||||
title = PREF_DOMAIN_TITLE
|
||||
entries = PREF_DOMAIN_ENTRIES
|
||||
entryValues = PREF_DOMAIN_VALUES
|
||||
setDefaultValue(PREF_DOMAIN_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
return sortedWith(
|
||||
compareBy { it.quality.contains(quality) },
|
||||
).reversed()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||
private const val PREF_QUALITY_TITLE = "Preferred quality"
|
||||
private const val PREF_QUALITY_DEFAULT = "720p"
|
||||
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p", "240p")
|
||||
|
||||
private const val PREF_DOMAIN_KEY = "pref_domain_key"
|
||||
private const val PREF_DOMAIN_TITLE = "Preferred domain"
|
||||
private const val PREF_DOMAIN_DEFAULT = "https://animeblkom.net"
|
||||
private val PREF_DOMAIN_ENTRIES = arrayOf("animeblkom.net", "animeblkom.tv", "blkom.com")
|
||||
private val PREF_DOMAIN_VALUES by lazy {
|
||||
PREF_DOMAIN_ENTRIES.map { "https://$it" }.toTypedArray()
|
||||
}
|
||||
}
|
||||
}
|
7
src/ar/animeiat/build.gradle
Normal file
|
@ -0,0 +1,7 @@
|
|||
ext {
|
||||
extName = 'Animeiat'
|
||||
extClass = '.Animeiat'
|
||||
extVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/ar/animeiat/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
src/ar/animeiat/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
src/ar/animeiat/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
src/ar/animeiat/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
src/ar/animeiat/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
src/ar/animeiat/res/web_hi_res_512.png
Normal file
After Width: | Height: | Size: 76 KiB |
|
@ -0,0 +1,232 @@
|
|||
package eu.kanade.tachiyomi.animeextension.ar.animeiat
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animeextension.ar.animeiat.dto.AnimeEpisodesList
|
||||
import eu.kanade.tachiyomi.animeextension.ar.animeiat.dto.AnimePageResponse
|
||||
import eu.kanade.tachiyomi.animeextension.ar.animeiat.dto.LatestAnimeResponse
|
||||
import eu.kanade.tachiyomi.animeextension.ar.animeiat.dto.PopularAnimeResponse
|
||||
import eu.kanade.tachiyomi.animeextension.ar.animeiat.dto.StreamLinks
|
||||
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.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Base64
|
||||
|
||||
class Animeiat : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
|
||||
override val name = "Animeiat"
|
||||
|
||||
override val baseUrl = "https://api.animeiat.co/v1"
|
||||
|
||||
override val lang = "ar"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
// ============================== Popular ===============================
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val responseJson = json.decodeFromString<PopularAnimeResponse>(response.body.string())
|
||||
val animeList = responseJson.data.map {
|
||||
SAnime.create().apply {
|
||||
url = "/anime/${it.slug}"
|
||||
title = it.anime_name
|
||||
thumbnail_url = "https://api.animeiat.co/storage/${it.poster_path}"
|
||||
}
|
||||
}
|
||||
val hasNextPage = responseJson.meta.current_page < responseJson.meta.last_page
|
||||
return AnimesPage(animeList, hasNextPage)
|
||||
}
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/anime?page=$page")
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val episodeList = mutableListOf<SEpisode>()
|
||||
val pagesUrl = response.request.url.toString() + "/episodes"
|
||||
val firstPage = client.newCall(GET(pagesUrl)).execute()
|
||||
fun addEpisodes(res: Response) {
|
||||
val jr = json.decodeFromString<AnimeEpisodesList>(res.body.string())
|
||||
episodeList.addAll(
|
||||
jr.data.map {
|
||||
SEpisode.create().apply {
|
||||
name = it.title
|
||||
episode_number = it.number
|
||||
url = "episode/${it.slug}"
|
||||
}
|
||||
},
|
||||
)
|
||||
jr.links.next?.let {
|
||||
addEpisodes(client.newCall(GET(it)).execute())
|
||||
}
|
||||
}
|
||||
addEpisodes(firstPage)
|
||||
return episodeList.reversed()
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override fun videoListRequest(episode: SEpisode): Request = GET("$baseUrl/${episode.url}")
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val playerHash = response.body.string().substringAfter("\"hash\":\"").substringBefore("\"")
|
||||
val playerID = String(Base64.getDecoder().decode(playerHash)).split("\"").reversed()[1]
|
||||
val playerUrls = client.newCall(GET("$baseUrl/video/$playerID")).execute()
|
||||
val jr = json.decodeFromString<StreamLinks>(playerUrls.body.string()).data
|
||||
return jr.sources.map {
|
||||
Video(
|
||||
it.file,
|
||||
"${it.label} ${it.quality}",
|
||||
it.file,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
return sortedWith(
|
||||
compareBy { it.quality.contains(quality) },
|
||||
).reversed()
|
||||
}
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun animeDetailsParse(response: Response): SAnime {
|
||||
val details = json.decodeFromString<AnimePageResponse>(response.body.string()).data
|
||||
val anime = SAnime.create().apply {
|
||||
url = "/anime/${details.slug}"
|
||||
title = details.anime_name
|
||||
status = when (details.status) {
|
||||
"ongoing" -> SAnime.ONGOING
|
||||
"completed" -> SAnime.COMPLETED
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
author = details.studios.joinToString { it.name }
|
||||
genre = details.genres.joinToString { it.name }
|
||||
description = details.story
|
||||
thumbnail_url = "https://api.animeiat.co/storage/${details.poster_path}"
|
||||
}
|
||||
return anime
|
||||
}
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun searchAnimeParse(response: Response): AnimesPage = popularAnimeParse(response)
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
if (query.isNotEmpty()) {
|
||||
return GET("$baseUrl/anime?q=$query&page=$page")
|
||||
} else {
|
||||
var type = ""
|
||||
var status = ""
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
is TypeCategoryList -> {
|
||||
type = getTypeFilterList()[filter.state].query
|
||||
}
|
||||
is StatCategoryList -> {
|
||||
status = getStatFilterList()[filter.state].query
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
type = if (type.isEmpty()) "" else "&type=$type"
|
||||
status = if (status.isEmpty()) "" else "&status=$status"
|
||||
return GET("$baseUrl/anime?page=$page$type$status")
|
||||
}
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||
val responseJson = json.decodeFromString<LatestAnimeResponse>(response.body.string())
|
||||
val animeList = responseJson.data.map {
|
||||
SAnime.create().apply {
|
||||
url = "/anime/${it.slug.substringBefore("-episode-")}"
|
||||
title = it.title
|
||||
thumbnail_url = "https://api.animeiat.co/storage/${it.poster_path}"
|
||||
}
|
||||
}
|
||||
val hasNextPage = responseJson.meta.current_page < responseJson.meta.last_page
|
||||
return AnimesPage(animeList, hasNextPage)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/home/sticky-episodes?page=$page")
|
||||
|
||||
// ============================== filters ==============================
|
||||
override fun getFilterList() = AnimeFilterList(
|
||||
AnimeFilter.Header("فلترة الموقع"),
|
||||
TypeCategoryList(typeFilterList),
|
||||
StatCategoryList(statFilterList),
|
||||
)
|
||||
private class TypeCategoryList(categories: Array<String>) : AnimeFilter.Select<String>("النوع", categories)
|
||||
private class StatCategoryList(categories: Array<String>) : AnimeFilter.Select<String>("الحالة", categories)
|
||||
|
||||
private data class CatUnit(val name: String, val query: String)
|
||||
|
||||
private val typeFilterList = getTypeFilterList().map { it.name }.toTypedArray()
|
||||
private val statFilterList = getStatFilterList().map { it.name }.toTypedArray()
|
||||
|
||||
private fun getTypeFilterList() = listOf(
|
||||
CatUnit("اختر", ""),
|
||||
CatUnit("فيلم", "movie"),
|
||||
CatUnit("اوفا", "ova"),
|
||||
CatUnit("اونا", "ona"),
|
||||
CatUnit("حلقة خاصة", "special"),
|
||||
)
|
||||
private fun getStatFilterList() = listOf(
|
||||
CatUnit("اختر", ""),
|
||||
CatUnit("جارى رفعة", "uploading"),
|
||||
CatUnit("مكتمل", "completed"),
|
||||
CatUnit("يعرض حاليا", "ongoing"),
|
||||
CatUnit("قريبا", "upcoming"),
|
||||
)
|
||||
|
||||
// =============================== Preferences ===============================
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val videoQualityPref = ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = PREF_QUALITY_TITLE
|
||||
entries = PREF_QUALITY_ENTRIES
|
||||
entryValues = PREF_QUALITY_VALUES
|
||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}
|
||||
screen.addPreference(videoQualityPref)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||
private const val PREF_QUALITY_TITLE = "Preferred quality"
|
||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p", "240p")
|
||||
private val PREF_QUALITY_VALUES by lazy {
|
||||
PREF_QUALITY_ENTRIES.map { it.substringBefore("p") }.toTypedArray()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
package eu.kanade.tachiyomi.animeextension.ar.animeiat.dto
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class PopularAnimeResponse(
|
||||
@SerialName("data")
|
||||
val `data`: List<PopularAnimeList>,
|
||||
val links: Links,
|
||||
val meta: Meta,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class LatestAnimeResponse(
|
||||
@SerialName("data")
|
||||
val `data`: List<EpisodesList>,
|
||||
val links: Links,
|
||||
val meta: Meta,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PopularAnimeList(
|
||||
val anime_name: String,
|
||||
val poster_path: String,
|
||||
val slug: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Links(
|
||||
val first: String?,
|
||||
val last: String?,
|
||||
val next: String?,
|
||||
val prev: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Meta(
|
||||
val current_page: Int,
|
||||
val last_page: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AnimePageResponse(
|
||||
@SerialName("data")
|
||||
val `data`: AnimeDetails,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AnimeDetails(
|
||||
val anime_name: String,
|
||||
val genres: List<Genre>,
|
||||
val poster_path: String,
|
||||
val slug: String,
|
||||
val status: String,
|
||||
val story: String,
|
||||
val studios: List<Studio>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Genre(val name: String)
|
||||
|
||||
@Serializable
|
||||
data class Studio(val name: String)
|
||||
|
||||
@Serializable
|
||||
data class AnimeEpisodesList(
|
||||
val `data`: List<EpisodesList>,
|
||||
val links: Links,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class EpisodesList(
|
||||
val number: Float,
|
||||
val slug: String,
|
||||
val title: String,
|
||||
val poster_path: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class StreamLinks(
|
||||
val `data`: VideoInformation,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VideoInformation(
|
||||
val sources: List<Sources>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Sources(
|
||||
val `file`: String,
|
||||
val label: String,
|
||||
val name: String,
|
||||
val newfile: String,
|
||||
val quality: String,
|
||||
)
|
14
src/ar/animelek/build.gradle
Normal file
|
@ -0,0 +1,14 @@
|
|||
ext {
|
||||
extName = 'AnimeLek'
|
||||
extClass = '.AnimeLek'
|
||||
extVersionCode = 29
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:streamtape-extractor'))
|
||||
implementation(project(':lib:okru-extractor'))
|
||||
implementation(project(':lib:dood-extractor'))
|
||||
implementation(project(':lib:vidbom-extractor'))
|
||||
}
|
BIN
src/ar/animelek/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
src/ar/animelek/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
src/ar/animelek/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
src/ar/animelek/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.8 KiB |
BIN
src/ar/animelek/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
src/ar/animelek/res/play_store_512.png
Normal file
After Width: | Height: | Size: 43 KiB |
|
@ -0,0 +1,197 @@
|
|||
package eu.kanade.tachiyomi.animeextension.ar.animelek
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animeextension.ar.animelek.extractors.SharedExtractor
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
|
||||
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||
import eu.kanade.tachiyomi.lib.vidbomextractor.VidBomExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
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
|
||||
|
||||
class AnimeLek : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val name = "AnimeLek"
|
||||
|
||||
override val baseUrl = "https://animelek.me"
|
||||
|
||||
override val lang = "ar"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
// ============================== Popular ===============================
|
||||
override fun popularAnimeSelector() = "div.slider-episode-container div.episodes-card-container"
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request = GET(baseUrl)
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
return SAnime.create().apply {
|
||||
val ahref = element.selectFirst("h3 a")!!
|
||||
setUrlWithoutDomain(ahref.attr("href"))
|
||||
title = ahref.text()
|
||||
thumbnail_url = element.selectFirst("img")?.attr("src")
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector() = null
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
override fun episodeListSelector() = "div.ep-card-anime-title-detail h3 a"
|
||||
|
||||
override fun episodeFromElement(element: Element): SEpisode {
|
||||
return SEpisode.create().apply {
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
val text = element.text()
|
||||
name = text
|
||||
val epNum = text.filter { it.isDigit() }
|
||||
episode_number = when {
|
||||
epNum.isNotEmpty() -> epNum.toFloatOrNull() ?: 1F
|
||||
else -> 1F
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun episodeListParse(response: Response) = super.episodeListParse(response).reversed()
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override fun videoListParse(response: Response) = videosFromElement(response.asJsoup())
|
||||
|
||||
override fun videoListSelector() = "ul#episode-servers li.watch a"
|
||||
|
||||
private fun videosFromElement(document: Document): List<Video> {
|
||||
val vidbomServers = listOf("vidbam", "vadbam", "vidbom", "vidbm")
|
||||
|
||||
return document.select(videoListSelector()).mapNotNull { element ->
|
||||
val url = element.attr("data-ep-url")
|
||||
val qualityy = element.text()
|
||||
|
||||
when {
|
||||
vidbomServers.any(url::contains) -> {
|
||||
VidBomExtractor(client).videosFromUrl(url)
|
||||
}
|
||||
|
||||
url.contains("dood") -> {
|
||||
DoodExtractor(client).videoFromUrl(url, qualityy)
|
||||
?.let(::listOf)
|
||||
}
|
||||
url.contains("ok.ru") -> {
|
||||
OkruExtractor(client).videosFromUrl(url)
|
||||
}
|
||||
url.contains("streamtape") -> {
|
||||
StreamTapeExtractor(client).videoFromUrl(url)
|
||||
?.let(::listOf)
|
||||
}
|
||||
url.contains("4shared") -> {
|
||||
SharedExtractor(client).videoFromUrl(url, qualityy)
|
||||
?.let(::listOf)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}.flatten()
|
||||
}
|
||||
|
||||
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
// =============================== Search ===============================
|
||||
// TODO: Add search filters
|
||||
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String = "li.page-item a[rel=next]"
|
||||
|
||||
override fun searchAnimeSelector() = "div.anime-list-content div.anime-card-container"
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) = GET("$baseUrl/search/?s=$query&page=$page")
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun animeDetailsParse(document: Document): SAnime {
|
||||
return SAnime.create().apply {
|
||||
val infos = document.selectFirst("div.anime-container-infos")!!
|
||||
val datas = document.selectFirst("div.anime-container-data")!!
|
||||
thumbnail_url = infos.selectFirst("img")!!.attr("src")
|
||||
title = datas.selectFirst("h1")!!.text()
|
||||
genre = datas.select("ul li > a").joinToString { it.text() }
|
||||
status = infos.selectFirst("div.full-list-info:contains(حالة الأنمي) a")?.text()?.let {
|
||||
when {
|
||||
it.contains("يعرض الان") -> SAnime.ONGOING
|
||||
it.contains("مكتمل", true) -> SAnime.COMPLETED
|
||||
else -> null
|
||||
}
|
||||
} ?: SAnime.UNKNOWN
|
||||
|
||||
artist = document.selectFirst("div:contains(المخرج) > span.info")?.text()
|
||||
|
||||
description = buildString {
|
||||
append(datas.selectFirst("p.anime-story")!!.text() + "\n\n")
|
||||
|
||||
infos.select("div.full-list-info").forEach {
|
||||
append(it.text() + "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesNextPageSelector() = searchAnimeNextPageSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/episode/?page=$page")
|
||||
|
||||
override fun latestUpdatesSelector() = "div.episodes-list-content div.episodes-card-container"
|
||||
|
||||
// ============================== Settings ==============================
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val videoQualityPref = ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = PREF_QUALITY_TITLE
|
||||
entries = PREF_QUALITY_ENTRIES
|
||||
entryValues = PREF_QUALITY_ENTRIES
|
||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}
|
||||
screen.addPreference(videoQualityPref)
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
return sortedWith(
|
||||
compareBy { it.quality.contains(quality) },
|
||||
).reversed()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||
private const val PREF_QUALITY_TITLE = "Preferred quality"
|
||||
private const val PREF_QUALITY_DEFAULT = "720p"
|
||||
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p", "Doodstream", "StreamTape")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package eu.kanade.tachiyomi.animeextension.ar.animelek.extractors
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class SharedExtractor(private val client: OkHttpClient) {
|
||||
fun videoFromUrl(url: String, quality: String): Video? {
|
||||
val document = client.newCall(GET(url)).execute().asJsoup()
|
||||
val check = document.select("div.error4shared").text()
|
||||
val videoUrl = document.select("source").attr("src")
|
||||
return if (check.contains("This file is not available any more")) {
|
||||
Video(url, "no 1video", "https")
|
||||
} else {
|
||||
Video(url, quality, videoUrl)
|
||||
}
|
||||
}
|
||||
}
|
19
src/ar/animerco/build.gradle
Normal file
|
@ -0,0 +1,19 @@
|
|||
ext {
|
||||
extName = 'Animerco'
|
||||
extClass = '.Animerco'
|
||||
extVersionCode = 36
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:uqload-extractor'))
|
||||
implementation(project(':lib:gdriveplayer-extractor'))
|
||||
implementation(project(':lib:streamtape-extractor'))
|
||||
implementation(project(':lib:dood-extractor'))
|
||||
implementation(project(':lib:vidbom-extractor'))
|
||||
implementation(project(':lib:mp4upload-extractor'))
|
||||
implementation(project(':lib:streamwish-extractor'))
|
||||
implementation(project(':lib:yourupload-extractor'))
|
||||
implementation(project(':lib:okru-extractor'))
|
||||
}
|
BIN
src/ar/animerco/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
src/ar/animerco/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
src/ar/animerco/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
src/ar/animerco/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
src/ar/animerco/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.3 KiB |
BIN
src/ar/animerco/res/play_store_512.png
Normal file
After Width: | Height: | Size: 21 KiB |
|
@ -0,0 +1,259 @@
|
|||
package eu.kanade.tachiyomi.animeextension.ar.animerco
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animeextension.ar.animerco.extractors.SharedExtractor
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
|
||||
import eu.kanade.tachiyomi.lib.gdriveplayerextractor.GdrivePlayerExtractor
|
||||
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
|
||||
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
||||
import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor
|
||||
import eu.kanade.tachiyomi.lib.vidbomextractor.VidBomExtractor
|
||||
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
||||
import okhttp3.FormBody
|
||||
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
|
||||
|
||||
class Animerco : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val name = "Animerco"
|
||||
|
||||
override val baseUrl = "https://animerco.org"
|
||||
|
||||
override val lang = "ar"
|
||||
|
||||
override val supportsLatest = false
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
// ============================== Popular ===============================
|
||||
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/animes/page/$page/")
|
||||
|
||||
override fun popularAnimeSelector() = "div.media-block > div > a.image"
|
||||
|
||||
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
thumbnail_url = element.attr("data-src")
|
||||
title = element.attr("title")
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector() = "ul.pagination li a:has(i.fa-left-long)"
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
|
||||
override fun latestUpdatesSelector(): String = throw UnsupportedOperationException()
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime = throw UnsupportedOperationException()
|
||||
override fun latestUpdatesNextPageSelector(): String = throw UnsupportedOperationException()
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) = GET("$baseUrl/page/$page/?s=$query")
|
||||
|
||||
override fun searchAnimeSelector() = popularAnimeSelector()
|
||||
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
|
||||
override fun searchAnimeNextPageSelector() = popularAnimeNextPageSelector()
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
|
||||
document.selectFirst("a.poster")?.run {
|
||||
thumbnail_url = attr("data-src")
|
||||
title = attr("title").ifEmpty {
|
||||
document.selectFirst("div.media-title h1")!!.text()
|
||||
}
|
||||
}
|
||||
|
||||
val infosDiv = document.selectFirst("ul.media-info")!!
|
||||
author = infosDiv.select("li:contains(الشبكات) a").eachText()
|
||||
.joinToString()
|
||||
.takeIf(String::isNotBlank)
|
||||
artist = infosDiv.select("li:contains(الأستوديو) a").eachText()
|
||||
.joinToString()
|
||||
.takeIf(String::isNotBlank)
|
||||
genre = document.select("nav.Nvgnrs a, ul.media-info li:contains(النوع) a")
|
||||
.eachText()
|
||||
.joinToString()
|
||||
|
||||
description = buildString {
|
||||
document.selectFirst("div.media-story p")?.also {
|
||||
append(it.text())
|
||||
}
|
||||
document.selectFirst("div.media-title > h3.alt-title")?.also {
|
||||
append("\n\nAlternative title: " + it.text())
|
||||
}
|
||||
}
|
||||
|
||||
status = document.select("ul.chapters-list a.se-title > span.badge")
|
||||
.eachText()
|
||||
.let { items ->
|
||||
when {
|
||||
items.all { it.contains("مكتمل") } -> SAnime.COMPLETED
|
||||
items.any { it.contains("يعرض الأن") } -> SAnime.ONGOING
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
override fun episodeListSelector() = "ul.chapters-list li a:has(h3)"
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val document = response.asJsoup()
|
||||
if (document.location().contains("/movies/")) {
|
||||
return listOf(
|
||||
SEpisode.create().apply {
|
||||
setUrlWithoutDomain(document.location())
|
||||
episode_number = 1F
|
||||
name = "Movie"
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return document.select(episodeListSelector()).flatMap { el ->
|
||||
val doc = client.newCall(GET(el.attr("abs:href"), headers)).execute()
|
||||
.asJsoup()
|
||||
val seasonName = doc.selectFirst("div.media-title h1")!!.text()
|
||||
val seasonNum = seasonName.substringAfterLast(" ").toIntOrNull() ?: 1
|
||||
doc.select(episodeListSelector()).map {
|
||||
episodeFromElement(it, seasonName, seasonNum)
|
||||
}.reversed()
|
||||
}.reversed()
|
||||
}
|
||||
|
||||
private fun episodeFromElement(element: Element, seasonName: String, seasonNum: Int) = SEpisode.create().apply {
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
val epText = element.selectFirst("h3")!!.ownText()
|
||||
name = "$seasonName: " + epText
|
||||
val epNum = epText.filter(Char::isDigit)
|
||||
// good luck trying to track this xD
|
||||
episode_number = "$seasonNum.${epNum.padStart(3, '0')}".toFloatOrNull() ?: 1F
|
||||
}
|
||||
|
||||
override fun episodeFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
val players = document.select(videoListSelector())
|
||||
return players.parallelCatchingFlatMapBlocking(::getPlayerVideos)
|
||||
}
|
||||
|
||||
override fun videoListSelector() = "li.dooplay_player_option" // ul#playeroptionsul
|
||||
|
||||
private val doodExtractor by lazy { DoodExtractor(client) }
|
||||
private val gdrivePlayerExtractor by lazy { GdrivePlayerExtractor(client) }
|
||||
private val streamTapeExtractor by lazy { StreamTapeExtractor(client) }
|
||||
private val sharedExtractor by lazy { SharedExtractor(client) }
|
||||
private val uqloadExtractor by lazy { UqloadExtractor(client) }
|
||||
private val vidBomExtractor by lazy { VidBomExtractor(client) }
|
||||
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
|
||||
private val okruExtractor by lazy { OkruExtractor(client) }
|
||||
private val streamWishExtractor by lazy { StreamWishExtractor(client, headers) }
|
||||
private val yourUploadExtractor by lazy { YourUploadExtractor(client) }
|
||||
|
||||
private fun getPlayerVideos(player: Element): List<Video> {
|
||||
val url = getPlayerUrl(player) ?: return emptyList()
|
||||
val name = player.selectFirst("span.title")!!.text().lowercase()
|
||||
return when {
|
||||
"ok.ru" in url -> okruExtractor.videosFromUrl(url)
|
||||
"mp4upload" in url -> mp4uploadExtractor.videosFromUrl(url, headers)
|
||||
"wish" in name -> streamWishExtractor.videosFromUrl(url)
|
||||
"yourupload" in url -> yourUploadExtractor.videoFromUrl(url, headers)
|
||||
"dood" in url -> doodExtractor.videoFromUrl(url)?.let(::listOf)
|
||||
"drive.google" in url -> {
|
||||
val newUrl = "https://gdriveplayer.to/embed2.php?link=$url"
|
||||
gdrivePlayerExtractor.videosFromUrl(newUrl, "GdrivePlayer", headers)
|
||||
}
|
||||
"streamtape" in url -> streamTapeExtractor.videoFromUrl(url)?.let(::listOf)
|
||||
"4shared" in url -> sharedExtractor.videoFromUrl(url)?.let(::listOf)
|
||||
"uqload" in url -> uqloadExtractor.videosFromUrl(url)
|
||||
VIDBOM_DOMAINS.any(url::contains) -> vidBomExtractor.videosFromUrl(url)
|
||||
else -> null
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
private fun getPlayerUrl(player: Element): String? {
|
||||
val body = FormBody.Builder()
|
||||
.add("action", "doo_player_ajax")
|
||||
.add("post", player.attr("data-post"))
|
||||
.add("nume", player.attr("data-nume"))
|
||||
.add("type", player.attr("data-type"))
|
||||
.build()
|
||||
|
||||
return client.newCall(POST("$baseUrl/wp-admin/admin-ajax.php", headers, body))
|
||||
.execute()
|
||||
.let { response ->
|
||||
response.body.string()
|
||||
.substringAfter("\"embed_url\":\"")
|
||||
.substringBefore("\",")
|
||||
.replace("\\", "")
|
||||
.takeIf(String::isNotBlank)
|
||||
}
|
||||
}
|
||||
|
||||
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
|
||||
return sortedWith(
|
||||
compareBy { it.quality.contains(quality) },
|
||||
).reversed()
|
||||
}
|
||||
|
||||
// ============================== Settings ==============================
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = PREF_QUALITY_TITLE
|
||||
entries = PREF_QUALITY_ENTRIES
|
||||
entryValues = PREF_QUALITY_VALUES
|
||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
companion object {
|
||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||
private const val PREF_QUALITY_TITLE = "Preferred quality"
|
||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p", "Doodstream", "StreamTape")
|
||||
private val PREF_QUALITY_VALUES = arrayOf("1080", "720", "480", "360", "Doodstream", "StreamTape")
|
||||
|
||||
private val VIDBOM_DOMAINS = 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",
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package eu.kanade.tachiyomi.animeextension.ar.animerco.extractors
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class SharedExtractor(private val client: OkHttpClient) {
|
||||
fun videoFromUrl(url: String, quality: String = "mirror"): Video? {
|
||||
val document = client.newCall(GET(url)).execute().asJsoup()
|
||||
return document.selectFirst("source")?.let {
|
||||
Video(it.attr("src"), "4Shared: $quality", it.attr("src"))
|
||||
}
|
||||
}
|
||||
}
|
7
src/ar/arabanime/build.gradle
Normal file
|
@ -0,0 +1,7 @@
|
|||
ext {
|
||||
extName = 'ArabAnime'
|
||||
extClass = '.ArabAnime'
|
||||
extVersionCode = 2
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/ar/arabanime/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
src/ar/arabanime/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
src/ar/arabanime/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src/ar/arabanime/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
src/ar/arabanime/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
src/ar/arabanime/res/web_hi_res_512.png
Normal file
After Width: | Height: | Size: 78 KiB |
|
@ -0,0 +1,268 @@
|
|||
package eu.kanade.tachiyomi.animeextension.ar.arabanime
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Base64
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animeextension.ar.arabanime.dto.AnimeItem
|
||||
import eu.kanade.tachiyomi.animeextension.ar.arabanime.dto.Episode
|
||||
import eu.kanade.tachiyomi.animeextension.ar.arabanime.dto.PopularAnimeResponse
|
||||
import eu.kanade.tachiyomi.animeextension.ar.arabanime.dto.ShowItem
|
||||
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.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class ArabAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
|
||||
override val name = "ArabAnime"
|
||||
|
||||
override val baseUrl = "https://www.arabanime.net"
|
||||
|
||||
override val lang = "ar"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
// ============================== Popular ===============================
|
||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/api?page=$page")
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val responseJson = json.decodeFromString<PopularAnimeResponse>(response.body.string())
|
||||
val animeList = responseJson.Shows.mapNotNull {
|
||||
runCatching {
|
||||
val animeJson = json.decodeFromString<AnimeItem>(it.decodeBase64())
|
||||
SAnime.create().apply {
|
||||
setUrlWithoutDomain(animeJson.info_src)
|
||||
title = animeJson.anime_name
|
||||
thumbnail_url = animeJson.anime_cover_image_url
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
val hasNextPage = responseJson.current_page < responseJson.last_page
|
||||
return AnimesPage(animeList, hasNextPage)
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesRequest(page: Int) = GET(baseUrl)
|
||||
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||
val latestEpisodes = response.asJsoup().select("div.as-episode")
|
||||
val animeList = latestEpisodes.map {
|
||||
SAnime.create().apply {
|
||||
val ahref = it.selectFirst("a.as-info")!!
|
||||
title = ahref.text()
|
||||
val url = ahref.attr("href").replace("watch", "show").substringBeforeLast("/")
|
||||
setUrlWithoutDomain(url)
|
||||
|
||||
thumbnail_url = it.selectFirst("img")?.absUrl("src")
|
||||
}
|
||||
}
|
||||
return AnimesPage(animeList, false)
|
||||
}
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
return if (query.isNotEmpty()) {
|
||||
val body = FormBody.Builder().add("searchq", query).build()
|
||||
POST("$baseUrl/searchq", body = body)
|
||||
} else {
|
||||
val type = filters.asQueryPart<TypeFilter>()
|
||||
val status = filters.asQueryPart<StatusFilter>()
|
||||
val order = filters.asQueryPart<OrderFilter>()
|
||||
GET("$baseUrl/api?order=$order&type=$type&stat=$status&tags=&page=$page")
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||
return if (response.body.contentType() == "application/json".toMediaType()) {
|
||||
popularAnimeParse(response)
|
||||
} else {
|
||||
val searchResult = response.asJsoup().select("div.show")
|
||||
val animeList = searchResult.map {
|
||||
SAnime.create().apply {
|
||||
setUrlWithoutDomain(it.selectFirst("a")!!.attr("href"))
|
||||
title = it.selectFirst("h3")!!.text()
|
||||
thumbnail_url = it.selectFirst("img")?.absUrl("src")
|
||||
}
|
||||
}
|
||||
return AnimesPage(animeList, false)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== filters ==============================
|
||||
override fun getFilterList() = AnimeFilterList(
|
||||
AnimeFilter.Header("فلترة الموقع"),
|
||||
OrderFilter(),
|
||||
TypeFilter(),
|
||||
StatusFilter(),
|
||||
)
|
||||
|
||||
open class QueryPartFilter(
|
||||
displayName: String,
|
||||
val vals: Array<Pair<String, String>>,
|
||||
) : AnimeFilter.Select<String>(
|
||||
displayName,
|
||||
vals.map { it.first }.toTypedArray(),
|
||||
) {
|
||||
fun toQueryPart() = vals[state].second
|
||||
}
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
|
||||
return (firstOrNull { it is R } as? QueryPartFilter)?.toQueryPart() ?: ""
|
||||
}
|
||||
|
||||
private class OrderFilter : QueryPartFilter("ترتيب", ORDER_LIST)
|
||||
private class TypeFilter : QueryPartFilter("النوع", TYPE_LIST)
|
||||
private class StatusFilter : QueryPartFilter("الحالة", STATUS_LIST)
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun animeDetailsParse(response: Response): SAnime {
|
||||
val showData = response.asJsoup().selectFirst("div#data")!!
|
||||
.text()
|
||||
.decodeBase64()
|
||||
|
||||
val details = json.decodeFromString<ShowItem>(showData).show[0]
|
||||
return SAnime.create().apply {
|
||||
url = "/show-${details.anime_id}/${details.anime_slug}"
|
||||
title = details.anime_name
|
||||
status = when (details.anime_status) {
|
||||
"Ongoing" -> SAnime.ONGOING
|
||||
"Completed" -> SAnime.COMPLETED
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
genre = details.anime_genres
|
||||
description = details.anime_description
|
||||
thumbnail_url = details.anime_cover_image_url
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val showData = response.asJsoup().selectFirst("div#data")
|
||||
?.text()
|
||||
?.decodeBase64()
|
||||
?: return emptyList()
|
||||
|
||||
val episodesJson = json.decodeFromString<ShowItem>(showData)
|
||||
return episodesJson.EPS.map {
|
||||
SEpisode.create().apply {
|
||||
name = it.episode_name
|
||||
episode_number = it.episode_number.toFloat()
|
||||
setUrlWithoutDomain(it.`info-src`)
|
||||
}
|
||||
}.reversed()
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val watchData = response.asJsoup().selectFirst("div#datawatch")
|
||||
?.text()
|
||||
?.decodeBase64()
|
||||
?: return emptyList()
|
||||
|
||||
val serversJson = json.decodeFromString<Episode>(watchData)
|
||||
val selectServer = serversJson.ep_info[0].stream_servers[0].decodeBase64()
|
||||
|
||||
val watchPage = client.newCall(GET(selectServer)).execute().asJsoup()
|
||||
return watchPage.select("option")
|
||||
.map { it.text() to it.attr("data-src").decodeBase64() } // server : url
|
||||
.filter { it.second.contains("$baseUrl/embed") } // filter urls
|
||||
.flatMap { (name, url) ->
|
||||
client.newCall(GET(url)).execute()
|
||||
.asJsoup()
|
||||
.select("source")
|
||||
.mapNotNull { source ->
|
||||
val videoUrl = source.attr("src")
|
||||
if (!videoUrl.contains("static")) {
|
||||
val quality = source.attr("label").let { q ->
|
||||
if (q.contains("p")) q else q + "p"
|
||||
}
|
||||
Video(videoUrl, "$name: $quality", videoUrl)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
return sortedWith(
|
||||
compareBy { it.quality.contains(quality) },
|
||||
).reversed()
|
||||
}
|
||||
|
||||
// =============================== Preferences ===============================
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val videoQualityPref = ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = PREF_QUALITY_TITLE
|
||||
entries = PREF_QUALITY_ENTRIES
|
||||
entryValues = PREF_QUALITY_VALUES
|
||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}
|
||||
screen.addPreference(videoQualityPref)
|
||||
}
|
||||
|
||||
private fun String.decodeBase64() = String(Base64.decode(this, Base64.DEFAULT))
|
||||
|
||||
companion object {
|
||||
private val ORDER_LIST = arrayOf(
|
||||
Pair("اختر", ""),
|
||||
Pair("التقييم", "2"),
|
||||
Pair("اخر الانميات المضافة", "1"),
|
||||
Pair("الابجدية", "0"),
|
||||
)
|
||||
|
||||
private val TYPE_LIST = arrayOf(
|
||||
Pair("اختر", ""),
|
||||
Pair("الكل", ""),
|
||||
Pair("فيلم", "0"),
|
||||
Pair("انمى", "1"),
|
||||
)
|
||||
|
||||
private val STATUS_LIST = arrayOf(
|
||||
Pair("اختر", ""),
|
||||
Pair("الكل", ""),
|
||||
Pair("مستمر", "1"),
|
||||
Pair("مكتمل", "0"),
|
||||
)
|
||||
|
||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||
private const val PREF_QUALITY_TITLE = "Preferred quality"
|
||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p", "240p")
|
||||
private val PREF_QUALITY_VALUES by lazy {
|
||||
PREF_QUALITY_ENTRIES.map { it.substringBefore("p") }.toTypedArray()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package eu.kanade.tachiyomi.animeextension.ar.arabanime.dto
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class PopularAnimeResponse(
|
||||
val Shows: List<String>,
|
||||
val current_page: Int,
|
||||
val last_page: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AnimeItem(
|
||||
val anime_cover_image_url: String,
|
||||
val anime_id: String,
|
||||
val anime_name: String,
|
||||
val anime_score: String,
|
||||
val anime_slug: String,
|
||||
val anime_type: String,
|
||||
val info_src: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ShowItem(
|
||||
val EPS: List<EPS>,
|
||||
val show: List<Show>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class EPS(
|
||||
val episode_name: String,
|
||||
val episode_number: Int,
|
||||
@SerialName("info-src")
|
||||
val `info-src`: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Show(
|
||||
val anime_cover_image_url: String,
|
||||
val anime_description: String,
|
||||
val anime_genres: String,
|
||||
val anime_id: Int,
|
||||
val anime_name: String,
|
||||
val anime_release_date: String,
|
||||
val anime_score: String,
|
||||
val anime_slug: String,
|
||||
val anime_status: String,
|
||||
val anime_type: String,
|
||||
val show_episode_count: Int,
|
||||
val wallpapaer: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Episode(
|
||||
val ep_info: List<EpInfo>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class EpInfo(
|
||||
val stream_servers: List<String>,
|
||||
)
|
13
src/ar/arabseed/build.gradle
Normal file
|
@ -0,0 +1,13 @@
|
|||
ext {
|
||||
extName = 'Arab Seed'
|
||||
extClass = '.ArabSeed'
|
||||
extVersionCode = 13
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib:dood-extractor"))
|
||||
implementation(project(":lib:voe-extractor"))
|
||||
implementation(project(":lib:streamwish-extractor"))
|
||||
}
|
BIN
src/ar/arabseed/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
src/ar/arabseed/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
src/ar/arabseed/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
src/ar/arabseed/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/ar/arabseed/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
src/ar/arabseed/res/web_hi_res_512.png
Normal file
After Width: | Height: | Size: 93 KiB |
|
@ -0,0 +1,237 @@
|
|||
package eu.kanade.tachiyomi.animeextension.ar.arabseed
|
||||
|
||||
import android.app.Application
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
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.lib.doodextractor.DoodExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
||||
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.lang.Exception
|
||||
|
||||
class ArabSeed : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val name = "عرب سيد"
|
||||
|
||||
// TODO: Check frequency of url changes to potentially
|
||||
// add back overridable baseurl preference
|
||||
override val baseUrl = "https://m.asd.homes"
|
||||
|
||||
override val lang = "ar"
|
||||
|
||||
override val supportsLatest = false
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder().add("Referer", baseUrl)
|
||||
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
// ============================== Popular ===============================
|
||||
override fun popularAnimeSelector() = "ul.Blocks-UL div.MovieBlock a"
|
||||
|
||||
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/movies/?offset=$page")
|
||||
|
||||
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
title = element.selectFirst("div.BlockName > h4")!!.text()
|
||||
thumbnail_url = element.selectFirst("div.Poster img")!!.attr("data-src")
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector() = "ul.page-numbers li a.next"
|
||||
|
||||
// ============================== Episode ===============================
|
||||
override fun episodeListSelector() = "div.ContainerEpisodesList a"
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val document = response.asJsoup()
|
||||
val episodes = document.select(episodeListSelector())
|
||||
return when {
|
||||
episodes.isEmpty() -> {
|
||||
SEpisode.create().apply {
|
||||
setUrlWithoutDomain(document.location())
|
||||
name = "مشاهدة"
|
||||
}.let(::listOf)
|
||||
}
|
||||
else -> episodes.map(::episodeFromElement)
|
||||
}
|
||||
}
|
||||
|
||||
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
name = element.text()
|
||||
episode_number = element.selectFirst("em")?.text()?.toFloatOrNull() ?: 0F
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val doc = response.asJsoup()
|
||||
val watchUrl = doc.selectFirst("a.watchBTn")!!.attr("href")
|
||||
val element = client.newCall(GET(watchUrl, headers)).execute().asJsoup()
|
||||
return videosFromElement(element)
|
||||
}
|
||||
|
||||
override fun videoListSelector() = "div.containerServers ul li"
|
||||
|
||||
private fun videosFromElement(document: Document): List<Video> {
|
||||
return document.select(videoListSelector()).parallelCatchingFlatMapBlocking { element ->
|
||||
val quality = element.text()
|
||||
val embedUrl = element.attr("data-link")
|
||||
getVideosFromUrl(embedUrl, quality)
|
||||
}
|
||||
}
|
||||
|
||||
private val doodExtractor by lazy { DoodExtractor(client) }
|
||||
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
|
||||
private val voeExtractor by lazy { VoeExtractor(client) }
|
||||
|
||||
private fun getVideosFromUrl(url: String, quality: String): List<Video> {
|
||||
return when {
|
||||
"reviewtech" in url || "reviewrate" in url -> {
|
||||
val iframeResponse = client.newCall(GET(url)).execute()
|
||||
.asJsoup()
|
||||
val videoUrl = iframeResponse.selectFirst("source")!!.attr("abs:src")
|
||||
listOf(Video(videoUrl, quality + "p", videoUrl))
|
||||
}
|
||||
"dood" in url -> doodExtractor.videosFromUrl(url)
|
||||
"fviplions" in url || "wish" in url -> streamwishExtractor.videosFromUrl(url)
|
||||
"voe.sx" in url -> voeExtractor.videosFromUrl(url)
|
||||
else -> null
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
return sortedWith(
|
||||
compareBy { it.quality.contains(quality) },
|
||||
).reversed()
|
||||
}
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
|
||||
override fun searchAnimeNextPageSelector() = popularAnimeNextPageSelector()
|
||||
override fun searchAnimeSelector() = popularAnimeSelector()
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val url = if (query.isNotBlank()) {
|
||||
"$baseUrl/find/?find=$query&offset=$page"
|
||||
} else {
|
||||
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||
val typeFilter = filterList.find { it is TypeFilter } as TypeFilter
|
||||
val category = typeFilter.toUriPart()
|
||||
if (category.isEmpty()) throw Exception("اختر فلتر")
|
||||
|
||||
"$baseUrl/category/$category"
|
||||
}
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
|
||||
thumbnail_url = document.selectFirst("div.Poster img")!!.let { img ->
|
||||
img.attr("abs:data-src")
|
||||
.ifEmpty { img.attr("abs:data-lazy-src") }
|
||||
.ifEmpty { img.attr("abs:src") }
|
||||
}
|
||||
title = document.selectFirst("div.BreadCrumbs ol li:last-child a span")!!
|
||||
.text()
|
||||
.replace(" مترجم", "").replace("فيلم ", "")
|
||||
genre = document.select("div.MetaTermsInfo > li:contains(النوع) > a").eachText().joinToString()
|
||||
description = document.selectFirst("div.StoryLine p")!!.text()
|
||||
status = when {
|
||||
document.location().contains("/selary/") -> SAnime.UNKNOWN
|
||||
else -> SAnime.COMPLETED
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Filters ===============================
|
||||
override fun getFilterList() = AnimeFilterList(
|
||||
AnimeFilter.Header("الفلترات مش هتشتغل لو بتبحث او وهي فاضيه"),
|
||||
TypeFilter(),
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
private class TypeFilter : UriPartFilter(
|
||||
"نوع الفلم",
|
||||
arrayOf(
|
||||
Pair("أختر", ""),
|
||||
Pair("افلام عربي", "arabic-movies-5/"),
|
||||
Pair("افلام اجنبى", "foreign-movies3/"),
|
||||
Pair("افلام اسيوية", "%d8%a7%d9%81%d9%84%d8%a7%d9%85-%d8%a7%d8%b3%d9%8a%d9%88%d9%8a%d8%a9/"),
|
||||
Pair("افلام هندى", "indian-movies/"),
|
||||
Pair("افلام تركية", "%d8%a7%d9%81%d9%84%d8%a7%d9%85-%d8%aa%d8%b1%d9%83%d9%8a%d8%a9/"),
|
||||
Pair("افلام انيميشن", "%d8%a7%d9%81%d9%84%d8%a7%d9%85-%d8%a7%d9%86%d9%8a%d9%85%d9%8a%d8%b4%d9%86/"),
|
||||
Pair("افلام كلاسيكيه", "%d8%a7%d9%81%d9%84%d8%a7%d9%85-%d9%83%d9%84%d8%a7%d8%b3%d9%8a%d9%83%d9%8a%d9%87/"),
|
||||
Pair("افلام مدبلجة", "%d8%a7%d9%81%d9%84%d8%a7%d9%85-%d9%85%d8%af%d8%a8%d9%84%d8%ac%d8%a9/"),
|
||||
Pair("افلام Netfilx", "netfilx/افلام-netfilx/"),
|
||||
Pair("مسلسلات عربي", "%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d8%b9%d8%b1%d8%a8%d9%8a/"),
|
||||
Pair("مسلسلات اجنبي", "foreign-series/"),
|
||||
Pair("مسلسلات تركيه", "turkish-series-1/"),
|
||||
Pair("برامج تلفزيونية", "%d8%a8%d8%b1%d8%a7%d9%85%d8%ac-%d8%aa%d9%84%d9%81%d8%b2%d9%8a%d9%88%d9%86%d9%8a%d8%a9/"),
|
||||
Pair("مسلسلات كرتون", "%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d9%83%d8%b1%d8%aa%d9%88%d9%86/"),
|
||||
Pair("مسلسلات رمضان 2019", "%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d8%b1%d9%85%d8%b6%d8%a7%d9%86-2019/"),
|
||||
Pair("مسلسلات رمضان 2020", "%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d8%b1%d9%85%d8%b6%d8%a7%d9%86-2020-hd/"),
|
||||
Pair("مسلسلات رمضان 2021", "%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d8%b1%d9%85%d8%b6%d8%a7%d9%86-2021/"),
|
||||
Pair("مسلسلات Netfilx", "netfilx/%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-netfilz/"),
|
||||
),
|
||||
)
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesNextPageSelector(): String? = throw UnsupportedOperationException()
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime = throw UnsupportedOperationException()
|
||||
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
|
||||
override fun latestUpdatesSelector(): String = throw UnsupportedOperationException()
|
||||
|
||||
// =============================== Preferences ===============================
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val videoQualityPref = ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = PREF_QUALITY_TITLE
|
||||
entries = PREF_QUALITY_ENTRIES
|
||||
entryValues = PREF_QUALITY_VALUES
|
||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}
|
||||
screen.addPreference(videoQualityPref)
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
companion object {
|
||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||
private const val PREF_QUALITY_TITLE = "Preferred quality"
|
||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p")
|
||||
private val PREF_QUALITY_VALUES by lazy {
|
||||
PREF_QUALITY_ENTRIES.map { it.substringBefore("p") }.toTypedArray()
|
||||
}
|
||||
}
|
||||
}
|
16
src/ar/asia2tv/build.gradle
Normal file
|
@ -0,0 +1,16 @@
|
|||
ext {
|
||||
extName = 'asia2tv'
|
||||
extClass = '.Asia2TV'
|
||||
extVersionCode = 17
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:dood-extractor'))
|
||||
implementation(project(':lib:okru-extractor'))
|
||||
implementation(project(':lib:streamtape-extractor'))
|
||||
implementation(project(':lib:streamwish-extractor'))
|
||||
implementation(project(':lib:uqload-extractor'))
|
||||
implementation(project(':lib:vidbom-extractor'))
|
||||
}
|
BIN
src/ar/asia2tv/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
src/ar/asia2tv/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
src/ar/asia2tv/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
src/ar/asia2tv/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
src/ar/asia2tv/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/ar/asia2tv/res/play_store_512.png
Normal file
After Width: | Height: | Size: 41 KiB |
|
@ -0,0 +1,266 @@
|
|||
package eu.kanade.tachiyomi.animeextension.ar.asia2tv
|
||||
|
||||
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.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.lib.doodextractor.DoodExtractor
|
||||
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
||||
import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor
|
||||
import eu.kanade.tachiyomi.lib.vidbomextractor.VidBomExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.lang.Exception
|
||||
|
||||
class Asia2TV : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val name = "Asia2TV"
|
||||
|
||||
override val baseUrl = "https://ww1.asia2tv.pw"
|
||||
|
||||
override val lang = "ar"
|
||||
|
||||
override val supportsLatest = false
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
// ============================== Popular ===============================
|
||||
override fun popularAnimeSelector(): String = "div.postmovie-photo a[title]"
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String = "div.nav-links a.next"
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/category/asian-drama/page/$page/") // page/$page
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.setUrlWithoutDomain(element.attr("href"))
|
||||
// anime.thumbnail_url = element.selectFirst("div.image img")!!.attr("data-src")
|
||||
anime.title = element.attr("title")
|
||||
return anime
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
override fun episodeListSelector() = "div.loop-episode a"
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
return super.episodeListParse(response).reversed()
|
||||
}
|
||||
|
||||
override fun episodeFromElement(element: Element): SEpisode {
|
||||
val episode = SEpisode.create()
|
||||
episode.setUrlWithoutDomain(element.attr("href"))
|
||||
episode.name = element.attr("href").substringAfterLast("-").substringBeforeLast("/") + " : الحلقة"
|
||||
return episode
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override fun videoListSelector() = "ul.server-list-menu li"
|
||||
|
||||
override fun videoListRequest(episode: SEpisode): Request {
|
||||
val document = client.newCall(GET(baseUrl + episode.url)).execute()
|
||||
.asJsoup()
|
||||
val link = document.selectFirst("div.loop-episode a.current")!!.attr("href")
|
||||
return GET(link)
|
||||
}
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
return document.select(videoListSelector()).parallelCatchingFlatMapBlocking {
|
||||
val url = it.attr("data-server")
|
||||
getVideosFromUrl(url)
|
||||
}
|
||||
}
|
||||
|
||||
private val doodExtractor by lazy { DoodExtractor(client) }
|
||||
private val okruExtractor by lazy { OkruExtractor(client) }
|
||||
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
|
||||
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
|
||||
private val uqloadExtractor by lazy { UqloadExtractor(client) }
|
||||
private val vidbomExtractor by lazy { VidBomExtractor(client) }
|
||||
|
||||
private fun getVideosFromUrl(url: String): List<Video> {
|
||||
return when {
|
||||
"dood" in url || "ds2play" in url -> doodExtractor.videosFromUrl(url)
|
||||
"ok.ru" in url || "odnoklassniki.ru" in url -> okruExtractor.videosFromUrl(url)
|
||||
"streamtape" in url -> streamtapeExtractor.videoFromUrl(url)?.let(::listOf)
|
||||
STREAM_WISH_DOMAINS.any(url::contains) -> streamwishExtractor.videosFromUrl(url)
|
||||
"uqload" in url -> uqloadExtractor.videosFromUrl(url)
|
||||
VID_BOM_DOMAINS.any(url::contains) -> vidbomExtractor.videosFromUrl(url)
|
||||
"youdbox" in url || "yodbox" in url -> {
|
||||
client.newCall(GET(url)).execute().let {
|
||||
val doc = it.asJsoup()
|
||||
val videoUrl = doc.selectFirst("source")?.attr("abs:src")
|
||||
when (videoUrl) {
|
||||
null -> emptyList()
|
||||
else -> listOf(Video(videoUrl, "Yodbox: mirror", videoUrl))
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException()
|
||||
|
||||
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 searchAnimeSelector(): String = "div.postmovie-photo a[title]"
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String = "div.nav-links a.next"
|
||||
|
||||
override fun searchAnimeFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.setUrlWithoutDomain(element.attr("href"))
|
||||
// anime.thumbnail_url = element.selectFirst("div.image img")!!.attr("data-src")
|
||||
anime.title = element.attr("title")
|
||||
return anime
|
||||
}
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val url = if (query.isNotBlank()) {
|
||||
"$baseUrl/page/$page/?s=$query"
|
||||
} else {
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
is TypeList -> {
|
||||
if (filter.state > 0) {
|
||||
val genreN = getTypeList()[filter.state].query
|
||||
val genreUrl = "$baseUrl/category/asian-drama/$genreN/page/$page/".toHttpUrlOrNull()!!.newBuilder()
|
||||
return GET(genreUrl.toString(), headers)
|
||||
}
|
||||
}
|
||||
is StatusList -> {
|
||||
if (filter.state > 0) {
|
||||
val statusN = getStatusList()[filter.state].query
|
||||
val statusUrl = "$baseUrl/$statusN/page/$page/".toHttpUrlOrNull()!!.newBuilder()
|
||||
return GET(statusUrl.toString(), headers)
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
throw Exception("اختر فلتر")
|
||||
}
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun animeDetailsParse(document: Document): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.title = document.select("h1 span.title").text()
|
||||
anime.thumbnail_url = document.select("div.single-thumb-bg > img").attr("src")
|
||||
anime.description = document.select("div.getcontent p").text()
|
||||
anime.genre = document.select("div.box-tags a, li:contains(البلد) a").joinToString(", ") { it.text() }
|
||||
|
||||
return anime
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesNextPageSelector(): String = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesSelector(): String = throw UnsupportedOperationException()
|
||||
|
||||
// ============================== Filters ===============================
|
||||
override fun getFilterList() = AnimeFilterList(
|
||||
AnimeFilter.Header("الفلترات مش هتشتغل لو بتبحث او وهي فاضيه"),
|
||||
TypeList(typesName),
|
||||
StatusList(statusesName),
|
||||
)
|
||||
|
||||
private class TypeList(types: Array<String>) : AnimeFilter.Select<String>("نوع الدراما", types)
|
||||
private data class Type(val name: String, val query: String)
|
||||
private val typesName = getTypeList().map {
|
||||
it.name
|
||||
}.toTypedArray()
|
||||
|
||||
private class StatusList(statuse: Array<String>) : AnimeFilter.Select<String>("حالة الدراما", statuse)
|
||||
private data class Status(val name: String, val query: String)
|
||||
private val statusesName = getStatusList().map {
|
||||
it.name
|
||||
}.toTypedArray()
|
||||
|
||||
private fun getTypeList() = listOf(
|
||||
Type("اختر", ""),
|
||||
Type("الدراما الكورية", "korean"),
|
||||
Type("الدراما اليابانية", "japanese"),
|
||||
Type("الدراما الصينية والتايوانية", "chinese-taiwanese"),
|
||||
Type("الدراما التايلاندية", "thai"),
|
||||
Type("برامج الترفيه", "kshow"),
|
||||
)
|
||||
|
||||
private fun getStatusList() = listOf(
|
||||
Status("أختر", ""),
|
||||
Status("يبث حاليا", "status/ongoing-drama"),
|
||||
Status("الدراما المكتملة", "completed-dramas"),
|
||||
Status("الدراما القادمة", "status/upcoming-drama"),
|
||||
|
||||
)
|
||||
|
||||
// ============================== Settings ==============================
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val videoQualityPref = ListPreference(screen.context).apply {
|
||||
key = "preferred_quality"
|
||||
title = "Preferred quality & Server"
|
||||
entries = arrayOf("StreamTape", "DooDStream", "1080p", "720p", "480p", "360p")
|
||||
entryValues = arrayOf("StreamTape", "Dood", "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)
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
companion object {
|
||||
private val STREAM_WISH_DOMAINS by lazy { listOf("wishfast", "fviplions", "filelions", "streamwish", "dwish") }
|
||||
private val VID_BOM_DOMAINS by lazy { listOf("vidbam", "vadbam", "vidbom", "vidbm") }
|
||||
}
|
||||
}
|
11
src/ar/cimaleek/build.gradle
Normal file
|
@ -0,0 +1,11 @@
|
|||
ext {
|
||||
extName = 'Cimaleek'
|
||||
extClass = '.Cimaleek'
|
||||
extVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib:playlist-utils"))
|
||||
}
|
BIN
src/ar/cimaleek/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
src/ar/cimaleek/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
src/ar/cimaleek/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
BIN
src/ar/cimaleek/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/ar/cimaleek/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 20 KiB |
|
@ -0,0 +1,281 @@
|
|||
package eu.kanade.tachiyomi.animeextension.ar.cimaleek
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animeextension.ar.cimaleek.interceptor.WebViewResolver
|
||||
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.Track
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
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
|
||||
|
||||
class Cimaleek : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val name = "سيما ليك"
|
||||
|
||||
override val baseUrl = "https://m.cimaleek.to"
|
||||
|
||||
override val lang = "ar"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
private val webViewResolver by lazy { WebViewResolver(headers) }
|
||||
|
||||
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
||||
|
||||
// ============================== Popular ===============================
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.title = element.select("div.data .title").text()
|
||||
anime.thumbnail_url = element.select("img").attr("data-src")
|
||||
anime.setUrlWithoutDomain(element.select("a").attr("href"))
|
||||
return anime
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String = "div.pagination div.pagination-num i#nextpagination"
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/trending/page/$page/", headers)
|
||||
|
||||
override fun popularAnimeSelector(): String = "div.film_list-wrap div.item"
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException()
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val episodes = mutableListOf<SEpisode>()
|
||||
val document = response.asJsoup()
|
||||
val url = response.request.url.toString()
|
||||
if (url.contains("movies")) {
|
||||
val episode = SEpisode.create().apply {
|
||||
name = "مشاهدة"
|
||||
setUrlWithoutDomain("$url/watch/")
|
||||
}
|
||||
episodes.add(episode)
|
||||
} else {
|
||||
document.select(seasonListSelector()).parallelCatchingFlatMapBlocking { sElement ->
|
||||
val seasonNum = sElement.select("span.se-a").text()
|
||||
val seasonUrl = sElement.attr("href")
|
||||
val seasonPage = client.newCall(GET(seasonUrl, headers)).execute().asJsoup()
|
||||
seasonPage.select(episodeListSelector()).map { eElement ->
|
||||
val episodeNum = eElement.select("span.serie").text().substringAfter("(").substringBefore(")")
|
||||
val episodeUrl = eElement.attr("href")
|
||||
val finalNum = ("$seasonNum.$episodeNum").toFloat()
|
||||
val episodeTitle = "الموسم ${seasonNum.toInt()} الحلقة ${episodeNum.toInt()}"
|
||||
val episode = SEpisode.create().apply {
|
||||
name = episodeTitle
|
||||
episode_number = finalNum
|
||||
setUrlWithoutDomain("$episodeUrl/watch/")
|
||||
}
|
||||
episodes.add(episode)
|
||||
}
|
||||
}
|
||||
}
|
||||
return episodes.sortedBy { it.episode_number }.reversed()
|
||||
}
|
||||
|
||||
override fun episodeListSelector(): String = "div.season-a ul.episodios li.episodesList a"
|
||||
|
||||
private fun seasonListSelector(): String = "div.season-a ul.seas-list li.sealist a"
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun animeDetailsParse(document: Document): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.thumbnail_url = document.select("div.ani_detail-stage div.film-poster img").attr("src")
|
||||
anime.title = document.select("div.anisc-more-info div.item:contains(الاسم) span:nth-child(3)").text()
|
||||
anime.author = document.select("div.anisc-more-info div.item:contains(البلد) span:nth-child(3)").text()
|
||||
anime.genre = document.select("div.anisc-detail div.item-list a").joinToString(", ") { it.text() }
|
||||
anime.description = document.select("div.anisc-detail div.film-description div.text").text()
|
||||
anime.status = if (document.select("div.anisc-detail div.item-list").text().contains("افلام")) SAnime.COMPLETED else SAnime.UNKNOWN
|
||||
return anime
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoListSelector(): String = "div#servers-content div.server-item div"
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
val script = document.selectFirst("script:containsData(dtAjax)")!!.data()
|
||||
val version = script.substringAfter("ver\":\"").substringBefore("\"")
|
||||
return document.select(videoListSelector()).parallelCatchingFlatMapBlocking {
|
||||
extractVideos(it, version)
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateRandomString(): String {
|
||||
val characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
val result = StringBuilder(16)
|
||||
for (i in 0 until 16) {
|
||||
val randomIndex = (Math.random() * characters.length).toInt()
|
||||
result.append(characters[randomIndex])
|
||||
}
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
private fun extractVideos(element: Element, version: String): List<Video> {
|
||||
val videoUrl = "$baseUrl/wp-json/lalaplayer/v2/".toHttpUrl().newBuilder()
|
||||
videoUrl.addQueryParameter("p", element.attr("data-post"))
|
||||
videoUrl.addQueryParameter("t", element.attr("data-type"))
|
||||
videoUrl.addQueryParameter("n", element.attr("data-nume"))
|
||||
videoUrl.addQueryParameter("ver", version)
|
||||
videoUrl.addQueryParameter("rand", generateRandomString())
|
||||
val videoFrame = client.newCall(GET(videoUrl.toString(), headers)).execute().body.string()
|
||||
val embedUrl = videoFrame.substringAfter("embed_url\":\"").substringBefore("\"")
|
||||
val referer = headers.newBuilder().add("Referer", "$baseUrl/").build()
|
||||
val webViewResult = webViewResolver.getUrl(embedUrl, referer)
|
||||
return when {
|
||||
".mp4" in webViewResult.url -> {
|
||||
Video(webViewResult.url, element.text(), webViewResult.url, headers = referer).let(::listOf)
|
||||
}
|
||||
".m3u8" in webViewResult.url -> {
|
||||
val subtitleList = if (webViewResult.subtitle.isNotBlank()) Track(webViewResult.subtitle, "Arabic").let(::listOf) else emptyList()
|
||||
playlistUtils.extractFromHls(webViewResult.url, videoNameGen = { "${element.text()}: $it" }, subtitleList = subtitleList)
|
||||
}
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString("preferred_quality", "1080")!!
|
||||
return sortedWith(
|
||||
compareBy { it.quality.contains(quality) },
|
||||
).reversed()
|
||||
}
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||
val sectionFilter = filterList.find { it is SectionFilter } as SectionFilter
|
||||
val categoryFilter = filterList.find { it is CategoryFilter } as CategoryFilter
|
||||
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
|
||||
return if (query.isNotBlank()) {
|
||||
GET("$baseUrl/page/$page?s=$query", headers)
|
||||
} else {
|
||||
val url = baseUrl.toHttpUrl().newBuilder()
|
||||
if (sectionFilter.state != 0) {
|
||||
url.addPathSegment("category")
|
||||
url.addPathSegment(sectionFilter.toUriPart())
|
||||
} else if (categoryFilter.state != 0) {
|
||||
url.addPathSegment("genre")
|
||||
url.addPathSegment(genreFilter.toUriPart().lowercase())
|
||||
} else {
|
||||
throw Exception("من فضلك اختر قسم او نوع")
|
||||
}
|
||||
url.addPathSegment("page")
|
||||
url.addPathSegment("$page")
|
||||
if (categoryFilter.state != 0) {
|
||||
url.addQueryParameter("type", categoryFilter.toUriPart())
|
||||
}
|
||||
GET(url.toString(), headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchAnimeSelector(): String = popularAnimeSelector()
|
||||
|
||||
// ============================ Filters =============================
|
||||
|
||||
override fun getFilterList() = AnimeFilterList(
|
||||
AnimeFilter.Header("هذا القسم يعمل لو كان البحث فارع"),
|
||||
SectionFilter(),
|
||||
AnimeFilter.Separator(),
|
||||
AnimeFilter.Header("الفلتره تعمل فقط لو كان اقسام الموقع على 'اختر'"),
|
||||
CategoryFilter(),
|
||||
GenreFilter(),
|
||||
)
|
||||
private class SectionFilter : PairFilter(
|
||||
"اقسام الموقع",
|
||||
arrayOf(
|
||||
Pair("اختر", "none"),
|
||||
Pair("افلام اجنبي", "aflam-online"),
|
||||
Pair("افلام نتفليكس", "netflix-movies"),
|
||||
Pair("افلام هندي", "indian-movies"),
|
||||
Pair("افلام اسيوي", "asian-aflam"),
|
||||
Pair("افلام كرتون", "cartoon-movies"),
|
||||
Pair("افلام انمي", "anime-movies"),
|
||||
Pair("مسلسلات اجنبي", "english-series"),
|
||||
Pair("مسلسلات نتفليكس", "netflix-series"),
|
||||
Pair("مسلسلات اسيوي", "asian-series"),
|
||||
Pair("مسلسلات كرتون", "anime-series"),
|
||||
Pair("مسلسلات انمي", "netflix-anime"),
|
||||
),
|
||||
)
|
||||
private class CategoryFilter : PairFilter(
|
||||
"النوع",
|
||||
arrayOf(
|
||||
Pair("اختر", "none"),
|
||||
Pair("افلام", "movies"),
|
||||
Pair("مسلسلات", "series"),
|
||||
),
|
||||
)
|
||||
private class GenreFilter : SingleFilter(
|
||||
"التصنيف",
|
||||
arrayOf(
|
||||
"Action", "Adventure", "Animation", "Western", "Documentary", "Fantasy", "Science-fiction", "Romance", "Comedy", "Family", "Drama", "Thriller", "Crime", "Horror",
|
||||
).sortedArray(),
|
||||
)
|
||||
|
||||
open class SingleFilter(displayName: String, private val vals: Array<String>) :
|
||||
AnimeFilter.Select<String>(displayName, vals) {
|
||||
fun toUriPart() = vals[state]
|
||||
}
|
||||
open class PairFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
|
||||
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime = popularAnimeFromElement(element)
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/recent/page/$page/", headers)
|
||||
|
||||
override fun latestUpdatesSelector(): String = popularAnimeSelector()
|
||||
|
||||
// =============================== Settings ===============================
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val videoQualityPref = ListPreference(screen.context).apply {
|
||||
key = "preferred_quality"
|
||||
title = "Preferred quality"
|
||||
entries = arrayOf("1080p", "720p", "480p", "360p", "240p")
|
||||
entryValues = arrayOf("1080", "720", "480", "360", "240")
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package eu.kanade.tachiyomi.animeextension.ar.cimaleek.interceptor
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import okhttp3.Headers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class WebViewResolver(private val globalHeaders: Headers) {
|
||||
private val context: Application by injectLazy()
|
||||
private val handler by lazy { Handler(Looper.getMainLooper()) }
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
fun getUrl(origRequestUrl: String, origRequestheader: Headers): Result {
|
||||
val latch = CountDownLatch(2)
|
||||
var webView: WebView? = null
|
||||
val result = Result("", "")
|
||||
val headers = origRequestheader.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
|
||||
|
||||
handler.post {
|
||||
val webview = WebView(context)
|
||||
webView = webview
|
||||
with(webview.settings) {
|
||||
javaScriptEnabled = true
|
||||
domStorageEnabled = true
|
||||
databaseEnabled = true
|
||||
useWideViewPort = false
|
||||
loadWithOverviewMode = false
|
||||
userAgentString = globalHeaders["User-Agent"]
|
||||
}
|
||||
webview.webViewClient = object : WebViewClient() {
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
): WebResourceResponse? {
|
||||
val url = request.url.toString()
|
||||
if ("vtt" in url) {
|
||||
result.subtitle = url
|
||||
latch.countDown()
|
||||
}
|
||||
if (VIDEO_REGEX.containsMatchIn(url)) {
|
||||
result.url = url
|
||||
latch.countDown()
|
||||
}
|
||||
return super.shouldInterceptRequest(view, request)
|
||||
}
|
||||
}
|
||||
|
||||
webView?.loadUrl(origRequestUrl, headers)
|
||||
}
|
||||
|
||||
latch.await(TIMEOUT_SEC, TimeUnit.SECONDS)
|
||||
|
||||
handler.post {
|
||||
webView?.stopLoading()
|
||||
webView?.destroy()
|
||||
webView = null
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TIMEOUT_SEC: Long = 25
|
||||
private val VIDEO_REGEX by lazy { Regex("\\.(mp4|m3u8)") }
|
||||
}
|
||||
|
||||
data class Result(var url: String, var subtitle: String)
|
||||
}
|
13
src/ar/egydead/build.gradle
Normal file
|
@ -0,0 +1,13 @@
|
|||
ext {
|
||||
extName = 'Egy Dead'
|
||||
extClass = '.EgyDead'
|
||||
extVersionCode = 12
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:dood-extractor'))
|
||||
implementation(project(':lib:mixdrop-extractor'))
|
||||
implementation(project(':lib:streamwish-extractor'))
|
||||
}
|
BIN
src/ar/egydead/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
src/ar/egydead/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
src/ar/egydead/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
src/ar/egydead/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
src/ar/egydead/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/ar/egydead/res/web_hi_res_512.png
Normal file
After Width: | Height: | Size: 50 KiB |
|
@ -0,0 +1,320 @@
|
|||
package eu.kanade.tachiyomi.animeextension.ar.egydead
|
||||
|
||||
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.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.lib.doodextractor.DoodExtractor
|
||||
import eu.kanade.tachiyomi.lib.mixdropextractor.MixDropExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMap
|
||||
import okhttp3.FormBody
|
||||
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
|
||||
|
||||
class EgyDead : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val name = "Egy Dead"
|
||||
|
||||
// TODO: Check frequency of url changes to potentially
|
||||
// add back overridable baseurl preference
|
||||
override val baseUrl = "https://egydead.space"
|
||||
|
||||
override val lang = "ar"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
// ================================== popular ==================================
|
||||
|
||||
override fun popularAnimeSelector(): String = "div.pin-posts-list li.movieItem"
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String = "div.whatever"
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request = GET(baseUrl)
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.setUrlWithoutDomain(element.select("a").attr("href"))
|
||||
anime.title = element.select("h1.BottomTitle").text()
|
||||
anime.thumbnail_url = element.select("a img").attr("src")
|
||||
return anime
|
||||
}
|
||||
|
||||
// ================================== episodes ==================================
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val episodes = mutableListOf<SEpisode>()
|
||||
fun episodeExtract(element: Element): SEpisode {
|
||||
val episode = SEpisode.create()
|
||||
episode.setUrlWithoutDomain(element.attr("href"))
|
||||
episode.name = element.attr("title")
|
||||
return episode
|
||||
}
|
||||
fun addEpisodes(res: Response, final: Boolean = false) {
|
||||
val document = res.asJsoup()
|
||||
val url = res.request.url.toString()
|
||||
if (final) {
|
||||
document.select(episodeListSelector()).map {
|
||||
val episode = episodeFromElement(it)
|
||||
val season = document.select("div.infoBox div.singleTitle").text()
|
||||
val seasonTxt = season.substringAfter("الموسم ").substringBefore(" ")
|
||||
episode.name = if (season.contains("موسم"))"الموسم $seasonTxt ${episode.name}" else episode.name
|
||||
episodes.add(episode)
|
||||
}
|
||||
} else if (url.contains("assembly")) {
|
||||
document.select("div.salery-list li.movieItem a").map {
|
||||
episodes.add(episodeExtract(it))
|
||||
}
|
||||
} else if (url.contains("serie") || url.contains("season")) {
|
||||
if (document.select("div.seasons-list li.movieItem a").isNullOrEmpty()) {
|
||||
document.select(episodeListSelector()).map {
|
||||
episodes.add(episodeFromElement(it))
|
||||
}
|
||||
} else {
|
||||
document.select("div.seasons-list li.movieItem a").map {
|
||||
addEpisodes(client.newCall(GET(it.attr("href"))).execute(), true)
|
||||
}
|
||||
}
|
||||
} else if (url.contains("episode")) {
|
||||
document.selectFirst("#breadcrumbs li a[itemprop=url]")?.let {
|
||||
addEpisodes(client.newCall(GET(it.attr("href"))).execute())
|
||||
}
|
||||
} else {
|
||||
val episode = SEpisode.create()
|
||||
episode.name = "مشاهدة"
|
||||
episode.setUrlWithoutDomain(url)
|
||||
episodes.add(episode)
|
||||
}
|
||||
// document.select(episodeListSelector()).map { episodes.add(episodeFromElement(it)) }
|
||||
}
|
||||
addEpisodes(response)
|
||||
return episodes
|
||||
}
|
||||
|
||||
override fun episodeListSelector() = "div.EpsList li a"
|
||||
|
||||
override fun episodeFromElement(element: Element): SEpisode {
|
||||
val episode = SEpisode.create()
|
||||
episode.setUrlWithoutDomain(element.attr("href"))
|
||||
episode.name = element.select("a").text()
|
||||
episode.episode_number = element.select("a").text().filter { it.isDigit() }.toFloat()
|
||||
return episode
|
||||
}
|
||||
|
||||
// ================================== video urls ==================================
|
||||
private val streamWishExtractor by lazy { StreamWishExtractor(client, headers) }
|
||||
|
||||
override suspend fun getVideoList(episode: SEpisode): List<Video> {
|
||||
val requestBody = FormBody.Builder().add("View", "1").build()
|
||||
|
||||
val document = client.newCall(POST(baseUrl + episode.url, body = requestBody))
|
||||
.await()
|
||||
.asJsoup()
|
||||
return document.select(videoListSelector()).parallelCatchingFlatMap {
|
||||
val url = it.attr("data-link")
|
||||
extractVideos(url)
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractVideos(url: String): List<Video> {
|
||||
return when {
|
||||
DOOD_REGEX.containsMatchIn(url) -> {
|
||||
DoodExtractor(client).videoFromUrl(url, "Dood mirror")?.let(::listOf)
|
||||
}
|
||||
url.contains("mdbekjwqa") -> {
|
||||
MixDropExtractor(client).videoFromUrl(url)
|
||||
}
|
||||
url.contains("ahvsh") -> {
|
||||
val request = client.newCall(GET(url, headers)).execute().asJsoup()
|
||||
val script = request.selectFirst("script:containsData(sources)")!!.data()
|
||||
val streamLink = Regex("sources:\\s*\\[\\{\\s*\\t*file:\\s*[\"']([^\"']+)").find(script)!!.groupValues[1]
|
||||
val quality = Regex("'qualityLabels'\\s*:\\s*\\{\\s*\".*?\"\\s*:\\s*\"(.*?)\"").find(script)!!.groupValues[1]
|
||||
Video(streamLink, "StreamHide: $quality", streamLink).let(::listOf)
|
||||
}
|
||||
STREAMWISH_REGEX.containsMatchIn(url) -> {
|
||||
streamWishExtractor.videosFromUrl(url)
|
||||
}
|
||||
url.contains("fanakishtuna") -> {
|
||||
val request = client.newCall(GET(url, headers)).execute().asJsoup()
|
||||
val data = request.selectFirst("script:containsData(sources)")!!.data()
|
||||
val streamLink = Regex("sources:\\s*\\[\\{\\s*\\t*file:\\s*[\"']([^\"']+)").find(data)!!.groupValues[1]
|
||||
listOf(Video(streamLink, "Mirror: High Quality", streamLink))
|
||||
}
|
||||
url.contains("uqload") -> {
|
||||
val newURL = url.replace("https://uqload.co/", "https://www.uqload.co/")
|
||||
val request = client.newCall(GET(newURL, headers)).execute().asJsoup()
|
||||
val data = request.selectFirst("script:containsData(sources)")!!.data()
|
||||
val streamLink = data.substringAfter("sources: [\"").substringBefore("\"]")
|
||||
listOf(Video(streamLink, "Uqload: Mirror", streamLink))
|
||||
}
|
||||
|
||||
else -> null
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
override fun videoListSelector() = "ul.serversList li"
|
||||
|
||||
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString("preferred_quality", "1080p")
|
||||
if (quality != null) {
|
||||
val newList = mutableListOf<Video>()
|
||||
var preferred = 0
|
||||
for (video in this) {
|
||||
if (video.quality == quality) {
|
||||
newList.add(preferred, video)
|
||||
preferred++
|
||||
} else {
|
||||
newList.add(video)
|
||||
}
|
||||
}
|
||||
return newList
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
// ================================== search ==================================
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String = "div.pagination-two a:contains(›)"
|
||||
|
||||
override fun searchAnimeSelector(): String = "div.catHolder li.movieItem"
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val url = if (query.isNotBlank()) {
|
||||
"$baseUrl/page/$page/?s=$query"
|
||||
} else {
|
||||
val url = baseUrl
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
is CategoryList -> {
|
||||
if (filter.state > 0) {
|
||||
val catQ = getCategoryList()[filter.state].query
|
||||
val catUrl = "$baseUrl/$catQ/?page=$page/"
|
||||
return GET(catUrl, headers)
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
return GET(url, headers)
|
||||
}
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun searchAnimeFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.setUrlWithoutDomain(element.select("a").attr("href"))
|
||||
anime.title = element.select("h1.BottomTitle").text()
|
||||
anime.thumbnail_url = element.select("a img").attr("src")
|
||||
return anime
|
||||
}
|
||||
|
||||
override fun getFilterList() = AnimeFilterList(
|
||||
CategoryList(categoriesName),
|
||||
)
|
||||
|
||||
private class CategoryList(categories: Array<String>) : AnimeFilter.Select<String>("الأقسام", categories)
|
||||
|
||||
private data class CatUnit(val name: String, val query: String)
|
||||
|
||||
private val categoriesName = getCategoryList().map {
|
||||
it.name
|
||||
}.toTypedArray()
|
||||
|
||||
private fun getCategoryList() = listOf(
|
||||
CatUnit("اختر القسم", ""),
|
||||
CatUnit("افلام اجنبى", "category/افلام-اجنبي"),
|
||||
CatUnit("افلام اسلام الجيزاوى", "category/ترجمات-اسلام-الجيزاوي"),
|
||||
CatUnit("افلام انمى", "category/افلام-كرتون"),
|
||||
CatUnit("افلام تركيه", "category/افلام-تركية"),
|
||||
CatUnit("افلام اسيويه", "category/افلام-اسيوية"),
|
||||
CatUnit("افلام مدبلجة", "category/افلام-اجنبية-مدبلجة"),
|
||||
CatUnit("سلاسل افلام", "assembly"),
|
||||
CatUnit("مسلسلات اجنبية", "series-category/مسلسلات-اجنبي"),
|
||||
CatUnit("مسلسلات انمى", "series-category/مسلسلات-انمي"),
|
||||
CatUnit("مسلسلات تركية", "series-category/مسلسلات-تركية"),
|
||||
CatUnit("مسلسلات اسيوىة", "series-category/مسلسلات-اسيوية"),
|
||||
CatUnit("مسلسلات لاتينية", "series-category/مسلسلات-لاتينية"),
|
||||
CatUnit("المسلسلات الكاملة", "serie"),
|
||||
CatUnit("المواسم الكاملة", "season"),
|
||||
)
|
||||
|
||||
// ================================== details ==================================
|
||||
|
||||
override fun animeDetailsParse(document: Document): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.thumbnail_url = document.select("div.single-thumbnail img").attr("src")
|
||||
anime.title = document.select("div.infoBox div.singleTitle").text()
|
||||
anime.author = document.select("div.LeftBox li:contains(البلد) a").text()
|
||||
anime.artist = document.select("div.LeftBox li:contains(القسم) a").text()
|
||||
anime.genre = document.select("div.LeftBox li:contains(النوع) a, div.LeftBox li:contains(اللغه) a, div.LeftBox li:contains(السنه) a").joinToString(", ") { it.text() }
|
||||
anime.description = document.select("div.infoBox div.extra-content p").text()
|
||||
anime.status = if (anime.title.contains("كامل") || anime.title.contains("فيلم")) SAnime.COMPLETED else SAnime.ONGOING
|
||||
return anime
|
||||
}
|
||||
|
||||
// ================================== latest ==================================
|
||||
|
||||
override fun latestUpdatesSelector(): String = "section.main-section li.movieItem"
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = "div.pagination ul.page-numbers li a.next"
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/?page=$page/")
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.setUrlWithoutDomain(element.select("a").attr("href"))
|
||||
anime.title = element.select("h1.BottomTitle").text()
|
||||
anime.thumbnail_url = element.select("a img").attr("src")
|
||||
return anime
|
||||
}
|
||||
|
||||
// ================================== preferences ==================================
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val videoQualityPref = ListPreference(screen.context).apply {
|
||||
key = "preferred_quality"
|
||||
title = "Preferred quality"
|
||||
entries = arrayOf("1080p", "720p", "480p", "360p", "240p", "DoodStream", "Uqload")
|
||||
entryValues = arrayOf("1080", "720", "480", "360", "240", "Dood", "Uqload")
|
||||
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)
|
||||
}
|
||||
|
||||
// like|kharabnahk
|
||||
companion object {
|
||||
private val DOOD_REGEX = Regex("(do*d(?:stream)?\\.(?:com?|watch|to|s[ho]|cx|la|w[sf]|pm|re|yt|stream))/[de]/([0-9a-zA-Z]+)|ds2play")
|
||||
private val STREAMWISH_REGEX = Regex("ajmidyad|alhayabambi|atabknh[ks]|https://.*\\.sbs/e/")
|
||||
}
|
||||
}
|
11
src/ar/faselhd/build.gradle
Normal file
|
@ -0,0 +1,11 @@
|
|||
ext {
|
||||
extName = 'FASELHD'
|
||||
extClass = '.FASELHD'
|
||||
extVersionCode = 15
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib:playlist-utils"))
|
||||
}
|
BIN
src/ar/faselhd/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
src/ar/faselhd/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
src/ar/faselhd/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
src/ar/faselhd/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 10 KiB |