Disaster Scans (#16696)

* remove from madara

* DisasterScans: partial implementation

* use updated cdnUrl for existing thumbnails

* url intent
This commit is contained in:
AwkwardPeak7 2023-06-12 23:59:13 +05:00 committed by GitHub
parent 0e8aad3b9f
commit c2a1c620cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 375 additions and 24 deletions

View File

@ -1,23 +0,0 @@
package eu.kanade.tachiyomi.extension.en.disasterscans
import eu.kanade.tachiyomi.multisrc.madara.Madara
import eu.kanade.tachiyomi.source.model.SManga
import org.jsoup.nodes.Document
class DisasterScans : Madara("Disaster Scans", "https://disasterscans.com", "en") {
override val popularMangaUrlSelector = "div.post-title a:last-child"
override fun mangaDetailsParse(document: Document): SManga {
val manga = super.mangaDetailsParse(document)
with(document) {
select("div.post-title h1").first()?.let {
manga.title = it.ownText()
}
}
return manga
}
override val useNewChapterEndpoint: Boolean = true
}

View File

@ -78,7 +78,6 @@ class MadaraGenerator : ThemeSourceGenerator {
SingleLang("Dark Scans", "https://darkscans.com", "en"),
SingleLang("Decadence Scans", "https://reader.decadencescans.com", "en", isNsfw = true, overrideVersionCode = 2),
SingleLang("DiamondFansub", "https://diamondfansub.com", "tr", overrideVersionCode = 1),
SingleLang("Disaster Scans", "https://disasterscans.com", "en", overrideVersionCode = 2),
SingleLang("DokkoManga", "https://dokkomanga.com", "es", overrideVersionCode = 1),
SingleLang("Doodmanga", "https://www.doodmanga.com", "th"),
SingleLang("DoujinHentai", "https://doujinhentai.net", "es", isNsfw = true, overrideVersionCode = 1),

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi.extension">
<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

@ -0,0 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Disaster Scans'
pkgNameSuffix = 'en.disasterscans'
extClass = '.DisasterScans'
extVersionCode = 32
}
apply from: "$rootDir/common.gradle"

View File

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

Before

Width:  |  Height:  |  Size: 316 KiB

After

Width:  |  Height:  |  Size: 316 KiB

View File

@ -0,0 +1,209 @@
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.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
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() {
override val name = "Disaster Scans"
override val lang = "en"
override val versionId = 2
override val baseUrl = "https://disasterscans.com"
private val apiUrl = "https://api.disasterscans.com"
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("Not Used")
override fun imageUrlParse(response: Response) =
throw UnsupportedOperationException("Not Used")
override fun latestUpdatesParse(response: Response) =
throw UnsupportedOperationException("Not Implemented")
override fun latestUpdatesRequest(page: Int) =
throw UnsupportedOperationException("Not Implemented")
}

View File

@ -0,0 +1,96 @@
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

@ -0,0 +1,34 @@
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)
}
}