Remove SlimeRead (#9028)

Remove slimeread
This commit is contained in:
Chopper 2025-05-30 19:55:15 -03:00 committed by Draff
parent adaca0c48e
commit b364d56096
Signed by: Draff
GPG Key ID: E8A89F3211677653
20 changed files with 0 additions and 608 deletions

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="eu.kanade.tachiyomi.multisrc.slimereadtheme.SlimeReadThemeUrlActivity"
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="${SOURCEHOST}"
android:pathPattern="/manga/..*"
android:scheme="${SOURCESCHEME}" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 4

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,273 +0,0 @@
package eu.kanade.tachiyomi.multisrc.slimereadtheme
import app.cash.quickjs.QuickJs
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.ChapterDto
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.LatestResponseDto
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.MangaInfoDto
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.PageListDto
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.PopularMangaDto
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.toSMangaList
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
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 kotlinx.serialization.json.decodeFromStream
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
import kotlin.math.min
abstract class SlimeReadTheme(
override val name: String,
override val baseUrl: String,
override val lang: String,
private val scanId: String = "",
) : HttpSource() {
protected open val apiUrl: String by lazy { getApiUrlFromPage() }
override val supportsLatest = true
override val client = network.cloudflareClient
private val json: Json by injectLazy()
protected open val urlInfix: String = "slimeread.com"
protected open fun getApiUrlFromPage(): String {
val initClient = network.cloudflareClient
val response = initClient.newCall(GET(baseUrl, headers)).execute()
if (!response.isSuccessful) throw Exception("HTTP error ${response.code}")
val document = response.asJsoup()
val scriptUrl = document.selectFirst("script[src*=pages/_app]")?.attr("abs:src")
?: throw Exception("Could not find script URL")
val scriptResponse = initClient.newCall(GET(scriptUrl, headers)).execute()
if (!scriptResponse.isSuccessful) throw Exception("HTTP error ${scriptResponse.code}")
val script = scriptResponse.body.string()
val apiUrl = FUNCTION_REGEX.find(script)?.let { result ->
val varBlock = result.groupValues[1]
val varUrlInfix = result.groupValues[2]
val block = """${varBlock.replace(varUrlInfix, "\"$urlInfix\"")}.toString()"""
try {
QuickJs.create().use { it.evaluate(block) as String }
} catch (e: Exception) {
null
}
}
return apiUrl?.let { "https://$it" } ?: throw Exception("Could not find API URL")
}
// ============================== Popular ===============================
private var popularMangeCache: MangasPage? = null
override fun popularMangaRequest(page: Int): Request {
val url = "$apiUrl/book_search?order=1&status=0".toHttpUrl().newBuilder()
.addIfNotBlank("scan_id", scanId)
.build()
return GET(url, headers)
}
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
popularMangeCache = popularMangeCache?.takeIf { page != 1 }
?: super.fetchPopularManga(page).toBlocking().last()
return pageableOf(page, popularMangeCache!!)
}
override fun popularMangaParse(response: Response): MangasPage {
val items = response.parseAs<List<PopularMangaDto>>()
val mangaList = items.toSMangaList()
return MangasPage(mangaList, mangaList.isNotEmpty())
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request {
val url = "$apiUrl/books?page=$page".toHttpUrl().newBuilder()
.addIfNotBlank("scan_id", scanId)
.build()
return GET(url, headers)
}
override fun latestUpdatesParse(response: Response): MangasPage {
val dto = response.parseAs<LatestResponseDto>()
val mangaList = dto.data.toSMangaList()
val hasNextPage = dto.page < dto.pages
return MangasPage(mangaList, hasNextPage)
}
// =============================== Search ===============================
private var searchMangaCache: MangasPage? = null
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val id = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$apiUrl/book/$id", headers))
.asObservableSuccess()
.map(::searchMangaByIdParse)
} else {
searchMangaCache = searchMangaCache?.takeIf { page != 1 }
?: super.fetchSearchManga(page, query, filters).toBlocking().last()
pageableOf(page, searchMangaCache!!)
}
}
private fun searchMangaByIdParse(response: Response): MangasPage {
val details = mangaDetailsParse(response)
return MangasPage(listOf(details), false)
}
override fun getFilterList() = SlimeReadThemeFilters.FILTER_LIST
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val params = SlimeReadThemeFilters.getSearchParameters(filters)
val url = "$apiUrl/book_search".toHttpUrl().newBuilder()
.addIfNotBlank("query", query)
.addIfNotBlank("genre[]", params.genre)
.addIfNotBlank("status", params.status)
.addIfNotBlank("searchMethod", params.searchMethod)
.addIfNotBlank("scan_id", scanId)
.apply {
params.categories.forEach {
addQueryParameter("categories[]", it)
}
}.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response) = popularMangaParse(response)
// =========================== Manga Details ============================
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.replace("/book/", "/manga/")
override fun mangaDetailsRequest(manga: SManga) = GET(apiUrl + manga.url, headers)
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
val info = response.parseAs<MangaInfoDto>()
thumbnail_url = info.thumbnail_url
title = info.name
description = info.description
genre = info.categories.joinToString()
url = "/book/${info.id}"
status = when (info.status) {
1 -> SManga.ONGOING
2 -> SManga.COMPLETED
3, 4 -> SManga.CANCELLED
5 -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
}
// ============================== Chapters ==============================
override fun chapterListRequest(manga: SManga) =
GET("$apiUrl/book_cap_units_all?manga_id=${manga.url.substringAfterLast("/")}", headers)
override fun chapterListParse(response: Response): List<SChapter> {
val items = response.parseAs<List<ChapterDto>>()
val mangaId = response.request.url.queryParameter("manga_id")!!
return items.map {
SChapter.create().apply {
name = "Cap " + parseChapterNumber(it.number)
date_upload = parseChapterDate(it.updated_at)
chapter_number = it.number
scanlator = it.scan?.scan_name
url = "/book_cap_units?manga_id=$mangaId&cap=${it.number}"
}
}.reversed()
}
private fun parseChapterNumber(number: Float): String {
val cap = number + 1F
return "%.2f".format(cap)
.let { if (cap < 10F) "0$it" else it }
.replace(",00", "")
.replace(",", ".")
}
private fun parseChapterDate(date: String): Long {
return try { dateFormat.parse(date)!!.time } catch (_: Exception) { 0L }
}
override fun getChapterUrl(chapter: SChapter): String {
val url = "$baseUrl${chapter.url}".toHttpUrl()
val id = url.queryParameter("manga_id")!!
val cap = url.queryParameter("cap")!!.toFloat()
val num = parseChapterNumber(cap)
return "$baseUrl/ler/$id/cap-$num"
}
// =============================== Pages ================================
override fun pageListRequest(chapter: SChapter) = GET(apiUrl + chapter.url, headers)
override fun pageListParse(response: Response): List<Page> {
val body = response.body.string()
val pages = if (body.startsWith("{")) {
json.decodeFromString<Map<String, PageListDto>>(body).values.flatMap { it.pages }
} else {
json.decodeFromString<List<PageListDto>>(body).flatMap { it.pages }
}
return pages.mapIndexed { index, item ->
Page(index, "", item.url)
}
}
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException()
}
// ============================= Utilities ==============================
/**
* Handles a large manga list and returns a paginated response.
* The app can't handle the large JSON list without pagination.
*
* @param page The page number to retrieve.
* @param cache The cached manga page containing the full list of mangas.
*/
private fun pageableOf(page: Int, cache: MangasPage) = Observable.just(cache).map { mangaPage ->
val mangas = mangaPage.mangas
val pageSize = 15
val currentSlice = (page - 1) * pageSize
val startIndex = min(mangas.size, currentSlice)
val endIndex = min(mangas.size, currentSlice + pageSize)
val slice = mangas.subList(startIndex, endIndex)
MangasPage(slice, hasNextPage = endIndex < mangas.size)
}
private inline fun <reified T> Response.parseAs(): T = use {
json.decodeFromStream(it.body.byteStream())
}
private fun HttpUrl.Builder.addIfNotBlank(query: String, value: String): HttpUrl.Builder {
if (value.isNotBlank()) addQueryParameter(query, value)
return this
}
companion object {
const val PREFIX_SEARCH = "id:"
val FUNCTION_REGEX = """(\[""\.concat\("[^,]+,"\."\)\.concat\(([^,]+),":\d+"\)\])""".toRegex(RegexOption.DOT_MATCHES_ALL)
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
}
}

