Initial commit

This commit is contained in:
almightyhak 2024-06-20 11:54:12 +07:00
commit 98ed7e8839
2263 changed files with 108711 additions and 0 deletions

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".tr.hdfilmcehennemi.HDFilmCehennemiUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="www.hdfilmcehennemi.us"
android:pathPattern="/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,13 @@
ext {
extName = 'HDFilmCehennemi'
extClass = '.HDFilmCehennemi'
extVersionCode = 15
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:unpacker"))
implementation(project(":lib:playlist-utils"))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -0,0 +1,305 @@
package eu.kanade.tachiyomi.animeextension.tr.hdfilmcehennemi
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.tr.hdfilmcehennemi.extractors.CloseloadExtractor
import eu.kanade.tachiyomi.animeextension.tr.hdfilmcehennemi.extractors.VidmolyExtractor
import eu.kanade.tachiyomi.animeextension.tr.hdfilmcehennemi.extractors.XBetExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import eu.kanade.tachiyomi.util.parallelMapBlocking
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.Serializable
import okhttp3.FormBody
import okhttp3.MultipartBody
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.text.SimpleDateFormat
import java.util.Locale
class HDFilmCehennemi : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "HDFilmCehennemi"
override val baseUrl = "https://www.hdfilmcehennemi.us"
override val lang = "tr"
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
.add("Origin", baseUrl)
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/en-cok-begenilen-filmleri-izle/page/$page/")
override fun popularAnimeSelector() = "div.row div.poster > a"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
title = element.selectFirst("h2.title")!!.text()
thumbnail_url = element.selectFirst("img")?.absUrl("data-src")
}
override fun popularAnimeNextPageSelector() = "ul.pagination > li > a[rel=next]"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/page/$page/")
override fun latestUpdatesSelector() = popularAnimeSelector()
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector()
// =============================== Search ===============================
override fun getFilterList() = HDFilmCehennemiFilters.FILTER_LIST
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val id = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/$id"))
.awaitSuccess()
.use(::searchAnimeByIdParse)
} else {
super.getSearchAnime(page, query, filters)
}
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
return AnimesPage(listOf(details), false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val headers = headersBuilder()
.add("X-Requested-With", "XMLHttpRequest")
.build()
return when {
query.isNotBlank() -> {
val body = FormBody.Builder().add("query", query).build()
POST("$baseUrl/search/", headers, body)
}
else -> {
val params = HDFilmCehennemiFilters.getSearchParameters(filters)
val form = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("kesfet[type]", params.type)
.addFormDataPart("kesfet[genres]", params.genres)
.addFormDataPart("kesfet[years]", params.years)
.addFormDataPart("kesfet[imdb]", params.imdbScore)
.addFormDataPart("kesfet[orderBy]", params.order)
.addFormDataPart("page", page.toString())
.build()
POST("$baseUrl/movies/load/", headers, form)
}
}
}
@Serializable
data class SearchResponse(val result: List<ItemDto>)
@Serializable
data class ItemDto(val title: String, val poster: String, val slug: String, val slug_prefix: String)
@Serializable
data class FilterSearchResponse(val html: String, val showMore: Boolean, val status: Int)
override fun searchAnimeParse(response: Response): AnimesPage {
return when {
response.request.url.toString().contains("/search/") -> { // Text search
val data = response.parseAs<SearchResponse>()
val items = data.result.map {
SAnime.create().apply {
title = it.title
thumbnail_url = "$baseUrl/uploads/poster/" + it.poster
url = "/" + it.slug_prefix + it.slug
}
}
AnimesPage(items, false)
}
else -> { // Filter search
val data = response.parseAs<FilterSearchResponse>()
if (data.status != 1) return AnimesPage(emptyList(), false)
val doc = response.asJsoup(data.html)
val items = doc.select(searchAnimeSelector()).map(::searchAnimeFromElement)
AnimesPage(items, data.showMore)
}
}
}
override fun searchAnimeSelector() = "div.poster > a"
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector(): String? {
throw UnsupportedOperationException()
}
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
status = when {
document.location().contains("/dizi/") -> SAnime.UNKNOWN // serie
else -> SAnime.COMPLETED // movie
}
val div = document.selectFirst("div.card-body > div.row")!!
div.selectFirst("img")!!.run {
thumbnail_url = absUrl("src")
title = attr("alt")
}
genre = div.select("div > a[href*=tur/]").eachText().joinToString().takeIf(String::isNotEmpty)
artist = div.select("a.chip[href*=oyuncu/]").eachText().joinToString().takeIf(String::isNotEmpty)
description = div.selectFirst("article > p")?.text()
}
// ============================== Episodes ==============================
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
// Series
if (anime.url.contains("/dizi/")) return super.getEpisodeList(anime)
// Movies
return listOf(
SEpisode.create().apply {
url = anime.url
name = "Movie"
episode_number = 1F
},
)
}
override fun episodeListParse(response: Response) =
super.episodeListParse(response).sortedByDescending { it.episode_number }
override fun episodeListSelector() = "div#seasonsTabs-tabContent div.card-list-item > a"
private val numberRegex by lazy { Regex("(\\d+)\\.") }
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
setUrlWithoutDomain(element.attr("href"))
name = element.selectFirst("h3")!!.text()
date_upload = element.selectFirst("date")?.attr("datetime")?.toDate() ?: 0L
val (seasonNum, epNum) = numberRegex.findAll(name).map { it.groupValues.last() }.toList()
// good luck trying to track this xD
episode_number = "$seasonNum.${epNum.padStart(3, '0')}".toFloatOrNull() ?: 1F
}
// ============================ Video Links =============================
private val vidmolyExtractor by lazy { VidmolyExtractor(client, headers) }
private val closeloadExtractor by lazy { CloseloadExtractor(client, headers) }
private val xbetExtractor by lazy { XBetExtractor(client, headers) }
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
return doc.select("div.card-body > nav > a:not([href^=#])")
.drop(1)
.parallelMapBlocking { client.newCall(GET(it.absUrl("href") + "/")).await().asJsoup() }
.let { listOf(doc) + it }
.mapNotNull { it.selectFirst("div.card-video > iframe") }
.map { it.attr("data-src").ifBlank { it.attr("src") } }
.filter(String::isNotBlank)
.parallelCatchingFlatMapBlocking { url ->
when {
url.contains("https://closeload") -> closeloadExtractor.videosFromUrl(url)
url.contains("vidmoly") -> vidmolyExtractor.videosFromUrl(url)
url.contains("trstx.org") -> xbetExtractor.videosFromUrl(url)
else -> emptyList()
}
}
}
override fun videoListSelector(): String {
throw UnsupportedOperationException()
}
override fun videoFromElement(element: Element): Video {
throw UnsupportedOperationException()
}
override fun videoUrlParse(document: Document): String {
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()
}
private fun String.toDate(): Long {
return runCatching { DATE_FORMATTER.parse(trim())?.time }
.getOrNull() ?: 0L
}
companion object {
const val PREFIX_SEARCH = "id:"
private val DATE_FORMATTER by lazy {
SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
}
private const val PREF_QUALITY_KEY = "pref_quality_key"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "720p"
private val PREF_QUALITY_ENTRIES = arrayOf("360p", "480p", "720p", "1080p")
private val PREF_QUALITY_VALUES = PREF_QUALITY_ENTRIES
}
}

