Fix DisasterScans (#7673)

* Fix DisasterScans

* lint

* requested changes

* changes

* idk
This commit is contained in:
Creepler13 2025-02-21 16:21:08 +01:00 committed by Draff
parent 0431f55f9b
commit 6149261e7b
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
5 changed files with 102 additions and 335 deletions

View File

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".en.disasterscans.DisasterScansUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="disasterscans.com" />
<data android:scheme="https" />
<data android:pathPattern="/comics/..*" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,7 +1,7 @@
ext {
extName = 'Disaster Scans'
extClass = '.DisasterScans'
extVersionCode = 32
extVersionCode = 33
}
apply from: "$rootDir/common.gradle"

View File

@ -1,209 +1,129 @@
package eu.kanade.tachiyomi.extension.en.disasterscans
import android.app.Application
import android.content.SharedPreferences
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
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.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class DisasterScans : HttpSource() {
class DisasterScans : ParsedHttpSource() {
override val name = "Disaster Scans"
override val lang = "en"
override val versionId = 2
override val versionId = 3
override val baseUrl = "https://disasterscans.com"
override val supportsLatest = true
private val apiUrl = "https://api.disasterscans.com"
private val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US)
private val json by injectLazy<Json>()
override val supportsLatest = false
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.addInterceptor { chain ->
val request = chain.request()
val url = request.url
if (url.fragment == "thumbnail") {
val cdnUrl = preferences.getCdnUrl()
val requestUrl = url.toString().substringBefore("=") + "="
if (cdnUrl != requestUrl) {
val fileId = url.queryParameterValues("fileId").first()
return@addInterceptor chain.proceed(
request.newBuilder()
.url("$cdnUrl$fileId")
.build(),
)
}
}
return@addInterceptor chain.proceed(request)
}
.rateLimit(1)
.build()
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun popularMangaRequest(page: Int): Request {
return GET("$apiUrl/comics/search/comics", headers)
}
override fun popularMangaParse(response: Response): MangasPage {
val comics = response.parseAs<List<ApiSearchComic>>()
val cdnUrl = preferences.getCdnUrl()
return MangasPage(comics.map { it.toSManga(cdnUrl) }, false)
}
override fun fetchSearchManga(
page: Int,
query: String,
filters: FilterList,
): Observable<MangasPage> {
return if (query.startsWith(PREFIX_SLUG)) {
val url = "/comics/${query.substringAfter(PREFIX_SLUG)}"
val manga = SManga.create().apply { this.url = url }
client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.map { mangaDetailsParse(it).apply { this.url = url } }
.map { MangasPage(listOf(it), false) }
} else {
client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess()
.map { searchMangaParse(it, query) }
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = popularMangaRequest(page)
private fun searchMangaParse(response: Response, query: String): MangasPage {
val comics = response.parseAs<List<ApiSearchComic>>()
val cdnUrl = preferences.getCdnUrl()
return comics
.filter { it.ComicTitle.contains(query, true) }
.map { it.toSManga(cdnUrl) }
.let { MangasPage(it, false) }
}
override fun mangaDetailsRequest(manga: SManga): Request {
return GET("$apiUrl${manga.url}", headers)
}
override fun mangaDetailsParse(response: Response): SManga {
val comic = response.parseAs<ApiComic>()
return comic.toSManga(json, preferences.getCdnUrl())
}
override fun getMangaUrl(manga: SManga) = "$baseUrl${manga.url}"
override fun chapterListRequest(manga: SManga): Request {
val url = "$apiUrl${manga.url.replace("comics", "chapters")}"
return GET(url, headers)
}
override fun chapterListParse(response: Response): List<SChapter> {
val chapters = response.parseAs<List<ApiChapter>>()
val mangaUrl = response.request.url.toString()
.substringAfter(apiUrl)
.replace("chapters", "comics")
return chapters.map { it.toSChapter(mangaUrl) }
}
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
val chapterPages = document.select("#__NEXT_DATA__").html()
.parseAs<NextData<ApiChapterPages>>()
.props.pageProps.chapter.pages
val pages = chapterPages.parseAs<List<String>>()
val cdnUrl = updatedCdnUrl(document)
return pages.mapIndexed { idx, image ->
Page(idx, "", "$cdnUrl$image")
}
}
private fun updatedCdnUrl(document: Document): String {
val cdnUrlFromPage = document.selectFirst("main div.maxWidth img")
?.attr("src")
?.substringBefore("?")
?.let { "$it?fileId=" }
return preferences.getCdnUrl()
.let {
if (it != cdnUrlFromPage && cdnUrlFromPage != null) {
preferences.putCdnUrl(cdnUrlFromPage)
cdnUrlFromPage
} else {
it
}
}
}
private inline fun <reified T> String.parseAs(): T =
json.decodeFromString(this)
private inline fun <reified T> Response.parseAs(): T =
body.string().parseAs()
private fun SharedPreferences.getCdnUrl(): String {
return getString(cdnPref, fallbackCdnUrl) ?: fallbackCdnUrl
}
private fun SharedPreferences.putCdnUrl(url: String) {
edit().putString(cdnPref, url).commit()
}
companion object {
private const val fallbackCdnUrl = "https://f005.backblazeb2.com/b2api/v1/b2_download_file_by_id?fileId="
private const val cdnPref = "cdn_pref"
val titleSpecialCharactersRegex = "[^a-z0-9]+".toRegex()
val trailingHyphenRegex = "-+$".toRegex()
val dateFormat by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
}
const val PREFIX_SLUG = "slug:"
}
override fun searchMangaParse(response: Response) =
throw UnsupportedOperationException()
override fun imageUrlParse(response: Response) =
throw UnsupportedOperationException()
override fun latestUpdatesParse(response: Response) =
throw UnsupportedOperationException()
override fun popularMangaRequest(page: Int): Request =
GET("$baseUrl/home", headers)
override fun latestUpdatesRequest(page: Int) =
throw UnsupportedOperationException()
popularMangaRequest(page)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request =
GET("$baseUrl/comics", headers)
override fun popularMangaSelector(): String = "div:has(span:contains(POPULAR)) + section a:has(img)"
override fun latestUpdatesSelector(): String = "div:has(span:contains(LATEST)) + section a:has(img)"
override fun searchMangaSelector(): String = ".grid a"
private fun mangaFromElement(element: Element): SManga = SManga.create().apply {
setUrlWithoutDomain(element.absUrl("href"))
thumbnail_url = element.selectFirst("img")?.absUrl("src")
}
override fun popularMangaFromElement(element: Element): SManga = mangaFromElement(element).apply {
title = element.selectFirst("h5")!!.text()
}
override fun latestUpdatesFromElement(element: Element): SManga = mangaFromElement(element).apply {
title = element.parent()?.selectFirst("div a")!!.text()
}
override fun searchMangaFromElement(element: Element): SManga = mangaFromElement(element).apply {
title = element.selectFirst("h1")!!.text()
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
val response = client.newCall(searchMangaRequest(page, query, filters)).execute()
val mangaList = response.asJsoup().select(searchMangaSelector())
.map { searchMangaFromElement(it) }
.filter { it.title.lowercase().contains(query.lowercase()) }
return Observable.just(MangasPage(mangaList, false))
}
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
author = document.selectFirst("span:contains(Author) + span")!!.text()
document.selectFirst("section div div")?.children()?.also { infoRows ->
infoRows[0].selectFirst("h1")?.text()?.let { title = it }
description = infoRows[2].text()
with(infoRows[1].select("span")) {
status = when (this.removeAt(0)?.text()?.lowercase()) {
"ongoing" -> SManga.ONGOING
else -> SManga.UNKNOWN
}
genre = this.joinToString { text() }
}
}
}
@Serializable
class ChapterDTO(val chapterID: Int, val ChapterNumber: String, val ChapterName: String, val chapterDate: String)
private val chapterDataRegex = Regex("""\\"chapters\\":(\[.*]),\\"param\\":\\"(\S+)\\"\}""")
override fun chapterListParse(response: Response): List<SChapter> {
chapterDataRegex.find(response.body.string())?.destructured?.also { (chapterData, mangaId) ->
return json.decodeFromString<List<ChapterDTO>>(chapterData).map { chapter ->
SChapter.create().apply {
name = "Chapter ${chapter.ChapterNumber} - ${chapter.ChapterName}"
setUrlWithoutDomain(
baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("comics")
addPathSegment(mangaId)
addPathSegment("${chapter.chapterID}-chapter-${chapter.ChapterNumber}")
}.build().toString(),
)
date_upload = try {
dateFormat.parse(chapter.chapterDate)?.time ?: 0
} catch (_: Exception) {
0
}
}
}
}
return listOf()
}
override fun pageListParse(document: Document): List<Page> =
document.select("section img").mapIndexed { index, img -> Page(index, imageUrl = img.absUrl("src")) }
override fun popularMangaNextPageSelector(): String? = null
override fun latestUpdatesNextPageSelector(): String? = null
override fun searchMangaNextPageSelector(): String? = null
override fun imageUrlParse(document: Document): String = ""
override fun chapterListSelector(): String = ""
override fun chapterFromElement(element: Element): SChapter = throw UnsupportedOperationException()
}

