Compare commits

..

No commits in common. "555051eba4fea254f25177714eaac4f16a7b4e6f" and "f6cb65688da0559d695847f7d3c11debe7396343" have entirely different histories.

237 changed files with 1377 additions and 4395 deletions

24
.github/renovate.json vendored
View File

@ -1,30 +1,8 @@
{ {
"extends": [ "extends": [
"config:recommended" "config:base"
], ],
"schedule": ["on sunday"],
"includePaths": [ "includePaths": [
"buildSrc/gradle/**",
"gradle/**",
".github/**" ".github/**"
],
"ignoreDeps": ["keiyoushi/issue-moderator-action"],
"packageRules": [
{
"matchManagers": ["github-actions"],
"groupName": "{{manager}} dependencies"
},
{
"matchManagers": ["gradle"],
"enabled": false
},
{
"matchPackageNames": [
"com.android.tools.build:gradle",
"gradle"
],
"draftPR": true,
"enabled": true
}
] ]
} }

View File

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

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@ -18,6 +18,8 @@ android {
} }
} }
// TODO: use versionCatalogs.named("libs") in Gradle 8.5
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies { dependencies {
compileOnly(versionCatalogs.named("libs").findBundle("common").get()) compileOnly(libs.findBundle("common").get())
} }

View File

@ -36,8 +36,10 @@ kotlinter {
) )
} }
// TODO: use versionCatalogs.named("libs") in Gradle 8.5
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies { dependencies {
compileOnly(versionCatalogs.named("libs").findBundle("common").get()) compileOnly(libs.findBundle("common").get())
} }
tasks { tasks {

Binary file not shown.

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

14
gradlew generated vendored
View File

@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045 # shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045 # shellcheck disable=SC3045
ulimit -n "$MAX_FD" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac
@ -202,11 +202,11 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command: # Collect all arguments for the java command;
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# and any embedded shellness will be escaped. # shell script including quotes and variable substitutions, so put them in
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # double quotes to make sure that they get re-expanded; and
# treated as '${Hostname}' itself on the command line. # * put everything else in single quotes, so that it's not re-expanded.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \

20
gradlew.bat generated vendored
View File

@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2 echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo. 1>&2 echo.
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation. 1>&2 echo location of your Java installation.
goto fail goto fail
@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. 1>&2 echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo. 1>&2 echo.
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation. 1>&2 echo location of your Java installation.
goto fail goto fail

View File

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

View File

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

View File

@ -10,6 +10,9 @@ class BloggerDto(
@Serializable @Serializable
class BloggerFeedDto( class BloggerFeedDto(
@SerialName("openSearch\$totalResults") val totalResults: BloggerTextDto,
@SerialName("openSearch\$startIndex") val startIndex: BloggerTextDto,
@SerialName("openSearch\$itemsPerPage") val itemsPerPage: BloggerTextDto,
val category: List<BloggerCategoryDto> = emptyList(), val category: List<BloggerCategoryDto> = emptyList(),
val entry: List<BloggerFeedEntryDto> = emptyList(), val entry: List<BloggerFeedEntryDto> = emptyList(),
) )

View File

@ -65,7 +65,7 @@ abstract class GravureBlogger(
initialized = true initialized = true
} }
} }
val hasNextPage = data.feed.entry.size == MAX_RESULTS val hasNextPage = (data.feed.startIndex.t.toInt() + data.feed.itemsPerPage.t.toInt()) <= data.feed.totalResults.t.toInt()
return MangasPage(manga, hasNextPage) return MangasPage(manga, hasNextPage)
} }

View File

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

View File

