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:
Creepler13 2024-11-28 18:25:03 +01:00 committed by Draff
parent b462b1429b
commit abcea6a91a
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
3 changed files with 395 additions and 33 deletions

View File

@ -1,9 +1,7 @@
ext { ext {
extName = 'Flame Comics' extName = 'Flame Comics'
extClass = '.FlameComics' extClass = '.FlameComics'
themePkg = 'mangathemesia' extVersionCode = 35
baseUrl = 'https://flamecomics.xyz'
overrideVersionCode = 4
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -4,62 +4,323 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Rect 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.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.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.Interceptor
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Protocol import okhttp3.Protocol
import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody import okhttp3.ResponseBody.Companion.toResponseBody
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import uy.kohesive.injekt.injectLazy
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
class FlameComics : MangaThemesia( class FlameComics : HttpSource() {
"Flame Comics", override val name = "Flame Comics"
"https://flamecomics.xyz", override val lang = "en"
"en", override val supportsLatest = true
mangaUrlDirectory = "/series", override val versionId: Int = 2
) { override val baseUrl = "https://flamecomics.xyz"
private val cdn = "https://cdn.flamecomics.xyz"
// Flame Scans -> Flame Comics private val json: Json by injectLazy()
override val id = 6350607071566689772
override val client = super.client.newBuilder() override val client = super.client.newBuilder()
.rateLimit(2, 7) .rateLimit(2, 7)
.addInterceptor(::buildIdOutdatedInterceptor)
.addInterceptor(::composedImageIntercept) .addInterceptor(::composedImageIntercept)
.build() .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 fun dataApiReqBuilder() = baseUrl.toHttpUrl().newBuilder().apply {
private val composedSelector: String = "#readerarea div.figure_container div.composed_figure" addPathSegment("_next")
addPathSegment("data")
override fun pageListParse(document: Document): List<Page> { addPathSegment(buildId)
val hasSplitImages = document
.select(composedSelector)
.firstOrNull() != null
if (!hasSplitImages) {
return super.pageListParse(document)
} }
return document.select("#readerarea p:has(img), $composedSelector").toList() private fun imageApiUrlBuilder(dataUrl: String) = baseUrl.toHttpUrl().newBuilder().apply {
.filter { addPathSegment("_next")
it.select("img").all { imgEl -> addPathSegment("image")
imgEl.attr("abs:src").isNullOrEmpty().not() }.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 { } else {
val imageUrls = el.select("img") response.request.url.fragment!!.split("&")[0].toInt()
.joinToString("|") { it.attr("abs:src") } }
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 { private fun composedImageIntercept(chain: Interceptor.Chain): Response {

View File

@ -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>) {}
}