View file

@ -0,0 +1,155 @@
package eu.kanade.tachiyomi.animeextension.tr.hdfilmcehennemi
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object HDFilmCehennemiFilters {
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
}
open class CheckBoxFilterList(name: String, val pairs: Array<Pair<String, String>>) :
AnimeFilter.Group<AnimeFilter.CheckBox>(name, pairs.map { CheckBoxVal(it.first, false) })
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return (first { it is R } as QueryPartFilter).toQueryPart()
}
private inline fun <reified R> AnimeFilterList.parseCheckbox(
options: Array<Pair<String, String>>,
): String {
return (first { it is R } as CheckBoxFilterList).state
.asSequence()
.filter { it.state }
.map { checkbox -> options.find { it.first == checkbox.name }!!.second }
.joinToString(",")
}
class TypeFilter : QueryPartFilter("Türü", HDFilmCehennemiFiltersData.TYPES)
class GenresFilter : CheckBoxFilterList("Türler", HDFilmCehennemiFiltersData.GENRES)
class YearsFilter : CheckBoxFilterList("Yıllar", HDFilmCehennemiFiltersData.YEARS)
class IMDBScoreFilter : CheckBoxFilterList("IMDb Puanı", HDFilmCehennemiFiltersData.SCORES)
class SortFilter : AnimeFilter.Sort(
"Sıralama Türü",
HDFilmCehennemiFiltersData.ORDERS.map { it.first }.toTypedArray(),
Selection(0, false),
)
val FILTER_LIST get() = AnimeFilterList(
AnimeFilter.Header("NOTE: Ignored if using text search!"),
AnimeFilter.Separator(),
TypeFilter(),
SortFilter(),
AnimeFilter.Separator(),
IMDBScoreFilter(),
GenresFilter(),
YearsFilter(),
)
data class FilterSearchParams(
val type: String = "1",
val order: String = "posts.imdb desc",
val imdbScore: String = "",
val genres: String = "",
val years: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
val sortFilter = filters.firstOrNull { it is SortFilter } as? SortFilter
val orderBy = sortFilter?.state?.run {
val order = HDFilmCehennemiFiltersData.ORDERS[index].second
val orderWay = if (ascending) "asc" else "desc"
"$order $orderWay"
} ?: "posts.imdb desc"
return FilterSearchParams(
filters.asQueryPart<TypeFilter>(),
orderBy,
filters.parseCheckbox<IMDBScoreFilter>(HDFilmCehennemiFiltersData.SCORES),
filters.parseCheckbox<GenresFilter>(HDFilmCehennemiFiltersData.GENRES),
filters.parseCheckbox<YearsFilter>(HDFilmCehennemiFiltersData.YEARS),
)
}
private object HDFilmCehennemiFiltersData {
val TYPES = arrayOf(
Pair("Filmler", "1"),
Pair("Diziler", "2"),
)
val GENRES = arrayOf(
Pair("Adult", "40"),
Pair("Aile", "8"),
Pair("Aksiyon", "1"),
Pair("Animasyon", "3"),
Pair("Belgesel", "6"),
Pair("Bilim Kurgu", "24"),
Pair("Biyografi", "26"),
Pair("Dram", "7"),
Pair("Fantastik", "9"),
Pair("Film-Noir", "39"),
Pair("Game-Show", "34"),
Pair("Gerilim", "16"),
Pair("Gizem", "13"),
Pair("Komedi", "4"),
Pair("Korku", "11"),
Pair("Macera", "2"),
Pair("Müzik", "12"),
Pair("Müzik", "27"),
Pair("Polisiye", "32"),
Pair("Reality", "37"),
Pair("Reality-TV", "33"),
Pair("Romantik", "14"),
Pair("Savaş", "17"),
Pair("Short", "35"),
Pair("Spor", "28"),
Pair("Suç", "5"),
Pair("Tarih", "10"),
Pair("Western", "18"),
)
val YEARS = arrayOf(
Pair("2024", "2024"),
Pair("2023", "2023"),
Pair("2022", "2022"),
Pair("2021", "2021"),
Pair("2020", "2020"),
Pair("2019", "2019"),
Pair("2018", "2018"),
Pair("2017", "2017"),
Pair("2016", "2016"),
Pair("2015-2010 arası", "2010-2015"),
Pair("2010-2000 arası", "2000-2010"),
Pair("2000 öncesi", "1901-2000"),
)
val SCORES = arrayOf(
Pair("9", "9-10"),
Pair("8", "8-9"),
Pair("7", "7-8"),
Pair("6", "6-7"),
Pair("5 ve altı", "0-6"),
)
val ORDERS = arrayOf(
Pair("IMDb Puanına", "posts.imdb"),
Pair("Site Puanı", "avg"),
Pair("Yıla", "posts.year"),
Pair("İzlenme", "views"),
)
}
}