@ -60,7 +60,7 @@ class KemonoPostDto(
if (file.path != null) add(KemonoAttachmentDto(file.name!!, file.path)) if (file.path != null) add(KemonoAttachmentDto(file.name!!, file.path))
addAll(attachments) addAll(attachments)
}.filter { }.filter {
when (it.path.substringAfterLast('.').lowercase()) { when (it.name.substringAfterLast('.').lowercase()) {
"png", "jpg", "gif", "jpeg", "webp" -> true "png", "jpg", "gif", "jpeg", "webp" -> true
else -> false else -> false
} }
@ -91,7 +91,6 @@ class KemonoPostDto(
@Serializable @Serializable
class KemonoFileDto(val name: String? = null, val path: String? = null) class KemonoFileDto(val name: String? = null, val path: String? = null)
// name might have ".jpe" extension for JPEG, path might have ".m4v" extension for MP4
@Serializable @Serializable
class KemonoAttachmentDto(val name: String, val path: String) { class KemonoAttachmentDto(val name: String, val path: String) {
override fun toString() = "$path?f=$name" override fun toString() = "$path?f=$name"

View File

@ -1,30 +0,0 @@
alt_names_heading=Alternative Names:
author_filter_title=Author
year_filter_title=Year
status_filter_title=Status
status_filter_option_all=All
status_filter_option_ongoing=Ongoing
status_filter_option_completed=Completed
status_filter_option_hiatus=Hiatus
status_filter_option_dropped=Dropped
type_filter_title=Type
type_filter_option_all=All
type_filter_option_manga=Manga
type_filter_option_manhwa=Manhwa
type_filter_option_manhua=Manhua
type_filter_option_comic=Comic
order_by_filter_title=Sort By
order_by_filter_default=Default
order_by_filter_az=A-Z
order_by_filter_za=Z-A
order_by_filter_latest_update=Latest Update
order_by_filter_latest_added=Latest Added
order_by_filter_popular=Popular
project_filter_title=Filter Project
project_filter_all_manga=Show all manga
project_filter_only_project=Show only project manga
genre_filter_title=Genre
genre_missing_warning=Press 'Reset' to attempt to show the genres
genre_exclusion_warning=Genre exclusion is not available for all sources
project_filter_warning=NOTE: Can't be used with other filter!
project_filter_name=%s Project List page

View File

@ -1,23 +0,0 @@
alt_names_heading=Nombres alternativos:
author_filter_title=Autor
year_filter_title=Año
status_filter_title=Estado
status_filter_option_all=Todos
status_filter_option_ongoing=En curso
status_filter_option_completed=Completado
status_filter_option_hiatus=En pausa
status_filter_option_dropped=Abandonado
type_filter_title=Tipo
type_filter_option_all=Todos
order_by_filter_title=Ordenar por
order_by_filter_default=Por defecto
order_by_filter_latest_update=Última actualización
order_by_filter_latest_added=Último añadido
project_filter_title=Filtrar proyectos
project_filter_all_manga=Mostrar todos los mangas
project_filter_only_project=Mostrar solo los proyectos
genre_filter_title=Género
genre_missing_warning=Presione 'Restablecer' para intentar cargar los géneros
genre_exclusion_warning=La exclusión de géneros puede no funcionar correctamente
project_filter_warning=NOTA: ¡No se puede usar con otros filtros!
project_filter_name=%s Página de proyectos

View File

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

View File

@ -1,8 +1,15 @@
package eu.kanade.tachiyomi.multisrc.mangathemesia package eu.kanade.tachiyomi.multisrc.mangathemesia
import eu.kanade.tachiyomi.lib.i18n.Intl import android.app.Application
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.lib.randomua.addRandomUAPreferenceToScreen
import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA
import eu.kanade.tachiyomi.lib.randomua.getPrefUAType
import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter 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
@ -14,56 +21,64 @@ import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Call
import okhttp3.Callback
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.jsoup.select.Elements import org.jsoup.select.Elements
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.lang.IllegalArgumentException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit
// Formerly WPMangaStream & WPMangaReader -> MangaThemesia // Formerly WPMangaStream & WPMangaReader -> MangaThemesia
abstract class MangaThemesia( abstract class MangaThemesia(
override val name: String, override val name: String,
override val baseUrl: String, override val baseUrl: String,
final override val lang: String, override val lang: String,
val mangaUrlDirectory: String = "/manga", val mangaUrlDirectory: String = "/manga",
val dateFormat: SimpleDateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale.US), val dateFormat: SimpleDateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale.US),
) : ParsedHttpSource() { ) : ParsedHttpSource(), ConfigurableSource {
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
protected open val json: Json by injectLazy() protected open val json: Json by injectLazy()
override val supportsLatest = true override val supportsLatest = true
override val client = network.cloudflareClient override val client: OkHttpClient by lazy {
network.cloudflareClient.newBuilder()
.setRandomUserAgent(
preferences.getPrefUAType(),
preferences.getPrefCustomUA(),
)
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
}
override fun headersBuilder() = super.headersBuilder() override fun headersBuilder() = super.headersBuilder()
.set("Referer", "$baseUrl/") .set("Referer", "$baseUrl/")
protected val intl = Intl(
language = lang,
baseLanguage = "en",
availableLanguages = setOf("en", "es"),
classLoader = javaClass.classLoader!!,
)
open val projectPageString = "/project" open val projectPageString = "/project"
// Popular (Search with popular order and nothing else) // Popular (Search with popular order and nothing else)
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", popularFilter) override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", FilterList(OrderByFilter("popular")))
override fun popularMangaParse(response: Response) = searchMangaParse(response) override fun popularMangaParse(response: Response) = searchMangaParse(response)
// Latest (Search with update order and nothing else) // Latest (Search with update order and nothing else)
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", latestFilter) override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", FilterList(OrderByFilter("update")))
override fun latestUpdatesParse(response: Response) = searchMangaParse(response) override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
// Search // Search
@ -151,96 +166,22 @@ abstract class MangaThemesia(
override fun searchMangaNextPageSelector() = "div.pagination .next, div.hpage .r" override fun searchMangaNextPageSelector() = "div.pagination .next, div.hpage .r"
// Manga details // Manga details
private fun selector(selector: String, contains: List<String>): String {
return contains.joinToString(", ") { selector.replace("%s", it) }
}
open val seriesDetailsSelector = "div.bigcontent, div.animefull, div.main-info, div.postbody" open val seriesDetailsSelector = "div.bigcontent, div.animefull, div.main-info, div.postbody"
open val seriesTitleSelector = "h1.entry-title"
open val seriesTitleSelector = "h1.entry-title, .ts-breadcrumb li:last-child span" open val seriesArtistSelector = ".infotable tr:contains(artist) td:last-child, .tsinfo .imptdt:contains(artist) i, .fmed b:contains(artist)+span, span:contains(artist)"
open val seriesAuthorSelector = ".infotable tr:contains(author) td:last-child, .tsinfo .imptdt:contains(author) i, .fmed b:contains(author)+span, span:contains(author)"
open val seriesArtistSelector = selector(
".infotable tr:contains(%s) td:last-child, .tsinfo .imptdt:contains(%s) i, .fmed b:contains(%s)+span, span:contains(%s)",
listOf(
"artist",
"Artiste",
"Artista",
"الرسام",
"الناشر",
"İllüstratör",
"Çizer",
),
)
open val seriesAuthorSelector = selector(
".infotable tr:contains(%s) td:last-child, .tsinfo .imptdt:contains(%s) i, .fmed b:contains(%s)+span, span:contains(%s)",
listOf(
"Author",
"Auteur",
"autor",
"المؤلف",
"Mangaka",
"seniman",
"Pengarang",
"Yazar",
),
)
open val seriesDescriptionSelector = ".desc, .entry-content[itemprop=description]" open val seriesDescriptionSelector = ".desc, .entry-content[itemprop=description]"
open val seriesAltNameSelector = ".alternative, .wd-full:contains(alt) span, .alter, .seriestualt"
open val seriesAltNameSelector = ".alternative, .wd-full:contains(alt) span, .alter, .seriestualt, " + open val seriesGenreSelector = "div.gnr a, .mgen a, .seriestugenre a, span:contains(genre)"
selector( open val seriesTypeSelector = ".infotable tr:contains(type) td:last-child, .tsinfo .imptdt:contains(type) i, .tsinfo .imptdt:contains(type) a, .fmed b:contains(type)+span, span:contains(type) a, a[href*=type\\=]"
".infotable tr:contains(%s) td:last-child", open val seriesStatusSelector = ".infotable tr:contains(status) td:last-child, .tsinfo .imptdt:contains(status) i, .fmed b:contains(status)+span span:contains(status)"
listOf(
"Alternative",
"Alternatif",
"الأسماء الثانوية",
),
)
open val seriesGenreSelector = "div.gnr a, .mgen a, .seriestugenre a, " +
selector(
"span:contains(%s)",
listOf(
"genre",
"التصنيف",
),
)
open val seriesTypeSelector = selector(
".infotable tr:contains(%s) td:last-child, .tsinfo .imptdt:contains(%s) i, .tsinfo .imptdt:contains(%s) a, .fmed b:contains(%s)+span, span:contains(%s) a",
listOf(
"type",
"ประเภท",
"النوع",
"tipe",
"Türü",
),
) + ", a[href*=type\\=]"
open val seriesStatusSelector = selector(
".infotable tr:contains(%s) td:last-child, .tsinfo .imptdt:contains(%s) i, .fmed b:contains(%s)+span span:contains(%s)",
listOf(
"status",
"Statut",
"Durum",
"連載状況",
"Estado",
"الحالة",
"حالة العمل",
"สถานะ",
"stato",
"Statüsü",
),
)
open val seriesThumbnailSelector = ".infomanga > div[itemprop=image] img, .thumb img" open val seriesThumbnailSelector = ".infomanga > div[itemprop=image] img, .thumb img"
open val altNamePrefix = "${intl["alt_names_heading"]} " open val altNamePrefix = "Alternative Name: "
override fun mangaDetailsParse(document: Document) = SManga.create().apply { override fun mangaDetailsParse(document: Document) = SManga.create().apply {
document.selectFirst(seriesDetailsSelector)?.let { seriesDetails -> document.selectFirst(seriesDetailsSelector)?.let { seriesDetails ->
title = seriesDetails.selectFirst(seriesTitleSelector)!!.text() title = seriesDetails.selectFirst(seriesTitleSelector)?.text().orEmpty()
artist = seriesDetails.selectFirst(seriesArtistSelector)?.ownText().removeEmptyPlaceholder() artist = seriesDetails.selectFirst(seriesArtistSelector)?.ownText().removeEmptyPlaceholder()
author = seriesDetails.selectFirst(seriesAuthorSelector)?.ownText().removeEmptyPlaceholder() author = seriesDetails.selectFirst(seriesAuthorSelector)?.ownText().removeEmptyPlaceholder()
description = seriesDetails.select(seriesDescriptionSelector).joinToString("\n") { it.text() }.trim() description = seriesDetails.select(seriesDescriptionSelector).joinToString("\n") { it.text() }.trim()
@ -269,32 +210,16 @@ abstract class MangaThemesia(
} }
protected fun String?.removeEmptyPlaceholder(): String? { protected fun String?.removeEmptyPlaceholder(): String? {
return if (this.isNullOrBlank() || this == "-" || this == "N/A" || this == "n/a") null else this return if (this.isNullOrBlank() || this == "-" || this == "N/A") null else this
} }
open fun String?.parseStatus(): Int { open fun String?.parseStatus(): Int = when {
if (this == null) return SManga.UNKNOWN this == null -> SManga.UNKNOWN
listOf("ongoing", "publishing").any { this.contains(it, ignoreCase = true) } -> SManga.ONGOING
return when (this.lowercase().trim()) { this.contains("hiatus", ignoreCase = true) -> SManga.ON_HIATUS
"مستمرة", "en curso", "ongoing", "on going", "ativo", "en cours", this.contains("completed", ignoreCase = true) -> SManga.COMPLETED
"en cours de publication", "đang tiến hành", "em lançamento", "онгоінг", "publishing", listOf("dropped", "cancelled").any { this.contains(it, ignoreCase = true) } -> SManga.CANCELLED
"devam ediyor", "em andamento", "in corso", "güncel", "berjalan", "продолжается", "updating", "lançando", "in arrivo", "emision", else -> SManga.UNKNOWN
"en emision", "مستمر", "curso", "en marcha", "publicandose", "publicando", "连载中", "devam etmekte", "連載中",
-> SManga.ONGOING
"completed", "completo", "complété", "fini", "achevé", "terminé", "tamamlandı", "đã hoàn thành", "hoàn thành",
"مكتملة", "завершено", "finished", "finalizado", "completata", "one-shot", "bitti", "tamat", "completado", "concluído", "完結",
"concluido", "已完结", "bitmiş",
-> SManga.COMPLETED
"canceled", "cancelled", "cancelado", "cancellato", "cancelados", "dropped", "discontinued", "abandonné",
-> SManga.CANCELLED
"hiatus", "on hold", "pausado", "en espera", "en pause", "en attente",
-> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
} }
// Chapter list // Chapter list
@ -302,9 +227,6 @@ abstract class MangaThemesia(
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup() val document = response.asJsoup()
countViews(document)
val chapters = document.select(chapterListSelector()).map { chapterFromElement(it) } val chapters = document.select(chapterListSelector()).map { chapterFromElement(it) }
// Add timestamp to latest chapter, taken from "Updated On". // Add timestamp to latest chapter, taken from "Updated On".
@ -316,6 +238,8 @@ abstract class MangaThemesia(
if (date.isNotEmpty()) chapters.first().date_upload = parseUpdatedOnDate(date) if (date.isNotEmpty()) chapters.first().date_upload = parseUpdatedOnDate(date)
} }
countViews(document)
return chapters return chapters
} }
@ -343,13 +267,13 @@ abstract class MangaThemesia(
open val pageSelector = "div#readerarea img" open val pageSelector = "div#readerarea img"
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
countViews(document)
val chapterUrl = document.location() val chapterUrl = document.location()
val htmlPages = document.select(pageSelector) val htmlPages = document.select(pageSelector)
.filterNot { it.imgAttr().isEmpty() } .filterNot { it.imgAttr().isEmpty() }
.mapIndexed { i, img -> Page(i, chapterUrl, img.imgAttr()) } .mapIndexed { i, img -> Page(i, chapterUrl, img.imgAttr()) }
countViews(document)
// Some sites also loads pages via javascript // Some sites also loads pages via javascript
if (htmlPages.isNotEmpty()) { return htmlPages } if (htmlPages.isNotEmpty()) { return htmlPages }
@ -396,6 +320,8 @@ abstract class MangaThemesia(
.build() .build()
val newHeaders = headersBuilder() val newHeaders = headersBuilder()
.set("Content-Length", formBody.contentLength().toString())
.set("Content-Type", formBody.contentType().toString())
.set("Referer", document.location()) .set("Referer", document.location())
.build() .build()
@ -413,22 +339,17 @@ abstract class MangaThemesia(
} }
val request = countViewsRequest(document) ?: return val request = countViewsRequest(document) ?: return
val callback = object : Callback { runCatching { client.newCall(request).execute().close() }
override fun onResponse(call: Call, response: Response) = response.close()
override fun onFailure(call: Call, e: IOException) = Unit
}
client.newCall(request).enqueue(callback)
} }
// Filters // Filters
protected class AuthorFilter(name: String) : Filter.Text(name) protected class AuthorFilter : Filter.Text("Author")
protected class YearFilter(name: String) : Filter.Text(name) protected class YearFilter : Filter.Text("Year")
open class SelectFilter( open class SelectFilter(
displayName: String, displayName: String,
private val vals: Array<Pair<String, String>>, val vals: Array<Pair<String, String>>,
defaultValue: String? = null, defaultValue: String? = null,
) : Filter.Select<String>( ) : Filter.Select<String>(
displayName, displayName,
@ -438,91 +359,63 @@ abstract class MangaThemesia(
fun selectedValue() = vals[state].second fun selectedValue() = vals[state].second
} }
protected class StatusFilter( protected class StatusFilter : SelectFilter(
name: String, "Status",
options: Array<Pair<String, String>>, arrayOf(
) : SelectFilter( Pair("All", ""),
name, Pair("Ongoing", "ongoing"),
options, Pair("Completed", "completed"),
Pair("Hiatus", "hiatus"),
Pair("Dropped", "dropped"),
),
) )
protected open val statusOptions = arrayOf( protected class TypeFilter : SelectFilter(
Pair(intl["status_filter_option_all"], ""), "Type",
Pair(intl["status_filter_option_ongoing"], "ongoing"), arrayOf(
Pair(intl["status_filter_option_completed"], "completed"), Pair("All", ""),
Pair(intl["status_filter_option_hiatus"], "hiatus"), Pair("Manga", "Manga"),
Pair(intl["status_filter_option_dropped"], "dropped"), Pair("Manhwa", "Manhwa"),
Pair("Manhua", "Manhua"),
Pair("Comic", "Comic"),
),
) )
protected class TypeFilter( protected class OrderByFilter(defaultOrder: String? = null) : SelectFilter(
name: String, "Sort By",
options: Array<Pair<String, String>>, arrayOf(
) : SelectFilter( Pair("Default", ""),
name, Pair("A-Z", "title"),
options, Pair("Z-A", "titlereverse"),
) Pair("Latest Update", "update"),
Pair("Latest Added", "latest"),
protected open val typeFilterOptions = arrayOf( Pair("Popular", "popular"),
Pair(intl["type_filter_option_all"], ""), ),
Pair(intl["type_filter_option_manga"], "Manga"),
Pair(intl["type_filter_option_manhwa"], "Manhwa"),
Pair(intl["type_filter_option_manhua"], "Manhua"),
Pair(intl["type_filter_option_comic"], "Comic"),
)
protected class OrderByFilter(
name: String,
options: Array<Pair<String, String>>,
defaultOrder: String? = null,
) : SelectFilter(
name,
options,
defaultOrder, defaultOrder,
) )
protected open val orderByFilterOptions = arrayOf( protected class ProjectFilter : SelectFilter(
Pair(intl["order_by_filter_default"], ""), "Filter Project",
Pair(intl["order_by_filter_az"], "title"), arrayOf(
Pair(intl["order_by_filter_za"], "titlereverse"), Pair("Show all manga", ""),
Pair(intl["order_by_filter_latest_update"], "update"), Pair("Show only project manga", "project-filter-on"),
Pair(intl["order_by_filter_latest_added"], "latest"), ),
Pair(intl["order_by_filter_popular"], "popular"),
)
protected val popularFilter by lazy { FilterList(OrderByFilter("", orderByFilterOptions, "popular")) }
protected val latestFilter by lazy { FilterList(OrderByFilter("", orderByFilterOptions, "update")) }
protected class ProjectFilter(
name: String,
options: Array<Pair<String, String>>,
) : SelectFilter(
name,
options,
)
protected open val projectFilterOptions = arrayOf(
Pair(intl["project_filter_all_manga"], ""),
Pair(intl["project_filter_only_project"], "project-filter-on"),
)
protected class GenreData(
val name: String,
val value: String,
val state: Int = Filter.TriState.STATE_IGNORE,
) )
protected class Genre( protected class Genre(
name: String, name: String,
val value: String, val value: String,
state: Int, state: Int = STATE_IGNORE,
) : Filter.TriState(name, state) ) : Filter.TriState(name, state)
protected class GenreListFilter(name: String, genres: List<Genre>) : Filter.Group<Genre>(name, genres) protected class GenreListFilter(genres: List<Genre>) : Filter.Group<Genre>("Genre", genres)
protected var genrelist: List<GenreData>? = null
private var genrelist: List<Genre>? = null
protected open fun getGenreList(): List<Genre> { protected open fun getGenreList(): List<Genre> {
return genrelist?.map { Genre(it.name, it.value, it.state) }.orEmpty() // Filters are fetched immediately once an extension loads
// We're only able to get filters after a loading the manga directory,
// and resetting the filters is the only thing that seems to reinflate the view
return genrelist ?: listOf(Genre("Press reset to attempt to fetch genres", ""))
} }
open val hasProjectPage = false open val hasProjectPage = false
@ -530,31 +423,21 @@ abstract class MangaThemesia(
override fun getFilterList(): FilterList { override fun getFilterList(): FilterList {
val filters = mutableListOf<Filter<*>>( val filters = mutableListOf<Filter<*>>(
Filter.Separator(), Filter.Separator(),
AuthorFilter(intl["author_filter_title"]), AuthorFilter(),
YearFilter(intl["year_filter_title"]), YearFilter(),
StatusFilter(intl["status_filter_title"], statusOptions), StatusFilter(),
TypeFilter(intl["type_filter_title"], typeFilterOptions), TypeFilter(),
OrderByFilter(intl["order_by_filter_title"], orderByFilterOptions), OrderByFilter(),
Filter.Header("Genre exclusion is not available for all sources"),
GenreListFilter(getGenreList()),
) )
if (!genrelist.isNullOrEmpty()) {
filters.addAll(
listOf(
Filter.Header(intl["genre_exclusion_warning"]),
GenreListFilter(intl["genre_filter_title"], getGenreList()),
),
)
} else {
filters.add(
Filter.Header(intl["genre_missing_warning"]),
)
}
if (hasProjectPage) { if (hasProjectPage) {
filters.addAll( filters.addAll(
mutableListOf<Filter<*>>( mutableListOf<Filter<*>>(
Filter.Separator(), Filter.Separator(),
Filter.Header(intl["project_filter_warning"]), Filter.Header("NOTE: Can't be used with other filter!"),
Filter.Header(intl.format("project_filter_name", name)), Filter.Header("$name Project List page"),
ProjectFilter(intl["project_filter_title"], projectFilterOptions), ProjectFilter(),
), ),
) )
} }
@ -602,9 +485,9 @@ abstract class MangaThemesia(
(!strict && url.pathSegments.size == n + 1 && url.pathSegments[n].isEmpty()) (!strict && url.pathSegments.size == n + 1 && url.pathSegments[n].isEmpty())
} }
private fun parseGenres(document: Document): List<GenreData>? { private fun parseGenres(document: Document): List<Genre>? {
return document.selectFirst("ul.genrez")?.select("li")?.map { li -> return document.selectFirst("ul.genrez")?.select("li")?.map { li ->
GenreData( Genre(
li.selectFirst("label")!!.text(), li.selectFirst("label")!!.text(),
li.selectFirst("input[type=checkbox]")!!.attr("value"), li.selectFirst("input[type=checkbox]")!!.attr("value"),
) )
@ -631,10 +514,15 @@ abstract class MangaThemesia(
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException() override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
override fun setupPreferenceScreen(screen: PreferenceScreen) {
addRandomUAPreferenceToScreen(screen)
}
companion object { companion object {
const val URL_SEARCH_PREFIX = "url:" const val URL_SEARCH_PREFIX = "url:"
// More info: https://issuetracker.google.com/issues/36970498 // More info: https://issuetracker.google.com/issues/36970498
@Suppress("RegExpRedundantEscape")
private val MANGA_PAGE_ID_REGEX = "post_id\\s*:\\s*(\\d+)\\}".toRegex() private val MANGA_PAGE_ID_REGEX = "post_id\\s*:\\s*(\\d+)\\}".toRegex()
private val CHAPTER_PAGE_ID_REGEX = "chapter_id\\s*=\\s*(\\d+);".toRegex() private val CHAPTER_PAGE_ID_REGEX = "chapter_id\\s*=\\s*(\\d+);".toRegex()

View File

@ -21,7 +21,7 @@ data class PizzaReaderDto(
@Serializable @Serializable
data class PizzaComicDto( data class PizzaComicDto(
val artist: String? = null, val artist: String = "",
val author: String = "", val author: String = "",
val chapters: List<PizzaChapterDto> = emptyList(), val chapters: List<PizzaChapterDto> = emptyList(),
val description: String = "", val description: String = "",

View File

@ -3,7 +3,7 @@
<application> <application>
<activity <activity
android:name=".pt.huntersscans.HuntersScansUrlActivity" android:name="eu.kanade.tachiyomi.multisrc.bilibili.BilibiliUrlActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
android:theme="@android:style/Theme.NoDisplay"> android:theme="@android:style/Theme.NoDisplay">
@ -13,9 +13,13 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:host="huntersscan.xyz" />
<data android:scheme="https" /> <data android:scheme="https" />
<data android:pathPattern="/manga/..*" />
<data android:host="bilibilicomics.com" />
<data android:host="m.bilibilicomics.com" />
<data android:host="www.bilibilicomics.com" />
<data android:pathPattern="/detail/mc..*" />
</intent-filter> </intent-filter>
</activity> </activity>
</application> </application>

View File

@ -0,0 +1,9 @@
ext {
extName = 'BILIBILI COMICS'
extClass = '.BilibiliComicsFactory'
themePkg = 'bilibili'
baseUrl = 'https://www.bilibilicomics.com'
overrideVersionCode = 3
}
apply from: "$rootDir/common.gradle"

View File

@ -0,0 +1,411 @@
package eu.kanade.tachiyomi.extension.all.bilibilicomics
import eu.kanade.tachiyomi.multisrc.bilibili.Bilibili
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliAccessToken
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliAccessTokenCookie
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliComicDto
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliCredential
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliGetCredential
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliIntl
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliSearchDto
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliTag
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliUnlockedEpisode
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliUserEpisodes
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okio.Buffer
import java.io.IOException
import java.net.URLDecoder
import java.util.Calendar
class BilibiliComicsFactory : SourceFactory {
override fun createSources() = listOf(
BilibiliComicsEn(),
BilibiliComicsCn(),
BilibiliComicsId(),
BilibiliComicsEs(),
BilibiliComicsFr(),
)
}
abstract class BilibiliComics(lang: String) : Bilibili(
"BILIBILI COMICS",
"https://www.bilibilicomics.com",
lang,
) {
override val client: OkHttpClient = super.client.newBuilder()
.apply { interceptors().add(0, Interceptor { chain -> signedInIntercept(chain) }) }
.build()
init {
setAccessTokenCookie(baseUrl.toHttpUrl())
}
override val signedIn: Boolean
get() = accessTokenCookie != null
private val globalApiSubDomain: String
get() = GLOBAL_API_SUBDOMAINS[(accessTokenCookie?.area?.toIntOrNull() ?: 1) - 1]
private val globalApiBaseUrl: String
get() = "https://$globalApiSubDomain.bilibilicomics.com"
private var accessTokenCookie: BilibiliAccessTokenCookie? = null
private val dayOfWeek: Int
get() = Calendar.getInstance().get(Calendar.DAY_OF_WEEK) - 1
override fun latestUpdatesRequest(page: Int): Request {
val jsonPayload = buildJsonObject { put("day", dayOfWeek) }
val requestBody = jsonPayload.toString().toRequestBody(JSON_MEDIA_TYPE)
val newHeaders = headersBuilder()
.add("Content-Length", requestBody.contentLength().toString())
.add("Content-Type", requestBody.contentType().toString())
.set("Referer", "$baseUrl/schedule")
.build()
val apiUrl = "$baseUrl/$API_COMIC_V1_COMIC_ENDPOINT/GetSchedule".toHttpUrl().newBuilder()
.addCommonParameters()
.toString()
return POST(apiUrl, newHeaders, requestBody)
}
override fun latestUpdatesParse(response: Response): MangasPage {
val result = response.parseAs<BilibiliSearchDto>()
if (result.code != 0) {
return MangasPage(emptyList(), hasNextPage = false)
}
val comicList = result.data!!.list.map(::latestMangaFromObject)
return MangasPage(comicList, hasNextPage = false)
}
protected open fun latestMangaFromObject(comic: BilibiliComicDto): SManga = SManga.create().apply {
title = comic.title
thumbnail_url = comic.verticalCover + THUMBNAIL_RESOLUTION
url = "/detail/mc${comic.comicId}"
}
override fun chapterListParse(response: Response): List<SChapter> {
if (!signedIn) {
return super.chapterListParse(response)
}
val result = response.parseAs<BilibiliComicDto>()
if (result.code != 0) {
return emptyList()
}
val comic = result.data!!
val userEpisodesRequest = userEpisodesRequest(comic.id)
val userEpisodesResponse = client.newCall(userEpisodesRequest).execute()
val unlockedEpisodes = userEpisodesParse(userEpisodesResponse)
return comic.episodeList.map { ep -> chapterFromObject(ep, comic.id, isUnlocked = ep.id in unlockedEpisodes) }
}
private fun userEpisodesRequest(comicId: Int): Request {
val jsonPayload = buildJsonObject { put("comic_id", comicId) }
val requestBody = jsonPayload.toString().toRequestBody(JSON_MEDIA_TYPE)
val newHeaders = headersBuilder()
.set("Referer", baseUrl)
.build()
val apiUrl = "$globalApiBaseUrl/$API_COMIC_V1_USER_ENDPOINT/GetUserEpisodes".toHttpUrl()
.newBuilder()
.addCommonParameters()
.toString()
return POST(apiUrl, newHeaders, requestBody)
}
private fun userEpisodesParse(response: Response): List<Int> {
if (!response.isSuccessful) {
return emptyList()
}
val result = response.parseAs<BilibiliUserEpisodes>()
if (result.code != 0) {
return emptyList()
}
return result.data!!.unlockedEpisodes.orEmpty()
.map(BilibiliUnlockedEpisode::id)
}
override fun pageListRequest(chapter: SChapter): Request {
if (!signedIn) {
return super.pageListRequest(chapter)
}
val chapterPaths = (baseUrl + chapter.url).toHttpUrl().pathSegments
val comicId = chapterPaths[0].removePrefix("mc").toInt()
val episodeId = chapterPaths[1].toInt()
val jsonPayload = BilibiliGetCredential(comicId, episodeId, 1)
val requestBody = json.encodeToString(jsonPayload).toRequestBody(JSON_MEDIA_TYPE)
val newHeaders = headersBuilder()
.set("Referer", baseUrl + chapter.url)
.build()
val apiUrl = "$globalApiBaseUrl/$API_GLOBAL_V1_USER_ENDPOINT/GetCredential".toHttpUrl()
.newBuilder()
.addCommonParameters()
.toString()
return POST(apiUrl, newHeaders, requestBody)
}
override fun pageListParse(response: Response): List<Page> {
if (!signedIn) {
return super.pageListParse(response)
}
if (!response.isSuccessful) {
throw Exception(intl.failedToGetCredential)
}
val result = response.parseAs<BilibiliCredential>()
val credential = result.data?.credential ?: ""
val requestPayload = response.request.bodyString
val credentialInfo = json.decodeFromString<BilibiliGetCredential>(requestPayload)
val chapterUrl = "/mc${credentialInfo.comicId}/${credentialInfo.episodeId}"
val imageIndexRequest = imageIndexRequest(chapterUrl, credential)
val imageIndexResponse = client.newCall(imageIndexRequest).execute()
return super.pageListParse(imageIndexResponse)
}
private fun setAccessTokenCookie(url: HttpUrl) {
val authCookie = client.cookieJar.loadForRequest(url)
.firstOrNull { cookie -> cookie.name == ACCESS_TOKEN_COOKIE_NAME }
?.let { cookie -> URLDecoder.decode(cookie.value, "UTF-8") }
?.let { jsonString -> json.decodeFromString<BilibiliAccessTokenCookie>(jsonString) }
if (accessTokenCookie == null) {
accessTokenCookie = authCookie
} else if (authCookie == null) {
accessTokenCookie = null
}
}
private fun signedInIntercept(chain: Interceptor.Chain): Response {
var request = chain.request()
val requestUrl = request.url.toString()
if (!requestUrl.contains("bilibilicomics.com")) {
return chain.proceed(request)
}
setAccessTokenCookie(request.url)
if (!accessTokenCookie?.accessToken.isNullOrEmpty()) {
request = request.newBuilder()
.addHeader("Authorization", "Bearer ${accessTokenCookie!!.accessToken}")
.build()
}
val response = chain.proceed(request)
// Try to refresh the token if it expired.
if (response.code == 401 && !accessTokenCookie?.refreshToken.isNullOrEmpty()) {
response.close()
val refreshTokenRequest = refreshTokenRequest(
accessTokenCookie!!.accessToken,
accessTokenCookie!!.refreshToken,
)
val refreshTokenResponse = chain.proceed(refreshTokenRequest)
accessTokenCookie = refreshTokenParse(refreshTokenResponse)
refreshTokenResponse.close()
request = request.newBuilder()
.header("Authorization", "Bearer ${accessTokenCookie!!.accessToken}")
.build()
return chain.proceed(request)
}
return response
}
private fun refreshTokenRequest(accessToken: String, refreshToken: String): Request {
val jsonPayload = buildJsonObject { put("refresh_token", refreshToken) }
val requestBody = jsonPayload.toString().toRequestBody(JSON_MEDIA_TYPE)
val newHeaders = headersBuilder()
.add("Authorization", "Bearer $accessToken")
.set("Referer", baseUrl)
.build()
val apiUrl = "$globalApiBaseUrl/$API_GLOBAL_V1_USER_ENDPOINT/RefreshToken".toHttpUrl()
.newBuilder()
.addCommonParameters()
.toString()
return POST(apiUrl, newHeaders, requestBody)
}
private fun refreshTokenParse(response: Response): BilibiliAccessTokenCookie {
if (!response.isSuccessful) {
throw IOException(intl.failedToRefreshToken)
}
val result = response.parseAs<BilibiliAccessToken>()
if (result.code != 0) {
throw IOException(intl.failedToRefreshToken)
}
val accessToken = result.data!!
return BilibiliAccessTokenCookie(
accessToken.accessToken,
accessToken.refreshToken,
accessTokenCookie!!.area,
)
}
private val Request.bodyString: String
get() {
val requestCopy = newBuilder().build()
val buffer = Buffer()
return runCatching { buffer.apply { requestCopy.body!!.writeTo(this) }.readUtf8() }
.getOrNull() ?: ""
}
companion object {
private const val ACCESS_TOKEN_COOKIE_NAME = "access_token"
private val GLOBAL_API_SUBDOMAINS = arrayOf("us-user", "sg-user")
private const val API_GLOBAL_V1_USER_ENDPOINT = "twirp/global.v1.User"
private const val API_COMIC_V1_USER_ENDPOINT = "twirp/comic.v1.User"
}
}
class BilibiliComicsEn : BilibiliComics(BilibiliIntl.ENGLISH) {
override fun getAllGenres(): Array<BilibiliTag> = arrayOf(
BilibiliTag("All", -1),
BilibiliTag("Action", 19),
BilibiliTag("Adventure", 22),
BilibiliTag("BL", 3),
BilibiliTag("Comedy", 14),
BilibiliTag("Eastern", 30),
BilibiliTag("Fantasy", 11),
BilibiliTag("GL", 16),
BilibiliTag("Harem", 15),
BilibiliTag("Historical", 12),
BilibiliTag("Horror", 23),
BilibiliTag("Mistery", 17),
BilibiliTag("Romance", 13),
BilibiliTag("Slice of Life", 21),
BilibiliTag("Suspense", 41),
BilibiliTag("Teen", 20),
)
}
class BilibiliComicsCn : BilibiliComics(BilibiliIntl.SIMPLIFIED_CHINESE) {
override fun getAllGenres(): Array<BilibiliTag> = arrayOf(
BilibiliTag("全部", -1),
BilibiliTag("校园", 18),
BilibiliTag("都市", 9),
BilibiliTag("耽美", 3),
BilibiliTag("少女", 20),
BilibiliTag("恋爱", 13),
BilibiliTag("奇幻", 11),
BilibiliTag("热血", 19),
BilibiliTag("冒险", 22),
BilibiliTag("古风", 12),
BilibiliTag("百合", 16),
BilibiliTag("玄幻", 30),
BilibiliTag("悬疑", 41),
BilibiliTag("科幻", 8),
)
}
class BilibiliComicsId : BilibiliComics(BilibiliIntl.INDONESIAN) {
override fun getAllGenres(): Array<BilibiliTag> = arrayOf(
BilibiliTag("Semua", -1),
BilibiliTag("Aksi", 19),
BilibiliTag("Fantasi Timur", 30),
BilibiliTag("Fantasi", 11),
BilibiliTag("Historis", 12),
BilibiliTag("Horror", 23),
BilibiliTag("Kampus", 18),
BilibiliTag("Komedi", 14),
BilibiliTag("Menegangkan", 41),
BilibiliTag("Remaja", 20),
BilibiliTag("Romantis", 13),
)
}
class BilibiliComicsEs : BilibiliComics(BilibiliIntl.SPANISH) {
override fun getAllGenres(): Array<BilibiliTag> = arrayOf(
BilibiliTag("Todos", -1),
BilibiliTag("Adolescencia", 105),
BilibiliTag("BL", 3),
BilibiliTag("Ciberdeportes", 104),
BilibiliTag("Ciencia ficción", 8),
BilibiliTag("Comedia", 14),
BilibiliTag("Fantasía occidental", 106),
BilibiliTag("Fantasía", 11),
BilibiliTag("Ficción Realista", 116),
BilibiliTag("GL", 16),
BilibiliTag("Histórico", 12),
BilibiliTag("Horror", 23),
BilibiliTag("Juvenil", 20),
BilibiliTag("Moderno", 111),
BilibiliTag("Oriental", 30),
BilibiliTag("Romance", 13),
BilibiliTag("Suspenso", 41),
BilibiliTag("Urbano", 9),
BilibiliTag("Wuxia", 103),
)
}
class BilibiliComicsFr : BilibiliComics(BilibiliIntl.FRENCH) {
override fun getAllGenres(): Array<BilibiliTag> = arrayOf(
BilibiliTag("Tout", -1),
BilibiliTag("BL", 3),
BilibiliTag("Science Fiction", 8),
BilibiliTag("Historique", 12),
BilibiliTag("Romance", 13),
BilibiliTag("GL", 16),
BilibiliTag("Fantasy Orientale", 30),
BilibiliTag("Suspense", 41),
BilibiliTag("Moderne", 111),
)
}

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'LANraragi' extName = 'LANraragi'
extClass = '.LANraragiFactory' extClass = '.LANraragiFactory'
extVersionCode = 17 extVersionCode = 16
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -82,17 +82,6 @@ open class LANraragi(private val suffix: String = "") : ConfigurableSource, Unme
return archiveToSManga(archive) return archiveToSManga(archive)
} }
override fun getMangaUrl(manga: SManga): String {
val namespace = preferences.getString(URL_TAG_PREFIX_KEY, URL_TAG_PREFIX_DEFAULT)
if (namespace.isNullOrEmpty()) {
return super.getMangaUrl(manga)
}
val tag = manga.genre?.split(", ")?.find { it.startsWith("$namespace") }
return tag?.substringAfter("$namespace") ?: super.getMangaUrl(manga)
}
override fun chapterListRequest(manga: SManga): Request { override fun chapterListRequest(manga: SManga): Request {
val id = if (manga.url.startsWith("/api/search/random")) randomArchiveID else getReaderId(manga.url) val id = if (manga.url.startsWith("/api/search/random")) randomArchiveID else getReaderId(manga.url)
val uri = getApiUriBuilder("/api/archives/$id/metadata").build() val uri = getApiUriBuilder("/api/archives/$id/metadata").build()
@ -321,7 +310,6 @@ open class LANraragi(private val suffix: String = "") : ConfigurableSource, Unme
screen.addPreference(screen.checkBoxPreference(CLEAR_NEW_KEY, "Clear New status", CLEAR_NEW_DEFAULT, "Clear an entry's New status when its details are viewed.")) screen.addPreference(screen.checkBoxPreference(CLEAR_NEW_KEY, "Clear New status", CLEAR_NEW_DEFAULT, "Clear an entry's New status when its details are viewed."))
screen.addPreference(screen.checkBoxPreference(NEW_ONLY_KEY, "Latest - New Only", NEW_ONLY_DEFAULT)) screen.addPreference(screen.checkBoxPreference(NEW_ONLY_KEY, "Latest - New Only", NEW_ONLY_DEFAULT))
screen.addPreference(screen.editTextPreference(SORT_BY_NS_KEY, "Latest - Sort by Namespace", SORT_BY_NS_DEFAULT, "Sort by the given namespace for Latest, such as date_added.")) screen.addPreference(screen.editTextPreference(SORT_BY_NS_KEY, "Latest - Sort by Namespace", SORT_BY_NS_DEFAULT, "Sort by the given namespace for Latest, such as date_added."))
screen.addPreference(screen.editTextPreference(URL_TAG_PREFIX_KEY, "Set tag prefix to get WebView URL", URL_TAG_PREFIX_DEFAULT, "Example: 'source:' will try to get the URL from the first tag starting with 'source:' and it will open it in the WebView. Leave empty for the default behavior."))
} }
private fun androidx.preference.PreferenceScreen.checkBoxPreference(key: String, title: String, default: Boolean, summary: String = ""): androidx.preference.CheckBoxPreference { private fun androidx.preference.PreferenceScreen.checkBoxPreference(key: String, title: String, default: Boolean, summary: String = ""): androidx.preference.CheckBoxPreference {
@ -506,7 +494,5 @@ open class LANraragi(private val suffix: String = "") : ConfigurableSource, Unme
private const val SORT_BY_NS_KEY = "latestNamespacePref" private const val SORT_BY_NS_KEY = "latestNamespacePref"
private const val CLEAR_NEW_KEY = "clearNew" private const val CLEAR_NEW_KEY = "clearNew"
private const val CLEAR_NEW_DEFAULT = true private const val CLEAR_NEW_DEFAULT = true
private const val URL_TAG_PREFIX_KEY = "urlTagPrefix"
private const val URL_TAG_PREFIX_DEFAULT = ""
} }
} }

View File

@ -290,7 +290,6 @@ enum class Language {
RUSSIAN, RUSSIAN,
THAI, THAI,
VIETNAMESE, VIETNAMESE,
GERMAN,
} }
@Serializable @Serializable

View File

@ -13,6 +13,5 @@ class MangaPlusFactory : SourceFactory {
MangaPlus("ru", "rus", Language.RUSSIAN), MangaPlus("ru", "rus", Language.RUSSIAN),
MangaPlus("th", "tha", Language.THAI), MangaPlus("th", "tha", Language.THAI),
MangaPlus("vi", "vie", Language.VIETNAMESE), MangaPlus("vi", "vie", Language.VIETNAMESE),
MangaPlus("de", "deu", Language.GERMAN),
) )
} }

View File

@ -33,11 +33,10 @@ open class MiauScan(lang: String) : MangaThemesia(
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val genreFilterIndex = filters.indexOfFirst { it is GenreListFilter } val genreFilterIndex = filters.indexOfFirst { it is GenreListFilter }
val genreFilter = filters.getOrNull(genreFilterIndex) as? GenreListFilter val genreFilter = filters.getOrNull(genreFilterIndex) as? GenreListFilter
?: GenreListFilter("", emptyList()) ?: GenreListFilter(emptyList())
val overloadedGenreFilter = GenreListFilter( val overloadedGenreFilter = GenreListFilter(
genreFilter.name, genres = genreFilter.state + listOf(
genreFilter.state + listOf(
Genre("", PORTUGUESE_GENRE_ID, portugueseMode), Genre("", PORTUGUESE_GENRE_ID, portugueseMode),
), ),
) )

View File

@ -31,8 +31,8 @@ class Mihentai : MangaThemesia("Mihentai", "https://mihentai.com", "all") {
listOf( listOf(
StatusFilter(), StatusFilter(),
TypeFilter(), TypeFilter(),
OrderByFilter(intl["order_by_filter_title"], orderByFilterOptions), OrderByFilter(),
GenreListFilter(intl["genre_filter_title"], getGenreList()), GenreListFilter(getGenreList()),
), ),
) )
} }

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.extension.ar.areamanga package eu.kanade.tachiyomi.extension.ar.areamanga
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import eu.kanade.tachiyomi.source.model.SManga
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@ -9,4 +10,21 @@ class AreaManga : MangaThemesia(
"https://www.areascans.net", "https://www.areascans.net",
"ar", "ar",
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")), dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
) ) {
override val seriesArtistSelector =
".tsinfo .imptdt:contains(الرسام) i, ${super.seriesArtistSelector}"
override val seriesAuthorSelector =
".tsinfo .imptdt:contains(المؤلف) i, ${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
this.contains("مستمر", ignoreCase = true) -> SManga.ONGOING
this.contains("مكتمل", ignoreCase = true) -> SManga.COMPLETED
this.contains("متوقف", ignoreCase = true) -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
}

View File

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

View File

@ -7,7 +7,6 @@ import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.extension.BuildConfig import eu.kanade.tachiyomi.extension.BuildConfig
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.FilterList
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
@ -24,14 +23,12 @@ import java.util.Locale
private const val swatUrl = "https://swatmanhua.com" private const val swatUrl = "https://swatmanhua.com"
class MangaSwat : class MangaSwat : MangaThemesia(
MangaThemesia( "MangaSwat",
"MangaSwat", swatUrl,
swatUrl, "ar",
"ar", dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")), ) {
),
ConfigurableSource {
private val defaultBaseUrl = swatUrl private val defaultBaseUrl = swatUrl
override val baseUrl by lazy { getPrefBaseUrl() } override val baseUrl by lazy { getPrefBaseUrl() }
@ -61,7 +58,6 @@ class MangaSwat :
override fun searchMangaNextPageSelector() = "a[rel=next]" override fun searchMangaNextPageSelector() = "a[rel=next]"
override val seriesTitleSelector = "h1[itemprop=headline]"
override val seriesArtistSelector = "span:contains(الناشر) i" override val seriesArtistSelector = "span:contains(الناشر) i"
override val seriesAuthorSelector = "span:contains(المؤلف) i" override val seriesAuthorSelector = "span:contains(المؤلف) i"
override val seriesGenreSelector = "span:contains(التصنيف) a, .mgen a" override val seriesGenreSelector = "span:contains(التصنيف) a, .mgen a"
@ -117,6 +113,8 @@ class MangaSwat :
} }
} }
screen.addPreference(baseUrlPref) screen.addPreference(baseUrlPref)
super.setupPreferenceScreen(screen)
} }
private fun getPrefBaseUrl(): String = preferences.getString(BASE_URL_PREF, defaultBaseUrl)!! private fun getPrefBaseUrl(): String = preferences.getString(BASE_URL_PREF, defaultBaseUrl)!!

View File

@ -1,7 +0,0 @@
ext {
extName = 'RE Manga (Arabic)'
extClass = '.REManga'
extVersionCode = 2
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,258 +0,0 @@
package eu.kanade.tachiyomi.extension.ar.remangaarabic
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.Locale
class REManga : ParsedHttpSource() {
override val name = "RE Manga"
override val baseUrl = "https://re-manga.com"
override val lang = "ar"
override val supportsLatest = true
// Popular
override fun popularMangaRequest(page: Int): Request =
GET("$baseUrl/manga-list/?title=&order=popular&status=&type=")
override fun popularMangaSelector() = "article.animpost"
override fun popularMangaFromElement(element: Element): SManga =
SManga.create().apply {
setUrlWithoutDomain(element.select("a").attr("abs:href"))
element.select("img").let {
thumbnail_url = it.attr("abs:src")
title = it.attr("title")
}
}
override fun popularMangaNextPageSelector(): String? = null
// Latest
override fun latestUpdatesRequest(page: Int): Request =
GET("$baseUrl/manga-list/?title=&order=update&status=&type=")
override fun latestUpdatesSelector() = popularMangaSelector()
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun latestUpdatesNextPageSelector(): String? = popularMangaNextPageSelector()
// Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/manga-list/?".toHttpUrl().newBuilder()
.addQueryParameter("title", query)
filters.forEach { filter ->
when (filter) {
is SortFilter -> url.addQueryParameter("order", filter.toUriPart())
is StatusFilter -> url.addQueryParameter("status", filter.toUriPart())
is TypeFilter -> url.addQueryParameter("type", filter.toUriPart())
is GenreFilter -> {
filter.state
.filter { it.state != Filter.TriState.STATE_IGNORE }
.forEach { url.addQueryParameter("genre[]", it.id) }
}
is YearFilter -> {
filter.state
.filter { it.state != Filter.TriState.STATE_IGNORE }
.forEach { url.addQueryParameter("years[]", it.id) }
}
else -> {}
}
}
return GET(url.build(), headers)
}
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
// Details
override fun mangaDetailsParse(document: Document): SManga {
return SManga.create().apply {
document.select("div.infox").first()!!.let { info ->
title = info.select("h1").text()
}
description = document.select("div.desc > div > p").text()
genre = document.select("div.spe > span:contains(نوع), div.genre-info > a").joinToString { it.text() }
document.select("div.spe > span:contains(الحالة)").first()?.text()?.also { statusText ->
when {
statusText.contains("مستمر", true) -> status = SManga.ONGOING
else -> status = SManga.COMPLETED
}
}
}
}
// Chapters
override fun chapterListSelector() = ".lsteps li"
override fun chapterFromElement(element: Element): SChapter {
return SChapter.create().apply {
setUrlWithoutDomain(element.select("a").first()!!.attr("abs:href"))
val chNum = element.select(".eps > a").first()!!.text()
val chTitle = element.select(".lchx > a").first()!!.text()
name = when {
chTitle.startsWith("الفصل ") -> chTitle
else -> "الفصل $chNum - $chTitle"
}
element.select(".date").first()?.text()?.let { date ->
date_upload = DATE_FORMATTER.parse(date)?.time ?: 0L
}
}
}
// Pages
override fun pageListParse(document: Document): List<Page> {
return document.select("div.reader-area img").mapIndexed { i, img ->
Page(i, "", img.attr("abs:src"))
}
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
// Filters
override fun getFilterList() = FilterList(
SortFilter(getSortFilters()),
StatusFilter(getStatusFilters()),
TypeFilter(getTypeFilter()),
Filter.Separator(),
Filter.Header("exclusion not available for This source"),
GenreFilter(getGenreFilters()),
YearFilter(getYearFilters()),
)
private class SortFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Sort by", vals)
private class TypeFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Type", vals)
private class StatusFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Status", vals)
class Genre(name: String, val id: String = name) : Filter.TriState(name)
private class GenreFilter(genres: List<Genre>) : Filter.Group<Genre>("Genre", genres)
class Year(name: String, val id: String = name) : Filter.TriState(name)
private class YearFilter(years: List<Year>) : Filter.Group<Year>("Year", years)
private fun getSortFilters(): Array<Pair<String?, String>> = arrayOf(
Pair("title", "A-Z"),
Pair("titlereverse", "Z-A"),
Pair("update", "Latest Update"),
Pair("latest", "Latest Added"),
Pair("popular", "Popular"),
)
private fun getStatusFilters(): Array<Pair<String?, String>> = arrayOf(
Pair("", "All"),
Pair("Publishing", "مستمر"),
Pair("Finished", "تاريخ انتهي"),
)
private fun getTypeFilter(): Array<Pair<String?, String>> = arrayOf(
Pair("", "All"),
Pair("Manga", "Manga"),
Pair("Manhwa", "Manhwa"),
Pair("Manhua", "Manhua"),
)
private fun getGenreFilters(): List<Genre> = listOf(
Genre("Action", "action"),
Genre("Adventure", "adventure"),
Genre("Comedy", "comedy"),
Genre("Dementia", "dementia"),
Genre("Demons", "demons"),
Genre("Drama", "drama"),
Genre("Ecchi", "ecchi"),
Genre("Fantasy", "fantasy"),
Genre("Harem", "harem"),
Genre("Historical", "historical"),
Genre("Horror", "horror"),
Genre("Josei", "josei"),
Genre("Magic", "magic"),
Genre("Martial Arts", "martial-arts"),
Genre("Military", "military"),
Genre("Mystery", "mystery"),
Genre("Parody", "parody"),
Genre("Psychological", "psychological"),
Genre("Romance", "romance"),
Genre("Samurai", "samurai"),
Genre("School", "school"),
Genre("Sci-Fi", "sci-fi"),
Genre("Seinen", "seinen"),
Genre("Shounen", "shounen"),
Genre("Slice of Life", "slice-of-life"),
Genre("Sports", "sports"),
Genre("Super Power", "super-power"),
Genre("Supernatural", "supernatural"),
Genre("Vampire", "vampire"),
)
private fun getYearFilters(): List<Year> = listOf(
Year("1970", "1970"),
Year("1986", "1986"),
Year("1989", "1989"),
Year("1995", "1995"),
Year("1997", "1997"),
Year("1998", "1998"),
Year("1999", "1999"),
Year("2000", "2000"),
Year("2002", "2002"),
Year("2003", "2003"),
Year("2004", "2004"),
Year("2005", "2005"),
Year("2006", "2006"),
Year("2007", "2007"),
Year("2008", "2008"),
Year("2009", "2009"),
Year("2010", "2010"),
Year("2011", "2011"),
Year("2012", "2012"),
Year("2013", "2013"),
Year("2014", "2014"),
Year("2016", "2016"),
Year("2017", "2017"),
Year("2018", "2018"),
Year("2019", "2019"),
Year("2020", "2020"),
)
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String?, String>>) :
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray()) {
fun toUriPart() = vals[state].first
}
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("MMM d, yyy", Locale("ar"))
}
}
}

