Compare commits
No commits in common. "555051eba4fea254f25177714eaac4f16a7b4e6f" and "f6cb65688da0559d695847f7d3c11debe7396343" have entirely different histories.
555051eba4
...
f6cb65688d
|
@ -1,30 +1,8 @@
|
||||||
{
|
{
|
||||||
"extends": [
|
"extends": [
|
||||||
"config:recommended"
|
"config:base"
|
||||||
],
|
],
|
||||||
"schedule": ["on sunday"],
|
|
||||||
"includePaths": [
|
"includePaths": [
|
||||||
"buildSrc/gradle/**",
|
|
||||||
"gradle/**",
|
|
||||||
".github/**"
|
".github/**"
|
||||||
],
|
|
||||||
"ignoreDeps": ["keiyoushi/issue-moderator-action"],
|
|
||||||
"packageRules": [
|
|
||||||
{
|
|
||||||
"matchManagers": ["github-actions"],
|
|
||||||
"groupName": "{{manager}} dependencies"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"matchManagers": ["gradle"],
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"matchPackageNames": [
|
|
||||||
"com.android.tools.build:gradle",
|
|
||||||
"gradle"
|
|
||||||
],
|
|
||||||
"draftPR": true,
|
|
||||||
"enabled": true
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ jobs:
|
||||||
echo ${{ secrets.SIGNING_KEY }} | base64 -d > signingkey.jks
|
echo ${{ secrets.SIGNING_KEY }} | base64 -d > signingkey.jks
|
||||||
|
|
||||||
- name: Set up Gradle
|
- name: Set up Gradle
|
||||||
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0
|
uses: gradle/actions/setup-gradle@v3
|
||||||
|
|
||||||
- name: Build extensions
|
- name: Build extensions
|
||||||
env:
|
env:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
@ -18,6 +18,8 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: use versionCatalogs.named("libs") in Gradle 8.5
|
||||||
|
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(versionCatalogs.named("libs").findBundle("common").get())
|
compileOnly(libs.findBundle("common").get())
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,8 +36,10 @@ kotlinter {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: use versionCatalogs.named("libs") in Gradle 8.5
|
||||||
|
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(versionCatalogs.named("libs").findBundle("common").get())
|
compileOnly(libs.findBundle("common").get())
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
max*)
|
max*)
|
||||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
# shellcheck disable=SC2039,SC3045
|
# shellcheck disable=SC3045
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
warn "Could not query maximum file descriptor limit"
|
warn "Could not query maximum file descriptor limit"
|
||||||
esac
|
esac
|
||||||
|
@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
'' | soft) :;; #(
|
'' | soft) :;; #(
|
||||||
*)
|
*)
|
||||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
# shellcheck disable=SC2039,SC3045
|
# shellcheck disable=SC3045
|
||||||
ulimit -n "$MAX_FD" ||
|
ulimit -n "$MAX_FD" ||
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
esac
|
esac
|
||||||
|
@ -202,11 +202,11 @@ fi
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
# Collect all arguments for the java command;
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||||
# and any embedded shellness will be escaped.
|
# shell script including quotes and variable substitutions, so put them in
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
# double quotes to make sure that they get re-expanded; and
|
||||||
# treated as '${Hostname}' itself on the command line.
|
# * put everything else in single quotes, so that it's not re-expanded.
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
|
|
@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation.
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
|
@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation.
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
plugins {
|
||||||
|
id("lib-multisrc")
|
||||||
|
}
|
||||||
|
|
||||||
|
baseVersionCode = 9
|
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 3
|
baseVersionCode = 2
|
||||||
|
|
|
@ -10,6 +10,9 @@ class BloggerDto(
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class BloggerFeedDto(
|
class BloggerFeedDto(
|
||||||
|
@SerialName("openSearch\$totalResults") val totalResults: BloggerTextDto,
|
||||||
|
@SerialName("openSearch\$startIndex") val startIndex: BloggerTextDto,
|
||||||
|
@SerialName("openSearch\$itemsPerPage") val itemsPerPage: BloggerTextDto,
|
||||||
val category: List<BloggerCategoryDto> = emptyList(),
|
val category: List<BloggerCategoryDto> = emptyList(),
|
||||||
val entry: List<BloggerFeedEntryDto> = emptyList(),
|
val entry: List<BloggerFeedEntryDto> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -65,7 +65,7 @@ abstract class GravureBlogger(
|
||||||
initialized = true
|
initialized = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val hasNextPage = data.feed.entry.size == MAX_RESULTS
|
val hasNextPage = (data.feed.startIndex.t.toInt() + data.feed.itemsPerPage.t.toInt()) <= data.feed.totalResults.t.toInt()
|
||||||
|
|
||||||
return MangasPage(manga, hasNextPage)
|
return MangasPage(manga, hasNextPage)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 11
|
baseVersionCode = 10
|
||||||
|
|
|
@ -60,7 +60,7 @@ class KemonoPostDto(
|
||||||
if (file.path != null) add(KemonoAttachmentDto(file.name!!, file.path))
|
if (file.path != null) add(KemonoAttachmentDto(file.name!!, file.path))
|
||||||
addAll(attachments)
|
addAll(attachments)
|
||||||
}.filter {
|
}.filter {
|
||||||
when (it.path.substringAfterLast('.').lowercase()) {
|
when (it.name.substringAfterLast('.').lowercase()) {
|
||||||
"png", "jpg", "gif", "jpeg", "webp" -> true
|
"png", "jpg", "gif", "jpeg", "webp" -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
@ -91,7 +91,6 @@ class KemonoPostDto(
|
||||||
@Serializable
|
@Serializable
|
||||||
class KemonoFileDto(val name: String? = null, val path: String? = null)
|
class KemonoFileDto(val name: String? = null, val path: String? = null)
|
||||||
|
|
||||||
// name might have ".jpe" extension for JPEG, path might have ".m4v" extension for MP4
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class KemonoAttachmentDto(val name: String, val path: String) {
|
class KemonoAttachmentDto(val name: String, val path: String) {
|
||||||
override fun toString() = "$path?f=$name"
|
override fun toString() = "$path?f=$name"
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
alt_names_heading=Alternative Names:
|
|
||||||
author_filter_title=Author
|
|
||||||
year_filter_title=Year
|
|
||||||
status_filter_title=Status
|
|
||||||
status_filter_option_all=All
|
|
||||||
status_filter_option_ongoing=Ongoing
|
|
||||||
status_filter_option_completed=Completed
|
|
||||||
status_filter_option_hiatus=Hiatus
|
|
||||||
status_filter_option_dropped=Dropped
|
|
||||||
type_filter_title=Type
|
|
||||||
type_filter_option_all=All
|
|
||||||
type_filter_option_manga=Manga
|
|
||||||
type_filter_option_manhwa=Manhwa
|
|
||||||
type_filter_option_manhua=Manhua
|
|
||||||
type_filter_option_comic=Comic
|
|
||||||
order_by_filter_title=Sort By
|
|
||||||
order_by_filter_default=Default
|
|
||||||
order_by_filter_az=A-Z
|
|
||||||
order_by_filter_za=Z-A
|
|
||||||
order_by_filter_latest_update=Latest Update
|
|
||||||
order_by_filter_latest_added=Latest Added
|
|
||||||
order_by_filter_popular=Popular
|
|
||||||
project_filter_title=Filter Project
|
|
||||||
project_filter_all_manga=Show all manga
|
|
||||||
project_filter_only_project=Show only project manga
|
|
||||||
genre_filter_title=Genre
|
|
||||||
genre_missing_warning=Press 'Reset' to attempt to show the genres
|
|
||||||
genre_exclusion_warning=Genre exclusion is not available for all sources
|
|
||||||
project_filter_warning=NOTE: Can't be used with other filter!
|
|
||||||
project_filter_name=%s Project List page
|
|
|
@ -1,23 +0,0 @@
|
||||||
alt_names_heading=Nombres alternativos:
|
|
||||||
author_filter_title=Autor
|
|
||||||
year_filter_title=Año
|
|
||||||
status_filter_title=Estado
|
|
||||||
status_filter_option_all=Todos
|
|
||||||
status_filter_option_ongoing=En curso
|
|
||||||
status_filter_option_completed=Completado
|
|
||||||
status_filter_option_hiatus=En pausa
|
|
||||||
status_filter_option_dropped=Abandonado
|
|
||||||
type_filter_title=Tipo
|
|
||||||
type_filter_option_all=Todos
|
|
||||||
order_by_filter_title=Ordenar por
|
|
||||||
order_by_filter_default=Por defecto
|
|
||||||
order_by_filter_latest_update=Última actualización
|
|
||||||
order_by_filter_latest_added=Último añadido
|
|
||||||
project_filter_title=Filtrar proyectos
|
|
||||||
project_filter_all_manga=Mostrar todos los mangas
|
|
||||||
project_filter_only_project=Mostrar solo los proyectos
|
|
||||||
genre_filter_title=Género
|
|
||||||
genre_missing_warning=Presione 'Restablecer' para intentar cargar los géneros
|
|
||||||
genre_exclusion_warning=La exclusión de géneros puede no funcionar correctamente
|
|
||||||
project_filter_warning=NOTA: ¡No se puede usar con otros filtros!
|
|
||||||
project_filter_name=%s Página de proyectos
|
|
|
@ -2,8 +2,8 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 29
|
baseVersionCode = 28
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:i18n"))
|
api(project(":lib:randomua"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
package eu.kanade.tachiyomi.multisrc.mangathemesia
|
package eu.kanade.tachiyomi.multisrc.mangathemesia
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.lib.i18n.Intl
|
import android.app.Application
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import eu.kanade.tachiyomi.lib.randomua.addRandomUAPreferenceToScreen
|
||||||
|
import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA
|
||||||
|
import eu.kanade.tachiyomi.lib.randomua.getPrefUAType
|
||||||
|
import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
@ -14,56 +21,64 @@ import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.jsonArray
|
import kotlinx.serialization.json.jsonArray
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
import okhttp3.Call
|
|
||||||
import okhttp3.Callback
|
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import org.jsoup.select.Elements
|
import org.jsoup.select.Elements
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.IOException
|
import java.lang.IllegalArgumentException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
// Formerly WPMangaStream & WPMangaReader -> MangaThemesia
|
// Formerly WPMangaStream & WPMangaReader -> MangaThemesia
|
||||||
abstract class MangaThemesia(
|
abstract class MangaThemesia(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val baseUrl: String,
|
override val baseUrl: String,
|
||||||
final override val lang: String,
|
override val lang: String,
|
||||||
val mangaUrlDirectory: String = "/manga",
|
val mangaUrlDirectory: String = "/manga",
|
||||||
val dateFormat: SimpleDateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale.US),
|
val dateFormat: SimpleDateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale.US),
|
||||||
) : ParsedHttpSource() {
|
) : ParsedHttpSource(), ConfigurableSource {
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences by lazy {
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
}
|
||||||
|
|
||||||
protected open val json: Json by injectLazy()
|
protected open val json: Json by injectLazy()
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
override val client = network.cloudflareClient
|
override val client: OkHttpClient by lazy {
|
||||||
|
network.cloudflareClient.newBuilder()
|
||||||
|
.setRandomUserAgent(
|
||||||
|
preferences.getPrefUAType(),
|
||||||
|
preferences.getPrefCustomUA(),
|
||||||
|
)
|
||||||
|
.connectTimeout(10, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
.set("Referer", "$baseUrl/")
|
.set("Referer", "$baseUrl/")
|
||||||
|
|
||||||
protected val intl = Intl(
|
|
||||||
language = lang,
|
|
||||||
baseLanguage = "en",
|
|
||||||
availableLanguages = setOf("en", "es"),
|
|
||||||
classLoader = javaClass.classLoader!!,
|
|
||||||
)
|
|
||||||
|
|
||||||
open val projectPageString = "/project"
|
open val projectPageString = "/project"
|
||||||
|
|
||||||
// Popular (Search with popular order and nothing else)
|
// Popular (Search with popular order and nothing else)
|
||||||
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", popularFilter)
|
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", FilterList(OrderByFilter("popular")))
|
||||||
override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
||||||
|
|
||||||
// Latest (Search with update order and nothing else)
|
// Latest (Search with update order and nothing else)
|
||||||
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", latestFilter)
|
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", FilterList(OrderByFilter("update")))
|
||||||
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
|
@ -151,96 +166,22 @@ abstract class MangaThemesia(
|
||||||
override fun searchMangaNextPageSelector() = "div.pagination .next, div.hpage .r"
|
override fun searchMangaNextPageSelector() = "div.pagination .next, div.hpage .r"
|
||||||
|
|
||||||
// Manga details
|
// Manga details
|
||||||
private fun selector(selector: String, contains: List<String>): String {
|
|
||||||
return contains.joinToString(", ") { selector.replace("%s", it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
open val seriesDetailsSelector = "div.bigcontent, div.animefull, div.main-info, div.postbody"
|
open val seriesDetailsSelector = "div.bigcontent, div.animefull, div.main-info, div.postbody"
|
||||||
|
open val seriesTitleSelector = "h1.entry-title"
|
||||||
open val seriesTitleSelector = "h1.entry-title, .ts-breadcrumb li:last-child span"
|
open val seriesArtistSelector = ".infotable tr:contains(artist) td:last-child, .tsinfo .imptdt:contains(artist) i, .fmed b:contains(artist)+span, span:contains(artist)"
|
||||||
|
open val seriesAuthorSelector = ".infotable tr:contains(author) td:last-child, .tsinfo .imptdt:contains(author) i, .fmed b:contains(author)+span, span:contains(author)"
|
||||||
open val seriesArtistSelector = selector(
|
|
||||||
".infotable tr:contains(%s) td:last-child, .tsinfo .imptdt:contains(%s) i, .fmed b:contains(%s)+span, span:contains(%s)",
|
|
||||||
listOf(
|
|
||||||
"artist",
|
|
||||||
"Artiste",
|
|
||||||
"Artista",
|
|
||||||
"الرسام",
|
|
||||||
"الناشر",
|
|
||||||
"İllüstratör",
|
|
||||||
"Çizer",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
open val seriesAuthorSelector = selector(
|
|
||||||
".infotable tr:contains(%s) td:last-child, .tsinfo .imptdt:contains(%s) i, .fmed b:contains(%s)+span, span:contains(%s)",
|
|
||||||
listOf(
|
|
||||||
"Author",
|
|
||||||
"Auteur",
|
|
||||||
"autor",
|
|
||||||
"المؤلف",
|
|
||||||
"Mangaka",
|
|
||||||
"seniman",
|
|
||||||
"Pengarang",
|
|
||||||
"Yazar",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
open val seriesDescriptionSelector = ".desc, .entry-content[itemprop=description]"
|
open val seriesDescriptionSelector = ".desc, .entry-content[itemprop=description]"
|
||||||
|
open val seriesAltNameSelector = ".alternative, .wd-full:contains(alt) span, .alter, .seriestualt"
|
||||||
open val seriesAltNameSelector = ".alternative, .wd-full:contains(alt) span, .alter, .seriestualt, " +
|
open val seriesGenreSelector = "div.gnr a, .mgen a, .seriestugenre a, span:contains(genre)"
|
||||||
selector(
|
open val seriesTypeSelector = ".infotable tr:contains(type) td:last-child, .tsinfo .imptdt:contains(type) i, .tsinfo .imptdt:contains(type) a, .fmed b:contains(type)+span, span:contains(type) a, a[href*=type\\=]"
|
||||||
".infotable tr:contains(%s) td:last-child",
|
open val seriesStatusSelector = ".infotable tr:contains(status) td:last-child, .tsinfo .imptdt:contains(status) i, .fmed b:contains(status)+span span:contains(status)"
|
||||||
listOf(
|
|
||||||
"Alternative",
|
|
||||||
"Alternatif",
|
|
||||||
"الأسماء الثانوية",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
open val seriesGenreSelector = "div.gnr a, .mgen a, .seriestugenre a, " +
|
|
||||||
selector(
|
|
||||||
"span:contains(%s)",
|
|
||||||
listOf(
|
|
||||||
"genre",
|
|
||||||
"التصنيف",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
open val seriesTypeSelector = selector(
|
|
||||||
".infotable tr:contains(%s) td:last-child, .tsinfo .imptdt:contains(%s) i, .tsinfo .imptdt:contains(%s) a, .fmed b:contains(%s)+span, span:contains(%s) a",
|
|
||||||
listOf(
|
|
||||||
"type",
|
|
||||||
"ประเภท",
|
|
||||||
"النوع",
|
|
||||||
"tipe",
|
|
||||||
"Türü",
|
|
||||||
),
|
|
||||||
) + ", a[href*=type\\=]"
|
|
||||||
|
|
||||||
open val seriesStatusSelector = selector(
|
|
||||||
".infotable tr:contains(%s) td:last-child, .tsinfo .imptdt:contains(%s) i, .fmed b:contains(%s)+span span:contains(%s)",
|
|
||||||
listOf(
|
|
||||||
"status",
|
|
||||||
"Statut",
|
|
||||||
"Durum",
|
|
||||||
"連載状況",
|
|
||||||
"Estado",
|
|
||||||
"الحالة",
|
|
||||||
"حالة العمل",
|
|
||||||
"สถานะ",
|
|
||||||
"stato",
|
|
||||||
"Statüsü",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
open val seriesThumbnailSelector = ".infomanga > div[itemprop=image] img, .thumb img"
|
open val seriesThumbnailSelector = ".infomanga > div[itemprop=image] img, .thumb img"
|
||||||
|
|
||||||
open val altNamePrefix = "${intl["alt_names_heading"]} "
|
open val altNamePrefix = "Alternative Name: "
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||||
document.selectFirst(seriesDetailsSelector)?.let { seriesDetails ->
|
document.selectFirst(seriesDetailsSelector)?.let { seriesDetails ->
|
||||||
title = seriesDetails.selectFirst(seriesTitleSelector)!!.text()
|
title = seriesDetails.selectFirst(seriesTitleSelector)?.text().orEmpty()
|
||||||
artist = seriesDetails.selectFirst(seriesArtistSelector)?.ownText().removeEmptyPlaceholder()
|
artist = seriesDetails.selectFirst(seriesArtistSelector)?.ownText().removeEmptyPlaceholder()
|
||||||
author = seriesDetails.selectFirst(seriesAuthorSelector)?.ownText().removeEmptyPlaceholder()
|
author = seriesDetails.selectFirst(seriesAuthorSelector)?.ownText().removeEmptyPlaceholder()
|
||||||
description = seriesDetails.select(seriesDescriptionSelector).joinToString("\n") { it.text() }.trim()
|
description = seriesDetails.select(seriesDescriptionSelector).joinToString("\n") { it.text() }.trim()
|
||||||
|
@ -269,32 +210,16 @@ abstract class MangaThemesia(
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun String?.removeEmptyPlaceholder(): String? {
|
protected fun String?.removeEmptyPlaceholder(): String? {
|
||||||
return if (this.isNullOrBlank() || this == "-" || this == "N/A" || this == "n/a") null else this
|
return if (this.isNullOrBlank() || this == "-" || this == "N/A") null else this
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun String?.parseStatus(): Int {
|
open fun String?.parseStatus(): Int = when {
|
||||||
if (this == null) return SManga.UNKNOWN
|
this == null -> SManga.UNKNOWN
|
||||||
|
listOf("ongoing", "publishing").any { this.contains(it, ignoreCase = true) } -> SManga.ONGOING
|
||||||
return when (this.lowercase().trim()) {
|
this.contains("hiatus", ignoreCase = true) -> SManga.ON_HIATUS
|
||||||
"مستمرة", "en curso", "ongoing", "on going", "ativo", "en cours",
|
this.contains("completed", ignoreCase = true) -> SManga.COMPLETED
|
||||||
"en cours de publication", "đang tiến hành", "em lançamento", "онгоінг", "publishing",
|
listOf("dropped", "cancelled").any { this.contains(it, ignoreCase = true) } -> SManga.CANCELLED
|
||||||
"devam ediyor", "em andamento", "in corso", "güncel", "berjalan", "продолжается", "updating", "lançando", "in arrivo", "emision",
|
else -> SManga.UNKNOWN
|
||||||
"en emision", "مستمر", "curso", "en marcha", "publicandose", "publicando", "连载中", "devam etmekte", "連載中",
|
|
||||||
-> SManga.ONGOING
|
|
||||||
|
|
||||||
"completed", "completo", "complété", "fini", "achevé", "terminé", "tamamlandı", "đã hoàn thành", "hoàn thành",
|
|
||||||
"مكتملة", "завершено", "finished", "finalizado", "completata", "one-shot", "bitti", "tamat", "completado", "concluído", "完結",
|
|
||||||
"concluido", "已完结", "bitmiş",
|
|
||||||
-> SManga.COMPLETED
|
|
||||||
|
|
||||||
"canceled", "cancelled", "cancelado", "cancellato", "cancelados", "dropped", "discontinued", "abandonné",
|
|
||||||
-> SManga.CANCELLED
|
|
||||||
|
|
||||||
"hiatus", "on hold", "pausado", "en espera", "en pause", "en attente",
|
|
||||||
-> SManga.ON_HIATUS
|
|
||||||
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chapter list
|
// Chapter list
|
||||||
|
@ -302,9 +227,6 @@ abstract class MangaThemesia(
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
|
|
||||||
countViews(document)
|
|
||||||
|
|
||||||
val chapters = document.select(chapterListSelector()).map { chapterFromElement(it) }
|
val chapters = document.select(chapterListSelector()).map { chapterFromElement(it) }
|
||||||
|
|
||||||
// Add timestamp to latest chapter, taken from "Updated On".
|
// Add timestamp to latest chapter, taken from "Updated On".
|
||||||
|
@ -316,6 +238,8 @@ abstract class MangaThemesia(
|
||||||
if (date.isNotEmpty()) chapters.first().date_upload = parseUpdatedOnDate(date)
|
if (date.isNotEmpty()) chapters.first().date_upload = parseUpdatedOnDate(date)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
countViews(document)
|
||||||
|
|
||||||
return chapters
|
return chapters
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -343,13 +267,13 @@ abstract class MangaThemesia(
|
||||||
open val pageSelector = "div#readerarea img"
|
open val pageSelector = "div#readerarea img"
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
countViews(document)
|
|
||||||
|
|
||||||
val chapterUrl = document.location()
|
val chapterUrl = document.location()
|
||||||
val htmlPages = document.select(pageSelector)
|
val htmlPages = document.select(pageSelector)
|
||||||
.filterNot { it.imgAttr().isEmpty() }
|
.filterNot { it.imgAttr().isEmpty() }
|
||||||
.mapIndexed { i, img -> Page(i, chapterUrl, img.imgAttr()) }
|
.mapIndexed { i, img -> Page(i, chapterUrl, img.imgAttr()) }
|
||||||
|
|
||||||
|
countViews(document)
|
||||||
|
|
||||||
// Some sites also loads pages via javascript
|
// Some sites also loads pages via javascript
|
||||||
if (htmlPages.isNotEmpty()) { return htmlPages }
|
if (htmlPages.isNotEmpty()) { return htmlPages }
|
||||||
|
|
||||||
|
@ -396,6 +320,8 @@ abstract class MangaThemesia(
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val newHeaders = headersBuilder()
|
val newHeaders = headersBuilder()
|
||||||
|
.set("Content-Length", formBody.contentLength().toString())
|
||||||
|
.set("Content-Type", formBody.contentType().toString())
|
||||||
.set("Referer", document.location())
|
.set("Referer", document.location())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
@ -413,22 +339,17 @@ abstract class MangaThemesia(
|
||||||
}
|
}
|
||||||
|
|
||||||
val request = countViewsRequest(document) ?: return
|
val request = countViewsRequest(document) ?: return
|
||||||
val callback = object : Callback {
|
runCatching { client.newCall(request).execute().close() }
|
||||||
override fun onResponse(call: Call, response: Response) = response.close()
|
|
||||||
override fun onFailure(call: Call, e: IOException) = Unit
|
|
||||||
}
|
|
||||||
|
|
||||||
client.newCall(request).enqueue(callback)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
protected class AuthorFilter(name: String) : Filter.Text(name)
|
protected class AuthorFilter : Filter.Text("Author")
|
||||||
|
|
||||||
protected class YearFilter(name: String) : Filter.Text(name)
|
protected class YearFilter : Filter.Text("Year")
|
||||||
|
|
||||||
open class SelectFilter(
|
open class SelectFilter(
|
||||||
displayName: String,
|
displayName: String,
|
||||||
private val vals: Array<Pair<String, String>>,
|
val vals: Array<Pair<String, String>>,
|
||||||
defaultValue: String? = null,
|
defaultValue: String? = null,
|
||||||
) : Filter.Select<String>(
|
) : Filter.Select<String>(
|
||||||
displayName,
|
displayName,
|
||||||
|
@ -438,91 +359,63 @@ abstract class MangaThemesia(
|
||||||
fun selectedValue() = vals[state].second
|
fun selectedValue() = vals[state].second
|
||||||
}
|
}
|
||||||
|
|
||||||
protected class StatusFilter(
|
protected class StatusFilter : SelectFilter(
|
||||||
name: String,
|
"Status",
|
||||||
options: Array<Pair<String, String>>,
|
arrayOf(
|
||||||
) : SelectFilter(
|
Pair("All", ""),
|
||||||
name,
|
Pair("Ongoing", "ongoing"),
|
||||||
options,
|
Pair("Completed", "completed"),
|
||||||
|
Pair("Hiatus", "hiatus"),
|
||||||
|
Pair("Dropped", "dropped"),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
protected open val statusOptions = arrayOf(
|
protected class TypeFilter : SelectFilter(
|
||||||
Pair(intl["status_filter_option_all"], ""),
|
"Type",
|
||||||
Pair(intl["status_filter_option_ongoing"], "ongoing"),
|
arrayOf(
|
||||||
Pair(intl["status_filter_option_completed"], "completed"),
|
Pair("All", ""),
|
||||||
Pair(intl["status_filter_option_hiatus"], "hiatus"),
|
Pair("Manga", "Manga"),
|
||||||
Pair(intl["status_filter_option_dropped"], "dropped"),
|
Pair("Manhwa", "Manhwa"),
|
||||||
|
Pair("Manhua", "Manhua"),
|
||||||
|
Pair("Comic", "Comic"),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
protected class TypeFilter(
|
protected class OrderByFilter(defaultOrder: String? = null) : SelectFilter(
|
||||||
name: String,
|
"Sort By",
|
||||||
options: Array<Pair<String, String>>,
|
arrayOf(
|
||||||
) : SelectFilter(
|
Pair("Default", ""),
|
||||||
name,
|
Pair("A-Z", "title"),
|
||||||
options,
|
Pair("Z-A", "titlereverse"),
|
||||||
)
|
Pair("Latest Update", "update"),
|
||||||
|
Pair("Latest Added", "latest"),
|
||||||
protected open val typeFilterOptions = arrayOf(
|
Pair("Popular", "popular"),
|
||||||
Pair(intl["type_filter_option_all"], ""),
|
),
|
||||||
Pair(intl["type_filter_option_manga"], "Manga"),
|
|
||||||
Pair(intl["type_filter_option_manhwa"], "Manhwa"),
|
|
||||||
Pair(intl["type_filter_option_manhua"], "Manhua"),
|
|
||||||
Pair(intl["type_filter_option_comic"], "Comic"),
|
|
||||||
)
|
|
||||||
|
|
||||||
protected class OrderByFilter(
|
|
||||||
name: String,
|
|
||||||
options: Array<Pair<String, String>>,
|
|
||||||
defaultOrder: String? = null,
|
|
||||||
) : SelectFilter(
|
|
||||||
name,
|
|
||||||
options,
|
|
||||||
defaultOrder,
|
defaultOrder,
|
||||||
)
|
)
|
||||||
|
|
||||||
protected open val orderByFilterOptions = arrayOf(
|
protected class ProjectFilter : SelectFilter(
|
||||||
Pair(intl["order_by_filter_default"], ""),
|
"Filter Project",
|
||||||
Pair(intl["order_by_filter_az"], "title"),
|
arrayOf(
|
||||||
Pair(intl["order_by_filter_za"], "titlereverse"),
|
Pair("Show all manga", ""),
|
||||||
Pair(intl["order_by_filter_latest_update"], "update"),
|
Pair("Show only project manga", "project-filter-on"),
|
||||||
Pair(intl["order_by_filter_latest_added"], "latest"),
|
),
|
||||||
Pair(intl["order_by_filter_popular"], "popular"),
|
|
||||||
)
|
|
||||||
|
|
||||||
protected val popularFilter by lazy { FilterList(OrderByFilter("", orderByFilterOptions, "popular")) }
|
|
||||||
protected val latestFilter by lazy { FilterList(OrderByFilter("", orderByFilterOptions, "update")) }
|
|
||||||
|
|
||||||
protected class ProjectFilter(
|
|
||||||
name: String,
|
|
||||||
options: Array<Pair<String, String>>,
|
|
||||||
) : SelectFilter(
|
|
||||||
name,
|
|
||||||
options,
|
|
||||||
)
|
|
||||||
|
|
||||||
protected open val projectFilterOptions = arrayOf(
|
|
||||||
Pair(intl["project_filter_all_manga"], ""),
|
|
||||||
Pair(intl["project_filter_only_project"], "project-filter-on"),
|
|
||||||
)
|
|
||||||
|
|
||||||
protected class GenreData(
|
|
||||||
val name: String,
|
|
||||||
val value: String,
|
|
||||||
val state: Int = Filter.TriState.STATE_IGNORE,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
protected class Genre(
|
protected class Genre(
|
||||||
name: String,
|
name: String,
|
||||||
val value: String,
|
val value: String,
|
||||||
state: Int,
|
state: Int = STATE_IGNORE,
|
||||||
) : Filter.TriState(name, state)
|
) : Filter.TriState(name, state)
|
||||||
|
|
||||||
protected class GenreListFilter(name: String, genres: List<Genre>) : Filter.Group<Genre>(name, genres)
|
protected class GenreListFilter(genres: List<Genre>) : Filter.Group<Genre>("Genre", genres)
|
||||||
|
|
||||||
protected var genrelist: List<GenreData>? = null
|
|
||||||
|
|
||||||
|
private var genrelist: List<Genre>? = null
|
||||||
protected open fun getGenreList(): List<Genre> {
|
protected open fun getGenreList(): List<Genre> {
|
||||||
return genrelist?.map { Genre(it.name, it.value, it.state) }.orEmpty()
|
// Filters are fetched immediately once an extension loads
|
||||||
|
// We're only able to get filters after a loading the manga directory,
|
||||||
|
// and resetting the filters is the only thing that seems to reinflate the view
|
||||||
|
return genrelist ?: listOf(Genre("Press reset to attempt to fetch genres", ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
open val hasProjectPage = false
|
open val hasProjectPage = false
|
||||||
|
@ -530,31 +423,21 @@ abstract class MangaThemesia(
|
||||||
override fun getFilterList(): FilterList {
|
override fun getFilterList(): FilterList {
|
||||||
val filters = mutableListOf<Filter<*>>(
|
val filters = mutableListOf<Filter<*>>(
|
||||||
Filter.Separator(),
|
Filter.Separator(),
|
||||||
AuthorFilter(intl["author_filter_title"]),
|
AuthorFilter(),
|
||||||
YearFilter(intl["year_filter_title"]),
|
YearFilter(),
|
||||||
StatusFilter(intl["status_filter_title"], statusOptions),
|
StatusFilter(),
|
||||||
TypeFilter(intl["type_filter_title"], typeFilterOptions),
|
TypeFilter(),
|
||||||
OrderByFilter(intl["order_by_filter_title"], orderByFilterOptions),
|
OrderByFilter(),
|
||||||
|
Filter.Header("Genre exclusion is not available for all sources"),
|
||||||
|
GenreListFilter(getGenreList()),
|
||||||
)
|
)
|
||||||
if (!genrelist.isNullOrEmpty()) {
|
|
||||||
filters.addAll(
|
|
||||||
listOf(
|
|
||||||
Filter.Header(intl["genre_exclusion_warning"]),
|
|
||||||
GenreListFilter(intl["genre_filter_title"], getGenreList()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
filters.add(
|
|
||||||
Filter.Header(intl["genre_missing_warning"]),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (hasProjectPage) {
|
if (hasProjectPage) {
|
||||||
filters.addAll(
|
filters.addAll(
|
||||||
mutableListOf<Filter<*>>(
|
mutableListOf<Filter<*>>(
|
||||||
Filter.Separator(),
|
Filter.Separator(),
|
||||||
Filter.Header(intl["project_filter_warning"]),
|
Filter.Header("NOTE: Can't be used with other filter!"),
|
||||||
Filter.Header(intl.format("project_filter_name", name)),
|
Filter.Header("$name Project List page"),
|
||||||
ProjectFilter(intl["project_filter_title"], projectFilterOptions),
|
ProjectFilter(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -602,9 +485,9 @@ abstract class MangaThemesia(
|
||||||
(!strict && url.pathSegments.size == n + 1 && url.pathSegments[n].isEmpty())
|
(!strict && url.pathSegments.size == n + 1 && url.pathSegments[n].isEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseGenres(document: Document): List<GenreData>? {
|
private fun parseGenres(document: Document): List<Genre>? {
|
||||||
return document.selectFirst("ul.genrez")?.select("li")?.map { li ->
|
return document.selectFirst("ul.genrez")?.select("li")?.map { li ->
|
||||||
GenreData(
|
Genre(
|
||||||
li.selectFirst("label")!!.text(),
|
li.selectFirst("label")!!.text(),
|
||||||
li.selectFirst("input[type=checkbox]")!!.attr("value"),
|
li.selectFirst("input[type=checkbox]")!!.attr("value"),
|
||||||
)
|
)
|
||||||
|
@ -631,10 +514,15 @@ abstract class MangaThemesia(
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
addRandomUAPreferenceToScreen(screen)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val URL_SEARCH_PREFIX = "url:"
|
const val URL_SEARCH_PREFIX = "url:"
|
||||||
|
|
||||||
// More info: https://issuetracker.google.com/issues/36970498
|
// More info: https://issuetracker.google.com/issues/36970498
|
||||||
|
@Suppress("RegExpRedundantEscape")
|
||||||
private val MANGA_PAGE_ID_REGEX = "post_id\\s*:\\s*(\\d+)\\}".toRegex()
|
private val MANGA_PAGE_ID_REGEX = "post_id\\s*:\\s*(\\d+)\\}".toRegex()
|
||||||
private val CHAPTER_PAGE_ID_REGEX = "chapter_id\\s*=\\s*(\\d+);".toRegex()
|
private val CHAPTER_PAGE_ID_REGEX = "chapter_id\\s*=\\s*(\\d+);".toRegex()
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ data class PizzaReaderDto(
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PizzaComicDto(
|
data class PizzaComicDto(
|
||||||
val artist: String? = null,
|
val artist: String = "",
|
||||||
val author: String = "",
|
val author: String = "",
|
||||||
val chapters: List<PizzaChapterDto> = emptyList(),
|
val chapters: List<PizzaChapterDto> = emptyList(),
|
||||||
val description: String = "",
|
val description: String = "",
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
<application>
|
<application>
|
||||||
<activity
|
<activity
|
||||||
android:name=".pt.huntersscans.HuntersScansUrlActivity"
|
android:name="eu.kanade.tachiyomi.multisrc.bilibili.BilibiliUrlActivity"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@android:style/Theme.NoDisplay">
|
android:theme="@android:style/Theme.NoDisplay">
|
||||||
|
@ -13,9 +13,13 @@
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data android:host="huntersscan.xyz" />
|
|
||||||
<data android:scheme="https" />
|
<data android:scheme="https" />
|
||||||
<data android:pathPattern="/manga/..*" />
|
|
||||||
|
<data android:host="bilibilicomics.com" />
|
||||||
|
<data android:host="m.bilibilicomics.com" />
|
||||||
|
<data android:host="www.bilibilicomics.com" />
|
||||||
|
|
||||||
|
<data android:pathPattern="/detail/mc..*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
|
@ -0,0 +1,9 @@
|
||||||
|
ext {
|
||||||
|
extName = 'BILIBILI COMICS'
|
||||||
|
extClass = '.BilibiliComicsFactory'
|
||||||
|
themePkg = 'bilibili'
|
||||||
|
baseUrl = 'https://www.bilibilicomics.com'
|
||||||
|
overrideVersionCode = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
|
@ -0,0 +1,411 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.bilibilicomics
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.bilibili.Bilibili
|
||||||
|
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliAccessToken
|
||||||
|
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliAccessTokenCookie
|
||||||
|
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliComicDto
|
||||||
|
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliCredential
|
||||||
|
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliGetCredential
|
||||||
|
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliIntl
|
||||||
|
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliSearchDto
|
||||||
|
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliTag
|
||||||
|
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliUnlockedEpisode
|
||||||
|
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliUserEpisodes
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import okhttp3.Response
|
||||||
|
import okio.Buffer
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.URLDecoder
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
|
class BilibiliComicsFactory : SourceFactory {
|
||||||
|
override fun createSources() = listOf(
|
||||||
|
BilibiliComicsEn(),
|
||||||
|
BilibiliComicsCn(),
|
||||||
|
BilibiliComicsId(),
|
||||||
|
BilibiliComicsEs(),
|
||||||
|
BilibiliComicsFr(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class BilibiliComics(lang: String) : Bilibili(
|
||||||
|
"BILIBILI COMICS",
|
||||||
|
"https://www.bilibilicomics.com",
|
||||||
|
lang,
|
||||||
|
) {
|
||||||
|
|
||||||
|
override val client: OkHttpClient = super.client.newBuilder()
|
||||||
|
.apply { interceptors().add(0, Interceptor { chain -> signedInIntercept(chain) }) }
|
||||||
|
.build()
|
||||||
|
|
||||||
|
init {
|
||||||
|
setAccessTokenCookie(baseUrl.toHttpUrl())
|
||||||
|
}
|
||||||
|
|
||||||
|
override val signedIn: Boolean
|
||||||
|
get() = accessTokenCookie != null
|
||||||
|
|
||||||
|
private val globalApiSubDomain: String
|
||||||
|
get() = GLOBAL_API_SUBDOMAINS[(accessTokenCookie?.area?.toIntOrNull() ?: 1) - 1]
|
||||||
|
|
||||||
|
private val globalApiBaseUrl: String
|
||||||
|
get() = "https://$globalApiSubDomain.bilibilicomics.com"
|
||||||
|
|
||||||
|
private var accessTokenCookie: BilibiliAccessTokenCookie? = null
|
||||||
|
|
||||||
|
private val dayOfWeek: Int
|
||||||
|
get() = Calendar.getInstance().get(Calendar.DAY_OF_WEEK) - 1
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
val jsonPayload = buildJsonObject { put("day", dayOfWeek) }
|
||||||
|
val requestBody = jsonPayload.toString().toRequestBody(JSON_MEDIA_TYPE)
|
||||||
|
|
||||||
|
val newHeaders = headersBuilder()
|
||||||
|
.add("Content-Length", requestBody.contentLength().toString())
|
||||||
|
.add("Content-Type", requestBody.contentType().toString())
|
||||||
|
.set("Referer", "$baseUrl/schedule")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val apiUrl = "$baseUrl/$API_COMIC_V1_COMIC_ENDPOINT/GetSchedule".toHttpUrl().newBuilder()
|
||||||
|
.addCommonParameters()
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
return POST(apiUrl, newHeaders, requestBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
|
val result = response.parseAs<BilibiliSearchDto>()
|
||||||
|
|
||||||
|
if (result.code != 0) {
|
||||||
|
return MangasPage(emptyList(), hasNextPage = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
val comicList = result.data!!.list.map(::latestMangaFromObject)
|
||||||
|
|
||||||
|
return MangasPage(comicList, hasNextPage = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun latestMangaFromObject(comic: BilibiliComicDto): SManga = SManga.create().apply {
|
||||||
|
title = comic.title
|
||||||
|
thumbnail_url = comic.verticalCover + THUMBNAIL_RESOLUTION
|
||||||
|
url = "/detail/mc${comic.comicId}"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
if (!signedIn) {
|
||||||
|
return super.chapterListParse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = response.parseAs<BilibiliComicDto>()
|
||||||
|
|
||||||
|
if (result.code != 0) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val comic = result.data!!
|
||||||
|
|
||||||
|
val userEpisodesRequest = userEpisodesRequest(comic.id)
|
||||||
|
val userEpisodesResponse = client.newCall(userEpisodesRequest).execute()
|
||||||
|
val unlockedEpisodes = userEpisodesParse(userEpisodesResponse)
|
||||||
|
|
||||||
|
return comic.episodeList.map { ep -> chapterFromObject(ep, comic.id, isUnlocked = ep.id in unlockedEpisodes) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun userEpisodesRequest(comicId: Int): Request {
|
||||||
|
val jsonPayload = buildJsonObject { put("comic_id", comicId) }
|
||||||
|
val requestBody = jsonPayload.toString().toRequestBody(JSON_MEDIA_TYPE)
|
||||||
|
|
||||||
|
val newHeaders = headersBuilder()
|
||||||
|
.set("Referer", baseUrl)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val apiUrl = "$globalApiBaseUrl/$API_COMIC_V1_USER_ENDPOINT/GetUserEpisodes".toHttpUrl()
|
||||||
|
.newBuilder()
|
||||||
|
.addCommonParameters()
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
return POST(apiUrl, newHeaders, requestBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun userEpisodesParse(response: Response): List<Int> {
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = response.parseAs<BilibiliUserEpisodes>()
|
||||||
|
|
||||||
|
if (result.code != 0) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data!!.unlockedEpisodes.orEmpty()
|
||||||
|
.map(BilibiliUnlockedEpisode::id)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
|
if (!signedIn) {
|
||||||
|
return super.pageListRequest(chapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
val chapterPaths = (baseUrl + chapter.url).toHttpUrl().pathSegments
|
||||||
|
val comicId = chapterPaths[0].removePrefix("mc").toInt()
|
||||||
|
val episodeId = chapterPaths[1].toInt()
|
||||||
|
|
||||||
|
val jsonPayload = BilibiliGetCredential(comicId, episodeId, 1)
|
||||||
|
val requestBody = json.encodeToString(jsonPayload).toRequestBody(JSON_MEDIA_TYPE)
|
||||||
|
|
||||||
|
val newHeaders = headersBuilder()
|
||||||
|
.set("Referer", baseUrl + chapter.url)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val apiUrl = "$globalApiBaseUrl/$API_GLOBAL_V1_USER_ENDPOINT/GetCredential".toHttpUrl()
|
||||||
|
.newBuilder()
|
||||||
|
.addCommonParameters()
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
return POST(apiUrl, newHeaders, requestBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
if (!signedIn) {
|
||||||
|
return super.pageListParse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
throw Exception(intl.failedToGetCredential)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = response.parseAs<BilibiliCredential>()
|
||||||
|
val credential = result.data?.credential ?: ""
|
||||||
|
|
||||||
|
val requestPayload = response.request.bodyString
|
||||||
|
val credentialInfo = json.decodeFromString<BilibiliGetCredential>(requestPayload)
|
||||||
|
val chapterUrl = "/mc${credentialInfo.comicId}/${credentialInfo.episodeId}"
|
||||||
|
|
||||||
|
val imageIndexRequest = imageIndexRequest(chapterUrl, credential)
|
||||||
|
val imageIndexResponse = client.newCall(imageIndexRequest).execute()
|
||||||
|
|
||||||
|
return super.pageListParse(imageIndexResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setAccessTokenCookie(url: HttpUrl) {
|
||||||
|
val authCookie = client.cookieJar.loadForRequest(url)
|
||||||
|
.firstOrNull { cookie -> cookie.name == ACCESS_TOKEN_COOKIE_NAME }
|
||||||
|
?.let { cookie -> URLDecoder.decode(cookie.value, "UTF-8") }
|
||||||
|
?.let { jsonString -> json.decodeFromString<BilibiliAccessTokenCookie>(jsonString) }
|
||||||
|
|
||||||
|
if (accessTokenCookie == null) {
|
||||||
|
accessTokenCookie = authCookie
|
||||||
|
} else if (authCookie == null) {
|
||||||
|
accessTokenCookie = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun signedInIntercept(chain: Interceptor.Chain): Response {
|
||||||
|
var request = chain.request()
|
||||||
|
val requestUrl = request.url.toString()
|
||||||
|
|
||||||
|
if (!requestUrl.contains("bilibilicomics.com")) {
|
||||||
|
return chain.proceed(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
setAccessTokenCookie(request.url)
|
||||||
|
|
||||||
|
if (!accessTokenCookie?.accessToken.isNullOrEmpty()) {
|
||||||
|
request = request.newBuilder()
|
||||||
|
.addHeader("Authorization", "Bearer ${accessTokenCookie!!.accessToken}")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = chain.proceed(request)
|
||||||
|
|
||||||
|
// Try to refresh the token if it expired.
|
||||||
|
if (response.code == 401 && !accessTokenCookie?.refreshToken.isNullOrEmpty()) {
|
||||||
|
response.close()
|
||||||
|
|
||||||
|
val refreshTokenRequest = refreshTokenRequest(
|
||||||
|
accessTokenCookie!!.accessToken,
|
||||||
|
accessTokenCookie!!.refreshToken,
|
||||||
|
)
|
||||||
|
val refreshTokenResponse = chain.proceed(refreshTokenRequest)
|
||||||
|
|
||||||
|
accessTokenCookie = refreshTokenParse(refreshTokenResponse)
|
||||||
|
refreshTokenResponse.close()
|
||||||
|
|
||||||
|
request = request.newBuilder()
|
||||||
|
.header("Authorization", "Bearer ${accessTokenCookie!!.accessToken}")
|
||||||
|
.build()
|
||||||
|
return chain.proceed(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshTokenRequest(accessToken: String, refreshToken: String): Request {
|
||||||
|
val jsonPayload = buildJsonObject { put("refresh_token", refreshToken) }
|
||||||
|
val requestBody = jsonPayload.toString().toRequestBody(JSON_MEDIA_TYPE)
|
||||||
|
|
||||||
|
val newHeaders = headersBuilder()
|
||||||
|
.add("Authorization", "Bearer $accessToken")
|
||||||
|
.set("Referer", baseUrl)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val apiUrl = "$globalApiBaseUrl/$API_GLOBAL_V1_USER_ENDPOINT/RefreshToken".toHttpUrl()
|
||||||
|
.newBuilder()
|
||||||
|
.addCommonParameters()
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
return POST(apiUrl, newHeaders, requestBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshTokenParse(response: Response): BilibiliAccessTokenCookie {
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
throw IOException(intl.failedToRefreshToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = response.parseAs<BilibiliAccessToken>()
|
||||||
|
|
||||||
|
if (result.code != 0) {
|
||||||
|
throw IOException(intl.failedToRefreshToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
val accessToken = result.data!!
|
||||||
|
|
||||||
|
return BilibiliAccessTokenCookie(
|
||||||
|
accessToken.accessToken,
|
||||||
|
accessToken.refreshToken,
|
||||||
|
accessTokenCookie!!.area,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val Request.bodyString: String
|
||||||
|
get() {
|
||||||
|
val requestCopy = newBuilder().build()
|
||||||
|
val buffer = Buffer()
|
||||||
|
|
||||||
|
return runCatching { buffer.apply { requestCopy.body!!.writeTo(this) }.readUtf8() }
|
||||||
|
.getOrNull() ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ACCESS_TOKEN_COOKIE_NAME = "access_token"
|
||||||
|
|
||||||
|
private val GLOBAL_API_SUBDOMAINS = arrayOf("us-user", "sg-user")
|
||||||
|
private const val API_GLOBAL_V1_USER_ENDPOINT = "twirp/global.v1.User"
|
||||||
|
private const val API_COMIC_V1_USER_ENDPOINT = "twirp/comic.v1.User"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BilibiliComicsEn : BilibiliComics(BilibiliIntl.ENGLISH) {
|
||||||
|
|
||||||
|
override fun getAllGenres(): Array<BilibiliTag> = arrayOf(
|
||||||
|
BilibiliTag("All", -1),
|
||||||
|
BilibiliTag("Action", 19),
|
||||||
|
BilibiliTag("Adventure", 22),
|
||||||
|
BilibiliTag("BL", 3),
|
||||||
|
BilibiliTag("Comedy", 14),
|
||||||
|
BilibiliTag("Eastern", 30),
|
||||||
|
BilibiliTag("Fantasy", 11),
|
||||||
|
BilibiliTag("GL", 16),
|
||||||
|
BilibiliTag("Harem", 15),
|
||||||
|
BilibiliTag("Historical", 12),
|
||||||
|
BilibiliTag("Horror", 23),
|
||||||
|
BilibiliTag("Mistery", 17),
|
||||||
|
BilibiliTag("Romance", 13),
|
||||||
|
BilibiliTag("Slice of Life", 21),
|
||||||
|
BilibiliTag("Suspense", 41),
|
||||||
|
BilibiliTag("Teen", 20),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class BilibiliComicsCn : BilibiliComics(BilibiliIntl.SIMPLIFIED_CHINESE) {
|
||||||
|
|
||||||
|
override fun getAllGenres(): Array<BilibiliTag> = arrayOf(
|
||||||
|
BilibiliTag("全部", -1),
|
||||||
|
BilibiliTag("校园", 18),
|
||||||
|
BilibiliTag("都市", 9),
|
||||||
|
BilibiliTag("耽美", 3),
|
||||||
|
BilibiliTag("少女", 20),
|
||||||
|
BilibiliTag("恋爱", 13),
|
||||||
|
BilibiliTag("奇幻", 11),
|
||||||
|
BilibiliTag("热血", 19),
|
||||||
|
BilibiliTag("冒险", 22),
|
||||||
|
BilibiliTag("古风", 12),
|
||||||
|
BilibiliTag("百合", 16),
|
||||||
|
BilibiliTag("玄幻", 30),
|
||||||
|
BilibiliTag("悬疑", 41),
|
||||||
|
BilibiliTag("科幻", 8),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class BilibiliComicsId : BilibiliComics(BilibiliIntl.INDONESIAN) {
|
||||||
|
|
||||||
|
override fun getAllGenres(): Array<BilibiliTag> = arrayOf(
|
||||||
|
BilibiliTag("Semua", -1),
|
||||||
|
BilibiliTag("Aksi", 19),
|
||||||
|
BilibiliTag("Fantasi Timur", 30),
|
||||||
|
BilibiliTag("Fantasi", 11),
|
||||||
|
BilibiliTag("Historis", 12),
|
||||||
|
BilibiliTag("Horror", 23),
|
||||||
|
BilibiliTag("Kampus", 18),
|
||||||
|
BilibiliTag("Komedi", 14),
|
||||||
|
BilibiliTag("Menegangkan", 41),
|
||||||
|
BilibiliTag("Remaja", 20),
|
||||||
|
BilibiliTag("Romantis", 13),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class BilibiliComicsEs : BilibiliComics(BilibiliIntl.SPANISH) {
|
||||||
|
|
||||||
|
override fun getAllGenres(): Array<BilibiliTag> = arrayOf(
|
||||||
|
BilibiliTag("Todos", -1),
|
||||||
|
BilibiliTag("Adolescencia", 105),
|
||||||
|
BilibiliTag("BL", 3),
|
||||||
|
BilibiliTag("Ciberdeportes", 104),
|
||||||
|
BilibiliTag("Ciencia ficción", 8),
|
||||||
|
BilibiliTag("Comedia", 14),
|
||||||
|
BilibiliTag("Fantasía occidental", 106),
|
||||||
|
BilibiliTag("Fantasía", 11),
|
||||||
|
BilibiliTag("Ficción Realista", 116),
|
||||||
|
BilibiliTag("GL", 16),
|
||||||
|
BilibiliTag("Histórico", 12),
|
||||||
|
BilibiliTag("Horror", 23),
|
||||||
|
BilibiliTag("Juvenil", 20),
|
||||||
|
BilibiliTag("Moderno", 111),
|
||||||
|
BilibiliTag("Oriental", 30),
|
||||||
|
BilibiliTag("Romance", 13),
|
||||||
|
BilibiliTag("Suspenso", 41),
|
||||||
|
BilibiliTag("Urbano", 9),
|
||||||
|
BilibiliTag("Wuxia", 103),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class BilibiliComicsFr : BilibiliComics(BilibiliIntl.FRENCH) {
|
||||||
|
|
||||||
|
override fun getAllGenres(): Array<BilibiliTag> = arrayOf(
|
||||||
|
BilibiliTag("Tout", -1),
|
||||||
|
BilibiliTag("BL", 3),
|
||||||
|
BilibiliTag("Science Fiction", 8),
|
||||||
|
BilibiliTag("Historique", 12),
|
||||||
|
BilibiliTag("Romance", 13),
|
||||||
|
BilibiliTag("GL", 16),
|
||||||
|
BilibiliTag("Fantasy Orientale", 30),
|
||||||
|
BilibiliTag("Suspense", 41),
|
||||||
|
BilibiliTag("Moderne", 111),
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'LANraragi'
|
extName = 'LANraragi'
|
||||||
extClass = '.LANraragiFactory'
|
extClass = '.LANraragiFactory'
|
||||||
extVersionCode = 17
|
extVersionCode = 16
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -82,17 +82,6 @@ open class LANraragi(private val suffix: String = "") : ConfigurableSource, Unme
|
||||||
return archiveToSManga(archive)
|
return archiveToSManga(archive)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMangaUrl(manga: SManga): String {
|
|
||||||
val namespace = preferences.getString(URL_TAG_PREFIX_KEY, URL_TAG_PREFIX_DEFAULT)
|
|
||||||
|
|
||||||
if (namespace.isNullOrEmpty()) {
|
|
||||||
return super.getMangaUrl(manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
val tag = manga.genre?.split(", ")?.find { it.startsWith("$namespace") }
|
|
||||||
return tag?.substringAfter("$namespace") ?: super.getMangaUrl(manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga): Request {
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
val id = if (manga.url.startsWith("/api/search/random")) randomArchiveID else getReaderId(manga.url)
|
val id = if (manga.url.startsWith("/api/search/random")) randomArchiveID else getReaderId(manga.url)
|
||||||
val uri = getApiUriBuilder("/api/archives/$id/metadata").build()
|
val uri = getApiUriBuilder("/api/archives/$id/metadata").build()
|
||||||
|
@ -321,7 +310,6 @@ open class LANraragi(private val suffix: String = "") : ConfigurableSource, Unme
|
||||||
screen.addPreference(screen.checkBoxPreference(CLEAR_NEW_KEY, "Clear New status", CLEAR_NEW_DEFAULT, "Clear an entry's New status when its details are viewed."))
|
screen.addPreference(screen.checkBoxPreference(CLEAR_NEW_KEY, "Clear New status", CLEAR_NEW_DEFAULT, "Clear an entry's New status when its details are viewed."))
|
||||||
screen.addPreference(screen.checkBoxPreference(NEW_ONLY_KEY, "Latest - New Only", NEW_ONLY_DEFAULT))
|
screen.addPreference(screen.checkBoxPreference(NEW_ONLY_KEY, "Latest - New Only", NEW_ONLY_DEFAULT))
|
||||||
screen.addPreference(screen.editTextPreference(SORT_BY_NS_KEY, "Latest - Sort by Namespace", SORT_BY_NS_DEFAULT, "Sort by the given namespace for Latest, such as date_added."))
|
screen.addPreference(screen.editTextPreference(SORT_BY_NS_KEY, "Latest - Sort by Namespace", SORT_BY_NS_DEFAULT, "Sort by the given namespace for Latest, such as date_added."))
|
||||||
screen.addPreference(screen.editTextPreference(URL_TAG_PREFIX_KEY, "Set tag prefix to get WebView URL", URL_TAG_PREFIX_DEFAULT, "Example: 'source:' will try to get the URL from the first tag starting with 'source:' and it will open it in the WebView. Leave empty for the default behavior."))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun androidx.preference.PreferenceScreen.checkBoxPreference(key: String, title: String, default: Boolean, summary: String = ""): androidx.preference.CheckBoxPreference {
|
private fun androidx.preference.PreferenceScreen.checkBoxPreference(key: String, title: String, default: Boolean, summary: String = ""): androidx.preference.CheckBoxPreference {
|
||||||
|
@ -506,7 +494,5 @@ open class LANraragi(private val suffix: String = "") : ConfigurableSource, Unme
|
||||||
private const val SORT_BY_NS_KEY = "latestNamespacePref"
|
private const val SORT_BY_NS_KEY = "latestNamespacePref"
|
||||||
private const val CLEAR_NEW_KEY = "clearNew"
|
private const val CLEAR_NEW_KEY = "clearNew"
|
||||||
private const val CLEAR_NEW_DEFAULT = true
|
private const val CLEAR_NEW_DEFAULT = true
|
||||||
private const val URL_TAG_PREFIX_KEY = "urlTagPrefix"
|
|
||||||
private const val URL_TAG_PREFIX_DEFAULT = ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -290,7 +290,6 @@ enum class Language {
|
||||||
RUSSIAN,
|
RUSSIAN,
|
||||||
THAI,
|
THAI,
|
||||||
VIETNAMESE,
|
VIETNAMESE,
|
||||||
GERMAN,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
|
@ -13,6 +13,5 @@ class MangaPlusFactory : SourceFactory {
|
||||||
MangaPlus("ru", "rus", Language.RUSSIAN),
|
MangaPlus("ru", "rus", Language.RUSSIAN),
|
||||||
MangaPlus("th", "tha", Language.THAI),
|
MangaPlus("th", "tha", Language.THAI),
|
||||||
MangaPlus("vi", "vie", Language.VIETNAMESE),
|
MangaPlus("vi", "vie", Language.VIETNAMESE),
|
||||||
MangaPlus("de", "deu", Language.GERMAN),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,11 +33,10 @@ open class MiauScan(lang: String) : MangaThemesia(
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val genreFilterIndex = filters.indexOfFirst { it is GenreListFilter }
|
val genreFilterIndex = filters.indexOfFirst { it is GenreListFilter }
|
||||||
val genreFilter = filters.getOrNull(genreFilterIndex) as? GenreListFilter
|
val genreFilter = filters.getOrNull(genreFilterIndex) as? GenreListFilter
|
||||||
?: GenreListFilter("", emptyList())
|
?: GenreListFilter(emptyList())
|
||||||
|
|
||||||
val overloadedGenreFilter = GenreListFilter(
|
val overloadedGenreFilter = GenreListFilter(
|
||||||
genreFilter.name,
|
genres = genreFilter.state + listOf(
|
||||||
genreFilter.state + listOf(
|
|
||||||
Genre("", PORTUGUESE_GENRE_ID, portugueseMode),
|
Genre("", PORTUGUESE_GENRE_ID, portugueseMode),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -31,8 +31,8 @@ class Mihentai : MangaThemesia("Mihentai", "https://mihentai.com", "all") {
|
||||||
listOf(
|
listOf(
|
||||||
StatusFilter(),
|
StatusFilter(),
|
||||||
TypeFilter(),
|
TypeFilter(),
|
||||||
OrderByFilter(intl["order_by_filter_title"], orderByFilterOptions),
|
OrderByFilter(),
|
||||||
GenreListFilter(intl["genre_filter_title"], getGenreList()),
|
GenreListFilter(getGenreList()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package eu.kanade.tachiyomi.extension.ar.areamanga
|
package eu.kanade.tachiyomi.extension.ar.areamanga
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
@ -9,4 +10,21 @@ class AreaManga : MangaThemesia(
|
||||||
"https://www.areascans.net",
|
"https://www.areascans.net",
|
||||||
"ar",
|
"ar",
|
||||||
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
||||||
)
|
) {
|
||||||
|
override val seriesArtistSelector =
|
||||||
|
".tsinfo .imptdt:contains(الرسام) i, ${super.seriesArtistSelector}"
|
||||||
|
override val seriesAuthorSelector =
|
||||||
|
".tsinfo .imptdt:contains(المؤلف) i, ${super.seriesAuthorSelector}"
|
||||||
|
override val seriesStatusSelector =
|
||||||
|
".tsinfo .imptdt:contains(الحالة) i, ${super.seriesStatusSelector}"
|
||||||
|
override val seriesTypeSelector =
|
||||||
|
".tsinfo .imptdt:contains(النوع) i, ${super.seriesTypeSelector}"
|
||||||
|
|
||||||
|
override fun String?.parseStatus() = when {
|
||||||
|
this == null -> SManga.UNKNOWN
|
||||||
|
this.contains("مستمر", ignoreCase = true) -> SManga.ONGOING
|
||||||
|
this.contains("مكتمل", ignoreCase = true) -> SManga.COMPLETED
|
||||||
|
this.contains("متوقف", ignoreCase = true) -> SManga.ON_HIATUS
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.MangaSwat'
|
extClass = '.MangaSwat'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://swatmanhua.com'
|
baseUrl = 'https://swatmanhua.com'
|
||||||
overrideVersionCode = 17
|
overrideVersionCode = 16
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -7,7 +7,6 @@ import androidx.preference.PreferenceScreen
|
||||||
import eu.kanade.tachiyomi.extension.BuildConfig
|
import eu.kanade.tachiyomi.extension.BuildConfig
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
@ -24,14 +23,12 @@ import java.util.Locale
|
||||||
|
|
||||||
private const val swatUrl = "https://swatmanhua.com"
|
private const val swatUrl = "https://swatmanhua.com"
|
||||||
|
|
||||||
class MangaSwat :
|
class MangaSwat : MangaThemesia(
|
||||||
MangaThemesia(
|
"MangaSwat",
|
||||||
"MangaSwat",
|
swatUrl,
|
||||||
swatUrl,
|
"ar",
|
||||||
"ar",
|
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
||||||
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
) {
|
||||||
),
|
|
||||||
ConfigurableSource {
|
|
||||||
private val defaultBaseUrl = swatUrl
|
private val defaultBaseUrl = swatUrl
|
||||||
|
|
||||||
override val baseUrl by lazy { getPrefBaseUrl() }
|
override val baseUrl by lazy { getPrefBaseUrl() }
|
||||||
|
@ -61,7 +58,6 @@ class MangaSwat :
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = "a[rel=next]"
|
override fun searchMangaNextPageSelector() = "a[rel=next]"
|
||||||
|
|
||||||
override val seriesTitleSelector = "h1[itemprop=headline]"
|
|
||||||
override val seriesArtistSelector = "span:contains(الناشر) i"
|
override val seriesArtistSelector = "span:contains(الناشر) i"
|
||||||
override val seriesAuthorSelector = "span:contains(المؤلف) i"
|
override val seriesAuthorSelector = "span:contains(المؤلف) i"
|
||||||
override val seriesGenreSelector = "span:contains(التصنيف) a, .mgen a"
|
override val seriesGenreSelector = "span:contains(التصنيف) a, .mgen a"
|
||||||
|
@ -117,6 +113,8 @@ class MangaSwat :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
screen.addPreference(baseUrlPref)
|
screen.addPreference(baseUrlPref)
|
||||||
|
|
||||||
|
super.setupPreferenceScreen(screen)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPrefBaseUrl(): String = preferences.getString(BASE_URL_PREF, defaultBaseUrl)!!
|
private fun getPrefBaseUrl(): String = preferences.getString(BASE_URL_PREF, defaultBaseUrl)!!
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'RE Manga (Arabic)'
|
|
||||||
extClass = '.REManga'
|
|
||||||
extVersionCode = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 25 KiB |
|
@ -1,258 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.ar.remangaarabic
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Request
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class REManga : ParsedHttpSource() {
|
|
||||||
|
|
||||||
override val name = "RE Manga"
|
|
||||||
|
|
||||||
override val baseUrl = "https://re-manga.com"
|
|
||||||
|
|
||||||
override val lang = "ar"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
// Popular
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request =
|
|
||||||
GET("$baseUrl/manga-list/?title=&order=popular&status=&type=")
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = "article.animpost"
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element): SManga =
|
|
||||||
SManga.create().apply {
|
|
||||||
setUrlWithoutDomain(element.select("a").attr("abs:href"))
|
|
||||||
element.select("img").let {
|
|
||||||
thumbnail_url = it.attr("abs:src")
|
|
||||||
title = it.attr("title")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector(): String? = null
|
|
||||||
|
|
||||||
// Latest
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request =
|
|
||||||
GET("$baseUrl/manga-list/?title=&order=update&status=&type=")
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector(): String? = popularMangaNextPageSelector()
|
|
||||||
|
|
||||||
// Search
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val url = "$baseUrl/manga-list/?".toHttpUrl().newBuilder()
|
|
||||||
.addQueryParameter("title", query)
|
|
||||||
filters.forEach { filter ->
|
|
||||||
when (filter) {
|
|
||||||
is SortFilter -> url.addQueryParameter("order", filter.toUriPart())
|
|
||||||
|
|
||||||
is StatusFilter -> url.addQueryParameter("status", filter.toUriPart())
|
|
||||||
|
|
||||||
is TypeFilter -> url.addQueryParameter("type", filter.toUriPart())
|
|
||||||
|
|
||||||
is GenreFilter -> {
|
|
||||||
filter.state
|
|
||||||
.filter { it.state != Filter.TriState.STATE_IGNORE }
|
|
||||||
.forEach { url.addQueryParameter("genre[]", it.id) }
|
|
||||||
}
|
|
||||||
|
|
||||||
is YearFilter -> {
|
|
||||||
filter.state
|
|
||||||
.filter { it.state != Filter.TriState.STATE_IGNORE }
|
|
||||||
.forEach { url.addQueryParameter("years[]", it.id) }
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return GET(url.build(), headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = popularMangaSelector()
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
|
||||||
|
|
||||||
// Details
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga {
|
|
||||||
return SManga.create().apply {
|
|
||||||
document.select("div.infox").first()!!.let { info ->
|
|
||||||
title = info.select("h1").text()
|
|
||||||
}
|
|
||||||
description = document.select("div.desc > div > p").text()
|
|
||||||
genre = document.select("div.spe > span:contains(نوع), div.genre-info > a").joinToString { it.text() }
|
|
||||||
document.select("div.spe > span:contains(الحالة)").first()?.text()?.also { statusText ->
|
|
||||||
when {
|
|
||||||
statusText.contains("مستمر", true) -> status = SManga.ONGOING
|
|
||||||
else -> status = SManga.COMPLETED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chapters
|
|
||||||
|
|
||||||
override fun chapterListSelector() = ".lsteps li"
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element): SChapter {
|
|
||||||
return SChapter.create().apply {
|
|
||||||
setUrlWithoutDomain(element.select("a").first()!!.attr("abs:href"))
|
|
||||||
|
|
||||||
val chNum = element.select(".eps > a").first()!!.text()
|
|
||||||
val chTitle = element.select(".lchx > a").first()!!.text()
|
|
||||||
|
|
||||||
name = when {
|
|
||||||
chTitle.startsWith("الفصل ") -> chTitle
|
|
||||||
else -> "الفصل $chNum - $chTitle"
|
|
||||||
}
|
|
||||||
|
|
||||||
element.select(".date").first()?.text()?.let { date ->
|
|
||||||
date_upload = DATE_FORMATTER.parse(date)?.time ?: 0L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pages
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
|
||||||
return document.select("div.reader-area img").mapIndexed { i, img ->
|
|
||||||
Page(i, "", img.attr("abs:src"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
// Filters
|
|
||||||
|
|
||||||
override fun getFilterList() = FilterList(
|
|
||||||
SortFilter(getSortFilters()),
|
|
||||||
StatusFilter(getStatusFilters()),
|
|
||||||
TypeFilter(getTypeFilter()),
|
|
||||||
Filter.Separator(),
|
|
||||||
Filter.Header("exclusion not available for This source"),
|
|
||||||
GenreFilter(getGenreFilters()),
|
|
||||||
YearFilter(getYearFilters()),
|
|
||||||
)
|
|
||||||
|
|
||||||
private class SortFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Sort by", vals)
|
|
||||||
|
|
||||||
private class TypeFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Type", vals)
|
|
||||||
|
|
||||||
private class StatusFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Status", vals)
|
|
||||||
|
|
||||||
class Genre(name: String, val id: String = name) : Filter.TriState(name)
|
|
||||||
private class GenreFilter(genres: List<Genre>) : Filter.Group<Genre>("Genre", genres)
|
|
||||||
|
|
||||||
class Year(name: String, val id: String = name) : Filter.TriState(name)
|
|
||||||
private class YearFilter(years: List<Year>) : Filter.Group<Year>("Year", years)
|
|
||||||
|
|
||||||
private fun getSortFilters(): Array<Pair<String?, String>> = arrayOf(
|
|
||||||
Pair("title", "A-Z"),
|
|
||||||
Pair("titlereverse", "Z-A"),
|
|
||||||
Pair("update", "Latest Update"),
|
|
||||||
Pair("latest", "Latest Added"),
|
|
||||||
Pair("popular", "Popular"),
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun getStatusFilters(): Array<Pair<String?, String>> = arrayOf(
|
|
||||||
Pair("", "All"),
|
|
||||||
Pair("Publishing", "مستمر"),
|
|
||||||
Pair("Finished", "تاريخ انتهي"),
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun getTypeFilter(): Array<Pair<String?, String>> = arrayOf(
|
|
||||||
Pair("", "All"),
|
|
||||||
Pair("Manga", "Manga"),
|
|
||||||
Pair("Manhwa", "Manhwa"),
|
|
||||||
Pair("Manhua", "Manhua"),
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun getGenreFilters(): List<Genre> = listOf(
|
|
||||||
Genre("Action", "action"),
|
|
||||||
Genre("Adventure", "adventure"),
|
|
||||||
Genre("Comedy", "comedy"),
|
|
||||||
Genre("Dementia", "dementia"),
|
|
||||||
Genre("Demons", "demons"),
|
|
||||||
Genre("Drama", "drama"),
|
|
||||||
Genre("Ecchi", "ecchi"),
|
|
||||||
Genre("Fantasy", "fantasy"),
|
|
||||||
Genre("Harem", "harem"),
|
|
||||||
Genre("Historical", "historical"),
|
|
||||||
Genre("Horror", "horror"),
|
|
||||||
Genre("Josei", "josei"),
|
|
||||||
Genre("Magic", "magic"),
|
|
||||||
Genre("Martial Arts", "martial-arts"),
|
|
||||||
Genre("Military", "military"),
|
|
||||||
Genre("Mystery", "mystery"),
|
|
||||||
Genre("Parody", "parody"),
|
|
||||||
Genre("Psychological", "psychological"),
|
|
||||||
Genre("Romance", "romance"),
|
|
||||||
Genre("Samurai", "samurai"),
|
|
||||||
Genre("School", "school"),
|
|
||||||
Genre("Sci-Fi", "sci-fi"),
|
|
||||||
Genre("Seinen", "seinen"),
|
|
||||||
Genre("Shounen", "shounen"),
|
|
||||||
Genre("Slice of Life", "slice-of-life"),
|
|
||||||
Genre("Sports", "sports"),
|
|
||||||
Genre("Super Power", "super-power"),
|
|
||||||
Genre("Supernatural", "supernatural"),
|
|
||||||
Genre("Vampire", "vampire"),
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun getYearFilters(): List<Year> = listOf(
|
|
||||||
Year("1970", "1970"),
|
|
||||||
Year("1986", "1986"),
|
|
||||||
Year("1989", "1989"),
|
|
||||||
Year("1995", "1995"),
|
|
||||||
Year("1997", "1997"),
|
|
||||||
Year("1998", "1998"),
|
|
||||||
Year("1999", "1999"),
|
|
||||||
Year("2000", "2000"),
|
|
||||||
Year("2002", "2002"),
|
|
||||||
Year("2003", "2003"),
|
|
||||||
Year("2004", "2004"),
|
|
||||||
Year("2005", "2005"),
|
|
||||||
Year("2006", "2006"),
|
|
||||||
Year("2007", "2007"),
|
|
||||||
Year("2008", "2008"),
|
|
||||||
Year("2009", "2009"),
|
|
||||||
Year("2010", "2010"),
|
|
||||||
Year("2011", "2011"),
|
|
||||||
Year("2012", "2012"),
|
|
||||||
Year("2013", "2013"),
|
|
||||||
Year("2014", "2014"),
|
|
||||||
Year("2016", "2016"),
|
|
||||||
Year("2017", "2017"),
|
|
||||||
Year("2018", "2018"),
|
|
||||||
Year("2019", "2019"),
|
|
||||||
Year("2020", "2020"),
|
|
||||||
)
|
|
||||||
|
|
||||||
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String?, String>>) :
|
|
||||||
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray()) {
|
|
||||||
fun toUriPart() = vals[state].first
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val DATE_FORMATTER by lazy {
|
|
||||||
SimpleDateFormat("MMM d, yyy", Locale("ar"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'StellarSaber'
|
|
||||||
extClass = '.StellarSaber'
|
|
||||||
themePkg = 'mangathemesia'
|
|
||||||
baseUrl = 'https://stellarsaber.pro'
|
|
||||||
overrideVersionCode = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 20 KiB |
|
@ -1,12 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.ar.stellarsaber
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class StellarSaber : MangaThemesia(
|
|
||||||
"StellarSaber",
|
|
||||||
"https://stellarsaber.pro",
|
|
||||||
"ar",
|
|
||||||
dateFormat = SimpleDateFormat("MMMMM dd, yyyy", Locale("ar")),
|
|
||||||
)
|
|
|
@ -1,7 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'Alandal'
|
|
||||||
extClass = '.Alandal'
|
|
||||||
extVersionCode = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 11 KiB |
|
@ -1,189 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.alandal
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
class Alandal : HttpSource() {
|
|
||||||
|
|
||||||
override val name = "Alandal"
|
|
||||||
|
|
||||||
override val baseUrl = "https://alandal.com"
|
|
||||||
private val apiUrl = "https://qq.alandal.com/api"
|
|
||||||
|
|
||||||
override val lang = "en"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override val client = network.cloudflareClient.newBuilder()
|
|
||||||
.rateLimit(1)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder().apply {
|
|
||||||
add("Referer", "$baseUrl/")
|
|
||||||
}
|
|
||||||
|
|
||||||
private val apiHeaders by lazy { apiHeadersBuilder.build() }
|
|
||||||
|
|
||||||
private val apiHeadersBuilder = headersBuilder().apply {
|
|
||||||
add("Accept", "application/json")
|
|
||||||
add("Host", apiUrl.toHttpUrl().host)
|
|
||||||
add("Origin", baseUrl)
|
|
||||||
add("Sec-Fetch-Dest", "empty")
|
|
||||||
add("Sec-Fetch-Mode", "cors")
|
|
||||||
add("Sec-Fetch-Site", "same-origin")
|
|
||||||
}
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
// ============================== Popular ===============================
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request =
|
|
||||||
searchMangaRequest(page, "", FilterList(SortFilter("popular")))
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage =
|
|
||||||
searchMangaParse(response)
|
|
||||||
|
|
||||||
// =============================== Latest ===============================
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request =
|
|
||||||
searchMangaRequest(page, "", FilterList(SortFilter("new")))
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage =
|
|
||||||
searchMangaParse(response)
|
|
||||||
|
|
||||||
// =============================== Search ===============================
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val url = apiUrl.toHttpUrl().newBuilder().apply {
|
|
||||||
addPathSegment("series")
|
|
||||||
if (query.isNotBlank()) {
|
|
||||||
addQueryParameter("name", query)
|
|
||||||
}
|
|
||||||
addQueryParameter("type", "comic")
|
|
||||||
|
|
||||||
val filterList = filters.ifEmpty { getFilterList() }
|
|
||||||
filterList.filterIsInstance<UriFilter>().forEach {
|
|
||||||
it.addToUri(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
addQueryParameter("page", page.toString())
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return GET(url, apiHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
|
||||||
val data = response.parseAs<ResponseDto<SearchSeriesDto>>().data.series
|
|
||||||
val mangaList = data.data.map { it.toSManga() }
|
|
||||||
val hasNextPage = data.currentPage < data.lastPage
|
|
||||||
return MangasPage(mangaList, hasNextPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================== Filters ==============================
|
|
||||||
|
|
||||||
override fun getFilterList(): FilterList = FilterList(
|
|
||||||
GenreFilter(),
|
|
||||||
SortFilter(),
|
|
||||||
StatusFilter(),
|
|
||||||
)
|
|
||||||
|
|
||||||
// =========================== Manga Details ============================
|
|
||||||
|
|
||||||
override fun getMangaUrl(manga: SManga): String =
|
|
||||||
baseUrl + manga.url.replace("series/", "series/comic-")
|
|
||||||
|
|
||||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
|
||||||
val url = apiUrl.toHttpUrl().newBuilder().apply {
|
|
||||||
addPathSegments(manga.url.substringAfter("/"))
|
|
||||||
addQueryParameter("type", "comic")
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return GET(url, apiHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga =
|
|
||||||
response.parseAs<ResponseDto<MangaDetailsDto>>().data.series.toSManga()
|
|
||||||
|
|
||||||
// ============================== Chapters ==============================
|
|
||||||
|
|
||||||
override fun getChapterUrl(chapter: SChapter): String {
|
|
||||||
return baseUrl + chapter.url
|
|
||||||
.replace("series/", "chapter/comic-")
|
|
||||||
.replace("chapters/", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga): Request {
|
|
||||||
val url = "$apiUrl${manga.url}".toHttpUrl().newBuilder().apply {
|
|
||||||
addPathSegment("chapters")
|
|
||||||
addQueryParameter("type", "comic")
|
|
||||||
addQueryParameter("from", "0")
|
|
||||||
addQueryParameter("to", "999")
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return GET(url, apiHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
|
||||||
val slug = response.request.url.newBuilder()
|
|
||||||
.query(null)
|
|
||||||
.removePathSegment(0) // Remove /api
|
|
||||||
.build()
|
|
||||||
.encodedPath
|
|
||||||
|
|
||||||
return response.parseAs<ChapterResponseDto>().data.map {
|
|
||||||
it.toSChapter(slug)
|
|
||||||
}.reversed()
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================== Pages ================================
|
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
|
||||||
if (chapter.name.startsWith("[LOCKED]")) {
|
|
||||||
throw Exception("Log in and unlock chapter in webview, then refresh chapter list")
|
|
||||||
}
|
|
||||||
|
|
||||||
val url = "$apiUrl${chapter.url}".toHttpUrl().newBuilder().apply {
|
|
||||||
addQueryParameter("type", "comic")
|
|
||||||
addQueryParameter("traveler", "0")
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return GET(url, apiHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val data = response.parseAs<PagesResponseDto>().data.chapter.chapter
|
|
||||||
|
|
||||||
return data.pages.mapIndexed { index, s ->
|
|
||||||
Page(index, imageUrl = s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun imageRequest(page: Page): Request {
|
|
||||||
val pageHeaders = headersBuilder().apply {
|
|
||||||
add("Accept", "image/avif,image/webp,*/*")
|
|
||||||
add("Host", page.imageUrl!!.toHttpUrl().host)
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return GET(page.imageUrl!!, pageHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================= Utilities ==============================
|
|
||||||
|
|
||||||
private inline fun <reified T> Response.parseAs(): T = use {
|
|
||||||
json.decodeFromStream(it.body.byteStream())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,118 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.alandal
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import org.jsoup.Jsoup
|
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class ResponseDto<T>(
|
|
||||||
val data: ResultDto<T>,
|
|
||||||
) {
|
|
||||||
@Serializable
|
|
||||||
class ResultDto<T>(
|
|
||||||
val series: T,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class SearchSeriesDto(
|
|
||||||
@SerialName("current_page") val currentPage: Int,
|
|
||||||
@SerialName("last_page") val lastPage: Int,
|
|
||||||
val data: List<SearchEntryDto>,
|
|
||||||
) {
|
|
||||||
@Serializable
|
|
||||||
class SearchEntryDto(
|
|
||||||
val name: String,
|
|
||||||
val slug: String,
|
|
||||||
val cover: String,
|
|
||||||
) {
|
|
||||||
fun toSManga(): SManga = SManga.create().apply {
|
|
||||||
title = name
|
|
||||||
url = "/series/$slug"
|
|
||||||
thumbnail_url = cover
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class MangaDetailsDto(
|
|
||||||
val name: String,
|
|
||||||
val summary: String,
|
|
||||||
val status: NamedObject,
|
|
||||||
val genres: List<NamedObject>,
|
|
||||||
val creators: List<NamedObject>,
|
|
||||||
val cover: String,
|
|
||||||
) {
|
|
||||||
@Serializable
|
|
||||||
class NamedObject(
|
|
||||||
val name: String,
|
|
||||||
val type: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun toSManga(): SManga = SManga.create().apply {
|
|
||||||
title = name
|
|
||||||
thumbnail_url = cover
|
|
||||||
description = Jsoup.parseBodyFragment(summary).text()
|
|
||||||
genre = genres.joinToString { it.name }
|
|
||||||
author = creators.filter { it.type!! == "author" }.joinToString { it.name }
|
|
||||||
status = this@MangaDetailsDto.status.name.parseStatus()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.parseStatus(): Int = when (this.lowercase()) {
|
|
||||||
"ongoing" -> SManga.ONGOING
|
|
||||||
"completed" -> SManga.COMPLETED
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class ChapterResponseDto(
|
|
||||||
val data: List<ChapterDto>,
|
|
||||||
) {
|
|
||||||
@Serializable
|
|
||||||
class ChapterDto(
|
|
||||||
val name: String,
|
|
||||||
@SerialName("published_at") val published: String,
|
|
||||||
val access: Boolean,
|
|
||||||
) {
|
|
||||||
fun toSChapter(slug: String): SChapter = SChapter.create().apply {
|
|
||||||
val prefix = if (access) "" else "[LOCKED] "
|
|
||||||
name = "${prefix}Chapter ${this@ChapterDto.name}"
|
|
||||||
date_upload = try {
|
|
||||||
dateFormat.parse(published)!!.time
|
|
||||||
} catch (_: ParseException) {
|
|
||||||
0L
|
|
||||||
}
|
|
||||||
url = "$slug/${this@ChapterDto.name}"
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.ENGLISH)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class PagesResponseDto(
|
|
||||||
val data: PagesDataDto,
|
|
||||||
) {
|
|
||||||
@Serializable
|
|
||||||
class PagesDataDto(
|
|
||||||
val chapter: PagesChapterDto,
|
|
||||||
) {
|
|
||||||
@Serializable
|
|
||||||
class PagesChapterDto(
|
|
||||||
val chapter: PagesChapterImagesDto,
|
|
||||||
) {
|
|
||||||
@Serializable
|
|
||||||
class PagesChapterImagesDto(
|
|
||||||
val pages: List<String>,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,89 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.alandal
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
|
|
||||||
interface UriFilter {
|
|
||||||
fun addToUri(builder: HttpUrl.Builder)
|
|
||||||
}
|
|
||||||
|
|
||||||
open class UriPartFilter(
|
|
||||||
name: String,
|
|
||||||
private val param: String,
|
|
||||||
private val vals: Array<Pair<String, String>>,
|
|
||||||
defaultValue: String? = null,
|
|
||||||
) : Filter.Select<String>(
|
|
||||||
name,
|
|
||||||
vals.map { it.first }.toTypedArray(),
|
|
||||||
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
|
|
||||||
),
|
|
||||||
UriFilter {
|
|
||||||
override fun addToUri(builder: HttpUrl.Builder) {
|
|
||||||
builder.addQueryParameter(param, vals[state].second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open class UriMultiSelectOption(name: String, val value: String) : Filter.CheckBox(name)
|
|
||||||
|
|
||||||
open class UriMultiSelectFilter(
|
|
||||||
name: String,
|
|
||||||
private val param: String,
|
|
||||||
private val vals: Array<Pair<String, String>>,
|
|
||||||
) : Filter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
|
|
||||||
override fun addToUri(builder: HttpUrl.Builder) {
|
|
||||||
val checked = state.filter { it.state }
|
|
||||||
|
|
||||||
if (checked.isEmpty()) {
|
|
||||||
builder.addQueryParameter(param, "-1")
|
|
||||||
} else {
|
|
||||||
checked.forEach {
|
|
||||||
builder.addQueryParameter(param, it.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GenreFilter : UriMultiSelectFilter(
|
|
||||||
"Genre",
|
|
||||||
"genres",
|
|
||||||
arrayOf(
|
|
||||||
Pair("Action", "1"),
|
|
||||||
Pair("Fantasy", "2"),
|
|
||||||
Pair("Regression", "3"),
|
|
||||||
Pair("Overpowered", "4"),
|
|
||||||
Pair("Ascension", "5"),
|
|
||||||
Pair("Revenge", "6"),
|
|
||||||
Pair("Martial Arts", "7"),
|
|
||||||
Pair("Magic", "8"),
|
|
||||||
Pair("Necromancer", "9"),
|
|
||||||
Pair("Adventure", "10"),
|
|
||||||
Pair("Tower", "11"),
|
|
||||||
Pair("Dungeons", "12"),
|
|
||||||
Pair("Psychological", "13"),
|
|
||||||
Pair("Isekai", "14"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
class SortFilter(defaultSort: String? = null) : UriPartFilter(
|
|
||||||
"Sort By",
|
|
||||||
"sort",
|
|
||||||
arrayOf(
|
|
||||||
Pair("Popularity", "popular"),
|
|
||||||
Pair("Name", "name"),
|
|
||||||
Pair("Chapters", "chapters"),
|
|
||||||
Pair("Rating", "Rating"),
|
|
||||||
Pair("New", "new"),
|
|
||||||
),
|
|
||||||
defaultSort,
|
|
||||||
)
|
|
||||||
|
|
||||||
class StatusFilter : UriPartFilter(
|
|
||||||
"Status",
|
|
||||||
"status",
|
|
||||||
arrayOf(
|
|
||||||
Pair("Any", "-1"),
|
|
||||||
Pair("Ongoing", "1"),
|
|
||||||
Pair("Coming Soon", "5"),
|
|
||||||
Pair("Completed", "6"),
|
|
||||||
),
|
|
||||||
)
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Anchira'
|
extName = 'Anchira'
|
||||||
extClass = '.Anchira'
|
extClass = '.Anchira'
|
||||||
extVersionCode = 10
|
extVersionCode = 8
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ import android.content.SharedPreferences
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.createChapter
|
|
||||||
import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.getPathFromUrl
|
import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.getPathFromUrl
|
||||||
import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.prepareTags
|
import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.prepareTags
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
@ -24,7 +23,6 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
@ -35,8 +33,6 @@ import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.math.ceil
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
class Anchira : HttpSource(), ConfigurableSource {
|
class Anchira : HttpSource(), ConfigurableSource {
|
||||||
override val name = "Anchira"
|
override val name = "Anchira"
|
||||||
|
@ -113,23 +109,6 @@ class Anchira : HttpSource(), ConfigurableSource {
|
||||||
fetchMangaDetails(manga).map {
|
fetchMangaDetails(manga).map {
|
||||||
MangasPage(listOf(it), false)
|
MangasPage(listOf(it), false)
|
||||||
}
|
}
|
||||||
} else if (query.startsWith(SLUG_BUNDLE_PREFIX)) {
|
|
||||||
// bundle entries as chapters
|
|
||||||
val url = applyFilters(
|
|
||||||
page,
|
|
||||||
query.substringAfter(SLUG_BUNDLE_PREFIX),
|
|
||||||
filters,
|
|
||||||
).removeAllQueryParameters("page")
|
|
||||||
if (
|
|
||||||
url.build().queryParameter("sort") == "4"
|
|
||||||
) {
|
|
||||||
url.removeAllQueryParameters("sort")
|
|
||||||
}
|
|
||||||
val manga = SManga.create()
|
|
||||||
.apply { this.url = "?${url.build().query}" }
|
|
||||||
fetchMangaDetails(manga).map {
|
|
||||||
MangasPage(listOf(it), false)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// regular filtering without text search
|
// regular filtering without text search
|
||||||
client.newCall(searchMangaRequest(page, query, filters))
|
client.newCall(searchMangaRequest(page, query, filters))
|
||||||
|
@ -137,171 +116,108 @@ class Anchira : HttpSource(), ConfigurableSource {
|
||||||
.map(::searchMangaParse)
|
.map(::searchMangaParse)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
|
||||||
GET(applyFilters(page, query, filters).build(), headers)
|
|
||||||
|
|
||||||
private fun applyFilters(page: Int, query: String, filters: FilterList): HttpUrl.Builder {
|
|
||||||
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
|
||||||
val trendingFilter = filterList.findInstance<TrendingFilter>()
|
|
||||||
val sortTrendingFilter = filters.findInstance<SortTrendingFilter>()
|
|
||||||
var url = libraryUrl.toHttpUrl().newBuilder()
|
var url = libraryUrl.toHttpUrl().newBuilder()
|
||||||
|
|
||||||
if (trendingFilter?.state == true) {
|
url.addQueryParameter("page", page.toString())
|
||||||
val interval = when (sortTrendingFilter?.state) {
|
|
||||||
1 -> "3"
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interval.isNotBlank()) url.setQueryParameter("interval", interval)
|
if (query.isNotBlank()) {
|
||||||
|
url.addQueryParameter("s", query)
|
||||||
|
}
|
||||||
|
|
||||||
url = url.toString().replace("library", "trending").toHttpUrl()
|
filters.forEach { filter ->
|
||||||
.newBuilder()
|
when (filter) {
|
||||||
} else {
|
is CategoryGroup -> {
|
||||||
if (query.isNotBlank()) {
|
var sum = 0
|
||||||
url.setQueryParameter("s", query)
|
|
||||||
}
|
|
||||||
|
|
||||||
filters.forEach { filter ->
|
filter.state.forEach { category ->
|
||||||
when (filter) {
|
when (category.name) {
|
||||||
is CategoryGroup -> {
|
"Manga" -> if (category.state) sum = sum or 1
|
||||||
var sum = 0
|
"Doujinshi" -> if (category.state) sum = sum or 2
|
||||||
|
"Illustration" -> if (category.state) sum = sum or 4
|
||||||
filter.state.forEach { category ->
|
|
||||||
when (category.name) {
|
|
||||||
"Manga" -> if (category.state) sum = sum or 1
|
|
||||||
"Doujinshi" -> if (category.state) sum = sum or 2
|
|
||||||
"Illustration" -> if (category.state) sum = sum or 4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sum > 0) url.setQueryParameter("cat", sum.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
is SortFilter -> {
|
|
||||||
val sort = when (filter.state?.index) {
|
|
||||||
0 -> "1"
|
|
||||||
1 -> "2"
|
|
||||||
2 -> "4"
|
|
||||||
4 -> "32"
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sort.isNotEmpty()) url.setQueryParameter("sort", sort)
|
|
||||||
if (filter.state?.ascending == true) url.setQueryParameter("order", "1")
|
|
||||||
}
|
|
||||||
|
|
||||||
is FavoritesFilter -> {
|
|
||||||
if (filter.state) {
|
|
||||||
if (!isLoggedIn()) {
|
|
||||||
throw IOException("No login cookie found")
|
|
||||||
}
|
|
||||||
|
|
||||||
url = url.toString().replace("library", "user/favorites").toHttpUrl()
|
|
||||||
.newBuilder()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
if (sum > 0) url.addQueryParameter("cat", sum.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is SortFilter -> {
|
||||||
|
val sort = when (filter.state?.index) {
|
||||||
|
0 -> "1"
|
||||||
|
1 -> "2"
|
||||||
|
2 -> "4"
|
||||||
|
4 -> "32"
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sort.isNotEmpty()) url.addQueryParameter("sort", sort)
|
||||||
|
if (filter.state?.ascending == true) url.addQueryParameter("order", "1")
|
||||||
|
}
|
||||||
|
|
||||||
|
is FavoritesFilter -> {
|
||||||
|
if (filter.state) {
|
||||||
|
if (!isLoggedIn()) {
|
||||||
|
throw IOException("No login cookie found")
|
||||||
|
}
|
||||||
|
|
||||||
|
url = url.toString().replace("library", "user/favorites").toHttpUrl()
|
||||||
|
.newBuilder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (page > 1) {
|
return GET(url.build(), headers)
|
||||||
url.setQueryParameter("page", page.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
return url
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response) = latestUpdatesParse(response)
|
override fun searchMangaParse(response: Response) = latestUpdatesParse(response)
|
||||||
|
|
||||||
// Details
|
// Details
|
||||||
|
|
||||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
override fun mangaDetailsRequest(manga: SManga) =
|
||||||
return if (manga.url.startsWith("?")) {
|
GET("$libraryUrl/${getPathFromUrl(manga.url)}", headers)
|
||||||
GET(libraryUrl + manga.url, headers)
|
|
||||||
} else {
|
|
||||||
GET("$libraryUrl/${getPathFromUrl(manga.url)}", headers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
return if (response.request.url.pathSegments.count() == libraryUrl.toHttpUrl().pathSegments.count()) {
|
val data = json.decodeFromString<Entry>(response.body.string())
|
||||||
val manga = latestUpdatesParse(response).mangas.first()
|
|
||||||
val query = response.request.url.queryParameter("s")
|
|
||||||
val cleanTitle = CHAPTER_SUFFIX_RE.replace(manga.title, "").trim()
|
|
||||||
manga.apply {
|
|
||||||
url = "?${response.request.url.query}"
|
|
||||||
description = "Bundled from $query"
|
|
||||||
title = "[Bundle] $cleanTitle"
|
|
||||||
update_strategy = UpdateStrategy.ALWAYS_UPDATE
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val data = json.decodeFromString<Entry>(response.body.string())
|
|
||||||
|
|
||||||
SManga.create().apply {
|
return SManga.create().apply {
|
||||||
url = "/g/${data.id}/${data.key}"
|
url = "/g/${data.id}/${data.key}"
|
||||||
title = data.title
|
title = data.title
|
||||||
thumbnail_url =
|
thumbnail_url =
|
||||||
"$cdnUrl/${data.id}/${data.key}/b/${data.thumbnailIndex + 1}"
|
"$cdnUrl/${data.id}/${data.key}/b/${data.thumbnailIndex + 1}"
|
||||||
artist = data.tags.filter { it.namespace == 1 }.joinToString(", ") { it.name }
|
artist = data.tags.filter { it.namespace == 1 }.joinToString(", ") { it.name }
|
||||||
author = data.tags.filter { it.namespace == 2 }.joinToString(", ") { it.name }
|
author = data.tags.filter { it.namespace == 2 }.joinToString(", ") { it.name }
|
||||||
genre = prepareTags(data.tags, preferences.useTagGrouping)
|
genre = prepareTags(data.tags, preferences.useTagGrouping)
|
||||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
||||||
status = SManga.COMPLETED
|
status = SManga.COMPLETED
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMangaUrl(manga: SManga) =
|
override fun getMangaUrl(manga: SManga) = if (preferences.openSource) {
|
||||||
if (preferences.openSource && !manga.url.startsWith("?")) {
|
val id = manga.url.split("/").reversed()[1].toInt()
|
||||||
val id = manga.url.split("/").reversed()[1].toInt()
|
anchiraData.find { it.id == id }?.url ?: "$baseUrl${manga.url}"
|
||||||
anchiraData.find { it.id == id }?.url ?: "$baseUrl${manga.url}"
|
} else {
|
||||||
} else {
|
"$baseUrl${manga.url}"
|
||||||
"$baseUrl${manga.url}"
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Chapter
|
// Chapter
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga): Request {
|
override fun chapterListRequest(manga: SManga) =
|
||||||
return if (manga.url.startsWith("?")) {
|
GET("$libraryUrl/${getPathFromUrl(manga.url)}", headers)
|
||||||
GET(libraryUrl + manga.url, headers)
|
|
||||||
} else {
|
|
||||||
GET("$libraryUrl/${getPathFromUrl(manga.url)}", headers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
val chapterList = mutableListOf<SChapter>()
|
val data = json.decodeFromString<Entry>(response.body.string())
|
||||||
if (response.request.url.pathSegments.count() == libraryUrl.toHttpUrl().pathSegments.count()) {
|
|
||||||
var results = json.decodeFromString<LibraryResponse>(response.body.string())
|
return listOf(
|
||||||
val pages = min(5, ceil((results.total.toFloat() / results.limit)).toInt())
|
SChapter.create().apply {
|
||||||
for (page in 1..pages) {
|
url = "/g/${data.id}/${data.key}"
|
||||||
results.entries.forEach { data ->
|
name = "Chapter"
|
||||||
chapterList.add(
|
date_upload = data.publishedAt * 1000
|
||||||
createChapter(data, response, anchiraData),
|
chapter_number = 1f
|
||||||
)
|
},
|
||||||
}
|
)
|
||||||
if (page < pages) {
|
|
||||||
results = json.decodeFromString<LibraryResponse>(
|
|
||||||
client.newCall(
|
|
||||||
GET(
|
|
||||||
response.request.url.newBuilder()
|
|
||||||
.setQueryParameter("page", (page + 1).toString()).build(),
|
|
||||||
headers,
|
|
||||||
),
|
|
||||||
).execute().body.string(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val data = json.decodeFromString<Entry>(response.body.string())
|
|
||||||
chapterList.add(
|
|
||||||
createChapter(data, response, anchiraData),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return chapterList
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getChapterUrl(chapter: SChapter) = "$baseUrl/g/${getPathFromUrl(chapter.url)}"
|
override fun getChapterUrl(chapter: SChapter) = "$baseUrl/g/${getPathFromUrl(chapter.url)}"
|
||||||
|
@ -362,16 +278,14 @@ class Anchira : HttpSource(), ConfigurableSource {
|
||||||
val openSourcePref = SwitchPreferenceCompat(screen.context).apply {
|
val openSourcePref = SwitchPreferenceCompat(screen.context).apply {
|
||||||
key = OPEN_SOURCE_PREF
|
key = OPEN_SOURCE_PREF
|
||||||
title = "Open source website in WebView"
|
title = "Open source website in WebView"
|
||||||
summary =
|
summary = "Enable to open the original source website of the gallery (if available) instead of Anchira."
|
||||||
"Enable to open the original source website of the gallery (if available) instead of Anchira."
|
|
||||||
setDefaultValue(false)
|
setDefaultValue(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
val useTagGrouping = SwitchPreferenceCompat(screen.context).apply {
|
val useTagGrouping = SwitchPreferenceCompat(screen.context).apply {
|
||||||
key = USE_TAG_GROUPING
|
key = USE_TAG_GROUPING
|
||||||
title = "Group tags"
|
title = "Group tags"
|
||||||
summary =
|
summary = "Enable to group tags together by artist, circle, parody, magazine and general tags"
|
||||||
"Enable to group tags together by artist, circle, parody, magazine and general tags"
|
|
||||||
setDefaultValue(false)
|
setDefaultValue(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -384,10 +298,6 @@ class Anchira : HttpSource(), ConfigurableSource {
|
||||||
CategoryGroup(),
|
CategoryGroup(),
|
||||||
SortFilter(),
|
SortFilter(),
|
||||||
FavoritesFilter(),
|
FavoritesFilter(),
|
||||||
Filter.Separator(),
|
|
||||||
Filter.Header("Others are ignored if trending only"),
|
|
||||||
TrendingFilter(),
|
|
||||||
SortTrendingFilter(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private class CategoryFilter(name: String) : Filter.CheckBox(name, false)
|
private class CategoryFilter(name: String) : Filter.CheckBox(name, false)
|
||||||
|
@ -407,18 +317,6 @@ class Anchira : HttpSource(), ConfigurableSource {
|
||||||
Selection(2, false),
|
Selection(2, false),
|
||||||
)
|
)
|
||||||
|
|
||||||
private class TrendingFilter : Filter.CheckBox(
|
|
||||||
"Show only trending",
|
|
||||||
)
|
|
||||||
|
|
||||||
private class SortTrendingFilter : PartFilter(
|
|
||||||
"Sort By",
|
|
||||||
arrayOf("Trending: Weekly", "Trending: Monthly"),
|
|
||||||
)
|
|
||||||
|
|
||||||
private open class PartFilter(displayName: String, value: Array<String>) :
|
|
||||||
Filter.Select<String>(displayName, value)
|
|
||||||
|
|
||||||
private val SharedPreferences.imageQuality
|
private val SharedPreferences.imageQuality
|
||||||
get() = getString(IMAGE_QUALITY_PREF, "b")!!
|
get() = getString(IMAGE_QUALITY_PREF, "b")!!
|
||||||
|
|
||||||
|
@ -464,11 +362,8 @@ class Anchira : HttpSource(), ConfigurableSource {
|
||||||
.use { json.decodeFromStream<List<EntryKey>>(it.body.byteStream()) }
|
.use { json.decodeFromStream<List<EntryKey>>(it.body.byteStream()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val SLUG_SEARCH_PREFIX = "id:"
|
const val SLUG_SEARCH_PREFIX = "id:"
|
||||||
const val SLUG_BUNDLE_PREFIX = "bundle:"
|
|
||||||
private const val IMAGE_QUALITY_PREF = "image_quality"
|
private const val IMAGE_QUALITY_PREF = "image_quality"
|
||||||
private const val OPEN_SOURCE_PREF = "use_manga_source"
|
private const val OPEN_SOURCE_PREF = "use_manga_source"
|
||||||
private const val USE_TAG_GROUPING = "use_tag_grouping"
|
private const val USE_TAG_GROUPING = "use_tag_grouping"
|
||||||
|
@ -476,5 +371,3 @@ class Anchira : HttpSource(), ConfigurableSource {
|
||||||
"https://gist.githubusercontent.com/LetrixZ/2b559cc5829d1c221c701e02ecd81411/raw/data-v5.json"
|
"https://gist.githubusercontent.com/LetrixZ/2b559cc5829d1c221c701e02ecd81411/raw/data-v5.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val CHAPTER_SUFFIX_RE = Regex("(?<!20\\d\\d-)\\b[\\d.]{1,4}$")
|
|
||||||
|
|
|
@ -3,6 +3,15 @@ package eu.kanade.tachiyomi.extension.en.anchira
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ListEntry(
|
||||||
|
val id: Int,
|
||||||
|
val key: String,
|
||||||
|
val title: String,
|
||||||
|
@SerialName("thumb_index") val thumbnailIndex: Int,
|
||||||
|
val tags: List<Tag> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Tag(
|
data class Tag(
|
||||||
var name: String,
|
var name: String,
|
||||||
|
@ -11,7 +20,7 @@ data class Tag(
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class LibraryResponse(
|
data class LibraryResponse(
|
||||||
val entries: List<Entry> = emptyList(),
|
val entries: List<ListEntry> = emptyList(),
|
||||||
val total: Int,
|
val total: Int,
|
||||||
val page: Int,
|
val page: Int,
|
||||||
val limit: Int,
|
val limit: Int,
|
||||||
|
@ -21,12 +30,11 @@ data class LibraryResponse(
|
||||||
data class Entry(
|
data class Entry(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val key: String,
|
val key: String,
|
||||||
@SerialName("published_at") val publishedAt: Long = 0L,
|
@SerialName("published_at") val publishedAt: Long,
|
||||||
val title: String,
|
val title: String,
|
||||||
@SerialName("thumb_index") val thumbnailIndex: Int = 1,
|
@SerialName("thumb_index") val thumbnailIndex: Int,
|
||||||
val tags: List<Tag> = emptyList(),
|
val tags: List<Tag> = emptyList(),
|
||||||
val url: String? = null,
|
val url: String? = null,
|
||||||
val pages: Int = 1,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.anchira
|
package eu.kanade.tachiyomi.extension.en.anchira
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import okhttp3.Response
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
object AnchiraHelper {
|
object AnchiraHelper {
|
||||||
fun getPathFromUrl(url: String) = "${url.split("/").reversed()[1]}/${url.split("/").last()}"
|
fun getPathFromUrl(url: String) = "${url.split("/").reversed()[1]}/${url.split("/").last()}"
|
||||||
|
|
||||||
|
@ -29,31 +25,4 @@ object AnchiraHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.joinToString(", ") { it }
|
.joinToString(", ") { it }
|
||||||
|
|
||||||
fun createChapter(entry: Entry, response: Response, anchiraData: List<EntryKey>) =
|
|
||||||
SChapter.create().apply {
|
|
||||||
val ch =
|
|
||||||
CHAPTER_SUFFIX_RE.find(entry.title)?.value?.trim('.') ?: "1"
|
|
||||||
val source = anchiraData.find { it.id == entry.id }?.url
|
|
||||||
?: response.request.url.toString()
|
|
||||||
url = "/g/${entry.id}/${entry.key}"
|
|
||||||
name = "$ch. ${entry.title.removeSuffix(" $ch")}"
|
|
||||||
date_upload = entry.publishedAt * 1000
|
|
||||||
chapter_number = ch.toFloat()
|
|
||||||
scanlator = buildString {
|
|
||||||
append(
|
|
||||||
Regex("fakku|irodori|anchira").find(source)?.value.orEmpty()
|
|
||||||
.replaceFirstChar {
|
|
||||||
if (it.isLowerCase()) {
|
|
||||||
it.titlecase(
|
|
||||||
Locale.getDefault(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
it.toString()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
append(" - ${entry.pages} pages")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.anchira
|
||||||
|
|
||||||
|
object XXTEA {
|
||||||
|
|
||||||
|
private const val DELTA = -0x61c88647
|
||||||
|
|
||||||
|
@Suppress("NOTHING_TO_INLINE", "FunctionName")
|
||||||
|
private inline fun MX(sum: Int, y: Int, z: Int, p: Int, e: Int, k: IntArray): Int {
|
||||||
|
return (z.ushr(5) xor (y shl 2)) + (y.ushr(3) xor (z shl 4)) xor (sum xor y) + (k[p and 3 xor e] xor z)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decrypt(data: ByteArray, key: ByteArray): ByteArray =
|
||||||
|
data.takeIf { it.isNotEmpty() }
|
||||||
|
?.let {
|
||||||
|
decrypt(data.toIntArray(false), key.fixKey().toIntArray(false))
|
||||||
|
.toByteArray(true)
|
||||||
|
} ?: data
|
||||||
|
|
||||||
|
fun decrypt(data: ByteArray, key: String): ByteArray? =
|
||||||
|
kotlin.runCatching { decrypt(data, key.toByteArray(Charsets.UTF_8)) }.getOrNull()
|
||||||
|
|
||||||
|
fun decryptToString(data: ByteArray, key: String): String? =
|
||||||
|
kotlin.runCatching { decrypt(data, key)?.toString(Charsets.UTF_8) }.getOrNull()
|
||||||
|
|
||||||
|
private fun decrypt(v: IntArray, k: IntArray): IntArray {
|
||||||
|
val n = v.size - 1
|
||||||
|
|
||||||
|
if (n < 1) {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
var p: Int
|
||||||
|
val q = 6 + 52 / (n + 1)
|
||||||
|
var z: Int
|
||||||
|
var y = v[0]
|
||||||
|
var sum = q * DELTA
|
||||||
|
var e: Int
|
||||||
|
|
||||||
|
while (sum != 0) {
|
||||||
|
e = sum.ushr(2) and 3
|
||||||
|
p = n
|
||||||
|
while (p > 0) {
|
||||||
|
z = v[p - 1]
|
||||||
|
v[p] -= MX(sum, y, z, p, e, k)
|
||||||
|
y = v[p]
|
||||||
|
p--
|
||||||
|
}
|
||||||
|
z = v[n]
|
||||||
|
v[0] -= MX(sum, y, z, p, e, k)
|
||||||
|
y = v[0]
|
||||||
|
sum -= DELTA
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ByteArray.fixKey(): ByteArray {
|
||||||
|
if (size == 16) return this
|
||||||
|
val fixedKey = ByteArray(16)
|
||||||
|
|
||||||
|
if (size < 16) {
|
||||||
|
copyInto(fixedKey)
|
||||||
|
} else {
|
||||||
|
copyInto(fixedKey, endIndex = 16)
|
||||||
|
}
|
||||||
|
return fixedKey
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ByteArray.toIntArray(includeLength: Boolean): IntArray {
|
||||||
|
var n = if (size and 3 == 0) {
|
||||||
|
size.ushr(2)
|
||||||
|
} else {
|
||||||
|
size.ushr(2) + 1
|
||||||
|
}
|
||||||
|
val result: IntArray
|
||||||
|
|
||||||
|
if (includeLength) {
|
||||||
|
result = IntArray(n + 1)
|
||||||
|
result[n] = size
|
||||||
|
} else {
|
||||||
|
result = IntArray(n)
|
||||||
|
}
|
||||||
|
n = size
|
||||||
|
for (i in 0 until n) {
|
||||||
|
result[i.ushr(2)] =
|
||||||
|
result[i.ushr(2)] or (0x000000ff and this[i].toInt() shl (i and 3 shl 3))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun IntArray.toByteArray(includeLength: Boolean): ByteArray? {
|
||||||
|
var n = size shl 2
|
||||||
|
|
||||||
|
if (includeLength) {
|
||||||
|
val m = this[size - 1]
|
||||||
|
n -= 4
|
||||||
|
if (m < n - 3 || m > n) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
n = m
|
||||||
|
}
|
||||||
|
val result = ByteArray(n)
|
||||||
|
|
||||||
|
for (i in 0 until n) {
|
||||||
|
result[i] = this[i.ushr(2)].ushr(i and 3 shl 3).toByte()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,6 @@ import androidx.preference.PreferenceScreen
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
@ -27,14 +26,12 @@ import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class AsuraScans :
|
class AsuraScans : MangaThemesia(
|
||||||
MangaThemesia(
|
"Asura Scans",
|
||||||
"Asura Scans",
|
"https://asuratoon.com",
|
||||||
"https://asuratoon.com",
|
"en",
|
||||||
"en",
|
dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.US),
|
||||||
dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.US),
|
) {
|
||||||
),
|
|
||||||
ConfigurableSource {
|
|
||||||
|
|
||||||
private val preferences by lazy {
|
private val preferences by lazy {
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
@ -284,6 +281,8 @@ class AsuraScans :
|
||||||
summary = PREF_PERM_MANGA_URL_SUMMARY
|
summary = PREF_PERM_MANGA_URL_SUMMARY
|
||||||
setDefaultValue(true)
|
setDefaultValue(true)
|
||||||
}.also(screen::addPreference)
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
|
super.setupPreferenceScreen(screen)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val SharedPreferences.permaUrlPref
|
private val SharedPreferences.permaUrlPref
|
||||||
|
|
|
@ -8,7 +8,3 @@ ext {
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(project(":lib:randomua"))
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,14 +2,11 @@ package eu.kanade.tachiyomi.extension.en.constellarscans
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
import eu.kanade.tachiyomi.lib.randomua.addRandomUAPreferenceToScreen
|
|
||||||
import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA
|
import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA
|
||||||
import eu.kanade.tachiyomi.lib.randomua.getPrefUAType
|
import eu.kanade.tachiyomi.lib.randomua.getPrefUAType
|
||||||
import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent
|
import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import kotlinx.serialization.json.jsonArray
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
@ -22,29 +19,22 @@ import okhttp3.Request
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class ConstellarScans :
|
class ConstellarScans : MangaThemesia("Constellar Scans", "https://constellarcomic.com", "en") {
|
||||||
MangaThemesia(
|
|
||||||
"Constellar Scans",
|
|
||||||
"https://constellarcomic.com",
|
|
||||||
"en",
|
|
||||||
),
|
|
||||||
ConfigurableSource {
|
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
private val preferences: SharedPreferences by lazy {
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
|
||||||
addRandomUAPreferenceToScreen(screen)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val client: OkHttpClient by lazy {
|
override val client: OkHttpClient by lazy {
|
||||||
network.cloudflareClient.newBuilder()
|
network.cloudflareClient.newBuilder()
|
||||||
.setRandomUserAgent(
|
.setRandomUserAgent(
|
||||||
preferences.getPrefUAType(),
|
preferences.getPrefUAType(),
|
||||||
preferences.getPrefCustomUA(),
|
preferences.getPrefCustomUA(),
|
||||||
)
|
)
|
||||||
|
.connectTimeout(10, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
.rateLimit(1, 1)
|
.rateLimit(1, 1)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
@ -71,8 +61,6 @@ class ConstellarScans :
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
countViews(document)
|
|
||||||
|
|
||||||
val html = document.toString()
|
val html = document.toString()
|
||||||
if (!html.contains("ts_rea_der_._run(\"")) {
|
if (!html.contains("ts_rea_der_._run(\"")) {
|
||||||
return super.pageListParse(document)
|
return super.pageListParse(document)
|
||||||
|
@ -92,6 +80,7 @@ class ConstellarScans :
|
||||||
}
|
}
|
||||||
.joinToString("")
|
.joinToString("")
|
||||||
|
|
||||||
|
countViews(document)
|
||||||
return json.parseToJsonElement(tsReaderRawData).jsonObject["sources"]!!.jsonArray[0].jsonObject["images"]!!.jsonArray.mapIndexed { idx, it ->
|
return json.parseToJsonElement(tsReaderRawData).jsonObject["sources"]!!.jsonArray[0].jsonObject["images"]!!.jsonArray.mapIndexed { idx, it ->
|
||||||
Page(idx, imageUrl = it.jsonPrimitive.content)
|
Page(idx, imageUrl = it.jsonPrimitive.content)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Winter Sun'
|
extName = '1st Manhwa'
|
||||||
extClass = '.WinterSun'
|
extClass = '.FirstManhwa'
|
||||||
themePkg = 'madara'
|
themePkg = 'madara'
|
||||||
baseUrl = 'https://wintersunscan.xyz'
|
baseUrl = 'https://1stmanhwa.com'
|
||||||
overrideVersionCode = 0
|
overrideVersionCode = 0
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 6.6 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 18 KiB |
|
@ -0,0 +1,9 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.firstmanhwa
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||||
|
|
||||||
|
class FirstManhwa : Madara("1st Manhwa", "https://1stmanhwa.com", "en") {
|
||||||
|
override val useNewChapterEndpoint = true
|
||||||
|
override val filterNonMangaItems = false
|
||||||
|
override val mangaDetailsSelectorStatus = "div.summary-heading:contains(Status) + div.summary-content"
|
||||||
|
}
|
|
@ -9,7 +9,6 @@ import androidx.preference.PreferenceScreen
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
@ -26,14 +25,12 @@ import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
class FlameComics :
|
class FlameComics : MangaThemesia(
|
||||||
MangaThemesia(
|
"Flame Comics",
|
||||||
"Flame Comics",
|
"https://flamecomics.com",
|
||||||
"https://flamecomics.com",
|
"en",
|
||||||
"en",
|
mangaUrlDirectory = "/series",
|
||||||
mangaUrlDirectory = "/series",
|
) {
|
||||||
),
|
|
||||||
ConfigurableSource {
|
|
||||||
|
|
||||||
// Flame Scans -> Flame Comics
|
// Flame Scans -> Flame Comics
|
||||||
override val id = 6350607071566689772
|
override val id = 6350607071566689772
|
||||||
|
|
|
@ -2,8 +2,8 @@ ext {
|
||||||
extName = 'Infernal Void Scans'
|
extName = 'Infernal Void Scans'
|
||||||
extClass = '.InfernalVoidScans'
|
extClass = '.InfernalVoidScans'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://hivescans.com'
|
baseUrl = 'https://void-scans.com'
|
||||||
overrideVersionCode = 6
|
overrideVersionCode = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -2,6 +2,6 @@ package eu.kanade.tachiyomi.extension.en.infernalvoidscans
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
|
|
||||||
class InfernalVoidScans : MangaThemesia("Infernal Void Scans", "https://hivescans.com", "en") {
|
class InfernalVoidScans : MangaThemesia("Infernal Void Scans", "https://void-scans.com", "en") {
|
||||||
override val pageSelector = "div#readerarea > p > img"
|
override val pageSelector = "div#readerarea > p > img"
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import androidx.preference.PreferenceScreen
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
@ -21,15 +20,7 @@ import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class LuminousScans :
|
class LuminousScans : MangaThemesia("Luminous Scans", "https://lumitoon.com", "en", mangaUrlDirectory = "/series") {
|
||||||
MangaThemesia(
|
|
||||||
"Luminous Scans",
|
|
||||||
"https://lumitoon.com",
|
|
||||||
"en",
|
|
||||||
mangaUrlDirectory = "/series",
|
|
||||||
),
|
|
||||||
ConfigurableSource {
|
|
||||||
|
|
||||||
override val client = super.client.newBuilder()
|
override val client = super.client.newBuilder()
|
||||||
.addInterceptor(::urlChangeInterceptor)
|
.addInterceptor(::urlChangeInterceptor)
|
||||||
.rateLimit(2)
|
.rateLimit(2)
|
||||||
|
@ -210,6 +201,8 @@ class LuminousScans :
|
||||||
summary = PREF_PERM_MANGA_URL_SUMMARY
|
summary = PREF_PERM_MANGA_URL_SUMMARY
|
||||||
setDefaultValue(true)
|
setDefaultValue(true)
|
||||||
}.also(screen::addPreference)
|
}.also(screen::addPreference)
|
||||||
|
|
||||||
|
super.setupPreferenceScreen(screen)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val SharedPreferences.permaUrlPref
|
private val SharedPreferences.permaUrlPref
|
||||||
|
|
|
@ -33,29 +33,19 @@ class LunarScans : MangaThemesia(
|
||||||
val filters = mutableListOf<Filter<*>>(
|
val filters = mutableListOf<Filter<*>>(
|
||||||
Filter.Header("Note: Can't be used with text search!"),
|
Filter.Header("Note: Can't be used with text search!"),
|
||||||
Filter.Separator(),
|
Filter.Separator(),
|
||||||
StatusFilter(intl["status_filter_title"], statusOptions),
|
StatusFilter(),
|
||||||
TypeFilter(intl["type_filter_title"], typeFilterOptions),
|
TypeFilter(),
|
||||||
OrderByFilter(intl["order_by_filter_title"], orderByFilterOptions),
|
OrderByFilter(),
|
||||||
|
Filter.Header("Genre exclusion is not available for all sources"),
|
||||||
|
GenreListFilter(getGenreList()),
|
||||||
)
|
)
|
||||||
if (!genrelist.isNullOrEmpty()) {
|
|
||||||
filters.addAll(
|
|
||||||
listOf(
|
|
||||||
Filter.Header(intl["genre_exclusion_warning"]),
|
|
||||||
GenreListFilter(intl["genre_filter_title"], getGenreList()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
filters.add(
|
|
||||||
Filter.Header(intl["genre_missing_warning"]),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (hasProjectPage) {
|
if (hasProjectPage) {
|
||||||
filters.addAll(
|
filters.addAll(
|
||||||
mutableListOf<Filter<*>>(
|
mutableListOf<Filter<*>>(
|
||||||
Filter.Separator(),
|
Filter.Separator(),
|
||||||
Filter.Header(intl["project_filter_warning"]),
|
Filter.Header("NOTE: Can't be used with other filter!"),
|
||||||
Filter.Header(intl.format("project_filter_name", name)),
|
Filter.Header("$name Project List page"),
|
||||||
ProjectFilter(intl["project_filter_title"], projectFilterOptions),
|
ProjectFilter(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
ext {
|
||||||
|
extName = '247Manga'
|
||||||
|
extClass = '.Manga247'
|
||||||
|
themePkg = 'madara'
|
||||||
|
baseUrl = 'https://247manga.com'
|
||||||
|
overrideVersionCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 17 KiB |
|
@ -0,0 +1,5 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.manga247
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||||
|
|
||||||
|
class Manga247 : Madara("247Manga", "https://247manga.com", "en")
|
|
@ -1,9 +1,9 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Manga Galaxy'
|
extName = 'Manga Galaxy'
|
||||||
extClass = '.MangaGalaxy'
|
extClass = '.MangaGalaxy'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'madara'
|
||||||
baseUrl = 'https://mangagalaxy.me'
|
baseUrl = 'https://mangagalaxy.me'
|
||||||
overrideVersionCode = 9
|
overrideVersionCode = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.mangagalaxy
|
package eu.kanade.tachiyomi.extension.en.mangagalaxy
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class MangaGalaxy : MangaThemesia(
|
class MangaGalaxy : Madara(
|
||||||
"Manga Galaxy",
|
"Manga Galaxy",
|
||||||
"https://mangagalaxy.me",
|
"https://mangagalaxy.me",
|
||||||
"en",
|
"en",
|
||||||
mangaUrlDirectory = "/series",
|
dateFormat = SimpleDateFormat("MM/dd/yyyy", Locale.US),
|
||||||
) {
|
) {
|
||||||
// moved from Madara to MangaThemesia
|
override val mangaDetailsSelectorStatus = "div.summary-heading:contains(status) + div.summary-content"
|
||||||
override val versionId = 2
|
override val mangaDetailsSelectorDescription = "div.summary-heading:contains(Summary) + div"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'MangaMonks'
|
|
||||||
extClass = '.MangaMonks'
|
|
||||||
extVersionCode = 1
|
|
||||||
isNsfw = true
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 12 KiB |
|
@ -1,267 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.mangamonks
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.extension.en.mangamonks.MangaMonksHelper.buildApiHeaders
|
|
||||||
import eu.kanade.tachiyomi.extension.en.mangamonks.MangaMonksHelper.toDate
|
|
||||||
import eu.kanade.tachiyomi.extension.en.mangamonks.MangaMonksHelper.toFormRequestBody
|
|
||||||
import eu.kanade.tachiyomi.extension.en.mangamonks.MangaMonksHelper.toStatus
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import kotlinx.serialization.MissingFieldException
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
class MangaMonks : ParsedHttpSource() {
|
|
||||||
|
|
||||||
override val name = "MangaMonks"
|
|
||||||
|
|
||||||
override val baseUrl = "https://mangamonks.com"
|
|
||||||
|
|
||||||
override val lang = "en"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
// popular
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
|
||||||
return GET("$baseUrl/popular-manga/$page", headers)
|
|
||||||
}
|
|
||||||
override fun popularMangaSelector() = ".main-slide"
|
|
||||||
override fun popularMangaNextPageSelector() = "li:nth-last-child(2) a.page-btn"
|
|
||||||
override fun popularMangaFromElement(element: Element): SManga {
|
|
||||||
return SManga.create().apply {
|
|
||||||
title = element.selectFirst(".detail a")!!.text()
|
|
||||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
|
|
||||||
thumbnail_url = element.select("img").attr("data-src")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// latest
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
|
||||||
return GET("$baseUrl/latest-releases/$page", headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = ".tab-pane .row .col-12"
|
|
||||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
|
||||||
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
|
|
||||||
|
|
||||||
// search
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val filterList = filters.let { if (it.isEmpty()) getFilterList() else it }
|
|
||||||
return if (query.isNotEmpty()) {
|
|
||||||
val requestBody = query.toFormRequestBody()
|
|
||||||
val requestHeaders = headersBuilder().buildApiHeaders(requestBody)
|
|
||||||
|
|
||||||
POST("$baseUrl/search/live", requestHeaders, requestBody)
|
|
||||||
} else {
|
|
||||||
val url = "$baseUrl/genre/".toHttpUrl().newBuilder()
|
|
||||||
filterList.forEach { filter ->
|
|
||||||
when (filter) {
|
|
||||||
is GenreFilter -> filter.toUriPart().let {
|
|
||||||
url.apply {
|
|
||||||
addPathSegment(it)
|
|
||||||
addQueryParameter("include[]", filter.toGenreValue())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is StatusFilter -> filter.toUriPart().let {
|
|
||||||
url.apply {
|
|
||||||
addQueryParameter("term", query)
|
|
||||||
addQueryParameter("status[]", it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
url.addPathSegment(page.toString())
|
|
||||||
GET(url.build(), headers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = ".main-slide .item"
|
|
||||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
|
||||||
val isJson = response.header("Content-Type")?.contains("application/json") ?: false
|
|
||||||
if (isJson) {
|
|
||||||
return try {
|
|
||||||
val result = json.decodeFromString<MangaList>(response.body.string())
|
|
||||||
val mangaList = result.manga.map {
|
|
||||||
SManga.create().apply {
|
|
||||||
title = it.title
|
|
||||||
setUrlWithoutDomain(it.url)
|
|
||||||
thumbnail_url = it.image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val hasNextPage = false
|
|
||||||
MangasPage(mangaList, hasNextPage)
|
|
||||||
} catch (_: MissingFieldException) {
|
|
||||||
MangasPage(emptyList(), false)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
|
|
||||||
val mangas = document.select(searchMangaSelector()).map { element ->
|
|
||||||
searchMangaFromElement(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
val hasNextPage = searchMangaNextPageSelector().let { selector ->
|
|
||||||
document.select(selector).first()
|
|
||||||
} != null
|
|
||||||
|
|
||||||
return MangasPage(mangas, hasNextPage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
|
||||||
|
|
||||||
// details
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga {
|
|
||||||
return SManga.create().apply {
|
|
||||||
author = document.selectFirst(".publisher a")!!.text()
|
|
||||||
status = document.selectFirst(".info-detail .source")!!.text().toStatus()
|
|
||||||
genre = document.select(".info-detail .tags a").joinToString { it.text() }
|
|
||||||
description = document.select(".info-desc p").text()
|
|
||||||
thumbnail_url = document.select(".img-holder img").attr("data-src")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// chapters
|
|
||||||
override fun chapterListSelector() = ".chapter-list li"
|
|
||||||
override fun chapterFromElement(element: Element): SChapter {
|
|
||||||
return SChapter.create().apply {
|
|
||||||
setUrlWithoutDomain(element.select("a").attr("href"))
|
|
||||||
name = element.select(".chapter-number").text()
|
|
||||||
date_upload = element.select(".time").text().trim().toDate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
// pages
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
|
||||||
return document.select("#zoomContainer .image img").mapIndexed { i, it ->
|
|
||||||
val src = it.attr("src")
|
|
||||||
val imageUrl = if (src.startsWith("https")) src else baseUrl + src
|
|
||||||
Page(i, imageUrl = imageUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// filters
|
|
||||||
override fun getFilterList() = FilterList(
|
|
||||||
Filter.Header("NOTE: Ignored if using text search!"),
|
|
||||||
Filter.Separator(),
|
|
||||||
StatusFilter(),
|
|
||||||
GenreFilter(),
|
|
||||||
)
|
|
||||||
private class StatusFilter : UriPartFilter(
|
|
||||||
"Status",
|
|
||||||
arrayOf(
|
|
||||||
Pair("Ongoing", "ongoing"),
|
|
||||||
Pair("Completed", "completed"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
private class GenreFilter : GenreValueFilter(
|
|
||||||
"Genre",
|
|
||||||
arrayOf(
|
|
||||||
Triple("Action", "2", "action"),
|
|
||||||
Triple("Adventure", "3", "adventure"),
|
|
||||||
Triple("Comedy", "5", "comedy"),
|
|
||||||
Triple("Cooking", "6", "cooking"),
|
|
||||||
Triple("Doujinshi", "7", "doujinshi"),
|
|
||||||
Triple("Drama", "8", "drama"),
|
|
||||||
Triple("Ecchi", "9", "ecchi"),
|
|
||||||
Triple("Yaoi", "11", "yaoi"),
|
|
||||||
Triple("Fantasy", "12", "fantasy"),
|
|
||||||
Triple("Gender Bender", "13", "gender-bender"),
|
|
||||||
Triple("Harem", "14", "harem"),
|
|
||||||
Triple("Historical", "15", "historical"),
|
|
||||||
Triple("Horror", "16", "horror"),
|
|
||||||
Triple("Josei", "17", "josei"),
|
|
||||||
Triple("Manhua", "18", "manhua"),
|
|
||||||
Triple("Manhwa", "19", "manhwa"),
|
|
||||||
Triple("Mecha", "21", "mecha"),
|
|
||||||
Triple("Mystery", "24", "mystery"),
|
|
||||||
Triple("One Shot", "25", "one-shot"),
|
|
||||||
Triple("Psychological", "26", "psychological"),
|
|
||||||
Triple("Romance", "27", "romance"),
|
|
||||||
Triple("School Life", "28", "school-life"),
|
|
||||||
Triple("Sci-fi", "29", "sci-fi"),
|
|
||||||
Triple("Seinen", "30", "seinen"),
|
|
||||||
Triple("Yuri", "31", "yuri"),
|
|
||||||
Triple("Shoujo", "32", "shoujo"),
|
|
||||||
Triple("Shounen", "34", "shounen"),
|
|
||||||
Triple("Shounen Ai", "35", "shounen-ai"),
|
|
||||||
Triple("Slice of Life", "36", "slice-of-life"),
|
|
||||||
Triple("Sports", "37", "sports"),
|
|
||||||
Triple("Supernatural", "38", "supernatural"),
|
|
||||||
Triple("Tragedy", "39", "tragedy"),
|
|
||||||
Triple("Webtoons", "40", "webtoons"),
|
|
||||||
Triple("Full Color", "42", "full-color"),
|
|
||||||
Triple("Isekai", "44", "isekai"),
|
|
||||||
Triple("Reincarnation", "45", "reincarnation"),
|
|
||||||
Triple("Time Travel", "46", "time-travel"),
|
|
||||||
Triple("Martial arts", "48", "martial-arts"),
|
|
||||||
Triple("Monsters", "49", "monsters-monsters"),
|
|
||||||
Triple("Thriller", "51", "thriller"),
|
|
||||||
Triple("Adaptation", "52", "adaptation"),
|
|
||||||
Triple("Reverse Harem", "53", "reverse-harem"),
|
|
||||||
Triple("Cross-dressing", "54", "cross-dressing"),
|
|
||||||
Triple("Zombies", "55", "zombies"),
|
|
||||||
Triple("Crime", "56", "crime"),
|
|
||||||
Triple("Ghosts", "57", "ghosts"),
|
|
||||||
Triple("Magic", "58", "magic"),
|
|
||||||
Triple("Gore", "59", "gore"),
|
|
||||||
Triple("+18", "84", "18"),
|
|
||||||
Triple("LGBT", "47", "lgbt"),
|
|
||||||
Triple("erotic", "62", "erotic"),
|
|
||||||
Triple("Harem", "63", "harem-harem"),
|
|
||||||
Triple("MILF", "64", "milf"),
|
|
||||||
Triple("Yaoi/boy's love", "65", "yaoiboys-love"),
|
|
||||||
Triple("Yuri/girl's love", "66", "yurigirls-love"),
|
|
||||||
Triple("BBW", "67", "bbw"),
|
|
||||||
Triple("Shota", "68", "shota"),
|
|
||||||
Triple("NTR/cheating", "69", "ntrcheating"),
|
|
||||||
Triple("BDSM", "70", "bdsm"),
|
|
||||||
Triple("tentacle", "71", "tentacle"),
|
|
||||||
Triple("Oyasumi/sleeping", "72", "oyasumisleeping"),
|
|
||||||
Triple("Elf Hentai", "74", "elf-hentai"),
|
|
||||||
Triple("Rape", "75", "rape"),
|
|
||||||
Triple("Incest", "76", "incest"),
|
|
||||||
Triple("Inseki", "77", "inseki"),
|
|
||||||
Triple("LGBTQ", "78", "lgbtq"),
|
|
||||||
Triple("Beastiality", "79", "bestiality"),
|
|
||||||
Triple("Defloration", "80", "defloration"),
|
|
||||||
Triple("loli", "81", "loli"),
|
|
||||||
Triple("Raw", "83", "raw"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
private open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
|
|
||||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
|
||||||
fun toUriPart() = vals[state].second
|
|
||||||
}
|
|
||||||
private open class GenreValueFilter(displayName: String, private val vals: Array<Triple<String, String, String>>) :
|
|
||||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
|
||||||
fun toUriPart() = vals[state].third
|
|
||||||
fun toGenreValue() = vals[state].second
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class MangaList(val manga: List<MangaItem>)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class MangaItem(val title: String, val url: String, val image: String)
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.mangamonks
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.RequestBody
|
|
||||||
import java.util.Calendar
|
|
||||||
|
|
||||||
object MangaMonksHelper {
|
|
||||||
fun Headers.Builder.buildApiHeaders(requestBody: RequestBody) = this
|
|
||||||
.add("Content-Length", requestBody.contentLength().toString())
|
|
||||||
.add("Content-Type", requestBody.contentType().toString())
|
|
||||||
.add("Accept", "application/json")
|
|
||||||
.add("X-Requested-With", "XMLHttpRequest")
|
|
||||||
.build()
|
|
||||||
inline fun <reified T : Any> T.toFormRequestBody(): RequestBody {
|
|
||||||
return FormBody.Builder()
|
|
||||||
.add("dataType", "json")
|
|
||||||
.add("phrase", this.toString())
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
fun String?.toStatus(): Int {
|
|
||||||
return when {
|
|
||||||
this == null -> SManga.UNKNOWN
|
|
||||||
this.contains("Ongoing", true) -> SManga.ONGOING
|
|
||||||
this.contains("Completed", true) -> SManga.COMPLETED
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun String?.toDate(): Long {
|
|
||||||
val trimmedDate = this!!.substringBefore(" ago").removeSuffix("s").split(" ")
|
|
||||||
val calendar = Calendar.getInstance()
|
|
||||||
|
|
||||||
when {
|
|
||||||
trimmedDate[1].contains(
|
|
||||||
"Year",
|
|
||||||
ignoreCase = true,
|
|
||||||
) -> calendar.apply { add(Calendar.YEAR, -trimmedDate[0].toInt()) }
|
|
||||||
|
|
||||||
trimmedDate[1].contains(
|
|
||||||
"Month",
|
|
||||||
ignoreCase = true,
|
|
||||||
) -> calendar.apply { add(Calendar.MONTH, -trimmedDate[0].toInt()) }
|
|
||||||
|
|
||||||
trimmedDate[1].contains(
|
|
||||||
"Week",
|
|
||||||
ignoreCase = true,
|
|
||||||
) -> calendar.apply { add(Calendar.WEEK_OF_MONTH, -trimmedDate[0].toInt()) }
|
|
||||||
|
|
||||||
trimmedDate[1].contains(
|
|
||||||
"Day",
|
|
||||||
ignoreCase = true,
|
|
||||||
) -> calendar.apply { add(Calendar.DAY_OF_MONTH, -trimmedDate[0].toInt()) }
|
|
||||||
|
|
||||||
trimmedDate[1].contains(
|
|
||||||
"Hour",
|
|
||||||
ignoreCase = true,
|
|
||||||
) -> calendar.apply { add(Calendar.HOUR_OF_DAY, -trimmedDate[0].toInt()) }
|
|
||||||
|
|
||||||
trimmedDate[1].contains(
|
|
||||||
"Minute",
|
|
||||||
ignoreCase = true,
|
|
||||||
) -> calendar.apply { add(Calendar.MINUTE, -trimmedDate[0].toInt()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
return calendar.timeInMillis
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'MangaOwl.To'
|
|
||||||
extClass = '.MangaOwlToFactory'
|
|
||||||
extVersionCode = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 2.5 KiB |