diff --git a/src/pt/donghuanosekai/AndroidManifest.xml b/src/pt/donghuanosekai/AndroidManifest.xml new file mode 100644 index 000000000..80a1ea8da --- /dev/null +++ b/src/pt/donghuanosekai/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/src/pt/donghuanosekai/build.gradle b/src/pt/donghuanosekai/build.gradle new file mode 100644 index 000000000..29397e7eb --- /dev/null +++ b/src/pt/donghuanosekai/build.gradle @@ -0,0 +1,18 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) +} + +ext { + extName = 'Donghua no Sekai' + pkgNameSuffix = 'pt.donghuanosekai' + extClass = '.DonghuaNoSekai' + extVersionCode = 1 +} + +dependencies { + implementation(project(":lib-streamsb-extractor")) +} + +apply from: "$rootDir/common.gradle" diff --git a/src/pt/donghuanosekai/res/mipmap-hdpi/ic_launcher.png b/src/pt/donghuanosekai/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..ed1d5498b Binary files /dev/null and b/src/pt/donghuanosekai/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/pt/donghuanosekai/res/mipmap-mdpi/ic_launcher.png b/src/pt/donghuanosekai/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..977a8b222 Binary files /dev/null and b/src/pt/donghuanosekai/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/pt/donghuanosekai/res/mipmap-xhdpi/ic_launcher.png b/src/pt/donghuanosekai/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..81a531aa4 Binary files /dev/null and b/src/pt/donghuanosekai/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/pt/donghuanosekai/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/donghuanosekai/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..6b94548ea Binary files /dev/null and b/src/pt/donghuanosekai/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/pt/donghuanosekai/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/donghuanosekai/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..173a03115 Binary files /dev/null and b/src/pt/donghuanosekai/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/pt/donghuanosekai/src/eu/kanade/tachiyomi/animeextension/pt/donghuanosekai/DonghuaNoSekai.kt b/src/pt/donghuanosekai/src/eu/kanade/tachiyomi/animeextension/pt/donghuanosekai/DonghuaNoSekai.kt new file mode 100644 index 000000000..984cbe1fa --- /dev/null +++ b/src/pt/donghuanosekai/src/eu/kanade/tachiyomi/animeextension/pt/donghuanosekai/DonghuaNoSekai.kt @@ -0,0 +1,302 @@ +package eu.kanade.tachiyomi.animeextension.pt.donghuanosekai + +import android.app.Application +import android.content.SharedPreferences +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.animeextension.pt.donghuanosekai.extractors.DonghuaNoSekaiExtractor +import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource +import eu.kanade.tachiyomi.animesource.model.AnimeFilterList +import eu.kanade.tachiyomi.animesource.model.AnimesPage +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.asObservableSuccess +import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import okhttp3.FormBody +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import rx.Observable +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy +import java.text.SimpleDateFormat +import java.util.Locale + +class DonghuaNoSekai : ConfigurableAnimeSource, ParsedAnimeHttpSource() { + + override val name = "Donghua no Sekai" + + override val baseUrl = "https://donghuanosekai.com" + + override val lang = "pt-BR" + + override val supportsLatest = true + + override fun headersBuilder() = super.headersBuilder() + .add("Referer", baseUrl) + .add("Origin", baseUrl) + + private val json: Json by injectLazy() + + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + // ============================== Popular =============================== + override fun popularAnimeRequest(page: Int) = GET(baseUrl, headers) + + override fun popularAnimeSelector() = "div.sidebarContent div.navItensTop li > a" + + override fun popularAnimeFromElement(element: Element) = SAnime.create().apply { + setUrlWithoutDomain(element.attr("href")) + title = element.attr("title") + thumbnail_url = element.selectFirst("img")!!.attr("src") + } + + override fun popularAnimeNextPageSelector() = null + + // =============================== Latest =============================== + override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/lancamentos?pagina=$page", headers) + + override fun latestUpdatesSelector() = "div.boxContent div.itemE > a" + + override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply { + setUrlWithoutDomain(element.attr("href")) + title = element.selectFirst("div.title h3")!!.text() + thumbnail_url = element.selectFirst("div.thumb img")!!.attr("src") + } + + override fun latestUpdatesNextPageSelector() = "ul.content-pagination > li.next" + + // =============================== Search =============================== + override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable { + return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler + val id = query.removePrefix(PREFIX_SEARCH) + client.newCall(GET("$baseUrl/$id")) + .asObservableSuccess() + .map(::searchAnimeByIdParse) + } else { + super.fetchSearchAnime(page, query, filters) + } + } + + private fun searchAnimeByIdParse(response: Response): AnimesPage { + val details = animeDetailsParse(response.asJsoup()) + return AnimesPage(listOf(details), false) + } + + @Serializable + data class SearchResponseDto( + val results: List, + val page: Int, + val total_page: Int = 1, + ) + + private val searchToken by lazy { + client.newCall(GET("$baseUrl/donghuas", headers)).execute() + .use { it.asJsoup() } + .selectFirst("div.menu_filter_box")!! + .attr("data-secury") + } + + override fun getFilterList() = DonghuaNoSekaiFilters.FILTER_LIST + + override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { + val params = DonghuaNoSekaiFilters.getSearchParameters(filters) + val body = FormBody.Builder().apply { + add("type", "lista") + add("action", "getListFilter") + add("limit", "30") + add("token", searchToken) + add("search", query.ifBlank { "0" }) + add("pagina", "$page") + val filterData = baseUrl.toHttpUrl().newBuilder().apply { + addQueryParameter("filter_animation", params.animation) + addQueryParameter("filter_audio", "undefined") + addQueryParameter("filter_letter", params.letter) + addQueryParameter("filter_order", params.orderBy) + addQueryParameter("filter_status", params.status) + addQueryParameter("type_url", "ONA") + }.build().encodedQuery + + val genres = params.genres.joinToString { "\"$it\"" } + val delgenres = params.deleted_genres.joinToString { "\"$it\"" } + + add("filters", """{"filter_data": "$filterData", "filter_genre_add": [$genres], "filter_genre_del": [$delgenres]}""") + }.build() + + return POST("$baseUrl/wp-admin/admin-ajax.php", body = body, headers = headers) + } + + override fun searchAnimeParse(response: Response): AnimesPage { + return runCatching { + val data = response.parseAs() + val animes = data.results.map(Jsoup::parse) + .mapNotNull { it.selectFirst(searchAnimeSelector()) } + .map(::searchAnimeFromElement) + val hasNext = data.total_page > data.page + AnimesPage(animes, hasNext) + }.getOrElse { AnimesPage(emptyList(), false) } + } + + override fun searchAnimeSelector() = "div.itemE > a" + + override fun searchAnimeFromElement(element: Element) = latestUpdatesFromElement(element) + + override fun searchAnimeNextPageSelector(): String? { + throw UnsupportedOperationException("Not used.") + } + + // =========================== Anime Details ============================ + override fun animeDetailsParse(document: Document) = SAnime.create().apply { + val doc = getRealDoc(document) + setUrlWithoutDomain(doc.location()) + thumbnail_url = doc.selectFirst("div.poster > img")!!.attr("src") + val infos = doc.selectFirst("div.dados")!! + + title = infos.selectFirst("h1")!!.text() + genre = infos.select("div.genresL > a").eachText().joinToString() + artist = infos.selectFirst("ul > li:contains(Estúdio)")?.ownText() + author = infos.selectFirst("ul > li:contains(Fansub)")?.ownText() + status = infos.selectFirst("ul > li:contains(Status)")?.ownText().parseStatus() + + description = buildString { + doc.select("div.articleContent:has(div:contains(Sinopse)) > div.context > p") + .eachText() + .joinToString("\n\n") + .let(::append) + + append("\n") + + infos.select("ul.b_flex > li") + .eachText() + .forEach { append("\n$it") } + } + } + + // ============================== Episodes ============================== + override fun episodeListParse(response: Response): List { + val doc = response.use { getRealDoc(it.asJsoup()) } + return doc.select(episodeListSelector()).map(::episodeFromElement) + } + + override fun episodeListSelector() = "div.episode_list > div.item > a" + + override fun episodeFromElement(element: Element) = SEpisode.create().apply { + setUrlWithoutDomain(element.attr("href")) + element.selectFirst("span.episode")!!.text().let { + name = it + episode_number = it.substringAfterLast(" ").toFloatOrNull() ?: 0F + } + date_upload = element.selectFirst("div.data")?.text().orEmpty().toDate() + } + + // ============================ Video Links ============================= + override fun videoListParse(response: Response): List