View File

@ -1,9 +0,0 @@
ext {
extName = 'StellarSaber'
extClass = '.StellarSaber'
themePkg = 'mangathemesia'
baseUrl = 'https://stellarsaber.pro'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,12 +0,0 @@
package eu.kanade.tachiyomi.extension.ar.stellarsaber
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import java.text.SimpleDateFormat
import java.util.Locale
class StellarSaber : MangaThemesia(
"StellarSaber",
"https://stellarsaber.pro",
"ar",
dateFormat = SimpleDateFormat("MMMMM dd, yyyy", Locale("ar")),
)

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,189 +0,0 @@
package eu.kanade.tachiyomi.extension.en.alandal
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.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 kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
class Alandal : HttpSource() {
override val name = "Alandal"
override val baseUrl = "https://alandal.com"
private val apiUrl = "https://qq.alandal.com/api"
override val lang = "en"
override val supportsLatest = true
override val client = network.cloudflareClient.newBuilder()
.rateLimit(1)
.build()
override fun headersBuilder() = super.headersBuilder().apply {
add("Referer", "$baseUrl/")
}
private val apiHeaders by lazy { apiHeadersBuilder.build() }
private val apiHeadersBuilder = headersBuilder().apply {
add("Accept", "application/json")
add("Host", apiUrl.toHttpUrl().host)
add("Origin", baseUrl)
add("Sec-Fetch-Dest", "empty")
add("Sec-Fetch-Mode", "cors")
add("Sec-Fetch-Site", "same-origin")
}
private val json: Json by injectLazy()
// ============================== Popular ===============================
override fun popularMangaRequest(page: Int): Request =
searchMangaRequest(page, "", FilterList(SortFilter("popular")))
override fun popularMangaParse(response: Response): MangasPage =
searchMangaParse(response)
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request =
searchMangaRequest(page, "", FilterList(SortFilter("new")))
override fun latestUpdatesParse(response: Response): MangasPage =
searchMangaParse(response)
// =============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = apiUrl.toHttpUrl().newBuilder().apply {
addPathSegment("series")
if (query.isNotBlank()) {
addQueryParameter("name", query)
}
addQueryParameter("type", "comic")
val filterList = filters.ifEmpty { getFilterList() }
filterList.filterIsInstance<UriFilter>().forEach {
it.addToUri(this)
}
addQueryParameter("page", page.toString())
}.build()
return GET(url, apiHeaders)
}
override fun searchMangaParse(response: Response): MangasPage {
val data = response.parseAs<ResponseDto<SearchSeriesDto>>().data.series
val mangaList = data.data.map { it.toSManga() }
val hasNextPage = data.currentPage < data.lastPage
return MangasPage(mangaList, hasNextPage)
}
// =============================== Filters ==============================
override fun getFilterList(): FilterList = FilterList(
GenreFilter(),
SortFilter(),
StatusFilter(),
)
// =========================== Manga Details ============================
override fun getMangaUrl(manga: SManga): String =
baseUrl + manga.url.replace("series/", "series/comic-")
override fun mangaDetailsRequest(manga: SManga): Request {
val url = apiUrl.toHttpUrl().newBuilder().apply {
addPathSegments(manga.url.substringAfter("/"))
addQueryParameter("type", "comic")
}.build()
return GET(url, apiHeaders)
}
override fun mangaDetailsParse(response: Response): SManga =
response.parseAs<ResponseDto<MangaDetailsDto>>().data.series.toSManga()
// ============================== Chapters ==============================
override fun getChapterUrl(chapter: SChapter): String {
return baseUrl + chapter.url
.replace("series/", "chapter/comic-")
.replace("chapters/", "")
}
override fun chapterListRequest(manga: SManga): Request {
val url = "$apiUrl${manga.url}".toHttpUrl().newBuilder().apply {
addPathSegment("chapters")
addQueryParameter("type", "comic")
addQueryParameter("from", "0")
addQueryParameter("to", "999")
}.build()
return GET(url, apiHeaders)
}
override fun chapterListParse(response: Response): List<SChapter> {
val slug = response.request.url.newBuilder()
.query(null)
.removePathSegment(0) // Remove /api
.build()
.encodedPath
return response.parseAs<ChapterResponseDto>().data.map {
it.toSChapter(slug)
}.reversed()
}
// =============================== Pages ================================
override fun pageListRequest(chapter: SChapter): Request {
if (chapter.name.startsWith("[LOCKED]")) {
throw Exception("Log in and unlock chapter in webview, then refresh chapter list")
}
val url = "$apiUrl${chapter.url}".toHttpUrl().newBuilder().apply {
addQueryParameter("type", "comic")
addQueryParameter("traveler", "0")
}.build()
return GET(url, apiHeaders)
}
override fun pageListParse(response: Response): List<Page> {
val data = response.parseAs<PagesResponseDto>().data.chapter.chapter
return data.pages.mapIndexed { index, s ->
Page(index, imageUrl = s)
}
}
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
override fun imageRequest(page: Page): Request {
val pageHeaders = headersBuilder().apply {
add("Accept", "image/avif,image/webp,*/*")
add("Host", page.imageUrl!!.toHttpUrl().host)
}.build()
return GET(page.imageUrl!!, pageHeaders)
}
// ============================= Utilities ==============================
private inline fun <reified T> Response.parseAs(): T = use {
json.decodeFromStream(it.body.byteStream())
}
}

View File

@ -1,118 +0,0 @@
package eu.kanade.tachiyomi.extension.en.alandal
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.jsoup.Jsoup
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
@Serializable
class ResponseDto<T>(
val data: ResultDto<T>,
) {
@Serializable
class ResultDto<T>(
val series: T,
)
}
@Serializable
class SearchSeriesDto(
@SerialName("current_page") val currentPage: Int,
@SerialName("last_page") val lastPage: Int,
val data: List<SearchEntryDto>,
) {
@Serializable
class SearchEntryDto(
val name: String,
val slug: String,
val cover: String,
) {
fun toSManga(): SManga = SManga.create().apply {
title = name
url = "/series/$slug"
thumbnail_url = cover
}
}
}
@Serializable
class MangaDetailsDto(
val name: String,
val summary: String,
val status: NamedObject,
val genres: List<NamedObject>,
val creators: List<NamedObject>,
val cover: String,
) {
@Serializable
class NamedObject(
val name: String,
val type: String? = null,
)
fun toSManga(): SManga = SManga.create().apply {
title = name
thumbnail_url = cover
description = Jsoup.parseBodyFragment(summary).text()
genre = genres.joinToString { it.name }
author = creators.filter { it.type!! == "author" }.joinToString { it.name }
status = this@MangaDetailsDto.status.name.parseStatus()
}
private fun String.parseStatus(): Int = when (this.lowercase()) {
"ongoing" -> SManga.ONGOING
"completed" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
}
@Serializable
class ChapterResponseDto(
val data: List<ChapterDto>,
) {
@Serializable
class ChapterDto(
val name: String,
@SerialName("published_at") val published: String,
val access: Boolean,
) {
fun toSChapter(slug: String): SChapter = SChapter.create().apply {
val prefix = if (access) "" else "[LOCKED] "
name = "${prefix}Chapter ${this@ChapterDto.name}"
date_upload = try {
dateFormat.parse(published)!!.time
} catch (_: ParseException) {
0L
}
url = "$slug/${this@ChapterDto.name}"
}
companion object {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.ENGLISH)
}
}
}
@Serializable
class PagesResponseDto(
val data: PagesDataDto,
) {
@Serializable
class PagesDataDto(
val chapter: PagesChapterDto,
) {
@Serializable
class PagesChapterDto(
val chapter: PagesChapterImagesDto,
) {
@Serializable
class PagesChapterImagesDto(
val pages: List<String>,
)
}
}
}

View File

@ -1,89 +0,0 @@
package eu.kanade.tachiyomi.extension.en.alandal
import eu.kanade.tachiyomi.source.model.Filter
import okhttp3.HttpUrl
interface UriFilter {
fun addToUri(builder: HttpUrl.Builder)
}
open class UriPartFilter(
name: String,
private val param: String,
private val vals: Array<Pair<String, String>>,
defaultValue: String? = null,
) : Filter.Select<String>(
name,
vals.map { it.first }.toTypedArray(),
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
),
UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
builder.addQueryParameter(param, vals[state].second)
}
}
open class UriMultiSelectOption(name: String, val value: String) : Filter.CheckBox(name)
open class UriMultiSelectFilter(
name: String,
private val param: String,
private val vals: Array<Pair<String, String>>,
) : Filter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
val checked = state.filter { it.state }
if (checked.isEmpty()) {
builder.addQueryParameter(param, "-1")
} else {
checked.forEach {
builder.addQueryParameter(param, it.value)
}
}
}
}
class GenreFilter : UriMultiSelectFilter(
"Genre",
"genres",
arrayOf(
Pair("Action", "1"),
Pair("Fantasy", "2"),
Pair("Regression", "3"),
Pair("Overpowered", "4"),
Pair("Ascension", "5"),
Pair("Revenge", "6"),
Pair("Martial Arts", "7"),
Pair("Magic", "8"),
Pair("Necromancer", "9"),
Pair("Adventure", "10"),
Pair("Tower", "11"),
Pair("Dungeons", "12"),
Pair("Psychological", "13"),
Pair("Isekai", "14"),
),
)
class SortFilter(defaultSort: String? = null) : UriPartFilter(
"Sort By",
"sort",
arrayOf(
Pair("Popularity", "popular"),
Pair("Name", "name"),
Pair("Chapters", "chapters"),
Pair("Rating", "Rating"),
Pair("New", "new"),
),
defaultSort,
)
class StatusFilter : UriPartFilter(
"Status",
"status",
arrayOf(
Pair("Any", "-1"),
Pair("Ongoing", "1"),
Pair("Coming Soon", "5"),
Pair("Completed", "6"),
),
)

