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