View File

@ -1,141 +0,0 @@
package eu.kanade.tachiyomi.multisrc.slimereadtheme
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
object SlimeReadThemeFilters {
open class SelectFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
) : Filter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
val selected get() = vals[state].second
}
private inline fun <reified R> FilterList.getSelected(): String {
return (first { it is R } as SelectFilter).selected
}
open class CheckBoxFilterList(name: String, val pairs: Array<Pair<String, String>>) :
Filter.Group<Filter.CheckBox>(name, pairs.map { CheckBoxVal(it.first) })
private class CheckBoxVal(name: String) : Filter.CheckBox(name, false)
private inline fun <reified R> FilterList.parseCheckbox(
options: Array<Pair<String, String>>,
): Sequence<String> {
return (first { it is R } as CheckBoxFilterList).state
.asSequence()
.filter { it.state }
.map { checkbox -> options.find { it.first == checkbox.name }!!.second }
}
internal class CategoriesFilter : CheckBoxFilterList("Categorias", SlimeReadFiltersData.CATEGORIES)
internal class GenreFilter : SelectFilter("Gênero", SlimeReadFiltersData.GENRES)
internal class SearchMethodFilter : SelectFilter("Método de busca", SlimeReadFiltersData.SEARCH_METHODS)
internal class StatusFilter : SelectFilter("Status", SlimeReadFiltersData.STATUS)
val FILTER_LIST get() = FilterList(
CategoriesFilter(),
GenreFilter(),
SearchMethodFilter(),
StatusFilter(),
)
data class FilterSearchParams(
val categories: Sequence<String> = emptySequence(),
val genre: String = "",
val searchMethod: String = "",
val status: String = "",
)
internal fun getSearchParameters(filters: FilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.parseCheckbox<CategoriesFilter>(SlimeReadFiltersData.CATEGORIES),
filters.getSelected<GenreFilter>(),
filters.getSelected<SearchMethodFilter>(),
filters.getSelected<StatusFilter>(),
)
}
private object SlimeReadFiltersData {
val CATEGORIES = arrayOf(
Pair("Adulto", "125"),
Pair("Artes Marciais", "117"),
Pair("Avant Garde", "154"),
Pair("Aventura", "112"),
Pair("Ação", "146"),
Pair("Comédia", "147"),
Pair("Culinária", "126"),
Pair("Doujinshi", "113"),
Pair("Drama", "148"),
Pair("Ecchi", "127"),
Pair("Erotico", "152"),
Pair("Esporte", "135"),
Pair("Fantasia", "114"),
Pair("Ficção Científica", "120"),
Pair("Filosofico", "150"),
Pair("Harém", "128"),
Pair("Histórico", "115"),
Pair("Isekai", "129"),
Pair("Josei", "116"),
Pair("Mecha", "130"),
Pair("Militar", "149"),
Pair("Mistério", "142"),
Pair("Médico", "118"),
Pair("One-shot", "131"),
Pair("Premiado", "155"),
Pair("Psicológico", "119"),
Pair("Romance", "141"),
Pair("Seinen", "140"),
Pair("Shoujo", "133"),
Pair("Shoujo-ai", "121"),
Pair("Shounen", "139"),
Pair("Shounen-ai", "134"),
Pair("Slice-of-life", "122"),
Pair("Sobrenatural", "123"),
Pair("Sugestivo", "153"),
Pair("Terror", "144"),
Pair("Thriller", "151"),
Pair("Tragédia", "137"),
Pair("Vida Escolar", "132"),
Pair("Yaoi", "124"),
Pair("Yuri", "136"),
)
private val SELECT = Pair("Selecione", "")
val GENRES = arrayOf(
SELECT,
Pair("Manga", "29"),
Pair("Light Novel", "34"),
Pair("Manhua", "31"),
Pair("Manhwa", "30"),
Pair("Novel", "33"),
Pair("Webcomic", "35"),
Pair("Webnovel", "36"),
Pair("Webtoon", "32"),
Pair("4-Koma", "37"),
)
val SEARCH_METHODS = arrayOf(
SELECT,
Pair("Preciso", "0"),
Pair("Geral", "1"),
)
val STATUS = arrayOf(
SELECT,
Pair("Em andamento", "1"),
Pair("Completo", "2"),
Pair("Dropado", "3"),
Pair("Cancelado", "4"),
Pair("Hiato", "5"),
)
}
}

