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": [
|
"extends": [
|
||||||
"config:base"
|
"config:recommended"
|
||||||
],
|
],
|
||||||
|
"schedule": ["on sunday"],
|
||||||
"includePaths": [
|
"includePaths": [
|
||||||
|
"buildSrc/gradle/**",
|
||||||
|
"gradle/**",
|
||||||
".github/**"
|
".github/**"
|
||||||
|
],
|
||||||
|
"ignoreDeps": ["keiyoushi/issue-moderator-action"],
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"matchManagers": ["github-actions"],
|
||||||
|
"groupName": "{{manager}} dependencies"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchManagers": ["gradle"],
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchPackageNames": [
|
||||||
|
"com.android.tools.build:gradle",
|
||||||
|
"gradle"
|
||||||
|
],
|
||||||
|
"draftPR": true,
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ jobs:
|
||||||
echo ${{ secrets.SIGNING_KEY }} | base64 -d > signingkey.jks
|
echo ${{ secrets.SIGNING_KEY }} | base64 -d > signingkey.jks
|
||||||
|
|
||||||
- name: Set up Gradle
|
- name: Set up Gradle
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0
|
||||||
|
|
||||||
- name: Build extensions
|
- name: Build extensions
|
||||||
env:
|
env:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
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 {
|
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 {
|
dependencies {
|
||||||
compileOnly(libs.findBundle("common").get())
|
compileOnly(versionCatalogs.named("libs").findBundle("common").get())
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
max*)
|
max*)
|
||||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
# shellcheck disable=SC3045
|
# shellcheck disable=SC2039,SC3045
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
warn "Could not query maximum file descriptor limit"
|
warn "Could not query maximum file descriptor limit"
|
||||||
esac
|
esac
|
||||||
|
@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
'' | soft) :;; #(
|
'' | soft) :;; #(
|
||||||
*)
|
*)
|
||||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
# shellcheck disable=SC3045
|
# shellcheck disable=SC2039,SC3045
|
||||||
ulimit -n "$MAX_FD" ||
|
ulimit -n "$MAX_FD" ||
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
esac
|
esac
|
||||||
|
@ -202,11 +202,11 @@ fi
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command;
|
# Collect all arguments for the java command:
|
||||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
# shell script including quotes and variable substitutions, so put them in
|
# and any embedded shellness will be escaped.
|
||||||
# double quotes to make sure that they get re-expanded; and
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
# * put everything else in single quotes, so that it's not re-expanded.
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
|
|
@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation.
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
|
@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation.
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
plugins {
|
|
||||||
id("lib-multisrc")
|
|
||||||
}
|
|
||||||
|
|
||||||
baseVersionCode = 9
|
|
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 2
|
baseVersionCode = 3
|
||||||
|
|
|
@ -10,9 +10,6 @@ class BloggerDto(
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class BloggerFeedDto(
|
class BloggerFeedDto(
|
||||||
@SerialName("openSearch\$totalResults") val totalResults: BloggerTextDto,
|
|
||||||
@SerialName("openSearch\$startIndex") val startIndex: BloggerTextDto,
|
|
||||||
@SerialName("openSearch\$itemsPerPage") val itemsPerPage: BloggerTextDto,
|
|
||||||
val category: List<BloggerCategoryDto> = emptyList(),
|
val category: List<BloggerCategoryDto> = emptyList(),
|
||||||
val entry: List<BloggerFeedEntryDto> = emptyList(),
|
val entry: List<BloggerFeedEntryDto> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -65,7 +65,7 @@ abstract class GravureBlogger(
|
||||||
initialized = true
|
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)
|
return MangasPage(manga, hasNextPage)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 10
|
baseVersionCode = 11
|
||||||
|
|
|
@ -60,7 +60,7 @@ class KemonoPostDto(
|
||||||
if (file.path != null) add(KemonoAttachmentDto(file.name!!, file.path))
|
if (file.path != null) add(KemonoAttachmentDto(file.name!!, file.path))
|
||||||
addAll(attachments)
|
addAll(attachments)
|
||||||
}.filter {
|
}.filter {
|
||||||
when (it.name.substringAfterLast('.').lowercase()) {
|
when (it.path.substringAfterLast('.').lowercase()) {
|
||||||
"png", "jpg", "gif", "jpeg", "webp" -> true
|
"png", "jpg", "gif", "jpeg", "webp" -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
@ -91,6 +91,7 @@ class KemonoPostDto(
|
||||||
@Serializable
|
@Serializable
|
||||||
class KemonoFileDto(val name: String? = null, val path: String? = null)
|
class KemonoFileDto(val name: String? = null, val path: String? = null)
|
||||||
|
|
||||||
|
// name might have ".jpe" extension for JPEG, path might have ".m4v" extension for MP4
|
||||||
@Serializable
|
@Serializable
|
||||||
class KemonoAttachmentDto(val name: String, val path: String) {
|
class KemonoAttachmentDto(val name: String, val path: String) {
|
||||||
override fun toString() = "$path?f=$name"
|
override fun toString() = "$path?f=$name"
|
||||||
|
|
|
@ -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")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 28
|
baseVersionCode = 29
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:randomua"))
|
api(project(":lib:i18n"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,8 @@
|
||||||
package eu.kanade.tachiyomi.multisrc.mangathemesia
|
package eu.kanade.tachiyomi.multisrc.mangathemesia
|
||||||
|
|
||||||
import android.app.Application
|
import eu.kanade.tachiyomi.lib.i18n.Intl
|
||||||
import android.content.SharedPreferences
|
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
import eu.kanade.tachiyomi.lib.randomua.addRandomUAPreferenceToScreen
|
|
||||||
import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA
|
|
||||||
import eu.kanade.tachiyomi.lib.randomua.getPrefUAType
|
|
||||||
import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
@ -21,64 +14,56 @@ import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.jsonArray
|
import kotlinx.serialization.json.jsonArray
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import okhttp3.Call
|
||||||
|
import okhttp3.Callback
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import org.jsoup.select.Elements
|
import org.jsoup.select.Elements
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.lang.IllegalArgumentException
|
import java.io.IOException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
// Formerly WPMangaStream & WPMangaReader -> MangaThemesia
|
// Formerly WPMangaStream & WPMangaReader -> MangaThemesia
|
||||||
abstract class MangaThemesia(
|
abstract class MangaThemesia(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val baseUrl: String,
|
override val baseUrl: String,
|
||||||
override val lang: String,
|
final override val lang: String,
|
||||||
val mangaUrlDirectory: String = "/manga",
|
val mangaUrlDirectory: String = "/manga",
|
||||||
val dateFormat: SimpleDateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale.US),
|
val dateFormat: SimpleDateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale.US),
|
||||||
) : ParsedHttpSource(), ConfigurableSource {
|
) : ParsedHttpSource() {
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open val json: Json by injectLazy()
|
protected open val json: Json by injectLazy()
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
override val client: OkHttpClient by lazy {
|
override val client = network.cloudflareClient
|
||||||
network.cloudflareClient.newBuilder()
|
|
||||||
.setRandomUserAgent(
|
|
||||||
preferences.getPrefUAType(),
|
|
||||||
preferences.getPrefCustomUA(),
|
|
||||||
)
|
|
||||||
.connectTimeout(10, TimeUnit.SECONDS)
|
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
.set("Referer", "$baseUrl/")
|
.set("Referer", "$baseUrl/")
|
||||||
|
|
||||||
|
protected val intl = Intl(
|
||||||
|
language = lang,
|
||||||
|
baseLanguage = "en",
|
||||||
|
availableLanguages = setOf("en", "es"),
|
||||||
|
classLoader = javaClass.classLoader!!,
|
||||||
|
)
|
||||||
|
|
||||||
open val projectPageString = "/project"
|
open val projectPageString = "/project"
|
||||||
|
|
||||||
// Popular (Search with popular order and nothing else)
|
// Popular (Search with popular order and nothing else)
|
||||||
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", FilterList(OrderByFilter("popular")))
|
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", popularFilter)
|
||||||
override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
||||||
|
|
||||||
// Latest (Search with update order and nothing else)
|
// Latest (Search with update order and nothing else)
|
||||||
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", FilterList(OrderByFilter("update")))
|
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", latestFilter)
|
||||||
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
|
@ -166,22 +151,96 @@ abstract class MangaThemesia(
|
||||||
override fun searchMangaNextPageSelector() = "div.pagination .next, div.hpage .r"
|
override fun searchMangaNextPageSelector() = "div.pagination .next, div.hpage .r"
|
||||||
|
|
||||||
// Manga details
|
// Manga details
|
||||||
|
private fun selector(selector: String, contains: List<String>): String {
|
||||||
|
return contains.joinToString(", ") { selector.replace("%s", it) }
|
||||||
|
}
|
||||||
|
|
||||||
open val seriesDetailsSelector = "div.bigcontent, div.animefull, div.main-info, div.postbody"
|
open val seriesDetailsSelector = "div.bigcontent, div.animefull, div.main-info, div.postbody"
|
||||||
open val seriesTitleSelector = "h1.entry-title"
|
|
||||||
open val seriesArtistSelector = ".infotable tr:contains(artist) td:last-child, .tsinfo .imptdt:contains(artist) i, .fmed b:contains(artist)+span, span:contains(artist)"
|
open val seriesTitleSelector = "h1.entry-title, .ts-breadcrumb li:last-child span"
|
||||||
open val seriesAuthorSelector = ".infotable tr:contains(author) td:last-child, .tsinfo .imptdt:contains(author) i, .fmed b:contains(author)+span, span:contains(author)"
|
|
||||||
|
open val seriesArtistSelector = selector(
|
||||||
|
".infotable tr:contains(%s) td:last-child, .tsinfo .imptdt:contains(%s) i, .fmed b:contains(%s)+span, span:contains(%s)",
|
||||||
|
listOf(
|
||||||
|
"artist",
|
||||||
|
"Artiste",
|
||||||
|
"Artista",
|
||||||
|
"الرسام",
|
||||||
|
"الناشر",
|
||||||
|
"İllüstratör",
|
||||||
|
"Çizer",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
open val seriesAuthorSelector = selector(
|
||||||
|
".infotable tr:contains(%s) td:last-child, .tsinfo .imptdt:contains(%s) i, .fmed b:contains(%s)+span, span:contains(%s)",
|
||||||
|
listOf(
|
||||||
|
"Author",
|
||||||
|
"Auteur",
|
||||||
|
"autor",
|
||||||
|
"المؤلف",
|
||||||
|
"Mangaka",
|
||||||
|
"seniman",
|
||||||
|
"Pengarang",
|
||||||
|
"Yazar",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
open val seriesDescriptionSelector = ".desc, .entry-content[itemprop=description]"
|
open val seriesDescriptionSelector = ".desc, .entry-content[itemprop=description]"
|
||||||
open val seriesAltNameSelector = ".alternative, .wd-full:contains(alt) span, .alter, .seriestualt"
|
|
||||||
open val seriesGenreSelector = "div.gnr a, .mgen a, .seriestugenre a, span:contains(genre)"
|
open val seriesAltNameSelector = ".alternative, .wd-full:contains(alt) span, .alter, .seriestualt, " +
|
||||||
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\\=]"
|
selector(
|
||||||
open val seriesStatusSelector = ".infotable tr:contains(status) td:last-child, .tsinfo .imptdt:contains(status) i, .fmed b:contains(status)+span span:contains(status)"
|
".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 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 {
|
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||||
document.selectFirst(seriesDetailsSelector)?.let { seriesDetails ->
|
document.selectFirst(seriesDetailsSelector)?.let { seriesDetails ->
|
||||||
title = seriesDetails.selectFirst(seriesTitleSelector)?.text().orEmpty()
|
title = seriesDetails.selectFirst(seriesTitleSelector)!!.text()
|
||||||
artist = seriesDetails.selectFirst(seriesArtistSelector)?.ownText().removeEmptyPlaceholder()
|
artist = seriesDetails.selectFirst(seriesArtistSelector)?.ownText().removeEmptyPlaceholder()
|
||||||
author = seriesDetails.selectFirst(seriesAuthorSelector)?.ownText().removeEmptyPlaceholder()
|
author = seriesDetails.selectFirst(seriesAuthorSelector)?.ownText().removeEmptyPlaceholder()
|
||||||
description = seriesDetails.select(seriesDescriptionSelector).joinToString("\n") { it.text() }.trim()
|
description = seriesDetails.select(seriesDescriptionSelector).joinToString("\n") { it.text() }.trim()
|
||||||
|
@ -210,23 +269,42 @@ abstract class MangaThemesia(
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun String?.removeEmptyPlaceholder(): String? {
|
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 {
|
open fun String?.parseStatus(): Int {
|
||||||
this == null -> SManga.UNKNOWN
|
if (this == null) return SManga.UNKNOWN
|
||||||
listOf("ongoing", "publishing").any { this.contains(it, ignoreCase = true) } -> SManga.ONGOING
|
|
||||||
this.contains("hiatus", ignoreCase = true) -> SManga.ON_HIATUS
|
return when (this.lowercase().trim()) {
|
||||||
this.contains("completed", ignoreCase = true) -> SManga.COMPLETED
|
"مستمرة", "en curso", "ongoing", "on going", "ativo", "en cours",
|
||||||
listOf("dropped", "cancelled").any { this.contains(it, ignoreCase = true) } -> SManga.CANCELLED
|
"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
|
else -> SManga.UNKNOWN
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Chapter list
|
// Chapter list
|
||||||
override fun chapterListSelector() = "div.bxcl li, div.cl li, #chapterlist li, ul li:has(div.chbox):has(div.eph-num)"
|
override fun chapterListSelector() = "div.bxcl li, div.cl li, #chapterlist li, ul li:has(div.chbox):has(div.eph-num)"
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
|
|
||||||
|
countViews(document)
|
||||||
|
|
||||||
val chapters = document.select(chapterListSelector()).map { chapterFromElement(it) }
|
val chapters = document.select(chapterListSelector()).map { chapterFromElement(it) }
|
||||||
|
|
||||||
// Add timestamp to latest chapter, taken from "Updated On".
|
// Add timestamp to latest chapter, taken from "Updated On".
|
||||||
|
@ -238,8 +316,6 @@ abstract class MangaThemesia(
|
||||||
if (date.isNotEmpty()) chapters.first().date_upload = parseUpdatedOnDate(date)
|
if (date.isNotEmpty()) chapters.first().date_upload = parseUpdatedOnDate(date)
|
||||||
}
|
}
|
||||||
|
|
||||||
countViews(document)
|
|
||||||
|
|
||||||
return chapters
|
return chapters
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -267,13 +343,13 @@ abstract class MangaThemesia(
|
||||||
open val pageSelector = "div#readerarea img"
|
open val pageSelector = "div#readerarea img"
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
countViews(document)
|
||||||
|
|
||||||
val chapterUrl = document.location()
|
val chapterUrl = document.location()
|
||||||
val htmlPages = document.select(pageSelector)
|
val htmlPages = document.select(pageSelector)
|
||||||
.filterNot { it.imgAttr().isEmpty() }
|
.filterNot { it.imgAttr().isEmpty() }
|
||||||
.mapIndexed { i, img -> Page(i, chapterUrl, img.imgAttr()) }
|
.mapIndexed { i, img -> Page(i, chapterUrl, img.imgAttr()) }
|
||||||
|
|
||||||
countViews(document)
|
|
||||||
|
|
||||||
// Some sites also loads pages via javascript
|
// Some sites also loads pages via javascript
|
||||||
if (htmlPages.isNotEmpty()) { return htmlPages }
|
if (htmlPages.isNotEmpty()) { return htmlPages }
|
||||||
|
|
||||||
|
@ -320,8 +396,6 @@ abstract class MangaThemesia(
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val newHeaders = headersBuilder()
|
val newHeaders = headersBuilder()
|
||||||
.set("Content-Length", formBody.contentLength().toString())
|
|
||||||
.set("Content-Type", formBody.contentType().toString())
|
|
||||||
.set("Referer", document.location())
|
.set("Referer", document.location())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
@ -339,17 +413,22 @@ abstract class MangaThemesia(
|
||||||
}
|
}
|
||||||
|
|
||||||
val request = countViewsRequest(document) ?: return
|
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
|
// 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(
|
open class SelectFilter(
|
||||||
displayName: String,
|
displayName: String,
|
||||||
val vals: Array<Pair<String, String>>,
|
private val vals: Array<Pair<String, String>>,
|
||||||
defaultValue: String? = null,
|
defaultValue: String? = null,
|
||||||
) : Filter.Select<String>(
|
) : Filter.Select<String>(
|
||||||
displayName,
|
displayName,
|
||||||
|
@ -359,63 +438,91 @@ abstract class MangaThemesia(
|
||||||
fun selectedValue() = vals[state].second
|
fun selectedValue() = vals[state].second
|
||||||
}
|
}
|
||||||
|
|
||||||
protected class StatusFilter : SelectFilter(
|
protected class StatusFilter(
|
||||||
"Status",
|
name: String,
|
||||||
arrayOf(
|
options: Array<Pair<String, String>>,
|
||||||
Pair("All", ""),
|
) : SelectFilter(
|
||||||
Pair("Ongoing", "ongoing"),
|
name,
|
||||||
Pair("Completed", "completed"),
|
options,
|
||||||
Pair("Hiatus", "hiatus"),
|
|
||||||
Pair("Dropped", "dropped"),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
protected class TypeFilter : SelectFilter(
|
protected open val statusOptions = arrayOf(
|
||||||
"Type",
|
Pair(intl["status_filter_option_all"], ""),
|
||||||
arrayOf(
|
Pair(intl["status_filter_option_ongoing"], "ongoing"),
|
||||||
Pair("All", ""),
|
Pair(intl["status_filter_option_completed"], "completed"),
|
||||||
Pair("Manga", "Manga"),
|
Pair(intl["status_filter_option_hiatus"], "hiatus"),
|
||||||
Pair("Manhwa", "Manhwa"),
|
Pair(intl["status_filter_option_dropped"], "dropped"),
|
||||||
Pair("Manhua", "Manhua"),
|
|
||||||
Pair("Comic", "Comic"),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
protected class OrderByFilter(defaultOrder: String? = null) : SelectFilter(
|
protected class TypeFilter(
|
||||||
"Sort By",
|
name: String,
|
||||||
arrayOf(
|
options: Array<Pair<String, String>>,
|
||||||
Pair("Default", ""),
|
) : SelectFilter(
|
||||||
Pair("A-Z", "title"),
|
name,
|
||||||
Pair("Z-A", "titlereverse"),
|
options,
|
||||||
Pair("Latest Update", "update"),
|
)
|
||||||
Pair("Latest Added", "latest"),
|
|
||||||
Pair("Popular", "popular"),
|
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,
|
defaultOrder,
|
||||||
)
|
)
|
||||||
|
|
||||||
protected class ProjectFilter : SelectFilter(
|
protected open val orderByFilterOptions = arrayOf(
|
||||||
"Filter Project",
|
Pair(intl["order_by_filter_default"], ""),
|
||||||
arrayOf(
|
Pair(intl["order_by_filter_az"], "title"),
|
||||||
Pair("Show all manga", ""),
|
Pair(intl["order_by_filter_za"], "titlereverse"),
|
||||||
Pair("Show only project manga", "project-filter-on"),
|
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(
|
protected class Genre(
|
||||||
name: String,
|
name: String,
|
||||||
val value: String,
|
val value: String,
|
||||||
state: Int = STATE_IGNORE,
|
state: Int,
|
||||||
) : Filter.TriState(name, state)
|
) : 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> {
|
protected open fun getGenreList(): List<Genre> {
|
||||||
// Filters are fetched immediately once an extension loads
|
return genrelist?.map { Genre(it.name, it.value, it.state) }.orEmpty()
|
||||||
// We're only able to get filters after a loading the manga directory,
|
|
||||||
// and resetting the filters is the only thing that seems to reinflate the view
|
|
||||||
return genrelist ?: listOf(Genre("Press reset to attempt to fetch genres", ""))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open val hasProjectPage = false
|
open val hasProjectPage = false
|
||||||
|
@ -423,21 +530,31 @@ abstract class MangaThemesia(
|
||||||
override fun getFilterList(): FilterList {
|
override fun getFilterList(): FilterList {
|
||||||
val filters = mutableListOf<Filter<*>>(
|
val filters = mutableListOf<Filter<*>>(
|
||||||
Filter.Separator(),
|
Filter.Separator(),
|
||||||
AuthorFilter(),
|
AuthorFilter(intl["author_filter_title"]),
|
||||||
YearFilter(),
|
YearFilter(intl["year_filter_title"]),
|
||||||
StatusFilter(),
|
StatusFilter(intl["status_filter_title"], statusOptions),
|
||||||
TypeFilter(),
|
TypeFilter(intl["type_filter_title"], typeFilterOptions),
|
||||||
OrderByFilter(),
|
OrderByFilter(intl["order_by_filter_title"], orderByFilterOptions),
|
||||||
Filter.Header("Genre exclusion is not available for all sources"),
|
|
||||||
GenreListFilter(getGenreList()),
|
|
||||||
)
|
)
|
||||||
|
if (!genrelist.isNullOrEmpty()) {
|
||||||
|
filters.addAll(
|
||||||
|
listOf(
|
||||||
|
Filter.Header(intl["genre_exclusion_warning"]),
|
||||||
|
GenreListFilter(intl["genre_filter_title"], getGenreList()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
filters.add(
|
||||||
|
Filter.Header(intl["genre_missing_warning"]),
|
||||||
|
)
|
||||||
|
}
|
||||||
if (hasProjectPage) {
|
if (hasProjectPage) {
|
||||||
filters.addAll(
|
filters.addAll(
|
||||||
mutableListOf<Filter<*>>(
|
mutableListOf<Filter<*>>(
|
||||||
Filter.Separator(),
|
Filter.Separator(),
|
||||||
Filter.Header("NOTE: Can't be used with other filter!"),
|
Filter.Header(intl["project_filter_warning"]),
|
||||||
Filter.Header("$name Project List page"),
|
Filter.Header(intl.format("project_filter_name", name)),
|
||||||
ProjectFilter(),
|
ProjectFilter(intl["project_filter_title"], projectFilterOptions),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -485,9 +602,9 @@ abstract class MangaThemesia(
|
||||||
(!strict && url.pathSegments.size == n + 1 && url.pathSegments[n].isEmpty())
|
(!strict && url.pathSegments.size == n + 1 && url.pathSegments[n].isEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseGenres(document: Document): List<Genre>? {
|
private fun parseGenres(document: Document): List<GenreData>? {
|
||||||
return document.selectFirst("ul.genrez")?.select("li")?.map { li ->
|
return document.selectFirst("ul.genrez")?.select("li")?.map { li ->
|
||||||
Genre(
|
GenreData(
|
||||||
li.selectFirst("label")!!.text(),
|
li.selectFirst("label")!!.text(),
|
||||||
li.selectFirst("input[type=checkbox]")!!.attr("value"),
|
li.selectFirst("input[type=checkbox]")!!.attr("value"),
|
||||||
)
|
)
|
||||||
|
@ -514,15 +631,10 @@ abstract class MangaThemesia(
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
|
||||||
addRandomUAPreferenceToScreen(screen)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val URL_SEARCH_PREFIX = "url:"
|
const val URL_SEARCH_PREFIX = "url:"
|
||||||
|
|
||||||
// More info: https://issuetracker.google.com/issues/36970498
|
// More info: https://issuetracker.google.com/issues/36970498
|
||||||
@Suppress("RegExpRedundantEscape")
|
|
||||||
private val MANGA_PAGE_ID_REGEX = "post_id\\s*:\\s*(\\d+)\\}".toRegex()
|
private val MANGA_PAGE_ID_REGEX = "post_id\\s*:\\s*(\\d+)\\}".toRegex()
|
||||||
private val CHAPTER_PAGE_ID_REGEX = "chapter_id\\s*=\\s*(\\d+);".toRegex()
|
private val CHAPTER_PAGE_ID_REGEX = "chapter_id\\s*=\\s*(\\d+);".toRegex()
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ data class PizzaReaderDto(
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PizzaComicDto(
|
data class PizzaComicDto(
|
||||||
val artist: String = "",
|
val artist: String? = null,
|
||||||
val author: String = "",
|
val author: String = "",
|
||||||
val chapters: List<PizzaChapterDto> = emptyList(),
|
val chapters: List<PizzaChapterDto> = emptyList(),
|
||||||
val description: String = "",
|
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 {
|
ext {
|
||||||
extName = 'LANraragi'
|
extName = 'LANraragi'
|
||||||
extClass = '.LANraragiFactory'
|
extClass = '.LANraragiFactory'
|
||||||
extVersionCode = 16
|
extVersionCode = 17
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -82,6 +82,17 @@ open class LANraragi(private val suffix: String = "") : ConfigurableSource, Unme
|
||||||
return archiveToSManga(archive)
|
return archiveToSManga(archive)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getMangaUrl(manga: SManga): String {
|
||||||
|
val namespace = preferences.getString(URL_TAG_PREFIX_KEY, URL_TAG_PREFIX_DEFAULT)
|
||||||
|
|
||||||
|
if (namespace.isNullOrEmpty()) {
|
||||||
|
return super.getMangaUrl(manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
val tag = manga.genre?.split(", ")?.find { it.startsWith("$namespace") }
|
||||||
|
return tag?.substringAfter("$namespace") ?: super.getMangaUrl(manga)
|
||||||
|
}
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga): Request {
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
val id = if (manga.url.startsWith("/api/search/random")) randomArchiveID else getReaderId(manga.url)
|
val id = if (manga.url.startsWith("/api/search/random")) randomArchiveID else getReaderId(manga.url)
|
||||||
val uri = getApiUriBuilder("/api/archives/$id/metadata").build()
|
val uri = getApiUriBuilder("/api/archives/$id/metadata").build()
|
||||||
|
@ -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(CLEAR_NEW_KEY, "Clear New status", CLEAR_NEW_DEFAULT, "Clear an entry's New status when its details are viewed."))
|
||||||
screen.addPreference(screen.checkBoxPreference(NEW_ONLY_KEY, "Latest - New Only", NEW_ONLY_DEFAULT))
|
screen.addPreference(screen.checkBoxPreference(NEW_ONLY_KEY, "Latest - New Only", NEW_ONLY_DEFAULT))
|
||||||
screen.addPreference(screen.editTextPreference(SORT_BY_NS_KEY, "Latest - Sort by Namespace", SORT_BY_NS_DEFAULT, "Sort by the given namespace for Latest, such as date_added."))
|
screen.addPreference(screen.editTextPreference(SORT_BY_NS_KEY, "Latest - Sort by Namespace", SORT_BY_NS_DEFAULT, "Sort by the given namespace for Latest, such as date_added."))
|
||||||
|
screen.addPreference(screen.editTextPreference(URL_TAG_PREFIX_KEY, "Set tag prefix to get WebView URL", URL_TAG_PREFIX_DEFAULT, "Example: 'source:' will try to get the URL from the first tag starting with 'source:' and it will open it in the WebView. Leave empty for the default behavior."))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun androidx.preference.PreferenceScreen.checkBoxPreference(key: String, title: String, default: Boolean, summary: String = ""): androidx.preference.CheckBoxPreference {
|
private fun androidx.preference.PreferenceScreen.checkBoxPreference(key: String, title: String, default: Boolean, summary: String = ""): androidx.preference.CheckBoxPreference {
|
||||||
|
@ -494,5 +506,7 @@ open class LANraragi(private val suffix: String = "") : ConfigurableSource, Unme
|
||||||
private const val SORT_BY_NS_KEY = "latestNamespacePref"
|
private const val SORT_BY_NS_KEY = "latestNamespacePref"
|
||||||
private const val CLEAR_NEW_KEY = "clearNew"
|
private const val CLEAR_NEW_KEY = "clearNew"
|
||||||
private const val CLEAR_NEW_DEFAULT = true
|
private const val CLEAR_NEW_DEFAULT = true
|
||||||
|
private const val URL_TAG_PREFIX_KEY = "urlTagPrefix"
|
||||||
|
private const val URL_TAG_PREFIX_DEFAULT = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -290,6 +290,7 @@ enum class Language {
|
||||||
RUSSIAN,
|
RUSSIAN,
|
||||||
THAI,
|
THAI,
|
||||||
VIETNAMESE,
|
VIETNAMESE,
|
||||||
|
GERMAN,
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
|
@ -13,5 +13,6 @@ class MangaPlusFactory : SourceFactory {
|
||||||
MangaPlus("ru", "rus", Language.RUSSIAN),
|
MangaPlus("ru", "rus", Language.RUSSIAN),
|
||||||
MangaPlus("th", "tha", Language.THAI),
|
MangaPlus("th", "tha", Language.THAI),
|
||||||
MangaPlus("vi", "vie", Language.VIETNAMESE),
|
MangaPlus("vi", "vie", Language.VIETNAMESE),
|
||||||
|
MangaPlus("de", "deu", Language.GERMAN),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,10 +33,11 @@ open class MiauScan(lang: String) : MangaThemesia(
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val genreFilterIndex = filters.indexOfFirst { it is GenreListFilter }
|
val genreFilterIndex = filters.indexOfFirst { it is GenreListFilter }
|
||||||
val genreFilter = filters.getOrNull(genreFilterIndex) as? GenreListFilter
|
val genreFilter = filters.getOrNull(genreFilterIndex) as? GenreListFilter
|
||||||
?: GenreListFilter(emptyList())
|
?: GenreListFilter("", emptyList())
|
||||||
|
|
||||||
val overloadedGenreFilter = GenreListFilter(
|
val overloadedGenreFilter = GenreListFilter(
|
||||||
genres = genreFilter.state + listOf(
|
genreFilter.name,
|
||||||
|
genreFilter.state + listOf(
|
||||||
Genre("", PORTUGUESE_GENRE_ID, portugueseMode),
|
Genre("", PORTUGUESE_GENRE_ID, portugueseMode),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -31,8 +31,8 @@ class Mihentai : MangaThemesia("Mihentai", "https://mihentai.com", "all") {
|
||||||
listOf(
|
listOf(
|
||||||
StatusFilter(),
|
StatusFilter(),
|
||||||
TypeFilter(),
|
TypeFilter(),
|
||||||
OrderByFilter(),
|
OrderByFilter(intl["order_by_filter_title"], orderByFilterOptions),
|
||||||
GenreListFilter(getGenreList()),
|
GenreListFilter(intl["genre_filter_title"], getGenreList()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package eu.kanade.tachiyomi.extension.ar.areamanga
|
package eu.kanade.tachiyomi.extension.ar.areamanga
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
@ -10,21 +9,4 @@ class AreaManga : MangaThemesia(
|
||||||
"https://www.areascans.net",
|
"https://www.areascans.net",
|
||||||
"ar",
|
"ar",
|
||||||
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
||||||
) {
|
)
|
||||||
override val seriesArtistSelector =
|
|
||||||
".tsinfo .imptdt:contains(الرسام) i, ${super.seriesArtistSelector}"
|
|
||||||
override val seriesAuthorSelector =
|
|
||||||
".tsinfo .imptdt:contains(المؤلف) i, ${super.seriesAuthorSelector}"
|
|
||||||
override val seriesStatusSelector =
|
|
||||||
".tsinfo .imptdt:contains(الحالة) i, ${super.seriesStatusSelector}"
|
|
||||||
override val seriesTypeSelector =
|
|
||||||
".tsinfo .imptdt:contains(النوع) i, ${super.seriesTypeSelector}"
|
|
||||||
|
|
||||||
override fun String?.parseStatus() = when {
|
|
||||||
this == null -> SManga.UNKNOWN
|
|
||||||
this.contains("مستمر", ignoreCase = true) -> SManga.ONGOING
|
|
||||||
this.contains("مكتمل", ignoreCase = true) -> SManga.COMPLETED
|
|
||||||
this.contains("متوقف", ignoreCase = true) -> SManga.ON_HIATUS
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.MangaSwat'
|
extClass = '.MangaSwat'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://swatmanhua.com'
|
baseUrl = 'https://swatmanhua.com'
|
||||||
overrideVersionCode = 16
|
overrideVersionCode = 17
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -7,6 +7,7 @@ import androidx.preference.PreferenceScreen
|
||||||
import eu.kanade.tachiyomi.extension.BuildConfig
|
import eu.kanade.tachiyomi.extension.BuildConfig
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
@ -23,12 +24,14 @@ import java.util.Locale
|
||||||
|
|
||||||
private const val swatUrl = "https://swatmanhua.com"
|
private const val swatUrl = "https://swatmanhua.com"
|
||||||
|
|
||||||
class MangaSwat : MangaThemesia(
|
class MangaSwat :
|
||||||
|
MangaThemesia(
|
||||||
"MangaSwat",
|
"MangaSwat",
|
||||||
swatUrl,
|
swatUrl,
|
||||||
"ar",
|
"ar",
|
||||||
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
||||||
) {
|
),
|
||||||
|
ConfigurableSource {
|
||||||
private val defaultBaseUrl = swatUrl
|
private val defaultBaseUrl = swatUrl
|
||||||
|
|
||||||
override val baseUrl by lazy { getPrefBaseUrl() }
|
override val baseUrl by lazy { getPrefBaseUrl() }
|
||||||
|
@ -58,6 +61,7 @@ class MangaSwat : MangaThemesia(
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = "a[rel=next]"
|
override fun searchMangaNextPageSelector() = "a[rel=next]"
|
||||||
|
|
||||||
|
override val seriesTitleSelector = "h1[itemprop=headline]"
|
||||||
override val seriesArtistSelector = "span:contains(الناشر) i"
|
override val seriesArtistSelector = "span:contains(الناشر) i"
|
||||||
override val seriesAuthorSelector = "span:contains(المؤلف) i"
|
override val seriesAuthorSelector = "span:contains(المؤلف) i"
|
||||||
override val seriesGenreSelector = "span:contains(التصنيف) a, .mgen a"
|
override val seriesGenreSelector = "span:contains(التصنيف) a, .mgen a"
|
||||||
|
@ -113,8 +117,6 @@ class MangaSwat : MangaThemesia(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
screen.addPreference(baseUrlPref)
|
screen.addPreference(baseUrlPref)
|
||||||
|
|
||||||
super.setupPreferenceScreen(screen)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPrefBaseUrl(): String = preferences.getString(BASE_URL_PREF, defaultBaseUrl)!!
|
private fun getPrefBaseUrl(): String = preferences.getString(BASE_URL_PREF, defaultBaseUrl)!!
|
||||||
|
|
|
@ -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 {
|
ext {
|
||||||
extName = 'Anchira'
|
extName = 'Anchira'
|
||||||
extClass = '.Anchira'
|
extClass = '.Anchira'
|
||||||
extVersionCode = 8
|
extVersionCode = 10
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import android.content.SharedPreferences
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
|
import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.createChapter
|
||||||
import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.getPathFromUrl
|
import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.getPathFromUrl
|
||||||
import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.prepareTags
|
import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.prepareTags
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
@ -23,6 +24,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
@ -33,6 +35,8 @@ import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.math.ceil
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
class Anchira : HttpSource(), ConfigurableSource {
|
class Anchira : HttpSource(), ConfigurableSource {
|
||||||
override val name = "Anchira"
|
override val name = "Anchira"
|
||||||
|
@ -109,6 +113,23 @@ class Anchira : HttpSource(), ConfigurableSource {
|
||||||
fetchMangaDetails(manga).map {
|
fetchMangaDetails(manga).map {
|
||||||
MangasPage(listOf(it), false)
|
MangasPage(listOf(it), false)
|
||||||
}
|
}
|
||||||
|
} else if (query.startsWith(SLUG_BUNDLE_PREFIX)) {
|
||||||
|
// bundle entries as chapters
|
||||||
|
val url = applyFilters(
|
||||||
|
page,
|
||||||
|
query.substringAfter(SLUG_BUNDLE_PREFIX),
|
||||||
|
filters,
|
||||||
|
).removeAllQueryParameters("page")
|
||||||
|
if (
|
||||||
|
url.build().queryParameter("sort") == "4"
|
||||||
|
) {
|
||||||
|
url.removeAllQueryParameters("sort")
|
||||||
|
}
|
||||||
|
val manga = SManga.create()
|
||||||
|
.apply { this.url = "?${url.build().query}" }
|
||||||
|
fetchMangaDetails(manga).map {
|
||||||
|
MangasPage(listOf(it), false)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// regular filtering without text search
|
// regular filtering without text search
|
||||||
client.newCall(searchMangaRequest(page, query, filters))
|
client.newCall(searchMangaRequest(page, query, filters))
|
||||||
|
@ -116,13 +137,29 @@ class Anchira : HttpSource(), ConfigurableSource {
|
||||||
.map(::searchMangaParse)
|
.map(::searchMangaParse)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||||
|
GET(applyFilters(page, query, filters).build(), headers)
|
||||||
|
|
||||||
|
private fun applyFilters(page: Int, query: String, filters: FilterList): HttpUrl.Builder {
|
||||||
|
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||||
|
val trendingFilter = filterList.findInstance<TrendingFilter>()
|
||||||
|
val sortTrendingFilter = filters.findInstance<SortTrendingFilter>()
|
||||||
var url = libraryUrl.toHttpUrl().newBuilder()
|
var url = libraryUrl.toHttpUrl().newBuilder()
|
||||||
|
|
||||||
url.addQueryParameter("page", page.toString())
|
if (trendingFilter?.state == true) {
|
||||||
|
val interval = when (sortTrendingFilter?.state) {
|
||||||
|
1 -> "3"
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interval.isNotBlank()) url.setQueryParameter("interval", interval)
|
||||||
|
|
||||||
|
url = url.toString().replace("library", "trending").toHttpUrl()
|
||||||
|
.newBuilder()
|
||||||
|
} else {
|
||||||
if (query.isNotBlank()) {
|
if (query.isNotBlank()) {
|
||||||
url.addQueryParameter("s", query)
|
url.setQueryParameter("s", query)
|
||||||
}
|
}
|
||||||
|
|
||||||
filters.forEach { filter ->
|
filters.forEach { filter ->
|
||||||
|
@ -138,7 +175,7 @@ class Anchira : HttpSource(), ConfigurableSource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sum > 0) url.addQueryParameter("cat", sum.toString())
|
if (sum > 0) url.setQueryParameter("cat", sum.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
is SortFilter -> {
|
is SortFilter -> {
|
||||||
|
@ -150,8 +187,8 @@ class Anchira : HttpSource(), ConfigurableSource {
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sort.isNotEmpty()) url.addQueryParameter("sort", sort)
|
if (sort.isNotEmpty()) url.setQueryParameter("sort", sort)
|
||||||
if (filter.state?.ascending == true) url.addQueryParameter("order", "1")
|
if (filter.state?.ascending == true) url.setQueryParameter("order", "1")
|
||||||
}
|
}
|
||||||
|
|
||||||
is FavoritesFilter -> {
|
is FavoritesFilter -> {
|
||||||
|
@ -168,21 +205,42 @@ class Anchira : HttpSource(), ConfigurableSource {
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return GET(url.build(), headers)
|
if (page > 1) {
|
||||||
|
url.setQueryParameter("page", page.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response) = latestUpdatesParse(response)
|
override fun searchMangaParse(response: Response) = latestUpdatesParse(response)
|
||||||
|
|
||||||
// Details
|
// Details
|
||||||
|
|
||||||
override fun mangaDetailsRequest(manga: SManga) =
|
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||||
|
return if (manga.url.startsWith("?")) {
|
||||||
|
GET(libraryUrl + manga.url, headers)
|
||||||
|
} else {
|
||||||
GET("$libraryUrl/${getPathFromUrl(manga.url)}", headers)
|
GET("$libraryUrl/${getPathFromUrl(manga.url)}", headers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
|
return if (response.request.url.pathSegments.count() == libraryUrl.toHttpUrl().pathSegments.count()) {
|
||||||
|
val 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())
|
val data = json.decodeFromString<Entry>(response.body.string())
|
||||||
|
|
||||||
return SManga.create().apply {
|
SManga.create().apply {
|
||||||
url = "/g/${data.id}/${data.key}"
|
url = "/g/${data.id}/${data.key}"
|
||||||
title = data.title
|
title = data.title
|
||||||
thumbnail_url =
|
thumbnail_url =
|
||||||
|
@ -194,8 +252,10 @@ class Anchira : HttpSource(), ConfigurableSource {
|
||||||
status = SManga.COMPLETED
|
status = SManga.COMPLETED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getMangaUrl(manga: SManga) = if (preferences.openSource) {
|
override fun getMangaUrl(manga: SManga) =
|
||||||
|
if (preferences.openSource && !manga.url.startsWith("?")) {
|
||||||
val id = manga.url.split("/").reversed()[1].toInt()
|
val id = manga.url.split("/").reversed()[1].toInt()
|
||||||
anchiraData.find { it.id == id }?.url ?: "$baseUrl${manga.url}"
|
anchiraData.find { it.id == id }?.url ?: "$baseUrl${manga.url}"
|
||||||
} else {
|
} else {
|
||||||
|
@ -204,21 +264,45 @@ class Anchira : HttpSource(), ConfigurableSource {
|
||||||
|
|
||||||
// Chapter
|
// Chapter
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga) =
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
|
return if (manga.url.startsWith("?")) {
|
||||||
|
GET(libraryUrl + manga.url, headers)
|
||||||
|
} else {
|
||||||
GET("$libraryUrl/${getPathFromUrl(manga.url)}", headers)
|
GET("$libraryUrl/${getPathFromUrl(manga.url)}", headers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
val data = json.decodeFromString<Entry>(response.body.string())
|
val chapterList = mutableListOf<SChapter>()
|
||||||
|
if (response.request.url.pathSegments.count() == libraryUrl.toHttpUrl().pathSegments.count()) {
|
||||||
return listOf(
|
var results = json.decodeFromString<LibraryResponse>(response.body.string())
|
||||||
SChapter.create().apply {
|
val pages = min(5, ceil((results.total.toFloat() / results.limit)).toInt())
|
||||||
url = "/g/${data.id}/${data.key}"
|
for (page in 1..pages) {
|
||||||
name = "Chapter"
|
results.entries.forEach { data ->
|
||||||
date_upload = data.publishedAt * 1000
|
chapterList.add(
|
||||||
chapter_number = 1f
|
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)}"
|
override fun getChapterUrl(chapter: SChapter) = "$baseUrl/g/${getPathFromUrl(chapter.url)}"
|
||||||
|
|
||||||
|
@ -278,14 +362,16 @@ class Anchira : HttpSource(), ConfigurableSource {
|
||||||
val openSourcePref = SwitchPreferenceCompat(screen.context).apply {
|
val openSourcePref = SwitchPreferenceCompat(screen.context).apply {
|
||||||
key = OPEN_SOURCE_PREF
|
key = OPEN_SOURCE_PREF
|
||||||
title = "Open source website in WebView"
|
title = "Open source website in WebView"
|
||||||
summary = "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)
|
setDefaultValue(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
val useTagGrouping = SwitchPreferenceCompat(screen.context).apply {
|
val useTagGrouping = SwitchPreferenceCompat(screen.context).apply {
|
||||||
key = USE_TAG_GROUPING
|
key = USE_TAG_GROUPING
|
||||||
title = "Group tags"
|
title = "Group tags"
|
||||||
summary = "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)
|
setDefaultValue(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -298,6 +384,10 @@ class Anchira : HttpSource(), ConfigurableSource {
|
||||||
CategoryGroup(),
|
CategoryGroup(),
|
||||||
SortFilter(),
|
SortFilter(),
|
||||||
FavoritesFilter(),
|
FavoritesFilter(),
|
||||||
|
Filter.Separator(),
|
||||||
|
Filter.Header("Others are ignored if trending only"),
|
||||||
|
TrendingFilter(),
|
||||||
|
SortTrendingFilter(),
|
||||||
)
|
)
|
||||||
|
|
||||||
private class CategoryFilter(name: String) : Filter.CheckBox(name, false)
|
private class CategoryFilter(name: String) : Filter.CheckBox(name, false)
|
||||||
|
@ -317,6 +407,18 @@ class Anchira : HttpSource(), ConfigurableSource {
|
||||||
Selection(2, false),
|
Selection(2, false),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private class TrendingFilter : Filter.CheckBox(
|
||||||
|
"Show only trending",
|
||||||
|
)
|
||||||
|
|
||||||
|
private class SortTrendingFilter : PartFilter(
|
||||||
|
"Sort By",
|
||||||
|
arrayOf("Trending: Weekly", "Trending: Monthly"),
|
||||||
|
)
|
||||||
|
|
||||||
|
private open class PartFilter(displayName: String, value: Array<String>) :
|
||||||
|
Filter.Select<String>(displayName, value)
|
||||||
|
|
||||||
private val SharedPreferences.imageQuality
|
private val SharedPreferences.imageQuality
|
||||||
get() = getString(IMAGE_QUALITY_PREF, "b")!!
|
get() = getString(IMAGE_QUALITY_PREF, "b")!!
|
||||||
|
|
||||||
|
@ -362,8 +464,11 @@ class Anchira : HttpSource(), ConfigurableSource {
|
||||||
.use { json.decodeFromStream<List<EntryKey>>(it.body.byteStream()) }
|
.use { json.decodeFromStream<List<EntryKey>>(it.body.byteStream()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val SLUG_SEARCH_PREFIX = "id:"
|
const val SLUG_SEARCH_PREFIX = "id:"
|
||||||
|
const val SLUG_BUNDLE_PREFIX = "bundle:"
|
||||||
private const val IMAGE_QUALITY_PREF = "image_quality"
|
private const val IMAGE_QUALITY_PREF = "image_quality"
|
||||||
private const val OPEN_SOURCE_PREF = "use_manga_source"
|
private const val OPEN_SOURCE_PREF = "use_manga_source"
|
||||||
private const val USE_TAG_GROUPING = "use_tag_grouping"
|
private const val USE_TAG_GROUPING = "use_tag_grouping"
|
||||||
|
@ -371,3 +476,5 @@ class Anchira : HttpSource(), ConfigurableSource {
|
||||||
"https://gist.githubusercontent.com/LetrixZ/2b559cc5829d1c221c701e02ecd81411/raw/data-v5.json"
|
"https://gist.githubusercontent.com/LetrixZ/2b559cc5829d1c221c701e02ecd81411/raw/data-v5.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val CHAPTER_SUFFIX_RE = Regex("(?<!20\\d\\d-)\\b[\\d.]{1,4}$")
|
||||||
|
|
|
@ -3,15 +3,6 @@ package eu.kanade.tachiyomi.extension.en.anchira
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ListEntry(
|
|
||||||
val id: Int,
|
|
||||||
val key: String,
|
|
||||||
val title: String,
|
|
||||||
@SerialName("thumb_index") val thumbnailIndex: Int,
|
|
||||||
val tags: List<Tag> = emptyList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Tag(
|
data class Tag(
|
||||||
var name: String,
|
var name: String,
|
||||||
|
@ -20,7 +11,7 @@ data class Tag(
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class LibraryResponse(
|
data class LibraryResponse(
|
||||||
val entries: List<ListEntry> = emptyList(),
|
val entries: List<Entry> = emptyList(),
|
||||||
val total: Int,
|
val total: Int,
|
||||||
val page: Int,
|
val page: Int,
|
||||||
val limit: Int,
|
val limit: Int,
|
||||||
|
@ -30,11 +21,12 @@ data class LibraryResponse(
|
||||||
data class Entry(
|
data class Entry(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val key: String,
|
val key: String,
|
||||||
@SerialName("published_at") val publishedAt: Long,
|
@SerialName("published_at") val publishedAt: Long = 0L,
|
||||||
val title: String,
|
val title: String,
|
||||||
@SerialName("thumb_index") val thumbnailIndex: Int,
|
@SerialName("thumb_index") val thumbnailIndex: Int = 1,
|
||||||
val tags: List<Tag> = emptyList(),
|
val tags: List<Tag> = emptyList(),
|
||||||
val url: String? = null,
|
val url: String? = null,
|
||||||
|
val pages: Int = 1,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.anchira
|
package eu.kanade.tachiyomi.extension.en.anchira
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import okhttp3.Response
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
object AnchiraHelper {
|
object AnchiraHelper {
|
||||||
fun getPathFromUrl(url: String) = "${url.split("/").reversed()[1]}/${url.split("/").last()}"
|
fun getPathFromUrl(url: String) = "${url.split("/").reversed()[1]}/${url.split("/").last()}"
|
||||||
|
|
||||||
|
@ -25,4 +29,31 @@ object AnchiraHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.joinToString(", ") { it }
|
.joinToString(", ") { it }
|
||||||
|
|
||||||
|
fun createChapter(entry: Entry, response: Response, anchiraData: List<EntryKey>) =
|
||||||
|
SChapter.create().apply {
|
||||||
|
val ch =
|
||||||
|
CHAPTER_SUFFIX_RE.find(entry.title)?.value?.trim('.') ?: "1"
|
||||||
|
val source = anchiraData.find { it.id == entry.id }?.url
|
||||||
|
?: response.request.url.toString()
|
||||||
|
url = "/g/${entry.id}/${entry.key}"
|
||||||
|
name = "$ch. ${entry.title.removeSuffix(" $ch")}"
|
||||||
|
date_upload = entry.publishedAt * 1000
|
||||||
|
chapter_number = ch.toFloat()
|
||||||
|
scanlator = buildString {
|
||||||
|
append(
|
||||||
|
Regex("fakku|irodori|anchira").find(source)?.value.orEmpty()
|
||||||
|
.replaceFirstChar {
|
||||||
|
if (it.isLowerCase()) {
|
||||||
|
it.titlecase(
|
||||||
|
Locale.getDefault(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
it.toString()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
append(" - ${entry.pages} pages")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 androidx.preference.SwitchPreferenceCompat
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
@ -26,12 +27,14 @@ import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class AsuraScans : MangaThemesia(
|
class AsuraScans :
|
||||||
|
MangaThemesia(
|
||||||
"Asura Scans",
|
"Asura Scans",
|
||||||
"https://asuratoon.com",
|
"https://asuratoon.com",
|
||||||
"en",
|
"en",
|
||||||
dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.US),
|
dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.US),
|
||||||
) {
|
),
|
||||||
|
ConfigurableSource {
|
||||||
|
|
||||||
private val preferences by lazy {
|
private val preferences by lazy {
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
@ -281,8 +284,6 @@ class AsuraScans : MangaThemesia(
|
||||||
summary = PREF_PERM_MANGA_URL_SUMMARY
|
summary = PREF_PERM_MANGA_URL_SUMMARY
|
||||||
setDefaultValue(true)
|
setDefaultValue(true)
|
||||||
}.also(screen::addPreference)
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
super.setupPreferenceScreen(screen)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val SharedPreferences.permaUrlPref
|
private val SharedPreferences.permaUrlPref
|
||||||
|
|
|
@ -8,3 +8,7 @@ ext {
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
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.app.Application
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import eu.kanade.tachiyomi.lib.randomua.addRandomUAPreferenceToScreen
|
||||||
import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA
|
import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA
|
||||||
import eu.kanade.tachiyomi.lib.randomua.getPrefUAType
|
import eu.kanade.tachiyomi.lib.randomua.getPrefUAType
|
||||||
import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent
|
import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import kotlinx.serialization.json.jsonArray
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
@ -19,22 +22,29 @@ import okhttp3.Request
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class ConstellarScans : MangaThemesia("Constellar Scans", "https://constellarcomic.com", "en") {
|
class ConstellarScans :
|
||||||
|
MangaThemesia(
|
||||||
|
"Constellar Scans",
|
||||||
|
"https://constellarcomic.com",
|
||||||
|
"en",
|
||||||
|
),
|
||||||
|
ConfigurableSource {
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
private val preferences: SharedPreferences by lazy {
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
addRandomUAPreferenceToScreen(screen)
|
||||||
|
}
|
||||||
|
|
||||||
override val client: OkHttpClient by lazy {
|
override val client: OkHttpClient by lazy {
|
||||||
network.cloudflareClient.newBuilder()
|
network.cloudflareClient.newBuilder()
|
||||||
.setRandomUserAgent(
|
.setRandomUserAgent(
|
||||||
preferences.getPrefUAType(),
|
preferences.getPrefUAType(),
|
||||||
preferences.getPrefCustomUA(),
|
preferences.getPrefCustomUA(),
|
||||||
)
|
)
|
||||||
.connectTimeout(10, TimeUnit.SECONDS)
|
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
|
||||||
.rateLimit(1, 1)
|
.rateLimit(1, 1)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
@ -61,6 +71,8 @@ class ConstellarScans : MangaThemesia("Constellar Scans", "https://constellarcom
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
countViews(document)
|
||||||
|
|
||||||
val html = document.toString()
|
val html = document.toString()
|
||||||
if (!html.contains("ts_rea_der_._run(\"")) {
|
if (!html.contains("ts_rea_der_._run(\"")) {
|
||||||
return super.pageListParse(document)
|
return super.pageListParse(document)
|
||||||
|
@ -80,7 +92,6 @@ class ConstellarScans : MangaThemesia("Constellar Scans", "https://constellarcom
|
||||||
}
|
}
|
||||||
.joinToString("")
|
.joinToString("")
|
||||||
|
|
||||||
countViews(document)
|
|
||||||
return json.parseToJsonElement(tsReaderRawData).jsonObject["sources"]!!.jsonArray[0].jsonObject["images"]!!.jsonArray.mapIndexed { idx, it ->
|
return json.parseToJsonElement(tsReaderRawData).jsonObject["sources"]!!.jsonArray[0].jsonObject["images"]!!.jsonArray.mapIndexed { idx, it ->
|
||||||
Page(idx, imageUrl = it.jsonPrimitive.content)
|
Page(idx, imageUrl = it.jsonPrimitive.content)
|
||||||
}
|
}
|
||||||
|
|
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 androidx.preference.SwitchPreferenceCompat
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
@ -25,12 +26,14 @@ import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
class FlameComics : MangaThemesia(
|
class FlameComics :
|
||||||
|
MangaThemesia(
|
||||||
"Flame Comics",
|
"Flame Comics",
|
||||||
"https://flamecomics.com",
|
"https://flamecomics.com",
|
||||||
"en",
|
"en",
|
||||||
mangaUrlDirectory = "/series",
|
mangaUrlDirectory = "/series",
|
||||||
) {
|
),
|
||||||
|
ConfigurableSource {
|
||||||
|
|
||||||
// Flame Scans -> Flame Comics
|
// Flame Scans -> Flame Comics
|
||||||
override val id = 6350607071566689772
|
override val id = 6350607071566689772
|
||||||
|
|
|
@ -2,8 +2,8 @@ ext {
|
||||||
extName = 'Infernal Void Scans'
|
extName = 'Infernal Void Scans'
|
||||||
extClass = '.InfernalVoidScans'
|
extClass = '.InfernalVoidScans'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://void-scans.com'
|
baseUrl = 'https://hivescans.com'
|
||||||
overrideVersionCode = 5
|
overrideVersionCode = 6
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -2,6 +2,6 @@ package eu.kanade.tachiyomi.extension.en.infernalvoidscans
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
|
|
||||||
class InfernalVoidScans : MangaThemesia("Infernal Void Scans", "https://void-scans.com", "en") {
|
class InfernalVoidScans : MangaThemesia("Infernal Void Scans", "https://hivescans.com", "en") {
|
||||||
override val pageSelector = "div#readerarea > p > img"
|
override val pageSelector = "div#readerarea > p > img"
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import androidx.preference.PreferenceScreen
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
@ -20,7 +21,15 @@ import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class LuminousScans : 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()
|
override val client = super.client.newBuilder()
|
||||||
.addInterceptor(::urlChangeInterceptor)
|
.addInterceptor(::urlChangeInterceptor)
|
||||||
.rateLimit(2)
|
.rateLimit(2)
|
||||||
|
@ -201,8 +210,6 @@ class LuminousScans : MangaThemesia("Luminous Scans", "https://lumitoon.com", "e
|
||||||
summary = PREF_PERM_MANGA_URL_SUMMARY
|
summary = PREF_PERM_MANGA_URL_SUMMARY
|
||||||
setDefaultValue(true)
|
setDefaultValue(true)
|
||||||
}.also(screen::addPreference)
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
super.setupPreferenceScreen(screen)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val SharedPreferences.permaUrlPref
|
private val SharedPreferences.permaUrlPref
|
||||||
|
|
|
@ -33,19 +33,29 @@ class LunarScans : MangaThemesia(
|
||||||
val filters = mutableListOf<Filter<*>>(
|
val filters = mutableListOf<Filter<*>>(
|
||||||
Filter.Header("Note: Can't be used with text search!"),
|
Filter.Header("Note: Can't be used with text search!"),
|
||||||
Filter.Separator(),
|
Filter.Separator(),
|
||||||
StatusFilter(),
|
StatusFilter(intl["status_filter_title"], statusOptions),
|
||||||
TypeFilter(),
|
TypeFilter(intl["type_filter_title"], typeFilterOptions),
|
||||||
OrderByFilter(),
|
OrderByFilter(intl["order_by_filter_title"], orderByFilterOptions),
|
||||||
Filter.Header("Genre exclusion is not available for all sources"),
|
|
||||||
GenreListFilter(getGenreList()),
|
|
||||||
)
|
)
|
||||||
|
if (!genrelist.isNullOrEmpty()) {
|
||||||
|
filters.addAll(
|
||||||
|
listOf(
|
||||||
|
Filter.Header(intl["genre_exclusion_warning"]),
|
||||||
|
GenreListFilter(intl["genre_filter_title"], getGenreList()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
filters.add(
|
||||||
|
Filter.Header(intl["genre_missing_warning"]),
|
||||||
|
)
|
||||||
|
}
|
||||||
if (hasProjectPage) {
|
if (hasProjectPage) {
|
||||||
filters.addAll(
|
filters.addAll(
|
||||||
mutableListOf<Filter<*>>(
|
mutableListOf<Filter<*>>(
|
||||||
Filter.Separator(),
|
Filter.Separator(),
|
||||||
Filter.Header("NOTE: Can't be used with other filter!"),
|
Filter.Header(intl["project_filter_warning"]),
|
||||||
Filter.Header("$name Project List page"),
|
Filter.Header(intl.format("project_filter_name", name)),
|
||||||
ProjectFilter(),
|
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 {
|
ext {
|
||||||
extName = 'Manga Galaxy'
|
extName = 'Manga Galaxy'
|
||||||
extClass = '.MangaGalaxy'
|
extClass = '.MangaGalaxy'
|
||||||
themePkg = 'madara'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://mangagalaxy.me'
|
baseUrl = 'https://mangagalaxy.me'
|
||||||
overrideVersionCode = 1
|
overrideVersionCode = 9
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.mangagalaxy
|
package eu.kanade.tachiyomi.extension.en.mangagalaxy
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class MangaGalaxy : Madara(
|
class MangaGalaxy : MangaThemesia(
|
||||||
"Manga Galaxy",
|
"Manga Galaxy",
|
||||||
"https://mangagalaxy.me",
|
"https://mangagalaxy.me",
|
||||||
"en",
|
"en",
|
||||||
dateFormat = SimpleDateFormat("MM/dd/yyyy", Locale.US),
|
mangaUrlDirectory = "/series",
|
||||||
) {
|
) {
|
||||||
override val mangaDetailsSelectorStatus = "div.summary-heading:contains(status) + div.summary-content"
|
// moved from Madara to MangaThemesia
|
||||||
override val mangaDetailsSelectorDescription = "div.summary-heading:contains(Summary) + div"
|
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
|
||||||
|
}
|