View file

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.tr.hdfilmcehennemi
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://www.hdfilmcehennemi.us/<item> intents
* and redirects them to the main Aniyomi process.
*/
class HDFilmCehennemiUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size == 1) {
val item = pathSegments.first()
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${HDFilmCehennemi.PREFIX_SEARCH}$item")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View file

@ -0,0 +1,62 @@
package eu.kanade.tachiyomi.animeextension.tr.hdfilmcehennemi.extractors
import android.util.Base64
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.unpacker.Unpacker
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 okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
class CloseloadExtractor(private val client: OkHttpClient, private val headers: Headers) {
suspend fun videosFromUrl(url: String): List<Video> {
val doc = client.newCall(GET(url, headers)).await().asJsoup()
val script = doc.selectFirst("script:containsData(eval):containsData(PlayerInit)")?.data()
?: return emptyList()
val unpackedScript = Unpacker.unpack(script).takeIf(String::isNotEmpty)
?: return emptyList()
val varName = unpackedScript.substringAfter("atob(").substringBefore(")")
val playlistUrl = unpackedScript.getProperty("$varName=")
.let { String(Base64.decode(it, Base64.DEFAULT)) }
val hostUrl = "https://" + url.toHttpUrl().host
val videoHeaders = headers.newBuilder()
.set("Referer", url)
.set("origin", hostUrl)
.build()
runCatching { tryAjaxPost(unpackedScript, hostUrl) }
val subtitles = doc.select("track[src]").map {
Track(it.absUrl("src"), it.attr("label").ifEmpty { it.attr("srclang") })
}
return listOf(Video(playlistUrl, "Closeload", playlistUrl, videoHeaders, subtitleTracks = subtitles))
}
private suspend fun tryAjaxPost(script: String, hostUrl: String) {
val hash = script.getProperty("hash:")
val url = script.getProperty("url:").let {
when {
it.startsWith("//") -> "https:$it"
it.startsWith("/") -> "https://" + hostUrl + it
!it.startsWith("https://") -> "https://$it"
else -> it
}
}
val body = FormBody.Builder().add("hash", hash).build()
client.newCall(POST(url, headers, body)).await().close()
}
private fun String.getProperty(before: String) =
substringAfter("$before\"").substringBefore('"')
}

View file

@ -0,0 +1,23 @@
package eu.kanade.tachiyomi.animeextension.tr.hdfilmcehennemi.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
import okhttp3.Headers
import okhttp3.OkHttpClient
class VidmolyExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
suspend fun videosFromUrl(url: String): List<Video> {
val body = client.newCall(GET(url, headers)).await()
.body.string()
val playlistUrl = body.substringAfter("file:\"", "").substringBefore('"', "")
.takeIf(String::isNotBlank)
?: return emptyList()
return playlistUtils.extractFromHls(playlistUrl, url, videoNameGen = { "Vidmoly - $it" })
}
}

