diff --git a/src/en/disasterscans/AndroidManifest.xml b/src/en/disasterscans/AndroidManifest.xml
deleted file mode 100644
index 3b3e56a7a..000000000
--- a/src/en/disasterscans/AndroidManifest.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/en/disasterscans/build.gradle b/src/en/disasterscans/build.gradle
index cab0013f0..24c642934 100644
--- a/src/en/disasterscans/build.gradle
+++ b/src/en/disasterscans/build.gradle
@@ -1,7 +1,7 @@
ext {
extName = 'Disaster Scans'
extClass = '.DisasterScans'
- extVersionCode = 32
+ extVersionCode = 33
}
apply from: "$rootDir/common.gradle"
diff --git a/src/en/disasterscans/src/eu/kanade/tachiyomi/extension/en/disasterscans/DisasterScans.kt b/src/en/disasterscans/src/eu/kanade/tachiyomi/extension/en/disasterscans/DisasterScans.kt
index 393d16466..e1b18fe40 100644
--- a/src/en/disasterscans/src/eu/kanade/tachiyomi/extension/en/disasterscans/DisasterScans.kt
+++ b/src/en/disasterscans/src/eu/kanade/tachiyomi/extension/en/disasterscans/DisasterScans.kt
@@ -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()
- 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().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>()
-
- val cdnUrl = preferences.getCdnUrl()
-
- return MangasPage(comics.map { it.toSManga(cdnUrl) }, false)
- }
-
- override fun fetchSearchManga(
- page: Int,
- query: String,
- filters: FilterList,
- ): Observable {
- 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>()
-
- 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()
-
- 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 {
- val chapters = response.parseAs>()
-
- val mangaUrl = response.request.url.toString()
- .substringAfter(apiUrl)
- .replace("chapters", "comics")
-
- return chapters.map { it.toSChapter(mangaUrl) }
- }
-
- override fun pageListParse(response: Response): List {
- val document = response.asJsoup()
-
- val chapterPages = document.select("#__NEXT_DATA__").html()
- .parseAs>()
- .props.pageProps.chapter.pages
-
- val pages = chapterPages.parseAs>()
-
- 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 String.parseAs(): T =
- json.decodeFromString(this)
-
- private inline fun 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 {
+ 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 {
+ chapterDataRegex.find(response.body.string())?.destructured?.also { (chapterData, mangaId) ->
+ return json.decodeFromString>(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 =
+ 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()
}
diff --git a/src/en/disasterscans/src/eu/kanade/tachiyomi/extension/en/disasterscans/DisasterScansDto.kt b/src/en/disasterscans/src/eu/kanade/tachiyomi/extension/en/disasterscans/DisasterScansDto.kt
deleted file mode 100644
index 490674e42..000000000
--- a/src/en/disasterscans/src/eu/kanade/tachiyomi/extension/en/disasterscans/DisasterScansDto.kt
+++ /dev/null
@@ -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>(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(
- val props: Props,
-) {
- @Serializable
- data class Props(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
- }
-}
diff --git a/src/en/disasterscans/src/eu/kanade/tachiyomi/extension/en/disasterscans/DisasterScansUrlActivity.kt b/src/en/disasterscans/src/eu/kanade/tachiyomi/extension/en/disasterscans/DisasterScansUrlActivity.kt
deleted file mode 100644
index 63fdd53a6..000000000
--- a/src/en/disasterscans/src/eu/kanade/tachiyomi/extension/en/disasterscans/DisasterScansUrlActivity.kt
+++ /dev/null
@@ -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)
- }
-}