View File

@ -1,41 +0,0 @@
package eu.kanade.tachiyomi.multisrc.slimereadtheme
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://slimeread.com/manga/<id>/<slug> intents
* and redirects them to the main Tachiyomi process.
*/
class SlimeReadThemeUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val item = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${SlimeReadTheme.PREFIX_SEARCH}$item")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View File

@ -1,74 +0,0 @@
package eu.kanade.tachiyomi.extension.pt.slimeread.dto
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class PopularMangaDto(
@SerialName("book_image") val thumbnail_url: String?,
@SerialName("book_id") val id: Int,
@SerialName("book_name_original") val name: String,
)
@Serializable
data class LatestResponseDto(
val pages: Int,
val page: Int,
val data: List<PopularMangaDto>,
)
fun List<PopularMangaDto>.toSMangaList(): List<SManga> = map { item ->
SManga.create().apply {
thumbnail_url = item.thumbnail_url
title = item.name
url = "/book/${item.id}"
}
}
@Serializable
data class MangaInfoDto(
@SerialName("book_id") val id: Int,
@SerialName("book_image") val thumbnail_url: String?,
@SerialName("book_name_original") val name: String,
@SerialName("book_status") val status: Int,
@SerialName("book_synopsis") val description: String?,
@SerialName("book_categories") private val _categories: List<CategoryDto>,
) {
@Serializable
data class CategoryDto(val categories: CatDto)
@Serializable
data class CatDto(@SerialName("cat_name_ptBR") val name: String)
val categories = _categories.map { it.categories.name }
}
@Serializable
data class ChapterDto(
@SerialName("btc_cap") val number: Float,
@SerialName("btc_date_updated") val updated_at: String,
val scan: ScanDto?,
) {
@Serializable
data class ScanDto(val scan_name: String?)
}
@Serializable
data class PageListDto(@SerialName("book_temp_cap_unit") val pages: List<PageDto>)
@Serializable
data class PageDto(
@SerialName("btcu_image") private val path: String,
@SerialName("btcu_provider_host") private val hostId: Int?,
) {
val url by lazy {
val baseUrl = when (hostId) {
2 -> "https://cdn.slimeread.com/"
5 -> "https://black.slimeread.com/"
else -> "https://objects.slimeread.com/"
}
baseUrl + path
}
}