View File

@ -1,96 +0,0 @@
package eu.kanade.tachiyomi.extension.en.disasterscans
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
@Serializable
data class ApiSearchComic(
val id: String,
val ComicTitle: String,
val CoverImage: String,
) {
fun toSManga(cdnUrl: String) = SManga.create().apply {
title = ComicTitle
thumbnail_url = "$cdnUrl$CoverImage#thumbnail"
url = "/comics/$id-${ComicTitle.titleToSlug()}"
}
}
@Serializable
data class ApiComic(
val id: String,
val ComicTitle: String,
val Description: String,
val CoverImage: String,
val Status: String,
val Genres: String,
val Author: String,
val Artist: String,
) {
fun toSManga(json: Json, cdnUrl: String) = SManga.create().apply {
title = ComicTitle
thumbnail_url = "$cdnUrl$CoverImage#thumbnail"
url = "/comics/$id-${ComicTitle.titleToSlug()}"
description = Description
author = Author
artist = Artist
genre = json.decodeFromString<List<String>>(Genres).joinToString()
status = Status.parseStatus()
}
}
@Serializable
data class ApiChapter(
val chapterID: Int,
val chapterNumber: String,
val ChapterName: String,
val chapterDate: String,
) {
fun toSChapter(mangaUrl: String) = SChapter.create().apply {
url = "$mangaUrl/$chapterID-chapter-$chapterNumber"
chapter_number = chapterNumber.toFloat()
name = "Chapter $chapterNumber"
if (ChapterName.isNotEmpty()) {
name += ": $ChapterName"
}
date_upload = chapterDate.parseDate()
}
}
@Serializable
data class NextData<T>(
val props: Props<T>,
) {
@Serializable
data class Props<T>(val pageProps: T)
}
@Serializable
data class ApiChapterPages(
val chapter: ApiPages,
) {
@Serializable
data class ApiPages(val pages: String)
}
private fun String.titleToSlug() = this.trim()
.lowercase()
.replace(DisasterScans.titleSpecialCharactersRegex, "-")
.replace(DisasterScans.trailingHyphenRegex, "")
private fun String.parseDate(): Long {
return runCatching {
DisasterScans.dateFormat.parse(this)!!.time
}.getOrDefault(0L)
}
private fun String.parseStatus(): Int {
return when {
contains("ongoing", true) -> SManga.ONGOING
contains("completed", true) -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
}

View File

@ -1,34 +0,0 @@
package eu.kanade.tachiyomi.extension.en.disasterscans
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
class DisasterScansUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val slug = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${DisasterScans.PREFIX_SLUG}$slug")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("DisasterScansUrl", e.toString())
}
} else {
Log.e("DisasterScansUrl", "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}