Removed other sources
5
.gitattributes
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/gradlew linguist-generated
|
||||||
|
/gradlew.bat linguist-generated
|
||||||
|
|
||||||
|
* text=auto eol=lf
|
||||||
|
*.bat text eol=crlf
|
||||||
4
.github/always_build.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[
|
||||||
|
"ko.newtoki",
|
||||||
|
"ko.wolfdotcom"
|
||||||
|
]
|
||||||
11
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/" # Location of package manifests
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
88
.github/scripts/create-repo.py
vendored
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from zipfile import ZipFile
|
||||||
|
|
||||||
|
PACKAGE_NAME_REGEX = re.compile(r"package: name='([^']+)'")
|
||||||
|
VERSION_CODE_REGEX = re.compile(r"versionCode='([^']+)'")
|
||||||
|
VERSION_NAME_REGEX = re.compile(r"versionName='([^']+)'")
|
||||||
|
IS_NSFW_REGEX = re.compile(r"'tachiyomi.extension.nsfw' value='([^']+)'")
|
||||||
|
APPLICATION_LABEL_REGEX = re.compile(r"^application-label:'([^']+)'", re.MULTILINE)
|
||||||
|
APPLICATION_ICON_320_REGEX = re.compile(r"^application-icon-320:'([^']+)'", re.MULTILINE)
|
||||||
|
LANGUAGE_REGEX = re.compile(r"tachiyomi-([^.]+)")
|
||||||
|
|
||||||
|
*_, ANDROID_BUILD_TOOLS = (Path(os.environ["ANDROID_HOME"]) / "build-tools").iterdir()
|
||||||
|
REPO_DIR = Path("repo")
|
||||||
|
REPO_APK_DIR = REPO_DIR / "apk"
|
||||||
|
REPO_ICON_DIR = REPO_DIR / "icon"
|
||||||
|
|
||||||
|
REPO_ICON_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
with open("output.json", encoding="utf-8") as f:
|
||||||
|
inspector_data = json.load(f)
|
||||||
|
|
||||||
|
index_min_data = []
|
||||||
|
|
||||||
|
for apk in REPO_APK_DIR.iterdir():
|
||||||
|
badging = subprocess.check_output(
|
||||||
|
[
|
||||||
|
ANDROID_BUILD_TOOLS / "aapt",
|
||||||
|
"dump",
|
||||||
|
"--include-meta-data",
|
||||||
|
"badging",
|
||||||
|
apk,
|
||||||
|
]
|
||||||
|
).decode()
|
||||||
|
|
||||||
|
package_info = next(x for x in badging.splitlines() if x.startswith("package: "))
|
||||||
|
package_name = PACKAGE_NAME_REGEX.search(package_info).group(1)
|
||||||
|
application_icon = APPLICATION_ICON_320_REGEX.search(badging).group(1)
|
||||||
|
|
||||||
|
with ZipFile(apk) as z, z.open(application_icon) as i, (
|
||||||
|
REPO_ICON_DIR / f"{package_name}.png"
|
||||||
|
).open("wb") as f:
|
||||||
|
f.write(i.read())
|
||||||
|
|
||||||
|
language = LANGUAGE_REGEX.search(apk.name).group(1)
|
||||||
|
sources = inspector_data[package_name]
|
||||||
|
|
||||||
|
if len(sources) == 1:
|
||||||
|
source_language = sources[0]["lang"]
|
||||||
|
|
||||||
|
if (
|
||||||
|
source_language != language
|
||||||
|
and source_language not in {"all", "other"}
|
||||||
|
and language not in {"all", "other"}
|
||||||
|
):
|
||||||
|
language = source_language
|
||||||
|
|
||||||
|
common_data = {
|
||||||
|
"name": APPLICATION_LABEL_REGEX.search(badging).group(1),
|
||||||
|
"pkg": package_name,
|
||||||
|
"apk": apk.name,
|
||||||
|
"lang": language,
|
||||||
|
"code": int(VERSION_CODE_REGEX.search(package_info).group(1)),
|
||||||
|
"version": VERSION_NAME_REGEX.search(package_info).group(1),
|
||||||
|
"nsfw": int(IS_NSFW_REGEX.search(badging).group(1)),
|
||||||
|
}
|
||||||
|
min_data = {
|
||||||
|
**common_data,
|
||||||
|
"sources": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
for source in sources:
|
||||||
|
min_data["sources"].append(
|
||||||
|
{
|
||||||
|
"name": source["name"],
|
||||||
|
"lang": source["lang"],
|
||||||
|
"id": source["id"],
|
||||||
|
"baseUrl": source["baseUrl"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
index_min_data.append(min_data)
|
||||||
|
|
||||||
|
with REPO_DIR.joinpath("index.min.json").open("w", encoding="utf-8") as index_file:
|
||||||
|
json.dump(index_min_data, index_file, ensure_ascii=False, separators=(",", ":"))
|
||||||
113
.github/scripts/generate-build-matrices.py
vendored
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import itertools
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import NoReturn
|
||||||
|
|
||||||
|
EXTENSION_REGEX = re.compile(r"^src/(?P<lang>\w+)/(?P<extension>\w+)")
|
||||||
|
MULTISRC_LIB_REGEX = re.compile(r"^lib-multisrc/(?P<multisrc>\w+)")
|
||||||
|
LIB_REGEX = re.compile(r"^lib/(?P<lib>\w+)")
|
||||||
|
MODULE_REGEX = re.compile(r"^:src:(?P<lang>\w+):(?P<extension>\w+)$")
|
||||||
|
CORE_FILES_REGEX = re.compile(
|
||||||
|
r"^(buildSrc/|core/|gradle/|build\.gradle\.kts|common\.gradle|gradle\.properties|settings\.gradle\.kts|utils/)"
|
||||||
|
)
|
||||||
|
|
||||||
|
def run_command(command: str) -> str:
|
||||||
|
result = subprocess.run(command, capture_output=True, text=True, shell=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(result.stderr.strip())
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
def get_module_list(ref: str) -> tuple[list[str], list[str]]:
|
||||||
|
changed_files = run_command(f"git diff --name-only {ref}").splitlines()
|
||||||
|
|
||||||
|
modules = set()
|
||||||
|
libs = set()
|
||||||
|
deleted = set()
|
||||||
|
core_files_changed = False
|
||||||
|
|
||||||
|
for file in map(lambda x: Path(x).as_posix(), changed_files):
|
||||||
|
if CORE_FILES_REGEX.search(file):
|
||||||
|
core_files_changed = True
|
||||||
|
elif match := EXTENSION_REGEX.search(file):
|
||||||
|
lang = match.group("lang")
|
||||||
|
extension = match.group("extension")
|
||||||
|
if Path("src", lang, extension).is_dir():
|
||||||
|
modules.add(f':src:{lang}:{extension}')
|
||||||
|
deleted.add(f"{lang}.{extension}")
|
||||||
|
elif match := MULTISRC_LIB_REGEX.search(file):
|
||||||
|
multisrc = match.group("multisrc")
|
||||||
|
if Path("lib-multisrc", multisrc).is_dir():
|
||||||
|
libs.add(f":lib-multisrc:{multisrc}:printDependentExtensions")
|
||||||
|
elif match := LIB_REGEX.search(file):
|
||||||
|
lib = match.group("lib")
|
||||||
|
if Path("lib", lib).is_dir():
|
||||||
|
libs.add(f":lib:{lib}:printDependentExtensions")
|
||||||
|
|
||||||
|
def is_extension_module(module: str) -> bool:
|
||||||
|
if not (match := MODULE_REGEX.search(module)):
|
||||||
|
return False
|
||||||
|
lang = match.group("lang")
|
||||||
|
extension = match.group("extension")
|
||||||
|
deleted.add(f"{lang}.{extension}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if len(libs) != 0 and not core_files_changed:
|
||||||
|
modules.update([
|
||||||
|
module for module in
|
||||||
|
run_command("./gradlew -q " + " ".join(libs)).splitlines()
|
||||||
|
if is_extension_module(module)
|
||||||
|
])
|
||||||
|
|
||||||
|
if os.getenv("IS_PR_CHECK") != "true" and not core_files_changed:
|
||||||
|
with Path(".github/always_build.json").open() as always_build_file:
|
||||||
|
always_build = json.load(always_build_file)
|
||||||
|
for extension in always_build:
|
||||||
|
modules.add(":src:" + extension.replace(".", ":"))
|
||||||
|
deleted.add(extension)
|
||||||
|
|
||||||
|
if core_files_changed:
|
||||||
|
(all_modules, all_deleted) = get_all_modules()
|
||||||
|
|
||||||
|
modules.update(all_modules)
|
||||||
|
deleted.update(all_deleted)
|
||||||
|
|
||||||
|
return list(modules), list(deleted)
|
||||||
|
|
||||||
|
def get_all_modules() -> tuple[list[str], list[str]]:
|
||||||
|
modules = []
|
||||||
|
deleted = []
|
||||||
|
for lang in Path("src").iterdir():
|
||||||
|
for extension in lang.iterdir():
|
||||||
|
modules.append(f":src:{lang.name}:{extension.name}")
|
||||||
|
deleted.append(f"{lang.name}.{extension.name}")
|
||||||
|
return modules, deleted
|
||||||
|
|
||||||
|
def main() -> NoReturn:
|
||||||
|
_, ref, build_type = sys.argv
|
||||||
|
modules, deleted = get_module_list(ref)
|
||||||
|
|
||||||
|
chunked = {
|
||||||
|
"chunk": [
|
||||||
|
{"number": i + 1, "modules": modules}
|
||||||
|
for i, modules in
|
||||||
|
enumerate(itertools.batched(
|
||||||
|
map(lambda x: f"{x}:assemble{build_type}", modules),
|
||||||
|
int(os.getenv("CI_CHUNK_SIZE", 65))
|
||||||
|
))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"Module chunks to build:\n{json.dumps(chunked, indent=2)}\n\nModule to delete:\n{json.dumps(deleted, indent=2)}")
|
||||||
|
|
||||||
|
if os.getenv("CI") == "true":
|
||||||
|
with open(os.getenv("GITHUB_OUTPUT"), 'a') as out_file:
|
||||||
|
out_file.write(f"matrix={json.dumps(chunked)}\n")
|
||||||
|
out_file.write(f"delete={json.dumps(deleted)}\n")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
50
.github/scripts/merge-repo.py
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import html
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
REMOTE_REPO: Path = Path.cwd()
|
||||||
|
LOCAL_REPO: Path = REMOTE_REPO.parent.joinpath(sys.argv[2])
|
||||||
|
|
||||||
|
to_delete: list[str] = json.loads(sys.argv[1])
|
||||||
|
|
||||||
|
for module in to_delete:
|
||||||
|
apk_name = f"tachiyomi-{module}-v*.*.*.apk"
|
||||||
|
icon_name = f"eu.kanade.tachiyomi.extension.{module}.png"
|
||||||
|
for file in REMOTE_REPO.joinpath("apk").glob(apk_name):
|
||||||
|
print(file.name)
|
||||||
|
file.unlink(missing_ok=True)
|
||||||
|
for file in REMOTE_REPO.joinpath("icon").glob(icon_name):
|
||||||
|
print(file.name)
|
||||||
|
file.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
shutil.copytree(src=LOCAL_REPO.joinpath("apk"), dst=REMOTE_REPO.joinpath("apk"), dirs_exist_ok = True)
|
||||||
|
shutil.copytree(src=LOCAL_REPO.joinpath("icon"), dst=REMOTE_REPO.joinpath("icon"), dirs_exist_ok = True)
|
||||||
|
|
||||||
|
with REMOTE_REPO.joinpath("index.min.json").open() as remote_index_file:
|
||||||
|
remote_index = json.load(remote_index_file)
|
||||||
|
|
||||||
|
with LOCAL_REPO.joinpath("index.min.json").open() as local_index_file:
|
||||||
|
local_index = json.load(local_index_file)
|
||||||
|
|
||||||
|
index = [
|
||||||
|
item for item in remote_index
|
||||||
|
if not any([item["pkg"].endswith(f".{module}") for module in to_delete])
|
||||||
|
]
|
||||||
|
index.extend(local_index)
|
||||||
|
index.sort(key=lambda x: x["pkg"])
|
||||||
|
|
||||||
|
with REMOTE_REPO.joinpath("index.json").open("w", encoding="utf-8") as index_file:
|
||||||
|
json.dump(index, index_file, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
with REMOTE_REPO.joinpath("index.min.json").open("w", encoding="utf-8") as index_min_file:
|
||||||
|
json.dump(index, index_min_file, ensure_ascii=False, separators=(",", ":"))
|
||||||
|
|
||||||
|
with REMOTE_REPO.joinpath("index.html").open("w", encoding="utf-8") as index_html_file:
|
||||||
|
index_html_file.write('<!DOCTYPE html>\n<html>\n<head>\n<meta charset="UTF-8">\n<title>apks</title>\n</head>\n<body>\n<pre>\n')
|
||||||
|
for entry in index:
|
||||||
|
apk_escaped = 'apk/' + html.escape(entry["apk"])
|
||||||
|
name_escaped = html.escape(entry["name"])
|
||||||
|
index_html_file.write(f'<a href="{apk_escaped}">{name_escaped}</a>\n')
|
||||||
|
index_html_file.write('</pre>\n</body>\n</html>\n')
|
||||||
12
.github/scripts/move-built-apks.py
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
REPO_APK_DIR = Path("repo/apk")
|
||||||
|
|
||||||
|
shutil.rmtree(REPO_APK_DIR, ignore_errors=True)
|
||||||
|
REPO_APK_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
for apk in Path.home().joinpath("apk-artifacts").glob("**/*.apk"):
|
||||||
|
apk_name = apk.name.replace("-release.apk", ".apk")
|
||||||
|
|
||||||
|
shutil.move(apk, REPO_APK_DIR.joinpath(apk_name))
|
||||||
22
.github/workflows/codeberg_mirror.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Sync repo to the Codeberg mirror
|
||||||
|
name: Mirror Sync
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "master" ]
|
||||||
|
workflow_dispatch: # Manual dispatch
|
||||||
|
schedule:
|
||||||
|
- cron: "0 */8 * * *"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
codeberg:
|
||||||
|
if: github.repository == 'yuzono/tachiyomi-extensions'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: pixta-dev/repository-mirroring-action@674e65a7d483ca28dafaacba0d07351bdcc8bd75 # v1.1.1
|
||||||
|
with:
|
||||||
|
target_repo_url: "git@codeberg.org:cuong-tran/komikku-extensions.git"
|
||||||
|
ssh_private_key: ${{ secrets.CODEBERG_SSH }}
|
||||||
6
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v5.0.0 # Use the latest stable version
|
||||||
|
hooks:
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: trailing-whitespace
|
||||||
7
buildSrc/settings.gradle.kts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
dependencyResolutionManagement {
|
||||||
|
versionCatalogs {
|
||||||
|
create("libs") {
|
||||||
|
from(files("../gradle/libs.versions.toml"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
buildSrc/src/main/kotlin/Extensions.kt
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import groovy.lang.MissingPropertyException
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.api.artifacts.ProjectDependency
|
||||||
|
import org.gradle.api.plugins.ExtensionAware
|
||||||
|
import org.gradle.kotlin.dsl.extra
|
||||||
|
|
||||||
|
var ExtensionAware.baseVersionCode: Int
|
||||||
|
get() = (extra.get("baseVersionCode") as Int) + kmkBaseVersionCode
|
||||||
|
set(value) = extra.set("baseVersionCode", value)
|
||||||
|
|
||||||
|
fun Project.getDependents(): Set<Project> {
|
||||||
|
val dependentProjects = mutableSetOf<Project>()
|
||||||
|
|
||||||
|
rootProject.allprojects.forEach { project ->
|
||||||
|
project.configurations.forEach { configuration ->
|
||||||
|
configuration.dependencies.forEach { dependency ->
|
||||||
|
if (dependency is ProjectDependency && dependency.path == path) {
|
||||||
|
dependentProjects.add(project)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependentProjects
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Project.printDependentExtensions() {
|
||||||
|
getDependents().forEach { project ->
|
||||||
|
if (project.path.startsWith(":src:")) {
|
||||||
|
println(project.path)
|
||||||
|
} else if (project.path.startsWith(":lib-multisrc:")) {
|
||||||
|
project.getDependents().forEach { println(it.path) }
|
||||||
|
} else if (project.path.startsWith(":lib:")) {
|
||||||
|
project.printDependentExtensions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ExtensionAware.kmkBaseVersionCode: Int
|
||||||
|
get() {
|
||||||
|
return try {
|
||||||
|
extra.get("kmkBaseVersionCode") as Int
|
||||||
|
} catch (e: MissingPropertyException) {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set(value) = extra.set("kmkBaseVersionCode", value)
|
||||||
29
buildSrc/src/main/kotlin/lib-android.gradle.kts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
kotlin("android")
|
||||||
|
id("kotlinx-serialization")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdk = AndroidConfig.compileSdk
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = AndroidConfig.minSdk
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace = "eu.kanade.tachiyomi.lib.${project.name}"
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
androidResources = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly(versionCatalogs.named("libs").findBundle("common").get())
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("printDependentExtensions") {
|
||||||
|
doLast {
|
||||||
|
project.printDependentExtensions()
|
||||||
|
}
|
||||||
|
}
|
||||||
60
buildSrc/src/main/kotlin/lib-multisrc.gradle.kts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
kotlin("android")
|
||||||
|
id("kotlinx-serialization")
|
||||||
|
id("org.jmailen.kotlinter")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdk = AndroidConfig.compileSdk
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = AndroidConfig.minSdk
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace = "eu.kanade.tachiyomi.multisrc.${project.name}"
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
named("main") {
|
||||||
|
manifest.srcFile("AndroidManifest.xml")
|
||||||
|
java.setSrcDirs(listOf("src"))
|
||||||
|
res.setSrcDirs(listOf("res"))
|
||||||
|
assets.setSrcDirs(listOf("assets"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
freeCompilerArgs += "-opt-in=kotlinx.serialization.ExperimentalSerializationApi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinter {
|
||||||
|
experimentalRules = true
|
||||||
|
disabledRules = arrayOf(
|
||||||
|
"experimental:argument-list-wrapping", // Doesn't play well with Android Studio
|
||||||
|
"experimental:comment-wrapping",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly(versionCatalogs.named("libs").findBundle("common").get())
|
||||||
|
implementation(project(":utils"))
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
preBuild {
|
||||||
|
dependsOn(lintKotlin)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (System.getenv("CI") != "true") {
|
||||||
|
lintKotlin {
|
||||||
|
dependsOn(formatKotlin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("printDependentExtensions") {
|
||||||
|
doLast {
|
||||||
|
project.printDependentExtensions()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.gmanga
|
|
||||||
|
|
||||||
import android.util.Base64
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.spec.IvParameterSpec
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
fun decrypt(responseData: String): String {
|
|
||||||
val enc = responseData.split("|")
|
|
||||||
val secretKey = enc[3].sha256().hexStringToByteArray()
|
|
||||||
|
|
||||||
return enc[0].aesDecrypt(secretKey, enc[2])
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.hexStringToByteArray(): ByteArray {
|
|
||||||
val len = length
|
|
||||||
val data = ByteArray(len / 2)
|
|
||||||
var i = 0
|
|
||||||
while (i < len) {
|
|
||||||
data[i / 2] = (
|
|
||||||
(Character.digit(this[i], 16) shl 4) +
|
|
||||||
Character.digit(this[i + 1], 16)
|
|
||||||
).toByte()
|
|
||||||
i += 2
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.sha256(): String {
|
|
||||||
return MessageDigest
|
|
||||||
.getInstance("SHA-256")
|
|
||||||
.digest(toByteArray())
|
|
||||||
.fold("") { str, it -> str + "%02x".format(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.aesDecrypt(secretKey: ByteArray, ivString: String): String {
|
|
||||||
val c = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
|
||||||
val sk = SecretKeySpec(secretKey, "AES")
|
|
||||||
val iv = IvParameterSpec(Base64.decode(ivString.toByteArray(Charsets.UTF_8), Base64.DEFAULT))
|
|
||||||
c.init(Cipher.DECRYPT_MODE, sk, iv)
|
|
||||||
|
|
||||||
val byteStr = Base64.decode(toByteArray(Charsets.UTF_8), Base64.DEFAULT)
|
|
||||||
return String(c.doFinal(byteStr))
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.mangaworld
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import okhttp3.Cookie
|
|
||||||
import okhttp3.Interceptor
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
|
||||||
|
|
||||||
class CookieRedirectInterceptor(private val client: OkHttpClient) : Interceptor {
|
|
||||||
private val cookieRegex = Regex("""document\.cookie="(MWCookie[^"]+)""")
|
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
|
||||||
val request = chain.request()
|
|
||||||
val response = chain.proceed(request)
|
|
||||||
|
|
||||||
val contentType = response.header("content-type")
|
|
||||||
if (contentType != null && contentType.startsWith("image/", ignoreCase = true)) {
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignore requests that already have completed the JS challenge
|
|
||||||
if (response.headers["vary"] != null) return response
|
|
||||||
|
|
||||||
val content = response.body.string()
|
|
||||||
val results = cookieRegex.find(content)
|
|
||||||
?: return response.newBuilder().body(content.toResponseBody(response.body.contentType())).build()
|
|
||||||
val (cookieString) = results.destructured
|
|
||||||
return chain.proceed(loadCookie(request, cookieString))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadCookie(request: Request, cookieString: String): Request {
|
|
||||||
val cookie = Cookie.parse(request.url, cookieString)!!
|
|
||||||
client.cookieJar.saveFromResponse(request.url, listOf(cookie))
|
|
||||||
val headers = request.headers.newBuilder()
|
|
||||||
.add("Cookie", cookie.toString())
|
|
||||||
.build()
|
|
||||||
return GET(request.url, headers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
package eu.kanade.tachiyomi.lib.zipinterceptor
|
||||||
|
|
||||||
|
import android.app.ActivityManager
|
||||||
|
import android.app.Application
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.util.Base64
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
|
import tachiyomi.decoder.ImageDecoder
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
|
|
||||||
|
object ImageDecoderWrapper {
|
||||||
|
private var decodeMethod: Method
|
||||||
|
private var newInstanceMethod: Method
|
||||||
|
private var classSignature = ClassSignature.Newest
|
||||||
|
|
||||||
|
private enum class ClassSignature {
|
||||||
|
Old, New, Newest
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
val rectClass = Rect::class.java
|
||||||
|
val booleanClass = Boolean::class.java
|
||||||
|
val intClass = Int::class.java
|
||||||
|
val byteArrayClass = ByteArray::class.java
|
||||||
|
val inputStreamClass = InputStream::class.java
|
||||||
|
|
||||||
|
try {
|
||||||
|
classSignature = ClassSignature.Newest
|
||||||
|
|
||||||
|
// decode(region, sampleSize)
|
||||||
|
decodeMethod = ImageDecoder::class.java.getMethod(
|
||||||
|
"decode",
|
||||||
|
rectClass,
|
||||||
|
intClass,
|
||||||
|
)
|
||||||
|
|
||||||
|
// newInstance(stream, cropBorders, displayProfile)
|
||||||
|
newInstanceMethod = ImageDecoder.Companion::class.java.getMethod(
|
||||||
|
"newInstance",
|
||||||
|
inputStreamClass,
|
||||||
|
booleanClass,
|
||||||
|
byteArrayClass,
|
||||||
|
)
|
||||||
|
} catch (_: NoSuchMethodException) {
|
||||||
|
try {
|
||||||
|
classSignature = ClassSignature.New
|
||||||
|
|
||||||
|
// decode(region, rgb565, sampleSize, applyColorManagement, displayProfile)
|
||||||
|
decodeMethod = ImageDecoder::class.java.getMethod(
|
||||||
|
"decode",
|
||||||
|
rectClass,
|
||||||
|
booleanClass,
|
||||||
|
intClass,
|
||||||
|
booleanClass,
|
||||||
|
byteArrayClass,
|
||||||
|
)
|
||||||
|
|
||||||
|
// newInstance(stream, cropBorders)
|
||||||
|
newInstanceMethod = ImageDecoder.Companion::class.java.getMethod(
|
||||||
|
"newInstance",
|
||||||
|
inputStreamClass,
|
||||||
|
booleanClass,
|
||||||
|
)
|
||||||
|
} catch (_: NoSuchMethodException) {
|
||||||
|
classSignature = ClassSignature.Old
|
||||||
|
|
||||||
|
// decode(region, rgb565, sampleSize)
|
||||||
|
decodeMethod =
|
||||||
|
ImageDecoder::class.java.getMethod(
|
||||||
|
"decode",
|
||||||
|
rectClass,
|
||||||
|
booleanClass,
|
||||||
|
intClass,
|
||||||
|
)
|
||||||
|
|
||||||
|
// newInstance(stream, cropBorders)
|
||||||
|
newInstanceMethod = ImageDecoder.Companion::class.java.getMethod(
|
||||||
|
"newInstance",
|
||||||
|
inputStreamClass,
|
||||||
|
booleanClass,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decodeImage(data: ByteArray, rgb565: Boolean, filename: String, entryName: String): Bitmap {
|
||||||
|
val decoder = when (classSignature) {
|
||||||
|
ClassSignature.Newest -> newInstanceMethod.invoke(ImageDecoder.Companion, ByteArrayInputStream(data), false, null)
|
||||||
|
else -> newInstanceMethod.invoke(ImageDecoder.Companion, ByteArrayInputStream(data), false)
|
||||||
|
} as ImageDecoder?
|
||||||
|
|
||||||
|
if (decoder == null || decoder.width <= 0 || decoder.height <= 0) {
|
||||||
|
throw IOException("Falha ao inicializar o decodificador de imagem")
|
||||||
|
}
|
||||||
|
|
||||||
|
val rect = Rect(0, 0, decoder.width, decoder.height)
|
||||||
|
val bitmap = when (classSignature) {
|
||||||
|
ClassSignature.Newest -> decodeMethod.invoke(decoder, rect, 1)
|
||||||
|
ClassSignature.New -> decodeMethod.invoke(decoder, rect, rgb565, 1, false, null)
|
||||||
|
else -> decodeMethod.invoke(decoder, rect, rgb565, 1)
|
||||||
|
} as Bitmap?
|
||||||
|
|
||||||
|
decoder.recycle()
|
||||||
|
|
||||||
|
if (bitmap == null) {
|
||||||
|
throw IOException("Não foi possível decodificar a imagem $filename#$entryName")
|
||||||
|
}
|
||||||
|
|
||||||
|
return bitmap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open class ZipInterceptor {
|
||||||
|
private val dataUriRegex = Regex("""base64,([0-9a-zA-Z/+=\s]+)""")
|
||||||
|
|
||||||
|
open fun zipGetByteStream(request: Request, response: Response): InputStream {
|
||||||
|
return response.body.byteStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun requestIsZipImage(request: Request): Boolean {
|
||||||
|
return request.url.fragment == "page" && request.url.pathSegments.last().contains(".zip")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun zipImageInterceptor(chain: Interceptor.Chain): Response {
|
||||||
|
val request = chain.request()
|
||||||
|
val response = chain.proceed(request)
|
||||||
|
val filename = request.url.pathSegments.last()
|
||||||
|
|
||||||
|
if (requestIsZipImage(request).not()) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
val zis = ZipInputStream(zipGetByteStream(request, response))
|
||||||
|
|
||||||
|
val images = generateSequence { zis.nextEntry }
|
||||||
|
.mapNotNull {
|
||||||
|
val entryName = it.name
|
||||||
|
val splitEntryName = entryName.split('.')
|
||||||
|
val entryIndex = splitEntryName.first().toInt()
|
||||||
|
val entryType = splitEntryName.last()
|
||||||
|
|
||||||
|
val imageData = if (entryType == "avif" || splitEntryName.size == 1) {
|
||||||
|
zis.readBytes()
|
||||||
|
} else {
|
||||||
|
val svgBytes = zis.readBytes()
|
||||||
|
val svgContent = svgBytes.toString(Charsets.UTF_8)
|
||||||
|
val b64 = dataUriRegex.find(svgContent)?.groupValues?.get(1)
|
||||||
|
?: return@mapNotNull null
|
||||||
|
|
||||||
|
Base64.decode(b64, Base64.DEFAULT)
|
||||||
|
}
|
||||||
|
|
||||||
|
entryIndex to ImageDecoderWrapper.decodeImage(imageData, isLowRamDevice, filename, entryName)
|
||||||
|
}
|
||||||
|
.sortedBy { it.first }
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
zis.closeEntry()
|
||||||
|
zis.close()
|
||||||
|
|
||||||
|
val totalWidth = images.maxOf { it.second.width }
|
||||||
|
val totalHeight = images.sumOf { it.second.height }
|
||||||
|
|
||||||
|
val result = Bitmap.createBitmap(totalWidth, totalHeight, Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = Canvas(result)
|
||||||
|
|
||||||
|
var dy = 0
|
||||||
|
|
||||||
|
images.forEach {
|
||||||
|
val srcRect = Rect(0, 0, it.second.width, it.second.height)
|
||||||
|
val dstRect = Rect(0, dy, it.second.width, dy + it.second.height)
|
||||||
|
|
||||||
|
canvas.drawBitmap(it.second, srcRect, dstRect, null)
|
||||||
|
|
||||||
|
dy += it.second.height
|
||||||
|
}
|
||||||
|
|
||||||
|
val output = ByteArrayOutputStream()
|
||||||
|
result.compress(Bitmap.CompressFormat.JPEG, 90, output)
|
||||||
|
|
||||||
|
val image = output.toByteArray()
|
||||||
|
val body = image.toResponseBody("image/jpeg".toMediaType())
|
||||||
|
|
||||||
|
return response.newBuilder()
|
||||||
|
.body(body)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ActivityManager#isLowRamDevice is based on a system property, which isn't
|
||||||
|
* necessarily trustworthy. 1GB is supposedly the regular threshold.
|
||||||
|
*
|
||||||
|
* Instead, we consider anything with less than 3GB of RAM as low memory
|
||||||
|
* considering how heavy image processing can be.
|
||||||
|
*/
|
||||||
|
private val isLowRamDevice by lazy {
|
||||||
|
val ctx = Injekt.get<Application>()
|
||||||
|
val activityManager = ctx.getSystemService("activity") as ActivityManager
|
||||||
|
val memInfo = ActivityManager.MemoryInfo()
|
||||||
|
|
||||||
|
activityManager.getMemoryInfo(memInfo)
|
||||||
|
|
||||||
|
memInfo.totalMem < 3L * 1024 * 1024 * 1024
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id("com.android.library")
|
|
||||||
kotlin("android")
|
|
||||||
id("kotlinx-serialization")
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileSdk = AndroidConfig.compileSdk
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdk = 29
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace = "eu.kanade.tachiyomi.lib.themesources"
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
freeCompilerArgs += "-opt-in=kotlinx.serialization.ExperimentalSerializationApi"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
configurations {
|
|
||||||
compileOnly {
|
|
||||||
isCanBeResolved = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
compileOnly(libs.bundles.common)
|
|
||||||
|
|
||||||
// Implements all :lib libraries on the multisrc generator
|
|
||||||
// Note that this does not mean that generated sources are going to
|
|
||||||
// implement them too; this is just to be able to compile and generate sources.
|
|
||||||
rootProject.subprojects
|
|
||||||
.filter { it.path.startsWith(":lib") }
|
|
||||||
.forEach(::implementation)
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks {
|
|
||||||
register<JavaExec>("generateExtensions") {
|
|
||||||
val buildDir = layout.buildDirectory.asFile.get()
|
|
||||||
classpath = configurations.compileOnly.get() +
|
|
||||||
configurations.androidApis.get() + // android.jar path
|
|
||||||
files("$buildDir/intermediates/aar_main_jar/debug/classes.jar") // jar made from this module
|
|
||||||
mainClass.set("generator.GeneratorMainKt")
|
|
||||||
|
|
||||||
workingDir = workingDir.parentFile // project root
|
|
||||||
|
|
||||||
errorOutput = System.out // for GitHub workflow commands
|
|
||||||
|
|
||||||
if (!logger.isInfoEnabled) {
|
|
||||||
standardOutput = org.gradle.internal.io.NullOutputStream.INSTANCE
|
|
||||||
}
|
|
||||||
|
|
||||||
dependsOn("ktLint", "assembleDebug")
|
|
||||||
}
|
|
||||||
|
|
||||||
register<org.jmailen.gradle.kotlinter.tasks.LintTask>("ktLint") {
|
|
||||||
if (project.hasProperty("theme")) {
|
|
||||||
val theme = project.property("theme")
|
|
||||||
source(files("src/main/java/eu/kanade/tachiyomi/multisrc/$theme", "overrides/$theme"))
|
|
||||||
return@register
|
|
||||||
}
|
|
||||||
source(files("src", "overrides"))
|
|
||||||
}
|
|
||||||
|
|
||||||
register<org.jmailen.gradle.kotlinter.tasks.FormatTask>("ktFormat") {
|
|
||||||
if (project.hasProperty("theme")) {
|
|
||||||
val theme = project.property("theme")
|
|
||||||
source(files("src/main/java/eu/kanade/tachiyomi/multisrc/$theme", "overrides/$theme"))
|
|
||||||
return@register
|
|
||||||
}
|
|
||||||
source(files("src", "overrides"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 34 KiB |
@@ -1,22 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<application>
|
|
||||||
<activity
|
|
||||||
android:name="eu.kanade.tachiyomi.multisrc.a3manga.A3MangaUrlActivity"
|
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:exported="true"
|
|
||||||
android:theme="@android:style/Theme.NoDisplay">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data android:host="${SOURCEHOST}" />
|
|
||||||
<data android:host="*.${SOURCEHOST}" />
|
|
||||||
<data android:pathPattern="/truyen-tranh/..*"
|
|
||||||
android:scheme="${SOURCESCHEME}" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
||||||
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 50 KiB |
@@ -1,7 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.vi.ngonphong
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.a3manga.A3Manga
|
|
||||||
|
|
||||||
class NgonPhong : A3Manga("Ngôn Phong", "https://www.ngonphong.com", "vi") {
|
|
||||||
override val id: Long = 7268977637085631557
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 139 KiB |
@@ -1,68 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.en.manhwaxxl
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.bakamanga.BakaManga
|
|
||||||
|
|
||||||
class ManhwaXXL : BakaManga(
|
|
||||||
"Manhwa XXL",
|
|
||||||
"https://manhwaxxl.com",
|
|
||||||
"en",
|
|
||||||
) {
|
|
||||||
override fun getGenreList() = arrayOf(
|
|
||||||
Pair("All", ""),
|
|
||||||
Pair("Action", "action"),
|
|
||||||
Pair("Adaptation", "adaptation"),
|
|
||||||
Pair("Adult", "adult"),
|
|
||||||
Pair("Adventure", "adventure"),
|
|
||||||
Pair("BL", "bl"),
|
|
||||||
Pair("Comedy", "comedy"),
|
|
||||||
Pair("Cooking", "cooking"),
|
|
||||||
Pair("Demons", "demons"),
|
|
||||||
Pair("Drama", "drama"),
|
|
||||||
Pair("Ecchi", "ecchi"),
|
|
||||||
Pair("Fantasy", "fantasy"),
|
|
||||||
Pair("Full color", "full-color"),
|
|
||||||
Pair("Game", "game"),
|
|
||||||
Pair("Gender Bender", "gender-bender"),
|
|
||||||
Pair("GL", "gl"),
|
|
||||||
Pair("Harem", "harem"),
|
|
||||||
Pair("Historical", "historical"),
|
|
||||||
Pair("Horror", "horror"),
|
|
||||||
Pair("Isekai", "isekai"),
|
|
||||||
Pair("Josei", "josei"),
|
|
||||||
Pair("Live action", "live-action"),
|
|
||||||
Pair("Love & Romance", "love-romance"),
|
|
||||||
Pair("Magic", "magic"),
|
|
||||||
Pair("Manga", "manga"),
|
|
||||||
Pair("Manhua", "manhua"),
|
|
||||||
Pair("Manhwa", "manhwa"),
|
|
||||||
Pair("Martial Arts", "martial-arts"),
|
|
||||||
Pair("Mature", "mature"),
|
|
||||||
Pair("Mecha", "mecha"),
|
|
||||||
Pair("Mystery", "mystery"),
|
|
||||||
Pair("Omegaverse", "omegaverse"),
|
|
||||||
Pair("Psychological", "psychological"),
|
|
||||||
Pair("Raw", "raw"),
|
|
||||||
Pair("Reincarnation", "reincarnation"),
|
|
||||||
Pair("Romance", "romance"),
|
|
||||||
Pair("RPG", "rpg"),
|
|
||||||
Pair("School Life", "school-life"),
|
|
||||||
Pair("Sci-fi", "sci-fi"),
|
|
||||||
Pair("Seinen", "seinen"),
|
|
||||||
Pair("Shoujo", "shoujo"),
|
|
||||||
Pair("Shoujo Ai", "shoujo-ai"),
|
|
||||||
Pair("Shounen", "shounen"),
|
|
||||||
Pair("Slice of Life", "slice-of-life"),
|
|
||||||
Pair("Smut", "smut"),
|
|
||||||
Pair("Sports", "sports"),
|
|
||||||
Pair("Supernatural", "supernatural"),
|
|
||||||
Pair("Thriller", "thriller"),
|
|
||||||
Pair("Tragedy", "tragedy"),
|
|
||||||
Pair("Vampire", "vampire"),
|
|
||||||
Pair("Vanilla", "vanilla"),
|
|
||||||
Pair("Webtoon", "webtoon"),
|
|
||||||
Pair("Webtoons", "webtoons"),
|
|
||||||
Pair("Yaoi", "yaoi"),
|
|
||||||
Pair("Yuri", "yuri"),
|
|
||||||
Pair("Zombie", "zombie"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.en.bakkinselfhosted
|
|
||||||
|
|
||||||
import androidx.preference.EditTextPreference
|
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
import eu.kanade.tachiyomi.multisrc.bakkin.BakkinReaderX
|
|
||||||
|
|
||||||
class BakkinSelfHosted : BakkinReaderX("Bakkin Self-hosted", "", "en") {
|
|
||||||
override val baseUrl by lazy {
|
|
||||||
preferences.getString("baseUrl", "http://127.0.0.1/")!!
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
|
||||||
super.setupPreferenceScreen(screen)
|
|
||||||
EditTextPreference(screen.context).apply {
|
|
||||||
key = "baseUrl"
|
|
||||||
title = "Custom URL"
|
|
||||||
summary = "Connect to a self-hosted Bakkin Reader X server"
|
|
||||||
setDefaultValue("http://127.0.0.1/")
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
// Make sure the URL ends with one slash
|
|
||||||
val url = (newValue as String).trimEnd('/') + '/'
|
|
||||||
preferences.edit().putString("baseUrl", url).commit()
|
|
||||||
}
|
|
||||||
}.let(screen::addPreference)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 30 KiB |
@@ -1,26 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<application>
|
|
||||||
<activity
|
|
||||||
android:name="eu.kanade.tachiyomi.multisrc.bilibili.BilibiliUrlActivity"
|
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:exported="true"
|
|
||||||
android:theme="@android:style/Theme.NoDisplay">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data android:scheme="https" />
|
|
||||||
|
|
||||||
<data android:host="bilibilicomics.com" />
|
|
||||||
<data android:host="m.bilibilicomics.com" />
|
|
||||||
<data android:host="www.bilibilicomics.com" />
|
|
||||||
|
|
||||||
<data android:pathPattern="/detail/mc..*" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
||||||
@@ -1,411 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.bilibilicomics
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.bilibili.Bilibili
|
|
||||||
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliAccessToken
|
|
||||||
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliAccessTokenCookie
|
|
||||||
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliComicDto
|
|
||||||
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliCredential
|
|
||||||
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliGetCredential
|
|
||||||
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliIntl
|
|
||||||
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliSearchDto
|
|
||||||
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliTag
|
|
||||||
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliUnlockedEpisode
|
|
||||||
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliUserEpisodes
|
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
|
||||||
import kotlinx.serialization.json.put
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Interceptor
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import okhttp3.Response
|
|
||||||
import okio.Buffer
|
|
||||||
import java.io.IOException
|
|
||||||
import java.net.URLDecoder
|
|
||||||
import java.util.Calendar
|
|
||||||
|
|
||||||
class BilibiliComicsFactory : SourceFactory {
|
|
||||||
override fun createSources() = listOf(
|
|
||||||
BilibiliComicsEn(),
|
|
||||||
BilibiliComicsCn(),
|
|
||||||
BilibiliComicsId(),
|
|
||||||
BilibiliComicsEs(),
|
|
||||||
BilibiliComicsFr(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class BilibiliComics(lang: String) : Bilibili(
|
|
||||||
"BILIBILI COMICS",
|
|
||||||
"https://www.bilibilicomics.com",
|
|
||||||
lang,
|
|
||||||
) {
|
|
||||||
|
|
||||||
override val client: OkHttpClient = super.client.newBuilder()
|
|
||||||
.apply { interceptors().add(0, Interceptor { chain -> signedInIntercept(chain) }) }
|
|
||||||
.build()
|
|
||||||
|
|
||||||
init {
|
|
||||||
setAccessTokenCookie(baseUrl.toHttpUrl())
|
|
||||||
}
|
|
||||||
|
|
||||||
override val signedIn: Boolean
|
|
||||||
get() = accessTokenCookie != null
|
|
||||||
|
|
||||||
private val globalApiSubDomain: String
|
|
||||||
get() = GLOBAL_API_SUBDOMAINS[(accessTokenCookie?.area?.toIntOrNull() ?: 1) - 1]
|
|
||||||
|
|
||||||
private val globalApiBaseUrl: String
|
|
||||||
get() = "https://$globalApiSubDomain.bilibilicomics.com"
|
|
||||||
|
|
||||||
private var accessTokenCookie: BilibiliAccessTokenCookie? = null
|
|
||||||
|
|
||||||
private val dayOfWeek: Int
|
|
||||||
get() = Calendar.getInstance().get(Calendar.DAY_OF_WEEK) - 1
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
|
||||||
val jsonPayload = buildJsonObject { put("day", dayOfWeek) }
|
|
||||||
val requestBody = jsonPayload.toString().toRequestBody(JSON_MEDIA_TYPE)
|
|
||||||
|
|
||||||
val newHeaders = headersBuilder()
|
|
||||||
.add("Content-Length", requestBody.contentLength().toString())
|
|
||||||
.add("Content-Type", requestBody.contentType().toString())
|
|
||||||
.set("Referer", "$baseUrl/schedule")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val apiUrl = "$baseUrl/$API_COMIC_V1_COMIC_ENDPOINT/GetSchedule".toHttpUrl().newBuilder()
|
|
||||||
.addCommonParameters()
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
return POST(apiUrl, newHeaders, requestBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
|
||||||
val result = response.parseAs<BilibiliSearchDto>()
|
|
||||||
|
|
||||||
if (result.code != 0) {
|
|
||||||
return MangasPage(emptyList(), hasNextPage = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
val comicList = result.data!!.list.map(::latestMangaFromObject)
|
|
||||||
|
|
||||||
return MangasPage(comicList, hasNextPage = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun latestMangaFromObject(comic: BilibiliComicDto): SManga = SManga.create().apply {
|
|
||||||
title = comic.title
|
|
||||||
thumbnail_url = comic.verticalCover + THUMBNAIL_RESOLUTION
|
|
||||||
url = "/detail/mc${comic.comicId}"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
|
||||||
if (!signedIn) {
|
|
||||||
return super.chapterListParse(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = response.parseAs<BilibiliComicDto>()
|
|
||||||
|
|
||||||
if (result.code != 0) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
val comic = result.data!!
|
|
||||||
|
|
||||||
val userEpisodesRequest = userEpisodesRequest(comic.id)
|
|
||||||
val userEpisodesResponse = client.newCall(userEpisodesRequest).execute()
|
|
||||||
val unlockedEpisodes = userEpisodesParse(userEpisodesResponse)
|
|
||||||
|
|
||||||
return comic.episodeList.map { ep -> chapterFromObject(ep, comic.id, isUnlocked = ep.id in unlockedEpisodes) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun userEpisodesRequest(comicId: Int): Request {
|
|
||||||
val jsonPayload = buildJsonObject { put("comic_id", comicId) }
|
|
||||||
val requestBody = jsonPayload.toString().toRequestBody(JSON_MEDIA_TYPE)
|
|
||||||
|
|
||||||
val newHeaders = headersBuilder()
|
|
||||||
.set("Referer", baseUrl)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val apiUrl = "$globalApiBaseUrl/$API_COMIC_V1_USER_ENDPOINT/GetUserEpisodes".toHttpUrl()
|
|
||||||
.newBuilder()
|
|
||||||
.addCommonParameters()
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
return POST(apiUrl, newHeaders, requestBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun userEpisodesParse(response: Response): List<Int> {
|
|
||||||
if (!response.isSuccessful) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = response.parseAs<BilibiliUserEpisodes>()
|
|
||||||
|
|
||||||
if (result.code != 0) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.data!!.unlockedEpisodes.orEmpty()
|
|
||||||
.map(BilibiliUnlockedEpisode::id)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
|
||||||
if (!signedIn) {
|
|
||||||
return super.pageListRequest(chapter)
|
|
||||||
}
|
|
||||||
|
|
||||||
val chapterPaths = (baseUrl + chapter.url).toHttpUrl().pathSegments
|
|
||||||
val comicId = chapterPaths[0].removePrefix("mc").toInt()
|
|
||||||
val episodeId = chapterPaths[1].toInt()
|
|
||||||
|
|
||||||
val jsonPayload = BilibiliGetCredential(comicId, episodeId, 1)
|
|
||||||
val requestBody = json.encodeToString(jsonPayload).toRequestBody(JSON_MEDIA_TYPE)
|
|
||||||
|
|
||||||
val newHeaders = headersBuilder()
|
|
||||||
.set("Referer", baseUrl + chapter.url)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val apiUrl = "$globalApiBaseUrl/$API_GLOBAL_V1_USER_ENDPOINT/GetCredential".toHttpUrl()
|
|
||||||
.newBuilder()
|
|
||||||
.addCommonParameters()
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
return POST(apiUrl, newHeaders, requestBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
if (!signedIn) {
|
|
||||||
return super.pageListParse(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.isSuccessful) {
|
|
||||||
throw Exception(intl.failedToGetCredential)
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = response.parseAs<BilibiliCredential>()
|
|
||||||
val credential = result.data?.credential ?: ""
|
|
||||||
|
|
||||||
val requestPayload = response.request.bodyString
|
|
||||||
val credentialInfo = json.decodeFromString<BilibiliGetCredential>(requestPayload)
|
|
||||||
val chapterUrl = "/mc${credentialInfo.comicId}/${credentialInfo.episodeId}"
|
|
||||||
|
|
||||||
val imageIndexRequest = imageIndexRequest(chapterUrl, credential)
|
|
||||||
val imageIndexResponse = client.newCall(imageIndexRequest).execute()
|
|
||||||
|
|
||||||
return super.pageListParse(imageIndexResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setAccessTokenCookie(url: HttpUrl) {
|
|
||||||
val authCookie = client.cookieJar.loadForRequest(url)
|
|
||||||
.firstOrNull { cookie -> cookie.name == ACCESS_TOKEN_COOKIE_NAME }
|
|
||||||
?.let { cookie -> URLDecoder.decode(cookie.value, "UTF-8") }
|
|
||||||
?.let { jsonString -> json.decodeFromString<BilibiliAccessTokenCookie>(jsonString) }
|
|
||||||
|
|
||||||
if (accessTokenCookie == null) {
|
|
||||||
accessTokenCookie = authCookie
|
|
||||||
} else if (authCookie == null) {
|
|
||||||
accessTokenCookie = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun signedInIntercept(chain: Interceptor.Chain): Response {
|
|
||||||
var request = chain.request()
|
|
||||||
val requestUrl = request.url.toString()
|
|
||||||
|
|
||||||
if (!requestUrl.contains("bilibilicomics.com")) {
|
|
||||||
return chain.proceed(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
setAccessTokenCookie(request.url)
|
|
||||||
|
|
||||||
if (!accessTokenCookie?.accessToken.isNullOrEmpty()) {
|
|
||||||
request = request.newBuilder()
|
|
||||||
.addHeader("Authorization", "Bearer ${accessTokenCookie!!.accessToken}")
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = chain.proceed(request)
|
|
||||||
|
|
||||||
// Try to refresh the token if it expired.
|
|
||||||
if (response.code == 401 && !accessTokenCookie?.refreshToken.isNullOrEmpty()) {
|
|
||||||
response.close()
|
|
||||||
|
|
||||||
val refreshTokenRequest = refreshTokenRequest(
|
|
||||||
accessTokenCookie!!.accessToken,
|
|
||||||
accessTokenCookie!!.refreshToken,
|
|
||||||
)
|
|
||||||
val refreshTokenResponse = chain.proceed(refreshTokenRequest)
|
|
||||||
|
|
||||||
accessTokenCookie = refreshTokenParse(refreshTokenResponse)
|
|
||||||
refreshTokenResponse.close()
|
|
||||||
|
|
||||||
request = request.newBuilder()
|
|
||||||
.header("Authorization", "Bearer ${accessTokenCookie!!.accessToken}")
|
|
||||||
.build()
|
|
||||||
return chain.proceed(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun refreshTokenRequest(accessToken: String, refreshToken: String): Request {
|
|
||||||
val jsonPayload = buildJsonObject { put("refresh_token", refreshToken) }
|
|
||||||
val requestBody = jsonPayload.toString().toRequestBody(JSON_MEDIA_TYPE)
|
|
||||||
|
|
||||||
val newHeaders = headersBuilder()
|
|
||||||
.add("Authorization", "Bearer $accessToken")
|
|
||||||
.set("Referer", baseUrl)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val apiUrl = "$globalApiBaseUrl/$API_GLOBAL_V1_USER_ENDPOINT/RefreshToken".toHttpUrl()
|
|
||||||
.newBuilder()
|
|
||||||
.addCommonParameters()
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
return POST(apiUrl, newHeaders, requestBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun refreshTokenParse(response: Response): BilibiliAccessTokenCookie {
|
|
||||||
if (!response.isSuccessful) {
|
|
||||||
throw IOException(intl.failedToRefreshToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = response.parseAs<BilibiliAccessToken>()
|
|
||||||
|
|
||||||
if (result.code != 0) {
|
|
||||||
throw IOException(intl.failedToRefreshToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
val accessToken = result.data!!
|
|
||||||
|
|
||||||
return BilibiliAccessTokenCookie(
|
|
||||||
accessToken.accessToken,
|
|
||||||
accessToken.refreshToken,
|
|
||||||
accessTokenCookie!!.area,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val Request.bodyString: String
|
|
||||||
get() {
|
|
||||||
val requestCopy = newBuilder().build()
|
|
||||||
val buffer = Buffer()
|
|
||||||
|
|
||||||
return runCatching { buffer.apply { requestCopy.body!!.writeTo(this) }.readUtf8() }
|
|
||||||
.getOrNull() ?: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val ACCESS_TOKEN_COOKIE_NAME = "access_token"
|
|
||||||
|
|
||||||
private val GLOBAL_API_SUBDOMAINS = arrayOf("us-user", "sg-user")
|
|
||||||
private const val API_GLOBAL_V1_USER_ENDPOINT = "twirp/global.v1.User"
|
|
||||||
private const val API_COMIC_V1_USER_ENDPOINT = "twirp/comic.v1.User"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class BilibiliComicsEn : BilibiliComics(BilibiliIntl.ENGLISH) {
|
|
||||||
|
|
||||||
override fun getAllGenres(): Array<BilibiliTag> = arrayOf(
|
|
||||||
BilibiliTag("All", -1),
|
|
||||||
BilibiliTag("Action", 19),
|
|
||||||
BilibiliTag("Adventure", 22),
|
|
||||||
BilibiliTag("BL", 3),
|
|
||||||
BilibiliTag("Comedy", 14),
|
|
||||||
BilibiliTag("Eastern", 30),
|
|
||||||
BilibiliTag("Fantasy", 11),
|
|
||||||
BilibiliTag("GL", 16),
|
|
||||||
BilibiliTag("Harem", 15),
|
|
||||||
BilibiliTag("Historical", 12),
|
|
||||||
BilibiliTag("Horror", 23),
|
|
||||||
BilibiliTag("Mistery", 17),
|
|
||||||
BilibiliTag("Romance", 13),
|
|
||||||
BilibiliTag("Slice of Life", 21),
|
|
||||||
BilibiliTag("Suspense", 41),
|
|
||||||
BilibiliTag("Teen", 20),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
class BilibiliComicsCn : BilibiliComics(BilibiliIntl.SIMPLIFIED_CHINESE) {
|
|
||||||
|
|
||||||
override fun getAllGenres(): Array<BilibiliTag> = arrayOf(
|
|
||||||
BilibiliTag("全部", -1),
|
|
||||||
BilibiliTag("校园", 18),
|
|
||||||
BilibiliTag("都市", 9),
|
|
||||||
BilibiliTag("耽美", 3),
|
|
||||||
BilibiliTag("少女", 20),
|
|
||||||
BilibiliTag("恋爱", 13),
|
|
||||||
BilibiliTag("奇幻", 11),
|
|
||||||
BilibiliTag("热血", 19),
|
|
||||||
BilibiliTag("冒险", 22),
|
|
||||||
BilibiliTag("古风", 12),
|
|
||||||
BilibiliTag("百合", 16),
|
|
||||||
BilibiliTag("玄幻", 30),
|
|
||||||
BilibiliTag("悬疑", 41),
|
|
||||||
BilibiliTag("科幻", 8),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
class BilibiliComicsId : BilibiliComics(BilibiliIntl.INDONESIAN) {
|
|
||||||
|
|
||||||
override fun getAllGenres(): Array<BilibiliTag> = arrayOf(
|
|
||||||
BilibiliTag("Semua", -1),
|
|
||||||
BilibiliTag("Aksi", 19),
|
|
||||||
BilibiliTag("Fantasi Timur", 30),
|
|
||||||
BilibiliTag("Fantasi", 11),
|
|
||||||
BilibiliTag("Historis", 12),
|
|
||||||
BilibiliTag("Horror", 23),
|
|
||||||
BilibiliTag("Kampus", 18),
|
|
||||||
BilibiliTag("Komedi", 14),
|
|
||||||
BilibiliTag("Menegangkan", 41),
|
|
||||||
BilibiliTag("Remaja", 20),
|
|
||||||
BilibiliTag("Romantis", 13),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
class BilibiliComicsEs : BilibiliComics(BilibiliIntl.SPANISH) {
|
|
||||||
|
|
||||||
override fun getAllGenres(): Array<BilibiliTag> = arrayOf(
|
|
||||||
BilibiliTag("Todos", -1),
|
|
||||||
BilibiliTag("Adolescencia", 105),
|
|
||||||
BilibiliTag("BL", 3),
|
|
||||||
BilibiliTag("Ciberdeportes", 104),
|
|
||||||
BilibiliTag("Ciencia ficción", 8),
|
|
||||||
BilibiliTag("Comedia", 14),
|
|
||||||
BilibiliTag("Fantasía occidental", 106),
|
|
||||||
BilibiliTag("Fantasía", 11),
|
|
||||||
BilibiliTag("Ficción Realista", 116),
|
|
||||||
BilibiliTag("GL", 16),
|
|
||||||
BilibiliTag("Histórico", 12),
|
|
||||||
BilibiliTag("Horror", 23),
|
|
||||||
BilibiliTag("Juvenil", 20),
|
|
||||||
BilibiliTag("Moderno", 111),
|
|
||||||
BilibiliTag("Oriental", 30),
|
|
||||||
BilibiliTag("Romance", 13),
|
|
||||||
BilibiliTag("Suspenso", 41),
|
|
||||||
BilibiliTag("Urbano", 9),
|
|
||||||
BilibiliTag("Wuxia", 103),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
class BilibiliComicsFr : BilibiliComics(BilibiliIntl.FRENCH) {
|
|
||||||
|
|
||||||
override fun getAllGenres(): Array<BilibiliTag> = arrayOf(
|
|
||||||
BilibiliTag("Tout", -1),
|
|
||||||
BilibiliTag("BL", 3),
|
|
||||||
BilibiliTag("Science Fiction", 8),
|
|
||||||
BilibiliTag("Historique", 12),
|
|
||||||
BilibiliTag("Romance", 13),
|
|
||||||
BilibiliTag("GL", 16),
|
|
||||||
BilibiliTag("Fantasy Orientale", 30),
|
|
||||||
BilibiliTag("Suspense", 41),
|
|
||||||
BilibiliTag("Moderne", 111),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<application>
|
|
||||||
<activity
|
|
||||||
android:name="eu.kanade.tachiyomi.multisrc.bilibili.BilibiliUrlActivity"
|
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:exported="true"
|
|
||||||
android:theme="@android:style/Theme.NoDisplay">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data android:scheme="https" />
|
|
||||||
|
|
||||||
<data android:host="manga.bilibili.com" />
|
|
||||||
|
|
||||||
<data android:pathPattern="/detail/mc..*" />
|
|
||||||
<data android:pathPattern="/m/detail/mc..*" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
||||||
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 209 KiB |
@@ -1,83 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.zh.bilibilimanga
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.bilibili.Bilibili
|
|
||||||
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliComicDto
|
|
||||||
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliIntl
|
|
||||||
import eu.kanade.tachiyomi.multisrc.bilibili.BilibiliTag
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.Response
|
|
||||||
|
|
||||||
class BilibiliManga : Bilibili(
|
|
||||||
"哔哩哔哩漫画",
|
|
||||||
"https://manga.bilibili.com",
|
|
||||||
BilibiliIntl.SIMPLIFIED_CHINESE,
|
|
||||||
) {
|
|
||||||
|
|
||||||
override val id: Long = 3561131545129718586
|
|
||||||
|
|
||||||
override fun headersBuilder() = Headers.Builder().apply {
|
|
||||||
add("User-Agent", DEFAULT_USER_AGENT)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
|
||||||
val result = response.parseAs<BilibiliComicDto>()
|
|
||||||
|
|
||||||
if (result.code != 0) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.data!!.episodeList
|
|
||||||
.filter { episode -> episode.isInFree || !episode.isLocked }
|
|
||||||
.map { ep -> chapterFromObject(ep, result.data.id) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override val defaultPopularSort: Int = 0
|
|
||||||
|
|
||||||
override val defaultLatestSort: Int = 1
|
|
||||||
|
|
||||||
override fun getAllSortOptions(): Array<BilibiliTag> = arrayOf(
|
|
||||||
BilibiliTag(intl.sortPopular, 0),
|
|
||||||
BilibiliTag(intl.sortUpdated, 1),
|
|
||||||
BilibiliTag(intl.sortFollowers, 2),
|
|
||||||
BilibiliTag(intl.sortAdded, 3),
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun getAllPrices(): Array<String> =
|
|
||||||
arrayOf(intl.priceAll, intl.priceFree, intl.pricePaid, intl.priceWaitForFree)
|
|
||||||
|
|
||||||
override fun getAllGenres(): Array<BilibiliTag> = arrayOf(
|
|
||||||
BilibiliTag("全部", -1),
|
|
||||||
BilibiliTag("竞技", 1034),
|
|
||||||
BilibiliTag("冒险", 1013),
|
|
||||||
BilibiliTag("热血", 999),
|
|
||||||
BilibiliTag("搞笑", 994),
|
|
||||||
BilibiliTag("恋爱", 995),
|
|
||||||
BilibiliTag("少女", 1026),
|
|
||||||
BilibiliTag("日常", 1020),
|
|
||||||
BilibiliTag("校园", 1001),
|
|
||||||
BilibiliTag("治愈", 1007),
|
|
||||||
BilibiliTag("古风", 997),
|
|
||||||
BilibiliTag("玄幻", 1016),
|
|
||||||
BilibiliTag("奇幻", 998),
|
|
||||||
BilibiliTag("惊奇", 996),
|
|
||||||
BilibiliTag("悬疑", 1023),
|
|
||||||
BilibiliTag("都市", 1002),
|
|
||||||
BilibiliTag("剧情", 1030),
|
|
||||||
BilibiliTag("总裁", 1004),
|
|
||||||
BilibiliTag("科幻", 1015),
|
|
||||||
BilibiliTag("正能量", 1028),
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun getAllAreas(): Array<BilibiliTag> = arrayOf(
|
|
||||||
BilibiliTag("全部", -1),
|
|
||||||
BilibiliTag("大陆", 1),
|
|
||||||
BilibiliTag("日本", 2),
|
|
||||||
BilibiliTag("韩国", 6),
|
|
||||||
BilibiliTag("其他", 5),
|
|
||||||
)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<application>
|
|
||||||
<activity
|
|
||||||
android:name="eu.kanade.tachiyomi.multisrc.bilibili.BilibiliUrlActivity"
|
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:exported="true"
|
|
||||||
android:theme="@android:style/Theme.NoDisplay">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
<data
|
|
||||||
android:host="${SOURCEHOST}"
|
|
||||||
android:pathPattern="/detail/mc..*"
|
|
||||||
android:scheme="${SOURCESCHEME}" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
||||||
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 235 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1006 B |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |