From cbc9a26b7dc0414ec1454857635b640857cb2fb1 Mon Sep 17 00:00:00 2001 From: pan <1029559041@qq.com> Date: Sun, 27 Jun 2021 00:48:03 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=98=E5=8E=9F=E6=BC=AB=E7=94=BB=E5=9B=BE?= =?UTF-8?q?=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 115 ++++++++ README.md | 3 + build.gradle.kts | 125 ++++++++ gradle.properties | 12 + gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 185 ++++++++++++ gradlew.bat | 89 ++++++ settings.gradle.kts | 3 + src/commonMain/kotlin/Data.kt | 37 +++ src/jsMain/kotlin/client.kt | 21 ++ src/jsMain/kotlin/image.kt | 308 ++++++++++++++++++++ src/jsMain/kotlin/welcome.kt | 153 ++++++++++ src/jsTest/kotlin/Test.kt | 45 +++ src/jvmMain/kotlin/plugins/HTTP.kt | 19 ++ src/jvmMain/kotlin/plugins/Monitoring.kt | 14 + src/jvmMain/kotlin/plugins/Routing.kt | 157 ++++++++++ src/jvmMain/kotlin/plugins/Serialization.kt | 11 + src/jvmMain/kotlin/plugins/WebSockets.kt | 8 + src/jvmMain/kotlin/server.kt | 47 +++ src/jvmMain/resources/application.conf | 22 ++ src/jvmMain/resources/logback.xml | 14 + src/jvmMain/resources/static/image/.gitkeep | 0 src/jvmTest/kotlin/ApplicationTest.kt | 78 +++++ 23 files changed, 1471 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts create mode 100644 src/commonMain/kotlin/Data.kt create mode 100644 src/jsMain/kotlin/client.kt create mode 100644 src/jsMain/kotlin/image.kt create mode 100644 src/jsMain/kotlin/welcome.kt create mode 100644 src/jsTest/kotlin/Test.kt create mode 100644 src/jvmMain/kotlin/plugins/HTTP.kt create mode 100644 src/jvmMain/kotlin/plugins/Monitoring.kt create mode 100644 src/jvmMain/kotlin/plugins/Routing.kt create mode 100644 src/jvmMain/kotlin/plugins/Serialization.kt create mode 100644 src/jvmMain/kotlin/plugins/WebSockets.kt create mode 100644 src/jvmMain/kotlin/server.kt create mode 100644 src/jvmMain/resources/application.conf create mode 100644 src/jvmMain/resources/logback.xml create mode 100644 src/jvmMain/resources/static/image/.gitkeep create mode 100644 src/jvmTest/kotlin/ApplicationTest.kt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf79ecf --- /dev/null +++ b/.gitignore @@ -0,0 +1,115 @@ +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Gradle template +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +### Kotlin template +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +.idea diff --git a/README.md b/README.md new file mode 100644 index 0000000..0e81f93 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# 爬取[takeshobo](https://gammaplus.takeshobo.co.jp/) 网站上的漫画 +## 服务端:[ktor](https://ktor.io/) +## 客户端:[Kotlin/JS+React](https://kotlinlang.org/docs/js-get-started.html) \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..0cc22a3 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,125 @@ +import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpack +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + val kotlin_version="1.5.20" + kotlin("multiplatform") version kotlin_version + kotlin("plugin.serialization") version kotlin_version + application +} + +val serialization_version:String by project +val kotlin_version:String by project +val logback_version:String by project +val ktor_version:String by project +val `kotlin-react-version`:String by project +val `kotlin-styled-version`:String by project +group = "jp.co.takeshobo" +version = "1.0-SNAPSHOT" + +repositories { + jcenter() + mavenCentral() + maven { url = uri("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/kotlin-js-wrappers") } + maven { url = uri("https://maven.pkg.jetbrains.space/public/p/kotlinx-html/maven") } +} + +kotlin { + jvm { + compilations.all { + kotlinOptions.jvmTarget = "1.8" + } + testRuns["test"].executionTask.configure { + useJUnit() + } + withJava() + } + js(LEGACY) { + binaries.executable() + browser { + commonWebpackConfig { + cssSupport.enabled = true + } + testTask { + useKarma { + useChrome() + } + } + } + } + sourceSets { + val commonMain by getting{ + dependencies { + implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlin_version") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version") + } + } + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + val jvmMain by getting { + dependencies { + implementation("io.ktor:ktor-server-netty:$ktor_version") + implementation("io.ktor:ktor-client-core:$ktor_version") + implementation("io.ktor:ktor-client-cio:$ktor_version") + implementation("io.ktor:ktor-html-builder:$ktor_version") + implementation("io.ktor:ktor-serialization:$ktor_version") + implementation("io.ktor:ktor-websockets:$ktor_version") + + implementation("ch.qos.logback:logback-classic:$logback_version") + + } + } + val jvmTest by getting{ + dependencies { + implementation("io.ktor:ktor-server-tests:$ktor_version") + } + + } + val jsMain by getting { + dependencies { + implementation("org.jetbrains.kotlin-wrappers:kotlin-react:${`kotlin-react-version`}") + implementation("org.jetbrains.kotlin-wrappers:kotlin-react-dom:${`kotlin-react-version`}") + implementation("org.jetbrains.kotlin-wrappers:kotlin-styled:${`kotlin-styled-version`}") + } + } + val jsTest by getting + } +} + +application{ + mainClass.set("ServerKt") +} + +tasks { + "run"(JavaExec::class) { + environment("CHROME_BIN","F:\\ChromeUpdater\\chrome.exe") + } +} + +tasks.getByName("jsBrowserProductionWebpack") { + outputFileName = "js.js" +} + +tasks.getByName("jvmJar") { + dependsOn(tasks.getByName("jsBrowserProductionWebpack")) + val jsBrowserProductionWebpack = tasks.getByName("jsBrowserProductionWebpack") + from(File(jsBrowserProductionWebpack.destinationDirectory, jsBrowserProductionWebpack.outputFileName)) +} + +tasks.getByName("run") { + dependsOn(tasks.getByName("jvmJar")) + classpath(tasks.getByName("jvmJar")) +} + +tasks.withType { + kotlinOptions { + freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn") + } +} + +tasks.withType{ + duplicatesStrategy = DuplicatesStrategy.INCLUDE +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..aaf35a2 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,12 @@ +kotlin.code.style=official +kotlin.mpp.enableGranularSourceSetsMetadata=true +kotlin.native.enableDependencyPropagation=false +kotlin.js.generate.executable.default=false +kotlin.mpp.stability.nowarn=true +serialization_version=1.2.1 +kotlin_version=1.5.20 +logback_version=1.2.3 +ktor_version=1.6.0 +kotlin-react-version=17.0.2-pre.213-kotlin-1.5.20 +kotlin-styled-version=5.3.0-pre.213-kotlin-1.5.20 +systemProp.file.encoding=UTF-8 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..69a9715 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..5278f56 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,3 @@ + +rootProject.name = "manga" + diff --git a/src/commonMain/kotlin/Data.kt b/src/commonMain/kotlin/Data.kt new file mode 100644 index 0000000..b1999d4 --- /dev/null +++ b/src/commonMain/kotlin/Data.kt @@ -0,0 +1,37 @@ +import kotlinx.serialization.Contextual +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class i(val src: String, val width: Int, val height: Int) + +@Serializable +data class resources(val i: i) + +@Serializable +data class area(val href: String, val left: Int, val top: Int, val right: Int, val bottom: Int) + +@Serializable +data class view(val width: Int, val height: Int, val coords: List, val areas: List? = null) + +@Serializable +data class t(@SerialName("ptimg-version") val ptimg_version: Int, + val resources: resources, val views: List) + +@Serializable +data class ApiResponse(val message:String,@Contextual val body:T?=null) + +@Serializable +data class MessageResponse(val message: String) + +@Serializable +data class UrlResult(val originImagePath:String,val serverImagePath:String, val t:t) + +@Serializable +data class ParseTask(val total:Int,val finish:Int,val percentage:Float) + +data class UrlParam(val url:String,val html:String) + +const val websocketPath="/webSocket" + +const val websiteTitle="朴实无华的漫画解析工具" \ No newline at end of file diff --git a/src/jsMain/kotlin/client.kt b/src/jsMain/kotlin/client.kt new file mode 100644 index 0000000..544c1fa --- /dev/null +++ b/src/jsMain/kotlin/client.kt @@ -0,0 +1,21 @@ +import react.dom.render +import kotlinx.browser.document +import kotlinx.browser.window +import org.w3c.dom.WebSocket +import org.w3c.dom.events.Event + +fun main() { + window.onload = { + val webSocket=WebSocket("ws://localhost:8080${websocketPath}") + webSocket.onopen={event: Event -> console.info("打开连接:${event}") } + webSocket.onclose={event: Event -> console.info("关闭连接:${event}") } + webSocket.onerror={event: Event -> console.error("发生错误:${event}") } + render(document.getElementById("root")) { + child(Welcome::class) { + attrs { + this.webSocket=webSocket + } + } + } + } +} diff --git a/src/jsMain/kotlin/image.kt b/src/jsMain/kotlin/image.kt new file mode 100644 index 0000000..2f44867 --- /dev/null +++ b/src/jsMain/kotlin/image.kt @@ -0,0 +1,308 @@ +import kotlinext.js.getOwnPropertyNames +import kotlinx.browser.document +import kotlinx.css.head +import org.khronos.webgl.Uint8Array +import org.khronos.webgl.set +import org.w3c.dom.CanvasRenderingContext2D +import org.w3c.dom.HTMLCanvasElement +import org.w3c.dom.Image +import org.w3c.dom.events.Event +import org.w3c.dom.url.URL +import org.w3c.files.Blob +import org.w3c.files.BlobPropertyBag +import kotlin.js.Promise +import kotlin.math.ceil +import kotlin.math.max +import kotlin.math.min +import kotlin.math.round + + +data class un(val width: Int, val height: Int) + + +data class coord( + val resid: String, + val xsrc: Int, + val ysrc: Int, + val width: Int, + val height: Int, + val xdest: Int, + val ydest: Int +) + +data class formatview(val width: Int, val height: Int, val coords: List, val areas: List? = null) + +data class transfer(val index:Int,val coords: List) + +data class n(val width: Int,val height: Int,val transfers:List) + +data class N( + val left: Int, val top: Int, val width: Int, val height: Int, + val bottom: Int = top + height, val right: Int = left + width +) + +object p { + //speedbinb.js?dmy=016301:formatted:3998 + fun Rectangle(t: Int, i: Int, n: Int, r: Int): N { + return N(left = t, top = i, width = n, height = r) + } + + //speedbinb.js?dmy=016301:formatted:3966 + fun intersect(t:N,i:N):N?{ + val n = t.left + val r = t.left + t.width + val e = t.top + val s = t.top + t.height + val h = i.left + val u = i.left + i.width + val o = i.top + val a = i.top + i.height + if (n < u && h < r && e < a && o < s) { + val f = max(n, h) + val c = max(e, o) + return N(f,c,min(r, u) - f,min(s, a) - c) + } + return null + } +} + +class e(val Xs: Int = 3, private val Ws: Int = 8, private val Ys: Int = 4, val un: un) { + + //speedbinb.js?dmy=016301:formatted:8848 + fun Us(t: Int): N { + val i = this.un.height + val n = ceil( (i + this.Ys * (this.Xs - 1)) / this.Ws.toDouble()).toInt() + val r = ceil(t * n / this.Xs.toDouble()).toInt() * this.Ws + val e = ceil((t + 1) * n / this.Xs.toDouble()).toInt() * this.Ws + val s = n * this.Ws + val h = r * i / s + val u = e * i / s + val o = e - r + val a = if (1 == this.Xs) 0 else round(h + (u - h - o) * t / (this.Xs - 1).toDouble()).toInt() + return p.Rectangle(t = 0, i = a, n = this.un.width, r = o) + } +} + +object d { + + data class e(val resources: resources,val views: List) + + lateinit var urlResult: UrlResult + + //speedbinb.js?dmy=016301:formatted:8699 + private fun RS(t: resources, i: String): coord { + val n = (Regex("^([^:]+):(\\d+),(\\d+)\\+(\\d+),(\\d+)>(\\d+),(\\d+)\$").matchEntire(i) + ?: throw IllegalArgumentException("Invalid format for Image Transfer : $i")).groupValues + val r = n[1] + if (r !in t.getOwnPropertyNames()) + throw IllegalArgumentException("resid $r not found.") + return coord( + resid = r, xsrc = n[2].toInt(10), ysrc = n[3].toInt(10), width = n[4].toInt(10), + height = n[5].toInt(10), xdest = n[6].toInt(10), ydest = n[7].toInt(10) + ) + } + + //speedbinb.js?dmy=016301:formatted:8632 + private fun FS():e { + val t=urlResult.t + val n = + t.resources.copy(i = t.resources.i.copy(src =urlResult.originImagePath)) + return e(resources = n,views = t.views.map { it -> + formatview(width = it.width, height = it.height, coords = it.coords.map { + RS(t = n, i = it) + }, areas = it.areas) + }) + } + + //speedbinb.js?dmy=016301:formatted:8604 + fun Gs():n{ + val e= FS() + val u=e.views[0] + return n(width = u.width,height = u.height,transfers = listOf(transfer(index = 0,coords = u.coords))) + } + +} + + +class ImageLoader(val urlResult: UrlResult){ + + private val canvasHtml=createCanvas() + + private var imageHeight=0.0 + + //speedbinb.js?dmy=016301:formatted:8766 + private fun callback(t:un): List { + d.urlResult=urlResult + val n=d.Gs() +// console.info("Gs:") +// console.info(n) + val u=e(un=un(width = n.width,height = n.height)) + val s=n.transfers[0].coords + val h= mutableListOf() + repeat(u.Xs){ + val r=u.Us(t=it) +// console.info("r:") +// console.info(r) + val e= mutableListOf() + s.forEach { + t-> +// console.info("-------------------") +// console.info("t:") +// console.info(t) + val _i=p.Rectangle(t=t.xdest,i=t.ydest,n=t.width,r=t.height) +// console.info("p.Rectangle(i):") +// console.info(_i) + p.intersect(t=r,i=_i)?.let { + n-> +// console.info("p.Rectangle.intersect(n):") +// console.info(n) +// console.info("-------------------") + e.add(coord(resid = t.resid, + xsrc = t.xsrc+(n.left-t.xdest), + ysrc= t.ysrc + (n.top - t.ydest), + width= n.width, + height= n.height, + xdest= n.left - r.left, + ydest = n.top - r.top)) + + } + + } +// console.info("e.size=${e.size}") + h.add(n(width = r.width,height = r.height,transfers = listOf(transfer(index = 0,coords = e)))) + } +// console.info(h) + return h.toList() + } + + private fun createCanvas(): HTMLCanvasElement { + return document.createElement("canvas") as HTMLCanvasElement + } + + //speedbinb.js?dmy=016301:formatted:7976 + private fun us(t:n,image:Image){ + console.info("开始绘制漫画页") + val canvas:HTMLCanvasElement= createCanvas() + canvas.apply { + width=t.width + height=t.height + val ctx:CanvasRenderingContext2D= getContext("2d") as CanvasRenderingContext2D + ctx.clearRect(0.0, 0.0, width.toDouble(), height.toDouble()) + t.transfers.forEach { + i-> + i.coords.forEach { + t-> + ctx.drawImage(image = image,sx=t.xsrc.toDouble(),sy=t.ysrc.toDouble(),sw=t.width.toDouble(),sh=t.height.toDouble(), + dx=t.xdest.toDouble(),dy=t.ydest.toDouble(),dw=t.width.toDouble(),dh=t.height.toDouble()) + } + } + + } + canvasToBlob(t=canvas,{blob: Blob -> + console.info("blob.size:${blob.size}") + val i=URL.createObjectURL(blob=blob) + console.info("漫画URL:${i}") + Image().apply { + onload={event: Event -> + canvasHtml.let { + canvas-> + val ctx:CanvasRenderingContext2D= canvas.getContext("2d") as CanvasRenderingContext2D + ctx.drawImage(this,0.0,imageHeight,naturalWidth.toDouble(),naturalHeight.toDouble().apply { + imageHeight+=this + console.info("拼接图片imageHeight:${imageHeight}") + }) + } + } + src=i + } + }) + } + + fun create(){ + Image().apply { + onload={event: Event -> + console.info("拼接图片大小naturalWidth:${naturalWidth},naturalHeight:${naturalHeight}") + } + src=canvasHtml.toDataURL() + } + + } + + + //speedbinb.js?dmy=016301:formatted:3798 + fun canvasToBlob( + t:HTMLCanvasElement, callback:(t:Blob)->Unit, n:String="image/jpeg", r: Double =.9){ + val i=t.toDataURL(type=n,quality = r).split(",")[1] + console.info("url length:${i.length}") + val blobTransfer=w(t=i) +// console.info("w[i]:") +// console.info(blobTransfer) + val blob= Blob(blobParts=arrayOf(blobTransfer),options = BlobPropertyBag(type = n)) + console.info("blob size:${blob.size}") + callback(blob) + } + + //speedbinb.js?dmy=016301:formatted:3602 + fun _m(t: String): Array { + val i= arrayOf() + repeat(t.length){ + i[t[it].code]=it + } + return i + } + + val m=_m("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") + + //speedbinb.js?dmy=016301:formatted:3608 + fun w(t:String): Uint8Array { + val e=t.length + val s = t.slice(IntRange(e-2,e-1)).split("=").count() - 1 + val h = 3 * ((e + 3) / 4) - s + val u = Uint8Array(h) + var i=0 + var n=i + while (n +// val n=t(width = t.width,height = t.height) + us(t=t,image=it) + } + } + } + + //speedbinb.js?dmy=016301:formatted:7949 + private fun hs(): Promise { + return Promise(executor = { resolve, reject -> + val e=Image() +// e.crossOrigin="anonymous" + val t=urlResult.serverImagePath + e.onload={ + resolve(e) + } + e.onerror = { _: dynamic, _: String, _: Int, _: Int, _: Any? -> + reject(Error("Failed to load image. : $t")) + } + e.onabort = { + reject(Error("Failed to load image. : $t")) + } + e.src=t.apply { console.info("img path:${this}") } + }) + } +} + + diff --git a/src/jsMain/kotlin/welcome.kt b/src/jsMain/kotlin/welcome.kt new file mode 100644 index 0000000..e21dfea --- /dev/null +++ b/src/jsMain/kotlin/welcome.kt @@ -0,0 +1,153 @@ +import kotlinx.browser.document +import kotlinx.browser.window +import kotlinx.css.* +import kotlinx.html.InputType +import kotlinx.html.js.onChangeFunction +import kotlinx.html.js.onClickFunction +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import org.w3c.dom.HTMLInputElement +import org.w3c.dom.MessageEvent +import org.w3c.dom.WebSocket +import org.w3c.dom.events.Event +import org.w3c.fetch.RequestInit +import org.w3c.xhr.FormData +import react.RBuilder +import react.RComponent +import react.RProps +import react.RState +import react.dom.* +import styled.css +import styled.styledDiv +import styled.styledH1 +import styled.styledInput + +external interface WelcomeProps : RProps { + var webSocket:WebSocket +} + +data class WelcomeState(var url:String="",var result:String="",var percentage:kotlin.Float=0F) : RState + +fun Double.format(digits: Int): String = this.asDynamic().toFixed(digits) +fun Float.format(digits: Int): String = this.asDynamic().toFixed(digits) + +@OptIn(ExperimentalJsExport::class) +@JsExport +class Welcome(props: WelcomeProps) : RComponent(props) { + + init { + state=WelcomeState() + props.webSocket.onmessage={messageEvent: MessageEvent -> + when(val data=messageEvent.data){ + is String-> { + if(data.contains("ptimg-version")){ + val urlResult=Json.decodeFromString(data) + console.info("ptimg_version:${urlResult.t.ptimg_version}") + ImageLoader(urlResult = urlResult).apply { + rebuild() + create() + } + }else{ + val task=Json.decodeFromString(data) + state.result="解析进度:${task.percentage}%" + setState(state) + } + } + else->{ + console.info("unknow data:${data}") + } + } + } + } + + override fun RBuilder.render() { + + styledH1 { + css{ + textAlign=TextAlign.center + } + +"朴实无华的" + a { + attrs { + href="https://gammaplus.takeshobo.co.jp" + target="_blank" + } + +"漫画" + } + +"解析工具" + } + + styledDiv{ + css{ + textAlign=TextAlign.center + } + styledDiv { + css { + width = LinearDimension.fitContent + margin="0 auto" + } + styledInput { + css { + width = 100.pct + } + attrs{ + type=InputType.text + value = state.url + onChangeFunction = { event -> + (event.target as HTMLInputElement).let { + console.info(it.value) + state.url=it.value + setState( + state + ) + } + + } + } + } + styledDiv { + css{ + visibility=Visibility.hidden + height=0.px + paddingLeft=10.px + } + + state.url + } + } + + button { + attrs { + onClickFunction={ + + val formData=FormData() + formData.append("url",state.url) + window.fetch("/api/json", RequestInit(method = "post",body = formData)).then { + it.text() + }.then { + console.info(it) + } + } + } + +"开始解析" + } + + if(state.result!=""){ + styledDiv { + +state.result + } + button { + attrs { + onClickFunction={ + state.result="" + setState(state) + } + } + +"清空消息" + } + } + + + } + + } +} diff --git a/src/jsTest/kotlin/Test.kt b/src/jsTest/kotlin/Test.kt new file mode 100644 index 0000000..7461f57 --- /dev/null +++ b/src/jsTest/kotlin/Test.kt @@ -0,0 +1,45 @@ +import kotlinx.css.p +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.khronos.webgl.Uint8Array +import org.khronos.webgl.set + +import kotlin.test.Test + + +class JsTest { + + @Serializable + data class FF(@SerialName("ptimg-version") val c:String) + + @Test + fun testParse(){ + val c=Json.decodeFromString("{\"ptimg-version\":1,\"resources\":{\"i\":{\"src\":\"0012.jpg\",\"width\":908,\"height\":1264}},\"views\":[{\"width\":844,\"height\":1200,\"coords\":[\"i:4,4+106,150>420,1050\",\"i:118,4+106,150>530,450\",\"i:232,4+106,150>738,0\",\"i:346,4+106,150>526,750\",\"i:460,4+106,150>420,750\",\"i:574,4+106,150>106,300\",\"i:688,4+106,150>318,600\",\"i:802,4+102,150>742,600\",\"i:4,162+106,150>106,450\",\"i:118,162+106,150>526,1050\",\"i:232,162+106,150>0,900\",\"i:346,162+106,150>636,150\",\"i:460,162+102,150>424,900\",\"i:570,162+106,150>0,1050\",\"i:684,162+106,150>106,1050\",\"i:798,162+106,150>0,600\",\"i:4,320+106,150>738,1050\",\"i:118,320+106,150>106,150\",\"i:232,320+106,150>424,600\",\"i:346,320+106,150>420,300\",\"i:460,320+106,150>530,150\",\"i:574,320+106,150>424,0\",\"i:688,320+102,150>742,150\",\"i:798,320+106,150>0,450\",\"i:4,478+106,150>738,750\",\"i:118,478+106,150>632,1050\",\"i:232,478+106,150>106,900\",\"i:346,478+102,150>106,750\",\"i:456,478+106,150>106,600\",\"i:570,478+106,150>424,450\",\"i:684,478+106,150>212,900\",\"i:798,478+106,150>318,450\",\"i:4,636+106,150>632,300\",\"i:118,636+106,150>314,1050\",\"i:232,636+106,150>106,0\",\"i:346,636+106,150>318,0\",\"i:460,636+106,150>314,750\",\"i:574,636+102,150>636,0\",\"i:684,636+106,150>632,900\",\"i:798,636+106,150>738,900\",\"i:4,794+106,150>318,150\",\"i:118,794+106,150>530,600\",\"i:232,794+106,150>212,0\",\"i:346,794+106,150>212,450\",\"i:460,794+106,150>0,150\",\"i:574,794+102,150>636,450\",\"i:684,794+106,150>738,300\",\"i:798,794+106,150>0,750\",\"i:4,952+106,150>208,750\",\"i:118,952+106,150>212,150\",\"i:232,952+106,150>636,600\",\"i:346,952+106,150>212,300\",\"i:460,952+106,150>0,300\",\"i:574,952+102,150>212,1050\",\"i:684,952+106,150>526,900\",\"i:798,952+106,150>424,150\",\"i:4,1110+106,150>530,0\",\"i:118,1110+106,150>526,300\",\"i:232,1110+106,150>632,750\",\"i:346,1110+106,150>738,450\",\"i:460,1110+106,150>212,600\",\"i:574,1110+106,150>0,0\",\"i:688,1110+102,150>318,300\",\"i:798,1110+106,150>318,900\"]}]}") + println(c.ptimg_version) + println(c.resources.i) + println(c.views) + } + + + @Test + fun testRS(){ + + val f=Regex("^([^:]+):(\\d+),(\\d+)\\+(\\d+),(\\d+)>(\\d+),(\\d+)\$").matchEntire("i:574,4+106,150>106,600") + println(f?.groupValues) + } + + @Test + fun testUnitArray(){ + ImageLoader(UrlResult(originImagePath = "",serverImagePath = "", + t=t(ptimg_version = 0,resources = resources(i = i(src = "",width = 0,height = 0)),views = listOf()))) + .apply { + m.forEach { + println("m;${it}") + } + } + } + +} diff --git a/src/jvmMain/kotlin/plugins/HTTP.kt b/src/jvmMain/kotlin/plugins/HTTP.kt new file mode 100644 index 0000000..a276d95 --- /dev/null +++ b/src/jvmMain/kotlin/plugins/HTTP.kt @@ -0,0 +1,19 @@ +package cool.kirito.bili.live.server.plugins + +import io.ktor.http.* +import io.ktor.application.* +import io.ktor.features.* + +fun Application.configureHTTP() { + install(CORS) { + method(HttpMethod.Options) + method(HttpMethod.Put) + method(HttpMethod.Delete) + method(HttpMethod.Patch) + header(HttpHeaders.Authorization) + header("MyCustomHeader") + allowCredentials = true + anyHost() // @TODO: Don't do this in production if possible. Try to limit it. + } + +} diff --git a/src/jvmMain/kotlin/plugins/Monitoring.kt b/src/jvmMain/kotlin/plugins/Monitoring.kt new file mode 100644 index 0000000..b61a1d3 --- /dev/null +++ b/src/jvmMain/kotlin/plugins/Monitoring.kt @@ -0,0 +1,14 @@ +package cool.kirito.bili.live.server.plugins + +import io.ktor.features.* +import org.slf4j.event.* +import io.ktor.application.* +import io.ktor.request.* + +fun Application.configureMonitoring() { + install(CallLogging) { + level = Level.INFO + filter { call -> call.request.path().startsWith("/") } + } + +} diff --git a/src/jvmMain/kotlin/plugins/Routing.kt b/src/jvmMain/kotlin/plugins/Routing.kt new file mode 100644 index 0000000..2f234a6 --- /dev/null +++ b/src/jvmMain/kotlin/plugins/Routing.kt @@ -0,0 +1,157 @@ +import io.ktor.application.* +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.html.* +import io.ktor.http.* +import io.ktor.http.cio.websocket.* +import io.ktor.http.content.* +import io.ktor.request.* +import io.ktor.response.* +import io.ktor.routing.* +import io.ktor.websocket.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import kotlinx.html.HTML +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.io.File + + +const val website = "https://gammaplus.takeshobo.co.jp" + +var taskChannel = Channel() +val urlResultChannel = Channel() +val htmlChannel= Channel() + +fun Application.parse(){ + log.info("初始化解析任务") + + val uploadDir = environment.config.property("ktor.deployment.filePath").getString() + val filePath = environment.classLoader.getResource(uploadDir)?.path.apply { log.info("图片存储目录:${this}") }?:throw IllegalArgumentException("图片存储目录初始化失败") + + launch { + while (true){ + val resHtml= htmlChannel.receive() + launch { + log.info("开始解析:${resHtml.url}") + val client = HttpClient(CIO) + + val titlePrefix="title>" + val title=Regex("$titlePrefix[\\u4e00-\\u9fa5\\u0800-\\u4e00\\s\\d\\uff00-\\uffef]+").find(resHtml.html)?.value?.replace(titlePrefix,"")?:throw IllegalArgumentException("无法解析漫画标题") + val romajiPrefix="/manga" + val romajiTitle=Regex("$romajiPrefix/\\w+").find(resHtml.url)?.value?.replace(romajiPrefix,"")?:throw IllegalArgumentException("无法解析漫画罗马音标题") + log.info("解析漫画标题") + val imageDir= File(filePath,romajiTitle).apply { if(!exists()) mkdir() } + log.info("漫画图片存储到:${imageDir.absolutePath}") + + Regex("data/.*.json").findAll(resHtml.html) + .apply { + withIndex() +//TODO .forEach { + .first().let { + log.info("开始解析:${it.value.value}") + val urlPath = "${resHtml.url}/${it.value.value}" + val jsonRes: HttpResponse = client.get(urlPath) + if (jsonRes.status == HttpStatusCode.OK) { + log.info("url:${urlPath} request OK") + val t:t=Json.decodeFromString(jsonRes.readText()) + val originImagePath="/data/${t.resources.i.src}" + val imageUrl="${resHtml.url}${originImagePath}" + val imgRes:HttpResponse=client.get(imageUrl) + if(imgRes.status==HttpStatusCode.OK&&imgRes.contentType()==ContentType.Image.JPEG){ + val filename="${it.index}.jpg" + val file=File(imageDir,filename).apply { + writeBytes(imgRes.readBytes()) + } + log.info("存储漫画图片:${file.absolutePath}") + val serverImagePath="/${uploadDir}/${romajiTitle}/${filename}" + log.info("serverImagePath:${serverImagePath}") + urlResultChannel.send(UrlResult(originImagePath = originImagePath, t = t, + serverImagePath = serverImagePath)) + }else{ + log.warn("image url:${imageUrl} 响应码${jsonRes.status} 响应类型${imgRes.contentType()}") + } + } else { + log.warn("json url:${urlPath} 响应码:${jsonRes.status}") + } + taskChannel.send(ParseTask(total = count(), finish = it.index + 1,percentage = if(count()==it.index+1) 100F else String.format("%.2f",(it.index+1)*100F/count()).toFloat())) + } + } + client.close() + }.apply { + invokeOnCompletion { + log.info("${resHtml.url}解析完成") + } + } + } + } + +} + +fun Application.configureRouting() { + + routing { + val uploadDir=environment.config.property("ktor.deployment.filePath").getString() + static(uploadDir) { + resources(uploadDir) + } + + static("/static") { + resources() + } + + webSocket(websocketPath) { // websocketSession + launch { + while (true) { + val urlResult = urlResultChannel.receive() + outgoing.send(Frame.Text(Json.encodeToString(urlResult))) + } + } + launch { + while (true){ + val task=taskChannel.receive() + outgoing.send(Frame.Text(Json.encodeToString(task))) + } + } + for (frame in incoming) { + when (frame) { + is Frame.Text -> { + val text = frame.readText() + outgoing.send(Frame.Text("YOU SAID: $text")) + if (text.equals("bye", ignoreCase = true)) { + close(CloseReason(CloseReason.Codes.NORMAL, "Client said BYE")) + } + } + } + } + } + + get("/") { + call.respondHtml(HttpStatusCode.OK, HTML::index) + } + + post("/api/json") { + val formParameters = call.receiveParameters() + val urlParam = formParameters["url"] ?: "" + log.info("urlParam:${urlParam}") + if (urlParam.startsWith(website)) { + val client = HttpClient(CIO) + val response: HttpResponse = client.get(urlParam) + if (response.status == HttpStatusCode.OK) { + val resHtml = response.readText() + htmlChannel.send(UrlParam(url = urlParam,html = resHtml)) + call.respond(MessageResponse(message = "开始执行解析任务")) + } else { + log.warn("http code:${response.status}") + call.respond(MessageResponse(message = "请求失败")) + } + client.close() + } else { + call.respond(MessageResponse(message = "${urlParam}:非法漫画地址")) + } + } + } +} diff --git a/src/jvmMain/kotlin/plugins/Serialization.kt b/src/jvmMain/kotlin/plugins/Serialization.kt new file mode 100644 index 0000000..423b0ad --- /dev/null +++ b/src/jvmMain/kotlin/plugins/Serialization.kt @@ -0,0 +1,11 @@ +package cool.kirito.bili.live.server.plugins + +import io.ktor.features.* +import io.ktor.application.* +import io.ktor.serialization.* + +fun Application.configureSerialization() { + install(ContentNegotiation) { + json() + } +} diff --git a/src/jvmMain/kotlin/plugins/WebSockets.kt b/src/jvmMain/kotlin/plugins/WebSockets.kt new file mode 100644 index 0000000..b7d224b --- /dev/null +++ b/src/jvmMain/kotlin/plugins/WebSockets.kt @@ -0,0 +1,8 @@ +package plugins + +import io.ktor.application.* +import io.ktor.websocket.* + +fun Application.configureWebSockets() { + install(WebSockets) +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/server.kt b/src/jvmMain/kotlin/server.kt new file mode 100644 index 0000000..0bcb469 --- /dev/null +++ b/src/jvmMain/kotlin/server.kt @@ -0,0 +1,47 @@ +import cool.kirito.bili.live.server.plugins.configureHTTP +import cool.kirito.bili.live.server.plugins.configureMonitoring +import cool.kirito.bili.live.server.plugins.configureSerialization +import io.ktor.application.* +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.features.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.features.* +import io.ktor.html.respondHtml +import io.ktor.http.* +import io.ktor.routing.get +import io.ktor.routing.routing +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.http.content.resources +import io.ktor.http.content.static +import io.ktor.response.* +import io.ktor.serialization.* +import kotlinx.html.* +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import plugins.configureWebSockets + +fun HTML.index() { + head { + title(websiteTitle) + } + body { + div { + id = "root" + } + script(src = "/static/js.js") {} + } +} + +fun main(args: Array): Unit = io.ktor.server.netty.EngineMain.main(args) + +fun Application.module(testing: Boolean = false) { + configureWebSockets() + configureRouting() + configureHTTP() + configureMonitoring() + configureSerialization() + parse() +} \ No newline at end of file diff --git a/src/jvmMain/resources/application.conf b/src/jvmMain/resources/application.conf new file mode 100644 index 0000000..a489825 --- /dev/null +++ b/src/jvmMain/resources/application.conf @@ -0,0 +1,22 @@ +ktor { + #开发模式 + development = true + deployment { + port = 8080 + port = ${?PORT} + mysql = { + username = root + password = 123456 + jdbcUrl = "jdbc:mysql://localhost:3306/csams?serverTimezone=Asia/Shanghai" + driverClassName = "com.mysql.cj.jdbc.Driver" + } + filePath = static/image + #免重启自动重载classes目录 + watch = [ classes ] + } + application { + modules = [ + ServerKt.module, + ] + } +} diff --git a/src/jvmMain/resources/logback.xml b/src/jvmMain/resources/logback.xml new file mode 100644 index 0000000..8c82f8b --- /dev/null +++ b/src/jvmMain/resources/logback.xml @@ -0,0 +1,14 @@ + + + + %d{YYYY-MM-dd HH:mm:ss} [%thread] %-5level - %C:%L - %msg%n + UTF-8 + + + + + + + + + diff --git a/src/jvmMain/resources/static/image/.gitkeep b/src/jvmMain/resources/static/image/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/jvmTest/kotlin/ApplicationTest.kt b/src/jvmTest/kotlin/ApplicationTest.kt new file mode 100644 index 0000000..33e9da4 --- /dev/null +++ b/src/jvmTest/kotlin/ApplicationTest.kt @@ -0,0 +1,78 @@ +import cool.kirito.bili.live.server.plugins.configureSerialization +import io.ktor.http.* + +import io.ktor.config.* +import kotlin.test.* +import io.ktor.server.testing.* +import kotlinx.coroutines.channels.Channel +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import plugins.configureWebSockets + + +class ApplicationTest { + @Test + fun testRoot() { + withTestApplication({ + configureWebSockets() + configureRouting() + configureSerialization() + }) { + handleRequest(HttpMethod.Post, "/").apply { +// assertEquals(HttpStatusCode.OK, response.status()) + environment.log.info("返回!!!!!!!!!!!!!!!:${response.content}") + } + } + } + + @Test + fun testParse(){ + withTestApplication({ + configureWebSockets() + configureRouting() + configureSerialization() + }) { + handleRequest(HttpMethod.Post,"/api/json"){ + addHeader(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString()) + setBody("url=https://gammaplus.takeshobo.co.jp/manga/kobito_recipe/_files/02_1/") + } + .apply { + assertEquals(HttpStatusCode.OK,response.status()) + environment.log.info("json:${response.content}") + } + } + + } + + @Test + fun testRegex(){ + val s="
\n" + + "\t\t\t
\n" + + "\t\t\t
\n" + + "\t\t\t
\n" + + "\t\t\t
\n" + + "\t\t\t
" + + Regex("data/.*.json").findAll(s).forEach { + println(it.value) + } + } + + @Test + fun testNumber(){ + println(String.format("%.2f",3*100/22F)) + } + + @Test + fun testUrl(){ + println(Regex("/manga/\\w+") + .find("https://gammaplus.takeshobo.co.jp/manga/kobito_recipe/_files/02_1/")?.value) + } + + @Test + fun testSlice(){ + val t="sdfsdfsdfsd==" + println(t.slice(IntRange(t.length-2,t.length-1))) + } +} \ No newline at end of file