View File

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

View File

@ -6,7 +6,6 @@ import android.content.SharedPreferences
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.createChapter
import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.getPathFromUrl import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.getPathFromUrl
import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.prepareTags import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.prepareTags
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
@ -24,7 +23,6 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.decodeFromStream
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -35,8 +33,6 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.IOException import java.io.IOException
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.math.ceil
import kotlin.math.min
class Anchira : HttpSource(), ConfigurableSource { class Anchira : HttpSource(), ConfigurableSource {
override val name = "Anchira" override val name = "Anchira"
@ -113,23 +109,6 @@ class Anchira : HttpSource(), ConfigurableSource {
fetchMangaDetails(manga).map { fetchMangaDetails(manga).map {
MangasPage(listOf(it), false) MangasPage(listOf(it), false)
} }
} else if (query.startsWith(SLUG_BUNDLE_PREFIX)) {
// bundle entries as chapters
val url = applyFilters(
page,
query.substringAfter(SLUG_BUNDLE_PREFIX),
filters,
).removeAllQueryParameters("page")
if (
url.build().queryParameter("sort") == "4"
) {
url.removeAllQueryParameters("sort")
}
val manga = SManga.create()
.apply { this.url = "?${url.build().query}" }
fetchMangaDetails(manga).map {
MangasPage(listOf(it), false)
}
} else { } else {
// regular filtering without text search // regular filtering without text search
client.newCall(searchMangaRequest(page, query, filters)) client.newCall(searchMangaRequest(page, query, filters))
@ -137,171 +116,108 @@ class Anchira : HttpSource(), ConfigurableSource {
.map(::searchMangaParse) .map(::searchMangaParse)
} }
} }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
GET(applyFilters(page, query, filters).build(), headers)
private fun applyFilters(page: Int, query: String, filters: FilterList): HttpUrl.Builder {
val filterList = if (filters.isEmpty()) getFilterList() else filters
val trendingFilter = filterList.findInstance<TrendingFilter>()
val sortTrendingFilter = filters.findInstance<SortTrendingFilter>()
var url = libraryUrl.toHttpUrl().newBuilder() var url = libraryUrl.toHttpUrl().newBuilder()
if (trendingFilter?.state == true) { url.addQueryParameter("page", page.toString())
val interval = when (sortTrendingFilter?.state) {
1 -> "3"
else -> ""
}
if (interval.isNotBlank()) url.setQueryParameter("interval", interval) if (query.isNotBlank()) {
url.addQueryParameter("s", query)
}
url = url.toString().replace("library", "trending").toHttpUrl() filters.forEach { filter ->
.newBuilder() when (filter) {
} else { is CategoryGroup -> {
if (query.isNotBlank()) { var sum = 0
url.setQueryParameter("s", query)
}
filters.forEach { filter -> filter.state.forEach { category ->
when (filter) { when (category.name) {
is CategoryGroup -> { "Manga" -> if (category.state) sum = sum or 1
var sum = 0 "Doujinshi" -> if (category.state) sum = sum or 2
"Illustration" -> if (category.state) sum = sum or 4
filter.state.forEach { category ->
when (category.name) {
"Manga" -> if (category.state) sum = sum or 1
"Doujinshi" -> if (category.state) sum = sum or 2
"Illustration" -> if (category.state) sum = sum or 4
}
}
if (sum > 0) url.setQueryParameter("cat", sum.toString())
}
is SortFilter -> {
val sort = when (filter.state?.index) {
0 -> "1"
1 -> "2"
2 -> "4"
4 -> "32"
else -> ""
}
if (sort.isNotEmpty()) url.setQueryParameter("sort", sort)
if (filter.state?.ascending == true) url.setQueryParameter("order", "1")
}
is FavoritesFilter -> {
if (filter.state) {
if (!isLoggedIn()) {
throw IOException("No login cookie found")
}
url = url.toString().replace("library", "user/favorites").toHttpUrl()
.newBuilder()
} }
} }
else -> {} if (sum > 0) url.addQueryParameter("cat", sum.toString())
} }
is SortFilter -> {
val sort = when (filter.state?.index) {
0 -> "1"
1 -> "2"
2 -> "4"
4 -> "32"
else -> ""
}
if (sort.isNotEmpty()) url.addQueryParameter("sort", sort)
if (filter.state?.ascending == true) url.addQueryParameter("order", "1")
}
is FavoritesFilter -> {
if (filter.state) {
if (!isLoggedIn()) {
throw IOException("No login cookie found")
}
url = url.toString().replace("library", "user/favorites").toHttpUrl()
.newBuilder()
}
}
else -> {}
} }
} }
if (page > 1) { return GET(url.build(), headers)
url.setQueryParameter("page", page.toString())
}
return url
} }
override fun searchMangaParse(response: Response) = latestUpdatesParse(response) override fun searchMangaParse(response: Response) = latestUpdatesParse(response)
// Details // Details
override fun mangaDetailsRequest(manga: SManga): Request { override fun mangaDetailsRequest(manga: SManga) =
return if (manga.url.startsWith("?")) { GET("$libraryUrl/${getPathFromUrl(manga.url)}", headers)
GET(libraryUrl + manga.url, headers)
} else {
GET("$libraryUrl/${getPathFromUrl(manga.url)}", headers)
}
}
override fun mangaDetailsParse(response: Response): SManga { override fun mangaDetailsParse(response: Response): SManga {
return if (response.request.url.pathSegments.count() == libraryUrl.toHttpUrl().pathSegments.count()) { val data = json.decodeFromString<Entry>(response.body.string())
val manga = latestUpdatesParse(response).mangas.first()
val query = response.request.url.queryParameter("s")
val cleanTitle = CHAPTER_SUFFIX_RE.replace(manga.title, "").trim()
manga.apply {
url = "?${response.request.url.query}"
description = "Bundled from $query"
title = "[Bundle] $cleanTitle"
update_strategy = UpdateStrategy.ALWAYS_UPDATE
}
} else {
val data = json.decodeFromString<Entry>(response.body.string())
SManga.create().apply { return SManga.create().apply {
url = "/g/${data.id}/${data.key}" url = "/g/${data.id}/${data.key}"
title = data.title title = data.title
thumbnail_url = thumbnail_url =
"$cdnUrl/${data.id}/${data.key}/b/${data.thumbnailIndex + 1}" "$cdnUrl/${data.id}/${data.key}/b/${data.thumbnailIndex + 1}"
artist = data.tags.filter { it.namespace == 1 }.joinToString(", ") { it.name } artist = data.tags.filter { it.namespace == 1 }.joinToString(", ") { it.name }
author = data.tags.filter { it.namespace == 2 }.joinToString(", ") { it.name } author = data.tags.filter { it.namespace == 2 }.joinToString(", ") { it.name }
genre = prepareTags(data.tags, preferences.useTagGrouping) genre = prepareTags(data.tags, preferences.useTagGrouping)
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
status = SManga.COMPLETED status = SManga.COMPLETED
}
} }
} }
override fun getMangaUrl(manga: SManga) = override fun getMangaUrl(manga: SManga) = if (preferences.openSource) {
if (preferences.openSource && !manga.url.startsWith("?")) { val id = manga.url.split("/").reversed()[1].toInt()
val id = manga.url.split("/").reversed()[1].toInt() anchiraData.find { it.id == id }?.url ?: "$baseUrl${manga.url}"
anchiraData.find { it.id == id }?.url ?: "$baseUrl${manga.url}" } else {
} else { "$baseUrl${manga.url}"
"$baseUrl${manga.url}" }
}
// Chapter // Chapter
override fun chapterListRequest(manga: SManga): Request { override fun chapterListRequest(manga: SManga) =
return if (manga.url.startsWith("?")) { GET("$libraryUrl/${getPathFromUrl(manga.url)}", headers)
GET(libraryUrl + manga.url, headers)
} else {
GET("$libraryUrl/${getPathFromUrl(manga.url)}", headers)
}
}
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val chapterList = mutableListOf<SChapter>() val data = json.decodeFromString<Entry>(response.body.string())
if (response.request.url.pathSegments.count() == libraryUrl.toHttpUrl().pathSegments.count()) {
var results = json.decodeFromString<LibraryResponse>(response.body.string()) return listOf(
val pages = min(5, ceil((results.total.toFloat() / results.limit)).toInt()) SChapter.create().apply {
for (page in 1..pages) { url = "/g/${data.id}/${data.key}"
results.entries.forEach { data -> name = "Chapter"
chapterList.add( date_upload = data.publishedAt * 1000
createChapter(data, response, anchiraData), chapter_number = 1f
) },
} )
if (page < pages) {
results = json.decodeFromString<LibraryResponse>(
client.newCall(
GET(
response.request.url.newBuilder()
.setQueryParameter("page", (page + 1).toString()).build(),
headers,
),
).execute().body.string(),
)
}
}
} else {
val data = json.decodeFromString<Entry>(response.body.string())
chapterList.add(
createChapter(data, response, anchiraData),
)
}
return chapterList
} }
override fun getChapterUrl(chapter: SChapter) = "$baseUrl/g/${getPathFromUrl(chapter.url)}" override fun getChapterUrl(chapter: SChapter) = "$baseUrl/g/${getPathFromUrl(chapter.url)}"
@ -362,16 +278,14 @@ class Anchira : HttpSource(), ConfigurableSource {
val openSourcePref = SwitchPreferenceCompat(screen.context).apply { val openSourcePref = SwitchPreferenceCompat(screen.context).apply {
key = OPEN_SOURCE_PREF key = OPEN_SOURCE_PREF
title = "Open source website in WebView" title = "Open source website in WebView"
summary = summary = "Enable to open the original source website of the gallery (if available) instead of Anchira."
"Enable to open the original source website of the gallery (if available) instead of Anchira."
setDefaultValue(false) setDefaultValue(false)
} }
val useTagGrouping = SwitchPreferenceCompat(screen.context).apply { val useTagGrouping = SwitchPreferenceCompat(screen.context).apply {
key = USE_TAG_GROUPING key = USE_TAG_GROUPING
title = "Group tags" title = "Group tags"
summary = summary = "Enable to group tags together by artist, circle, parody, magazine and general tags"
"Enable to group tags together by artist, circle, parody, magazine and general tags"
setDefaultValue(false) setDefaultValue(false)
} }
@ -384,10 +298,6 @@ class Anchira : HttpSource(), ConfigurableSource {
CategoryGroup(), CategoryGroup(),
SortFilter(), SortFilter(),
FavoritesFilter(), FavoritesFilter(),
Filter.Separator(),
Filter.Header("Others are ignored if trending only"),
TrendingFilter(),
SortTrendingFilter(),
) )
private class CategoryFilter(name: String) : Filter.CheckBox(name, false) private class CategoryFilter(name: String) : Filter.CheckBox(name, false)
@ -407,18 +317,6 @@ class Anchira : HttpSource(), ConfigurableSource {
Selection(2, false), Selection(2, false),
) )
private class TrendingFilter : Filter.CheckBox(
"Show only trending",
)
private class SortTrendingFilter : PartFilter(
"Sort By",
arrayOf("Trending: Weekly", "Trending: Monthly"),
)
private open class PartFilter(displayName: String, value: Array<String>) :
Filter.Select<String>(displayName, value)
private val SharedPreferences.imageQuality private val SharedPreferences.imageQuality
get() = getString(IMAGE_QUALITY_PREF, "b")!! get() = getString(IMAGE_QUALITY_PREF, "b")!!
@ -464,11 +362,8 @@ class Anchira : HttpSource(), ConfigurableSource {
.use { json.decodeFromStream<List<EntryKey>>(it.body.byteStream()) } .use { json.decodeFromStream<List<EntryKey>>(it.body.byteStream()) }
} }
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
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 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"
@ -476,5 +371,3 @@ class Anchira : HttpSource(), ConfigurableSource {
"https://gist.githubusercontent.com/LetrixZ/2b559cc5829d1c221c701e02ecd81411/raw/data-v5.json" "https://gist.githubusercontent.com/LetrixZ/2b559cc5829d1c221c701e02ecd81411/raw/data-v5.json"
} }
} }
val CHAPTER_SUFFIX_RE = Regex("(?<!20\\d\\d-)\\b[\\d.]{1,4}$")

