commit
cbc9a26b7d
@ -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 |
@ -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) |
@ -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<KotlinWebpack>("jsBrowserProductionWebpack") { |
||||
outputFileName = "js.js" |
||||
} |
||||
|
||||
tasks.getByName<Jar>("jvmJar") { |
||||
dependsOn(tasks.getByName("jsBrowserProductionWebpack")) |
||||
val jsBrowserProductionWebpack = tasks.getByName<KotlinWebpack>("jsBrowserProductionWebpack") |
||||
from(File(jsBrowserProductionWebpack.destinationDirectory, jsBrowserProductionWebpack.outputFileName)) |
||||
} |
||||
|
||||
tasks.getByName<JavaExec>("run") { |
||||
dependsOn(tasks.getByName<Jar>("jvmJar")) |
||||
classpath(tasks.getByName<Jar>("jvmJar")) |
||||
} |
||||
|
||||
tasks.withType<KotlinCompile> { |
||||
kotlinOptions { |
||||
freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn") |
||||
} |
||||
} |
||||
|
||||
tasks.withType<ProcessResources>{ |
||||
duplicatesStrategy = DuplicatesStrategy.INCLUDE |
||||
} |
@ -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 |
@ -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 |
@ -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" "$@" |
@ -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 |
@ -0,0 +1,3 @@ |
||||
|
||||
rootProject.name = "manga" |
||||
|
@ -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<String>, val areas: List<area>? = null) |
||||
|
||||
@Serializable |
||||
data class t(@SerialName("ptimg-version") val ptimg_version: Int, |
||||
val resources: resources, val views: List<view>) |
||||
|
||||
@Serializable |
||||
data class ApiResponse<T>(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="朴实无华的漫画解析工具" |
@ -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 |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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<coord>, val areas: List<area>? = null) |
||||
|
||||
data class transfer(val index:Int,val coords: List<coord>) |
||||
|
||||
data class n(val width: Int,val height: Int,val transfers:List<transfer>) |
||||
|
||||
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<formatview>) |
||||
|
||||
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<n> { |
||||
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<n>() |
||||
repeat(u.Xs){ |
||||
val r=u.Us(t=it) |
||||
// console.info("r:") |
||||
// console.info(r) |
||||
val e= mutableListOf<coord>() |
||||
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<Int> { |
||||
val i= arrayOf<Int>() |
||||
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<e){ |
||||
val r=m[t[n].code] shl 18 or (m[t[n + 1].code] shl 12) or (m[t[n + 2].code] shl 6) or m[t[n + 3].code] |
||||
u[i]=(r shr 16 and 255).toByte() |
||||
u[i+1]=(r shr 8 and 255).toByte() |
||||
u[i+2]=(255 and r).toByte() |
||||
i+=3 |
||||
n+=4 |
||||
} |
||||
return u |
||||
} |
||||
|
||||
//speedbinb.js?dmy=016301:formatted:8020 |
||||
fun rebuild(){ |
||||
hs().then { |
||||
console.info("image(naturalWidth:${it.naturalWidth},naturalHeight=${it.naturalHeight})") |
||||
val f=un(width = it.naturalWidth,height = it.naturalHeight) |
||||
callback(t=f).map { |
||||
t-> |
||||
// val n=t(width = t.width,height = t.height) |
||||
us(t=t,image=it) |
||||
} |
||||
} |
||||
} |
||||
|
||||
//speedbinb.js?dmy=016301:formatted:7949 |
||||
private fun hs(): Promise<Image> { |
||||
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}") } |
||||
}) |
||||
} |
||||
} |
||||
|
||||
|
@ -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<WelcomeProps, WelcomeState>(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<UrlResult>(data) |
||||
console.info("ptimg_version:${urlResult.t.ptimg_version}") |
||||
ImageLoader(urlResult = urlResult).apply { |
||||
rebuild() |
||||
create() |
||||
} |
||||
}else{ |
||||
val task=Json.decodeFromString<ParseTask>(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) |
||||
} |
||||
} |
||||
+"清空消息" |
||||
} |
||||
} |
||||
|
||||
|
||||
} |
||||
|
||||
} |
||||
} |
@ -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<t>("{\"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}") |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
@ -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. |
||||
} |
||||
|
||||
} |
@ -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("/") } |
||||
} |
||||
|
||||
} |
@ -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<ParseTask>() |
||||
val urlResultChannel = Channel<UrlResult>() |
||||
val htmlChannel= Channel<UrlParam>() |
||||
|
||||
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}:非法漫画地址")) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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() |
||||
} |
||||
} |
@ -0,0 +1,8 @@ |
||||
package plugins |
||||
|
||||
import io.ktor.application.* |
||||
import io.ktor.websocket.* |
||||
|
||||
fun Application.configureWebSockets() { |
||||
install(WebSockets) |
||||
} |
@ -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<String>): Unit = io.ktor.server.netty.EngineMain.main(args) |
||||
|
||||
fun Application.module(testing: Boolean = false) { |
||||
configureWebSockets() |
||||
configureRouting() |
||||
configureHTTP() |
||||
configureMonitoring() |
||||
configureSerialization() |
||||
parse() |
||||
} |
@ -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, |
||||
] |
||||
} |
||||
} |
@ -0,0 +1,14 @@ |
||||
<configuration> |
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> |
||||
<encoder> |
||||
<pattern>%d{YYYY-MM-dd HH:mm:ss} [%thread] %-5level - %C:%L - %msg%n</pattern> |
||||
<charset>UTF-8</charset> |
||||
</encoder> |
||||
</appender> |
||||
<root level="trace"> |
||||
<appender-ref ref="STDOUT"/> |
||||
</root> |
||||
<logger name="org.eclipse.jetty" level="INFO"/> |
||||
<logger name="io.netty" level="INFO"/> |
||||
<logger name="com.zaxxer.hikari" level="INFO"/> |
||||
</configuration> |
@ -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="<div id=\"content\" class=\"pages ptbinb-container\" data-binbsp-direction=\"rtl\" data-binbsp-toc=\"toc\" data-binbsp-recommend=\"../recommend_01/index.html#more[next] ../recommend2/[next]\">\n" + |
||||
"\t\t\t<div data-ptimg=\"data/0001.ptimg.json\" data-binbsp-spread=\"center\" data-binbsp-anchors=\"L_book_000\"></div>\n" + |
||||
"\t\t\t<div data-ptimg=\"data/0002.ptimg.json\" data-binbsp-spread=\"right\"></div>\n" + |
||||
"\t\t\t<div data-ptimg=\"data/0003.ptimg.json\" data-binbsp-spread=\"left\"></div>\n" + |
||||
"\t\t\t<div data-ptimg=\"data/0004.ptimg.json\" data-binbsp-spread=\"right\"></div>\n" + |
||||
"\t\t\t<div data-ptimg=\"data/0005.ptimg.json\" data-binbsp-spread=\"left\"></div>" |
||||
|
||||
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))) |
||||
} |
||||
} |
Loading…
Reference in new issue