Compare commits
51 Commits
f6cb65688d
...
555051eba4
Author | SHA1 | Date |
---|---|---|
AwkwardPeak7 | 555051eba4 | |
nomaxsnx | 536e080aec | |
bapeey | 84664872c4 | |
Claudemirovsky | 46a6e5e7f3 | |
mohamedotaku | a3ff15c263 | |
Chopper | b68bc4cfd0 | |
Chopper | e78bd29133 | |
bapeey | cf4a208d08 | |
bapeey | 5344c62b6b | |
bapeey | d0edd1855f | |
bapeey | 87246f5443 | |
bapeey | d48b870553 | |
bapeey | 99f355d65b | |
stevenyomi | 8e7c8da40e | |
bapeey | a8cd270d86 | |
BrutuZ | 2bb5ef9059 | |
KirinRaikage | 58b5aa2f3d | |
Chopper | 2a0588e8d9 | |
stevenyomi | 33f95f820c | |
keiyoushi-bot | 008fc27cf9 | |
stevenyomi | b860dcb132 | |
stevenyomi | b65648aa6c | |
airis | 2725ac93f6 | |
Fermín Cirella | bb0317bef3 | |
bapeey | 882c1d9738 | |
OtakuArab | 70ccdbc637 | |
AwkwardPeak7 | c2b7c1cb20 | |
AwkwardPeak7 | feef718f2a | |
AwkwardPeak7 | e971eb7fbc | |
nausicaa | d60149eb6b | |
AwkwardPeak7 | 88dba59eef | |
AwkwardPeak7 | 8f18229563 | |
stevenyomi | 19f09c8967 | |
Eshlender | 80c5340a02 | |
bapeey | a0457871e2 | |
stevenyomi | e2f24a8f91 | |
stevenyomi | 56863063fc | |
Cuong M. Tran | f7cd89926a | |
Tef | ac9f307979 | |
Rama Bondan Prakoso | 420c7d9cd5 | |
AwkwardPeak7 | 48ca6558ed | |
Cuong M. Tran | c81adc7829 | |
AwkwardPeak7 | 24087b9688 | |
Secozzi | 5cb0af3b2d | |
Henry | 12e3079af3 | |
Cuong M. Tran | 82e6f7b6da | |
Fermín Cirella | 18fab95708 | |
Secozzi | be407aa637 | |
Vetle Ledaal | 9c064fb3eb | |
bapeey | 5a643095ad | |
stevenyomi | 37c80ab2f6 |
|
@ -1,8 +1,30 @@
|
|||
{
|
||||
"extends": [
|
||||
"config:base"
|
||||
"config:recommended"
|
||||
],
|
||||
"schedule": ["on sunday"],
|
||||
"includePaths": [
|
||||
"buildSrc/gradle/**",
|
||||
"gradle/**",
|
||||
".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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ jobs:
|
|||
echo ${{ secrets.SIGNING_KEY }} | base64 -d > signingkey.jks
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0
|
||||
|
||||
- name: Build extensions
|
||||
env:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
@ -18,8 +18,6 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: use versionCatalogs.named("libs") in Gradle 8.5
|
||||
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
|
||||
dependencies {
|
||||
compileOnly(libs.findBundle("common").get())
|
||||
compileOnly(versionCatalogs.named("libs").findBundle("common").get())
|
||||
}
|
||||
|
|
|
@ -36,10 +36,8 @@ kotlinter {
|
|||
)
|
||||
}
|
||||
|
||||
// TODO: use versionCatalogs.named("libs") in Gradle 8.5
|
||||
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
|
||||
dependencies {
|
||||
compileOnly(libs.findBundle("common").get())
|
||||
compileOnly(versionCatalogs.named("libs").findBundle("common").get())
|
||||
}
|
||||
|
||||
tasks {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
|
@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
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.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
|
|
|
@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
|
|||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
|
@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 9
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 2
|
||||
baseVersionCode = 3
|
||||
|
|
|
@ -10,9 +10,6 @@ class BloggerDto(
|
|||
|
||||
@Serializable
|
||||
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 entry: List<BloggerFeedEntryDto> = emptyList(),
|
||||
)
|
||||
|
|
|
@ -65,7 +65,7 @@ abstract class GravureBlogger(
|
|||
initialized = true
|
||||
}
|
||||
}
|
||||
val hasNextPage = (data.feed.startIndex.t.toInt() + data.feed.itemsPerPage.t.toInt()) <= data.feed.totalResults.t.toInt()
|
||||
val hasNextPage = data.feed.entry.size == MAX_RESULTS
|
||||
|
||||
return MangasPage(manga, hasNextPage)
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 10
|
||||
baseVersionCode = 11
|
||||
|
|
|
@ -60,7 +60,7 @@ class KemonoPostDto(
|
|||
if (file.path != null) add(KemonoAttachmentDto(file.name!!, file.path))
|
||||
addAll(attachments)
|
||||
}.filter {
|
||||
when (it.name.substringAfterLast('.').lowercase()) {
|
||||
when (it.path.substringAfterLast('.').lowercase()) {
|
||||
"png", "jpg", "gif", "jpeg", "webp" -> true
|
||||
else -> false
|
||||
}
|
||||
|
@ -91,6 +91,7 @@ class KemonoPostDto(
|
|||
@Serializable
|
||||
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
|
||||
class KemonoAttachmentDto(val name: String, val path: String) {
|
||||
override fun toString() = "$path?f=$name"
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
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
|
|
@ -0,0 +1,23 @@
|
|||
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
|
|
@ -2,8 +2,8 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 28
|
||||
baseVersionCode = 29
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:randomua"))
|
||||
api(project(":lib:i18n"))
|
||||
}
|
||||
|
|
|
@ -1,15 +1,8 @@
|
|||
package eu.kanade.tachiyomi.multisrc.mangathemesia
|
||||
|
||||
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.lib.i18n.Intl
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
|
@ -21,64 +14,56 @@ import eu.kanade.tachiyomi.util.asJsoup
|
|||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Elements
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
// Formerly WPMangaStream & WPMangaReader -> MangaThemesia
|
||||
abstract class MangaThemesia(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String,
|
||||
final override val lang: String,
|
||||
val mangaUrlDirectory: String = "/manga",
|
||||
val dateFormat: SimpleDateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale.US),
|
||||
) : ParsedHttpSource(), ConfigurableSource {
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
) : ParsedHttpSource() {
|
||||
|
||||
protected open val json: Json by injectLazy()
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient by lazy {
|
||||
network.cloudflareClient.newBuilder()
|
||||
.setRandomUserAgent(
|
||||
preferences.getPrefUAType(),
|
||||
preferences.getPrefCustomUA(),
|
||||
)
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.build()
|
||||
}
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.set("Referer", "$baseUrl/")
|
||||
|
||||
protected val intl = Intl(
|
||||
language = lang,
|
||||
baseLanguage = "en",
|
||||
availableLanguages = setOf("en", "es"),
|
||||
classLoader = javaClass.classLoader!!,
|
||||
)
|
||||
|
||||
open val projectPageString = "/project"
|
||||
|
||||
// Popular (Search with popular order and nothing else)
|
||||
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", FilterList(OrderByFilter("popular")))
|
||||
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", popularFilter)
|
||||
override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
||||
|
||||
// Latest (Search with update order and nothing else)
|
||||
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", FilterList(OrderByFilter("update")))
|
||||
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", latestFilter)
|
||||
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||
|
||||
// Search
|
||||
|
@ -166,22 +151,96 @@ abstract class MangaThemesia(
|
|||
override fun searchMangaNextPageSelector() = "div.pagination .next, div.hpage .r"
|
||||
|
||||
// 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 seriesTitleSelector = "h1.entry-title"
|
||||
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 seriesTitleSelector = "h1.entry-title, .ts-breadcrumb li:last-child span"
|
||||
|
||||
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 seriesAltNameSelector = ".alternative, .wd-full:contains(alt) span, .alter, .seriestualt"
|
||||
open val seriesGenreSelector = "div.gnr a, .mgen a, .seriestugenre a, span:contains(genre)"
|
||||
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\\=]"
|
||||
open val seriesStatusSelector = ".infotable tr:contains(status) td:last-child, .tsinfo .imptdt:contains(status) i, .fmed b:contains(status)+span span:contains(status)"
|
||||
|
||||
open val seriesAltNameSelector = ".alternative, .wd-full:contains(alt) span, .alter, .seriestualt, " +
|
||||
selector(
|
||||
".infotable tr:contains(%s) td:last-child",
|
||||
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 altNamePrefix = "Alternative Name: "
|
||||
open val altNamePrefix = "${intl["alt_names_heading"]} "
|
||||
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
document.selectFirst(seriesDetailsSelector)?.let { seriesDetails ->
|
||||
title = seriesDetails.selectFirst(seriesTitleSelector)?.text().orEmpty()
|
||||
title = seriesDetails.selectFirst(seriesTitleSelector)!!.text()
|
||||
artist = seriesDetails.selectFirst(seriesArtistSelector)?.ownText().removeEmptyPlaceholder()
|
||||
author = seriesDetails.selectFirst(seriesAuthorSelector)?.ownText().removeEmptyPlaceholder()
|
||||
description = seriesDetails.select(seriesDescriptionSelector).joinToString("\n") { it.text() }.trim()
|
||||
|
@ -210,16 +269,32 @@ abstract class MangaThemesia(
|
|||
}
|
||||
|
||||
protected fun String?.removeEmptyPlaceholder(): String? {
|
||||
return if (this.isNullOrBlank() || this == "-" || this == "N/A") null else this
|
||||
return if (this.isNullOrBlank() || this == "-" || this == "N/A" || this == "n/a") null else this
|
||||
}
|
||||
|
||||
open fun String?.parseStatus(): Int = when {
|
||||
this == null -> SManga.UNKNOWN
|
||||
listOf("ongoing", "publishing").any { this.contains(it, ignoreCase = true) } -> SManga.ONGOING
|
||||
this.contains("hiatus", ignoreCase = true) -> SManga.ON_HIATUS
|
||||
this.contains("completed", ignoreCase = true) -> SManga.COMPLETED
|
||||
listOf("dropped", "cancelled").any { this.contains(it, ignoreCase = true) } -> SManga.CANCELLED
|
||||
else -> SManga.UNKNOWN
|
||||
open fun String?.parseStatus(): Int {
|
||||
if (this == null) return SManga.UNKNOWN
|
||||
|
||||
return when (this.lowercase().trim()) {
|
||||
"مستمرة", "en curso", "ongoing", "on going", "ativo", "en cours",
|
||||
"en cours de publication", "đang tiến hành", "em lançamento", "онгоінг", "publishing",
|
||||
"devam ediyor", "em andamento", "in corso", "güncel", "berjalan", "продолжается", "updating", "lançando", "in arrivo", "emision",
|
||||
"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
|
||||
|
@ -227,6 +302,9 @@ abstract class MangaThemesia(
|
|||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
|
||||
countViews(document)
|
||||
|
||||
val chapters = document.select(chapterListSelector()).map { chapterFromElement(it) }
|
||||
|
||||
// Add timestamp to latest chapter, taken from "Updated On".
|
||||
|
@ -238,8 +316,6 @@ abstract class MangaThemesia(
|
|||
if (date.isNotEmpty()) chapters.first().date_upload = parseUpdatedOnDate(date)
|
||||
}
|
||||
|
||||
countViews(document)
|
||||
|
||||
return chapters
|
||||
}
|
||||
|
||||
|
@ -267,13 +343,13 @@ abstract class MangaThemesia(
|
|||
open val pageSelector = "div#readerarea img"
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
countViews(document)
|
||||
|
||||
val chapterUrl = document.location()
|
||||
val htmlPages = document.select(pageSelector)
|
||||
.filterNot { it.imgAttr().isEmpty() }
|
||||
.mapIndexed { i, img -> Page(i, chapterUrl, img.imgAttr()) }
|
||||
|
||||
countViews(document)
|
||||
|
||||
// Some sites also loads pages via javascript
|
||||
if (htmlPages.isNotEmpty()) { return htmlPages }
|
||||
|
||||
|
@ -320,8 +396,6 @@ abstract class MangaThemesia(
|
|||
.build()
|
||||
|
||||
val newHeaders = headersBuilder()
|
||||
.set("Content-Length", formBody.contentLength().toString())
|
||||
.set("Content-Type", formBody.contentType().toString())
|
||||
.set("Referer", document.location())
|
||||
.build()
|
||||
|
||||
|
@ -339,17 +413,22 @@ abstract class MangaThemesia(
|
|||
}
|
||||
|
||||
val request = countViewsRequest(document) ?: return
|
||||
runCatching { client.newCall(request).execute().close() }
|
||||
val callback = object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) = response.close()
|
||||
override fun onFailure(call: Call, e: IOException) = Unit
|
||||
}
|
||||
|
||||
client.newCall(request).enqueue(callback)
|
||||
}
|
||||
|
||||
// Filters
|
||||
protected class AuthorFilter : Filter.Text("Author")
|
||||
protected class AuthorFilter(name: String) : Filter.Text(name)
|
||||
|
||||
protected class YearFilter : Filter.Text("Year")
|
||||
protected class YearFilter(name: String) : Filter.Text(name)
|
||||
|
||||
open class SelectFilter(
|
||||
displayName: String,
|
||||
val vals: Array<Pair<String, String>>,
|
||||
private val vals: Array<Pair<String, String>>,
|
||||
defaultValue: String? = null,
|
||||
) : Filter.Select<String>(
|
||||
displayName,
|
||||
|
@ -359,63 +438,91 @@ abstract class MangaThemesia(
|
|||
fun selectedValue() = vals[state].second
|
||||
}
|
||||
|
||||
protected class StatusFilter : SelectFilter(
|
||||
"Status",
|
||||
arrayOf(
|
||||
Pair("All", ""),
|
||||
Pair("Ongoing", "ongoing"),
|
||||
Pair("Completed", "completed"),
|
||||
Pair("Hiatus", "hiatus"),
|
||||
Pair("Dropped", "dropped"),
|
||||
),
|
||||
protected class StatusFilter(
|
||||
name: String,
|
||||
options: Array<Pair<String, String>>,
|
||||
) : SelectFilter(
|
||||
name,
|
||||
options,
|
||||
)
|
||||
|
||||
protected class TypeFilter : SelectFilter(
|
||||
"Type",
|
||||
arrayOf(
|
||||
Pair("All", ""),
|
||||
Pair("Manga", "Manga"),
|
||||
Pair("Manhwa", "Manhwa"),
|
||||
Pair("Manhua", "Manhua"),
|
||||
Pair("Comic", "Comic"),
|
||||
),
|
||||
protected open val statusOptions = arrayOf(
|
||||
Pair(intl["status_filter_option_all"], ""),
|
||||
Pair(intl["status_filter_option_ongoing"], "ongoing"),
|
||||
Pair(intl["status_filter_option_completed"], "completed"),
|
||||
Pair(intl["status_filter_option_hiatus"], "hiatus"),
|
||||
Pair(intl["status_filter_option_dropped"], "dropped"),
|
||||
)
|
||||
|
||||
protected class OrderByFilter(defaultOrder: String? = null) : SelectFilter(
|
||||
"Sort By",
|
||||
arrayOf(
|
||||
Pair("Default", ""),
|
||||
Pair("A-Z", "title"),
|
||||
Pair("Z-A", "titlereverse"),
|
||||
Pair("Latest Update", "update"),
|
||||
Pair("Latest Added", "latest"),
|
||||
Pair("Popular", "popular"),
|
||||
),
|
||||
protected class TypeFilter(
|
||||
name: String,
|
||||
options: Array<Pair<String, String>>,
|
||||
) : SelectFilter(
|
||||
name,
|
||||
options,
|
||||
)
|
||||
|
||||
protected open val typeFilterOptions = arrayOf(
|
||||
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,
|
||||
)
|
||||
|
||||
protected class ProjectFilter : SelectFilter(
|
||||
"Filter Project",
|
||||
arrayOf(
|
||||
Pair("Show all manga", ""),
|
||||
Pair("Show only project manga", "project-filter-on"),
|
||||
),
|
||||
protected open val orderByFilterOptions = arrayOf(
|
||||
Pair(intl["order_by_filter_default"], ""),
|
||||
Pair(intl["order_by_filter_az"], "title"),
|
||||
Pair(intl["order_by_filter_za"], "titlereverse"),
|
||||
Pair(intl["order_by_filter_latest_update"], "update"),
|
||||
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(
|
||||
name: String,
|
||||
val value: String,
|
||||
state: Int = STATE_IGNORE,
|
||||
state: Int,
|
||||
) : Filter.TriState(name, state)
|
||||
|
||||
protected class GenreListFilter(genres: List<Genre>) : Filter.Group<Genre>("Genre", genres)
|
||||
protected class GenreListFilter(name: String, genres: List<Genre>) : Filter.Group<Genre>(name, genres)
|
||||
|
||||
protected var genrelist: List<GenreData>? = null
|
||||
|
||||
private var genrelist: List<Genre>? = null
|
||||
protected open fun getGenreList(): List<Genre> {
|
||||
// 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", ""))
|
||||
return genrelist?.map { Genre(it.name, it.value, it.state) }.orEmpty()
|
||||
}
|
||||
|
||||
open val hasProjectPage = false
|
||||
|
@ -423,21 +530,31 @@ abstract class MangaThemesia(
|
|||
override fun getFilterList(): FilterList {
|
||||
val filters = mutableListOf<Filter<*>>(
|
||||
Filter.Separator(),
|
||||
AuthorFilter(),
|
||||
YearFilter(),
|
||||
StatusFilter(),
|
||||
TypeFilter(),
|
||||
OrderByFilter(),
|
||||
Filter.Header("Genre exclusion is not available for all sources"),
|
||||
GenreListFilter(getGenreList()),
|
||||
AuthorFilter(intl["author_filter_title"]),
|
||||
YearFilter(intl["year_filter_title"]),
|
||||
StatusFilter(intl["status_filter_title"], statusOptions),
|
||||
TypeFilter(intl["type_filter_title"], typeFilterOptions),
|
||||
OrderByFilter(intl["order_by_filter_title"], orderByFilterOptions),
|
||||
)
|
||||
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) {
|
||||
filters.addAll(
|
||||
mutableListOf<Filter<*>>(
|
||||
Filter.Separator(),
|
||||
Filter.Header("NOTE: Can't be used with other filter!"),
|
||||
Filter.Header("$name Project List page"),
|
||||
ProjectFilter(),
|
||||
Filter.Header(intl["project_filter_warning"]),
|
||||
Filter.Header(intl.format("project_filter_name", name)),
|
||||
ProjectFilter(intl["project_filter_title"], projectFilterOptions),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -485,9 +602,9 @@ abstract class MangaThemesia(
|
|||
(!strict && url.pathSegments.size == n + 1 && url.pathSegments[n].isEmpty())
|
||||
}
|
||||
|
||||
private fun parseGenres(document: Document): List<Genre>? {
|
||||
private fun parseGenres(document: Document): List<GenreData>? {
|
||||
return document.selectFirst("ul.genrez")?.select("li")?.map { li ->
|
||||
Genre(
|
||||
GenreData(
|
||||
li.selectFirst("label")!!.text(),
|
||||
li.selectFirst("input[type=checkbox]")!!.attr("value"),
|
||||
)
|
||||
|
@ -514,15 +631,10 @@ abstract class MangaThemesia(
|
|||
|
||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
addRandomUAPreferenceToScreen(screen)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val URL_SEARCH_PREFIX = "url:"
|
||||
|
||||
// More info: https://issuetracker.google.com/issues/36970498
|
||||
@Suppress("RegExpRedundantEscape")
|
||||
private val MANGA_PAGE_ID_REGEX = "post_id\\s*:\\s*(\\d+)\\}".toRegex()
|
||||
private val CHAPTER_PAGE_ID_REGEX = "chapter_id\\s*=\\s*(\\d+);".toRegex()
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ data class PizzaReaderDto(
|
|||
|
||||
@Serializable
|
||||
data class PizzaComicDto(
|
||||
val artist: String = "",
|
||||
val artist: String? = null,
|
||||
val author: String = "",
|
||||
val chapters: List<PizzaChapterDto> = emptyList(),
|
||||
val description: String = "",
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
ext {
|
||||
extName = 'BILIBILI COMICS'
|
||||
extClass = '.BilibiliComicsFactory'
|
||||
themePkg = 'bilibili'
|
||||
baseUrl = 'https://www.bilibilicomics.com'
|
||||
overrideVersionCode = 3
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
|
@ -1,411 +0,0 @@
|
|||
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),
|
||||
)
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'LANraragi'
|
||||
extClass = '.LANraragiFactory'
|
||||
extVersionCode = 16
|
||||
extVersionCode = 17
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -82,6 +82,17 @@ open class LANraragi(private val suffix: String = "") : ConfigurableSource, Unme
|
|||
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 {
|
||||
val id = if (manga.url.startsWith("/api/search/random")) randomArchiveID else getReaderId(manga.url)
|
||||
val uri = getApiUriBuilder("/api/archives/$id/metadata").build()
|
||||
|
@ -310,6 +321,7 @@ 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(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(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 {
|
||||
|
@ -494,5 +506,7 @@ open class LANraragi(private val suffix: String = "") : ConfigurableSource, Unme
|
|||
private const val SORT_BY_NS_KEY = "latestNamespacePref"
|
||||
private const val CLEAR_NEW_KEY = "clearNew"
|
||||
private const val CLEAR_NEW_DEFAULT = true
|
||||
private const val URL_TAG_PREFIX_KEY = "urlTagPrefix"
|
||||
private const val URL_TAG_PREFIX_DEFAULT = ""
|
||||
}
|
||||
}
|
||||
|
|
|
@ -290,6 +290,7 @@ enum class Language {
|
|||
RUSSIAN,
|
||||
THAI,
|
||||
VIETNAMESE,
|
||||
GERMAN,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
|
|
@ -13,5 +13,6 @@ class MangaPlusFactory : SourceFactory {
|
|||
MangaPlus("ru", "rus", Language.RUSSIAN),
|
||||
MangaPlus("th", "tha", Language.THAI),
|
||||
MangaPlus("vi", "vie", Language.VIETNAMESE),
|
||||
MangaPlus("de", "deu", Language.GERMAN),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -33,10 +33,11 @@ open class MiauScan(lang: String) : MangaThemesia(
|
|||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val genreFilterIndex = filters.indexOfFirst { it is GenreListFilter }
|
||||
val genreFilter = filters.getOrNull(genreFilterIndex) as? GenreListFilter
|
||||
?: GenreListFilter(emptyList())
|
||||
?: GenreListFilter("", emptyList())
|
||||
|
||||
val overloadedGenreFilter = GenreListFilter(
|
||||
genres = genreFilter.state + listOf(
|
||||
genreFilter.name,
|
||||
genreFilter.state + listOf(
|
||||
Genre("", PORTUGUESE_GENRE_ID, portugueseMode),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -31,8 +31,8 @@ class Mihentai : MangaThemesia("Mihentai", "https://mihentai.com", "all") {
|
|||
listOf(
|
||||
StatusFilter(),
|
||||
TypeFilter(),
|
||||
OrderByFilter(),
|
||||
GenreListFilter(getGenreList()),
|
||||
OrderByFilter(intl["order_by_filter_title"], orderByFilterOptions),
|
||||
GenreListFilter(intl["genre_filter_title"], getGenreList()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package eu.kanade.tachiyomi.extension.ar.areamanga
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
|
@ -10,21 +9,4 @@ class AreaManga : MangaThemesia(
|
|||
"https://www.areascans.net",
|
||||
"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
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.MangaSwat'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://swatmanhua.com'
|
||||
overrideVersionCode = 16
|
||||
overrideVersionCode = 17
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -7,6 +7,7 @@ import androidx.preference.PreferenceScreen
|
|||
import eu.kanade.tachiyomi.extension.BuildConfig
|
||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
|
@ -23,12 +24,14 @@ import java.util.Locale
|
|||
|
||||
private const val swatUrl = "https://swatmanhua.com"
|
||||
|
||||
class MangaSwat : MangaThemesia(
|
||||
"MangaSwat",
|
||||
swatUrl,
|
||||
"ar",
|
||||
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
||||
) {
|
||||
class MangaSwat :
|
||||
MangaThemesia(
|
||||
"MangaSwat",
|
||||
swatUrl,
|
||||
"ar",
|
||||
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
||||
),
|
||||
ConfigurableSource {
|
||||
private val defaultBaseUrl = swatUrl
|
||||
|
||||
override val baseUrl by lazy { getPrefBaseUrl() }
|
||||
|
@ -58,6 +61,7 @@ class MangaSwat : MangaThemesia(
|
|||
|
||||
override fun searchMangaNextPageSelector() = "a[rel=next]"
|
||||
|
||||
override val seriesTitleSelector = "h1[itemprop=headline]"
|
||||
override val seriesArtistSelector = "span:contains(الناشر) i"
|
||||
override val seriesAuthorSelector = "span:contains(المؤلف) i"
|
||||
override val seriesGenreSelector = "span:contains(التصنيف) a, .mgen a"
|
||||
|
@ -113,8 +117,6 @@ class MangaSwat : MangaThemesia(
|
|||
}
|
||||
}
|
||||
screen.addPreference(baseUrlPref)
|
||||
|
||||
super.setupPreferenceScreen(screen)
|
||||
}
|
||||
|
||||
private fun getPrefBaseUrl(): String = preferences.getString(BASE_URL_PREF, defaultBaseUrl)!!
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
ext {
|
||||
extName = 'RE Manga (Arabic)'
|
||||
extClass = '.REManga'
|
||||
extVersionCode = 2
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 8.1 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 25 KiB |
|
@ -0,0 +1,258 @@
|
|||
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"))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
ext {
|
||||
extName = 'StellarSaber'
|
||||
extClass = '.StellarSaber'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://stellarsaber.pro'
|
||||
overrideVersionCode = 0
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 8.0 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 20 KiB |
|
@ -0,0 +1,12 @@
|
|||
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")),
|
||||
)
|
|
@ -0,0 +1,7 @@
|
|||
ext {
|
||||
extName = 'Alandal'
|
||||
extClass = '.Alandal'
|
||||
extVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 8.3 KiB |
After Width: | Height: | Size: 11 KiB |
|
@ -0,0 +1,189 @@
|
|||
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())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
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>,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
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"),
|
||||
),
|
||||
)
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Anchira'
|
||||
extClass = '.Anchira'
|
||||
extVersionCode = 8
|
||||
extVersionCode = 10
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import android.content.SharedPreferences
|
|||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
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.prepareTags
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
|
@ -23,6 +24,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
|||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
|
@ -33,6 +35,8 @@ import uy.kohesive.injekt.Injekt
|
|||
import uy.kohesive.injekt.api.get
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.min
|
||||
|
||||
class Anchira : HttpSource(), ConfigurableSource {
|
||||
override val name = "Anchira"
|
||||
|
@ -109,6 +113,23 @@ class Anchira : HttpSource(), ConfigurableSource {
|
|||
fetchMangaDetails(manga).map {
|
||||
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 {
|
||||
// regular filtering without text search
|
||||
client.newCall(searchMangaRequest(page, query, filters))
|
||||
|
@ -116,108 +137,171 @@ class Anchira : HttpSource(), ConfigurableSource {
|
|||
.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()
|
||||
|
||||
url.addQueryParameter("page", page.toString())
|
||||
if (trendingFilter?.state == true) {
|
||||
val interval = when (sortTrendingFilter?.state) {
|
||||
1 -> "3"
|
||||
else -> ""
|
||||
}
|
||||
|
||||
if (query.isNotBlank()) {
|
||||
url.addQueryParameter("s", query)
|
||||
}
|
||||
if (interval.isNotBlank()) url.setQueryParameter("interval", interval)
|
||||
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is CategoryGroup -> {
|
||||
var sum = 0
|
||||
url = url.toString().replace("library", "trending").toHttpUrl()
|
||||
.newBuilder()
|
||||
} else {
|
||||
if (query.isNotBlank()) {
|
||||
url.setQueryParameter("s", query)
|
||||
}
|
||||
|
||||
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
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is CategoryGroup -> {
|
||||
var sum = 0
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
if (sum > 0) url.addQueryParameter("cat", sum.toString())
|
||||
else -> {}
|
||||
}
|
||||
|
||||
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 -> {}
|
||||
}
|
||||
}
|
||||
|
||||
return GET(url.build(), headers)
|
||||
if (page > 1) {
|
||||
url.setQueryParameter("page", page.toString())
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response) = latestUpdatesParse(response)
|
||||
|
||||
// Details
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga) =
|
||||
GET("$libraryUrl/${getPathFromUrl(manga.url)}", headers)
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val data = json.decodeFromString<Entry>(response.body.string())
|
||||
|
||||
return SManga.create().apply {
|
||||
url = "/g/${data.id}/${data.key}"
|
||||
title = data.title
|
||||
thumbnail_url =
|
||||
"$cdnUrl/${data.id}/${data.key}/b/${data.thumbnailIndex + 1}"
|
||||
artist = data.tags.filter { it.namespace == 1 }.joinToString(", ") { it.name }
|
||||
author = data.tags.filter { it.namespace == 2 }.joinToString(", ") { it.name }
|
||||
genre = prepareTags(data.tags, preferences.useTagGrouping)
|
||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
||||
status = SManga.COMPLETED
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
return if (manga.url.startsWith("?")) {
|
||||
GET(libraryUrl + manga.url, headers)
|
||||
} else {
|
||||
GET("$libraryUrl/${getPathFromUrl(manga.url)}", headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMangaUrl(manga: SManga) = if (preferences.openSource) {
|
||||
val id = manga.url.split("/").reversed()[1].toInt()
|
||||
anchiraData.find { it.id == id }?.url ?: "$baseUrl${manga.url}"
|
||||
} else {
|
||||
"$baseUrl${manga.url}"
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
return if (response.request.url.pathSegments.count() == libraryUrl.toHttpUrl().pathSegments.count()) {
|
||||
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 {
|
||||
url = "/g/${data.id}/${data.key}"
|
||||
title = data.title
|
||||
thumbnail_url =
|
||||
"$cdnUrl/${data.id}/${data.key}/b/${data.thumbnailIndex + 1}"
|
||||
artist = data.tags.filter { it.namespace == 1 }.joinToString(", ") { it.name }
|
||||
author = data.tags.filter { it.namespace == 2 }.joinToString(", ") { it.name }
|
||||
genre = prepareTags(data.tags, preferences.useTagGrouping)
|
||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
||||
status = SManga.COMPLETED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMangaUrl(manga: SManga) =
|
||||
if (preferences.openSource && !manga.url.startsWith("?")) {
|
||||
val id = manga.url.split("/").reversed()[1].toInt()
|
||||
anchiraData.find { it.id == id }?.url ?: "$baseUrl${manga.url}"
|
||||
} else {
|
||||
"$baseUrl${manga.url}"
|
||||
}
|
||||
|
||||
// Chapter
|
||||
|
||||
override fun chapterListRequest(manga: SManga) =
|
||||
GET("$libraryUrl/${getPathFromUrl(manga.url)}", headers)
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
return if (manga.url.startsWith("?")) {
|
||||
GET(libraryUrl + manga.url, headers)
|
||||
} else {
|
||||
GET("$libraryUrl/${getPathFromUrl(manga.url)}", headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val data = json.decodeFromString<Entry>(response.body.string())
|
||||
|
||||
return listOf(
|
||||
SChapter.create().apply {
|
||||
url = "/g/${data.id}/${data.key}"
|
||||
name = "Chapter"
|
||||
date_upload = data.publishedAt * 1000
|
||||
chapter_number = 1f
|
||||
},
|
||||
)
|
||||
val chapterList = mutableListOf<SChapter>()
|
||||
if (response.request.url.pathSegments.count() == libraryUrl.toHttpUrl().pathSegments.count()) {
|
||||
var results = json.decodeFromString<LibraryResponse>(response.body.string())
|
||||
val pages = min(5, ceil((results.total.toFloat() / results.limit)).toInt())
|
||||
for (page in 1..pages) {
|
||||
results.entries.forEach { data ->
|
||||
chapterList.add(
|
||||
createChapter(data, response, anchiraData),
|
||||
)
|
||||
}
|
||||
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)}"
|
||||
|
@ -278,14 +362,16 @@ class Anchira : HttpSource(), ConfigurableSource {
|
|||
val openSourcePref = SwitchPreferenceCompat(screen.context).apply {
|
||||
key = OPEN_SOURCE_PREF
|
||||
title = "Open source website in WebView"
|
||||
summary = "Enable to open the original source website of the gallery (if available) instead of Anchira."
|
||||
summary =
|
||||
"Enable to open the original source website of the gallery (if available) instead of Anchira."
|
||||
setDefaultValue(false)
|
||||
}
|
||||
|
||||
val useTagGrouping = SwitchPreferenceCompat(screen.context).apply {
|
||||
key = USE_TAG_GROUPING
|
||||
title = "Group tags"
|
||||
summary = "Enable to group tags together by artist, circle, parody, magazine and general tags"
|
||||
summary =
|
||||
"Enable to group tags together by artist, circle, parody, magazine and general tags"
|
||||
setDefaultValue(false)
|
||||
}
|
||||
|
||||
|
@ -298,6 +384,10 @@ class Anchira : HttpSource(), ConfigurableSource {
|
|||
CategoryGroup(),
|
||||
SortFilter(),
|
||||
FavoritesFilter(),
|
||||
Filter.Separator(),
|
||||
Filter.Header("Others are ignored if trending only"),
|
||||
TrendingFilter(),
|
||||
SortTrendingFilter(),
|
||||
)
|
||||
|
||||
private class CategoryFilter(name: String) : Filter.CheckBox(name, false)
|
||||
|
@ -317,6 +407,18 @@ class Anchira : HttpSource(), ConfigurableSource {
|
|||
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
|
||||
get() = getString(IMAGE_QUALITY_PREF, "b")!!
|
||||
|
||||
|
@ -362,8 +464,11 @@ class Anchira : HttpSource(), ConfigurableSource {
|
|||
.use { json.decodeFromStream<List<EntryKey>>(it.body.byteStream()) }
|
||||
}
|
||||
|
||||
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
|
||||
|
||||
companion object {
|
||||
const val SLUG_SEARCH_PREFIX = "id:"
|
||||
const val SLUG_BUNDLE_PREFIX = "bundle:"
|
||||
private const val IMAGE_QUALITY_PREF = "image_quality"
|
||||
private const val OPEN_SOURCE_PREF = "use_manga_source"
|
||||
private const val USE_TAG_GROUPING = "use_tag_grouping"
|
||||
|
@ -371,3 +476,5 @@ class Anchira : HttpSource(), ConfigurableSource {
|
|||
"https://gist.githubusercontent.com/LetrixZ/2b559cc5829d1c221c701e02ecd81411/raw/data-v5.json"
|
||||
}
|
||||
}
|
||||
|
||||
val CHAPTER_SUFFIX_RE = Regex("(?<!20\\d\\d-)\\b[\\d.]{1,4}$")
|
||||
|
|
|
@ -3,15 +3,6 @@ package eu.kanade.tachiyomi.extension.en.anchira
|
|||
import kotlinx.serialization.SerialName
|
||||
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
|
||||
data class Tag(
|
||||
var name: String,
|
||||
|
@ -20,7 +11,7 @@ data class Tag(
|
|||
|
||||
@Serializable
|
||||
data class LibraryResponse(
|
||||
val entries: List<ListEntry> = emptyList(),
|
||||
val entries: List<Entry> = emptyList(),
|
||||
val total: Int,
|
||||
val page: Int,
|
||||
val limit: Int,
|
||||
|
@ -30,11 +21,12 @@ data class LibraryResponse(
|
|||
data class Entry(
|
||||
val id: Int,
|
||||
val key: String,
|
||||
@SerialName("published_at") val publishedAt: Long,
|
||||
@SerialName("published_at") val publishedAt: Long = 0L,
|
||||
val title: String,
|
||||
@SerialName("thumb_index") val thumbnailIndex: Int,
|
||||
@SerialName("thumb_index") val thumbnailIndex: Int = 1,
|
||||
val tags: List<Tag> = emptyList(),
|
||||
val url: String? = null,
|
||||
val pages: Int = 1,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
package eu.kanade.tachiyomi.extension.en.anchira
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import okhttp3.Response
|
||||
import java.util.Locale
|
||||
|
||||
object AnchiraHelper {
|
||||
fun getPathFromUrl(url: String) = "${url.split("/").reversed()[1]}/${url.split("/").last()}"
|
||||
|
||||
|
@ -25,4 +29,31 @@ object AnchiraHelper {
|
|||
}
|
||||
}
|
||||
.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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,107 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import androidx.preference.PreferenceScreen
|
|||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
|
@ -26,12 +27,14 @@ import java.text.SimpleDateFormat
|
|||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class AsuraScans : MangaThemesia(
|
||||
"Asura Scans",
|
||||
"https://asuratoon.com",
|
||||
"en",
|
||||
dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.US),
|
||||
) {
|
||||
class AsuraScans :
|
||||
MangaThemesia(
|
||||
"Asura Scans",
|
||||
"https://asuratoon.com",
|
||||
"en",
|
||||
dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.US),
|
||||
),
|
||||
ConfigurableSource {
|
||||
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
|
@ -281,8 +284,6 @@ class AsuraScans : MangaThemesia(
|
|||
summary = PREF_PERM_MANGA_URL_SUMMARY
|
||||
setDefaultValue(true)
|
||||
}.also(screen::addPreference)
|
||||
|
||||
super.setupPreferenceScreen(screen)
|
||||
}
|
||||
|
||||
private val SharedPreferences.permaUrlPref
|
||||
|
|
|
@ -8,3 +8,7 @@ ext {
|
|||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib:randomua"))
|
||||
}
|
||||
|
|
|
@ -2,11 +2,14 @@ package eu.kanade.tachiyomi.extension.en.constellarscans
|
|||
|
||||
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.multisrc.mangathemesia.MangaThemesia
|
||||
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.SChapter
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
|
@ -19,22 +22,29 @@ import okhttp3.Request
|
|||
import org.jsoup.nodes.Document
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ConstellarScans : MangaThemesia("Constellar Scans", "https://constellarcomic.com", "en") {
|
||||
class ConstellarScans :
|
||||
MangaThemesia(
|
||||
"Constellar Scans",
|
||||
"https://constellarcomic.com",
|
||||
"en",
|
||||
),
|
||||
ConfigurableSource {
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
addRandomUAPreferenceToScreen(screen)
|
||||
}
|
||||
|
||||
override val client: OkHttpClient by lazy {
|
||||
network.cloudflareClient.newBuilder()
|
||||
.setRandomUserAgent(
|
||||
preferences.getPrefUAType(),
|
||||
preferences.getPrefCustomUA(),
|
||||
)
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.rateLimit(1, 1)
|
||||
.build()
|
||||
}
|
||||
|
@ -61,6 +71,8 @@ class ConstellarScans : MangaThemesia("Constellar Scans", "https://constellarcom
|
|||
.build()
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
countViews(document)
|
||||
|
||||
val html = document.toString()
|
||||
if (!html.contains("ts_rea_der_._run(\"")) {
|
||||
return super.pageListParse(document)
|
||||
|
@ -80,7 +92,6 @@ class ConstellarScans : MangaThemesia("Constellar Scans", "https://constellarcom
|
|||
}
|
||||
.joinToString("")
|
||||
|
||||
countViews(document)
|
||||
return json.parseToJsonElement(tsReaderRawData).jsonObject["sources"]!!.jsonArray[0].jsonObject["images"]!!.jsonArray.mapIndexed { idx, it ->
|
||||
Page(idx, imageUrl = it.jsonPrimitive.content)
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 18 KiB |
|
@ -1,9 +0,0 @@
|
|||
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"
|
||||
}
|
|
@ -9,6 +9,7 @@ import androidx.preference.PreferenceScreen
|
|||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
|
@ -25,12 +26,14 @@ import uy.kohesive.injekt.Injekt
|
|||
import uy.kohesive.injekt.api.get
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
class FlameComics : MangaThemesia(
|
||||
"Flame Comics",
|
||||
"https://flamecomics.com",
|
||||
"en",
|
||||
mangaUrlDirectory = "/series",
|
||||
) {
|
||||
class FlameComics :
|
||||
MangaThemesia(
|
||||
"Flame Comics",
|
||||
"https://flamecomics.com",
|
||||
"en",
|
||||
mangaUrlDirectory = "/series",
|
||||
),
|
||||
ConfigurableSource {
|
||||
|
||||
// Flame Scans -> Flame Comics
|
||||
override val id = 6350607071566689772
|
||||
|
|
|
@ -2,8 +2,8 @@ ext {
|
|||
extName = 'Infernal Void Scans'
|
||||
extClass = '.InfernalVoidScans'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://void-scans.com'
|
||||
overrideVersionCode = 5
|
||||
baseUrl = 'https://hivescans.com'
|
||||
overrideVersionCode = 6
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -2,6 +2,6 @@ package eu.kanade.tachiyomi.extension.en.infernalvoidscans
|
|||
|
||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||
|
||||
class InfernalVoidScans : MangaThemesia("Infernal Void Scans", "https://void-scans.com", "en") {
|
||||
class InfernalVoidScans : MangaThemesia("Infernal Void Scans", "https://hivescans.com", "en") {
|
||||
override val pageSelector = "div#readerarea > p > img"
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.preference.PreferenceScreen
|
|||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
|
@ -20,7 +21,15 @@ import uy.kohesive.injekt.Injekt
|
|||
import uy.kohesive.injekt.api.get
|
||||
import java.io.IOException
|
||||
|
||||
class LuminousScans : MangaThemesia("Luminous Scans", "https://lumitoon.com", "en", mangaUrlDirectory = "/series") {
|
||||
class LuminousScans :
|
||||
MangaThemesia(
|
||||
"Luminous Scans",
|
||||
"https://lumitoon.com",
|
||||
"en",
|
||||
mangaUrlDirectory = "/series",
|
||||
),
|
||||
ConfigurableSource {
|
||||
|
||||
override val client = super.client.newBuilder()
|
||||
.addInterceptor(::urlChangeInterceptor)
|
||||
.rateLimit(2)
|
||||
|
@ -201,8 +210,6 @@ class LuminousScans : MangaThemesia("Luminous Scans", "https://lumitoon.com", "e
|
|||
summary = PREF_PERM_MANGA_URL_SUMMARY
|
||||
setDefaultValue(true)
|
||||
}.also(screen::addPreference)
|
||||
|
||||
super.setupPreferenceScreen(screen)
|
||||
}
|
||||
|
||||
private val SharedPreferences.permaUrlPref
|
||||
|
|
|
@ -33,19 +33,29 @@ class LunarScans : MangaThemesia(
|
|||
val filters = mutableListOf<Filter<*>>(
|
||||
Filter.Header("Note: Can't be used with text search!"),
|
||||
Filter.Separator(),
|
||||
StatusFilter(),
|
||||
TypeFilter(),
|
||||
OrderByFilter(),
|
||||
Filter.Header("Genre exclusion is not available for all sources"),
|
||||
GenreListFilter(getGenreList()),
|
||||
StatusFilter(intl["status_filter_title"], statusOptions),
|
||||
TypeFilter(intl["type_filter_title"], typeFilterOptions),
|
||||
OrderByFilter(intl["order_by_filter_title"], orderByFilterOptions),
|
||||
)
|
||||
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) {
|
||||
filters.addAll(
|
||||
mutableListOf<Filter<*>>(
|
||||
Filter.Separator(),
|
||||
Filter.Header("NOTE: Can't be used with other filter!"),
|
||||
Filter.Header("$name Project List page"),
|
||||
ProjectFilter(),
|
||||
Filter.Header(intl["project_filter_warning"]),
|
||||
Filter.Header(intl.format("project_filter_name", name)),
|
||||
ProjectFilter(intl["project_filter_title"], projectFilterOptions),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
ext {
|
||||
extName = '247Manga'
|
||||
extClass = '.Manga247'
|
||||
themePkg = 'madara'
|
||||
baseUrl = 'https://247manga.com'
|
||||
overrideVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 17 KiB |
|
@ -1,5 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.en.manga247
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||
|
||||
class Manga247 : Madara("247Manga", "https://247manga.com", "en")
|
|
@ -1,9 +1,9 @@
|
|||
ext {
|
||||
extName = 'Manga Galaxy'
|
||||
extClass = '.MangaGalaxy'
|
||||
themePkg = 'madara'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://mangagalaxy.me'
|
||||
overrideVersionCode = 1
|
||||
overrideVersionCode = 9
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
package eu.kanade.tachiyomi.extension.en.mangagalaxy
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||
|
||||
class MangaGalaxy : Madara(
|
||||
class MangaGalaxy : MangaThemesia(
|
||||
"Manga Galaxy",
|
||||
"https://mangagalaxy.me",
|
||||
"en",
|
||||
dateFormat = SimpleDateFormat("MM/dd/yyyy", Locale.US),
|
||||
mangaUrlDirectory = "/series",
|
||||
) {
|
||||
override val mangaDetailsSelectorStatus = "div.summary-heading:contains(status) + div.summary-content"
|
||||
override val mangaDetailsSelectorDescription = "div.summary-heading:contains(Summary) + div"
|
||||
// moved from Madara to MangaThemesia
|
||||
override val versionId = 2
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
ext {
|
||||
extName = 'MangaMonks'
|
||||
extClass = '.MangaMonks'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 8.5 KiB |
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,267 @@
|
|||
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)
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
ext {
|
||||
extName = 'MangaOwl.To'
|
||||
extClass = '.MangaOwlToFactory'
|
||||
extVersionCode = 2
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 9.3 KiB |
|
@ -0,0 +1,168 @@
|
|||
package eu.kanade.tachiyomi.extension.en.mangaowlto
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class MangaOwlTo(
|
||||
private val collection: String,
|
||||
extraName: String,
|
||||
private val genresList: List<Genre>,
|
||||
) : ConfigurableSource, HttpSource() {
|
||||
override val name: String = "MangaOwl.To $extraName"
|
||||
override val lang = "en"
|
||||
override val supportsLatest = true
|
||||
|
||||
private val preferences: SharedPreferences =
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
|
||||
private val defaultDomain: String =
|
||||
preferences.getString(MIRROR_PREF_KEY, MIRROR_PREF_DEFAULT_VALUE)!!
|
||||
|
||||
override val baseUrl = "https://$defaultDomain"
|
||||
|
||||
private val apiUrl = "https://api.$defaultDomain/v1"
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val mirrorPref = ListPreference(screen.context).apply {
|
||||
key = MIRROR_PREF_KEY
|
||||
title = "Mirror (Requires Restart)"
|
||||
entries = MIRROR_PREF_ENTRIES
|
||||
entryValues = MIRROR_PREF_ENTRY_VALUES
|
||||
setDefaultValue(MIRROR_PREF_DEFAULT_VALUE)
|
||||
summary = "%s"
|
||||
}
|
||||
screen.addPreference(mirrorPref)
|
||||
}
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
GET("$apiUrl/stories?type=$collection&ordering=-view_count&page=$page".toHttpUrl(), headers)
|
||||
|
||||
override fun popularMangaParse(response: Response) =
|
||||
json.decodeFromString<MangaOwlToStories>(response.body.string()).toMangasPage()
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
GET("$apiUrl/stories?type=$collection&ordering=-modified_at&page=$page".toHttpUrl(), headers)
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
||||
|
||||
// Search
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
return if (query.isNotEmpty() || filters.isEmpty()) {
|
||||
// Search won't work together with filter
|
||||
val url = "$apiUrl/search".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("q", query)
|
||||
.addQueryParameter("page", page.toString())
|
||||
.build()
|
||||
GET(url, headers)
|
||||
} else {
|
||||
val url = "$apiUrl/stories?type=$collection".toHttpUrl().newBuilder()
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is SortFilter -> if (!filter.toUriPart().isNullOrEmpty()) {
|
||||
url.addQueryParameter("ordering", filter.toUriPart())
|
||||
}
|
||||
is StatusFilter -> if (!filter.toUriPart().isNullOrEmpty()) {
|
||||
url.addQueryParameter("status", filter.toUriPart())
|
||||
}
|
||||
is GenresFilter ->
|
||||
filter.state
|
||||
.filter { it.state }
|
||||
.forEach { url.addQueryParameter("genres", it.uriPart) }
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
url.addQueryParameter("page", page.toString())
|
||||
GET(url.build(), headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response) = popularMangaParse(response)
|
||||
|
||||
// Manga summary page
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
return GET("$apiUrl/stories/${manga.url}", headers)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response) =
|
||||
json.decodeFromString<MangaOwlToStory>(response.body.string()).toSManga()
|
||||
|
||||
override fun getMangaUrl(manga: SManga): String {
|
||||
return "$baseUrl/comic/${manga.url}"
|
||||
}
|
||||
|
||||
// Chapters
|
||||
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
|
||||
|
||||
override fun chapterListParse(response: Response) =
|
||||
json.decodeFromString<MangaOwlToStory>(response.body.string()).chaptersList
|
||||
|
||||
override fun getChapterUrl(chapter: SChapter): String {
|
||||
return "$baseUrl${chapter.url}"
|
||||
}
|
||||
|
||||
// Pages
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val id = chapter.url.substringAfterLast("/")
|
||||
return GET("$apiUrl/chapters/$id/images?page_size=1000", headers)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response) =
|
||||
json.decodeFromString<MangaOwlToChapterPages>(response.body.string()).toPages()
|
||||
|
||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
||||
|
||||
// Filters
|
||||
override fun getFilterList() = FilterList(
|
||||
Filter.Header("Search query won't use filters"),
|
||||
GenresFilter(genresList),
|
||||
StatusFilter(),
|
||||
SortFilter(),
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val MIRROR_PREF_KEY = "MIRROR"
|
||||
private val MIRROR_PREF_ENTRIES get() = arrayOf(
|
||||
"mangaowl.to",
|
||||
"mangabuddy.to",
|
||||
"mangafreak.to",
|
||||
"toonily.to",
|
||||
"manganato.so",
|
||||
"mangakakalot.so", // Redirected from mangago.to
|
||||
)
|
||||
private val MIRROR_PREF_ENTRY_VALUES get() = arrayOf(
|
||||
"mangaowl.to",
|
||||
"mangabuddy.to",
|
||||
"mangafreak.to",
|
||||
"toonily.to",
|
||||
"manganato.so",
|
||||
"mangago.to", // API for domain mangakakalot.so
|
||||
)
|
||||
private val MIRROR_PREF_DEFAULT_VALUE get() = MIRROR_PREF_ENTRY_VALUES[0]
|
||||
|
||||
const val ONGOING = "ongoing"
|
||||
const val COMPLETED = "completed"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
package eu.kanade.tachiyomi.extension.en.mangaowlto
|
||||
|
||||
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.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
@Serializable
|
||||
class MangaOwlToStories(
|
||||
private val next: String?,
|
||||
private val results: List<MangaOwlToStory>,
|
||||
) {
|
||||
fun toMangasPage() = MangasPage(
|
||||
mangas = results.map { it.toSManga() },
|
||||
hasNextPage = !next.isNullOrEmpty(),
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class MangaOwlToStory(
|
||||
private val name: String,
|
||||
private val slug: String,
|
||||
@SerialName("status") private val titleStatus: String?, // ongoing & completed
|
||||
@SerialName("thumbnail") private val thumbnailUrl: String,
|
||||
@SerialName("al_name") private val altName: String?,
|
||||
private val rating: Float?,
|
||||
@SerialName("view_count") private val views: Int,
|
||||
private val description: String?,
|
||||
private val genres: List<MangaOwlToGenre> = emptyList(),
|
||||
private val authors: List<MangaOwlToAuthor> = emptyList(),
|
||||
private val chapters: List<MangaOwlToChapter> = emptyList(),
|
||||
) {
|
||||
private val fullDescription: String
|
||||
get() = buildString {
|
||||
append(description)
|
||||
altName?.let { append("\n\n $it") }
|
||||
append("\n\nRating: $rating")
|
||||
append("\nViews: $views")
|
||||
}
|
||||
|
||||
val chaptersList: List<SChapter>
|
||||
get() = chapters.reversed().map { it.toSChapter(slug) }
|
||||
|
||||
fun toSManga(): SManga = SManga.create().apply {
|
||||
title = name
|
||||
author = authors.joinToString { it.name }
|
||||
description = fullDescription.trim()
|
||||
genre = genres.joinToString { it.name }
|
||||
status = when (titleStatus) {
|
||||
MangaOwlTo.ONGOING -> SManga.ONGOING
|
||||
MangaOwlTo.COMPLETED -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
thumbnail_url = thumbnailUrl
|
||||
url = slug
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class MangaOwlToGenre(
|
||||
val name: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class MangaOwlToAuthor(
|
||||
val name: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class MangaOwlToChapter(
|
||||
private val id: Int,
|
||||
@SerialName("name") private val title: String,
|
||||
@SerialName("created_at") private val createdAt: String,
|
||||
) {
|
||||
fun toSChapter(slug: String): SChapter = SChapter.create().apply {
|
||||
name = title
|
||||
date_upload = parseDate()
|
||||
url = "/reading/$slug/$id"
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.US)
|
||||
}
|
||||
|
||||
private fun parseDate(): Long = try {
|
||||
dateFormat.parse(createdAt)!!.time
|
||||
} catch (_: ParseException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class MangaOwlToChapterPages(
|
||||
@SerialName("results") private val pages: List<MangaOwlToPage> = emptyList(),
|
||||
) {
|
||||
fun toPages() =
|
||||
pages.mapIndexed { idx, page ->
|
||||
Page(
|
||||
index = idx,
|
||||
imageUrl = page.imageUrl,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class MangaOwlToPage(
|
||||
@SerialName("image") val imageUrl: String,
|
||||
)
|
|
@ -0,0 +1,103 @@
|
|||
package eu.kanade.tachiyomi.extension.en.mangaowlto
|
||||
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
class MangaOwlToFactory : SourceFactory {
|
||||
override fun createSources(): List<Source> = listOf(
|
||||
MangaOwlTo(
|
||||
collection = "manga",
|
||||
extraName = "Manga",
|
||||
genresList = listOf(
|
||||
Genre("Action", "13"),
|
||||
Genre("Adult", "29"),
|
||||
Genre("Adventure", "31"),
|
||||
Genre("Comedy", "14"),
|
||||
Genre("Cooking", "37"),
|
||||
Genre("Doujinshi", "39"),
|
||||
Genre("Drama", "17"),
|
||||
Genre("Ecchi", "15"),
|
||||
Genre("Fantasy", "16"),
|
||||
Genre("Gender bender", "35"),
|
||||
Genre("Harem", "27"),
|
||||
Genre("Historical", "18"),
|
||||
Genre("Horror", "28"),
|
||||
Genre("Isekai", "40"),
|
||||
Genre("Josei", "23"),
|
||||
Genre("Manhua", "38"),
|
||||
Genre("Manhwa", "36"),
|
||||
Genre("Martial arts", "30"),
|
||||
Genre("Mature", "20"),
|
||||
Genre("Mecha", "33"),
|
||||
Genre("Medical", "41"),
|
||||
Genre("Mystery", "7"),
|
||||
Genre("One shot", "1"),
|
||||
Genre("Psychological", "8"),
|
||||
Genre("Romance", "10"),
|
||||
Genre("School life", "3"),
|
||||
Genre("Sci fi", "19"),
|
||||
Genre("Seinen", "25"),
|
||||
Genre("Shoujo ai", "5"),
|
||||
Genre("Shoujo", "12"),
|
||||
Genre("Shounen ai", "4"),
|
||||
Genre("Shounen", "9"),
|
||||
Genre("Slice of life", "26"),
|
||||
Genre("Smut", "32"),
|
||||
Genre("Sports", "21"),
|
||||
Genre("Supernatural", "24"),
|
||||
Genre("Tragedy", "22"),
|
||||
Genre("Webtoons", "34"),
|
||||
Genre("Yaoi", "2"),
|
||||
Genre("Yuri", "6"),
|
||||
),
|
||||
),
|
||||
MangaOwlTo(
|
||||
collection = "comic",
|
||||
extraName = "Comic",
|
||||
genresList = listOf(
|
||||
Genre("215 Ink", "189"),
|
||||
Genre("Ablaze", "98"),
|
||||
Genre("Action Lab", "204"),
|
||||
Genre("Aftershock Comics", "68"),
|
||||
Genre("American Mythology", "130"),
|
||||
Genre("Antartic Press", "261"),
|
||||
Genre("Archie", "178"),
|
||||
Genre("Aspen", "487"),
|
||||
Genre("Avatar Press", "177"),
|
||||
Genre("Black Mask", "107"),
|
||||
Genre("Boom Studios", "65"),
|
||||
Genre("Comics Experience", "159"),
|
||||
Genre("Dark Horse", "92"),
|
||||
Genre("DC Comics", "133"),
|
||||
Genre("Devils Due", "290"),
|
||||
Genre("Dynamite", "173"),
|
||||
Genre("Europe Comics", "67"),
|
||||
Genre("Heavy Metal", "55"),
|
||||
Genre("Humanoids", "85"),
|
||||
Genre("IDW", "110"),
|
||||
Genre("Image Comics", "60"),
|
||||
Genre("Inverse", "384"),
|
||||
Genre("Lion Forge", "162"),
|
||||
Genre("Mad Cave", "96"),
|
||||
Genre("MAD", "485"),
|
||||
Genre("Magnetic Press", "114"),
|
||||
Genre("Marvel Comics", "45"),
|
||||
Genre("One Shots & TPBs", "136"),
|
||||
Genre("Oni Press", "338"),
|
||||
Genre("Rebellion", "50"),
|
||||
Genre("Red 5", "88"),
|
||||
Genre("SAF Comics", "378"),
|
||||
Genre("Soleil", "156"),
|
||||
Genre("Source Point Press", "57"),
|
||||
Genre("Space Goat Productions", "421"),
|
||||
Genre("Top Cow", "138"),
|
||||
Genre("Top Shelf", "101"),
|
||||
Genre("Upshot", "396"),
|
||||
Genre("Valiant", "87"),
|
||||
Genre("Vault", "360"),
|
||||
Genre("Vertigo", "457"),
|
||||
Genre("Zenescope", "119"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package eu.kanade.tachiyomi.extension.en.mangaowlto
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
class Genre(val name: String, val uriPart: String)
|
||||
|
||||
class GenreCheckBox(name: String, val uriPart: String) : Filter.CheckBox(name)
|
||||
|
||||
class GenresFilter(genres: List<Genre>) :
|
||||
Filter.Group<GenreCheckBox>("Genres", genres.map { GenreCheckBox(it.name, it.uriPart) })
|
||||
|
||||
class SortFilter : UriPartFilter(
|
||||
"Sort by",
|
||||
arrayOf(
|
||||
Pair("Default", null),
|
||||
Pair("Most view", "-view_count"),
|
||||
Pair("Added", "created_at"),
|
||||
Pair("Last update", "-modified_at"),
|
||||
Pair("High rating", "rating"),
|
||||
),
|
||||
)
|
||||
|
||||
class StatusFilter : UriPartFilter(
|
||||
"Status",
|
||||
arrayOf(
|
||||
Pair("Any", null),
|
||||
Pair("Completed", MangaOwlTo.COMPLETED),
|
||||
Pair("Ongoing", MangaOwlTo.ONGOING),
|
||||
),
|
||||
)
|
||||
|
||||
open class UriPartFilter(displayName: String, private val pairs: Array<Pair<String, String?>>) :
|
||||
Filter.Select<String>(displayName, pairs.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = pairs[state].second
|
||||
}
|