You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

346 lines
15 KiB

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.util.*
import io.ktor.websocket.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
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
import java.io.FileOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
const val website = "https://gammaplus.takeshobo.co.jp"
//漫画url
val urlChannel = Channel<String>()
//当前漫画解析任务
var currentJob: Job? = null
//合成图片打包数据
val imageDataChannel = Channel<ImageFileData>()
val webSocketChannel = Channel<TestData>()
//图片JSON解析
fun Application.parse() {
launch {
log.info("初始化解析任务")
val uploadDir = environment.config.property("ktor.deployment.filePath").getString()
val filePath = environment.classLoader.getResource(uploadDir)?.path.apply { log.info("图片存储目录:${this}") }
?: throw IllegalArgumentException("图片存储目录初始化失败")
while (true) {
val urlParam = urlChannel.receive()
//
// currentJob?.let {
// if(it.isActive) this.cancel()
// }
launch {
log.info("开始解析:${urlParam}")
val client = HttpClient(CIO)
val response: HttpResponse = client.get(urlParam)
if (response.status == HttpStatusCode.OK) {
val resHtml = response.readText()
// log.info("resHtml:\n$resHtml")
val titlePrefix = "title>"
val title =
Regex("$titlePrefix[\\u4e00-\\u9fa5\\u0800-\\u4e00\\s\\d\\uff00-\\uffef]+").find(resHtml)?.value?.replace(
titlePrefix,
""
) ?: throw IllegalArgumentException("无法解析漫画标题")
val romajiPrefix = "/manga/"
val romajiTitle = Regex("$romajiPrefix\\w+").find(resHtml)?.value?.replace(romajiPrefix, "")
?: throw IllegalArgumentException("无法解析漫画罗马音标题")
log.info("漫画标题title=$title,romajiTitle=$romajiTitle")
webSocketChannel.send(MangaInfo(href = urlParam, title = title, romajiTitle = romajiTitle))
val imageDir = File(filePath, romajiTitle).apply { if (!exists()) mkdir() }
log.info("漫画图片存储到:${imageDir.absolutePath}")
Regex("data/.*.json").findAll(resHtml)
.apply {
withIndex().forEach {
log.info("开始解析:${it.value.value}")
val urlPath = "${urlParam}/${it.value.value}"
val jsonRes: HttpResponse = client.get(urlPath)
if (jsonRes.status == HttpStatusCode.OK) {
log.info("$urlPath 请求成功")
val t: t = Json.decodeFromString(jsonRes.readText())
val originImagePath = "/data/${t.resources.i.src}"
val imageUrl = "${urlParam}${originImagePath}"
val imgRes: HttpResponse = client.get(imageUrl)
if (imgRes.status == HttpStatusCode.OK && imgRes.contentType() == ContentType.Image.JPEG) {
log.info("$imageUrl 请求成功")
val filename = "${it.index + 1}.jpg"
val file = File(imageDir, filename).apply {
writeBytes(imgRes.readBytes())
}
log.info("存储漫画图片:${file.absolutePath}")
val serverImagePath = "/${uploadDir}/${romajiTitle}/${filename}"
log.info("serverImagePath:${serverImagePath}")
webSocketChannel.send(
UrlResult(
originImagePath = originImagePath,
t = t,
serverImagePath = serverImagePath,
romajiTitle = romajiTitle,
filename = filename,
isLast = count() == it.index + 1
)
)
} else {
log.warn("image url:${imageUrl} 响应码${jsonRes.status} 响应类型${imgRes.contentType()}")
}
} else {
log.warn("json url:${urlPath} 响应码:${jsonRes.status}")
}
webSocketChannel.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()
)
)
}
}
} else {
log.warn("http code:${response.status}")
webSocketChannel.send(StringResult(message = "${urlParam}解析失败"))
}
client.close()
}.apply {
currentJob = this
invokeOnCompletion {
log.info("${urlParam}解析完成")
}
}
}
}
launch {
log.info("初始化合成图片任务")
val zipDir = environment.config.property("ktor.deployment.zipPath").getString()
val zipPath = environment.classLoader.getResource(zipDir)?.path.apply { log.info("图片压缩目录:${this}") }
?: throw IllegalArgumentException("图片压缩目录初始化失败")
while (true) {
val imageFileData = imageDataChannel.receive()
File(zipPath, imageFileData.romajiTitle).apply {
imageFileData.data.apply {
launch(Dispatchers.IO) {
File(zipPath, "${imageFileData.romajiTitle}.zip").apply {
ZipOutputStream(FileOutputStream(this)).use { out ->
forEach { file ->
out.putNextEntry(
ZipEntry(
file.originalFileName ?: throw IllegalArgumentException("无法获取图片名字")
)
)
out.write(file.streamProvider().readBytes())
out.closeEntry()
}
}
webSocketChannel.send(
ZipResult(
zipUrl = "/${zipDir}/${name}",
name = name,
size = String.format("%.2fMB", length() / 1024 / 1024F)
)
)
webSocketChannel.send(StringResult(message = "打包任务完成"))
}
}
}
}
}
}
}
fun createFrameText(message: String): Frame.Text {
return Frame.Text(Json.encodeToString(WebSocketServer(body = message, dataType = WebSocketResType.Text.name)))
}
fun Application.configureRouting() {
routing {
val uploadDir = environment.config.property("ktor.deployment.filePath").getString()
static(uploadDir) {
resources(uploadDir)
}
val zipDir = environment.config.property("ktor.deployment.zipPath").getString()
static(zipDir) {
resources(zipDir)
}
static("/static") {
resources()
}
webSocket(Api.websocketPath) { // websocketSession
launch {
while (true) {
when (val data = webSocketChannel.receive()) {
is StringResult -> outgoing.send(createFrameText(data.message))
is UrlResult -> outgoing.send(
Frame.Text(
Json.encodeToString(
WebSocketServer(
body = data,
dataType = WebSocketResType.Image.name
)
)
)
)
is MangaInfo -> outgoing.send(
Frame.Text(
Json.encodeToString(
WebSocketServer(
body = data,
dataType = WebSocketResType.Manga.name
)
)
)
)
is ZipResult -> outgoing.send(
Frame.Text(
Json.encodeToString(
WebSocketServer(
body = data,
dataType = WebSocketResType.Zip.name
)
)
)
)
is ParseTask -> outgoing.send(
Frame.Text(
Json.encodeToString(
WebSocketServer(
body = data,
dataType = WebSocketResType.Task.name
)
)
)
)
}
}
}
for (frame in incoming) {
when (frame) {
is Frame.Text -> {
val input = frame.readText()
try {
val webSocketMessage: WebSocketClient = Json.decodeFromString(input)
when (webSocketMessage.command) {
WebSocketClientCommand.Heart.name -> {
outgoing.send(
Frame.Text(
Json.encodeToString(
WebSocketServer(dataType = WebSocketResType.Heart.name, body = "心跳返回")
)
)
)
}
WebSocketClientCommand.Cancel.name -> {
if (currentJob?.isActive == true) {
currentJob?.cancel()
outgoing.send(
Frame.Text(
Json.encodeToString(
WebSocketServer(
dataType = WebSocketResType.Cancel.name,
body = true
)
)
)
)
} else {
outgoing.send(createFrameText(message = "当前服务器没有正在运行的解析任务"))
}
}
else -> outgoing.send(createFrameText(message = "未知命令:${webSocketMessage.command}"))
}
} catch (e: Exception) {
log.error(e)
outgoing.send(Frame.Text("非法命令:${input}"))
}
}
is Frame.Close -> {
currentJob?.cancel()
log.info("取消解析任务")
}
else -> log.warn("无法处理${frame.frameType}类型消息")
}
}
}
get("/") {
call.respondHtml(HttpStatusCode.OK, HTML::index)
}
//打包漫画图片
post(Api.IMAGE_API) {
val d = call.receiveMultipart()
d.readAllParts().apply {
var romajiTitle = ""
filterIsInstance<PartData.FormItem>().forEach {
if (it.name == "romajiTitle") romajiTitle = it.value
}
val fileList = filterIsInstance<PartData.FileItem>()
if (romajiTitle.isNotEmpty() && fileList.isNotEmpty()) {
launch {
imageDataChannel.send(ImageFileData(romajiTitle = romajiTitle, data = fileList))
}
call.respond(MessageResponse(message = "初始化打包任务。。。"))
} else {
log.warn("无法打包漫画图片,参数不合法[romajiTitle:${romajiTitle},fileList=${fileList}]")
call.respond(MessageResponse(message = "无法打包漫画图片,请联系管理员"))
}
}
}
//解析漫画图片数据
post(Api.JSON_API) {
val formParameters = call.receiveParameters()
val urlParam = formParameters["url"] ?: ""
log.info("urlParam:${urlParam}")
if (urlParam.startsWith(website)) {
launch {
urlChannel.send(urlParam.let { if (it.endsWith("/")) it else "$it/" })
}
call.respond(MessageResponse(message = "初始化解析任务。。。"))
} else {
call.respond(MessageResponse(message = "${urlParam}:非法漫画地址"))
}
}
}
}