diff --git a/multisrc/overrides/mangareader/rawotaku/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/mangareader/rawotaku/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..4f174fb9d Binary files /dev/null and b/multisrc/overrides/mangareader/rawotaku/res/mipmap-hdpi/ic_launcher.png differ diff --git a/multisrc/overrides/mangareader/rawotaku/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/mangareader/rawotaku/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..22d5a1e48 Binary files /dev/null and b/multisrc/overrides/mangareader/rawotaku/res/mipmap-mdpi/ic_launcher.png differ diff --git a/multisrc/overrides/mangareader/rawotaku/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/mangareader/rawotaku/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..80bf79123 Binary files /dev/null and b/multisrc/overrides/mangareader/rawotaku/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/mangareader/rawotaku/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/mangareader/rawotaku/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..77a610baf Binary files /dev/null and b/multisrc/overrides/mangareader/rawotaku/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/mangareader/rawotaku/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/mangareader/rawotaku/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..7302681e8 Binary files /dev/null and b/multisrc/overrides/mangareader/rawotaku/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/mangareader/rawotaku/src/RawOtaku.kt b/multisrc/overrides/mangareader/rawotaku/src/RawOtaku.kt new file mode 100644 index 000000000..8ac981d98 --- /dev/null +++ b/multisrc/overrides/mangareader/rawotaku/src/RawOtaku.kt @@ -0,0 +1,251 @@ +package eu.kanade.tachiyomi.extension.ja.rawotaku + +import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.network.interceptor.rateLimit +import eu.kanade.tachiyomi.source.model.Filter +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.util.asJsoup +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +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 org.jsoup.nodes.TextNode +import org.jsoup.select.Evaluator +import rx.Observable +import java.net.URLEncoder + +class RawOtaku : MangaReader() { + + override val name = "Raw Otaku" + + override val lang = "ja" + + override val baseUrl = "https://rawotaku.com" + + override val client = network.cloudflareClient.newBuilder() + .rateLimit(2) + .build() + + override fun headersBuilder() = super.headersBuilder() + .add("Referer", "$baseUrl/") + + // ============================== Popular =============================== + + override fun popularMangaRequest(page: Int) = + GET("$baseUrl/filter/?type=all&status=all&language=all&sort=most-viewed&p=$page", headers) + + // =============================== Latest =============================== + + override fun latestUpdatesRequest(page: Int) = + GET("$baseUrl/filter/?type=all&status=all&language=all&sort=latest-updated&p=$page", headers) + + // =============================== Search =============================== + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = baseUrl.toHttpUrl().newBuilder().apply { + if (query.isNotBlank()) { + addQueryParameter("q", query) + } else { + addPathSegment("filter") + addPathSegment("") + + filters.ifEmpty(::getFilterList).forEach { filter -> + when (filter) { + is TypeFilter -> { + addQueryParameter(filter.param, filter.selection) + } + is StatusFilter -> { + addQueryParameter(filter.param, filter.selection) + } + + is LanguageFilter -> { + addQueryParameter(filter.param, filter.selection) + } + is SortFilter -> { + addQueryParameter(filter.param, filter.selection) + } + is GenresFilter -> { + filter.state.forEach { + if (it.state) { + addQueryParameter(filter.param, it.id) + } + } + } + else -> { } + } + } + } + + addQueryParameter("p", page.toString()) + }.build() + + return GET(url, headers) + } + + override fun searchMangaSelector() = ".manga_list-sbs .manga-poster" + + override fun searchMangaFromElement(element: Element) = + SManga.create().apply { + setUrlWithoutDomain(element.attr("href")) + element.selectFirst(Evaluator.Tag("img"))!!.let { + title = it.attr("alt") + thumbnail_url = it.imgAttr() + } + } + + override fun searchMangaNextPageSelector() = "ul.pagination > li.active + li" + + // =============================== Filters ============================== + + override fun getFilterList() = + FilterList( + Note, + Filter.Separator(), + TypeFilter(), + StatusFilter(), + LanguageFilter(), + SortFilter(), + GenresFilter(), + ) + + // =========================== Manga Details ============================ + + override fun mangaDetailsParse(document: Document) = SManga.create().apply { + val root = document.selectFirst(Evaluator.Id("ani_detail"))!! + val mangaTitle = root.selectFirst(Evaluator.Class("manga-name"))!!.ownText() + title = mangaTitle + description = buildString { + root.selectFirst(".description")?.ownText()?.let { append(it) } + append("\n\n") + root.selectFirst(".manga-name-or")?.ownText()?.let { + if (it.isNotEmpty() && it != mangaTitle) { + append("Alternative Title: ") + append(it) + } + } + }.trim() + thumbnail_url = root.selectFirst(Evaluator.Tag("img"))!!.imgAttr() + genre = root.selectFirst(Evaluator.Class("genres"))!!.children().joinToString { it.ownText() } + for (item in root.selectFirst(Evaluator.Class("anisc-info"))!!.children()) { + if (item.hasClass("item").not()) continue + when (item.selectFirst(Evaluator.Class("item-head"))!!.ownText()) { + "著者:" -> item.parseAuthorsTo(this) + "地位:" -> status = when (item.selectFirst(Evaluator.Class("name"))!!.ownText().lowercase()) { + "ongoing" -> SManga.ONGOING + "completed" -> SManga.COMPLETED + "on-hold" -> SManga.ON_HIATUS + "canceled" -> SManga.CANCELLED + else -> SManga.UNKNOWN + } + } + } + } + + private fun Element.parseAuthorsTo(manga: SManga) { + val authors = select(Evaluator.Tag("a")) + val text = authors.map { it.ownText().replace(",", "") } + val count = authors.size + when (count) { + 0 -> return + 1 -> { + manga.author = text[0] + return + } + } + val authorList = ArrayList(count) + val artistList = ArrayList(count) + for ((index, author) in authors.withIndex()) { + val textNode = author.nextSibling() as? TextNode + val list = if (textNode != null && "(Art)" in textNode.wholeText) artistList else authorList + list.add(text[index]) + } + if (authorList.isEmpty().not()) manga.author = authorList.joinToString() + if (artistList.isEmpty().not()) manga.artist = artistList.joinToString() + } + + // ============================== Chapters ============================== + + override fun chapterListRequest(mangaUrl: String, type: String): Request = + GET(baseUrl + mangaUrl, headers) + + override fun parseChapterElements(response: Response, isVolume: Boolean): List { + TODO("Not yet implemented") + } + + override val chapterType = "" + override val volumeType = "" + + override fun fetchChapterList(manga: SManga): Observable> { + return client.newCall(chapterListRequest(manga)) + .asObservableSuccess() + .map(::parseChapterList) + } + + private fun parseChapterList(response: Response): List { + val document = response.use { it.asJsoup() } + + return document.select(chapterListSelector()) + .map(::chapterFromElement) + } + + private fun chapterListSelector(): String = "#ja-chaps > .chapter-item" + + private fun chapterFromElement(element: Element): SChapter = SChapter.create().apply { + val id = element.attr("data-id") + element.selectFirst("a")!!.run { + setUrlWithoutDomain(attr("href") + "#$id") + name = selectFirst(".name")?.text() ?: text() + } + } + + // =============================== Pages ================================ + + override fun fetchPageList(chapter: SChapter): Observable> = Observable.fromCallable { + val id = chapter.url.substringAfterLast("#") + + val ajaxHeaders = super.headersBuilder().apply { + add("Accept", "application/json, text/javascript, */*; q=0.01") + add("Referer", URLEncoder.encode(baseUrl + chapter.url.substringBeforeLast("#"), "utf-8")) + add("X-Requested-With", "XMLHttpRequest") + }.build() + + val ajaxUrl = "$baseUrl/json/chapter?mode=vertical&id=$id" + client.newCall(GET(ajaxUrl, ajaxHeaders)).execute().let(::pageListParse) + } + + override fun pageListParse(response: Response): List { + val document = response.use { it.parseHtmlProperty() } + + val pageList = document.select(".container-reader-chapter > div > img").map { + val index = it.attr("alt").toInt() + val imgUrl = it.imgAttr() + + Page(index, imageUrl = imgUrl) + } + + return pageList + } + + // ============================= Utilities ============================== + + private fun Element.imgAttr(): String = when { + hasAttr("data-lazy-src") -> attr("abs:data-lazy-src") + hasAttr("data-src") -> attr("abs:data-src") + else -> attr("abs:src") + } + + private fun Response.parseHtmlProperty(): Document { + val html = Json.parseToJsonElement(body.string()).jsonObject["html"]!!.jsonPrimitive.content + return Jsoup.parseBodyFragment(html) + } +} diff --git a/multisrc/overrides/mangareader/rawotaku/src/RawOtakuFilters.kt b/multisrc/overrides/mangareader/rawotaku/src/RawOtakuFilters.kt new file mode 100644 index 000000000..cc443b5d5 --- /dev/null +++ b/multisrc/overrides/mangareader/rawotaku/src/RawOtakuFilters.kt @@ -0,0 +1,110 @@ +package eu.kanade.tachiyomi.extension.ja.rawotaku + +import eu.kanade.tachiyomi.source.model.Filter + +object Note : Filter.Header("NOTE: Ignored if using text search!") + +sealed class Select( + name: String, + val param: String, + values: Array, +) : Filter.Select(name, values) { + open val selection: String + get() = if (state == 0) "" else state.toString() +} + +class TypeFilter( + values: Array = types.keys.toTypedArray(), +) : Select("タイプ", "type", values) { + override val selection: String + get() = types[values[state]]!! + + companion object { + private val types = mapOf( + "全て" to "all", + "Raw Manga" to "Raw Manga", + "BLコミック" to "BLコミック", + "TLコミック" to "TLコミック", + "オトナコミック" to "オトナコミック", + "女性マンガ" to "女性マンガ", + "少女マンガ" to "少女マンガ", + "少年マンガ" to "少年マンガ", + "青年マンガ" to "青年マンガ", + ) + } +} + +class StatusFilter( + values: Array = statuses.keys.toTypedArray(), +) : Select("地位", "status", values) { + override val selection: String + get() = statuses[values[state]]!! + + companion object { + private val statuses = mapOf( + "全て" to "all", + "Publishing" to "Publishing", + "Finished" to "Finished", + ) + } +} + +class LanguageFilter( + values: Array = languages.keys.toTypedArray(), +) : Select("言語", "language", values) { + override val selection: String + get() = languages[values[state]]!! + + companion object { + private val languages = mapOf( + "全て" to "all", + "Japanese" to "ja", + "English" to "en", + ) + } +} + +class SortFilter( + values: Array = sort.keys.toTypedArray(), +) : Select("選別", "sort", values) { + override val selection: String + get() = sort[values[state]]!! + + companion object { + private val sort = mapOf( + "デフォルト" to "default", + "最新の更新" to "latest-updated", + "最も見られました" to "most-viewed", + "Title [A-Z]" to "title-az", + "Title [Z-A]" to "title-za", + ) + } +} + +class Genre(name: String, val id: String) : Filter.CheckBox(name) + +class GenresFilter( + values: List = genres, +) : Filter.Group("ジャンル", values) { + val param = "genre[]" + + companion object { + private val genres: List + get() = listOf( + Genre("アクション", "55"), + Genre("エッチ", "15706"), + Genre("コメディ", "91"), + Genre("ドラマ", "56"), + Genre("ハーレム", "20"), + Genre("ファンタジー", "1"), + Genre("冒険", "54"), + Genre("悪魔", "6820"), + Genre("武道", "1064"), + Genre("歴史的", "9600"), + Genre("警察・特殊部隊", "6089"), + Genre("車・バイク", "4329"), + Genre("音楽", "473"), + Genre("魔法", "1416"), + ) + } +} diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mangareader/MangaReaderGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mangareader/MangaReaderGenerator.kt index eaee555b4..ca321e107 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mangareader/MangaReaderGenerator.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mangareader/MangaReaderGenerator.kt @@ -33,6 +33,12 @@ class MangaReaderGenerator : ThemeSourceGenerator { pkgName = "comickiba", overrideVersionCode = 33, ), + SingleLang( + name = "Raw Otaku", + baseUrl = "https://rawotaku.com", + lang = "ja", + isNsfw = true, + ), ) companion object {