Compare commits
2 Commits
master
...
source/neo
| Author | SHA1 | Date |
|---|---|---|
|
|
089191d6f1 | 3 years ago |
|
|
eebb6f0c44 | 4 years ago |
@ -1 +0,0 @@
|
||||
total: 1256
|
||||
@ -1,25 +0,0 @@
|
||||
name: Check & Test latest parsers
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
check-and-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository 🌏
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up enviroment 🔧
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Set up Gradle 📦
|
||||
uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3
|
||||
|
||||
- name: Compile parsers 🚀
|
||||
run: ./gradlew compileKotlin
|
||||
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.6.21" />
|
||||
</component>
|
||||
</project>
|
||||
@ -0,0 +1,74 @@
|
||||
import tasks.ReportGenerateTask
|
||||
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'org.jetbrains.kotlin.jvm'
|
||||
id 'com.google.devtools.ksp'
|
||||
id 'maven-publish'
|
||||
}
|
||||
|
||||
group = 'org.koitharu'
|
||||
version = '1.0'
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
compileKotlin {
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
freeCompilerArgs += [
|
||||
'-opt-in=kotlin.RequiresOptIn',
|
||||
'-opt-in=kotlin.contracts.ExperimentalContracts',
|
||||
'-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
|
||||
'-opt-in=org.koitharu.kotatsu.parsers.InternalParsersApi',
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
compileTestKotlin {
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
freeCompilerArgs += [
|
||||
'-opt-in=kotlin.RequiresOptIn',
|
||||
'-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
|
||||
'-opt-in=org.koitharu.kotatsu.parsers.InternalParsersApi',
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
main.kotlin.srcDirs += 'build/generated/ksp/main/kotlin'
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
publishing {
|
||||
publications {
|
||||
mavenJava(MavenPublication) {
|
||||
from components.java
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
|
||||
implementation 'com.squareup.okio:okio:3.2.0'
|
||||
api 'org.jsoup:jsoup:1.15.3'
|
||||
implementation 'org.json:json:20220320'
|
||||
implementation 'androidx.collection:collection-ktx:1.2.0'
|
||||
|
||||
ksp project(':kotatsu-parsers-ksp')
|
||||
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.0'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.0'
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
|
||||
testImplementation 'io.webfolder:quickjs:1.1.0'
|
||||
}
|
||||
|
||||
//noinspection ConfigurationAvoidance
|
||||
task generateTestsReport(type: ReportGenerateTask)
|
||||
@ -1,64 +0,0 @@
|
||||
import tasks.ReportGenerateTask
|
||||
|
||||
plugins {
|
||||
`java-library`
|
||||
`maven-publish`
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
alias(libs.plugins.ksp)
|
||||
}
|
||||
|
||||
group = "org.koitharu"
|
||||
version = "1.0"
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
ksp {
|
||||
arg("summaryOutputDir", "${projectDir}/.github")
|
||||
}
|
||||
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.addAll(
|
||||
"-opt-in=kotlin.RequiresOptIn",
|
||||
"-opt-in=kotlin.contracts.ExperimentalContracts",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=org.koitharu.kotatsu.parsers.InternalParsersApi",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(8)
|
||||
explicitApiWarning()
|
||||
sourceSets["main"].kotlin.srcDirs("build/generated/ksp/main/kotlin")
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
create<MavenPublication>("mavenJava") {
|
||||
from(components["java"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.okio)
|
||||
implementation(libs.json)
|
||||
implementation(libs.androidx.collection)
|
||||
api(libs.jsoup)
|
||||
|
||||
ksp(project(":kotatsu-parsers-ksp"))
|
||||
|
||||
testImplementation(libs.junit.api)
|
||||
testImplementation(libs.junit.engine)
|
||||
testImplementation(libs.junit.params)
|
||||
testRuntimeOnly(libs.junit.launcher)
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.quickjs)
|
||||
}
|
||||
|
||||
tasks.register<ReportGenerateTask>("generateTestsReport")
|
||||
@ -0,0 +1,13 @@
|
||||
plugins {
|
||||
id('org.jetbrains.kotlin.jvm') version '1.6.21'
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation gradleApi()
|
||||
implementation 'org.simpleframework:simple-xml:2.7.1'
|
||||
implementation 'com.soywiz.korlibs.korte:korte-jvm:3.0.0-Beta5'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3'
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(8)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.korte)
|
||||
implementation(libs.simplexml)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
}
|
||||
Binary file not shown.
@ -1,7 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
dependencyResolutionManagement {
|
||||
versionCatalogs {
|
||||
create("libs") {
|
||||
from(files("../gradle/libs.versions.toml"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,804 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<gaphor xmlns="https://gaphor.org/model" xmlns:Core="https://gaphor.org/modelinglanguage/Core" xmlns:UML="https://gaphor.org/modelinglanguage/UML" xmlns:general="https://gaphor.org/modelinglanguage/general" version="4" gaphor-version="3.1.0">
|
||||
<model>
|
||||
<Core:StyleSheet id="58d6989a-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
<UML:Package id="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed">
|
||||
<name>
|
||||
<val>Новая модель</val>
|
||||
</name>
|
||||
<ownedDiagram>
|
||||
<reflist>
|
||||
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</reflist>
|
||||
</ownedDiagram>
|
||||
<ownedType>
|
||||
<reflist>
|
||||
<ref refid="0b54a350-f59d-11ef-bfb1-4cbb5880a0b8"/>
|
||||
<ref refid="a300f58a-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="ad4c68d0-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="198a3108-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="32081654-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</ownedType>
|
||||
<packagedElement>
|
||||
<reflist>
|
||||
<ref refid="0b54a350-f59d-11ef-bfb1-4cbb5880a0b8"/>
|
||||
<ref refid="a300f58a-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="ad4c68d0-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="198a3108-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="32081654-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="25a17c58-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="41318a02-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</packagedElement>
|
||||
</UML:Package>
|
||||
<UML:Diagram id="58d6c536-66f8-11ec-b4c8-0456e5e540ed">
|
||||
<element>
|
||||
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</element>
|
||||
<name>
|
||||
<val>Новая диаграмма</val>
|
||||
</name>
|
||||
<ownedPresentation>
|
||||
<reflist>
|
||||
<ref refid="0b54edc4-f59d-11ef-bfb1-4cbb5880a0b8"/>
|
||||
<ref refid="a3018fc2-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="ad4d2aae-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="198aace6-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="320868c0-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="b760bcd4-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="bff32eae-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="d5113ca4-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="e8042ad8-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="531831f2-f5c0-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="5ccae8d4-f5c0-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="b6e0240e-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="254e40f6-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="2bb6e87a-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="40e36d2c-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="bcc07c64-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="f6b48e4c-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="f89ba010-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="fabe0540-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="6f993af6-f5c0-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</ownedPresentation>
|
||||
</UML:Diagram>
|
||||
<UML:Class id="0b54a350-f59d-11ef-bfb1-4cbb5880a0b8">
|
||||
<clientDependency>
|
||||
<reflist>
|
||||
<ref refid="25a17c58-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</clientDependency>
|
||||
<comment>
|
||||
<reflist>
|
||||
<ref refid="bff31afe-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</comment>
|
||||
<isAbstract>
|
||||
<val>1</val>
|
||||
</isAbstract>
|
||||
<name>
|
||||
<val>AbstractMangaParser</val>
|
||||
</name>
|
||||
<owningPackage>
|
||||
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</owningPackage>
|
||||
<package>
|
||||
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</package>
|
||||
<presentation>
|
||||
<reflist>
|
||||
<ref refid="0b54edc4-f59d-11ef-bfb1-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</presentation>
|
||||
<specialization>
|
||||
<reflist>
|
||||
<ref refid="b969dac6-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="2c236356-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</specialization>
|
||||
</UML:Class>
|
||||
<UML:ClassItem id="0b54edc4-f59d-11ef-bfb1-4cbb5880a0b8">
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 405.16796875, 388.8671875)</val>
|
||||
</matrix>
|
||||
<top-left>
|
||||
<val>(0.0, 0.0)</val>
|
||||
</top-left>
|
||||
<width>
|
||||
<val>158.0</val>
|
||||
</width>
|
||||
<height>
|
||||
<val>60.0</val>
|
||||
</height>
|
||||
<diagram>
|
||||
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</diagram>
|
||||
<show_attributes>
|
||||
<val>0</val>
|
||||
</show_attributes>
|
||||
<show_operations>
|
||||
<val>0</val>
|
||||
</show_operations>
|
||||
<subject>
|
||||
<ref refid="0b54a350-f59d-11ef-bfb1-4cbb5880a0b8"/>
|
||||
</subject>
|
||||
</UML:ClassItem>
|
||||
<UML:Class id="a300f58a-f5bd-11ef-9ec2-4cbb5880a0b8">
|
||||
<comment>
|
||||
<reflist>
|
||||
<ref refid="d5112a70-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</comment>
|
||||
<generalization>
|
||||
<reflist>
|
||||
<ref refid="b969dac6-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</generalization>
|
||||
<isAbstract>
|
||||
<val>1</val>
|
||||
</isAbstract>
|
||||
<name>
|
||||
<val>PagedMangaParser</val>
|
||||
</name>
|
||||
<owningPackage>
|
||||
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</owningPackage>
|
||||
<package>
|
||||
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</package>
|
||||
<presentation>
|
||||
<reflist>
|
||||
<ref refid="a3018fc2-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</presentation>
|
||||
</UML:Class>
|
||||
<UML:ClassItem id="a3018fc2-f5bd-11ef-9ec2-4cbb5880a0b8">
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 476.3368367667698, 525.76953125)</val>
|
||||
</matrix>
|
||||
<top-left>
|
||||
<val>(0.0, 0.0)</val>
|
||||
</top-left>
|
||||
<width>
|
||||
<val>142.0</val>
|
||||
</width>
|
||||
<height>
|
||||
<val>60.0</val>
|
||||
</height>
|
||||
<diagram>
|
||||
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</diagram>
|
||||
<show_attributes>
|
||||
<val>0</val>
|
||||
</show_attributes>
|
||||
<show_operations>
|
||||
<val>0</val>
|
||||
</show_operations>
|
||||
<subject>
|
||||
<ref refid="a300f58a-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</subject>
|
||||
</UML:ClassItem>
|
||||
<UML:Class id="ad4c68d0-f5bd-11ef-9ec2-4cbb5880a0b8">
|
||||
<comment>
|
||||
<reflist>
|
||||
<ref refid="e80418f4-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</comment>
|
||||
<generalization>
|
||||
<reflist>
|
||||
<ref refid="2c236356-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</generalization>
|
||||
<isAbstract>
|
||||
<val>1</val>
|
||||
</isAbstract>
|
||||
<name>
|
||||
<val>SinglePageMangaParser</val>
|
||||
</name>
|
||||
<owningPackage>
|
||||
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</owningPackage>
|
||||
<package>
|
||||
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</package>
|
||||
<presentation>
|
||||
<reflist>
|
||||
<ref refid="ad4d2aae-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</presentation>
|
||||
</UML:Class>
|
||||
<UML:ClassItem id="ad4d2aae-f5bd-11ef-9ec2-4cbb5880a0b8">
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 405.16796875, 627.46875)</val>
|
||||
</matrix>
|
||||
<top-left>
|
||||
<val>(0.0, 0.0)</val>
|
||||
</top-left>
|
||||
<width>
|
||||
<val>175.0</val>
|
||||
</width>
|
||||
<height>
|
||||
<val>60.0</val>
|
||||
</height>
|
||||
<diagram>
|
||||
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</diagram>
|
||||
<show_attributes>
|
||||
<val>0</val>
|
||||
</show_attributes>
|
||||
<show_operations>
|
||||
<val>0</val>
|
||||
</show_operations>
|
||||
<subject>
|
||||
<ref refid="ad4c68d0-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</subject>
|
||||
</UML:ClassItem>
|
||||
<UML:GeneralizationItem id="b6e0240e-f5bd-11ef-9ec2-4cbb5880a0b8">
|
||||
<diagram>
|
||||
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</diagram>
|
||||
<horizontal>
|
||||
<val>False</val>
|
||||
</horizontal>
|
||||
<orthogonal>
|
||||
<val>False</val>
|
||||
</orthogonal>
|
||||
<subject>
|
||||
<ref refid="b969dac6-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</subject>
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 499.2109069824219, 463.45703125)</val>
|
||||
</matrix>
|
||||
<points>
|
||||
<val>[(28.486861756586336, 62.3125), (25.111328125, -14.58984375)]</val>
|
||||
</points>
|
||||
<head-connection>
|
||||
<ref refid="a3018fc2-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</head-connection>
|
||||
<tail-connection>
|
||||
<ref refid="0b54edc4-f59d-11ef-bfb1-4cbb5880a0b8"/>
|
||||
</tail-connection>
|
||||
</UML:GeneralizationItem>
|
||||
<UML:Generalization id="b969dac6-f5bd-11ef-9ec2-4cbb5880a0b8">
|
||||
<general>
|
||||
<ref refid="0b54a350-f59d-11ef-bfb1-4cbb5880a0b8"/>
|
||||
</general>
|
||||
<presentation>
|
||||
<reflist>
|
||||
<ref refid="b6e0240e-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</presentation>
|
||||
<specific>
|
||||
<ref refid="a300f58a-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</specific>
|
||||
</UML:Generalization>
|
||||
<UML:Interface id="198a3108-f5be-11ef-9ec2-4cbb5880a0b8">
|
||||
<name>
|
||||
<val>MangaParser</val>
|
||||
</name>
|
||||
<owningPackage>
|
||||
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</owningPackage>
|
||||
<package>
|
||||
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</package>
|
||||
<presentation>
|
||||
<reflist>
|
||||
<ref refid="198aace6-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</presentation>
|
||||
<supplierDependency>
|
||||
<reflist>
|
||||
<ref refid="25a17c58-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||
<ref refid="41318a02-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</supplierDependency>
|
||||
</UML:Interface>
|
||||
<UML:InterfaceItem id="198aace6-f5be-11ef-9ec2-4cbb5880a0b8">
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 278.00391387939453, 232.92578125)</val>
|
||||
</matrix>
|
||||
<top-left>
|
||||
<val>(0.0, 0.0)</val>
|
||||
</top-left>
|
||||
<width>
|
||||
<val>105.0</val>
|
||||
</width>
|
||||
<height>
|
||||
<val>80.0</val>
|
||||
</height>
|
||||
<diagram>
|
||||
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</diagram>
|
||||
<show_attributes>
|
||||
<val>0</val>
|
||||
</show_attributes>
|
||||
<show_operations>
|
||||
<val>0</val>
|
||||
</show_operations>
|
||||
<subject>
|
||||
<ref refid="198a3108-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</subject>
|
||||
<folded>
|
||||
<val>0</val>
|
||||
</folded>
|
||||
</UML:InterfaceItem>
|
||||
<UML:InterfaceRealizationItem id="254e40f6-f5be-11ef-9ec2-4cbb5880a0b8">
|
||||
<diagram>
|
||||
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</diagram>
|
||||
<horizontal>
|
||||
<val>False</val>
|
||||
</horizontal>
|
||||
<orthogonal>
|
||||
<val>False</val>
|
||||
</orthogonal>
|
||||
<subject>
|
||||
<ref refid="25a17c58-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</subject>
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 306.1445007324219, 270.0625)</val>
|
||||
</matrix>
|
||||
<points>
|
||||
<val>[(55.866059373910275, 42.86328125), (164.5765002560883, 118.8046875)]</val>
|
||||
</points>
|
||||
<head-connection>
|
||||
<ref refid="198aace6-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</head-connection>
|
||||
<tail-connection>
|
||||
<ref refid="0b54edc4-f59d-11ef-bfb1-4cbb5880a0b8"/>
|
||||
</tail-connection>
|
||||
</UML:InterfaceRealizationItem>
|
||||
<UML:InterfaceRealization id="25a17c58-f5be-11ef-9ec2-4cbb5880a0b8">
|
||||
<client>
|
||||
<reflist>
|
||||
<ref refid="0b54a350-f59d-11ef-bfb1-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</client>
|
||||
<owningPackage>
|
||||
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</owningPackage>
|
||||
<presentation>
|
||||
<reflist>
|
||||
<ref refid="254e40f6-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</presentation>
|
||||
<supplier>
|
||||
<reflist>
|
||||
<ref refid="198a3108-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</supplier>
|
||||
</UML:InterfaceRealization>
|
||||
<UML:GeneralizationItem id="2bb6e87a-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||
<diagram>
|
||||
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</diagram>
|
||||
<horizontal>
|
||||
<val>False</val>
|
||||
</horizontal>
|
||||
<orthogonal>
|
||||
<val>False</val>
|
||||
</orthogonal>
|
||||
<subject>
|
||||
<ref refid="2c236356-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</subject>
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 436.2929382324219, 439.1913757324219)</val>
|
||||
</matrix>
|
||||
<points>
|
||||
<val>[(20.37646032737257, 188.27737426757812), (18.488327026367188, 9.675811767578125)]</val>
|
||||
</points>
|
||||
<head-connection>
|
||||
<ref refid="ad4d2aae-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</head-connection>
|
||||
<tail-connection>
|
||||
<ref refid="0b54edc4-f59d-11ef-bfb1-4cbb5880a0b8"/>
|
||||
</tail-connection>
|
||||
</UML:GeneralizationItem>
|
||||
<UML:Generalization id="2c236356-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||
<general>
|
||||
<ref refid="0b54a350-f59d-11ef-bfb1-4cbb5880a0b8"/>
|
||||
</general>
|
||||
<presentation>
|
||||
<reflist>
|
||||
<ref refid="2bb6e87a-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</presentation>
|
||||
<specific>
|
||||
<ref refid="ad4c68d0-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</specific>
|
||||
</UML:Generalization>
|
||||
<UML:Class id="32081654-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||
<clientDependency>
|
||||
<reflist>
|
||||
<ref refid="41318a02-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</clientDependency>
|
||||
<name>
|
||||
<val>MangaParserWrapper</val>
|
||||
</name>
|
||||
<note>
|
||||
<val></val>
|
||||
</note>
|
||||
<owningPackage>
|
||||
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</owningPackage>
|
||||
<package>
|
||||
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</package>
|
||||
<presentation>
|
||||
<reflist>
|
||||
<ref refid="320868c0-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</presentation>
|
||||
</UML:Class>
|
||||
<UML:ClassItem id="320868c0-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 128.5008992667698, 410.48990205860804)</val>
|
||||
</matrix>
|
||||
<top-left>
|
||||
<val>(0.0, 0.0)</val>
|
||||
</top-left>
|
||||
<width>
|
||||
<val>158.0</val>
|
||||
</width>
|
||||
<height>
|
||||
<val>60.0</val>
|
||||
</height>
|
||||
<diagram>
|
||||
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</diagram>
|
||||
<show_attributes>
|
||||
<val>0</val>
|
||||
</show_attributes>
|
||||
<show_operations>
|
||||
<val>0</val>
|
||||
</show_operations>
|
||||
<subject>
|
||||
<ref refid="32081654-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</subject>
|
||||
</UML:ClassItem>
|
||||
<UML:InterfaceRealizationItem id="40e36d2c-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||
<diagram>
|
||||
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</diagram>
|
||||
<horizontal>
|
||||
<val>False</val>
|
||||
</horizontal>
|
||||
<orthogonal>
|
||||
<val>False</val>
|
||||
</orthogonal>
|
||||
<subject>
|
||||
<ref refid="41318a02-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</subject>
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 306.0585632324219, 249.69920349121094)</val>
|
||||
</matrix>
|
||||
<points>
|
||||
<val>[(11.759223915218172, 63.22657775878906), (-98.55766396565207, 160.7906985673971)]</val>
|
||||
</points>
|
||||
<head-connection>
|
||||
<ref refid="198aace6-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</head-connection>
|
||||
<tail-connection>
|
||||
<ref refid="320868c0-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</tail-connection>
|
||||
</UML:InterfaceRealizationItem>
|
||||
<UML:InterfaceRealization id="41318a02-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||
<client>
|
||||
<reflist>
|
||||
<ref refid="32081654-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</client>
|
||||
<owningPackage>
|
||||
<ref refid="58d6c2e8-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</owningPackage>
|
||||
<presentation>
|
||||
<reflist>
|
||||
<ref refid="40e36d2c-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</presentation>
|
||||
<supplier>
|
||||
<reflist>
|
||||
<ref refid="198a3108-f5be-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</supplier>
|
||||
</UML:InterfaceRealization>
|
||||
<UML:Comment id="b760ac44-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||
<body>
|
||||
<val>Used for providing external api. Do not use this class directly</val>
|
||||
</body>
|
||||
<presentation>
|
||||
<reflist>
|
||||
<ref refid="b760bcd4-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</presentation>
|
||||
</UML:Comment>
|
||||
<UML:CommentItem id="b760bcd4-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 108.0561294327963, 550.1347579956054)</val>
|
||||
</matrix>
|
||||
<top-left>
|
||||
<val>(0.0, 0.0)</val>
|
||||
</top-left>
|
||||
<width>
|
||||
<val>183.21868896484375</val>
|
||||
</width>
|
||||
<height>
|
||||
<val>91.23829650878906</val>
|
||||
</height>
|
||||
<diagram>
|
||||
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</diagram>
|
||||
<subject>
|
||||
<ref refid="b760ac44-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</subject>
|
||||
</UML:CommentItem>
|
||||
<UML:CommentLineItem id="bcc07c64-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||
<diagram>
|
||||
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</diagram>
|
||||
<horizontal>
|
||||
<val>False</val>
|
||||
</horizontal>
|
||||
<orthogonal>
|
||||
<val>False</val>
|
||||
</orthogonal>
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 549.205520203852, 278.05499559311954)</val>
|
||||
</matrix>
|
||||
<points>
|
||||
<val>[(-349.5400462886338, 192.4349064654885), (-349.5400462886338, 272.0797624024858)]</val>
|
||||
</points>
|
||||
<tail-connection>
|
||||
<ref refid="b760bcd4-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</tail-connection>
|
||||
</UML:CommentLineItem>
|
||||
<UML:Comment id="bff31afe-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||
<annotatedElement>
|
||||
<reflist>
|
||||
<ref refid="0b54a350-f59d-11ef-bfb1-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</annotatedElement>
|
||||
<body>
|
||||
<val>Extend this class if your manga source provides standard limit-offset based lists (get manga list by offset)</val>
|
||||
</body>
|
||||
<presentation>
|
||||
<reflist>
|
||||
<ref refid="bff32eae-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</presentation>
|
||||
</UML:Comment>
|
||||
<UML:CommentItem id="bff32eae-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 673.0610499890082, 367.0515553989646)</val>
|
||||
</matrix>
|
||||
<top-left>
|
||||
<val>(0.0, 0.0)</val>
|
||||
</top-left>
|
||||
<width>
|
||||
<val>228.8028016098773</val>
|
||||
</width>
|
||||
<height>
|
||||
<val>88.0</val>
|
||||
</height>
|
||||
<diagram>
|
||||
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</diagram>
|
||||
<subject>
|
||||
<ref refid="bff31afe-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</subject>
|
||||
</UML:CommentItem>
|
||||
<UML:Comment id="d5112a70-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||
<annotatedElement>
|
||||
<reflist>
|
||||
<ref refid="a300f58a-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</annotatedElement>
|
||||
<body>
|
||||
<val>Extend this class if your manga source provides paged-based lists (get manga list by page number)</val>
|
||||
</body>
|
||||
<presentation>
|
||||
<reflist>
|
||||
<ref refid="d5113ca4-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</presentation>
|
||||
</UML:Comment>
|
||||
<UML:CommentItem id="d5113ca4-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 666.7924311664914, 507.7539062499999)</val>
|
||||
</matrix>
|
||||
<top-left>
|
||||
<val>(0.0, 0.0)</val>
|
||||
</top-left>
|
||||
<width>
|
||||
<val>214.34368896484375</val>
|
||||
</width>
|
||||
<height>
|
||||
<val>88.0</val>
|
||||
</height>
|
||||
<diagram>
|
||||
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</diagram>
|
||||
<subject>
|
||||
<ref refid="d5112a70-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</subject>
|
||||
</UML:CommentItem>
|
||||
<UML:Comment id="e80418f4-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||
<annotatedElement>
|
||||
<reflist>
|
||||
<ref refid="ad4c68d0-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</annotatedElement>
|
||||
<body>
|
||||
<val>Extend this class if your manga source does not provide pagination (all manga provided in one list)</val>
|
||||
</body>
|
||||
<presentation>
|
||||
<reflist>
|
||||
<ref refid="e8042ad8-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</presentation>
|
||||
</UML:Comment>
|
||||
<UML:CommentItem id="e8042ad8-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 666.7924311664914, 560.9671898788581)</val>
|
||||
</matrix>
|
||||
<top-left>
|
||||
<val>(0.0, 58.00435704705592)</val>
|
||||
</top-left>
|
||||
<width>
|
||||
<val>263.9307954323941</val>
|
||||
</width>
|
||||
<height>
|
||||
<val>78.01706672440287</val>
|
||||
</height>
|
||||
<diagram>
|
||||
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</diagram>
|
||||
<subject>
|
||||
<ref refid="e80418f4-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</subject>
|
||||
</UML:CommentItem>
|
||||
<UML:CommentLineItem id="f6b48e4c-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||
<diagram>
|
||||
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</diagram>
|
||||
<horizontal>
|
||||
<val>False</val>
|
||||
</horizontal>
|
||||
<orthogonal>
|
||||
<val>False</val>
|
||||
</orthogonal>
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 561.8951626340418, 549.6101338901756)</val>
|
||||
</matrix>
|
||||
<points>
|
||||
<val>[(56.44167413272805, 7.038279316310902), (104.89726853244963, 8.304008355003589)]</val>
|
||||
</points>
|
||||
<head-connection>
|
||||
<ref refid="a3018fc2-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</head-connection>
|
||||
<tail-connection>
|
||||
<ref refid="d5113ca4-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</tail-connection>
|
||||
</UML:CommentLineItem>
|
||||
<UML:CommentLineItem id="f89ba010-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||
<diagram>
|
||||
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</diagram>
|
||||
<horizontal>
|
||||
<val>False</val>
|
||||
</horizontal>
|
||||
<orthogonal>
|
||||
<val>False</val>
|
||||
</orthogonal>
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 559.3873501340418, 413.0007588901755)</val>
|
||||
</matrix>
|
||||
<points>
|
||||
<val>[(3.7806186159582467, 0.0), (113.67369985496646, 1.6012844908540842)]</val>
|
||||
</points>
|
||||
<head-connection>
|
||||
<ref refid="0b54edc4-f59d-11ef-bfb1-4cbb5880a0b8"/>
|
||||
</head-connection>
|
||||
<tail-connection>
|
||||
<ref refid="bff32eae-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</tail-connection>
|
||||
</UML:CommentLineItem>
|
||||
<UML:CommentLineItem id="fabe0540-f5bf-11ef-9ec2-4cbb5880a0b8">
|
||||
<diagram>
|
||||
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</diagram>
|
||||
<horizontal>
|
||||
<val>False</val>
|
||||
</horizontal>
|
||||
<orthogonal>
|
||||
<val>False</val>
|
||||
</orthogonal>
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 522.3600063840418, 652.6882588901756)</val>
|
||||
</matrix>
|
||||
<points>
|
||||
<val>[(57.80796236595825, 5.29182139794003), (144.43242478244963, 5.657840086725969)]</val>
|
||||
</points>
|
||||
<head-connection>
|
||||
<ref refid="ad4d2aae-f5bd-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</head-connection>
|
||||
<tail-connection>
|
||||
<ref refid="e8042ad8-f5bf-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</tail-connection>
|
||||
</UML:CommentLineItem>
|
||||
<general:Box id="531831f2-f5c0-11ef-9ec2-4cbb5880a0b8">
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 375.05802564891326, 349.05453145170736)</val>
|
||||
</matrix>
|
||||
<top-left>
|
||||
<val>(0.0, 3.15625)</val>
|
||||
</top-left>
|
||||
<width>
|
||||
<val>590.6594026101285</val>
|
||||
</width>
|
||||
<height>
|
||||
<val>368.44140625</val>
|
||||
</height>
|
||||
<diagram>
|
||||
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</diagram>
|
||||
</general:Box>
|
||||
<UML:Comment id="5ccad4ca-f5c0-11ef-9ec2-4cbb5880a0b8">
|
||||
<body>
|
||||
<val>To create your own parser you have to extends one of these classes</val>
|
||||
</body>
|
||||
<presentation>
|
||||
<reflist>
|
||||
<ref refid="5ccae8d4-f5c0-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</reflist>
|
||||
</presentation>
|
||||
</UML:Comment>
|
||||
<UML:CommentItem id="5ccae8d4-f5c0-11ef-9ec2-4cbb5880a0b8">
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 756.725301794198, 225.57697659840966)</val>
|
||||
</matrix>
|
||||
<top-left>
|
||||
<val>(0.0, 0.0)</val>
|
||||
</top-left>
|
||||
<width>
|
||||
<val>208.99212646484375</val>
|
||||
</width>
|
||||
<height>
|
||||
<val>73.47482464883183</val>
|
||||
</height>
|
||||
<diagram>
|
||||
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</diagram>
|
||||
<subject>
|
||||
<ref refid="5ccad4ca-f5c0-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</subject>
|
||||
</UML:CommentItem>
|
||||
<UML:CommentLineItem id="6f993af6-f5c0-11ef-9ec2-4cbb5880a0b8">
|
||||
<diagram>
|
||||
<ref refid="58d6c536-66f8-11ec-b4c8-0456e5e540ed"/>
|
||||
</diagram>
|
||||
<horizontal>
|
||||
<val>False</val>
|
||||
</horizontal>
|
||||
<orthogonal>
|
||||
<val>False</val>
|
||||
</orthogonal>
|
||||
<matrix>
|
||||
<val>(1.0, 0.0, 0.0, 1.0, 943.6141683885666, 419.2772168262177)</val>
|
||||
</matrix>
|
||||
<points>
|
||||
<val>[(-27.404961772030788, -67.06643537451032), (-27.404961772030788, -120.2254155789762)]</val>
|
||||
</points>
|
||||
<head-connection>
|
||||
<ref refid="531831f2-f5c0-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</head-connection>
|
||||
<tail-connection>
|
||||
<ref refid="5ccae8d4-f5c0-11ef-9ec2-4cbb5880a0b8"/>
|
||||
</tail-connection>
|
||||
</UML:CommentLineItem>
|
||||
</model>
|
||||
</gaphor>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 41 KiB |
@ -1,34 +0,0 @@
|
||||
[versions]
|
||||
kotlin = "2.2.10"
|
||||
ksp = "2.2.10-2.0.2"
|
||||
coroutines = "1.10.2"
|
||||
junit = "5.10.1"
|
||||
okhttp = "5.1.0"
|
||||
okio = "3.16.0"
|
||||
json = "20240303"
|
||||
androidx-collection = "1.5.0"
|
||||
jsoup = "1.21.2"
|
||||
quickjs = "1.1.0"
|
||||
korte = "4.0.10"
|
||||
simplexml = "2.7.1"
|
||||
|
||||
[plugins]
|
||||
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||
|
||||
[libraries]
|
||||
ksp-symbol-processing-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
|
||||
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" }
|
||||
junit-launcher = { group = "org.junit.platform", name = "junit-platform-launcher" }
|
||||
junit-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" }
|
||||
junit-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "junit" }
|
||||
junit-params = { group = "org.junit.jupiter", name = "junit-jupiter-params", version.ref = "junit" }
|
||||
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
|
||||
json = { module = "org.json:json", version.ref = "json" }
|
||||
androidx-collection = { module = "androidx.collection:collection", version.ref = "androidx-collection" }
|
||||
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
||||
quickjs = { module = "io.webfolder:quickjs", version.ref = "quickjs" }
|
||||
korte = { module = "com.soywiz.korlibs.korte:korte-jvm", version.ref = "korte" }
|
||||
simplexml = { module = "org.simpleframework:simple-xml", version.ref = "simplexml" }
|
||||
Binary file not shown.
@ -1,7 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
jdk:
|
||||
- openjdk17
|
||||
@ -0,0 +1,7 @@
|
||||
plugins {
|
||||
id 'org.jetbrains.kotlin.jvm'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.devtools.ksp:symbol-processing-api:1.6.21-1.0.5'
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(8)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.ksp.symbol.processing.api)
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
pluginManagement {
|
||||
plugins {
|
||||
id 'com.google.devtools.ksp' version '1.6.21-1.0.5'
|
||||
id 'org.jetbrains.kotlin.jvm' version '1.6.21'
|
||||
}
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
google()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = 'kotatsu-parsers'
|
||||
include 'kotatsu-parsers-ksp'
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "kotatsu-parsers"
|
||||
include("kotatsu-parsers-ksp")
|
||||
@ -1,14 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers
|
||||
|
||||
/**
|
||||
* Annotate [MangaParser] implementation to mark this parser as broken instead of removing it
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
internal annotation class Broken(
|
||||
|
||||
/**
|
||||
* Reason why this parser is broken
|
||||
*/
|
||||
val message: String = "",
|
||||
)
|
||||
@ -1,18 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers
|
||||
|
||||
public object ErrorMessages {
|
||||
|
||||
public const val FILTER_MULTIPLE_STATES_NOT_SUPPORTED: String = "Multiple states are not supported by this source"
|
||||
public const val FILTER_MULTIPLE_GENRES_NOT_SUPPORTED: String = "Multiple genres are not supported by this source"
|
||||
public const val FILTER_MULTIPLE_CONTENT_RATING_NOT_SUPPORTED: String =
|
||||
"Multiple Content ratings are not supported by this source"
|
||||
public const val FILTER_MULTIPLE_CONTENT_TYPES_NOT_SUPPORTED: String =
|
||||
"Multiple Content types are not supported by this source"
|
||||
public const val FILTER_MULTIPLE_DEMOGRAPHICS_NOT_SUPPORTED: String =
|
||||
"Multiple Demographics are not supported by this source"
|
||||
public const val FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED: String =
|
||||
"Filtering by both genres and locale is not supported by this source"
|
||||
public const val FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED: String =
|
||||
"Filtering by both genres and states is not supported by this source"
|
||||
public const val SEARCH_NOT_SUPPORTED: String = "Search is not supported by this source"
|
||||
}
|
||||
@ -1,78 +1,29 @@
|
||||
package org.koitharu.kotatsu.parsers
|
||||
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import org.koitharu.kotatsu.parsers.bitmap.Bitmap
|
||||
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.LinkResolver
|
||||
import java.util.*
|
||||
|
||||
public abstract class MangaLoaderContext {
|
||||
abstract class MangaLoaderContext {
|
||||
|
||||
public abstract val httpClient: OkHttpClient
|
||||
abstract val httpClient: OkHttpClient
|
||||
|
||||
public abstract val cookieJar: CookieJar
|
||||
abstract val cookieJar: CookieJar
|
||||
|
||||
public fun newParserInstance(source: MangaParserSource): MangaParser = source.newParser(this)
|
||||
open fun encodeBase64(data: ByteArray): String = Base64.getEncoder().encodeToString(data)
|
||||
|
||||
public fun newLinkResolver(link: HttpUrl): LinkResolver = LinkResolver(this, link)
|
||||
open fun decodeBase64(data: String): ByteArray = Base64.getDecoder().decode(data)
|
||||
|
||||
public fun newLinkResolver(link: String): LinkResolver = newLinkResolver(link.toHttpUrl())
|
||||
|
||||
public open fun encodeBase64(data: ByteArray): String = Base64.getEncoder().encodeToString(data)
|
||||
|
||||
public open fun decodeBase64(data: String): ByteArray = Base64.getDecoder().decode(data)
|
||||
|
||||
public open fun getPreferredLocales(): List<Locale> = listOf(Locale.getDefault())
|
||||
|
||||
/**
|
||||
* Execute JavaScript code and return result
|
||||
* @param script JavaScript source code
|
||||
* @return execution result as string, may be null
|
||||
*/
|
||||
@Deprecated("Provide a base url")
|
||||
public abstract suspend fun evaluateJs(script: String): String?
|
||||
open fun getPreferredLocales(): List<Locale> = listOf(Locale.getDefault())
|
||||
|
||||
/**
|
||||
* Execute JavaScript code and return result
|
||||
* @param script JavaScript source code
|
||||
* @param baseUrl url of page script will be executed in context of
|
||||
* @return execution result as string, may be null
|
||||
*/
|
||||
public abstract suspend fun evaluateJs(baseUrl: String, script: String): String?
|
||||
abstract suspend fun evaluateJs(script: String): String?
|
||||
|
||||
/**
|
||||
* Open [url] in browser for some external action (e.g. captcha solving or non cookie-based authorization)
|
||||
*/
|
||||
public open fun requestBrowserAction(parser: MangaParser, url: String): Nothing {
|
||||
throw UnsupportedOperationException("Browser is not available")
|
||||
}
|
||||
|
||||
public abstract fun getConfig(source: MangaSource): MangaSourceConfig
|
||||
|
||||
public abstract fun getDefaultUserAgent(): String
|
||||
|
||||
/**
|
||||
* Helper function to be used in an interceptor
|
||||
* to descramble images
|
||||
* @param response Image response
|
||||
* @param redraw lambda function to implement descrambling logic
|
||||
*/
|
||||
public abstract fun redrawImageResponse(
|
||||
response: Response,
|
||||
redraw: (image: Bitmap) -> Bitmap,
|
||||
): Response
|
||||
|
||||
/**
|
||||
* create a new empty Bitmap with given dimensions
|
||||
*/
|
||||
public abstract fun createBitmap(
|
||||
width: Int,
|
||||
height: Int,
|
||||
): Bitmap
|
||||
abstract fun getConfig(source: MangaSource): MangaSourceConfig
|
||||
}
|
||||
|
||||
@ -1,88 +1,126 @@
|
||||
package org.koitharu.kotatsu.parsers
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery
|
||||
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQueryCapabilities
|
||||
import org.koitharu.kotatsu.parsers.util.LinkResolver
|
||||
import org.koitharu.kotatsu.parsers.util.convertToMangaSearchQuery
|
||||
import org.koitharu.kotatsu.parsers.util.toMangaListFilterCapabilities
|
||||
import org.koitharu.kotatsu.parsers.network.OkHttpWebClient
|
||||
import org.koitharu.kotatsu.parsers.network.WebClient
|
||||
import org.koitharu.kotatsu.parsers.util.FaviconParser
|
||||
import org.koitharu.kotatsu.parsers.util.domain
|
||||
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
|
||||
import java.util.*
|
||||
|
||||
public interface MangaParser : Interceptor {
|
||||
|
||||
public val source: MangaParserSource
|
||||
abstract class MangaParser @InternalParsersApi constructor(
|
||||
@property:InternalParsersApi val context: MangaLoaderContext,
|
||||
val source: MangaSource,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Supported [SortOrder] variants. Must not be empty.
|
||||
*
|
||||
* For better performance use [EnumSet] for more than one item.
|
||||
*/
|
||||
public val availableSortOrders: Set<SortOrder>
|
||||
|
||||
@Deprecated("Too complex. Use filterCapabilities instead")
|
||||
public val searchQueryCapabilities: MangaSearchQueryCapabilities
|
||||
abstract val sortOrders: Set<SortOrder>
|
||||
|
||||
public val filterCapabilities: MangaListFilterCapabilities
|
||||
val config by lazy { context.getConfig(source) }
|
||||
|
||||
public val config: MangaSourceConfig
|
||||
|
||||
public val authorizationProvider: MangaParserAuthProvider?
|
||||
get() = this as? MangaParserAuthProvider
|
||||
open val sourceLocale: Locale
|
||||
get() = source.locale?.let { Locale(it) } ?: Locale.ROOT
|
||||
|
||||
/**
|
||||
* Provide default domain and available alternatives, if any.
|
||||
*
|
||||
* Never hardcode domain in requests, use [domain] instead.
|
||||
* Never hardcode domain in requests, use [getDomain] instead.
|
||||
*/
|
||||
public val configKeyDomain: ConfigKey.Domain
|
||||
@InternalParsersApi
|
||||
abstract val configKeyDomain: ConfigKey.Domain
|
||||
|
||||
public val domain: String
|
||||
open val headers: Headers? = null
|
||||
|
||||
/**
|
||||
* Used as fallback if value of `sortOrder` passed to [getList] is null
|
||||
*/
|
||||
protected open val defaultSortOrder: SortOrder
|
||||
get() {
|
||||
val supported = sortOrders
|
||||
return SortOrder.values().first { it in supported }
|
||||
}
|
||||
|
||||
protected val webClient: WebClient = OkHttpWebClient(context.httpClient, source)
|
||||
|
||||
/**
|
||||
* Parse list of manga by specified criteria
|
||||
*
|
||||
* @param offset starting from 0 and used for pagination.
|
||||
* Note than passed value may not be divisible by internal page size, so you should adjust it manually.
|
||||
* @param query search query, may be null or empty if no search needed
|
||||
* @param tags genres for filtering, values from [getTags] and [Manga.tags]. May be null or empty
|
||||
* @param sortOrder one of [sortOrders] or null for default value
|
||||
*/
|
||||
@JvmSynthetic
|
||||
@InternalParsersApi
|
||||
abstract suspend fun getList(
|
||||
offset: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga>
|
||||
|
||||
@Deprecated("Too complex. Use getList with filter instead")
|
||||
public suspend fun getList(query: MangaSearchQuery): List<Manga>
|
||||
/**
|
||||
* Parse list of manga with search by text query
|
||||
*
|
||||
* @param offset starting from 0 and used for pagination.
|
||||
* @param query search query
|
||||
*/
|
||||
open suspend fun getList(offset: Int, query: String): List<Manga> {
|
||||
return getList(offset, query, null, defaultSortOrder)
|
||||
}
|
||||
|
||||
public suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga>
|
||||
/**
|
||||
* Parse list of manga by specified criteria
|
||||
*
|
||||
* @param offset starting from 0 and used for pagination.
|
||||
* Note than passed value may not be divisible by internal page size, so you should adjust it manually.
|
||||
* @param tags genres for filtering, values from [getTags] and [Manga.tags]. May be null or empty
|
||||
* @param sortOrder one of [sortOrders] or null for default value
|
||||
*/
|
||||
open suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): List<Manga> {
|
||||
return getList(offset, null, tags, sortOrder ?: defaultSortOrder)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse details for [Manga]: chapters list, description, large cover, etc.
|
||||
* Must return the same manga, may change any fields excepts id, url and source
|
||||
* @see Manga.copy
|
||||
*/
|
||||
public suspend fun getDetails(manga: Manga): Manga
|
||||
abstract suspend fun getDetails(manga: Manga): Manga
|
||||
|
||||
/**
|
||||
* Parse pages list for specified chapter.
|
||||
* @see MangaPage for details
|
||||
*/
|
||||
public suspend fun getPages(chapter: MangaChapter): List<MangaPage>
|
||||
abstract suspend fun getPages(chapter: MangaChapter): List<MangaPage>
|
||||
|
||||
/**
|
||||
* Fetch direct link to the page image.
|
||||
*/
|
||||
public suspend fun getPageUrl(page: MangaPage): String
|
||||
|
||||
public suspend fun getFilterOptions(): MangaListFilterOptions
|
||||
open suspend fun getPageUrl(page: MangaPage): String = page.url.toAbsoluteUrl(domain)
|
||||
|
||||
/**
|
||||
* Parse favicons from the main page of the source`s website
|
||||
* Fetch available tags (genres) for source
|
||||
*/
|
||||
public suspend fun getFavicons(): Favicons
|
||||
|
||||
public fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>)
|
||||
|
||||
public suspend fun getRelatedManga(seed: Manga): List<Manga>
|
||||
|
||||
public fun getRequestHeaders(): Headers
|
||||
abstract suspend fun getTags(): Set<MangaTag>
|
||||
|
||||
/**
|
||||
* Return [Manga] object by web link to it
|
||||
* @see [Manga.publicUrl]
|
||||
* Parse favicons from the main page of the source`s website
|
||||
*/
|
||||
@InternalParsersApi
|
||||
public suspend fun resolveLink(resolver: LinkResolver, link: HttpUrl): Manga?
|
||||
open suspend fun getFavicons(): Favicons {
|
||||
return FaviconParser(webClient, domain).parseFavicons()
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
open fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
|
||||
keys.add(configKeyDomain)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,28 +1,20 @@
|
||||
package org.koitharu.kotatsu.parsers
|
||||
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
|
||||
/**
|
||||
* Annotate each [MangaParser] implementation with this annotation, used by codegen
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
internal annotation class MangaSourceParser(
|
||||
annotation class MangaSourceParser(
|
||||
/**
|
||||
* Name of manga source. Used as an Enum value, must be UPPER_CASE and unique.
|
||||
*/
|
||||
val name: String,
|
||||
/**
|
||||
* User-friendly title of manga source. In most case equals the website name.
|
||||
* Avoid extra whitespaces between the words if it is not required.
|
||||
*/
|
||||
val title: String,
|
||||
/**
|
||||
* Language code (for example "en" or "ru") or blank if parser provide manga on different languages.
|
||||
*/
|
||||
val locale: String = "",
|
||||
/**
|
||||
* Type of content provided by parser. See [ContentType] for more info
|
||||
*/
|
||||
val type: ContentType = ContentType.MANGA,
|
||||
)
|
||||
@ -0,0 +1,51 @@
|
||||
package org.koitharu.kotatsu.parsers
|
||||
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.util.Paginator
|
||||
|
||||
@InternalParsersApi
|
||||
abstract class PagedMangaParser(
|
||||
context: MangaLoaderContext,
|
||||
source: MangaSource,
|
||||
pageSize: Int,
|
||||
searchPageSize: Int = pageSize,
|
||||
) : MangaParser(context, source) {
|
||||
|
||||
protected val paginator = Paginator(pageSize)
|
||||
protected val searchPaginator = Paginator(searchPageSize)
|
||||
|
||||
override suspend fun getList(offset: Int, query: String): List<Manga> {
|
||||
return getList(searchPaginator, offset, query, null, defaultSortOrder)
|
||||
}
|
||||
|
||||
override suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): List<Manga> {
|
||||
return getList(paginator, offset, null, tags, sortOrder ?: defaultSortOrder)
|
||||
}
|
||||
|
||||
@InternalParsersApi
|
||||
@Deprecated("You should use getListPage for PagedMangaParser", level = DeprecationLevel.HIDDEN)
|
||||
final override suspend fun getList(
|
||||
offset: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> = throw UnsupportedOperationException("You should use getListPage for PagedMangaParser")
|
||||
|
||||
abstract suspend fun getListPage(page: Int, query: String?, tags: Set<MangaTag>?, sortOrder: SortOrder): List<Manga>
|
||||
|
||||
private suspend fun getList(
|
||||
paginator: Paginator,
|
||||
offset: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val page = paginator.getPage(offset)
|
||||
val list = getListPage(page, query, tags, sortOrder)
|
||||
paginator.onListReceived(offset, page, list.size)
|
||||
return list
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.bitmap
|
||||
|
||||
public interface Bitmap {
|
||||
|
||||
public val width: Int
|
||||
public val height: Int
|
||||
|
||||
public fun drawBitmap(sourceBitmap: Bitmap, src: Rect, dst: Rect)
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.bitmap
|
||||
|
||||
public data class Rect(
|
||||
val left: Int = 0,
|
||||
val top: Int = 0,
|
||||
val right: Int = 0,
|
||||
val bottom: Int = 0,
|
||||
) {
|
||||
|
||||
val width: Int
|
||||
get() = right - left
|
||||
|
||||
val height: Int
|
||||
get() = bottom - top
|
||||
}
|
||||
@ -1,37 +1,21 @@
|
||||
package org.koitharu.kotatsu.parsers.config
|
||||
|
||||
public sealed class ConfigKey<T>(
|
||||
@JvmField public val key: String,
|
||||
sealed class ConfigKey<T>(
|
||||
@JvmField val key: String,
|
||||
) {
|
||||
|
||||
public abstract val defaultValue: T
|
||||
abstract val defaultValue: T
|
||||
|
||||
public class Domain(
|
||||
@JvmField @JvmSuppressWildcards public vararg val presetValues: String,
|
||||
) : ConfigKey<String>("domain") {
|
||||
|
||||
init {
|
||||
require(presetValues.isNotEmpty()) { "You must provide at least one domain" }
|
||||
}
|
||||
|
||||
override val defaultValue: String
|
||||
get() = presetValues.first()
|
||||
}
|
||||
class Domain(
|
||||
override val defaultValue: String,
|
||||
@JvmField val presetValues: Array<String>?,
|
||||
) : ConfigKey<String>("domain")
|
||||
|
||||
public class ShowSuspiciousContent(
|
||||
class ShowSuspiciousContent(
|
||||
override val defaultValue: Boolean,
|
||||
) : ConfigKey<Boolean>("show_suspicious")
|
||||
|
||||
public class UserAgent(
|
||||
class UserAgent(
|
||||
override val defaultValue: String,
|
||||
) : ConfigKey<String>("user_agent")
|
||||
|
||||
public class SplitByTranslations(
|
||||
override val defaultValue: Boolean,
|
||||
) : ConfigKey<Boolean>("split_translations")
|
||||
|
||||
public class PreferredImageServer(
|
||||
public val presetValues: Map<String?, String?>,
|
||||
override val defaultValue: String?,
|
||||
) : ConfigKey<String?>("img_server")
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package org.koitharu.kotatsu.parsers.config
|
||||
|
||||
public interface MangaSourceConfig {
|
||||
interface MangaSourceConfig {
|
||||
|
||||
public operator fun <T> get(key: ConfigKey<T>): T
|
||||
operator fun <T> get(key: ConfigKey<T>): T
|
||||
}
|
||||
@ -1,105 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.core
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import org.koitharu.kotatsu.parsers.InternalParsersApi
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaParser
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery
|
||||
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQueryCapabilities
|
||||
import org.koitharu.kotatsu.parsers.network.OkHttpWebClient
|
||||
import org.koitharu.kotatsu.parsers.network.WebClient
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.util.*
|
||||
|
||||
@Suppress("OVERRIDE_DEPRECATION")
|
||||
@InternalParsersApi
|
||||
public abstract class AbstractMangaParser @InternalParsersApi constructor(
|
||||
@property:InternalParsersApi public val context: MangaLoaderContext,
|
||||
public final override val source: MangaParserSource,
|
||||
) : MangaParser {
|
||||
|
||||
public final override val searchQueryCapabilities: MangaSearchQueryCapabilities
|
||||
get() = filterCapabilities.toMangaSearchQueryCapabilities()
|
||||
|
||||
public override val config: MangaSourceConfig by lazy { context.getConfig(source) }
|
||||
|
||||
public open val sourceLocale: Locale
|
||||
get() = if (source.locale.isEmpty()) Locale.ROOT else Locale(source.locale)
|
||||
|
||||
protected val sourceContentRating: ContentRating?
|
||||
get() = if (source.contentType == ContentType.HENTAI) {
|
||||
ContentRating.ADULT
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
protected val isNsfwSource: Boolean = source.contentType == ContentType.HENTAI
|
||||
|
||||
protected open val userAgentKey: ConfigKey.UserAgent = ConfigKey.UserAgent(context.getDefaultUserAgent())
|
||||
|
||||
override fun getRequestHeaders(): Headers = Headers.Builder()
|
||||
.add("User-Agent", config[userAgentKey])
|
||||
.build()
|
||||
|
||||
/**
|
||||
* Used as fallback if value of `order` passed to [getList] is null
|
||||
*/
|
||||
public open val defaultSortOrder: SortOrder
|
||||
get() {
|
||||
val supported = availableSortOrders
|
||||
return SortOrder.entries.first { it in supported }
|
||||
}
|
||||
|
||||
final override val domain: String
|
||||
get() = config[configKeyDomain]
|
||||
|
||||
@JvmField
|
||||
protected val webClient: WebClient = OkHttpWebClient(context.httpClient, source)
|
||||
|
||||
/**
|
||||
* Search list of manga by specified searchQuery
|
||||
*
|
||||
* @param query searchQuery
|
||||
*/
|
||||
public final override suspend fun getList(query: MangaSearchQuery): List<Manga> = getList(
|
||||
offset = query.offset,
|
||||
order = query.order ?: defaultSortOrder,
|
||||
filter = convertToMangaListFilter(query),
|
||||
)
|
||||
|
||||
/**
|
||||
* Fetch direct link to the page image.
|
||||
*/
|
||||
public override suspend fun getPageUrl(page: MangaPage): String = page.url.toAbsoluteUrl(domain)
|
||||
|
||||
/**
|
||||
* Parse favicons from the main page of the source`s website
|
||||
*/
|
||||
public override suspend fun getFavicons(): Favicons {
|
||||
return FaviconParser(webClient, domain).parseFavicons()
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
public override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
|
||||
keys.add(configKeyDomain)
|
||||
}
|
||||
|
||||
public override suspend fun getRelatedManga(seed: Manga): List<Manga> {
|
||||
return RelatedMangaFinder(listOf(this)).invoke(seed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return [Manga] object by web link to it
|
||||
* @see [Manga.publicUrl]
|
||||
*/
|
||||
override suspend fun resolveLink(resolver: LinkResolver, link: HttpUrl): Manga? = null
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response = chain.proceed(chain.request())
|
||||
}
|
||||
@ -1,94 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.core
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import org.koitharu.kotatsu.parsers.InternalParsersApi
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaParser
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.network.OkHttpWebClient
|
||||
import org.koitharu.kotatsu.parsers.network.WebClient
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.util.*
|
||||
|
||||
@Deprecated("Too complex. Use AbstractMangaParser instead")
|
||||
internal abstract class FlexibleMangaParser @InternalParsersApi constructor(
|
||||
@property:InternalParsersApi val context: MangaLoaderContext,
|
||||
final override val source: MangaParserSource,
|
||||
) : MangaParser {
|
||||
|
||||
override val config: MangaSourceConfig by lazy { context.getConfig(source) }
|
||||
|
||||
open val sourceLocale: Locale
|
||||
get() = if (source.locale.isEmpty()) Locale.ROOT else Locale(source.locale)
|
||||
|
||||
protected open val userAgentKey: ConfigKey.UserAgent = ConfigKey.UserAgent(context.getDefaultUserAgent())
|
||||
|
||||
final override val filterCapabilities: MangaListFilterCapabilities
|
||||
get() = searchQueryCapabilities.toMangaListFilterCapabilities()
|
||||
|
||||
protected val sourceContentRating: ContentRating?
|
||||
get() = if (source.contentType == ContentType.HENTAI) {
|
||||
ContentRating.ADULT
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
final override val domain: String
|
||||
get() = config[configKeyDomain]
|
||||
|
||||
@Deprecated("Override intercept() instead")
|
||||
override fun getRequestHeaders(): Headers = Headers.Builder()
|
||||
.add("User-Agent", config[userAgentKey])
|
||||
.build()
|
||||
|
||||
/**
|
||||
* Used as fallback if value of `order` passed to [getList] is null
|
||||
*/
|
||||
open val defaultSortOrder: SortOrder
|
||||
get() {
|
||||
val supported = availableSortOrders
|
||||
return SortOrder.entries.first { it in supported }
|
||||
}
|
||||
|
||||
@JvmField
|
||||
protected val webClient: WebClient = OkHttpWebClient(context.httpClient, source)
|
||||
|
||||
/**
|
||||
* Fetch direct link to the page image.
|
||||
*/
|
||||
override suspend fun getPageUrl(page: MangaPage): String = page.url.toAbsoluteUrl(domain)
|
||||
|
||||
final override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
|
||||
return getList(convertToMangaSearchQuery(offset, order, filter))
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse favicons from the main page of the source`s website
|
||||
*/
|
||||
override suspend fun getFavicons(): Favicons {
|
||||
return FaviconParser(webClient, domain).parseFavicons()
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
|
||||
keys.add(configKeyDomain)
|
||||
}
|
||||
|
||||
override suspend fun getRelatedManga(seed: Manga): List<Manga> {
|
||||
return RelatedMangaFinder(listOf(this)).invoke(seed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return [Manga] object by web link to it
|
||||
* @see [Manga.publicUrl]
|
||||
*/
|
||||
override suspend fun resolveLink(resolver: LinkResolver, link: HttpUrl): Manga? = null
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response = chain.proceed(chain.request())
|
||||
}
|
||||
@ -1,60 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.core
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery
|
||||
import org.koitharu.kotatsu.parsers.model.search.SearchableField
|
||||
import org.koitharu.kotatsu.parsers.util.Paginator
|
||||
|
||||
@Deprecated("Too complex. Use PagedMangaParser instead")
|
||||
internal abstract class FlexiblePagedMangaParser(
|
||||
context: MangaLoaderContext,
|
||||
source: MangaParserSource,
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) @JvmField public val pageSize: Int,
|
||||
searchPageSize: Int = pageSize,
|
||||
) : FlexibleMangaParser(context, source) {
|
||||
|
||||
@JvmField
|
||||
protected val paginator: Paginator = Paginator(pageSize)
|
||||
|
||||
@JvmField
|
||||
protected val searchPaginator: Paginator = Paginator(searchPageSize)
|
||||
|
||||
final override suspend fun getList(query: MangaSearchQuery): List<Manga> {
|
||||
var containTitleNameCriteria = false
|
||||
query.criteria.forEach {
|
||||
if (it.field == SearchableField.TITLE_NAME) {
|
||||
containTitleNameCriteria = true
|
||||
}
|
||||
}
|
||||
|
||||
return searchManga(
|
||||
paginator = if (containTitleNameCriteria) {
|
||||
paginator
|
||||
} else {
|
||||
searchPaginator
|
||||
},
|
||||
query = query,
|
||||
)
|
||||
}
|
||||
|
||||
public abstract suspend fun getListPage(query: MangaSearchQuery, page: Int): List<Manga>
|
||||
|
||||
protected fun setFirstPage(firstPage: Int, firstPageForSearch: Int = firstPage) {
|
||||
paginator.firstPage = firstPage
|
||||
searchPaginator.firstPage = firstPageForSearch
|
||||
}
|
||||
|
||||
private suspend fun searchManga(
|
||||
paginator: Paginator,
|
||||
query: MangaSearchQuery,
|
||||
): List<Manga> {
|
||||
val offset: Int = query.offset
|
||||
val page = paginator.getPage(offset)
|
||||
val list = getListPage(query, page)
|
||||
paginator.onListReceived(offset, page, list.size)
|
||||
return list
|
||||
}
|
||||
}
|
||||
@ -1,77 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.core
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.koitharu.kotatsu.parsers.MangaParser
|
||||
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery
|
||||
import org.koitharu.kotatsu.parsers.util.mergeWith
|
||||
|
||||
internal class MangaParserWrapper(
|
||||
private val delegate: MangaParser,
|
||||
) : MangaParser by delegate {
|
||||
|
||||
override val authorizationProvider: MangaParserAuthProvider?
|
||||
get() = delegate as? MangaParserAuthProvider
|
||||
|
||||
@Deprecated("Too complex. Use getList with filter instead")
|
||||
override suspend fun getList(query: MangaSearchQuery): List<Manga> = withContext(Dispatchers.Default) {
|
||||
if (!query.skipValidation) {
|
||||
searchQueryCapabilities.validate(query)
|
||||
}
|
||||
delegate.getList(query)
|
||||
}
|
||||
|
||||
override suspend fun getList(
|
||||
offset: Int,
|
||||
order: SortOrder,
|
||||
filter: MangaListFilter,
|
||||
): List<Manga> = withContext(Dispatchers.Default) {
|
||||
delegate.getList(offset, order, filter)
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga = withContext(Dispatchers.Default) {
|
||||
delegate.getDetails(manga)
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = withContext(Dispatchers.Default) {
|
||||
delegate.getPages(chapter)
|
||||
}
|
||||
|
||||
override suspend fun getPageUrl(page: MangaPage): String = withContext(Dispatchers.Default) {
|
||||
delegate.getPageUrl(page)
|
||||
}
|
||||
|
||||
override suspend fun getFilterOptions(): MangaListFilterOptions = withContext(Dispatchers.Default) {
|
||||
delegate.getFilterOptions()
|
||||
}
|
||||
|
||||
override suspend fun getFavicons(): Favicons = withContext(Dispatchers.Default) {
|
||||
delegate.getFavicons()
|
||||
}
|
||||
|
||||
override suspend fun getRelatedManga(seed: Manga): List<Manga> = withContext(Dispatchers.Default) {
|
||||
delegate.getRelatedManga(seed)
|
||||
}
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val headers = request.headers.newBuilder()
|
||||
.mergeWith(delegate.getRequestHeaders(), replaceExisting = false)
|
||||
.build()
|
||||
val newRequest = request.newBuilder().headers(headers).build()
|
||||
return delegate.intercept(ProxyChain(chain, newRequest))
|
||||
}
|
||||
|
||||
private class ProxyChain(
|
||||
private val delegate: Interceptor.Chain,
|
||||
private val request: Request,
|
||||
) : Interceptor.Chain by delegate {
|
||||
|
||||
override fun request(): Request = request
|
||||
}
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.core
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import org.koitharu.kotatsu.parsers.InternalParsersApi
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.util.Paginator
|
||||
|
||||
@InternalParsersApi
|
||||
public abstract class PagedMangaParser(
|
||||
context: MangaLoaderContext,
|
||||
source: MangaParserSource,
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) @JvmField public val pageSize: Int,
|
||||
searchPageSize: Int = pageSize,
|
||||
) : AbstractMangaParser(context, source) {
|
||||
|
||||
@JvmField
|
||||
protected val paginator: Paginator = Paginator(pageSize)
|
||||
|
||||
@JvmField
|
||||
protected val searchPaginator: Paginator = Paginator(searchPageSize)
|
||||
|
||||
final override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
|
||||
return getList(
|
||||
paginator = if (filter.query.isNullOrEmpty()) {
|
||||
paginator
|
||||
} else {
|
||||
searchPaginator
|
||||
},
|
||||
offset = offset,
|
||||
order = order,
|
||||
filter = filter,
|
||||
)
|
||||
}
|
||||
|
||||
public abstract suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga>
|
||||
|
||||
protected fun setFirstPage(firstPage: Int, firstPageForSearch: Int = firstPage) {
|
||||
paginator.firstPage = firstPage
|
||||
searchPaginator.firstPage = firstPageForSearch
|
||||
}
|
||||
|
||||
private suspend fun getList(
|
||||
paginator: Paginator,
|
||||
offset: Int,
|
||||
order: SortOrder,
|
||||
filter: MangaListFilter,
|
||||
): List<Manga> {
|
||||
val page = paginator.getPage(offset)
|
||||
val list = getListPage(page, order, filter)
|
||||
paginator.onListReceived(offset, page, list.size)
|
||||
return list
|
||||
}
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.core
|
||||
|
||||
import org.koitharu.kotatsu.parsers.InternalParsersApi
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
|
||||
@InternalParsersApi
|
||||
public abstract class SinglePageMangaParser(
|
||||
context: MangaLoaderContext,
|
||||
source: MangaParserSource,
|
||||
) : AbstractMangaParser(context, source) {
|
||||
|
||||
final override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
|
||||
if (offset > 0) {
|
||||
return emptyList()
|
||||
}
|
||||
return getList(order, filter)
|
||||
}
|
||||
|
||||
public abstract suspend fun getList(order: SortOrder, filter: MangaListFilter): List<Manga>
|
||||
}
|
||||
@ -1,13 +1,11 @@
|
||||
package org.koitharu.kotatsu.parsers.exception
|
||||
|
||||
import okio.IOException
|
||||
import org.koitharu.kotatsu.parsers.InternalParsersApi
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
/**
|
||||
* Authorization is required for access to the requested content
|
||||
*/
|
||||
public class AuthRequiredException @InternalParsersApi @JvmOverloads constructor(
|
||||
public val source: MangaSource,
|
||||
cause: Throwable? = null,
|
||||
) : IOException("Authorization required", cause)
|
||||
class AuthRequiredException @InternalParsersApi constructor(
|
||||
val source: MangaSource,
|
||||
) : RuntimeException("Authorization required")
|
||||
@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.parsers
|
||||
package org.koitharu.kotatsu.parsers.exception
|
||||
|
||||
import okhttp3.Headers
|
||||
import okio.IOException
|
||||
@ -1,3 +1,3 @@
|
||||
package org.koitharu.kotatsu.parsers.exception
|
||||
|
||||
public class ContentUnavailableException(message: String) : RuntimeException(message)
|
||||
class ContentUnavailableException(message: String) : RuntimeException(message)
|
||||
@ -1,31 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.exception
|
||||
|
||||
import okio.IOException
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
public class TooManyRequestExceptions(
|
||||
public val url: String,
|
||||
retryAfter: Long,
|
||||
) : IOException("Too man requests") {
|
||||
|
||||
public val retryAt: Instant? = if (retryAfter > 0 && retryAfter < Long.MAX_VALUE) {
|
||||
Instant.now().plusMillis(retryAfter)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
public fun getRetryDelay(): Long {
|
||||
if (retryAt == null) {
|
||||
return -1L
|
||||
}
|
||||
return Instant.now().until(retryAt, ChronoUnit.MILLIS).coerceAtLeast(0L)
|
||||
}
|
||||
|
||||
override val message: String?
|
||||
get() = if (retryAt != null) {
|
||||
"${super.message}, retry at $retryAt"
|
||||
} else {
|
||||
super.message
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.model
|
||||
|
||||
public enum class ContentRating {
|
||||
SAFE,
|
||||
SUGGESTIVE,
|
||||
ADULT
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.model
|
||||
|
||||
public enum class ContentType {
|
||||
|
||||
/**
|
||||
* Standard manga, manhua, webtoons, etc
|
||||
*/
|
||||
MANGA,
|
||||
|
||||
MANHWA,
|
||||
|
||||
MANHUA,
|
||||
|
||||
/**
|
||||
* Use this if the source provides mostly nsfw content.
|
||||
*/
|
||||
HENTAI,
|
||||
|
||||
/**
|
||||
* Western comics
|
||||
*/
|
||||
COMICS,
|
||||
|
||||
NOVEL,
|
||||
|
||||
/**
|
||||
* Use this type if no other suits your needs. For example, for an indie manga
|
||||
*/
|
||||
|
||||
ONE_SHOT,
|
||||
DOUJINSHI,
|
||||
IMAGE_SET,
|
||||
ARTIST_CG,
|
||||
GAME_CG,
|
||||
OTHER,
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.model
|
||||
|
||||
public enum class Demographic {
|
||||
SHOUNEN,
|
||||
SHOUJO,
|
||||
SEINEN,
|
||||
JOSEI,
|
||||
KODOMO,
|
||||
NONE,
|
||||
}
|
||||
@ -1,203 +1,162 @@
|
||||
package org.koitharu.kotatsu.parsers.model
|
||||
|
||||
import androidx.collection.ArrayMap
|
||||
import org.koitharu.kotatsu.parsers.util.findById
|
||||
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
|
||||
import org.koitharu.kotatsu.parsers.InternalParsersApi
|
||||
|
||||
public data class Manga(
|
||||
class Manga(
|
||||
/**
|
||||
* Unique identifier for manga
|
||||
*/
|
||||
@JvmField public val id: Long,
|
||||
@JvmField val id: Long,
|
||||
/**
|
||||
* Manga title, human-readable
|
||||
*/
|
||||
@JvmField public val title: String,
|
||||
@JvmField val title: String,
|
||||
/**
|
||||
* Alternative titles (for example on other language), may be empty
|
||||
* Alternative title (for example on other language), may be null
|
||||
*/
|
||||
@JvmField public val altTitles: Set<String>,
|
||||
@JvmField val altTitle: String?,
|
||||
/**
|
||||
* Relative url to manga (**without** a domain) or any other uri.
|
||||
* Used principally in parsers
|
||||
*/
|
||||
@JvmField public val url: String,
|
||||
@JvmField val url: String,
|
||||
/**
|
||||
* Absolute url to manga, must be ready to open in browser
|
||||
*/
|
||||
@JvmField public val publicUrl: String,
|
||||
@JvmField val publicUrl: String,
|
||||
/**
|
||||
* Normalized manga rating, must be in range of 0..1 or [RATING_UNKNOWN] if rating s unknown
|
||||
* @see hasRating
|
||||
*/
|
||||
@JvmField public val rating: Float,
|
||||
@JvmField val rating: Float,
|
||||
/**
|
||||
* Indicates that manga may contain sensitive information (18+, NSFW)
|
||||
*/
|
||||
@JvmField public val contentRating: ContentRating?,
|
||||
@JvmField val isNsfw: Boolean,
|
||||
/**
|
||||
* Absolute link to the cover
|
||||
* @see largeCoverUrl
|
||||
*/
|
||||
@JvmField public val coverUrl: String?,
|
||||
@JvmField val coverUrl: String,
|
||||
/**
|
||||
* Tags (genres) of the manga
|
||||
*/
|
||||
@JvmField public val tags: Set<MangaTag>,
|
||||
@JvmField val tags: Set<MangaTag>,
|
||||
/**
|
||||
* Manga status (ongoing, finished) or null if unknown
|
||||
*/
|
||||
@JvmField public val state: MangaState?,
|
||||
@JvmField val state: MangaState?,
|
||||
/**
|
||||
* Authors of the manga
|
||||
* Author of the manga, may be null
|
||||
*/
|
||||
@JvmField public val authors: Set<String>,
|
||||
@JvmField val author: String?,
|
||||
/**
|
||||
* Large cover url (absolute), null if is no large cover
|
||||
* @see coverUrl
|
||||
*/
|
||||
@JvmField public val largeCoverUrl: String? = null,
|
||||
@JvmField val largeCoverUrl: String? = null,
|
||||
/**
|
||||
* Manga description, may be html or null
|
||||
*/
|
||||
@JvmField public val description: String? = null,
|
||||
@JvmField val description: String? = null,
|
||||
/**
|
||||
* List of chapters
|
||||
*/
|
||||
@JvmField public val chapters: List<MangaChapter>? = null,
|
||||
@JvmField val chapters: List<MangaChapter>? = null,
|
||||
/**
|
||||
* Manga source
|
||||
*/
|
||||
@JvmField public val source: MangaSource,
|
||||
@JvmField val source: MangaSource,
|
||||
) {
|
||||
|
||||
@Deprecated("Use other constructor")
|
||||
public constructor(
|
||||
/**
|
||||
* Unique identifier for manga
|
||||
*/
|
||||
id: Long,
|
||||
/**
|
||||
* Manga title, human-readable
|
||||
*/
|
||||
title: String,
|
||||
/**
|
||||
* Alternative title (for example on other language), may be null
|
||||
*/
|
||||
altTitle: String?,
|
||||
/**
|
||||
* Relative url to manga (**without** a domain) or any other uri.
|
||||
* Used principally in parsers
|
||||
*/
|
||||
url: String,
|
||||
/**
|
||||
* Absolute url to manga, must be ready to open in browser
|
||||
*/
|
||||
publicUrl: String,
|
||||
/**
|
||||
* Normalized manga rating, must be in range of 0..1 or [RATING_UNKNOWN] if rating s unknown
|
||||
* @see hasRating
|
||||
*/
|
||||
rating: Float,
|
||||
/**
|
||||
* Indicates that manga may contain sensitive information (18+, NSFW)
|
||||
*/
|
||||
isNsfw: Boolean,
|
||||
/**
|
||||
* Absolute link to the cover
|
||||
* @see largeCoverUrl
|
||||
*/
|
||||
coverUrl: String?,
|
||||
/**
|
||||
* Tags (genres) of the manga
|
||||
*/
|
||||
tags: Set<MangaTag>,
|
||||
/**
|
||||
* Manga status (ongoing, finished) or null if unknown
|
||||
*/
|
||||
state: MangaState?,
|
||||
/**
|
||||
* Authors of the manga
|
||||
*/
|
||||
author: String?,
|
||||
/**
|
||||
* Large cover url (absolute), null if is no large cover
|
||||
* @see coverUrl
|
||||
*/
|
||||
largeCoverUrl: String? = null,
|
||||
/**
|
||||
* Manga description, may be html or null
|
||||
*/
|
||||
description: String? = null,
|
||||
/**
|
||||
* List of chapters
|
||||
*/
|
||||
chapters: List<MangaChapter>? = null,
|
||||
/**
|
||||
* Manga source
|
||||
* Return if manga has a specified rating
|
||||
* @see rating
|
||||
*/
|
||||
source: MangaSource,
|
||||
) : this(
|
||||
val hasRating: Boolean
|
||||
get() = rating > 0f && rating <= 1f
|
||||
|
||||
fun getChapters(branch: String?): List<MangaChapter>? {
|
||||
return chapters?.filter { x -> x.branch == branch }
|
||||
}
|
||||
|
||||
@InternalParsersApi
|
||||
fun copy(
|
||||
title: String = this.title,
|
||||
altTitle: String? = this.altTitle,
|
||||
publicUrl: String = this.publicUrl,
|
||||
rating: Float = this.rating,
|
||||
isNsfw: Boolean = this.isNsfw,
|
||||
coverUrl: String = this.coverUrl,
|
||||
tags: Set<MangaTag> = this.tags,
|
||||
state: MangaState? = this.state,
|
||||
author: String? = this.author,
|
||||
largeCoverUrl: String? = this.largeCoverUrl,
|
||||
description: String? = this.description,
|
||||
chapters: List<MangaChapter>? = this.chapters,
|
||||
) = Manga(
|
||||
id = id,
|
||||
title = title,
|
||||
altTitles = setOfNotNull(altTitle?.nullIfEmpty()),
|
||||
altTitle = altTitle,
|
||||
url = url,
|
||||
publicUrl = publicUrl,
|
||||
rating = rating,
|
||||
contentRating = if (isNsfw) ContentRating.ADULT else null,
|
||||
coverUrl = coverUrl?.nullIfEmpty(),
|
||||
isNsfw = isNsfw,
|
||||
coverUrl = coverUrl,
|
||||
tags = tags,
|
||||
state = state,
|
||||
authors = setOfNotNull(author),
|
||||
largeCoverUrl = largeCoverUrl?.nullIfEmpty(),
|
||||
description = description?.nullIfEmpty(),
|
||||
author = author,
|
||||
largeCoverUrl = largeCoverUrl,
|
||||
description = description,
|
||||
chapters = chapters,
|
||||
source = source,
|
||||
)
|
||||
|
||||
/**
|
||||
* Author of the manga, may be null
|
||||
*/
|
||||
@Deprecated("Please use authors")
|
||||
public val author: String?
|
||||
get() = authors.firstOrNull()
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
/**
|
||||
* Alternative title (for example on other language), may be null
|
||||
*/
|
||||
@Deprecated("Please use altTitles")
|
||||
public val altTitle: String?
|
||||
get() = altTitles.firstOrNull()
|
||||
|
||||
/**
|
||||
* Return if manga has a specified rating
|
||||
* @see rating
|
||||
*/
|
||||
public val hasRating: Boolean
|
||||
get() = rating > 0f && rating <= 1f
|
||||
other as Manga
|
||||
|
||||
@Deprecated("Use contentRating instead", ReplaceWith("contentRating == ContentRating.ADULT"))
|
||||
public val isNsfw: Boolean
|
||||
get() = contentRating == ContentRating.ADULT
|
||||
if (id != other.id) return false
|
||||
if (title != other.title) return false
|
||||
if (altTitle != other.altTitle) return false
|
||||
if (url != other.url) return false
|
||||
if (publicUrl != other.publicUrl) return false
|
||||
if (rating != other.rating) return false
|
||||
if (isNsfw != other.isNsfw) return false
|
||||
if (coverUrl != other.coverUrl) return false
|
||||
if (tags != other.tags) return false
|
||||
if (state != other.state) return false
|
||||
if (author != other.author) return false
|
||||
if (largeCoverUrl != other.largeCoverUrl) return false
|
||||
if (description != other.description) return false
|
||||
if (chapters != other.chapters) return false
|
||||
if (source != other.source) return false
|
||||
|
||||
public fun getChapters(branch: String?): List<MangaChapter> {
|
||||
return chapters?.filter { x -> x.branch == branch }.orEmpty()
|
||||
return true
|
||||
}
|
||||
|
||||
public fun findChapterById(id: Long): MangaChapter? = chapters?.findById(id)
|
||||
|
||||
public fun requireChapterById(id: Long): MangaChapter = findChapterById(id)
|
||||
?: throw NoSuchElementException("Chapter with id $id not found")
|
||||
|
||||
public fun getBranches(): Map<String?, Int> {
|
||||
if (chapters.isNullOrEmpty()) {
|
||||
return emptyMap()
|
||||
}
|
||||
val result = ArrayMap<String?, Int>()
|
||||
chapters.forEach {
|
||||
val key = it.branch
|
||||
result[key] = result.getOrDefault(key, 0) + 1
|
||||
}
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + title.hashCode()
|
||||
result = 31 * result + (altTitle?.hashCode() ?: 0)
|
||||
result = 31 * result + url.hashCode()
|
||||
result = 31 * result + publicUrl.hashCode()
|
||||
result = 31 * result + rating.hashCode()
|
||||
result = 31 * result + isNsfw.hashCode()
|
||||
result = 31 * result + coverUrl.hashCode()
|
||||
result = 31 * result + tags.hashCode()
|
||||
result = 31 * result + (state?.hashCode() ?: 0)
|
||||
result = 31 * result + (author?.hashCode() ?: 0)
|
||||
result = 31 * result + (largeCoverUrl?.hashCode() ?: 0)
|
||||
result = 31 * result + (description?.hashCode() ?: 0)
|
||||
result = 31 * result + (chapters?.hashCode() ?: 0)
|
||||
result = 31 * result + source.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "Manga($id - \"$title\" [$url] - $source)"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,65 +1,74 @@
|
||||
package org.koitharu.kotatsu.parsers.model
|
||||
|
||||
import org.koitharu.kotatsu.parsers.util.formatSimple
|
||||
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
|
||||
|
||||
public data class MangaChapter(
|
||||
class MangaChapter(
|
||||
/**
|
||||
* An unique id of chapter
|
||||
*/
|
||||
@JvmField public val id: Long,
|
||||
/**
|
||||
* User-readable name of chapter if provided by parser or null instead
|
||||
* Do not pass manga title or chapter number here
|
||||
*/
|
||||
@JvmField public val title: String?,
|
||||
@JvmField val id: Long,
|
||||
/**
|
||||
* Chapter number starting from 1, 0 if unknown
|
||||
* User-readable name of chapter
|
||||
*/
|
||||
@JvmField public val number: Float,
|
||||
@JvmField val name: String,
|
||||
/**
|
||||
* Volume number starting from 1, 0 if unknown
|
||||
* Chapter number starting from 1
|
||||
*/
|
||||
@JvmField public val volume: Int,
|
||||
@JvmField val number: Int,
|
||||
/**
|
||||
* Relative url to chapter (**without** a domain) or any other uri.
|
||||
* Used principally in parsers
|
||||
*/
|
||||
@JvmField public val url: String,
|
||||
@JvmField val url: String,
|
||||
/**
|
||||
* User-readable name of scanlator (releaser) or null if unknown
|
||||
*/
|
||||
@JvmField public val scanlator: String?,
|
||||
@JvmField val scanlator: String?,
|
||||
/**
|
||||
* Chapter upload date in milliseconds
|
||||
*/
|
||||
@JvmField public val uploadDate: Long,
|
||||
@JvmField val uploadDate: Long,
|
||||
/**
|
||||
* User-readable name of branch.
|
||||
* A branch is a group of chapters that overlap (e.g. different languages)
|
||||
*/
|
||||
@JvmField public val branch: String?,
|
||||
@JvmField public val source: MangaSource,
|
||||
) {
|
||||
@JvmField val branch: String?,
|
||||
@JvmField val source: MangaSource,
|
||||
) : Comparable<MangaChapter> {
|
||||
|
||||
@Deprecated("Use title instead", ReplaceWith("title"))
|
||||
val name: String
|
||||
get() = title.ifNullOrEmpty {
|
||||
buildString {
|
||||
if (volume > 0) append("Vol ").append(volume).append(' ')
|
||||
if (number > 0) append("Chapter ").append(number.formatSimple()) else append("Unnamed")
|
||||
override fun compareTo(other: MangaChapter): Int {
|
||||
return number.compareTo(other.number)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as MangaChapter
|
||||
|
||||
if (id != other.id) return false
|
||||
if (name != other.name) return false
|
||||
if (number != other.number) return false
|
||||
if (url != other.url) return false
|
||||
if (scanlator != other.scanlator) return false
|
||||
if (uploadDate != other.uploadDate) return false
|
||||
if (branch != other.branch) return false
|
||||
if (source != other.source) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
public fun numberString(): String? = if (number > 0f) {
|
||||
number.formatSimple()
|
||||
} else {
|
||||
null
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + name.hashCode()
|
||||
result = 31 * result + number
|
||||
result = 31 * result + url.hashCode()
|
||||
result = 31 * result + (scanlator?.hashCode() ?: 0)
|
||||
result = 31 * result + uploadDate.hashCode()
|
||||
result = 31 * result + (branch?.hashCode() ?: 0)
|
||||
result = 31 * result + source.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
public fun volumeString(): String? = if (volume > 0) {
|
||||
volume.toString()
|
||||
} else {
|
||||
null
|
||||
override fun toString(): String {
|
||||
return "MangaChapter($id - #$number [$url] - $source)"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,88 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.model
|
||||
|
||||
import java.util.*
|
||||
|
||||
public data class MangaListFilter(
|
||||
@JvmField val query: String? = null,
|
||||
@JvmField val tags: Set<MangaTag> = emptySet(),
|
||||
@JvmField val tagsExclude: Set<MangaTag> = emptySet(),
|
||||
@JvmField val locale: Locale? = null,
|
||||
@JvmField val originalLocale: Locale? = null,
|
||||
@JvmField val states: Set<MangaState> = emptySet(),
|
||||
@JvmField val contentRating: Set<ContentRating> = emptySet(),
|
||||
@JvmField val types: Set<ContentType> = emptySet(),
|
||||
@JvmField val demographics: Set<Demographic> = emptySet(),
|
||||
@JvmField val year: Int = YEAR_UNKNOWN,
|
||||
@JvmField val yearFrom: Int = YEAR_UNKNOWN,
|
||||
@JvmField val yearTo: Int = YEAR_UNKNOWN,
|
||||
@JvmField val author: String? = null,
|
||||
) {
|
||||
|
||||
private fun isNonSearchOptionsEmpty(): Boolean = tags.isEmpty() &&
|
||||
tagsExclude.isEmpty() &&
|
||||
locale == null &&
|
||||
originalLocale == null &&
|
||||
states.isEmpty() &&
|
||||
contentRating.isEmpty() &&
|
||||
year == YEAR_UNKNOWN &&
|
||||
yearFrom == YEAR_UNKNOWN &&
|
||||
yearTo == YEAR_UNKNOWN &&
|
||||
types.isEmpty() &&
|
||||
demographics.isEmpty() &&
|
||||
author.isNullOrEmpty()
|
||||
|
||||
public fun isEmpty(): Boolean = isNonSearchOptionsEmpty() && query.isNullOrEmpty()
|
||||
|
||||
public fun isNotEmpty(): Boolean = !isEmpty()
|
||||
|
||||
public fun hasNonSearchOptions(): Boolean = !isNonSearchOptionsEmpty()
|
||||
|
||||
public companion object {
|
||||
|
||||
@JvmStatic
|
||||
public val EMPTY: MangaListFilter = MangaListFilter()
|
||||
}
|
||||
|
||||
internal class Builder {
|
||||
private var query: String? = null
|
||||
private val tags: MutableSet<MangaTag> = mutableSetOf()
|
||||
private val tagsExclude: MutableSet<MangaTag> = mutableSetOf()
|
||||
private var locale: Locale? = null
|
||||
private var originalLocale: Locale? = null
|
||||
private val states: MutableSet<MangaState> = mutableSetOf()
|
||||
private val contentRating: MutableSet<ContentRating> = mutableSetOf()
|
||||
private val types: MutableSet<ContentType> = mutableSetOf()
|
||||
private val demographics: MutableSet<Demographic> = mutableSetOf()
|
||||
private var year: Int = YEAR_UNKNOWN
|
||||
private var yearFrom: Int = YEAR_UNKNOWN
|
||||
private var yearTo: Int = YEAR_UNKNOWN
|
||||
|
||||
fun query(query: String?): Builder = apply { this.query = query }
|
||||
fun addTag(tag: MangaTag): Builder = apply { tags.add(tag) }
|
||||
fun addTags(tags: Collection<MangaTag>): Builder = apply { this.tags.addAll(tags) }
|
||||
fun excludeTag(tag: MangaTag): Builder = apply { tagsExclude.add(tag) }
|
||||
fun excludeTags(tags: Collection<MangaTag>): Builder = apply { this.tagsExclude.addAll(tags) }
|
||||
fun locale(locale: Locale?): Builder = apply { this.locale = locale }
|
||||
fun originalLocale(locale: Locale?): Builder = apply { this.originalLocale = locale }
|
||||
fun addState(state: MangaState): Builder = apply { states.add(state) }
|
||||
fun addStates(states: Collection<MangaState>): Builder = apply { this.states.addAll(states) }
|
||||
fun addContentRating(rating: ContentRating): Builder = apply { contentRating.add(rating) }
|
||||
fun addContentRatings(ratings: Collection<ContentRating>): Builder =
|
||||
apply { this.contentRating.addAll(ratings) }
|
||||
|
||||
fun addType(type: ContentType): Builder = apply { types.add(type) }
|
||||
fun addTypes(types: Collection<ContentType>): Builder = apply { this.types.addAll(types) }
|
||||
fun addDemographic(demographic: Demographic): Builder = apply { demographics.add(demographic) }
|
||||
fun addDemographics(demographics: Collection<Demographic>): Builder =
|
||||
apply { this.demographics.addAll(demographics) }
|
||||
|
||||
fun year(year: Int): Builder = apply { this.year = year }
|
||||
fun yearFrom(year: Int): Builder = apply { this.yearFrom = year }
|
||||
fun yearTo(year: Int): Builder = apply { this.yearTo = year }
|
||||
|
||||
fun build(): MangaListFilter = MangaListFilter(
|
||||
query, tags, tagsExclude, locale, originalLocale, states,
|
||||
contentRating, types, demographics, year, yearFrom, yearTo,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.model
|
||||
|
||||
import org.koitharu.kotatsu.parsers.InternalParsersApi
|
||||
|
||||
public data class MangaListFilterCapabilities @InternalParsersApi constructor(
|
||||
|
||||
/**
|
||||
* Whether parser supports filtering by more than one tag
|
||||
* @see [MangaListFilter.tags]
|
||||
* @see [MangaListFilterOptions.availableTags]
|
||||
*/
|
||||
val isMultipleTagsSupported: Boolean = false,
|
||||
|
||||
/**
|
||||
* Whether parser supports tagsExclude field in filter
|
||||
* @see [MangaListFilter.tagsExclude]
|
||||
* @see [MangaListFilterOptions.availableTags]
|
||||
*/
|
||||
val isTagsExclusionSupported: Boolean = false,
|
||||
|
||||
/**
|
||||
* Whether parser supports searching by string query
|
||||
* @see [MangaListFilter.query]
|
||||
*/
|
||||
val isSearchSupported: Boolean = false,
|
||||
|
||||
/**
|
||||
* Whether parser supports searching by string query combined within other filters
|
||||
*/
|
||||
val isSearchWithFiltersSupported: Boolean = false,
|
||||
|
||||
/**
|
||||
* Whether parser supports searching/filtering by year
|
||||
* @see [MangaListFilter.year]
|
||||
*/
|
||||
val isYearSupported: Boolean = false,
|
||||
|
||||
/**
|
||||
* Whether parser supports searching by year range
|
||||
* @see [MangaListFilter.yearFrom] and [MangaListFilter.yearTo]
|
||||
*/
|
||||
val isYearRangeSupported: Boolean = false,
|
||||
|
||||
/**
|
||||
* Whether parser supports searching Original Languages
|
||||
* @see [MangaListFilter.originalLocale]
|
||||
* @see [MangaListFilterOptions.availableLocales]
|
||||
*/
|
||||
val isOriginalLocaleSupported: Boolean = false,
|
||||
|
||||
/**
|
||||
* Whether parser supports searching by author name
|
||||
* @see [MangaListFilter.author]
|
||||
*/
|
||||
val isAuthorSearchSupported: Boolean = false,
|
||||
)
|
||||
@ -1,45 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.model
|
||||
|
||||
import org.koitharu.kotatsu.parsers.InternalParsersApi
|
||||
import java.util.*
|
||||
|
||||
public data class MangaListFilterOptions @InternalParsersApi constructor(
|
||||
|
||||
/**
|
||||
* Available tags (genres)
|
||||
*/
|
||||
public val availableTags: Set<MangaTag> = emptySet(),
|
||||
|
||||
/**
|
||||
* Supported [MangaState] variants for filtering. May be empty.
|
||||
*
|
||||
* For better performance use [EnumSet] for more than one item.
|
||||
*/
|
||||
public val availableStates: Set<MangaState> = emptySet(),
|
||||
|
||||
/**
|
||||
* Supported [ContentRating] variants for filtering. May be empty.
|
||||
*
|
||||
* For better performance use [EnumSet] for more than one item.
|
||||
*/
|
||||
public val availableContentRating: Set<ContentRating> = emptySet(),
|
||||
|
||||
/**
|
||||
* Supported [ContentType] variants for filtering. May be empty.
|
||||
*
|
||||
* For better performance use [EnumSet] for more than one item.
|
||||
*/
|
||||
public val availableContentTypes: Set<ContentType> = emptySet(),
|
||||
|
||||
/**
|
||||
* Supported [Demographic] variants for filtering. May be empty.
|
||||
*
|
||||
* For better performance use [EnumSet] for more than one item.
|
||||
*/
|
||||
public val availableDemographics: Set<Demographic> = emptySet(),
|
||||
|
||||
/**
|
||||
* Supported content locales for multilingual sources
|
||||
*/
|
||||
public val availableLocales: Set<Locale> = emptySet(),
|
||||
)
|
||||
@ -1,6 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.model
|
||||
|
||||
public interface MangaSource {
|
||||
|
||||
public val name: String
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
package org.koitharu.kotatsu.parsers.model
|
||||
|
||||
public enum class MangaState {
|
||||
ONGOING, FINISHED, ABANDONED, PAUSED, UPCOMING, RESTRICTED
|
||||
enum class MangaState {
|
||||
ONGOING, FINISHED
|
||||
}
|
||||
@ -1,22 +1,9 @@
|
||||
package org.koitharu.kotatsu.parsers.model
|
||||
|
||||
public enum class SortOrder {
|
||||
enum class SortOrder {
|
||||
UPDATED,
|
||||
UPDATED_ASC,
|
||||
POPULARITY,
|
||||
POPULARITY_ASC,
|
||||
RATING,
|
||||
RATING_ASC,
|
||||
NEWEST,
|
||||
NEWEST_ASC,
|
||||
ALPHABETICAL,
|
||||
ALPHABETICAL_DESC,
|
||||
ADDED,
|
||||
ADDED_ASC,
|
||||
RELEVANCE,
|
||||
POPULARITY_HOUR,
|
||||
POPULARITY_TODAY,
|
||||
POPULARITY_WEEK,
|
||||
POPULARITY_MONTH,
|
||||
POPULARITY_YEAR,
|
||||
ALPHABETICAL
|
||||
}
|
||||
@ -1,90 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.model.search
|
||||
|
||||
import androidx.collection.ArrayMap
|
||||
import androidx.collection.ArraySet
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
|
||||
/**
|
||||
* Represents a search query for filtering and sorting manga search results.
|
||||
* This class is immutable and must be constructed using the [Builder].
|
||||
*
|
||||
* @property criteria The set of search criteria applied to the query.
|
||||
* @property order The sorting order for the results (optional).
|
||||
* @property offset The offset number for paginated search results (optional).
|
||||
*/
|
||||
|
||||
@Deprecated("Too complex. Use MangaListFilter instead")
|
||||
@ConsistentCopyVisibility
|
||||
public data class MangaSearchQuery private constructor(
|
||||
@JvmField public val criteria: Set<QueryCriteria<*>>,
|
||||
@JvmField public val order: SortOrder?,
|
||||
@JvmField public val offset: Int,
|
||||
@JvmField public val skipValidation: Boolean,
|
||||
) {
|
||||
|
||||
public fun newBuilder(): Builder = Builder(this)
|
||||
|
||||
public class Builder {
|
||||
|
||||
private val criteria = ArraySet<QueryCriteria<*>>()
|
||||
private var order: SortOrder? = null
|
||||
private var offset: Int = 0
|
||||
private var skipValidation: Boolean = false
|
||||
|
||||
public constructor()
|
||||
|
||||
public constructor(query: MangaSearchQuery) : this() {
|
||||
criteria.addAll(query.criteria)
|
||||
order = query.order
|
||||
offset = query.offset
|
||||
}
|
||||
|
||||
public fun criterion(criterion: QueryCriteria<*>): Builder = apply { criteria.add(criterion) }
|
||||
|
||||
public fun order(order: SortOrder?): Builder = apply { this.order = order }
|
||||
|
||||
public fun offset(offset: Int): Builder = apply { this.offset = offset }
|
||||
|
||||
public fun skipValidation(skip: Boolean): Builder = apply { this.skipValidation = skip }
|
||||
|
||||
@Throws(IllegalArgumentException::class)
|
||||
public fun build(): MangaSearchQuery {
|
||||
return MangaSearchQuery(deduplicateCriteria(criteria), order, offset, skipValidation)
|
||||
}
|
||||
|
||||
private fun deduplicateCriteria(criteria: Set<QueryCriteria<*>>): Set<QueryCriteria<*>> {
|
||||
val uniqueCriteria =
|
||||
ArrayMap<Pair<SearchableField, Class<out QueryCriteria<*>>>, QueryCriteria<*>>(criteria.size)
|
||||
|
||||
for (criterion in criteria) {
|
||||
val key = criterion.field to criterion::class.java
|
||||
val existing = uniqueCriteria[key]
|
||||
|
||||
when {
|
||||
existing == null -> uniqueCriteria[key] = criterion
|
||||
|
||||
existing is QueryCriteria.Include<*> && criterion is QueryCriteria.Include<*> -> {
|
||||
uniqueCriteria[key] =
|
||||
QueryCriteria.Include(criterion.field, existing.values union criterion.values)
|
||||
}
|
||||
|
||||
existing is QueryCriteria.Exclude<*> && criterion is QueryCriteria.Exclude<*> -> {
|
||||
uniqueCriteria[key] =
|
||||
QueryCriteria.Exclude(criterion.field, existing.values union criterion.values)
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException(
|
||||
"Match and Range have only one criterion per type, but found duplicates for: ${criterion.field} in ${criterion::class.simpleName}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueCriteria.values.toSet()
|
||||
}
|
||||
}
|
||||
|
||||
public companion object {
|
||||
|
||||
public val EMPTY: MangaSearchQuery = MangaSearchQuery(emptySet(), null, 0, false)
|
||||
}
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.model.search
|
||||
|
||||
import androidx.collection.ArraySet
|
||||
import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.*
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
|
||||
@Deprecated("Too complex. Use MangaListFilterCapabilities instead")
|
||||
@ExposedCopyVisibility
|
||||
public data class MangaSearchQueryCapabilities internal constructor(
|
||||
public val capabilities: Set<SearchCapability>,
|
||||
) {
|
||||
|
||||
public constructor(vararg capabilities: SearchCapability) : this(ArraySet(capabilities))
|
||||
|
||||
internal fun validate(query: MangaSearchQuery) {
|
||||
val strictFields = capabilities.filter { it.isExclusive }.mapToSet { it.field }
|
||||
val usedStrictFields = query.criteria.mapToSet { it.field }.intersect(strictFields)
|
||||
|
||||
require(usedStrictFields.isEmpty() || query.criteria.size <= 1) {
|
||||
"Query contains multiple criteria, but at least one field (${usedStrictFields.joinToString()}) does not support multiple criteria."
|
||||
}
|
||||
for (criterion in query.criteria) {
|
||||
val capability = requireNotNull(capabilities.find { it.field == criterion.field }) {
|
||||
"Unsupported search field: ${criterion.field}"
|
||||
}
|
||||
|
||||
require(criterion::class in capability.criteriaTypes) {
|
||||
"Unsupported search criterion: ${criterion::class.simpleName} for field ${criterion.field}"
|
||||
}
|
||||
|
||||
// Ensure single value per criterion if supportMultiValue is false
|
||||
if (!capability.isMultiple) {
|
||||
when (criterion) {
|
||||
is Include<*> -> require(criterion.values.size <= 1) {
|
||||
"Multiple values are not allowed for field ${criterion.field}"
|
||||
}
|
||||
|
||||
is Exclude<*> -> require(criterion.values.size <= 1) {
|
||||
"Multiple values are not allowed for field ${criterion.field}"
|
||||
}
|
||||
|
||||
is Range<*> -> Unit // Range is always valid (from, to)
|
||||
is Match<*> -> Unit // Match always has a single value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,106 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.model.search
|
||||
|
||||
/**
|
||||
* Represents a generic search criterion used for filtering manga search results.
|
||||
* Each criterion applies a specific condition to a [SearchableField] and operates on values of type [T].
|
||||
*
|
||||
* @param T The type of value associated with the search criterion.
|
||||
* @property field The field to which this search criterion applies.
|
||||
*/
|
||||
@Deprecated("Too complex")
|
||||
public sealed interface QueryCriteria<T> {
|
||||
|
||||
public val field: SearchableField
|
||||
|
||||
override fun equals(other: Any?): Boolean
|
||||
|
||||
override fun hashCode(): Int
|
||||
|
||||
/**
|
||||
* Represents an inclusion criterion that allows search results based on a set of allowed values.
|
||||
*
|
||||
* @param T The type of value being included in the search.
|
||||
* @property values The set of values that should be included in the search results.
|
||||
*
|
||||
* ### Example Usage:
|
||||
* ```kotlin
|
||||
* val genreFilter = QueryCriteria.Include(SearchableField.STATE, setOf(MangaState.ONGOING, MangaState.FINISHED))
|
||||
* ```
|
||||
*/
|
||||
public data class Include<T : Any>(
|
||||
public override val field: SearchableField,
|
||||
@JvmField public val values: Set<T>,
|
||||
) : QueryCriteria<T> {
|
||||
|
||||
init {
|
||||
check(values.all { x -> field.type.isInstance(x) })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an exclusion criterion that exclude results containing certain values.
|
||||
*
|
||||
* @param T The type of value being excluded from the search.
|
||||
* @property values The set of values that should be excluded from the search results.
|
||||
*
|
||||
* ### Example Usage:
|
||||
* ```kotlin
|
||||
* val excludeTag = QueryCriteria.Exclude(SearchableField.TAG, setOf(MangaTag(key, title, source)))
|
||||
* ```
|
||||
*/
|
||||
public data class Exclude<T : Any>(
|
||||
public override val field: SearchableField,
|
||||
@JvmField public val values: Set<T>,
|
||||
) : QueryCriteria<T> {
|
||||
|
||||
init {
|
||||
check(values.all { x -> field.type.isInstance(x) })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a range criterion that allows search based on a range of values.
|
||||
*
|
||||
* @param T The type of value used in the range (must be comparable).
|
||||
* @property from The starting value of the range (inclusive).
|
||||
* @property to The ending value of the range (inclusive).
|
||||
*
|
||||
* ### Example Usage:
|
||||
* ```kotlin
|
||||
* val yearRange = QueryCriteria.Range(SearchableField.PUBLICATION_YEAR, 2000, 2020)
|
||||
* ```
|
||||
*/
|
||||
public data class Range<T : Comparable<T>>(
|
||||
public override val field: SearchableField,
|
||||
@JvmField public val from: T,
|
||||
@JvmField public val to: T,
|
||||
) : QueryCriteria<T> {
|
||||
|
||||
init {
|
||||
check(field.type.isInstance(from))
|
||||
check(field.type.isInstance(to))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Represents a match criterion that search results based on an exact match of a value.
|
||||
*
|
||||
* @param T The type of value being matched.
|
||||
* @property value The exact value that must be matched.
|
||||
*
|
||||
* ### Example Usage:
|
||||
* ```kotlin
|
||||
* val titleMatch = QueryCriteria.Match(SearchableField.TITLE, "manga title")
|
||||
* ```
|
||||
*/
|
||||
public data class Match<T : Any>(
|
||||
public override val field: SearchableField,
|
||||
@JvmField public val value: T,
|
||||
) : QueryCriteria<T> {
|
||||
|
||||
init {
|
||||
check(field.type.isInstance(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.model.search
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* Defines the search capabilities of a given field in the manga search query.
|
||||
*
|
||||
* @property field The searchable field that this capability applies to.
|
||||
* Example values:
|
||||
* - `SearchableField.TITLE_NAME` for searching by title.
|
||||
* - `SearchableField.AUTHOR` for searching by author names.
|
||||
* - `SearchableField.TAG` for filtering by tags.
|
||||
* @property criteriaTypes The set of supported criteria types for the field.
|
||||
* Example values:
|
||||
* - `setOf(Include::class, Exclude::class)` selected field supports inclusion/exclusion criteria.
|
||||
* - `setOf(Range::class)` selected field support numerical range criteria.
|
||||
* @property isMultiValue Indicates whether the field supports multiple values.
|
||||
* - `true` if multiple values can be provided (e.g., multiple tags or authors).
|
||||
* - `false` if only a single value is allowed (e.g., only one tag or author).
|
||||
* @property isExclusive Specifies whether the field can be used alongside other criteria.
|
||||
* - `true` if this field can be used with other search criteria.
|
||||
* - `false` if using this field requires it to be the only criterion in query.
|
||||
*/
|
||||
@Deprecated("Too complex")
|
||||
public data class SearchCapability(
|
||||
/** The searchable field that this capability applies to. */
|
||||
@JvmField public val field: SearchableField,
|
||||
/** The set of supported criteria types for this field. */
|
||||
@JvmField public val criteriaTypes: Set<KClass<out QueryCriteria<*>>>,
|
||||
/** Indicates whether the field supports multiple values. */
|
||||
@JvmField public val isMultiple: Boolean,
|
||||
/** Specifies whether the field can be used alongside other criteria. */
|
||||
@JvmField public val isExclusive: Boolean = false,
|
||||
)
|
||||
@ -1,24 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.model.search
|
||||
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Represents the various fields that can be used for searching manga.
|
||||
* Each field is associated with a specific data type that defines its expected values.
|
||||
*
|
||||
* @property type The Java class representing the expected type of values for this field.
|
||||
*/
|
||||
@Deprecated("Too complex")
|
||||
public enum class SearchableField(public val type: Class<*>) {
|
||||
TITLE_NAME(String::class.java),
|
||||
TAG(MangaTag::class.java),
|
||||
AUTHOR(MangaTag::class.java),
|
||||
LANGUAGE(Locale::class.java),
|
||||
ORIGINAL_LANGUAGE(Locale::class.java),
|
||||
STATE(MangaState::class.java),
|
||||
CONTENT_TYPE(ContentType::class.java),
|
||||
CONTENT_RATING(ContentRating::class.java),
|
||||
DEMOGRAPHIC(Demographic::class.java),
|
||||
PUBLICATION_YEAR(Int::class.javaObjectType);
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.network
|
||||
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import java.net.HttpURLConnection.HTTP_FORBIDDEN
|
||||
import java.net.HttpURLConnection.HTTP_UNAVAILABLE
|
||||
|
||||
public object CloudFlareHelper {
|
||||
|
||||
public const val PROTECTION_NOT_DETECTED: Int = 0
|
||||
public const val PROTECTION_CAPTCHA: Int = 1
|
||||
public const val PROTECTION_BLOCKED: Int = 2
|
||||
|
||||
private const val CF_CLEARANCE = "cf_clearance"
|
||||
|
||||
public fun checkResponseForProtection(response: Response): Int {
|
||||
if (response.code != HTTP_FORBIDDEN && response.code != HTTP_UNAVAILABLE) {
|
||||
return PROTECTION_NOT_DETECTED
|
||||
}
|
||||
val content = try {
|
||||
response.peekBody(Long.MAX_VALUE).use {
|
||||
Jsoup.parse(it.byteStream(), Charsets.UTF_8.name(), response.request.url.toString())
|
||||
}
|
||||
} catch (_: IllegalStateException) {
|
||||
return PROTECTION_NOT_DETECTED
|
||||
}
|
||||
return when {
|
||||
content.selectFirst("h2[data-translate=\"blocked_why_headline\"]") != null -> PROTECTION_BLOCKED
|
||||
content.getElementById("challenge-error-title") != null || content.getElementById("challenge-error-text") != null -> PROTECTION_CAPTCHA
|
||||
|
||||
else -> PROTECTION_NOT_DETECTED
|
||||
}
|
||||
}
|
||||
|
||||
public fun getClearanceCookie(cookieJar: CookieJar, url: String): String? {
|
||||
return cookieJar.loadForRequest(url.toHttpUrl()).find { it.name == CF_CLEARANCE }?.value
|
||||
}
|
||||
|
||||
public fun isCloudFlareCookie(name: String): Boolean {
|
||||
return name.startsWith("cf_")
|
||||
|| name.startsWith("_cf")
|
||||
|| name.startsWith("__cf")
|
||||
|| name == "csrftoken"
|
||||
}
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
package org.koitharu.kotatsu.parsers.network
|
||||
|
||||
public object UserAgents {
|
||||
|
||||
public const val CHROME_MOBILE: String =
|
||||
"Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.196 Mobile Safari/537.36"
|
||||
|
||||
public const val FIREFOX_MOBILE: String =
|
||||
"Mozilla/5.0 (Android 14; Mobile; LG-M255; rv:123.0) Gecko/123.0 Firefox/123.0"
|
||||
|
||||
public const val CHROME_DESKTOP: String =
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
|
||||
|
||||
public const val FIREFOX_DESKTOP: String = "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/116.0"
|
||||
|
||||
public const val KOTATSU: String = "Kotatsu/6.8 (Android 13;;; en)"
|
||||
}
|
||||
@ -0,0 +1,215 @@
|
||||
package org.koitharu.kotatsu.parsers.site
|
||||
|
||||
import androidx.collection.ArraySet
|
||||
import androidx.collection.SparseArrayCompat
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaParser
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import org.koitharu.kotatsu.parsers.util.json.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* https://api.comick.fun/docs/static/index.html
|
||||
*/
|
||||
|
||||
private const val PAGE_SIZE = 20
|
||||
private const val CHAPTERS_LIMIT = 99999
|
||||
|
||||
@MangaSourceParser("COMICK_FUN", "ComicK")
|
||||
internal class ComickFunParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.COMICK_FUN) {
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("comick.app", null)
|
||||
|
||||
override val sortOrders: Set<SortOrder> = EnumSet.of(
|
||||
SortOrder.POPULARITY,
|
||||
SortOrder.UPDATED,
|
||||
SortOrder.RATING,
|
||||
)
|
||||
|
||||
@Volatile
|
||||
private var cachedTags: SparseArrayCompat<MangaTag>? = null
|
||||
|
||||
override suspend fun getList(
|
||||
offset: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val domain = domain
|
||||
val url = buildString {
|
||||
append("https://api.")
|
||||
append(domain)
|
||||
append("/v1.0/search?tachiyomi=true")
|
||||
if (!query.isNullOrEmpty()) {
|
||||
if (offset > 0) {
|
||||
return emptyList()
|
||||
}
|
||||
append("&q=")
|
||||
append(query.urlEncoded())
|
||||
} else {
|
||||
append("&limit=")
|
||||
append(PAGE_SIZE)
|
||||
append("&page=")
|
||||
append((offset / PAGE_SIZE) + 1)
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
append("&genres=")
|
||||
appendAll(tags, "&genres=", MangaTag::key)
|
||||
}
|
||||
append("&sort=") // view, uploaded, rating, follow, user_follow_count
|
||||
append(
|
||||
when (sortOrder) {
|
||||
SortOrder.POPULARITY -> "view"
|
||||
SortOrder.RATING -> "rating"
|
||||
else -> "uploaded"
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
val ja = webClient.httpGet(url).parseJsonArray()
|
||||
val tagsMap = cachedTags ?: loadTags()
|
||||
return ja.mapJSON { jo ->
|
||||
val slug = jo.getString("slug")
|
||||
Manga(
|
||||
id = generateUid(slug),
|
||||
title = jo.getString("title"),
|
||||
altTitle = null,
|
||||
url = slug,
|
||||
publicUrl = "https://$domain/comic/$slug",
|
||||
rating = jo.getDoubleOrDefault("rating", -10.0).toFloat() / 10f,
|
||||
isNsfw = false,
|
||||
coverUrl = jo.getString("cover_url"),
|
||||
largeCoverUrl = null,
|
||||
description = jo.getStringOrNull("desc"),
|
||||
tags = jo.selectGenres("genres", tagsMap),
|
||||
state = runCatching {
|
||||
if (jo.getBoolean("translation_completed")) {
|
||||
MangaState.FINISHED
|
||||
} else {
|
||||
MangaState.ONGOING
|
||||
}
|
||||
}.getOrNull(),
|
||||
author = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val domain = domain
|
||||
val url = "https://api.$domain/comic/${manga.url}?tachiyomi=true"
|
||||
val jo = webClient.httpGet(url).parseJson()
|
||||
val comic = jo.getJSONObject("comic")
|
||||
return manga.copy(
|
||||
title = comic.getString("title"),
|
||||
altTitle = null, // TODO
|
||||
isNsfw = jo.getBoolean("matureContent") || comic.getBoolean("hentai"),
|
||||
description = comic.getStringOrNull("parsed") ?: comic.getString("desc"),
|
||||
tags = manga.tags + jo.getJSONArray("genres").mapJSONToSet {
|
||||
MangaTag(
|
||||
title = it.getString("name"),
|
||||
key = it.getString("slug"),
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
author = jo.getJSONArray("artists").optJSONObject(0)?.getString("name"),
|
||||
chapters = getChapters(comic.getString("hid")),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val jo = webClient.httpGet(
|
||||
"https://api.${domain}/chapter/${chapter.url}?tachiyomi=true",
|
||||
).parseJson().getJSONObject("chapter")
|
||||
val referer = "https://${domain}/"
|
||||
return jo.getJSONArray("images").mapJSON {
|
||||
val url = it.getString("url")
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getTags(): Set<MangaTag> {
|
||||
val sparseArray = cachedTags ?: loadTags()
|
||||
val set = ArraySet<MangaTag>(sparseArray.size())
|
||||
for (i in 0 until sparseArray.size()) {
|
||||
set.add(sparseArray.valueAt(i))
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
private suspend fun loadTags(): SparseArrayCompat<MangaTag> {
|
||||
val ja = webClient.httpGet("https://api.${domain}/genre").parseJsonArray()
|
||||
val tags = SparseArrayCompat<MangaTag>(ja.length())
|
||||
for (jo in ja.JSONIterator()) {
|
||||
tags.append(
|
||||
jo.getInt("id"),
|
||||
MangaTag(
|
||||
title = jo.getString("name"),
|
||||
key = jo.getString("slug"),
|
||||
source = source,
|
||||
),
|
||||
)
|
||||
}
|
||||
cachedTags = tags
|
||||
return tags
|
||||
}
|
||||
|
||||
private suspend fun getChapters(hid: String): List<MangaChapter> {
|
||||
val ja = webClient.httpGet(
|
||||
url = "https://api.${domain}/comic/$hid/chapters?limit=$CHAPTERS_LIMIT",
|
||||
).parseJson().getJSONArray("chapters")
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd")
|
||||
val counters = HashMap<Locale, Int>()
|
||||
return ja.mapReversed { jo ->
|
||||
val locale = Locale.forLanguageTag(jo.getString("lang"))
|
||||
var number = counters[locale] ?: 0
|
||||
number++
|
||||
counters[locale] = number
|
||||
MangaChapter(
|
||||
id = generateUid(jo.getLong("id")),
|
||||
name = buildString {
|
||||
jo.getStringOrNull("vol")?.let { append("Vol ").append(it).append(' ') }
|
||||
jo.getStringOrNull("chap")?.let { append("Chap ").append(it) }
|
||||
jo.getStringOrNull("title")?.let { append(": ").append(it) }
|
||||
},
|
||||
number = number,
|
||||
url = jo.getString("hid"),
|
||||
scanlator = jo.optJSONArray("group_name")?.optString(0),
|
||||
uploadDate = dateFormat.tryParse(jo.getString("created_at").substringBefore('T')),
|
||||
branch = locale.getDisplayName(locale).toTitleCase(locale),
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <R> JSONArray.mapReversed(block: (JSONObject) -> R): List<R> {
|
||||
val len = length()
|
||||
val destination = ArrayList<R>(len)
|
||||
for (i in (0 until len).reversed()) {
|
||||
val jo = getJSONObject(i)
|
||||
destination.add(block(jo))
|
||||
}
|
||||
return destination
|
||||
}
|
||||
|
||||
private fun JSONObject.selectGenres(name: String, tags: SparseArrayCompat<MangaTag>): Set<MangaTag> {
|
||||
val array = optJSONArray(name) ?: return emptySet()
|
||||
val res = ArraySet<MangaTag>(array.length())
|
||||
for (i in 0 until array.length()) {
|
||||
val id = array.getInt(i)
|
||||
val tag = tags.get(id) ?: continue
|
||||
res.add(tag)
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,179 @@
|
||||
package org.koitharu.kotatsu.parsers.site
|
||||
|
||||
import androidx.collection.ArrayMap
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
import org.koitharu.kotatsu.parsers.exception.ParseException
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSONIndexed
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSONToSet
|
||||
import java.util.*
|
||||
|
||||
@MangaSourceParser("DESUME", "Desu.me", "ru")
|
||||
internal class DesuMeParser(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.DESUME, 20) {
|
||||
|
||||
override val configKeyDomain = ConfigKey.Domain("desu.me", null)
|
||||
|
||||
override val sortOrders: Set<SortOrder> = EnumSet.of(
|
||||
SortOrder.UPDATED,
|
||||
SortOrder.POPULARITY,
|
||||
SortOrder.NEWEST,
|
||||
SortOrder.ALPHABETICAL,
|
||||
)
|
||||
|
||||
private val tagsCache = SuspendLazy(::fetchTags)
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
if (query != null && page != searchPaginator.firstPage) {
|
||||
return emptyList()
|
||||
}
|
||||
val domain = domain
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/manga/api/?limit=20&order=")
|
||||
append(getSortKey(sortOrder))
|
||||
append("&page=")
|
||||
append(page)
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
append("&genres=")
|
||||
appendAll(tags, ",") { it.key }
|
||||
}
|
||||
if (query != null) {
|
||||
append("&search=")
|
||||
append(query)
|
||||
}
|
||||
}
|
||||
val json = webClient.httpGet(url).parseJson().getJSONArray("response")
|
||||
?: throw ParseException("Invalid response", url)
|
||||
val total = json.length()
|
||||
val list = ArrayList<Manga>(total)
|
||||
val tagsMap = tagsCache.tryGet().getOrNull()
|
||||
for (i in 0 until total) {
|
||||
val jo = json.getJSONObject(i)
|
||||
val cover = jo.getJSONObject("image")
|
||||
val id = jo.getLong("id")
|
||||
val genres = jo.getString("genres").split(',')
|
||||
list += Manga(
|
||||
url = "/manga/api/$id",
|
||||
publicUrl = jo.getString("url"),
|
||||
source = MangaSource.DESUME,
|
||||
title = jo.getString("russian"),
|
||||
altTitle = jo.getString("name"),
|
||||
coverUrl = cover.getString("preview"),
|
||||
largeCoverUrl = cover.getString("original"),
|
||||
state = when {
|
||||
jo.getInt("ongoing") == 1 -> MangaState.ONGOING
|
||||
else -> null
|
||||
},
|
||||
rating = jo.getDouble("score").toFloat().coerceIn(0f, 1f),
|
||||
id = generateUid(id),
|
||||
isNsfw = false,
|
||||
tags = if (!tagsMap.isNullOrEmpty()) {
|
||||
genres.mapNotNullToSet { g ->
|
||||
tagsMap[g.trim().toTitleCase()]
|
||||
}
|
||||
} else {
|
||||
emptySet()
|
||||
},
|
||||
author = null,
|
||||
description = jo.getString("description"),
|
||||
)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val url = manga.url.toAbsoluteUrl(domain)
|
||||
val json = webClient.httpGet(url).parseJson().getJSONObject("response")
|
||||
?: throw ParseException("Invalid response", url)
|
||||
val baseChapterUrl = manga.url + "/chapter/"
|
||||
val chaptersList = json.getJSONObject("chapters").getJSONArray("list")
|
||||
val totalChapters = chaptersList.length()
|
||||
return manga.copy(
|
||||
tags = json.getJSONArray("genres").mapJSONToSet {
|
||||
MangaTag(
|
||||
key = it.getString("text"),
|
||||
title = it.getString("russian").toTitleCase(),
|
||||
source = manga.source,
|
||||
)
|
||||
},
|
||||
publicUrl = json.getString("url"),
|
||||
description = json.getString("description"),
|
||||
chapters = chaptersList.mapJSONIndexed { i, it ->
|
||||
val chid = it.getLong("id")
|
||||
val volChap = "Том " + it.optString("vol", "0") + ". " + "Глава " + it.optString("ch", "0")
|
||||
val title = it.optString("title", "null").takeUnless { it == "null" }
|
||||
MangaChapter(
|
||||
id = generateUid(chid),
|
||||
source = manga.source,
|
||||
url = "$baseChapterUrl$chid",
|
||||
uploadDate = it.getLong("date") * 1000,
|
||||
name = if (title.isNullOrEmpty()) volChap else "$volChap: $title",
|
||||
number = totalChapters - i,
|
||||
scanlator = null,
|
||||
branch = null,
|
||||
)
|
||||
}.reversed(),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val fullUrl = chapter.url.toAbsoluteUrl(domain)
|
||||
val json = webClient.httpGet(fullUrl)
|
||||
.parseJson()
|
||||
.getJSONObject("response") ?: throw ParseException("Invalid response", fullUrl)
|
||||
return json.getJSONObject("pages").getJSONArray("list").mapJSON { jo ->
|
||||
MangaPage(
|
||||
id = generateUid(jo.getLong("id")),
|
||||
preview = null,
|
||||
source = chapter.source,
|
||||
url = jo.getString("img"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getTags(): Set<MangaTag> {
|
||||
return tagsCache.get().values.toSet()
|
||||
}
|
||||
|
||||
private fun getSortKey(sortOrder: SortOrder) =
|
||||
when (sortOrder) {
|
||||
SortOrder.ALPHABETICAL -> "name"
|
||||
SortOrder.POPULARITY -> "popular"
|
||||
SortOrder.UPDATED -> "updated"
|
||||
SortOrder.NEWEST -> "id"
|
||||
else -> "updated"
|
||||
}
|
||||
|
||||
private suspend fun fetchTags(): Map<String, MangaTag> {
|
||||
val doc = webClient.httpGet("https://${domain}/manga/").parseHtml()
|
||||
val root = doc.body().requireElementById("animeFilter")
|
||||
.selectFirstOrThrow(".catalog-genres")
|
||||
val li = root.select("li")
|
||||
val result = ArrayMap<String, MangaTag>(li.size)
|
||||
li.forEach {
|
||||
val input = it.selectFirstOrThrow("input")
|
||||
val tag = MangaTag(
|
||||
source = source,
|
||||
key = input.attr("data-genre-slug").ifEmpty {
|
||||
it.parseFailed("data-genre-slug is empty")
|
||||
},
|
||||
title = input.attr("data-genre-name").toTitleCase().ifEmpty {
|
||||
it.parseFailed("data-genre-name is empty")
|
||||
},
|
||||
)
|
||||
result[tag.title] = tag
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,141 @@
|
||||
package org.koitharu.kotatsu.parsers.site
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@MangaSourceParser("DOUJINDESU", "DoujinDesu", "id")
|
||||
class DoujinDesuParser(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.DOUJINDESU, pageSize = 18) {
|
||||
|
||||
override val configKeyDomain: ConfigKey.Domain
|
||||
get() = ConfigKey.Domain("212.32.226.234", null)
|
||||
|
||||
override val sortOrders: Set<SortOrder>
|
||||
get() = EnumSet.of(SortOrder.UPDATED, SortOrder.NEWEST, SortOrder.ALPHABETICAL, SortOrder.POPULARITY)
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val docs = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml().selectFirstOrThrow("#archive")
|
||||
val chapterDateFormat = SimpleDateFormat("EEEE, dd MMMM yyyy", sourceLocale)
|
||||
val metadataEl = docs.selectFirst(".wrapper > .metadata tbody")
|
||||
val state = when (metadataEl?.selectFirst("tr:contains(Status)")?.selectLast("td")?.text()) {
|
||||
"Finished" -> MangaState.FINISHED
|
||||
"Publishing" -> MangaState.ONGOING
|
||||
else -> null
|
||||
}
|
||||
return manga.copy(
|
||||
author = metadataEl?.selectFirst("tr:contains(Author)")?.selectLast("td")?.text(),
|
||||
description = docs.selectFirst(".wrapper > .metadata > .pb-2")?.selectFirst("p")?.html(),
|
||||
state = state,
|
||||
rating = metadataEl?.selectFirst(".rating-prc")?.ownText()?.toFloatOrNull()?.div(10f) ?: RATING_UNKNOWN,
|
||||
tags = docs.select(".tags > a").mapToSet {
|
||||
MangaTag(
|
||||
key = it.attr("title"),
|
||||
title = it.text(),
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
chapters = docs.requireElementById("chapter_list")
|
||||
.select("ul > li")
|
||||
.mapChapters(reversed = true) { index, element ->
|
||||
val titleTag = element.selectFirstOrThrow(".epsleft > .lchx > a")
|
||||
val url = titleTag.attrAsRelativeUrl("href")
|
||||
MangaChapter(
|
||||
id = generateUid(url),
|
||||
name = titleTag.text(),
|
||||
number = index + 1,
|
||||
url = url,
|
||||
scanlator = null,
|
||||
uploadDate = chapterDateFormat.tryParse(element.select(".epsleft > .date").text()),
|
||||
branch = null,
|
||||
source = source,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
val url = urlBuilder().apply {
|
||||
addPathSegment("manga")
|
||||
addPathSegment("page")
|
||||
addPathSegment("$page/")
|
||||
val order = when (sortOrder) {
|
||||
SortOrder.UPDATED -> "update"
|
||||
SortOrder.POPULARITY -> "popular"
|
||||
SortOrder.ALPHABETICAL -> "title"
|
||||
SortOrder.NEWEST -> "latest"
|
||||
else -> throw IllegalArgumentException("Sort order not supported")
|
||||
}
|
||||
addQueryParameter("order", order)
|
||||
addQueryParameter("title", query.orEmpty())
|
||||
tags?.forEach {
|
||||
addEncodedQueryParameter("genre[]".urlEncoded(), it.key.urlEncoded())
|
||||
}
|
||||
}.build()
|
||||
|
||||
return webClient.httpGet(url).parseHtml()
|
||||
.requireElementById("archives")
|
||||
.selectFirstOrThrow("div.entries")
|
||||
.select(".entry")
|
||||
.map {
|
||||
val titleTag = it.selectFirstOrThrow(".metadata > a")
|
||||
val relativeUrl = titleTag.attrAsRelativeUrl("href")
|
||||
Manga(
|
||||
id = generateUid(relativeUrl),
|
||||
title = titleTag.attr("title"),
|
||||
altTitle = null,
|
||||
url = relativeUrl,
|
||||
publicUrl = relativeUrl.toAbsoluteUrl(domain),
|
||||
rating = RATING_UNKNOWN,
|
||||
isNsfw = true,
|
||||
coverUrl = it.selectFirst(".thumbnail > img")?.attrAsAbsoluteUrl("src").orEmpty(),
|
||||
tags = emptySet(),
|
||||
state = null,
|
||||
author = null,
|
||||
largeCoverUrl = null,
|
||||
description = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val id = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
.requireElementById("reader")
|
||||
.attr("data-id")
|
||||
return webClient.httpPost("/themes/ajax/ch.php".toAbsoluteUrl(domain), "id=$id").parseHtml()
|
||||
.select("img")
|
||||
.map {
|
||||
val url = it.attrAsRelativeUrl("src")
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getTags(): Set<MangaTag> {
|
||||
return webClient.httpGet("/genre/".toAbsoluteUrl(domain)).parseHtml()
|
||||
.requireElementById("taxonomy")
|
||||
.selectFirstOrThrow(".entries")
|
||||
.select(".entry > a")
|
||||
.mapToSet {
|
||||
MangaTag(
|
||||
key = it.attr("title"),
|
||||
title = it.attr("title"),
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,307 @@
|
||||
package org.koitharu.kotatsu.parsers.site
|
||||
|
||||
import androidx.collection.SparseArrayCompat
|
||||
import androidx.collection.set
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import org.jsoup.nodes.Element
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
|
||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.util.*
|
||||
import java.util.*
|
||||
import kotlin.math.pow
|
||||
|
||||
private const val DOMAIN_UNAUTHORIZED = "e-hentai.org"
|
||||
private const val DOMAIN_AUTHORIZED = "exhentai.org"
|
||||
|
||||
@MangaSourceParser("EXHENTAI", "ExHentai")
|
||||
internal class ExHentaiParser(
|
||||
context: MangaLoaderContext,
|
||||
) : PagedMangaParser(context, MangaSource.EXHENTAI, pageSize = 25), MangaParserAuthProvider {
|
||||
|
||||
override val sortOrders: Set<SortOrder> = Collections.singleton(
|
||||
SortOrder.NEWEST,
|
||||
)
|
||||
|
||||
override val configKeyDomain: ConfigKey.Domain
|
||||
get() = ConfigKey.Domain(if (isAuthorized) DOMAIN_AUTHORIZED else DOMAIN_UNAUTHORIZED, null)
|
||||
|
||||
override val authUrl: String
|
||||
get() = "https://${domain}/bounce_login.php"
|
||||
|
||||
private val ratingPattern = Regex("-?[0-9]+px")
|
||||
private val authCookies = arrayOf("ipb_member_id", "ipb_pass_hash")
|
||||
private var updateDm = false
|
||||
private val nextPages = SparseArrayCompat<Long>()
|
||||
private val suspiciousContentKey = ConfigKey.ShowSuspiciousContent(true)
|
||||
|
||||
override val isAuthorized: Boolean
|
||||
get() {
|
||||
val authorized = isAuthorized(DOMAIN_UNAUTHORIZED)
|
||||
if (authorized) {
|
||||
if (!isAuthorized(DOMAIN_AUTHORIZED)) {
|
||||
context.cookieJar.copyCookies(
|
||||
DOMAIN_UNAUTHORIZED,
|
||||
DOMAIN_AUTHORIZED,
|
||||
authCookies,
|
||||
)
|
||||
context.cookieJar.insertCookies(DOMAIN_AUTHORIZED, "yay=louder")
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
init {
|
||||
context.cookieJar.insertCookies(DOMAIN_AUTHORIZED, "nw=1", "sl=dm_2")
|
||||
context.cookieJar.insertCookies(DOMAIN_UNAUTHORIZED, "nw=1", "sl=dm_2")
|
||||
paginator.firstPage = 0
|
||||
}
|
||||
|
||||
override suspend fun getListPage(
|
||||
page: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
var search = query?.urlEncoded().orEmpty()
|
||||
val next = nextPages.get(page, 0L)
|
||||
if (page > 0 && next == 0L) {
|
||||
assert(false) { "Page timestamp not found" }
|
||||
return emptyList()
|
||||
}
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(domain)
|
||||
append("/?next=")
|
||||
append(next)
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
var fCats = 0
|
||||
for (tag in tags) {
|
||||
tag.key.toIntOrNull()?.let { fCats = fCats or it } ?: run {
|
||||
search += tag.key + " "
|
||||
}
|
||||
}
|
||||
if (fCats != 0) {
|
||||
append("&f_cats=")
|
||||
append(1023 - fCats)
|
||||
}
|
||||
}
|
||||
if (search.isNotEmpty()) {
|
||||
append("&f_search=")
|
||||
append(search.trim().replace(' ', '+'))
|
||||
}
|
||||
// by unknown reason cookie "sl=dm_2" is ignored, so, we should request it again
|
||||
if (updateDm) {
|
||||
append("&inline_set=dm_e")
|
||||
}
|
||||
append("&advsearch=1")
|
||||
if (config[suspiciousContentKey]) {
|
||||
append("&f_sh=on")
|
||||
}
|
||||
}
|
||||
val body = webClient.httpGet(url).parseHtml().body()
|
||||
val root = body.selectFirst("table.itg")
|
||||
?.selectFirst("tbody")
|
||||
?: if (updateDm) {
|
||||
body.parseFailed("Cannot find root")
|
||||
} else {
|
||||
updateDm = true
|
||||
return getListPage(page, query, tags, sortOrder)
|
||||
}
|
||||
updateDm = false
|
||||
nextPages[page + 1] = getNextTimestamp(body)
|
||||
return root.children().mapNotNull { tr ->
|
||||
if (tr.childrenSize() != 2) return@mapNotNull null
|
||||
val (td1, td2) = tr.children()
|
||||
val glink = td2.selectFirstOrThrow("div.glink")
|
||||
val a = glink.parents().select("a").first() ?: glink.parseFailed("link not found")
|
||||
val href = a.attrAsRelativeUrl("href")
|
||||
val tagsDiv = glink.nextElementSibling() ?: glink.parseFailed("tags div not found")
|
||||
val mainTag = td2.selectFirst("div.cn")?.let { div ->
|
||||
MangaTag(
|
||||
title = div.text().toTitleCase(),
|
||||
key = tagIdByClass(div.classNames()) ?: return@let null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
Manga(
|
||||
id = generateUid(href),
|
||||
title = glink.text().cleanupTitle(),
|
||||
altTitle = null,
|
||||
url = href,
|
||||
publicUrl = a.absUrl("href"),
|
||||
rating = td2.selectFirst("div.ir")?.parseRating() ?: RATING_UNKNOWN,
|
||||
isNsfw = true,
|
||||
coverUrl = td1.selectFirst("img")?.absUrl("src").orEmpty(),
|
||||
tags = setOfNotNull(mainTag),
|
||||
state = null,
|
||||
author = tagsDiv.getElementsContainingOwnText("artist:").first()
|
||||
?.nextElementSibling()?.text(),
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val root = doc.body().selectFirstOrThrow("div.gm")
|
||||
val cover = root.getElementById("gd1")?.children()?.first()
|
||||
val title = root.getElementById("gd2")
|
||||
val taglist = root.getElementById("taglist")
|
||||
val tabs = doc.body().selectFirst("table.ptt")?.selectFirst("tr")
|
||||
return manga.copy(
|
||||
title = title?.getElementById("gn")?.text()?.cleanupTitle() ?: manga.title,
|
||||
altTitle = title?.getElementById("gj")?.text()?.cleanupTitle() ?: manga.altTitle,
|
||||
publicUrl = doc.baseUri().ifEmpty { manga.publicUrl },
|
||||
rating = root.getElementById("rating_label")?.text()
|
||||
?.substringAfterLast(' ')
|
||||
?.toFloatOrNull()
|
||||
?.div(5f) ?: manga.rating,
|
||||
largeCoverUrl = cover?.styleValueOrNull("background")?.cssUrl(),
|
||||
description = taglist?.select("tr")?.joinToString("<br>") { tr ->
|
||||
val (tc, td) = tr.children()
|
||||
val subtags = td.select("a").joinToString { it.html() }
|
||||
"<b>${tc.html()}</b> $subtags"
|
||||
},
|
||||
chapters = tabs?.select("a")?.findLast { a ->
|
||||
a.text().toIntOrNull() != null
|
||||
}?.let { a ->
|
||||
val count = a.text().toInt()
|
||||
val chapters = ChaptersListBuilder(count)
|
||||
for (i in 1..count) {
|
||||
val url = "${manga.url}?p=${i - 1}"
|
||||
chapters += MangaChapter(
|
||||
id = generateUid(url),
|
||||
name = "${manga.title} #$i",
|
||||
number = i,
|
||||
url = url,
|
||||
uploadDate = 0L,
|
||||
source = source,
|
||||
scanlator = null,
|
||||
branch = null,
|
||||
)
|
||||
}
|
||||
chapters.toList()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
val root = doc.body().requireElementById("gdt")
|
||||
return root.select("a").map { a ->
|
||||
val url = a.attrAsRelativeUrl("href")
|
||||
MangaPage(
|
||||
id = generateUid(url),
|
||||
url = url,
|
||||
preview = null,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPageUrl(page: MangaPage): String {
|
||||
val doc = webClient.httpGet(page.url.toAbsoluteUrl(domain)).parseHtml()
|
||||
return doc.body().requireElementById("img").attrAsAbsoluteUrl("src")
|
||||
}
|
||||
|
||||
override suspend fun getTags(): Set<MangaTag> {
|
||||
val doc = webClient.httpGet("https://${domain}").parseHtml()
|
||||
val root = doc.body().requireElementById("searchbox").selectFirstOrThrow("table")
|
||||
return root.select("div.cs").mapNotNullToSet { div ->
|
||||
val id = div.id().substringAfterLast('_').toIntOrNull()
|
||||
?: return@mapNotNullToSet null
|
||||
MangaTag(
|
||||
title = div.text().toTitleCase(),
|
||||
key = id.toString(),
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getUsername(): String {
|
||||
val doc = webClient.httpGet("https://forums.$DOMAIN_UNAUTHORIZED/").parseHtml().body()
|
||||
val username = doc.getElementById("userlinks")
|
||||
?.getElementsByAttributeValueContaining("href", "showuser=")
|
||||
?.firstOrNull()
|
||||
?.ownText()
|
||||
?: if (doc.getElementById("userlinksguest") != null) {
|
||||
throw AuthRequiredException(source)
|
||||
} else {
|
||||
doc.parseFailed()
|
||||
}
|
||||
return username
|
||||
}
|
||||
|
||||
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
|
||||
super.onCreateConfig(keys)
|
||||
keys.add(suspiciousContentKey)
|
||||
}
|
||||
|
||||
private fun isAuthorized(domain: String): Boolean {
|
||||
val cookies = context.cookieJar.getCookies(domain).mapToSet { x -> x.name }
|
||||
return authCookies.all { it in cookies }
|
||||
}
|
||||
|
||||
private fun Element.parseRating(): Float {
|
||||
return runCatching {
|
||||
val style = requireNotNull(attr("style"))
|
||||
val (v1, v2) = ratingPattern.find(style)!!.destructured
|
||||
var p1 = v1.dropLast(2).toInt()
|
||||
val p2 = v2.dropLast(2).toInt()
|
||||
if (p2 != -1) {
|
||||
p1 += 8
|
||||
}
|
||||
(80 - p1) / 80f
|
||||
}.getOrDefault(RATING_UNKNOWN)
|
||||
}
|
||||
|
||||
private fun String.cleanupTitle(): String {
|
||||
val result = StringBuilder(length)
|
||||
var skip = false
|
||||
for (c in this) {
|
||||
when {
|
||||
c == '[' -> skip = true
|
||||
c == ']' -> skip = false
|
||||
c.isWhitespace() && result.isEmpty() -> continue
|
||||
!skip -> result.append(c)
|
||||
}
|
||||
}
|
||||
while (result.lastOrNull()?.isWhitespace() == true) {
|
||||
result.deleteCharAt(result.lastIndex)
|
||||
}
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
private fun String.cssUrl(): String? {
|
||||
val fromIndex = indexOf("url(")
|
||||
if (fromIndex == -1) {
|
||||
return null
|
||||
}
|
||||
val toIndex = indexOf(')', startIndex = fromIndex)
|
||||
return if (toIndex == -1) {
|
||||
null
|
||||
} else {
|
||||
substring(fromIndex + 4, toIndex).trim()
|
||||
}
|
||||
}
|
||||
|
||||
private fun tagIdByClass(classNames: Collection<String>): String? {
|
||||
val className = classNames.find { x -> x.startsWith("ct") } ?: return null
|
||||
val num = className.drop(2).toIntOrNull(16) ?: return null
|
||||
return 2.0.pow(num).toInt().toString()
|
||||
}
|
||||
|
||||
private fun getNextTimestamp(root: Element): Long {
|
||||
return root.getElementById("unext")
|
||||
?.attrAsAbsoluteUrlOrNull("href")
|
||||
?.toHttpUrlOrNull()
|
||||
?.queryParameter("next")
|
||||
?.toLongOrNull() ?: 1
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue