This commit is contained in:
death-claw
2024-12-25 16:23:13 +01:00
committed by GitHub
parent 9652c7725e
commit 2bcdd81eeb
20 changed files with 119 additions and 166 deletions
@@ -1,8 +1,6 @@
package me.vripper.download
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import me.vripper.entities.ImageEntity
import me.vripper.entities.PostEntity
@@ -39,7 +37,6 @@ internal class DownloadService(
private val pending: MutableMap<Byte, MutableList<ImageDownloadRunnable>> = mutableMapOf()
private val lock = ReentrantLock()
private val condition = lock.newCondition()
private val coroutineScope = CoroutineScope(SupervisorJob())
init {
Thread.ofVirtual().name("Download Loop").unstarted(Runnable {
@@ -74,14 +71,10 @@ internal class DownloadService(
fun stop(postIds: List<Long> = emptyList()) {
if (postIds.isNotEmpty()) {
stopInternal(postIds)
coroutineScope.launch {
eventBus.publishEvent(StoppedEvent(postIds))
}
eventBus.publishEvent(StoppedEvent(postIds))
} else {
stopAll()
coroutineScope.launch {
eventBus.publishEvent(StoppedEvent(listOf(-1)))
}
eventBus.publishEvent(StoppedEvent(listOf(-1)))
}
}
@@ -89,8 +82,9 @@ internal class DownloadService(
if (postEntityIds.isNotEmpty()) {
restart(postEntityIds.associateWith { dataTransaction.findByPostIdAndIsNotCompleted(it.postId) })
} else {
restart(dataTransaction.findAllPosts()
.associateWith { dataTransaction.findByPostIdAndIsNotCompleted(it.postId) })
restart(
dataTransaction.findAllPosts()
.associateWith { dataTransaction.findByPostIdAndIsNotCompleted(it.postId) })
}
}
@@ -237,9 +231,7 @@ internal class DownloadService(
private fun scheduleForDownload(imageDownloadRunnable: ImageDownloadRunnable) {
log.debug("Scheduling a job for ${imageDownloadRunnable.context.imageEntity.url}")
GlobalScopeCoroutine.launch {
coroutineScope.launch {
eventBus.publishEvent(QueueStateEvent(QueueState(runningCount(), pendingCount())))
}
eventBus.publishEvent(QueueStateEvent(QueueState(runningCount(), pendingCount())))
try {
Failsafe.with<Any, RetryPolicy<Any>>(retryPolicyService.buildRetryPolicyForDownload("Failed to download ${imageDownloadRunnable.context.imageEntity.url}: "))
.onFailure {
@@ -253,18 +245,14 @@ internal class DownloadService(
}
.onComplete {
afterJobFinish(imageDownloadRunnable)
coroutineScope.launch {
eventBus.publishEvent(
QueueStateEvent(
QueueState(
runningCount(), pendingCount()
)
eventBus.publishEvent(
QueueStateEvent(
QueueState(
runningCount(), pendingCount()
)
)
}
coroutineScope.launch {
eventBus.publishEvent(ErrorCountEvent(ErrorCount(dataTransaction.countImagesInError())))
}
)
eventBus.publishEvent(ErrorCountEvent(ErrorCount(dataTransaction.countImagesInError())))
log.debug(
"Finished downloading ${imageDownloadRunnable.context.imageEntity.url}"
)
@@ -1,5 +1,6 @@
package me.vripper.download
import kotlinx.coroutines.*
import me.vripper.entities.ImageEntity
import me.vripper.model.Settings
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase
@@ -8,10 +9,23 @@ import org.apache.hc.client5.http.protocol.HttpClientContext
import org.koin.core.component.KoinComponent
internal class ImageDownloadContext(val imageEntity: ImageEntity, val settings: Settings) : KoinComponent {
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val jobs = mutableListOf<Job>()
val httpContext: HttpClientContext =
HttpClientContext.create().apply { cookieStore = BasicCookieStore() }
val requests = mutableListOf<HttpUriRequestBase>()
val postId = imageEntity.postIdRef
var stopped = false
var completed = false
fun cancelCoroutines() {
runBlocking {
coroutineScope.cancel()
jobs.forEach { job -> job.cancelAndJoin() }
}
}
fun launchCoroutine(block: suspend CoroutineScope.() -> Unit): Job {
return coroutineScope.launch(block = block).also { job -> jobs.add(job) }
}
}
@@ -8,7 +8,7 @@ import me.vripper.host.DownloadedImage
import me.vripper.host.Host
import me.vripper.host.ImageMimeType
import me.vripper.model.Settings
import me.vripper.services.*
import me.vripper.services.DataTransaction
import me.vripper.utilities.LoggerDelegate
import me.vripper.utilities.PathUtils.getExtension
import me.vripper.utilities.PathUtils.getFileNameWithoutExtension
@@ -21,6 +21,7 @@ import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.util.*
import kotlin.Throws
import kotlin.io.path.Path
import kotlin.io.path.pathString
@@ -127,6 +128,7 @@ internal class ImageDownloadRunnable(
download()
} finally {
context.completed = true
context.cancelCoroutines()
}
}
@@ -143,6 +145,8 @@ internal class ImageDownloadRunnable(
fun stop() {
context.requests.forEach { it.abort() }
context.cancelCoroutines()
context.stopped = true
dataTransaction.updateImage(context.imageEntity)
}
}
@@ -1,14 +1,15 @@
package me.vripper.event
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
object EventBus {
private val _events = MutableSharedFlow<Any>()
private val _events = MutableSharedFlow<Any>(0, Int.MAX_VALUE, BufferOverflow.DROP_OLDEST)
val events = _events.asSharedFlow()
suspend fun publishEvent(event: Any) {
_events.emit(event)
fun publishEvent(event: Any) {
_events.tryEmit(event)
}
}
@@ -1,5 +1,9 @@
package me.vripper.host
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.runBlocking
import me.vripper.download.ImageDownloadContext
import me.vripper.exception.DownloadException
import me.vripper.exception.HostException
@@ -15,10 +19,9 @@ import org.apache.hc.core5.http.ClassicHttpResponse
import org.apache.hc.core5.http.Header
import org.jetbrains.exposed.sql.transactions.transaction
import org.w3c.dom.Document
import java.io.BufferedOutputStream
import java.nio.file.Files
import java.nio.file.Path
import java.time.Duration
import java.time.LocalDateTime
import kotlin.Throws
internal abstract class Host(
@@ -100,7 +103,7 @@ internal abstract class Host(
"vripper_",
".tmp"
)
return Files.newOutputStream(tempImage).use { fos ->
return BufferedOutputStream(Files.newOutputStream(tempImage)).use { bos ->
val image = context.imageEntity
synchronized(image.postId.toString().intern()) {
val post = dataTransaction.findPostById(context.postId).orElseThrow()
@@ -124,20 +127,22 @@ internal abstract class Host(
)
val buffer = ByteArray(READ_BUFFER_SIZE)
var read: Int
var lastImageUpdateDate = LocalDateTime.now()
while (response.entity.content.read(buffer, 0, READ_BUFFER_SIZE)
val reporterJob = context.launchCoroutine {
while (isActive) {
dataTransaction.updateImage(image, false)
delay(100)
}
}
while (response.entity.content.read(buffer)
.also { read = it } != -1 && !context.stopped
) {
fos.write(buffer, 0, read)
bos.write(buffer, 0, read)
image.downloaded += read
if (Duration.between(lastImageUpdateDate, LocalDateTime.now()).toMillis() > 1750) {
dataTransaction.updateImage(image)
lastImageUpdateDate = LocalDateTime.now()
} else {
dataTransaction.updateImage(image, false)
}
downloadSpeedService.reportDownloadedBytes(read.toLong())
}
runBlocking {
reporterJob.cancelAndJoin()
}
dataTransaction.updateImage(image)
Pair(tempImage, mimeType)
}
@@ -1,9 +1,5 @@
package me.vripper.services
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import me.vripper.data.repositories.ImageRepository
import me.vripper.data.repositories.MetadataRepository
import me.vripper.data.repositories.PostDownloadStateRepository
@@ -28,7 +24,6 @@ internal class DataTransaction(
private val eventBus: EventBus,
) {
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val nextRank = AtomicInteger(transaction { getQueuePosition() }?.plus(1) ?: 0)
private fun save(postEntities: List<PostEntity>): List<PostEntity> {
@@ -42,44 +37,32 @@ internal class DataTransaction(
save(images.map { it.copy(postIdRef = savedPost.id) })
savedPost
}
coroutineScope.launch {
eventBus.publishEvent(PostCreateEvent(listOf(savedPost)))
}
eventBus.publishEvent(PostCreateEvent(listOf(savedPost)))
}
fun updatePosts(postEntities: List<PostEntity>) {
transaction { postDownloadStateRepository.update(postEntities) }
coroutineScope.launch {
eventBus.publishEvent(PostUpdateEvent(postEntities))
}
eventBus.publishEvent(PostUpdateEvent(postEntities))
}
fun updatePost(postEntity: PostEntity) {
transaction { postDownloadStateRepository.update(postEntity) }
coroutineScope.launch {
eventBus.publishEvent(PostUpdateEvent(listOf(postEntity)))
}
eventBus.publishEvent(PostUpdateEvent(listOf(postEntity)))
}
fun save(threadEntity: ThreadEntity) {
val savedThread = transaction { threadRepository.save(threadEntity) }
coroutineScope.launch {
eventBus.publishEvent(ThreadCreateEvent(savedThread))
}
eventBus.publishEvent(ThreadCreateEvent(savedThread))
}
fun update(threadEntity: ThreadEntity) {
transaction { threadRepository.update(threadEntity) }
coroutineScope.launch {
eventBus.publishEvent(ThreadUpdateEvent(threadEntity))
}
eventBus.publishEvent(ThreadUpdateEvent(threadEntity))
}
fun updateImages(imageEntities: List<ImageEntity>) {
transaction { imageRepository.update(imageEntities) }
coroutineScope.launch {
eventBus.publishEvent(ImageEvent(imageEntities))
}
eventBus.publishEvent(ImageEvent(imageEntities))
}
fun updateImage(imageEntity: ImageEntity, persist: Boolean = true) {
@@ -88,9 +71,7 @@ internal class DataTransaction(
imageRepository.update(imageEntity)
}
}
coroutineScope.launch {
eventBus.publishEvent(ImageEvent(listOf(imageEntity)))
}
eventBus.publishEvent(ImageEvent(listOf(imageEntity)))
}
fun exists(postId: Long): Boolean {
@@ -142,9 +123,7 @@ internal class DataTransaction(
}
savedPosts
}
coroutineScope.launch {
eventBus.publishEvent(PostCreateEvent(savedPosts))
}
eventBus.publishEvent(PostCreateEvent(savedPosts))
return savedPosts
}
@@ -191,20 +170,13 @@ internal class DataTransaction(
postDownloadStateRepository.deleteAll(postIds)
sortPostsByRank()
}
coroutineScope.launch {
eventBus.publishEvent(PostDeleteEvent(postIds = postIds))
}
coroutineScope.launch {
eventBus.publishEvent(ErrorCountEvent(ErrorCount(countImagesInError())))
}
eventBus.publishEvent(PostDeleteEvent(postIds = postIds))
eventBus.publishEvent(ErrorCountEvent(ErrorCount(countImagesInError())))
}
fun removeThread(threadId: Long) {
transaction { threadRepository.deleteByThreadId(threadId) }
coroutineScope.launch {
eventBus.publishEvent(ThreadDeleteEvent(threadId))
}
eventBus.publishEvent(ThreadDeleteEvent(threadId))
}
fun clearCompleted(): List<Long> {
@@ -231,16 +203,12 @@ internal class DataTransaction(
fun saveMetadata(metadataEntity: MetadataEntity) {
transaction { metadataRepository.save(metadataEntity) }
coroutineScope.launch {
eventBus.publishEvent(MetadataUpdateEvent(metadataEntity))
}
eventBus.publishEvent(MetadataUpdateEvent(metadataEntity))
}
fun clearQueueLinks() {
transaction { threadRepository.deleteAll() }
coroutineScope.launch {
eventBus.publishEvent(ThreadClearEvent())
}
eventBus.publishEvent(ThreadClearEvent())
}
@Synchronized
@@ -30,7 +30,6 @@ internal class DownloadSpeedService(
while (isActive) {
delay(DOWNLOAD_POLL_RATE.toLong())
val newValue = bytesCount.getAndSet(0)
eventBus.publishEvent(DownloadSpeedEvent(DownloadSpeed(((newValue * 1000) / DOWNLOAD_POLL_RATE))))
}
}
@@ -1,9 +1,5 @@
package me.vripper.services
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
@@ -24,7 +20,6 @@ class SettingsService(private val eventBus: EventBus) {
private val configPath = VRIPPER_DIR.resolve("config.json")
private val customProxiesPath = VRIPPER_DIR.resolve("proxies.json")
private val proxies: MutableSet<String> = HashSet()
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val json = Json {
encodeDefaults = true
@@ -40,9 +35,7 @@ class SettingsService(private val eventBus: EventBus) {
private fun init() {
loadViperProxies()
restore()
coroutineScope.launch {
eventBus.publishEvent(SettingsUpdateEvent(settings))
}
eventBus.publishEvent(SettingsUpdateEvent(settings))
}
private fun loadViperProxies() {
@@ -100,9 +93,7 @@ class SettingsService(private val eventBus: EventBus) {
}
this.settings = settings.copy(viperSettings = viperSettings)
save()
coroutineScope.launch {
eventBus.publishEvent(SettingsUpdateEvent(this@SettingsService.settings))
}
eventBus.publishEvent(SettingsUpdateEvent(this@SettingsService.settings))
}
private fun restore() {
@@ -50,10 +50,7 @@ internal class VGAuthService(
log.debug("Authentication option is disabled")
context.cookieStore.clear()
loggedUser = ""
coroutineScope.launch {
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
}
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
return
}
val username = settings.viperSettings.username
@@ -62,11 +59,7 @@ internal class VGAuthService(
log.error("Cannot authenticate with ViperGirls credentials, username or password is empty")
context.cookieStore.clear()
loggedUser = ""
coroutineScope.launch {
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
}
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
return
}
val postAuth = HttpPost(settings.viperSettings.host + "/login.php?do=login").also {
@@ -103,11 +96,7 @@ internal class VGAuthService(
} catch (e: Exception) {
context.cookieStore.clear()
loggedUser = ""
coroutineScope.launch {
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
}
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
log.error(
"Failed to authenticate user with " + settings.viperSettings.host, e
)
@@ -115,10 +104,7 @@ internal class VGAuthService(
}
authenticated = true
loggedUser = username
coroutineScope.launch {
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
}
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
}
fun leaveThanks(postEntity: PostEntity) {
@@ -1,9 +1,5 @@
package me.vripper.tasks
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
@@ -13,14 +9,11 @@ internal 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) {
coroutineScope.launch {
eventBus.publishEvent(LoadingTasks(true))
}
eventBus.publishEvent(LoadingTasks(true))
}
current += 1
}
@@ -29,9 +22,7 @@ internal object Tasks : KoinComponent {
fun decrement() {
current -= 1
if (current == 0) {
coroutineScope.launch {
eventBus.publishEvent(LoadingTasks(false))
}
eventBus.publishEvent(LoadingTasks(false))
}
}
}
@@ -49,8 +49,8 @@ class VripperGuiApplication : App(
with(stage) {
width = widgetsController.currentSettings.width
height = widgetsController.currentSettings.height
minWidth = 800.0
minHeight = 600.0
minWidth = 100.0
minHeight = 100.0
icons.addAll(
listOf(
Image("icons/16x16.png"),
@@ -1,19 +0,0 @@
package me.vripper.gui.components.fragments
import javafx.beans.property.SimpleBooleanProperty
import javafx.collections.FXCollections
import tornadofx.Fragment
import tornadofx.listview
import tornadofx.useCheckbox
class ColumnSelectionFragment : Fragment("Column Selection") {
val map: MutableMap<String, SimpleBooleanProperty> by param()
override val root = listview<String> {
items = FXCollections.observableArrayList(map.keys)
useCheckbox { listItem ->
map[listItem]!!
}
}
}
@@ -1,10 +1,13 @@
package me.vripper.gui.components.fragments
import atlantafx.base.theme.Styles
import javafx.beans.property.SimpleStringProperty
import javafx.geometry.Pos
import javafx.scene.control.ComboBox
import kotlinx.coroutines.*
import me.vripper.gui.controller.PostController
import org.kordamp.ikonli.feather.Feather
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.*
class RenameFragment : Fragment("Rename download post") {
@@ -35,6 +38,8 @@ class RenameFragment : Fragment("Rename download post") {
}
}
button("Rename") {
graphic = FontIcon.of(Feather.EDIT)
addClass(Styles.ACCENT)
disableWhen(comboBox.editor.textProperty().isEmpty)
action {
coroutineScope.launch {
@@ -31,8 +31,9 @@ class SettingsFragment : Fragment("Settings") {
tabpane {
tabClosingPolicy = TabPane.TabClosingPolicy.UNAVAILABLE
VBox.setVgrow(this, Priority.ALWAYS)
minWidth = 700.0
minHeight = 400.0
minWidth = 100.0
minHeight = 100.0
prefHeight = 400.0
tab(downloadSettingsFragment.title) {
add(downloadSettingsFragment)
graphic = FontIcon.of(Feather.FOLDER)
@@ -60,7 +60,10 @@ class ActionBarView : View() {
action {
find<AddLinksFragment>().apply {
input.clear()
}.openModal()
}.openModal()?.apply {
minWidth = 100.0
minHeight = 100.0
}
}
}
separator(Orientation.VERTICAL)
@@ -111,7 +114,10 @@ class ActionBarView : View() {
contentDisplay = ContentDisplay.GRAPHIC_ONLY
tooltip("Open settings menu [Ctrl+P]")
action {
find<SettingsFragment>().openModal(owner = primaryStage)
find<SettingsFragment>().openModal()?.apply {
minWidth = 100.0
minHeight = 100.0
}
}
}
separator(Orientation.VERTICAL)
@@ -51,10 +51,12 @@ class LoadingView : View("VRipper") {
if (!grpcEndpointService.ready()) {
val sessionView = find<SessionFragment>()
runLater {
sessionView.openModal().also {
it?.setOnCloseRequest {
sessionView.openModal()?.apply {
setOnCloseRequest {
VripperGuiApplication.APP_INSTANCE.stop()
}
minWidth = 100.0
minHeight = 100.0
}
}
}
@@ -228,8 +228,8 @@ class LogTableView : View() {
private fun openLog(item: LogModel) {
find<LogMessageFragment>(mapOf(LogMessageFragment::logModel to item)).openModal()?.apply {
minWidth = 600.0
minHeight = 400.0
minWidth = 100.0
minHeight = 100.0
}
}
}
@@ -67,7 +67,10 @@ class MenuBarView : View() {
action {
find<AddLinksFragment>().apply {
input.clear()
}.openModal()
}.openModal()?.apply {
minWidth = 100.0
minHeight = 100.0
}
}
}
separator()
@@ -113,14 +116,20 @@ class MenuBarView : View() {
item("Settings", KeyCodeCombination(KeyCode.P, KeyCombination.CONTROL_DOWN)).apply {
graphic = FontIcon.of(Feather.SETTINGS)
action {
find<SettingsFragment>().openModal(owner = primaryStage)
find<SettingsFragment>().openModal()?.apply {
minWidth = 100.0
minHeight = 100.0
}
}
}
separator()
item("Change session", KeyCodeCombination(KeyCode.S, KeyCombination.SHIFT_DOWN)) {
graphic = FontIcon.of(Feather.LINK_2)
action {
find<SessionFragment>().openModal()
find<SessionFragment>().openModal()?.apply {
minWidth = 100.0
minHeight = 100.0
}
}
}
separator()
@@ -206,8 +215,8 @@ class MenuBarView : View() {
graphic = FontIcon.of(Feather.INFO)
action {
find<AboutFragment>().openModal()?.apply {
this.minWidth = 625.0
this.minHeight = 200.0
this.minWidth = 100.0
this.minHeight = 100.0
}
}
}
@@ -178,7 +178,10 @@ class PostsTableView : View() {
action {
find<AddLinksFragment>().apply {
input.clear()
}.openModal()
}.openModal()?.apply {
minWidth = 100.0
minHeight = 100.0
}
}
})
column("Preview", PostModel::previewListProperty) {
@@ -469,7 +472,8 @@ class PostsTableView : View() {
RenameFragment::altTitles to post.altTitles
)
).openModal()?.apply {
minWidth = 450.0
minWidth = 100.0
minHeight = 100.0
}
}
@@ -248,10 +248,8 @@ class ThreadTableView : View() {
private fun selectPosts(threadId: Long) {
find<ThreadSelectionTableFragment>(mapOf(ThreadSelectionTableFragment::threadId to threadId)).openModal()
?.apply {
minWidth = 600.0
minHeight = 400.0
width = 800.0
height = 600.0
minWidth = 100.0
minHeight = 100.0
}
}
}