View File

@ -3,6 +3,15 @@ package eu.kanade.tachiyomi.extension.en.anchira
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable
data class ListEntry(
val id: Int,
val key: String,
val title: String,
@SerialName("thumb_index") val thumbnailIndex: Int,
val tags: List<Tag> = emptyList(),
)
@Serializable @Serializable
data class Tag( data class Tag(
var name: String, var name: String,
@ -11,7 +20,7 @@ data class Tag(
@Serializable @Serializable
data class LibraryResponse( data class LibraryResponse(
val entries: List<Entry> = emptyList(), val entries: List<ListEntry> = emptyList(),
val total: Int, val total: Int,
val page: Int, val page: Int,
val limit: Int, val limit: Int,
@ -21,12 +30,11 @@ data class LibraryResponse(
data class Entry( data class Entry(
val id: Int, val id: Int,
val key: String, val key: String,
@SerialName("published_at") val publishedAt: Long = 0L, @SerialName("published_at") val publishedAt: Long,
val title: String, val title: String,
@SerialName("thumb_index") val thumbnailIndex: Int = 1, @SerialName("thumb_index") val thumbnailIndex: Int,
val tags: List<Tag> = emptyList(), val tags: List<Tag> = emptyList(),
val url: String? = null, val url: String? = null,
val pages: Int = 1,
) )
@Serializable @Serializable

View File

@ -1,9 +1,5 @@
package eu.kanade.tachiyomi.extension.en.anchira package eu.kanade.tachiyomi.extension.en.anchira
import eu.kanade.tachiyomi.source.model.SChapter
import okhttp3.Response
import java.util.Locale
object AnchiraHelper { object AnchiraHelper {
fun getPathFromUrl(url: String) = "${url.split("/").reversed()[1]}/${url.split("/").last()}" fun getPathFromUrl(url: String) = "${url.split("/").reversed()[1]}/${url.split("/").last()}"
@ -29,31 +25,4 @@ object AnchiraHelper {
} }
} }
.joinToString(", ") { it } .joinToString(", ") { it }
fun createChapter(entry: Entry, response: Response, anchiraData: List<EntryKey>) =
SChapter.create().apply {
val ch =
CHAPTER_SUFFIX_RE.find(entry.title)?.value?.trim('.') ?: "1"
val source = anchiraData.find { it.id == entry.id }?.url
?: response.request.url.toString()
url = "/g/${entry.id}/${entry.key}"
name = "$ch. ${entry.title.removeSuffix(" $ch")}"
date_upload = entry.publishedAt * 1000
chapter_number = ch.toFloat()
scanlator = buildString {
append(
Regex("fakku|irodori|anchira").find(source)?.value.orEmpty()
.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(
Locale.getDefault(),
)
} else {
it.toString()
}
},
)
append(" - ${entry.pages} pages")
}
}
} }

View File

@ -0,0 +1,107 @@
package eu.kanade.tachiyomi.extension.en.anchira
object XXTEA {
private const val DELTA = -0x61c88647
@Suppress("NOTHING_TO_INLINE", "FunctionName")
private inline fun MX(sum: Int, y: Int, z: Int, p: Int, e: Int, k: IntArray): Int {
return (z.ushr(5) xor (y shl 2)) + (y.ushr(3) xor (z shl 4)) xor (sum xor y) + (k[p and 3 xor e] xor z)
}
private fun decrypt(data: ByteArray, key: ByteArray): ByteArray =
data.takeIf { it.isNotEmpty() }
?.let {
decrypt(data.toIntArray(false), key.fixKey().toIntArray(false))
.toByteArray(true)
} ?: data
fun decrypt(data: ByteArray, key: String): ByteArray? =
kotlin.runCatching { decrypt(data, key.toByteArray(Charsets.UTF_8)) }.getOrNull()
fun decryptToString(data: ByteArray, key: String): String? =
kotlin.runCatching { decrypt(data, key)?.toString(Charsets.UTF_8) }.getOrNull()
private fun decrypt(v: IntArray, k: IntArray): IntArray {
val n = v.size - 1
if (n < 1) {
return v
}
var p: Int
val q = 6 + 52 / (n + 1)
var z: Int
var y = v[0]
var sum = q * DELTA
var e: Int
while (sum != 0) {
e = sum.ushr(2) and 3
p = n
while (p > 0) {
z = v[p - 1]
v[p] -= MX(sum, y, z, p, e, k)
y = v[p]
p--
}
z = v[n]
v[0] -= MX(sum, y, z, p, e, k)
y = v[0]
sum -= DELTA
}
return v
}
private fun ByteArray.fixKey(): ByteArray {
if (size == 16) return this
val fixedKey = ByteArray(16)
if (size < 16) {
copyInto(fixedKey)
} else {
copyInto(fixedKey, endIndex = 16)
}
return fixedKey
}
private fun ByteArray.toIntArray(includeLength: Boolean): IntArray {
var n = if (size and 3 == 0) {
size.ushr(2)
} else {
size.ushr(2) + 1
}
val result: IntArray
if (includeLength) {
result = IntArray(n + 1)
result[n] = size
} else {
result = IntArray(n)
}
n = size
for (i in 0 until n) {
result[i.ushr(2)] =
result[i.ushr(2)] or (0x000000ff and this[i].toInt() shl (i and 3 shl 3))
}
return result
}
private fun IntArray.toByteArray(includeLength: Boolean): ByteArray? {
var n = size shl 2
if (includeLength) {
val m = this[size - 1]
n -= 4
if (m < n - 3 || m > n) {
return null
}
n = m
}
val result = ByteArray(n)
for (i in 0 until n) {
result[i] = this[i.ushr(2)].ushr(i and 3 shl 3).toByte()
}
return result
}
}

View File

@ -6,7 +6,6 @@ import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat 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.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
@ -27,14 +26,12 @@ import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class AsuraScans : class AsuraScans : MangaThemesia(
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), ) {
),
ConfigurableSource {
private val preferences by lazy { private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
@ -284,6 +281,8 @@ class AsuraScans :
summary = PREF_PERM_MANGA_URL_SUMMARY summary = PREF_PERM_MANGA_URL_SUMMARY
setDefaultValue(true) setDefaultValue(true)
}.also(screen::addPreference) }.also(screen::addPreference)
super.setupPreferenceScreen(screen)
} }
private val SharedPreferences.permaUrlPref private val SharedPreferences.permaUrlPref

View File

@ -8,7 +8,3 @@ ext {
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:randomua"))
}

View File