View file

@ -0,0 +1,58 @@
package eu.kanade.tachiyomi.animeextension.tr.hdfilmcehennemi.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
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.parseAs
import kotlinx.serialization.Serializable
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
class XBetExtractor(
private val client: OkHttpClient,
private val headers: Headers,
) {
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
suspend fun videosFromUrl(url: String): List<Video> {
val doc = client.newCall(GET(url, headers)).await().asJsoup()
val script = doc.selectFirst("script:containsData(playerConfigs =)")?.data()
?: return emptyList()
val host = "https://${url.toHttpUrl().host}"
val postPath = script.substringAfter("file\":\"").substringBefore('"')
.replace("\\", "")
val postHeaders = headers.newBuilder()
.set("Referer", url)
.set("Origin", host)
.build()
val postRes = client.newCall(POST(host + postPath, postHeaders)).await()
.parseAs<List<VideoItemDto>> { it.replace("[],", "") }
return postRes.flatMap { video ->
runCatching {
val playlistUrl = client.newCall(POST(host + video.path, postHeaders)).await()
.body.string()
playlistUtils.extractFromHls(
playlistUrl,
url,
videoNameGen = { "[${video.title}] XBet - $it" },
)
}.getOrElse { emptyList() }
}
}
@Serializable
data class VideoItemDto(val file: String, val title: String) {
val path = "/playlist/${file.removeSuffix("~")}.txt"
}
}