diff --git a/src/en/hahomoe/build.gradle b/src/en/hahomoe/build.gradle index fbb7e54419..8c0bad2775 100644 --- a/src/en/hahomoe/build.gradle +++ b/src/en/hahomoe/build.gradle @@ -1,11 +1,13 @@ -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) +} ext { extName = 'haho.moe' pkgNameSuffix = 'en.hahomoe' extClass = '.HahoMoe' - extVersionCode = 8 + extVersionCode = 9 libVersion = '13' containsNsfw = true } diff --git a/src/en/hahomoe/src/eu/kanade/tachiyomi/animeextension/en/hahomoe/HahoMoe.kt b/src/en/hahomoe/src/eu/kanade/tachiyomi/animeextension/en/hahomoe/HahoMoe.kt index 2f73f0f19d..fa816c8423 100644 --- a/src/en/hahomoe/src/eu/kanade/tachiyomi/animeextension/en/hahomoe/HahoMoe.kt +++ b/src/en/hahomoe/src/eu/kanade/tachiyomi/animeextension/en/hahomoe/HahoMoe.kt @@ -1,7 +1,9 @@ package eu.kanade.tachiyomi.animeextension.en.hahomoe -import android.annotation.SuppressLint -import eu.kanade.tachiyomi.animesource.model.AnimeFilter +import android.app.Application +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 @@ -9,20 +11,20 @@ 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 okhttp3.Headers.Companion.toHeaders -import okhttp3.OkHttpClient +import okhttp3.Cookie +import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Request import okhttp3.Response import org.jsoup.nodes.Document import org.jsoup.nodes.Element +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import java.lang.Exception -import java.lang.Float.parseFloat +import java.net.URLEncoder import java.text.SimpleDateFormat -import java.util.Date import java.util.Locale -import kotlin.collections.ArrayList -class HahoMoe : ParsedAnimeHttpSource() { +class HahoMoe : ConfigurableAnimeSource, ParsedAnimeHttpSource() { override val name = "haho.moe" @@ -32,83 +34,125 @@ class HahoMoe : ParsedAnimeHttpSource() { override val supportsLatest = true - override val client: OkHttpClient = network.cloudflareClient + override val client = network.cloudflareClient - override fun popularAnimeSelector(): String = "ul.anime-loop.loop li a" + private val preferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + init { + // Save the cookie that enables thumbnails in results (popular, latest, search...) + val httpUrl = baseUrl.toHttpUrl() + val cookie = Cookie.parse(httpUrl, "loop-view=thumb")!! + client.cookieJar.saveFromResponse(httpUrl, listOf(cookie)) + } + + // ============================== Popular =============================== + override fun popularAnimeRequest(page: Int) = GET("$baseUrl/anime?s=vdy-d&page=$page") - override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/anime?s=vdy-d&page=$page") + override fun popularAnimeSelector() = "ul.anime-loop.loop > li > a" - override fun popularAnimeFromElement(element: Element): SAnime { - val anime = SAnime.create() - anime.setUrlWithoutDomain(element.attr("href") + "?s=srt-d") - anime.title = element.select("div span").not(".badge").text() - return anime + override fun popularAnimeFromElement(element: Element) = SAnime.create().apply { + setUrlWithoutDomain(element.attr("href") + "?s=srt-d") + title = element.selectFirst("div.label > span, div span.thumb-title")!!.text() + thumbnail_url = element.selectFirst("img")?.absUrl("src") } - override fun popularAnimeNextPageSelector(): String = "ul.pagination li.page-item a[rel=next]" + override fun popularAnimeNextPageSelector() = "ul.pagination li.page-item a[rel=next]" - override fun episodeListSelector() = "ul.episode-loop li a" + // =============================== Latest =============================== + override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/anime?s=rel-d&page=$page") - private fun episodeNextPageSelector() = popularAnimeNextPageSelector() + override fun latestUpdatesSelector() = popularAnimeSelector() - override fun episodeListParse(response: Response): List { - val episodes = mutableListOf() - fun addEpisodes(document: Document) { - document.select(episodeListSelector()).map { episodes.add(episodeFromElement(it)) } - document.select(episodeNextPageSelector()).firstOrNull() - ?.let { addEpisodes(client.newCall(GET(it.attr("href"), headers)).execute().asJsoup()) } - } + override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element) + + override fun latestUpdatesNextPageSelector() = popularAnimeNextPageSelector() + + // =============================== Search =============================== + override fun getFilterList() = HahoMoeFilters.FILTER_LIST + + override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { + val (includedTags, excludedTags, orderBy, ordering) = HahoMoeFilters.getSearchParameters(filters) + + val httpQuery = buildString { + if (query.isNotBlank()) append(query.trim()) + if (includedTags.isNotEmpty()) { + append(includedTags.joinToString(" genre:", prefix = " genre:")) + } + if (excludedTags.isNotEmpty()) { + append(excludedTags.joinToString(" -genre:", prefix = " -genre:")) + } + }.let { URLEncoder.encode(it, "UTF-8") } + + return GET("$baseUrl/anime?page=$page&s=$orderBy$ordering&q=$httpQuery") + } + + override fun searchAnimeSelector() = popularAnimeSelector() + + override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element) + + override fun searchAnimeNextPageSelector() = popularAnimeNextPageSelector() - addEpisodes(response.asJsoup()) - return episodes + // =========================== Anime Details ============================ + override fun animeDetailsParse(document: Document) = SAnime.create().apply { + setUrlWithoutDomain(document.location()) + thumbnail_url = document.selectFirst("img.cover-image.img-thumbnail")?.absUrl("src") + title = document.selectFirst("li.breadcrumb-item.active")!!.text() + genre = document.select("li.genre span.value").joinToString { it.text() } + description = document.selectFirst("div.card-body")?.text() + author = document.select("li.production span.value").joinToString { it.text() } + artist = document.selectFirst("li.group span.value")?.text() + status = parseStatus(document.selectFirst("li.status span.value")?.text()) } - override fun episodeFromElement(element: Element): SEpisode { - val episode = SEpisode.create() - episode.setUrlWithoutDomain(element.attr("href")) - val episodeNumberString = element.select("div.episode-number").text().removePrefix("Episode ") - var numeric = true - try { - parseFloat(episodeNumberString) - } catch (e: NumberFormatException) { - numeric = false + private fun parseStatus(statusString: String?): Int { + return when (statusString) { + "Ongoing" -> SAnime.ONGOING + "Completed" -> SAnime.COMPLETED + else -> SAnime.UNKNOWN } - episode.episode_number = if (numeric) episodeNumberString.toFloat() else element.parent()!!.className().removePrefix("episode").toFloat() - episode.name = element.select("div.episode-number").text() + ": " + element.select("div.episode-label").text() + element.select("div.episode-title").text() - val date: String = element.select("div.date").text() - val parsedDate = parseDate(date) - if (parsedDate.time != -1L) episode.date_upload = parsedDate.time - return episode } - @SuppressLint("SimpleDateFormat") - private fun parseDate(date: String): Date { - val knownPatterns: MutableList = ArrayList() - knownPatterns.add(SimpleDateFormat("dd'th of 'MMM, yyyy")) - knownPatterns.add(SimpleDateFormat("dd'nd of 'MMM, yyyy")) - knownPatterns.add(SimpleDateFormat("dd'st of 'MMM, yyyy")) - knownPatterns.add(SimpleDateFormat("dd'rd of 'MMM, yyyy")) - - for (pattern in knownPatterns) { - try { - // Take a try - return Date(pattern.parse(date)!!.time) - } catch (e: Throwable) { - // Loop on - } + // ============================== Episodes ============================== + override fun episodeListSelector() = "ul.episode-loop li a" + + private fun episodeNextPageSelector() = popularAnimeNextPageSelector() + + override fun episodeListParse(response: Response): List { + var doc = response.use { it.asJsoup() } + return buildList { + do { + if (isNotEmpty()) { + val url = doc.selectFirst(episodeNextPageSelector())!!.absUrl("href") + doc = client.newCall(GET(url)).execute().use { it.asJsoup() } + } + + doc.select(episodeListSelector()) + .map(::episodeFromElement) + .also(::addAll) + } while (doc.selectFirst(episodeNextPageSelector()) != null) } - return Date(-1L) } + override fun episodeFromElement(element: Element) = SEpisode.create().apply { + setUrlWithoutDomain(element.attr("href")) + + val episodeNumberString = element.selectFirst("div.episode-number")!!.text() + episode_number = episodeNumberString.removePrefix("Episode ").toFloatOrNull() ?: 1F + name = "$episodeNumberString: " + element.selectFirst("div.episode-label")?.text().orEmpty() + date_upload = element.selectFirst("div.date")?.text().orEmpty().toDate() + } + + // ============================ Video Links ============================= override fun videoListParse(response: Response): List