Skip to content
This repository was archived by the owner on Nov 4, 2025. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Workflow name
name: Build JAR

# Triggers for the workflow
on:
# Allows manual triggering of the workflow from the GitHub Actions UI
workflow_dispatch:
inputs:
# Input field for the full repository path of the new parser
# e.g., "NewOwner/NewParserRepo"
parser_repo_full:
description: 'Full repository for the new parser (e.g., NewOwner/NewParserRepo)'
required: true
default: 'KotatsuApp/kotatsu-parsers' # Default to the original repo, perhaps to update to its latest commit
# Input field for the branch of the new parser repository
parser_branch:
description: 'Branch for the new parser (e.g., master or main)'
required: true
default: 'master' # Default branch, can be 'main' or other depending on the target repo

# Defines the jobs to be run in the workflow
jobs:
build:
# Specifies the runner environment for the job
runs-on: ubuntu-latest
# Defines the sequence of steps for the 'build' job
steps:
# Step 1: Check out the repository code
- name: Checkout code
uses: actions/checkout@v4 # Uses the v4 of the checkout action

# Step 2: Set up JDK 17
- name: Set up JDK 17
uses: actions/setup-java@v3 # Uses the v3 of the setup-java action
with:
java-version: '17' # Specifies Java version 17
distribution: 'temurin' # Specifies the Temurin distribution of JDK

# Step 3: Get the latest commit hash and repository details for the new parser
- name: Get new parser commit hash and repo details
id: get_new_parser_info # Sets an ID for this step to reference its outputs later
run: |
# Get inputs from the workflow dispatch event
NEW_PARSER_REPO_FULL="${{ github.event.inputs.parser_repo_full }}"
PARSER_BRANCH="${{ github.event.inputs.parser_branch }}"