View File

@ -1,10 +0,0 @@
ext {
extName = 'MahouScan'
extClass = '.MahouScan'
themePkg = 'slimereadtheme'
baseUrl = 'https://mahouscan.com'
overrideVersionCode = 0
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

View File

@ -1,15 +0,0 @@
package eu.kanade.tachiyomi.extension.pt.mahouscan
import eu.kanade.tachiyomi.multisrc.slimereadtheme.SlimeReadTheme
import eu.kanade.tachiyomi.network.interceptor.rateLimit
class MahouScan : SlimeReadTheme(
"MahouScan",
"https://mahouscan.com",
"pt-BR",
scanId = "1292193100",
) {
override val client = super.client.newBuilder()
.rateLimit(2)
.build()
}

View File

@ -1,10 +0,0 @@
ext {
extName = 'SlimeRead'
extClass = '.SlimeRead'
themePkg = 'slimereadtheme'
baseUrl = 'https://slimeread.com'
overrideVersionCode = 15
isNsfw = true
}
apply from: "$rootDir/common.gradle"

View File

@ -1,17 +0,0 @@
package eu.kanade.tachiyomi.extension.pt.slimeread
import eu.kanade.tachiyomi.multisrc.slimereadtheme.SlimeReadTheme
import eu.kanade.tachiyomi.network.interceptor.rateLimit
class SlimeRead : SlimeReadTheme(
"SlimeRead",
"https://slimeread.com",
"pt-BR",
) {
override fun headersBuilder() = super.headersBuilder()
.add("Origin", baseUrl)
override val client = super.client.newBuilder()
.rateLimit(2)
.build()
}