Add Gangan Online (#11280)

* add gangan

* refactor

* rm
This commit is contained in:
manti 2025-10-28 15:37:05 +01:00 committed by Draff
parent 5f6e00499a
commit f019c2a273
Signed by: Draff
GPG Key ID: E8A89F3211677653
8 changed files with 246 additions and 0 deletions

View File

@ -0,0 +1,7 @@
ext {
extName = 'Gangan Online'
extClass = '.GanganOnline'
extVersionCode = 1
isNsfw = false
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -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<T>(
val props: Props<T>,
)
@Serializable
class Props<T>(
val pageProps: PageProps<T>,
)
@Serializable
class PageProps<T>(
val data: T,
)
@Serializable
class MangaListDto(
val titleSections: List<MangaSectionDto>?, // Popular/Finished
val sections: List<SearchSectionDto>?, // Search
val ongoingTitleSection: MangaSectionDto?, // GA
val finishedTitleSection: MangaSectionDto?, // GA
)
@Serializable
class MangaSectionDto(
val titles: List<MangaDto>,
)
@Serializable
class SearchSectionDto(
val titleLinks: List<MangaDto>,
)
@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<MangaDto>?,
)
@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<ChapterDto>,
) {
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<PageDto>,
)
@Serializable
class PageDto(
val image: PageImageUrlDto?,
val linkImage: PageImageUrlDto?,
)
@Serializable
class PageImageUrlDto(
val imageUrl: String,
)

View File

@ -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<CategoryFilter>()
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<MangaListDto>()
data.sections?.flatMap { it.titleLinks }
?.filter { it.isNovel != true }
?.map { it.toSManga(baseUrl) }
}
"/rensai" in url || "/finish" in url -> {
val data = response.parseAsNextData<MangaListDto>()
data.titleSections?.flatMap { it.titles }
?.filter { it.isNovel != true }
?.map { it.toSManga(baseUrl) }
}
"/ga" in url -> {
val data = response.parseAsNextData<MangaListDto>()
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<PixivPageDto>()
data.ganganTitles?.map { it.toSManga(baseUrl) }
}
else -> null
}
return MangasPage(mangas!!, false)
}
override fun mangaDetailsParse(response: Response): SManga {
return response.parseAsNextData<MangaDetailDto>().default.toSManga(baseUrl)
}
override fun chapterListParse(response: Response): List<SChapter> {
val mangaUrl = response.request.url.toString()
.substringBefore("/chapter")
.substringAfter(baseUrl)
val data = response.parseAsNextData<MangaDetailDto>().default
return data.chapters
.filter { it.status == null || it.status >= 4 }
.map { it.toSChapter(mangaUrl, dateFormat) }
}
override fun pageListParse(response: Response): List<Page> {
val data = response.parseAsNextData<PageListDto>()
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<Pair<String, String>>) :
Filter.Select<String>("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 <reified T> Response.parseAsNextData(): T {
val script = this.asJsoup().selectFirst("script#__NEXT_DATA__")!!.data()
return script.parseAs<NextData<T>>().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()
}