@ -1,7 +1,10 @@
|
||||
ext {
|
||||
extName = 'Manga Cross'
|
||||
extClass = '.MangaCross'
|
||||
extVersionCode = 5
|
||||
extName = "Champion Cross"
|
||||
extClass = ".MangaCross"
|
||||
themePkg = 'comiciviewer'
|
||||
baseUrl = "https://championcross.jp"
|
||||
overrideVersionCode = 5
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
@ -1,115 +1,12 @@
|
||||
package eu.kanade.tachiyomi.extension.ja.mangacross
|
||||
|
||||
import android.util.Log
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
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.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import kotlin.concurrent.thread
|
||||
import eu.kanade.tachiyomi.multisrc.comiciviewer.ComiciViewer
|
||||
|
||||
class MangaCross : HttpSource() {
|
||||
override val name = "Manga Cross"
|
||||
override val lang = "ja"
|
||||
override val baseUrl = "https://mangacross.jp"
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
// Pagination does not work. 9999 is a dummy large number.
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/api/comics.json?count=9999", headers)
|
||||
override fun popularMangaParse(response: Response) =
|
||||
MangasPage(response.parseAs<MCComicList>().comics.map { it.toSManga() }, false)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/api/episodes.json?page=$page", headers)
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val result: MCEpisodeList = response.parseAs()
|
||||
return MangasPage(result.episodes.map { it.comic!!.toSManga() }, result.current_page < result.total_pages)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||
if (query.isNotEmpty()) {
|
||||
GET("$baseUrl/api/comics/keywords/$query.json", headers)
|
||||
} else {
|
||||
when (val tag = filters.filterIsInstance<TagFilter>().firstOrNull()?.getTag()) {
|
||||
null -> popularMangaRequest(page)
|
||||
is MCComicCategory -> GET("$baseUrl/api/comics/categories/${tag.name}.json", headers)
|
||||
is MCComicGenre -> GET("$baseUrl/api/comics/tags/${tag.name}.json", headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response) = popularMangaParse(response)
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> =
|
||||
client.newCall(chapterListRequest(manga)).asObservableSuccess()
|
||||
.map { mangaDetailsParse(it).apply { initialized = true } }
|
||||
|
||||
// mangaDetailsRequest untouched in order to let WebView open web page instead of json
|
||||
|
||||
override fun mangaDetailsParse(response: Response) = response.parseAs<MCComicDetails>().comic.toSManga()
|
||||
|
||||
override fun chapterListRequest(manga: SManga) = GET("$baseUrl/api${manga.url}.json", headers)
|
||||
|
||||
override fun chapterListParse(response: Response) = response.parseAs<MCComicDetails>().comic.toSChapterList()
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
return try {
|
||||
response.parseAs<MCViewer>().episode_pages.mapIndexed { i, it ->
|
||||
Page(i, imageUrl = it.image.original_url)
|
||||
}
|
||||
} catch (e: SerializationException) {
|
||||
throw Exception("Chapter is no longer available!")
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
private lateinit var tags: List<Pair<String, MCComicTag?>>
|
||||
private var isFetchingTags = false
|
||||
|
||||
private fun fetchTags() {
|
||||
if (isFetchingTags) return
|
||||
isFetchingTags = true
|
||||
thread {
|
||||
try {
|
||||
val response = client.newCall(GET("$baseUrl/api/menus.json", headers)).execute()
|
||||
val filterList = response.parseAs<MCMenu>().toFilterList()
|
||||
tags = listOf(Pair("All", null)) + filterList
|
||||
} catch (e: Exception) {
|
||||
Log.e("MangaCross", "Failed to fetch filters ($e)")
|
||||
} finally {
|
||||
isFetchingTags = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFilterList() =
|
||||
if (::tags.isInitialized) {
|
||||
FilterList(
|
||||
Filter.Header("NOTE: Ignored if using text search!"),
|
||||
TagFilter("Tag", tags),
|
||||
)
|
||||
} else {
|
||||
fetchTags()
|
||||
FilterList(
|
||||
Filter.Header("Fetching tags..."),
|
||||
Filter.Header("Go back to previous screen and retry."),
|
||||
)
|
||||
}
|
||||
|
||||
private class TagFilter(name: String, private val tags: List<Pair<String, MCComicTag?>>) :
|
||||
Filter.Select<String>(name, tags.map { it.first }.toTypedArray()) {
|
||||
fun getTag() = tags[state].second
|
||||
}
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T = json.decodeFromStream(this.body.byteStream())
|
||||
// MangaCross became ChampionCross
|
||||
class MangaCross : ComiciViewer(
|
||||
"Champion Cross",
|
||||
"https://championcross.jp",
|
||||
"ja",
|
||||
) {
|
||||
override val versionId = 2
|
||||
}
|
||||
|
||||
@ -1,185 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.ja.mangacross
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jsoup.Jsoup
|
||||
import java.text.DateFormat.getDateTimeInstance
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
@Serializable
|
||||
data class MCComicList(
|
||||
val comics: List<MCComic>,
|
||||
// Note: Pagination does not work. Pages after 1 return nothing.
|
||||
// val current_count: Int,
|
||||
// val current_page: Int?,
|
||||
// val total_count: Int,
|
||||
// val total_pages: Int,
|
||||
)
|
||||
|
||||
// Useless fields are omitted while interesting ones are commented
|
||||
@Serializable
|
||||
data class MCComic(
|
||||
val dir_name: String,
|
||||
val title: String,
|
||||
val author: String,
|
||||
val comic_category: MCComicCategory? = null,
|
||||
val comic_tags: List<MCComicGenre>,
|
||||
val image_double_url: String, // horizontal
|
||||
val list_image_double_url: String, // square
|
||||
// val restricted: Boolean, // is NSFW
|
||||
// Details below
|
||||
val outline: String? = null,
|
||||
val episodes: List<MCEpisode>? = null,
|
||||
// val books: List<MCBook>? = null,
|
||||
) {
|
||||
fun toSManga() = SManga.create().apply {
|
||||
url = "/comics/$dir_name"
|
||||
title = this@MCComic.title
|
||||
author = this@MCComic.author
|
||||
description = getDescription()
|
||||
genre = getGenre()
|
||||
status = getStatus()
|
||||
thumbnail_url = list_image_double_url
|
||||
}
|
||||
|
||||
fun toSChapterList() = episodes!!
|
||||
// .filter { it.status == "public" } // preserve private chapters in case user downloaded before
|
||||
.map { it.toSChapter("/comics/$dir_name") }
|
||||
|
||||
private fun getDescription() = listOfNotNull(
|
||||
episodes?.firstOrNull()?.getNextDatePrefix(),
|
||||
outline?.stripHtml(),
|
||||
).joinToString("\n")
|
||||
|
||||
private fun getGenre() = listOfNotNull(
|
||||
comic_category?.display_name,
|
||||
comic_tags.joinToString(", ") { it.name },
|
||||
).joinToString(", ")
|
||||
|
||||
private fun getStatus() = when {
|
||||
episodes?.firstOrNull()?.episode_next_date.isNullOrEmpty() -> SManga.UNKNOWN
|
||||
else -> SManga.ONGOING
|
||||
}
|
||||
|
||||
private fun String.stripHtml() = Jsoup.parseBodyFragment(this).text()
|
||||
}
|
||||
|
||||
sealed class MCComicTag
|
||||
|
||||
@Serializable
|
||||
data class MCComicCategory(val name: String, val display_name: String) : MCComicTag()
|
||||
|
||||
@Serializable
|
||||
data class MCComicGenre(val name: String) : MCComicTag()
|
||||
|
||||
@Serializable
|
||||
data class MCComicDetails(val comic: MCComic)
|
||||
|
||||
@Serializable
|
||||
data class MCEpisodeList(
|
||||
val episodes: List<MCEpisode>,
|
||||
// Note: Pagination works.
|
||||
val current_count: Int,
|
||||
val current_page: Int,
|
||||
val total_count: Int,
|
||||
val total_pages: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MCEpisode(
|
||||
// val id: Long,
|
||||
val volume: String,
|
||||
val sort_volume: Int,
|
||||
val title: String,
|
||||
val publish_start: String, // all dates are in ISO time format
|
||||
val publish_end: String?,
|
||||
// Note: AFAIK these dates are always identical to those above.
|
||||
// val member_publish_start: String,
|
||||
// val member_publish_end: String?,
|
||||
val status: String, // public or private
|
||||
// val page_url: String,
|
||||
// val list_image_double_url: String,
|
||||
val episode_next_date: String?,
|
||||
val next_date_customize_text: String?,
|
||||
val comic: MCComic? = null, // in latest
|
||||
) {
|
||||
fun toSChapter(urlPrefix: String) = SChapter.create().apply {
|
||||
url = "$urlPrefix/$sort_volume/viewer.json"
|
||||
val prefix = if (status == "public") "" else "🔒 "
|
||||
name = "$prefix$volume $title"
|
||||
// milliseconds are always 000
|
||||
date_upload = JST_FORMAT_LIST.parseJST(publish_start)!!.time
|
||||
// show end date in scanlator field
|
||||
scanlator = publish_end?.let { "~" + LOCAL_FORMAT_LIST.format(JST_FORMAT_LIST.parseJST(it)!!) }
|
||||
}
|
||||
|
||||
fun getNextDatePrefix(): String? = when {
|
||||
!episode_next_date.isNullOrEmpty() -> {
|
||||
val date = JST_FORMAT_DESC.parseJST(episode_next_date)!!.apply {
|
||||
time += 10 * 3600 * 1000 // 10 am JST
|
||||
}
|
||||
"【Next: ${LOCAL_FORMAT_DESC.format(date)}】"
|
||||
}
|
||||
!next_date_customize_text.isNullOrEmpty() -> "【$next_date_customize_text】"
|
||||
else -> null
|
||||
}
|
||||
|
||||
companion object {
|
||||
// for thread-safety
|
||||
private val JST_FORMAT_DESC = getJSTFormat()
|
||||
private val JST_FORMAT_LIST = getJSTFormat()
|
||||
private val LOCAL_FORMAT_DESC = getDateTimeInstance()
|
||||
private val LOCAL_FORMAT_LIST = getDateTimeInstance()
|
||||
|
||||
private fun SimpleDateFormat.parseJST(date: String) = parse(date.removeSuffix("+09:00"))
|
||||
private fun getJSTFormat() =
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.ENGLISH).apply {
|
||||
timeZone = TimeZone.getTimeZone("GMT+09:00")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class MCBook(
|
||||
val cover_url: String, // resolution is too low
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MCViewer(
|
||||
// val sort_volume: Int,
|
||||
// val created_at: String,
|
||||
// val updated_at: String,
|
||||
// val volume: String,
|
||||
// val title: String,
|
||||
// val page_count: Int,
|
||||
// episode_viewer_setting: { page_direction: "horizontal" }
|
||||
val episode_pages: List<MCEpisodePage>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MCEpisodePage(
|
||||
// val order_index: Int,
|
||||
val image: MCImage,
|
||||
// val is_spread_start_page: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MCImage(
|
||||
// val pc_url: String, // has highest resolution but is upscaled from original, thus unnecessary
|
||||
// val sp_url: String,
|
||||
// val thumbnail_url: String,
|
||||
val original_url: String,
|
||||
// {pc,sp,thumbnail,original}_geometry: { width: Int, height: Int }
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MCMenu(
|
||||
val comic_categories: List<MCComicCategory>,
|
||||
val comic_tags: List<MCComicGenre>,
|
||||
) {
|
||||
fun toFilterList(): List<Pair<String, MCComicTag>> =
|
||||
comic_categories.map { Pair(it.display_name, it) } + comic_tags.map { Pair(it.name, it) }
|
||||
}
|
||||