diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..5302184 --- /dev/null +++ b/.github/workflows/build.yml @@ -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 diff --git a/build.gradle.kts b/build.gradle.kts index 1cae00e..7cb9e06 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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" @@ -18,7 +18,9 @@ tasks.withType { archiveBaseName = "kotatsu-dl" archiveClassifier = "" archiveVersion = "" - minimize() + minimize { + exclude(dependency("org.openjdk.nashorn:.*:.*")) + } } repositories { @@ -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") @@ -45,4 +47,4 @@ tasks.test { } kotlin { jvmToolchain(17) -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/dl/Main.kt b/src/main/kotlin/org/koitharu/kotatsu/dl/Main.kt index 5ab6b33..12f0c31 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/dl/Main.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/dl/Main.kt @@ -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") } @@ -141,4 +141,4 @@ class Main : AppCommand(name = "kotatsu-dl") { } } -suspend fun main(args: Array) = Main().main(args) \ No newline at end of file +suspend fun main(args: Array) = Main().main(args) diff --git a/src/main/kotlin/org/koitharu/kotatsu/dl/parsers/CommonHeadersInterceptor.kt b/src/main/kotlin/org/koitharu/kotatsu/dl/parsers/CommonHeadersInterceptor.kt index 9149121..09f3347 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/dl/parsers/CommonHeadersInterceptor.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/dl/parsers/CommonHeadersInterceptor.kt @@ -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 diff --git a/src/main/kotlin/org/koitharu/kotatsu/dl/parsers/MangaLoaderContextImpl.kt b/src/main/kotlin/org/koitharu/kotatsu/dl/parsers/MangaLoaderContextImpl.kt index b0c8c76..e19cc9a 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/dl/parsers/MangaLoaderContextImpl.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/dl/parsers/MangaLoaderContextImpl.kt @@ -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)) - } -} \ No newline at end of file +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)) + } + +}