2022-09-14 22:16:36 +00:00
|
|
|
package com.hexated
|
|
|
|
|
|
|
|
import com.fasterxml.jackson.annotation.JsonProperty
|
|
|
|
import com.lagradost.cloudstream3.*
|
2022-12-09 14:54:20 +00:00
|
|
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
|
|
|
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId
|
2022-12-09 14:08:31 +00:00
|
|
|
import com.lagradost.cloudstream3.utils.*
|
2022-09-14 22:16:36 +00:00
|
|
|
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
|
|
|
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
2022-12-09 14:08:31 +00:00
|
|
|
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
|
|
|
import com.lagradost.nicehttp.RequestBodyTypes
|
|
|
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|
|
|
import okhttp3.RequestBody.Companion.toRequestBody
|
2022-09-14 22:16:36 +00:00
|
|
|
|
|
|
|
class Loklok : MainAPI() {
|
|
|
|
override var name = "Loklok"
|
|
|
|
override val hasMainPage = true
|
|
|
|
override val hasChromecastSupport = true
|
|
|
|
override val instantLinkLoading = true
|
2022-12-09 20:28:26 +00:00
|
|
|
override val hasQuickSearch = true
|
2022-09-14 22:16:36 +00:00
|
|
|
override val supportedTypes = setOf(
|
|
|
|
TvType.Movie,
|
|
|
|
TvType.TvSeries,
|
|
|
|
TvType.Anime,
|
|
|
|
TvType.AsianDrama,
|
|
|
|
)
|
|
|
|
|
|
|
|
private val headers = mapOf(
|
|
|
|
"lang" to "en",
|
|
|
|
"versioncode" to "11",
|
|
|
|
"clienttype" to "ios_jike_default"
|
|
|
|
)
|
|
|
|
|
|
|
|
// no license found
|
|
|
|
// thanks to https://github.com/napthedev/filmhot for providing API
|
2022-09-15 02:40:50 +00:00
|
|
|
companion object {
|
2022-10-17 06:32:34 +00:00
|
|
|
private val api = base64DecodeAPI("dg==LnQ=b2s=a2w=bG8=aS4=YXA=ZS0=aWw=b2I=LW0=Z2E=Ly8=czo=dHA=aHQ=")
|
2022-09-15 02:40:50 +00:00
|
|
|
private val apiUrl = "$api/${base64Decode("Y21zL2FwcA==")}"
|
2022-10-02 06:38:07 +00:00
|
|
|
private val searchApi = base64Decode("aHR0cHM6Ly9sb2tsb2suY29t")
|
2022-12-09 14:54:20 +00:00
|
|
|
private const val jikanAPI = "https://api.jikan.moe/v4"
|
2022-09-15 02:40:50 +00:00
|
|
|
private const val mainImageUrl = "https://images.weserv.nl"
|
2022-10-17 06:32:34 +00:00
|
|
|
|
|
|
|
private fun base64DecodeAPI(api: String): String {
|
|
|
|
return api.chunked(4).map { base64Decode(it) }.reversed().joinToString("")
|
|
|
|
}
|
|
|
|
|
2022-09-15 02:40:50 +00:00
|
|
|
}
|
2022-09-14 22:16:36 +00:00
|
|
|
|
2022-10-01 23:57:22 +00:00
|
|
|
private fun encode(input: String): String =
|
|
|
|
java.net.URLEncoder.encode(input, "utf-8").replace("+", "%20")
|
2022-09-14 22:16:36 +00:00
|
|
|
|
|
|
|
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
|
|
|
|
val home = ArrayList<HomePageList>()
|
2022-10-28 12:09:11 +00:00
|
|
|
for (i in 0..6) {
|
|
|
|
// delay(500)
|
2022-09-14 22:16:36 +00:00
|
|
|
app.get("$apiUrl/homePage/getHome?page=$i", headers = headers)
|
|
|
|
.parsedSafe<Home>()?.data?.recommendItems
|
|
|
|
?.filterNot { it.homeSectionType == "BLOCK_GROUP" }
|
|
|
|
?.filterNot { it.homeSectionType == "BANNER" }
|
|
|
|
?.mapNotNull { res ->
|
|
|
|
val header = res.homeSectionName ?: return@mapNotNull null
|
|
|
|
val media = res.media?.mapNotNull { media -> media.toSearchResponse() }
|
|
|
|
?: throw ErrorLoadingException("Invalid Json Reponse")
|
|
|
|
home.add(HomePageList(header, media))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return HomePageResponse(home)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun Media.toSearchResponse(): SearchResponse? {
|
|
|
|
|
|
|
|
return newMovieSearchResponse(
|
|
|
|
title ?: name ?: return null,
|
|
|
|
UrlData(id, category).toJson(),
|
|
|
|
TvType.Movie,
|
|
|
|
) {
|
|
|
|
this.posterUrl = (imageUrl ?: coverVerticalUrl)?.let {
|
|
|
|
"$mainImageUrl/?url=${encode(it)}&w=175&h=246&fit=cover&output=webp"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-09 20:28:26 +00:00
|
|
|
override suspend fun quickSearch(query: String): List<SearchResponse>? {
|
|
|
|
val body = mapOf(
|
|
|
|
"searchKeyWord" to query,
|
|
|
|
"size" to "50",
|
|
|
|
"sort" to "",
|
|
|
|
"searchType" to "",
|
|
|
|
).toJson().toRequestBody(RequestBodyTypes.JSON.toMediaTypeOrNull())
|
|
|
|
|
|
|
|
return app.post(
|
|
|
|
"$apiUrl/search/v1/searchWithKeyWord",
|
|
|
|
requestBody = body,
|
|
|
|
headers = headers
|
|
|
|
).parsedSafe<QuickSearchRes>()?.data?.searchResults?.mapNotNull { media ->
|
|
|
|
media.toSearchResponse()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-14 22:16:36 +00:00
|
|
|
override suspend fun search(query: String): List<SearchResponse> {
|
|
|
|
val res = app.get(
|
2022-10-02 06:38:07 +00:00
|
|
|
"$searchApi/search?keyword=$query",
|
2022-10-01 22:06:16 +00:00
|
|
|
).document
|
|
|
|
|
|
|
|
val script = res.select("script").find { it.data().contains("function(a,b,c,d,e") }?.data()
|
|
|
|
?.substringAfter("searchResults:[")?.substringBefore("]}],fetch")
|
|
|
|
|
2022-10-01 23:57:22 +00:00
|
|
|
return res.select("div.search-list div.search-video-card").mapIndexed { num, block ->
|
|
|
|
val name = block.selectFirst("h2.title")?.text()
|
|
|
|
val data = block.selectFirst("a")?.attr("href")?.split("/")
|
|
|
|
val id = data?.last()
|
|
|
|
val type = data?.get(2)?.toInt()
|
2022-10-02 06:38:07 +00:00
|
|
|
val image = Regex("coverVerticalUrl:\"(.*?)\",").findAll(script.toString())
|
2022-10-02 00:19:47 +00:00
|
|
|
.map { it.groupValues[1] }.toList().getOrNull(num)?.replace("\\u002F", "/")
|
|
|
|
|
2022-10-01 22:06:16 +00:00
|
|
|
|
2022-09-14 22:16:36 +00:00
|
|
|
newMovieSearchResponse(
|
2022-10-01 22:06:16 +00:00
|
|
|
"$name",
|
|
|
|
UrlData(id, type).toJson(),
|
2022-09-14 22:16:36 +00:00
|
|
|
TvType.Movie,
|
|
|
|
) {
|
2022-10-01 22:06:16 +00:00
|
|
|
this.posterUrl = image
|
2022-09-14 22:16:36 +00:00
|
|
|
}
|
2022-10-01 22:06:16 +00:00
|
|
|
|
2022-10-01 23:57:22 +00:00
|
|
|
}
|
2022-10-01 22:06:16 +00:00
|
|
|
|
2022-09-14 22:16:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
override suspend fun load(url: String): LoadResponse? {
|
|
|
|
val data = parseJson<UrlData>(url)
|
|
|
|
val res = app.get(
|
|
|
|
"$apiUrl/movieDrama/get?id=${data.id}&category=${data.category}",
|
|
|
|
headers = headers
|
|
|
|
).parsedSafe<Load>()?.data ?: throw ErrorLoadingException("Invalid Json Reponse")
|
|
|
|
|
|
|
|
val episodes = res.episodeVo?.map { eps ->
|
|
|
|
val definition = eps.definitionList?.map {
|
|
|
|
Definition(
|
|
|
|
it.code,
|
|
|
|
it.description,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
val subtitling = eps.subtitlingList?.map {
|
|
|
|
Subtitling(
|
|
|
|
it.languageAbbr,
|
|
|
|
it.language,
|
|
|
|
it.subtitlingUrl
|
|
|
|
)
|
|
|
|
}
|
|
|
|
Episode(
|
|
|
|
data = UrlEpisode(
|
|
|
|
data.id.toString(),
|
|
|
|
data.category,
|
|
|
|
eps.id,
|
|
|
|
definition,
|
|
|
|
subtitling
|
|
|
|
).toJson(),
|
|
|
|
episode = eps.seriesNo
|
|
|
|
)
|
|
|
|
} ?: throw ErrorLoadingException("No Episode Found")
|
|
|
|
val recommendations = res.likeList?.mapNotNull { rec ->
|
|
|
|
rec.toSearchResponse()
|
|
|
|
}
|
|
|
|
|
2022-10-07 17:32:11 +00:00
|
|
|
val type = when {
|
|
|
|
data.category == 0 -> {
|
|
|
|
TvType.Movie
|
|
|
|
}
|
|
|
|
data.category != 0 && res.tagNameList?.contains("Anime") == true -> {
|
|
|
|
TvType.Anime
|
|
|
|
}
|
|
|
|
else -> {
|
|
|
|
TvType.TvSeries
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-09 14:54:20 +00:00
|
|
|
val animeType = if(type == TvType.Anime && data.category == 0) "movie" else "tv"
|
|
|
|
|
|
|
|
val malId = if(type == TvType.Anime) {
|
2022-12-09 20:28:26 +00:00
|
|
|
app.get("${jikanAPI}/anime?q=${res.name}&start_date=${res.year}&type=$animeType&order_by=start_date&limit=1")
|
2022-12-09 14:54:20 +00:00
|
|
|
.parsedSafe<JikanResponse>()?.data?.firstOrNull()?.mal_id
|
|
|
|
} else {
|
|
|
|
null
|
|
|
|
}
|
|
|
|
val anilistId = if(malId != null) {
|
|
|
|
app.post(
|
|
|
|
"https://graphql.anilist.co/", data = mapOf(
|
|
|
|
"query" to "{Media(idMal:$malId,type:ANIME){id}}",
|
|
|
|
)
|
|
|
|
).parsedSafe<DataAni>()?.data?.media?.id
|
|
|
|
} else {
|
|
|
|
null
|
|
|
|
}
|
|
|
|
|
2022-09-14 22:16:36 +00:00
|
|
|
return newTvSeriesLoadResponse(
|
|
|
|
res.name ?: return null,
|
|
|
|
url,
|
2022-10-07 17:32:11 +00:00
|
|
|
type,
|
2022-09-14 22:16:36 +00:00
|
|
|
episodes
|
|
|
|
) {
|
|
|
|
this.posterUrl = res.coverVerticalUrl
|
2022-12-09 14:21:35 +00:00
|
|
|
this.backgroundPosterUrl = res.coverHorizontalUrl
|
2022-09-14 22:16:36 +00:00
|
|
|
this.year = res.year
|
|
|
|
this.plot = res.introduction
|
|
|
|
this.tags = res.tagNameList
|
|
|
|
this.rating = res.score.toRatingInt()
|
2022-12-09 14:54:20 +00:00
|
|
|
addMalId(malId?.toIntOrNull())
|
|
|
|
addAniListId(anilistId?.toIntOrNull())
|
2022-09-14 22:16:36 +00:00
|
|
|
this.recommendations = recommendations
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun getLanguage(str: String): String {
|
|
|
|
return when (str) {
|
|
|
|
"in_ID" -> "Indonesian"
|
2022-09-16 10:23:45 +00:00
|
|
|
"pt" -> "Portuguese"
|
2022-09-14 22:16:36 +00:00
|
|
|
else -> str.split("_").first().let {
|
|
|
|
SubtitleHelper.fromTwoLettersToLanguage(it).toString()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override suspend fun loadLinks(
|
|
|
|
data: String,
|
|
|
|
isCasting: Boolean,
|
|
|
|
subtitleCallback: (SubtitleFile) -> Unit,
|
|
|
|
callback: (ExtractorLink) -> Unit
|
|
|
|
): Boolean {
|
|
|
|
val res = parseJson<UrlEpisode>(data)
|
|
|
|
|
2022-12-09 21:21:13 +00:00
|
|
|
res.definitionList?.apmap { video ->
|
2022-12-09 20:28:26 +00:00
|
|
|
val body = """[{"category":${res.category},"contentId":"${res.id}","episodeId":${res.epId},"definition":"${video.code}"}]""".toRequestBody(RequestBodyTypes.JSON.toMediaTypeOrNull())
|
|
|
|
val response = app.post(
|
|
|
|
"$apiUrl/media/bathGetplayInfo",
|
|
|
|
requestBody = body,
|
|
|
|
headers = headers,
|
|
|
|
).text
|
|
|
|
val json = tryParseJson<PreviewResponse>(response)?.data?.firstOrNull()
|
2022-12-09 14:08:31 +00:00
|
|
|
callback.invoke(
|
|
|
|
ExtractorLink(
|
|
|
|
this.name,
|
|
|
|
this.name,
|
2022-12-09 21:21:13 +00:00
|
|
|
json?.mediaUrl ?: return@apmap null,
|
2022-12-09 14:08:31 +00:00
|
|
|
"",
|
2022-12-09 20:28:26 +00:00
|
|
|
getQuality(json.currentDefinition ?: ""),
|
2022-12-09 14:08:31 +00:00
|
|
|
isM3u8 = true,
|
|
|
|
)
|
|
|
|
)
|
2022-09-14 22:16:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
res.subtitlingList?.map { sub ->
|
|
|
|
subtitleCallback.invoke(
|
|
|
|
SubtitleFile(
|
|
|
|
getLanguage(sub.languageAbbr ?: return@map),
|
|
|
|
sub.subtitlingUrl ?: return@map
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2022-12-09 14:08:31 +00:00
|
|
|
private fun getQuality(quality: String): Int {
|
|
|
|
return when (quality) {
|
|
|
|
"GROOT_FD" -> Qualities.P360.value
|
|
|
|
"GROOT_LD" -> Qualities.P480.value
|
|
|
|
"GROOT_SD" -> Qualities.P720.value
|
|
|
|
"GROOT_HD" -> Qualities.P1080.value
|
|
|
|
else -> Qualities.Unknown.value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-14 22:16:36 +00:00
|
|
|
data class UrlData(
|
|
|
|
val id: Any? = null,
|
|
|
|
val category: Int? = null,
|
|
|
|
)
|
|
|
|
|
|
|
|
data class Subtitling(
|
|
|
|
val languageAbbr: String? = null,
|
|
|
|
val language: String? = null,
|
|
|
|
val subtitlingUrl: String? = null,
|
|
|
|
)
|
|
|
|
|
|
|
|
data class Definition(
|
|
|
|
val code: String? = null,
|
|
|
|
val description: String? = null,
|
|
|
|
)
|
|
|
|
|
|
|
|
data class UrlEpisode(
|
|
|
|
val id: String? = null,
|
|
|
|
val category: Int? = null,
|
|
|
|
val epId: Int? = null,
|
|
|
|
val definitionList: List<Definition>? = arrayListOf(),
|
|
|
|
val subtitlingList: List<Subtitling>? = arrayListOf(),
|
|
|
|
)
|
|
|
|
|
2022-12-09 20:28:26 +00:00
|
|
|
data class QuickSearchData(
|
|
|
|
@JsonProperty("searchResults") val searchResults: ArrayList<Media>? = arrayListOf(),
|
|
|
|
)
|
|
|
|
|
|
|
|
data class QuickSearchRes(
|
|
|
|
@JsonProperty("data") val data: QuickSearchData? = null,
|
2022-12-09 14:08:31 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
data class PreviewResponse(
|
|
|
|
@JsonProperty("data") val data: ArrayList<PreviewVideos>? = arrayListOf(),
|
2022-09-14 22:16:36 +00:00
|
|
|
)
|
|
|
|
|
2022-12-09 14:08:31 +00:00
|
|
|
data class PreviewVideos(
|
|
|
|
@JsonProperty("mediaUrl") val mediaUrl: String? = null,
|
|
|
|
@JsonProperty("currentDefinition") val currentDefinition: String? = null,
|
2022-09-14 22:16:36 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
data class SubtitlingList(
|
|
|
|
@JsonProperty("languageAbbr") val languageAbbr: String? = null,
|
|
|
|
@JsonProperty("language") val language: String? = null,
|
|
|
|
@JsonProperty("subtitlingUrl") val subtitlingUrl: String? = null,
|
|
|
|
)
|
|
|
|
|
|
|
|
data class DefinitionList(
|
|
|
|
@JsonProperty("code") val code: String? = null,
|
|
|
|
@JsonProperty("description") val description: String? = null,
|
|
|
|
)
|
|
|
|
|
|
|
|
data class EpisodeVo(
|
|
|
|
@JsonProperty("id") val id: Int? = null,
|
|
|
|
@JsonProperty("seriesNo") val seriesNo: Int? = null,
|
|
|
|
@JsonProperty("definitionList") val definitionList: ArrayList<DefinitionList>? = arrayListOf(),
|
|
|
|
@JsonProperty("subtitlingList") val subtitlingList: ArrayList<SubtitlingList>? = arrayListOf(),
|
|
|
|
)
|
|
|
|
|
|
|
|
data class MediaDetail(
|
|
|
|
@JsonProperty("name") val name: String? = null,
|
|
|
|
@JsonProperty("introduction") val introduction: String? = null,
|
|
|
|
@JsonProperty("year") val year: Int? = null,
|
|
|
|
@JsonProperty("category") val category: String? = null,
|
|
|
|
@JsonProperty("coverVerticalUrl") val coverVerticalUrl: String? = null,
|
2022-12-09 14:21:35 +00:00
|
|
|
@JsonProperty("coverHorizontalUrl") val coverHorizontalUrl: String? = null,
|
2022-09-14 22:16:36 +00:00
|
|
|
@JsonProperty("score") val score: String? = null,
|
|
|
|
@JsonProperty("episodeVo") val episodeVo: ArrayList<EpisodeVo>? = arrayListOf(),
|
|
|
|
@JsonProperty("likeList") val likeList: ArrayList<Media>? = arrayListOf(),
|
|
|
|
@JsonProperty("tagNameList") val tagNameList: ArrayList<String>? = arrayListOf(),
|
|
|
|
)
|
|
|
|
|
|
|
|
data class Load(
|
|
|
|
@JsonProperty("data") val data: MediaDetail? = null,
|
|
|
|
)
|
|
|
|
|
|
|
|
data class Media(
|
|
|
|
@JsonProperty("id") val id: Any? = null,
|
|
|
|
@JsonProperty("category") val category: Int? = null,
|
|
|
|
@JsonProperty("imageUrl") val imageUrl: String? = null,
|
|
|
|
@JsonProperty("coverVerticalUrl") val coverVerticalUrl: String? = null,
|
|
|
|
@JsonProperty("title") val title: String? = null,
|
|
|
|
@JsonProperty("name") val name: String? = null,
|
|
|
|
@JsonProperty("jumpAddress") val jumpAddress: String? = null,
|
|
|
|
)
|
|
|
|
|
|
|
|
data class RecommendItems(
|
|
|
|
@JsonProperty("homeSectionName") val homeSectionName: String? = null,
|
|
|
|
@JsonProperty("homeSectionType") val homeSectionType: String? = null,
|
|
|
|
@JsonProperty("recommendContentVOList") val media: ArrayList<Media>? = arrayListOf(),
|
|
|
|
)
|
|
|
|
|
|
|
|
data class Data(
|
|
|
|
@JsonProperty("recommendItems") val recommendItems: ArrayList<RecommendItems>? = arrayListOf(),
|
|
|
|
)
|
|
|
|
|
|
|
|
data class Home(
|
|
|
|
@JsonProperty("data") val data: Data? = null,
|
|
|
|
)
|
|
|
|
|
2022-12-09 14:54:20 +00:00
|
|
|
data class DataMal(
|
|
|
|
@JsonProperty("mal_id") val mal_id: String? = null,
|
|
|
|
)
|
|
|
|
|
|
|
|
data class JikanResponse(
|
|
|
|
@JsonProperty("data") val data: ArrayList<DataMal>? = arrayListOf(),
|
|
|
|
)
|
|
|
|
|
|
|
|
private data class IdAni(
|
|
|
|
@JsonProperty("id") val id: String? = null,
|
|
|
|
)
|
|
|
|
|
|
|
|
private data class MediaAni(
|
|
|
|
@JsonProperty("Media") val media: IdAni? = null,
|
|
|
|
)
|
|
|
|
|
|
|
|
private data class DataAni(
|
|
|
|
@JsonProperty("data") val data: MediaAni? = null,
|
|
|
|
)
|
|
|
|
|
2022-09-14 22:16:36 +00:00
|
|
|
}
|
|
|
|
|