From 2392762c40e0825ac9bcb625dde5cc959792fcca Mon Sep 17 00:00:00 2001 From: Willy Nur Wahyudi Date: Sat, 20 Jan 2024 19:39:00 +0700 Subject: [PATCH 1/4] fix(id/oploverz): Update baseUrl (#2796) --- src/id/oploverz/build.gradle | 2 +- .../eu/kanade/tachiyomi/animeextension/id/oploverz/Oploverz.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/id/oploverz/build.gradle b/src/id/oploverz/build.gradle index 4ec7bf9ea9..896aeec732 100644 --- a/src/id/oploverz/build.gradle +++ b/src/id/oploverz/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'Oploverz' extClass = '.Oploverz' - extVersionCode = 21 + extVersionCode = 22 } apply from: "$rootDir/common.gradle" diff --git a/src/id/oploverz/src/eu/kanade/tachiyomi/animeextension/id/oploverz/Oploverz.kt b/src/id/oploverz/src/eu/kanade/tachiyomi/animeextension/id/oploverz/Oploverz.kt index 4119be79d6..7e11055889 100644 --- a/src/id/oploverz/src/eu/kanade/tachiyomi/animeextension/id/oploverz/Oploverz.kt +++ b/src/id/oploverz/src/eu/kanade/tachiyomi/animeextension/id/oploverz/Oploverz.kt @@ -28,7 +28,7 @@ import java.util.Locale class Oploverz : ConfigurableAnimeSource, AnimeHttpSource() { override val name: String = "Oploverz" - override val baseUrl: String = "https://oploverz.cool" + override val baseUrl: String = "https://oploverz.wiki" override val lang: String = "id" override val supportsLatest: Boolean = true From 255bf2d6a607b8d616e35d88e9518170985d565a Mon Sep 17 00:00:00 2001 From: Samfun75 <38332931+Samfun75@users.noreply.github.com> Date: Sat, 20 Jan 2024 20:28:36 +0300 Subject: [PATCH 2/4] fix(en/putlocker): Update episode list extraction and refactor (#2798) --- src/en/putlocker/build.gradle | 6 +- .../animeextension/en/putlocker/PutLocker.kt | 360 +++++++----------- .../en/putlocker/PutLockerDto.kt | 34 ++ .../extractors/PutServerExtractor.kt | 114 ++++++ 4 files changed, 281 insertions(+), 233 deletions(-) create mode 100644 src/en/putlocker/src/eu/kanade/tachiyomi/animeextension/en/putlocker/PutLockerDto.kt create mode 100644 src/en/putlocker/src/eu/kanade/tachiyomi/animeextension/en/putlocker/extractors/PutServerExtractor.kt diff --git a/src/en/putlocker/build.gradle b/src/en/putlocker/build.gradle index 497016c2ff..6bbbf8828f 100644 --- a/src/en/putlocker/build.gradle +++ b/src/en/putlocker/build.gradle @@ -1,7 +1,11 @@ ext { extName = 'PutLocker' extClass = '.PutLocker' - extVersionCode = 3 + extVersionCode = 4 } apply from: "$rootDir/common.gradle" + +dependencies { + implementation project(':lib-playlist-utils') +} \ No newline at end of file diff --git a/src/en/putlocker/src/eu/kanade/tachiyomi/animeextension/en/putlocker/PutLocker.kt b/src/en/putlocker/src/eu/kanade/tachiyomi/animeextension/en/putlocker/PutLocker.kt index 761c71ddee..32411c739a 100644 --- a/src/en/putlocker/src/eu/kanade/tachiyomi/animeextension/en/putlocker/PutLocker.kt +++ b/src/en/putlocker/src/eu/kanade/tachiyomi/animeextension/en/putlocker/PutLocker.kt @@ -4,25 +4,22 @@ import android.app.Application import android.content.SharedPreferences import androidx.preference.ListPreference import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.animeextension.en.putlocker.extractors.PutServerExtractor 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.Track import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.parallelCatchingFlatMap +import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Serializable -import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonPrimitive -import okhttp3.Headers -import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Request import okhttp3.Response import org.jsoup.Jsoup @@ -37,7 +34,7 @@ class PutLocker : ConfigurableAnimeSource, ParsedAnimeHttpSource() { override val name = "PutLocker" - override val baseUrl by lazy { preferences.getString("preferred_domain", "https://putlocker.vip")!! } + override val baseUrl = "https://ww7.putlocker.vip" override val lang = "en" @@ -49,38 +46,50 @@ class PutLocker : ConfigurableAnimeSource, ParsedAnimeHttpSource() { Injekt.get().getSharedPreferences("source_$id", 0x0000) } + private val putServerExtractor by lazy { PutServerExtractor(client) } + // ============================== Popular =============================== override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/putlocker/") - override fun popularAnimeSelector(): String = "div#movie-featured > div.ml-item" + override fun popularAnimeSelector(): String = + "div#tab-movie > div.ml-item, div#tab-tv-show > div.ml-item" - override fun popularAnimeNextPageSelector(): String = "Nothing" + override fun popularAnimeNextPageSelector(): String? = null override fun popularAnimeFromElement(element: Element): SAnime { return SAnime.create().apply { - setUrlWithoutDomain(element.select("a").attr("abs:href")) - thumbnail_url = element.select("a > img").attr("abs:data-original") - title = element.select("a > div.mli-info > h2").text() + setUrlWithoutDomain( + element.select("div.mli-poster > a") + .attr("abs:href"), + ) + title = element.select("div.mli-info h3").text() + thumbnail_url = element.select("div.mli-poster > a > img") + .attr("abs:data-original") } } // =============================== Latest =============================== - override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/filter/$page?genre=all&country=all&types=all&year=all&sort=updated") + override fun latestUpdatesRequest(page: Int): Request = + GET("$baseUrl/filter/$page?genre=all&country=all&types=all&year=all&sort=updated") - override fun latestUpdatesSelector(): String = "div.movies-list > div.ml-item" + override fun latestUpdatesSelector(): String = "div.movies-list > div.ml-item > div.mli-poster" - override fun latestUpdatesNextPageSelector(): String = "div#pagination li a[title=Last]" + override fun latestUpdatesNextPageSelector(): String = "div#pagination li.active ~ li" override fun latestUpdatesFromElement(element: Element): SAnime = popularAnimeFromElement(element) // =============================== Search =============================== override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { - val re = Regex("[^A-Za-z0-9 ]") - val cleanQuery = re.replace(query, "").replace(" ", "+").lowercase() - return GET("$baseUrl/movie/search/$cleanQuery/$page/") + return "[^A-Za-z0-9 ]".toRegex() + .replace(query, "") + .replace(" ", "+") + .lowercase() + .let { + GET("$baseUrl/movie/search/$it/$page/") + } } override fun searchAnimeSelector(): String = latestUpdatesSelector() @@ -91,68 +100,96 @@ class PutLocker : ConfigurableAnimeSource, ParsedAnimeHttpSource() { // =========================== Anime Details ============================ - override fun animeDetailsParse(document: Document): SAnime { - val anime = SAnime.create() - val descElement = document.select("div.mvic-desc") - anime.title = descElement.select("h3").text() - anime.genre = descElement.select("div.mvic-info > div.mvici-left p:contains(Genre) a").joinToString { it.text() } - anime.author = document.select("div.mvic-info > div.mvici-left p:contains(Director) a").joinToString { it.text() } - anime.status = document.select("div.mvic-info > div.mvici-right p:contains(Episode)")?.let { - if (it.text().isNullOrBlank()) SAnime.COMPLETED else SAnime.UNKNOWN - } ?: SAnime.COMPLETED - - var description = descElement.select("div.desc").text()?.let { it + "\n" } - val extraDescription = document.select("div.mvic-info > div.mvici-right") - extraDescription.select("p:contains(Quality)").text().let { - description += if (it.isNotBlank()) "\n$it" else "" - } - extraDescription.select("p:contains(Release)").text().let { - description += if (it.isNotBlank()) "\n$it" else "" - } - extraDescription.select("p:contains(IMDb)").text().let { - description += if (it.isNotBlank()) "\n$it" else "" + override fun animeDetailsParse(document: Document): SAnime = SAnime.create().apply { + document.select("div.mvic-desc").let { descElement -> + val mLeft = descElement + .select("div.mvic-info > div.mvici-left") + val mRight = descElement + .select("div.mvic-info > div.mvici-right") + + status = SAnime.COMPLETED + genre = mLeft.select("p:contains(Genre) a").joinToString { it.text() } + author = mLeft.select("p:contains(Production) a").first()?.text() + description = buildString { + appendLine(descElement.select("div.desc").text()) + appendLine() + appendLine(mLeft.select("p:contains(Production) a").joinToString { it.text() }) + appendLine(mLeft.select("p:contains(Country)").text()) + mRight.select("p").mapNotNull { appendLine(it.text()) } + } } - anime.description = description - - return anime } // ============================== Episodes ============================== - override fun episodeListRequest(anime: SAnime): Request { - val id = anime.url.split("-").last().replace("/", "") - return GET("$baseUrl/ajax/movie_episodes/$id") - } + override fun episodeListRequest(anime: SAnime): Request = + GET("$baseUrl${anime.url}/watching.html") override fun episodeListParse(response: Response): List { - val html = json.decodeFromString(response.body.string())["html"]!!.jsonPrimitive.content - val parsedHtml = Jsoup.parse(JSONUtil.unescape(html)) - val rawEpisodes = parsedHtml.select("div[id^=sv]").mapNotNull { server -> - val linkElement = server.select("div.les-content > a") - linkElement.map { epLinkElement -> - val dataId = epLinkElement.attr("data-id")!! - val ep = dataId.substringAfter("_").substringBefore("_").toInt() - val title = if (ep == 0) { - "Movie" - } else { - "Episode $ep: " + epLinkElement.attr("title").substringAfter("Episode $ep") - .replace("-", "").trim() - } - Pair(title, dataId) + val doc = response.use { it.asJsoup() } + val (type, mediaId) = doc.selectFirst("script:containsData(total_episode)") + ?.data() + ?.let { + val t = it + .substringBefore("name:") + .substringAfter("type:") + .replace(",", "") + .trim() + val mId = it + .substringAfter("id:") + .substringBefore(",") + .replace("\"", "") + .trim() + Pair(t, mId) + } ?: return emptyList() + + return when (type) { + "1" -> { + listOf( + SEpisode.create().apply { + url = EpLinks( + dataId = "1_full", + mediaId = mediaId, + ).toJson() + name = "Movie" + episode_number = 1F + }, + ) } - }.flatten() - - return rawEpisodes.groupBy { it.second.substringAfter("_").substringBefore("_").toInt() } - .mapNotNull { group -> - SEpisode.create().apply { - url = EpLinks( - ep_num = group.key, - ids = group.value.map { it.second }, - ).toJson() - name = group.value.first().first - episode_number = group.key.toFloat() - } + else -> { + client.newCall( + GET("$baseUrl/ajax/movie/seasons/$mediaId"), + ).execute() + .use { it.body.string() } + .parseHtml() + .select("div.dropdown-menu > a") + .mapNotNull { it.attr("data-id") } + .sortedDescending() + .parallelCatchingFlatMapBlocking { season -> + client.newCall( + GET("$baseUrl/ajax/movie/season/episodes/${mediaId}_$season"), + ).execute() + .use { it.body.string() } + .parseHtml() + .select("a") + .mapNotNull { elem -> + val dataId = elem.attr("data-id") + val epFloat = dataId + .substringAfter("_") + .toFloatOrNull() + ?: 0F + SEpisode.create().apply { + url = EpLinks( + dataId = dataId, + mediaId = mediaId, + ).toJson() + name = "Season $season ${elem.text()}" + episode_number = epFloat + } + }.sortedByDescending { it.episode_number } + } } + } } override fun episodeListSelector(): String = throw UnsupportedOperationException() @@ -162,9 +199,22 @@ class PutLocker : ConfigurableAnimeSource, ParsedAnimeHttpSource() { // ============================ Video Links ============================= override suspend fun getVideoList(episode: SEpisode): List