Removed FlameComics Multisrc/Added individuall solution (#6241)
* A Working version * Fix Search Function * updated verionID, added altTitles to search * Cleanup * lint * Fix * Update src/en/flamecomics/build.gradle Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com> * Changed to HttpSource * Changed to api * Cleanup * Fixed getMangaUrl and getChapterUrl * Fix wrong url in db * LINT * lint? --------- Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
This commit is contained in:
parent
b462b1429b
commit
abcea6a91a
|
@ -1,9 +1,7 @@
|
|||
ext {
|
||||
extName = 'Flame Comics'
|
||||
extClass = '.FlameComics'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://flamecomics.xyz'
|
||||
overrideVersionCode = 4
|
||||
extVersionCode = 35
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -4,62 +4,323 @@ import android.graphics.Bitmap
|
|||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
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 kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Protocol
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.jsoup.nodes.Document
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
class FlameComics : MangaThemesia(
|
||||
"Flame Comics",
|
||||
"https://flamecomics.xyz",
|
||||
"en",
|
||||
mangaUrlDirectory = "/series",
|
||||
) {
|
||||
class FlameComics : HttpSource() {
|
||||
override val name = "Flame Comics"
|
||||
override val lang = "en"
|
||||
override val supportsLatest = true
|
||||
override val versionId: Int = 2
|
||||
override val baseUrl = "https://flamecomics.xyz"
|
||||
private val cdn = "https://cdn.flamecomics.xyz"
|
||||
|
||||
// Flame Scans -> Flame Comics
|
||||
override val id = 6350607071566689772
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override val client = super.client.newBuilder()
|
||||
.rateLimit(2, 7)
|
||||
.addInterceptor(::buildIdOutdatedInterceptor)
|
||||
.addInterceptor(::composedImageIntercept)
|
||||
.build()
|
||||
|
||||
override val pageSelector = "div#readerarea img:not(noscript img)[class*=wp-image]"
|
||||
private val removeSpecialCharsregex = Regex("[^A-Za-z0-9 ]")
|
||||
|
||||
// Split Image Fixer Start
|
||||
private val composedSelector: String = "#readerarea div.figure_container div.composed_figure"
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val hasSplitImages = document
|
||||
.select(composedSelector)
|
||||
.firstOrNull() != null
|
||||
|
||||
if (!hasSplitImages) {
|
||||
return super.pageListParse(document)
|
||||
private fun dataApiReqBuilder() = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("_next")
|
||||
addPathSegment("data")
|
||||
addPathSegment(buildId)
|
||||
}
|
||||
|
||||
return document.select("#readerarea p:has(img), $composedSelector").toList()
|
||||
.filter {
|
||||
it.select("img").all { imgEl ->
|
||||
imgEl.attr("abs:src").isNullOrEmpty().not()
|
||||
private fun imageApiUrlBuilder(dataUrl: String) = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("_next")
|
||||
addPathSegment("image")
|
||||
}.build().toString() + "?url=$dataUrl"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request =
|
||||
GET(
|
||||
dataApiReqBuilder().apply {
|
||||
addPathSegment("browse.json")
|
||||
fragment("$page&${removeSpecialCharsregex.replace(query.lowercase(), "")}")
|
||||
}.build(),
|
||||
headers,
|
||||
)
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET(
|
||||
dataApiReqBuilder().apply {
|
||||
addPathSegment("browse.json")
|
||||
fragment("$page")
|
||||
}.build(),
|
||||
headers,
|
||||
)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET(
|
||||
dataApiReqBuilder().apply {
|
||||
addPathSegment("index.json")
|
||||
}.build(),
|
||||
headers,
|
||||
)
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage =
|
||||
mangaParse(response) { seriesList ->
|
||||
val query = response.request.url.fragment!!.split("&")[1]
|
||||
seriesList.filter { series ->
|
||||
val titles = mutableListOf(series.title)
|
||||
if (series.altTitles != null) {
|
||||
titles += json.decodeFromString<List<String>>(series.altTitles)
|
||||
}
|
||||
titles.any { title ->
|
||||
removeSpecialCharsregex.replace(
|
||||
query.lowercase(),
|
||||
"",
|
||||
) in removeSpecialCharsregex.replace(
|
||||
title.lowercase(),
|
||||
"",
|
||||
)
|
||||
}
|
||||
}
|
||||
.mapIndexed { i, el ->
|
||||
if (el.tagName() == "p") {
|
||||
Page(i, "", el.select("img").attr("abs:src"))
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val latestData = json.decodeFromString<LatestPageData>(response.body.string())
|
||||
return MangasPage(
|
||||
latestData.pageProps.latestEntries.blocks[0].series.map { seriesData ->
|
||||
SManga.create().apply {
|
||||
title = seriesData.title
|
||||
setUrlWithoutDomain(
|
||||
dataApiReqBuilder().apply {
|
||||
val seriesID =
|
||||
seriesData.series_id
|
||||
addPathSegment("series")
|
||||
addPathSegment("$seriesID.json")
|
||||
addQueryParameter("id", seriesData.series_id.toString())
|
||||
}.build().toString(),
|
||||
)
|
||||
thumbnail_url = imageApiUrlBuilder(
|
||||
cdn.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("series")
|
||||
addPathSegment(seriesData.series_id.toString())
|
||||
addPathSegment(seriesData.cover)
|
||||
}.build()
|
||||
.toString() + "&w=640&q=75", // for some reason they don`t include the ?
|
||||
)
|
||||
}
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage =
|
||||
mangaParse(response) { list -> list.sortedByDescending { it.views } }
|
||||
|
||||
private fun mangaParse(
|
||||
response: Response,
|
||||
transform: (List<Series>) -> List<Series>,
|
||||
): MangasPage {
|
||||
val searchedSeriesData =
|
||||
json.decodeFromString<SearchPageData>(response.body.string()).pageProps.series
|
||||
|
||||
val page = if (!response.request.url.fragment?.contains("&")!!) {
|
||||
response.request.url.fragment!!.toInt()
|
||||
} else {
|
||||
val imageUrls = el.select("img")
|
||||
.joinToString("|") { it.attr("abs:src") }
|
||||
response.request.url.fragment!!.split("&")[0].toInt()
|
||||
}
|
||||
|
||||
Page(i, document.location(), imageUrls + COMPOSED_SUFFIX)
|
||||
val manga = transform(searchedSeriesData).map { seriesData ->
|
||||
SManga.create().apply {
|
||||
title = seriesData.title
|
||||
setUrlWithoutDomain(
|
||||
baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("series")
|
||||
addPathSegment(seriesData.series_id.toString())
|
||||
}.build().toString(),
|
||||
)
|
||||
thumbnail_url = imageApiUrlBuilder(
|
||||
cdn.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("series")
|
||||
addPathSegment(seriesData.series_id.toString())
|
||||
addPathSegment(seriesData.cover)
|
||||
}.build()
|
||||
.toString() + "&w=640&q=75", // for some reason they don`t include the ?
|
||||
)
|
||||
}
|
||||
}
|
||||
var lastPage = page * 20
|
||||
if (lastPage > manga.size) {
|
||||
lastPage = manga.size
|
||||
}
|
||||
if (lastPage < 0) lastPage = 0
|
||||
return MangasPage(manga.subList((page - 1) * 20, lastPage), lastPage < manga.size)
|
||||
}
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request = GET(
|
||||
dataApiReqBuilder().apply {
|
||||
val seriesID =
|
||||
("$baseUrl/${manga.url}").toHttpUrl().pathSegments.last()
|
||||
addPathSegment("series")
|
||||
addPathSegment("$seriesID.json")
|
||||
addQueryParameter("id", seriesID)
|
||||
}.build(),
|
||||
headers,
|
||||
)
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request = mangaDetailsRequest(manga)
|
||||
|
||||
override fun getMangaUrl(manga: SManga): String = "$baseUrl/${manga.url}"
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply {
|
||||
val seriesData =
|
||||
json.decodeFromString<MangaPageData>(response.body.string()).pageProps.series
|
||||
title = seriesData.title
|
||||
thumbnail_url = imageApiUrlBuilder(
|
||||
cdn.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("series")
|
||||
addPathSegment(seriesData.series_id.toString())
|
||||
addPathSegment(seriesData.cover)
|
||||
}.build().toString() + "&w=640&q=75",
|
||||
)
|
||||
description = seriesData.description
|
||||
author = seriesData.author
|
||||
status = when (seriesData.status.lowercase()) {
|
||||
"ongoing" -> SManga.ONGOING
|
||||
"dropped" -> SManga.CANCELLED
|
||||
"hiatus" -> SManga.ON_HIATUS
|
||||
"completed" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val mangaPageData = json.decodeFromString<MangaPageData>(response.body.string())
|
||||
return mangaPageData.pageProps.chapters.map { chapter ->
|
||||
SChapter.create().apply {
|
||||
setUrlWithoutDomain(
|
||||
baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("series")
|
||||
addPathSegment(chapter.series_id.toString())
|
||||
addPathSegment(chapter.token)
|
||||
}.build().toString(),
|
||||
)
|
||||
chapter_number = chapter.chapter.toFloat()
|
||||
date_upload = chapter.release_date * 1000
|
||||
name = buildString {
|
||||
append("Chapter ${chapter.chapter.toInt()} ")
|
||||
append(chapter.title ?: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request = GET(
|
||||
dataApiReqBuilder().apply {
|
||||
val seriesID = ("$baseUrl/${chapter.url}").toHttpUrl().pathSegments[2]
|
||||
val token = ("$baseUrl/${chapter.url}").toHttpUrl().pathSegments[3]
|
||||
addPathSegment("series")
|
||||
addPathSegment(seriesID)
|
||||
addPathSegment("$token.json")
|
||||
addQueryParameter("id", seriesID)
|
||||
addQueryParameter("token", token)
|
||||
}.build(),
|
||||
headers,
|
||||
)
|
||||
|
||||
override fun getChapterUrl(chapter: SChapter): String = "$baseUrl/${chapter.url}"
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val chapter =
|
||||
json.decodeFromString<ChapterPageData>(response.body.string()).pageProps.chapter
|
||||
return chapter.images.mapIndexed { idx, page ->
|
||||
Page(
|
||||
idx,
|
||||
imageUrl = imageApiUrlBuilder(
|
||||
cdn.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("series")
|
||||
addPathSegment(chapter.series_id.toString())
|
||||
addPathSegment(chapter.token)
|
||||
addPathSegment(page.name)
|
||||
addQueryParameter(
|
||||
chapter.release_date.toString(),
|
||||
value = null,
|
||||
)
|
||||
addQueryParameter("w", "1920")
|
||||
addQueryParameter("q", "100")
|
||||
}.build().toString(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String = ""
|
||||
|
||||
private fun fetchBuildId(document: Document? = null): String {
|
||||
val realDocument = document
|
||||
?: client.newCall(GET(baseUrl, headers)).execute().use { it.asJsoup() }
|
||||
|
||||
val nextData = realDocument.selectFirst("script#__NEXT_DATA__")?.data()
|
||||
?: throw Exception("Failed to find __NEXT_DATA__")
|
||||
|
||||
val dto = json.decodeFromString<NewBuildID>(nextData)
|
||||
return dto.buildId
|
||||
}
|
||||
|
||||
private var buildId = ""
|
||||
get() {
|
||||
if (field == "") {
|
||||
field = fetchBuildId()
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
private fun buildIdOutdatedInterceptor(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val response = chain.proceed(request)
|
||||
|
||||
if (
|
||||
response.code == 404 &&
|
||||
request.url.run {
|
||||
host == baseUrl.removePrefix("https://") &&
|
||||
pathSegments.getOrNull(0) == "_next" &&
|
||||
pathSegments.getOrNull(1) == "data" &&
|
||||
fragment != "DO_NOT_RETRY"
|
||||
} &&
|
||||
response.header("Content-Type")?.contains("text/html") != false
|
||||
) {
|
||||
// The 404 page should have the current buildId
|
||||
val document = response.asJsoup()
|
||||
buildId = fetchBuildId(document)
|
||||
|
||||
// Redo request with new buildId
|
||||
val url = request.url.newBuilder()
|
||||
.setPathSegment(2, buildId)
|
||||
.fragment("DO_NOT_RETRY")
|
||||
.build()
|
||||
val newRequest = request.newBuilder()
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
return chain.proceed(newRequest)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
private fun composedImageIntercept(chain: Interceptor.Chain): Response {
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
package eu.kanade.tachiyomi.extension.en.flamecomics
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.builtins.MapSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
@Serializable
|
||||
class NewBuildID(
|
||||
val buildId: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class MangaPageData(
|
||||
val pageProps: PageProps,
|
||||
) {
|
||||
@Serializable
|
||||
class PageProps(
|
||||
val chapters: List<Chapter>,
|
||||
val series: Series,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class SearchPageData(
|
||||
val pageProps: PageProps,
|
||||
) {
|
||||
@Serializable
|
||||
class PageProps(
|
||||
val series: List<Series>,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class LatestPageData(
|
||||
val pageProps: PageProps,
|
||||
) {
|
||||
@Serializable
|
||||
class PageProps(
|
||||
val latestEntries: LatestEntries,
|
||||
) {
|
||||
@Serializable
|
||||
class LatestEntries(
|
||||
val blocks: List<Block>,
|
||||
) {
|
||||
@Serializable
|
||||
class Block(
|
||||
val series: List<Series>,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ChapterPageData(
|
||||
val pageProps: PageProps,
|
||||
) {
|
||||
@Serializable
|
||||
class PageProps(
|
||||
val chapter: Chapter,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class Series(
|
||||
val title: String,
|
||||
val altTitles: String?,
|
||||
val description: String,
|
||||
val cover: String,
|
||||
val author: String?,
|
||||
val status: String,
|
||||
val series_id: Int,
|
||||
val views: Int?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Chapter(
|
||||
val chapter: Double,
|
||||
val title: String?,
|
||||
val release_date: Long,
|
||||
val series_id: Int,
|
||||
val token: String,
|
||||
@Serializable(with = KeysToListSerializer::class)
|
||||
val images: List<Page>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Page(
|
||||
val name: String,
|
||||
)
|
||||
|
||||
class KeysToListSerializer : KSerializer<List<Page>> {
|
||||
private val listSer = MapSerializer(String.serializer(), Page.serializer())
|
||||
override val descriptor: SerialDescriptor = listSer.descriptor
|
||||
override fun deserialize(decoder: Decoder): List<Page> {
|
||||
return listSer.deserialize(decoder).flatMap { k -> listOf(k.value) }
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: List<Page>) {}
|
||||
}
|
Loading…
Reference in New Issue