Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(src/ar): New Soruce: Cima Leek #3251

Merged
merged 14 commits into from
May 26, 2024
7 changes: 7 additions & 0 deletions src/ar/cimaleek/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
ext {
extName = 'Cimaleek'
extClass = '.Cimaleek'
extVersionCode = 1
}

apply from: "$rootDir/common.gradle"
Binary file added src/ar/cimaleek/res/mipmap-hdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/ar/cimaleek/res/mipmap-mdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/ar/cimaleek/res/mipmap-xhdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/ar/cimaleek/res/mipmap-xxhdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Secozzi marked this conversation as resolved.
Show resolved Hide resolved
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
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.GetSourcesInterceptor
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 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

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 interceptor by lazy { GetSourcesInterceptor(VIDEO_REGEX, 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)).execute().asJsoup()
Secozzi marked this conversation as resolved.
Show resolved Hide resolved
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 videoList = mutableListOf<Video>()
val videoUrl = "$baseUrl/wp-json/lalaplayer/v2/".toHttpUrlOrNull()!!.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())).execute().body.string()
adly98 marked this conversation as resolved.
Show resolved Hide resolved
val embedUrl = videoFrame.substringAfter("embed_url\":\"").substringBefore("\"")
val referer = headers.newBuilder().add("Referer", "$baseUrl/").build()
val webViewInterceptor = client.newBuilder().addInterceptor(interceptor).build()
val videoResponse = webViewInterceptor.newCall(GET(embedUrl, referer)).execute()
val trueVideoUrl = videoResponse.request.url.toString()
when {
"index-v1-a1.m3u8" in trueVideoUrl || "list.m3u8" in trueVideoUrl || ".mp4" in trueVideoUrl -> {
videoList.add(Video(trueVideoUrl, element.text(), trueVideoUrl, headers = referer))
}
"master.m3u8" in trueVideoUrl -> {
videoResponse.body.string().substringAfter("#EXT-X-STREAM-INF:").split("#EXT-X-STREAM-INF:").forEach {
val quality = it.substringAfter("RESOLUTION=").substringBefore("\n").substringAfter("x").substringBefore(",") + "p"
val playUrl = it.substringAfter("\n").substringBefore("\n")
val url = if (playUrl.startsWith("index")) trueVideoUrl.replace("master", "index-v1-a1") else playUrl
videoList.add(Video(url, "${element.text()}: $quality", url, headers = referer))
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use the playlist-utils lib?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the idea is i don't want to repeat request as the interceptor already return response of the m3u8 url

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true, the most ideal way is to modify the interceptor so you don't need to resend the request, but if you dont want to spend the time, that's fine

}
return videoList
}

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/".toHttpUrlOrNull()!!.newBuilder()
Secozzi marked this conversation as resolved.
Show resolved Hide resolved
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)
}

companion object {
private val VIDEO_REGEX by lazy { Regex("""m3u8|.mp4""") }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.Headers.Companion.toHeaders
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.IOException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

class GetSourcesInterceptor(private val searchRegex: Regex, private val globalHeaders: Headers) : Interceptor {
private val context = Injekt.get<Application>()
private val handler by lazy { Handler(Looper.getMainLooper()) }

private val initWebView by lazy {
WebSettings.getDefaultUserAgent(context)
}

@Synchronized
override fun intercept(chain: Interceptor.Chain): Response {
initWebView

val request = chain.request()

try {
val newRequest = resolveWithWebView(request)

return chain.proceed(newRequest ?: request)
} catch (e: Exception) {
throw IOException(e)
}
}

@SuppressLint("SetJavaScriptEnabled")
private fun resolveWithWebView(request: Request): Request? {
Copy link
Contributor

@Secozzi Secozzi May 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be better to return the string of the url instead of a request? It wouldn't need to be an interceptor and then you'd be able to use playlist-utils instead, simplifying some of the process

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm let me try

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but can i override intercept function to return string instead of response ?? @Secozzi

Copy link
Contributor

@Secozzi Secozzi May 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It shouldn't be an interceptor, just call it an extractor or something instead

Copy link
Contributor Author

@adly98 adly98 May 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what exactly ? i mean in Interceptor Class the intercept function returns Response i can't override that to return a String instead
i use interceptor cuz i can't make an extractor

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You shouldn't inherit from interceptor in the first place

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O.o i totally don't get it, i mean then how am i supposed to intercept the requests ??

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You load the URL in webview like you did before

val latch = CountDownLatch(1)

var webView: WebView? = null

val origRequestUrl = request.url.toString()
val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
var newRequest: Request? = null

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 (searchRegex.containsMatchIn(url)) {
val newHeaders = request.requestHeaders.toHeaders()
newRequest = GET(url, newHeaders)
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 newRequest
}

companion object {
const val TIMEOUT_SEC: Long = 20
}
}