Add Web Comic Gamma and Plus (#11982)
17
.run/ComicGammaGenerator.run.xml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="ComicGammaGenerator" type="JetRunConfigurationType" nameIsGenerated="true">
|
||||||
|
<module name="tachiyomi-extensions.multisrc" />
|
||||||
|
<option name="VM_PARAMETERS" value="" />
|
||||||
|
<option name="PROGRAM_PARAMETERS" value="" />
|
||||||
|
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||||
|
<option name="ALTERNATIVE_JRE_PATH" />
|
||||||
|
<option name="PASS_PARENT_ENVS" value="true" />
|
||||||
|
<option name="MAIN_CLASS_NAME" value="eu.kanade.tachiyomi.multisrc.comicgamma.ComicGammaGenerator" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Make" enabled="true" />
|
||||||
|
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktFormat" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="" />
|
||||||
|
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktLint" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
After Width: | Height: | Size: 8.1 KiB |
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 216 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 96 KiB |
@ -0,0 +1,100 @@
|
|||||||
|
package eu.kanade.tachiyomi.multisrc.comicgamma
|
||||||
|
|
||||||
|
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.ParsedHttpSource
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import rx.Observable
|
||||||
|
import java.text.DateFormat.getDateInstance
|
||||||
|
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 = true
|
||||||
|
|
||||||
|
override val client = network.client.newBuilder().addInterceptor(PtImgInterceptor()).build()
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int) = GET("$baseUrl/manga/", headers)
|
||||||
|
override fun popularMangaNextPageSelector(): String? = null
|
||||||
|
override fun popularMangaSelector() = ".work_list li"
|
||||||
|
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
||||||
|
setUrlWithoutDomain(element.select("a").attr("abs:href"))
|
||||||
|
val image = element.select("img")
|
||||||
|
title = image.attr("alt").removePrefix("『").removeSuffix("』作品ページへ")
|
||||||
|
thumbnail_url = image.attr("abs:src")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/", headers)
|
||||||
|
override fun latestUpdatesNextPageSelector(): String? = null
|
||||||
|
override fun latestUpdatesSelector() = ".whatsnew li:contains(読む)"
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element) = SManga.create().apply {
|
||||||
|
val url = element.select(".show_detail").attr("abs:href")
|
||||||
|
setUrlWithoutDomain(url)
|
||||||
|
title = element.select("h3").textNodes()[0].text()
|
||||||
|
thumbnail_url = url + "main.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
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("Not used.")
|
||||||
|
override fun searchMangaSelector() = throw UnsupportedOperationException("Not used.")
|
||||||
|
override fun searchMangaFromElement(element: Element) = throw UnsupportedOperationException("Not used.")
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||||
|
throw UnsupportedOperationException("Not used.")
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||||
|
title = document.select("h1").text()
|
||||||
|
author = document.select(".author").text()
|
||||||
|
description = document.select(".work_sammary").text()
|
||||||
|
val nextDate = document.select(".episode_caption:contains(【次回更新】)")
|
||||||
|
if (nextDate.isNotEmpty()) {
|
||||||
|
val dateStr = nextDate.textNodes()
|
||||||
|
.filter { it.text().contains("【次回更新】") }[0]
|
||||||
|
.text().trim().removePrefix("【次回更新】")
|
||||||
|
val localDate = JST_FORMAT.parse(dateStr)?.let { LOCAL_FORMAT.format(it) }
|
||||||
|
if (localDate != null) description = "【Next/Repeat: $localDate】\n$description"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() =
|
||||||
|
".box_episode > .box_episode_L:contains(読む), .box_episode > .box_episode_M:contains(読む)" // filter out purchase links
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||||
|
val url = element.select("a[id^=read]").attr("abs:href")
|
||||||
|
setUrlWithoutDomain(url)
|
||||||
|
val chapterNumber = url.removeSuffix("/").substringAfterLast('/').replace('_', '.')
|
||||||
|
val title = element.select(".episode_title").textNodes().filterNot {
|
||||||
|
it.text().contains("集中連載") || it.text().contains("配信中!!")
|
||||||
|
}.joinToString("/") { it.text() }
|
||||||
|
name = "$chapterNumber $title"
|
||||||
|
val date = element.select(".episode_caption").textNodes().firstOrNull { it.text().contains("【公開日】") }
|
||||||
|
?.text()?.trim()?.removePrefix("【公開日】")
|
||||||
|
date_upload = date?.let { JST_FORMAT.parse(it)?.time } ?: -1L // hide unknown ones
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document) =
|
||||||
|
document.select("#content > div[data-ptimg]").mapIndexed { i, e ->
|
||||||
|
Page(i, imageUrl = e.attr("abs:data-ptimg"))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not used.")
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val JST_FORMAT by lazy {
|
||||||
|
SimpleDateFormat("yyyy年M月dd日(E)", Locale.JAPANESE).apply { timeZone = TimeZone.getTimeZone("JST") }
|
||||||
|
}
|
||||||
|
private val LOCAL_FORMAT by lazy { getDateInstance() }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
package eu.kanade.tachiyomi.multisrc.comicgamma
|
||||||
|
|
||||||
|
import generator.ThemeSourceData.SingleLang
|
||||||
|
import generator.ThemeSourceGenerator
|
||||||
|
|
||||||
|
class ComicGammaGenerator : ThemeSourceGenerator {
|
||||||
|
override val themeClass = "ComicGamma"
|
||||||
|
override val themePkg = "comicgamma"
|
||||||
|
override val baseVersionCode = 1
|
||||||
|
override val sources = listOf(
|
||||||
|
SingleLang(
|
||||||
|
name = "Web Comic Gamma",
|
||||||
|
baseUrl = "https://webcomicgamma.takeshobo.co.jp",
|
||||||
|
lang = "ja",
|
||||||
|
isNsfw = false,
|
||||||
|
className = "WebComicGamma",
|
||||||
|
pkgName = "webcomicgamma",
|
||||||
|
sourceName = "Web Comic Gamma",
|
||||||
|
overrideVersionCode = 0,
|
||||||
|
),
|
||||||
|
SingleLang(
|
||||||
|
name = "Web Comic Gamma Plus",
|
||||||
|
baseUrl = "https://gammaplus.takeshobo.co.jp",
|
||||||
|
lang = "ja",
|
||||||
|
isNsfw = true,
|
||||||
|
className = "WebComicGammaPlus",
|
||||||
|
pkgName = "webcomicgammaplus",
|
||||||
|
sourceName = "Web Comic Gamma Plus",
|
||||||
|
overrideVersionCode = 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
ComicGammaGenerator().createAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
package eu.kanade.tachiyomi.multisrc.comicgamma
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
val COORD_REGEX = Regex("""^i:(\d+),(\d+)\+(\d+),(\d+)>(\d+),(\d+)$""")
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PtImg(val resources: Resource, val views: List<View>) {
|
||||||
|
fun getFilename() = resources.i.src
|
||||||
|
fun getViewSize() = Pair(views[0].width, views[0].height)
|
||||||
|
fun getTranslations() = views[0].coords.map(::toTranslation)
|
||||||
|
|
||||||
|
private fun toTranslation(coord: String): Translation {
|
||||||
|
val v = COORD_REGEX.matchEntire(coord)!!.destructured.toList().map(String::toInt)
|
||||||
|
return Translation(v[0], v[1], v[2], v[3], v[4], v[5])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Resource(val i: Image)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Image(val src: String, val width: Int, val height: Int)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class View(val width: Int, val height: Int, val coords: List<String>)
|
||||||
|
|
||||||
|
data class Translation(val ix: Int, val iy: Int, val w: Int, val h: Int, val vx: Int, val vy: Int)
|
@ -0,0 +1,46 @@
|
|||||||
|
package eu.kanade.tachiyomi.multisrc.comicgamma
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Rect
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
|
class PtImgInterceptor : Interceptor {
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val request = chain.request()
|
||||||
|
val response = chain.proceed(request)
|
||||||
|
val url = request.url.toString()
|
||||||
|
if (!url.endsWith(".ptimg.json")) return response
|
||||||
|
|
||||||
|
val metadata = json.decodeFromString<PtImg>(response.body!!.string())
|
||||||
|
val imgRequest = request.newBuilder()
|
||||||
|
.url(url.replaceAfterLast('/', metadata.getFilename())).build()
|
||||||
|
val imgResponse = chain.proceed(imgRequest)
|
||||||
|
val image = BitmapFactory.decodeStream(imgResponse.body!!.byteStream())
|
||||||
|
val (width, height) = metadata.getViewSize()
|
||||||
|
val result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||||
|
val view = Canvas(result)
|
||||||
|
|
||||||
|
metadata.getTranslations().forEach {
|
||||||
|
val src = Rect(it.ix, it.iy, it.ix + it.w, it.iy + it.h)
|
||||||
|
val dst = Rect(it.vx, it.vy, it.vx + it.w, it.vy + it.h)
|
||||||
|
view.drawBitmap(image, src, dst, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
val output = ByteArrayOutputStream()
|
||||||
|
result.compress(Bitmap.CompressFormat.JPEG, 90, output)
|
||||||
|
val responseBody = output.toByteArray().toResponseBody("image/jpeg".toMediaType())
|
||||||
|
return imgResponse.newBuilder().body(responseBody).build()
|
||||||
|
}
|
||||||
|
}
|