Compare commits
56 Commits
5292a9ff0a
...
ce09450e2a
Author | SHA1 | Date |
---|---|---|
![]() |
ce09450e2a | |
![]() |
575d831400 | |
![]() |
879bb4c3eb | |
![]() |
473b3e98ab | |
![]() |
c2faa2774b | |
![]() |
a06318f264 | |
![]() |
cf2cef1985 | |
![]() |
217846f693 | |
![]() |
d2fec71228 | |
![]() |
02d7019b46 | |
![]() |
3c6449bf24 | |
![]() |
87a9effa82 | |
![]() |
7d1cffdd10 | |
![]() |
ed9e49fd11 | |
![]() |
063b526748 | |
![]() |
6317d70538 | |
![]() |
86474f6d98 | |
![]() |
d7f5c6c1d3 | |
![]() |
41cab37a37 | |
![]() |
b14ccb1c89 | |
![]() |
dbcd4deb1f | |
![]() |
8ef429f4bc | |
![]() |
99b0273848 | |
![]() |
e7c326bb6c | |
![]() |
b4f5680364 | |
![]() |
a0fa7fa458 | |
![]() |
455f57d209 | |
![]() |
34887a83a8 | |
![]() |
867f0844d1 | |
![]() |
6a1d7dc1ca | |
![]() |
e1c77ab678 | |
![]() |
046c2aa421 | |
![]() |
0a0ff7c1ac | |
![]() |
16bcd6bbd9 | |
![]() |
f057af4dbf | |
![]() |
b5c0daba37 | |
![]() |
61b3d9a2fb | |
![]() |
62bd6c0817 | |
![]() |
dfb4b93953 | |
![]() |
34429ffa0a | |
![]() |
ebf7e277e3 | |
![]() |
3f73aec7cf | |
![]() |
0c4abef20c | |
![]() |
b21ab37da9 | |
![]() |
e67d78f435 | |
![]() |
82bb3dafd8 | |
![]() |
9554653678 | |
![]() |
35a2715ad5 | |
![]() |
ca5365cc6c | |
![]() |
e10e72ad98 | |
![]() |
488adf9ba6 | |
![]() |
0594d08440 | |
![]() |
d7c2e7b9da | |
![]() |
f2f809f35d | |
![]() |
5ad927dbf4 | |
![]() |
7560276087 |
|
@ -24,7 +24,7 @@ jobs:
|
||||||
CI_MODULE_GEN: true
|
CI_MODULE_GEN: true
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
build_individual:
|
build_individual:
|
||||||
name: Build individual modules
|
name: Build individual modules
|
||||||
|
@ -32,10 +32,10 @@ jobs:
|
||||||
runs-on: arch
|
runs-on: arch
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout master branch
|
- name: Checkout master branch
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
|
@ -77,7 +77,7 @@ jobs:
|
||||||
path: ~/apk-artifacts
|
path: ~/apk-artifacts
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 1
|
baseVersionCode = 2
|
||||||
|
|
|
@ -29,12 +29,12 @@ class SearchMangaDto(
|
||||||
class BrowseManga(
|
class BrowseManga(
|
||||||
private val id: Int,
|
private val id: Int,
|
||||||
private val title: String,
|
private val title: String,
|
||||||
private val cover: String,
|
private val cover: String? = null,
|
||||||
) {
|
) {
|
||||||
fun toSManga(createThumbnail: (String, String) -> String) = SManga.create().apply {
|
fun toSManga(createThumbnail: (String, String) -> String) = SManga.create().apply {
|
||||||
url = "/mangas/$id"
|
url = "/mangas/$id"
|
||||||
title = this@BrowseManga.title
|
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
|
@Serializable
|
||||||
class Manga(
|
class Manga(
|
||||||
private val id: Int,
|
private val id: Int,
|
||||||
private val cover: String,
|
private val cover: String? = null,
|
||||||
private val title: String,
|
private val title: String,
|
||||||
private val summary: String? = null,
|
private val summary: String? = null,
|
||||||
private val artists: List<NameDto>,
|
private val artists: List<NameDto>,
|
||||||
|
@ -74,7 +74,7 @@ class Manga(
|
||||||
) {
|
) {
|
||||||
fun toSManga(createThumbnail: (String, String) -> String) = SManga.create().apply {
|
fun toSManga(createThumbnail: (String, String) -> String) = SManga.create().apply {
|
||||||
title = this@Manga.title
|
title = this@Manga.title
|
||||||
thumbnail_url = createThumbnail(id.toString(), cover)
|
thumbnail_url = cover?.let { createThumbnail(id.toString(), cover) }
|
||||||
artist = artists.joinToString { it.name }
|
artist = artists.joinToString { it.name }
|
||||||
author = authors.joinToString { it.name }
|
author = authors.joinToString { it.name }
|
||||||
status = when (this@Manga.status) {
|
status = when (this@Manga.status) {
|
||||||
|
@ -105,6 +105,8 @@ class Manga(
|
||||||
}
|
}
|
||||||
|
|
||||||
val titles = listOfNotNull(synonyms, arTitle, jpTitle, enTitle)
|
val titles = listOfNotNull(synonyms, arTitle, jpTitle, enTitle)
|
||||||
|
.filterNot(String::isEmpty)
|
||||||
|
|
||||||
if (titles.isNotEmpty()) {
|
if (titles.isNotEmpty()) {
|
||||||
append("\n\n")
|
append("\n\n")
|
||||||
append("مسميّات أخرى")
|
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")
|
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 android.content.SharedPreferences
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
|
import eu.kanade.tachiyomi.lib.i18n.Intl
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.IOException
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
@ -39,35 +33,15 @@ abstract class HeanCms(
|
||||||
protected val apiUrl: String = baseUrl.replace("://", "://api."),
|
protected val apiUrl: String = baseUrl.replace("://", "://api."),
|
||||||
) : ConfigurableSource, HttpSource() {
|
) : ConfigurableSource, HttpSource() {
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
protected val preferences: SharedPreferences by lazy {
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
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 supportsLatest = true
|
||||||
|
|
||||||
override val client: OkHttpClient = network.cloudflareClient
|
override val client: OkHttpClient = network.cloudflareClient
|
||||||
|
|
||||||
protected open val slugStrategy = SlugStrategy.NONE
|
protected open val useNewChapterEndpoint = false
|
||||||
|
|
||||||
protected open val useNewQueryEndpoint = false
|
|
||||||
|
|
||||||
private var seriesSlugMap: Map<String, HeanCmsTitle>? = null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Json instance to make usage of `encodeDefaults`,
|
* Custom Json instance to make usage of `encodeDefaults`,
|
||||||
|
@ -79,9 +53,14 @@ abstract class HeanCms(
|
||||||
encodeDefaults = true
|
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"
|
protected open val mangaSubDirectory: String = "series"
|
||||||
|
|
||||||
|
@ -92,29 +71,6 @@ abstract class HeanCms(
|
||||||
.add("Referer", "$baseUrl/")
|
.add("Referer", "$baseUrl/")
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
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()
|
val url = "$apiUrl/query".toHttpUrl().newBuilder()
|
||||||
.addQueryParameter("query_string", "")
|
.addQueryParameter("query_string", "")
|
||||||
.addQueryParameter("series_status", "All")
|
.addQueryParameter("series_status", "All")
|
||||||
|
@ -124,66 +80,14 @@ abstract class HeanCms(
|
||||||
.addQueryParameter("page", page.toString())
|
.addQueryParameter("page", page.toString())
|
||||||
.addQueryParameter("perPage", "12")
|
.addQueryParameter("perPage", "12")
|
||||||
.addQueryParameter("tags_ids", "[]")
|
.addQueryParameter("tags_ids", "[]")
|
||||||
|
.addQueryParameter("adult", "true")
|
||||||
|
|
||||||
return GET(url.build(), headers)
|
return GET(url.build(), headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
||||||
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 latestUpdatesRequest(page: Int): Request {
|
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()
|
val url = "$apiUrl/query".toHttpUrl().newBuilder()
|
||||||
.addQueryParameter("query_string", "")
|
.addQueryParameter("query_string", "")
|
||||||
.addQueryParameter("series_status", "All")
|
.addQueryParameter("series_status", "All")
|
||||||
|
@ -193,6 +97,7 @@ abstract class HeanCms(
|
||||||
.addQueryParameter("page", page.toString())
|
.addQueryParameter("page", page.toString())
|
||||||
.addQueryParameter("perPage", "12")
|
.addQueryParameter("perPage", "12")
|
||||||
.addQueryParameter("tags_ids", "[]")
|
.addQueryParameter("tags_ids", "[]")
|
||||||
|
.addQueryParameter("adult", "true")
|
||||||
|
|
||||||
return GET(url.build(), headers)
|
return GET(url.build(), headers)
|
||||||
}
|
}
|
||||||
|
@ -206,12 +111,8 @@ abstract class HeanCms(
|
||||||
|
|
||||||
val slug = query.substringAfter(SEARCH_PREFIX)
|
val slug = query.substringAfter(SEARCH_PREFIX)
|
||||||
val manga = SManga.create().apply {
|
val manga = SManga.create().apply {
|
||||||
url = if (slugStrategy != SlugStrategy.NONE) {
|
val mangaId = getIdBySlug(slug)
|
||||||
val mangaId = getIdBySlug(slug)
|
url = "/$mangaSubDirectory/$slug#$mangaId"
|
||||||
"/$mangaSubDirectory/${slug.toPermSlugIfNeeded()}#$mangaId"
|
|
||||||
} else {
|
|
||||||
"/$mangaSubDirectory/$slug"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetchMangaDetails(manga).map { MangasPage(listOf(it), false) }
|
return fetchMangaDetails(manga).map { MangasPage(listOf(it), false) }
|
||||||
|
@ -224,57 +125,12 @@ abstract class HeanCms(
|
||||||
|
|
||||||
val seriesDetail = json.parseAs<HeanCmsSeriesDto>()
|
val seriesDetail = json.parseAs<HeanCmsSeriesDto>()
|
||||||
|
|
||||||
preferences.slugMap = preferences.slugMap.toMutableMap()
|
|
||||||
.also { it[seriesDetail.slug.toPermSlugIfNeeded()] = seriesDetail.slug }
|
|
||||||
|
|
||||||
seriesDetail.id
|
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 {
|
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 sortByFilter = filters.firstInstanceOrNull<SortByFilter>()
|
||||||
val statusFilter = filters.firstInstanceOrNull<StatusFilter>()
|
val statusFilter = filters.firstInstanceOrNull<StatusFilter>()
|
||||||
|
|
||||||
|
@ -292,6 +148,7 @@ abstract class HeanCms(
|
||||||
.addQueryParameter("page", page.toString())
|
.addQueryParameter("page", page.toString())
|
||||||
.addQueryParameter("perPage", "12")
|
.addQueryParameter("perPage", "12")
|
||||||
.addQueryParameter("tags_ids", tagIds)
|
.addQueryParameter("tags_ids", tagIds)
|
||||||
|
.addQueryParameter("adult", "true")
|
||||||
|
|
||||||
return GET(url.build(), headers)
|
return GET(url.build(), headers)
|
||||||
}
|
}
|
||||||
|
@ -299,95 +156,34 @@ abstract class HeanCms(
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
val json = response.body.string()
|
val json = response.body.string()
|
||||||
|
|
||||||
if (response.request.url.pathSegments.last() == "search") {
|
val result = json.parseAs<HeanCmsQuerySearchDto>()
|
||||||
fetchAllTitles()
|
val mangaList = result.data.map {
|
||||||
|
it.toSManga(apiUrl, coverPath, mangaSubDirectory)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (json.startsWith("{")) {
|
return MangasPage(mangaList, result.meta?.hasNextPage() ?: false)
|
||||||
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 getMangaUrl(manga: SManga): String {
|
override fun getMangaUrl(manga: SManga): String {
|
||||||
val seriesSlug = manga.url
|
val seriesSlug = manga.url
|
||||||
.substringAfterLast("/")
|
.substringAfterLast("/")
|
||||||
.substringBefore("#")
|
.substringBefore("#")
|
||||||
.toPermSlugIfNeeded()
|
|
||||||
|
|
||||||
val currentSlug = if (slugStrategy != SlugStrategy.NONE) {
|
return "$baseUrl/$mangaSubDirectory/$seriesSlug"
|
||||||
preferences.slugMap[seriesSlug] ?: seriesSlug
|
|
||||||
} else {
|
|
||||||
seriesSlug
|
|
||||||
}
|
|
||||||
|
|
||||||
return "$baseUrl/$mangaSubDirectory/$currentSlug"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||||
if (slugStrategy != SlugStrategy.NONE && (manga.url.contains(TIMESTAMP_REGEX))) {
|
if (!manga.url.contains("#")) {
|
||||||
throw Exception(intl.urlChangedError(name))
|
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("#")
|
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()
|
val apiHeaders = headersBuilder()
|
||||||
.add("Accept", ACCEPT_JSON)
|
.add("Accept", ACCEPT_JSON)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
return if (slugStrategy == SlugStrategy.ID) {
|
return GET("$apiUrl/series/id/$seriesId", apiHeaders)
|
||||||
GET("$apiUrl/series/id/$seriesId", apiHeaders)
|
|
||||||
} else {
|
|
||||||
GET("$apiUrl/series/$currentSlug#$currentStatus", apiHeaders)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
|
@ -395,14 +191,10 @@ abstract class HeanCms(
|
||||||
|
|
||||||
val result = runCatching { response.parseAs<HeanCmsSeriesDto>() }
|
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) {
|
val seriesDetails = seriesResult.toSManga(apiUrl, coverPath, mangaSubDirectory)
|
||||||
preferences.slugMap = preferences.slugMap.toMutableMap()
|
|
||||||
.also { it[seriesResult.slug.toPermSlugIfNeeded()] = seriesResult.slug }
|
|
||||||
}
|
|
||||||
|
|
||||||
val seriesDetails = seriesResult.toSManga(apiUrl, coverPath, mangaSubDirectory, slugStrategy)
|
|
||||||
|
|
||||||
return seriesDetails.apply {
|
return seriesDetails.apply {
|
||||||
status = status.takeUnless { it == SManga.UNKNOWN }
|
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 seriesId = manga.url.substringAfterLast("#")
|
||||||
val result = response.parseAs<HeanCmsSeriesDto>()
|
val seriesSlug = manga.url.substringAfterLast("/").substringBefore("#")
|
||||||
|
|
||||||
if (slugStrategy == SlugStrategy.ID) {
|
val url = "$apiUrl/chapter/query".toHttpUrl().newBuilder()
|
||||||
preferences.slugMap = preferences.slugMap.toMutableMap()
|
.addQueryParameter("page", "1")
|
||||||
.also { it[result.slug.toPermSlugIfNeeded()] = result.slug }
|
.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
|
val showPaidChapters = preferences.showPaidChapters
|
||||||
|
|
||||||
if (useNewQueryEndpoint) {
|
if (useNewChapterEndpoint) {
|
||||||
return result.seasons.orEmpty()
|
val apiHeaders = headersBuilder()
|
||||||
.flatMap { it.chapters.orEmpty() }
|
.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 }
|
.filter { it.price == 0 || showPaidChapters }
|
||||||
.map { it.toSChapter(result.slug, mangaSubDirectory, dateFormat, slugStrategy) }
|
.map { it.toSChapter(seriesSlug, mangaSubDirectory, dateFormat) }
|
||||||
.filter { it.date_upload <= currentTimestamp }
|
.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 }
|
.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 }
|
.filter { it.date_upload <= currentTimestamp }
|
||||||
.reversed()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getChapterUrl(chapter: SChapter): String {
|
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url.substringBeforeLast("#")
|
||||||
if (slugStrategy == SlugStrategy.NONE) return baseUrl + chapter.url
|
|
||||||
|
|
||||||
val seriesSlug = chapter.url
|
override fun pageListRequest(chapter: SChapter) =
|
||||||
.substringAfter("/$mangaSubDirectory/")
|
GET(apiUrl + chapter.url.replace("/$mangaSubDirectory/", "/chapter/"), headers)
|
||||||
.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 pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
if (useNewQueryEndpoint) {
|
val result = response.parseAs<HeanCmsPagePayloadDto>()
|
||||||
val paidChapter = response.request.url.fragment?.contains("-paid")
|
|
||||||
|
|
||||||
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")
|
return if (useNewChapterEndpoint) {
|
||||||
|
result.chapter.chapterData?.images.orEmpty().mapIndexed { i, img ->
|
||||||
if (images == null && paidChapter == true) {
|
Page(i, imageUrl = img)
|
||||||
throw IOException(intl.paidChapterError)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
return images?.select("img").orEmpty().mapIndexed { i, img ->
|
result.data.orEmpty().mapIndexed { i, img ->
|
||||||
val imageUrl = if (img.hasClass("lazy")) img.absUrl("data-src") else img.absUrl("src")
|
Page(i, imageUrl = img)
|
||||||
Page(i, "", imageUrl)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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!!)
|
override fun fetchImageUrl(page: Page): Observable<String> = Observable.just(page.imageUrl!!)
|
||||||
|
@ -523,121 +307,18 @@ abstract class HeanCms(
|
||||||
return GET(page.imageUrl!!, imageHeaders)
|
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(
|
protected open fun getStatusList(): List<Status> = listOf(
|
||||||
Status(intl.statusAll, "All"),
|
Status(intl["status_all"], "All"),
|
||||||
Status(intl.statusOngoing, "Ongoing"),
|
Status(intl["status_ongoing"], "Ongoing"),
|
||||||
Status(intl.statusOnHiatus, "Hiatus"),
|
Status(intl["status_onhiatus"], "Hiatus"),
|
||||||
Status(intl.statusDropped, "Dropped"),
|
Status(intl["status_dropped"], "Dropped"),
|
||||||
)
|
)
|
||||||
|
|
||||||
protected open fun getSortProperties(): List<SortProperty> = listOf(
|
protected open fun getSortProperties(): List<SortProperty> = listOf(
|
||||||
SortProperty(intl.sortByTitle, "title"),
|
SortProperty(intl["sort_by_title"], "title"),
|
||||||
SortProperty(intl.sortByViews, "total_views"),
|
SortProperty(intl["sort_by_views"], "total_views"),
|
||||||
SortProperty(intl.sortByLatest, "latest"),
|
SortProperty(intl["sort_by_latest"], "latest"),
|
||||||
SortProperty(intl.sortByCreatedAt, "created_at"),
|
SortProperty(intl["sort_by_created_at"], "created_at"),
|
||||||
)
|
)
|
||||||
|
|
||||||
protected open fun getGenreList(): List<Genre> = emptyList()
|
protected open fun getGenreList(): List<Genre> = emptyList()
|
||||||
|
@ -646,15 +327,24 @@ abstract class HeanCms(
|
||||||
val genres = getGenreList()
|
val genres = getGenreList()
|
||||||
|
|
||||||
val filters = listOfNotNull(
|
val filters = listOfNotNull(
|
||||||
Filter.Header(intl.filterWarning),
|
StatusFilter(intl["status_filter_title"], getStatusList()),
|
||||||
StatusFilter(intl.statusFilterTitle, getStatusList()),
|
SortByFilter(intl["sort_by_filter_title"], getSortProperties()),
|
||||||
SortByFilter(intl.sortByFilterTitle, getSortProperties()),
|
GenreFilter(intl["genre_filter_title"], genres).takeIf { genres.isNotEmpty() },
|
||||||
GenreFilter(intl.genreFilterTitle, genres).takeIf { genres.isNotEmpty() },
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return FilterList(filters)
|
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 {
|
protected inline fun <reified T> Response.parseAs(): T = use {
|
||||||
it.body.string().parseAs()
|
it.body.string().parseAs()
|
||||||
}
|
}
|
||||||
|
@ -664,18 +354,6 @@ abstract class HeanCms(
|
||||||
protected inline fun <reified R> List<*>.firstInstanceOrNull(): R? =
|
protected inline fun <reified R> List<*>.firstInstanceOrNull(): R? =
|
||||||
filterIsInstance<R>().firstOrNull()
|
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
|
private val SharedPreferences.showPaidChapters: Boolean
|
||||||
get() = getBoolean(SHOW_PAID_CHAPTERS_PREF, SHOW_PAID_CHAPTERS_DEFAULT)
|
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_IMAGE = "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"
|
||||||
private const val ACCEPT_JSON = "application/json, text/plain, */*"
|
private const val ACCEPT_JSON = "application/json, text/plain, */*"
|
||||||
|
|
||||||
private val JSON_MEDIA_TYPE = "application/json".toMediaType()
|
private const val PER_PAGE_CHAPTERS = 1000
|
||||||
|
|
||||||
val TIMESTAMP_REGEX = """-\d{13}$""".toRegex()
|
|
||||||
|
|
||||||
private const val PER_PAGE_MANGA_TITLES = 10000
|
|
||||||
|
|
||||||
const val SEARCH_PREFIX = "slug:"
|
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_PREF = "pref_show_paid_chap"
|
||||||
private const val SHOW_PAID_CHAPTERS_DEFAULT = false
|
private const val SHOW_PAID_CHAPTERS_DEFAULT = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package eu.kanade.tachiyomi.multisrc.heancms
|
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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
|
@ -9,59 +8,30 @@ import org.jsoup.Jsoup
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class HeanCmsQuerySearchDto(
|
class HeanCmsQuerySearchDto(
|
||||||
val data: List<HeanCmsSeriesDto> = emptyList(),
|
val data: List<HeanCmsSeriesDto> = emptyList(),
|
||||||
val meta: HeanCmsQuerySearchMetaDto? = null,
|
val meta: HeanCmsQuerySearchMetaDto? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class HeanCmsQuerySearchMetaDto(
|
class HeanCmsQuerySearchMetaDto(
|
||||||
@SerialName("current_page") val currentPage: Int,
|
@SerialName("current_page") private val currentPage: Int,
|
||||||
@SerialName("last_page") val lastPage: Int,
|
@SerialName("last_page") private val lastPage: Int,
|
||||||
) {
|
) {
|
||||||
|
fun hasNextPage() = currentPage < lastPage
|
||||||
val hasNextPage: Boolean
|
|
||||||
get() = currentPage < lastPage
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class HeanCmsSearchDto(
|
class HeanCmsSeriesDto(
|
||||||
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(
|
|
||||||
val id: Int,
|
val id: Int,
|
||||||
@SerialName("series_slug") val slug: String,
|
@SerialName("series_slug") val slug: String,
|
||||||
@SerialName("series_type") val type: String = "Comic",
|
private val author: String? = null,
|
||||||
val author: String? = null,
|
private val description: String? = null,
|
||||||
val description: String? = null,
|
private val studio: String? = null,
|
||||||
val studio: String? = null,
|
private val status: String? = null,
|
||||||
val status: String? = null,
|
private val thumbnail: String,
|
||||||
val thumbnail: String,
|
private val title: String,
|
||||||
val title: String,
|
private val tags: List<HeanCmsTagDto>? = emptyList(),
|
||||||
val tags: List<HeanCmsTagDto>? = emptyList(),
|
|
||||||
val chapters: List<HeanCmsChapterDto>? = emptyList(),
|
|
||||||
val seasons: List<HeanCmsSeasonsDto>? = emptyList(),
|
val seasons: List<HeanCmsSeasonsDto>? = emptyList(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -69,10 +39,8 @@ data class HeanCmsSeriesDto(
|
||||||
apiUrl: String,
|
apiUrl: String,
|
||||||
coverPath: String,
|
coverPath: String,
|
||||||
mangaSubDirectory: String,
|
mangaSubDirectory: String,
|
||||||
slugStrategy: SlugStrategy,
|
|
||||||
): SManga = SManga.create().apply {
|
): SManga = SManga.create().apply {
|
||||||
val descriptionBody = this@HeanCmsSeriesDto.description?.let(Jsoup::parseBodyFragment)
|
val descriptionBody = this@HeanCmsSeriesDto.description?.let(Jsoup::parseBodyFragment)
|
||||||
val slugOnly = slug.toPermSlugIfNeeded(slugStrategy)
|
|
||||||
|
|
||||||
title = this@HeanCmsSeriesDto.title
|
title = this@HeanCmsSeriesDto.title
|
||||||
author = this@HeanCmsSeriesDto.author?.trim()
|
author = this@HeanCmsSeriesDto.author?.trim()
|
||||||
|
@ -86,89 +54,84 @@ data class HeanCmsSeriesDto(
|
||||||
thumbnail_url = thumbnail.ifEmpty { null }
|
thumbnail_url = thumbnail.ifEmpty { null }
|
||||||
?.toAbsoluteThumbnailUrl(apiUrl, coverPath)
|
?.toAbsoluteThumbnailUrl(apiUrl, coverPath)
|
||||||
status = this@HeanCmsSeriesDto.status?.toStatus() ?: SManga.UNKNOWN
|
status = this@HeanCmsSeriesDto.status?.toStatus() ?: SManga.UNKNOWN
|
||||||
url = if (slugStrategy != SlugStrategy.NONE) {
|
url = "/$mangaSubDirectory/$slug#$id"
|
||||||
"/$mangaSubDirectory/$slugOnly#$id"
|
|
||||||
} else {
|
|
||||||
"/$mangaSubDirectory/$slug"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class HeanCmsSeasonsDto(
|
class HeanCmsSeasonsDto(
|
||||||
val index: Int,
|
|
||||||
val chapters: List<HeanCmsChapterDto>? = emptyList(),
|
val chapters: List<HeanCmsChapterDto>? = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class HeanCmsTagDto(val name: String)
|
class HeanCmsTagDto(val name: String)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class HeanCmsChapterDto(
|
class HeanCmsChapterPayloadDto(
|
||||||
val id: Int,
|
val data: List<HeanCmsChapterDto>,
|
||||||
@SerialName("chapter_name") val name: String,
|
val meta: HeanCmsChapterMetaDto,
|
||||||
@SerialName("chapter_slug") val slug: String,
|
)
|
||||||
val index: String,
|
|
||||||
@SerialName("created_at") val createdAt: String,
|
@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,
|
val price: Int? = null,
|
||||||
) {
|
) {
|
||||||
fun toSChapter(
|
fun toSChapter(
|
||||||
seriesSlug: String,
|
seriesSlug: String,
|
||||||
mangaSubDirectory: String,
|
mangaSubDirectory: String,
|
||||||
dateFormat: SimpleDateFormat,
|
dateFormat: SimpleDateFormat,
|
||||||
slugStrategy: SlugStrategy,
|
|
||||||
): SChapter = SChapter.create().apply {
|
): SChapter = SChapter.create().apply {
|
||||||
val seriesSlugOnly = seriesSlug.toPermSlugIfNeeded(slugStrategy)
|
|
||||||
name = this@HeanCmsChapterDto.name.trim()
|
name = this@HeanCmsChapterDto.name.trim()
|
||||||
|
|
||||||
if (price != 0) {
|
if (price != 0) {
|
||||||
name += " \uD83D\uDD12"
|
name += " \uD83D\uDD12"
|
||||||
}
|
}
|
||||||
|
|
||||||
date_upload = runCatching { dateFormat.parse(createdAt)?.time }
|
date_upload = try {
|
||||||
.getOrNull() ?: 0L
|
dateFormat.parse(createdAt)?.time ?: 0L
|
||||||
|
} catch (_: Exception) {
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
|
||||||
val paidStatus = if (price != 0 && price != null) "-paid" else ""
|
url = "/$mangaSubDirectory/$seriesSlug/$slug#$id"
|
||||||
|
|
||||||
url = "/$mangaSubDirectory/$seriesSlugOnly/$slug#$id$paidStatus"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class HeanCmsReaderDto(
|
class HeanCmsChapterMetaDto(
|
||||||
val content: HeanCmsReaderContentDto? = null,
|
@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
|
@Serializable
|
||||||
data class HeanCmsReaderContentDto(
|
class HeanCmsPageDataDto(
|
||||||
val images: List<String>? = emptyList(),
|
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 {
|
private fun String.toAbsoluteThumbnailUrl(apiUrl: String, coverPath: String): String {
|
||||||
return if (startsWith("https://")) this else "$apiUrl/$coverPath$this"
|
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) {
|
fun String.toStatus(): Int = when (this) {
|
||||||
"Ongoing" -> SManga.ONGOING
|
"Ongoing" -> SManga.ONGOING
|
||||||
"Hiatus" -> SManga.ON_HIATUS
|
"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
|
genre_exclusion_warning=Genre exclusion is not available for all sources
|
||||||
project_filter_warning=NOTE: Can't be used with other filter!
|
project_filter_warning=NOTE: Can't be used with other filter!
|
||||||
project_filter_name=%s Project List page
|
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")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 2
|
baseVersionCode = 3
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Only PeachScan sources uses the image-decoder dependency.
|
compileOnly("com.github.tachiyomiorg:image-decoder:398d3c074f")
|
||||||
//noinspection UseTomlInstead
|
|
||||||
compileOnly("com.github.tachiyomiorg:image-decoder:fbd6601290")
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 4
|
baseVersionCode = 5
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api(project(":lib:i18n"))
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package eu.kanade.tachiyomi.multisrc.wpcomics
|
package eu.kanade.tachiyomi.multisrc.wpcomics
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.lib.i18n.Intl
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
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.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
@ -20,23 +24,28 @@ import java.util.Locale
|
||||||
abstract class WPComics(
|
abstract class WPComics(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val baseUrl: String,
|
override val baseUrl: String,
|
||||||
override val lang: String,
|
final override val lang: String,
|
||||||
private val dateFormat: SimpleDateFormat = SimpleDateFormat("HH:mm - dd/MM/yyyy Z", Locale.US),
|
protected val dateFormat: SimpleDateFormat = SimpleDateFormat("HH:mm - dd/MM/yyyy Z", Locale.US),
|
||||||
private val gmtOffset: String? = "+0500",
|
protected val gmtOffset: String? = "+0500",
|
||||||
) : ParsedHttpSource() {
|
) : ParsedHttpSource() {
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
override val client: OkHttpClient = network.cloudflareClient
|
override val client: OkHttpClient = network.cloudflareClient
|
||||||
|
|
||||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0")
|
.add("Referer", "$baseUrl/")
|
||||||
.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
|
// Popular
|
||||||
|
|
||||||
open val popularPath = "hot"
|
open val popularPath = "hot"
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
@ -58,7 +67,6 @@ abstract class WPComics(
|
||||||
override fun popularMangaNextPageSelector() = "a.next-page, a[rel=next]"
|
override fun popularMangaNextPageSelector() = "a.next-page, a[rel=next]"
|
||||||
|
|
||||||
// Latest
|
// Latest
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
return GET(baseUrl + if (page > 1) "?page=$page" else "", headers)
|
return GET(baseUrl + if (page > 1) "?page=$page" else "", headers)
|
||||||
}
|
}
|
||||||
|
@ -70,35 +78,27 @@ abstract class WPComics(
|
||||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
|
|
||||||
protected open val searchPath = "tim-truyen"
|
protected open val searchPath = "tim-truyen"
|
||||||
protected open val queryParam = "keyword"
|
protected open val queryParam = "keyword"
|
||||||
|
|
||||||
protected open fun String.replaceSearchPath() = this
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val filterList = filters.let { if (it.isEmpty()) getFilterList() else it }
|
val url = "$baseUrl/$searchPath".toHttpUrl().newBuilder()
|
||||||
return if (filterList.isEmpty()) {
|
|
||||||
GET("$baseUrl/?s=$query&post_type=comics&page=$page")
|
|
||||||
} else {
|
|
||||||
val url = "$baseUrl/$searchPath".toHttpUrl().newBuilder()
|
|
||||||
|
|
||||||
filterList.forEach { filter ->
|
filters.forEach { filter ->
|
||||||
when (filter) {
|
when (filter) {
|
||||||
is GenreFilter -> filter.toUriPart()?.let { url.addPathSegment(it) }
|
is GenreFilter -> filter.toUriPart()?.let { url.addPathSegment(it) }
|
||||||
is StatusFilter -> filter.toUriPart()?.let { url.addQueryParameter("status", it) }
|
is StatusFilter -> filter.toUriPart()?.let { url.addQueryParameter("status", it) }
|
||||||
else -> {}
|
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"
|
override fun searchMangaSelector() = "div.items div.item"
|
||||||
|
@ -116,22 +116,23 @@ abstract class WPComics(
|
||||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
|
||||||
// Details
|
// Details
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga {
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
return SManga.create().apply {
|
return SManga.create().apply {
|
||||||
document.select("article#item-detail").let { info ->
|
document.select("article#item-detail").let { info ->
|
||||||
author = info.select("li.author p.col-xs-8").text()
|
author = info.select("li.author p.col-xs-8").text()
|
||||||
status = info.select("li.status p.col-xs-8").text().toStatus()
|
status = info.select("li.status p.col-xs-8").text().toStatus()
|
||||||
genre = info.select("li.kind p.col-xs-8 a").joinToString { it.text() }
|
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()!!)
|
thumbnail_url = imageOrNull(info.select("div.col-image img").first()!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun String?.toStatus(): Int {
|
open fun String?.toStatus(): Int {
|
||||||
val ongoingWords = listOf("Ongoing", "Updating", "Đang tiến hành")
|
val ongoingWords = listOf("Ongoing", "Updating", "Đang tiến hành", "連載中")
|
||||||
val completedWords = listOf("Complete", "Completed", "Hoàn thành")
|
val completedWords = listOf("Complete", "Completed", "Hoàn thành", "完結済み")
|
||||||
return when {
|
return when {
|
||||||
this == null -> SManga.UNKNOWN
|
this == null -> SManga.UNKNOWN
|
||||||
ongoingWords.doesInclude(this) -> SManga.ONGOING
|
ongoingWords.doesInclude(this) -> SManga.ONGOING
|
||||||
|
@ -141,7 +142,6 @@ abstract class WPComics(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chapters
|
// Chapters
|
||||||
|
|
||||||
override fun chapterListSelector() = "div.list-chapter li.row:not(.heading)"
|
override fun chapterListSelector() = "div.list-chapter li.row:not(.heading)"
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element): SChapter {
|
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 {
|
protected open fun String?.toDate(): Long {
|
||||||
this ?: return 0
|
this ?: return 0L
|
||||||
|
|
||||||
val secondWords = listOf("second", "giây")
|
val secondWords = listOf("second", "giây")
|
||||||
val minuteWords = listOf("minute", "phút")
|
val minuteWords = listOf("minute", "phút")
|
||||||
|
@ -182,10 +182,10 @@ abstract class WPComics(
|
||||||
(if (gmtOffset == null) this.substringAfterLast(" ") else "$this $gmtOffset").let {
|
(if (gmtOffset == null) this.substringAfterLast(" ") else "$this $gmtOffset").let {
|
||||||
// timestamp has year
|
// timestamp has year
|
||||||
if (Regex("""\d+/\d+/\d\d""").find(it)?.value != null) {
|
if (Regex("""\d+/\d+/\d\d""").find(it)?.value != null) {
|
||||||
dateFormat.parse(it)?.time ?: 0
|
dateFormat.parse(it)?.time ?: 0L
|
||||||
} else {
|
} else {
|
||||||
// MangaSum - timestamp sometimes doesn't have year (current year implied)
|
// 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
|
// Pages
|
||||||
|
|
||||||
// sources sometimes have an image element with an empty attr that isn't really an image
|
|
||||||
open fun imageOrNull(element: Element): String? {
|
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 {
|
fun Element.hasValidAttr(attr: String): Boolean {
|
||||||
val regex = Regex("""https?://.*""", RegexOption.IGNORE_CASE)
|
val regex = Regex("""https?://.*""", RegexOption.IGNORE_CASE)
|
||||||
return when {
|
return when {
|
||||||
|
@ -226,80 +225,74 @@ abstract class WPComics(
|
||||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
||||||
|
|
||||||
// Filters
|
// 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(name: String, pairs: List<Pair<String?, String>>) : UriPartFilter(name, pairs)
|
||||||
protected class GenreFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Genre", vals)
|
|
||||||
|
|
||||||
protected open fun getStatusList(): Array<Pair<String?, String>> = arrayOf(
|
protected open fun getStatusList(): List<Pair<String?, String>> =
|
||||||
Pair(null, "Tất cả"),
|
listOf(
|
||||||
Pair("1", "Đang tiến hành"),
|
Pair(null, intl["STATUS_ALL"]),
|
||||||
Pair("2", "Đã hoàn thành"),
|
Pair("1", intl["STATUS_ONGOING"]),
|
||||||
Pair("3", "Tạm ngừng"),
|
Pair("2", intl["STATUS_COMPLETED"]),
|
||||||
)
|
)
|
||||||
protected open fun getGenreList(): Array<Pair<String?, String>> = arrayOf(
|
|
||||||
null to "Tất cả",
|
protected var genreList: List<Pair<String?, String>> = emptyList()
|
||||||
"action" to "Action",
|
|
||||||
"adult" to "Adult",
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
"adventure" to "Adventure",
|
|
||||||
"anime" to "Anime",
|
protected fun launchIO(block: () -> Unit) = scope.launch { block() }
|
||||||
"chuyen-sinh" to "Chuyển Sinh",
|
|
||||||
"comedy" to "Comedy",
|
private var fetchGenresAttempts: Int = 0
|
||||||
"comic" to "Comic",
|
|
||||||
"cooking" to "Cooking",
|
protected fun fetchGenres() {
|
||||||
"co-dai" to "Cổ Đại",
|
if (fetchGenresAttempts < 3 && genreList.isEmpty()) {
|
||||||
"doujinshi" to "Doujinshi",
|
try {
|
||||||
"drama" to "Drama",
|
genreList =
|
||||||
"dam-my" to "Đam Mỹ",
|
client.newCall(genresRequest()).execute()
|
||||||
"ecchi" to "Ecchi",
|
.asJsoup()
|
||||||
"fantasy" to "Fantasy",
|
.let(::parseGenres)
|
||||||
"gender-bender" to "Gender Bender",
|
} catch (_: Exception) {
|
||||||
"harem" to "Harem",
|
} finally {
|
||||||
"historical" to "Historical",
|
fetchGenresAttempts++
|
||||||
"horror" to "Horror",
|
}
|
||||||
"josei" to "Josei",
|
}
|
||||||
"live-action" to "Live action",
|
}
|
||||||
"manga" to "Manga",
|
|
||||||
"manhua" to "Manhua",
|
protected open fun genresRequest() = GET("$baseUrl/$searchPath", headers)
|
||||||
"manhwa" to "Manhwa",
|
|
||||||
"martial-arts" to "Martial Arts",
|
protected open val genresSelector = ".genres ul.nav li:not(.active) a"
|
||||||
"mature" to "Mature",
|
|
||||||
"mecha" to "Mecha",
|
protected open val genresUrlDelimiter = "/"
|
||||||
"mystery" to "Mystery",
|
|
||||||
"ngon-tinh" to "Ngôn Tình",
|
protected open fun parseGenres(document: Document): List<Pair<String?, String>> {
|
||||||
"one-shot" to "One shot",
|
val items = document.select(genresSelector)
|
||||||
"psychological" to "Psychological",
|
return buildList(items.size + 1) {
|
||||||
"romance" to "Romance",
|
add(Pair(null, intl["STATUS_ALL"]))
|
||||||
"school-life" to "School Life",
|
items.mapTo(this) {
|
||||||
"sci-fi" to "Sci-fi",
|
Pair(
|
||||||
"seinen" to "Seinen",
|
it.attr("href")
|
||||||
"shoujo" to "Shoujo",
|
.removeSuffix("/")
|
||||||
"shoujo-ai" to "Shoujo Ai",
|
.substringAfterLast(genresUrlDelimiter),
|
||||||
"shounen" to "Shounen",
|
it.text(),
|
||||||
"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",
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun getFilterList(): FilterList {
|
override fun getFilterList(): FilterList {
|
||||||
|
launchIO { fetchGenres() }
|
||||||
return FilterList(
|
return FilterList(
|
||||||
StatusFilter(getStatusList()),
|
StatusFilter(intl["STATUS"], getStatusList()),
|
||||||
GenreFilter(getGenreList()),
|
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>>) :
|
protected open class UriPartFilter(displayName: String, private val pairs: List<Pair<String?, String>>) :
|
||||||
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray()) {
|
Filter.Select<String>(displayName, pairs.map { it.second }.toTypedArray()) {
|
||||||
fun toUriPart() = vals[state].first
|
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 {
|
ext {
|
||||||
extName = 'Beast Scans'
|
extName = 'Umi Manga'
|
||||||
extClass = '.BeastScans'
|
extClass = '.UmiManga'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://beastscans.net'
|
baseUrl = 'https://www.umimanga.com'
|
||||||
overrideVersionCode = 1
|
overrideVersionCode = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
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 {
|
ext {
|
||||||
extName = 'Hentai Slayer'
|
extName = 'Hentai Slayer'
|
||||||
extClass = '.HentaiSlayer'
|
extClass = '.HentaiSlayer'
|
||||||
extVersionCode = 1
|
extVersionCode = 2
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,10 +48,10 @@ class HentaiSlayer : ParsedHttpSource(), ConfigurableSource {
|
||||||
override fun popularMangaSelector() = "div > div:has(div#card-real)"
|
override fun popularMangaSelector() = "div > div:has(div#card-real)"
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
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"))
|
setUrlWithoutDomain(absUrl("href"))
|
||||||
selectFirst("figure")?.run {
|
with(selectFirst("figure")!!) {
|
||||||
selectFirst("img.object-cover")?.run {
|
with(selectFirst("img.object-cover")!!) {
|
||||||
thumbnail_url = imgAttr()
|
thumbnail_url = imgAttr()
|
||||||
title = attr("alt")
|
title = attr("alt")
|
||||||
}
|
}
|
||||||
|
@ -98,21 +98,17 @@ class HentaiSlayer : ParsedHttpSource(), ConfigurableSource {
|
||||||
|
|
||||||
// =========================== Manga Details ============================
|
// =========================== Manga Details ============================
|
||||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||||
document.selectFirst("main section")?.run {
|
with(document.selectFirst("main section")!!) {
|
||||||
selectFirst("img#manga-cover")?.run {
|
thumbnail_url = selectFirst("img#manga-cover")!!.imgAttr()
|
||||||
thumbnail_url = imgAttr()
|
with(selectFirst("section > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div:nth-child(2)")!!) {
|
||||||
title = attr("alt")
|
|
||||||
}
|
|
||||||
selectFirst("section > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div:nth-child(2)")?.run {
|
|
||||||
status = parseStatus(select("a[href*='?status=']").text())
|
status = parseStatus(select("a[href*='?status=']").text())
|
||||||
genre = select("a[href*='?type=']").text()
|
genre = select("a[href*='?type=']").text()
|
||||||
author = select("p:has(span:contains(المؤلف)) span:nth-child(2)").text()
|
author = select("p:has(span:contains(المؤلف)) span:nth-child(2)").text()
|
||||||
artist = 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 {
|
var desc = "\u061C"
|
||||||
select("h1").text().takeIf { it.isNotEmpty() }?.let {
|
with(selectFirst("section > div:nth-child(1) > div:nth-child(2)")!!) {
|
||||||
title = it
|
title = selectFirst("h1")!!.text()
|
||||||
}
|
|
||||||
genre = select("a[href*='?genre=']")
|
genre = select("a[href*='?genre=']")
|
||||||
.map { it.text() }
|
.map { it.text() }
|
||||||
.let {
|
.let {
|
||||||
|
@ -120,10 +116,10 @@ class HentaiSlayer : ParsedHttpSource(), ConfigurableSource {
|
||||||
}
|
}
|
||||||
.joinToString()
|
.joinToString()
|
||||||
select("h2").text().takeIf { it.isNotEmpty() }?.let {
|
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 {
|
ext {
|
||||||
extName = 'Hijala'
|
extName = 'Hijala'
|
||||||
extClass = '.Hijala'
|
extClass = '.Hijala'
|
||||||
themePkg = 'zeistmanga'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://hijala.blogspot.com'
|
baseUrl = 'https://www.hijala.com'
|
||||||
overrideVersionCode = 0
|
overrideVersionCode = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,57 +1,15 @@
|
||||||
package eu.kanade.tachiyomi.extension.ar.hijala
|
package eu.kanade.tachiyomi.extension.ar.hijala
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.zeistmanga.Genre
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
import eu.kanade.tachiyomi.multisrc.zeistmanga.ZeistManga
|
import java.text.SimpleDateFormat
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import java.util.Locale
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
|
|
||||||
class Hijala : ZeistManga("Hijala", "https://hijala.blogspot.com", "ar") {
|
class Hijala : MangaThemesia(
|
||||||
|
"Hijala",
|
||||||
override val hasFilters = true
|
"https://www.hijala.com",
|
||||||
override val hasLanguageFilter = false
|
"ar",
|
||||||
|
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
||||||
override val supportsLatest = false
|
) {
|
||||||
|
// Site moved from ZeistManga to MangaThemesia
|
||||||
override fun popularMangaRequest(page: Int): Request = latestUpdatesRequest(page)
|
override val versionId get() = 2
|
||||||
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("ميكا", "ميكا"),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
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'
|
extName = 'Manga Flame'
|
||||||
extClass = '.MangaFlame'
|
extClass = '.MangaFlame'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://mangaflame.org'
|
baseUrl = 'https://arisescans.com'
|
||||||
overrideVersionCode = 1
|
overrideVersionCode = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
package eu.kanade.tachiyomi.extension.ar.mangaflame
|
package eu.kanade.tachiyomi.extension.ar.mangaflame
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class MangaFlame : MangaThemesia(
|
class MangaFlame : MangaThemesia(
|
||||||
"Manga Flame",
|
"Manga Flame",
|
||||||
"https://mangaflame.org",
|
"https://arisescans.com",
|
||||||
"ar",
|
"ar",
|
||||||
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
||||||
) {
|
) {
|
||||||
override val id = 1501237443119573205
|
override val id = 1501237443119573205
|
||||||
|
|
||||||
|
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||||
|
.readTimeout(3, TimeUnit.MINUTES)
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,8 @@ ext {
|
||||||
extName = 'Mangalek'
|
extName = 'Mangalek'
|
||||||
extClass = '.Mangalek'
|
extClass = '.Mangalek'
|
||||||
themePkg = 'madara'
|
themePkg = 'madara'
|
||||||
baseUrl = 'https://manga-lek.net'
|
baseUrl = 'https://lekmanga.net'
|
||||||
overrideVersionCode = 4
|
overrideVersionCode = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -21,10 +21,11 @@ import uy.kohesive.injekt.api.get
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
private const val mangalekUrl = "https://lekmanga.net"
|
||||||
class Mangalek :
|
class Mangalek :
|
||||||
Madara(
|
Madara(
|
||||||
"مانجا ليك",
|
"مانجا ليك",
|
||||||
"https://manga-lek.net",
|
mangalekUrl,
|
||||||
"ar",
|
"ar",
|
||||||
SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
||||||
),
|
),
|
||||||
|
@ -34,7 +35,7 @@ class Mangalek :
|
||||||
override val useLoadMoreRequest = LoadMoreStrategy.Always
|
override val useLoadMoreRequest = LoadMoreStrategy.Always
|
||||||
override val chapterUrlSuffix = ""
|
override val chapterUrlSuffix = ""
|
||||||
|
|
||||||
private val defaultBaseUrl = "https://manga-lek.net"
|
private val defaultBaseUrl = mangalekUrl
|
||||||
override val baseUrl by lazy { getPrefBaseUrl() }
|
override val baseUrl by lazy { getPrefBaseUrl() }
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
private val preferences: SharedPreferences by lazy {
|
||||||
|
|
|
@ -2,8 +2,9 @@ ext {
|
||||||
extName = 'MangaLionz'
|
extName = 'MangaLionz'
|
||||||
extClass = '.MangaLionz'
|
extClass = '.MangaLionz'
|
||||||
themePkg = 'madara'
|
themePkg = 'madara'
|
||||||
baseUrl = 'https://mangalionz.org'
|
baseUrl = 'https://manga-lionz.com'
|
||||||
overrideVersionCode = 3
|
overrideVersionCode = 4
|
||||||
|
isNsfw = false
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
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.multisrc.madara.Madara
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import org.jsoup.nodes.Element
|
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 val useLoadMoreRequest = LoadMoreStrategy.Always
|
||||||
override fun popularMangaFromElement(element: Element): SManga {
|
override fun popularMangaFromElement(element: Element): SManga {
|
||||||
val manga = SManga.create()
|
val manga = SManga.create()
|
||||||
|
|
|
@ -2,8 +2,8 @@ ext {
|
||||||
extName = 'MangaSpark'
|
extName = 'MangaSpark'
|
||||||
extClass = '.MangaSpark'
|
extClass = '.MangaSpark'
|
||||||
themePkg = 'madara'
|
themePkg = 'madara'
|
||||||
baseUrl = 'https://mangaspark.org'
|
baseUrl = 'https://manga-spark.net'
|
||||||
overrideVersionCode = 5
|
overrideVersionCode = 6
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -6,7 +6,7 @@ import java.util.Locale
|
||||||
|
|
||||||
class MangaSpark : Madara(
|
class MangaSpark : Madara(
|
||||||
"MangaSpark",
|
"MangaSpark",
|
||||||
"https://mangaspark.org",
|
"https://manga-spark.net",
|
||||||
"ar",
|
"ar",
|
||||||
dateFormat = SimpleDateFormat("d MMMM، yyyy", Locale("ar")),
|
dateFormat = SimpleDateFormat("d MMMM، yyyy", Locale("ar")),
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -2,8 +2,8 @@ ext {
|
||||||
extName = 'Manga Starz'
|
extName = 'Manga Starz'
|
||||||
extClass = '.MangaStarz'
|
extClass = '.MangaStarz'
|
||||||
themePkg = 'madara'
|
themePkg = 'madara'
|
||||||
baseUrl = 'https://mangastarz.org'
|
baseUrl = 'https://manga-starz.com'
|
||||||
overrideVersionCode = 6
|
overrideVersionCode = 7
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -6,7 +6,7 @@ import java.util.Locale
|
||||||
|
|
||||||
class MangaStarz : Madara(
|
class MangaStarz : Madara(
|
||||||
"Manga Starz",
|
"Manga Starz",
|
||||||
"https://mangastarz.org",
|
"https://manga-starz.com",
|
||||||
"ar",
|
"ar",
|
||||||
dateFormat = SimpleDateFormat("d MMMM، yyyy", Locale("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 {
|
ext {
|
||||||
extName = 'Ozul Scans'
|
extName = 'King Of Manga'
|
||||||
extClass = '.OzulScans'
|
extClass = '.KingOfManga'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://kingofmanga.com'
|
baseUrl = 'https://king-ofmanga.com'
|
||||||
overrideVersionCode = 2
|
overrideVersionCode = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
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 {
|
ext {
|
||||||
extName = 'Anchira'
|
extName = 'Anchira'
|
||||||
extClass = '.Anchira'
|
extClass = '.Anchira'
|
||||||
extVersionCode = 10
|
extVersionCode = 11
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -120,11 +120,6 @@ class Anchira : HttpSource(), ConfigurableSource {
|
||||||
query.substringAfter(SLUG_BUNDLE_PREFIX),
|
query.substringAfter(SLUG_BUNDLE_PREFIX),
|
||||||
filters,
|
filters,
|
||||||
).removeAllQueryParameters("page")
|
).removeAllQueryParameters("page")
|
||||||
if (
|
|
||||||
url.build().queryParameter("sort") == "4"
|
|
||||||
) {
|
|
||||||
url.removeAllQueryParameters("sort")
|
|
||||||
}
|
|
||||||
val manga = SManga.create()
|
val manga = SManga.create()
|
||||||
.apply { this.url = "?${url.build().query}" }
|
.apply { this.url = "?${url.build().query}" }
|
||||||
fetchMangaDetails(manga).map {
|
fetchMangaDetails(manga).map {
|
||||||
|
@ -280,7 +275,7 @@ class Anchira : HttpSource(), ConfigurableSource {
|
||||||
for (page in 1..pages) {
|
for (page in 1..pages) {
|
||||||
results.entries.forEach { data ->
|
results.entries.forEach { data ->
|
||||||
chapterList.add(
|
chapterList.add(
|
||||||
createChapter(data, response, anchiraData),
|
createChapter(data, anchiraData),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (page < pages) {
|
if (page < pages) {
|
||||||
|
@ -298,7 +293,7 @@ class Anchira : HttpSource(), ConfigurableSource {
|
||||||
} else {
|
} else {
|
||||||
val data = json.decodeFromString<Entry>(response.body.string())
|
val data = json.decodeFromString<Entry>(response.body.string())
|
||||||
chapterList.add(
|
chapterList.add(
|
||||||
createChapter(data, response, anchiraData),
|
createChapter(data, anchiraData),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return chapterList
|
return chapterList
|
||||||
|
@ -468,7 +463,7 @@ class Anchira : HttpSource(), ConfigurableSource {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val SLUG_SEARCH_PREFIX = "id:"
|
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 IMAGE_QUALITY_PREF = "image_quality"
|
||||||
private const val OPEN_SOURCE_PREF = "use_manga_source"
|
private const val OPEN_SOURCE_PREF = "use_manga_source"
|
||||||
private const val USE_TAG_GROUPING = "use_tag_grouping"
|
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
|
package eu.kanade.tachiyomi.extension.en.anchira
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import okhttp3.Response
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
object AnchiraHelper {
|
object AnchiraHelper {
|
||||||
|
@ -13,6 +12,7 @@ object AnchiraHelper {
|
||||||
}
|
}
|
||||||
it
|
it
|
||||||
}
|
}
|
||||||
|
.sortedBy { it.name }
|
||||||
.sortedBy { it.namespace }
|
.sortedBy { it.namespace }
|
||||||
.map {
|
.map {
|
||||||
val tag = it.name.lowercase()
|
val tag = it.name.lowercase()
|
||||||
|
@ -30,30 +30,31 @@ object AnchiraHelper {
|
||||||
}
|
}
|
||||||
.joinToString(", ") { it }
|
.joinToString(", ") { it }
|
||||||
|
|
||||||
fun createChapter(entry: Entry, response: Response, anchiraData: List<EntryKey>) =
|
fun createChapter(entry: Entry, anchiraData: List<EntryKey>) =
|
||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
val ch =
|
val chSuffix = CHAPTER_SUFFIX_RE.find(entry.title)?.value.orEmpty()
|
||||||
CHAPTER_SUFFIX_RE.find(entry.title)?.value?.trim('.') ?: "1"
|
val chNumber =
|
||||||
val source = anchiraData.find { it.id == entry.id }?.url
|
chSuffix.replace(Regex("[^.\\d]"), "").trim('.').takeUnless { it.isEmpty() } ?: "1"
|
||||||
?: response.request.url.toString()
|
val source = Regex("fakku|irodori").find(
|
||||||
|
anchiraData.find { it.id == entry.id }?.url.orEmpty(),
|
||||||
|
)?.value.orEmpty().titleCase()
|
||||||
url = "/g/${entry.id}/${entry.key}"
|
url = "/g/${entry.id}/${entry.key}"
|
||||||
name = "$ch. ${entry.title.removeSuffix(" $ch")}"
|
name = "$chNumber. ${entry.title.removeSuffix(chSuffix)}"
|
||||||
date_upload = entry.publishedAt * 1000
|
date_upload = entry.publishedAt * 1000
|
||||||
chapter_number = ch.toFloat()
|
chapter_number = chNumber.toFloat()
|
||||||
scanlator = buildString {
|
scanlator = buildString {
|
||||||
append(
|
if (source.isNotEmpty()) {
|
||||||
Regex("fakku|irodori|anchira").find(source)?.value.orEmpty()
|
append("$source - ")
|
||||||
.replaceFirstChar {
|
}
|
||||||
if (it.isLowerCase()) {
|
append("${entry.pages} pages")
|
||||||
it.titlecase(
|
|
||||||
Locale.getDefault(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
it.toString()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
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'
|
extClass = '.AsuraScans'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://asuratoon.com'
|
baseUrl = 'https://asuratoon.com'
|
||||||
overrideVersionCode = 1
|
overrideVersionCode = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,53 +1,35 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.asurascans
|
package eu.kanade.tachiyomi.extension.en.asurascans
|
||||||
|
|
||||||
import android.app.Application
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesiaAlt
|
||||||
import android.content.SharedPreferences
|
|
||||||
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.network.interceptor.rateLimit
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
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.Request
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
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.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class AsuraScans :
|
class AsuraScans : MangaThemesiaAlt(
|
||||||
MangaThemesia(
|
"Asura Scans",
|
||||||
"Asura Scans",
|
"https://asuratoon.com",
|
||||||
"https://asuratoon.com",
|
"en",
|
||||||
"en",
|
dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.US),
|
||||||
dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.US),
|
randomUrlPrefKey = "pref_permanent_manga_url_2_en",
|
||||||
),
|
) {
|
||||||
ConfigurableSource {
|
init {
|
||||||
|
// remove legacy preferences
|
||||||
private val preferences by lazy {
|
preferences.run {
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
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 {
|
override val client = super.client.newBuilder()
|
||||||
preferences.baseUrlHost.let { "https://$it" }
|
.rateLimit(1, 3)
|
||||||
}
|
|
||||||
|
|
||||||
override val client: OkHttpClient = super.client.newBuilder()
|
|
||||||
.addInterceptor(::urlChangeInterceptor)
|
|
||||||
.addInterceptor(::domainChangeIntercept)
|
|
||||||
.rateLimit(1, 3, TimeUnit.SECONDS)
|
|
||||||
.apply {
|
.apply {
|
||||||
val interceptors = interceptors()
|
val interceptors = interceptors()
|
||||||
val index = interceptors.indexOfFirst { "Brotli" in it.javaClass.simpleName }
|
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, " +
|
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"
|
"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 {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val request = super.searchMangaRequest(page, query, filters)
|
val request = super.searchMangaRequest(page, query, filters)
|
||||||
if (query.isBlank()) return request
|
if (query.isBlank()) return request
|
||||||
|
@ -93,232 +62,10 @@ class AsuraScans :
|
||||||
.build()
|
.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
|
// Skip scriptPages
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
return document.select(pageSelector)
|
return document.select(pageSelector)
|
||||||
.filterNot { it.attr("src").isNullOrEmpty() }
|
.filterNot { it.attr("src").isNullOrEmpty() }
|
||||||
.mapIndexed { i, img -> Page(i, document.location(), img.attr("abs:src")) }
|
.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 {
|
ext {
|
||||||
extName = 'EarlyManga'
|
extName = 'EarlyManga'
|
||||||
extClass = '.EarlyManga'
|
extClass = '.EarlyManga'
|
||||||
extVersionCode = 26
|
extVersionCode = 27
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -218,14 +218,14 @@ class EarlyManga : HttpSource() {
|
||||||
val chapterUrl = response.request.url.toString()
|
val chapterUrl = response.request.url.toString()
|
||||||
.replace("/api", "")
|
.replace("/api", "")
|
||||||
val preSlug = if (result.on_disk != 0 && result.on_disk != null) {
|
val preSlug = if (result.on_disk != 0 && result.on_disk != null) {
|
||||||
"storage/uploads/manga"
|
"$baseUrl/storage/uploads/manga"
|
||||||
} else {
|
} else {
|
||||||
"e-storage/uploads/manga"
|
"https://images.${baseUrl.removePrefix("https://")}/manga"
|
||||||
}
|
}
|
||||||
return result.images
|
return result.images
|
||||||
.filterNot { it.endsWith(".ico") }
|
.filterNot { it.endsWith(".ico") }
|
||||||
.mapIndexed { index, img ->
|
.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'
|
extName = 'Elarc Toon'
|
||||||
extClass = '.ElarcPage'
|
extClass = '.ElarcPage'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://elarctoon.com'
|
baseUrl = 'https://elarctoons.com'
|
||||||
overrideVersionCode = 4
|
overrideVersionCode = 5
|
||||||
isNsfw = false
|
isNsfw = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import java.io.IOException
|
||||||
|
|
||||||
class ElarcPage : MangaThemesia(
|
class ElarcPage : MangaThemesia(
|
||||||
"Elarc Toon",
|
"Elarc Toon",
|
||||||
"https://elarctoon.com",
|
"https://elarctoons.com",
|
||||||
"en",
|
"en",
|
||||||
) {
|
) {
|
||||||
override val id = 5482125641807211052
|
override val id = 5482125641807211052
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.FlameComics'
|
extClass = '.FlameComics'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://flamecomics.com'
|
baseUrl = 'https://flamecomics.com'
|
||||||
overrideVersionCode = 0
|
overrideVersionCode = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,47 +1,30 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.flamecomics
|
package eu.kanade.tachiyomi.extension.en.flamecomics
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
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.Interceptor
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.Protocol
|
import okhttp3.Protocol
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
class FlameComics :
|
class FlameComics : MangaThemesia(
|
||||||
MangaThemesia(
|
"Flame Comics",
|
||||||
"Flame Comics",
|
"https://flamecomics.com",
|
||||||
"https://flamecomics.com",
|
"en",
|
||||||
"en",
|
mangaUrlDirectory = "/series",
|
||||||
mangaUrlDirectory = "/series",
|
) {
|
||||||
),
|
|
||||||
ConfigurableSource {
|
|
||||||
|
|
||||||
// Flame Scans -> Flame Comics
|
// Flame Scans -> Flame Comics
|
||||||
override val id = 6350607071566689772
|
override val id = 6350607071566689772
|
||||||
|
|
||||||
private val preferences by lazy {
|
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val client = super.client.newBuilder()
|
override val client = super.client.newBuilder()
|
||||||
.rateLimit(2, 7)
|
.rateLimit(2, 7)
|
||||||
.addInterceptor(::composedImageIntercept)
|
.addInterceptor(::composedImageIntercept)
|
||||||
|
@ -130,114 +113,8 @@ class FlameComics :
|
||||||
}
|
}
|
||||||
// Split Image Fixer End
|
// 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 {
|
companion object {
|
||||||
private const val COMPOSED_SUFFIX = "?comp"
|
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()
|
private val MEDIA_TYPE = "image/png".toMediaType()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.KingofShojo'
|
extClass = '.KingofShojo'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://kingofshojo.com'
|
baseUrl = 'https://kingofshojo.com'
|
||||||
overrideVersionCode = 1
|
overrideVersionCode = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 5.4 KiB |