Add TakeComic / Remove Web Comic Ganma Plus & Web Comic Ganma & multisrc (#11183)

* add storiadash

* remove and add takecomic

* api and refactor

* apiUrl
This commit is contained in:
manti 2025-10-22 07:29:15 +02:00 committed by Draff
parent 3af84ded97
commit 89f33e0106
Signed by: Draff
GPG Key ID: E8A89F3211677653
24 changed files with 417 additions and 168 deletions

View File

@ -1,9 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 9
dependencies {
api(project(":lib:speedbinb"))
}

View File

@ -1,129 +0,0 @@
package eu.kanade.tachiyomi.multisrc.comicgamma
import eu.kanade.tachiyomi.lib.speedbinb.SpeedBinbInterceptor
import eu.kanade.tachiyomi.lib.speedbinb.SpeedBinbReader
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.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import kotlinx.serialization.json.Json
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.select.Evaluator
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
open class ComicGamma(
override val name: String,
override val baseUrl: String,
override val lang: String = "ja",
) : ParsedHttpSource() {
override val supportsLatest = false
private val json = Injekt.get<Json>()
override val client = network.cloudflareClient.newBuilder()
.addInterceptor(SpeedBinbInterceptor(json))
.build()
override fun popularMangaRequest(page: Int) = GET("$baseUrl/manga/", headers)
override fun popularMangaNextPageSelector(): String? = null
override fun popularMangaSelector() = ".tab_panel.active .manga_item"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
url = element.selectFirst(Evaluator.Tag("a"))!!.attr("href")
title = element.selectFirst(Evaluator.Class("manga_title"))!!.text()
author = element.selectFirst(Evaluator.Class("manga_author"))!!.text()
val genreList = element.select(Evaluator.Tag("li")).map { it.text() }
genre = genreList.joinToString()
status = when {
genreList.contains("完結") && !genreList.contains("リピート配信") -> SManga.COMPLETED
else -> SManga.ONGOING
}
thumbnail_url = element.selectFirst(Evaluator.Tag("img"))!!.absUrl("src")
}
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
override fun latestUpdatesSelector() = throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
fetchPopularManga(page).map { p -> MangasPage(p.mangas.filter { it.title.contains(query) }, false) }
override fun searchMangaNextPageSelector() = throw UnsupportedOperationException()
override fun searchMangaSelector() = throw UnsupportedOperationException()
override fun searchMangaFromElement(element: Element) = throw UnsupportedOperationException()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
throw UnsupportedOperationException()
private val reader by lazy { SpeedBinbReader(client, headers, json) }
override fun pageListParse(document: Document) = reader.pageListParse(document)
override fun mangaDetailsParse(document: Document): SManga {
val titleElement = document.selectFirst(Evaluator.Class("manga__title"))!!
val titleName = titleElement.child(0).text()
val desc = document.selectFirst(".detail__item > p:not(:empty)")?.run {
select(Evaluator.Tag("br")).prepend("\\n")
this.text().replace("\\n", "\n").replace("\n ", "\n")
}
val listResponse = client.newCall(popularMangaRequest(0)).execute()
val manga = popularMangaParse(listResponse).mangas.find { it.title == titleName }
return manga?.apply { description = desc } ?: SManga.create().apply {
author = titleElement.child(1).text()
description = desc
status = SManga.UNKNOWN
val slug = document.location().removeSuffix("/").substringAfterLast("/")
thumbnail_url = "$baseUrl/img/manga_thumb/${slug}_list.jpg"
}
}
override fun chapterListSelector() = ".read__area .read__outer > a:not([href=#comics])"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
url = element.attr("href").toOldChapterUrl()
val number = url.removeSuffix("/").substringAfterLast('/').replace('_', '.')
val list = element.selectFirst(Evaluator.Class("read__contents"))!!.children()
name = "[$number] ${list[0].text()}"
if (list.size >= 3) {
date_upload = dateFormat.parseJST(list[2].text())?.time ?: 0L
}
}
override fun pageListRequest(chapter: SChapter) =
GET(baseUrl + chapter.url.toNewChapterUrl(), headers)
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
companion object {
internal fun SimpleDateFormat.parseJST(date: String) = parse(date)?.apply {
time += 12 * 3600 * 1000 // updates at 12 noon
}
private fun getJSTFormat(datePattern: String) =
SimpleDateFormat(datePattern, Locale.JAPANESE).apply {
timeZone = TimeZone.getTimeZone("GMT+09:00")
}
private val dateFormat by lazy { getJSTFormat("yyyy年M月dd日") }
private fun String.toOldChapterUrl(): String {
// ../../../_files/madeinabyss/063_2/
val segments = split('/')
val size = segments.size
val slug = segments[size - 3]
val number = segments[size - 2]
return "/manga/$slug/_files/$number/"
}
private fun String.toNewChapterUrl(): String {
val segments = split('/')
return "/_files/${segments[2]}/${segments[4]}/"
}
}
}