echo "Fetching latest commit from $NEW_PARSER_REPO_FULL on branch $PARSER_BRANCH"
# Fetch the latest commit SHA from the specified GitHub repository and branch
# -s for silent, -L to follow redirects
# Accept header requests only the SHA (plain text)
# Authorization uses the automatically provided GITHUB_TOKEN for API rate limits and private repo access
LATEST_HASH=$(curl -s -L \
-H "Accept: application/vnd.github.v3.sha" \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://api.github.com/repos/$NEW_PARSER_REPO_FULL/commits/$PARSER_BRANCH")

# Check if fetching the hash failed (empty, "Not Found", API errors, etc.)
if [ -z "$LATEST_HASH" ] || [ "$LATEST_HASH" == "Not Found" ] || [[ "$LATEST_HASH" == *"API rate limit exceeded"* ]] || [[ "$LATEST_HASH" == *"Problems parsing JSON"* ]]; then
echo "Error fetching commit hash for $NEW_PARSER_REPO_FULL: $LATEST_HASH"
echo "Please ensure the repository and branch are correct and accessible."
# Fallback to a hardcoded original version if fetching fails.
# This prevents the build from failing immediately due to transient API issues.
# You might want to adjust this fallback (e.g., fail the build, use a different default).
LATEST_HASH="16b8bf9328" # Original hash of KotatsuApp:kotatsu-parsers as a fallback
NEW_PARSER_REPO_FULL="KotatsuApp/kotatsu-parsers" # Original repo as a fallback
echo "Using fallback version: $LATEST_HASH for $NEW_PARSER_REPO_FULL"
fi

# Extract the owner and repository name from the full repository path (Owner/RepoName)
NEW_PARSER_OWNER=$(echo "$NEW_PARSER_REPO_FULL" | cut -d'/' -f1)
NEW_PARSER_REPO_NAME=$(echo "$NEW_PARSER_REPO_FULL" | cut -d'/' -f2)
# Get the first 10 characters of the hash
SHORT_HASH=$(echo "$LATEST_HASH" | cut -c1-10)

echo "New Parser Owner: $NEW_PARSER_OWNER"
echo "New Parser Repo Name: $NEW_PARSER_REPO_NAME"
echo "New Short hash: $SHORT_HASH"

# Set outputs for this step to be used by subsequent steps
echo "::set-output name=new_parser_owner::$NEW_PARSER_OWNER"
echo "::set-output name=new_parser_repo_name::$NEW_PARSER_REPO_NAME"
echo "::set-output name=new_short_hash::$SHORT_HASH"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Provide the GitHub token to the script environment

# Step 4: Update the specific parser dependency line in build.gradle.kts
- name: Update specific parser version in build.gradle.kts
run: |
# Retrieve the outputs from the previous step
NEW_OWNER="${{ steps.get_new_parser_info.outputs.new_parser_owner }}"
NEW_REPO_NAME="${{ steps.get_new_parser_info.outputs.new_parser_repo_name }}"
NEW_HASH="${{ steps.get_new_parser_info.outputs.new_short_hash }}"

# Check if any of the required information is empty
if [ -z "$NEW_OWNER" ] || [ -z "$NEW_REPO_NAME" ] || [ -z "$NEW_HASH" ]; then
echo "New parser owner, repo name, or short hash is empty. Skipping update."
# Optionally, exit with an error if update is mandatory:
# exit 1
echo "Build will continue with the existing parser version in build.gradle.kts."
else
# Define the owner and repository name of the *original* dependency line to be replaced
# Ensure these match the dependency you intend to update in your build.gradle.kts
ORIGINAL_PARSER_OWNER="KotatsuApp"
ORIGINAL_PARSER_REPO_NAME="kotatsu-parsers"

echo "Attempting to replace: com.github.$ORIGINAL_PARSER_OWNER:$ORIGINAL_PARSER_REPO_NAME"
echo "With: com.github.$NEW_OWNER:$NEW_REPO_NAME:$NEW_HASH"

# Use sed to find and replace the specific dependency line.
# This command targets the line for "com.github.KotatsuApp:kotatsu-parsers:ANY_VERSION"
# and replaces its owner, repo name, and version with the new dynamic values.
# -i for in-place edit, -E for extended regex.
# The pattern specifically matches the ORIGINAL_PARSER_OWNER and ORIGINAL_PARSER_REPO_NAME.
# `[^"]+` matches the old version string (any characters except a quote).
# `\1` and `\2` are backreferences to the captured groups in the pattern.
sed -i -E 's,(implementation\("com\.github\.)'$ORIGINAL_PARSER_OWNER':'$ORIGINAL_PARSER_REPO_NAME':[^"]+("\)),\1'"$NEW_OWNER"':'"$NEW_REPO_NAME"':'"$NEW_HASH"'\2,' build.gradle.kts

echo "build.gradle.kts after potential update:"
cat build.gradle.kts # Print the file content for verification
fi

# Step 5: Verify build.gradle.kts to ensure critical dependencies are intact
- name: Verify build.gradle.kts
run: |
echo "Verifying updated parser dependency (if update occurred):"
# If the new parser info was successfully fetched and an update was attempted,
# check if the new parser line exists in the build file.
if [ -n "${{ steps.get_new_parser_info.outputs.new_short_hash }}" ] && \
[ -n "${{ steps.get_new_parser_info.outputs.new_parser_owner }}" ] && \
[ -n "${{ steps.get_new_parser_info.outputs.new_parser_repo_name }}" ]; then
EXPECTED_PARSER_LINE="implementation(\"com.github.${{ steps.get_new_parser_info.outputs.new_parser_owner }}:${{ steps.get_new_parser_info.outputs.new_parser_repo_name }}:${{ steps.get_new_parser_info.outputs.new_short_hash }}\")"
echo "Expected new parser line: $EXPECTED_PARSER_LINE"
# Use grep -F for fixed string search.
# This check might show a false negative if the update was skipped (e.g. due to empty NEW_OWNER)
# The `|| echo ...` part prevents the script from exiting if grep doesn't find the line,
# which might be okay if the update was intentionally skipped.
grep -F "$EXPECTED_PARSER_LINE" build.gradle.kts || echo "Parser line might not have been updated as expected (e.g., if fallback was used or skip logic triggered)."
fi

# Step 6: Set up Gradle
- name: Setup Gradle
uses: gradle/gradle-build-action@v2 # Uses the official Gradle action

# Step 7: Build the project with Gradle, running the shadowJar task
- name: Build with Gradle
run: ./gradlew shadowJar # Executes the shadowJar task to create the fat JAR

# Step 8: Upload the built JAR as an artifact
- name: Upload JAR artifact
uses: actions/upload-artifact@v4 # Uses v4 of the upload-artifact action
with:
name: kotatsu-dl # Name of the artifact to be uploaded
path: build/libs/kotatsu-dl.jar # Path to the JAR file to upload
12 changes: 7 additions & 5 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar

plugins {
kotlin("jvm") version "2.0.20"
id("com.gradleup.shadow") version "8.3.3"
kotlin("jvm") version "2.2.20"
id("com.gradleup.shadow") version "8.3.8"
}

group = "org.koitharu"
Expand All @@ -18,7 +18,9 @@ tasks.withType<ShadowJar> {
archiveBaseName = "kotatsu-dl"
archiveClassifier = ""
archiveVersion = ""
minimize()
minimize {
exclude(dependency("org.openjdk.nashorn:.*:.*"))
}
}

repositories {
Expand All @@ -34,7 +36,7 @@ dependencies {
implementation("com.github.KotatsuApp:kotatsu-parsers:16b8bf9328")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okio:okio:3.11.0")
implementation("io.webfolder:quickjs:1.1.0")
implementation("org.openjdk.nashorn:nashorn-core:15.6")
implementation("org.json:json:20240303")
implementation("me.tongfei:progressbar:0.10.1")
implementation("androidx.collection:collection:1.5.0")
Expand All @@ -45,4 +47,4 @@ tasks.test {
}
kotlin {
jvmToolchain(17)
}
}
4 changes: 2 additions & 2 deletions src/main/kotlin/org/koitharu/kotatsu/dl/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class Main : AppCommand(name = "kotatsu-dl") {
print("Resolving link…")
val source = linkResolver.getSource()
val manga = linkResolver.getManga()
if (source == null || source == MangaParserSource.DUMMY) {
if (source == null) {
println()
error("Unsupported manga source")
}
Expand Down Expand Up @@ -141,4 +141,4 @@ class Main : AppCommand(name = "kotatsu-dl") {
}
}

suspend fun main(args: Array<String>) = Main().main(args)
suspend fun main(args: Array<String>) = Main().main(args)
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import org.koitharu.kotatsu.dl.util.CommonHeaders
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.domain
import org.koitharu.kotatsu.parsers.util.mergeWith
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.net.IDN
Expand Down
Original file line number Diff line number Diff line change
@@ -1,58 +1,71 @@
package org.koitharu.kotatsu.dl.parsers

import com.koushikdutta.quack.QuackContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import okhttp3.CookieJar
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.bitmap.Bitmap
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.util.requireBody
import java.awt.image.BufferedImage
import java.util.concurrent.TimeUnit
import javax.imageio.ImageIO

class MangaLoaderContextImpl : MangaLoaderContext() {

override val cookieJar: CookieJar = InMemoryCookieJar()

override val httpClient: OkHttpClient = OkHttpClient.Builder()
.cookieJar(cookieJar)
.addInterceptor(CloudFlareInterceptor())
.addInterceptor(GZipInterceptor())
.addInterceptor(RateLimitInterceptor())
.addInterceptor(CommonHeadersInterceptor(this))
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.build()

override suspend fun evaluateJs(script: String): String? = runInterruptible(Dispatchers.Default) {
QuackContext.create().use {
it.evaluate(script)?.toString()
}
}

override fun getConfig(source: MangaSource): MangaSourceConfig = DefaultMangaSourceConfig()

override fun getDefaultUserAgent(): String = UserAgents.FIREFOX_DESKTOP

override fun redrawImageResponse(response: Response, redraw: (Bitmap) -> Bitmap): Response {
val srcImage = response.requireBody().byteStream().use { ImageIO.read(it) }
checkNotNull(srcImage) { "Cannot decode image" }
val resImage = (redraw(BitmapImpl(srcImage)) as BitmapImpl)
return response.newBuilder()
.body(resImage.compress("png").toResponseBody("image/png".toMediaTypeOrNull()))
.build()
}

override fun createBitmap(width: Int, height: Int): Bitmap {
return BitmapImpl(BufferedImage(width, height, BufferedImage.TYPE_INT_RGB))
}
}
package org.koitharu.kotatsu.dl.parsers

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import okhttp3.CookieJar
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.bitmap.Bitmap
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.util.requireBody
import java.awt.image.BufferedImage
import java.util.concurrent.TimeUnit
import javax.imageio.ImageIO
import javax.script.ScriptEngineManager


class MangaLoaderContextImpl : MangaLoaderContext() {

override val cookieJar: CookieJar = InMemoryCookieJar()

private val scriptEngineManager = ScriptEngineManager()

override val httpClient: OkHttpClient = OkHttpClient.Builder()
.cookieJar(cookieJar)
.addInterceptor(CloudFlareInterceptor())
.addInterceptor(GZipInterceptor())
.addInterceptor(RateLimitInterceptor())
.addInterceptor(CommonHeadersInterceptor(this))
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.build()

@Deprecated("Provide a base url")
override suspend fun evaluateJs(script: String): String? = runInterruptible(Dispatchers.Default) {
val nashorn = scriptEngineManager.getEngineByName("nashorn")
?: error("JavaScript engine is not available")
nashorn.eval(script)?.toString()?.takeUnless { it.isEmpty() || it == "null" }
}

override suspend fun evaluateJs(baseUrl: String, script: String): String? {
return runInterruptible(Dispatchers.Default) {
val nashorn = scriptEngineManager.getEngineByName("nashorn")
?: error("JavaScript engine is not available")
nashorn.eval(script)?.toString()?.takeUnless { it.isEmpty() || it == "null" }
}
}

override fun getConfig(source: MangaSource): MangaSourceConfig = DefaultMangaSourceConfig()

override fun getDefaultUserAgent(): String = UserAgents.FIREFOX_DESKTOP

override fun redrawImageResponse(response: Response, redraw: (Bitmap) -> Bitmap): Response {
val srcImage = response.requireBody().byteStream().use { ImageIO.read(it) }
checkNotNull(srcImage) { "Cannot decode image" }
val resImage = (redraw(BitmapImpl(srcImage)) as BitmapImpl)
return response.newBuilder()
.body(resImage.compress("png").toResponseBody("image/png".toMediaTypeOrNull()))
.build()
}

override fun createBitmap(width: Int, height: Int): Bitmap {
return BitmapImpl(BufferedImage(width, height, BufferedImage.TYPE_INT_RGB))
}

}