Compare commits

...

56 Commits

Author SHA1 Message Date
renovate[bot] ce09450e2a Update gradle/wrapper-validation-action action to v2.1.2 (#2076)
CI / Prepare job (push) Successful in 7s Details
CI / Build individual modules (push) Successful in 3m7s Details
CI / Publish repo (push) Successful in 1m37s Details
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-24 21:15:09 +00:00
AwkwardPeak7 575d831400 MangaThemesiaAlt: random url part fixes (#2054)
* trim description

* don't add anything to url if source disabled it

* early extract random part when browsing

* cache in preference for webview url

* fix

* new titles fix where no random part

* bump

* KingOfManga update url

* bump luminous too

* move preference title & summary to intl
2024-03-24 21:13:55 +00:00
mohamedotaku 879bb4c3eb Update Url Gufengmh "zh" (#2062) 2024-03-24 21:13:55 +00:00
mohamedotaku 473b3e98ab Add source GMANGA (unoriginal) "ar" (#2069) 2024-03-24 21:13:55 +00:00
Vetle Ledaal c2faa2774b Sekaikomik: update domain (#2075) 2024-03-24 21:13:55 +00:00
Vetle Ledaal a06318f264 NineHentai: update domain (#2074) 2024-03-24 21:13:55 +00:00
Vetle Ledaal cf2cef1985 YaoiLib: update domain (#2073) 2024-03-24 21:13:55 +00:00
Chopper 217846f693 Add LerMangas (#2068) 2024-03-24 21:13:55 +00:00
Chopper d2fec71228 Manhastro: Fix manga status and add icon (#2066)
* Fix manga status

* Use 'contains' selector

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

* Add icons

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2024-03-24 21:13:54 +00:00
Vetle Ledaal 02d7019b46 SummerToon: update domain (#2055)
* SummerToon: update domain

* isNsfw = false
2024-03-24 21:13:54 +00:00
mohamedotaku 3c6449bf24 Update Url Mangalek "ar" (#1992)
* Update Url Mangalek "ar"

* update defaultBaseUrl
2024-03-24 21:13:54 +00:00
bapeey 87a9effa82 Legends no Fansub: Rename to LegnMangas and change icon (#2035)
Rename and change logo
2024-03-24 21:13:54 +00:00
Vetle Ledaal 7d1cffdd10 Gri Melek: update domain (#2029) 2024-03-24 21:13:54 +00:00
Vetle Ledaal ed9e49fd11 InariManga: update domain (#2028) 2024-03-24 21:13:54 +00:00
Vetle Ledaal 063b526748 MangaLionz: update domain, fix date format (#2026) 2024-03-24 21:13:54 +00:00
Vetle Ledaal 6317d70538 Elarc Toon: update domain (#2025) 2024-03-24 21:13:54 +00:00
WarmSeeker6 86474f6d98 URL update for LXhentai (#2013) 2024-03-24 21:13:54 +00:00
GGaro d7f5c6c1d3 Update Url MangaBat (#2008) 2024-03-24 21:13:54 +00:00
mohamedotaku 41cab37a37 rebrand manhwalist to komiknesia : Update URL (#1995) 2024-03-24 21:13:54 +00:00
mohamedotaku b14ccb1c89 Update Url MangaSpark "ar" (#1994) 2024-03-24 21:13:54 +00:00
mohamedotaku dbcd4deb1f Update Url MangaStarz "ar" (#1993) 2024-03-24 21:13:54 +00:00
bapeey 8ef429f4bc Jobsibe: Disable non-manga items filter (#1991)
Disable non-manga items filter
2024-03-24 21:13:54 +00:00
bapeey 99b0273848 Add JeazScans (#1990) 2024-03-24 21:13:54 +00:00
bapeey e7c326bb6c Add InmoralNoFansub (#1989) 2024-03-24 21:13:54 +00:00
bapeey b4f5680364 Add LuckyManga (#1988) 2024-03-24 21:13:54 +00:00
Cuong M. Tran a0fa7fa458 WPComics update & add new sources (#1909)
* remove non-relevant query

* WPComics: query for genres instead of hard-code

* language assets to support dual-lang

* update XoxoComics, Nhattruyen, Nettruyen to support updated WPComics

* remove unused status

* JManga with new WPComics

* Fix JManga NextPageSelector

* Allow override some more methods

* correct jmanga's location

* remove redundant XoxoComics override

* Get alternative name and JManga's description

* add sources:
- NetTruyenX
- NhatTruyenS
- NetTruyenCO

* revert format changes

* Update NetTruyen to latest domain

* Minor changes:

- Named parameters;
- intl, lazy;

* Remove NetTruyen’s replaceSearchPath. It’s not necessary

* remove the japanese’s mtl

* remove hard-code user-agent

* remove some unnecessary named parameters

* Use super.headersBuilder & fix Referer

* remove redundant import
2024-03-24 21:13:54 +00:00
bapeey 455f57d209 Update HeanCMS theme (#1969)
* i hate this theme

* bump

* remove useless slug update

* lint

* Update series slug on chapter list update

This was made for sources that changed slugs constantly.

Currently no one uses it, but who knows if they enable that again

* what an unstable experience

* Remove empty lines

* Fix intl

* newline

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* why my build took 5 minutes

* I hate iguanas

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2024-03-24 21:13:54 +00:00
Vetle Ledaal 34887a83a8 SobatManKu: update domain (#1948) 2024-03-24 21:13:54 +00:00
Chopper 867f0844d1 Fix Bakai source (#1940)
Fix popular and search URL
2024-03-24 21:13:54 +00:00
Vetle Ledaal 6a1d7dc1ca Beast Scans -> Umi Manga: update domain (#1932)
* Beast Scans -> Umi Manga: update domain

* rename class

* remove unnecessary override
2024-03-24 21:13:54 +00:00
AwkwardPeak7 e1c77ab678 move King of Shojo to en (#1918)
* move King of Shojo to en

* remove id override and reset version
2024-03-24 21:13:54 +00:00
BrutuZ 046c2aa421 Anchira: Improve chapter suffix parsing (#1904)
* Improve chapter suffix parsing

* Don't convert Tags to sequence before sorting
2024-03-24 21:13:54 +00:00
altaccosc 0a0ff7c1ac Add Clown Corps comic source (#1808)
* Add ClownCorps comic source

* Apply suggestions from code review

Thank you very much!

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* Only loop through the present pages

Don't just loop forever until a 404 is returned

* Disable reduntant sorting code

* Add date to chapters

* Apply suggestions from code review

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* Remove commented snippet

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* Move vars to companion obj & inline description

* Un-move some constants & Use Observable.just

* Extract SManga creation to separate function

And use where necessary

* Omit unnecessary function call

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>

* Add caching

I've tried a great many different ways of caching today, and as far as I can reason with my fried brain, I think this one now works pretty well.
I shall continue testing it on my phone.

* Change SerializableChapter implementation

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>

* Code cleanliness

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>

* Optimise requests for page 1 away

* Explicitly sort chapters by upload date

* Move other preference options into separate functions

* Assume response document always contains what we're asking

And throw a runtime exception if it doesn't, so the problem can be noticed and fixed.

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2024-03-24 21:13:54 +00:00
Vetle Ledaal 16bcd6bbd9 Nocturne Summer: update domain (#1933) 2024-03-24 21:13:54 +00:00
renovate[bot] f057af4dbf Update actions/setup-java action to v4.2.1 (#1938)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-24 21:13:49 +00:00
Vetle Ledaal b5c0daba37 Manga Flame: update domain (#1931)
* Manga Flame: update domain

* raise read timeout
2024-03-24 21:11:42 +00:00
Vetle Ledaal 61b3d9a2fb SayHentai: update domain (#1930) 2024-03-24 21:11:42 +00:00
AwkwardPeak7 62bd6c0817 Ozul Scans to King of Manga (#1917)
also enable dynamic urls
2024-03-24 21:11:42 +00:00
AwkwardPeak7 dfb4b93953 Earlym: fix page list (#1916)
* Earlym: fix page list

* Update EarlyManga.kt

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2024-03-24 21:11:42 +00:00
haruki-takeshi 34429ffa0a NetTruyen change domain! (#1911)
* Update NetTruyen.kt

* Update build.gradle
2024-03-24 21:11:42 +00:00
Fermín Cirella ebf7e277e3 Add Doujin.io - J18 (#1891)
* Add Doujin.io - J18

* Apply corrections

* Reduce indentation
2024-03-24 21:11:42 +00:00
bapeey 3f73aec7cf Tres Daos Scan: Change theme to MangaThemesia (#1914)
Move to themesia
2024-03-24 21:11:42 +00:00
beerpsi 0c4abef20c Dua Leo Truyen: Update base URL (#1898) 2024-03-24 21:11:42 +00:00
Cuong M. Tran b21ab37da9 HentaiSlayer: translate "alternative name" (#1843)
* HentaiSlayer: translate "alternative name"

* Fix “alternative name”

* Fix non-nullable & optimize code

* revert format changes caused by AndroidStudio’s ktlint plugin

* Update HentaiSlayer.kt

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2024-03-24 21:11:42 +00:00
stevenyomi e67d78f435 Roumanwu: update URL (#1884) 2024-03-24 21:11:42 +00:00
bapeey 82bb3dafd8 Remove Kumanga (#1857)
* Remove Kumanga

* Add rule to issue_moderator

* Rebuild

* Update regex

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2024-03-24 21:11:32 +00:00
mohamedotaku 9554653678 Change Url and theme for Hijala "ar" (#1853)
* Change Url and theme for Hijala "ar"

* Update build.gradle

* bump versionId

* Update build.gradle
2024-03-24 21:10:27 +00:00
Cuong M. Tran 35a2715ad5 New source: Arabs Hentai (ar) (#1842)
* New source: Arabs Hentai (ar)

* fix “alternative name”

* fix: escape query string

* Fix non-nullable & improve code

* it’s ok for thumbnail to be null
2024-03-24 21:10:27 +00:00
bapeey ca5365cc6c SlimeRead: Fix "Unexpected JSON token" on pageList and choose apiUrl randomly (#1837)
* Fix pageList and update apiUrl (again)

* Random apiUrl

* Inline

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2024-03-24 21:10:27 +00:00
quangpao e10e72ad98 NhatTruyen: Update the domain (#1831)
Update domain of NhatTruyen
2024-03-24 21:10:27 +00:00
anenasa 488adf9ba6 Dm5: Fix image not loading (#1795) 2024-03-24 21:10:27 +00:00
AwkwardPeak7 0594d08440 MangaThemesia: add class to handle dynamic urls in sources (#1793)
* MangaThemesia: add alternative class to handle dynamic urls

* use MangaThemesiaAlt on Asura & Luminous

* use MangaThemesiaAlt on Rizz

* don't update in getMangaUrl

* small cleanup

* remove other pref as well
LuminousScans

* wording

* remove from FlameComics, since they no longer appear to do it

* review comments

* lint

* actual old pref key

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>

* actual old pref key x2

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>

---------

Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
2024-03-24 21:10:27 +00:00
mohamedotaku d7c2e7b9da Add source Manga Time "ar" (#1852)
* Add source Manga Time "ar"

* Update MangaTime.kt

* Update MangaTime.kt
2024-03-24 21:10:27 +00:00
AwkwardPeak7 f2f809f35d Gmanga Multisrc: make cover nullable (#1854)
* Gmanga Multisrc: make cover nullable

* filter empty alt titles
2024-03-24 21:10:27 +00:00
inipew 5ad927dbf4 Shinigami: update baseurl (#1825) 2024-03-24 21:10:27 +00:00
AwkwardPeak7 7560276087 bump image-decoder (#1839) 2024-03-24 21:10:27 +00:00
244 changed files with 2234 additions and 2434 deletions

View File

@ -24,7 +24,7 @@ jobs:
CI_MODULE_GEN: true
steps:
- name: Clone repo
uses: actions/checkout@v4
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
build_individual:
name: Build individual modules
@ -32,10 +32,10 @@ jobs:
runs-on: arch
steps:
- name: Checkout master branch
uses: actions/checkout@v4
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up JDK
uses: actions/setup-java@v4
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
with:
java-version: 17
distribution: temurin
@ -77,7 +77,7 @@ jobs:
path: ~/apk-artifacts
- name: Set up JDK
uses: actions/setup-java@v3
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
with:
java-version: 17
distribution: temurin

View File

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

View File

@ -29,12 +29,12 @@ class SearchMangaDto(
class BrowseManga(
private val id: Int,
private val title: String,
private val cover: String,
private val cover: String? = null,
) {
fun toSManga(createThumbnail: (String, String) -> String) = SManga.create().apply {
url = "/mangas/$id"
title = this@BrowseManga.title
thumbnail_url = createThumbnail(id.toString(), cover)
thumbnail_url = cover?.let { createThumbnail(id.toString(), cover) }
}
}
@ -58,7 +58,7 @@ class MangaDetailsDto(
@Serializable
class Manga(
private val id: Int,
private val cover: String,
private val cover: String? = null,
private val title: String,
private val summary: String? = null,
private val artists: List<NameDto>,
@ -74,7 +74,7 @@ class Manga(
) {
fun toSManga(createThumbnail: (String, String) -> String) = SManga.create().apply {
title = this@Manga.title
thumbnail_url = createThumbnail(id.toString(), cover)
thumbnail_url = cover?.let { createThumbnail(id.toString(), cover) }
artist = artists.joinToString { it.name }
author = authors.joinToString { it.name }
status = when (this@Manga.status) {
@ -105,6 +105,8 @@ class Manga(
}
val titles = listOfNotNull(synonyms, arTitle, jpTitle, enTitle)
.filterNot(String::isEmpty)
if (titles.isNotEmpty()) {
append("\n\n")
append("مسميّات أخرى")

View File

@ -0,0 +1,17 @@
genre_filter_title=Genres
status_filter_title=Status
status_all=All
status_ongoing=Ongoing
status_onhiatus=On hiatus
status_dropped=Dropped
sort_by_filter_title=Sort By
sort_by_title=Title
sort_by_views=Views
sort_by_latest=Latest
sort_by_created_at=Created at
pref_show_paid_chapter_title=Display paid chapters
pref_show_paid_chapter_summary_on=Paid chapters will appear.
pref_show_paid_chapter_summary_off=Only free chapters will be displayed.
url_changed_error=The URL of the series has changed. Migrate from %s to %s to update the URL
paid_chapter_error=Paid chapter unavailable.
id_not_found_error=Failed to get the ID for slug: %s

View File

@ -0,0 +1,17 @@
genre_filter_title=Géneros
status_filter_title=Estado
status_all=Todos
status_ongoing=En curso
status_onhiatus=En hiatus
status_dropped=Abandonada
sort_by_filter_title=Ordenar por
sort_by_title=Título
sort_by_views=Número de vistas
sort_by_latest=Recientes
sort_by_created_at=Fecha de creación
pref_show_paid_chapter_title=Mostrar capítulos de pago
pref_show_paid_chapter_summary_on=Se mostrarán capítulos de pago.
pref_show_paid_chapter_summary_off=Solo se mostrarán los capítulos gratuitos.
url_changed_error= La URL de la serie ha cambiado. Migre de %s a %s para actualizar la URL
paid_chapter_error=Capítulo no disponible.
id_not_found_error=No se pudo encontrar el ID para: %s

View File

@ -0,0 +1,13 @@
genre_filter_title=Gêneros
status_filter_title=Estado
status_all=Todos
status_ongoing=Em andamento
status_onhiatus=Em hiato
status_dropped=Cancelada
sort_by_filter_title=Ordenar por
sort_by_title=Título
sort_by_views=Visualizações
sort_by_latest=Recentes
sort_by_created_at=Data de criação
url_changed_error=A URL da série mudou. Migre de %s para %s para atualizar a URL
id_not_found_error=Falha ao obter o ID do slug: %s

View File

@ -2,4 +2,8 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 20
baseVersionCode = 21
dependencies {
api(project(":lib:i18n"))
}

View File

@ -4,31 +4,25 @@ import android.app.Application
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.lib.i18n.Intl
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
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.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 okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Locale
@ -39,35 +33,15 @@ abstract class HeanCms(
protected val apiUrl: String = baseUrl.replace("://", "://api."),
) : ConfigurableSource, HttpSource() {
private val preferences: SharedPreferences by lazy {
protected val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = SHOW_PAID_CHAPTERS_PREF
title = intl.prefShowPaidChapterTitle
summaryOn = intl.prefShowPaidChapterSummaryOn
summaryOff = intl.prefShowPaidChapterSummaryOff
setDefaultValue(SHOW_PAID_CHAPTERS_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit()
.putBoolean(SHOW_PAID_CHAPTERS_PREF, newValue as Boolean)
.commit()
}
}.also(screen::addPreference)
}
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
protected open val slugStrategy = SlugStrategy.NONE
protected open val useNewQueryEndpoint = false
private var seriesSlugMap: Map<String, HeanCmsTitle>? = null
protected open val useNewChapterEndpoint = false
/**
* Custom Json instance to make usage of `encodeDefaults`,
@ -79,9 +53,14 @@ abstract class HeanCms(
encodeDefaults = true
}
protected val intl by lazy { HeanCmsIntl(lang) }
protected val intl = Intl(
language = lang,
baseLanguage = "en",
availableLanguages = setOf("en", "pt-BR", "es"),
classLoader = this::class.java.classLoader!!,
)
protected open val coverPath: String = "cover/"
protected open val coverPath: String = ""
protected open val mangaSubDirectory: String = "series"
@ -92,29 +71,6 @@ abstract class HeanCms(
.add("Referer", "$baseUrl/")
override fun popularMangaRequest(page: Int): Request {
if (useNewQueryEndpoint) {
return newEndpointPopularMangaRequest(page)
}
val payloadObj = HeanCmsQuerySearchPayloadDto(
page = page,
order = "desc",
orderBy = "total_views",
status = "All",
type = "Comic",
)
val payload = json.encodeToString(payloadObj).toRequestBody(JSON_MEDIA_TYPE)
val apiHeaders = headersBuilder()
.add("Accept", ACCEPT_JSON)
.add("Content-Type", payload.contentType().toString())
.build()
return POST("$apiUrl/series/querysearch", apiHeaders, payload)
}
protected fun newEndpointPopularMangaRequest(page: Int): Request {
val url = "$apiUrl/query".toHttpUrl().newBuilder()
.addQueryParameter("query_string", "")
.addQueryParameter("series_status", "All")
@ -124,66 +80,14 @@ abstract class HeanCms(
.addQueryParameter("page", page.toString())
.addQueryParameter("perPage", "12")
.addQueryParameter("tags_ids", "[]")
.addQueryParameter("adult", "true")
return GET(url.build(), headers)
}
override fun popularMangaParse(response: Response): MangasPage {
val json = response.body.string()
if (json.startsWith("{")) {
val result = json.parseAs<HeanCmsQuerySearchDto>()
val mangaList = result.data.map {
if (slugStrategy != SlugStrategy.NONE) {
preferences.slugMap = preferences.slugMap.toMutableMap()
.also { map -> map[it.slug.toPermSlugIfNeeded()] = it.slug }
}
it.toSManga(apiUrl, coverPath, mangaSubDirectory, slugStrategy)
}
fetchAllTitles()
return MangasPage(mangaList, result.meta?.hasNextPage ?: false)
}
val mangaList = json.parseAs<List<HeanCmsSeriesDto>>()
.map {
if (slugStrategy != SlugStrategy.NONE) {
preferences.slugMap = preferences.slugMap.toMutableMap()
.also { map -> map[it.slug.toPermSlugIfNeeded()] = it.slug }
}
it.toSManga(apiUrl, coverPath, mangaSubDirectory, slugStrategy)
}
fetchAllTitles()
return MangasPage(mangaList, hasNextPage = false)
}
override fun popularMangaParse(response: Response) = searchMangaParse(response)
override fun latestUpdatesRequest(page: Int): Request {
if (useNewQueryEndpoint) {
return newEndpointLatestUpdatesRequest(page)
}
val payloadObj = HeanCmsQuerySearchPayloadDto(
page = page,
order = "desc",
orderBy = "latest",
status = "All",
type = "Comic",
)
val payload = json.encodeToString(payloadObj).toRequestBody(JSON_MEDIA_TYPE)
val apiHeaders = headersBuilder()
.add("Accept", ACCEPT_JSON)
.add("Content-Type", payload.contentType().toString())
.build()
return POST("$apiUrl/series/querysearch", apiHeaders, payload)
}
protected fun newEndpointLatestUpdatesRequest(page: Int): Request {
val url = "$apiUrl/query".toHttpUrl().newBuilder()
.addQueryParameter("query_string", "")
.addQueryParameter("series_status", "All")
@ -193,6 +97,7 @@ abstract class HeanCms(
.addQueryParameter("page", page.toString())
.addQueryParameter("perPage", "12")
.addQueryParameter("tags_ids", "[]")
.addQueryParameter("adult", "true")
return GET(url.build(), headers)
}
@ -206,12 +111,8 @@ abstract class HeanCms(
val slug = query.substringAfter(SEARCH_PREFIX)
val manga = SManga.create().apply {
url = if (slugStrategy != SlugStrategy.NONE) {
val mangaId = getIdBySlug(slug)
"/$mangaSubDirectory/${slug.toPermSlugIfNeeded()}#$mangaId"
} else {
"/$mangaSubDirectory/$slug"
}
val mangaId = getIdBySlug(slug)
url = "/$mangaSubDirectory/$slug#$mangaId"
}
return fetchMangaDetails(manga).map { MangasPage(listOf(it), false) }
@ -224,57 +125,12 @@ abstract class HeanCms(
val seriesDetail = json.parseAs<HeanCmsSeriesDto>()
preferences.slugMap = preferences.slugMap.toMutableMap()
.also { it[seriesDetail.slug.toPermSlugIfNeeded()] = seriesDetail.slug }
seriesDetail.id
}
return result.getOrNull() ?: throw Exception(intl.idNotFoundError + slug)
return result.getOrNull() ?: throw Exception(intl.format("id_not_found_error", slug))
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (useNewQueryEndpoint) {
return newEndpointSearchMangaRequest(page, query, filters)
}
if (query.isNotBlank()) {
val searchPayloadObj = HeanCmsSearchPayloadDto(query)
val searchPayload = json.encodeToString(searchPayloadObj)
.toRequestBody(JSON_MEDIA_TYPE)
val apiHeaders = headersBuilder()
.add("Accept", ACCEPT_JSON)
.add("Content-Type", searchPayload.contentType().toString())
.build()
return POST("$apiUrl/series/search", apiHeaders, searchPayload)
}
val sortByFilter = filters.firstInstanceOrNull<SortByFilter>()
val payloadObj = HeanCmsQuerySearchPayloadDto(
page = page,
order = if (sortByFilter?.state?.ascending == true) "asc" else "desc",
orderBy = sortByFilter?.selected ?: "total_views",
status = filters.firstInstanceOrNull<StatusFilter>()?.selected?.value ?: "Ongoing",
type = "Comic",
tagIds = filters.firstInstanceOrNull<GenreFilter>()?.state
?.filter(Genre::state)
?.map(Genre::id)
.orEmpty(),
)
val payload = json.encodeToString(payloadObj).toRequestBody(JSON_MEDIA_TYPE)
val apiHeaders = headersBuilder()
.add("Accept", ACCEPT_JSON)
.add("Content-Type", payload.contentType().toString())
.build()
return POST("$apiUrl/series/querysearch", apiHeaders, payload)
}
protected fun newEndpointSearchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val sortByFilter = filters.firstInstanceOrNull<SortByFilter>()
val statusFilter = filters.firstInstanceOrNull<StatusFilter>()
@ -292,6 +148,7 @@ abstract class HeanCms(
.addQueryParameter("page", page.toString())
.addQueryParameter("perPage", "12")
.addQueryParameter("tags_ids", tagIds)
.addQueryParameter("adult", "true")
return GET(url.build(), headers)
}
@ -299,95 +156,34 @@ abstract class HeanCms(
override fun searchMangaParse(response: Response): MangasPage {
val json = response.body.string()
if (response.request.url.pathSegments.last() == "search") {
fetchAllTitles()
val result = json.parseAs<List<HeanCmsSearchDto>>()
val mangaList = result
.filter { it.type == "Comic" }
.map {
it.slug = it.slug.toPermSlugIfNeeded()
it.toSManga(apiUrl, coverPath, mangaSubDirectory, seriesSlugMap.orEmpty(), slugStrategy)
}
return MangasPage(mangaList, false)
val result = json.parseAs<HeanCmsQuerySearchDto>()
val mangaList = result.data.map {
it.toSManga(apiUrl, coverPath, mangaSubDirectory)
}
if (json.startsWith("{")) {
val result = json.parseAs<HeanCmsQuerySearchDto>()
val mangaList = result.data.map {
if (slugStrategy != SlugStrategy.NONE) {
preferences.slugMap = preferences.slugMap.toMutableMap()
.also { map -> map[it.slug.toPermSlugIfNeeded()] = it.slug }
}
it.toSManga(apiUrl, coverPath, mangaSubDirectory, slugStrategy)
}
fetchAllTitles()
return MangasPage(mangaList, result.meta?.hasNextPage ?: false)
}
val mangaList = json.parseAs<List<HeanCmsSeriesDto>>()
.map {
if (slugStrategy != SlugStrategy.NONE) {
preferences.slugMap = preferences.slugMap.toMutableMap()
.also { map -> map[it.slug.toPermSlugIfNeeded()] = it.slug }
}
it.toSManga(apiUrl, coverPath, mangaSubDirectory, slugStrategy)
}
fetchAllTitles()
return MangasPage(mangaList, hasNextPage = false)
return MangasPage(mangaList, result.meta?.hasNextPage() ?: false)
}
override fun getMangaUrl(manga: SManga): String {
val seriesSlug = manga.url
.substringAfterLast("/")
.substringBefore("#")
.toPermSlugIfNeeded()
val currentSlug = if (slugStrategy != SlugStrategy.NONE) {
preferences.slugMap[seriesSlug] ?: seriesSlug
} else {
seriesSlug
}
return "$baseUrl/$mangaSubDirectory/$currentSlug"
return "$baseUrl/$mangaSubDirectory/$seriesSlug"
}
override fun mangaDetailsRequest(manga: SManga): Request {
if (slugStrategy != SlugStrategy.NONE && (manga.url.contains(TIMESTAMP_REGEX))) {
throw Exception(intl.urlChangedError(name))
if (!manga.url.contains("#")) {
throw Exception(intl.format("url_changed_error", name, name))
}
if (slugStrategy == SlugStrategy.ID && !manga.url.contains("#")) {
throw Exception(intl.urlChangedError(name))
}
val seriesSlug = manga.url
.substringAfterLast("/")
.substringBefore("#")
.toPermSlugIfNeeded()
val seriesId = manga.url.substringAfterLast("#")
fetchAllTitles()
val seriesDetails = seriesSlugMap?.get(seriesSlug)
val currentSlug = seriesDetails?.slug ?: seriesSlug
val currentStatus = seriesDetails?.status ?: manga.status
val apiHeaders = headersBuilder()
.add("Accept", ACCEPT_JSON)
.build()
return if (slugStrategy == SlugStrategy.ID) {
GET("$apiUrl/series/id/$seriesId", apiHeaders)
} else {
GET("$apiUrl/series/$currentSlug#$currentStatus", apiHeaders)
}
return GET("$apiUrl/series/id/$seriesId", apiHeaders)
}
override fun mangaDetailsParse(response: Response): SManga {
@ -395,14 +191,10 @@ abstract class HeanCms(
val result = runCatching { response.parseAs<HeanCmsSeriesDto>() }
val seriesResult = result.getOrNull() ?: throw Exception(intl.urlChangedError(name))
val seriesResult = result.getOrNull()
?: throw Exception(intl.format("url_changed_error", name, name))
if (slugStrategy != SlugStrategy.NONE) {
preferences.slugMap = preferences.slugMap.toMutableMap()
.also { it[seriesResult.slug.toPermSlugIfNeeded()] = seriesResult.slug }
}
val seriesDetails = seriesResult.toSManga(apiUrl, coverPath, mangaSubDirectory, slugStrategy)
val seriesDetails = seriesResult.toSManga(apiUrl, coverPath, mangaSubDirectory)
return seriesDetails.apply {
status = status.takeUnless { it == SManga.UNKNOWN }
@ -410,105 +202,97 @@ abstract class HeanCms(
}
}
override fun chapterListRequest(manga: SManga): Request = mangaDetailsRequest(manga)
override fun chapterListRequest(manga: SManga): Request {
if (useNewChapterEndpoint) {
if (!manga.url.contains("#")) {
throw Exception(intl.format("url_changed_error", name, name))
}
override fun chapterListParse(response: Response): List<SChapter> {
val result = response.parseAs<HeanCmsSeriesDto>()
val seriesId = manga.url.substringAfterLast("#")
val seriesSlug = manga.url.substringAfterLast("/").substringBefore("#")
if (slugStrategy == SlugStrategy.ID) {
preferences.slugMap = preferences.slugMap.toMutableMap()
.also { it[result.slug.toPermSlugIfNeeded()] = result.slug }
val url = "$apiUrl/chapter/query".toHttpUrl().newBuilder()
.addQueryParameter("page", "1")
.addQueryParameter("perPage", PER_PAGE_CHAPTERS.toString())
.addQueryParameter("series_id", seriesId)
.fragment(seriesSlug)
return GET(url.build(), headers)
}
val currentTimestamp = System.currentTimeMillis()
return mangaDetailsRequest(manga)
}
override fun chapterListParse(response: Response): List<SChapter> {
val showPaidChapters = preferences.showPaidChapters
if (useNewQueryEndpoint) {
return result.seasons.orEmpty()
.flatMap { it.chapters.orEmpty() }
if (useNewChapterEndpoint) {
val apiHeaders = headersBuilder()
.add("Accept", ACCEPT_JSON)
.build()
val seriesId = response.request.url.queryParameter("series_id")
val seriesSlug = response.request.url.fragment!!
var result = response.parseAs<HeanCmsChapterPayloadDto>()
val currentTimestamp = System.currentTimeMillis()
val chapterList = mutableListOf<HeanCmsChapterDto>()
chapterList.addAll(result.data)
var page = 2
while (result.meta.hasNextPage()) {
val url = "$apiUrl/chapter/query".toHttpUrl().newBuilder()
.addQueryParameter("page", page.toString())
.addQueryParameter("perPage", PER_PAGE_CHAPTERS.toString())
.addQueryParameter("series_id", seriesId)
.build()
val nextResponse = client.newCall(GET(url, apiHeaders)).execute()
result = nextResponse.parseAs<HeanCmsChapterPayloadDto>()
chapterList.addAll(result.data)
page++
}
return chapterList
.filter { it.price == 0 || showPaidChapters }
.map { it.toSChapter(result.slug, mangaSubDirectory, dateFormat, slugStrategy) }
.map { it.toSChapter(seriesSlug, mangaSubDirectory, dateFormat) }
.filter { it.date_upload <= currentTimestamp }
}
return result.chapters.orEmpty()
val result = response.parseAs<HeanCmsSeriesDto>()
val currentTimestamp = System.currentTimeMillis()
return result.seasons.orEmpty()
.flatMap { it.chapters.orEmpty() }
.filter { it.price == 0 || showPaidChapters }
.map { it.toSChapter(result.slug, mangaSubDirectory, dateFormat, slugStrategy) }
.map { it.toSChapter(result.slug, mangaSubDirectory, dateFormat) }
.filter { it.date_upload <= currentTimestamp }
.reversed()
}
override fun getChapterUrl(chapter: SChapter): String {
if (slugStrategy == SlugStrategy.NONE) return baseUrl + chapter.url
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url.substringBeforeLast("#")
val seriesSlug = chapter.url
.substringAfter("/$mangaSubDirectory/")
.substringBefore("/")
.toPermSlugIfNeeded()
val currentSlug = preferences.slugMap[seriesSlug] ?: seriesSlug
val chapterUrl = chapter.url.replaceFirst(seriesSlug, currentSlug)
return baseUrl + chapterUrl
}
override fun pageListRequest(chapter: SChapter): Request {
if (useNewQueryEndpoint) {
if (slugStrategy != SlugStrategy.NONE) {
val seriesPermSlug = chapter.url.substringAfter("/$mangaSubDirectory/").substringBefore("/")
val seriesSlug = preferences.slugMap[seriesPermSlug] ?: seriesPermSlug
val chapterUrl = chapter.url.replaceFirst(seriesPermSlug, seriesSlug)
return GET(baseUrl + chapterUrl, headers)
}
return GET(baseUrl + chapter.url, headers)
}
val chapterId = chapter.url.substringAfterLast("#").substringBefore("-paid")
val apiHeaders = headersBuilder()
.add("Accept", ACCEPT_JSON)
.build()
return GET("$apiUrl/series/chapter/$chapterId", apiHeaders)
}
override fun pageListRequest(chapter: SChapter) =
GET(apiUrl + chapter.url.replace("/$mangaSubDirectory/", "/chapter/"), headers)
override fun pageListParse(response: Response): List<Page> {
if (useNewQueryEndpoint) {
val paidChapter = response.request.url.fragment?.contains("-paid")
val result = response.parseAs<HeanCmsPagePayloadDto>()
val document = response.asJsoup()
if (result.isPaywalled()) throw Exception(intl["paid_chapter_error"])
val images = document.selectFirst("div.min-h-screen > div.container > p.items-center")
if (images == null && paidChapter == true) {
throw IOException(intl.paidChapterError)
return if (useNewChapterEndpoint) {
result.chapter.chapterData?.images.orEmpty().mapIndexed { i, img ->
Page(i, imageUrl = img)
}
return images?.select("img").orEmpty().mapIndexed { i, img ->
val imageUrl = if (img.hasClass("lazy")) img.absUrl("data-src") else img.absUrl("src")
Page(i, "", imageUrl)
} else {
result.data.orEmpty().mapIndexed { i, img ->
Page(i, imageUrl = img)
}
}
val images = response.parseAs<HeanCmsReaderDto>().content?.images.orEmpty()
val paidChapter = response.request.url.fragment?.contains("-paid")
if (images.isEmpty() && paidChapter == true) {
throw IOException(intl.paidChapterError)
}
return images.filterNot { imageUrl ->
// Their image server returns HTTP 403 for hidden files that starts
// with a dot in the file name. To avoid download errors, these are removed.
imageUrl
.removeSuffix("/")
.substringAfterLast("/")
.startsWith(".")
}
.mapIndexed { i, url ->
Page(i, imageUrl = if (url.startsWith("http")) url else "$apiUrl/$url")
}
}
override fun fetchImageUrl(page: Page): Observable<String> = Observable.just(page.imageUrl!!)
@ -523,121 +307,18 @@ abstract class HeanCms(
return GET(page.imageUrl!!, imageHeaders)
}
protected open fun fetchAllTitles() {
if (!seriesSlugMap.isNullOrEmpty() || slugStrategy != SlugStrategy.FETCH_ALL) {
return
}
val result = runCatching {
var hasNextPage = true
var page = 1
val tempMap = mutableMapOf<String, HeanCmsTitle>()
while (hasNextPage) {
val response = client.newCall(allTitlesRequest(page)).execute()
val json = response.body.string()
if (json.startsWith("{")) {
val result = json.parseAs<HeanCmsQuerySearchDto>()
tempMap.putAll(parseAllTitles(result.data))
hasNextPage = result.meta?.hasNextPage ?: false
page++
} else {
val result = json.parseAs<List<HeanCmsSeriesDto>>()
tempMap.putAll(parseAllTitles(result))
hasNextPage = false
}
}
tempMap.toMap()
}
seriesSlugMap = result.getOrNull()
preferences.slugMap = preferences.slugMap.toMutableMap()
.also { it.putAll(seriesSlugMap.orEmpty().mapValues { (_, v) -> v.slug }) }
}
protected open fun allTitlesRequest(page: Int): Request {
if (useNewQueryEndpoint) {
val url = "$apiUrl/query".toHttpUrl().newBuilder()
.addQueryParameter("series_type", "Comic")
.addQueryParameter("page", page.toString())
.addQueryParameter("perPage", PER_PAGE_MANGA_TITLES.toString())
return GET(url.build(), headers)
}
val payloadObj = HeanCmsQuerySearchPayloadDto(
page = page,
order = "desc",
orderBy = "total_views",
type = "Comic",
)
val payload = json.encodeToString(payloadObj).toRequestBody(JSON_MEDIA_TYPE)
val apiHeaders = headersBuilder()
.add("Accept", ACCEPT_JSON)
.add("Content-Type", payload.contentType().toString())
.build()
return POST("$apiUrl/series/querysearch", apiHeaders, payload)
}
protected open fun parseAllTitles(result: List<HeanCmsSeriesDto>): Map<String, HeanCmsTitle> {
return result
.filter { it.type == "Comic" }
.associateBy(
keySelector = { it.slug.replace(TIMESTAMP_REGEX, "") },
valueTransform = {
HeanCmsTitle(
slug = it.slug,
thumbnailFileName = it.thumbnail,
status = it.status?.toStatus() ?: SManga.UNKNOWN,
)
},
)
}
/**
* Used to store the current slugs for sources that change it periodically and for the
* search that doesn't return the thumbnail URLs.
*/
data class HeanCmsTitle(val slug: String, val thumbnailFileName: String, val status: Int)
/**
* Used to specify the strategy to use when fetching the slug for a manga.
* This is needed because some sources change the slug periodically.
* [NONE]: Use series_slug without changes.
* [ID]: Use series_id to fetch the slug from the API.
* IMPORTANT: [ID] is only available in the new query endpoint.
* [FETCH_ALL]: Convert the slug to a permanent slug by removing the timestamp.
* At extension start, all the slugs are fetched and stored in a map.
*/
enum class SlugStrategy {
NONE, ID, FETCH_ALL
}
private fun String.toPermSlugIfNeeded(): String {
return if (slugStrategy != SlugStrategy.NONE) {
this.replace(TIMESTAMP_REGEX, "")
} else {
this
}
}
protected open fun getStatusList(): List<Status> = listOf(
Status(intl.statusAll, "All"),
Status(intl.statusOngoing, "Ongoing"),
Status(intl.statusOnHiatus, "Hiatus"),
Status(intl.statusDropped, "Dropped"),
Status(intl["status_all"], "All"),
Status(intl["status_ongoing"], "Ongoing"),
Status(intl["status_onhiatus"], "Hiatus"),
Status(intl["status_dropped"], "Dropped"),
)
protected open fun getSortProperties(): List<SortProperty> = listOf(
SortProperty(intl.sortByTitle, "title"),
SortProperty(intl.sortByViews, "total_views"),
SortProperty(intl.sortByLatest, "latest"),
SortProperty(intl.sortByCreatedAt, "created_at"),
SortProperty(intl["sort_by_title"], "title"),
SortProperty(intl["sort_by_views"], "total_views"),
SortProperty(intl["sort_by_latest"], "latest"),
SortProperty(intl["sort_by_created_at"], "created_at"),
)
protected open fun getGenreList(): List<Genre> = emptyList()
@ -646,15 +327,24 @@ abstract class HeanCms(
val genres = getGenreList()
val filters = listOfNotNull(
Filter.Header(intl.filterWarning),
StatusFilter(intl.statusFilterTitle, getStatusList()),
SortByFilter(intl.sortByFilterTitle, getSortProperties()),
GenreFilter(intl.genreFilterTitle, genres).takeIf { genres.isNotEmpty() },
StatusFilter(intl["status_filter_title"], getStatusList()),
SortByFilter(intl["sort_by_filter_title"], getSortProperties()),
GenreFilter(intl["genre_filter_title"], genres).takeIf { genres.isNotEmpty() },
)
return FilterList(filters)
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = SHOW_PAID_CHAPTERS_PREF
title = intl["pref_show_paid_chapter_title"]
summaryOn = intl["pref_show_paid_chapter_summary_on"]
summaryOff = intl["pref_show_paid_chapter_summary_off"]
setDefaultValue(SHOW_PAID_CHAPTERS_DEFAULT)
}.also(screen::addPreference)
}
protected inline fun <reified T> Response.parseAs(): T = use {
it.body.string().parseAs()
}
@ -664,18 +354,6 @@ abstract class HeanCms(
protected inline fun <reified R> List<*>.firstInstanceOrNull(): R? =
filterIsInstance<R>().firstOrNull()
protected var SharedPreferences.slugMap: MutableMap<String, String>
get() {
val jsonMap = getString(PREF_URL_MAP_SLUG, "{}")!!
val slugMap = runCatching { json.decodeFromString<Map<String, String>>(jsonMap) }
return slugMap.getOrNull()?.toMutableMap() ?: mutableMapOf()
}
set(newSlugMap) {
edit()
.putString(PREF_URL_MAP_SLUG, json.encodeToString(newSlugMap))
.apply()
}
private val SharedPreferences.showPaidChapters: Boolean
get() = getBoolean(SHOW_PAID_CHAPTERS_PREF, SHOW_PAID_CHAPTERS_DEFAULT)
@ -683,16 +361,10 @@ abstract class HeanCms(
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 val JSON_MEDIA_TYPE = "application/json".toMediaType()
val TIMESTAMP_REGEX = """-\d{13}$""".toRegex()
private const val PER_PAGE_MANGA_TITLES = 10000
private const val PER_PAGE_CHAPTERS = 1000
const val SEARCH_PREFIX = "slug:"
private const val PREF_URL_MAP_SLUG = "pref_url_map"
private const val SHOW_PAID_CHAPTERS_PREF = "pref_show_paid_chap"
private const val SHOW_PAID_CHAPTERS_DEFAULT = false
}

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.multisrc.heancms
import eu.kanade.tachiyomi.multisrc.heancms.HeanCms.SlugStrategy
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
@ -9,59 +8,30 @@ import org.jsoup.Jsoup
import java.text.SimpleDateFormat
@Serializable
data class HeanCmsQuerySearchDto(
class HeanCmsQuerySearchDto(
val data: List<HeanCmsSeriesDto> = emptyList(),
val meta: HeanCmsQuerySearchMetaDto? = null,
)
@Serializable
data class HeanCmsQuerySearchMetaDto(
@SerialName("current_page") val currentPage: Int,
@SerialName("last_page") val lastPage: Int,
class HeanCmsQuerySearchMetaDto(
@SerialName("current_page") private val currentPage: Int,
@SerialName("last_page") private val lastPage: Int,
) {
val hasNextPage: Boolean
get() = currentPage < lastPage
fun hasNextPage() = currentPage < lastPage
}
@Serializable
data class HeanCmsSearchDto(
val description: String? = null,
@SerialName("series_slug") var slug: String,
@SerialName("series_type") val type: String,
val title: String,
val thumbnail: String? = null,
) {
fun toSManga(
apiUrl: String,
coverPath: String,
mangaSubDirectory: String,
slugMap: Map<String, HeanCms.HeanCmsTitle>,
slugStrategy: SlugStrategy,
): SManga = SManga.create().apply {
val slugOnly = slug.toPermSlugIfNeeded(slugStrategy)
val thumbnailFileName = slugMap[slugOnly]?.thumbnailFileName
title = this@HeanCmsSearchDto.title
thumbnail_url = thumbnail?.toAbsoluteThumbnailUrl(apiUrl, coverPath)
?: thumbnailFileName?.toAbsoluteThumbnailUrl(apiUrl, coverPath)
url = "/$mangaSubDirectory/$slugOnly"
}
}
@Serializable
data class HeanCmsSeriesDto(
class HeanCmsSeriesDto(
val id: Int,
@SerialName("series_slug") val slug: String,
@SerialName("series_type") val type: String = "Comic",
val author: String? = null,
val description: String? = null,
val studio: String? = null,
val status: String? = null,
val thumbnail: String,
val title: String,
val tags: List<HeanCmsTagDto>? = emptyList(),
val chapters: List<HeanCmsChapterDto>? = emptyList(),
private val author: String? = null,
private val description: String? = null,
private val studio: String? = null,
private val status: String? = null,
private val thumbnail: String,
private val title: String,
private val tags: List<HeanCmsTagDto>? = emptyList(),
val seasons: List<HeanCmsSeasonsDto>? = emptyList(),
) {
@ -69,10 +39,8 @@ data class HeanCmsSeriesDto(
apiUrl: String,
coverPath: String,
mangaSubDirectory: String,
slugStrategy: SlugStrategy,
): SManga = SManga.create().apply {
val descriptionBody = this@HeanCmsSeriesDto.description?.let(Jsoup::parseBodyFragment)
val slugOnly = slug.toPermSlugIfNeeded(slugStrategy)
title = this@HeanCmsSeriesDto.title
author = this@HeanCmsSeriesDto.author?.trim()
@ -86,89 +54,84 @@ data class HeanCmsSeriesDto(
thumbnail_url = thumbnail.ifEmpty { null }
?.toAbsoluteThumbnailUrl(apiUrl, coverPath)
status = this@HeanCmsSeriesDto.status?.toStatus() ?: SManga.UNKNOWN
url = if (slugStrategy != SlugStrategy.NONE) {
"/$mangaSubDirectory/$slugOnly#$id"
} else {
"/$mangaSubDirectory/$slug"
}
url = "/$mangaSubDirectory/$slug#$id"
}
}
@Serializable
data class HeanCmsSeasonsDto(
val index: Int,
class HeanCmsSeasonsDto(
val chapters: List<HeanCmsChapterDto>? = emptyList(),
)
@Serializable
data class HeanCmsTagDto(val name: String)
class HeanCmsTagDto(val name: String)
@Serializable
data class HeanCmsChapterDto(
val id: Int,
@SerialName("chapter_name") val name: String,
@SerialName("chapter_slug") val slug: String,
val index: String,
@SerialName("created_at") val createdAt: String,
class HeanCmsChapterPayloadDto(
val data: List<HeanCmsChapterDto>,
val meta: HeanCmsChapterMetaDto,
)
@Serializable
class HeanCmsChapterDto(
private val id: Int,
@SerialName("chapter_name") private val name: String,
@SerialName("chapter_slug") private val slug: String,
@SerialName("created_at") private val createdAt: String,
val price: Int? = null,
) {
fun toSChapter(
seriesSlug: String,
mangaSubDirectory: String,
dateFormat: SimpleDateFormat,
slugStrategy: SlugStrategy,
): SChapter = SChapter.create().apply {
val seriesSlugOnly = seriesSlug.toPermSlugIfNeeded(slugStrategy)
name = this@HeanCmsChapterDto.name.trim()
if (price != 0) {
name += " \uD83D\uDD12"
}
date_upload = runCatching { dateFormat.parse(createdAt)?.time }
.getOrNull() ?: 0L
date_upload = try {
dateFormat.parse(createdAt)?.time ?: 0L
} catch (_: Exception) {
0L
}
val paidStatus = if (price != 0 && price != null) "-paid" else ""
url = "/$mangaSubDirectory/$seriesSlugOnly/$slug#$id$paidStatus"
url = "/$mangaSubDirectory/$seriesSlug/$slug#$id"
}
}
@Serializable
data class HeanCmsReaderDto(
val content: HeanCmsReaderContentDto? = null,
class HeanCmsChapterMetaDto(
@SerialName("current_page") private val currentPage: Int,
@SerialName("last_page") private val lastPage: Int,
) {
fun hasNextPage() = currentPage < lastPage
}
@Serializable
class HeanCmsPagePayloadDto(
val chapter: HeanCmsPageDto,
private val paywall: Boolean = false,
val data: List<String>? = emptyList(),
) {
fun isPaywalled() = paywall
}
@Serializable
class HeanCmsPageDto(
@SerialName("chapter_data") val chapterData: HeanCmsPageDataDto?,
)
@Serializable
data class HeanCmsReaderContentDto(
class HeanCmsPageDataDto(
val images: List<String>? = emptyList(),
)
@Serializable
data class HeanCmsQuerySearchPayloadDto(
val order: String,
val page: Int,
@SerialName("order_by") val orderBy: String,
@SerialName("series_status") val status: String? = null,
@SerialName("series_type") val type: String,
@SerialName("tags_ids") val tagIds: List<Int> = emptyList(),
)
@Serializable
data class HeanCmsSearchPayloadDto(val term: String)
private fun String.toAbsoluteThumbnailUrl(apiUrl: String, coverPath: String): String {
return if (startsWith("https://")) this else "$apiUrl/$coverPath$this"
}
private fun String.toPermSlugIfNeeded(slugStrategy: SlugStrategy): String {
return if (slugStrategy != SlugStrategy.NONE) {
this.replace(HeanCms.TIMESTAMP_REGEX, "")
} else {
this
}
}
fun String.toStatus(): Int = when (this) {
"Ongoing" -> SManga.ONGOING
"Hiatus" -> SManga.ON_HIATUS

View File

@ -1,124 +0,0 @@
package eu.kanade.tachiyomi.multisrc.heancms
class HeanCmsIntl(lang: String) {
val availableLang: String = if (lang in AVAILABLE_LANGS) lang else ENGLISH
val genreFilterTitle: String = when (availableLang) {
BRAZILIAN_PORTUGUESE -> "Gêneros"
SPANISH -> "Géneros"
else -> "Genres"
}
val statusFilterTitle: String = when (availableLang) {
BRAZILIAN_PORTUGUESE -> "Estado"
SPANISH -> "Estado"
else -> "Status"
}
val statusAll: String = when (availableLang) {
BRAZILIAN_PORTUGUESE -> "Todos"
SPANISH -> "Todos"
else -> "All"
}
val statusOngoing: String = when (availableLang) {
BRAZILIAN_PORTUGUESE -> "Em andamento"
SPANISH -> "En curso"
else -> "Ongoing"
}
val statusOnHiatus: String = when (availableLang) {
BRAZILIAN_PORTUGUESE -> "Em hiato"
SPANISH -> "En hiatus"
else -> "On Hiatus"
}
val statusDropped: String = when (availableLang) {
BRAZILIAN_PORTUGUESE -> "Cancelada"
SPANISH -> "Abandonada"
else -> "Dropped"
}
val sortByFilterTitle: String = when (availableLang) {
BRAZILIAN_PORTUGUESE -> "Ordenar por"
SPANISH -> "Ordenar por"
else -> "Sort by"
}
val sortByTitle: String = when (availableLang) {
BRAZILIAN_PORTUGUESE -> "Título"
SPANISH -> "Titulo"
else -> "Title"
}
val sortByViews: String = when (availableLang) {
BRAZILIAN_PORTUGUESE -> "Visualizações"
SPANISH -> "Número de vistas"
else -> "Views"
}
val sortByLatest: String = when (availableLang) {
BRAZILIAN_PORTUGUESE -> "Recentes"
SPANISH -> "Recientes"
else -> "Latest"
}
val sortByCreatedAt: String = when (availableLang) {
BRAZILIAN_PORTUGUESE -> "Data de criação"
SPANISH -> "Fecha de creación"
else -> "Created at"
}
val filterWarning: String = when (availableLang) {
BRAZILIAN_PORTUGUESE -> "Os filtros serão ignorados se a busca não estiver vazia."
SPANISH -> "Los filtros serán ignorados si la búsqueda no está vacía."
else -> "Filters will be ignored if the search is not empty."
}
val prefShowPaidChapterTitle: String = when (availableLang) {
SPANISH -> "Mostrar capítulos de pago"
else -> "Display paid chapters"
}
val prefShowPaidChapterSummaryOn: String = when (availableLang) {
SPANISH -> "Se mostrarán capítulos de pago. Deberá iniciar sesión"
else -> "Paid chapters will appear. A login might be needed!"
}
val prefShowPaidChapterSummaryOff: String = when (availableLang) {
SPANISH -> "Solo se mostrarán los capítulos gratuitos"
else -> "Only free chapters will be displayed."
}
val paidChapterError: String = when (availableLang) {
SPANISH -> "Capítulo no disponible. Debe iniciar sesión en Webview y tener el capítulo comprado."
else -> "Paid chapter unavailable.\nA login/purchase might be needed (using webview)."
}
fun urlChangedError(sourceName: String): String = when (availableLang) {
BRAZILIAN_PORTUGUESE ->
"A URL da série mudou. Migre de $sourceName " +
"para $sourceName para atualizar a URL."
SPANISH ->
"La URL de la serie ha cambiado. Migre de $sourceName a " +
"$sourceName para actualizar la URL."
else ->
"The URL of the series has changed. Migrate from $sourceName " +
"to $sourceName to update the URL."
}
val idNotFoundError: String = when (availableLang) {
BRAZILIAN_PORTUGUESE -> "Falha ao obter o ID do slug: "
SPANISH -> "No se pudo encontrar el ID para: "
else -> "Failed to get the ID for slug: "
}
companion object {
const val BRAZILIAN_PORTUGUESE = "pt-BR"
const val ENGLISH = "en"
const val SPANISH = "es"
val AVAILABLE_LANGS = arrayOf(BRAZILIAN_PORTUGUESE, ENGLISH, SPANISH)
}
}

View File

@ -28,3 +28,5 @@ genre_missing_warning=Press 'Reset' to attempt to show the genres
genre_exclusion_warning=Genre exclusion is not available for all sources
project_filter_warning=NOTE: Can't be used with other filter!
project_filter_name=%s Project List page
pref_dynamic_url_title=Automatically update dynamic URLs
pref_dynamic_url_summary=Automatically update random numbers in manga URLs.\nHelps mitigating HTTP 404 errors during update and "in library" marks when browsing.\nNote: This setting may require clearing database in advanced settings and migrating all manga to the same source.

View File

@ -0,0 +1,259 @@
package eu.kanade.tachiyomi.multisrc.mangathemesia
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.lang.ref.SoftReference
import java.text.SimpleDateFormat
import java.util.Locale
abstract class MangaThemesiaAlt(
name: String,
baseUrl: String,
lang: String,
mangaUrlDirectory: String = "/manga",
dateFormat: SimpleDateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale.US),
private val randomUrlPrefKey: String = "pref_auto_random_url",
) : MangaThemesia(name, baseUrl, lang, mangaUrlDirectory, dateFormat), ConfigurableSource {
protected val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = randomUrlPrefKey
title = intl["pref_dynamic_url_title"]
summary = intl["pref_dynamic_url_summary"]
setDefaultValue(true)
}.also(screen::addPreference)
}
private fun getRandomUrlPref() = preferences.getBoolean(randomUrlPrefKey, true)
private var randomPartCache = SuspendLazy(::getUpdatedRandomPart) { preferences.randomPartCache = it }
// cache in preference for webview urls
private var SharedPreferences.randomPartCache: String
get() = getString("__random_part_cache", "")!!
set(newValue) = edit().putString("__random_part_cache", newValue).apply()
// some new titles don't have random part
// se we save their slug and when they
// finally add it, we remove the slug in the interceptor
private var SharedPreferences.titlesWithoutRandomPart: MutableSet<String>
get() {
val value = getString("titles_without_random_part", null)
?: return mutableSetOf()
return json.decodeFromString(value)
}
set(newValue) {
val encodedValue = json.encodeToString(newValue)
edit().putString("titles_without_random_part", encodedValue).apply()
}
protected open fun getRandomPartFromUrl(url: String): String {
val slug = url
.removeSuffix("/")
.substringAfterLast("/")
return slugRegex.find(slug)?.groupValues?.get(1) ?: ""
}
protected open fun getRandomPartFromResponse(response: Response): String {
return response.asJsoup()
.selectFirst(searchMangaSelector())!!
.select("a").attr("href")
.let(::getRandomPartFromUrl)
}
protected suspend fun getUpdatedRandomPart(): String =
client.newCall(GET("$baseUrl$mangaUrlDirectory/", headers))
.await()
.use(::getRandomPartFromResponse)
override fun searchMangaParse(response: Response): MangasPage {
val mp = super.searchMangaParse(response)
if (!getRandomUrlPref()) return mp
// extract random part during browsing to avoid extra call
mp.mangas.firstOrNull()?.run {
val randomPart = getRandomPartFromUrl(url)
if (randomPart.isNotEmpty()) {
randomPartCache.set(randomPart)
}
}
val mangas = mp.mangas.toPermanentMangaUrls()
return MangasPage(mangas, mp.hasNextPage)
}
protected fun List<SManga>.toPermanentMangaUrls(): List<SManga> {
// some absolutely new titles don't have the random part yet
// save them so we know where to not apply it
val foundTitlesWithoutRandomPart = mutableSetOf<String>()
for (i in indices) {
val slug = this[i].url
.removeSuffix("/")
.substringAfterLast("/")
if (slugRegex.find(slug)?.groupValues?.get(1) == null) {
foundTitlesWithoutRandomPart.add(slug)
}
val permaSlug = slug
.replaceFirst(slugRegex, "")
this[i].url = "$mangaUrlDirectory/$permaSlug/"
}
if (foundTitlesWithoutRandomPart.isNotEmpty()) {
foundTitlesWithoutRandomPart.addAll(preferences.titlesWithoutRandomPart)
preferences.titlesWithoutRandomPart = foundTitlesWithoutRandomPart
}
return this
}
protected open val slugRegex = Regex("""^(\d+-)""")
override val client = super.client.newBuilder()
.addInterceptor { chain ->
val request = chain.request()
val response = chain.proceed(request)
if (request.url.fragment != "titlesWithoutRandomPart") {
return@addInterceptor response
}
if (!response.isSuccessful && response.code == 404) {
response.close()
val slug = request.url.toString()
.substringBefore("#")
.removeSuffix("/")
.substringAfterLast("/")
preferences.titlesWithoutRandomPart.run {
remove(slug)
preferences.titlesWithoutRandomPart = this
}
val randomPart = randomPartCache.blockingGet()
val newRequest = request.newBuilder()
.url("$baseUrl$mangaUrlDirectory/$randomPart$slug/")
.build()
return@addInterceptor chain.proceed(newRequest)
}
return@addInterceptor response
}
.build()
override fun mangaDetailsRequest(manga: SManga): Request {
if (!getRandomUrlPref()) return super.mangaDetailsRequest(manga)
val slug = manga.url
.substringBefore("#")
.removeSuffix("/")
.substringAfterLast("/")
.replaceFirst(slugRegex, "")
if (preferences.titlesWithoutRandomPart.contains(slug)) {
return GET("$baseUrl${manga.url}#titlesWithoutRandomPart")
}
val randomPart = randomPartCache.blockingGet()
return GET("$baseUrl$mangaUrlDirectory/$randomPart$slug/", headers)
}
override fun getMangaUrl(manga: SManga): String {
if (!getRandomUrlPref()) return super.getMangaUrl(manga)
val slug = manga.url
.substringBefore("#")
.removeSuffix("/")
.substringAfterLast("/")
.replaceFirst(slugRegex, "")
if (preferences.titlesWithoutRandomPart.contains(slug)) {
return "$baseUrl${manga.url}"
}
val randomPart = randomPartCache.peek() ?: preferences.randomPartCache
return "$baseUrl$mangaUrlDirectory/$randomPart$slug/"
}
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
}
internal class SuspendLazy(
private val initializer: suspend () -> String,
private val saveCache: (String) -> Unit,
) {
private val mutex = Mutex()
private var cachedValue: SoftReference<String>? = null
private var fetchTime = 0L
suspend fun get(): String {
if (fetchTime + 3600000 < System.currentTimeMillis()) {
// reset cache
cachedValue = null
}
// fast way
cachedValue?.get()?.let {
return it
}
return mutex.withLock {
cachedValue?.get()?.let {
return it
}
initializer().also { set(it) }
}
}
fun set(newVal: String) {
cachedValue = SoftReference(newVal)
fetchTime = System.currentTimeMillis()
saveCache(newVal)
}
fun peek(): String? {
return cachedValue?.get()
}
fun blockingGet(): String {
return runBlocking { get() }
}
}

View File

@ -2,10 +2,8 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 2
baseVersionCode = 3
dependencies {
// Only PeachScan sources uses the image-decoder dependency.
//noinspection UseTomlInstead
compileOnly("com.github.tachiyomiorg:image-decoder:fbd6601290")
compileOnly("com.github.tachiyomiorg:image-decoder:398d3c074f")
}

View File

@ -0,0 +1,7 @@
STATUS=Status
STATUS_ALL=All
STATUS_ONGOING=Ongoing
STATUS_COMPLETED=Completed
GENRE=Genre
GENRES_RESET=Tap 'Reset' to load genres
OTHER_NAME=Alternate Name

View File

@ -0,0 +1,5 @@
STATUS=状態
STATUS_ALL=全て
STATUS_ONGOING=連載中
STATUS_COMPLETED=完結済み
GENRE=ジャンル

View File

@ -0,0 +1,7 @@
STATUS=Trạng thái
STATUS_ALL=Tất cả
STATUS_ONGOING=Đang tiến hành
STATUS_COMPLETED=Hoàn thành
GENRE=Thể loại
GENRES_RESET=Ấn 'Reset' để tải danh sách thể loại
OTHER_NAME=Tên khác

View File

@ -2,4 +2,8 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 4
baseVersionCode = 5
dependencies {
api(project(":lib:i18n"))
}

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.multisrc.wpcomics
import eu.kanade.tachiyomi.lib.i18n.Intl
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
@ -7,7 +8,10 @@ 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.Headers
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.OkHttpClient
import okhttp3.Request
@ -20,23 +24,28 @@ import java.util.Locale
abstract class WPComics(
override val name: String,
override val baseUrl: String,
override val lang: String,
private val dateFormat: SimpleDateFormat = SimpleDateFormat("HH:mm - dd/MM/yyyy Z", Locale.US),
private val gmtOffset: String? = "+0500",
final override val lang: String,
protected val dateFormat: SimpleDateFormat = SimpleDateFormat("HH:mm - dd/MM/yyyy Z", Locale.US),
protected val gmtOffset: String? = "+0500",
) : ParsedHttpSource() {
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0")
.add("Referer", baseUrl)
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
private fun List<String>.doesInclude(thisWord: String): Boolean = this.any { it.contains(thisWord, ignoreCase = true) }
open val intl = Intl(
language = lang,
baseLanguage = "en",
availableLanguages = setOf("en", "vi", "ja"),
classLoader = this::class.java.classLoader!!,
)
protected fun List<String>.doesInclude(thisWord: String): Boolean = this.any { it.contains(thisWord, ignoreCase = true) }
// Popular
open val popularPath = "hot"
override fun popularMangaRequest(page: Int): Request {
@ -58,7 +67,6 @@ abstract class WPComics(
override fun popularMangaNextPageSelector() = "a.next-page, a[rel=next]"
// Latest
override fun latestUpdatesRequest(page: Int): Request {
return GET(baseUrl + if (page > 1) "?page=$page" else "", headers)
}
@ -70,35 +78,27 @@ abstract class WPComics(
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
// Search
protected open val searchPath = "tim-truyen"
protected open val queryParam = "keyword"
protected open fun String.replaceSearchPath() = this
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val filterList = filters.let { if (it.isEmpty()) getFilterList() else it }
return if (filterList.isEmpty()) {
GET("$baseUrl/?s=$query&post_type=comics&page=$page")
} else {
val url = "$baseUrl/$searchPath".toHttpUrl().newBuilder()
val url = "$baseUrl/$searchPath".toHttpUrl().newBuilder()
filterList.forEach { filter ->
when (filter) {
is GenreFilter -> filter.toUriPart()?.let { url.addPathSegment(it) }
is StatusFilter -> filter.toUriPart()?.let { url.addQueryParameter("status", it) }
else -> {}
}
filters.forEach { filter ->
when (filter) {
is GenreFilter -> filter.toUriPart()?.let { url.addPathSegment(it) }
is StatusFilter -> filter.toUriPart()?.let { url.addQueryParameter("status", it) }
else -> {}
}
url.apply {
addQueryParameter(queryParam, query)
addQueryParameter("page", page.toString())
addQueryParameter("sort", "0")
}
GET(url.toString().replaceSearchPath(), headers)
}
url.apply {
addQueryParameter(queryParam, query)
addQueryParameter("page", page.toString())
addQueryParameter("sort", "0")
}
return GET(url.toString(), headers)
}
override fun searchMangaSelector() = "div.items div.item"
@ -116,22 +116,23 @@ abstract class WPComics(
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
// Details
override fun mangaDetailsParse(document: Document): SManga {
return SManga.create().apply {
document.select("article#item-detail").let { info ->
author = info.select("li.author p.col-xs-8").text()
status = info.select("li.status p.col-xs-8").text().toStatus()
genre = info.select("li.kind p.col-xs-8 a").joinToString { it.text() }
description = info.select("div.detail-content p").text()
val otherName = info.select("h2.other-name").text()
description = info.select("div.detail-content p").text() +
if (otherName.isNotBlank()) "\n\n ${intl["OTHER_NAME"]}: $otherName" else ""
thumbnail_url = imageOrNull(info.select("div.col-image img").first()!!)
}
}
}
open fun String?.toStatus(): Int {
val ongoingWords = listOf("Ongoing", "Updating", "Đang tiến hành")
val completedWords = listOf("Complete", "Completed", "Hoàn thành")
val ongoingWords = listOf("Ongoing", "Updating", "Đang tiến hành", "連載中")
val completedWords = listOf("Complete", "Completed", "Hoàn thành", "完結済み")
return when {
this == null -> SManga.UNKNOWN
ongoingWords.doesInclude(this) -> SManga.ONGOING
@ -141,7 +142,6 @@ abstract class WPComics(
}
// Chapters
override fun chapterListSelector() = "div.list-chapter li.row:not(.heading)"
override fun chapterFromElement(element: Element): SChapter {
@ -154,10 +154,10 @@ abstract class WPComics(
}
}
private val currentYear by lazy { Calendar.getInstance(Locale.US)[1].toString().takeLast(2) }
protected val currentYear by lazy { Calendar.getInstance(Locale.US)[1].toString().takeLast(2) }
protected fun String?.toDate(): Long {
this ?: return 0
protected open fun String?.toDate(): Long {
this ?: return 0L
val secondWords = listOf("second", "giây")
val minuteWords = listOf("minute", "phút")
@ -182,10 +182,10 @@ abstract class WPComics(
(if (gmtOffset == null) this.substringAfterLast(" ") else "$this $gmtOffset").let {
// timestamp has year
if (Regex("""\d+/\d+/\d\d""").find(it)?.value != null) {
dateFormat.parse(it)?.time ?: 0
dateFormat.parse(it)?.time ?: 0L
} else {
// MangaSum - timestamp sometimes doesn't have year (current year implied)
dateFormat.parse("$it/$currentYear")?.time ?: 0
dateFormat.parse("$it/$currentYear")?.time ?: 0L
}
}
}
@ -195,9 +195,8 @@ abstract class WPComics(
}
// Pages
// sources sometimes have an image element with an empty attr that isn't really an image
open fun imageOrNull(element: Element): String? {
// sources sometimes have an image element with an empty attr that isn't really an image
fun Element.hasValidAttr(attr: String): Boolean {
val regex = Regex("""https?://.*""", RegexOption.IGNORE_CASE)
return when {
@ -226,80 +225,74 @@ abstract class WPComics(
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
// Filters
protected class StatusFilter(name: String, pairs: List<Pair<String?, String>>) : UriPartFilter(name, pairs)
protected class StatusFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Status", vals)
protected class GenreFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Genre", vals)
protected class GenreFilter(name: String, pairs: List<Pair<String?, String>>) : UriPartFilter(name, pairs)
protected open fun getStatusList(): Array<Pair<String?, String>> = arrayOf(
Pair(null, "Tất cả"),
Pair("1", "Đang tiến hành"),
Pair("2", "Đã hoàn thành"),
Pair("3", "Tạm ngừng"),
)
protected open fun getGenreList(): Array<Pair<String?, String>> = arrayOf(
null to "Tất cả",
"action" to "Action",
"adult" to "Adult",
"adventure" to "Adventure",
"anime" to "Anime",
"chuyen-sinh" to "Chuyển Sinh",
"comedy" to "Comedy",
"comic" to "Comic",
"cooking" to "Cooking",
"co-dai" to "Cổ Đại",
"doujinshi" to "Doujinshi",
"drama" to "Drama",
"dam-my" to "Đam Mỹ",
"ecchi" to "Ecchi",
"fantasy" to "Fantasy",
"gender-bender" to "Gender Bender",
"harem" to "Harem",
"historical" to "Historical",
"horror" to "Horror",
"josei" to "Josei",
"live-action" to "Live action",
"manga" to "Manga",
"manhua" to "Manhua",
"manhwa" to "Manhwa",
"martial-arts" to "Martial Arts",
"mature" to "Mature",
"mecha" to "Mecha",
"mystery" to "Mystery",
"ngon-tinh" to "Ngôn Tình",
"one-shot" to "One shot",
"psychological" to "Psychological",
"romance" to "Romance",
"school-life" to "School Life",
"sci-fi" to "Sci-fi",
"seinen" to "Seinen",
"shoujo" to "Shoujo",
"shoujo-ai" to "Shoujo Ai",
"shounen" to "Shounen",
"shounen-ai" to "Shounen Ai",
"slice-of-life" to "Slice of Life",
"smut" to "Smut",
"soft-yaoi" to "Soft Yaoi",
"soft-yuri" to "Soft Yuri",
"sports" to "Sports",
"supernatural" to "Supernatural",
"thieu-nhi" to "Thiếu Nhi",
"tragedy" to "Tragedy",
"trinh-tham" to "Trinh Thám",
"truyen-scan" to "Truyện scan",
"truyen-mau" to "Truyện Màu",
"webtoon" to "Webtoon",
"xuyen-khong" to "Xuyên Không",
)
protected open fun getStatusList(): List<Pair<String?, String>> =
listOf(
Pair(null, intl["STATUS_ALL"]),
Pair("1", intl["STATUS_ONGOING"]),
Pair("2", intl["STATUS_COMPLETED"]),
)
protected var genreList: List<Pair<String?, String>> = emptyList()
private val scope = CoroutineScope(Dispatchers.IO)
protected fun launchIO(block: () -> Unit) = scope.launch { block() }
private var fetchGenresAttempts: Int = 0
protected fun fetchGenres() {
if (fetchGenresAttempts < 3 && genreList.isEmpty()) {
try {
genreList =
client.newCall(genresRequest()).execute()
.asJsoup()
.let(::parseGenres)
} catch (_: Exception) {
} finally {
fetchGenresAttempts++
}
}
}
protected open fun genresRequest() = GET("$baseUrl/$searchPath", headers)
protected open val genresSelector = ".genres ul.nav li:not(.active) a"
protected open val genresUrlDelimiter = "/"
protected open fun parseGenres(document: Document): List<Pair<String?, String>> {
val items = document.select(genresSelector)
return buildList(items.size + 1) {
add(Pair(null, intl["STATUS_ALL"]))
items.mapTo(this) {
Pair(
it.attr("href")
.removeSuffix("/")
.substringAfterLast(genresUrlDelimiter),
it.text(),
)
}
}
}
override fun getFilterList(): FilterList {
launchIO { fetchGenres() }
return FilterList(
StatusFilter(getStatusList()),
GenreFilter(getGenreList()),
StatusFilter(intl["STATUS"], getStatusList()),
if (genreList.isEmpty()) {
Filter.Header(intl["GENRES_RESET"])
} else {
GenreFilter(intl["GENRE"], genreList)
},
)
}
protected open class UriPartFilter(displayName: String, val vals: Array<Pair<String?, String>>) :
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray()) {
fun toUriPart() = vals[state].first
protected open class UriPartFilter(displayName: String, private val pairs: List<Pair<String?, String>>) :
Filter.Select<String>(displayName, pairs.map { it.second }.toTypedArray()) {
fun toUriPart() = pairs[state].first
}
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 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,219 @@
package eu.kanade.tachiyomi.extension.ar.arabshentai
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit
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 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 org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
class ArabsHentai : ParsedHttpSource() {
override val name = "هنتاي العرب"
override val baseUrl = "https://arabshentai.com"
override val lang = "ar"
private val dateFormat = SimpleDateFormat("d MMM\u060c yyy", Locale("ar"))
override val supportsLatest = true
override val client =
network.cloudflareClient.newBuilder()
.rateLimit(2)
.build()
override fun headersBuilder() =
super.headersBuilder()
.set("Referer", "$baseUrl/")
.set("Origin", baseUrl)
// ============================== Popular ===============================
override fun popularMangaRequest(page: Int) = GET("$baseUrl/manga/page/$page/?orderby=new-manga", headers)
override fun popularMangaSelector() = "#archive-content .wp-manga"
override fun popularMangaFromElement(element: Element) =
SManga.create().apply {
element.selectFirst(".data h3 a")!!.run {
setUrlWithoutDomain(absUrl("href"))
title = text()
}
thumbnail_url = element.selectFirst("a .poster img")?.imgAttr()
}
override fun popularMangaNextPageSelector() = ".pagination a.arrow_pag i#nextpagination"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/manga/page/$page/?orderby=new_chapter", 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/page/$page/".toHttpUrl().newBuilder()
url.addQueryParameter("s", query)
filters.forEach { filter ->
when (filter) {
is GenresOpFilter -> url.addQueryParameter("op", filter.toUriPart())
is GenresFilter ->
filter.state
.filter { it.state }
.forEach { url.addQueryParameter("genre[]", it.uriPart) }
is StatusFilter ->
filter.state
.filter { it.state }
.forEach { url.addQueryParameter("status[]", it.uriPart) }
else -> {}
}
}
return GET(url.build(), headers)
}
override fun searchMangaSelector() = ".search-page .result-item article"
override fun searchMangaFromElement(element: Element) =
SManga.create().apply {
element.selectFirst(".details .title")!!.run {
setUrlWithoutDomain(selectFirst("a")!!.absUrl("href"))
title = ownText()
}
thumbnail_url = element.selectFirst(".image .thumbnail a img")?.imgAttr()
}
override fun searchMangaNextPageSelector() = ".pagination span.current + a"
// =========================== Manga Details ============================
override fun mangaDetailsParse(document: Document) =
SManga.create().apply {
document.selectFirst(".content")!!.run {
title = selectFirst(".sheader .data h1")!!.text()
thumbnail_url = selectFirst(".sheader .poster img")?.imgAttr()
val genres = mutableListOf<String>()
selectFirst("#manga-info")?.run {
description = "\u061C" + select(".wp-content p").text() +
"\n" + "أسماء أُخرى: " + select("div b:contains(أسماء أُخرى) + span").text()
status = select("div b:contains(حالة المانجا) + span").text().parseStatus()
author = select("div b:contains(الكاتب) + span a").text()
artist = select("div b:contains(الرسام) + span a").text()
genres += select("div b:contains(نوع العمل) + span a").text()
}
genres += select(".data .sgeneros a").map { it.text() }
genre = genres.joinToString()
}
}
private fun String?.parseStatus() =
when {
this == null -> SManga.UNKNOWN
this.contains("مستمر", ignoreCase = true) -> SManga.ONGOING
this.contains("مكتمل", ignoreCase = true) -> SManga.COMPLETED
this.contains("متوقف", ignoreCase = true) -> SManga.ON_HIATUS
this.contains("ملغية", ignoreCase = true) -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
// ============================== Chapters ==============================
override fun chapterListSelector() = "#chapter-list a[href*='/manga/'], .oneshot-reader .images .image-item a[href$='manga-paged=1']"
override fun chapterFromElement(element: Element) =
SChapter.create().apply {
val url = element.attr("href")
if (url.contains("style=paged")) {
setUrlWithoutDomain(url.substringBeforeLast("?"))
name = "ونشوت"
date_upload = 0L
} else {
name = element.select(".chapternum").text()
date_upload = element.select(".chapterdate").text().parseChapterDate()
setUrlWithoutDomain(url)
}
}
private fun String?.parseChapterDate(): Long {
if (this == null) return 0L
return try {
dateFormat.parse(this)!!.time
} catch (_: ParseException) {
0L
}
}
// =============================== Pages ================================
override fun pageListParse(document: Document): List<Page> {
return document.select(".chapter_image img.wp-manga-chapter-img").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 {
launchIO { fetchGenres() }
return FilterList(
GenresFilter(),
GenresOpFilter(),
StatusFilter(),
)
}
private val scope = CoroutineScope(Dispatchers.IO)
private fun launchIO(block: () -> Unit) = scope.launch { block() }
private var fetchGenresAttempts: Int = 0
private fun fetchGenres() {
if (fetchGenresAttempts < 3 && genreList.isEmpty()) {
try {
genreList = client.newCall(genresRequest()).execute()
.asJsoup()
.let(::parseGenres)
} catch (_: Exception) {
} finally {
fetchGenresAttempts++
}
}
}
private fun genresRequest(): Request {
return GET("$baseUrl/%d8%aa%d8%b5%d9%86%d9%8a%d9%81%d8%a7%d8%aa", headers)
}
private fun parseGenres(document: Document): List<Pair<String, String>> {
val items = document.select("#archive-content ul.genre-list li.item-genre .genre-data a")
return buildList(items.size) {
items.mapTo(this) {
val value = it.ownText()
Pair(value, value)
}
}
}
}

View File

@ -0,0 +1,34 @@
package eu.kanade.tachiyomi.extension.ar.arabshentai
import eu.kanade.tachiyomi.source.model.Filter
class StatusFilter :
Filter.Group<FilterCheckbox>(
"الحالة",
arrayOf(
Pair("مستمرة", "on-going"),
Pair("مكتملة", "end"),
Pair("ملغية", "canceled"),
Pair("متوقفة حالياً", "on-hold"),
).map { FilterCheckbox(it.first, it.second) },
)
internal var genreList: List<Pair<String, String>> = emptyList()
class FilterCheckbox(name: String, val uriPart: String) : Filter.CheckBox(name)
class GenresFilter :
Filter.Group<FilterCheckbox>("التصنيفات", genreList.map { FilterCheckbox(it.first, it.second) })
class GenresOpFilter : UriPartFilter(
"شرط التصنيفات",
arrayOf(
Pair("يحتوي على إحدى التصنيفات المدرجة", ""),
Pair("يحتوي على جميع التصنيفات المدرجة", "1"),
),
)
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,9 +1,9 @@
ext {
extName = 'Beast Scans'
extClass = '.BeastScans'
extName = 'Umi Manga'
extClass = '.UmiManga'
themePkg = 'mangathemesia'
baseUrl = 'https://beastscans.net'
overrideVersionCode = 1
baseUrl = 'https://www.umimanga.com'
overrideVersionCode = 2
}
apply from: "$rootDir/common.gradle"

View File

@ -1,31 +0,0 @@
package eu.kanade.tachiyomi.extension.ar.beastscans
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import eu.kanade.tachiyomi.source.model.SManga
import java.text.SimpleDateFormat
import java.util.Locale
class BeastScans : MangaThemesia(
"Beast Scans",
"https://beastscans.net",
"ar",
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
) {
override val seriesArtistSelector =
".infox .fmed:contains(الرسام) span, ${super.seriesArtistSelector}"
override val seriesAuthorSelector =
".infox .fmed:contains(المؤلف) span, ${super.seriesAuthorSelector}"
override val seriesStatusSelector =
".tsinfo .imptdt:contains(الحالة) i, ${super.seriesStatusSelector}"
override val seriesTypeSelector =
".tsinfo .imptdt:contains(النوع) i, ${super.seriesTypeSelector}"
override fun String?.parseStatus() = when {
this == null -> SManga.UNKNOWN
listOf("مستمر", "ongoing", "publishing").any { this.contains(it, ignoreCase = true) } -> SManga.ONGOING
listOf("متوقف", "hiatus").any { this.contains(it, ignoreCase = true) } -> SManga.ON_HIATUS
listOf("مكتمل", "completed").any { this.contains(it, ignoreCase = true) } -> SManga.COMPLETED
listOf("dropped", "cancelled").any { this.contains(it, ignoreCase = true) } -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
}

View File

@ -0,0 +1,20 @@
package eu.kanade.tachiyomi.extension.ar.beastscans
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.TimeUnit
class UmiManga : MangaThemesia(
"Umi Manga",
"https://www.umimanga.com",
"ar",
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
) {
// Beast Scans -> Umi Manga
override val id = 6404296554681042513
override val client = network.cloudflareClient.newBuilder()
.readTimeout(3, TimeUnit.MINUTES)
.build()
}

View File

@ -0,0 +1,9 @@
ext {
extName = 'GMANGA (unoriginal)'
extClass = '.GmangaSite'
themePkg = 'madara'
baseUrl = 'https://gmanga.site'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,16 @@
package eu.kanade.tachiyomi.extension.ar.gmangasite
import eu.kanade.tachiyomi.multisrc.madara.Madara
import java.text.SimpleDateFormat
import java.util.Locale
class GmangaSite : Madara(
"GMANGA (unoriginal)",
"https://gmanga.site",
"ar",
dateFormat = SimpleDateFormat("MMMM dd، yyyy", Locale("ar")),
) {
override val chapterUrlSuffix = ""
override val useLoadMoreRequest = LoadMoreStrategy.Always
override val useNewChapterEndpoint = true
}

View File

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

View File

@ -48,10 +48,10 @@ class HentaiSlayer : ParsedHttpSource(), ConfigurableSource {
override fun popularMangaSelector() = "div > div:has(div#card-real)"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
element.selectFirst("div#card-real a")?.run {
with(element.selectFirst("div#card-real a")!!) {
setUrlWithoutDomain(absUrl("href"))
selectFirst("figure")?.run {
selectFirst("img.object-cover")?.run {
with(selectFirst("figure")!!) {
with(selectFirst("img.object-cover")!!) {
thumbnail_url = imgAttr()
title = attr("alt")
}
@ -98,21 +98,17 @@ class HentaiSlayer : ParsedHttpSource(), ConfigurableSource {
// =========================== Manga Details ============================
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
document.selectFirst("main section")?.run {
selectFirst("img#manga-cover")?.run {
thumbnail_url = imgAttr()
title = attr("alt")
}
selectFirst("section > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div:nth-child(2)")?.run {
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()
}
selectFirst("section > div:nth-child(1) > div:nth-child(2)")?.run {
select("h1").text().takeIf { it.isNotEmpty() }?.let {
title = it
}
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 {
@ -120,10 +116,10 @@ class HentaiSlayer : ParsedHttpSource(), ConfigurableSource {
}
.joinToString()
select("h2").text().takeIf { it.isNotEmpty() }?.let {
description = "Alternative name: $it\n"
desc += "أسماء أُخرى: $it\n"
}
}
description += select("#description").text()
description = desc + select("#description").text()
}
}

View File

@ -1,8 +1,8 @@
ext {
extName = 'Hijala'
extClass = '.Hijala'
themePkg = 'zeistmanga'
baseUrl = 'https://hijala.blogspot.com'
themePkg = 'mangathemesia'
baseUrl = 'https://www.hijala.com'
overrideVersionCode = 0
}

View File

@ -1,57 +1,15 @@
package eu.kanade.tachiyomi.extension.ar.hijala
import eu.kanade.tachiyomi.multisrc.zeistmanga.Genre
import eu.kanade.tachiyomi.multisrc.zeistmanga.ZeistManga
import eu.kanade.tachiyomi.source.model.MangasPage
import okhttp3.Request
import okhttp3.Response
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import java.text.SimpleDateFormat
import java.util.Locale
class Hijala : ZeistManga("Hijala", "https://hijala.blogspot.com", "ar") {
override val hasFilters = true
override val hasLanguageFilter = false
override val supportsLatest = false
override fun popularMangaRequest(page: Int): Request = latestUpdatesRequest(page)
override fun popularMangaParse(response: Response): MangasPage = latestUpdatesParse(response)
override fun getGenreList(): List<Genre> = listOf(
Genre("أكشن", "Action"),
Genre("أثارة", "Thriller"),
Genre("أتشي", "Ecchi"),
Genre("حياة مدرسية", "School Life"),
Genre("تاريخي", "Historical"),
Genre("ألعاب", "Game"),
Genre("خيال علمي", "Sci-Fi"),
Genre("خيال", "Fantasy"),
Genre("خارق للطبيعة", "Supernatural"),
Genre("رومانسي", "Romance"),
Genre("رعب", "Horror"),
Genre("دراما", "Drama"),
Genre("سينين", "Seinen"),
Genre("سحري", "Magic"),
Genre("رياضي", "Sports"),
Genre("شونين", "Shounen"),
Genre("شوجو", "Shoujo"),
Genre("شريحة من الحياة", "Slice of Life"),
Genre("علاجي", "Medical"),
Genre("عسكري", "Military"),
Genre("طبخ", "Cooking"),
Genre("فنون قتال", "Martial Arts"),
Genre("غموض", "Mystery"),
Genre("عوالم متعددة", "Isekai"),
Genre("مانها", "مانها"),
Genre("مأساوي", "Tragedy"),
Genre("كوميديا", "Comedy"),
Genre("مغامرات", "Adventure"),
Genre("مصاص دماء", "مصاص دماء"),
Genre("مانهوا", "مانهوا"),
Genre("موسيقي", "موسيقي"),
Genre("موسيقى", "Music"),
Genre("مغامرات", "مغامرات"),
Genre("نفسي", "نفسي"),
Genre("نفسي", "Psychological"),
Genre("ميكا", "ميكا"),
)
class Hijala : MangaThemesia(
"Hijala",
"https://www.hijala.com",
"ar",
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
) {
// Site moved from ZeistManga to MangaThemesia
override val versionId get() = 2
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

View File

@ -1,10 +0,0 @@
package eu.kanade.tachiyomi.extension.ar.kingofshojo
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import java.text.SimpleDateFormat
import java.util.Locale
class KingofShojo : MangaThemesia("King of Shojo", "https://kingofshojo.com", "ar", dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar"))) {
override val hasProjectPage = true
}

View File

@ -2,8 +2,8 @@ ext {
extName = 'Manga Flame'
extClass = '.MangaFlame'
themePkg = 'mangathemesia'
baseUrl = 'https://mangaflame.org'
overrideVersionCode = 1
baseUrl = 'https://arisescans.com'
overrideVersionCode = 2
}
apply from: "$rootDir/common.gradle"

View File

@ -1,14 +1,20 @@
package eu.kanade.tachiyomi.extension.ar.mangaflame
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import okhttp3.OkHttpClient
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.TimeUnit
class MangaFlame : MangaThemesia(
"Manga Flame",
"https://mangaflame.org",
"https://arisescans.com",
"ar",
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
) {
override val id = 1501237443119573205
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.readTimeout(3, TimeUnit.MINUTES)
.build()
}

View File

@ -2,8 +2,8 @@ ext {
extName = 'Mangalek'
extClass = '.Mangalek'
themePkg = 'madara'
baseUrl = 'https://manga-lek.net'
overrideVersionCode = 4
baseUrl = 'https://lekmanga.net'
overrideVersionCode = 5
}
apply from: "$rootDir/common.gradle"

View File

@ -21,10 +21,11 @@ import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
private const val mangalekUrl = "https://lekmanga.net"
class Mangalek :
Madara(
"مانجا ليك",
"https://manga-lek.net",
mangalekUrl,
"ar",
SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
),
@ -34,7 +35,7 @@ class Mangalek :
override val useLoadMoreRequest = LoadMoreStrategy.Always
override val chapterUrlSuffix = ""
private val defaultBaseUrl = "https://manga-lek.net"
private val defaultBaseUrl = mangalekUrl
override val baseUrl by lazy { getPrefBaseUrl() }
private val preferences: SharedPreferences by lazy {

View File

@ -2,8 +2,9 @@ ext {
extName = 'MangaLionz'
extClass = '.MangaLionz'
themePkg = 'madara'
baseUrl = 'https://mangalionz.org'
overrideVersionCode = 3
baseUrl = 'https://manga-lionz.com'
overrideVersionCode = 4
isNsfw = false
}
apply from: "$rootDir/common.gradle"

View File

@ -3,8 +3,15 @@ package eu.kanade.tachiyomi.extension.ar.mangalionz
import eu.kanade.tachiyomi.multisrc.madara.Madara
import eu.kanade.tachiyomi.source.model.SManga
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.Locale
class MangaLionz : Madara("MangaLionz", "https://mangalionz.org", "ar") {
class MangaLionz : Madara(
"MangaLionz",
"https://manga-lionz.com",
"ar",
dateFormat = SimpleDateFormat("MMMM d, yyyy", Locale("ar")),
) {
override val useLoadMoreRequest = LoadMoreStrategy.Always
override fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create()

View File

@ -2,8 +2,8 @@ ext {
extName = 'MangaSpark'
extClass = '.MangaSpark'
themePkg = 'madara'
baseUrl = 'https://mangaspark.org'
overrideVersionCode = 5
baseUrl = 'https://manga-spark.net'
overrideVersionCode = 6
}
apply from: "$rootDir/common.gradle"

View File

@ -6,7 +6,7 @@ import java.util.Locale
class MangaSpark : Madara(
"MangaSpark",
"https://mangaspark.org",
"https://manga-spark.net",
"ar",
dateFormat = SimpleDateFormat("d MMMM، yyyy", Locale("ar")),
) {

View File

@ -2,8 +2,8 @@ ext {
extName = 'Manga Starz'
extClass = '.MangaStarz'
themePkg = 'madara'
baseUrl = 'https://mangastarz.org'
overrideVersionCode = 6
baseUrl = 'https://manga-starz.com'
overrideVersionCode = 7
}
apply from: "$rootDir/common.gradle"

View File

@ -6,7 +6,7 @@ import java.util.Locale
class MangaStarz : Madara(
"Manga Starz",
"https://mangastarz.org",
"https://manga-starz.com",
"ar",
dateFormat = SimpleDateFormat("d MMMM، yyyy", Locale("ar")),
) {

View File

@ -0,0 +1,9 @@
ext {
extName = 'Manga Time'
extClass = '.MangaTime'
themePkg = 'madara'
baseUrl = 'https://anime-time.net'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,14 @@
package eu.kanade.tachiyomi.extension.ar.mangatime
import eu.kanade.tachiyomi.multisrc.madara.Madara
import java.text.SimpleDateFormat
import java.util.Locale
class MangaTime : Madara(
"Manga Time",
"https://anime-time.net",
"ar",
dateFormat = SimpleDateFormat("dd MMMM، yyyy", Locale("ar")),
) {
override val useNewChapterEndpoint = true
}

View File

@ -1,9 +1,9 @@
ext {
extName = 'Ozul Scans'
extClass = '.OzulScans'
extName = 'King Of Manga'
extClass = '.KingOfManga'
themePkg = 'mangathemesia'
baseUrl = 'https://kingofmanga.com'
overrideVersionCode = 2
baseUrl = 'https://king-ofmanga.com'
overrideVersionCode = 4
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,15 @@
package eu.kanade.tachiyomi.extension.ar.ozulscans
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesiaAlt
import java.text.SimpleDateFormat
import java.util.Locale
class KingOfManga : MangaThemesiaAlt(
"King Of Manga",
"https://king-ofmanga.com",
"ar",
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
) {
// Ozul Scans -> King of Manga
override val id = 3453769904666687440
}

View File

@ -1,12 +0,0 @@
package eu.kanade.tachiyomi.extension.ar.ozulscans
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import java.text.SimpleDateFormat
import java.util.Locale
class OzulScans : MangaThemesia(
"Ozul Scans",
"https://kingofmanga.com",
"ar",
dateFormat = SimpleDateFormat("MMM d, yyy", Locale("ar")),
)

View File

@ -1,7 +1,7 @@
ext {
extName = 'Anchira'
extClass = '.Anchira'
extVersionCode = 10
extVersionCode = 11
isNsfw = true
}

View File

@ -120,11 +120,6 @@ class Anchira : HttpSource(), ConfigurableSource {
query.substringAfter(SLUG_BUNDLE_PREFIX),
filters,
).removeAllQueryParameters("page")
if (
url.build().queryParameter("sort") == "4"
) {
url.removeAllQueryParameters("sort")
}
val manga = SManga.create()
.apply { this.url = "?${url.build().query}" }
fetchMangaDetails(manga).map {
@ -280,7 +275,7 @@ class Anchira : HttpSource(), ConfigurableSource {
for (page in 1..pages) {
results.entries.forEach { data ->
chapterList.add(
createChapter(data, response, anchiraData),
createChapter(data, anchiraData),
)
}
if (page < pages) {
@ -298,7 +293,7 @@ class Anchira : HttpSource(), ConfigurableSource {
} else {
val data = json.decodeFromString<Entry>(response.body.string())
chapterList.add(
createChapter(data, response, anchiraData),
createChapter(data, anchiraData),
)
}
return chapterList
@ -468,7 +463,7 @@ class Anchira : HttpSource(), ConfigurableSource {
companion object {
const val SLUG_SEARCH_PREFIX = "id:"
const val SLUG_BUNDLE_PREFIX = "bundle:"
private const val SLUG_BUNDLE_PREFIX = "bundle:"
private const val IMAGE_QUALITY_PREF = "image_quality"
private const val OPEN_SOURCE_PREF = "use_manga_source"
private const val USE_TAG_GROUPING = "use_tag_grouping"
@ -477,4 +472,5 @@ class Anchira : HttpSource(), ConfigurableSource {
}
}
val CHAPTER_SUFFIX_RE = Regex("(?<!20\\d\\d-)\\b[\\d.]{1,4}$")
val CHAPTER_SUFFIX_RE =
Regex("\\W*(?:Ch\\.?|Chapter|Part|Vol\\.?|Volume|#)?\\W?(?<!20\\d{2}-?)\\b[\\d.]{1,4}\\W?")

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.extension.en.anchira
import eu.kanade.tachiyomi.source.model.SChapter
import okhttp3.Response
import java.util.Locale
object AnchiraHelper {
@ -13,6 +12,7 @@ object AnchiraHelper {
}
it
}
.sortedBy { it.name }
.sortedBy { it.namespace }
.map {
val tag = it.name.lowercase()
@ -30,30 +30,31 @@ object AnchiraHelper {
}
.joinToString(", ") { it }
fun createChapter(entry: Entry, response: Response, anchiraData: List<EntryKey>) =
fun createChapter(entry: Entry, anchiraData: List<EntryKey>) =
SChapter.create().apply {
val ch =
CHAPTER_SUFFIX_RE.find(entry.title)?.value?.trim('.') ?: "1"
val source = anchiraData.find { it.id == entry.id }?.url
?: response.request.url.toString()
val chSuffix = CHAPTER_SUFFIX_RE.find(entry.title)?.value.orEmpty()
val chNumber =
chSuffix.replace(Regex("[^.\\d]"), "").trim('.').takeUnless { it.isEmpty() } ?: "1"
val source = Regex("fakku|irodori").find(
anchiraData.find { it.id == entry.id }?.url.orEmpty(),
)?.value.orEmpty().titleCase()
url = "/g/${entry.id}/${entry.key}"
name = "$ch. ${entry.title.removeSuffix(" $ch")}"
name = "$chNumber. ${entry.title.removeSuffix(chSuffix)}"
date_upload = entry.publishedAt * 1000
chapter_number = ch.toFloat()
chapter_number = chNumber.toFloat()
scanlator = buildString {
append(
Regex("fakku|irodori|anchira").find(source)?.value.orEmpty()
.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(
Locale.getDefault(),
)
} else {
it.toString()
}
},
)
append(" - ${entry.pages} pages")
if (source.isNotEmpty()) {
append("$source - ")
}
append("${entry.pages} pages")
}
}
private fun String.titleCase() = replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(Locale.getDefault())
} else {
it.toString()
}
}
}

View File

@ -3,7 +3,7 @@ ext {
extClass = '.AsuraScans'
themePkg = 'mangathemesia'
baseUrl = 'https://asuratoon.com'
overrideVersionCode = 1
overrideVersionCode = 3
}
apply from: "$rootDir/common.gradle"

View File

@ -1,53 +1,35 @@
package eu.kanade.tachiyomi.extension.en.asurascans
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesiaAlt
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
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 kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import okhttp3.Interceptor
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 java.io.IOException
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.TimeUnit
class AsuraScans :
MangaThemesia(
"Asura Scans",
"https://asuratoon.com",
"en",
dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.US),
),
ConfigurableSource {
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
class AsuraScans : MangaThemesiaAlt(
"Asura Scans",
"https://asuratoon.com",
"en",
dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.US),
randomUrlPrefKey = "pref_permanent_manga_url_2_en",
) {
init {
// remove legacy preferences
preferences.run {
if (contains("pref_url_map")) {
edit().remove("pref_url_map").apply()
}
if (contains("pref_base_url_host")) {
edit().remove("pref_base_url_host").apply()
}
}
}
override val baseUrl by lazy {
preferences.baseUrlHost.let { "https://$it" }
}
override val client: OkHttpClient = super.client.newBuilder()
.addInterceptor(::urlChangeInterceptor)
.addInterceptor(::domainChangeIntercept)
.rateLimit(1, 3, TimeUnit.SECONDS)
override val client = super.client.newBuilder()
.rateLimit(1, 3)
.apply {
val interceptors = interceptors()
val index = interceptors.indexOfFirst { "Brotli" in it.javaClass.simpleName }
@ -64,19 +46,6 @@ class AsuraScans :
override val pageSelector = "div.rdminimal > img, div.rdminimal > p > img, div.rdminimal > a > img, div.rdminimal > p > a > img, " +
"div.rdminimal > noscript > img, div.rdminimal > p > noscript > img, div.rdminimal > a > noscript > img, div.rdminimal > p > a > noscript > img"
// Permanent Url for Manga/Chapter End
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return super.fetchPopularManga(page).tempUrlToPermIfNeeded()
}
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return super.fetchLatestUpdates(page).tempUrlToPermIfNeeded()
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return super.fetchSearchManga(page, query, filters).tempUrlToPermIfNeeded()
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val request = super.searchMangaRequest(page, query, filters)
if (query.isBlank()) return request
@ -93,232 +62,10 @@ class AsuraScans :
.build()
}
// Temp Url for manga/chapter
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val newManga = manga.titleToUrlFrag()
return super.fetchChapterList(newManga)
}
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
val newManga = manga.titleToUrlFrag()
return super.fetchMangaDetails(newManga)
}
override fun getMangaUrl(manga: SManga): String {
val dbSlug = manga.url
.substringBefore("#")
.removeSuffix("/")
.substringAfterLast("/")
val storedSlug = preferences.slugMap[dbSlug] ?: dbSlug
return "$baseUrl$mangaUrlDirectory/$storedSlug/"
}
// Skip scriptPages
override fun pageListParse(document: Document): List<Page> {
return document.select(pageSelector)
.filterNot { it.attr("src").isNullOrEmpty() }
.mapIndexed { i, img -> Page(i, document.location(), img.attr("abs:src")) }
}
private fun Observable<MangasPage>.tempUrlToPermIfNeeded(): Observable<MangasPage> {
return this.map { mangasPage ->
MangasPage(
mangasPage.mangas.map { it.tempUrlToPermIfNeeded() },
mangasPage.hasNextPage,
)
}
}
private fun SManga.tempUrlToPermIfNeeded(): SManga {
if (!preferences.permaUrlPref) return this
val slugMap = preferences.slugMap
val sMangaTitleFirstWord = this.title.split(" ")[0]
if (!this.url.contains("/$sMangaTitleFirstWord", ignoreCase = true)) {
val currentSlug = this.url
.removeSuffix("/")
.substringAfterLast("/")
val permaSlug = currentSlug.replaceFirst(TEMP_TO_PERM_REGEX, "")
slugMap[permaSlug] = currentSlug
this.url = "$mangaUrlDirectory/$permaSlug/"
}
preferences.slugMap = slugMap
return this
}
private fun SManga.titleToUrlFrag(): SManga {
return try {
this.apply {
url = "$url#${title.toSearchQuery()}"
}
} catch (e: UninitializedPropertyAccessException) {
// when called from deep link, title is not present
this
}
}
private fun urlChangeInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request()
val frag = request.url.fragment
if (frag.isNullOrEmpty()) {
return chain.proceed(request)
}
val dbSlug = request.url.toString()
.substringBefore("#")
.removeSuffix("/")
.substringAfterLast("/")
val slugMap = preferences.slugMap
val storedSlug = slugMap[dbSlug] ?: dbSlug
val response = chain.proceed(
request.newBuilder()
.url("$baseUrl$mangaUrlDirectory/$storedSlug/")
.build(),
)
if (!response.isSuccessful && response.code == 404) {
response.close()
val newSlug = getNewSlug(storedSlug, frag)
?: throw IOException("Migrate from Asura to Asura")
slugMap[dbSlug] = newSlug
preferences.slugMap = slugMap
return chain.proceed(
request.newBuilder()
.url("$baseUrl$mangaUrlDirectory/$newSlug/")
.build(),
)
}
return response
}
private fun getNewSlug(existingSlug: String, frag: String): String? {
val permaSlug = existingSlug
.replaceFirst(TEMP_TO_PERM_REGEX, "")
val search = frag.substringBefore("#")
val mangas = client.newCall(searchMangaRequest(1, search, FilterList()))
.execute()
.use {
searchMangaParse(it)
}
return mangas.mangas.firstOrNull { newManga ->
newManga.url.contains(permaSlug, true)
}
?.url
?.removeSuffix("/")
?.substringAfterLast("/")
}
private fun String.toSearchQuery(): String {
return this.trim()
.lowercase()
.replace(titleSpecialCharactersRegex, "+")
.replace(trailingPlusRegex, "")
}
private var lastDomain = ""
private fun domainChangeIntercept(chain: Interceptor.Chain): Response {
val request = chain.request()
if (request.url.host !in listOf(preferences.baseUrlHost, lastDomain)) {
return chain.proceed(request)
}
if (lastDomain.isNotEmpty()) {
val newUrl = request.url.newBuilder()
.host(preferences.baseUrlHost)
.build()
return chain.proceed(
request.newBuilder()
.url(newUrl)
.build(),
)
}
val response = chain.proceed(request)
if (request.url.host == response.request.url.host) return response
response.close()
preferences.baseUrlHost = response.request.url.host
lastDomain = request.url.host
val newUrl = request.url.newBuilder()
.host(response.request.url.host)
.build()
return chain.proceed(
request.newBuilder()
.url(newUrl)
.build(),
)
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = PREF_PERM_MANGA_URL_KEY_PREFIX + lang
title = PREF_PERM_MANGA_URL_TITLE
summary = PREF_PERM_MANGA_URL_SUMMARY
setDefaultValue(true)
}.also(screen::addPreference)
}
private val SharedPreferences.permaUrlPref
get() = getBoolean(PREF_PERM_MANGA_URL_KEY_PREFIX + lang, true)
private var SharedPreferences.slugMap: MutableMap<String, String>
get() {
val serialized = getString(PREF_URL_MAP, null) ?: return mutableMapOf()
return try {
json.decodeFromString(serialized)
} catch (e: Exception) {
mutableMapOf()
}
}
set(slugMap) {
val serialized = json.encodeToString(slugMap)
edit().putString(PREF_URL_MAP, serialized).commit()
}
private var SharedPreferences.baseUrlHost
get() = getString(BASE_URL_PREF, defaultBaseUrlHost) ?: defaultBaseUrlHost
set(newHost) {
edit().putString(BASE_URL_PREF, newHost).commit()
}
companion object {
private const val PREF_PERM_MANGA_URL_KEY_PREFIX = "pref_permanent_manga_url_2_"
private const val PREF_PERM_MANGA_URL_TITLE = "Permanent Manga URL"
private const val PREF_PERM_MANGA_URL_SUMMARY = "Turns all manga urls into permanent ones."
private const val PREF_URL_MAP = "pref_url_map"
private const val BASE_URL_PREF = "pref_base_url_host"
private const val defaultBaseUrlHost = "asuratoon.com"
private val TEMP_TO_PERM_REGEX = Regex("""^\d+-""")
private val titleSpecialCharactersRegex = Regex("""[^a-z0-9]+""")
private val trailingPlusRegex = Regex("""\++$""")
}
}

View File

@ -0,0 +1,11 @@
ext {
extName = 'Clown Corps'
extClass = '.ClownCorps'
extVersionCode = 1
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:textinterceptor'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -0,0 +1,245 @@
package eu.kanade.tachiyomi.extension.en.clowncorps
import android.app.Application
import android.content.SharedPreferences
import android.widget.Toast
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptor
import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptorHelper
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
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.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.Response
import org.jsoup.nodes.Document
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
class ClownCorps : ConfigurableSource, HttpSource() {
override val baseUrl = "https://clowncorps.net"
override val lang = "en"
override val name = "Clown Corps"
override val supportsLatest = false
override val client = network.client.newBuilder()
.addInterceptor(TextInterceptor())
.build()
private fun getManga() = SManga.create().apply {
title = name
artist = CREATOR
author = CREATOR
status = SManga.ONGOING
initialized = true
// Image and description from: https://clowncorps.net/about/
thumbnail_url = "$baseUrl/wp-content/uploads/2022/11/clowns41.jpg"
description = "Clown Corps is a comic about crime-fighting clowns.\n" +
"It's pronounced \"core.\" Like marine corps."
url = "/comic"
}
override fun fetchPopularManga(page: Int): Observable<MangasPage> =
Observable.just(MangasPage(listOf(getManga()), hasNextPage = false))
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
fetchPopularManga(page)
override fun fetchMangaDetails(manga: SManga): Observable<SManga> =
Observable.just(getManga())
@Serializable
class SerializableChapter(val fullLink: String, val name: String, val dateUpload: Long) {
override fun hashCode() = fullLink.hashCode()
override fun equals(other: Any?) =
other is SerializableChapter && fullLink == other.fullLink
}
override fun chapterListParse(response: Response): List<SChapter> {
// The total number of webpages with chapters on them
val document = response.asJsoup()
val currentPageIndicator = document.select("#paginav li.paginav-pages").text()
val totalWebpageCount = currentPageIndicator.split(" ").last().toInt()
val allChapters = getChaptersFromCache().toMutableSet()
// Fetch all the chapters from the website until we reached where the cache left off
for (webpageIndex in 1..totalWebpageCount) {
val pageDoc = if (webpageIndex == 1) document else fetchChapterWebpage(webpageIndex)
val anyChaptersWereAdded = allChapters.addAll(extractChapters(pageDoc))
if (!anyChaptersWereAdded) break // No new chapters were added from this webpage, so we're done
}
// Save the chapters to cache
val fullJsonString = Json.encodeToString(allChapters)
setChapterCache(fullJsonString)
// Convert the serializable chapters to SChapters
return allChapters
.sortedByDescending { it.dateUpload }
.map { chapter ->
SChapter.create().apply {
setUrlWithoutDomain(chapter.fullLink)
name = chapter.name
date_upload = chapter.dateUpload
}
}
}
private fun getChaptersFromCache(): Set<SerializableChapter> {
val cachedChaps = getChapterCache() ?: return emptySet()
return Json.decodeFromString(cachedChaps)
}
private fun fetchChapterWebpage(webpageIndex: Int): Document {
val url = "$baseUrl/comic/page/$webpageIndex/"
return client.newCall(GET(url, headers)).execute().asJsoup()
}
private fun extractChapters(document: Document): List<SerializableChapter> {
val comics = document.select(".comic")
return comics.map {
val link = it.selectFirst(".post-title a")!!.attr("href")
val title = it.selectFirst(".post-title a")!!.text()
val postDate = it.selectFirst(".post-date")!!.text()
val postTime = it.selectFirst(".post-time")!!.text()
val date = parseDate("$postDate $postTime")
SerializableChapter(link, title, date)
}
}
private fun parseDate(dateStr: String): Long {
return try {
dateFormat.parse(dateStr)!!.time
} catch (_: ParseException) {
0L
}
}
private val dateFormat by lazy {
SimpleDateFormat("MMMM dd, yyyy hh:mm aa", Locale.ENGLISH)
}
override fun pageListParse(response: Response): List<Page> {
val doc = response.asJsoup()
val pages = mutableListOf<Page>()
val image = doc.selectFirst("#comic img") ?: return pages
val url = image.attr("src")
pages.add(Page(0, "", url))
if (getShowAuthorsNotesPref()) {
val title = image.attr("title")
// Ignore chapters that don't really have author's notes
val ignoreRegex = Regex("""^chapter \d+ page \d+$""", RegexOption.IGNORE_CASE)
if (ignoreRegex.matches(title)) return pages
val localURL = TextInterceptorHelper.createUrl("Author's Notes from $CREATOR", title)
val textPage = Page(pages.size, "", localURL)
pages.add(textPage)
}
return pages
}
override fun imageUrlParse(response: Response) =
throw UnsupportedOperationException()
override fun latestUpdatesParse(response: Response) =
throw UnsupportedOperationException()
override fun latestUpdatesRequest(page: Int) =
throw UnsupportedOperationException()
override fun mangaDetailsParse(response: Response) =
throw UnsupportedOperationException()
override fun popularMangaParse(response: Response) =
throw UnsupportedOperationException()
override fun popularMangaRequest(page: Int) =
throw UnsupportedOperationException()
override fun searchMangaParse(response: Response) =
throw UnsupportedOperationException()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
throw UnsupportedOperationException()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private fun getShowAuthorsNotesPref() =
preferences.getBoolean(SETTING_KEY_SHOW_AUTHORS_NOTES, false)
private fun getChapterCache() =
preferences.getString(CACHE_KEY_CHAPTERS, null)
private fun setChapterCache(json: String) =
preferences.edit().putString(CACHE_KEY_CHAPTERS, json).apply()
private fun clearChapterCache() =
preferences.edit().remove(CACHE_KEY_CHAPTERS).apply()
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val authorsNotesPref = SwitchPreferenceCompat(screen.context).apply {
key = SETTING_KEY_SHOW_AUTHORS_NOTES
title = "Show author's notes"
summary =
"Enable to see the author's notes at the end of chapters (if they're there)."
setDefaultValue(false)
}
screen.addPreference(authorsNotesPref)
// I couldn't find a way to create a simple button, so here's a workaround that uses
// a MultiSelectListPreference with a single option as a kind of confirmation window.
val clearCachePref = MultiSelectListPreference(screen.context).apply {
key = SETTING_KEY_CLEAR_CHAPTER_CACHE
title = "Clear chapter cache"
summary = "Clears the chapter cache, forcing a full re-fetch from the website."
dialogTitle = "Are you sure you want to clear the chapter cache?"
entries = arrayOf("Yes, I'm sure")
entryValues = arrayOf(VALUE_CONFIRM)
setDefaultValue(emptySet<String>())
setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Set<*>
if (checkValue.contains(VALUE_CONFIRM)) {
clearChapterCache()
Toast.makeText(screen.context, "Cleared chapter cache", Toast.LENGTH_SHORT)
.show()
}
false // Don't actually save the "yes"
}
}
screen.addPreference(clearCachePref)
}
companion object {
private const val CREATOR = "Joe Chouinard"
private const val SETTING_KEY_SHOW_AUTHORS_NOTES = "showAuthorsNotes"
private const val CACHE_KEY_CHAPTERS = "chaptersCache"
private const val SETTING_KEY_CLEAR_CHAPTER_CACHE = "clearChapterCache"
private const val VALUE_CONFIRM = "yes"
}
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,144 @@
package eu.kanade.tachiyomi.extension.en.doujinio
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
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.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
const val LATEST_LIMIT = 20
class Doujinio : HttpSource() {
override val name = "Doujin.io - J18"
override val baseUrl = "https://doujin.io"
override val lang = "en"
override val supportsLatest = true
override val client = network.cloudflareClient.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 2)
.build()
// Latest
override fun latestUpdatesRequest(page: Int) =
POST(
"$baseUrl/api/mangas/newest",
headers,
body = json.encodeToString(
LatestRequest(
limit = LATEST_LIMIT,
offset = (page - 1) * LATEST_LIMIT,
),
).toRequestBody("application/json".toMediaType()),
)
override fun latestUpdatesParse(response: Response): MangasPage {
val latest = response.parseAs<List<Manga>>().map { it.toSManga() }
return MangasPage(latest, hasNextPage = latest.size >= LATEST_LIMIT)
}
// Popular
override fun popularMangaRequest(page: Int) = GET("$baseUrl/api/mangas/popular", headers)
override fun popularMangaParse(response: Response) = MangasPage(
response.parseAs<List<Manga>>().map { it.toSManga() },
hasNextPage = false,
)
// Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = POST(
"$baseUrl/api/mangas/search",
headers,
body = json.encodeToString(
SearchRequest(
keyword = query,
page = page,
tags = filters.findInstance<TagGroup>()?.state?.filter { it.state }?.mapNotNull {
tags.find { tag -> tag.name == it.name }?.id
} ?: emptyList(),
),
).toRequestBody("application/json".toMediaType()),
)
override fun searchMangaParse(response: Response): MangasPage {
val result = response.parseAs<SearchResponse>()
return MangasPage(
result.data.map { it.toSManga() },
hasNextPage = result.to?.let { it < result.total } ?: false,
)
}
// Details
override fun mangaDetailsRequest(manga: SManga) =
GET("https://doujin.io/api/mangas/${getIdFromUrl(manga.url)}", headers)
override fun mangaDetailsParse(response: Response) = response.parseAs<Manga>().toSManga()
override fun getMangaUrl(manga: SManga) = "$baseUrl/manga/${getIdFromUrl(manga.url)}"
// Chapter
override fun chapterListRequest(manga: SManga) =
GET("$baseUrl/api/chapters?manga_id=${getIdFromUrl(manga.url)}", headers)
override fun chapterListParse(response: Response) =
response.parseAs<List<Chapter>>().map { it.toSChapter() }.reversed()
// Page List
override fun pageListRequest(chapter: SChapter) =
GET(
"$baseUrl/api/mangas/${getIdsFromUrl(chapter.url)}/manifest",
headers.newBuilder().apply {
add(
"referer",
"https://doujin.io/manga/${getIdsFromUrl(chapter.url).split("/").joinToString("/chapter/")}",
)
}.build(),
)
override fun pageListParse(response: Response) =
if (response.headers["content-type"] == "text/html; charset=UTF-8") {
throw Exception("You need to login first through the WebView to read the chapter.")
} else {
json.decodeFromString<ChapterManifest>(
response.body.string(),
).toPageList()
}
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
override fun getFilterList() = FilterList(
TagGroup(),
)
private class TagFilter(name: String) : Filter.CheckBox(name, false)
private class TagGroup : Filter.Group<TagFilter>(
"Tags",
tags.map { TagFilter(it.name) },
)
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
private inline fun <reified T> Response.parseAs(): T =
json.decodeFromString<PageResponse<T>>(body.string()).data
}

View File

@ -0,0 +1,98 @@
package eu.kanade.tachiyomi.extension.en.doujinio
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class PageResponse<T>(val data: T)
@Serializable
class Manga(
@SerialName("optimus_id")
private val id: Int,
private val title: String,
private val description: String,
private val thumb: String,
private val tags: List<Tag>,
@SerialName("creator_name")
private val artist: String,
) {
fun toSManga() = SManga.create().apply {
url = "/manga/$id"
title = this@Manga.title
description = this@Manga.description
thumbnail_url = this@Manga.thumb
artist = this@Manga.artist
genre = this@Manga.tags.joinToString(", ") { it.name }
status = SManga.COMPLETED
initialized = true
}
}
@Serializable
class Tag(val id: Int, val name: String)
@Serializable
class Chapter(
@SerialName("optimus_id")
private val id: Int,
@SerialName("manga_optimus_id")
private val mangaId: Int,
@SerialName("chapter_name")
private val name: String,
@SerialName("chapter_order")
private val order: Int,
@SerialName("published_at")
private val publishedAt: String,
) {
fun toSChapter() = SChapter.create().apply {
url = "manga/$mangaId/chapter/$id"
name = this@Chapter.name
chapter_number = (order + 1).toFloat()
date_upload = parseDate(publishedAt)
}
}
@Serializable
class ChapterMetadata(val identifier: String)
@Serializable
class ChapterPage(val href: String)
@Serializable
class ChapterManifest(
private val metadata: ChapterMetadata,
@SerialName("readingOrder")
private val pages: List<ChapterPage>,
) {
fun toPageList() = pages.mapIndexed { i, page ->
Page(
index = i,
url = metadata.identifier,
imageUrl = page.href,
)
}
}
@Serializable
class LatestRequest(
val limit: Int,
val offset: Int,
)
@Serializable
class SearchRequest(
val keyword: String,
val page: Int,
val tags: List<Int> = emptyList(),
)
@Serializable
class SearchResponse(
val data: List<Manga>,
val to: Int?,
val total: Int,
)

View File

@ -0,0 +1,82 @@
package eu.kanade.tachiyomi.extension.en.doujinio
import kotlinx.serialization.json.Json
import uy.kohesive.injekt.injectLazy
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
val json: Json by injectLazy()
val tags = listOf(
Tag(id = 22, name = "Aggressive Sex"),
Tag(id = 23, name = "Anal"),
Tag(id = 104, name = "BBM"),
Tag(id = 105, name = "BSS"),
Tag(id = 62, name = "Big Breasts"),
Tag(id = 26, name = "Blowjob"),
Tag(id = 27, name = "Bondage"),
Tag(id = 29, name = "Cheating"),
Tag(id = 32, name = "Creampie"),
Tag(id = 33, name = "Crossdressing"),
Tag(id = 34, name = "Cunnilingus"),
Tag(id = 35, name = "Dark Skin"),
Tag(id = 36, name = "Defloration"),
Tag(id = 38, name = "Demon Girl"),
Tag(id = 51, name = "Dickgirl"),
Tag(id = 112, name = "Doll Joints"),
Tag(id = 41, name = "Elf"),
Tag(id = 106, name = "Exhibitionism"),
Tag(id = 107, name = "Family"),
Tag(id = 44, name = "Femdom"),
Tag(id = 46, name = "Footjob"),
Tag(id = 49, name = "Full Color"),
Tag(id = 50, name = "Furry"),
Tag(id = 53, name = "Gender Bender"),
Tag(id = 54, name = "Group"),
Tag(id = 55, name = "Gyaru"),
Tag(id = 56, name = "Gym Uniform"),
Tag(id = 114, name = "Kemonomimi"),
Tag(id = 61, name = "Lactation"),
Tag(id = 9, name = "Maid Uniform"),
Tag(id = 65, name = "Mind Control"),
Tag(id = 108, name = "Mindbreak"),
Tag(id = 109, name = "Monster Girl"),
Tag(id = 69, name = "Muscle"),
Tag(id = 71, name = "Netorare"),
Tag(id = 73, name = "Ninja Outfit"),
Tag(id = 74, name = "Non-H"),
Tag(id = 75, name = "Nun Outfit"),
Tag(id = 76, name = "Nurse Outfit"),
Tag(id = 78, name = "Old Man"),
Tag(id = 82, name = "Pay To Play"),
Tag(id = 80, name = "Petite"),
Tag(id = 81, name = "Pregnant"),
Tag(id = 83, name = "Rimjob"),
Tag(id = 84, name = "School Uniform"),
Tag(id = 110, name = "Small Breasts"),
Tag(id = 63, name = "Solo Action"),
Tag(id = 90, name = "Swimsuit"),
Tag(id = 91, name = "Tanlines"),
Tag(id = 92, name = "Tentacles"),
Tag(id = 93, name = "Titjob"),
Tag(id = 94, name = "Toys"),
Tag(id = 95, name = "Urination"),
Tag(id = 99, name = "Yaoi"),
)
fun parseDate(dateStr: String): Long {
return try {
dateFormat.parse(dateStr)!!.time
} catch (_: ParseException) {
0L
}
}
private val dateFormat by lazy {
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH)
}
fun getIdFromUrl(url: String) = url.split("/").last()
fun getIdsFromUrl(url: String) = "${url.split("/")[1]}/${url.split("/").last()}"

View File

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

View File

@ -218,14 +218,14 @@ class EarlyManga : HttpSource() {
val chapterUrl = response.request.url.toString()
.replace("/api", "")
val preSlug = if (result.on_disk != 0 && result.on_disk != null) {
"storage/uploads/manga"
"$baseUrl/storage/uploads/manga"
} else {
"e-storage/uploads/manga"
"https://images.${baseUrl.removePrefix("https://")}/manga"
}
return result.images
.filterNot { it.endsWith(".ico") }
.mapIndexed { index, img ->
Page(index = index, url = chapterUrl, imageUrl = "$baseUrl/$preSlug/manga_${result.manga_id}/chapter_${result.slug}/$img")
Page(index = index, url = chapterUrl, imageUrl = "$preSlug/manga_${result.manga_id}/chapter_${result.slug}/$img")
}
}

View File

@ -2,8 +2,8 @@ ext {
extName = 'Elarc Toon'
extClass = '.ElarcPage'
themePkg = 'mangathemesia'
baseUrl = 'https://elarctoon.com'
overrideVersionCode = 4
baseUrl = 'https://elarctoons.com'
overrideVersionCode = 5
isNsfw = false
}

View File

@ -9,7 +9,7 @@ import java.io.IOException
class ElarcPage : MangaThemesia(
"Elarc Toon",
"https://elarctoon.com",
"https://elarctoons.com",
"en",
) {
override val id = 5482125641807211052

View File

@ -3,7 +3,7 @@ ext {
extClass = '.FlameComics'
themePkg = 'mangathemesia'
baseUrl = 'https://flamecomics.com'
overrideVersionCode = 0
overrideVersionCode = 1
}
apply from: "$rootDir/common.gradle"

View File

@ -1,47 +1,30 @@
package eu.kanade.tachiyomi.extension.en.flamecomics
import android.app.Application
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Rect
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
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 okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Protocol
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import org.jsoup.nodes.Document
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.ByteArrayOutputStream
class FlameComics :
MangaThemesia(
"Flame Comics",
"https://flamecomics.com",
"en",
mangaUrlDirectory = "/series",
),
ConfigurableSource {
class FlameComics : MangaThemesia(
"Flame Comics",
"https://flamecomics.com",
"en",
mangaUrlDirectory = "/series",
) {
// Flame Scans -> Flame Comics
override val id = 6350607071566689772
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override val client = super.client.newBuilder()
.rateLimit(2, 7)
.addInterceptor(::composedImageIntercept)
@ -130,114 +113,8 @@ class FlameComics :
}
// Split Image Fixer End
// Permanent Url start
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return super.fetchPopularManga(page).tempUrlToPermIfNeeded()
}
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return super.fetchLatestUpdates(page).tempUrlToPermIfNeeded()
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return super.fetchSearchManga(page, query, filters).tempUrlToPermIfNeeded()
}
private fun Observable<MangasPage>.tempUrlToPermIfNeeded(): Observable<MangasPage> {
return this.map { mangasPage ->
MangasPage(
mangasPage.mangas.map { it.tempUrlToPermIfNeeded() },
mangasPage.hasNextPage,
)
}
}
private fun SManga.tempUrlToPermIfNeeded(): SManga {
val turnTempUrlToPerm = preferences.getBoolean(getPermanentMangaUrlPreferenceKey(), true)
if (!turnTempUrlToPerm) return this
val path = this.url.removePrefix("/").removeSuffix("/").split("/")
path.lastOrNull()?.let { slug -> this.url = "$mangaUrlDirectory/${deobfuscateSlug(slug)}/" }
return this
}
override fun fetchChapterList(manga: SManga) = super.fetchChapterList(manga.tempUrlToPermIfNeeded())
.map { sChapterList -> sChapterList.map { it.tempUrlToPermIfNeeded() } }
private fun SChapter.tempUrlToPermIfNeeded(): SChapter {
val turnTempUrlToPerm = preferences.getBoolean(getPermanentChapterUrlPreferenceKey(), true)
if (!turnTempUrlToPerm) return this
val path = this.url.removePrefix("/").removeSuffix("/").split("/")
path.lastOrNull()?.let { slug -> this.url = "/${deobfuscateSlug(slug)}/" }
return this
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val permanentMangaUrlPref = SwitchPreferenceCompat(screen.context).apply {
key = getPermanentMangaUrlPreferenceKey()
title = PREF_PERM_MANGA_URL_TITLE
summary = PREF_PERM_MANGA_URL_SUMMARY
setDefaultValue(true)
setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Boolean
preferences.edit()
.putBoolean(getPermanentMangaUrlPreferenceKey(), checkValue)
.commit()
}
}
val permanentChapterUrlPref = SwitchPreferenceCompat(screen.context).apply {
key = getPermanentChapterUrlPreferenceKey()
title = PREF_PERM_CHAPTER_URL_TITLE
summary = PREF_PERM_CHAPTER_URL_SUMMARY
setDefaultValue(true)
setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Boolean
preferences.edit()
.putBoolean(getPermanentChapterUrlPreferenceKey(), checkValue)
.commit()
}
}
screen.addPreference(permanentMangaUrlPref)
screen.addPreference(permanentChapterUrlPref)
}
private fun getPermanentMangaUrlPreferenceKey(): String {
return PREF_PERM_MANGA_URL_KEY_PREFIX + lang
}
private fun getPermanentChapterUrlPreferenceKey(): String {
return PREF_PERM_CHAPTER_URL_KEY_PREFIX + lang
}
// Permanent Url for Manga/Chapter End
companion object {
private const val COMPOSED_SUFFIX = "?comp"
private const val PREF_PERM_MANGA_URL_KEY_PREFIX = "pref_permanent_manga_url_"
private const val PREF_PERM_MANGA_URL_TITLE = "Permanent Manga URL"
private const val PREF_PERM_MANGA_URL_SUMMARY = "Turns all manga urls into permanent ones."
private const val PREF_PERM_CHAPTER_URL_KEY_PREFIX = "pref_permanent_chapter_url"
private const val PREF_PERM_CHAPTER_URL_TITLE = "Permanent Chapter URL"
private const val PREF_PERM_CHAPTER_URL_SUMMARY = "Turns all chapter urls into permanent ones."
/**
*
* De-obfuscates the slug of a series or chapter to the permanent slug
* * For a series: "12345678-this-is-a-series" -> "this-is-a-series"
* * For a chapter: "12345678-this-is-a-series-chapter-1" -> "this-is-a-series-chapter-1"
*
* @param obfuscated_slug the obfuscated slug of a series or chapter
*
* @return
*/
private fun deobfuscateSlug(obfuscated_slug: String) = obfuscated_slug
.replaceFirst(Regex("""^\d+-"""), "")
private val MEDIA_TYPE = "image/png".toMediaType()
}
}

View File

@ -3,7 +3,7 @@ ext {
extClass = '.KingofShojo'
themePkg = 'mangathemesia'
baseUrl = 'https://kingofshojo.com'
overrideVersionCode = 1
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Some files were not shown because too many files have changed in this diff Show More