Compare commits

...
Author SHA1 Message Date
dev-claw 7ddc5d00b9 fix counter 2025-08-07 15:19:41 +01:00
dev-claw 00039e230b version bump 2025-08-07 14:56:30 +01:00
dev-clawandGitHub f4821aafac fixes #237 (#238) 2025-08-07 14:55:03 +01:00
dev-clawandGitHub d7c8377a71 fixes #231 (#232) 2025-04-15 00:05:27 +01:00
61 changed files with 1017 additions and 1446 deletions
+3 -2
View File
@@ -22,9 +22,10 @@ Direct access to `vipergirls.to` domain.
<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/death-claw/vripper-project/releases/download/6.5.3/vripper-windows-installer-6.5.3.exe) <br /> [Installer (MSI)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-windows-installer-6.5.3.msi) <br /> [Portable (ZIP)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-windows-portable-6.5.3.zip) | [Installer (DMG)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-macos-6.5.3.x86_64.dmg) <br /> [Installer (PKG)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-macos-6.5.3.x86_64.pkg) <br /> [Portable (ZIP)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-macos-portable-6.5.3.x86_64.zip) | [Installer (DMG)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-macos-6.5.3.arm64.dmg) <br /> [Installer (PKG)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-macos-6.5.3.arm64.pkg) <br /> [Portable (ZIP)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-macos-portable-6.5.3.arm64.zip) | [Linux (amd64) (DEB)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-linux-6.5.3_amd64.deb) <br /> [Linux (x86_64) (RPM)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-linux-6.5.3.x86_64.rpm) <br /> [Portable (ZIP)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-linux-portable-6.5.3.zip) | [Java GUI (noarch)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-noarch-gui-6.5.3.jar) <br /> [Java Web (noarch)](https://github.com/death-claw/vripper-project/releases/download/6.5.3/vripper-noarch-web-6.5.3.jar)
[Installer (EXE)](https://github.com/dev-claw/vripper-project/releases/download/6.5.4/vripper-windows-installer-6.5.4.exe) <br /> [Installer (MSI)](https://github.com/dev-claw/vripper-project/releases/download/6.5.4/vripper-windows-installer-6.5.4.msi) <br /> [Portable (ZIP)](https://github.com/dev-claw/vripper-project/releases/download/6.5.4/vripper-windows-portable-6.5.4.zip) | [Installer (DMG)](https://github.com/dev-claw/vripper-project/releases/download/6.5.4/vripper-macos-6.5.4.x86_64.dmg) <br /> [Installer (PKG)](https://github.com/dev-claw/vripper-project/releases/download/6.5.4/vripper-macos-6.5.4.x86_64.pkg) <br /> [Portable (ZIP)](https://github.com/dev-claw/vripper-project/releases/download/6.5.4/vripper-macos-portable-6.5.4.x86_64.zip) | [Installer (DMG)](https://github.com/dev-claw/vripper-project/releases/download/6.5.4/vripper-macos-6.5.4.arm64.dmg) <br /> [Installer (PKG)](https://github.com/dev-claw/vripper-project/releases/download/6.5.4/vripper-macos-6.5.4.arm64.pkg) <br /> [Portable (ZIP)](https://github.com/dev-claw/vripper-project/releases/download/6.5.4/vripper-macos-portable-6.5.4.arm64.zip) | [Linux (amd64) (DEB)](https://github.com/dev-claw/vripper-project/releases/download/6.5.4/vripper-linux-6.5.3_amd64.deb) <br /> [Linux (x86_64) (RPM)](https://github.com/dev-claw/vripper-project/releases/download/6.5.4/vripper-linux-6.5.4.x86_64.rpm) <br /> [Portable (ZIP)](https://github.com/dev-claw/vripper-project/releases/download/6.5.4/vripper-linux-portable-6.5.4.zip) | [Java GUI (noarch)](https://github.com/dev-claw/vripper-project/releases/download/6.5.4/vripper-noarch-gui-6.5.4.jar) <br /> [Java Web (noarch)](https://github.com/dev-claw/vripper-project/releases/download/6.5.4/vripper-noarch-web-6.5.4.jar)
Source code and previous versions are available on the [Releases page](https://github.com/death-claw/vripper-project/releases).
Source code and previous versions are available on
the [Releases page](https://github.com/dev-claw/vripper-project/releases).
Application data (application logs, settings and persisted data) is stored in:
* Windows --> `C:\USERS\<your Windows username>\vripper`
+1 -1
View File
@@ -5,4 +5,4 @@
--win-per-user-install
--win-shortcut
--win-shortcut-prompt
--win-update-url https://github.com/death-claw/vripper-project/releases
--win-update-url https://github.com/dev-claw/vripper-project/releases
+1 -1
View File
@@ -5,4 +5,4 @@
--name VRipper
--description "Image ripper tool for vipergirls"
--dest dist
--vendor "death-claw"
--vendor "dev-claw"
+1 -1
View File
@@ -15,7 +15,7 @@
<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.5.3</revision>
<revision>6.5.4</revision>
<kotlin.version>2.0.21</kotlin.version>
<slf4j.version>2.0.16</slf4j.version>
<logback.version>1.5.12</logback.version>
@@ -71,51 +71,51 @@ val coreModule = module {
MetadataService(get(), get())
}
single {
AcidimgHost(get(), get(), get())
AcidimgHost(get())
} bind Host::class
single {
DPicMeHost(get(), get(), get())
DPicMeHost()
} bind Host::class
single {
ImageBamHost(get(), get(), get())
ImageBamHost()
} bind Host::class
single {
ImageTwistHost(get(), get(), get())
ImageTwistHost()
} bind Host::class
single {
ImageVenueHost(get(), get(), get())
ImageVenueHost()
} bind Host::class
single {
ImageZillaHost(get(), get(), get())
ImageZillaHost()
} bind Host::class
single {
ImgboxHost(get(), get(), get())
ImgboxHost()
} bind Host::class
single {
ImgSpiceHost(get(), get(), get())
ImgSpiceHost()
} bind Host::class
single {
ImxHost(get(), get(), get())
ImxHost()
} bind Host::class
single {
PimpandhostHost(get(), get(), get())
PimpandhostHost()
} bind Host::class
single {
PixhostHost(get(), get(), get())
PixhostHost()
} bind Host::class
single {
PixRouteHost(get(), get(), get())
PixRouteHost()
} bind Host::class
single {
PixxxelsHost(get(), get(), get())
PixxxelsHost()
} bind Host::class
single {
PostImgHost(get(), get(), get())
PostImgHost()
} bind Host::class
single {
TurboImageHost(get(), get(), get())
TurboImageHost()
} bind Host::class
single {
ViprImHost(get(), get(), get())
ViprImHost()
} bind Host::class
}
@@ -1,10 +1,9 @@
package me.vripper.host
import me.vripper.entities.ImageEntity
import me.vripper.exception.HostException
import me.vripper.exception.XpathException
import me.vripper.services.DataTransaction
import me.vripper.services.DownloadService.ImageDownloadContext
import me.vripper.services.DownloadSpeedService
import me.vripper.services.HTTPService
import me.vripper.utilities.HtmlUtils
import me.vripper.utilities.LoggerDelegate
@@ -12,33 +11,31 @@ 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
import org.w3c.dom.Document
import org.w3c.dom.Node
internal class AcidimgHost(
private val httpService: HTTPService,
dataTransaction: DataTransaction,
downloadSpeedService: DownloadSpeedService,
) : Host("acidimg.cc", 0, httpService, dataTransaction, downloadSpeedService) {
) : Host("acidimg.cc", 0) {
private val log by LoggerDelegate()
@Throws(HostException::class)
override fun resolve(
url: String, document: Document, context: ImageDownloadContext
image: ImageEntity, context: ImageDownloadContext
): Pair<String, String> {
val document = fetchDocument(image.url, context)
try {
log.debug(
String.format(
"Looking for xpath expression %s in %s", CONTINUE_BUTTON_XPATH, url
"Looking for xpath expression %s in %s", CONTINUE_BUTTON_XPATH, image.url
)
)
XpathUtils.getAsNode(document, CONTINUE_BUTTON_XPATH)
} catch (e: XpathException) {
throw HostException(e)
}
log.debug(String.format("Click button found for %s", url))
val httpPost = HttpPost(url).also {
it.addHeader("Referer", url)
log.debug(String.format("Click button found for %s", image.url))
val httpPost = HttpPost(image.url).also {
it.addHeader("Referer", image.url)
it.entity = UrlEncodedFormEntity(
listOf(
BasicNameValuePair(
@@ -61,17 +58,17 @@ internal class AcidimgHost(
throw HostException(e)
}
val imgNode: Node = try {
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url))
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, image.url))
XpathUtils.getAsNode(doc, IMG_XPATH)
} catch (e: XpathException) {
throw HostException(e)
} ?: throw HostException(
String.format(
"Xpath '%s' cannot be found in '%s'", IMG_XPATH, url
"Xpath '%s' cannot be found in '%s'", IMG_XPATH, image.url
)
)
return try {
log.debug(String.format("Resolving name and image url for %s", url))
log.debug(String.format("Resolving name and image url for %s", image.url))
val imgTitle = imgNode.attributes.getNamedItem("alt").textContent.trim()
val imgUrl = imgNode.attributes.getNamedItem("src").textContent.trim()
Pair(
@@ -1,32 +1,25 @@
package me.vripper.host
import me.vripper.entities.ImageEntity
import me.vripper.exception.HostException
import me.vripper.exception.XpathException
import me.vripper.services.DataTransaction
import me.vripper.services.DownloadService.ImageDownloadContext
import me.vripper.services.DownloadSpeedService
import me.vripper.services.HTTPService
import me.vripper.utilities.LoggerDelegate
import me.vripper.utilities.XpathUtils
import org.w3c.dom.Document
import org.w3c.dom.Node
import java.util.*
internal class DPicMeHost(
httpService: HTTPService,
dataTransaction: DataTransaction,
downloadSpeedService: DownloadSpeedService,
) : Host("dpic.me", 1, httpService, dataTransaction, downloadSpeedService) {
internal class DPicMeHost : Host("dpic.me", 1) {
private val log by LoggerDelegate()
@Throws(HostException::class)
override fun resolve(
url: String,
document: Document,
image: ImageEntity,
context: ImageDownloadContext
): Pair<String, String> {
val document = fetchDocument(image.url, context)
val imgNode: Node = try {
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url))
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, image.url))
XpathUtils.getAsNode(document, IMG_XPATH)
} catch (e: XpathException) {
throw HostException(e)
@@ -34,11 +27,11 @@ internal class DPicMeHost(
String.format(
"Xpath '%s' cannot be found in '%s'",
IMG_XPATH,
url
image.url
)
)
return try {
log.debug(String.format("Resolving name and image url for %s", url))
log.debug(String.format("Resolving name and image url for %s", image.url))
val imgTitle =
Optional.ofNullable(imgNode.attributes.getNamedItem("alt"))
.map { e: Node -> e.textContent.trim() }
@@ -4,6 +4,7 @@ import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.runBlocking
import me.vripper.entities.ImageEntity
import me.vripper.exception.DownloadException
import me.vripper.exception.HostException
import me.vripper.services.DataTransaction
@@ -18,6 +19,8 @@ import org.apache.hc.client5.http.classic.methods.HttpHead
import org.apache.hc.core5.http.ClassicHttpResponse
import org.apache.hc.core5.http.Header
import org.jetbrains.exposed.sql.transactions.transaction
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.w3c.dom.Document
import java.io.BufferedOutputStream
import java.nio.file.Files
@@ -26,12 +29,13 @@ import java.nio.file.Path
internal abstract class Host(
val hostName: String,
val hostId: Byte,
private val httpService: HTTPService,
private val dataTransaction: DataTransaction,
private val downloadSpeedService: DownloadSpeedService
) {
) : KoinComponent {
private val log by LoggerDelegate()
private val httpService: HTTPService by inject()
private val dataTransaction: DataTransaction by inject()
private val downloadSpeedService: DownloadSpeedService by inject()
companion object {
private const val READ_BUFFER_SIZE = 8192
private val hosts: MutableMap<String, Byte> = mutableMapOf()
@@ -47,49 +51,49 @@ internal abstract class Host(
@Throws(HostException::class)
abstract fun resolve(
url: String,
document: Document,
image: ImageEntity,
context: ImageDownloadContext
): Pair<String, String>
@Throws(HostException::class)
fun downloadInternal(url: String, context: ImageDownloadContext): DownloadedImage {
val headers = head(url, context)
fun downloadInternal(image: ImageEntity, context: ImageDownloadContext): DownloadedImage {
if (hostId == 8.toByte()) {
return downloadByHost(image, context)
}
val headers = head(image.url, context)
// is the body of type image ?
val imageMimeType = getImageMimeType(headers)
val downloadedImage = if (imageMimeType != null) {
// a direct link, awesome
val downloadedImage = fetch(url, context) {
val downloadedImage = fetch(image.url, context) {
handleImageDownload(it, context)
}
DownloadedImage(getDefaultImageName(url), downloadedImage.first, downloadedImage.second)
DownloadedImage(getDefaultImageName(image.url), downloadedImage.first, downloadedImage.second)
} else {
// linked image ?
val value = headers.find { it.name.contains("content-type", true) }?.value
if (value != null) {
if (value.contains("text/html")) {
val document = fetch(url, context) {
HtmlUtils.clean(it.entity.content)
}
if (log.isDebugEnabled) {
log.debug("Cleaning $url response", url)
}
val resolvedImage = resolve(url, document, context)
val downloadImage: Pair<Path, ImageMimeType> =
fetch(resolvedImage.second, context) {
handleImageDownload(it, context)
}
DownloadedImage(resolvedImage.first, downloadImage.first, downloadImage.second)
downloadByHost(image, context)
} else {
throw HostException("Unable to download $url, can't process content type $value")
throw HostException("Unable to download ${image.url}, can't process content type $value")
}
} else {
throw HostException("Unexpected server response for $url, response have no content type")
throw HostException("Unexpected server response for ${image.url}, response have no content type")
}
}
return downloadedImage
}
private fun downloadByHost(image: ImageEntity, context: ImageDownloadContext): DownloadedImage {
val resolvedImage = resolve(image, context)
val downloadImage: Pair<Path, ImageMimeType> =
fetch(resolvedImage.second, context) {
handleImageDownload(it, context)
}
return DownloadedImage(resolvedImage.first, downloadImage.first, downloadImage.second)
}
private fun handleImageDownload(
response: ClassicHttpResponse,
context: ImageDownloadContext
@@ -183,6 +187,19 @@ internal abstract class Host(
}
}
fun fetchDocument(
url: String,
context: ImageDownloadContext
): Document {
return fetch(url, context) {
HtmlUtils.clean(it.entity.content)
}.also {
if (log.isDebugEnabled) {
log.debug("Cleaning $url response", url)
}
}
}
@OptIn(ExperimentalStdlibApi::class)
private fun getImageMimeType(headers: Array<Header>): ImageMimeType? {
@@ -1,37 +1,30 @@
package me.vripper.host
import me.vripper.entities.ImageEntity
import me.vripper.exception.HostException
import me.vripper.exception.XpathException
import me.vripper.services.DataTransaction
import me.vripper.services.DownloadService.ImageDownloadContext
import me.vripper.services.DownloadSpeedService
import me.vripper.services.HTTPService
import me.vripper.utilities.HtmlUtils
import me.vripper.utilities.LoggerDelegate
import me.vripper.utilities.XpathUtils
import org.apache.hc.client5.http.impl.cookie.BasicClientCookie
import org.w3c.dom.Document
import org.w3c.dom.Node
import java.sql.Date
import java.time.LocalDateTime
import java.time.ZoneId
import java.util.*
internal class ImageBamHost(
httpService: HTTPService,
dataTransaction: DataTransaction,
downloadSpeedService: DownloadSpeedService,
) : Host("imagebam.com", 2, httpService, dataTransaction, downloadSpeedService) {
internal class ImageBamHost : Host("imagebam.com", 2) {
private val log by LoggerDelegate()
@Throws(HostException::class)
override fun resolve(
url: String,
document: Document,
image: ImageEntity,
context: ImageDownloadContext
): Pair<String, String> {
val document = fetchDocument(image.url, context)
val doc = try {
log.debug(String.format("Looking for xpath expression %s in %s", CONTINUE_XPATH, url))
log.debug(String.format("Looking for xpath expression %s in %s", CONTINUE_XPATH, image.url))
if (XpathUtils.getAsNode(document, CONTINUE_XPATH) != null) {
val clientCookie = BasicClientCookie("nsfw_inter", "1")
clientCookie.domain = "www.imagebam.com"
@@ -41,7 +34,7 @@ internal class ImageBamHost(
LocalDateTime.now().plusDays(3).atZone(ZoneId.systemDefault()).toInstant()
)
context.httpContext.cookieStore.addCookie(clientCookie)
fetch(url, context) {
fetch(image.url, context) {
HtmlUtils.clean(it.entity.content)
}
} else {
@@ -51,7 +44,7 @@ internal class ImageBamHost(
throw HostException(e)
}
val imgNode: Node = try {
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url))
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, image.url))
XpathUtils.getAsNode(doc, IMG_XPATH)
} catch (e: XpathException) {
throw HostException(e)
@@ -59,11 +52,11 @@ internal class ImageBamHost(
String.format(
"Xpath '%s' cannot be found in '%s'",
IMG_XPATH,
url
image.url
)
)
return try {
log.debug(String.format("Resolving name and image url for %s", url))
log.debug(String.format("Resolving name and image url for %s", image.url))
val imgTitle = Optional.ofNullable(imgNode.attributes.getNamedItem("alt"))
.map { e: Node -> e.textContent.trim { it <= ' ' } }
.orElse("")
@@ -1,32 +1,25 @@
package me.vripper.host
import me.vripper.entities.ImageEntity
import me.vripper.exception.HostException
import me.vripper.exception.XpathException
import me.vripper.services.DataTransaction
import me.vripper.services.DownloadService.ImageDownloadContext
import me.vripper.services.DownloadSpeedService
import me.vripper.services.HTTPService
import me.vripper.utilities.LoggerDelegate
import me.vripper.utilities.XpathUtils
import org.w3c.dom.Document
import org.w3c.dom.Node
import java.util.*
internal class ImageTwistHost(
httpService: HTTPService,
dataTransaction: DataTransaction,
downloadSpeedService: DownloadSpeedService,
) : Host("imagetwist.com", 3, httpService, dataTransaction, downloadSpeedService) {
internal class ImageTwistHost : Host("imagetwist.com", 3) {
private val log by LoggerDelegate()
@Throws(HostException::class)
override fun resolve(
url: String,
document: Document,
image: ImageEntity,
context: ImageDownloadContext
): Pair<String, String> {
val document = fetchDocument(image.url, context)
val imgNode: Node = try {
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url))
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, image.url))
XpathUtils.getAsNode(document, IMG_XPATH)
} catch (e: XpathException) {
throw HostException(e)
@@ -34,11 +27,11 @@ internal class ImageTwistHost(
String.format(
"Xpath '%s' cannot be found in '%s'",
IMG_XPATH,
url
image.url
)
)
return try {
log.debug(String.format("Resolving name and image url for %s", url))
log.debug(String.format("Resolving name and image url for %s", image.url))
val imgTitle =
Optional.ofNullable(imgNode.attributes.getNamedItem("alt"))
.map { obj: Node -> obj.textContent }
@@ -1,41 +1,34 @@
package me.vripper.host
import me.vripper.entities.ImageEntity
import me.vripper.exception.HostException
import me.vripper.exception.XpathException
import me.vripper.services.DataTransaction
import me.vripper.services.DownloadService.ImageDownloadContext
import me.vripper.services.DownloadSpeedService
import me.vripper.services.HTTPService
import me.vripper.utilities.HtmlUtils
import me.vripper.utilities.LoggerDelegate
import me.vripper.utilities.XpathUtils
import org.w3c.dom.Document
import org.w3c.dom.Node
internal class ImageVenueHost(
httpService: HTTPService,
dataTransaction: DataTransaction,
downloadSpeedService: DownloadSpeedService,
) : Host("imagevenue.com", 4, httpService, dataTransaction, downloadSpeedService) {
internal class ImageVenueHost : Host("imagevenue.com", 4) {
private val log by LoggerDelegate()
@Throws(HostException::class)
override fun resolve(
url: String,
document: Document,
image: ImageEntity,
context: ImageDownloadContext
): Pair<String, String> {
val doc = try {
val document = fetchDocument(image.url, context)
log.debug(
String.format(
"Looking for xpath expression %s in %s",
CONTINUE_BUTTON_XPATH,
url
image.url
)
)
if (XpathUtils.getAsNode(document, CONTINUE_BUTTON_XPATH) != null) {
// Button detected. No need to actually click it, just make the call again.
fetch(url, context) {
fetch(image.url, context) {
HtmlUtils.clean(it.entity.content)
}
} else {
@@ -45,7 +38,7 @@ internal class ImageVenueHost(
throw HostException(e)
}
val imgNode: Node = try {
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url))
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, image.url))
XpathUtils.getAsNode(doc, IMG_XPATH)
} catch (e: XpathException) {
throw HostException(e)
@@ -53,11 +46,11 @@ internal class ImageVenueHost(
String.format(
"Xpath '%s' cannot be found in '%s'",
IMG_XPATH,
url
image.url
)
)
return try {
log.debug(String.format("Resolving name and image url for %s", url))
log.debug(String.format("Resolving name and image url for %s", image.url))
val imgTitle = imgNode.attributes.getNamedItem("alt").textContent.trim { it <= ' ' }
val imgUrl = imgNode.attributes.getNamedItem("src").textContent.trim { it <= ' ' }
Pair(
@@ -1,30 +1,23 @@
package me.vripper.host
import me.vripper.entities.ImageEntity
import me.vripper.exception.HostException
import me.vripper.exception.XpathException
import me.vripper.services.DataTransaction
import me.vripper.services.DownloadService.ImageDownloadContext
import me.vripper.services.DownloadSpeedService
import me.vripper.services.HTTPService
import me.vripper.utilities.LoggerDelegate
import me.vripper.utilities.XpathUtils
import org.w3c.dom.Document
internal class ImageZillaHost(
httpService: HTTPService,
dataTransaction: DataTransaction,
downloadSpeedService: DownloadSpeedService,
) : Host("imagezilla.net", 5, httpService, dataTransaction, downloadSpeedService) {
internal class ImageZillaHost : Host("imagezilla.net", 5) {
private val log by LoggerDelegate()
@Throws(HostException::class)
override fun resolve(
url: String,
document: Document,
image: ImageEntity,
context: ImageDownloadContext
): Pair<String, String> {
val document = fetchDocument(image.url, context)
val titleNode = try {
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url))
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, image.url))
XpathUtils.getAsNode(document, IMG_XPATH)
} catch (e: XpathException) {
throw HostException(e)
@@ -32,17 +25,17 @@ internal class ImageZillaHost(
String.format(
"Xpath '%s' cannot be found in '%s'",
IMG_XPATH,
url
image.url
)
)
log.debug(String.format("Resolving name for %s", url))
log.debug(String.format("Resolving name for %s", image.url))
var title = titleNode.attributes.getNamedItem("title").textContent.trim()
titleNode.textContent.trim()
if (title.isEmpty()) {
title = getDefaultImageName(url)
title = getDefaultImageName(image.url)
}
return try {
Pair(title, url.replace("show", "images"))
Pair(title, image.url.replace("show", "images"))
} catch (e: Exception) {
throw HostException("Unexpected error occurred", e)
}
@@ -1,31 +1,24 @@
package me.vripper.host
import me.vripper.entities.ImageEntity
import me.vripper.exception.HostException
import me.vripper.exception.XpathException
import me.vripper.services.DataTransaction
import me.vripper.services.DownloadService.ImageDownloadContext
import me.vripper.services.DownloadSpeedService
import me.vripper.services.HTTPService
import me.vripper.utilities.LoggerDelegate
import me.vripper.utilities.XpathUtils
import org.w3c.dom.Document
import org.w3c.dom.Node
internal class ImgSpiceHost(
httpService: HTTPService,
dataTransaction: DataTransaction,
downloadSpeedService: DownloadSpeedService,
) : Host("imgspice.com", 7, httpService, dataTransaction, downloadSpeedService) {
internal class ImgSpiceHost : Host("imgspice.com", 7) {
private val log by LoggerDelegate()
@Throws(HostException::class)
override fun resolve(
url: String,
document: Document,
image: ImageEntity,
context: ImageDownloadContext
): Pair<String, String> {
val document = fetchDocument(image.url, context)
val imgNode: Node = try {
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url))
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, image.url))
XpathUtils.getAsNode(document, IMG_XPATH)
} catch (e: XpathException) {
throw HostException(e)
@@ -33,11 +26,11 @@ internal class ImgSpiceHost(
String.format(
"Xpath '%s' cannot be found in '%s'",
IMG_XPATH,
url
image.url
)
)
return try {
log.debug(String.format("Resolving name and image url for %s", url))
log.debug(String.format("Resolving name and image url for %s", image.url))
val imgTitle = imgNode.attributes.getNamedItem("alt").textContent.trim { it <= ' ' }
val imgUrl = imgNode.attributes.getNamedItem("src").textContent.trim { it <= ' ' }
Pair(
@@ -1,31 +1,24 @@
package me.vripper.host
import me.vripper.entities.ImageEntity
import me.vripper.exception.HostException
import me.vripper.exception.XpathException
import me.vripper.services.DataTransaction
import me.vripper.services.DownloadService.ImageDownloadContext
import me.vripper.services.DownloadSpeedService
import me.vripper.services.HTTPService
import me.vripper.utilities.LoggerDelegate
import me.vripper.utilities.XpathUtils
import org.w3c.dom.Document
import org.w3c.dom.Node
internal class ImgboxHost(
httpService: HTTPService,
dataTransaction: DataTransaction,
downloadSpeedService: DownloadSpeedService,
) : Host("imgbox.com", 6, httpService, dataTransaction, downloadSpeedService) {
internal class ImgboxHost : Host("imgbox.com", 6) {
private val log by LoggerDelegate()
@Throws(HostException::class)
override fun resolve(
url: String,
document: Document,
image: ImageEntity,
context: ImageDownloadContext
): Pair<String, String> {
val document = fetchDocument(image.url, context)
val imgNode: Node = try {
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url))
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, image.url))
XpathUtils.getAsNode(document, IMG_XPATH)
} catch (e: XpathException) {
throw HostException(e)
@@ -33,11 +26,11 @@ internal class ImgboxHost(
String.format(
"Xpath '%s' cannot be found in '%s'",
IMG_XPATH,
url
image.url
)
)
return try {
log.debug(String.format("Resolving name and image url for %s", url))
log.debug(String.format("Resolving name and image url for %s", image.url))
val imgTitle = imgNode.attributes.getNamedItem("title").textContent.trim { it <= ' ' }
val imgUrl = imgNode.attributes.getNamedItem("src").textContent.trim { it <= ' ' }
Pair(imgTitle, imgUrl)
@@ -1,90 +1,71 @@
package me.vripper.host
import com.github.benmanes.caffeine.cache.Cache
import com.github.benmanes.caffeine.cache.Caffeine
import me.vripper.entities.ImageEntity
import me.vripper.exception.HostException
import me.vripper.exception.HtmlProcessorException
import me.vripper.exception.XpathException
import me.vripper.services.DataTransaction
import me.vripper.services.DownloadService.ImageDownloadContext
import me.vripper.services.DownloadSpeedService
import me.vripper.services.HTTPService
import me.vripper.utilities.HtmlUtils
import me.vripper.utilities.ApplicationProperties.IMX_SUBDOMAINS
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
import org.w3c.dom.Document
import org.w3c.dom.Node
import java.io.IOException
import me.vripper.utilities.RequestLimit
import org.apache.hc.client5.http.classic.methods.HttpHead
import org.apache.hc.client5.http.config.ConnectionConfig
import org.apache.hc.client5.http.impl.classic.HttpClients
import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager
import java.util.concurrent.TimeUnit
internal class ImxHost : Host("imx.to", 8) {
companion object {
private val resolvedHosts: Cache<Long, String> =
Caffeine.newBuilder().expireAfterAccess(5, TimeUnit.MINUTES).build()
}
internal class ImxHost(
private val httpService: HTTPService,
dataTransaction: DataTransaction,
downloadSpeedService: DownloadSpeedService,
) : Host("imx.to", 8, httpService, dataTransaction, downloadSpeedService) {
private val log by LoggerDelegate()
@Throws(HostException::class)
override fun resolve(
url: String,
document: Document,
image: ImageEntity,
context: ImageDownloadContext
): Pair<String, String> {
val httpsUrl = url.replace("http:", "https:")
var value: String? = null
try {
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("Resolving name and image url for ${image.url}")
val imgTitle = String.format("IMG_%04d", image.index + 1)
synchronized(image.postId.toString().intern()) {
val resolvedHost = resolvedHosts.getIfPresent(image.postId)
if (resolvedHost != null) {
val imgUrl = image.thumbUrl.replace("imx.to", resolvedHost).replace("u/t/", "i/")
.replace("t/", "i/")
return Pair(
imgTitle.ifEmpty { getDefaultImageName(imgUrl) }, imgUrl
)
} else {
IMX_SUBDOMAINS.forEach { subDomain ->
val imgUrl = image.thumbUrl.replace("imx.to", subDomain).replace("u/t/", "i/").replace("t/", "i/")
val result = runCatching {
RequestLimit.getPermit(1)
val httpHead = HttpHead(imgUrl).also { context.requests.add(it) }
HttpClients.custom().apply {
setConnectionManager(BasicHttpClientConnectionManager().apply {
connectionConfig = ConnectionConfig.custom()
.setConnectTimeout(5000, TimeUnit.MILLISECONDS)
.setSocketTimeout(5000, TimeUnit.MILLISECONDS)
.build()
})
}.build().execute(httpHead) { response ->
if (response.code / 100 != 2) {
throw HostException("Invalid response")
}
}
}
if (result.isSuccess) {
resolvedHosts.put(image.postId, subDomain)
return Pair(
imgTitle.ifEmpty { getDefaultImageName(imgUrl) }, imgUrl
)
}
}
}
} catch (e: XpathException) {
throw HostException(e)
}
if (value == null) {
throw HostException("Failed to obtain value attribute from continue input")
}
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 = try {
httpService.client.execute(
httpPost, context.httpContext
) { response ->
log.debug("Cleaning response for {}", httpPost)
HtmlUtils.clean(response.entity.content)
}
} catch (e: IOException) {
throw HostException(e)
} catch (e: HtmlProcessorException) {
throw HostException(e)
}
val imgNode: Node = try {
log.debug("Looking for xpath expression $IMG_XPATH in $httpsUrl")
XpathUtils.getAsNode(doc, IMG_XPATH)
} catch (e: XpathException) {
throw HostException(e)
} ?: throw HostException(
"Xpath $IMG_XPATH cannot be found in $httpsUrl"
)
return try {
log.debug("Resolving name and image url for $httpsUrl")
val imgTitle = imgNode.attributes.getNamedItem("alt").textContent.trim { it <= ' ' }
val imgUrl = imgNode.attributes.getNamedItem("src").textContent.trim { it <= ' ' }
Pair(
imgTitle.ifEmpty { getDefaultImageName(imgUrl) }, imgUrl
)
} catch (e: Exception) {
throw HostException("Unexpected error occurred", e)
}
}
companion object {
private const val CONTINUE_BUTTON_XPATH = "//*[@name='imgContinue']"
private const val IMG_XPATH = "//img[@class='centred']"
throw HostException("Unable to find full size image for ${image.url}")
}
}
@@ -1,35 +1,30 @@
package me.vripper.host
import me.vripper.entities.ImageEntity
import me.vripper.exception.HostException
import me.vripper.exception.XpathException
import me.vripper.services.DataTransaction
import me.vripper.services.DownloadService.ImageDownloadContext
import me.vripper.services.DownloadSpeedService
import me.vripper.services.HTTPService
import me.vripper.utilities.HtmlUtils
import me.vripper.utilities.LoggerDelegate
import me.vripper.utilities.XpathUtils
import org.w3c.dom.Document
import org.w3c.dom.Node
import java.net.URI
import java.net.URISyntaxException
internal class PimpandhostHost(
httpService: HTTPService,
dataTransaction: DataTransaction,
downloadSpeedService: DownloadSpeedService,
) : Host("pimpandhost.com", 9, httpService, dataTransaction, downloadSpeedService) {
internal class PimpandhostHost : Host("pimpandhost.com", 9) {
private val log by LoggerDelegate()
@Throws(HostException::class)
override fun resolve(
url: String,
document: Document,
image: ImageEntity,
context: ImageDownloadContext
): Pair<String, String> {
val newUrl: String
try {
newUrl = appendUri(url.replace("http://", "https://").replace("-medium(\\.html)?".toRegex(), ""), "size=original")
newUrl = appendUri(
image.url.replace("http://", "https://").replace("-medium(\\.html)?".toRegex(), ""),
"size=original"
)
} catch (e: Exception) {
throw HostException(e)
}
@@ -45,7 +40,7 @@ internal class PimpandhostHost(
String.format(
"Xpath '%s' cannot be found in '%s'",
IMG_XPATH,
url
image.url
)
)
return try {
@@ -1,31 +1,24 @@
package me.vripper.host
import me.vripper.entities.ImageEntity
import me.vripper.exception.HostException
import me.vripper.exception.XpathException
import me.vripper.services.DataTransaction
import me.vripper.services.DownloadService.ImageDownloadContext
import me.vripper.services.DownloadSpeedService
import me.vripper.services.HTTPService
import me.vripper.utilities.LoggerDelegate
import me.vripper.utilities.XpathUtils
import org.w3c.dom.Document
import org.w3c.dom.Node
internal class PixRouteHost(
httpService: HTTPService,
dataTransaction: DataTransaction,
downloadSpeedService: DownloadSpeedService,
) : Host("pixroute.com", 11, httpService, dataTransaction, downloadSpeedService) {
internal class PixRouteHost : Host("pixroute.com", 11) {
private val log by LoggerDelegate()
@Throws(HostException::class)
override fun resolve(
url: String,
document: Document,
image: ImageEntity,
context: ImageDownloadContext
): Pair<String, String> {
val document = fetchDocument(image.url, context)
val imgNode: Node = try {
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url))
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, image.url))
XpathUtils.getAsNode(document, IMG_XPATH)
} catch (e: XpathException) {
throw HostException(e)
@@ -33,11 +26,11 @@ internal class PixRouteHost(
String.format(
"Xpath '%s' cannot be found in '%s'",
IMG_XPATH,
url
image.url
)
)
return try {
log.debug(String.format("Resolving name and image url for %s", url))
log.debug(String.format("Resolving name and image url for %s", image.url))
Pair(imgNode.attributes.getNamedItem("alt").textContent.trim { it <= ' ' },
imgNode.attributes.getNamedItem("src").textContent.trim { it <= ' ' })
} catch (e: Exception) {
@@ -1,31 +1,24 @@
package me.vripper.host
import me.vripper.entities.ImageEntity
import me.vripper.exception.HostException
import me.vripper.exception.XpathException
import me.vripper.services.DataTransaction
import me.vripper.services.DownloadService.ImageDownloadContext
import me.vripper.services.DownloadSpeedService
import me.vripper.services.HTTPService
import me.vripper.utilities.LoggerDelegate
import me.vripper.utilities.XpathUtils
import org.w3c.dom.Document
import org.w3c.dom.Node
internal class PixhostHost(
httpService: HTTPService,
dataTransaction: DataTransaction,
downloadSpeedService: DownloadSpeedService,
) : Host("pixhost.to", 10, httpService, dataTransaction, downloadSpeedService) {
internal class PixhostHost : Host("pixhost.to", 10) {
private val log by LoggerDelegate()
@Throws(HostException::class)
override fun resolve(
url: String,
document: Document,
image: ImageEntity,
context: ImageDownloadContext
): Pair<String, String> {
val document = fetchDocument(image.url, context)
val imgNode: Node = try {
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url))
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, image.url))
XpathUtils.getAsNode(document, IMG_XPATH)
} catch (e: XpathException) {
throw HostException(e)
@@ -33,11 +26,11 @@ internal class PixhostHost(
String.format(
"Xpath '%s' cannot be found in '%s'",
IMG_XPATH,
url
image.url
)
)
return try {
log.debug(String.format("Resolving name and image url for %s", url))
log.debug(String.format("Resolving name and image url for %s", image.url))
val imgTitle = imgNode.attributes.getNamedItem("alt").textContent.trim { it <= ' ' }
val imgUrl = imgNode.attributes.getNamedItem("src").textContent.trim { it <= ' ' }
Pair(imgTitle.substring(imgTitle.indexOf('_') + 1), imgUrl)
@@ -1,30 +1,23 @@
package me.vripper.host
import me.vripper.entities.ImageEntity
import me.vripper.exception.HostException
import me.vripper.exception.XpathException
import me.vripper.services.DataTransaction
import me.vripper.services.DownloadService.ImageDownloadContext
import me.vripper.services.DownloadSpeedService
import me.vripper.services.HTTPService
import me.vripper.utilities.LoggerDelegate
import me.vripper.utilities.XpathUtils
import org.w3c.dom.Document
internal class PixxxelsHost(
httpService: HTTPService,
dataTransaction: DataTransaction,
downloadSpeedService: DownloadSpeedService,
) : Host("pixxxels.cc", 12, httpService, dataTransaction, downloadSpeedService) {
internal class PixxxelsHost : Host("pixxxels.cc", 12) {
private val log by LoggerDelegate()
@Throws(HostException::class)
override fun resolve(
url: String,
document: Document,
image: ImageEntity,
context: ImageDownloadContext
): Pair<String, String> {
val document = fetchDocument(image.url, context)
val imgNode = try {
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url))
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, image.url))
XpathUtils.getAsNode(document, IMG_XPATH)
} catch (e: XpathException) {
throw HostException(e)
@@ -32,11 +25,11 @@ internal class PixxxelsHost(
String.format(
"Xpath '%s' cannot be found in '%s'",
IMG_XPATH,
url
image.url
)
)
val titleNode = try {
log.debug(String.format("Looking for xpath expression %s in %s", TITLE_XPATH, url))
log.debug(String.format("Looking for xpath expression %s in %s", TITLE_XPATH, image.url))
XpathUtils.getAsNode(document, TITLE_XPATH)
} catch (e: XpathException) {
throw HostException(e)
@@ -44,11 +37,11 @@ internal class PixxxelsHost(
String.format(
"Xpath '%s' cannot be found in '%s'",
TITLE_XPATH,
url
image.url
)
)
return try {
log.debug(String.format("Resolving name and image url for %s", url))
log.debug(String.format("Resolving name and image url for %s", image.url))
val imgTitle = titleNode.textContent.trim { it <= ' ' }
val imgUrl = imgNode.attributes.getNamedItem("href").textContent.trim { it <= ' ' }
Pair(
@@ -1,32 +1,25 @@
package me.vripper.host
import me.vripper.entities.ImageEntity
import me.vripper.exception.HostException
import me.vripper.exception.XpathException
import me.vripper.services.DataTransaction
import me.vripper.services.DownloadService.ImageDownloadContext
import me.vripper.services.DownloadSpeedService
import me.vripper.services.HTTPService
import me.vripper.utilities.LoggerDelegate
import me.vripper.utilities.XpathUtils
import org.w3c.dom.Document
import org.w3c.dom.Node
import java.util.*
internal class PostImgHost(
httpService: HTTPService,
dataTransaction: DataTransaction,
downloadSpeedService: DownloadSpeedService,
) : Host("postimg.cc", 13, httpService, dataTransaction, downloadSpeedService) {
internal class PostImgHost : Host("postimg.cc", 13) {
private val log by LoggerDelegate()
@Throws(HostException::class)
override fun resolve(
url: String,
document: Document,
image: ImageEntity,
context: ImageDownloadContext
): Pair<String, String> {
val document = fetchDocument(image.url, context)
val titleNode = try {
log.debug(String.format("Looking for xpath expression %s in %s", TITLE_XPATH, url))
log.debug(String.format("Looking for xpath expression %s in %s", TITLE_XPATH, image.url))
XpathUtils.getAsNode(document, TITLE_XPATH)
} catch (e: XpathException) {
throw HostException(e)
@@ -34,11 +27,11 @@ internal class PostImgHost(
String.format(
"Xpath '%s' cannot be found in '%s'",
TITLE_XPATH,
url
image.url
)
)
val urlNode = try {
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url))
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, image.url))
XpathUtils.getAsNode(document, IMG_XPATH)
} catch (e: XpathException) {
throw HostException(e)
@@ -46,14 +39,14 @@ internal class PostImgHost(
String.format(
"Xpath '%s' cannot be found in '%s'",
IMG_XPATH,
url
image.url
)
)
return try {
log.debug(String.format("Resolving name and image url for %s", url))
log.debug(String.format("Resolving name and image url for %s", image.url))
val imgTitle = Optional.ofNullable(titleNode)
.map { node: Node -> node.textContent.trim { it <= ' ' } }
.orElseGet { getDefaultImageName(url) }
.orElseGet { getDefaultImageName(image.url) }
Pair(imgTitle, urlNode.attributes.getNamedItem("href").textContent.trim { it <= ' ' })
} catch (e: Exception) {
throw HostException("Unexpected error occurred", e)
@@ -1,47 +1,40 @@
package me.vripper.host
import me.vripper.entities.ImageEntity
import me.vripper.exception.HostException
import me.vripper.exception.XpathException
import me.vripper.services.DataTransaction
import me.vripper.services.DownloadService.ImageDownloadContext
import me.vripper.services.DownloadSpeedService
import me.vripper.services.HTTPService
import me.vripper.utilities.LoggerDelegate
import me.vripper.utilities.XpathUtils
import org.w3c.dom.Document
import org.w3c.dom.Node
internal class TurboImageHost(
httpService: HTTPService,
dataTransaction: DataTransaction,
downloadSpeedService: DownloadSpeedService,
) : Host("turboimagehost.com", 14, httpService, dataTransaction, downloadSpeedService) {
internal class TurboImageHost : Host("turboimagehost.com", 14) {
private val log by LoggerDelegate()
@Throws(HostException::class)
override fun resolve(
url: String,
document: Document,
image: ImageEntity,
context: ImageDownloadContext
): Pair<String, String> {
val document = fetchDocument(image.url, context)
var title: String?
title = try {
log.debug(String.format("Looking for xpath expression %s in %s", TITLE_XPATH, url))
log.debug(String.format("Looking for xpath expression %s in %s", TITLE_XPATH, image.url))
val titleNode: Node? = XpathUtils.getAsNode(document, TITLE_XPATH)
log.debug(String.format("Resolving name for %s", url))
log.debug(String.format("Resolving name for %s", image.url))
titleNode?.textContent?.trim { it <= ' ' }
} catch (e: XpathException) {
throw HostException(e)
}
if (title.isNullOrEmpty()) {
title = getDefaultImageName(url)
title = getDefaultImageName(image.url)
}
val urlNode: Node = XpathUtils.getAsNode(document, IMG_XPATH)
?: throw HostException(
String.format(
"Xpath '%s' cannot be found in '%s'",
IMG_XPATH,
url
image.url
)
)
return Pair(title, urlNode.attributes.getNamedItem("src").textContent.trim { it <= ' ' })
@@ -1,32 +1,25 @@
package me.vripper.host
import me.vripper.entities.ImageEntity
import me.vripper.exception.HostException
import me.vripper.exception.XpathException
import me.vripper.services.DataTransaction
import me.vripper.services.DownloadService.ImageDownloadContext
import me.vripper.services.DownloadSpeedService
import me.vripper.services.HTTPService
import me.vripper.utilities.LoggerDelegate
import me.vripper.utilities.XpathUtils
import org.w3c.dom.Document
import org.w3c.dom.Node
import java.util.*
internal class ViprImHost(
httpService: HTTPService,
dataTransaction: DataTransaction,
downloadSpeedService: DownloadSpeedService,
) : Host("vipr.im", 15, httpService, dataTransaction, downloadSpeedService) {
internal class ViprImHost : Host("vipr.im", 15) {
private val log by LoggerDelegate()
@Throws(HostException::class)
override fun resolve(
url: String,
document: Document,
image: ImageEntity,
context: ImageDownloadContext
): Pair<String, String> {
val document = fetchDocument(image.url, context)
val imgNode: Node = try {
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url))
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, image.url))
XpathUtils.getAsNode(document, IMG_XPATH)
} catch (e: XpathException) {
throw HostException(e)
@@ -34,11 +27,11 @@ internal class ViprImHost(
String.format(
"Xpath '%s' cannot be found in '%s'",
IMG_XPATH,
url
image.url
)
)
return try {
log.debug(String.format("Resolving name and image url for %s", url))
log.debug(String.format("Resolving name and image url for %s", image.url))
val imgTitle =
Optional.ofNullable(imgNode.attributes.getNamedItem("alt"))
.map { obj: Node -> obj.textContent }
@@ -5,7 +5,6 @@ import kotlinx.coroutines.flow.filterIsInstance
import me.vripper.event.EventBus
import me.vripper.event.SettingsUpdateEvent
import me.vripper.services.*
import me.vripper.utilities.DatabaseManager
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -38,7 +37,6 @@ object AppManager : KoinComponent {
threadCacheService.invalidate()
}
}
DatabaseManager.connect()
dataTransaction.setDownloadingToStopped()
dataTransaction.stopImagesByPostIdAndIsNotCompleted()
settingsService.init()
@@ -49,11 +47,8 @@ object AppManager : KoinComponent {
fun stop() {
job?.cancel()
if (DatabaseManager.isConnected()) {
downloadService.halt()
downloadService.stop()
downloadSpeedService.halt()
DatabaseManager.disconnect()
}
downloadService.halt()
downloadService.stop()
downloadSpeedService.halt()
}
}
@@ -451,4 +451,8 @@ internal class AppEndpointService(
"Successfully imported $postsCount posts and $threadCount threads"
}
}
override fun connectionState(): String {
return ""
}
}
@@ -19,13 +19,15 @@ import me.vripper.host.ImageMimeType
import me.vripper.model.ErrorCount
import me.vripper.model.QueueState
import me.vripper.model.Settings
import me.vripper.utilities.ApplicationProperties
import me.vripper.utilities.LoggerDelegate
import me.vripper.utilities.PathUtils.getExtension
import me.vripper.utilities.PathUtils.getFileNameWithoutExtension
import me.vripper.utilities.PathUtils.sanitize
import me.vripper.utilities.downloadRunner
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase
import org.apache.hc.client5.http.cookie.BasicCookieStore
import org.apache.hc.client5.http.cookie.Cookie
import org.apache.hc.client5.http.impl.cookie.BasicClientCookie
import org.apache.hc.client5.http.protocol.HttpClientContext
import org.jetbrains.exposed.sql.transactions.transaction
import org.koin.core.component.KoinComponent
@@ -34,11 +36,14 @@ import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.time.Instant
import java.util.*
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import kotlin.io.path.Path
import kotlin.io.path.listDirectoryEntries
import kotlin.io.path.pathString
import kotlin.io.path.readLines
internal class DownloadService(
private val settingsService: SettingsService,
@@ -46,6 +51,7 @@ internal class DownloadService(
private val retryPolicyService: RetryPolicyService,
private val eventBus: EventBus
) {
private val maxPoolSize: Int = 24
private val log by LoggerDelegate()
private val running: MutableMap<Byte, MutableList<ImageDownloadRunnable>> = mutableMapOf()
@@ -56,10 +62,52 @@ internal class DownloadService(
private var downloadMonitorThread: Thread? = null
internal class ImageDownloadContext(val imageEntity: ImageEntity, val settings: Settings) : KoinComponent {
private val log by LoggerDelegate()
init {
ApplicationProperties.VRIPPER_DIR
.listDirectoryEntries()
.filter { it.fileName.pathString.startsWith("cookies") }
.forEach { cookiesPath ->
loadCookies(cookiesPath).also { cookies ->
cookies.forEach { cookie ->
if (HTTPService.cookieStore.cookies.find { it.name == cookie.name } == null) {
log.info("Applying cookie: ${cookie.name}")
HTTPService.cookieStore.addCookie(cookie)
} else {
log.warn("Cookie already loaded: ${cookie.name}")
}
}
}
}
}
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val jobs = mutableListOf<Job>()
val httpContext: HttpClientContext =
HttpClientContext.create().apply { cookieStore = BasicCookieStore() }
val httpContext: HttpClientContext = HttpClientContext.create().apply {
cookieStore = HTTPService.cookieStore
}
private fun loadCookies(cookiesPath: Path): List<Cookie> {
return cookiesPath.readLines()
.map { it.trim() }
.filter { it.isNotBlank() }
.filter { !it.startsWith("#") }
.map { line ->
val cookieComponents = line.split("\t")
BasicClientCookie(cookieComponents[5], cookieComponents[6]).apply {
domain = cookieComponents[0]
isHttpOnly = cookieComponents[1].toBoolean()
path = cookieComponents[2]
isSecure = cookieComponents[3].toBoolean()
setExpiryDate(Instant.ofEpochSecond(cookieComponents[4].toLong()))
}
}.also {
log.info("Found ${it.size} cookies in $cookiesPath")
}
}
val requests = mutableListOf<HttpUriRequestBase>()
val postId = imageEntity.postIdRef
@@ -102,7 +150,7 @@ internal class DownloadService(
}
log.debug("Getting image url and name from ${imageEntity.url} using ${imageEntity.host}")
val host = hosts.first { it.isSupported(imageEntity.url) }
val downloadedImage = host.downloadInternal(imageEntity.url, context)
val downloadedImage = host.downloadInternal(imageEntity, context)
log.debug("Resolved name for ${imageEntity.url}: ${downloadedImage.name}")
log.debug("Downloaded image {} to {}", imageEntity.url, downloadedImage.path)
synchronized(imageEntity.postId.toString().intern()) {
@@ -206,6 +254,7 @@ internal class DownloadService(
fun init() {
downloadMonitorThread?.interrupt()
downloadMonitorThread = Thread.ofVirtual().name("Download Monitor").unstarted(Runnable {
log.info("Scheduler have been initialized")
val accepted: MutableList<ImageDownloadRunnable> = mutableListOf()
val candidates: MutableList<ImageDownloadRunnable> = mutableListOf()
while (!Thread.currentThread().isInterrupted) {
@@ -231,6 +280,7 @@ internal class DownloadService(
}
}
}
log.info("Scheduler have been shutdown")
})
downloadMonitorThread?.start()
}
@@ -4,6 +4,7 @@ import kotlinx.coroutines.*
import me.vripper.utilities.ApplicationProperties
import org.apache.hc.client5.http.config.ConnectionConfig
import org.apache.hc.client5.http.config.RequestConfig
import org.apache.hc.client5.http.cookie.BasicCookieStore
import org.apache.hc.client5.http.cookie.StandardCookieSpec
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient
@@ -16,6 +17,9 @@ import org.apache.hc.core5.pool.PoolReusePolicy
import org.apache.hc.core5.util.Timeout
internal class HTTPService {
companion object {
val cookieStore = BasicCookieStore()
}
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private var pcm: HttpClientConnectionManager = BasicHttpClientConnectionManager()
@@ -64,6 +68,7 @@ internal class HTTPService {
.setUserAgent(ApplicationProperties.USER_AGENT)
.disableAutomaticRetries()
.setDefaultRequestConfig(rc)
.setDefaultCookieStore(cookieStore)
.build()
}
}
}
@@ -44,4 +44,5 @@ interface IAppEndpointService {
suspend fun renameToFirst(postIds: List<Long>)
suspend fun dbMigration(): String
suspend fun initLogger()
fun connectionState(): String
}
@@ -22,6 +22,7 @@ internal class AddPostTask(private val items: List<ThreadPostId>) : KoinComponen
val toProcess = mutableListOf<PostItem>()
for ((threadId, postId) in items) {
if (dataTransaction.exists(postId)) {
log.info("Post $postId already loaded")
continue
}
@@ -56,6 +57,10 @@ internal class AddPostTask(private val items: List<ThreadPostId>) : KoinComponen
toProcess.add(postItem)
}
if (toProcess.isEmpty()) {
return
}
val posts = try {
dataTransaction.newPosts(toProcess.toList())
} catch (e: Exception) {
@@ -27,7 +27,10 @@ object ApplicationProperties {
ignoreUnknownKeys = true
}
const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0"
val USER_AGENT: String;
val USER_AGENT: String
const val DEFAULT_IMX_SUBDOMAINS = "i.imx.to,i001.imx.to,i002.imx.to,i004.imx.to,i006.imx.to,i003.imx.to"
val IMX_SUBDOMAINS: Set<String>
@Serializable
internal data class ReleaseResponse(@SerialName("tag_name") val tagName: String)
@@ -35,12 +38,19 @@ object ApplicationProperties {
init {
Files.createDirectories(VRIPPER_DIR)
System.setProperty("VRIPPER_DIR", VRIPPER_DIR.toRealPath().pathString)
val userAgentOverride = VRIPPER_DIR.resolve("user-agent")
val userAgentOverride = VRIPPER_DIR.resolve("user-agent.txt")
USER_AGENT = if (userAgentOverride.exists()) {
userAgentOverride.readLines().firstOrNull() ?: DEFAULT_USER_AGENT
} else {
DEFAULT_USER_AGENT
}
val imxSubdomainsOverride = VRIPPER_DIR.resolve("imx-subdomains.txt")
IMX_SUBDOMAINS = if (imxSubdomainsOverride.exists()) {
imxSubdomainsOverride.readLines().firstOrNull() ?: DEFAULT_IMX_SUBDOMAINS
} else {
DEFAULT_IMX_SUBDOMAINS
}.split(",").toSet()
}
fun latestVersion(): String {
@@ -7,34 +7,15 @@ import liquibase.database.DatabaseFactory
import liquibase.database.jvm.JdbcConnection
import liquibase.resource.ClassLoaderResourceAccessor
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.transactions.TransactionManager
import java.sql.Connection
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
object DatabaseManager {
private var database: Database? = null
private var connected: Boolean = false
private var lock = ReentrantLock()
fun connect() {
lock.withLock {
database =
Database.connect("jdbc:sqlite:${ApplicationProperties.VRIPPER_DIR}/vripper.db")
.also { update(it.connector.invoke().connection as Connection) }
connected = true
}
}
fun disconnect() {
lock.withLock {
database?.let { TransactionManager.closeAndUnregister(it) }
connected = false
}
}
fun isConnected(): Boolean {
return lock.withLock { connected }
database =
Database.connect("jdbc:sqlite:${ApplicationProperties.VRIPPER_DIR}/vripper.db")
.also { update(it.connector.invoke().connection as Connection) }
}
private fun update(connection: Connection) {
@@ -39,7 +39,7 @@ class VripperGuiApplication : App(
override fun start(stage: Stage) {
Thread.setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler { t, e ->
log.error("Thread $t threw an exception", e)
log.error("Thread $t threw an exception: ${e.message}", e)
})
if (widgetsController.currentSettings.darkMode) {
setUserAgentStylesheet(CupertinoDark().userAgentStylesheet)
@@ -17,7 +17,7 @@ class ConnectionSettingsFragment : Fragment("Connection Settings") {
init {
coroutineScope.launch {
async { connectionSettings = settingsController.findConnectionSettings() }.await()
connectionSettings = settingsController.findConnectionSettings()
runLater {
with(root) {
form {
@@ -21,16 +21,15 @@ class DownloadSettingsFragment : Fragment("Download Settings") {
init {
coroutineScope.launch {
async {
downloadSettings = settingsController.findDownloadSettings()
downloadSettingsModel.downloadPath = downloadSettings.downloadPath
downloadSettingsModel.autoStart = downloadSettings.autoStart
downloadSettingsModel.forceOrder = downloadSettings.forceOrder
downloadSettingsModel.forumSubfolder = downloadSettings.forumSubDirectory
downloadSettingsModel.threadSubLocation = downloadSettings.threadSubLocation
downloadSettingsModel.clearCompleted = downloadSettings.clearCompleted
downloadSettingsModel.appendPostId = downloadSettings.appendPostId
}.await()
downloadSettings = settingsController.findDownloadSettings() ?: DownloadSettings()
downloadSettingsModel.downloadPath = downloadSettings.downloadPath
downloadSettingsModel.autoStart = downloadSettings.autoStart
downloadSettingsModel.forceOrder = downloadSettings.forceOrder
downloadSettingsModel.forumSubfolder = downloadSettings.forumSubDirectory
downloadSettingsModel.threadSubLocation = downloadSettings.threadSubLocation
downloadSettingsModel.clearCompleted = downloadSettings.clearCompleted
downloadSettingsModel.appendPostId = downloadSettings.appendPostId
runLater {
with(root) {
form {
@@ -7,18 +7,17 @@ import javafx.scene.control.RadioButton
import javafx.scene.control.Spinner
import javafx.scene.control.ToggleGroup
import javafx.scene.layout.VBox
import kotlinx.coroutines.*
import kotlinx.coroutines.runBlocking
import me.vripper.gui.VripperGuiApplication
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.services.GrpcEndpointService
import me.vripper.gui.utils.ActiveUICoroutines
import me.vripper.gui.utils.AppEndpointManager
import me.vripper.listeners.AppManager
import tornadofx.*
class SessionFragment : Fragment("Change Session") {
private val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val widgetsController: WidgetsController by inject()
private val grpcEndpointService: GrpcEndpointService by di("remoteAppEndpointService")
private val toggleGroup = ToggleGroup()
@@ -67,57 +66,42 @@ class SessionFragment : Fragment("Change Session") {
isDefaultButton = true
action {
val selectedToggle = toggleGroup.selectedToggle
if (selectedToggle == null) {
VripperGuiApplication.APP_INSTANCE.stop()
runBlocking {
GuiEventBus.publishEvent(GuiEventBus.ChangingSession)
AppManager.stop()
grpcEndpointService.disconnect()
}
when ((selectedToggle as RadioButton).id) {
"localSession" -> {
coroutineScope.launch {
AppManager.stop()
runBlocking {
widgetsController.currentSettings.localSession = true
GuiEventBus.publishEvent(GuiEventBus.ChangingSession)
while (ActiveUICoroutines.all().isNotEmpty()) {
delay(200)
}
grpcEndpointService.disconnect()
AppEndpointManager.set(GuiEventBus.LocalSession)
AppManager.start()
GuiEventBus.publishEvent(GuiEventBus.LocalSession)
runLater {
close()
}
}
}
"remoteSession" -> {
coroutineScope.launch {
AppManager.stop()
runBlocking {
widgetsController.currentSettings.localSession = false
GuiEventBus.publishEvent(GuiEventBus.ChangingSession)
while (ActiveUICoroutines.all().isNotEmpty()) {
delay(200)
}
AppEndpointManager.set(GuiEventBus.RemoteSession)
grpcEndpointService.connect(
widgetsController.currentSettings.remoteSessionModel.host,
widgetsController.currentSettings.remoteSessionModel.port,
widgetsController.currentSettings.remoteSessionModel.passcode,
)
GuiEventBus.publishEvent(GuiEventBus.RemoteSession)
runLater {
close()
}
}
}
else -> VripperGuiApplication.APP_INSTANCE.stop()
}
runLater {
close()
}
}
}
}
}
}
}
override fun onUndock() {
coroutineScope.cancel()
}
}
@@ -21,10 +21,8 @@ class SystemSettingsFragment : Fragment("System Settings") {
init {
coroutineScope.launch {
async {
systemSettings = settingsController.findSystemSettings()
systemSettingsModel.tempPath = systemSettings.tempPath
}.await()
systemSettings = settingsController.findSystemSettings()
systemSettingsModel.tempPath = systemSettings.tempPath
runLater {
with(root) {
form {
@@ -20,16 +20,14 @@ class ViperSettingsFragment : Fragment("Viper Settings") {
init {
coroutineScope.launch {
async {
viperGirlsSettings = settingsController.findViperGirlsSettings()
viperSettingsModel.username = viperGirlsSettings.username
viperSettingsModel.password = viperGirlsSettings.password
viperSettingsModel.thanks = viperGirlsSettings.thanks
viperSettingsModel.host = viperGirlsSettings.host
viperSettingsModel.requestLimit = viperGirlsSettings.requestLimit
viperSettingsModel.fetchMetadata = viperGirlsSettings.fetchMetadata
proxies.addAll(settingsController.getProxies())
}.await()
viperGirlsSettings = settingsController.findViperGirlsSettings()
viperSettingsModel.username = viperGirlsSettings.username
viperSettingsModel.password = viperGirlsSettings.password
viperSettingsModel.thanks = viperGirlsSettings.thanks
viperSettingsModel.host = viperGirlsSettings.host
viperSettingsModel.requestLimit = viperGirlsSettings.requestLimit
viperSettingsModel.fetchMetadata = viperGirlsSettings.fetchMetadata
proxies.addAll(settingsController.getProxies())
runLater {
with(root) {
form {
@@ -1,6 +1,5 @@
package me.vripper.gui.components.views
import io.grpc.StatusException
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleIntegerProperty
import javafx.geometry.Orientation
@@ -8,14 +7,11 @@ import javafx.scene.control.ButtonType
import javafx.scene.control.ContentDisplay
import javafx.scene.layout.Priority
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.catch
import me.vripper.gui.components.Shared
import me.vripper.gui.components.fragments.AddLinksFragment
import me.vripper.gui.components.fragments.SettingsFragment
import me.vripper.gui.controller.ActionBarController
import me.vripper.gui.controller.PostController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.utils.ActiveUICoroutines
import me.vripper.services.IAppEndpointService
import org.kordamp.ikonli.feather.Feather
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.*
@@ -24,31 +20,13 @@ class ActionBarView : View() {
private val downloadActiveProperty = SimpleBooleanProperty(true)
private val postController: PostController by inject()
private val postsTableView: PostsTableView by inject()
private val actionBarController: ActionBarController by inject()
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val grpcEndpointService: IAppEndpointService by di("remoteAppEndpointService")
private val localEndpointService: IAppEndpointService by di("localAppEndpointService")
private val running = SimpleIntegerProperty(0)
override val root = toolbar {}
init {
coroutineScope.launch {
GuiEventBus.events.collect { event ->
when (event) {
is GuiEventBus.LocalSession -> {
connect(localEndpointService)
}
is GuiEventBus.RemoteSession -> {
connect(grpcEndpointService)
}
is GuiEventBus.ChangingSession -> {
ActiveUICoroutines.cancelActionBar()
}
}
}
}
with(root) {
id = "action_toolbar"
padding = insets(all = 5)
@@ -125,25 +103,13 @@ class ActionBarView : View() {
}
}
downloadActiveProperty.bind(running.greaterThan(0))
}
private fun connect(appEndpointService: IAppEndpointService) {
coroutineScope.launch {
appEndpointService.onQueueStateUpdate().catch {
ActiveUICoroutines.removeFromActionBar(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connect(appEndpointService)
}
}
}.collect {
actionBarController.onQueueStateUpdate.collect {
runLater {
running.set(it.running)
}
}
}.also { runBlocking { ActiveUICoroutines.addToActionBar(it) } }
}
}
}
@@ -11,7 +11,6 @@ import javafx.scene.control.cell.TextFieldTableCell
import javafx.scene.input.MouseButton
import javafx.util.Callback
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.javafx.asFlow
import me.vripper.entities.Status
@@ -20,12 +19,9 @@ import me.vripper.gui.components.cells.ProgressTableCell
import me.vripper.gui.components.cells.StatusTableCell
import me.vripper.gui.controller.ImageController
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.model.ImageModel
import me.vripper.gui.utils.ActiveUICoroutines
import me.vripper.gui.utils.Preview
import me.vripper.gui.utils.openLink
import me.vripper.services.IAppEndpointService
import org.kordamp.ikonli.feather.Feather
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.*
@@ -36,39 +32,14 @@ class ImagesTableView : View("Photos") {
private val tableView: TableView<ImageModel>
private val imageController: ImageController by inject()
private val widgetsController: WidgetsController by inject()
private val localAppEndpointService: IAppEndpointService by di("localAppEndpointService")
private val remoteAppEndpointService: IAppEndpointService by di("remoteAppEndpointService")
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val items: ObservableList<ImageModel> = FXCollections.observableArrayList()
private var preview: Preview? = null
val jobs = mutableListOf<Job>()
override val root = vbox(alignment = Pos.CENTER_RIGHT) {}
init {
if (widgetsController.currentSettings.localSession) {
imageController.appEndpointService = localAppEndpointService
} else {
imageController.appEndpointService = remoteAppEndpointService
}
coroutineScope.launch {
GuiEventBus.events.collect { event ->
when (event) {
is GuiEventBus.LocalSession -> {
imageController.appEndpointService = localAppEndpointService
}
is GuiEventBus.RemoteSession -> {
imageController.appEndpointService = remoteAppEndpointService
}
is GuiEventBus.ChangingSession -> {
ActiveUICoroutines.cancelImages()
}
}
}
}
with(root) {
tableView = tableview(items) {
isTableMenuButtonVisible = true
@@ -271,7 +242,10 @@ class ImagesTableView : View("Photos") {
}
fun setPostId(postId: Long?) {
runBlocking { ActiveUICoroutines.cancelImages() }
runBlocking {
jobs.forEach { it.cancelAndJoin() }
jobs.clear()
}
runLater {
items.clear()
}
@@ -279,19 +253,16 @@ class ImagesTableView : View("Photos") {
return
}
coroutineScope.launch {
val list = coroutineScope.async {
imageController.findImages(postId)
}.await()
val list = imageController.findImages(postId)
runLater {
items.addAll(list)
tableView.sort()
tableView.placeholder = Label("No content in table")
}
}
coroutineScope.launch {
imageController.onUpdateImages(postId).catch {
ActiveUICoroutines.removeFromImages(currentCoroutineContext().job)
}.collect { image ->
imageController.onUpdateImages(postId).collect { image ->
runLater {
val imageModel = items.find { it.id == image.id } ?: return@runLater
@@ -304,12 +275,10 @@ class ImagesTableView : View("Photos") {
)
}
}
}.also { runBlocking { ActiveUICoroutines.addToImages(it) } }
}.also { jobs.add(it) }
coroutineScope.launch {
imageController.onStopped().catch {
ActiveUICoroutines.removeFromImages(currentCoroutineContext().job)
}.collect {
imageController.onStopped().collect {
runLater {
items.forEach { imageModel ->
if (imageModel.status != Status.FINISHED.name) {
@@ -318,6 +287,6 @@ class ImagesTableView : View("Photos") {
}
}
}
}.also { runBlocking { ActiveUICoroutines.addToImages(it) } }
}.also { jobs.add(it) }
}
}
@@ -7,8 +7,11 @@ import kotlinx.coroutines.flow.filterIsInstance
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.services.GrpcEndpointService
import me.vripper.gui.utils.AppEndpointManager
import me.vripper.gui.utils.ClipboardManager
import me.vripper.gui.utils.Watcher
import me.vripper.listeners.AppManager
import me.vripper.utilities.DatabaseManager
import tornadofx.*
class LoadingView : View("VRipper") {
@@ -20,11 +23,15 @@ class LoadingView : View("VRipper") {
override val root = borderpane {}
init {
DatabaseManager.connect()
ClipboardManager.init()
coroutineScope.launch {
GuiEventBus.events.filterIsInstance(GuiEventBus.ApplicationInitialized::class).collect {
if (widgetsController.currentSettings.localSession) {
AppEndpointManager.set(GuiEventBus.LocalSession)
AppManager.start()
} else {
AppEndpointManager.set(GuiEventBus.RemoteSession)
grpcEndpointService.connect(
widgetsController.currentSettings.remoteSessionModel.host,
widgetsController.currentSettings.remoteSessionModel.port,
@@ -2,12 +2,10 @@ package me.vripper.gui.components.views
import atlantafx.base.theme.Styles
import atlantafx.base.theme.Tweaks
import io.grpc.StatusException
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.scene.control.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.javafx.asFlow
import me.vripper.gui.components.fragments.LogMessageFragment
@@ -15,8 +13,6 @@ import me.vripper.gui.controller.LogController
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.model.LogModel
import me.vripper.gui.utils.ActiveUICoroutines
import me.vripper.services.IAppEndpointService
import org.kordamp.ikonli.feather.Feather
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.*
@@ -25,12 +21,10 @@ class LogTableView : View() {
private val logController: LogController by inject()
private val widgetsController: WidgetsController by inject()
private val localAppEndpointService: IAppEndpointService by di("localAppEndpointService")
private val remoteAppEndpointService: IAppEndpointService by di("remoteAppEndpointService")
private val tableView: TableView<LogModel>
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val items: ObservableList<LogModel> = FXCollections.observableArrayList()
private var maxLogEvent = 0
private var maxLogEvent = 100
override val root = vbox {}
@@ -158,98 +152,59 @@ class LogTableView : View() {
tableView.prefHeightProperty().bind(root.heightProperty())
tableView.placeholder = Label("Loading")
tableView.sortOrder.add(tableView.columns.first { it.id == "time" })
}
override fun onDock() {
coroutineScope.launch {
GuiEventBus.events.collect { event ->
when (event) {
is GuiEventBus.LocalSession -> {
runLater {
tableView.placeholder = Label("No Content in table")
launch {
GuiEventBus.events.collect {
when (it) {
GuiEventBus.LocalSession, GuiEventBus.RemoteSession -> {
while (isActive) {
val result = runCatching { logController.getMaxEventLog() }
if (result.isSuccess) {
maxLogEvent = result.getOrNull()!!
break
}
}
while (isActive) {
val result = runCatching { logController.initLogger() }
if (result.isSuccess) {
break
}
}
}
logController.appEndpointService = localAppEndpointService
connect()
}
is GuiEventBus.RemoteSession -> {
runLater {
tableView.placeholder = Label("No Content in table")
}
logController.appEndpointService = remoteAppEndpointService
connect()
}
is GuiEventBus.ChangingSession -> {
ActiveUICoroutines.cancelLog()
runLater {
tableView.placeholder = Label("Loading")
GuiEventBus.ChangingSession -> runLater {
items.clear()
tableView.placeholder = Label("Loading")
}
else -> {}
}
}
}
launch {
logController.newLogs.collect {
runLater {
items.sortWith(Comparator.comparing { it.sequence })
while (items.isNotEmpty() && (items.size >= maxLogEvent)) {
items.removeFirst()
}
items.add(it)
tableView.sort()
}
}
}
launch {
logController.updateSettings.collect {
maxLogEvent = it.systemSettings.maxEventLog
}
}
}
}
private fun connect() {
runBlocking {
maxLogEvent = logController.appEndpointService.getSettings().systemSettings.maxEventLog
}
connectToNewLogs()
connectToSettingsUpdate()
runLater {
runBlocking {
logController.initLogger()
}
}
}
private fun connectToSettingsUpdate() {
coroutineScope.launch {
logController.onUpdateSettings().catch {
ActiveUICoroutines.removeFromLog(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToSettingsUpdate()
}
}
}.collect {
maxLogEvent = it.systemSettings.maxEventLog
}
}.also { runBlocking { ActiveUICoroutines.addToLog(it) } }
}
private fun connectToNewLogs() {
coroutineScope.launch {
logController.onNewLog().catch {
ActiveUICoroutines.removeFromLog(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToNewLogs()
}
}
}.collect {
runLater {
items.sortWith(Comparator.comparing { it.sequence })
while (items.isNotEmpty() && (items.size >= maxLogEvent)) {
items.removeFirst()
}
items.add(it)
tableView.sort()
}
}
}.also { runBlocking { ActiveUICoroutines.addToLog(it) } }
}
private fun openLog(item: LogModel) {
find<LogMessageFragment>(mapOf(LogMessageFragment::logModel to item)).openModal()?.apply {
minWidth = 100.0
@@ -1,6 +1,5 @@
package me.vripper.gui.components.views
import io.grpc.StatusException
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleIntegerProperty
import javafx.scene.control.ButtonType
@@ -8,16 +7,14 @@ import javafx.scene.input.KeyCode
import javafx.scene.input.KeyCodeCombination
import javafx.scene.input.KeyCombination
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.catch
import me.vripper.gui.VripperGuiApplication
import me.vripper.gui.components.fragments.AboutFragment
import me.vripper.gui.components.fragments.AddLinksFragment
import me.vripper.gui.components.fragments.SessionFragment
import me.vripper.gui.components.fragments.SettingsFragment
import me.vripper.gui.controller.ActionBarController
import me.vripper.gui.controller.PostController
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.utils.ActiveUICoroutines
import me.vripper.gui.utils.openLink
import me.vripper.services.IAppEndpointService
import me.vripper.utilities.ApplicationProperties
@@ -31,33 +28,13 @@ class MenuBarView : View() {
private val postsTableView: PostsTableView by inject()
private val widgetsController: WidgetsController by inject()
private val postController: PostController by inject()
private val grpcEndpointService: IAppEndpointService by di("remoteAppEndpointService")
private val localEndpointService: IAppEndpointService by di("localAppEndpointService")
private val actionBarController: ActionBarController by inject()
private lateinit var appEndpointService: IAppEndpointService
private val running = SimpleIntegerProperty(0)
override val root = menubar {}
init {
coroutineScope.launch {
GuiEventBus.events.collect { event ->
when (event) {
is GuiEventBus.LocalSession -> {
appEndpointService = localEndpointService
connect(localEndpointService)
}
is GuiEventBus.RemoteSession -> {
appEndpointService = grpcEndpointService
connect(grpcEndpointService)
}
is GuiEventBus.ChangingSession -> {
ActiveUICoroutines.cancelMenuBar()
}
}
}
}
with(root) {
menu("File") {
item("Add links", KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN)).apply {
@@ -165,7 +142,8 @@ class MenuBarView : View() {
title = "Database Migration"
) {
coroutineScope.launch {
val message = appEndpointService.dbMigration()
val message =
runCatching { appEndpointService.dbMigration() }.getOrDefault("Migration failed")
runLater {
information(
header = "",
@@ -221,25 +199,13 @@ class MenuBarView : View() {
}
}
downloadActiveProperty.bind(running.greaterThan(0))
}
private fun connect(appEndpointService: IAppEndpointService) {
coroutineScope.launch {
appEndpointService.onQueueStateUpdate().catch {
ActiveUICoroutines.removeFromMenuBar(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connect(appEndpointService)
}
}
}.collect {
actionBarController.onQueueStateUpdate.collect {
runLater {
running.set(it.running)
}
}
}.also { runBlocking { ActiveUICoroutines.addToMenuBar(it) } }
}
}
}
@@ -2,14 +2,13 @@ package me.vripper.gui.components.views
import javafx.collections.FXCollections
import javafx.scene.control.TabPane
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.launch
import me.vripper.gui.controller.PostController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.model.PostModel
import me.vripper.gui.utils.ActiveUICoroutines
import org.kordamp.ikonli.feather.Feather
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.*
@@ -25,11 +24,6 @@ class PostInfoView : View() {
override val root = tabpane()
init {
coroutineScope.launch {
GuiEventBus.events.filterIsInstance(GuiEventBus.ChangingSession::class).collect {
ActiveUICoroutines.cancelPostInfo()
}
}
with(root) {
id = "postinfo_panel"
tabClosingPolicy = TabPane.TabClosingPolicy.UNAVAILABLE
@@ -84,7 +78,6 @@ class PostInfoView : View() {
}
fun setPostId(postId: Long?) {
runBlocking { ActiveUICoroutines.cancelPostInfo() }
imagesTableView.setPostId(postId)
if (postId == null) {
postModel.apply {
@@ -108,13 +101,7 @@ class PostInfoView : View() {
return
}
coroutineScope.launch {
val model: PostModel? = async {
try {
postController.find(postId)
} catch (e: Exception) {
null
}
}.await()
val model: PostModel? = postController.find(postId)
if (model == null) {
return@launch
}
@@ -140,9 +127,7 @@ class PostInfoView : View() {
}
}
coroutineScope.launch {
postController.onUpdatePosts().catch {
ActiveUICoroutines.removeFromPostInfo(currentCoroutineContext().job)
}.filter {
postController.updatePostsFlow.filter {
it.postId == postModel.postId
}.collect { post ->
runLater {
@@ -159,12 +144,10 @@ class PostInfoView : View() {
postModel.folderName = post.folderName
}
}
}.also { runBlocking { ActiveUICoroutines.addToPostInfo(it) } }
}
coroutineScope.launch {
postController.onUpdateMetadata().catch {
ActiveUICoroutines.removeFromPostInfo(currentCoroutineContext().job)
}.filter {
postController.updateMetadataFlow.filter {
it.postId == postModel.postId
}.collect {
runLater {
@@ -172,6 +155,6 @@ class PostInfoView : View() {
postModel.postedBy = it.data.postedBy
}
}
}.also { runBlocking { ActiveUICoroutines.addToPostInfo(it) } }
}
}
}
@@ -2,7 +2,6 @@ package me.vripper.gui.components.views
import atlantafx.base.theme.Styles
import atlantafx.base.theme.Tweaks
import io.grpc.StatusException
import javafx.collections.FXCollections
import javafx.event.EventHandler
import javafx.geometry.Pos
@@ -12,10 +11,13 @@ import javafx.scene.input.KeyCode
import javafx.scene.input.KeyEvent
import javafx.scene.input.MouseButton
import javafx.util.Callback
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.javafx.asFlow
import kotlinx.coroutines.launch
import me.vripper.gui.components.Shared
import me.vripper.gui.components.cells.PreviewTableCell
import me.vripper.gui.components.cells.ProgressTableCell
@@ -26,13 +28,9 @@ import me.vripper.gui.controller.PostController
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.model.PostModel
import me.vripper.gui.services.ClipboardService
import me.vripper.gui.services.GrpcEndpointService
import me.vripper.gui.utils.ActiveUICoroutines
import me.vripper.gui.utils.Preview
import me.vripper.gui.utils.openFileDirectory
import me.vripper.gui.utils.openLink
import me.vripper.services.IAppEndpointService
import org.kordamp.ikonli.feather.Feather
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.*
@@ -42,10 +40,7 @@ class PostsTableView : View() {
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val postController: PostController by inject()
private val widgetsController: WidgetsController by inject()
private val clipboardService: ClipboardService by inject()
private val mainView: MainView by inject()
private val localAppEndpointService: IAppEndpointService by di("localAppEndpointService")
private val remoteAppEndpointService: GrpcEndpointService by di("remoteAppEndpointService")
val tableView: TableView<PostModel>
var items: SortedFilteredList<PostModel> = SortedFilteredList()
@@ -62,28 +57,6 @@ class PostsTableView : View() {
|| item.status.contains(query, ignoreCase = true)
|| item.path.contains(query, ignoreCase = true)
}
coroutineScope.launch {
GuiEventBus.events.collect { event ->
when (event) {
is GuiEventBus.LocalSession -> {
postController.appEndpointService = localAppEndpointService
connect()
}
is GuiEventBus.RemoteSession -> {
postController.appEndpointService = remoteAppEndpointService
connect()
}
is GuiEventBus.ChangingSession -> {
ActiveUICoroutines.cancelPosts()
runLater {
tableView.placeholder = Label("Loading")
}
}
}
}
}
with(root) {
tableView = tableview(items) {
@@ -391,126 +364,83 @@ class PostsTableView : View() {
}
tableView.prefHeightProperty().bind(root.heightProperty())
tableView.placeholder = Label("Loading")
coroutineScope.launch {
launch {
GuiEventBus.events.collect {
when (it) {
GuiEventBus.LocalSession, GuiEventBus.RemoteSession -> {
val postModelList = postController.findAllPosts().toList()
runLater {
items.addAll(postModelList)
tableView.sort()
tableView.placeholder = Label("No content in table")
}
}
GuiEventBus.ChangingSession -> runLater {
tableView.placeholder = Label("Loading")
items.clear()
}
else -> {}
}
}
}
launch {
postController.updateMetadataFlow.collect {
runLater {
val postModel = items.find { it.postId == it.postId } ?: return@runLater
postModel.altTitles = FXCollections.observableArrayList(it.data.resolvedNames)
postModel.postedBy = it.data.postedBy
}
}
}
launch {
postController.deletedPostsFlow.collect {
runLater {
items.items.removeIf { p -> p.postId == it }
tableView.sort()
}
}
}
launch {
postController.updatePostsFlow.collect { post ->
runLater {
val postModel = items.find { it.postId == post.postId } ?: return@runLater
postModel.status = post.status.name
postModel.progressCount = postController.progressCount(
post.total, post.done, post.downloaded
)
postModel.order = post.rank + 1
postModel.done = post.done
postModel.progress = postController.progress(
post.total, post.done
)
postModel.path = post.getDownloadFolder()
postModel.folderName = post.folderName
}
}
}
launch {
postController.newPostsFlow.collect {
runLater {
items.addAll(it)
tableView.sort()
}
}
}
}
}
private fun isCurrentTab(): Boolean = mainView.root.selectionModel.selectedItem.id == "download-tab"
private fun connect() {
coroutineScope.launch {
connectToOnNewPosts()
connectToOnPostUpdated()
connectToOnDeletedPost()
connectToOnMetadataUpdated()
}
}
private fun connectToOnMetadataUpdated() {
coroutineScope.launch {
postController.onUpdateMetadata().catch {
ActiveUICoroutines.removeFromPosts(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToOnMetadataUpdated()
}
}
}.collect {
runLater {
val postModel = items.find { it.postId == it.postId } ?: return@runLater
postModel.altTitles = FXCollections.observableArrayList(it.data.resolvedNames)
postModel.postedBy = it.data.postedBy
}
}
}.also { runBlocking { ActiveUICoroutines.addToPosts(it) } }
}
private fun connectToOnDeletedPost() {
coroutineScope.launch {
postController.onDeletePosts().catch {
ActiveUICoroutines.removeFromPosts(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToOnDeletedPost()
}
}
}.collect {
runLater {
items.items.removeIf { p -> p.postId == it }
tableView.sort()
}
}
}.also { runBlocking { ActiveUICoroutines.addToPosts(it) } }
}
private fun connectToOnPostUpdated() {
coroutineScope.launch {
postController.onUpdatePosts().catch {
ActiveUICoroutines.removeFromPosts(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToOnPostUpdated()
}
}
}.collect { post ->
runLater {
val postModel = items.find { it.postId == post.postId } ?: return@runLater
postModel.status = post.status.name
postModel.progressCount = postController.progressCount(
post.total, post.done, post.downloaded
)
postModel.order = post.rank + 1
postModel.done = post.done
postModel.progress = postController.progress(
post.total, post.done
)
postModel.path = post.getDownloadFolder()
postModel.folderName = post.folderName
}
}
}.also { runBlocking { ActiveUICoroutines.addToPosts(it) } }
}
private fun connectToOnNewPosts() {
coroutineScope.launch {
val postModelList = async { postController.findAllPosts() }.await()
runLater {
items.clear()
items.addAll(postModelList)
tableView.sort()
tableView.placeholder = Label("No content in table")
clipboardService.init(postController.appEndpointService)
}
}
coroutineScope.launch {
postController.onNewPosts().catch {
ActiveUICoroutines.removeFromPosts(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToOnNewPosts()
}
}
}.collect {
runLater {
items.addAll(it)
tableView.sort()
}
}
}.also { runBlocking { ActiveUICoroutines.addToPosts(it) } }
}
private fun rename(post: PostModel) {
find<RenameFragment>(
mapOf(
@@ -1,27 +1,21 @@
package me.vripper.gui.components.views
import io.grpc.ConnectivityState
import io.grpc.StatusException
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleStringProperty
import javafx.geometry.Orientation
import javafx.geometry.Pos
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.catch
import me.vripper.gui.controller.StatusBarController
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.services.GrpcEndpointService
import me.vripper.gui.utils.ActiveUICoroutines
import me.vripper.services.IAppEndpointService
import me.vripper.utilities.formatSI
import tornadofx.*
class StatusBarView : View("Status bar") {
private val widgetsController: WidgetsController by inject()
private val statusBarController: StatusBarController by inject()
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val grpcEndpointService: GrpcEndpointService by di("remoteAppEndpointService")
private val localEndpointService: IAppEndpointService by di("localAppEndpointService")
private val remoteText = SimpleStringProperty()
private val loggedUser = SimpleStringProperty()
private val tasksRunning = SimpleBooleanProperty(false)
@@ -32,171 +26,84 @@ class StatusBarView : View("Status bar") {
init {
coroutineScope.launch {
GuiEventBus.events.collect { event ->
runLater {
tasksRunning.set(false)
downloadSpeed.set(0L.formatSI())
running.set(0)
pending.set(0)
error.set(0)
}
when (event) {
is GuiEventBus.LocalSession -> {
connect(localEndpointService)
}
is GuiEventBus.RemoteSession -> {
connect(grpcEndpointService)
}
is GuiEventBus.ChangingSession -> {
ActiveUICoroutines.cancelStatusBar()
runLater {
remoteText.set("Connecting to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}")
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
}
delay(1000)
}
}
else -> {}
}
}
}
}
private fun connect(endpointService: IAppEndpointService) {
coroutineScope.launch {
val user = async { endpointService.loggedInUser() }.await()
runLater {
loggedUser.set(user)
}
}
connectToVGUserUpdate(endpointService)
connectToTasksRunning(endpointService)
connectToDownloadSpeed(endpointService)
connectToQueueStateUpdate(endpointService)
connectToErrorCountUpdate(endpointService)
if (widgetsController.currentSettings.localSession) {
runLater {
remoteText.set("")
}
} else {
coroutineScope.launch {
while (isActive) {
val text = when (grpcEndpointService.connectionState()) {
ConnectivityState.CONNECTING -> "Connecting to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}"
ConnectivityState.READY -> "Connected to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port} ${grpcEndpointService.getVersion()}"
ConnectivityState.TRANSIENT_FAILURE -> "Failing to connect to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}"
ConnectivityState.IDLE -> "Idle connection to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}"
ConnectivityState.SHUTDOWN -> "Connection shutdown to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}"
}
runLater {
remoteText.set(text)
}
delay(1000)
}
}
}
}
private fun connectToErrorCountUpdate(endpointService: IAppEndpointService) {
coroutineScope.launch {
endpointService.onErrorCountUpdate().catch {
ActiveUICoroutines.removeFromStatusBar(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToErrorCountUpdate(endpointService)
}
}
}.collect {
statusBarController.vgUserUpdate.collect {
runLater {
error.set(it.count)
loggedUser.set(it)
}
}
}.also { runBlocking { ActiveUICoroutines.addToStatusBar(it) } }
}
}
private fun connectToQueueStateUpdate(endpointService: IAppEndpointService) {
coroutineScope.launch {
endpointService.onQueueStateUpdate().catch {
ActiveUICoroutines.removeFromStatusBar(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToQueueStateUpdate(endpointService)
}
statusBarController.tasksRunning.collect {
runLater {
tasksRunning.set(it)
}
}.collect {
}
}
coroutineScope.launch {
statusBarController.downloadSpeed.collect {
runLater {
downloadSpeed.set(it.speed.formatSI())
}
}
}
coroutineScope.launch {
statusBarController.queueStateUpdate.collect {
runLater {
running.set(it.running)
pending.set(it.remaining)
}
}
}.also { runBlocking { ActiveUICoroutines.addToStatusBar(it) } }
}
}
private fun connectToDownloadSpeed(endpointService: IAppEndpointService) {
coroutineScope.launch {
endpointService.onDownloadSpeed().catch {
ActiveUICoroutines.removeFromStatusBar(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToDownloadSpeed(endpointService)
}
}
}.collect {
statusBarController.errorCountUpdate.collect {
runLater {
downloadSpeed.set(it.speed.formatSI())
error.set(it.count)
}
}
}.also { runBlocking { ActiveUICoroutines.addToStatusBar(it) } }
}
}
private fun connectToTasksRunning(endpointService: IAppEndpointService) {
coroutineScope.launch {
endpointService.onTasksRunning().catch {
ActiveUICoroutines.removeFromStatusBar(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToTasksRunning(endpointService)
}
while (isActive) {
val text = when (statusBarController.connectionState()) {
"CONNECTING" -> "Connecting to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}"
"READY" -> "Connected to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port} ${statusBarController.getVersion()}"
"TRANSIENT_FAILURE" -> "Failing to connect to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}"
"IDLE" -> "Idle connection to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}"
"SHUTDOWN" -> "Connection shutdown to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}"
else -> ""
}
}.collect {
runLater {
tasksRunning.set(it)
remoteText.set(text)
}
delay(1000)
}
}.also { runBlocking { ActiveUICoroutines.addToStatusBar(it) } }
}
private fun connectToVGUserUpdate(endpointService: IAppEndpointService) {
coroutineScope.launch {
endpointService.onVGUserUpdate().catch {
ActiveUICoroutines.removeFromStatusBar(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToVGUserUpdate(endpointService)
}
}
}.collect {
runLater {
loggedUser.set(it)
}
}
}.also { runBlocking { ActiveUICoroutines.addToStatusBar(it) } }
}
}
override val root = borderpane {
@@ -2,24 +2,24 @@ package me.vripper.gui.components.views
import atlantafx.base.theme.Styles
import atlantafx.base.theme.Tweaks
import io.grpc.StatusException
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.scene.control.*
import javafx.scene.input.KeyCode
import javafx.scene.input.KeyEvent
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.javafx.asFlow
import kotlinx.coroutines.launch
import me.vripper.gui.components.fragments.ThreadSelectionTableFragment
import me.vripper.gui.controller.ThreadController
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.model.ThreadModel
import me.vripper.gui.utils.ActiveUICoroutines
import me.vripper.gui.utils.openLink
import me.vripper.services.IAppEndpointService
import org.kordamp.ikonli.feather.Feather
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.*
@@ -30,37 +30,12 @@ class ThreadTableView : View() {
private val threadController: ThreadController by inject()
private val widgetsController: WidgetsController by inject()
private val mainView: MainView by inject()
private val localAppEndpointService: IAppEndpointService by di("localAppEndpointService")
private val remoteAppEndpointService: IAppEndpointService by di("remoteAppEndpointService")
private val tableView: TableView<ThreadModel>
private val items: ObservableList<ThreadModel> = FXCollections.observableArrayList()
override val root = vbox {}
init {
coroutineScope.launch {
GuiEventBus.events.collect { event ->
when (event) {
is GuiEventBus.LocalSession -> {
threadController.appEndpointService = localAppEndpointService
connect()
}
is GuiEventBus.RemoteSession -> {
threadController.appEndpointService = remoteAppEndpointService
connect()
}
is GuiEventBus.ChangingSession -> {
ActiveUICoroutines.cancelThreads()
runLater {
tableView.placeholder = Label("Loading")
}
}
}
}
}
with(root) {
tableView = tableview(items) {
isTableMenuButtonVisible = true
@@ -180,105 +155,65 @@ class ThreadTableView : View() {
})
tableView.prefHeightProperty().bind(root.heightProperty())
tableView.placeholder = Label("Loading")
}
fun connect() {
connectToNewThread()
connectToUpdateThread()
connectToDeleteThread()
connectToClearThread()
}
private fun connectToClearThread() {
coroutineScope.launch {
threadController.onClearThreads().catch {
ActiveUICoroutines.removeFromThreads(currentCoroutineContext().job)
launch {
GuiEventBus.events.collect {
when (it) {
GuiEventBus.LocalSession, GuiEventBus.RemoteSession -> {
val list = threadController.findAll().toList()
runLater {
items.clear()
items.addAll(list)
tableView.placeholder = Label("No content in table")
}
}
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToClearThread()
GuiEventBus.ChangingSession -> runLater {
tableView.placeholder = Label("Loading")
items.clear()
}
else -> {}
}
}
}.collect {
runLater {
tableView.items.clear()
}
}
}.also { runBlocking { ActiveUICoroutines.addToThreads(it) } }
}
private fun connectToDeleteThread() {
coroutineScope.launch {
threadController.onDeleteThread().catch {
ActiveUICoroutines.removeFromThreads(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToDeleteThread()
launch {
threadController.newThread.collect {
runLater {
items.add(it)
}
}
}.collect { threadId ->
runLater {
tableView.items.removeIf { it.threadId == threadId }
}
}
}.also { runBlocking { ActiveUICoroutines.addToThreads(it) } }
}
private fun connectToUpdateThread() {
coroutineScope.launch {
threadController.onUpdateThread().catch {
ActiveUICoroutines.removeFromThreads(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToUpdateThread()
launch {
threadController.updateThread.collect { thread ->
runLater {
val threadModel = items.find { it.threadId == thread.threadId } ?: return@runLater
threadModel.total = thread.total
threadModel.title = thread.title
}
}
}.collect { thread ->
runLater {
val threadModel = items.find { it.threadId == thread.threadId } ?: return@runLater
threadModel.total = thread.total
threadModel.title = thread.title
}
launch {
threadController.deleteThread.collect { threadId ->
runLater {
tableView.items.removeIf { it.threadId == threadId }
}
}
}
}.also { runBlocking { ActiveUICoroutines.addToThreads(it) } }
}
private fun connectToNewThread() {
coroutineScope.launch {
val list = async {
threadController.findAll()
}.await()
runLater {
items.clear()
items.addAll(list)
tableView.placeholder = Label("No content in table")
launch {
threadController.clearThreads.collect {
runLater {
tableView.items.clear()
}
}
}
}
coroutineScope.launch {
threadController.onNewThread().catch {
ActiveUICoroutines.removeFromThreads(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToNewThread()
}
}
}.collect {
runLater {
items.add(it)
}
}
}.also { runBlocking { ActiveUICoroutines.addToThreads(it) } }
}
private fun isCurrentTab(): Boolean = mainView.root.selectionModel.selectedItem.id == "thread-tab"
@@ -0,0 +1,14 @@
package me.vripper.gui.controller
import me.vripper.gui.utils.AppEndpointManager.localAppEndpointService
import me.vripper.gui.utils.AppEndpointManager.remoteAppEndpointService
import me.vripper.gui.utils.ChannelFlowBuilder
import tornadofx.Controller
class ActionBarController : Controller() {
val onQueueStateUpdate = ChannelFlowBuilder.build(
localAppEndpointService::onQueueStateUpdate,
remoteAppEndpointService::onQueueStateUpdate
)
}
@@ -1,14 +1,18 @@
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
import me.vripper.services.IAppEndpointService
import tornadofx.Controller
class ImageController : Controller() {
lateinit var appEndpointService: IAppEndpointService
suspend fun findImages(postId: Long): List<ImageModel> {
return appEndpointService.findImagesByPostId(postId).map(::mapper)
return runCatching { currentAppEndpointService().findImagesByPostId(postId).map(::mapper) }.getOrDefault(
emptyList()
)
}
private fun mapper(it: Image): ImageModel {
@@ -34,8 +38,7 @@ class ImageController : Controller() {
}
fun onUpdateImages(postId: Long) =
appEndpointService.onUpdateImagesByPostId(postId)
fun onStopped() = appEndpointService.onStopped()
currentAppEndpointService().onUpdateImagesByPostId(postId).retryWhen { _, _ -> delay(1000);true }
fun onStopped() = currentAppEndpointService().onStopped().retryWhen { _, _ -> delay(1000);true }
}
@@ -2,12 +2,25 @@ package me.vripper.gui.controller
import kotlinx.coroutines.flow.map
import me.vripper.gui.model.LogModel
import me.vripper.gui.utils.AppEndpointManager.currentAppEndpointService
import me.vripper.gui.utils.AppEndpointManager.localAppEndpointService
import me.vripper.gui.utils.AppEndpointManager.remoteAppEndpointService
import me.vripper.gui.utils.ChannelFlowBuilder
import me.vripper.model.LogEntry
import me.vripper.services.IAppEndpointService
import tornadofx.Controller
class LogController : Controller() {
lateinit var appEndpointService: IAppEndpointService
val newLogs = ChannelFlowBuilder.build(
{ localAppEndpointService.onNewLog().map(::mapper) },
{ remoteAppEndpointService.onNewLog().map(::mapper) }
)
val updateSettings =
ChannelFlowBuilder.build(
localAppEndpointService::onUpdateSettings,
remoteAppEndpointService::onUpdateSettings
)
private fun mapper(it: LogEntry): LogModel {
return LogModel(
@@ -21,13 +34,11 @@ class LogController : Controller() {
)
}
fun onNewLog() = appEndpointService.onNewLog().map(::mapper)
suspend fun initLogger() {
appEndpointService.initLogger()
currentAppEndpointService().initLogger()
}
fun onUpdateSettings() =
appEndpointService.onUpdateSettings()
suspend fun getMaxEventLog(): Int {
return currentAppEndpointService().getSettings().systemSettings.maxEventLog
}
}
@@ -1,9 +1,14 @@
package me.vripper.gui.controller
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import me.vripper.gui.model.PostModel
import me.vripper.gui.utils.AppEndpointManager.currentAppEndpointService
import me.vripper.gui.utils.AppEndpointManager.localAppEndpointService
import me.vripper.gui.utils.AppEndpointManager.remoteAppEndpointService
import me.vripper.gui.utils.ChannelFlowBuilder
import me.vripper.gui.utils.ChannelFlowBuilder.toFlow
import me.vripper.model.Post
import me.vripper.services.IAppEndpointService
import me.vripper.utilities.formatSI
import tornadofx.Controller
import java.time.format.DateTimeFormatter
@@ -12,42 +17,77 @@ class PostController : Controller() {
private val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss")
lateinit var appEndpointService: IAppEndpointService
val updatePostsFlow =
ChannelFlowBuilder.build(
localAppEndpointService::onUpdatePosts,
remoteAppEndpointService::onUpdatePosts
)
val newPostsFlow = ChannelFlowBuilder.build(
{
localAppEndpointService.onNewPosts().map { post ->
mapper(post)
}
}, {
remoteAppEndpointService.onNewPosts().map { post ->
mapper(post)
}
}
)
val deletedPostsFlow =
ChannelFlowBuilder.build(
localAppEndpointService::onDeletePosts,
remoteAppEndpointService::onDeletePosts
)
val updateMetadataFlow = ChannelFlowBuilder.build(
localAppEndpointService::onUpdateMetadata,
remoteAppEndpointService::onUpdateMetadata,
)
suspend fun scan(postLinks: String) {
appEndpointService.scanLinks(postLinks)
runCatching { currentAppEndpointService().scanLinks(postLinks) }
}
suspend fun start(postIdList: List<Long>) {
appEndpointService.restartAll(postIdList)
runCatching { currentAppEndpointService().restartAll(postIdList) }
}
suspend fun startAll() {
appEndpointService.restartAll()
runCatching { currentAppEndpointService().restartAll() }
}
suspend fun delete(postIdList: List<Long>) {
appEndpointService.remove(postIdList)
runCatching { currentAppEndpointService().remove(postIdList) }
}
suspend fun stop(postIdList: List<Long>) {
appEndpointService.stopAll(postIdList)
runCatching { currentAppEndpointService().stopAll(postIdList) }
}
suspend fun clearPosts(): List<Long> {
return appEndpointService.clearCompleted()
return runCatching { return currentAppEndpointService().clearCompleted() }.getOrDefault(emptyList())
}
suspend fun stopAll() {
appEndpointService.stopAll()
runCatching { currentAppEndpointService().stopAll() }
}
suspend fun find(postId: Long): PostModel {
return mapper(appEndpointService.findPost(postId))
suspend fun find(postId: Long): PostModel? {
return runCatching { mapper(currentAppEndpointService().findPost(postId)) }.getOrNull()
}
suspend fun findAllPosts(): List<PostModel> {
return appEndpointService.findAllPosts().map(::mapper)
fun findAllPosts(): Flow<PostModel> {
return toFlow { currentAppEndpointService().findAllPosts().map(::mapper) }
}
suspend fun rename(postId: Long, value: String) {
runCatching { currentAppEndpointService().rename(postId, value) }
}
suspend fun renameToFirst(postIds: List<Long>) {
runCatching { currentAppEndpointService().renameToFirst(postIds) }
}
private fun mapper(post: Post): PostModel {
@@ -79,26 +119,4 @@ class PostController : Controller() {
fun progress(total: Int, done: Int): Double {
return if (done == 0 && total == 0) 0.0 else (done.toDouble() / total)
}
suspend fun rename(postId: Long, value: String) {
appEndpointService.rename(postId, value)
}
suspend fun renameToFirst(postIds: List<Long>) {
appEndpointService.renameToFirst(postIds)
}
fun onNewPosts() =
appEndpointService.onNewPosts().map { post ->
mapper(post)
}
fun onUpdatePosts() =
appEndpointService.onUpdatePosts()
fun onDeletePosts() =
appEndpointService.onDeletePosts()
fun onUpdateMetadata() =
appEndpointService.onUpdateMetadata()
}
@@ -4,29 +4,36 @@ 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.utils.AppEndpointManager
import me.vripper.model.*
import me.vripper.services.IAppEndpointService
import tornadofx.Controller
class SettingsController : Controller() {
private val widgetsController: WidgetsController by inject()
private val appEndpointService: IAppEndpointService by di(if (widgetsController.currentSettings.localSession) "localAppEndpointService" else "remoteAppEndpointService")
suspend fun findDownloadSettings(): DownloadSettings {
return appEndpointService.getSettings().downloadSettings
return runCatching {
AppEndpointManager.currentAppEndpointService().getSettings().downloadSettings
}.getOrDefault(
DownloadSettings()
)
}
suspend fun findConnectionSettings(): ConnectionSettings {
return appEndpointService.getSettings().connectionSettings
return runCatching {
AppEndpointManager.currentAppEndpointService().getSettings().connectionSettings
}.getOrDefault(ConnectionSettings())
}
suspend fun findViperGirlsSettings(): ViperSettings {
return appEndpointService.getSettings().viperSettings
return runCatching { AppEndpointManager.currentAppEndpointService().getSettings().viperSettings }.getOrDefault(
ViperSettings()
)
}
suspend fun findSystemSettings(): SystemSettings {
return appEndpointService.getSettings().systemSettings
return runCatching { AppEndpointManager.currentAppEndpointService().getSettings().systemSettings }.getOrDefault(
SystemSettings()
)
}
suspend fun saveNewSettings(
@@ -35,45 +42,47 @@ class SettingsController : Controller() {
viperSettingsModel: ViperSettingsModel,
systemSettingsModel: SystemSettingsModel
) {
appEndpointService.saveSettings(
Settings(
downloadSettings = DownloadSettings(
downloadSettingsModel.downloadPath,
downloadSettingsModel.autoStart,
downloadSettingsModel.autoQueueThreshold,
downloadSettingsModel.forceOrder,
downloadSettingsModel.forumSubfolder,
downloadSettingsModel.threadSubLocation,
downloadSettingsModel.clearCompleted,
downloadSettingsModel.appendPostId
),
connectionSettings = ConnectionSettings(
connectionSettingsModel.maxThreads,
connectionSettingsModel.maxTotalThreads,
connectionSettingsModel.timeout,
connectionSettingsModel.maxAttempts,
),
viperSettings = ViperSettings(
viperSettingsModel.login,
viperSettingsModel.username,
viperSettingsModel.password,
viperSettingsModel.thanks,
viperSettingsModel.host,
viperSettingsModel.requestLimit,
viperSettingsModel.fetchMetadata,
),
systemSettings =
SystemSettings(
systemSettingsModel.tempPath,
systemSettingsModel.enable,
systemSettingsModel.pollingRate,
systemSettingsModel.logEntries
)
runCatching {
AppEndpointManager.currentAppEndpointService().saveSettings(
Settings(
downloadSettings = DownloadSettings(
downloadSettingsModel.downloadPath,
downloadSettingsModel.autoStart,
downloadSettingsModel.autoQueueThreshold,
downloadSettingsModel.forceOrder,
downloadSettingsModel.forumSubfolder,
downloadSettingsModel.threadSubLocation,
downloadSettingsModel.clearCompleted,
downloadSettingsModel.appendPostId
),
connectionSettings = ConnectionSettings(
connectionSettingsModel.maxThreads,
connectionSettingsModel.maxTotalThreads,
connectionSettingsModel.timeout,
connectionSettingsModel.maxAttempts,
),
viperSettings = ViperSettings(
viperSettingsModel.login,
viperSettingsModel.username,
viperSettingsModel.password,
viperSettingsModel.thanks,
viperSettingsModel.host,
viperSettingsModel.requestLimit,
viperSettingsModel.fetchMetadata,
),
systemSettings =
SystemSettings(
systemSettingsModel.tempPath,
systemSettingsModel.enable,
systemSettingsModel.pollingRate,
systemSettingsModel.logEntries
)
)
)
)
}
}
suspend fun getProxies(): List<String> {
return appEndpointService.getProxies()
return runCatching { AppEndpointManager.currentAppEndpointService().getProxies() }.getOrDefault(emptyList())
}
}
@@ -0,0 +1,46 @@
package me.vripper.gui.controller
import me.vripper.gui.utils.AppEndpointManager.currentAppEndpointService
import me.vripper.gui.utils.AppEndpointManager.localAppEndpointService
import me.vripper.gui.utils.AppEndpointManager.remoteAppEndpointService
import me.vripper.gui.utils.ChannelFlowBuilder
import tornadofx.Controller
class StatusBarController : Controller() {
suspend fun loggedInUser(): String {
return currentAppEndpointService().loggedInUser()
}
fun connectionState(): String {
return currentAppEndpointService().connectionState()
}
suspend fun getVersion(): String {
return currentAppEndpointService().getVersion()
}
val vgUserUpdate = ChannelFlowBuilder.build(
localAppEndpointService::onVGUserUpdate,
remoteAppEndpointService::onVGUserUpdate,
)
val tasksRunning = ChannelFlowBuilder.build(
localAppEndpointService::onTasksRunning,
remoteAppEndpointService::onTasksRunning,
)
val downloadSpeed = ChannelFlowBuilder.build(
localAppEndpointService::onDownloadSpeed,
remoteAppEndpointService::onDownloadSpeed,
)
val queueStateUpdate = ChannelFlowBuilder.build(
localAppEndpointService::onQueueStateUpdate,
remoteAppEndpointService::onQueueStateUpdate,
)
val errorCountUpdate = ChannelFlowBuilder.build(
localAppEndpointService::onErrorCountUpdate,
remoteAppEndpointService::onErrorCountUpdate
)
}
@@ -1,19 +1,47 @@
package me.vripper.gui.controller
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import me.vripper.entities.ThreadEntity
import me.vripper.gui.model.ThreadModel
import me.vripper.gui.model.ThreadSelectionModel
import me.vripper.gui.utils.AppEndpointManager.currentAppEndpointService
import me.vripper.gui.utils.AppEndpointManager.localAppEndpointService
import me.vripper.gui.utils.AppEndpointManager.remoteAppEndpointService
import me.vripper.gui.utils.ChannelFlowBuilder
import me.vripper.gui.utils.ChannelFlowBuilder.toFlow
import me.vripper.model.ThreadPostId
import me.vripper.services.IAppEndpointService
import org.koin.core.component.KoinComponent
import tornadofx.Controller
class ThreadController : KoinComponent, Controller() {
lateinit var appEndpointService: IAppEndpointService
suspend fun findAll(): List<ThreadModel> {
return appEndpointService.findAllThreads().map(::threadModelMapper)
val newThread = ChannelFlowBuilder.build(
{
localAppEndpointService.onNewThread().map(::threadModelMapper)
},
{
remoteAppEndpointService.onNewThread().map(::threadModelMapper)
},
)
val updateThread = ChannelFlowBuilder.build(
localAppEndpointService::onUpdateThread,
remoteAppEndpointService::onUpdateThread,
)
val deleteThread = ChannelFlowBuilder.build(
localAppEndpointService::onDeleteThread,
remoteAppEndpointService::onDeleteThread,
)
val clearThreads = ChannelFlowBuilder.build(
localAppEndpointService::onClearThreads,
remoteAppEndpointService::onClearThreads,
)
fun findAll(): Flow<ThreadModel> {
return toFlow { currentAppEndpointService().findAllThreads().map(::threadModelMapper) }
}
private fun threadModelMapper(it: ThreadEntity): ThreadModel {
@@ -26,15 +54,15 @@ class ThreadController : KoinComponent, Controller() {
}
suspend fun delete(threadIdList: List<Long>) {
appEndpointService.threadRemove(threadIdList)
currentAppEndpointService().threadRemove(threadIdList)
}
suspend fun clearAll() {
appEndpointService.threadClear()
currentAppEndpointService().threadClear()
}
suspend fun grab(threadId: Long): List<ThreadSelectionModel> =
appEndpointService.grab(threadId).map { postItem ->
currentAppEndpointService().grab(threadId).map { postItem ->
ThreadSelectionModel(
postItem.number,
postItem.title,
@@ -46,21 +74,12 @@ class ThreadController : KoinComponent, Controller() {
)
}
suspend fun download(selectedItems: List<ThreadSelectionModel>) {
appEndpointService.download(selectedItems.map {
currentAppEndpointService().download(selectedItems.map {
ThreadPostId(
it.threadId,
it.postId
)
})
}
fun onNewThread() = appEndpointService.onNewThread().map(::threadModelMapper)
fun onUpdateThread() = appEndpointService.onUpdateThread()
fun onDeleteThread() = appEndpointService.onDeleteThread()
fun onClearThreads() = appEndpointService.onClearThreads()
}
@@ -1,19 +1,22 @@
package me.vripper.gui.event
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
object GuiEventBus {
private val _events = MutableSharedFlow<Any>()
private val _events = MutableSharedFlow<GUIEvent>(0, Int.MAX_VALUE, BufferOverflow.DROP_OLDEST)
val events = _events.asSharedFlow()
suspend fun publishEvent(event: Any) {
suspend fun publishEvent(event: GUIEvent) {
_events.emit(event)
}
data class ApplicationInitialized(val args: List<String>)
object ChangingSession
object LocalSession
object RemoteSession
data class ApplicationInitialized(val args: List<String>) : GUIEvent
object ChangingSession : GUIEvent
object LocalSession : GUIEvent
object RemoteSession : GUIEvent
}
sealed interface GUIEvent
@@ -1,66 +0,0 @@
package me.vripper.gui.services
import io.grpc.StatusException
import javafx.scene.input.Clipboard
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.filterIsInstance
import me.vripper.gui.event.GuiEventBus
import me.vripper.model.Settings
import me.vripper.services.IAppEndpointService
import me.vripper.utilities.LoggerDelegate
import tornadofx.Controller
import tornadofx.runLater
class ClipboardService : Controller() {
private val logger by LoggerDelegate()
private var current: String? = null
private var coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var pollJob: Job? = null
private var subscribeJob: Job? = null
fun init(appEndpointService: IAppEndpointService) {
subscribeJob?.cancel()
subscribeJob = coroutineScope.launch {
appEndpointService.onUpdateSettings().catch {
if (it !is StatusException) {
logger.error("gRPC error", it)
}
}.collect {
run(it, appEndpointService)
}
}
coroutineScope.launch {
GuiEventBus.events.filterIsInstance(GuiEventBus.ChangingSession::class).collect {
subscribeJob?.cancel()
}
}
runBlocking {
run(appEndpointService.getSettings(), appEndpointService)
}
}
private fun run(settings: Settings, appEndpointService: IAppEndpointService) {
pollJob?.cancel()
if (settings.systemSettings.enableClipboardMonitoring) {
pollJob = coroutineScope.launch {
var value: String? = null
while (isActive) {
runLater {
val clipboard = Clipboard.getSystemClipboard()
if (clipboard.hasString()) {
value = clipboard.string
}
}
if (!value.isNullOrBlank() && value != current) {
current = value
appEndpointService.scanLinks(value)
}
delay(settings.systemSettings.clipboardPollingRate.toLong())
}
}
} else {
current = null
}
}
}
@@ -48,11 +48,9 @@ internal class GrpcEndpointService : IAppEndpointService {
override suspend fun findPost(postId: Long): Post =
mapper(endpointServiceCoroutineStub!!.findPost(Id.newBuilder().setId(postId).build()))
override suspend fun findAllPosts(): List<Post> =
endpointServiceCoroutineStub!!.findAllPosts(EmptyRequest.getDefaultInstance()).postsList.map(::mapper)
override suspend fun rename(postId: Long, newName: String) {
endpointServiceCoroutineStub!!.rename(
Rename.newBuilder().setPostId(postId).setName(newName).build()
@@ -375,5 +373,5 @@ internal class GrpcEndpointService : IAppEndpointService {
}
}
fun connectionState(): ConnectivityState = channel?.getState(false) ?: ConnectivityState.SHUTDOWN
override fun connectionState(): String = channel?.getState(true)?.name ?: ConnectivityState.SHUTDOWN.name
}
@@ -1,192 +0,0 @@
package me.vripper.gui.utils
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
object ActiveUICoroutines {
private val mutex = Mutex()
private val posts: MutableList<Job> = mutableListOf()
private val actionBar: MutableList<Job> = mutableListOf()
private val images: MutableList<Job> = mutableListOf()
private val logs: MutableList<Job> = mutableListOf()
private val menuBar: MutableList<Job> = mutableListOf()
private val postInfo: MutableList<Job> = mutableListOf()
private val statusBar: MutableList<Job> = mutableListOf()
private val threads: MutableList<Job> = mutableListOf()
suspend fun cancelPosts() {
mutex.withLock {
posts.forEach { it.cancelAndJoin() }
posts.clear()
}
}
suspend fun addToPosts(job: Job) {
mutex.withLock {
posts.add(job)
}
}
suspend fun removeFromPosts(job: Job) {
mutex.withLock {
job.cancel()
posts.remove(job)
}
}
suspend fun cancelActionBar() {
mutex.withLock {
actionBar.forEach { it.cancelAndJoin() }
actionBar.clear()
}
}
suspend fun addToActionBar(job: Job) {
mutex.withLock {
actionBar.add(job)
}
}
suspend fun removeFromActionBar(job: Job) {
mutex.withLock {
job.cancel()
actionBar.remove(job)
}
}
suspend fun cancelImages() {
mutex.withLock {
images.forEach { it.cancelAndJoin() }
images.clear()
}
}
suspend fun addToImages(job: Job) {
mutex.withLock {
images.add(job)
}
}
suspend fun removeFromImages(job: Job) {
mutex.withLock {
job.cancel()
images.remove(job)
}
}
suspend fun cancelLog() {
mutex.withLock {
logs.forEach { it.cancelAndJoin() }
logs.clear()
}
}
suspend fun addToLog(job: Job) {
mutex.withLock {
logs.add(job)
}
}
suspend fun removeFromLog(job: Job) {
mutex.withLock {
job.cancel()
logs.remove(job)
}
}
suspend fun cancelMenuBar() {
mutex.withLock {
menuBar.forEach { it.cancelAndJoin() }
menuBar.clear()
}
}
suspend fun addToMenuBar(job: Job) {
mutex.withLock {
menuBar.add(job)
}
}
suspend fun removeFromMenuBar(job: Job) {
mutex.withLock {
job.cancel()
menuBar.remove(job)
}
}
suspend fun cancelPostInfo() {
mutex.withLock {
postInfo.forEach { it.cancelAndJoin() }
postInfo.clear()
}
}
suspend fun addToPostInfo(job: Job) {
mutex.withLock {
postInfo.add(job)
}
}
suspend fun removeFromPostInfo(job: Job) {
mutex.withLock {
job.cancel()
postInfo.remove(job)
}
}
suspend fun cancelStatusBar() {
mutex.withLock {
statusBar.forEach { it.cancelAndJoin() }
statusBar.clear()
}
}
suspend fun addToStatusBar(job: Job) {
mutex.withLock {
statusBar.add(job)
}
}
suspend fun removeFromStatusBar(job: Job) {
mutex.withLock {
job.cancel()
statusBar.remove(job)
}
}
suspend fun cancelThreads() {
mutex.withLock {
threads.forEach { it.cancelAndJoin() }
threads.clear()
}
}
suspend fun addToThreads(job: Job) {
mutex.withLock {
threads.add(job)
}
}
suspend fun removeFromThreads(job: Job) {
mutex.withLock {
job.cancel()
threads.remove(job)
}
}
suspend fun all() = mutex.withLock {
listOf(
posts,
actionBar,
logs,
menuBar,
postInfo,
statusBar,
threads
).flatten()
}
}
@@ -0,0 +1,32 @@
package me.vripper.gui.utils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import me.vripper.gui.event.GUIEvent
import me.vripper.gui.event.GuiEventBus
import me.vripper.services.IAppEndpointService
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.qualifier.named
object AppEndpointManager : KoinComponent {
val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
val localAppEndpointService: IAppEndpointService by inject(named("localAppEndpointService"))
val remoteAppEndpointService: IAppEndpointService by inject(named("remoteAppEndpointService"))
private lateinit var current: GUIEvent
fun set(event: GUIEvent) {
this.current = event
}
fun currentAppEndpointService(): IAppEndpointService {
return when (current) {
GuiEventBus.LocalSession -> localAppEndpointService
GuiEventBus.RemoteSession -> remoteAppEndpointService
else -> throw IllegalStateException("Unknown current state: $current")
}
}
}
@@ -0,0 +1,53 @@
package me.vripper.gui.utils
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> {
return channelFlow {
var job: Job? = null
fun collect(localSession: Boolean) {
job = if (localSession) {
launch {
localFlow().cancellable().collect { if (isActive) send(it) }
}
} else {
launch {
remoteFlow().cancellable().retryWhen { _, _ ->
delay(1000)
true
}.collect { if (isActive) send(it) }
}
}
}
launch {
GuiEventBus
.events
.collect {
if (job != null && job.isActive) {
job.cancelAndJoin()
}
when (it) {
GuiEventBus.LocalSession -> collect(true)
GuiEventBus.RemoteSession -> collect(false)
else -> {}
}
}
}
}
}
fun <T> toFlow(source: suspend () -> List<T>): Flow<T> {
return channelFlow {
source().forEach { if (isActive) send(it) }
}.retryWhen { _, _ -> delay(1000); true }
}
}
@@ -0,0 +1,73 @@
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.utilities.LoggerDelegate
import org.koin.core.component.KoinComponent
import tornadofx.runLater
object ClipboardManager : KoinComponent {
private val logger by LoggerDelegate()
private var current: String? = null
private var coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var pollJob: Job? = null
private var settingsUpdateJob: Job? = null
fun init() {
coroutineScope.launch {
GuiEventBus.events.collect {
when (it) {
GuiEventBus.LocalSession, GuiEventBus.RemoteSession -> {
logger.info("Clipboard manager initialized")
while (isActive) {
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 -> {}
}
}
}
}
private fun update(settings: Settings) {
pollJob?.cancel()
if (settings.systemSettings.enableClipboardMonitoring) {
logger.info("Polling clipboard every ${settings.systemSettings.clipboardPollingRate}ms")
pollJob = coroutineScope.launch {
var value: String? = null
while (isActive) {
runLater {
val clipboard = Clipboard.getSystemClipboard()
if (clipboard.hasString()) {
value = clipboard.string
}
}
if (!value.isNullOrBlank() && value != current) {
current = value
runCatching { AppEndpointManager.currentAppEndpointService().scanLinks(value) }
}
delay(settings.systemSettings.clipboardPollingRate.toLong())
}
}
} else {
logger.info("Clipboard monitoring deactivated")
current = null
}
}
}
@@ -61,11 +61,11 @@ object Watcher : KoinComponent {
log.info("Handling thread ${it.context()}")
if (WidgetSettings.loadSettings().localSession) {
val link = localAppEndpointService.getSettings().viperSettings.host + "/threads/${it.context()}"
localAppEndpointService.scanLinks(link)
runCatching { localAppEndpointService.scanLinks(link) }
} else {
val link =
remoteAppEndpointService.getSettings().viperSettings.host + "/threads/${it.context()}"
remoteAppEndpointService.scanLinks(link)
runCatching { remoteAppEndpointService.scanLinks(link) }
}
path.resolve(it.context().toString()).deleteIfExists()
}
@@ -1,6 +1,7 @@
package me.vripper
import me.vripper.listeners.AppManager
import me.vripper.utilities.DatabaseManager
import org.koin.core.context.startKoin
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.builder.SpringApplicationBuilder
@@ -12,6 +13,7 @@ fun main(args: Array<String>) {
startKoin {
modules(coreModule)
}
DatabaseManager.connect()
AppManager.start()
SpringApplicationBuilder(VripperWebApplication::class.java).listeners(AppListener()).run(*args)
}