diff --git a/src/pt/bakai/AndroidManifest.xml b/src/pt/bakai/AndroidManifest.xml new file mode 100644 index 000000000..30deb7f79 --- /dev/null +++ b/src/pt/bakai/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/src/pt/bakai/build.gradle b/src/pt/bakai/build.gradle new file mode 100644 index 000000000..48c2b1447 --- /dev/null +++ b/src/pt/bakai/build.gradle @@ -0,0 +1,15 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'Bakai' + pkgNameSuffix = 'pt.bakai' + extClass = '.Bakai' + extVersionCode = 1 +} + +dependencies { + implementation project(':lib-ratelimit') +} + +apply from: "$rootDir/common.gradle" diff --git a/src/pt/bakai/res/mipmap-hdpi/ic_launcher.png b/src/pt/bakai/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..4dc069e59 Binary files /dev/null and b/src/pt/bakai/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/pt/bakai/res/mipmap-mdpi/ic_launcher.png b/src/pt/bakai/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..bf34eebcd Binary files /dev/null and b/src/pt/bakai/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/pt/bakai/res/mipmap-xhdpi/ic_launcher.png b/src/pt/bakai/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..3ebae1078 Binary files /dev/null and b/src/pt/bakai/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/pt/bakai/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/bakai/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..22957bfab Binary files /dev/null and b/src/pt/bakai/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/pt/bakai/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/bakai/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..1d637c67e Binary files /dev/null and b/src/pt/bakai/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/pt/bakai/res/web_hi_res_512.png b/src/pt/bakai/res/web_hi_res_512.png new file mode 100644 index 000000000..53a8f8dee Binary files /dev/null and b/src/pt/bakai/res/web_hi_res_512.png differ diff --git a/src/pt/bakai/src/eu/kanade/tachiyomi/extension/pt/bakai/Bakai.kt b/src/pt/bakai/src/eu/kanade/tachiyomi/extension/pt/bakai/Bakai.kt new file mode 100644 index 000000000..b5e2c9592 --- /dev/null +++ b/src/pt/bakai/src/eu/kanade/tachiyomi/extension/pt/bakai/Bakai.kt @@ -0,0 +1,138 @@ +package eu.kanade.tachiyomi.extension.pt.bakai + +import eu.kanade.tachiyomi.lib.ratelimit.SpecificHostRateLimitInterceptor +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.text.SimpleDateFormat +import java.util.Locale + +class Bakai : ParsedHttpSource() { + + override val name = "Bakai" + + override val baseUrl = "https://bakai.org" + + override val lang = "pt-BR" + + override val supportsLatest = true + + override val client: OkHttpClient = network.cloudflareClient.newBuilder() + .addInterceptor(SpecificHostRateLimitInterceptor(baseUrl.toHttpUrl(), 1, 2)) + .addInterceptor(SpecificHostRateLimitInterceptor(CDN_URL.toHttpUrl(), 1, 1)) + .build() + + override fun headersBuilder(): Headers.Builder = Headers.Builder() + .add("Referer", "$baseUrl/") + + // Source doesn't have a popular list, so use latest instead. + override fun popularMangaRequest(page: Int): Request = latestUpdatesRequest(page) + + override fun popularMangaSelector(): String = latestUpdatesSelector() + + override fun popularMangaFromElement(element: Element): SManga = latestUpdatesFromElement(element) + + override fun popularMangaNextPageSelector(): String = latestUpdatesNextPageSelector() + + override fun latestUpdatesRequest(page: Int): Request { + val path = if (page > 1) "home/page/$page/" else "" + return GET("$baseUrl/$path", headers) + } + + override fun latestUpdatesSelector() = "#elCmsPageWrap ul.ipsGrid article.ipsBox" + + override fun latestUpdatesFromElement(element: Element): SManga = SManga.create().apply { + title = element.selectFirst("h2.ipsType_pageTitle a")!!.text().trim() + thumbnail_url = element.selectFirst("img.ipsImage[alt]")!!.attr("abs:src") + setUrlWithoutDomain(element.selectFirst("a[title]")!!.attr("href")) + } + + override fun latestUpdatesNextPageSelector() = + "#elCmsPageWrap ul.ipsPagination li.ipsPagination_next:not(.ipsPagination_inactive)" + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = "$baseUrl/search/".toHttpUrl().newBuilder() + .addQueryParameter("q", query) + .addQueryParameter("type", "cms_records1") + .addQueryParameter("search_in", "titles") + .addQueryParameter("sortby", "relevancy") + .toString() + + return GET(url, headers) + } + + override fun searchMangaSelector() = "#elSearch_main ol.ipsStream li.ipsStreamItem" + + override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply { + title = element.selectFirst("h2.ipsStreamItem_title a")!!.text().trim() + thumbnail_url = element.selectFirst("span.ipsThumb img")!!.attr("abs:src") + setUrlWithoutDomain(element.selectFirst("h2.ipsStreamItem_title a")!!.attr("href")) + } + + override fun searchMangaNextPageSelector(): String? = null + + override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply { + val infoElement = document.selectFirst(chapterListSelector())!! + + author = infoElement.select("p:contains(Artista:) a") + .joinToString { it.text().trim() } + genre = infoElement.selectFirst("p:contains(Tags:) span.ipsBadge + span")!!.text() + status = SManga.COMPLETED + description = infoElement.select("section.ipsType_richText p") + .joinToString("\n\n") { it.text().trim() } + thumbnail_url = infoElement.selectFirst("div.cCmsRecord_image img.ipsImage")!!.attr("abs:src") + } + + override fun chapterListSelector() = "#ipsLayout_contentWrapper article.ipsContained" + + override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply { + name = element.selectFirst("p:contains(Tipo:) a")!!.text() + scanlator = element.select("p:contains(Tradução:) a").firstOrNull() + ?.text()?.trim() + date_upload = element.ownerDocument().select("div.ipsPageHeader__meta time").firstOrNull() + ?.attr("datetime")?.toDate() ?: 0L + setUrlWithoutDomain(element.ownerDocument().location()) + } + + override fun pageListParse(document: Document): List { + return document.select(chapterListSelector() + " div.ipsGrid img.ipsImage") + .mapIndexed { i, element -> + Page(i, document.location(), element.attr("abs:data-src")) + } + } + + override fun imageUrlParse(document: Document) = "" + + override fun imageRequest(page: Page): Request { + val newHeaders = headersBuilder() + .set("Accept", ACCEPT_IMAGE) + .set("Referer", page.url) + .build() + + return GET(page.imageUrl!!, newHeaders) + } + + private fun String.toDate(): Long { + return runCatching { DATE_FORMATTER.parse(trim())?.time } + .getOrNull() ?: 0L + } + + companion object { + private const val CDN_URL = "https://img.bakai.org" + + private const val ACCEPT_IMAGE = "image/webp,image/apng,image/*,*/*;q=0.8" + + private val DATE_FORMATTER by lazy { + SimpleDateFormat("yyyy-mm-dd'T'HH:mm:ss'Z'", Locale.ENGLISH) + } + } +}