View File

@ -0,0 +1,10 @@
ext {
extName = 'TakeComic'
extClass = '.TakeComic'
themePkg = 'comiciviewer'
baseUrl = 'https://takecomic.jp'
overrideVersionCode = 0
isNsfw = false
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,180 @@
package eu.kanade.tachiyomi.extension.ja.takecomic
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import keiyoushi.utils.parseAs
import kotlinx.serialization.Serializable
@Serializable
class ApiResponse(
val series: SeriesData,
)
@Serializable
class SeriesData(
val summary: SeriesSummary,
private val episodes: List<Episode> = emptyList(),
) {
fun toSChapter(accessMap: Map<String, EpisodeAccess>, showLocked: Boolean, showCampaignLocked: Boolean): List<SChapter> {
return this.episodes.mapNotNull { episode ->
val accessInfo = accessMap[episode.id]
val hasAccess = accessInfo?.hasAccess
val isCampaign = accessInfo?.isCampaign
val isLocked = !hasAccess!!
val isCampaignLocked = isLocked && isCampaign!!
if (isCampaignLocked && !showCampaignLocked) {
return@mapNotNull null
}
if (isLocked && !isCampaignLocked && !showLocked) {
return@mapNotNull null
}
SChapter.create().apply {
name = episode.title
date_upload = episode.datePublished * 1000L
when {
isCampaignLocked -> {
name = "\uFE0F $name"
url = "/episodes/${episode.id}#${TakeComic.LOGIN_SUFFIX}"
}
isLocked -> {
name = "🔒 $name"
url = "/episodes/${episode.id}"
}
else -> {
url = "/episodes/${episode.id}"
}
}
}
}
}
}
@Serializable
class SeriesSummary(
private val name: String,
private val description: String,
private val author: List<Author>,
private val images: List<SeriesImage>,
private val tag: List<Tag>,
) {
fun toSManga(seriesHash: String): SManga = SManga.create().apply {
url = "/series/$seriesHash"
title = this@SeriesSummary.name
author = this@SeriesSummary.author.joinToString { it.name }
artist = author
description = try {
this@SeriesSummary.description.parseAs<List<DescriptionNode>>()
.joinToString("\n") { node -> node.children.joinToString { it.text } }
} catch (e: Exception) {
this@SeriesSummary.description
}
genre = this@SeriesSummary.tag.joinToString { it.name }
thumbnail_url = this@SeriesSummary.images.joinToString { it.url }
}
}
@Serializable
class Author(
val name: String,
)
@Serializable
class SeriesImage(
val url: String,
)
@Serializable
class Tag(
val name: String,
)
@Serializable
class Episode(
val id: String,
val title: String,
val datePublished: Long,
)
@Serializable
class DescriptionNode(
val children: List<DescriptionChild>,
)
@Serializable
class DescriptionChild(
val text: String,
)
@Serializable
class AccessApiResponse(
val seriesAccess: SeriesAccess,
)
@Serializable
class SeriesAccess(
val episodeAccesses: List<EpisodeAccess>,
)
@Serializable
class EpisodeAccess(
val episodeId: String,
val hasAccess: Boolean,
val isCampaign: Boolean,
)
@Serializable
class SearchApiResponse(
val searchResult: SearchResult,
)
@Serializable
class SearchResult(
val series: SeriesResult,
)
@Serializable
class SeriesResult(
val total: Int,
val series: List<SearchSeries>,
)
@Serializable
class SearchSeries(
private val id: String,
private val name: String,
private val images: List<SeriesImage>,
) {
fun toSManga(): SManga = SManga.create().apply {
url = "/series/$id"
title = name
thumbnail_url = images.joinToString { it.url }
}
}
@Serializable
class UserInfoApiResponse(
val user: UserData?,
)
@Serializable
class UserData(
val id: String,
)
@Serializable
class EpisodeDetailsApiResponse(
val episode: EpisodeDetails,
)
@Serializable
class EpisodeDetails(
val content: List<EpisodeContent> = emptyList(),
)
@Serializable
class EpisodeContent(
val type: String,
val viewerId: String? = null,
)

View File

@ -0,0 +1,227 @@
package eu.kanade.tachiyomi.extension.ja.takecomic
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.multisrc.comiciviewer.ComiciViewer
import eu.kanade.tachiyomi.multisrc.comiciviewer.ViewerResponse
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.util.asJsoup
import keiyoushi.utils.firstInstance
import keiyoushi.utils.getPreferencesLazy
import keiyoushi.utils.parseAs
import okhttp3.CacheControl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Request
import okhttp3.Response
import kotlin.getValue
class TakeComic : ComiciViewer(
"TakeComic",
"https://takecomic.jp",
"ja",
) {
private val apiUrl = "$baseUrl/api"
private val preferences: SharedPreferences by getPreferencesLazy()
override fun popularMangaParse(response: Response): MangasPage {
return latestUpdatesParse(response)
}
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/series/list/up/$page", headers)
override fun latestUpdatesParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select("div.series-list-item").map { element ->
SManga.create().apply {
setUrlWithoutDomain(element.selectFirst("a.series-list-item-link")!!.attr("href"))
title = element.selectFirst("div.series-list-item-h span")!!.text()
thumbnail_url = element.selectFirst("img.series-list-item-img")?.attr("src")?.let { baseUrl.toHttpUrlOrNull()?.newBuilder(it)?.build()?.queryParameter("url") }
}
}
val hasNextPage = document.selectFirst("a.g-pager-link.mode-active + a.g-pager-link") != null
return MangasPage(mangas, hasNextPage)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isNotBlank()) {
val url = "$apiUrl/search".toHttpUrl().newBuilder()
.addQueryParameter("q", query)
.addQueryParameter("page", page.toString())
.addQueryParameter("size", SEARCH_PAGE_SIZE.toString())
.build()
return GET(url, headers)
}
val filterList = if (filters.isEmpty()) getFilterList() else filters
val browseFilter = filterList.firstInstance<BrowseFilter>()
val path = getFilterOptions()[browseFilter.state].second
val url = if (path == "/ranking/manga") {
"$baseUrl$path"
} else {
"$baseUrl$path/$page"
}
return GET(url, headers)
}
override fun searchMangaParse(response: Response): MangasPage {
val url = response.request.url.toString()
if (url.contains("/api/search")) {
val result = response.parseAs<SearchApiResponse>().searchResult.series
val mangas = result.series.map { it.toSManga() }
val page = response.request.url.queryParameter("page")!!.toInt()
val hasNextPage = result.total > page * SEARCH_PAGE_SIZE
return MangasPage(mangas, hasNextPage)
}
return latestUpdatesParse(response)
}
override fun mangaDetailsParse(response: Response): SManga {
val seriesHash = response.request.url.pathSegments.last()
val apiUrl = "$apiUrl/episodes".toHttpUrl().newBuilder()
.addQueryParameter("seriesHash", seriesHash)
.build()
val apiRequest = GET(apiUrl, headers)
val apiResponse = client.newCall(apiRequest).execute()
return apiResponse.parseAs<ApiResponse>().series.summary.toSManga(seriesHash)
}
override fun chapterListRequest(manga: SManga): Request {
val seriesHash = manga.url.substringAfterLast("/")
val apiUrl = "$apiUrl/episodes".toHttpUrl().newBuilder()
.addQueryParameter("seriesHash", seriesHash)
.addQueryParameter("episodeFrom", "1")
.addQueryParameter("episodeTo", "9999")
.build()
return GET(apiUrl, headers)
}
override fun chapterListParse(response: Response): List<SChapter> {
val apiResponse = response.parseAs<ApiResponse>()
val seriesHash = response.request.url.queryParameter("seriesHash")!!
val accessUrl = "$apiUrl/series/access".toHttpUrl().newBuilder()
.addQueryParameter("seriesHash", seriesHash)
.addQueryParameter("episodeFrom", "1")
.addQueryParameter("episodeTo", "9999")
.build()
val accessRequest = GET(accessUrl, headers, CacheControl.FORCE_NETWORK)
val accessResponse = client.newCall(accessRequest).execute()
val accessMap = accessResponse.parseAs<AccessApiResponse>().seriesAccess.episodeAccesses
.associateBy { it.episodeId }
val showLocked = preferences.getBoolean(SHOW_LOCKED_PREF_KEY, true)
val showCampaignLocked = preferences.getBoolean(SHOW_CAMPAIGN_LOCKED_PREF_KEY, true)
return apiResponse.series.toSChapter(accessMap, showLocked, showCampaignLocked).reversed()
}
override fun pageListRequest(chapter: SChapter): Request {
if (chapter.url.endsWith(LOGIN_SUFFIX)) {
throw Exception("This chapter is free but you need to log in via WebView and refresh the entry")
}
return super.pageListRequest(chapter)
}
override fun pageListParse(response: Response): List<Page> {
val episodeId = response.request.url.pathSegments.last()
var comiciViewerId: String? = null
var memberJwt: String? = null
try {
val apiUrl = "$apiUrl/episodes/$episodeId"
val accessRequest = GET(apiUrl, headers, CacheControl.FORCE_NETWORK)
val apiResponse = client.newCall(accessRequest).execute()
if (apiResponse.isSuccessful) {
comiciViewerId = apiResponse.parseAs<EpisodeDetailsApiResponse>()
.episode.content
.firstOrNull { it.type == "viewer" }?.viewerId
}
} catch (e: Exception) { comiciViewerId = null }
if (comiciViewerId == null) {
val document = response.asJsoup()
val viewer = document.selectFirst("#comici-viewer") ?: throw Exception("Log in via WebView and purchase this chapter to read")
comiciViewerId = viewer.attr("data-comici-viewer-id")
memberJwt = viewer.attr("data-member-jwt")
}
val userId = try {
val userInfoResponse = client.newCall(GET("$apiUrl/user/info", headers)).execute()
userInfoResponse.parseAs<UserInfoApiResponse>().user?.id
} catch (e: Exception) {
memberJwt
}
val requestUrl = "$apiUrl/book/contentsInfo".toHttpUrl().newBuilder()
.addQueryParameter("comici-viewer-id", comiciViewerId)
.addQueryParameter("user-id", userId)
.addQueryParameter("page-from", "0")
val pageTo = client.newCall(GET(requestUrl.addQueryParameter("page-to", "1").build(), headers))
.execute().use { initialResponse ->
if (!initialResponse.isSuccessful) {
throw Exception("Failed to get page list. HTTP ${initialResponse.code}")
}
initialResponse.parseAs<ViewerResponse>().totalPages.toString()
}
val getAllPagesUrl = requestUrl.setQueryParameter("page-to", pageTo).build()
return client.newCall(GET(getAllPagesUrl, headers)).execute().use { allPagesResponse ->
if (allPagesResponse.isSuccessful) {
allPagesResponse.parseAs<ViewerResponse>().result.map { resultItem ->
val urlBuilder = resultItem.imageUrl.toHttpUrl().newBuilder()
if (resultItem.scramble.isNotEmpty()) {
urlBuilder.addQueryParameter("scramble", resultItem.scramble)
}
Page(
index = resultItem.sort,
imageUrl = urlBuilder.build().toString(),
)
}.sortedBy { it.index }
} else {
throw Exception("Failed to get full page list")
}
}
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
super.setupPreferenceScreen(screen)
SwitchPreferenceCompat(screen.context).apply {
key = SHOW_CAMPAIGN_LOCKED_PREF_KEY
title = "Show 'Require Login' chapters"
summary = "Shows chapters that are free but require login"
setDefaultValue(true)
}.also(screen::addPreference)
}
override fun getFilterOptions(): List<Pair<String, String>> = listOf(
Pair("ランキング", "/ranking/manga"),
Pair("更新順", "/series/list/up"),
Pair("新作順", "/series/list/new"),
Pair("完結", "/category/manga/complete"),
Pair("月曜日", "/category/manga/day/1"),
Pair("火曜日", "/category/manga/day/2"),
Pair("水曜日", "/category/manga/day/3"),
Pair("木曜日", "/category/manga/day/4"),
Pair("金曜日", "/category/manga/day/5"),
Pair("土曜日", "/category/manga/day/6"),
Pair("日曜日", "/category/manga/day/7"),
Pair("その他", "/category/manga/day/8"),
)
companion object {
private const val SEARCH_PAGE_SIZE = 24
private const val SHOW_LOCKED_PREF_KEY = "pref_show_locked_chapters"
private const val SHOW_CAMPAIGN_LOCKED_PREF_KEY = "pref_show_campaign_locked_chapters"
const val LOGIN_SUFFIX = "#LOGIN"
}
}

View File

@ -1,10 +0,0 @@
ext {
extName = 'Web Comic Gamma'
extClass = '.WebComicGamma'
themePkg = 'comicgamma'
baseUrl = 'https://webcomicgamma.takeshobo.co.jp'
overrideVersionCode = 0
isNsfw = false
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,5 +0,0 @@
package eu.kanade.tachiyomi.extension.ja.webcomicgamma
import eu.kanade.tachiyomi.multisrc.comicgamma.ComicGamma
class WebComicGamma : ComicGamma("Web Comic Gamma", "https://webcomicgamma.takeshobo.co.jp", "ja")

View File

@ -1,10 +0,0 @@
ext {
extName = 'Web Comic Gamma Plus'
extClass = '.WebComicGammaPlus'
themePkg = 'comicgamma'
baseUrl = 'https://gammaplus.takeshobo.co.jp'
overrideVersionCode = 0
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -1,5 +0,0 @@
package eu.kanade.tachiyomi.extension.ja.webcomicgammaplus
import eu.kanade.tachiyomi.multisrc.comicgamma.ComicGamma
class WebComicGammaPlus : ComicGamma("Web Comic Gamma Plus", "https://gammaplus.takeshobo.co.jp", "ja")