diff --git a/src/ja/ganganonline/build.gradle b/src/ja/ganganonline/build.gradle new file mode 100644 index 000000000..599be39b1 --- /dev/null +++ b/src/ja/ganganonline/build.gradle @@ -0,0 +1,7 @@ +ext { + extName = 'Gangan Online' + extClass = '.GanganOnline' + extVersionCode = 1 + isNsfw = false +} +apply from: "$rootDir/common.gradle" diff --git a/src/ja/ganganonline/res/mipmap-hdpi/ic_launcher.png b/src/ja/ganganonline/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..e845cef87 Binary files /dev/null and b/src/ja/ganganonline/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/ja/ganganonline/res/mipmap-mdpi/ic_launcher.png b/src/ja/ganganonline/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..f609ae3d2 Binary files /dev/null and b/src/ja/ganganonline/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/ja/ganganonline/res/mipmap-xhdpi/ic_launcher.png b/src/ja/ganganonline/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..41ab2e21b Binary files /dev/null and b/src/ja/ganganonline/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/ja/ganganonline/res/mipmap-xxhdpi/ic_launcher.png b/src/ja/ganganonline/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..64a056c53 Binary files /dev/null and b/src/ja/ganganonline/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/ja/ganganonline/res/mipmap-xxxhdpi/ic_launcher.png b/src/ja/ganganonline/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..b7ae1ec82 Binary files /dev/null and b/src/ja/ganganonline/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/ja/ganganonline/src/eu/kanade/tachiyomi/extension/ja/ganganonline/Dto.kt b/src/ja/ganganonline/src/eu/kanade/tachiyomi/extension/ja/ganganonline/Dto.kt new file mode 100644 index 000000000..59c65711b --- /dev/null +++ b/src/ja/ganganonline/src/eu/kanade/tachiyomi/extension/ja/ganganonline/Dto.kt @@ -0,0 +1,112 @@ +package eu.kanade.tachiyomi.extension.ja.ganganonline + +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import keiyoushi.utils.tryParse +import kotlinx.serialization.Serializable +import java.text.SimpleDateFormat + +@Serializable +class NextData( + val props: Props, +) + +@Serializable +class Props( + val pageProps: PageProps, +) + +@Serializable +class PageProps( + val data: T, +) + +@Serializable +class MangaListDto( + val titleSections: List?, // Popular/Finished + val sections: List?, // Search + val ongoingTitleSection: MangaSectionDto?, // GA + val finishedTitleSection: MangaSectionDto?, // GA +) + +@Serializable +class MangaSectionDto( + val titles: List, +) + +@Serializable +class SearchSectionDto( + val titleLinks: List, +) + +@Serializable +class MangaDto( + private val titleId: Int, + private val header: String?, // Popular/Finished + private val name: String?, // Search + private val imageUrl: String?, + val isNovel: Boolean?, +) { + fun toSManga(baseUrl: String): SManga = SManga.create().apply { + url = "/title/$titleId" + title = header ?: name!! + thumbnail_url = baseUrl + imageUrl + } +} + +@Serializable +class PixivPageDto( + val ganganTitles: List?, +) + +@Serializable +class MangaDetailDto( + val default: MangaDetailDefaultDto, +) + +@Serializable +class MangaDetailDefaultDto( + private val titleName: String, + private val author: String, + private val description: String, + private val imageUrl: String, + val chapters: List, +) { + fun toSManga(baseUrl: String): SManga = SManga.create().apply { + title = titleName + author = this@MangaDetailDefaultDto.author + description = this@MangaDetailDefaultDto.description + thumbnail_url = baseUrl + imageUrl + } +} + +@Serializable +class ChapterDto( + private val id: Int, + val status: Int?, + private val mainText: String, + private val subText: String?, + private val publishingPeriod: String?, +) { + fun toSChapter(mangaUrl: String, dateFormat: SimpleDateFormat): SChapter = SChapter.create().apply { + url = "$mangaUrl/chapter/$id" + name = mainText + if (!subText.isNullOrEmpty()) " - $subText" else "" + date_upload = publishingPeriod?.substringBefore("〜").let { dateFormat.tryParse(it) } + } +} + +@Serializable +class PageListDto( + val pages: List, +) + +@Serializable +class PageDto( + val image: PageImageUrlDto?, + val linkImage: PageImageUrlDto?, +) + +@Serializable +class PageImageUrlDto( + val imageUrl: String, +) diff --git a/src/ja/ganganonline/src/eu/kanade/tachiyomi/extension/ja/ganganonline/GanganOnline.kt b/src/ja/ganganonline/src/eu/kanade/tachiyomi/extension/ja/ganganonline/GanganOnline.kt new file mode 100644 index 000000000..1c0ed6d2c --- /dev/null +++ b/src/ja/ganganonline/src/eu/kanade/tachiyomi/extension/ja/ganganonline/GanganOnline.kt @@ -0,0 +1,127 @@ +package eu.kanade.tachiyomi.extension.ja.ganganonline + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.Filter +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 eu.kanade.tachiyomi.util.asJsoup +import keiyoushi.utils.firstInstance +import keiyoushi.utils.parseAs +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import java.text.SimpleDateFormat +import java.util.Locale + +class GanganOnline : HttpSource() { + override val name = "Gangan Online" + override val baseUrl = "https://www.ganganonline.com" + override val lang = "ja" + override val supportsLatest = false + + private val dateFormat = SimpleDateFormat("yyyy.MM.dd", Locale.JAPAN) + + override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/rensai", headers) + + override fun popularMangaParse(response: Response): MangasPage = searchMangaParse(response) + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + if (query.isNotBlank()) { + val url = "$baseUrl/search/result".toHttpUrl().newBuilder() + .addQueryParameter("keyword", query) + .build() + return GET(url, headers) + } + + val filter = filters.firstInstance() + val url = baseUrl.toHttpUrl().newBuilder() + .addPathSegments(filter.toUriPart().removePrefix("/")) + .build() + return GET(url, headers) + } + + override fun searchMangaParse(response: Response): MangasPage { + val url = response.request.url.toString() + val mangas = when { + "/search/result" in url -> { + val data = response.parseAsNextData() + data.sections?.flatMap { it.titleLinks } + ?.filter { it.isNovel != true } + ?.map { it.toSManga(baseUrl) } + } + "/rensai" in url || "/finish" in url -> { + val data = response.parseAsNextData() + data.titleSections?.flatMap { it.titles } + ?.filter { it.isNovel != true } + ?.map { it.toSManga(baseUrl) } + } + "/ga" in url -> { + val data = response.parseAsNextData() + val ongoing = data.ongoingTitleSection?.titles!! + val finished = data.finishedTitleSection?.titles!! + (ongoing + finished) + .filter { it.isNovel != true } + .map { it.toSManga(baseUrl) } + } + "/pixiv" in url -> { + val data = response.parseAsNextData() + data.ganganTitles?.map { it.toSManga(baseUrl) } + } + else -> null + } + return MangasPage(mangas!!, false) + } + + override fun mangaDetailsParse(response: Response): SManga { + return response.parseAsNextData().default.toSManga(baseUrl) + } + + override fun chapterListParse(response: Response): List { + val mangaUrl = response.request.url.toString() + .substringBefore("/chapter") + .substringAfter(baseUrl) + val data = response.parseAsNextData().default + + return data.chapters + .filter { it.status == null || it.status >= 4 } + .map { it.toSChapter(mangaUrl, dateFormat) } + } + + override fun pageListParse(response: Response): List { + val data = response.parseAsNextData() + return data.pages.mapIndexed { i, page -> + val imageUrl = (page.image ?: page.linkImage)!!.imageUrl + Page(i, imageUrl = baseUrl + imageUrl) + } + } + + override fun getFilterList() = FilterList( + CategoryFilter(getCategoryList()), + ) + + private class CategoryFilter(private val category: Array>) : + Filter.Select("Category", category.map { it.first }.toTypedArray()) { + fun toUriPart() = category[state].second + } + + private fun getCategoryList() = arrayOf( + Pair("連載作品", "/rensai"), + Pair("連載終了作品", "/finish"), + Pair("ガンガンpixiv", "/pixiv"), + Pair("ガンガンGA", "/ga"), + ) + + private inline fun Response.parseAsNextData(): T { + val script = this.asJsoup().selectFirst("script#__NEXT_DATA__")!!.data() + return script.parseAs>().props.pageProps.data + } + + // Unsupported + override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException() + override fun latestUpdatesParse(response: Response): MangasPage = throw UnsupportedOperationException() + override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() +}