diff --git a/Loklok/build.gradle.kts b/Loklok/build.gradle.kts new file mode 100644 index 00000000..da0e834b --- /dev/null +++ b/Loklok/build.gradle.kts @@ -0,0 +1,29 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "id" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + authors = listOf("Hexated") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 1 // will be 3 if unspecified + tvTypes = listOf( + "AsianDrama", + "Anime", + "TvSeries", + "Movie", + ) + + + iconUrl = "https://www.google.com/s2/favicons?domain=loklok.com&sz=%size%" +} \ No newline at end of file diff --git a/Loklok/src/main/AndroidManifest.xml b/Loklok/src/main/AndroidManifest.xml new file mode 100644 index 00000000..c98063f8 --- /dev/null +++ b/Loklok/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Loklok/src/main/kotlin/com/hexated/Loklok.kt b/Loklok/src/main/kotlin/com/hexated/Loklok.kt new file mode 100644 index 00000000..2f5099ed --- /dev/null +++ b/Loklok/src/main/kotlin/com/hexated/Loklok.kt @@ -0,0 +1,304 @@ +package com.hexated + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.mvvm.safeApiCall +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.SubtitleHelper +import com.lagradost.cloudstream3.utils.getQualityFromName + +class Loklok : MainAPI() { + override var name = "Loklok" + override val hasMainPage = true + override val hasChromecastSupport = true + override val instantLinkLoading = true + 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 + private val api = base64Decode("aHR0cHM6Ly9nYS1tb2JpbGUtYXBpLmxva2xvay50dg==") + private val apiUrl = "$api/${base64Decode("Y21zL2FwcA==")}" + + private val mainImageUrl = "https://images.weserv.nl" + + private fun encode(input: String): String? = java.net.URLEncoder.encode(input, "utf-8") + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + val home = ArrayList() + for (i in 0..10) { + app.get("$apiUrl/homePage/getHome?page=$i", headers = headers) + .parsedSafe()?.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" + } + } + } + + override suspend fun search(query: String): List { +// val res = app.post( +// "$apiUrl/search/v1/searchWithKeyWord", data = mapOf( +// "searchKeyWord" to query, +// "size" to "50", +// "sort" to "", +// "searchType" to "" +// ), headers = headers +// ) + val searchApi = + base64Decode("aHR0cHM6Ly9maWxtaG90LmxpdmUvX25leHQvZGF0YS9NeXQzRm4tVHRXaHJ2a1RBaG45SGw=") + val res = app.get( + "$searchApi/search.json?q=$query", + headers = mapOf("x-nextjs-data" to "1") + ) + return res.parsedSafe()?.pageProps?.result?.mapNotNull { media -> + newMovieSearchResponse( + media.name ?: return@mapNotNull null, + UrlData(media.id?.toIntOrNull(), media.domainType).toJson(), + TvType.Movie, + ) { + this.posterUrl = media.coverVerticalUrl + } + } ?: throw ErrorLoadingException("Invalid Json Reponse") + } + + override suspend fun load(url: String): LoadResponse? { + val data = parseJson(url) + val res = app.get( + "$apiUrl/movieDrama/get?id=${data.id}&category=${data.category}", + headers = headers + ).parsedSafe()?.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() + } + + return newTvSeriesLoadResponse( + res.name ?: return null, + url, + if (data.category == 0) TvType.Movie else TvType.TvSeries, + episodes + ) { + this.posterUrl = res.coverVerticalUrl + this.year = res.year + this.plot = res.introduction + this.tags = res.tagNameList + this.rating = res.score.toRatingInt() + this.recommendations = recommendations + } + + } + + private fun getLanguage(str: String): String { + return when (str) { + "in_ID" -> "Indonesian" + 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(data) + + res.definitionList?.apmap { video -> + safeApiCall { + app.get( + "$apiUrl/media/previewInfo?category=${res.category}&contentId=${res.id}&episodeId=${res.epId}&definition=${video.code}", + headers = headers + ).parsedSafe