From 6149261e7b9a5be821c0561a0e983b5732b61a17 Mon Sep 17 00:00:00 2001 From: Creepler13 Date: Fri, 21 Feb 2025 16:21:08 +0100 Subject: [PATCH] Fix DisasterScans (#7673) * Fix DisasterScans * lint * requested changes * changes * idk --- src/en/disasterscans/AndroidManifest.xml | 23 -- src/en/disasterscans/build.gradle | 2 +- .../en/disasterscans/DisasterScans.kt | 282 +++++++----------- .../en/disasterscans/DisasterScansDto.kt | 96 ------ .../disasterscans/DisasterScansUrlActivity.kt | 34 --- 5 files changed, 102 insertions(+), 335 deletions(-) delete mode 100644 src/en/disasterscans/AndroidManifest.xml delete mode 100644 src/en/disasterscans/src/eu/kanade/tachiyomi/extension/en/disasterscans/DisasterScansDto.kt delete mode 100644 src/en/disasterscans/src/eu/kanade/tachiyomi/extension/en/disasterscans/DisasterScansUrlActivity.kt 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) - } -}