@ -2,14 +2,11 @@ package eu.kanade.tachiyomi.extension.en.constellarscans
import android.app.Application import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.lib.randomua.addRandomUAPreferenceToScreen
import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA
import eu.kanade.tachiyomi.lib.randomua.getPrefUAType import eu.kanade.tachiyomi.lib.randomua.getPrefUAType
import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent
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.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonArray
@ -22,29 +19,22 @@ import okhttp3.Request
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit
class ConstellarScans : class ConstellarScans : MangaThemesia("Constellar Scans", "https://constellarcomic.com", "en") {
MangaThemesia(
"Constellar Scans",
"https://constellarcomic.com",
"en",
),
ConfigurableSource {
private val preferences: SharedPreferences by lazy { private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
override fun setupPreferenceScreen(screen: PreferenceScreen) {
addRandomUAPreferenceToScreen(screen)
}
override val client: OkHttpClient by lazy { override val client: OkHttpClient by lazy {
network.cloudflareClient.newBuilder() network.cloudflareClient.newBuilder()
.setRandomUserAgent( .setRandomUserAgent(
preferences.getPrefUAType(), preferences.getPrefUAType(),
preferences.getPrefCustomUA(), preferences.getPrefCustomUA(),
) )
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.rateLimit(1, 1) .rateLimit(1, 1)
.build() .build()
} }
@ -71,8 +61,6 @@ class ConstellarScans :
.build() .build()
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
countViews(document)
val html = document.toString() val html = document.toString()
if (!html.contains("ts_rea_der_._run(\"")) { if (!html.contains("ts_rea_der_._run(\"")) {
return super.pageListParse(document) return super.pageListParse(document)
@ -92,6 +80,7 @@ class ConstellarScans :
} }
.joinToString("") .joinToString("")
countViews(document)
return json.parseToJsonElement(tsReaderRawData).jsonObject["sources"]!!.jsonArray[0].jsonObject["images"]!!.jsonArray.mapIndexed { idx, it -> return json.parseToJsonElement(tsReaderRawData).jsonObject["sources"]!!.jsonArray[0].jsonObject["images"]!!.jsonArray.mapIndexed { idx, it ->
Page(idx, imageUrl = it.jsonPrimitive.content) Page(idx, imageUrl = it.jsonPrimitive.content)
} }

View File

@ -1,8 +1,8 @@
ext { ext {
extName = 'Winter Sun' extName = '1st Manhwa'
extClass = '.WinterSun' extClass = '.FirstManhwa'
themePkg = 'madara' themePkg = 'madara'
baseUrl = 'https://wintersunscan.xyz' baseUrl = 'https://1stmanhwa.com'
overrideVersionCode = 0 overrideVersionCode = 0
isNsfw = true isNsfw = true
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,9 @@
package eu.kanade.tachiyomi.extension.en.firstmanhwa
import eu.kanade.tachiyomi.multisrc.madara.Madara
class FirstManhwa : Madara("1st Manhwa", "https://1stmanhwa.com", "en") {
override val useNewChapterEndpoint = true
override val filterNonMangaItems = false
override val mangaDetailsSelectorStatus = "div.summary-heading:contains(Status) + div.summary-content"
}

View File

@ -9,7 +9,6 @@ import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat 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.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
@ -26,14 +25,12 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get 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

View File

@ -2,8 +2,8 @@ ext {
extName = 'Infernal Void Scans' extName = 'Infernal Void Scans'
extClass = '.InfernalVoidScans' extClass = '.InfernalVoidScans'
themePkg = 'mangathemesia' themePkg = 'mangathemesia'
baseUrl = 'https://hivescans.com' baseUrl = 'https://void-scans.com'
overrideVersionCode = 6 overrideVersionCode = 5
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -2,6 +2,6 @@ package eu.kanade.tachiyomi.extension.en.infernalvoidscans
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
class InfernalVoidScans : MangaThemesia("Infernal Void Scans", "https://hivescans.com", "en") { class InfernalVoidScans : MangaThemesia("Infernal Void Scans", "https://void-scans.com", "en") {
override val pageSelector = "div#readerarea > p > img" override val pageSelector = "div#readerarea > p > img"
} }

View File

@ -6,7 +6,6 @@ import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat 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.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
@ -21,15 +20,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.IOException import java.io.IOException
class LuminousScans : class LuminousScans : MangaThemesia("Luminous Scans", "https://lumitoon.com", "en", mangaUrlDirectory = "/series") {
MangaThemesia(
"Luminous Scans",
"https://lumitoon.com",
"en",
mangaUrlDirectory = "/series",
),
ConfigurableSource {
override val client = super.client.newBuilder() override val client = super.client.newBuilder()
.addInterceptor(::urlChangeInterceptor) .addInterceptor(::urlChangeInterceptor)
.rateLimit(2) .rateLimit(2)
@ -210,6 +201,8 @@ class LuminousScans :
summary = PREF_PERM_MANGA_URL_SUMMARY summary = PREF_PERM_MANGA_URL_SUMMARY
setDefaultValue(true) setDefaultValue(true)
}.also(screen::addPreference) }.also(screen::addPreference)
super.setupPreferenceScreen(screen)
} }
private val SharedPreferences.permaUrlPref private val SharedPreferences.permaUrlPref

View File

@ -33,29 +33,19 @@ class LunarScans : MangaThemesia(
val filters = mutableListOf<Filter<*>>( val filters = mutableListOf<Filter<*>>(
Filter.Header("Note: Can't be used with text search!"), Filter.Header("Note: Can't be used with text search!"),
Filter.Separator(), Filter.Separator(),
StatusFilter(intl["status_filter_title"], statusOptions), StatusFilter(),
TypeFilter(intl["type_filter_title"], typeFilterOptions), TypeFilter(),
OrderByFilter(intl["order_by_filter_title"], orderByFilterOptions), OrderByFilter(),
Filter.Header("Genre exclusion is not available for all sources"),
GenreListFilter(getGenreList()),
) )
if (!genrelist.isNullOrEmpty()) {
filters.addAll(
listOf(
Filter.Header(intl["genre_exclusion_warning"]),
GenreListFilter(intl["genre_filter_title"], getGenreList()),
),
)
} else {
filters.add(
Filter.Header(intl["genre_missing_warning"]),
)
}
if (hasProjectPage) { if (hasProjectPage) {
filters.addAll( filters.addAll(
mutableListOf<Filter<*>>( mutableListOf<Filter<*>>(
Filter.Separator(), Filter.Separator(),
Filter.Header(intl["project_filter_warning"]), Filter.Header("NOTE: Can't be used with other filter!"),
Filter.Header(intl.format("project_filter_name", name)), Filter.Header("$name Project List page"),
ProjectFilter(intl["project_filter_title"], projectFilterOptions), ProjectFilter(),
), ),
) )
} }

View File

@ -0,0 +1,9 @@
ext {
extName = '247Manga'
extClass = '.Manga247'
themePkg = 'madara'
baseUrl = 'https://247manga.com'
overrideVersionCode = 1
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,5 @@
package eu.kanade.tachiyomi.extension.en.manga247
import eu.kanade.tachiyomi.multisrc.madara.Madara
class Manga247 : Madara("247Manga", "https://247manga.com", "en")

View File

@ -1,9 +1,9 @@
ext { ext {
extName = 'Manga Galaxy' extName = 'Manga Galaxy'
extClass = '.MangaGalaxy' extClass = '.MangaGalaxy'
themePkg = 'mangathemesia' themePkg = 'madara'
baseUrl = 'https://mangagalaxy.me' baseUrl = 'https://mangagalaxy.me'
overrideVersionCode = 9 overrideVersionCode = 1
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -1,13 +1,15 @@
package eu.kanade.tachiyomi.extension.en.mangagalaxy package eu.kanade.tachiyomi.extension.en.mangagalaxy
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia import eu.kanade.tachiyomi.multisrc.madara.Madara
import java.text.SimpleDateFormat
import java.util.Locale
class MangaGalaxy : MangaThemesia( class MangaGalaxy : Madara(
"Manga Galaxy", "Manga Galaxy",
"https://mangagalaxy.me", "https://mangagalaxy.me",
"en", "en",
mangaUrlDirectory = "/series", dateFormat = SimpleDateFormat("MM/dd/yyyy", Locale.US),
) { ) {
// moved from Madara to MangaThemesia override val mangaDetailsSelectorStatus = "div.summary-heading:contains(status) + div.summary-content"
override val versionId = 2 override val mangaDetailsSelectorDescription = "div.summary-heading:contains(Summary) + div"
} }

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,267 +0,0 @@
package eu.kanade.tachiyomi.extension.en.mangamonks
import eu.kanade.tachiyomi.extension.en.mangamonks.MangaMonksHelper.buildApiHeaders
import eu.kanade.tachiyomi.extension.en.mangamonks.MangaMonksHelper.toDate
import eu.kanade.tachiyomi.extension.en.mangamonks.MangaMonksHelper.toFormRequestBody
import eu.kanade.tachiyomi.extension.en.mangamonks.MangaMonksHelper.toStatus
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.MissingFieldException
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.injectLazy
class MangaMonks : ParsedHttpSource() {
override val name = "MangaMonks"
override val baseUrl = "https://mangamonks.com"
override val lang = "en"
override val supportsLatest = true
// popular
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/popular-manga/$page", headers)
}
override fun popularMangaSelector() = ".main-slide"
override fun popularMangaNextPageSelector() = "li:nth-last-child(2) a.page-btn"
override fun popularMangaFromElement(element: Element): SManga {
return SManga.create().apply {
title = element.selectFirst(".detail a")!!.text()
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
thumbnail_url = element.select("img").attr("data-src")
}
}
// latest
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/latest-releases/$page", headers)
}
override fun latestUpdatesSelector() = ".tab-pane .row .col-12"
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
// search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val filterList = filters.let { if (it.isEmpty()) getFilterList() else it }
return if (query.isNotEmpty()) {
val requestBody = query.toFormRequestBody()
val requestHeaders = headersBuilder().buildApiHeaders(requestBody)
POST("$baseUrl/search/live", requestHeaders, requestBody)
} else {
val url = "$baseUrl/genre/".toHttpUrl().newBuilder()
filterList.forEach { filter ->
when (filter) {
is GenreFilter -> filter.toUriPart().let {
url.apply {
addPathSegment(it)
addQueryParameter("include[]", filter.toGenreValue())
}
}
is StatusFilter -> filter.toUriPart().let {
url.apply {
addQueryParameter("term", query)
addQueryParameter("status[]", it)
}
}
else -> {}
}
}
url.addPathSegment(page.toString())
GET(url.build(), headers)
}
}
override fun searchMangaSelector() = ".main-slide .item"
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
private val json: Json by injectLazy()
override fun searchMangaParse(response: Response): MangasPage {
val isJson = response.header("Content-Type")?.contains("application/json") ?: false
if (isJson) {
return try {
val result = json.decodeFromString<MangaList>(response.body.string())
val mangaList = result.manga.map {
SManga.create().apply {
title = it.title
setUrlWithoutDomain(it.url)
thumbnail_url = it.image
}
}
val hasNextPage = false
MangasPage(mangaList, hasNextPage)
} catch (_: MissingFieldException) {
MangasPage(emptyList(), false)
}
} else {
val document = response.asJsoup()
val mangas = document.select(searchMangaSelector()).map { element ->
searchMangaFromElement(element)
}
val hasNextPage = searchMangaNextPageSelector().let { selector ->
document.select(selector).first()
} != null
return MangasPage(mangas, hasNextPage)
}
}
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
// details
override fun mangaDetailsParse(document: Document): SManga {
return SManga.create().apply {
author = document.selectFirst(".publisher a")!!.text()
status = document.selectFirst(".info-detail .source")!!.text().toStatus()
genre = document.select(".info-detail .tags a").joinToString { it.text() }
description = document.select(".info-desc p").text()
thumbnail_url = document.select(".img-holder img").attr("data-src")
}
}
// chapters
override fun chapterListSelector() = ".chapter-list li"
override fun chapterFromElement(element: Element): SChapter {
return SChapter.create().apply {
setUrlWithoutDomain(element.select("a").attr("href"))
name = element.select(".chapter-number").text()
date_upload = element.select(".time").text().trim().toDate()
}
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
// pages
override fun pageListParse(document: Document): List<Page> {
return document.select("#zoomContainer .image img").mapIndexed { i, it ->
val src = it.attr("src")
val imageUrl = if (src.startsWith("https")) src else baseUrl + src
Page(i, imageUrl = imageUrl)
}
}
// filters
override fun getFilterList() = FilterList(
Filter.Header("NOTE: Ignored if using text search!"),
Filter.Separator(),
StatusFilter(),
GenreFilter(),
)
private class StatusFilter : UriPartFilter(
"Status",
arrayOf(
Pair("Ongoing", "ongoing"),
Pair("Completed", "completed"),
),
)
private class GenreFilter : GenreValueFilter(
"Genre",
arrayOf(
Triple("Action", "2", "action"),
Triple("Adventure", "3", "adventure"),
Triple("Comedy", "5", "comedy"),
Triple("Cooking", "6", "cooking"),
Triple("Doujinshi", "7", "doujinshi"),
Triple("Drama", "8", "drama"),
Triple("Ecchi", "9", "ecchi"),
Triple("Yaoi", "11", "yaoi"),
Triple("Fantasy", "12", "fantasy"),
Triple("Gender Bender", "13", "gender-bender"),
Triple("Harem", "14", "harem"),
Triple("Historical", "15", "historical"),
Triple("Horror", "16", "horror"),
Triple("Josei", "17", "josei"),
Triple("Manhua", "18", "manhua"),
Triple("Manhwa", "19", "manhwa"),
Triple("Mecha", "21", "mecha"),
Triple("Mystery", "24", "mystery"),
Triple("One Shot", "25", "one-shot"),
Triple("Psychological", "26", "psychological"),
Triple("Romance", "27", "romance"),
Triple("School Life", "28", "school-life"),
Triple("Sci-fi", "29", "sci-fi"),
Triple("Seinen", "30", "seinen"),
Triple("Yuri", "31", "yuri"),
Triple("Shoujo", "32", "shoujo"),
Triple("Shounen", "34", "shounen"),
Triple("Shounen Ai", "35", "shounen-ai"),
Triple("Slice of Life", "36", "slice-of-life"),
Triple("Sports", "37", "sports"),
Triple("Supernatural", "38", "supernatural"),
Triple("Tragedy", "39", "tragedy"),
Triple("Webtoons", "40", "webtoons"),
Triple("Full Color", "42", "full-color"),
Triple("Isekai", "44", "isekai"),
Triple("Reincarnation", "45", "reincarnation"),
Triple("Time Travel", "46", "time-travel"),
Triple("Martial arts", "48", "martial-arts"),
Triple("Monsters", "49", "monsters-monsters"),
Triple("Thriller", "51", "thriller"),
Triple("Adaptation", "52", "adaptation"),
Triple("Reverse Harem", "53", "reverse-harem"),
Triple("Cross-dressing", "54", "cross-dressing"),
Triple("Zombies", "55", "zombies"),
Triple("Crime", "56", "crime"),
Triple("Ghosts", "57", "ghosts"),
Triple("Magic", "58", "magic"),
Triple("Gore", "59", "gore"),
Triple("+18", "84", "18"),
Triple("LGBT", "47", "lgbt"),
Triple("erotic", "62", "erotic"),
Triple("Harem", "63", "harem-harem"),
Triple("MILF", "64", "milf"),
Triple("Yaoi/boy's love", "65", "yaoiboys-love"),
Triple("Yuri/girl's love", "66", "yurigirls-love"),
Triple("BBW", "67", "bbw"),
Triple("Shota", "68", "shota"),
Triple("NTR/cheating", "69", "ntrcheating"),
Triple("BDSM", "70", "bdsm"),
Triple("tentacle", "71", "tentacle"),
Triple("Oyasumi/sleeping", "72", "oyasumisleeping"),
Triple("Elf Hentai", "74", "elf-hentai"),
Triple("Rape", "75", "rape"),
Triple("Incest", "76", "incest"),
Triple("Inseki", "77", "inseki"),
Triple("LGBTQ", "78", "lgbtq"),
Triple("Beastiality", "79", "bestiality"),
Triple("Defloration", "80", "defloration"),
Triple("loli", "81", "loli"),
Triple("Raw", "83", "raw"),
),
)
private open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
private open class GenreValueFilter(displayName: String, private val vals: Array<Triple<String, String, String>>) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].third
fun toGenreValue() = vals[state].second
}
@Serializable
class MangaList(val manga: List<MangaItem>)
@Serializable
class MangaItem(val title: String, val url: String, val image: String)
}

View File

@ -1,68 +0,0 @@
package eu.kanade.tachiyomi.extension.en.mangamonks
import eu.kanade.tachiyomi.source.model.SManga
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.RequestBody
import java.util.Calendar
object MangaMonksHelper {
fun Headers.Builder.buildApiHeaders(requestBody: RequestBody) = this
.add("Content-Length", requestBody.contentLength().toString())
.add("Content-Type", requestBody.contentType().toString())
.add("Accept", "application/json")
.add("X-Requested-With", "XMLHttpRequest")
.build()
inline fun <reified T : Any> T.toFormRequestBody(): RequestBody {
return FormBody.Builder()
.add("dataType", "json")
.add("phrase", this.toString())
.build()
}
fun String?.toStatus(): Int {
return when {
this == null -> SManga.UNKNOWN
this.contains("Ongoing", true) -> SManga.ONGOING
this.contains("Completed", true) -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
}
fun String?.toDate(): Long {
val trimmedDate = this!!.substringBefore(" ago").removeSuffix("s").split(" ")
val calendar = Calendar.getInstance()
when {
trimmedDate[1].contains(
"Year",
ignoreCase = true,
) -> calendar.apply { add(Calendar.YEAR, -trimmedDate[0].toInt()) }
trimmedDate[1].contains(
"Month",
ignoreCase = true,
) -> calendar.apply { add(Calendar.MONTH, -trimmedDate[0].toInt()) }
trimmedDate[1].contains(
"Week",
ignoreCase = true,
) -> calendar.apply { add(Calendar.WEEK_OF_MONTH, -trimmedDate[0].toInt()) }
trimmedDate[1].contains(
"Day",
ignoreCase = true,
) -> calendar.apply { add(Calendar.DAY_OF_MONTH, -trimmedDate[0].toInt()) }
trimmedDate[1].contains(
"Hour",
ignoreCase = true,
) -> calendar.apply { add(Calendar.HOUR_OF_DAY, -trimmedDate[0].toInt()) }
trimmedDate[1].contains(
"Minute",
ignoreCase = true,
) -> calendar.apply { add(Calendar.MINUTE, -trimmedDate[0].toInt()) }
}
return calendar.timeInMillis
}
}

View File

@ -1,7 +0,0 @@
ext {
extName = 'MangaOwl.To'
extClass = '.MangaOwlToFactory'
extVersionCode = 2
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

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