diff --git a/multisrc/overrides/madara/disasterscans/src/DisasterScans.kt b/multisrc/overrides/madara/disasterscans/src/DisasterScans.kt deleted file mode 100644 index 9a20cc782..000000000 --- a/multisrc/overrides/madara/disasterscans/src/DisasterScans.kt +++ /dev/null @@ -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 -} diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt index 87a12d96f..142d2a732 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt @@ -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), diff --git a/src/en/disasterscans/AndroidManifest.xml b/src/en/disasterscans/AndroidManifest.xml new file mode 100644 index 000000000..dd672cb93 --- /dev/null +++ b/src/en/disasterscans/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/en/disasterscans/build.gradle b/src/en/disasterscans/build.gradle new file mode 100644 index 000000000..1b4da244f --- /dev/null +++ b/src/en/disasterscans/build.gradle @@ -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" diff --git a/multisrc/overrides/madara/disasterscans/res/mipmap-hdpi/ic_launcher.png b/src/en/disasterscans/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from multisrc/overrides/madara/disasterscans/res/mipmap-hdpi/ic_launcher.png rename to src/en/disasterscans/res/mipmap-hdpi/ic_launcher.png diff --git a/multisrc/overrides/madara/disasterscans/res/mipmap-mdpi/ic_launcher.png b/src/en/disasterscans/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from multisrc/overrides/madara/disasterscans/res/mipmap-mdpi/ic_launcher.png rename to src/en/disasterscans/res/mipmap-mdpi/ic_launcher.png diff --git a/multisrc/overrides/madara/disasterscans/res/mipmap-xhdpi/ic_launcher.png b/src/en/disasterscans/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from multisrc/overrides/madara/disasterscans/res/mipmap-xhdpi/ic_launcher.png rename to src/en/disasterscans/res/mipmap-xhdpi/ic_launcher.png diff --git a/multisrc/overrides/madara/disasterscans/res/mipmap-xxhdpi/ic_launcher.png b/src/en/disasterscans/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from multisrc/overrides/madara/disasterscans/res/mipmap-xxhdpi/ic_launcher.png rename to src/en/disasterscans/res/mipmap-xxhdpi/ic_launcher.png diff --git a/multisrc/overrides/madara/disasterscans/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/disasterscans/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from multisrc/overrides/madara/disasterscans/res/mipmap-xxxhdpi/ic_launcher.png rename to src/en/disasterscans/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/multisrc/overrides/madara/disasterscans/res/web_hi_res_512.png b/src/en/disasterscans/res/web_hi_res_512.png similarity index 100% rename from multisrc/overrides/madara/disasterscans/res/web_hi_res_512.png rename to src/en/disasterscans/res/web_hi_res_512.png 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 new file mode 100644 index 000000000..5ae784f62 --- /dev/null +++ b/src/en/disasterscans/src/eu/kanade/tachiyomi/extension/en/disasterscans/DisasterScans.kt @@ -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().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("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") +} 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 new file mode 100644 index 000000000..490674e42 --- /dev/null +++ b/src/en/disasterscans/src/eu/kanade/tachiyomi/extension/en/disasterscans/DisasterScansDto.kt @@ -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>(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 new file mode 100644 index 000000000..63fdd53a6 --- /dev/null +++ b/src/en/disasterscans/src/eu/kanade/tachiyomi/extension/en/disasterscans/DisasterScansUrlActivity.kt @@ -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) + } +}