mirror of
https://github.com/dev-claw/vripper-project.git
synced 2026-08-19 08:35:41 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0108b670c5 | ||
|
|
378d493e2a | ||
|
|
00468b7ed3 | ||
|
|
0978ff37b0 | ||
|
|
5e52af0e00 | ||
|
|
0dec4bd3f3 | ||
|
|
bb053ca314 | ||
|
|
681ee81d1e | ||
|
|
bd027691b2 | ||
|
|
419f7e2ca7 | ||
|
|
123ee14dd6 | ||
|
|
a549105f7a | ||
|
|
8d1d74c5e8 | ||
|
|
eec3d5d75c | ||
|
|
3f5127cbe3 | ||
|
|
b28fbb98c2 | ||
|
|
c806eb9d25 | ||
|
|
4ba422eee1 | ||
|
|
e80d704b03 | ||
|
|
562c36bf28 | ||
|
|
c3d07a8503 | ||
|
|
30c9c9f24d | ||
|
|
ecf74b7b00 |
@@ -8,7 +8,7 @@ jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-13, macos-latest ]
|
||||
os: [ ubuntu-latest, windows-latest, macos-15-intel, macos-latest ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -105,8 +105,8 @@ jobs:
|
||||
jpackage --app-version ${{ github.event.release.tag_name }} "@jpackage.cfg" "@jpackage-macos.cfg" --resource-dir resources --type dmg
|
||||
mv dist/VRipper-${{ github.event.release.tag_name }}.pkg dist/vripper-macos-${{ github.event.release.tag_name }}.arm64.pkg
|
||||
mv dist/VRipper-${{ github.event.release.tag_name }}.dmg dist/vripper-macos-${{ github.event.release.tag_name }}.arm64.dmg
|
||||
|
||||
- if: matrix.os == 'macos-13'
|
||||
|
||||
- if: matrix.os == 'macos-15-intel'
|
||||
name: Package for macOS(x86_64)
|
||||
run: |
|
||||
cd jpackage
|
||||
@@ -142,8 +142,8 @@ jobs:
|
||||
directory: 'jpackage/dist'
|
||||
path: 'VRipper.app'
|
||||
filename: 'vripper-macos-portable-${{ github.event.release.tag_name }}.arm64.zip'
|
||||
|
||||
- if: matrix.os == 'macos-13'
|
||||
|
||||
- if: matrix.os == 'macos-15-intel'
|
||||
name: Zip macOS(x86_64) portable
|
||||
uses: thedoctor0/zip-release@0.7.1
|
||||
with:
|
||||
@@ -179,7 +179,7 @@ jobs:
|
||||
jpackage/dist/vripper-macos-${{ github.event.release.tag_name }}.arm64.dmg
|
||||
jpackage/dist/vripper-macos-portable-${{ github.event.release.tag_name }}.arm64.zip
|
||||
|
||||
- if: matrix.os == 'macos-13'
|
||||
- if: matrix.os == 'macos-15-intel'
|
||||
name: Release packages for macOS(x86_64)
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
|
||||
@@ -39,7 +39,7 @@ The following hosts are supported:
|
||||
|
||||
<img src="https://github.com/stashapp/stash/raw/develop/docs/readme_assets/windows_logo.svg" width="100%" height="75"> Windows | <img src="https://github.com/stashapp/stash/raw/develop/docs/readme_assets/mac_logo.svg" width="100%" height="75"> macOS (Intel) | <img src="https://github.com/stashapp/stash/raw/develop/docs/readme_assets/mac_logo.svg" width="100%" height="75"> macOS (Apple silicon) | <img src="https://github.com/stashapp/stash/raw/develop/docs/readme_assets/linux_logo.svg" width="100%" height="75"> Linux | <img src="https://images.vexels.com/media/users/3/166401/isolated/preview/b82aa7ac3f736dd78570dd3fa3fa9e24-java-programming-language-icon-by-vexels.png" width="100%" height="75"> Java
|
||||
:---:|:---:|:---:|:---:|:---:
|
||||
[Installer (EXE)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-windows-installer-6.5.8.exe) <br /> [Installer (MSI)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-windows-installer-6.5.8.msi) <br /> [Portable (ZIP)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-windows-portable-6.5.8.zip) | [Installer (DMG)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-macos-6.5.8.x86_64.dmg) <br /> [Installer (PKG)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-macos-6.5.8.x86_64.pkg) <br /> [Portable (ZIP)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-macos-portable-6.5.8.x86_64.zip) | [Installer (DMG)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-macos-6.5.8.arm64.dmg) <br /> [Installer (PKG)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-macos-6.5.8.arm64.pkg) <br /> [Portable (ZIP)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-macos-portable-6.5.8.arm64.zip) | [Linux (amd64) (DEB)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-linux-6.5.3_amd64.deb) <br /> [Linux (x86_64) (RPM)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-linux-6.5.8.x86_64.rpm) <br /> [Portable (ZIP)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-linux-portable-6.5.8.zip) | [Java GUI (noarch)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-noarch-gui-6.5.8.jar) <br /> [Java Web (noarch)](https://github.com/dev-claw/vripper-project/releases/download/6.5.8/vripper-noarch-web-6.5.8.jar)
|
||||
[Installer (EXE)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-windows-installer-6.10.0.exe) <br /> [Installer (MSI)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-windows-installer-6.10.0.msi) <br /> [Portable (ZIP)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-windows-portable-6.10.0.zip) | [Installer (DMG)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-macos-6.10.0.x86_64.dmg) <br /> [Installer (PKG)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-macos-6.10.0.x86_64.pkg) <br /> [Portable (ZIP)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-macos-portable-6.10.0.x86_64.zip) | [Installer (DMG)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-macos-6.10.0.arm64.dmg) <br /> [Installer (PKG)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-macos-6.10.0.arm64.pkg) <br /> [Portable (ZIP)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-macos-portable-6.10.0.arm64.zip) | [Linux (amd64) (DEB)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-linux-6.5.3_amd64.deb) <br /> [Linux (x86_64) (RPM)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-linux-6.10.0.x86_64.rpm) <br /> [Portable (ZIP)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-linux-portable-6.10.0.zip) | [Java GUI (noarch)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-noarch-gui-6.10.0.jar) <br /> [Java Web (noarch)](https://github.com/dev-claw/vripper-project/releases/download/6.10.0/vripper-noarch-web-6.10.0.jar)
|
||||
|
||||
Source code and previous versions are available on
|
||||
the [Releases page](https://github.com/dev-claw/vripper-project/releases).
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<revision>6.6.3</revision>
|
||||
<app-version>6.6.3-alpha</app-version>
|
||||
<revision>6.10.0</revision>
|
||||
<app-version>6.10.0</app-version>
|
||||
<kotlin.version>2.2.20</kotlin.version>
|
||||
<slf4j.version>2.0.16</slf4j.version>
|
||||
<logback.version>1.5.12</logback.version>
|
||||
|
||||
@@ -76,7 +76,7 @@ val coreModule = module {
|
||||
MetadataService(get(), get())
|
||||
}
|
||||
single {
|
||||
AcidimgHost(get())
|
||||
AcidimgHost()
|
||||
} bind Host::class
|
||||
single {
|
||||
DPicMeHost()
|
||||
|
||||
@@ -2,7 +2,6 @@ package me.vripper.host
|
||||
|
||||
import me.vripper.exception.HostException
|
||||
import me.vripper.exception.XpathException
|
||||
import me.vripper.services.HTTPService
|
||||
import me.vripper.services.download.ImageDownloadRunnable.Context
|
||||
import me.vripper.utilities.HtmlUtils
|
||||
import me.vripper.utilities.LoggerDelegate
|
||||
@@ -12,9 +11,7 @@ import org.apache.hc.client5.http.entity.UrlEncodedFormEntity
|
||||
import org.apache.hc.core5.http.message.BasicNameValuePair
|
||||
import org.w3c.dom.Node
|
||||
|
||||
internal class AcidimgHost(
|
||||
private val httpService: HTTPService,
|
||||
) : Host("acidimg.cc", 0) {
|
||||
internal class AcidimgHost : Host("acidimg.cc", 0) {
|
||||
private val log by LoggerDelegate()
|
||||
|
||||
@Throws(HostException::class)
|
||||
|
||||
@@ -31,7 +31,7 @@ internal abstract class Host(
|
||||
) : KoinComponent {
|
||||
private val log by LoggerDelegate()
|
||||
|
||||
private val httpService: HTTPService by inject()
|
||||
protected val httpService: HTTPService by inject()
|
||||
private val dataAccessService: DataAccessService by inject()
|
||||
private val downloadSpeedService: DownloadSpeedService by inject()
|
||||
|
||||
@@ -61,7 +61,7 @@ internal abstract class Host(
|
||||
val imageMimeType = getImageMimeType(headers)
|
||||
val downloadedImage = if (imageMimeType != null) {
|
||||
// a direct link, awesome
|
||||
val downloadedImage = fetch(context.imageEntity.url, context.imageEntity.url, context) {
|
||||
val downloadedImage = fetch(context.imageEntity.url, context) {
|
||||
handleImageDownload(it, context)
|
||||
}
|
||||
DownloadedImage(getDefaultImageName(context.imageEntity.url), downloadedImage.first, downloadedImage.second)
|
||||
@@ -84,7 +84,7 @@ internal abstract class Host(
|
||||
private fun downloadByHost(context: Context): DownloadedImage {
|
||||
val resolvedImage = resolve(context)
|
||||
val downloadImage: Pair<Path, ImageMimeType> =
|
||||
fetch(resolvedImage.second, context.imageEntity.url, context) {
|
||||
fetch(resolvedImage.second, context) {
|
||||
handleImageDownload(it, context)
|
||||
}
|
||||
return DownloadedImage(resolvedImage.first, downloadImage.first, downloadImage.second)
|
||||
@@ -152,6 +152,7 @@ internal abstract class Host(
|
||||
|
||||
fun head(context: Context): Array<Header> {
|
||||
val httpHead = HttpHead(context.imageEntity.url).also {
|
||||
it.addHeader("Referer", "https://vipergirls.to/")
|
||||
it.setAbsoluteRequestUri(true)
|
||||
context.requests.add(it)
|
||||
}
|
||||
@@ -169,10 +170,10 @@ internal abstract class Host(
|
||||
|
||||
fun <T> fetch(
|
||||
url: String,
|
||||
referer: String,
|
||||
context: Context,
|
||||
transformer: (ClassicHttpResponse) -> T
|
||||
): T {
|
||||
val referer = context.headers["Referer"] ?: "https://vipergirls.to/"
|
||||
val httpGet =
|
||||
HttpGet(url).also {
|
||||
it.addHeader("Referer", referer)
|
||||
@@ -191,7 +192,7 @@ internal abstract class Host(
|
||||
url: String,
|
||||
context: Context
|
||||
): Document {
|
||||
return fetch(url, url, context) {
|
||||
return fetch(url, context) {
|
||||
HtmlUtils.clean(it.entity.content)
|
||||
}.also {
|
||||
if (log.isDebugEnabled) {
|
||||
|
||||
@@ -8,7 +8,6 @@ import me.vripper.utilities.LoggerDelegate
|
||||
import me.vripper.utilities.XpathUtils
|
||||
import org.apache.hc.client5.http.impl.cookie.BasicClientCookie
|
||||
import org.w3c.dom.Node
|
||||
import java.sql.Date
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.util.*
|
||||
@@ -24,15 +23,20 @@ internal class ImageBamHost : Host("imagebam.com", 2) {
|
||||
val doc = try {
|
||||
log.debug(String.format("Looking for xpath expression %s in %s", CONTINUE_XPATH, context.imageEntity.url))
|
||||
if (XpathUtils.getAsNode(document, CONTINUE_XPATH) != null) {
|
||||
val clientCookie = BasicClientCookie("nsfw_inter", "1")
|
||||
clientCookie.domain = "www.imagebam.com"
|
||||
clientCookie.path = "/"
|
||||
clientCookie.expiryDate =
|
||||
Date.from(
|
||||
LocalDateTime.now().plusDays(3).atZone(ZoneId.systemDefault()).toInstant()
|
||||
)
|
||||
context.httpContext.cookieStore.addCookie(clientCookie)
|
||||
fetch(context.imageEntity.url, context.imageEntity.url, context) {
|
||||
val nsfwCookie = BasicClientCookie("nsfw_inter", "1").apply {
|
||||
domain = "www.imagebam.com"
|
||||
path = "/"
|
||||
setExpiryDate(LocalDateTime.now().plusDays(3).atZone(ZoneId.systemDefault()).toInstant())
|
||||
}
|
||||
val sfwCookie = BasicClientCookie("sfw_inter", "1").apply {
|
||||
domain = "www.imagebam.com"
|
||||
path = "/"
|
||||
setExpiryDate(LocalDateTime.now().plusDays(3).atZone(ZoneId.systemDefault()).toInstant())
|
||||
}
|
||||
|
||||
context.httpContext.cookieStore.addCookie(nsfwCookie)
|
||||
context.httpContext.cookieStore.addCookie(sfwCookie)
|
||||
fetch(context.imageEntity.url, context) {
|
||||
HtmlUtils.clean(it.entity.content)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -26,7 +26,7 @@ internal class ImageVenueHost : Host("imagevenue.com", 4) {
|
||||
)
|
||||
if (XpathUtils.getAsNode(document, CONTINUE_BUTTON_XPATH) != null) {
|
||||
// Button detected. No need to actually click it, just make the call again.
|
||||
fetch(context.imageEntity.url, context.imageEntity.url, context) {
|
||||
fetch(context.imageEntity.url, context) {
|
||||
HtmlUtils.clean(it.entity.content)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -2,8 +2,15 @@ package me.vripper.host
|
||||
|
||||
import me.vripper.entities.ImageEntity
|
||||
import me.vripper.exception.HostException
|
||||
import me.vripper.model.HostName
|
||||
import me.vripper.model.HostSettingKey
|
||||
import me.vripper.services.download.ImageDownloadRunnable
|
||||
import me.vripper.utilities.HtmlUtils
|
||||
import me.vripper.utilities.LoggerDelegate
|
||||
import me.vripper.utilities.XpathUtils
|
||||
import org.apache.hc.client5.http.classic.methods.HttpPost
|
||||
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity
|
||||
import org.apache.hc.core5.http.message.BasicNameValuePair
|
||||
|
||||
|
||||
internal class ImxHost : Host("imx.to", 8) {
|
||||
@@ -15,15 +22,74 @@ internal class ImxHost : Host("imx.to", 8) {
|
||||
context: ImageDownloadRunnable.Context
|
||||
): Pair<String, String> {
|
||||
log.debug("Resolving name and image url for ${context.imageEntity.url}")
|
||||
val imgTitle = String.format("IMG_%04d", context.imageEntity.index + 1)
|
||||
val imgTitle = getTitle(context)
|
||||
val imgUrl = findPattern(context.imageEntity)
|
||||
return Pair(
|
||||
imgTitle.ifEmpty { getDefaultImageName(imgUrl) }, imgUrl
|
||||
imgTitle, imgUrl
|
||||
)
|
||||
}
|
||||
|
||||
private fun findPattern(image: ImageEntity): String = image.thumbUrl
|
||||
.replace("http:", "https:")
|
||||
.replace("upload/small/", "u/i/")
|
||||
.replace("u/t/", "u/i/")
|
||||
private fun getTitle(context: ImageDownloadRunnable.Context): String {
|
||||
|
||||
return if (context.settings.hostSettings[HostName.IMX]?.get(HostSettingKey.TRY_TO_FETCH_ORIGINAL_FILENAME)
|
||||
.toBoolean()
|
||||
) {
|
||||
val httpsUrl = context.imageEntity.url.replace("http:", "https:")
|
||||
val document = fetchDocument(httpsUrl, context)
|
||||
var value: String? = null
|
||||
log.debug("Looking for xpath expression $CONTINUE_BUTTON_XPATH in $httpsUrl")
|
||||
val contDiv = XpathUtils.getAsNode(document, CONTINUE_BUTTON_XPATH)
|
||||
?: throw HostException("$CONTINUE_BUTTON_XPATH cannot be found")
|
||||
val node = contDiv.attributes.getNamedItem("value")
|
||||
if (node != null) {
|
||||
value = node.textContent
|
||||
}
|
||||
log.debug("Click button found for $httpsUrl")
|
||||
val httpPost: HttpPost = HttpPost(httpsUrl).also {
|
||||
it.entity = UrlEncodedFormEntity(listOf(BasicNameValuePair("imgContinue", value)))
|
||||
}.also { context.requests.add(it) }
|
||||
log.debug("Requesting {}", httpPost)
|
||||
val doc = httpService.client.execute(
|
||||
httpPost, context.httpContext
|
||||
) { response ->
|
||||
log.debug("Cleaning response for {}", httpPost)
|
||||
HtmlUtils.clean(response.entity.content)
|
||||
}
|
||||
|
||||
log.debug("Looking for xpath expression $IMG_XPATH in $httpsUrl")
|
||||
val imgNode = XpathUtils.getAsNode(doc, IMG_XPATH)
|
||||
|
||||
log.debug("Resolving name for $httpsUrl")
|
||||
val imgTitle = imgNode?.attributes?.getNamedItem("alt")?.textContent?.trim() ?: ""
|
||||
imgTitle
|
||||
|
||||
} else {
|
||||
getDefaultImageName(context.imageEntity.thumbUrl)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findPattern(image: ImageEntity): String {
|
||||
val url = image.thumbUrl
|
||||
.replace("http:", "https:")
|
||||
return if (url.startsWith("https://image.imx.to/u/t/")) {
|
||||
"https://image.imx.to/u/i/" + url.replace("https://image.imx.to/u/t/", "")
|
||||
} else if (url.startsWith("https://imx.to/u/t")) {
|
||||
"https://image.imx.to/u/i/" + url.replace("https://imx.to/u/t", "")
|
||||
} else if (url.startsWith("https://t.imx.to/t/")) {
|
||||
"https://image.imx.to/u/i/" + url.replace("https://t.imx.to/t/", "")
|
||||
} else if (url.startsWith("https://imx.to/upload/small/")) {
|
||||
"https://image.imx.to/u/i/" + url.replace("https://imx.to/upload/small/", "")
|
||||
} else if (url.startsWith("https://i.imx.to/t/")) {
|
||||
"https://image.imx.to/u/i/" + url.replace("https://i.imx.to/t/", "")
|
||||
} else if (url.startsWith("https://image.imx.to/u/i/")) {
|
||||
url
|
||||
} else {
|
||||
throw HostException("Cannot find pattern for url ${image.thumbUrl}")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTINUE_BUTTON_XPATH = "//*[@name='imgContinue']"
|
||||
private const val IMG_XPATH = "//img[@class='centred']"
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ internal class PimpandhostHost : Host("pimpandhost.com", 9) {
|
||||
} catch (e: Exception) {
|
||||
throw HostException(e)
|
||||
}
|
||||
val doc = fetch(newUrl, context.imageEntity.url, context) {
|
||||
val doc = fetch(newUrl, context) {
|
||||
HtmlUtils.clean(it.entity.content)
|
||||
}
|
||||
val imgNode: Node = try {
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
package me.vripper.host
|
||||
|
||||
import me.vripper.exception.HostException
|
||||
import me.vripper.exception.XpathException
|
||||
import me.vripper.services.download.ImageDownloadRunnable
|
||||
import me.vripper.utilities.LoggerDelegate
|
||||
import me.vripper.utilities.XpathUtils
|
||||
import org.w3c.dom.Node
|
||||
import java.util.*
|
||||
|
||||
internal class PostImgHost : Host("postimg.cc", 13) {
|
||||
private val log by LoggerDelegate()
|
||||
@@ -15,44 +12,22 @@ internal class PostImgHost : Host("postimg.cc", 13) {
|
||||
override fun resolve(
|
||||
context: ImageDownloadRunnable.Context
|
||||
): Pair<String, String> {
|
||||
val document = fetchDocument(context.imageEntity.url, context)
|
||||
val titleNode = try {
|
||||
log.debug(String.format("Looking for xpath expression %s in %s", TITLE_XPATH, context.imageEntity.url))
|
||||
XpathUtils.getAsNode(document, TITLE_XPATH)
|
||||
} catch (e: XpathException) {
|
||||
throw HostException(e)
|
||||
} ?: throw HostException(
|
||||
String.format(
|
||||
"Xpath '%s' cannot be found in '%s'",
|
||||
TITLE_XPATH,
|
||||
context.imageEntity.url
|
||||
)
|
||||
)
|
||||
val urlNode = try {
|
||||
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, context.imageEntity.url))
|
||||
XpathUtils.getAsNode(document, IMG_XPATH)
|
||||
} catch (e: XpathException) {
|
||||
throw HostException(e)
|
||||
} ?: throw HostException(
|
||||
val document = fetchDocument(context.imageEntity.url.replace("http:", "https:"), context)
|
||||
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, context.imageEntity.url))
|
||||
val node = XpathUtils.getAsNode(document, IMG_XPATH) ?: throw HostException(
|
||||
String.format(
|
||||
"Xpath '%s' cannot be found in '%s'",
|
||||
IMG_XPATH,
|
||||
context.imageEntity.url
|
||||
)
|
||||
)
|
||||
return try {
|
||||
log.debug(String.format("Resolving name and image url for %s", context.imageEntity.url))
|
||||
val imgTitle = Optional.ofNullable(titleNode)
|
||||
.map { node: Node -> node.textContent.trim { it <= ' ' } }
|
||||
.orElseGet { getDefaultImageName(context.imageEntity.url) }
|
||||
Pair(imgTitle, urlNode.attributes.getNamedItem("href").textContent.trim { it <= ' ' })
|
||||
} catch (e: Exception) {
|
||||
throw HostException("Unexpected error occurred", e)
|
||||
}
|
||||
return Pair(
|
||||
node.attributes.getNamedItem("alt").textContent.trim(),
|
||||
node.attributes.getNamedItem("src").textContent.trim()
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TITLE_XPATH = "//span[contains(@class,'imagename')]"
|
||||
private const val IMG_XPATH = "//a[@id='download']"
|
||||
private const val IMG_XPATH = "//img[contains(@class,'img-fluid')]"
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ internal class ViprImHost : Host("vipr.im", 15) {
|
||||
.map { obj: Node -> obj.textContent }
|
||||
.map { obj: String -> obj.trim() }.orElse(null)
|
||||
val imgUrl = imgNode.attributes.getNamedItem("src").textContent.trim()
|
||||
context.headers["Referer"] = "https://vipr.im/"
|
||||
Pair(imgTitle!!, imgUrl)
|
||||
} catch (e: Exception) {
|
||||
throw HostException("Unexpected error occurred", e)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package me.vripper.model
|
||||
|
||||
data class PostIdentifier(val siteProxy: String, val threadId: Long, val postId: Long)
|
||||
@@ -5,12 +5,27 @@ import me.vripper.utilities.ApplicationProperties.VRIPPER_DIR
|
||||
import java.nio.file.Files
|
||||
import kotlin.io.path.pathString
|
||||
|
||||
enum class HostName {
|
||||
IMX
|
||||
}
|
||||
|
||||
enum class HostSettingKey(val type: SettingType) {
|
||||
TRY_TO_FETCH_ORIGINAL_FILENAME(SettingType.BOOLEAN)
|
||||
}
|
||||
|
||||
enum class SettingType {
|
||||
STRING, BOOLEAN, INT
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Settings(
|
||||
val connectionSettings: ConnectionSettings = ConnectionSettings(),
|
||||
val downloadSettings: DownloadSettings = DownloadSettings(),
|
||||
val viperSettings: ViperSettings = ViperSettings(),
|
||||
val systemSettings: SystemSettings = SystemSettings()
|
||||
val systemSettings: SystemSettings = SystemSettings(),
|
||||
val hostSettings: Map<HostName, Map<HostSettingKey, String>> = mapOf(
|
||||
HostName.IMX to mapOf(HostSettingKey.TRY_TO_FETCH_ORIGINAL_FILENAME to "false")
|
||||
)
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -51,3 +66,4 @@ data class SystemSettings(
|
||||
val clipboardPollingRate: Int = 500,
|
||||
val maxEventLog: Int = 1_000,
|
||||
)
|
||||
|
||||
|
||||
@@ -14,11 +14,8 @@ import me.vripper.services.download.MovePosition
|
||||
import me.vripper.services.download.QueueManager
|
||||
import me.vripper.tasks.AddPostTask
|
||||
import me.vripper.tasks.ThreadLookupTask
|
||||
import me.vripper.utilities.ApplicationProperties
|
||||
import me.vripper.utilities.*
|
||||
import me.vripper.utilities.ApplicationProperties.VRIPPER_DIR
|
||||
import me.vripper.utilities.LoggerDelegate
|
||||
import me.vripper.utilities.PathUtils
|
||||
import me.vripper.utilities.taskRunner
|
||||
import org.h2.jdbc.JdbcSQLNonTransientConnectionException
|
||||
import java.sql.DriverManager
|
||||
import java.time.Duration
|
||||
@@ -50,15 +47,17 @@ internal class AppEndpointService(
|
||||
}
|
||||
val urlList = postLinks.split(Pattern.compile("\\r?\\n")).dropLastWhile { it.isBlank() }.map { it.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
val proxies = settingsService.getProxies()
|
||||
for (link in urlList) {
|
||||
log.debug("Scanning: $link")
|
||||
if (!link.startsWith(settingsService.settings.viperSettings.host)) {
|
||||
val matchingProxy = proxies.find { link.startsWith(it) }
|
||||
if (matchingProxy == null) {
|
||||
continue
|
||||
}
|
||||
var threadId: Long
|
||||
var postId: Long?
|
||||
val m = Pattern.compile(
|
||||
Pattern.quote(settingsService.settings.viperSettings.host) + "/threads/(\\d+)((.*p=)(\\d+))?"
|
||||
Pattern.quote(matchingProxy) + "/threads/(\\d+)((.*p=)(\\d+))?"
|
||||
).matcher(link)
|
||||
if (m.find()) {
|
||||
threadId = m.group(1).toLong()
|
||||
@@ -66,13 +65,13 @@ internal class AppEndpointService(
|
||||
if (postId == null) {
|
||||
taskRunner.submit(
|
||||
ThreadLookupTask(
|
||||
threadId, settingsService.settings
|
||||
matchingProxy, threadId, settingsService.settings
|
||||
)
|
||||
)
|
||||
} else {
|
||||
taskRunner.submit(
|
||||
AddPostTask(
|
||||
listOf(ThreadPostId(threadId, postId))
|
||||
listOf(PostIdentifier(matchingProxy, threadId, postId))
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -92,7 +91,13 @@ internal class AppEndpointService(
|
||||
}
|
||||
|
||||
override suspend fun download(posts: List<ThreadPostId>) {
|
||||
taskRunner.submit(AddPostTask(posts))
|
||||
posts.map {
|
||||
val thread = dataAccessService.findThreadByThreadId(it.threadId).getOrNull()
|
||||
?: throw PostParseException("Could not find thread with id ${it.threadId}")
|
||||
PostIdentifier(thread.link.extractBaseUrl(), it.threadId, it.postId)
|
||||
}.also {
|
||||
taskRunner.submit(AddPostTask(it))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun stopAll(postEntityIds: List<Long>) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import me.vripper.data.repositories.ThreadRepository
|
||||
import me.vripper.entities.*
|
||||
import me.vripper.event.*
|
||||
import me.vripper.model.ErrorCount
|
||||
import me.vripper.utilities.LoggerDelegate
|
||||
import me.vripper.utilities.PathUtils
|
||||
import me.vripper.utilities.PathUtils.sanitize
|
||||
import me.vripper.vgapi.PostItem
|
||||
@@ -26,6 +27,8 @@ internal class DataAccessService(
|
||||
private val eventBus: EventBus,
|
||||
) {
|
||||
|
||||
private val log by LoggerDelegate()
|
||||
|
||||
private val postEntityIdCache: LoadingCache<Long, PostEntity> =
|
||||
Caffeine.newBuilder().expireAfterAccess(5, TimeUnit.MINUTES).build { id ->
|
||||
transaction { postRepository.findById(id) }
|
||||
@@ -40,9 +43,10 @@ internal class DataAccessService(
|
||||
val savedPost =
|
||||
postRepository.save(listOf(postEntity)).first()
|
||||
save(images.map { it.copy(postEntityId = savedPost.id) })
|
||||
// Publish event inside transaction for consistency
|
||||
eventBus.publishEvent(PostCreateEvent(listOf(savedPost)))
|
||||
savedPost
|
||||
}
|
||||
eventBus.publishEvent(PostCreateEvent(listOf(savedPost)))
|
||||
return savedPost
|
||||
}
|
||||
|
||||
@@ -51,27 +55,34 @@ internal class DataAccessService(
|
||||
postEntities.forEach { postEntity ->
|
||||
postEntityIdCache.put(postEntity.id, postEntity)
|
||||
}
|
||||
// Log and publish event after transaction commit but before returning
|
||||
log.debug("[{}] Publishing event: PostUpdateEvent for {} posts", System.currentTimeMillis(), postEntities.size)
|
||||
eventBus.publishEvent(PostUpdateEvent(postEntities))
|
||||
}
|
||||
|
||||
fun updatePost(postEntity: PostEntity) {
|
||||
transaction { postRepository.update(postEntity) }
|
||||
postEntityIdCache.put(postEntity.id, postEntity)
|
||||
// Log and publish event after transaction commit but before returning
|
||||
log.debug("[{}] Publishing event: PostUpdateEvent for post {}", System.currentTimeMillis(), postEntity.id)
|
||||
eventBus.publishEvent(PostUpdateEvent(listOf(postEntity)))
|
||||
}
|
||||
|
||||
fun save(threadEntity: ThreadEntity) {
|
||||
val savedThread = transaction { threadRepository.save(threadEntity) }
|
||||
log.debug("[{}] Publishing event: ThreadCreateEvent for thread {}", System.currentTimeMillis(), savedThread.id)
|
||||
eventBus.publishEvent(ThreadCreateEvent(savedThread))
|
||||
}
|
||||
|
||||
fun update(threadEntity: ThreadEntity) {
|
||||
transaction { threadRepository.update(threadEntity) }
|
||||
log.debug("[{}] Publishing event: ThreadUpdateEvent for thread {}", System.currentTimeMillis(), threadEntity.id)
|
||||
eventBus.publishEvent(ThreadUpdateEvent(threadEntity))
|
||||
}
|
||||
|
||||
fun updateImages(imageEntities: List<ImageEntity>) {
|
||||
transaction { imageRepository.update(imageEntities) }
|
||||
log.debug("[{}] Publishing event: ImageEvent for {} images", System.currentTimeMillis(), imageEntities.size)
|
||||
eventBus.publishEvent(ImageEvent(imageEntities))
|
||||
}
|
||||
|
||||
@@ -81,6 +92,7 @@ internal class DataAccessService(
|
||||
imageRepository.update(imageEntity)
|
||||
}
|
||||
}
|
||||
log.debug("[{}] Publishing event: ImageEvent for image {}", System.currentTimeMillis(), imageEntity.id)
|
||||
eventBus.publishEvent(ImageEvent(listOf(imageEntity)))
|
||||
}
|
||||
|
||||
@@ -183,12 +195,15 @@ internal class DataAccessService(
|
||||
postEntityIdCache.get(postEntityId)?.let { postEntityIdCache.invalidate(it.id) }
|
||||
postEntityIdCache.invalidate(postEntityId)
|
||||
}
|
||||
log.debug("[{}] Publishing event: PostDeleteEvent for {} posts", System.currentTimeMillis(), postEntityIds.size)
|
||||
eventBus.publishEvent(PostDeleteEvent(postEntityIds = postEntityIds))
|
||||
log.debug("[{}] Publishing event: ErrorCountEvent after post deletion", System.currentTimeMillis())
|
||||
eventBus.publishEvent(ErrorCountEvent(ErrorCount(countImagesInError())))
|
||||
}
|
||||
|
||||
fun removeThread(threadId: Long) {
|
||||
transaction { threadRepository.deleteByThreadId(threadId) }
|
||||
log.debug("[{}] Publishing event: ThreadDeleteEvent for thread {}", System.currentTimeMillis(), threadId)
|
||||
eventBus.publishEvent(ThreadDeleteEvent(threadId))
|
||||
}
|
||||
|
||||
@@ -216,11 +231,17 @@ internal class DataAccessService(
|
||||
|
||||
fun saveMetadata(metadataEntity: MetadataEntity) {
|
||||
transaction { metadataRepository.save(metadataEntity) }
|
||||
log.debug(
|
||||
"[{}] Publishing event: MetadataUpdateEvent for post {}",
|
||||
System.currentTimeMillis(),
|
||||
metadataEntity.postIdRef
|
||||
)
|
||||
eventBus.publishEvent(MetadataUpdateEvent(metadataEntity))
|
||||
}
|
||||
|
||||
fun clearQueueLinks() {
|
||||
transaction { threadRepository.deleteAll() }
|
||||
log.debug("[{}] Publishing event: ThreadClearEvent", System.currentTimeMillis())
|
||||
eventBus.publishEvent(ThreadClearEvent())
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import me.vripper.event.DownloadSpeedEvent
|
||||
import me.vripper.event.EventBus
|
||||
import me.vripper.event.QueueStateEvent
|
||||
import me.vripper.model.DownloadSpeed
|
||||
import me.vripper.utilities.LoggerDelegate
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
internal class DownloadSpeedService(
|
||||
@@ -16,6 +17,7 @@ internal class DownloadSpeedService(
|
||||
const val DOWNLOAD_POLL_RATE = 2500
|
||||
}
|
||||
|
||||
private val log by LoggerDelegate()
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val bytesCount = AtomicLong(0)
|
||||
private var job: Job? = null
|
||||
@@ -32,7 +34,13 @@ internal class DownloadSpeedService(
|
||||
while (isActive) {
|
||||
delay(DOWNLOAD_POLL_RATE.toLong())
|
||||
val newValue = bytesCount.getAndSet(0)
|
||||
eventBus.publishEvent(DownloadSpeedEvent(DownloadSpeed(((newValue * 1000) / DOWNLOAD_POLL_RATE))))
|
||||
val speed = DownloadSpeed(((newValue * 1000) / DOWNLOAD_POLL_RATE))
|
||||
log.debug(
|
||||
"[{}] Publishing event: DownloadSpeedEvent({})",
|
||||
System.currentTimeMillis(),
|
||||
speed
|
||||
)
|
||||
eventBus.publishEvent(DownloadSpeedEvent(speed))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +48,7 @@ internal class DownloadSpeedService(
|
||||
job?.cancel()
|
||||
coroutineScope.launch {
|
||||
delay(DOWNLOAD_POLL_RATE + 500L)
|
||||
log.debug("[{}] Publishing event: DownloadSpeedEvent(0)", System.currentTimeMillis())
|
||||
eventBus.publishEvent(DownloadSpeedEvent(DownloadSpeed(0L)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ internal class HTTPService {
|
||||
connectionExpiryJob = coroutineScope.launch {
|
||||
while (isActive) {
|
||||
it.closeExpired()
|
||||
delay(60_000)
|
||||
delay(300_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,29 +2,21 @@ package me.vripper.services
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache
|
||||
import me.vripper.utilities.extractBaseUrl
|
||||
import me.vripper.vgapi.ThreadItem
|
||||
import me.vripper.vgapi.ThreadLookupAPIParser
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
internal class ThreadCacheService(val dataAccessService: DataAccessService) {
|
||||
|
||||
private val cache: LoadingCache<Long, ThreadItem> =
|
||||
Caffeine.newBuilder().expireAfterWrite(20, TimeUnit.MINUTES).build { threadId ->
|
||||
val threadItem = ThreadLookupAPIParser(threadId).parse()
|
||||
dataAccessService.findThreadByThreadId(threadItem.threadId).ifPresent {
|
||||
if (threadItem.postItemList.isNotEmpty()) {
|
||||
dataAccessService.update(it.copy(total = threadItem.postItemList.size))
|
||||
}
|
||||
}
|
||||
threadItem
|
||||
}
|
||||
Caffeine.newBuilder().expireAfterWrite(20, TimeUnit.MINUTES).build(::cacheLoader)
|
||||
|
||||
fun invalidate() {
|
||||
cache.invalidateAll()
|
||||
}
|
||||
|
||||
@Throws(ExecutionException::class)
|
||||
operator fun get(threadId: Long): ThreadItem {
|
||||
return cache[threadId]
|
||||
}
|
||||
@@ -32,4 +24,31 @@ internal class ThreadCacheService(val dataAccessService: DataAccessService) {
|
||||
fun getIfPresent(threadId: Long): ThreadItem? {
|
||||
return cache.getIfPresent(threadId)
|
||||
}
|
||||
|
||||
fun loadThenCache(threadId: Long, siteProxy: String): ThreadItem {
|
||||
return cache.get(threadId) {
|
||||
cacheLoader(threadId, siteProxy)
|
||||
}
|
||||
}
|
||||
|
||||
private fun cacheLoader(threadId: Long, siteProxy: String? = null): ThreadItem? {
|
||||
val result = if (siteProxy != null) {
|
||||
ThreadLookupAPIParser(siteProxy, threadId).parse()
|
||||
} else {
|
||||
dataAccessService.findThreadByThreadId(threadId).map { threadEntity ->
|
||||
ThreadLookupAPIParser(threadEntity.link.extractBaseUrl(), threadId).parse()
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
if (result == null) {
|
||||
throw Exception("Failed to load thread $threadId")
|
||||
}
|
||||
|
||||
dataAccessService.findThreadByThreadId(result.threadId).ifPresent {
|
||||
if (result.postItemList.isNotEmpty()) {
|
||||
dataAccessService.update(it.copy(total = result.postItemList.size))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -29,11 +29,14 @@ internal class VGAuthService(
|
||||
fun authenticate(settings: Settings) {
|
||||
if (!settings.viperSettings.login) {
|
||||
log.debug("Authentication option is disabled")
|
||||
authenticated = false
|
||||
loggedUser = ""
|
||||
synchronized(this) {
|
||||
authenticated = false
|
||||
loggedUser = ""
|
||||
}
|
||||
synchronized(vgCookies) {
|
||||
vgCookies.clear()
|
||||
}
|
||||
log.debug("[{}] Publishing event: VGUserLoginEvent", System.currentTimeMillis())
|
||||
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
|
||||
return
|
||||
}
|
||||
@@ -41,11 +44,14 @@ internal class VGAuthService(
|
||||
val password = settings.viperSettings.password
|
||||
if (username.isEmpty() || password.isEmpty()) {
|
||||
log.error("Cannot authenticate with ViperGirls credentials, username or password is empty")
|
||||
authenticated = false
|
||||
loggedUser = ""
|
||||
synchronized(this) {
|
||||
authenticated = false
|
||||
loggedUser = ""
|
||||
}
|
||||
synchronized(vgCookies) {
|
||||
vgCookies.clear()
|
||||
}
|
||||
log.debug("[{}] Publishing event: VGUserLoginEvent (empty credentials)", System.currentTimeMillis())
|
||||
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
|
||||
return
|
||||
}
|
||||
@@ -82,6 +88,7 @@ internal class VGAuthService(
|
||||
"Failed to authenticate user with {}, missing vg_userid/vg_password cookie",
|
||||
settings.viperSettings.host
|
||||
)
|
||||
log.debug("[${System.currentTimeMillis()}] Publishing event: VGUserLoginEvent (missing cookies)")
|
||||
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
|
||||
return
|
||||
}
|
||||
@@ -98,15 +105,21 @@ internal class VGAuthService(
|
||||
log.error(
|
||||
"Failed to authenticate user with " + settings.viperSettings.host, e
|
||||
)
|
||||
authenticated = false
|
||||
loggedUser = ""
|
||||
synchronized(this) {
|
||||
authenticated = false
|
||||
loggedUser = ""
|
||||
}
|
||||
log.debug("[{}] Publishing event: VGUserLoginEvent (exception)", System.currentTimeMillis())
|
||||
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
|
||||
return
|
||||
}
|
||||
authenticated = true
|
||||
loggedUser = username
|
||||
synchronized(this) {
|
||||
authenticated = true
|
||||
loggedUser = username
|
||||
}
|
||||
log.debug("[{}] Publishing event: VGUserLoginEvent for user {}", System.currentTimeMillis(), username)
|
||||
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
|
||||
log.info("Successfully logged in as: $loggedUser")
|
||||
log.info("Successfully logged in as: {}", loggedUser)
|
||||
}
|
||||
|
||||
fun leaveThanks(postEntity: PostEntity) {
|
||||
|
||||
@@ -54,12 +54,20 @@ internal class DownloadService(
|
||||
}
|
||||
|
||||
fun stop(postEntityIds: List<Long> = emptyList()) {
|
||||
if (postEntityIds.isNotEmpty()) {
|
||||
stopInternal(postEntityIds)
|
||||
eventBus.publishEvent(StoppedEvent(postEntityIds))
|
||||
} else {
|
||||
stopAll()
|
||||
eventBus.publishEvent(StoppedEvent(listOf(-1)))
|
||||
downloadManagerLock.withLock {
|
||||
if (postEntityIds.isNotEmpty()) {
|
||||
stopInternal(postEntityIds)
|
||||
log.debug(
|
||||
"[{}] Publishing event: StoppedEvent for {} posts",
|
||||
System.currentTimeMillis(),
|
||||
postEntityIds.size
|
||||
)
|
||||
eventBus.publishEvent(StoppedEvent(postEntityIds))
|
||||
} else {
|
||||
stopAll()
|
||||
log.debug("[{}] Publishing event: StoppedEvent for all posts", System.currentTimeMillis())
|
||||
eventBus.publishEvent(StoppedEvent(listOf(-1)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,24 +133,20 @@ internal class DownloadService(
|
||||
}
|
||||
|
||||
private fun stopAll() {
|
||||
downloadManagerLock.withLock {
|
||||
queueManager.clearPending()
|
||||
queueManager.clearRunning()
|
||||
dataAccessService.findAllNonCompletedPostEntityIds().forEach {
|
||||
dataAccessService.stopImagesByPostEntityIdAndIsNotCompleted(it)
|
||||
dataAccessService.finishPost(it)
|
||||
}
|
||||
queueManager.clearPending()
|
||||
queueManager.clearRunning()
|
||||
dataAccessService.findAllNonCompletedPostEntityIds().forEach {
|
||||
dataAccessService.stopImagesByPostEntityIdAndIsNotCompleted(it)
|
||||
dataAccessService.finishPost(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopInternal(postEntityIds: List<Long>) {
|
||||
downloadManagerLock.withLock {
|
||||
postEntityIds.forEach {
|
||||
queueManager.clearPending(it)
|
||||
queueManager.clearRunning(it)
|
||||
dataAccessService.stopImagesByPostEntityIdAndIsNotCompleted(it)
|
||||
dataAccessService.finishPost(it)
|
||||
}
|
||||
postEntityIds.forEach {
|
||||
queueManager.clearPending(it)
|
||||
queueManager.clearRunning(it)
|
||||
dataAccessService.stopImagesByPostEntityIdAndIsNotCompleted(it)
|
||||
dataAccessService.finishPost(it)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+16
-10
@@ -42,15 +42,18 @@ internal class ImageDownloadRunnable(
|
||||
class Context(val imageEntity: ImageEntity, val settings: Settings) {
|
||||
|
||||
private val log by LoggerDelegate()
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private lateinit var coroutineScope: CoroutineScope
|
||||
private val jobs = mutableListOf<Job>()
|
||||
|
||||
val httpContext: HttpClientContext = HttpClientContext.create().apply {
|
||||
cookieStore = HTTPService.cookieStore
|
||||
}
|
||||
val requests = mutableListOf<HttpUriRequestBase>()
|
||||
val headers = mutableMapOf<String, String>()
|
||||
var completed = false
|
||||
var stopped = false
|
||||
|
||||
init {
|
||||
fun init() {
|
||||
coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
ApplicationProperties.VRIPPER_DIR.listDirectoryEntries()
|
||||
.filter { it.fileName.pathString.startsWith("cookies") }.forEach { cookiesPath ->
|
||||
loadCookies(cookiesPath).also { cookies ->
|
||||
@@ -82,6 +85,7 @@ internal class ImageDownloadRunnable(
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun clear() {
|
||||
runBlocking {
|
||||
requests.forEach { it.abort() }
|
||||
@@ -101,8 +105,6 @@ internal class ImageDownloadRunnable(
|
||||
private val vgAuthService: VGAuthService by inject()
|
||||
private val hosts: List<Host> = getKoin().getAll()
|
||||
val context = Context(imageEntity, settings)
|
||||
var completed = false
|
||||
var stopped = false
|
||||
|
||||
fun download() {
|
||||
try {
|
||||
@@ -139,7 +141,7 @@ internal class ImageDownloadRunnable(
|
||||
dataAccessService.updateImage(context.imageEntity)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (stopped) {
|
||||
if (context.stopped) {
|
||||
return
|
||||
}
|
||||
context.imageEntity.status = Status.ERROR
|
||||
@@ -188,20 +190,24 @@ internal class ImageDownloadRunnable(
|
||||
|
||||
override fun run() {
|
||||
try {
|
||||
if (stopped) {
|
||||
context.init()
|
||||
if (context.stopped) {
|
||||
return
|
||||
}
|
||||
download()
|
||||
} finally {
|
||||
completed = true
|
||||
context.completed = true
|
||||
context.clear()
|
||||
if (context.stopped && context.imageEntity.downloaded != context.imageEntity.size) {
|
||||
context.imageEntity.status = Status.STOPPED
|
||||
}
|
||||
dataAccessService.updateImage(context.imageEntity)
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
stopped = true
|
||||
context.stopped = true
|
||||
context.clear()
|
||||
dataAccessService.updateImage(context.imageEntity)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
|
||||
@@ -55,7 +55,7 @@ internal class QueueManager(
|
||||
val toProcess =
|
||||
if (postEntityId != null) running.filter { it.context.imageEntity.postEntityId == postEntityId } else running
|
||||
toProcess.forEach { it.stop() }
|
||||
while (toProcess.count { !it.completed } > 0) {
|
||||
while (toProcess.count { !it.context.completed } > 0) {
|
||||
Thread.sleep(100)
|
||||
}
|
||||
running.removeAll(toProcess)
|
||||
@@ -66,19 +66,21 @@ internal class QueueManager(
|
||||
}
|
||||
|
||||
fun accept(accepted: List<ImageQueueElement>) {
|
||||
pending.forEach {
|
||||
it.removeAll(accepted)
|
||||
downloadManagerLock.withLock {
|
||||
pending.forEach {
|
||||
it.removeAll(accepted)
|
||||
}
|
||||
pending.removeIf { it.isEmpty() }
|
||||
accepted.map {
|
||||
ImageDownloadRunnable(
|
||||
dataAccessService.findImageById(it.imageEntityId).orElseThrow(), settingsService.settings.copy()
|
||||
)
|
||||
}.forEach {
|
||||
launch(it)
|
||||
running.add(it)
|
||||
}
|
||||
reportQueueState()
|
||||
}
|
||||
pending.removeIf { it.isEmpty() }
|
||||
accepted.map {
|
||||
ImageDownloadRunnable(
|
||||
dataAccessService.findImageById(it.imageEntityId).orElseThrow(), settingsService.settings.copy()
|
||||
)
|
||||
}.forEach {
|
||||
launch(it)
|
||||
running.add(it)
|
||||
}
|
||||
reportQueueState()
|
||||
}
|
||||
|
||||
fun pending(): List<ImageQueueElement> {
|
||||
@@ -121,8 +123,8 @@ internal class QueueManager(
|
||||
}
|
||||
}
|
||||
}
|
||||
reportQueueState()
|
||||
}
|
||||
reportQueueState()
|
||||
}
|
||||
|
||||
fun getQueueState(): QueueState {
|
||||
@@ -146,8 +148,6 @@ internal class QueueManager(
|
||||
dataAccessService.updateImage(image)
|
||||
}.onComplete {
|
||||
afterJobFinish(runnable)
|
||||
reportQueueState()
|
||||
eventBus.publishEvent(ErrorCountEvent(ErrorCount(dataAccessService.countImagesInError())))
|
||||
log.debug(
|
||||
"Finished downloading ${runnable.context.imageEntity.url}"
|
||||
)
|
||||
@@ -158,15 +158,24 @@ internal class QueueManager(
|
||||
downloadManagerLock.withLock {
|
||||
val image = imageDownloadRunnable.context.imageEntity
|
||||
clearRunningRunnable(imageDownloadRunnable.context.imageEntity.id)
|
||||
if (!isPending(image.postEntityId) && !isRunning(image.postEntityId) && !imageDownloadRunnable.stopped) {
|
||||
if (!isPending(image.postEntityId) && !isRunning(image.postEntityId) && !imageDownloadRunnable.context.stopped) {
|
||||
dataAccessService.finishPost(image.postEntityId, true)
|
||||
}
|
||||
reportQueueState()
|
||||
log.debug("[{}] Event published: ErrorCountEvent after job finish", System.currentTimeMillis())
|
||||
eventBus.publishEvent(ErrorCountEvent(ErrorCount(dataAccessService.countImagesInError())))
|
||||
downloadManagerCondition.signal()
|
||||
}
|
||||
}
|
||||
|
||||
private fun reportQueueState() {
|
||||
val queueState = getQueueState()
|
||||
log.debug(
|
||||
"[{}] Publishing event: QueueStateEvent(running={}, remaining={})",
|
||||
System.currentTimeMillis(),
|
||||
queueState.running,
|
||||
queueState.remaining
|
||||
)
|
||||
eventBus.publishEvent(QueueStateEvent(queueState))
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package me.vripper.tasks
|
||||
|
||||
import me.vripper.model.ThreadPostId
|
||||
import me.vripper.model.PostIdentifier
|
||||
import me.vripper.services.DataAccessService
|
||||
import me.vripper.services.MetadataService
|
||||
import me.vripper.services.SettingsService
|
||||
@@ -12,7 +12,7 @@ import me.vripper.vgapi.PostLookupAPIParser
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
internal class AddPostTask(private val items: List<ThreadPostId>) : KoinComponent, Runnable {
|
||||
internal class AddPostTask(private val items: List<PostIdentifier>) : KoinComponent, Runnable {
|
||||
private val log by LoggerDelegate()
|
||||
private val dataAccessService: DataAccessService by inject()
|
||||
private val settingsService: SettingsService by inject()
|
||||
@@ -24,19 +24,19 @@ internal class AddPostTask(private val items: List<ThreadPostId>) : KoinComponen
|
||||
try {
|
||||
Tasks.increment()
|
||||
val toProcess = mutableListOf<PostItem>()
|
||||
for ((threadId, postId) in items) {
|
||||
for ((siteProxy, threadId, postId) in items) {
|
||||
if (dataAccessService.existsPostId(postId)) {
|
||||
log.info("Post $postId already loaded")
|
||||
continue
|
||||
}
|
||||
|
||||
val link =
|
||||
"${settingsService.settings.viperSettings.host}/threads/$threadId?p=$postId&viewfull=1#post$postId"
|
||||
"$siteProxy/threads/$threadId?p=$postId&viewfull=1#post$postId"
|
||||
|
||||
val cachedThread = threadCacheService.getIfPresent(threadId)
|
||||
val threadItem = cachedThread ?:
|
||||
PostLookupAPIParser(
|
||||
threadId, postId
|
||||
siteProxy, threadId, postId
|
||||
).parse()
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
package me.vripper.tasks
|
||||
|
||||
import me.vripper.entities.ThreadEntity
|
||||
import me.vripper.model.PostIdentifier
|
||||
import me.vripper.model.Settings
|
||||
import me.vripper.model.ThreadPostId
|
||||
import me.vripper.services.DataAccessService
|
||||
import me.vripper.services.SettingsService
|
||||
import me.vripper.services.ThreadCacheService
|
||||
import me.vripper.utilities.LoggerDelegate
|
||||
import me.vripper.utilities.taskRunner
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
internal class ThreadLookupTask(private val threadId: Long, private val settings: Settings) : KoinComponent, Runnable {
|
||||
internal class ThreadLookupTask(
|
||||
private val siteProxy: String,
|
||||
private val threadId: Long,
|
||||
private val settings: Settings
|
||||
) : KoinComponent, Runnable {
|
||||
private val log by LoggerDelegate()
|
||||
private val dataAccessService by inject<DataAccessService>()
|
||||
private val settingsService by inject<SettingsService>()
|
||||
private val threadCacheService by inject<ThreadCacheService>()
|
||||
private val link: String = "${settingsService.settings.viperSettings.host}/threads/$threadId"
|
||||
private val link: String = "$siteProxy/threads/$threadId"
|
||||
|
||||
override fun run() {
|
||||
try {
|
||||
Tasks.increment()
|
||||
if (dataAccessService.findThreadByThreadId(threadId).isEmpty) {
|
||||
val threadLookupResult = threadCacheService[threadId]
|
||||
val threadLookupResult = threadCacheService.loadThenCache(threadId, siteProxy)
|
||||
if (threadLookupResult.error.isNotBlank()) {
|
||||
log.error("Error loading $link: ${threadLookupResult.error}")
|
||||
return
|
||||
@@ -35,8 +37,8 @@ internal class ThreadLookupTask(private val threadId: Long, private val settings
|
||||
if (threadLookupResult.postItemList.size <= settings.downloadSettings.autoQueueThreshold) {
|
||||
taskRunner.submit(
|
||||
AddPostTask(threadLookupResult.postItemList.map {
|
||||
ThreadPostId(
|
||||
it.threadId, it.postId
|
||||
PostIdentifier(
|
||||
siteProxy, it.threadId, it.postId
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
@@ -36,3 +36,7 @@ fun String.hash256(): String {
|
||||
val digest = md.digest(bytes)
|
||||
return digest.fold("") { str, it -> str + "%02x".format(it) }
|
||||
}
|
||||
|
||||
fun String.extractBaseUrl(): String {
|
||||
return this.replaceFirst(Regex("(https?://[^/]+).*"), "$1")
|
||||
}
|
||||
@@ -18,7 +18,11 @@ import org.koin.core.component.inject
|
||||
import java.io.ByteArrayInputStream
|
||||
import javax.xml.parsers.SAXParserFactory
|
||||
|
||||
internal class PostLookupAPIParser(private val threadId: Long, private val postId: Long) : KoinComponent {
|
||||
internal class PostLookupAPIParser(
|
||||
private val siteProxy: String,
|
||||
private val threadId: Long,
|
||||
private val postId: Long
|
||||
) : KoinComponent {
|
||||
private val log by LoggerDelegate()
|
||||
private val retryPolicyService: RetryPolicyService by inject()
|
||||
private val httpService: HTTPService by inject()
|
||||
@@ -33,7 +37,7 @@ internal class PostLookupAPIParser(private val threadId: Long, private val postI
|
||||
"p", postId.toString()
|
||||
)
|
||||
}.build()).also { it.setAbsoluteRequestUri(true) }
|
||||
val threadLookupAPIResponseHandler = ThreadLookupAPIResponseHandler()
|
||||
val threadLookupAPIResponseHandler = ThreadLookupAPIResponseHandler(siteProxy)
|
||||
Tasks.increment()
|
||||
return try {
|
||||
Failsafe.with(retryPolicyService.buildRetryPolicy<Any>("Failed to parse $httpGet: ")).onFailure {
|
||||
|
||||
@@ -18,7 +18,7 @@ import org.koin.core.component.inject
|
||||
import java.io.ByteArrayInputStream
|
||||
import javax.xml.parsers.SAXParserFactory
|
||||
|
||||
internal class ThreadLookupAPIParser(private val threadId: Long) : KoinComponent {
|
||||
internal class ThreadLookupAPIParser(private val siteProxy: String, private val threadId: Long) : KoinComponent {
|
||||
private val log by LoggerDelegate()
|
||||
private val cm: HTTPService by inject()
|
||||
private val retryPolicyService: RetryPolicyService by inject()
|
||||
@@ -34,7 +34,7 @@ internal class ThreadLookupAPIParser(private val threadId: Long) : KoinComponent
|
||||
threadId.toString()
|
||||
)
|
||||
}.build()).also { it.setAbsoluteRequestUri(true) }
|
||||
val threadLookupAPIResponseHandler = ThreadLookupAPIResponseHandler()
|
||||
val threadLookupAPIResponseHandler = ThreadLookupAPIResponseHandler(siteProxy)
|
||||
Tasks.increment()
|
||||
return try {
|
||||
Failsafe.with(retryPolicyService.buildRetryPolicy<Any>("Failed to parse $httpGet: ")).onFailure {
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
package me.vripper.vgapi
|
||||
|
||||
import me.vripper.host.Host
|
||||
import me.vripper.services.SettingsService
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.xml.sax.Attributes
|
||||
import org.xml.sax.helpers.DefaultHandler
|
||||
|
||||
internal class ThreadLookupAPIResponseHandler : KoinComponent, DefaultHandler() {
|
||||
internal class ThreadLookupAPIResponseHandler(private val siteProxy: String) : KoinComponent, DefaultHandler() {
|
||||
private val supportedHosts: List<Host> = getKoin().getAll()
|
||||
private val settingsService: SettingsService by inject()
|
||||
private var error: String = ""
|
||||
private val hostMap: MutableMap<Host, Int> = mutableMapOf()
|
||||
private val postItemList: MutableList<PostItem> = mutableListOf()
|
||||
@@ -74,7 +71,7 @@ internal class ThreadLookupAPIResponseHandler : KoinComponent, DefaultHandler()
|
||||
postCounter,
|
||||
postTitle,
|
||||
imageItemList.size,
|
||||
"${settingsService.settings.viperSettings.host}/threads/$threadId?p=$postId&viewfull=1#post$postId",
|
||||
"$siteProxy/threads/$threadId?p=$postId&viewfull=1#post$postId",
|
||||
hostMap.toMap().map { Pair(it.key.hostName, it.value) },
|
||||
securityToken,
|
||||
forum,
|
||||
|
||||
@@ -36,9 +36,14 @@ message SystemSettings {
|
||||
int32 maxEventLog = 4;
|
||||
}
|
||||
|
||||
message HostSettingsMap {
|
||||
map<string, string> settings = 1;
|
||||
}
|
||||
|
||||
message Settings {
|
||||
ConnectionSettings connectionSettings = 1;
|
||||
DownloadSettings downloadSettings = 2;
|
||||
ViperSettings viperSettings = 3;
|
||||
SystemSettings systemSettings = 4;
|
||||
map<string, HostSettingsMap> hostSettings = 5;
|
||||
}
|
||||
@@ -25,6 +25,18 @@
|
||||
|
||||
<appender name="RING" class="me.vripper.RingAppender"/>
|
||||
|
||||
<appender name="ASYNC-STDOUT" class="ch.qos.logback.classic.AsyncAppender">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</appender>
|
||||
|
||||
<appender name="ASYNC-FILE" class="ch.qos.logback.classic.AsyncAppender">
|
||||
<appender-ref ref="FILE"/>
|
||||
</appender>
|
||||
|
||||
<appender name="ASYNC-RING" class="ch.qos.logback.classic.AsyncAppender">
|
||||
<appender-ref ref="RING"/>
|
||||
</appender>
|
||||
|
||||
<logger name="me.vripper" level="info"/>
|
||||
<logger name="Exposed" level="off"/>
|
||||
<logger name="org.springframework.web.socket.config.WebSocketMessageBrokerStats" level="off"/>
|
||||
@@ -32,8 +44,8 @@
|
||||
<Logger name="org.apache.hc.client5.http.wire" level="off"/>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
<appender-ref ref="FILE"/>
|
||||
<appender-ref ref="RING"/>
|
||||
<appender-ref ref="ASYNC-STDOUT"/>
|
||||
<appender-ref ref="ASYNC-FILE"/>
|
||||
<appender-ref ref="ASYNC-RING"/>
|
||||
</root>
|
||||
</configuration>
|
||||
|
||||
@@ -31,8 +31,10 @@ class AddLinksFragment : Fragment("Add thread links") {
|
||||
action {
|
||||
coroutineScope.launch {
|
||||
postController.scan(textAreaProperty.value)
|
||||
runLater {
|
||||
close()
|
||||
}
|
||||
}
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package me.vripper.gui.components.fragments
|
||||
|
||||
import atlantafx.base.util.IntegerStringConverter
|
||||
import javafx.scene.control.Spinner
|
||||
import me.vripper.gui.model.settings.HostSettingsModel
|
||||
import me.vripper.model.HostName
|
||||
import me.vripper.model.HostSettingKey
|
||||
import me.vripper.model.SettingType
|
||||
import tornadofx.*
|
||||
|
||||
class HostSettingsFragment : Fragment("Host Settings") {
|
||||
|
||||
val hostSettings: Map<HostName, Map<HostSettingKey, String>> by param()
|
||||
val hostSettingsModels: List<HostSettingsModel>
|
||||
|
||||
override val root = vbox {}
|
||||
|
||||
init {
|
||||
val mutableHostSettingsModels = mutableListOf<HostSettingsModel>()
|
||||
|
||||
// Build models dynamically from hostSettings map
|
||||
hostSettings.forEach { (hostName, settingsMap) ->
|
||||
settingsMap.forEach { (settingKey, settingValue) ->
|
||||
try {
|
||||
val key = settingKey
|
||||
val model = HostSettingsModel(
|
||||
host = hostName,
|
||||
settingKey = key,
|
||||
initialValue = settingValue
|
||||
)
|
||||
mutableHostSettingsModels.add(model)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// Skip unknown settings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hostSettingsModels = mutableHostSettingsModels.toList()
|
||||
|
||||
// Build UI dynamically
|
||||
with(root) {
|
||||
val groupedByHost = hostSettingsModels.groupBy { it.host }
|
||||
|
||||
form {
|
||||
groupedByHost.forEach { (hostName, settings) ->
|
||||
fieldset(hostName.name) {
|
||||
settings.forEach { model ->
|
||||
field(
|
||||
model.settingKey.name.replace("_", " ").lowercase()
|
||||
.replaceFirstChar { it.uppercase() }) {
|
||||
when (model.settingKey.type) {
|
||||
SettingType.STRING -> textfield(model.valueProperty as javafx.beans.property.StringProperty)
|
||||
SettingType.BOOLEAN -> checkbox(
|
||||
"",
|
||||
model.valueProperty as javafx.beans.property.BooleanProperty
|
||||
)
|
||||
|
||||
SettingType.INT -> {
|
||||
add(Spinner<Int>(Int.MIN_VALUE, Int.MAX_VALUE, model.getValue().toInt()).apply {
|
||||
valueProperty().onChange {
|
||||
(model.valueProperty as javafx.beans.property.IntegerProperty).value =
|
||||
it ?: 0
|
||||
}
|
||||
isEditable = true
|
||||
IntegerStringConverter.createFor(this)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-1
@@ -24,6 +24,7 @@ class SettingsFragment : Fragment("Settings") {
|
||||
val connectionSettings: ConnectionSettings by param()
|
||||
val viperSettings: ViperSettings by param()
|
||||
val systemSettings: SystemSettings by param()
|
||||
val hostSettings: Map<String, Map<String, String>> by param()
|
||||
private val settingsController: SettingsController by inject()
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private val downloadSettingsFragment: DownloadSettingsFragment =
|
||||
@@ -34,6 +35,9 @@ class SettingsFragment : Fragment("Settings") {
|
||||
find(mapOf(ViperSettingsFragment::viperSettings to viperSettings))
|
||||
private val systemSettingsFragment: SystemSettingsFragment =
|
||||
find(mapOf(SystemSettingsFragment::systemSettings to systemSettings))
|
||||
private val hostSettingsFragment: HostSettingsFragment =
|
||||
find(mapOf(HostSettingsFragment::hostSettings to hostSettings))
|
||||
|
||||
|
||||
override val root = vbox(alignment = Pos.CENTER_RIGHT) {
|
||||
spacing = 5.0
|
||||
@@ -60,6 +64,10 @@ class SettingsFragment : Fragment("Settings") {
|
||||
add(viperSettingsFragment)
|
||||
graphic = FontIcon.of(Feather.LINK_2)
|
||||
}
|
||||
tab(hostSettingsFragment.title) {
|
||||
add(hostSettingsFragment)
|
||||
graphic = FontIcon.of(Feather.IMAGE)
|
||||
}
|
||||
}
|
||||
borderpane {
|
||||
right {
|
||||
@@ -75,7 +83,8 @@ class SettingsFragment : Fragment("Settings") {
|
||||
downloadSettingsFragment.downloadSettingsModel,
|
||||
connectionSettingsFragment.connectionSettingsModel,
|
||||
viperSettingsFragment.viperSettingsModel,
|
||||
systemSettingsFragment.systemSettingsModel
|
||||
systemSettingsFragment.systemSettingsModel,
|
||||
hostSettingsFragment.hostSettingsModels
|
||||
)
|
||||
runLater {
|
||||
close()
|
||||
|
||||
@@ -97,6 +97,7 @@ class ActionBarView : View() {
|
||||
val connectionSettings = settingsController.findConnectionSettings()
|
||||
val viperGirlsSettings = settingsController.findViperGirlsSettings()
|
||||
val systemSettings = settingsController.findSystemSettings()
|
||||
val hostSettings = settingsController.findHostSettings()
|
||||
runLater {
|
||||
find<SettingsFragment>(
|
||||
mapOf(
|
||||
@@ -104,6 +105,7 @@ class ActionBarView : View() {
|
||||
SettingsFragment::connectionSettings to connectionSettings,
|
||||
SettingsFragment::viperSettings to viperGirlsSettings,
|
||||
SettingsFragment::systemSettings to systemSettings,
|
||||
SettingsFragment::hostSettings to hostSettings,
|
||||
)
|
||||
).openModal()
|
||||
}
|
||||
|
||||
@@ -2,11 +2,19 @@ package me.vripper.gui.components.views
|
||||
|
||||
import atlantafx.base.theme.*
|
||||
import javafx.application.Application.setUserAgentStylesheet
|
||||
import javafx.event.EventHandler
|
||||
import javafx.scene.input.TransferMode
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import me.vripper.gui.controller.AppController
|
||||
import me.vripper.gui.controller.MainController
|
||||
import me.vripper.gui.controller.WidgetsController
|
||||
import tornadofx.View
|
||||
import tornadofx.onChange
|
||||
import tornadofx.vbox
|
||||
import java.net.URI
|
||||
|
||||
class AppView : View() {
|
||||
|
||||
@@ -17,6 +25,8 @@ class AppView : View() {
|
||||
private val statusBarView: StatusBarView by inject()
|
||||
private val actionBarView: ActionBarView by inject()
|
||||
private val widgetsController: WidgetsController by inject()
|
||||
private val appController: AppController by inject()
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
init {
|
||||
title = "VRipper ${mainController.version}"
|
||||
with(root) {
|
||||
@@ -30,6 +40,24 @@ class AppView : View() {
|
||||
}
|
||||
prefWidth = widgetsController.currentSettings.width
|
||||
prefHeight = widgetsController.currentSettings.height
|
||||
onDragOver = EventHandler { event ->
|
||||
if (event.dragboard.hasString()) {
|
||||
event.acceptTransferModes(TransferMode.COPY)
|
||||
}
|
||||
event.consume()
|
||||
}
|
||||
onDragDropped = EventHandler { event ->
|
||||
val dragBoard = event.dragboard
|
||||
if (dragBoard != null && dragBoard.hasString()) {
|
||||
val url = dragBoard.string.trim()
|
||||
runCatching {
|
||||
val uri = URI(url)
|
||||
coroutineScope.launch {
|
||||
appController.scan(uri.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
widgetsController.currentSettings.visibleToolbarPanelProperty.onChange { it ->
|
||||
|
||||
@@ -33,9 +33,11 @@ class LoadingView : View("VRipper") {
|
||||
|
||||
init {
|
||||
DatabaseManager.connect()
|
||||
ClipboardManager.init()
|
||||
coroutineScope.launch {
|
||||
GuiEventBus.events.filterIsInstance(GuiEventBus.ApplicationInitialized::class).collect {
|
||||
runLater {
|
||||
ClipboardManager.init()
|
||||
}
|
||||
message.set("")
|
||||
AppManager.stop()
|
||||
grpcEndpointService.disconnect()
|
||||
|
||||
@@ -200,6 +200,11 @@ class LogTableView : View() {
|
||||
tableView.placeholder = Label("Loading")
|
||||
}
|
||||
|
||||
is GuiEventBus.RemoteError -> runLater {
|
||||
items.clear()
|
||||
tableView.placeholder = Label("Connection Failure")
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ class MenuBarView : View() {
|
||||
val connectionSettings = settingsController.findConnectionSettings()
|
||||
val viperGirlsSettings = settingsController.findViperGirlsSettings()
|
||||
val systemSettings = settingsController.findSystemSettings()
|
||||
val hostSettings = settingsController.findHostSettings()
|
||||
runLater {
|
||||
find<SettingsFragment>(
|
||||
mapOf(
|
||||
@@ -106,6 +107,7 @@ class MenuBarView : View() {
|
||||
SettingsFragment::connectionSettings to connectionSettings,
|
||||
SettingsFragment::viperSettings to viperGirlsSettings,
|
||||
SettingsFragment::systemSettings to systemSettings,
|
||||
SettingsFragment::hostSettings to hostSettings,
|
||||
)
|
||||
).openModal()
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ class PostInfoView : View() {
|
||||
private val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private val imagesTableView: ImagesTableView by inject()
|
||||
private val postModel: PostModel = PostModel(
|
||||
-1, -1, "", 0.0, "", "", 0, 0, "", "", "*", "", "", "", emptyList(), emptyList(), "", 0
|
||||
-1, -1, "", 0.0, "", "", 0, 0, "", "", -1, "", "", "", emptyList(), emptyList(), "", 0
|
||||
)
|
||||
|
||||
override val root = tabpane()
|
||||
@@ -89,7 +89,7 @@ class PostInfoView : View() {
|
||||
this.total = 0
|
||||
this.hosts = ""
|
||||
this.addedOn = ""
|
||||
this.order = "*"
|
||||
this.order = -1
|
||||
this.path = ""
|
||||
this.folderName = ""
|
||||
this.progressCount = ""
|
||||
|
||||
@@ -203,7 +203,38 @@ class PostsTableView : View() {
|
||||
}
|
||||
}
|
||||
cellFactory = Callback {
|
||||
TextFieldTableCell<PostModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
|
||||
object : TableCell<PostModel, Number>() {
|
||||
override fun updateItem(item: Number?, empty: Boolean) {
|
||||
super.updateItem(item, empty)
|
||||
text = when {
|
||||
empty -> null
|
||||
item == null -> null
|
||||
item.toLong() == -1L -> "*" // Display "*" for -1
|
||||
else -> item.toString() // Display the actual value
|
||||
}
|
||||
alignment = Pos.CENTER_LEFT
|
||||
}
|
||||
}
|
||||
}
|
||||
comparator = Comparator { a, b ->
|
||||
when {
|
||||
a == null && b == null -> 0
|
||||
a == null -> 1 // Nulls to bottom
|
||||
b == null -> -1
|
||||
a.toLong() == -1L && b.toLong() == -1L -> 0 // Both -1, equal
|
||||
a.toLong() == -1L -> {
|
||||
// -1 always goes to bottom
|
||||
// In ascending: return 1 (a is greater, goes last)
|
||||
// In descending: return -1 (a is smaller, still goes last in reverse)
|
||||
if (sortType == TableColumn.SortType.ASCENDING) 1 else -1
|
||||
}
|
||||
|
||||
b.toLong() == -1L -> {
|
||||
if (sortType == TableColumn.SortType.ASCENDING) -1 else 1
|
||||
}
|
||||
|
||||
else -> a.toLong().compareTo(b.toLong()) // Normal numeric comparison
|
||||
}
|
||||
}
|
||||
}
|
||||
column("Preview", PostModel::previewListProperty) {
|
||||
@@ -485,6 +516,11 @@ class PostsTableView : View() {
|
||||
items.clear()
|
||||
}
|
||||
|
||||
is GuiEventBus.RemoteError -> runLater {
|
||||
items.clear()
|
||||
tableView.placeholder = Label("Connection Failure")
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
@@ -496,11 +532,11 @@ class PostsTableView : View() {
|
||||
val rank = queueState.rank.find { it.postEntityId == post.id }
|
||||
if (rank != null) {
|
||||
runLater {
|
||||
post.order = rank.rank.toString()
|
||||
post.order = rank.rank
|
||||
}
|
||||
} else if (post.order != "*") {
|
||||
} else if (post.order != -1L) {
|
||||
runLater {
|
||||
post.order = "*"
|
||||
post.order = -1L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,15 +29,15 @@ class StatusBarView : View("Status bar") {
|
||||
GuiEventBus.events.collect {
|
||||
when (it) {
|
||||
GuiEventBus.LocalSession, GuiEventBus.RemoteSession -> {
|
||||
while (isActive) {
|
||||
val result = runCatching { statusBarController.loggedInUser() }
|
||||
if (result.isSuccess) {
|
||||
runLater {
|
||||
loggedUser.set(result.getOrNull())
|
||||
}
|
||||
break
|
||||
val result = runCatching { statusBarController.loggedInUser() }
|
||||
if (result.isSuccess) {
|
||||
runLater {
|
||||
loggedUser.set(result.getOrNull())
|
||||
}
|
||||
} else {
|
||||
runLater {
|
||||
loggedUser.set(null)
|
||||
}
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -214,6 +214,11 @@ class ThreadTableView : View() {
|
||||
items.clear()
|
||||
}
|
||||
|
||||
is GuiEventBus.RemoteError -> runLater {
|
||||
items.clear()
|
||||
tableView.placeholder = Label("Connection Failure")
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package me.vripper.gui.controller
|
||||
|
||||
import me.vripper.gui.utils.AppEndpointManager.currentAppEndpointService
|
||||
import tornadofx.Controller
|
||||
|
||||
class AppController : Controller() {
|
||||
suspend fun scan(postLinks: String) {
|
||||
runCatching { currentAppEndpointService().scanLinks(postLinks) }
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package me.vripper.gui.controller
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.retryWhen
|
||||
import me.vripper.gui.model.ImageModel
|
||||
import me.vripper.gui.utils.AppEndpointManager.currentAppEndpointService
|
||||
import me.vripper.model.Image
|
||||
@@ -41,7 +39,7 @@ class ImageController : Controller() {
|
||||
}
|
||||
|
||||
fun onUpdateImages(postId: Long) =
|
||||
currentAppEndpointService().onUpdateImagesByPostEntityId(postId).retryWhen { _, _ -> delay(1000); true }
|
||||
currentAppEndpointService().onUpdateImagesByPostEntityId(postId)
|
||||
|
||||
fun onStopped() = currentAppEndpointService().onStopped().retryWhen { _, _ -> delay(1000);true }
|
||||
fun onStopped() = currentAppEndpointService().onStopped()
|
||||
}
|
||||
@@ -115,7 +115,7 @@ class PostController : Controller() {
|
||||
post.total,
|
||||
post.hosts.joinToString(separator = ", "),
|
||||
post.addedOn.format(dateTimeFormatter),
|
||||
"*",
|
||||
-1,
|
||||
post.getDownloadFolder(),
|
||||
post.folderName,
|
||||
progressCount(post.total, post.done, post.downloaded),
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package me.vripper.gui.controller
|
||||
|
||||
import me.vripper.gui.model.settings.ConnectionSettingsModel
|
||||
import me.vripper.gui.model.settings.DownloadSettingsModel
|
||||
import me.vripper.gui.model.settings.SystemSettingsModel
|
||||
import me.vripper.gui.model.settings.ViperSettingsModel
|
||||
import me.vripper.gui.model.settings.*
|
||||
import me.vripper.gui.utils.AppEndpointManager
|
||||
import me.vripper.model.*
|
||||
import tornadofx.Controller
|
||||
@@ -36,11 +33,18 @@ class SettingsController : Controller() {
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun findHostSettings(): Map<HostName, Map<HostSettingKey, String>> {
|
||||
return runCatching { AppEndpointManager.currentAppEndpointService().getSettings().hostSettings }.getOrDefault(
|
||||
emptyMap()
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun saveNewSettings(
|
||||
downloadSettingsModel: DownloadSettingsModel,
|
||||
connectionSettingsModel: ConnectionSettingsModel,
|
||||
viperSettingsModel: ViperSettingsModel,
|
||||
systemSettingsModel: SystemSettingsModel
|
||||
systemSettingsModel: SystemSettingsModel,
|
||||
hostSettingsModel: List<HostSettingsModel>,
|
||||
) {
|
||||
runCatching {
|
||||
AppEndpointManager.currentAppEndpointService().saveSettings(
|
||||
@@ -76,7 +80,17 @@ class SettingsController : Controller() {
|
||||
systemSettingsModel.enable,
|
||||
systemSettingsModel.pollingRate,
|
||||
systemSettingsModel.logEntries
|
||||
)
|
||||
),
|
||||
hostSettings = run {
|
||||
val hostSettingsMap = mutableMapOf<HostName, MutableMap<HostSettingKey, String>>()
|
||||
hostSettingsModel.forEach { model ->
|
||||
val hostName = model.host
|
||||
val settingKey = model.settingKey
|
||||
hostSettingsMap.computeIfAbsent(hostName) { mutableMapOf() }[settingKey] =
|
||||
model.valueProperty.value.toString()
|
||||
}
|
||||
hostSettingsMap
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ class StatusBarController : Controller() {
|
||||
}
|
||||
|
||||
fun connectionState(): String {
|
||||
return currentAppEndpointService().connectionState()
|
||||
return runCatching { currentAppEndpointService().connectionState() }.getOrElse { "" }
|
||||
}
|
||||
|
||||
suspend fun getVersion(): String {
|
||||
|
||||
@@ -17,6 +17,8 @@ object GuiEventBus {
|
||||
object ChangingSession : GUIEvent
|
||||
object LocalSession : GUIEvent
|
||||
object RemoteSession : GUIEvent
|
||||
object Undefined : GUIEvent
|
||||
data class RemoteError(val source: String, val message: String) : GUIEvent
|
||||
}
|
||||
|
||||
sealed interface GUIEvent
|
||||
@@ -1,9 +1,6 @@
|
||||
package me.vripper.gui.model
|
||||
|
||||
import javafx.beans.property.SimpleDoubleProperty
|
||||
import javafx.beans.property.SimpleIntegerProperty
|
||||
import javafx.beans.property.SimpleListProperty
|
||||
import javafx.beans.property.SimpleStringProperty
|
||||
import javafx.beans.property.*
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
import tornadofx.getValue
|
||||
@@ -20,7 +17,7 @@ class PostModel(
|
||||
total: Int,
|
||||
hosts: String,
|
||||
addedOn: String,
|
||||
order: String,
|
||||
order: Long,
|
||||
path: String,
|
||||
folderName: String,
|
||||
progressCount: String,
|
||||
@@ -54,8 +51,8 @@ class PostModel(
|
||||
val addedOnProperty = SimpleStringProperty(addedOn)
|
||||
var addedOn: String by addedOnProperty
|
||||
|
||||
val orderProperty = SimpleStringProperty(order)
|
||||
var order: String by orderProperty
|
||||
val orderProperty = SimpleLongProperty(order)
|
||||
var order: Long by orderProperty
|
||||
|
||||
val pathProperty = SimpleStringProperty(path)
|
||||
var path: String by pathProperty
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package me.vripper.gui.model.settings
|
||||
|
||||
import javafx.beans.property.Property
|
||||
import javafx.beans.property.SimpleBooleanProperty
|
||||
import javafx.beans.property.SimpleIntegerProperty
|
||||
import javafx.beans.property.SimpleStringProperty
|
||||
import me.vripper.model.HostName
|
||||
import me.vripper.model.HostSettingKey
|
||||
import me.vripper.model.SettingType
|
||||
|
||||
class HostSettingsModel(
|
||||
val host: HostName,
|
||||
val settingKey: HostSettingKey,
|
||||
initialValue: String,
|
||||
) {
|
||||
val valueProperty: Property<*> = when (settingKey.type) {
|
||||
SettingType.STRING -> SimpleStringProperty(initialValue)
|
||||
SettingType.BOOLEAN -> SimpleBooleanProperty(initialValue.toBoolean())
|
||||
SettingType.INT -> SimpleIntegerProperty(initialValue.toIntOrNull() ?: 0)
|
||||
}
|
||||
|
||||
fun getValue(): String = when (settingKey.type) {
|
||||
SettingType.STRING -> (valueProperty as SimpleStringProperty).value
|
||||
SettingType.BOOLEAN -> (valueProperty as SimpleBooleanProperty).value.toString()
|
||||
SettingType.INT -> (valueProperty as SimpleIntegerProperty).value.toString()
|
||||
}
|
||||
}
|
||||
@@ -223,6 +223,15 @@ internal class GrpcEndpointService : IAppEndpointService {
|
||||
this.downloadSettings = downloadSettings
|
||||
this.viperSettings = viperSettings
|
||||
this.systemSettings = systemSettings
|
||||
settings.hostSettings.forEach { (hostKey, hostSettings) ->
|
||||
val hostSetting = with(SettingsOuterClass.HostSettingsMap.newBuilder()) {
|
||||
hostSettings.forEach { (settingKey, settingValue) ->
|
||||
this.putSettings(settingKey.name, settingValue)
|
||||
}
|
||||
build()
|
||||
}
|
||||
this.putHostSettings(hostKey.name, hostSetting)
|
||||
}
|
||||
build()
|
||||
}
|
||||
endpointServiceCoroutineStub!!.saveSettings(settingsRequest)
|
||||
@@ -280,6 +289,14 @@ internal class GrpcEndpointService : IAppEndpointService {
|
||||
clipboardPollingRate = settings.systemSettings.clipboardPollingRate,
|
||||
maxEventLog = settings.systemSettings.maxEventLog,
|
||||
),
|
||||
hostSettings = settings.hostSettingsMap.entries.associate {
|
||||
val host = HostName.valueOf(it.key)
|
||||
val hostSettings = it.value.settingsMap.entries.associate { hostSettingsEntry ->
|
||||
val settingKey = HostSettingKey.valueOf(hostSettingsEntry.key)
|
||||
settingKey to hostSettingsEntry.value
|
||||
}
|
||||
host to hostSettings
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -385,7 +402,7 @@ internal class GrpcEndpointService : IAppEndpointService {
|
||||
|
||||
val result = runCatching {
|
||||
val version = getVersion()
|
||||
version >= "6.6.0"
|
||||
isAtLeastWithAlphaBeta(version, "6.6.0")
|
||||
}
|
||||
|
||||
return if (result.isSuccess) {
|
||||
@@ -395,5 +412,26 @@ internal class GrpcEndpointService : IAppEndpointService {
|
||||
}
|
||||
}
|
||||
|
||||
override fun connectionState(): String = channel?.getState(true)?.name ?: ConnectivityState.SHUTDOWN.name
|
||||
}
|
||||
fun isAtLeastWithAlphaBeta(actual: String, min: String): Boolean {
|
||||
fun core(s: String) = s.substringBefore('-')
|
||||
fun suffix(s: String): String? {
|
||||
val i = s.indexOf('-')
|
||||
return if (i >= 0 && i + 1 < s.length) s.substring(i + 1) else null
|
||||
}
|
||||
|
||||
val aCore = core(actual).split('.').map { it.toInt() }
|
||||
val mCore = core(min).split('.').map { it.toInt() }
|
||||
|
||||
val maxLen = maxOf(aCore.size, mCore.size)
|
||||
for (i in 0 until maxLen) {
|
||||
val ai = aCore.getOrElse(i) { 0 }
|
||||
val mi = mCore.getOrElse(i) { 0 }
|
||||
if (ai != mi) return ai > mi
|
||||
}
|
||||
|
||||
// numeric parts equal => alpha/beta is NOT >= final
|
||||
return suffix(actual) == null
|
||||
}
|
||||
|
||||
override fun connectionState(): String = channel?.getState(false)?.name ?: ConnectivityState.SHUTDOWN.name
|
||||
}
|
||||
@@ -16,7 +16,7 @@ object AppEndpointManager : KoinComponent {
|
||||
val localAppEndpointService: IAppEndpointService by inject(named("localAppEndpointService"))
|
||||
val remoteAppEndpointService: IAppEndpointService by inject(named("remoteAppEndpointService"))
|
||||
|
||||
private lateinit var current: GUIEvent
|
||||
private var current: GUIEvent = GuiEventBus.Undefined
|
||||
|
||||
fun set(event: GUIEvent) {
|
||||
this.current = event
|
||||
|
||||
@@ -4,12 +4,11 @@ import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.cancellable
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.retryWhen
|
||||
import me.vripper.gui.event.GuiEventBus
|
||||
|
||||
object ChannelFlowBuilder {
|
||||
|
||||
fun <T> build(localFlow: () -> Flow<T>, remoteFlow: () -> Flow<T>): Flow<T> {
|
||||
fun <T> build(localFlow: () -> Flow<T>, remoteFlow: () -> Flow<T>, source: String = "remote"): Flow<T> {
|
||||
return channelFlow {
|
||||
var job: Job? = null
|
||||
|
||||
@@ -20,10 +19,14 @@ object ChannelFlowBuilder {
|
||||
}
|
||||
} else {
|
||||
launch {
|
||||
remoteFlow().cancellable().retryWhen { _, _ ->
|
||||
delay(1000)
|
||||
true
|
||||
}.collect { if (isActive) send(it) }
|
||||
try {
|
||||
remoteFlow().cancellable().collect { if (isActive) send(it) }
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (t: Throwable) {
|
||||
t.printStackTrace()
|
||||
GuiEventBus.publishEvent(GuiEventBus.RemoteError(source, t.message ?: t.toString()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@ package me.vripper.gui.utils
|
||||
|
||||
import javafx.scene.input.Clipboard
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.retryWhen
|
||||
import me.vripper.gui.event.GuiEventBus
|
||||
import me.vripper.model.Settings
|
||||
import me.vripper.services.IAppEndpointService
|
||||
import me.vripper.utilities.LoggerDelegate
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.koin.core.qualifier.named
|
||||
import tornadofx.runLater
|
||||
|
||||
object ClipboardManager : KoinComponent {
|
||||
@@ -14,34 +16,44 @@ object ClipboardManager : KoinComponent {
|
||||
private var current: String? = null
|
||||
private var coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private var pollJob: Job? = null
|
||||
private var settingsUpdateJob: Job? = null
|
||||
private val systemClipboard = Clipboard.getSystemClipboard()
|
||||
val localAppEndpointService: IAppEndpointService by inject(named("localAppEndpointService"))
|
||||
val remoteAppEndpointService: IAppEndpointService by inject(named("remoteAppEndpointService"))
|
||||
var initialized = false
|
||||
|
||||
@Synchronized
|
||||
fun init() {
|
||||
coroutineScope.launch {
|
||||
GuiEventBus.events.collect {
|
||||
when (it) {
|
||||
GuiEventBus.LocalSession, GuiEventBus.RemoteSession -> {
|
||||
logger.info("Clipboard manager initialized")
|
||||
while (isActive) {
|
||||
if (initialized) {
|
||||
return
|
||||
}
|
||||
runBlocking {
|
||||
val updateSettings = ChannelFlowBuilder.build(
|
||||
localAppEndpointService::onUpdateSettings,
|
||||
remoteAppEndpointService::onUpdateSettings
|
||||
)
|
||||
updateSettings.let { flow ->
|
||||
coroutineScope.launch {
|
||||
flow.collect {
|
||||
update(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
coroutineScope.launch {
|
||||
GuiEventBus.events.collect { event ->
|
||||
when (event) {
|
||||
GuiEventBus.LocalSession, GuiEventBus.RemoteSession -> {
|
||||
logger.info("Clipboard manager initialized")
|
||||
val result = runCatching { AppEndpointManager.currentAppEndpointService().getSettings() }
|
||||
if (result.isSuccess) {
|
||||
update(result.getOrNull()!!)
|
||||
break
|
||||
}
|
||||
delay(1000)
|
||||
}
|
||||
settingsUpdateJob?.cancelAndJoin()
|
||||
settingsUpdateJob = launch {
|
||||
AppEndpointManager.currentAppEndpointService().onUpdateSettings()
|
||||
.retryWhen { _, _ -> delay(1000); true }.collect {
|
||||
update(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
initialized = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,9 +65,8 @@ object ClipboardManager : KoinComponent {
|
||||
var value: String? = null
|
||||
while (isActive) {
|
||||
runLater {
|
||||
val clipboard = Clipboard.getSystemClipboard()
|
||||
if (clipboard.hasString()) {
|
||||
value = clipboard.string
|
||||
if (systemClipboard.hasString()) {
|
||||
value = systemClipboard.string
|
||||
}
|
||||
}
|
||||
if (!value.isNullOrBlank() && value != current) {
|
||||
|
||||
@@ -8,7 +8,7 @@ fun openFileDirectory(path: String) {
|
||||
if(os.contains("Windows")) {
|
||||
Shell32.INSTANCE.ShellExecuteW(null, WString("open"), WString(path), null, null, 1)
|
||||
} else if(os.contains("Linux")) {
|
||||
Runtime.getRuntime().exec("xdg-open $path")
|
||||
Runtime.getRuntime().exec(arrayOf("xdg-open", path))
|
||||
} else if(os.contains("Mac")) {
|
||||
Runtime.getRuntime().exec("open -R $path")
|
||||
}
|
||||
|
||||
@@ -213,7 +213,14 @@ class GrpcServerAppEndpointService : EndpointServiceGrpcKt.EndpointServiceCorout
|
||||
enableClipboardMonitoring = request.systemSettings.enableClipboardMonitoring,
|
||||
clipboardPollingRate = request.systemSettings.clipboardPollingRate,
|
||||
maxEventLog = request.systemSettings.maxEventLog,
|
||||
)
|
||||
),
|
||||
hostSettings = request.hostSettingsMap.entries.associate { hostSettings ->
|
||||
HostName.valueOf(hostSettings.key) to hostSettings.value.settingsMap.entries.associate {
|
||||
HostSettingKey.valueOf(
|
||||
it.key
|
||||
) to it.value
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
return EndpointServiceOuterClass.EmptyResponse.getDefaultInstance()
|
||||
@@ -286,11 +293,22 @@ class GrpcServerAppEndpointService : EndpointServiceGrpcKt.EndpointServiceCorout
|
||||
build()
|
||||
}
|
||||
|
||||
val hostSettings = settings.hostSettings.entries.associate {
|
||||
val hostSettings = with(SettingsOuterClass.HostSettingsMap.newBuilder()) {
|
||||
this.putAllSettings(it.value.entries.associate { setting ->
|
||||
setting.key.name to setting.value
|
||||
})
|
||||
build()
|
||||
}
|
||||
it.key.name to hostSettings
|
||||
}
|
||||
|
||||
return with(SettingsOuterClass.Settings.newBuilder()) {
|
||||
this.connectionSettings = connectionSettings
|
||||
this.downloadSettings = downloadSettings
|
||||
this.viperSettings = viperSettings
|
||||
this.systemSettings = systemSettings
|
||||
this.putAllHostSettings(hostSettings)
|
||||
build()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user