diff --git a/src/en/moviesmod/build.gradle b/src/en/moviesmod/build.gradle new file mode 100644 index 00000000..9d9bcf60 --- /dev/null +++ b/src/en/moviesmod/build.gradle @@ -0,0 +1,7 @@ +ext { + extName = 'MoviesMod' + extClass = '.MoviesMod' + extVersionCode = 1 +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/moviesmod/res/mipmap-hdpi/ic_launcher.png b/src/en/moviesmod/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..0810c2fd Binary files /dev/null and b/src/en/moviesmod/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/moviesmod/res/mipmap-mdpi/ic_launcher.png b/src/en/moviesmod/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..57fec2ad Binary files /dev/null and b/src/en/moviesmod/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/moviesmod/res/mipmap-xhdpi/ic_launcher.png b/src/en/moviesmod/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..217ed310 Binary files /dev/null and b/src/en/moviesmod/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/moviesmod/res/mipmap-xxhdpi/ic_launcher.png b/src/en/moviesmod/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..caa4ea4a Binary files /dev/null and b/src/en/moviesmod/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/moviesmod/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/moviesmod/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..0586fe03 Binary files /dev/null and b/src/en/moviesmod/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/moviesmod/src/eu/kanade/tachiyomi/animeextension/en/moviesmod/MoviesMod.kt b/src/en/moviesmod/src/eu/kanade/tachiyomi/animeextension/en/moviesmod/MoviesMod.kt new file mode 100644 index 00000000..4f4c7600 --- /dev/null +++ b/src/en/moviesmod/src/eu/kanade/tachiyomi/animeextension/en/moviesmod/MoviesMod.kt @@ -0,0 +1,448 @@ +package eu.kanade.tachiyomi.animeextension.en.moviesmod + +import android.app.Application +import android.util.Base64 +import androidx.preference.EditTextPreference +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource +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.network.POST +import eu.kanade.tachiyomi.network.await +import eu.kanade.tachiyomi.util.asJsoup +import eu.kanade.tachiyomi.util.parallelCatchingFlatMap +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MultipartBody +import okhttp3.Request +import okhttp3.Response +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy +import java.net.URL + +class MoviesMod : ConfigurableAnimeSource, ParsedAnimeHttpSource() { + + override val name = "Movies Mod" + + override val baseUrl by lazy { + preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!! + } + + private val currentBaseUrl by lazy { + runCatching { + runBlocking { + withContext(Dispatchers.Default) { + client.newBuilder() + .followRedirects(false) + .build() + .newCall(GET("$baseUrl/")).await().use { resp -> + when (resp.code) { + 301 -> { + (resp.headers["location"]?.substringBeforeLast("/") ?: baseUrl).also { + preferences.edit().putString(PREF_DOMAIN_KEY, it).apply() + } + } + else -> baseUrl + } + } + } + } + }.getOrDefault(baseUrl) + } + + override val lang = "en" + + override val supportsLatest = false + + private val json: Json by injectLazy() + + private val preferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + // ============================== Popular =============================== + override fun popularAnimeRequest(page: Int): Request = GET("$currentBaseUrl/page/$page/") + + override fun popularAnimeSelector(): String = "div#content_box div.post-cards > article" + + override fun popularAnimeFromElement(element: Element) = SAnime.create().apply { + setUrlWithoutDomain(element.select("a").attr("abs:href")) + thumbnail_url = element.select("div.featured-thumbnail > img").attr("abs:src") + title = element.select("a").attr("title") + .replace("Download", "").trim() + } + + override fun popularAnimeNextPageSelector(): String = + "#content_box > nav > div > a.next.page-numbers" + + // =============================== Latest =============================== + override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException() + + override fun latestUpdatesSelector(): String = throw UnsupportedOperationException() + + override fun latestUpdatesFromElement(element: Element): SAnime = throw UnsupportedOperationException() + + override fun latestUpdatesNextPageSelector(): String = throw UnsupportedOperationException() + + // =============================== Search =============================== + override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { + val cleanQuery = query.replace(" ", "+").lowercase() + return GET("$currentBaseUrl/search/$cleanQuery/page/$page") + } + + override fun searchAnimeSelector(): String = popularAnimeSelector() + + override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element) + + override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector() + + // =========================== Anime Details ============================ + override fun animeDetailsParse(document: Document) = SAnime.create().apply { + initialized = true + title = document.selectFirst(".entry-title")?.text() + ?.replace("Download", "", true)?.trim() ?: "Movie" + status = SAnime.UNKNOWN + author = document.selectFirst("div.entry-content > div.thecontent > div.imdbwp > div.imdbwp__content > div.imdbwp__footer > span")?.text() + description = document.selectFirst("div.entry-content > div.thecontent > div.imdbwp > div.imdbwp__content > div.imdbwp__teaser")?.text() + } + + // ============================== Episodes ============================== + override fun episodeListRequest(anime: SAnime) = GET(currentBaseUrl + anime.url, headers) + + override fun episodeListParse(response: Response): List { + val doc = response.asJsoup() + val episodeElements = doc.select("p:has(a.maxbutton-episode-links,a.maxbutton-download-links)") + .asSequence() + + val qualityRegex = "\\d{3,4}p(?:\\s+\\w+)?".toRegex(RegexOption.IGNORE_CASE) + val seasonRegex = "[ .]?S(?:eason)?[ .]?(\\d{1,2})[ .]?".toRegex(RegexOption.IGNORE_CASE) + val movieTitleRegex = "^[^(]+\n?".toRegex(RegexOption.IGNORE_CASE) + + val isSerie = episodeElements.first().selectFirst("a")!!.text() == "Episode Links" + + val episodeList = episodeElements.map { row -> + val prevP = row.previousElementSibling()!!.text() + val quality = (qualityRegex.find(prevP)?.value ?: "HD") + val defaultName = if (isSerie) { + seasonRegex.find(prevP)?.value ?: "Season 1" + } else { + movieTitleRegex.find(prevP.replace("Download", "").trim())?.value ?: "Movie" + } + + val episodePageUrl = row.selectFirst("a[href]")?.attr("href")!! + val episodePageDocument = Jsoup.connect(extractChildUrl(episodePageUrl)).get() + + episodePageDocument.select("div.timed-content-client_show_0_5_0 a").asSequence() + .mapIndexedNotNull { index, linkElement -> + val episode = if (isSerie) { + linkElement.text() + .replace("Episode", "", true) + .trim() + .toIntOrNull() ?: (index + 1) + } else { + 0 + } + + val url = linkElement.attr("href").takeUnless(String::isBlank) + ?: return@mapIndexedNotNull null + + Triple( + Pair(defaultName, episode), + url, + if (isSerie) quality else quality + " " + linkElement.text(), + ) + } + }.flatten().groupBy { it.first }.values.mapIndexed { index, items -> + val (itemName, episodeNum) = items.first().first + + SEpisode.create().apply { + url = EpLinks( + urls = items.map { triple -> + EpUrl(url = triple.second, quality = triple.third) + }, + ).toJson() + + name = if (isSerie) "$itemName Ep $episodeNum" else itemName + + episode_number = if (isSerie) episodeNum.toFloat() else (index + 1).toFloat() + } + } + + if (episodeList.isEmpty()) throw Exception("Only Zip Pack Available") + return episodeList.reversed() + } + + private fun extractChildUrl(mainUrl: String): String { + // Parse the URL + val parsedUrl = URL(mainUrl) + + // Get query parameters + val queryParams = parsedUrl.query.split("&").associate { + val (key, value) = it.split("=") + key to value + } + + // Decode the Base64 string + val decodedUrl = String(Base64.decode(queryParams["url"], 1)) + + return decodedUrl + } + + override fun episodeListSelector(): String = "p:has(a.maxbutton-episode-links)" + + override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException() + + // ============================ Video Links ============================= + override suspend fun getVideoList(episode: SEpisode): List