mirror of
https://github.com/dev-claw/vripper-project.git
synced 2026-08-19 08:35:41 +02:00
@@ -9,7 +9,7 @@
|
||||
<version>${revision}</version>
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<kotlin.version>1.9.20</kotlin.version>
|
||||
<kotlin.version>1.9.22</kotlin.version>
|
||||
<maven.deploy.skip>false</maven.deploy.skip>
|
||||
<timestamp>${maven.build.timestamp}</timestamp>
|
||||
<maven.build.timestamp.format>yyyy-MM-dd HH:mm:ss</maven.build.timestamp.format>
|
||||
@@ -150,6 +150,11 @@
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<version>${kotlin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlinx</groupId>
|
||||
<artifactId>kotlinx-serialization-json</artifactId>
|
||||
<version>1.6.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<artifactId>jcl-over-slf4j</artifactId>
|
||||
<groupId>org.slf4j</groupId>
|
||||
|
||||
+21
-26
@@ -43,10 +43,6 @@
|
||||
<groupId>io.insert-koin</groupId>
|
||||
<artifactId>koin-core-jvm</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<artifactId>jackson-module-kotlin</artifactId>
|
||||
<groupId>com.fasterxml.jackson.module</groupId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<artifactId>h2</artifactId>
|
||||
<groupId>com.h2database</groupId>
|
||||
@@ -71,18 +67,6 @@
|
||||
<artifactId>caffeine</artifactId>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||
<artifactId>jackson-dataformat-yaml</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
<groupId>commons-codec</groupId>
|
||||
@@ -100,10 +84,14 @@
|
||||
<groupId>com.mattbertolini</groupId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-core</artifactId>
|
||||
</dependency>
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>io.projectreactor</groupId>-->
|
||||
<!-- <artifactId>reactor-core</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlinx</groupId>
|
||||
<artifactId>kotlinx-serialization-json</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
@@ -121,14 +109,21 @@
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<sourceDirs>
|
||||
<source>src/main/kotlin</source>
|
||||
<source>target/generated-sources/annotations</source>
|
||||
</sourceDirs>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<jvmTarget>1.8</jvmTarget>
|
||||
<compilerPlugins>
|
||||
<plugin>kotlinx-serialization</plugin>
|
||||
</compilerPlugins>
|
||||
</configuration>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-maven-serialization</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
|
||||
@@ -55,6 +55,9 @@ val coreModule = module {
|
||||
single<AppEndpointService> {
|
||||
AppEndpointService(get(), get(), get(), get())
|
||||
}
|
||||
single<MetadataService> {
|
||||
MetadataService(get(), get(), get(), get())
|
||||
}
|
||||
single {
|
||||
AcidimgHost(get(), get(), get())
|
||||
} bind Host::class
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package me.vripper.download
|
||||
|
||||
import kotlinx.coroutines.Runnable
|
||||
import kotlinx.coroutines.*
|
||||
import me.vripper.entities.Image
|
||||
import me.vripper.entities.LogEntry
|
||||
import me.vripper.entities.Post
|
||||
@@ -37,6 +37,7 @@ class DownloadService(
|
||||
private val pending: MutableMap<Byte, MutableList<ImageDownloadRunnable>> = mutableMapOf()
|
||||
private val lock = ReentrantLock()
|
||||
private val condition = lock.newCondition()
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
fun init() {
|
||||
Thread.ofVirtual().name("Download Loop").unstarted(Runnable {
|
||||
@@ -166,7 +167,8 @@ class DownloadService(
|
||||
pending.values.forEach { pending ->
|
||||
pending.removeIf { it.context.image.postId == postId }
|
||||
}
|
||||
running.values.flatten().filter { p: ImageDownloadRunnable -> p.context.image.postId == postId }
|
||||
running.values.flatten()
|
||||
.filter { p: ImageDownloadRunnable -> p.context.image.postId == postId }
|
||||
.forEach { obj: ImageDownloadRunnable -> obj.stop() }
|
||||
}
|
||||
postIds.forEach {
|
||||
@@ -220,7 +222,9 @@ class DownloadService(
|
||||
private fun scheduleForDownload(imageDownloadRunnable: ImageDownloadRunnable) {
|
||||
log.debug("Scheduling a job for ${imageDownloadRunnable.context.image.url}")
|
||||
CompletableFuture.runAsync({
|
||||
eventBus.publishEvent(QueueStateEvent(QueueState(runningCount(), pendingCount())))
|
||||
coroutineScope.launch {
|
||||
eventBus.publishEvent(QueueStateEvent(QueueState(runningCount(), pendingCount())))
|
||||
}
|
||||
Failsafe.with<Any, RetryPolicy<Any>>(retryPolicyService.buildRetryPolicyForDownload())
|
||||
.onFailure {
|
||||
try {
|
||||
@@ -243,14 +247,18 @@ class DownloadService(
|
||||
dataTransaction.updateImage(image)
|
||||
}.onComplete {
|
||||
afterJobFinish(imageDownloadRunnable)
|
||||
eventBus.publishEvent(
|
||||
QueueStateEvent(
|
||||
QueueState(
|
||||
runningCount(), pendingCount()
|
||||
coroutineScope.launch {
|
||||
eventBus.publishEvent(
|
||||
QueueStateEvent(
|
||||
QueueState(
|
||||
runningCount(), pendingCount()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
eventBus.publishEvent(ErrorCountEvent(ErrorCount(dataTransaction.countImagesInError())))
|
||||
}
|
||||
coroutineScope.launch {
|
||||
eventBus.publishEvent(ErrorCountEvent(ErrorCount(dataTransaction.countImagesInError())))
|
||||
}
|
||||
log.debug(
|
||||
"Finished downloading ${imageDownloadRunnable.context.image.url}"
|
||||
)
|
||||
|
||||
@@ -10,8 +10,9 @@ import me.vripper.host.Host
|
||||
import me.vripper.host.ImageMimeType
|
||||
import me.vripper.model.Settings
|
||||
import me.vripper.services.*
|
||||
import me.vripper.utilities.PathUtils
|
||||
import me.vripper.utilities.PathUtils.getExtension
|
||||
import me.vripper.utilities.PathUtils.getFileNameWithoutExtension
|
||||
import me.vripper.utilities.PathUtils.sanitize
|
||||
import net.jodah.failsafe.function.CheckedRunnable
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
@@ -56,10 +57,6 @@ class ImageDownloadRunnable(
|
||||
val downloadedImage = host.downloadInternal(image.url, context)
|
||||
log.debug("Resolved name for ${image.url}: ${downloadedImage.name}")
|
||||
log.debug("Downloaded image {} to {}", image.url, downloadedImage.path)
|
||||
val sanitizedFileName = PathUtils.sanitize(downloadedImage.name)
|
||||
log.debug(
|
||||
"Sanitizing image name from ${downloadedImage.name} to $sanitizedFileName"
|
||||
)
|
||||
synchronized(image.postId.toString().intern()) {
|
||||
val post = dataTransaction.findPostById(context.postId).orElseThrow()
|
||||
val downloadDirectory = Path(post.downloadDirectory, post.folderName).pathString
|
||||
@@ -91,6 +88,7 @@ class ImageDownloadRunnable(
|
||||
downloadDirectory: String, downloadedImage: DownloadedImage, index: Int
|
||||
) {
|
||||
val existingExtension = getExtension(downloadedImage.name).lowercase()
|
||||
val fileNameWithoutExtension = getFileNameWithoutExtension(downloadedImage.name)
|
||||
val extension = when (downloadedImage.type) {
|
||||
ImageMimeType.IMAGE_BMP -> "BMP"
|
||||
ImageMimeType.IMAGE_GIF -> "GIF"
|
||||
@@ -99,18 +97,22 @@ class ImageDownloadRunnable(
|
||||
ImageMimeType.IMAGE_WEBP -> "WEBP"
|
||||
}
|
||||
val filename =
|
||||
if (existingExtension.isBlank()) "${downloadedImage.name}.$extension" else downloadedImage.name
|
||||
if (existingExtension.isBlank()) "${sanitize(downloadedImage.name)}.$extension" else "${
|
||||
sanitize(
|
||||
fileNameWithoutExtension
|
||||
)
|
||||
}.$extension"
|
||||
try {
|
||||
val downloadDestinationFolder = Path.of(downloadDirectory)
|
||||
Files.createDirectories(downloadDestinationFolder)
|
||||
val image = downloadDestinationFolder.resolve(
|
||||
"${
|
||||
if (settings.downloadSettings.forceOrder) String.format(
|
||||
"%03d_", index + 1
|
||||
) else ""
|
||||
}$filename"
|
||||
)
|
||||
Files.copy(downloadedImage.path, image, StandardCopyOption.REPLACE_EXISTING)
|
||||
val finalFilename = "${
|
||||
if (settings.downloadSettings.forceOrder) String.format(
|
||||
"%03d_", index + 1
|
||||
) else ""
|
||||
}$filename"
|
||||
image.filename = finalFilename
|
||||
val imageDownloadPath = downloadDestinationFolder.resolve(finalFilename)
|
||||
Files.copy(downloadedImage.path, imageDownloadPath, StandardCopyOption.REPLACE_EXISTING)
|
||||
} catch (e: Exception) {
|
||||
throw HostException("Failed to rename the image", e)
|
||||
} finally {
|
||||
|
||||
@@ -13,6 +13,7 @@ data class Image(
|
||||
var size: Long = -1,
|
||||
var downloaded: Long = 0,
|
||||
var status: Status = Status.STOPPED,
|
||||
var filename: String = "",
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package me.vripper.entities
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat
|
||||
import java.time.LocalDateTime
|
||||
|
||||
data class LogEntry(
|
||||
val id: Long = -1,
|
||||
val type: Type,
|
||||
val status: Status,
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") val time: LocalDateTime = LocalDateTime.now(),
|
||||
val time: LocalDateTime = LocalDateTime.now(),
|
||||
val message: String,
|
||||
) {
|
||||
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
package me.vripper.entities
|
||||
|
||||
class Metadata {
|
||||
var postIdRef: Long? = null
|
||||
var postId: String? = null
|
||||
var postedBy: String? = null
|
||||
var resolvedNames = emptyList<String>()
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
companion object {
|
||||
fun from(metadata: Metadata): Metadata {
|
||||
val copy = Metadata()
|
||||
copy.postIdRef = metadata.postIdRef
|
||||
copy.postId = metadata.postId
|
||||
copy.postedBy = metadata.postedBy
|
||||
copy.resolvedNames = metadata.resolvedNames
|
||||
return copy
|
||||
}
|
||||
}
|
||||
}
|
||||
data class Metadata(val postId: Long, val data: Data) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
val postedBy: String,
|
||||
val resolvedNames: List<String>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package me.vripper.entities
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat
|
||||
import me.vripper.entities.domain.Status
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.io.path.Path
|
||||
@@ -18,7 +17,7 @@ data class Post(
|
||||
val total: Int,
|
||||
val hosts: Set<String>,
|
||||
val downloadDirectory: String,
|
||||
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss") val addedOn: LocalDateTime = LocalDateTime.now(),
|
||||
val addedOn: LocalDateTime = LocalDateTime.now(),
|
||||
var folderName: String,
|
||||
var status: Status = Status.STOPPED,
|
||||
var done: Int = 0,
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package me.vripper.event
|
||||
|
||||
import me.vripper.entities.Image
|
||||
import me.vripper.entities.LogEntry
|
||||
import me.vripper.entities.Post
|
||||
import me.vripper.entities.Thread
|
||||
import me.vripper.entities.*
|
||||
import me.vripper.model.DownloadSpeed
|
||||
import me.vripper.model.ErrorCount
|
||||
import me.vripper.model.QueueState
|
||||
@@ -21,6 +18,7 @@ data class DownloadSpeedEvent(val downloadSpeed: DownloadSpeed)
|
||||
data class QueueStateEvent(val queueState: QueueState)
|
||||
data class ErrorCountEvent(val errorCount: ErrorCount)
|
||||
data class SettingsUpdateEvent(val settings: Settings)
|
||||
data class MetadataUpdateEvent(val metadata: Metadata)
|
||||
|
||||
data class LogCreateEvent(val logEntry: LogEntry)
|
||||
data class LogUpdateEvent(val logEntry: LogEntry)
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
package me.vripper.event
|
||||
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Sinks
|
||||
import reactor.core.scheduler.Scheduler
|
||||
import reactor.core.scheduler.Schedulers
|
||||
import java.util.concurrent.Executors
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
|
||||
object EventBus {
|
||||
|
||||
private val scheduler: Scheduler = Schedulers.fromExecutor(Executors.newSingleThreadExecutor())
|
||||
private val _events = Sinks.many().multicast().onBackpressureBuffer<Any>()
|
||||
val events: Flux<Any> = _events.asFlux().publishOn(scheduler)
|
||||
fun publishEvent(event: Any) {
|
||||
_events.emitNext(event) { _, _ -> true }
|
||||
private val _events = MutableSharedFlow<Any>()
|
||||
val events = _events.asSharedFlow()
|
||||
|
||||
suspend fun publishEvent(event: Any) {
|
||||
_events.emit(event)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +165,7 @@ abstract class Host(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
private fun getImageMimeType(headers: Array<Header>): ImageMimeType? {
|
||||
|
||||
// first check if content type header exists
|
||||
|
||||
@@ -12,6 +12,7 @@ open class OnStartupListener : KoinComponent {
|
||||
private val retryPolicyService: RetryPolicyService by inject()
|
||||
private val vgAuthService: VGAuthService by inject()
|
||||
private val downloadSpeedService: DownloadSpeedService by inject()
|
||||
private val metadataService: MetadataService by inject()
|
||||
|
||||
open fun run() {
|
||||
dataTransaction.setDownloadingToStopped()
|
||||
@@ -20,5 +21,6 @@ open class OnStartupListener : KoinComponent {
|
||||
vgAuthService.init()
|
||||
downloadService.init()
|
||||
downloadSpeedService.init()
|
||||
metadataService.init()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,49 @@
|
||||
package me.vripper.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Settings(
|
||||
var connectionSettings: ConnectionSettings = ConnectionSettings(),
|
||||
var downloadSettings: DownloadSettings = DownloadSettings(),
|
||||
var viperSettings: ViperSettings = ViperSettings(),
|
||||
var systemSettings: SystemSettings = SystemSettings()
|
||||
val connectionSettings: ConnectionSettings = ConnectionSettings(),
|
||||
val downloadSettings: DownloadSettings = DownloadSettings(),
|
||||
val viperSettings: ViperSettings = ViperSettings(),
|
||||
val systemSettings: SystemSettings = SystemSettings()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ViperSettings(
|
||||
var login: Boolean = false,
|
||||
var username: String = "",
|
||||
var password: String = "",
|
||||
var thanks: Boolean = false,
|
||||
var host: String = "https://vipergirls.to",
|
||||
val login: Boolean = false,
|
||||
val username: String = "",
|
||||
val password: String = "",
|
||||
val thanks: Boolean = false,
|
||||
val host: String = "https://vipergirls.to",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DownloadSettings(
|
||||
var downloadPath: String = System.getProperty("user.home"),
|
||||
var autoStart: Boolean = true,
|
||||
var autoQueueThreshold: Int = 1,
|
||||
var forceOrder: Boolean = false,
|
||||
var forumSubDirectory: Boolean = false,
|
||||
var threadSubLocation: Boolean = false,
|
||||
var clearCompleted: Boolean = false,
|
||||
var appendPostId: Boolean = false
|
||||
val downloadPath: String = System.getProperty("user.home"),
|
||||
val autoStart: Boolean = true,
|
||||
val autoQueueThreshold: Int = 1,
|
||||
val forceOrder: Boolean = false,
|
||||
val forumSubDirectory: Boolean = false,
|
||||
val threadSubLocation: Boolean = false,
|
||||
val clearCompleted: Boolean = false,
|
||||
val appendPostId: Boolean = false
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ConnectionSettings(
|
||||
var maxConcurrentPerHost: Int = 2,
|
||||
var maxGlobalConcurrent: Int = 0,
|
||||
var timeout: Long = 30,
|
||||
var maxAttempts: Int = 3,
|
||||
val maxConcurrentPerHost: Int = 2,
|
||||
val maxGlobalConcurrent: Int = 0,
|
||||
val timeout: Long = 30,
|
||||
val maxAttempts: Int = 3,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SystemSettings(
|
||||
var tempPath: String = System.getProperty("java.io.tmpdir"),
|
||||
var cachePath: String = System.getProperty("java.io.tmpdir"),
|
||||
var enableClipboardMonitoring: Boolean = false,
|
||||
var clipboardPollingRate: Int = 500,
|
||||
var maxEventLog: Int = 1_000,
|
||||
val tempPath: String = System.getProperty("java.io.tmpdir"),
|
||||
val cachePath: String = System.getProperty("java.io.tmpdir"),
|
||||
val enableClipboardMonitoring: Boolean = false,
|
||||
val clipboardPollingRate: Int = 500,
|
||||
val maxEventLog: Int = 1_000,
|
||||
)
|
||||
|
||||
@@ -7,4 +7,5 @@ interface MetadataRepository {
|
||||
fun save(metadata: Metadata): Metadata
|
||||
fun findByPostId(postId: Long): Optional<Metadata>
|
||||
fun deleteByPostId(postId: Long): Int
|
||||
fun deleteAllByPostId(postIds: List<Long>)
|
||||
}
|
||||
@@ -22,6 +22,7 @@ class ImageRepositoryImpl : ImageRepository {
|
||||
it[url] = image.url
|
||||
it[thumbUrl] = image.thumbUrl
|
||||
it[postIdRef] = image.postIdRef
|
||||
it[filename] = image.filename
|
||||
}.value
|
||||
return image.copy(id = id)
|
||||
}
|
||||
@@ -37,6 +38,7 @@ class ImageRepositoryImpl : ImageRepository {
|
||||
this[ImageTable.url] = it.url
|
||||
this[ImageTable.thumbUrl] = it.thumbUrl
|
||||
this[ImageTable.postIdRef] = it.postIdRef
|
||||
this[ImageTable.filename] = it.filename
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +100,7 @@ class ImageRepositoryImpl : ImageRepository {
|
||||
it[status] = image.status.name
|
||||
it[downloaded] = image.downloaded
|
||||
it[size] = image.size
|
||||
it[filename] = image.filename
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +116,7 @@ class ImageRepositoryImpl : ImageRepository {
|
||||
this[ImageTable.url] = it.url
|
||||
this[ImageTable.thumbUrl] = it.thumbUrl
|
||||
this[ImageTable.postIdRef] = it.postIdRef
|
||||
this[ImageTable.filename] = it.filename
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +154,7 @@ class ImageRepositoryImpl : ImageRepository {
|
||||
val total = resultRow[ImageTable.size]
|
||||
val status = Status.valueOf(resultRow[ImageTable.status])
|
||||
val postIdRef = resultRow[ImageTable.postIdRef]
|
||||
val filename = resultRow[ImageTable.filename]
|
||||
return Image(
|
||||
id,
|
||||
postId,
|
||||
@@ -160,7 +165,8 @@ class ImageRepositoryImpl : ImageRepository {
|
||||
postIdRef,
|
||||
total,
|
||||
current,
|
||||
status
|
||||
status,
|
||||
filename
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+55
-2
@@ -1,20 +1,73 @@
|
||||
package me.vripper.repositories.impl
|
||||
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import me.vripper.entities.Metadata
|
||||
import me.vripper.repositories.MetadataRepository
|
||||
import me.vripper.tables.MetadataTable
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||
import java.sql.Connection
|
||||
import java.util.*
|
||||
|
||||
class MetadataRepositoryImpl: MetadataRepository {
|
||||
|
||||
override fun save(metadata: Metadata): Metadata {
|
||||
MetadataTable.insert {
|
||||
it[postId] = metadata.postId
|
||||
it[data] = Json.encodeToString(metadata.data)
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
override fun findByPostId(postId: Long): Optional<Metadata> {
|
||||
return Optional.empty()
|
||||
|
||||
val result = MetadataTable.select {
|
||||
MetadataTable.postId eq postId
|
||||
}.map(::transform)
|
||||
|
||||
return if (result.isEmpty()) {
|
||||
Optional.empty()
|
||||
} else {
|
||||
Optional.of(result.first())
|
||||
}
|
||||
}
|
||||
|
||||
private fun transform(row: ResultRow): Metadata {
|
||||
val id = row[MetadataTable.postId]
|
||||
val data = Json.decodeFromString(row[MetadataTable.data]) as Metadata.Data
|
||||
return Metadata(id, data)
|
||||
}
|
||||
|
||||
override fun deleteByPostId(postId: Long): Int {
|
||||
return 0
|
||||
return MetadataTable.deleteWhere { MetadataTable.postId eq postId }
|
||||
}
|
||||
|
||||
override fun deleteAllByPostId(postIds: List<Long>) {
|
||||
val conn = TransactionManager.current().connection.connection as Connection
|
||||
conn.prepareStatement("CREATE LOCAL TEMPORARY TABLE METADATA_DELETE(POST_ID BIGINT PRIMARY KEY)")
|
||||
.use {
|
||||
it.execute()
|
||||
}
|
||||
|
||||
conn.prepareStatement("INSERT INTO METADATA_DELETE VALUES ( ? )").use { ps ->
|
||||
postIds.forEach {
|
||||
ps.setLong(1, it)
|
||||
ps.addBatch()
|
||||
}
|
||||
ps.executeBatch()
|
||||
}
|
||||
|
||||
conn.prepareStatement("DELETE FROM METADATA WHERE POST_ID IN (SELECT POST_ID FROM METADATA_DELETE)")
|
||||
.use {
|
||||
it.execute()
|
||||
}
|
||||
|
||||
conn.prepareStatement("TRUNCATE TABLE METADATA_DELETE")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,14 +144,19 @@ class AppEndpointService(
|
||||
dataTransaction.deleteAllLogs()
|
||||
}
|
||||
|
||||
fun rename(postId: Long, name: String) {
|
||||
fun rename(postId: Long, newName: String) {
|
||||
CompletableFuture.runAsync({
|
||||
synchronized(postId.toString().intern()) {
|
||||
dataTransaction.findPostByPostId(postId).ifPresent { post ->
|
||||
if (Path(post.downloadDirectory, post.folderName).exists()) {
|
||||
PathUtils.rename(post.downloadDirectory, post.folderName, name)
|
||||
PathUtils.rename(
|
||||
dataTransaction.findImagesByPostId(postId),
|
||||
post.downloadDirectory,
|
||||
post.folderName,
|
||||
newName
|
||||
)
|
||||
}
|
||||
post.folderName = name
|
||||
post.folderName = PathUtils.sanitize(newName)
|
||||
dataTransaction.updatePost(post)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package me.vripper.services
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import me.vripper.entities.*
|
||||
import me.vripper.entities.domain.Status
|
||||
import me.vripper.event.*
|
||||
@@ -22,33 +26,45 @@ class DataTransaction(
|
||||
private val eventBus: EventBus,
|
||||
) {
|
||||
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
private fun save(posts: List<Post>): List<Post> {
|
||||
return transaction { postDownloadStateRepository.save(posts) }
|
||||
}
|
||||
|
||||
fun updatePosts(posts: List<Post>) {
|
||||
transaction { postDownloadStateRepository.update(posts) }
|
||||
eventBus.publishEvent(PostUpdateEvent(posts))
|
||||
coroutineScope.launch {
|
||||
eventBus.publishEvent(PostUpdateEvent(posts))
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePost(post: Post) {
|
||||
transaction { postDownloadStateRepository.update(post) }
|
||||
eventBus.publishEvent(PostUpdateEvent(listOf(post)))
|
||||
coroutineScope.launch {
|
||||
eventBus.publishEvent(PostUpdateEvent(listOf(post)))
|
||||
}
|
||||
}
|
||||
|
||||
fun save(thread: Thread) {
|
||||
val savedThread = transaction { threadRepository.save(thread) }
|
||||
eventBus.publishEvent(ThreadCreateEvent(savedThread))
|
||||
coroutineScope.launch {
|
||||
eventBus.publishEvent(ThreadCreateEvent(savedThread))
|
||||
}
|
||||
}
|
||||
|
||||
fun updateImages(images: List<Image>) {
|
||||
transaction { imageRepository.update(images) }
|
||||
eventBus.publishEvent(ImageEvent(images))
|
||||
coroutineScope.launch {
|
||||
eventBus.publishEvent(ImageEvent(images))
|
||||
}
|
||||
}
|
||||
|
||||
fun updateImage(image: Image) {
|
||||
transaction { imageRepository.update(image) }
|
||||
eventBus.publishEvent(ImageEvent(listOf(image)))
|
||||
coroutineScope.launch {
|
||||
eventBus.publishEvent(ImageEvent(listOf(image)))
|
||||
}
|
||||
}
|
||||
|
||||
fun exists(postId: Long): Boolean {
|
||||
@@ -104,7 +120,9 @@ class DataTransaction(
|
||||
}
|
||||
savedPosts
|
||||
}
|
||||
eventBus.publishEvent(PostCreateEvent(savedPosts))
|
||||
coroutineScope.launch {
|
||||
eventBus.publishEvent(PostCreateEvent(savedPosts))
|
||||
}
|
||||
return savedPosts
|
||||
}
|
||||
|
||||
@@ -146,21 +164,25 @@ class DataTransaction(
|
||||
private fun remove(postIds: List<Long>) {
|
||||
|
||||
transaction {
|
||||
metadataRepository.deleteAllByPostId(postIds)
|
||||
imageRepository.deleteAllByPostId(postIds)
|
||||
postDownloadStateRepository.deleteAll(postIds)
|
||||
sortPostsByRank()
|
||||
}
|
||||
|
||||
|
||||
eventBus.publishEvent(PostDeleteEvent(postIds = postIds))
|
||||
eventBus.publishEvent(ErrorCountEvent(ErrorCount(countImagesInError())))
|
||||
coroutineScope.launch {
|
||||
eventBus.publishEvent(PostDeleteEvent(postIds = postIds))
|
||||
}
|
||||
coroutineScope.launch {
|
||||
eventBus.publishEvent(ErrorCountEvent(ErrorCount(countImagesInError())))
|
||||
}
|
||||
}
|
||||
|
||||
fun removeThread(threadId: Long) {
|
||||
transaction { threadRepository.deleteByThreadId(threadId) }
|
||||
|
||||
eventBus.publishEvent(ThreadDeleteEvent(threadId))
|
||||
|
||||
coroutineScope.launch {
|
||||
eventBus.publishEvent(ThreadDeleteEvent(threadId))
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCompleted(): List<Long> {
|
||||
@@ -181,17 +203,18 @@ class DataTransaction(
|
||||
transaction { imageRepository.stopByPostIdAndIsNotCompleted(postId) }
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setMetadata(post: Post, metadata: Metadata) {
|
||||
if (metadataRepository.findByPostId(post.postId).isEmpty) {
|
||||
metadata.postIdRef = post.id
|
||||
metadataRepository.save(metadata)
|
||||
fun saveMetadata(metadata: Metadata) {
|
||||
transaction { metadataRepository.save(metadata) }
|
||||
coroutineScope.launch {
|
||||
eventBus.publishEvent(MetadataUpdateEvent(metadata))
|
||||
}
|
||||
}
|
||||
|
||||
fun clearQueueLinks() {
|
||||
transaction { threadRepository.deleteAll() }
|
||||
eventBus.publishEvent(ThreadClearEvent())
|
||||
coroutineScope.launch {
|
||||
eventBus.publishEvent(ThreadClearEvent())
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@@ -255,14 +278,20 @@ class DataTransaction(
|
||||
val deleted = logRepository.deleteOldest()
|
||||
Pair(saved, deleted)
|
||||
}
|
||||
eventBus.publishEvent(LogCreateEvent(pair.first))
|
||||
eventBus.publishEvent(LogDeleteEvent(pair.second))
|
||||
coroutineScope.launch {
|
||||
eventBus.publishEvent(LogCreateEvent(pair.first))
|
||||
}
|
||||
coroutineScope.launch {
|
||||
eventBus.publishEvent(LogDeleteEvent(pair.second))
|
||||
}
|
||||
return pair.first
|
||||
}
|
||||
|
||||
fun updateLog(logEntry: LogEntry) {
|
||||
transaction { logRepository.update(logEntry) }
|
||||
eventBus.publishEvent(LogUpdateEvent(logEntry))
|
||||
coroutineScope.launch {
|
||||
eventBus.publishEvent(LogUpdateEvent(logEntry))
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAllLogs() {
|
||||
@@ -276,4 +305,8 @@ class DataTransaction(
|
||||
fun findAllNonCompletedPostIds(): List<Long> {
|
||||
return transaction { postDownloadStateRepository.findAllNonCompletedPostIds() }
|
||||
}
|
||||
|
||||
fun findMetadataByPostId(postId: Long): Optional<Metadata> {
|
||||
return transaction { metadataRepository.findByPostId(postId) }
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package me.vripper.services
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import me.vripper.event.DownloadSpeedEvent
|
||||
import me.vripper.event.EventBus
|
||||
import me.vripper.event.QueueStateEvent
|
||||
@@ -21,7 +22,8 @@ class DownloadSpeedService(
|
||||
|
||||
fun init() {
|
||||
coroutineScope.launch {
|
||||
eventBus.events.ofType(QueueStateEvent::class.java).subscribe {
|
||||
eventBus.events.filterIsInstance(QueueStateEvent::class).collect {
|
||||
coroutineContext.ensureActive()
|
||||
if (it.queueState.running + it.queueState.remaining > 0) {
|
||||
if (job == null || job?.isActive == false) {
|
||||
job = coroutineScope.launch {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package me.vripper.services
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import me.vripper.event.EventBus
|
||||
import me.vripper.event.SettingsUpdateEvent
|
||||
import org.apache.hc.client5.http.config.ConnectionConfig
|
||||
@@ -17,7 +18,7 @@ import org.apache.hc.core5.util.TimeValue
|
||||
import org.apache.hc.core5.util.Timeout
|
||||
|
||||
class HTTPService(
|
||||
val eventBus: EventBus,
|
||||
private val eventBus: EventBus,
|
||||
settingsService: SettingsService
|
||||
) {
|
||||
|
||||
@@ -43,20 +44,22 @@ class HTTPService(
|
||||
pcm.closeIdle(TimeValue.ofSeconds(60))
|
||||
delay(15000)
|
||||
}
|
||||
eventBus
|
||||
.events
|
||||
.ofType(SettingsUpdateEvent::class.java)
|
||||
.subscribe {
|
||||
if (connectionTimeout != it.settings.connectionSettings.timeout) {
|
||||
connectionTimeout = it.settings.connectionSettings.timeout
|
||||
client.close()
|
||||
pcm.close()
|
||||
buildRequestConfig()
|
||||
buildConnectionConfig()
|
||||
buildConnectionPool()
|
||||
buildClientBuilder()
|
||||
coroutineScope.launch {
|
||||
eventBus
|
||||
.events
|
||||
.filterIsInstance(SettingsUpdateEvent::class)
|
||||
.collect {
|
||||
if (connectionTimeout != it.settings.connectionSettings.timeout) {
|
||||
connectionTimeout = it.settings.connectionSettings.timeout
|
||||
client.close()
|
||||
pcm.close()
|
||||
buildRequestConfig()
|
||||
buildConnectionConfig()
|
||||
buildConnectionPool()
|
||||
buildClientBuilder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildConnectionPool() {
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package me.vripper.services
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import me.vripper.entities.Metadata
|
||||
import me.vripper.exception.DownloadException
|
||||
import me.vripper.exception.VripperException
|
||||
import me.vripper.utilities.HtmlUtils
|
||||
import me.vripper.utilities.XpathUtils
|
||||
import org.apache.hc.client5.http.classic.methods.HttpGet
|
||||
import org.apache.hc.core5.net.URIBuilder
|
||||
import org.w3c.dom.Node
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.stream.Collectors
|
||||
|
||||
|
||||
class MetadataService(
|
||||
private val httpService: HTTPService,
|
||||
private val settingsService: SettingsService,
|
||||
private val dataTransaction: DataTransaction,
|
||||
private val vgAuthService: VGAuthService,
|
||||
) {
|
||||
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private val requestSemaphore = Semaphore(6)
|
||||
private val dictionary: List<String> =
|
||||
mutableListOf("download", "link", "rapidgator", "filefactory", "filefox")
|
||||
|
||||
fun init() {
|
||||
dataTransaction.findAllPosts()
|
||||
.filter { dataTransaction.findMetadataByPostId(it.postId).isEmpty }.map { it.postId }
|
||||
.forEach(::fetchMetadata)
|
||||
}
|
||||
|
||||
fun fetchMetadata(postId: Long) {
|
||||
coroutineScope.launch {
|
||||
requestSemaphore.withPermit {
|
||||
val httpGet =
|
||||
HttpGet(URIBuilder(settingsService.settings.viperSettings.host + "/threads/").also {
|
||||
it.setParameter(
|
||||
"p", postId.toString()
|
||||
)
|
||||
}.build())
|
||||
|
||||
val metadata = httpService.client.execute(httpGet, vgAuthService.context) {
|
||||
if (it.code / 100 != 2) {
|
||||
throw DownloadException("Unexpected response code '${it.code}' for $httpGet")
|
||||
}
|
||||
val document = HtmlUtils.clean(it.entity.content)
|
||||
val postNode: Node = XpathUtils.getAsNode(
|
||||
document,
|
||||
"//li[@id='post_$postId']/div[contains(@class, 'postdetails')]",
|
||||
|
||||
) ?: throw VripperException("Unable to find post #'$postId'")
|
||||
|
||||
val postedBy: String = XpathUtils.getAsNode(
|
||||
postNode,
|
||||
"./div[contains(@class, 'userinfo')]//a[contains(@class, 'username')]//font"
|
||||
)?.textContent?.trim()
|
||||
?: throw VripperException("Unable to find the poster for post #'$postId'")
|
||||
|
||||
|
||||
val node: Node = XpathUtils.getAsNode(
|
||||
document, java.lang.String.format("//div[@id='post_message_%s']", postId)
|
||||
) ?: throw VripperException("Unable to locate post content")
|
||||
val titles = findTitleInContent(node)
|
||||
Metadata(postId, Metadata.Data(postedBy, titles))
|
||||
}
|
||||
dataTransaction.saveMetadata(metadata)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun findTitleInContent(node: Node): List<String> {
|
||||
val altTitle: MutableList<String> = mutableListOf()
|
||||
findTitle(node, altTitle, AtomicBoolean(true))
|
||||
return altTitle.stream().distinct().collect(Collectors.toList())
|
||||
}
|
||||
|
||||
private fun findTitle(node: Node, altTitle: MutableList<String>, keepGoing: AtomicBoolean) {
|
||||
if (!keepGoing.get()) {
|
||||
return
|
||||
}
|
||||
if (node.nodeName == "a" || node.nodeName == "img") {
|
||||
keepGoing.set(false)
|
||||
return
|
||||
}
|
||||
if (node.nodeType == Node.ELEMENT_NODE) {
|
||||
for (i in 0 until node.childNodes.length) {
|
||||
val item = node.childNodes.item(i)
|
||||
findTitle(item, altTitle, keepGoing)
|
||||
if (!keepGoing.get()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if (node.nodeType == Node.TEXT_NODE) {
|
||||
val text = node.textContent.trim()
|
||||
if (text.isNotBlank() && dictionary.stream().noneMatch { e ->
|
||||
text.lowercase().contains(e.lowercase())
|
||||
}) {
|
||||
altTitle.add(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
package me.vripper.services
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.launch
|
||||
import me.vripper.event.EventBus
|
||||
import me.vripper.event.SettingsUpdateEvent
|
||||
import net.jodah.failsafe.RetryPolicy
|
||||
@@ -11,14 +16,16 @@ class RetryPolicyService(
|
||||
) {
|
||||
private val log by me.vripper.delegate.LoggerDelegate()
|
||||
private var maxAttempts: Int = settingsService.settings.connectionSettings.maxAttempts
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
fun init() {
|
||||
eventBus.events.ofType(SettingsUpdateEvent::class.java).subscribe {
|
||||
if (maxAttempts != it.settings.connectionSettings.maxAttempts) {
|
||||
maxAttempts = it.settings.connectionSettings.maxAttempts
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(SettingsUpdateEvent::class).collect {
|
||||
if (maxAttempts != it.settings.connectionSettings.maxAttempts) {
|
||||
maxAttempts = it.settings.connectionSettings.maxAttempts
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun <T> buildRetryPolicyForDownload(): RetryPolicy<T> {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package me.vripper.services
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import me.vripper.event.EventBus
|
||||
import me.vripper.event.SettingsUpdateEvent
|
||||
import me.vripper.exception.ValidationException
|
||||
@@ -13,42 +15,45 @@ import me.vripper.utilities.ApplicationProperties.VRIPPER_DIR
|
||||
import org.apache.commons.codec.digest.DigestUtils
|
||||
import java.io.FileWriter
|
||||
import java.nio.file.*
|
||||
import kotlin.io.path.readText
|
||||
|
||||
class SettingsService(private val eventBus: EventBus) {
|
||||
|
||||
private val log by me.vripper.delegate.LoggerDelegate()
|
||||
private val configPath = VRIPPER_DIR.resolve("config.yml")
|
||||
private val configPath = VRIPPER_DIR.resolve("config.json")
|
||||
private val customProxiesPath = VRIPPER_DIR.resolve("proxies.json")
|
||||
private val om = ObjectMapper(YAMLFactory())
|
||||
private val proxies: MutableSet<String> = HashSet()
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
|
||||
private val json = Json {
|
||||
encodeDefaults = true
|
||||
prettyPrint = true
|
||||
}
|
||||
var settings = Settings()
|
||||
|
||||
init {
|
||||
om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
.findAndRegisterModules().registerKotlinModule()
|
||||
|
||||
init()
|
||||
}
|
||||
|
||||
private fun init() {
|
||||
loadViperProxies()
|
||||
restore()
|
||||
eventBus.publishEvent(SettingsUpdateEvent(settings))
|
||||
coroutineScope.launch {
|
||||
eventBus.publishEvent(SettingsUpdateEvent(settings))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun loadViperProxies() {
|
||||
try {
|
||||
SettingsService::class.java.getResourceAsStream("/proxies.json")?.use {
|
||||
val defaultProxies: List<String> = om.readValue(it)
|
||||
val defaultProxies: List<String> = json.decodeFromStream(it)
|
||||
val customProxies: List<String> = if (customProxiesPath.toFile()
|
||||
.exists() && Files.isRegularFile(customProxiesPath)
|
||||
) {
|
||||
try {
|
||||
om.readValue(
|
||||
customProxiesPath.toFile()
|
||||
json.decodeFromString(
|
||||
customProxiesPath.readText()
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
@@ -81,36 +86,35 @@ class SettingsService(private val eventBus: EventBus) {
|
||||
}
|
||||
|
||||
fun newSettings(settings: Settings) {
|
||||
if (settings.viperSettings.login) {
|
||||
check(settings)
|
||||
val viperSettings = if (settings.viperSettings.login) {
|
||||
if (this.settings.viperSettings.password != settings.viperSettings.password) {
|
||||
settings.viperSettings.password =
|
||||
DigestUtils.md5Hex(settings.viperSettings.password)
|
||||
settings.viperSettings.copy(password = DigestUtils.md5Hex(settings.viperSettings.password))
|
||||
} else {
|
||||
settings.viperSettings
|
||||
}
|
||||
} else {
|
||||
settings.viperSettings.username = ""
|
||||
settings.viperSettings.password = ""
|
||||
settings.viperSettings.thanks = false
|
||||
settings.viperSettings.login = false
|
||||
settings.viperSettings.copy(username = "", password = "", thanks = false, login = false)
|
||||
}
|
||||
check(settings)
|
||||
this.settings = settings
|
||||
this.settings = settings.copy(viperSettings = viperSettings)
|
||||
save()
|
||||
|
||||
eventBus.publishEvent(SettingsUpdateEvent(settings))
|
||||
|
||||
coroutineScope.launch {
|
||||
eventBus.publishEvent(SettingsUpdateEvent(settings))
|
||||
}
|
||||
}
|
||||
|
||||
private fun restore() {
|
||||
try {
|
||||
if (configPath.toFile().exists()) {
|
||||
settings = om.readValue(configPath.toFile())
|
||||
settings = json.decodeFromString(configPath.readText())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log.error("Failed restore user settings", e)
|
||||
settings = Settings()
|
||||
}
|
||||
if (!proxies.contains(settings.viperSettings.host)) {
|
||||
settings.viperSettings.host = "https://vipergirls.to"
|
||||
val viperSetting = settings.viperSettings.copy(host = "https://vipergirls.to")
|
||||
settings = settings.copy(viperSettings = viperSetting)
|
||||
}
|
||||
try {
|
||||
check(settings)
|
||||
@@ -123,9 +127,9 @@ class SettingsService(private val eventBus: EventBus) {
|
||||
|
||||
fun save() {
|
||||
try {
|
||||
Files.write(
|
||||
Files.writeString(
|
||||
configPath,
|
||||
om.writeValueAsBytes(settings),
|
||||
json.encodeToString(settings),
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.WRITE,
|
||||
StandardOpenOption.TRUNCATE_EXISTING,
|
||||
|
||||
@@ -2,6 +2,11 @@ package me.vripper.services
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.launch
|
||||
import me.vripper.event.EventBus
|
||||
import me.vripper.event.SettingsUpdateEvent
|
||||
import me.vripper.model.ThreadItem
|
||||
@@ -12,14 +17,18 @@ import java.util.concurrent.TimeUnit
|
||||
|
||||
class ThreadCacheService(val eventBus: EventBus) {
|
||||
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
fun init() {
|
||||
eventBus.events.ofType(SettingsUpdateEvent::class.java).subscribe {
|
||||
cache.invalidateAll()
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(SettingsUpdateEvent::class).collect {
|
||||
cache.invalidateAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val cache: LoadingCache<Long, ThreadItem> =
|
||||
Caffeine.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).build { threadId ->
|
||||
Caffeine.newBuilder().expireAfterWrite(20, TimeUnit.MINUTES).build { threadId ->
|
||||
ThreadLookupAPIParser(threadId).parse()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package me.vripper.services
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.launch
|
||||
import me.vripper.entities.Post
|
||||
import me.vripper.event.EventBus
|
||||
import me.vripper.event.SettingsUpdateEvent
|
||||
@@ -18,8 +23,11 @@ import org.apache.hc.core5.http.message.BasicNameValuePair
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class VGAuthService(
|
||||
private val cm: HTTPService, private val settingsService: SettingsService, private val eventBus: EventBus
|
||||
private val cm: HTTPService,
|
||||
private val settingsService: SettingsService,
|
||||
private val eventBus: EventBus
|
||||
) {
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val log by me.vripper.delegate.LoggerDelegate()
|
||||
val context: HttpClientContext = HttpClientContext.create()
|
||||
var loggedUser = ""
|
||||
@@ -28,8 +36,10 @@ class VGAuthService(
|
||||
|
||||
fun init() {
|
||||
context.cookieStore = BasicCookieStore()
|
||||
eventBus.events.ofType(SettingsUpdateEvent::class.java).subscribe {
|
||||
authenticate(it.settings)
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(SettingsUpdateEvent::class).collect {
|
||||
authenticate(it.settings)
|
||||
}
|
||||
}
|
||||
authenticate(settingsService.settings)
|
||||
}
|
||||
@@ -40,7 +50,9 @@ class VGAuthService(
|
||||
log.debug("Authentication option is disabled")
|
||||
context.cookieStore.clear()
|
||||
loggedUser = ""
|
||||
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
|
||||
coroutineScope.launch {
|
||||
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -51,24 +63,26 @@ class VGAuthService(
|
||||
context.cookieStore.clear()
|
||||
loggedUser = ""
|
||||
|
||||
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
|
||||
coroutineScope.launch {
|
||||
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
val postAuth = HttpPost(settings.viperSettings.host + "/login.php?do=login").also {
|
||||
it.entity = UrlEncodedFormEntity(
|
||||
listOf(
|
||||
BasicNameValuePair("vb_login_username", username),
|
||||
BasicNameValuePair("cookieuser", "1"),
|
||||
BasicNameValuePair("do", "login"),
|
||||
BasicNameValuePair("vb_login_md5password", password)
|
||||
)
|
||||
it.entity = UrlEncodedFormEntity(
|
||||
listOf(
|
||||
BasicNameValuePair("vb_login_username", username),
|
||||
BasicNameValuePair("cookieuser", "1"),
|
||||
BasicNameValuePair("do", "login"),
|
||||
BasicNameValuePair("vb_login_md5password", password)
|
||||
)
|
||||
it.addHeader("Referer", settings.viperSettings.host)
|
||||
it.addHeader(
|
||||
"Host", settings.viperSettings.host.replace("https://", "").replace("http://", "")
|
||||
)
|
||||
}
|
||||
)
|
||||
it.addHeader("Referer", settings.viperSettings.host)
|
||||
it.addHeader(
|
||||
"Host", settings.viperSettings.host.replace("https://", "").replace("http://", "")
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
cm.client.execute(postAuth, context) { response ->
|
||||
@@ -81,7 +95,8 @@ class VGAuthService(
|
||||
if (context.cookieStore.cookies.stream().map { obj: Cookie -> obj.name }
|
||||
.noneMatch { e: String -> e == "vg_userid" }) {
|
||||
log.error(
|
||||
"Failed to authenticate user with {}, missing vg_userid cookie", settings.viperSettings.host
|
||||
"Failed to authenticate user with {}, missing vg_userid cookie",
|
||||
settings.viperSettings.host
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -89,7 +104,9 @@ class VGAuthService(
|
||||
context.cookieStore.clear()
|
||||
loggedUser = ""
|
||||
|
||||
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
|
||||
coroutineScope.launch {
|
||||
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
|
||||
}
|
||||
|
||||
log.error(
|
||||
"Failed to authenticate user with " + settings.viperSettings.host, e
|
||||
@@ -100,11 +117,14 @@ class VGAuthService(
|
||||
loggedUser = username
|
||||
log.info(String.format("Authenticated: %s", username))
|
||||
|
||||
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
|
||||
|
||||
coroutineScope.launch {
|
||||
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
|
||||
}
|
||||
}
|
||||
|
||||
fun leaveThanks(post: Post) {
|
||||
CompletableFuture.runAsync(LeaveThanksRunnable(post, authenticated, context), GLOBAL_EXECUTOR)
|
||||
CompletableFuture.runAsync(
|
||||
LeaveThanksRunnable(post, authenticated, context), GLOBAL_EXECUTOR
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ object ImageTable : LongIdTable(name = "IMAGE", columnName = "ID") {
|
||||
val index = integer("INDEX")
|
||||
val postId = long("POST_ID")
|
||||
val status = varchar("STATUS", 15)
|
||||
val filename = varchar("FILENAME", 260)
|
||||
val size = long("SIZE")
|
||||
val url = varchar("URL", 200)
|
||||
val thumbUrl = varchar("THUMB_URL", 200)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package me.vripper.tables
|
||||
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
|
||||
object MetadataTable : Table(name = "METADATA") {
|
||||
val postId = long("POST_ID")
|
||||
val data = varchar("DATA", 1_000_000)
|
||||
}
|
||||
@@ -28,64 +28,28 @@ class AddPostRunnable(private val items: List<ThreadPostId>) : KoinComponent, Ru
|
||||
private val retryPolicyService: RetryPolicyService by inject()
|
||||
private val downloadService: DownloadService by inject()
|
||||
private val cacheService: ThreadCacheService by inject()
|
||||
// private val logEntry: LogEntry
|
||||
|
||||
init {
|
||||
// logEntry = dataTransaction.saveLog(
|
||||
// LogEntry(
|
||||
// type = LogEntry.Type.POST, status = PENDING, message = "Processing $link"
|
||||
// )
|
||||
// )
|
||||
}
|
||||
private val metadataService: MetadataService by inject()
|
||||
|
||||
override fun run() {
|
||||
try {
|
||||
Tasks.increment()
|
||||
val toProcess = mutableListOf<PostItem>()
|
||||
for ((threadId, postId) in items) {
|
||||
// dataTransaction.updateLog(logEntry.copy(status = PROCESSING))
|
||||
if (dataTransaction.exists(postId)) {
|
||||
log.warn(String.format("skipping %s, already loaded", postId))
|
||||
continue
|
||||
}
|
||||
|
||||
val threadItem = cacheService[threadId]
|
||||
// val postItem: PostItem = try {
|
||||
val postItem: PostItem =
|
||||
threadItem.postItemList.find { it.postId == postId } ?: parse(postId, threadId)
|
||||
// } catch (e: PostParseException) {
|
||||
// val error = String.format("parsing failed for gallery %s", link)
|
||||
// log.error(error, e)
|
||||
// dataTransaction.updateLog(
|
||||
// logEntry.copy(
|
||||
// status = ERROR, message = """
|
||||
// $error
|
||||
// ${e.formatToString()}
|
||||
// """.trimIndent()
|
||||
// )
|
||||
// )
|
||||
// return@map
|
||||
// }
|
||||
// if (postItem.imageItemList.isEmpty()) {
|
||||
// val error = "Post $link contains no images to download"
|
||||
// log.error(error)
|
||||
// dataTransaction.updateLog(logEntry.copy(status = ERROR, message = error))
|
||||
// return@map
|
||||
// }
|
||||
|
||||
|
||||
// dataTransaction.updateLog(
|
||||
// logEntry.copy(
|
||||
// status = DONE, message = String.format(
|
||||
// "Post $link has been successfully added to download queue"
|
||||
// )
|
||||
// )
|
||||
// )
|
||||
toProcess.add(postItem)
|
||||
}
|
||||
|
||||
val posts = dataTransaction.newPosts(toProcess.toList())
|
||||
// metadataService.startFetchingMetadata(post)
|
||||
posts.forEach {
|
||||
metadataService.fetchMetadata(it.postId)
|
||||
}
|
||||
if (settingsService.settings.downloadSettings.autoStart) {
|
||||
log.debug("Auto start downloads option is enabled")
|
||||
downloadService.restartAll(posts)
|
||||
@@ -93,14 +57,6 @@ class AddPostRunnable(private val items: List<ThreadPostId>) : KoinComponent, Ru
|
||||
} catch (e: Exception) {
|
||||
val error = String.format("Error when adding galleries")
|
||||
log.error(error, e)
|
||||
// dataTransaction.updateLog(
|
||||
// logEntry.copy(
|
||||
// status = ERROR, message = """
|
||||
// $error
|
||||
// ${e.formatToString()}
|
||||
// """.trimIndent()
|
||||
// )
|
||||
// )
|
||||
} finally {
|
||||
Tasks.decrement()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package me.vripper.utilities
|
||||
|
||||
import me.vripper.entities.Image
|
||||
import me.vripper.exception.RenameException
|
||||
import me.vripper.model.Settings
|
||||
import java.io.IOException
|
||||
@@ -7,6 +8,7 @@ import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardCopyOption
|
||||
import kotlin.io.path.Path
|
||||
import kotlin.io.path.listDirectoryEntries
|
||||
|
||||
|
||||
object PathUtils {
|
||||
@@ -28,15 +30,32 @@ object PathUtils {
|
||||
}
|
||||
|
||||
@Throws(RenameException::class)
|
||||
fun rename(downloadDirectory: String, oldFolder: String, newFolder: String) {
|
||||
fun rename(
|
||||
imageList: List<Image>,
|
||||
downloadDirectory: String,
|
||||
oldFolder: String,
|
||||
newFolder: String
|
||||
) {
|
||||
val currentDownloadDirectory = Path(downloadDirectory, oldFolder)
|
||||
val newDownloadDirectory = Path(downloadDirectory, sanitize(newFolder))
|
||||
if (currentDownloadDirectory == newDownloadDirectory) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
Files.move(
|
||||
currentDownloadDirectory,
|
||||
newDownloadDirectory,
|
||||
StandardCopyOption.ATOMIC_MOVE
|
||||
)
|
||||
Files.createDirectories(newDownloadDirectory)
|
||||
imageList.filter { it.filename.isNotBlank() }.forEach {
|
||||
Files.move(
|
||||
currentDownloadDirectory.resolve(it.filename),
|
||||
newDownloadDirectory.resolve(it.filename),
|
||||
StandardCopyOption.ATOMIC_MOVE
|
||||
)
|
||||
}
|
||||
if (currentDownloadDirectory.listDirectoryEntries().isEmpty()) {
|
||||
try {
|
||||
Files.delete(currentDownloadDirectory)
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw RenameException(
|
||||
String.format(
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package me.vripper.utilities
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import me.vripper.event.EventBus
|
||||
import me.vripper.event.LoadingTasks
|
||||
import org.koin.core.component.KoinComponent
|
||||
@@ -9,11 +13,14 @@ object Tasks : KoinComponent {
|
||||
|
||||
private val eventBus: EventBus by inject()
|
||||
private var current = 0
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
@Synchronized
|
||||
fun increment() {
|
||||
if (current == 0) {
|
||||
eventBus.publishEvent(LoadingTasks(true))
|
||||
coroutineScope.launch {
|
||||
eventBus.publishEvent(LoadingTasks(true))
|
||||
}
|
||||
}
|
||||
current += 1
|
||||
}
|
||||
@@ -22,7 +29,9 @@ object Tasks : KoinComponent {
|
||||
fun decrement() {
|
||||
current -= 1
|
||||
if (current == 0) {
|
||||
eventBus.publishEvent(LoadingTasks(false))
|
||||
coroutineScope.launch {
|
||||
eventBus.publishEvent(LoadingTasks(false))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,9 +145,24 @@
|
||||
<column name="THREAD_ID"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
<changeSet id="0006" author="vripper">
|
||||
<addColumn tableName="POST">
|
||||
<column name="FOLDER_NAME" type="VARCHAR(260)" afterColumn="OUTPUT_PATH" defaultValue=""/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
<changeSet id="0006" author="vripper">
|
||||
<addColumn tableName="POST">
|
||||
<column name="FOLDER_NAME" type="VARCHAR(260)" afterColumn="OUTPUT_PATH" defaultValue=""/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
<changeSet author="vripper" id="0007">
|
||||
<createTable tableName="METADATA">
|
||||
<column name="POST_ID" type="BIGINT">
|
||||
<constraints unique="true" nullable="false"/>
|
||||
</column>
|
||||
<column name="DATA" type="VARCHAR">
|
||||
<constraints nullable="true"/>
|
||||
</column>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
<changeSet author="vripper" id="0008">
|
||||
<addColumn tableName="IMAGE">
|
||||
<column name="FILENAME" type="VARCHAR(260)" afterColumn="STATUS" defaultValue=""/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
|
||||
+15
-9
@@ -29,6 +29,10 @@
|
||||
<artifactId>kotlinx-coroutines-core-jvm</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlinx</groupId>
|
||||
<artifactId>kotlinx-serialization-json</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<artifactId>javafx-base</artifactId>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<version>${javafx.version}</version>
|
||||
@@ -119,19 +123,21 @@
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<sourceDirs>
|
||||
<source>src/main/kotlin</source>
|
||||
<source>target/generated-sources/annotations</source>
|
||||
</sourceDirs>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<args>
|
||||
<arg>-Xjsr305=strict</arg>
|
||||
</args>
|
||||
<jvmTarget>1.8</jvmTarget>
|
||||
<compilerPlugins>
|
||||
<plugin>kotlinx-serialization</plugin>
|
||||
</compilerPlugins>
|
||||
</configuration>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-maven-serialization</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
|
||||
@@ -2,12 +2,13 @@ package me.vripper.gui.clipboard
|
||||
|
||||
import javafx.scene.input.Clipboard
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import me.vripper.event.EventBus
|
||||
import me.vripper.event.SettingsUpdateEvent
|
||||
import me.vripper.model.Settings
|
||||
import me.vripper.services.AppEndpointService
|
||||
import me.vripper.services.SettingsService
|
||||
import tornadofx.runLater
|
||||
import tornadofx.*
|
||||
|
||||
class ClipboardService(
|
||||
private val appEndpointService: AppEndpointService,
|
||||
@@ -19,8 +20,10 @@ class ClipboardService(
|
||||
private var pollJob: Job? = null
|
||||
|
||||
fun init() {
|
||||
eventBus.events.ofType(SettingsUpdateEvent::class.java).subscribe {
|
||||
run(it.settings)
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(SettingsUpdateEvent::class).collect {
|
||||
run(it.settings)
|
||||
}
|
||||
}
|
||||
run(settingsService.settings)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package me.vripper.gui.controller
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.time.sample
|
||||
import me.vripper.event.*
|
||||
import me.vripper.event.EventBus
|
||||
import me.vripper.gui.model.GlobalStateModel
|
||||
import me.vripper.services.VGAuthService
|
||||
import me.vripper.utilities.formatSI
|
||||
import tornadofx.Controller
|
||||
import tornadofx.runLater
|
||||
import tornadofx.*
|
||||
import java.time.Duration
|
||||
|
||||
class GlobalStateController : Controller() {
|
||||
@@ -13,40 +16,51 @@ class GlobalStateController : Controller() {
|
||||
private val eventBus: EventBus by di()
|
||||
private val vgAuthService: VGAuthService by di()
|
||||
|
||||
var globalState: GlobalStateModel = GlobalStateModel(0, 0, 0, vgAuthService.loggedUser, 0L.formatSI(), false)
|
||||
var globalState: GlobalStateModel =
|
||||
GlobalStateModel(0, 0, 0, vgAuthService.loggedUser, 0L.formatSI(), false)
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
fun init() {
|
||||
eventBus.events.ofType(DownloadSpeedEvent::class.java).subscribe {
|
||||
runLater {
|
||||
globalState.downloadSpeed = it.downloadSpeed.speed.formatSI()
|
||||
}
|
||||
}
|
||||
|
||||
eventBus.events.ofType(VGUserLoginEvent::class.java).subscribe {
|
||||
runLater {
|
||||
globalState.loggedUser = it.username
|
||||
}
|
||||
}
|
||||
|
||||
eventBus.events.ofType(QueueStateEvent::class.java).subscribe {
|
||||
runLater {
|
||||
globalState.apply {
|
||||
running = it.queueState.running
|
||||
remaining = it.queueState.remaining
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(DownloadSpeedEvent::class).collect {
|
||||
runLater {
|
||||
globalState.downloadSpeed = it.downloadSpeed.speed.formatSI()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eventBus.events.ofType(ErrorCountEvent::class.java).subscribe {
|
||||
runLater {
|
||||
globalState.error = it.errorCount.count
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(VGUserLoginEvent::class).collect {
|
||||
runLater {
|
||||
globalState.loggedUser = it.username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eventBus.events.ofType(LoadingTasks::class.java).sample(Duration.ofMillis(500)).subscribe {
|
||||
runLater {
|
||||
globalState.loading = it.loading
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(QueueStateEvent::class).collect {
|
||||
runLater {
|
||||
globalState.apply {
|
||||
running = it.queueState.running
|
||||
remaining = it.queueState.remaining
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(ErrorCountEvent::class).collect {
|
||||
runLater {
|
||||
globalState.error = it.errorCount.count
|
||||
}
|
||||
}
|
||||
}
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(LoadingTasks::class).sample(Duration.ofMillis(500))
|
||||
.collect {
|
||||
runLater {
|
||||
globalState.loading = it.loading
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package me.vripper.gui.controller
|
||||
import me.vripper.entities.Image
|
||||
import me.vripper.gui.model.ImageModel
|
||||
import me.vripper.services.DataTransaction
|
||||
import tornadofx.Controller
|
||||
import tornadofx.*
|
||||
|
||||
class ImageController : Controller() {
|
||||
|
||||
@@ -21,7 +21,8 @@ class ImageController : Controller() {
|
||||
progress(it.size, it.downloaded),
|
||||
it.status.name,
|
||||
it.size,
|
||||
it.downloaded
|
||||
it.downloaded,
|
||||
it.filename
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,12 @@ package me.vripper.gui.controller
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import me.vripper.entities.Image
|
||||
import me.vripper.entities.Metadata
|
||||
import me.vripper.gui.model.PostModel
|
||||
import me.vripper.services.AppEndpointService
|
||||
import me.vripper.services.DataTransaction
|
||||
import me.vripper.utilities.formatSI
|
||||
import tornadofx.Controller
|
||||
import tornadofx.*
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
class PostController : Controller() {
|
||||
@@ -62,6 +63,7 @@ class PostController : Controller() {
|
||||
|
||||
fun mapper(id: Long): PostModel {
|
||||
val post = dataTransaction.findPostById(id).orElseThrow()
|
||||
val metadata = dataTransaction.findMetadataByPostId(post.postId)
|
||||
return PostModel(
|
||||
post.postId,
|
||||
post.postTitle,
|
||||
@@ -76,7 +78,8 @@ class PostController : Controller() {
|
||||
post.getDownloadFolder(),
|
||||
post.folderName,
|
||||
progressCount(post.total, post.done, post.downloaded),
|
||||
dataTransaction.findImagesByPostId(post.postId).map(Image::thumbUrl).take(4)
|
||||
dataTransaction.findImagesByPostId(post.postId).map(Image::thumbUrl).take(4),
|
||||
metadata.orElse(Metadata(post.postId, Metadata.Data("", emptyList())))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import me.vripper.gui.model.settings.SystemSettingsModel
|
||||
import me.vripper.gui.model.settings.ViperSettingsModel
|
||||
import me.vripper.model.*
|
||||
import me.vripper.services.SettingsService
|
||||
import tornadofx.Controller
|
||||
import tornadofx.*
|
||||
|
||||
class SettingsController : Controller() {
|
||||
|
||||
@@ -34,31 +34,32 @@ class SettingsController : Controller() {
|
||||
viperSettingsModel: ViperSettingsModel,
|
||||
systemSettingsModel: SystemSettingsModel
|
||||
) {
|
||||
settingsService.newSettings(Settings().apply {
|
||||
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,
|
||||
)
|
||||
systemSettings =
|
||||
settingsService.newSettings(
|
||||
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,
|
||||
),
|
||||
systemSettings =
|
||||
SystemSettings(
|
||||
systemSettingsModel.tempPath,
|
||||
systemSettingsModel.cachePath,
|
||||
@@ -66,8 +67,8 @@ class SettingsController : Controller() {
|
||||
if (systemSettingsModel.pollingRate.isBlank()) 500 else systemSettingsModel.pollingRate.toInt(),
|
||||
systemSettingsModel.logEntries.toInt()
|
||||
)
|
||||
})
|
||||
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun getProxies(): List<String> {
|
||||
|
||||
@@ -5,8 +5,7 @@ import javafx.beans.property.SimpleIntegerProperty
|
||||
import javafx.beans.property.SimpleLongProperty
|
||||
import javafx.beans.property.SimpleStringProperty
|
||||
import me.vripper.utilities.formatSI
|
||||
import tornadofx.getValue
|
||||
import tornadofx.setValue
|
||||
import tornadofx.*
|
||||
|
||||
class ImageModel(
|
||||
id: Long,
|
||||
@@ -15,7 +14,8 @@ class ImageModel(
|
||||
progress: Double,
|
||||
status: String,
|
||||
size: Long,
|
||||
downloaded: Long
|
||||
downloaded: Long,
|
||||
filename: String
|
||||
) {
|
||||
|
||||
val idProperty = SimpleLongProperty(id)
|
||||
@@ -33,6 +33,9 @@ class ImageModel(
|
||||
val statusProperty = SimpleStringProperty(status)
|
||||
var status: String by statusProperty
|
||||
|
||||
val filenameProperty = SimpleStringProperty(filename)
|
||||
var filename: String by filenameProperty
|
||||
|
||||
val sizeProperty = SimpleStringProperty(size.formatSI())
|
||||
var size = size
|
||||
set(value) {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package me.vripper.gui.model
|
||||
|
||||
import javafx.beans.property.SimpleDoubleProperty
|
||||
import javafx.beans.property.SimpleIntegerProperty
|
||||
import javafx.beans.property.SimpleLongProperty
|
||||
import javafx.beans.property.SimpleStringProperty
|
||||
import tornadofx.getValue
|
||||
import tornadofx.setValue
|
||||
import javafx.beans.property.*
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
import me.vripper.entities.Metadata
|
||||
import tornadofx.*
|
||||
|
||||
class PostModel(
|
||||
postId: Long,
|
||||
@@ -21,7 +20,8 @@ class PostModel(
|
||||
path: String,
|
||||
folderName: String,
|
||||
progressCount: String,
|
||||
previewList: List<String>
|
||||
previewList: List<String>,
|
||||
metadata: Metadata
|
||||
) {
|
||||
val postIdProperty = SimpleLongProperty(postId)
|
||||
var postId: Long by postIdProperty
|
||||
@@ -64,4 +64,11 @@ class PostModel(
|
||||
|
||||
val previewListProperty = SimpleStringProperty(previewList.joinToString("|"))
|
||||
var previewList: List<String> = previewListProperty.value.split("|")
|
||||
|
||||
val altTitlesProperty =
|
||||
SimpleListProperty(FXCollections.observableArrayList(metadata.data.resolvedNames))
|
||||
var altTitles: ObservableList<String> by altTitlesProperty
|
||||
|
||||
val postedByProperty = SimpleStringProperty(metadata.data.postedBy)
|
||||
var postedby: String by postedByProperty
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@ package me.vripper.gui.view.popup
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty
|
||||
import javafx.geometry.Pos
|
||||
import javafx.scene.control.TextField
|
||||
import javafx.scene.layout.Priority
|
||||
import javafx.scene.layout.VBox
|
||||
import javafx.scene.control.ComboBox
|
||||
import me.vripper.gui.controller.PostController
|
||||
import tornadofx.*
|
||||
|
||||
@@ -12,29 +10,32 @@ class RenameView : Fragment("Rename download post") {
|
||||
|
||||
val postId: Long by param()
|
||||
val name: String by param()
|
||||
private val textAreaProperty = SimpleStringProperty()
|
||||
val altTitles: List<String> by param()
|
||||
private val textInputProperty = SimpleStringProperty()
|
||||
private val postController: PostController by inject()
|
||||
lateinit var input: TextField
|
||||
private lateinit var comboBox: ComboBox<String>
|
||||
|
||||
override fun onDock() {
|
||||
textAreaProperty.value = name
|
||||
textInputProperty.value = name
|
||||
}
|
||||
|
||||
override val root = vbox(alignment = Pos.CENTER_RIGHT) {
|
||||
padding = insets(all = 5)
|
||||
spacing = 5.0
|
||||
input = textfield {
|
||||
VBox.setVgrow(this, Priority.ALWAYS)
|
||||
bind(textAreaProperty)
|
||||
}
|
||||
button("Ok") {
|
||||
imageview("search.png") {
|
||||
fitWidth = 18.0
|
||||
fitHeight = 18.0
|
||||
form {
|
||||
fieldset {
|
||||
field("Name") {
|
||||
comboBox = combobox(textInputProperty, altTitles.ifEmpty { listOf(name) }) {
|
||||
useMaxSize = true
|
||||
isEditable = true
|
||||
}
|
||||
}
|
||||
}
|
||||
disableWhen(textAreaProperty.isEmpty)
|
||||
}
|
||||
button("Rename") {
|
||||
disableWhen(comboBox.editor.textProperty().isEmpty)
|
||||
action {
|
||||
postController.rename(postId, textAreaProperty.value)
|
||||
postController.rename(postId, comboBox.editor.text.trim())
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import me.vripper.gui.controller.SettingsController
|
||||
import me.vripper.gui.model.settings.ConnectionSettingsModel
|
||||
import tornadofx.*
|
||||
|
||||
class ConnectionSettingsView : View("Connection settings") {
|
||||
class ConnectionSettingsView : View("Connection Settings") {
|
||||
private val settingsController: SettingsController by inject()
|
||||
val connectionSettingsModel = ConnectionSettingsModel()
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ import javafx.scene.image.ImageView
|
||||
import javafx.scene.input.MouseButton
|
||||
import javafx.util.Callback
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.map
|
||||
import me.vripper.entities.Image
|
||||
import me.vripper.event.EventBus
|
||||
import me.vripper.event.ImageEvent
|
||||
@@ -17,14 +20,11 @@ import me.vripper.gui.model.ImageModel
|
||||
import me.vripper.gui.view.ProgressTableCell
|
||||
import me.vripper.gui.view.StatusTableCell
|
||||
import me.vripper.gui.view.openLink
|
||||
import reactor.core.Disposable
|
||||
import tornadofx.*
|
||||
import java.time.Duration
|
||||
|
||||
class ImagesTableView : Fragment("Photos") {
|
||||
|
||||
private lateinit var tableView: TableView<ImageModel>
|
||||
private lateinit var eventsSubscription: Disposable
|
||||
private val imageController: ImageController by inject()
|
||||
private val eventBus: EventBus by di()
|
||||
private var items: ObservableList<ImageModel> = FXCollections.observableArrayList()
|
||||
@@ -47,22 +47,24 @@ class ImagesTableView : Fragment("Photos") {
|
||||
}
|
||||
}
|
||||
|
||||
eventsSubscription = eventBus.events.ofType(ImageEvent::class.java).map {
|
||||
it.copy(images = it.images.filter { image: Image -> image.postId == postId })
|
||||
}.filter {
|
||||
it.images.isNotEmpty()
|
||||
}.buffer(Duration.ofMillis(175))
|
||||
.subscribe { imageEvent ->
|
||||
runLater {
|
||||
for (image in imageEvent.map { it.images }.flatten().reversed().distinct()) {
|
||||
val imageModel = items.find { it.id == image.id } ?: continue
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(ImageEvent::class).map {
|
||||
it.copy(images = it.images.filter { image: Image -> image.postId == postId })
|
||||
}.filter {
|
||||
it.images.isNotEmpty()
|
||||
}.collect { imageEvent ->
|
||||
runLater {
|
||||
for (image in imageEvent.images) {
|
||||
val imageModel = items.find { it.id == image.id } ?: continue
|
||||
|
||||
imageModel.size = image.size
|
||||
imageModel.status = image.status.name
|
||||
imageModel.downloaded = image.downloaded
|
||||
imageModel.progress = imageController.progress(
|
||||
image.size, image.downloaded
|
||||
)
|
||||
imageModel.size = image.size
|
||||
imageModel.status = image.status.name
|
||||
imageModel.filename = image.filename
|
||||
imageModel.downloaded = image.downloaded
|
||||
imageModel.progress = imageController.progress(
|
||||
image.size, image.downloaded
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,8 +85,8 @@ class ImagesTableView : Fragment("Photos") {
|
||||
}
|
||||
val contextMenu = ContextMenu()
|
||||
contextMenu.items.addAll(urlItem)
|
||||
tableRow.contextMenuProperty()
|
||||
.bind(tableRow.emptyProperty().map { empty -> if (empty) null else contextMenu })
|
||||
tableRow.contextMenuProperty().bind(tableRow.emptyProperty()
|
||||
.map { empty -> if (empty) null else contextMenu })
|
||||
tableRow
|
||||
}
|
||||
column("Index", ImageModel::indexProperty) {
|
||||
@@ -120,6 +122,12 @@ class ImagesTableView : Fragment("Photos") {
|
||||
cell as TableCell<ImageModel, Number>
|
||||
}
|
||||
}
|
||||
column("Filename", ImageModel::filenameProperty) {
|
||||
prefWidth = 150.0
|
||||
cellFactory = Callback {
|
||||
TextFieldTableCell<ImageModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
|
||||
}
|
||||
}
|
||||
column("Status", ImageModel::statusProperty) {
|
||||
prefWidth = 50.0
|
||||
cellFactory = Callback {
|
||||
@@ -143,6 +151,5 @@ class ImagesTableView : Fragment("Photos") {
|
||||
|
||||
override fun onUndock() {
|
||||
coroutineScope.cancel()
|
||||
eventsSubscription.dispose()
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package me.vripper.gui.view.tables
|
||||
|
||||
import javafx.scene.control.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import me.vripper.event.EventBus
|
||||
import me.vripper.event.LogCreateEvent
|
||||
import me.vripper.event.LogDeleteEvent
|
||||
@@ -41,28 +42,34 @@ class LogTableView : View() {
|
||||
}
|
||||
}
|
||||
|
||||
eventBus.events.ofType(LogCreateEvent::class.java).subscribe {
|
||||
val logModel = logController.mapper(it.logEntry)
|
||||
runLater {
|
||||
items.add(logModel)
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(LogCreateEvent::class).collect {
|
||||
val logModel = logController.mapper(it.logEntry)
|
||||
runLater {
|
||||
items.add(logModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eventBus.events.ofType(LogUpdateEvent::class.java).subscribe {
|
||||
val logModel = logController.mapper(it.logEntry)
|
||||
val find = items.find { threadModel -> threadModel.id == logModel.id }
|
||||
if (find != null) {
|
||||
runLater {
|
||||
find.apply {
|
||||
status = logModel.status
|
||||
message = logModel.message
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(LogUpdateEvent::class).collect {
|
||||
val logModel = logController.mapper(it.logEntry)
|
||||
val find = items.find { threadModel -> threadModel.id == logModel.id }
|
||||
if (find != null) {
|
||||
runLater {
|
||||
find.apply {
|
||||
status = logModel.status
|
||||
message = logModel.message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
eventBus.events.ofType(LogDeleteEvent::class.java).subscribe { deleteEvent ->
|
||||
runLater {
|
||||
items.items.removeIf { it.id in deleteEvent.deleted }
|
||||
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(LogDeleteEvent::class).collect { deleteEvent ->
|
||||
runLater {
|
||||
items.items.removeIf { it.id in deleteEvent.deleted }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,10 @@ import javafx.util.Callback
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.launch
|
||||
import me.vripper.event.*
|
||||
import me.vripper.event.EventBus
|
||||
import me.vripper.event.PostCreateEvent
|
||||
import me.vripper.event.PostDeleteEvent
|
||||
import me.vripper.event.PostUpdateEvent
|
||||
import me.vripper.gui.clipboard.ClipboardService
|
||||
import me.vripper.gui.controller.PostController
|
||||
import me.vripper.gui.model.PostModel
|
||||
@@ -57,40 +56,60 @@ class PostsTableView : View() {
|
||||
}
|
||||
}
|
||||
|
||||
eventBus.events.ofType(PostCreateEvent::class.java).subscribe { postEvents ->
|
||||
val add = postEvents.posts.map { postController.mapper(it.id) }
|
||||
runLater {
|
||||
tableView.items.addAll(add)
|
||||
}
|
||||
}
|
||||
|
||||
eventBus.events.ofType(PostUpdateEvent::class.java).subscribe { postEvents ->
|
||||
runLater {
|
||||
for (post in postEvents.posts) {
|
||||
val postModel = items.find { it.postId == post.postId } ?: continue
|
||||
|
||||
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
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(PostCreateEvent::class).collect { postEvents ->
|
||||
val add = postEvents.posts.map { postController.mapper(it.id) }
|
||||
runLater {
|
||||
tableView.items.addAll(add)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eventBus.events.ofType(PostDeleteEvent::class.java).subscribe { postEvents ->
|
||||
runLater {
|
||||
postEvents.postIds.forEach {
|
||||
items.removeIf { p -> p.postId == it }
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(PostUpdateEvent::class).collect { postEvents ->
|
||||
runLater {
|
||||
for (post in postEvents.posts) {
|
||||
val postModel = items.find { it.postId == post.postId } ?: continue
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(PostDeleteEvent::class).collect { postEvents ->
|
||||
runLater {
|
||||
postEvents.postIds.forEach {
|
||||
items.removeIf { p -> p.postId == it }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(MetadataUpdateEvent::class)
|
||||
.collect { metadataUpdateEvent ->
|
||||
runLater {
|
||||
val postModel =
|
||||
items.find { it.postId == metadataUpdateEvent.metadata.postId }
|
||||
?: return@runLater
|
||||
|
||||
postModel.altTitles =
|
||||
FXCollections.observableArrayList(metadataUpdateEvent.metadata.data.resolvedNames)
|
||||
postModel.postedby = metadataUpdateEvent.metadata.data.postedBy
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val root = vbox {
|
||||
@@ -176,10 +195,17 @@ class PostsTableView : View() {
|
||||
|
||||
val contextMenu = ContextMenu()
|
||||
contextMenu.items.addAll(
|
||||
startItem, stopItem, renameItem, deleteItem, SeparatorMenuItem(), detailsItem, locationItem, urlItem
|
||||
startItem,
|
||||
stopItem,
|
||||
renameItem,
|
||||
deleteItem,
|
||||
SeparatorMenuItem(),
|
||||
detailsItem,
|
||||
locationItem,
|
||||
urlItem
|
||||
)
|
||||
tableRow.contextMenuProperty()
|
||||
.bind(tableRow.emptyProperty().map { empty -> if (empty) null else contextMenu })
|
||||
tableRow.contextMenuProperty().bind(tableRow.emptyProperty()
|
||||
.map { empty -> if (empty) null else contextMenu })
|
||||
tableRow
|
||||
}
|
||||
column("Preview", PostModel::previewListProperty) {
|
||||
@@ -287,6 +313,12 @@ class PostsTableView : View() {
|
||||
TextFieldTableCell<PostModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
|
||||
}
|
||||
}
|
||||
column("Posted By", PostModel::postedByProperty) {
|
||||
prefWidth = 100.0
|
||||
cellFactory = Callback {
|
||||
TextFieldTableCell<PostModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
|
||||
}
|
||||
}
|
||||
column("Order", PostModel::orderProperty) {
|
||||
prefWidth = 50.0
|
||||
sortOrder.add(this)
|
||||
@@ -298,10 +330,15 @@ class PostsTableView : View() {
|
||||
}
|
||||
|
||||
private fun rename(post: PostModel) {
|
||||
find<RenameView>(mapOf(RenameView::postId to post.postId, RenameView::name to post.folderName)).openModal()
|
||||
?.apply {
|
||||
minWidth = 300.0
|
||||
}
|
||||
find<RenameView>(
|
||||
mapOf(
|
||||
RenameView::postId to post.postId,
|
||||
RenameView::name to post.folderName,
|
||||
RenameView::altTitles to post.altTitles
|
||||
)
|
||||
).openModal()?.apply {
|
||||
minWidth = 450.0
|
||||
}
|
||||
}
|
||||
|
||||
fun renameSelected() {
|
||||
@@ -336,8 +373,8 @@ class PostsTableView : View() {
|
||||
|
||||
private fun openPhotos(postId: Long) {
|
||||
find<ImagesTableView>(mapOf(ImagesTableView::postId to postId)).openModal()?.apply {
|
||||
minWidth = 600.0
|
||||
minHeight = 400.0
|
||||
minWidth = 800.0
|
||||
minHeight = 600.0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import javafx.collections.ObservableList
|
||||
import javafx.scene.control.*
|
||||
import javafx.scene.image.ImageView
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import me.vripper.event.EventBus
|
||||
import me.vripper.event.ThreadClearEvent
|
||||
import me.vripper.event.ThreadCreateEvent
|
||||
@@ -44,22 +45,28 @@ class ThreadTableView : View() {
|
||||
tableView.placeholder = Label("No content in table")
|
||||
}
|
||||
}
|
||||
eventBus.events.ofType(ThreadCreateEvent::class.java).subscribe {
|
||||
val threadModelMapper = threadController.threadModelMapper(it.thread)
|
||||
runLater {
|
||||
items.add(threadModelMapper)
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(ThreadCreateEvent::class).collect {
|
||||
val threadModelMapper = threadController.threadModelMapper(it.thread)
|
||||
runLater {
|
||||
items.add(threadModelMapper)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eventBus.events.ofType(ThreadDeleteEvent::class.java).subscribe { event ->
|
||||
runLater {
|
||||
tableView.items.removeIf { it.threadId == event.threadId }
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(ThreadDeleteEvent::class).collect { event ->
|
||||
runLater {
|
||||
tableView.items.removeIf { it.threadId == event.threadId }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eventBus.events.ofType(ThreadClearEvent::class.java).subscribe {
|
||||
runLater {
|
||||
tableView.items.clear()
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(ThreadClearEvent::class).collect {
|
||||
runLater {
|
||||
tableView.items.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,8 +115,8 @@ class ThreadTableView : View() {
|
||||
|
||||
val contextMenu = ContextMenu()
|
||||
contextMenu.items.addAll(selectItem, urlItem, SeparatorMenuItem(), deleteItem)
|
||||
tableRow.contextMenuProperty()
|
||||
.bind(tableRow.emptyProperty().map { empty -> if (empty) null else contextMenu })
|
||||
tableRow.contextMenuProperty().bind(tableRow.emptyProperty()
|
||||
.map { empty -> if (empty) null else contextMenu })
|
||||
tableRow
|
||||
}
|
||||
column("Title", ThreadModel::titleProperty) {
|
||||
@@ -135,11 +142,12 @@ class ThreadTableView : View() {
|
||||
}
|
||||
|
||||
private fun selectPosts(threadId: Long) {
|
||||
find<ThreadSelectionTableView>(mapOf(ThreadSelectionTableView::threadId to threadId)).openModal()?.apply {
|
||||
minWidth = 600.0
|
||||
minHeight = 400.0
|
||||
width = 800.0
|
||||
height = 600.0
|
||||
}
|
||||
find<ThreadSelectionTableView>(mapOf(ThreadSelectionTableView::threadId to threadId)).openModal()
|
||||
?.apply {
|
||||
minWidth = 600.0
|
||||
minHeight = 400.0
|
||||
width = 800.0
|
||||
height = 600.0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
package me.vripper.web.wsendpoints
|
||||
|
||||
import jakarta.annotation.PostConstruct
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.time.sample
|
||||
import me.vripper.event.*
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
@@ -14,72 +18,110 @@ class DataBroadcast(
|
||||
) : KoinComponent {
|
||||
|
||||
private val eventBus: EventBus by inject()
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
@PostConstruct
|
||||
private fun run() {
|
||||
eventBus.events.ofType(PostCreateEvent::class.java).subscribe { events ->
|
||||
template.convertAndSend("/topic/posts/new", events.posts)
|
||||
}
|
||||
eventBus.events.ofType(PostUpdateEvent::class.java).subscribe { events ->
|
||||
template.convertAndSend("/topic/posts/updated", events.posts)
|
||||
}
|
||||
eventBus.events.ofType(PostDeleteEvent::class.java).subscribe { events ->
|
||||
template.convertAndSend("/topic/posts/deleted", events.postIds)
|
||||
}
|
||||
|
||||
eventBus.events.ofType(QueueStateEvent::class.java).subscribe {
|
||||
template.convertAndSend("/topic/queue-state", it.queueState)
|
||||
}
|
||||
|
||||
eventBus.events.ofType(DownloadSpeedEvent::class.java).subscribe {
|
||||
template.convertAndSend("/topic/download-speed", it.downloadSpeed)
|
||||
}
|
||||
|
||||
eventBus.events.ofType(VGUserLoginEvent::class.java).subscribe {
|
||||
template.convertAndSend("/topic/vg-username", it.username)
|
||||
}
|
||||
|
||||
eventBus.events.ofType(ErrorCountEvent::class.java).subscribe {
|
||||
template.convertAndSend("/topic/error-count", it.errorCount)
|
||||
}
|
||||
|
||||
eventBus.events.ofType(LoadingTasks::class.java).sample(Duration.ofMillis(500)).subscribe {
|
||||
template.convertAndSend("/topic/loading", it.loading)
|
||||
}
|
||||
|
||||
eventBus.events.ofType(ImageEvent::class.java).buffer(Duration.ofMillis(500)).subscribe { events ->
|
||||
events.reversed().map { it.images }.flatten().distinct().groupBy { it.postId }
|
||||
.forEach {
|
||||
template.convertAndSend("/topic/images/${it.key}", it.value)
|
||||
coroutineScope.launch {
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(PostCreateEvent::class).collect { events ->
|
||||
template.convertAndSend("/topic/posts/new", events.posts)
|
||||
}
|
||||
}
|
||||
|
||||
eventBus.events.ofType(ThreadCreateEvent::class.java).map { listOf(it.thread) }
|
||||
.subscribe { events ->
|
||||
template.convertAndSend("/topic/threads", events)
|
||||
}
|
||||
|
||||
eventBus.events.ofType(ThreadDeleteEvent::class.java).map { listOf(it.threadId) }
|
||||
.subscribe {
|
||||
template.convertAndSend("/topic/threads/deleted", it)
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(PostUpdateEvent::class).collect { events ->
|
||||
template.convertAndSend("/topic/posts/updated", events.posts)
|
||||
}
|
||||
}
|
||||
|
||||
eventBus.events.ofType(ThreadClearEvent::class.java).map { listOf(true) }.subscribe {
|
||||
template.convertAndSend("/topic/threads/deletedAll", it)
|
||||
}
|
||||
|
||||
eventBus.events.ofType(LogCreateEvent::class.java).map { it.logEntry }
|
||||
.subscribe { logCreateEvent ->
|
||||
template.convertAndSend("/topic/logs/new", listOf(logCreateEvent))
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(PostDeleteEvent::class).collect { events ->
|
||||
template.convertAndSend("/topic/posts/deleted", events.postIds)
|
||||
}
|
||||
}
|
||||
|
||||
eventBus.events.ofType(LogUpdateEvent::class.java).map { it.logEntry }
|
||||
.subscribe { logUpdateEvent ->
|
||||
template.convertAndSend("/topic/logs/updated", listOf(logUpdateEvent))
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(QueueStateEvent::class).collect {
|
||||
template.convertAndSend("/topic/queue-state", it.queueState)
|
||||
}
|
||||
}
|
||||
|
||||
eventBus.events.ofType(LogDeleteEvent::class.java).subscribe {
|
||||
template.convertAndSend("/topic/logs/deleted", listOf(it.deleted))
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(DownloadSpeedEvent::class).collect {
|
||||
template.convertAndSend("/topic/download-speed", it.downloadSpeed)
|
||||
}
|
||||
}
|
||||
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(VGUserLoginEvent::class).collect {
|
||||
template.convertAndSend("/topic/vg-username", it.username)
|
||||
}
|
||||
}
|
||||
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(ErrorCountEvent::class).collect {
|
||||
template.convertAndSend("/topic/error-count", it.errorCount)
|
||||
}
|
||||
}
|
||||
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(LoadingTasks::class).sample(Duration.ofMillis(500))
|
||||
.collect {
|
||||
template.convertAndSend("/topic/loading", it.loading)
|
||||
}
|
||||
}
|
||||
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(ImageEvent::class).collect { events ->
|
||||
events.images.groupBy { it.postId }.forEach {
|
||||
template.convertAndSend("/topic/images/${it.key}", it.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(ThreadCreateEvent::class).map { listOf(it.thread) }
|
||||
.collect { events ->
|
||||
template.convertAndSend("/topic/threads", events)
|
||||
}
|
||||
}
|
||||
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(ThreadDeleteEvent::class)
|
||||
.map { listOf(it.threadId) }
|
||||
.collect {
|
||||
template.convertAndSend("/topic/threads/deleted", it)
|
||||
}
|
||||
}
|
||||
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(ThreadClearEvent::class).map { listOf(true) }
|
||||
.collect {
|
||||
template.convertAndSend("/topic/threads/deletedAll", it)
|
||||
}
|
||||
}
|
||||
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(LogCreateEvent::class).map { it.logEntry }
|
||||
.collect { logCreateEvent ->
|
||||
template.convertAndSend("/topic/logs/new", listOf(logCreateEvent))
|
||||
}
|
||||
}
|
||||
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(LogUpdateEvent::class).map { it.logEntry }
|
||||
.collect { logUpdateEvent ->
|
||||
template.convertAndSend("/topic/logs/updated", listOf(logUpdateEvent))
|
||||
}
|
||||
}
|
||||
|
||||
coroutineScope.launch {
|
||||
eventBus.events.filterIsInstance(LogDeleteEvent::class).collect {
|
||||
template.convertAndSend("/topic/logs/deleted", listOf(it.deleted))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user