Compare commits
56 Commits
5292a9ff0a
...
ce09450e2a
Author | SHA1 | Date |
---|---|---|
renovate[bot] | ce09450e2a | |
AwkwardPeak7 | 575d831400 | |
mohamedotaku | 879bb4c3eb | |
mohamedotaku | 473b3e98ab | |
Vetle Ledaal | c2faa2774b | |
Vetle Ledaal | a06318f264 | |
Vetle Ledaal | cf2cef1985 | |
Chopper | 217846f693 | |
Chopper | d2fec71228 | |
Vetle Ledaal | 02d7019b46 | |
mohamedotaku | 3c6449bf24 | |
bapeey | 87a9effa82 | |
Vetle Ledaal | 7d1cffdd10 | |
Vetle Ledaal | ed9e49fd11 | |
Vetle Ledaal | 063b526748 | |
Vetle Ledaal | 6317d70538 | |
WarmSeeker6 | 86474f6d98 | |
GGaro | d7f5c6c1d3 | |
mohamedotaku | 41cab37a37 | |
mohamedotaku | b14ccb1c89 | |
mohamedotaku | dbcd4deb1f | |
bapeey | 8ef429f4bc | |
bapeey | 99b0273848 | |
bapeey | e7c326bb6c | |
bapeey | b4f5680364 | |
Cuong M. Tran | a0fa7fa458 | |
bapeey | 455f57d209 | |
Vetle Ledaal | 34887a83a8 | |
Chopper | 867f0844d1 | |
Vetle Ledaal | 6a1d7dc1ca | |
AwkwardPeak7 | e1c77ab678 | |
BrutuZ | 046c2aa421 | |
altaccosc | 0a0ff7c1ac | |
Vetle Ledaal | 16bcd6bbd9 | |
renovate[bot] | f057af4dbf | |
Vetle Ledaal | b5c0daba37 | |
Vetle Ledaal | 61b3d9a2fb | |
AwkwardPeak7 | 62bd6c0817 | |
AwkwardPeak7 | dfb4b93953 | |
haruki-takeshi | 34429ffa0a | |
Fermín Cirella | ebf7e277e3 | |
bapeey | 3f73aec7cf | |
beerpsi | 0c4abef20c | |
Cuong M. Tran | b21ab37da9 | |
stevenyomi | e67d78f435 | |
bapeey | 82bb3dafd8 | |
mohamedotaku | 9554653678 | |
Cuong M. Tran | 35a2715ad5 | |
bapeey | ca5365cc6c | |
quangpao | e10e72ad98 | |
anenasa | 488adf9ba6 | |
AwkwardPeak7 | 0594d08440 | |
mohamedotaku | d7c2e7b9da | |
AwkwardPeak7 | f2f809f35d | |
inipew | 5ad927dbf4 | |
AwkwardPeak7 | 7560276087 |
|
@ -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
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
||||
baseVersionCode = 2
|
||||
|
|
|
@ -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("مسميّات أخرى")
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -2,4 +2,8 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 20
|
||||
baseVersionCode = 21
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:i18n"))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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() }
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
STATUS=状態
|
||||
STATUS_ALL=全て
|
||||
STATUS_ONGOING=連載中
|
||||
STATUS_COMPLETED=完結済み
|
||||
GENRE=ジャンル
|
|
@ -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
|
|
@ -2,4 +2,8 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 4
|
||||
baseVersionCode = 5
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:i18n"))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
ext {
|
||||
extName = 'Arabs Hentai'
|
||||
extClass = '.ArabsHentai'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 5.5 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 7.9 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 23 KiB |
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
ext {
|
||||
extName = 'GMANGA (unoriginal)'
|
||||
extClass = '.GmangaSite'
|
||||
themePkg = 'madara'
|
||||
baseUrl = 'https://gmanga.site'
|
||||
overrideVersionCode = 0
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 18 KiB |
|
@ -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
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Hentai Slayer'
|
||||
extClass = '.HentaiSlayer'
|
||||
extVersionCode = 1
|
||||
extVersionCode = 2
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 27 KiB |
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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")),
|
||||
) {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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")),
|
||||
) {
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
ext {
|
||||
extName = 'Manga Time'
|
||||
extClass = '.MangaTime'
|
||||
themePkg = 'madara'
|
||||
baseUrl = 'https://anime-time.net'
|
||||
overrideVersionCode = 0
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 9.5 KiB |
After Width: | Height: | Size: 13 KiB |
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
|
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 28 KiB |
|
@ -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
|
||||
}
|
|
@ -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")),
|
||||
)
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Anchira'
|
||||
extClass = '.Anchira'
|
||||
extVersionCode = 10
|
||||
extVersionCode = 11
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -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?")
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.AsuraScans'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://asuratoon.com'
|
||||
overrideVersionCode = 1
|
||||
overrideVersionCode = 3
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -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("""\++$""")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
ext {
|
||||
extName = 'Clown Corps'
|
||||
extClass = '.ClownCorps'
|
||||
extVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:textinterceptor'))
|
||||
}
|
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 38 KiB |
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
ext {
|
||||
extName = 'Doujin.io - J18'
|
||||
extClass = '.Doujinio'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 7.5 KiB |
After Width: | Height: | Size: 10 KiB |
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
)
|
|
@ -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()}"
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'EarlyManga'
|
||||
extClass = '.EarlyManga'
|
||||
extVersionCode = 26
|
||||
extVersionCode = 27
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import java.io.IOException
|
|||
|
||||
class ElarcPage : MangaThemesia(
|
||||
"Elarc Toon",
|
||||
"https://elarctoon.com",
|
||||
"https://elarctoons.com",
|
||||
"en",
|
||||
) {
|
||||
override val id = 5482125641807211052
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.FlameComics'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://flamecomics.com'
|
||||
overrideVersionCode = 0
|
||||
overrideVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.KingofShojo'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://kingofshojo.com'
|
||||
overrideVersionCode = 1
|
||||
overrideVersionCode = 0
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 5.4 KiB |