diff --git a/src/pt/opex/AndroidManifest.xml b/src/pt/opex/AndroidManifest.xml new file mode 100644 index 000000000..30deb7f79 --- /dev/null +++ b/src/pt/opex/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/src/pt/opex/build.gradle b/src/pt/opex/build.gradle new file mode 100644 index 000000000..bbcc58090 --- /dev/null +++ b/src/pt/opex/build.gradle @@ -0,0 +1,16 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'One Piece Ex' + pkgNameSuffix = 'pt.opex' + extClass = '.OnePieceEx' + extVersionCode = 1 + libVersion = '1.2' +} + +dependencies { + implementation project(':lib-ratelimit') +} + +apply from: "$rootDir/common.gradle" diff --git a/src/pt/opex/res/mipmap-hdpi/ic_launcher.png b/src/pt/opex/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..b8172bbb8 Binary files /dev/null and b/src/pt/opex/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/pt/opex/res/mipmap-mdpi/ic_launcher.png b/src/pt/opex/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..57bd42a3d Binary files /dev/null and b/src/pt/opex/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/pt/opex/res/mipmap-xhdpi/ic_launcher.png b/src/pt/opex/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..da002668a Binary files /dev/null and b/src/pt/opex/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/pt/opex/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/opex/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..0d72b1b61 Binary files /dev/null and b/src/pt/opex/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/pt/opex/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/opex/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..a886ebbd3 Binary files /dev/null and b/src/pt/opex/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/pt/opex/res/web_hi_res_512.png b/src/pt/opex/res/web_hi_res_512.png new file mode 100644 index 000000000..37feaf7a4 Binary files /dev/null and b/src/pt/opex/res/web_hi_res_512.png differ diff --git a/src/pt/opex/src/eu/kanade/tachiyomi/extension/pt/opex/OnePieceEx.kt b/src/pt/opex/src/eu/kanade/tachiyomi/extension/pt/opex/OnePieceEx.kt new file mode 100644 index 000000000..2b4476304 --- /dev/null +++ b/src/pt/opex/src/eu/kanade/tachiyomi/extension/pt/opex/OnePieceEx.kt @@ -0,0 +1,239 @@ +package eu.kanade.tachiyomi.extension.pt.opex + +import com.github.salomonbrys.kotson.obj +import com.github.salomonbrys.kotson.string +import com.google.gson.JsonParser +import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage +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 eu.kanade.tachiyomi.util.asJsoup +import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import rx.Observable +import java.util.Locale +import java.util.concurrent.TimeUnit + +class OnePieceEx : ParsedHttpSource() { + + override val name = "One Piece Ex" + + override val baseUrl = "https://onepieceex.net" + + override val lang = "pt-BR" + + override val supportsLatest = false + + override val client: OkHttpClient = network.cloudflareClient.newBuilder() + .addInterceptor(RateLimitInterceptor(1, 1, TimeUnit.SECONDS)) + .build() + + override fun headersBuilder(): Headers.Builder = Headers.Builder() + .add("Accept", ACCEPT) + .add("Accept-Language", ACCEPT_LANGUAGE) + .add("Referer", "$baseUrl/mangas") + + override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/mangas", headers) + + override fun popularMangaParse(response: Response): MangasPage { + val mangaPage = super.popularMangaParse(response) + + val mainManga = SManga.create().apply { + title = "One Piece" + thumbnail_url = MAIN_SERIES_THUMBNAIL + url = "/mangas/?type=main" + } + + val sbsManga = SManga.create().apply { + title = "SBS" + thumbnail_url = DEFAULT_THUMBNAIL + url = "/mangas/?type=sbs" + } + + val allMangas = listOf(mainManga, sbsManga) + mangaPage.mangas.toMutableList() + + return MangasPage(allMangas, mangaPage.hasNextPage) + } + + override fun popularMangaSelector(): String = "#post > div.volume" + + override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply { + title = element.select("div.volume-nome h2").text() + " - " + + element.select("div.volume-nome h3").text() + thumbnail_url = THUMBNAIL_URL_MAP[title.toUpperCase(Locale.ROOT)] ?: DEFAULT_THUMBNAIL + + val customUrl = "$baseUrl/mangas/".toHttpUrlOrNull()!!.newBuilder() + .addQueryParameter("type", "special") + .addQueryParameter("title", title) + .toString() + + setUrlWithoutDomain(customUrl) + } + + override fun popularMangaNextPageSelector(): String? = null + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + return super.fetchSearchManga(page, query, filters) + .map { mangaPage -> + val filteredMangas = mangaPage.mangas.filter { m -> m.title.contains(query, true) } + MangasPage(filteredMangas, mangaPage.hasNextPage) + } + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = popularMangaRequest(page) + + override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response) + + override fun searchMangaSelector() = popularMangaSelector() + + override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element) + + override fun searchMangaNextPageSelector(): String? = null + + override fun mangaDetailsRequest(manga: SManga): Request { + val newHeaders = headersBuilder() + .set("Referer", "$baseUrl/") + .build() + + return GET(baseUrl + manga.url, newHeaders) + } + + override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply { + val mangaUrl = document.location().toHttpUrlOrNull()!! + + when (mangaUrl.queryParameter("type")!!) { + "main" -> { + title = "One Piece" + author = "Eiichiro Oda" + genre = "Ação, Aventura, Comédia, Fantasia, Superpoderes" + status = SManga.ONGOING + description = "Um romance marítimo pelo \"One Piece\"!!! Estamos na Grande " + + "Era dos Piratas. Nela, muitos piratas lutam pelo tesouro deixado pelo " + + "lendário Rei dos Piratas G. Roger, o \"One Piece\". Luffy, um garoto " + + "que almeja ser pirata, embarca numa jornada com o sonho de se tornar " + + "o Rei dos Piratas!!! (Fonte: MANGA Plus)" + thumbnail_url = MAIN_SERIES_THUMBNAIL + } + "sbs" -> { + title = "SBS" + author = "Eiichiro Oda" + description = "O SBS é uma coluna especial encontrada na maioria dos " + + "tankobons da coleção, começando a partir do volume 4. É geralmente " + + "formatada como uma coluna direta de perguntas e respostas, com o " + + "Eiichiro Oda respondendo as cartas de fãs sobre uma grande variedade " + + "de assuntos. (Fonte: One Piece Wiki)" + thumbnail_url = DEFAULT_THUMBNAIL + } + "special" -> { + title = mangaUrl.queryParameter("title")!! + + val volumeEl = document.select("#post > div.volume:contains(" + title.substringAfter(" - ") + ")").first()!! + author = if (title.contains("One Piece")) "Eiichiro Oda" else "OPEX" + description = volumeEl.select("li.resenha").text() + thumbnail_url = THUMBNAIL_URL_MAP[title.toUpperCase(Locale.ROOT)] ?: DEFAULT_THUMBNAIL + } + } + } + + override fun chapterListRequest(manga: SManga): Request = mangaDetailsRequest(manga) + + override fun chapterListParse(response: Response): List { + val mangaUrl = response.request.url + val mangaType = mangaUrl.queryParameter("type")!! + + val selectorComplement = when (mangaType) { + "main" -> "#volumes" + "sbs" -> "#volumes div.volume header:contains(SBS)" + else -> "#post > div.volume:contains(" + mangaUrl.queryParameter("title")!!.substringAfter(" - ") + ")" + } + + val chapterListSelector = selectorComplement + (if (mangaType == "sbs") "" else " " + chapterListSelector()) + + return response.asJsoup() + .select(chapterListSelector) + .map(::chapterFromElement) + .reversed() + } + + override fun chapterListSelector() = "div.capitulos li.volume-capitulo" + + override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply { + val mangaUrl = element.ownerDocument().location().toHttpUrlOrNull()!! + + when (mangaUrl.queryParameter("type")!!) { + "main" -> { + name = element.select("span").first()!!.text() + setUrlWithoutDomain(element.select("a.online").first()!!.attr("abs:href")) + } + "sbs" -> { + name = element.select("div.volume-nome h2").first()!!.text() + setUrlWithoutDomain(element.select("header p.extra a:contains(SBS)").first()!!.attr("abs:href")) + } + "special" -> { + name = element.ownText() + setUrlWithoutDomain(element.select("a.online").first()!!.attr("abs:href")) + } + } + + scanlator = this@OnePieceEx.name + } + + override fun pageListParse(document: Document): List { + return document.select("script:containsData(paginasLista)").first()!! + .data() + .substringAfter("paginasLista = \"") + .substringBefore("\";") + .replace("\\\"", "\"") + .replace("\\\\\\/", "/") + .replace("//", "/") + .let { JsonParser.parseString(it).obj } + .entrySet() + .mapIndexed { i, entry -> + Page(i, document.location(), "$baseUrl/${entry.value.string}") + } + } + + 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) + } + + override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException("Not used") + + override fun latestUpdatesSelector() = throw UnsupportedOperationException("Not used") + + override fun latestUpdatesFromElement(element: Element): SManga = throw UnsupportedOperationException("Not used") + + override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException("Not used") + + companion object { + private const val ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9," + + "image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" + private const val ACCEPT_IMAGE = "image/webp,image/apng,image/*,*/*;q=0.8" + private const val ACCEPT_LANGUAGE = "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7,es;q=0.6,gl;q=0.5" + + private const val DEFAULT_THUMBNAIL = "https://onepieceex.net/mangareader/sbs/capa/preview/nao.jpg" + private const val MAIN_SERIES_THUMBNAIL = "https://onepieceex.net/mangareader/sbs/capa/preview/Volume_1.jpg" + private val THUMBNAIL_URL_MAP = mapOf( + "OPEX - DENSETSU NO SEKAI" to "https://onepieceex.net/mangareader/especiais/501/00.jpg", + "OPEX - ESPECIAIS" to "https://onepieceex.net/mangareader/especiais/27/00.jpg", + "ONE PIECE - ESPECIAIS DE ONE PIECE" to "https://onepieceex.net/mangareader/especiais/5/002.png", + "ONE PIECE - HISTÓRIAS DE CAPA" to "https://onepieceex.net/mangareader/mangas/428/00_c.jpg" + ) + } +}