Compare commits

...

14 Commits

Author SHA1 Message Date
Norsze d726f9eee1 Add Serein Scan (#2319)
CI / Prepare job (push) Successful in 4s Details
CI / Build individual modules (push) Successful in 2m51s Details
CI / Publish repo (push) Successful in 41s Details
* Add Serein Scan

* Update build.gradle

corrected overrideVersionCode

* Update build.gradle

Fix extName

* Update src/tr/sereinscan/build.gradle

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>

* Update src/tr/sereinscan/src/eu/kanade/tachiyomi/extension/tr/sereinscan/SereinScan.kt

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>

* Update icon

* Turkish date format

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2024-04-10 01:26:52 +01:00
bapeey 8387d1a9de Jeaz Scans: Update domain (#2314)
* Update domain

* Bump
2024-04-10 01:26:52 +01:00
bapeey ad0d859a87 HeanCMS: Fix filters and fetch genres from api (#2312)
Fix filters
2024-04-10 01:26:52 +01:00
AwkwardPeak7 a9bfc1d348 Happymh: fix search (#2272) 2024-04-10 01:26:52 +01:00
Secozzi 158e5ce4e2 Add comicfans (#2299)
* add comicfans

* use api calls

* suggestions
2024-04-10 01:26:52 +01:00
Cuong M. Tran da6abfcbc6 TruyenHentai18: update domain (#2310) 2024-04-10 01:26:52 +01:00
Smol Ame 124ab39685 Night Comic: Updated URL (#2304)
* Night Comic: Updated URL

Removed "www." from link

* Set `useNewChapterEndpoint` to true

* Added override for author detail
2024-04-10 01:26:52 +01:00
Cuong M. Tran b2a8c207f7 Gmanga multisrc: filter out novel & fix null (#2303)
* Gmanga: filter out novel & fix null

* avoid null string in genre

* Update lib-multisrc/gmanga/src/eu/kanade/tachiyomi/multisrc/gmanga/Dto.kt
2024-04-10 01:26:52 +01:00
AwkwardPeak7 340edf19dc FuzzyDoodle: Make CloudRecess a multisrc and add/fix some sources (#2290)
* CloudRecess multisrc

* ScyllaScans: move to cloudrecess multisrc

* FleksyScans: new source

* remove baseUrl

* simplify popular

always use /manga which isn't technically popular but list of all manga which is good enough

* HentaiSlayer (ar): move to cloudrecess

* remove CloudRecess (en): site appears to be dead

* small change

* rename

* review changes

why do I forget headers

* add alternative titles to description

* parse filters from popular and search response

avoid extra call

* remove placeholder author/artist

* LelscanVF: move to FuzzyDoodle

also improve date parsing

* add icons to lelscanvf

* flexyScans: change icon

other one is of Aksy Scan which also posts on the site
2024-04-10 01:26:52 +01:00
renovate[bot] 7dd71fe7f7 Update gradle/actions action to v3.2.0 (#2294)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-10 01:26:35 +01:00
bapeey fba65e67ea Add ManhuaOnline (#2291) 2024-04-10 01:25:25 +01:00
Chopper ed0912d0d6 Add Spmanhwa (#2284)
* Add Spmanhwa

* Replace icons

* Remove redudant date format

* Fix no unused imports
2024-04-10 01:25:25 +01:00
Chopper c088f3fabb BlackoutComics: Fix selectors (#2283)
* Fix selectors

* Get element that contains no hidden element

* Fix latestUpdate and remove selector inversion for hidden elements
2024-04-10 01:25:25 +01:00
Chopper 8a2a65ac53 Add LeitorDeManga (#2280)
* Add LeitorDeManga

* Add icons

* Remove isNfsw

* Fix extension name

* Replace icons

* Add custom path
2024-04-10 01:25:25 +01:00
85 changed files with 981 additions and 763 deletions

View File

@ -45,7 +45,7 @@ jobs:
echo ${{ secrets.SIGNING_KEY }} | base64 -d > signingkey.jks echo ${{ secrets.SIGNING_KEY }} | base64 -d > signingkey.jks
- name: Set up Gradle - name: Set up Gradle
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0 uses: gradle/actions/setup-gradle@e24011a3b5db78bd5ab798036042d9312002f252 # v3.2.0
- name: Build extensions - name: Build extensions
env: env:

View File

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

View File

@ -0,0 +1,62 @@
package eu.kanade.tachiyomi.multisrc.fuzzydoodle
import eu.kanade.tachiyomi.source.model.Filter
import okhttp3.HttpUrl
interface UrlPartFilter {
fun addUrlParameter(url: HttpUrl.Builder)
}
abstract class SelectFilter(
name: String,
private val options: List<Pair<String, String>>,
private val urlParameter: String,
) : UrlPartFilter, Filter.Select<String>(
name,
options.map { it.first }.toTypedArray(),
) {
override fun addUrlParameter(url: HttpUrl.Builder) {
url.addQueryParameter(urlParameter, options[state].second)
}
}
class CheckBoxFilter(name: String, val value: String) : Filter.CheckBox(name)
abstract class CheckBoxGroup(
name: String,
options: List<Pair<String, String>>,
private val urlParameter: String,
) : UrlPartFilter, Filter.Group<CheckBoxFilter>(
name,
options.map { CheckBoxFilter(it.first, it.second) },
) {
override fun addUrlParameter(url: HttpUrl.Builder) {
state.filter { it.state }.forEach {
url.addQueryParameter(urlParameter, it.value)
}
}
}
class TypeFilter(
options: List<Pair<String, String>>,
) : SelectFilter(
"Type",
options,
"type",
)
class StatusFilter(
options: List<Pair<String, String>>,
) : SelectFilter(
"Status",
options,
"status",
)
class GenreFilter(
options: List<Pair<String, String>>,
) : CheckBoxGroup(
"Genres",
options,
"genre[]",
)

View File

@ -0,0 +1,317 @@
package eu.kanade.tachiyomi.multisrc.fuzzydoodle
import android.util.Log
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.model.Filter
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.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.lang.Exception
import java.util.Calendar
/*
* https://github.com/jhin1m/fuzzy-doodle
*/
abstract class FuzzyDoodle(
override val name: String,
override val baseUrl: String,
override val lang: String,
) : ParsedHttpSource() {
override val supportsLatest = true
override val client = network.cloudflareClient
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
// Popular
override fun popularMangaRequest(page: Int) =
GET("$baseUrl/manga?page=$page", headers)
override fun popularMangaSelector() = "div#card-real"
override fun popularMangaNextPageSelector() = "ul.pagination > li:last-child:not(.pagination-disabled)"
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
launchIO { fetchFilters(document) }
val entries = document.select(popularMangaSelector())
.map(::popularMangaFromElement)
val hasNextPage = document.selectFirst(popularMangaNextPageSelector()) != null
return MangasPage(entries, hasNextPage)
}
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
title = element.selectFirst("h2.text-sm")!!.text()
thumbnail_url = element.selectFirst("img")?.imgAttr()
}
// latest
protected open val latestFromHomePage = false
override fun latestUpdatesRequest(page: Int) =
if (latestFromHomePage) {
latestHomePageRequest(page)
} else {
latestPageRequest(page)
}
protected open fun latestHomePageRequest(page: Int) =
GET("$baseUrl/?page=$page", headers)
protected open fun latestPageRequest(page: Int) =
GET("$baseUrl/latest?page=$page", headers)
override fun latestUpdatesSelector() =
if (latestFromHomePage) {
"section:has(h2:containsOwn(Recent Chapters)) div#card-real," +
" section:has(h2:containsOwn(Chapitres récents)) div#card-real"
} else {
popularMangaSelector()
}
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
override fun latestUpdatesParse(response: Response): MangasPage {
launchIO { fetchFilters() }
return super.latestUpdatesParse(response)
}
// search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/manga".toHttpUrl().newBuilder().apply {
addQueryParameter("title", query.trim())
filters.filterIsInstance<UrlPartFilter>().forEach {
it.addUrlParameter(this)
}
if (page > 1) {
addQueryParameter("page", page.toString())
}
}.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response) = popularMangaParse(response)
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
// filters
protected var typeList = listOf<Pair<String, String>>()
protected var statusList = listOf<Pair<String, String>>()
protected var genreList = listOf<Pair<String, String>>()
private var fetchFilterAttempts = 0
protected suspend fun fetchFilters(document: Document? = null) {
if (fetchFilterAttempts < 3 && (typeList.isEmpty() || statusList.isEmpty() || genreList.isEmpty())) {
try {
val doc = document ?: client.newCall(filtersRequest())
.await()
.asJsoup()
parseFilters(doc)
} catch (e: Exception) {
Log.e("$name: Filters", e.stackTraceToString())
}
fetchFilterAttempts++
}
}
protected open fun filtersRequest() = GET("$baseUrl/manga", headers)
protected open fun parseFilters(document: Document) {
typeList = document.select("select[name=type] > option").map {
it.ownText() to it.attr("value")
}
statusList = document.select("select[name=status] > option").map {
it.ownText() to it.attr("value")
}
genreList = document.select("div.grid > div.flex:has(> input[name=genre[]])").mapNotNull {
val label = it.selectFirst("label")?.ownText()
?: return@mapNotNull null
val value = it.selectFirst("input")?.attr("value")
?: return@mapNotNull null
label to value
}
}
override fun getFilterList(): FilterList {
val filters = mutableListOf<Filter<*>>()
if (typeList.isNotEmpty()) {
filters.add(TypeFilter(typeList))
}
if (statusList.isNotEmpty()) {
filters.add(StatusFilter(statusList))
}
if (genreList.isNotEmpty()) {
filters.add(GenreFilter(genreList))
}
if (filters.size < 3) {
filters.add(0, Filter.Header("Press 'reset' to load more filters"))
}
return FilterList(filters)
}
private val scope = CoroutineScope(Dispatchers.IO)
protected fun launchIO(block: suspend () -> Unit) = scope.launch { block() }
// details
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
val genres = mutableListOf<String>()
with(document.selectFirst("main > section > div")!!) {
thumbnail_url = selectFirst("div.relative img")?.imgAttr()
title = selectFirst("div.flex > h1, div.flex > h2")!!.ownText()
genres.addAll(select("div.flex > a.inline-block").eachText())
description = buildString {
selectFirst("div:has(> p#description)")?.let {
it.selectFirst("span.font-semibold")?.remove()
it.select("#show-more").remove()
append(it.text())
}
selectFirst("div.flex > h1 + div > span.text-sm, div.flex > h2 + div > span.text-sm")?.text()?.let {
if (it.isNotEmpty()) {
append("\n\n")
append("Alternative Title: ")
append(it.trim())
}
}
}.trim()
}
document.selectFirst("div#buttons + div.hidden, div:has(> div#buttons) + div.flex")?.run {
status = (getInfo("Status") ?: getInfo("Statut")).parseStatus()
artist = (getInfo("Artist") ?: getInfo("المؤلف") ?: getInfo("Artiste")).removePlaceHolder()
author = (getInfo("Author") ?: getInfo("الرسام") ?: getInfo("Auteur")).removePlaceHolder()
(getInfo("Type") ?: getInfo("النوع"))?.also { genres.add(0, it) }
}
genre = genres.joinToString()
}
protected open fun String?.parseStatus(): Int {
this ?: return SManga.UNKNOWN
return when {
listOf("ongoing", "مستمر", "en cours").any { contains(it, true) } -> SManga.ONGOING
listOf("dropped", "cancelled", "متوقف").any { contains(it, true) } -> SManga.CANCELLED
listOf("completed", "مكتمل", "terminé").any { contains(it, true) } -> SManga.COMPLETED
listOf("hiatus").any { contains(it, true) } -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
}
protected fun Element.getInfo(text: String): String? =
selectFirst("p:has(span:containsOwn($text)) span.capitalize")
?.ownText()
?.trim()
protected fun String?.removePlaceHolder(): String? =
takeUnless { it == "-" }
// chapters
override fun chapterListParse(response: Response): List<SChapter> {
val originalUrl = response.request.url.toString()
val chapterList = buildList {
var page = 1
do {
val doc = when {
isEmpty() -> response // First page
else -> {
page++
client.newCall(GET("$originalUrl?page=$page", headers)).execute()
}
}.asJsoup()
addAll(doc.select(chapterListSelector()).map(::chapterFromElement))
} while (doc.selectFirst(chapterListNextPageSelector()) != null)
}
return chapterList
}
override fun chapterListSelector() = "div#chapters-list > a[href]"
protected fun chapterListNextPageSelector() = latestUpdatesNextPageSelector()
override fun chapterFromElement(element: Element) = SChapter.create().apply {
setUrlWithoutDomain(element.attr("href"))
name = element.selectFirst("#item-title, span")!!.ownText()
date_upload = element.selectFirst("span.text-gray-500")?.text().parseRelativeDate()
}
// from madara
protected open fun String?.parseRelativeDate(): Long {
this ?: return 0L
val number = Regex("""(\d+)""").find(this)?.value?.toIntOrNull() ?: return 0L
val cal = Calendar.getInstance()
return when {
listOf("detik", "segundo", "second", "วินาที").any { contains(it, true) } -> {
cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
}
listOf("menit", "dakika", "min", "minute", "minuto", "นาที", "دقائق").any { contains(it, true) } -> {
cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
}
listOf("jam", "saat", "heure", "hora", "hour", "ชั่วโมง", "giờ", "ore", "ساعة", "小时").any { contains(it, true) } -> {
cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
}
listOf("hari", "gün", "jour", "día", "dia", "day", "วัน", "ngày", "giorni", "أيام", "").any { contains(it, true) } -> {
cal.apply { add(Calendar.DAY_OF_YEAR, -number) }.timeInMillis
}
listOf("week", "sema").any { contains(it, true) } -> {
cal.apply { add(Calendar.WEEK_OF_YEAR, -number) }.timeInMillis
}
listOf("month", "mes").any { it in this } -> {
cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
}
listOf("year", "año").any { it in this } -> {
cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
}
else -> 0L
}
}
// pages
override fun pageListParse(document: Document): List<Page> {
return document.select("div#chapter-container > img").mapIndexed { idx, img ->
Page(idx, imageUrl = img.imgAttr())
}
}
private fun Element.imgAttr(): String {
return when {
hasAttr("srcset") -> attr("srcset").substringBefore(" ")
hasAttr("data-cfsrc") -> absUrl("data-cfsrc")
hasAttr("data-src") -> absUrl("data-src")
hasAttr("data-lazy-src") -> absUrl("data-lazy-src")
else -> absUrl("src")
}
}
override fun imageUrlParse(document: Document): String {
throw UnsupportedOperationException()
}
}

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 2 baseVersionCode = 3

View File

@ -30,6 +30,7 @@ class BrowseManga(
private val id: Int, private val id: Int,
private val title: String, private val title: String,
private val cover: String? = null, private val cover: String? = null,
@SerialName("is_novel") val isNovel: Boolean,
) { ) {
fun toSManga(createThumbnail: (String, String) -> String) = SManga.create().apply { fun toSManga(createThumbnail: (String, String) -> String) = SManga.create().apply {
url = "/mangas/$id" url = "/mangas/$id"
@ -83,7 +84,7 @@ class Manga(
else -> SManga.UNKNOWN else -> SManga.UNKNOWN
} }
genre = buildList { genre = buildList {
add(type.title) type.title?.let { add(it) }
add(type.name) add(type.name)
categories.forEach { add(it.name) } categories.forEach { add(it.name) }
}.joinToString() }.joinToString()
@ -123,7 +124,7 @@ class NameDto(val name: String)
@Serializable @Serializable
class TypeDto( class TypeDto(
val name: String, val name: String,
val title: String, val title: String?,
) )
@Serializable @Serializable

View File

@ -49,6 +49,7 @@ abstract class Gmanga(
override fun latestUpdatesParse(response: Response): MangasPage { override fun latestUpdatesParse(response: Response): MangasPage {
val releases = response.parseAs<LatestChaptersDto>().releases val releases = response.parseAs<LatestChaptersDto>().releases
.filterNot { it.manga.isNovel }
val entries = releases.map { it.manga.toSManga(::createThumbnail) } val entries = releases.map { it.manga.toSManga(::createThumbnail) }
.distinctBy { it.url } .distinctBy { it.url }

View File

@ -4,6 +4,8 @@ status_all=All
status_ongoing=Ongoing status_ongoing=Ongoing
status_onhiatus=On hiatus status_onhiatus=On hiatus
status_dropped=Dropped status_dropped=Dropped
status_completed=Completed
status_canceled=Canceled
sort_by_filter_title=Sort By sort_by_filter_title=Sort By
sort_by_title=Title sort_by_title=Title
sort_by_views=Views sort_by_views=Views
@ -19,3 +21,4 @@ pref_credentials_summary=Ignored if empty.
login_failed_unknown_error=Unknown error occurred while logging in login_failed_unknown_error=Unknown error occurred while logging in
paid_chapter_error=Paid chapter unavailable. paid_chapter_error=Paid chapter unavailable.
id_not_found_error=Failed to get the ID for slug: %s id_not_found_error=Failed to get the ID for slug: %s
genre_missing_warning=Press 'Reset' to attempt to show the genres

View File

@ -2,7 +2,7 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 22 baseVersionCode = 23
dependencies { dependencies {
api(project(":lib:i18n")) api(project(":lib:i18n"))

View File

@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.lib.i18n.Intl
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@ -29,6 +30,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import kotlin.concurrent.thread
abstract class HeanCms( abstract class HeanCms(
override val name: String, override val name: String,
@ -45,6 +47,8 @@ abstract class HeanCms(
override val client: OkHttpClient = network.cloudflareClient override val client: OkHttpClient = network.cloudflareClient
protected open val useNewQueryEndpoint = false
protected open val useNewChapterEndpoint = false protected open val useNewChapterEndpoint = false
protected open val enableLogin = false protected open val enableLogin = false
@ -117,7 +121,7 @@ abstract class HeanCms(
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
val url = "$apiUrl/query".toHttpUrl().newBuilder() val url = "$apiUrl/query".toHttpUrl().newBuilder()
.addQueryParameter("query_string", "") .addQueryParameter("query_string", "")
.addQueryParameter("series_status", "All") .addQueryParameter(if (useNewQueryEndpoint) "status" else "series_status", "All")
.addQueryParameter("order", "desc") .addQueryParameter("order", "desc")
.addQueryParameter("orderBy", "total_views") .addQueryParameter("orderBy", "total_views")
.addQueryParameter("series_type", "Comic") .addQueryParameter("series_type", "Comic")
@ -134,7 +138,7 @@ abstract class HeanCms(
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request {
val url = "$apiUrl/query".toHttpUrl().newBuilder() val url = "$apiUrl/query".toHttpUrl().newBuilder()
.addQueryParameter("query_string", "") .addQueryParameter("query_string", "")
.addQueryParameter("series_status", "All") .addQueryParameter(if (useNewQueryEndpoint) "status" else "series_status", "All")
.addQueryParameter("order", "desc") .addQueryParameter("order", "desc")
.addQueryParameter("orderBy", "latest") .addQueryParameter("orderBy", "latest")
.addQueryParameter("series_type", "Comic") .addQueryParameter("series_type", "Comic")
@ -185,7 +189,7 @@ abstract class HeanCms(
val url = "$apiUrl/query".toHttpUrl().newBuilder() val url = "$apiUrl/query".toHttpUrl().newBuilder()
.addQueryParameter("query_string", query) .addQueryParameter("query_string", query)
.addQueryParameter("series_status", statusFilter?.selected?.value ?: "All") .addQueryParameter(if (useNewQueryEndpoint) "status" else "series_status", statusFilter?.selected?.value ?: "All")
.addQueryParameter("order", if (sortByFilter?.state?.ascending == true) "asc" else "desc") .addQueryParameter("order", if (sortByFilter?.state?.ascending == true) "asc" else "desc")
.addQueryParameter("orderBy", sortByFilter?.selected ?: "total_views") .addQueryParameter("orderBy", sortByFilter?.selected ?: "total_views")
.addQueryParameter("series_type", "Comic") .addQueryParameter("series_type", "Comic")
@ -342,7 +346,7 @@ abstract class HeanCms(
} }
private fun String.toAbsoluteUrl(): String { private fun String.toAbsoluteUrl(): String {
return if (startsWith("https://") || startsWith("http://")) this else "$apiUrl/$this" return if (startsWith("https://") || startsWith("http://")) this else "$apiUrl/$coverPath$this"
} }
override fun fetchImageUrl(page: Page): Observable<String> = Observable.just(page.imageUrl!!) override fun fetchImageUrl(page: Page): Observable<String> = Observable.just(page.imageUrl!!)
@ -362,6 +366,8 @@ abstract class HeanCms(
Status(intl["status_ongoing"], "Ongoing"), Status(intl["status_ongoing"], "Ongoing"),
Status(intl["status_onhiatus"], "Hiatus"), Status(intl["status_onhiatus"], "Hiatus"),
Status(intl["status_dropped"], "Dropped"), Status(intl["status_dropped"], "Dropped"),
Status(intl["status_completed"], "Completed"),
Status(intl["status_canceled"], "Canceled"),
) )
protected open fun getSortProperties(): List<SortProperty> = listOf( protected open fun getSortProperties(): List<SortProperty> = listOf(
@ -371,17 +377,47 @@ abstract class HeanCms(
SortProperty(intl["sort_by_created_at"], "created_at"), SortProperty(intl["sort_by_created_at"], "created_at"),
) )
protected open fun getGenreList(): List<Genre> = emptyList() private var genresList: List<Genre> = emptyList()
private var fetchFiltersAttempts = 0
private var filtersState = FiltersState.NOT_FETCHED
private fun fetchFilters() {
if (filtersState != FiltersState.NOT_FETCHED || fetchFiltersAttempts >= 3) return
filtersState = FiltersState.FETCHING
fetchFiltersAttempts++
thread {
try {
val response = client.newCall(GET("$apiUrl/tags", headers)).execute()
val genres = json.decodeFromString<List<HeanCmsGenreDto>>(response.body.string())
genresList = genres.map { Genre(it.name, it.id) }
filtersState = FiltersState.FETCHED
} catch (e: Throwable) {
filtersState = FiltersState.NOT_FETCHED
}
}
}
override fun getFilterList(): FilterList { override fun getFilterList(): FilterList {
val genres = getGenreList() fetchFilters()
val filters = listOfNotNull( val filters = mutableListOf<Filter<*>>(
StatusFilter(intl["status_filter_title"], getStatusList()), StatusFilter(intl["status_filter_title"], getStatusList()),
SortByFilter(intl["sort_by_filter_title"], getSortProperties()), SortByFilter(intl["sort_by_filter_title"], getSortProperties()),
GenreFilter(intl["genre_filter_title"], genres).takeIf { genres.isNotEmpty() },
) )
if (filtersState == FiltersState.FETCHED) {
filters += listOfNotNull(
GenreFilter(intl["genre_filter_title"], genresList),
)
} else {
filters += listOf(
Filter.Separator(),
Filter.Header(intl["genre_missing_warning"]),
)
}
return FilterList(filters) return FilterList(filters)
} }
@ -448,6 +484,8 @@ abstract class HeanCms(
edit().putString(TOKEN_PREF, json.encodeToString(data)).apply() edit().putString(TOKEN_PREF, json.encodeToString(data)).apply()
} }
private enum class FiltersState { NOT_FETCHED, FETCHING, FETCHED }
companion object { companion object {
private const val ACCEPT_IMAGE = "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8" private const val ACCEPT_IMAGE = "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"
private const val ACCEPT_JSON = "application/json, text/plain, */*" private const val ACCEPT_JSON = "application/json, text/plain, */*"

View File

@ -155,6 +155,12 @@ class HeanCmsPageDataDto(
val images: List<String>? = emptyList(), val images: List<String>? = emptyList(),
) )
@Serializable
class HeanCmsGenreDto(
val id: Int,
val name: String,
)
private fun String.toAbsoluteThumbnailUrl(apiUrl: String, coverPath: String): String { private fun String.toAbsoluteThumbnailUrl(apiUrl: String, coverPath: String): String {
return if (startsWith("https://") || startsWith("http://")) this else "$apiUrl/$coverPath$this" return if (startsWith("https://") || startsWith("http://")) this else "$apiUrl/$coverPath$this"
} }

View File

@ -1,7 +1,8 @@
ext { ext {
extName = 'Hentai Slayer' extName = 'Hentai Slayer'
extClass = '.HentaiSlayer' extClass = '.HentaiSlayer'
extVersionCode = 2 themePkg = 'fuzzydoodle'
overrideVersionCode = 2
isNsfw = true isNsfw = true
} }

View File

@ -4,199 +4,28 @@ import android.app.Application
import android.widget.Toast import android.widget.Toast
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.multisrc.fuzzydoodle.FuzzyDoodle
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList
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.ParsedHttpSource
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.Calendar
class HentaiSlayer : ParsedHttpSource(), ConfigurableSource { class HentaiSlayer : FuzzyDoodle("هنتاي سلاير", "https://hentaislayer.net", "ar"), ConfigurableSource {
override val name = "هنتاي سلاير" override val client = super.client.newBuilder()
override val baseUrl = "https://hentaislayer.net"
override val lang = "ar"
override val supportsLatest = true
override val client = network.cloudflareClient.newBuilder()
.rateLimit(2) .rateLimit(2)
.build() .build()
override fun headersBuilder() = super.headersBuilder() override fun headersBuilder() = super.headersBuilder()
.set("Referer", "$baseUrl/")
.set("Origin", baseUrl) .set("Origin", baseUrl)
private val preferences by lazy { private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
// ============================== Popular =============================== override fun latestPageRequest(page: Int) = GET("$baseUrl/latest-${getLatestTypes()}?page=$page", headers)
override fun popularMangaRequest(page: Int) = GET("$baseUrl/manga?page=$page", headers)
override fun popularMangaSelector() = "div > div:has(div#card-real)"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
with(element.selectFirst("div#card-real a")!!) {
setUrlWithoutDomain(absUrl("href"))
with(selectFirst("figure")!!) {
with(selectFirst("img.object-cover")!!) {
thumbnail_url = imgAttr()
title = attr("alt")
}
genre = select("span p.drop-shadow-sm").text()
}
}
}
override fun popularMangaNextPageSelector() = "ul.pagination > li:last-child:not(.pagination-disabled)"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/latest-${getLatestTypes()}?page=$page", headers)
override fun latestUpdatesSelector() = popularMangaSelector()
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
// =============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/manga?title=$query".toHttpUrl().newBuilder()
filters.forEach { filter ->
when (filter) {
is TypeFilter -> url.addQueryParameter("type", filter.toUriPart())
is StatusFilter -> url.addQueryParameter("status", filter.toUriPart())
is GenresFilter ->
filter.state
.filter { it.state }
.forEach { url.addQueryParameter("genre[]", it.uriPart) }
else -> {}
}
}
url.addQueryParameter("page", page.toString())
return GET(url.build(), headers)
}
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
// =========================== Manga Details ============================
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
with(document.selectFirst("main section")!!) {
thumbnail_url = selectFirst("img#manga-cover")!!.imgAttr()
with(selectFirst("section > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div:nth-child(2)")!!) {
status = parseStatus(select("a[href*='?status=']").text())
genre = select("a[href*='?type=']").text()
author = select("p:has(span:contains(المؤلف)) span:nth-child(2)").text()
artist = select("p:has(span:contains(الرسام)) span:nth-child(2)").text()
}
var desc = "\u061C"
with(selectFirst("section > div:nth-child(1) > div:nth-child(2)")!!) {
title = selectFirst("h1")!!.text()
genre = select("a[href*='?genre=']")
.map { it.text() }
.let {
listOf(genre) + it
}
.joinToString()
select("h2").text().takeIf { it.isNotEmpty() }?.let {
desc += "أسماء أُخرى: $it\n"
}
}
description = desc + select("#description").text()
}
}
private fun parseStatus(status: String) = when {
status.contains("مستمر") -> SManga.ONGOING
status.contains("متوقف") -> SManga.CANCELLED
status.contains("مكتمل") -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
// ============================== Chapters ==============================
override fun chapterListSelector() = "main section #chapters-list a#chapter-item"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
setUrlWithoutDomain(element.attr("href"))
name = "\u061C" + element.select("#item-title").text() // Add unicode ARABIC LETTER MARK to ensure all titles are right to left
date_upload = parseRelativeDate(element.select("#item-title + span").text()) ?: 0L
}
/**
* Parses dates in this form:
* `11 days ago`
*/
private fun parseRelativeDate(date: String): Long? {
val trimmedDate = date.split(" ")
if (trimmedDate[2] != "ago") return null
val number = trimmedDate[0].toIntOrNull() ?: return null
val unit = trimmedDate[1].removeSuffix("s") // Remove 's' suffix
val now = Calendar.getInstance()
// Map English unit to Java unit
val javaUnit = when (unit) {
"year", "yr" -> Calendar.YEAR
"month" -> Calendar.MONTH
"week", "wk" -> Calendar.WEEK_OF_MONTH
"day" -> Calendar.DAY_OF_MONTH
"hour", "hr" -> Calendar.HOUR
"minute", "min" -> Calendar.MINUTE
"second", "sec" -> Calendar.SECOND
else -> return null
}
now.add(javaUnit, -number)
return now.timeInMillis
}
// =============================== Pages ================================
override fun pageListParse(document: Document): List<Page> {
return document.select("img.chapter-image").mapIndexed { index, item ->
Page(index = index, imageUrl = item.imgAttr())
}
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
private fun Element.imgAttr(): String? {
return when {
hasAttr("srcset") -> attr("abs:srcset").substringBefore(" ")
hasAttr("data-cfsrc") -> attr("abs:data-cfsrc")
hasAttr("data-src") -> attr("abs:data-src")
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
else -> attr("abs:src")
}
}
override fun getFilterList() = FilterList(
GenresFilter(),
TypeFilter(),
StatusFilter(),
)
// ============================== Settings ==============================
companion object { companion object {
private const val LATEST_PREF = "LatestType" private const val LATEST_PREF = "LatestType"
private val LATEST_PREF_ENTRIES get() = arrayOf( private val LATEST_PREF_ENTRIES get() = arrayOf(

View File

@ -1,81 +0,0 @@
package eu.kanade.tachiyomi.extension.ar.hentaislayer
import eu.kanade.tachiyomi.source.model.Filter
class StatusFilter : UriPartFilter(
"الحالة",
arrayOf(
Pair("الكل", ""),
Pair("مستمر", "مستمر"),
Pair("متوقف", "متوقف"),
Pair("مكتمل", "مكتمل"),
),
)
class TypeFilter : UriPartFilter(
"النوع",
arrayOf(
Pair("الكل", ""),
Pair("مانجا", "مانجا"),
Pair("مانهوا", "مانهوا"),
Pair("كوميكس", "كوميكس"),
),
)
private val genres = listOf(
Genre("أكشن", "أكشن"),
Genre("ألعاب جنسية", "ألعاب جنسية"),
Genre("إذلال", "إذلال"),
Genre("إيلف", "إيلف"),
Genre("ابتزاز", "ابتزاز"),
Genre("استعباد", "استعباد"),
Genre("اغتصاب", "اغتصاب"),
Genre("بدون حجب", "بدون حجب"),
Genre("بشرة سمراء", "بشرة سمراء"),
Genre("تاريخي", "تاريخي"),
Genre("تحكم بالعقل", "تحكم بالعقل"),
Genre("تراب", "تراب"),
Genre("تسوندري", "تسوندري"),
Genre("تصوير", "تصوير"),
Genre("جنس بالقدم", "جنس بالقدم"),
Genre("جنس جماعي", "جنس جماعي"),
Genre("جنس شرجي", "جنس شرجي"),
Genre("حريم", "حريم"),
Genre("حمل", "حمل"),
Genre("خادمة", "خادمة"),
Genre("خيال", "خيال"),
Genre("خيانة", "خيانة"),
Genre("دراغون بول", "دراغون بول"),
Genre("دراما", "دراما"),
Genre("رومانسي", "رومانسي"),
Genre("سحر", "سحر"),
Genre("شوتا", "شوتا"),
Genre("شيطانة", "شيطانة"),
Genre("شيميل", "شيميل"),
Genre("طالبة مدرسة", "طالبة مدرسة"),
Genre("عمة", "عمة"),
Genre("فوتا", "فوتا"),
Genre("لولي", "لولي"),
Genre("محارم", "محارم"),
Genre("مدرسي", "مدرسي"),
Genre("مكان عام", "مكان عام"),
Genre("ملون", "ملون"),
Genre("ميلف", "ميلف"),
Genre("ناروتو", "ناروتو"),
Genre("هجوم العمالقة", "هجوم العمالقة"),
Genre("ون بيس", "ون بيس"),
Genre("ياوي", "ياوي"),
Genre("يوري", "يوري"),
)
class Genre(val name: String, val uriPart: String)
class GenreCheckBox(name: String, val uriPart: String) : Filter.CheckBox(name)
class GenresFilter :
Filter.Group<GenreCheckBox>("التصنيفات", genres.map { GenreCheckBox(it.name, it.uriPart) })
open class UriPartFilter(displayName: String, private val pairs: Array<Pair<String, String>>) :
Filter.Select<String>(displayName, pairs.map { it.first }.toTypedArray()) {
fun toUriPart() = pairs[state].second
}

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=".en.cloudrecess.CloudRecessUrlActivity"
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="cloudrecess.io"
android:pathPattern="/manga/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,8 +0,0 @@
ext {
extName = 'CloudRecess'
extClass = '.CloudRecess'
extVersionCode = 2
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,175 +0,0 @@
package eu.kanade.tachiyomi.extension.en.cloudrecess
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
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.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
class CloudRecess : ParsedHttpSource() {
override val name = "CloudRecess"
override val baseUrl = "https://cloudrecess.io"
override val lang = "en"
override val supportsLatest = true
override val client by lazy {
network.cloudflareClient.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 2)
.build()
}
// To load images
override fun headersBuilder() = super.headersBuilder().add("Referer", "$baseUrl/")
// ============================== Popular ===============================
override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
override fun popularMangaSelector() = "swiper-container#popular-cards div#card-real > a"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.attr("href"))
title = element.selectFirst("h2.text-sm")?.text() ?: "Manga"
thumbnail_url = element.selectFirst("img")?.run {
absUrl("data-src").ifEmpty { absUrl("src") }
}
}
override fun popularMangaNextPageSelector() = null
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/?page=$page", headers)
override fun latestUpdatesSelector() = "section:has(h2:containsOwn(Recent Chapters)) div#card-real > a"
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
override fun latestUpdatesNextPageSelector() = "ul.pagination > li:last-child:not(.pagination-disabled)"
// =============================== Search ===============================
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("$baseUrl/manga/$id"))
.asObservableSuccess()
.map(::searchMangaByIdParse)
} else {
super.fetchSearchManga(page, query, filters)
}
}
private fun searchMangaByIdParse(response: Response): MangasPage {
val details = mangaDetailsParse(response.use { it.asJsoup() })
return MangasPage(listOf(details), false)
}
override fun getFilterList() = CloudRecessFilters.FILTER_LIST
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/manga?title=$query&page=$page".toHttpUrl().newBuilder().apply {
val params = CloudRecessFilters.getSearchParameters(filters)
if (params.type.isNotEmpty()) addQueryParameter("type", params.type)
if (params.status.isNotEmpty()) addQueryParameter("status", params.status)
params.genres.forEach { addQueryParameter("genre[]", it) }
}.build()
return GET(url, headers)
}
override fun searchMangaSelector() = "main div#card-real > a"
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector()
// =========================== Manga Details ============================
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
// Absolutely required element, so throwing a NPE when it's not present
// seems reasonable.
with(document.selectFirst("main > section > div")!!) {
thumbnail_url = selectFirst("div.relative img")?.absUrl("src")
title = selectFirst("div.flex > h2")?.ownText() ?: "No name"
genre = select("div.flex > a.inline-block").eachText().joinToString()
description = selectFirst("div.comicInfoExtend__synopsis")?.text()
}
document.selectFirst("div#buttons + div.hidden")?.run {
status = when (getInfo("Status").orEmpty()) {
"Cancelled" -> SManga.CANCELLED
"Completed" -> SManga.COMPLETED
"Hiatus" -> SManga.ON_HIATUS
"Ongoing" -> SManga.ONGOING
else -> SManga.UNKNOWN
}
artist = getInfo("Artist")
author = getInfo("Author")
}
}
private fun Element.getInfo(text: String): String? =
selectFirst("p:has(span:containsOwn($text)) span.capitalize")
?.ownText()
?.trim()
// ============================== Chapters ==============================
override fun chapterListSelector() = "div#chapters-list > a[href]"
override fun chapterListParse(response: Response): List<SChapter> {
val originalUrl = response.request.url.toString()
val chapterList = buildList {
var page = 1
do {
val doc = when {
isEmpty() -> response // First page
else -> {
page++
client.newCall(GET("$originalUrl?page=$page", headers)).execute()
}
}.use { it.asJsoup() }
addAll(doc.select(chapterListSelector()).map(::chapterFromElement))
} while (doc.selectFirst(latestUpdatesNextPageSelector()) != null)
}
return chapterList
}
override fun chapterFromElement(element: Element) = SChapter.create().apply {
setUrlWithoutDomain(element.attr("href"))
name = element.selectFirst("span")?.ownText() ?: "Chapter"
}
// =============================== Pages ================================
override fun pageListParse(document: Document): List<Page> {
return document.select("div#chapter-container > img").map { element ->
val id = element.attr("data-id").toIntOrNull() ?: 0
val url = element.run {
absUrl("data-src").ifEmpty { absUrl("src") }
}
Page(id, "", url)
}
}
override fun imageUrlParse(document: Document): String {
throw UnsupportedOperationException()
}
companion object {
const val PREFIX_SEARCH = "id:"
}
}

View File

@ -1,163 +0,0 @@
package eu.kanade.tachiyomi.extension.en.cloudrecess
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
object CloudRecessFilters {
open class QueryPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
) : Filter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
fun toQueryPart() = vals[state].second
}
private inline fun <reified R> FilterList.asQueryPart(): String {
return (first { it is R } as QueryPartFilter).toQueryPart()
}
open class CheckBoxFilterList(name: String, val items: List<String>) :
Filter.Group<Filter.CheckBox>(name, items.map(::CheckBoxVal))
private class CheckBoxVal(name: String) : Filter.CheckBox(name, false)
private inline fun <reified R> FilterList.checkedItems(): List<String> {
return (first { it is R } as CheckBoxFilterList).state
.filter { it.state }
.map { it.name }
}
internal class TypeFilter : QueryPartFilter("Type", FiltersData.TYPE_LIST)
internal class StatusFilter : QueryPartFilter("Status", FiltersData.STATUS_LIST)
internal class GenresFilter : CheckBoxFilterList("Genres", FiltersData.GENRES_LIST)
val FILTER_LIST get() = FilterList(
TypeFilter(),
StatusFilter(),
GenresFilter(),
)
data class FilterSearchParams(
val type: String = "",
val status: String = "",
val genres: List<String> = emptyList(),
)
internal fun getSearchParameters(filters: FilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.asQueryPart<TypeFilter>(),
filters.asQueryPart<StatusFilter>(),
filters.checkedItems<GenresFilter>(),
)
}
private object FiltersData {
val TYPE_LIST = arrayOf(
Pair("All Types", ""),
Pair("Manga", "manga"),
Pair("Manhwa", "manhwa"),
Pair("OEL/Original", "oel"),
Pair("One Shot", "one-shot"),
Pair("Webtoon", "webtoon"),
)
val STATUS_LIST = arrayOf(
Pair("All Status", ""),
Pair("Cancelled", "cancelled"),
Pair("Completed", "completed"),
Pair("Hiatus", "hiatus"),
Pair("Ongoing", "ongoing"),
Pair("Pending", "pending"),
)
val GENRES_LIST = listOf(
"3P Relationship/s",
"Action",
"Adventure",
"Age Gap",
"Amnesia/Memory Loss",
"Art/s or Creative/s",
"BL",
"Bloody",
"Boss/Employee",
"Childhood Friend/s",
"Comedy",
"Coming of Age",
"Contractual Relationship",
"Crime",
"Cross Dressing",
"Crush",
"Depraved",
"Drama",
"Enemies to Lovers",
"Family Life",
"Fantasy",
"Fetish",
"First Love",
"Food",
"Friends to Lovers",
"Fxckbuddy",
"GL",
"Games",
"Guideverse",
"Hardcore",
"Harem",
"Historical",
"Horror",
"Idols/Celeb/Showbiz",
"Infidelity",
"Intense",
"Isekai",
"Josei",
"Light Hearted",
"Living Together",
"Love Triangle",
"Love/Hate",
"Manipulative",
"Master/Servant",
"Mature",
"Military",
"Music",
"Mystery",
"Nameverse",
"Obsessive",
"Omegaverse",
"On Campus/College Life",
"One Sided Love",
"Part Timer",
"Photography",
"Psychological",
"Rebirth/Reincarnation",
"Red Light",
"Retro",
"Revenge",
"Rich Kids",
"Romance",
"Royalty/Nobility/Gentry",
"SM/BDSM/SUB-DOM",
"School Life",
"Sci-Fi",
"Self-Discovery",
"Shounen Ai",
"Slice of Life",
"Smut",
"Sports",
"Step Family",
"Supernatural",
"Teacher/Student",
"Thriller",
"Tragedy",
"Tsundere",
"Uncensored",
"Violence",
"Voyeur",
"Work Place/Office Workers",
"Yakuza/Gangsters",
)
}
}

View File

@ -1,41 +0,0 @@
package eu.kanade.tachiyomi.extension.en.cloudrecess
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://cloudrecess.io/manga/<item> intents
* and redirects them to the main Tachiyomi process.
*/
class CloudRecessUrlActivity : 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", "${CloudRecess.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

@ -0,0 +1,7 @@
ext {
extName = 'Comic Fans'
extClass = '.ComicFans'
extVersionCode = 1
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,205 @@
package eu.kanade.tachiyomi.extension.en.comicfans
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.model.Filter
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.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
class ComicFans : HttpSource() {
override val name = "Comic Fans"
override val baseUrl = "https://comicfans.io"
private val apiUrl = "https://api.comicfans.io/comic-backend/api/v1/content"
private val cdnUrl = "https://static.comicfans.io"
override val lang = "en"
override val supportsLatest = true
override val client = network.cloudflareClient.newBuilder()
.rateLimit(2)
.build()
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
private fun apiHeadersBuilder() = headersBuilder().apply {
add("Accept", "*/*")
add("Host", apiUrl.toHttpUrl().host)
add("Origin", baseUrl)
add("site-domain", "www.${baseUrl.toHttpUrl().host}")
}
private val apiHeaders by lazy { apiHeadersBuilder().build() }
private val json: Json by injectLazy()
// ============================== Popular ===============================
override fun popularMangaRequest(page: Int): Request {
val body = buildJsonObject {
put("conditionJson", "{\"title\":\"You may also like\",\"maxSize\":15}")
put("pageNumber", page)
put("pageSize", 30)
}.let(json::encodeToString).toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
val popularHeaders = apiHeadersBuilder().apply {
set("Accept", "application/json")
}.build()
return POST("$apiUrl/books/custom/MostPopularLocal#$page", popularHeaders, body)
}
override fun popularMangaParse(response: Response): MangasPage {
val data = response.parseAs<ListDataDto<MangaDto>>().data
val hasNextPage = response.request.url.fragment!!.toInt() < data.totalPages
return MangasPage(data.list.map { it.toSManga(cdnUrl) }, hasNextPage)
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl, headers)
override fun latestUpdatesParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangaList = document.select(
"div:has(>.block-title-bar > .title:contains(New Updates))" +
"> .book-container > .book",
).map { element ->
SManga.create().apply {
thumbnail_url = element.selectFirst("img")!!.attr("abs:src")
with(element.selectFirst(".book-name > a")!!) {
title = text()
setUrlWithoutDomain(attr("abs:href"))
}
}
}
return MangasPage(mangaList, false)
}
// =============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$apiUrl/books".toHttpUrl().newBuilder().apply {
addQueryParameter("pageNumber", page.toString())
addQueryParameter("pageSize", "20")
fragment(page.toString())
if (query.isNotBlank()) {
addPathSegment("search")
addQueryParameter("keyWord", query)
} else {
filters.getUriPart<GenreFilter>()?.let {
addQueryParameter("genre", it)
}
filters.getUriPart<LastUpdateFilter>()?.let {
addQueryParameter("withinDay", it)
}
filters.getUriPart<StatusFilter>()?.let {
addQueryParameter("status", it)
}
}
}.build()
return GET(url, apiHeaders)
}
override fun searchMangaParse(response: Response): MangasPage =
popularMangaParse(response)
// =============================== Filters ==============================
override fun getFilterList(): FilterList = FilterList(
Filter.Header("Text search ignores filters"),
Filter.Separator(),
GenreFilter(),
LastUpdateFilter(),
StatusFilter(),
)
// =========================== Manga Details ============================
override fun getMangaUrl(manga: SManga): String = baseUrl + manga.url
override fun mangaDetailsRequest(manga: SManga): Request {
val bookId = manga.url.substringAfter("/comic/")
.substringBefore("-")
return GET("$apiUrl/books/$bookId", apiHeaders)
}
override fun mangaDetailsParse(response: Response): SManga {
return response.parseAs<DataDto<MangaDto>>().data.toSManga(cdnUrl)
}
// ============================== Chapters ==============================
override fun getChapterUrl(chapter: SChapter): String = baseUrl + chapter.url
override fun chapterListRequest(manga: SManga): Request {
val bookId = manga.url.substringAfter("/comic/")
.substringBefore("-")
return GET("$apiUrl/chapters/page?sortDirection=ASC&bookId=$bookId&pageNumber=1&pageSize=9999", apiHeaders)
}
override fun chapterListParse(response: Response): List<SChapter> {
return response.parseAs<ListDataDto<ChapterDto>>().data.list.mapIndexed { index, chapterDto ->
chapterDto.toSChapter(index + 1)
}.reversed()
}
// =============================== Pages ================================
override fun pageListRequest(chapter: SChapter): Request {
val chapterId = chapter.url.substringAfter("/episode/")
.substringBefore("-")
return GET("$apiUrl/chapters/$chapterId", apiHeaders)
}
override fun pageListParse(response: Response): List<Page> {
return response.parseAs<DataDto<PageDataDto>>().data.comicImageList.map {
Page(it.sortNum, imageUrl = "$cdnUrl/${it.imageUrl}")
}
}
override fun imageRequest(page: Page): Request {
val imgHeaders = headersBuilder().apply {
add("Accept", "image/avif,image/webp,*/*")
add("Host", page.imageUrl!!.toHttpUrl().host)
}.build()
return GET(page.imageUrl!!, imgHeaders)
}
override fun imageUrlParse(response: Response): String =
throw UnsupportedOperationException()
// ============================= Utilities ==============================
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
}

View File

@ -0,0 +1,89 @@
package eu.kanade.tachiyomi.extension.en.comicfans
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.Serializable
typealias ListDataDto<T> = DataDto<ListDto<T>>
@Serializable
class ListDto<T>(
val totalPages: Int,
val list: List<T>,
)
@Serializable
class DataDto<T>(
val data: T,
)
@Serializable
class MangaDto(
val id: Int,
val title: String,
val coverImgUrl: String,
val status: Int,
val authorPseudonym: String? = null,
val synopsis: String? = null,
) {
fun toSManga(cdnUrl: String): SManga = SManga.create().apply {
title = this@MangaDto.title
thumbnail_url = "$cdnUrl/$coverImgUrl"
author = authorPseudonym
url = buildString {
append("/comic/")
append(slugify(id, title))
}
description = synopsis
status = when (this@MangaDto.status) {
0 -> SManga.ONGOING
1 -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
initialized = true
}
}
@Serializable
class ChapterDto(
val id: Int,
val title: String,
val updateTime: Long? = null,
) {
fun toSChapter(index: Int): SChapter = SChapter.create().apply {
name = "Ch. $index - $title"
chapter_number = index.toFloat()
date_upload = updateTime ?: 0L
url = buildString {
append("/episode/")
append(slugify(id, title))
}
}
}
@Serializable
class PageDataDto(
val comicImageList: List<PageDto>,
) {
@Serializable
class PageDto(
val imageUrl: String,
val sortNum: Int,
)
}
private val symbolsRegex = Regex("\\W")
private val hyphenRegex = Regex("-{2,}")
private fun slugify(id: Int, title: String): String = buildString {
append(id)
append("-")
append(
title.lowercase()
.replace(symbolsRegex, "-")
.replace(hyphenRegex, "-")
.removeSuffix("-")
.removePrefix("-"),
)
}

View File

@ -0,0 +1,64 @@
package eu.kanade.tachiyomi.extension.en.comicfans
import eu.kanade.tachiyomi.source.model.Filter
open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
inline fun <reified R> List<*>.getUriPart(): String? =
(filterIsInstance<R>().first() as UriPartFilter).toUriPart().takeIf { it.isNotEmpty() }
class GenreFilter : UriPartFilter(
"Genre",
arrayOf(
Pair("All", ""),
Pair("BL", "1001"),
Pair("Fantasy", "1002"),
Pair("GL", "1003"),
Pair("CEO", "1004"),
Pair("Romance", "1005"),
Pair("Harem", "1006"),
Pair("Action", "1007"),
Pair("Teen", "1008"),
Pair("Adventure", "1009"),
Pair("Eastern", "1010"),
Pair("Comedy", "1011"),
Pair("Esports", "1012"),
Pair("Historical", "1013"),
Pair("Mystery", "1014"),
Pair("Modern", "1015"),
Pair("Urban", "1016"),
Pair("Wuxia", "1017"),
Pair("Suspense", "1018"),
Pair("Female Lead", "1019"),
Pair("Western Fantasy", "1020"),
Pair("Horror", "1022"),
Pair("Realistic Fiction", "1023"),
Pair("Cute", "1024"),
Pair("Campus", "1025"),
Pair("Sci-fi", "1026"),
Pair("History", "1027"),
),
)
class LastUpdateFilter : UriPartFilter(
"Last Update",
arrayOf(
Pair("All", ""),
Pair("Within 3 Days", "3"),
Pair("Within 7 Days", "7"),
Pair("Within 15 Days", "15"),
Pair("Within 30 Days", "30"),
),
)
class StatusFilter : UriPartFilter(
"Status",
arrayOf(
Pair("All", ""),
Pair("Ongoing", "0"),
Pair("Completed", "1"),
),
)

View File

@ -0,0 +1,9 @@
ext {
extName = 'FleksyScans'
extClass = '.FleksyScans'
themePkg = 'fuzzydoodle'
overrideVersionCode = 0
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,5 @@
package eu.kanade.tachiyomi.extension.en.fleksyscans
import eu.kanade.tachiyomi.multisrc.fuzzydoodle.FuzzyDoodle
class FleksyScans : FuzzyDoodle("FleksyScans", "https://flexscans.com", "en")

View File

@ -2,8 +2,8 @@ ext {
extName = 'Night Comic' extName = 'Night Comic'
extClass = '.NightComic' extClass = '.NightComic'
themePkg = 'madara' themePkg = 'madara'
baseUrl = 'https://www.nightcomic.com' baseUrl = 'https://nightcomic.com'
overrideVersionCode = 1 overrideVersionCode = 2
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -2,4 +2,7 @@ package eu.kanade.tachiyomi.extension.en.nightcomic
import eu.kanade.tachiyomi.multisrc.madara.Madara import eu.kanade.tachiyomi.multisrc.madara.Madara
class NightComic : Madara("Night Comic", "https://www.nightcomic.com", "en") class NightComic : Madara("Night Comic", "https://nightcomic.com", "en") {
override val useNewChapterEndpoint = true
override val mangaDetailsSelectorAuthor = "div.manga-authors > a"
}

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.extension.en.omegascans package eu.kanade.tachiyomi.extension.en.omegascans
import eu.kanade.tachiyomi.multisrc.heancms.Genre
import eu.kanade.tachiyomi.multisrc.heancms.HeanCms import eu.kanade.tachiyomi.multisrc.heancms.HeanCms
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
@ -15,33 +14,6 @@ class OmegaScans : HeanCms("Omega Scans", "https://omegascans.org", "en") {
override val versionId = 2 override val versionId = 2
override val useNewChapterEndpoint = true override val useNewChapterEndpoint = true
override val useNewQueryEndpoint = true
override val enableLogin = true override val enableLogin = true
override fun getGenreList() = listOf(
Genre("Romance", 1),
Genre("Drama", 2),
Genre("Fantasy", 3),
Genre("Hardcore", 4),
Genre("SM", 5),
Genre("Harem", 8),
Genre("Hypnosis", 9),
Genre("Novel Adaptation", 10),
Genre("Netori", 11),
Genre("Netorare", 12),
Genre("Isekai", 13),
Genre("Yuri", 14),
Genre("MILF", 16),
Genre("Office", 17),
Genre("Short Story", 18),
Genre("Comedy", 19),
Genre("Campus", 20),
Genre("Crime", 21),
Genre("Revenge", 22),
Genre("Supernatural", 23),
Genre("Action", 24),
Genre("Military", 25),
Genre("Ability", 26),
Genre("Cohabitation", 27),
Genre("Training", 28),
)
} }

View File

@ -1,9 +1,8 @@
ext { ext {
extName = 'Scylla Scans' extName = 'Scylla Scans'
extClass = '.ScyllaScans' extClass = '.ScyllaScans'
themePkg = 'readerfront' themePkg = 'fuzzydoodle'
baseUrl = 'https://scyllascans.org' overrideVersionCode = 9
overrideVersionCode = 1
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -1,9 +1,11 @@
package eu.kanade.tachiyomi.extension.en.scyllascans package eu.kanade.tachiyomi.extension.en.scyllascans
import eu.kanade.tachiyomi.multisrc.readerfront.ReaderFront import eu.kanade.tachiyomi.multisrc.fuzzydoodle.FuzzyDoodle
class ScyllaScans : ReaderFront("Scylla Scans", "https://scyllascans.org", "en") { class ScyllaScans : FuzzyDoodle("Scylla Scans", "https://scyllascans.org", "en") {
override fun getImageCDN(path: String, width: Int) =
"https://i${(0..2).random()}.wp.com/api.scyllascans.org" + // readerfront -> fuzzydoodle
"$path?strip=all&quality=100&w=$width" override val versionId = 2
override val latestFromHomePage = true
} }

View File

@ -0,0 +1,9 @@
ext {
extName = 'Spmanhwa'
extClass = '.Spmanhwa'
themePkg = 'madara'
baseUrl = 'https://spmanhwa.online'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,5 @@
package eu.kanade.tachiyomi.extension.en.spmanhwa
import eu.kanade.tachiyomi.multisrc.madara.Madara
class Spmanhwa : Madara("Spmanhwa", "https://spmanhwa.online", "en")

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.extension.en.templescan package eu.kanade.tachiyomi.extension.en.templescan
import eu.kanade.tachiyomi.multisrc.heancms.Genre
import eu.kanade.tachiyomi.multisrc.heancms.HeanCms import eu.kanade.tachiyomi.multisrc.heancms.HeanCms
import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.network.interceptor.rateLimit
@ -18,21 +17,4 @@ class TempleScan : HeanCms(
.build() .build()
override val mangaSubDirectory = "comic" override val mangaSubDirectory = "comic"
override fun getGenreList() = listOf(
Genre("Drama", 1),
Genre("Josei", 2),
Genre("Romance", 3),
Genre("Girls Love", 4),
Genre("Reincarnation", 5),
Genre("Fantasia", 6),
Genre("Ecchi", 7),
Genre("Adventure", 8),
Genre("Boys Love", 9),
Genre("School Life", 10),
Genre("Action", 11),
Genre("Military", 13),
Genre("Comedy", 14),
Genre("Apocalypse", 15),
)
} }

View File

@ -2,8 +2,8 @@ ext {
extName = 'Jeaz Scans' extName = 'Jeaz Scans'
extClass = '.JeazScans' extClass = '.JeazScans'
themePkg = 'madara' themePkg = 'madara'
baseUrl = 'https://jeazscansv1.com' baseUrl = 'https://jeazscansv2.com'
overrideVersionCode = 0 overrideVersionCode = 1
isNsfw = false isNsfw = false
} }

View File

@ -8,7 +8,7 @@ import java.util.Locale
class JeazScans : Madara( class JeazScans : Madara(
"JeazScans", "JeazScans",
"https://jeazscansv1.com", "https://jeazscansv2.com",
"es", "es",
SimpleDateFormat("d MMMM, yyyy", Locale("es")), SimpleDateFormat("d MMMM, yyyy", Locale("es")),
) { ) {

View File

@ -0,0 +1,10 @@
ext {
extName = 'Manhua Online'
extClass = '.ManhuaOnline'
themePkg = 'madara'
baseUrl = 'https://manhuaonline.org'
overrideVersionCode = 0
isNsfw = false
}
apply from: "$rootDir/common.gradle"

View File

@ -0,0 +1,23 @@
package eu.kanade.tachiyomi.extension.es.manhuaonline
import eu.kanade.tachiyomi.multisrc.madara.Madara
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import okhttp3.OkHttpClient
import java.text.SimpleDateFormat
import java.util.Locale
class ManhuaOnline : Madara(
"Manhua Online",
"https://manhuaonline.org",
"es",
SimpleDateFormat("dd 'de' MMMM 'de' yyyy", Locale("es")),
) {
override val useLoadMoreRequest = LoadMoreStrategy.Never
override val useNewChapterEndpoint = true
override val mangaDetailsSelectorTitle = "div.summary-content span.rate-title"
override val client: OkHttpClient = super.client.newBuilder()
.rateLimit(3)
.build()
}

View File

@ -1,9 +1,8 @@
ext { ext {
extName = 'Lelscan-VF' extName = 'Lelscan-VF'
extClass = '.LelscanVF' extClass = '.LelscanVF'
themePkg = 'mmrcms' themePkg = 'fuzzydoodle'
baseUrl = 'https://lelscanvf.cc' overrideVersionCode = 13
overrideVersionCode = 2
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,10 +1,11 @@
package eu.kanade.tachiyomi.extension.fr.lelscanvf package eu.kanade.tachiyomi.extension.fr.lelscanvf
import eu.kanade.tachiyomi.multisrc.mmrcms.MMRCMS import eu.kanade.tachiyomi.multisrc.fuzzydoodle.FuzzyDoodle
class LelscanVF : MMRCMS( class LelscanVF : FuzzyDoodle("Lelscan-VF", "https://lelscanfr.com", "fr") {
"Lelscan-VF",
"https://lelscanvf.cc", // mmrcms -> FuzzyDoodle
"fr", override val versionId = 2
supportsAdvancedSearch = false,
) override val latestFromHomePage = true
}

View File

@ -19,5 +19,6 @@ class PerfScan : HeanCms("Perf Scan", "https://perf-scan.fr", "fr") {
} }
} }
override val useNewQueryEndpoint = true
override val useNewChapterEndpoint = true override val useNewChapterEndpoint = true
} }

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Blackout Comics' extName = 'Blackout Comics'
extClass = '.BlackoutComics' extClass = '.BlackoutComics'
extVersionCode = 2 extVersionCode = 3
isNsfw = true isNsfw = true
} }

View File

@ -48,8 +48,8 @@ class BlackoutComics : ParsedHttpSource() {
override fun popularMangaFromElement(element: Element) = SManga.create().apply { override fun popularMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.attr("href")) setUrlWithoutDomain(element.attr("href"))
thumbnail_url = element.selectFirst("img:not(.hidden)")?.absUrl("src") thumbnail_url = element.selectFirst("img.custom-image:not(.hidden), img.img-comics")?.absUrl("src")
title = element.selectFirst("p:not(.hidden), span.text-comic")!!.text() title = element.selectFirst("p.image-name:not(.hidden), span.text-comic")!!.text()
} }
override fun popularMangaNextPageSelector() = null override fun popularMangaNextPageSelector() = null
@ -96,9 +96,11 @@ class BlackoutComics : ParsedHttpSource() {
// =========================== Manga Details ============================ // =========================== Manga Details ============================
override fun mangaDetailsParse(document: Document) = SManga.create().apply { override fun mangaDetailsParse(document: Document) = SManga.create().apply {
val row = document.selectFirst("section > div.container > div.row")!! val row = document.selectFirst("div.special-edition")!!
thumbnail_url = row.selectFirst("img:not(.hidden)")?.absUrl("src") thumbnail_url = row.selectFirst("img:last-child")?.absUrl("src")
title = row.selectFirst("div.trailer-content > h2:not(.hidden)")!!.text() title = row.select("h2")
.first { it.classNames().isEmpty() }!!
.text()
with(row.selectFirst("div.trailer-content:has(h3:containsOwn(Detalhes))")!!) { with(row.selectFirst("div.trailer-content:has(h3:containsOwn(Detalhes))")!!) {
println(outerHtml()) println(outerHtml())
@ -153,7 +155,7 @@ class BlackoutComics : ParsedHttpSource() {
// =============================== Pages ================================ // =============================== Pages ================================
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
return document.select("div.chapter-image-ofc canvas").mapIndexed { index, item -> return document.select("div.chapter-image canvas").mapIndexed { index, item ->
Page(index, "", item.absUrl("data-src")) Page(index, "", item.absUrl("data-src"))
} }
} }

View File

@ -0,0 +1,9 @@
ext {
extName = 'Leitor de Mangá'
extClass = '.LeitorDeManga'
themePkg = 'madara'
baseUrl = 'https://leitordemanga.com'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -0,0 +1,21 @@
package eu.kanade.tachiyomi.extension.pt.leitordemanga
import eu.kanade.tachiyomi.multisrc.madara.Madara
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import okhttp3.OkHttpClient
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.TimeUnit
class LeitorDeManga : Madara(
"Leitor de Mangá",
"https://leitordemanga.com",
"pt-BR",
SimpleDateFormat("dd/MM/yyyy", Locale("pt", "BR")),
) {
override val mangaSubString = "ler-manga"
override val client: OkHttpClient = super.client.newBuilder()
.rateLimit(1, 2, TimeUnit.SECONDS)
.build()
}

View File

@ -0,0 +1,9 @@
ext {
extName = 'Serein Scan'
extClass = '.SereinScan'
themePkg = 'mangathemesia'
baseUrl = 'https://sereinscan.com'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -0,0 +1,12 @@
package eu.kanade.tachiyomi.extension.tr.sereinscan
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import java.text.SimpleDateFormat
import java.util.Locale
class SereinScan : MangaThemesia(
"Serein Scan",
"https://sereinscan.com",
"tr",
dateFormat = SimpleDateFormat("MMM d, yyy", Locale("tr")),
)

View File

@ -1,7 +1,7 @@
ext { ext {
extName = "Truyen Hentai 18+" extName = "Truyen Hentai 18+"
extClass = ".TruyenHentai18" extClass = ".TruyenHentai18"
extVersionCode = 1 extVersionCode = 2
isNsfw = true isNsfw = true
} }

View File

@ -18,7 +18,7 @@ class TruyenHentai18 : ParsedHttpSource() {
override val name = "Truyện Hentai 18+" override val name = "Truyện Hentai 18+"
override val baseUrl = "https://truyenhentai18.net" override val baseUrl = "https://truyenhentai18.org"
override val lang = "vi" override val lang = "vi"

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Happymh' extName = 'Happymh'
extClass = '.Happymh' extClass = '.Happymh'
extVersionCode = 9 extVersionCode = 10
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -95,9 +95,16 @@ class Happymh : HttpSource(), ConfigurableSource {
// Search // Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val body = FormBody.Builder().addEncoded("searchkey", query).build() val body = FormBody.Builder()
val header = headersBuilder().add("referer", "$baseUrl/sssearch").build() .addEncoded("searchkey", query)
return POST("$baseUrl/apis/m/ssearch", header, body) .add("v", "v2.13")
.build()
val header = headersBuilder()
.add("referer", "$baseUrl/sssearch")
.build()
return POST("$baseUrl/v2.0/apis/manga/ssearch", header, body)
} }
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {