diff --git a/src/ja/nicomanga/AndroidManifest.xml b/src/ja/nicomanga/AndroidManifest.xml new file mode 100644 index 00000000..8072ee00 --- /dev/null +++ b/src/ja/nicomanga/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/src/ja/nicomanga/build.gradle b/src/ja/nicomanga/build.gradle new file mode 100644 index 00000000..a1922742 --- /dev/null +++ b/src/ja/nicomanga/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' + +ext { + extName = 'Nicomanga' + pkgNameSuffix = 'ja.nicomanga' + extClass = '.Nicomanga' + extVersionCode = 1 +} + +apply from: "$rootDir/common.gradle" diff --git a/src/ja/nicomanga/res/mipmap-hdpi/ic_launcher.png b/src/ja/nicomanga/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..a39dbc09 Binary files /dev/null and b/src/ja/nicomanga/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/ja/nicomanga/res/mipmap-mdpi/ic_launcher.png b/src/ja/nicomanga/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..984b6ae5 Binary files /dev/null and b/src/ja/nicomanga/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/ja/nicomanga/res/mipmap-xhdpi/ic_launcher.png b/src/ja/nicomanga/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..f16ac0d3 Binary files /dev/null and b/src/ja/nicomanga/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/ja/nicomanga/res/mipmap-xxhdpi/ic_launcher.png b/src/ja/nicomanga/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..6108802c Binary files /dev/null and b/src/ja/nicomanga/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/ja/nicomanga/res/mipmap-xxxhdpi/ic_launcher.png b/src/ja/nicomanga/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..4cb40341 Binary files /dev/null and b/src/ja/nicomanga/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/ja/nicomanga/res/web_hi_res_512.png b/src/ja/nicomanga/res/web_hi_res_512.png new file mode 100644 index 00000000..389245ab Binary files /dev/null and b/src/ja/nicomanga/res/web_hi_res_512.png differ diff --git a/src/ja/nicomanga/src/eu/kanade/tachiyomi/extension/ja/nicomanga/NMRegex.kt b/src/ja/nicomanga/src/eu/kanade/tachiyomi/extension/ja/nicomanga/NMRegex.kt new file mode 100644 index 00000000..844e47e5 --- /dev/null +++ b/src/ja/nicomanga/src/eu/kanade/tachiyomi/extension/ja/nicomanga/NMRegex.kt @@ -0,0 +1,9 @@ +package eu.kanade.tachiyomi.extension.ja.nicomanga + +object NMRegex { + val thumbnailURLRegex: Regex = "background-image:[^;]url\\s*\\(\\s*'([^?']+)".toRegex() + val statusRegex: Regex = "(?<=-)[^.]+".toRegex() + val urlRegex: Regex = "(?<=manga-)[^/]+(?=\\.html\$)".toRegex() + val floatRegex: Regex = "\\d+(?:\\.\\d+)?".toRegex() + val chapterIdRegex: Regex = "(?<=imgsListchap\\()\\d+".toRegex() +} diff --git a/src/ja/nicomanga/src/eu/kanade/tachiyomi/extension/ja/nicomanga/Nicomanga.kt b/src/ja/nicomanga/src/eu/kanade/tachiyomi/extension/ja/nicomanga/Nicomanga.kt new file mode 100644 index 00000000..be2af6ce --- /dev/null +++ b/src/ja/nicomanga/src/eu/kanade/tachiyomi/extension/ja/nicomanga/Nicomanga.kt @@ -0,0 +1,157 @@ +package eu.kanade.tachiyomi.extension.ja.nicomanga + +import eu.kanade.tachiyomi.extension.ja.nicomanga.NMRegex.chapterIdRegex +import eu.kanade.tachiyomi.extension.ja.nicomanga.NMRegex.floatRegex +import eu.kanade.tachiyomi.extension.ja.nicomanga.NMRegex.statusRegex +import eu.kanade.tachiyomi.extension.ja.nicomanga.NMRegex.thumbnailURLRegex +import eu.kanade.tachiyomi.extension.ja.nicomanga.NMRegex.urlRegex +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.HttpSource +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import java.net.URL + +class Nicomanga : HttpSource() { + override val baseUrl: String = "https://nicomanga.com" + override val lang: String = "ja" + override val name: String = "Nicomanga" + override val supportsLatest: Boolean = true + override val client: OkHttpClient = network.cloudflareClient + + private fun mangaListParse(response: Response): MangasPage { + val doc = Jsoup.parse(response.body.string()) + val mangaList: ArrayList = doc.select(".row > .thumb-item-flow") + val hasNextPage = + if (doc.select(".pagination li:last-of-type").size > 0 && + doc.select(".pagination li:last-of-type")[0].text() == "ยป" + ) { + doc.select(".pagination li:last-of-type a.disabled").size == 0 + } else { + doc.select(".pagination li:last-of-type a.active").size == 0 + } + val mangas = mangaList.map { manga -> + SManga.create().apply { + val relURL = manga.selectFirst(".series-title a")?.attr("href") ?: "" + setUrlWithoutDomain(URL(URL(baseUrl), relURL).toString()) + title = manga.selectFirst(".series-title")?.text() ?: "" + thumbnail_url = thumbnailURLRegex.find(manga.selectFirst(".img-in-ratio.lazyloaded")?.attr("style") ?: "")?.groupValues?.get(1) + } + } + return MangasPage(mangas, hasNextPage) + } + + override fun latestUpdatesParse(response: Response): MangasPage = mangaListParse(response) + + override fun latestUpdatesRequest(page: Int): Request { + val url = "$baseUrl/manga-list.html".toHttpUrl().newBuilder() + .addQueryParameter("page", page.toString()) + .addQueryParameter("sort", "last_update") + .addQueryParameter("sort_type", "DESC") + .build() + return GET(url) + } + + override fun popularMangaParse(response: Response): MangasPage = mangaListParse(response) + + override fun popularMangaRequest(page: Int): Request { + val url = "$baseUrl/manga-list.html".toHttpUrl().newBuilder() + .addQueryParameter("page", page.toString()) + .addQueryParameter("sort", "views") + .addQueryParameter("sort_type", "DESC") + .build() + return GET(url) + } + + override fun searchMangaParse(response: Response): MangasPage = mangaListParse(response) + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = "$baseUrl/manga-list.html".toHttpUrl().newBuilder() + .addQueryParameter("page", page.toString()) + .addQueryParameter("artist", "") + .addQueryParameter("author", "") + .addQueryParameter("group", "") + .addQueryParameter("m_status", "") + .addQueryParameter("name", query) + .addQueryParameter("genre", "") + .addQueryParameter("ungenre", "") + .addQueryParameter("magazine", "") + .addQueryParameter("sort", "last_update") + .addQueryParameter("sort_type", "DESC") + .build() + return GET(url, headers) + } + + override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply { + val doc = Jsoup.parse(response.body.string()) + author = doc.select("ul.manga-info a[href^=\"manga-author\"]").joinToString { it.text() } + genre = doc.select("ul.manga-info a[href^=\"manga-list-genre\"]").joinToString { it.text() } + val statusText = statusRegex.find(doc.select(".manga-info li:has(i.fa-spinner) a").attr("href"))?.groupValues?.get(0) ?: "" + status = when (statusText) { + "on-going" -> { + SManga.ONGOING + } + "completed" -> { + SManga.COMPLETED + } + else -> { + SManga.UNKNOWN + } + } + } + + override fun chapterListParse(response: Response): List { + val doc = Jsoup.parse(response.body.string()) + val chapterList = doc.select("ul > a") + var lastNum = 0f + val chapters = chapterList.map { chapter -> + SChapter.create().apply { + name = chapter.attr("title").trim() + setUrlWithoutDomain(URL(URL(baseUrl), chapter.attr("href")).toString()) + chapter_number = floatRegex.find(chapter.attr("title").trim())?.groupValues?.get(0)?.toFloat() ?: (lastNum + 0.01f) + lastNum = chapter_number + } + } + return chapters + } + + override fun chapterListRequest(manga: SManga): Request { + val slug = urlRegex.find(manga.url)?.groupValues?.get(0) ?: "" + return GET("$baseUrl/app/manga/controllers/cont.Listchapterapi.php?slug=$slug") + } + + override fun pageListParse(response: Response): List { + val doc = Jsoup.parse(response.body.string()) + val pages = ArrayList() + // Nicovideo will refuse to serve any pages if the user has not logged in + val pageList = doc.select("img.chapter-img") + for ((i, page) in pageList.withIndex()) { + val url = page.attr("data-src") + pages.add(Page(i + 1, url, url)) + } + return pages + } + + override fun pageListRequest(chapter: SChapter): Request { + val r = client.newCall(GET(getChapterUrl(chapter))).execute() + val id = chapterIdRegex.find(r.body.string())?.groupValues?.get(0) ?: throw Exception("chapter-id not found") + val headers = headersBuilder().set("referer", getChapterUrl(chapter)).build() + return GET("$baseUrl/app/manga/controllers/cont.imgsList.php?cid=$id", headers) + } + + override fun imageRequest(page: Page): Request { + val headers = headersBuilder().set("referer", baseUrl).build() + return GET(page.imageUrl!!, headers) + } + + override fun imageUrlParse(response: Response): String = + throw UnsupportedOperationException("Not used") +}