From c74d71170211394f0e498dea61ac1220ec5e3b75 Mon Sep 17 00:00:00 2001
From: dev-claw <53543762+dev-claw@users.noreply.github.com>
Date: Fri, 27 Dec 2024 19:12:07 +0100
Subject: [PATCH] fixes #208 (#209)
---
pom.xml | 4 +-
vripper-core/pom.xml | 2 +-
.../src/main/kotlin/me/vripper/Module.kt | 8 +-
...adStateRepository.kt => PostRepository.kt} | 7 +-
...epositoryImpl.kt => PostRepositoryImpl.kt} | 24 ++---
.../me/vripper/download/DownloadService.kt | 90 ++++++++----------
.../vripper/download/ImageDownloadContext.kt | 2 -
.../vripper/download/ImageDownloadRunnable.kt | 36 +++----
.../src/main/kotlin/me/vripper/host/Host.kt | 4 +-
.../me/vripper/services/AppEndpointService.kt | 48 +++++-----
.../me/vripper/services/DataTransaction.kt | 61 ++++++++----
.../kotlin/me/vripper/services/HTTPService.kt | 12 +--
.../me/vripper/services/RetryPolicyService.kt | 19 ++--
.../me/vripper/services/ThreadCacheService.kt | 9 +-
.../me/vripper/services/VGAuthService.kt | 8 +-
.../me/vripper/tasks/ThreadLookupTask.kt | 13 +--
.../me/vripper/utilities/GlobalScope.kt | 14 ---
.../main/kotlin/me/vripper/utilities/Utils.kt | 6 ++
.../me/vripper/vgapi/PostLookupAPIParser.kt | 8 +-
.../me/vripper/vgapi/ThreadLookupAPIParser.kt | 8 +-
.../gui/components/fragments/AboutFragment.kt | 27 ++++--
.../resources/icons/buymeacoffee-logo.png | Bin 0 -> 1455 bytes
.../src/main/resources/icons/github-mark.png | Bin 0 -> 6393 bytes
23 files changed, 206 insertions(+), 204 deletions(-)
rename vripper-core/src/main/kotlin/me/vripper/data/repositories/{PostDownloadStateRepository.kt => PostRepository.kt} (76%)
rename vripper-core/src/main/kotlin/me/vripper/data/repositories/impl/{PostDownloadStateRepositoryImpl.kt => PostRepositoryImpl.kt} (92%)
delete mode 100644 vripper-core/src/main/kotlin/me/vripper/utilities/GlobalScope.kt
create mode 100644 vripper-core/src/main/kotlin/me/vripper/utilities/Utils.kt
create mode 100644 vripper-gui/src/main/resources/icons/buymeacoffee-logo.png
create mode 100644 vripper-gui/src/main/resources/icons/github-mark.png
diff --git a/pom.xml b/pom.xml
index 16b56ac6..356a5442 100644
--- a/pom.xml
+++ b/pom.xml
@@ -25,7 +25,7 @@
4.0.0
2.2.224
2.29
- 2.4.4
+ 3.3.2
3.1.8
5.14.0
1.7.3
@@ -142,8 +142,8 @@
${htmlcleaner.version}
+ dev.failsafe
failsafe
- net.jodah
${failsafe.version}
diff --git a/vripper-core/pom.xml b/vripper-core/pom.xml
index 4e347481..f6e788aa 100644
--- a/vripper-core/pom.xml
+++ b/vripper-core/pom.xml
@@ -69,7 +69,7 @@
failsafe
- net.jodah
+ dev.failsafe
caffeine
diff --git a/vripper-core/src/main/kotlin/me/vripper/Module.kt b/vripper-core/src/main/kotlin/me/vripper/Module.kt
index 1e7517f8..7478bea3 100644
--- a/vripper-core/src/main/kotlin/me/vripper/Module.kt
+++ b/vripper-core/src/main/kotlin/me/vripper/Module.kt
@@ -2,11 +2,11 @@ package me.vripper
import me.vripper.data.repositories.ImageRepository
import me.vripper.data.repositories.MetadataRepository
-import me.vripper.data.repositories.PostDownloadStateRepository
+import me.vripper.data.repositories.PostRepository
import me.vripper.data.repositories.ThreadRepository
import me.vripper.data.repositories.impl.ImageRepositoryImpl
import me.vripper.data.repositories.impl.MetadataRepositoryImpl
-import me.vripper.data.repositories.impl.PostDownloadStateRepositoryImpl
+import me.vripper.data.repositories.impl.PostRepositoryImpl
import me.vripper.data.repositories.impl.ThreadRepositoryImpl
import me.vripper.download.DownloadService
import me.vripper.event.EventBus
@@ -29,8 +29,8 @@ val coreModule = module {
single {
ImageRepositoryImpl()
}
- single {
- PostDownloadStateRepositoryImpl()
+ single {
+ PostRepositoryImpl()
}
single {
MetadataRepositoryImpl()
diff --git a/vripper-core/src/main/kotlin/me/vripper/data/repositories/PostDownloadStateRepository.kt b/vripper-core/src/main/kotlin/me/vripper/data/repositories/PostRepository.kt
similarity index 76%
rename from vripper-core/src/main/kotlin/me/vripper/data/repositories/PostDownloadStateRepository.kt
rename to vripper-core/src/main/kotlin/me/vripper/data/repositories/PostRepository.kt
index 3d0fe706..333c8810 100644
--- a/vripper-core/src/main/kotlin/me/vripper/data/repositories/PostDownloadStateRepository.kt
+++ b/vripper-core/src/main/kotlin/me/vripper/data/repositories/PostRepository.kt
@@ -1,12 +1,11 @@
package me.vripper.data.repositories
import me.vripper.entities.PostEntity
-import java.util.*
-internal interface PostDownloadStateRepository {
+internal interface PostRepository {
fun save(postEntities: List): List
- fun findByPostId(postId: Long): Optional
- fun findById(id: Long): Optional
+ fun findByPostId(postId: Long): PostEntity?
+ fun findById(id: Long): PostEntity?
fun findCompleted(): List
fun findAll(): List
fun existByPostId(postId: Long): Boolean
diff --git a/vripper-core/src/main/kotlin/me/vripper/data/repositories/impl/PostDownloadStateRepositoryImpl.kt b/vripper-core/src/main/kotlin/me/vripper/data/repositories/impl/PostRepositoryImpl.kt
similarity index 92%
rename from vripper-core/src/main/kotlin/me/vripper/data/repositories/impl/PostDownloadStateRepositoryImpl.kt
rename to vripper-core/src/main/kotlin/me/vripper/data/repositories/impl/PostRepositoryImpl.kt
index 2fcef1e9..6d0d3263 100644
--- a/vripper-core/src/main/kotlin/me/vripper/data/repositories/impl/PostDownloadStateRepositoryImpl.kt
+++ b/vripper-core/src/main/kotlin/me/vripper/data/repositories/impl/PostRepositoryImpl.kt
@@ -1,6 +1,6 @@
package me.vripper.data.repositories.impl
-import me.vripper.data.repositories.PostDownloadStateRepository
+import me.vripper.data.repositories.PostRepository
import me.vripper.data.tables.PostTable
import me.vripper.entities.PostEntity
import me.vripper.entities.Status
@@ -8,10 +8,9 @@ import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.TransactionManager
import java.sql.Connection
-import java.util.*
-internal class PostDownloadStateRepositoryImpl :
- PostDownloadStateRepository {
+internal class PostRepositoryImpl :
+ PostRepository {
private val delimiter = ";"
@@ -37,15 +36,11 @@ internal class PostDownloadStateRepositoryImpl :
}.map(::transform)
}
- override fun findByPostId(postId: Long): Optional {
+ override fun findByPostId(postId: Long): PostEntity? {
val result = PostTable.selectAll().where {
PostTable.postId eq postId
}.map(::transform)
- return if (result.isEmpty()) {
- Optional.empty()
- } else {
- Optional.of(result.first())
- }
+ return result.firstOrNull()
}
override fun findCompleted(): List {
@@ -54,16 +49,11 @@ internal class PostDownloadStateRepositoryImpl :
}.map { it[PostTable.postId] }
}
- override fun findById(id: Long): Optional {
+ override fun findById(id: Long): PostEntity? {
val result = PostTable.selectAll().where {
PostTable.id eq id
}.map { transform(it) }
-
- return if (result.isEmpty()) {
- Optional.empty()
- } else {
- Optional.of(result.first())
- }
+ return result.firstOrNull()
}
override fun findAll(): List {
diff --git a/vripper-core/src/main/kotlin/me/vripper/download/DownloadService.kt b/vripper-core/src/main/kotlin/me/vripper/download/DownloadService.kt
index 780c55bc..6ad1a0fe 100644
--- a/vripper-core/src/main/kotlin/me/vripper/download/DownloadService.kt
+++ b/vripper-core/src/main/kotlin/me/vripper/download/DownloadService.kt
@@ -1,7 +1,8 @@
package me.vripper.download
+import dev.failsafe.Failsafe
+import dev.failsafe.RetryPolicy
import kotlinx.coroutines.Runnable
-import kotlinx.coroutines.launch
import me.vripper.entities.ImageEntity
import me.vripper.entities.PostEntity
import me.vripper.entities.Status
@@ -16,10 +17,8 @@ import me.vripper.services.DataTransaction
import me.vripper.services.RetryPolicyService
import me.vripper.services.SettingsService
import me.vripper.services.VGAuthService
-import me.vripper.utilities.GlobalScopeCoroutine
import me.vripper.utilities.LoggerDelegate
-import net.jodah.failsafe.Failsafe
-import net.jodah.failsafe.RetryPolicy
+import me.vripper.utilities.executorService
import org.jetbrains.exposed.sql.transactions.transaction
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
@@ -46,14 +45,14 @@ internal class DownloadService(
lock.withLock {
candidates.addAll(getCandidates(candidateCount()))
candidates.forEach {
- if (canRun(it.context.imageEntity.host)) {
+ if (canRun(it.imageEntity.host)) {
accepted.add(it)
- running[it.context.imageEntity.host]!!.add(it)
- log.debug("${it.context.imageEntity.url} accepted to run")
+ running[it.imageEntity.host]!!.add(it)
+ log.debug("${it.imageEntity.url} accepted to run")
}
}
accepted.forEach {
- pending[it.context.imageEntity.host]?.remove(it)
+ pending[it.imageEntity.host]?.remove(it)
scheduleForDownload(it)
}
accepted.clear()
@@ -144,18 +143,18 @@ internal class DownloadService(
}
private fun isPending(postId: Long): Boolean {
- return pending.values.flatten().any { it.context.imageEntity.postId == postId }
+ return pending.values.flatten().any { it.imageEntity.postId == postId }
}
private fun isRunning(postId: Long): Boolean {
- return running.values.flatten().any { it.context.imageEntity.postId == postId }
+ return running.values.flatten().any { it.imageEntity.postId == postId }
}
private fun stopAll() {
lock.withLock {
pending.values.clear()
running.values.flatten().forEach { obj: ImageDownloadRunnable -> obj.stop() }
- while (running.values.flatten().count { !it.context.completed } > 0) {
+ while (running.values.flatten().count { !it.completed } > 0) {
Thread.sleep(100)
}
dataTransaction.findAllNonCompletedPostIds().forEach {
@@ -169,13 +168,13 @@ internal class DownloadService(
lock.withLock {
for (postId in postIds) {
pending.values.forEach { pending ->
- pending.removeIf { it.context.imageEntity.postId == postId }
+ pending.removeIf { it.imageEntity.postId == postId }
}
running.values.flatten()
- .filter { p: ImageDownloadRunnable -> p.context.imageEntity.postId == postId }
+ .filter { p: ImageDownloadRunnable -> p.imageEntity.postId == postId }
.forEach { obj: ImageDownloadRunnable -> obj.stop() }
while (running.values.flatten()
- .count { !it.context.completed && it.context.imageEntity.postId == postId } > 0
+ .count { !it.completed && it.imageEntity.postId == postId } > 0
) {
Thread.sleep(100)
}
@@ -213,7 +212,7 @@ internal class DownloadService(
val list: List =
pending[host]!!.sortedWith(Comparator.comparingInt { it.postRank }
- .thenComparingInt { it.context.imageEntity.index })
+ .thenComparingInt { it.imageEntity.index })
for (imageDownloadRunnable in list) {
val count = hostIntegerMap[host] ?: 0
@@ -229,47 +228,42 @@ internal class DownloadService(
}
private fun scheduleForDownload(imageDownloadRunnable: ImageDownloadRunnable) {
- log.debug("Scheduling a job for ${imageDownloadRunnable.context.imageEntity.url}")
- GlobalScopeCoroutine.launch {
- eventBus.publishEvent(QueueStateEvent(QueueState(runningCount(), pendingCount())))
- try {
- Failsafe.with>(retryPolicyService.buildRetryPolicyForDownload("Failed to download ${imageDownloadRunnable.context.imageEntity.url}: "))
- .onFailure {
- log.error(
- "Failed to download ${imageDownloadRunnable.context.imageEntity.url} after ${it.attemptCount} tries",
- it.failure
- )
- val image = imageDownloadRunnable.context.imageEntity
- image.status = Status.ERROR
- dataTransaction.updateImage(image)
- }
- .onComplete {
- afterJobFinish(imageDownloadRunnable)
- eventBus.publishEvent(
- QueueStateEvent(
- QueueState(
- runningCount(), pendingCount()
- )
- )
- )
- eventBus.publishEvent(ErrorCountEvent(ErrorCount(dataTransaction.countImagesInError())))
- log.debug(
- "Finished downloading ${imageDownloadRunnable.context.imageEntity.url}"
- )
- }.run(imageDownloadRunnable::run)
- } catch (e: Exception) {
- log.error("Download Failure", e)
+ log.debug("Scheduling a job for ${imageDownloadRunnable.imageEntity.url}")
+ eventBus.publishEvent(QueueStateEvent(QueueState(runningCount(), pendingCount())))
+ Failsafe.with>(retryPolicyService.buildRetryPolicy("Failed to download ${imageDownloadRunnable.imageEntity.url}: "))
+ .with(executorService)
+ .onFailure {
+ log.error(
+ "Failed to download ${imageDownloadRunnable.imageEntity.url} after ${it.attemptCount} tries",
+ it.exception
+ )
+ val image = imageDownloadRunnable.imageEntity
+ image.status = Status.ERROR
+ dataTransaction.updateImage(image)
}
- }
+ .onComplete {
+ afterJobFinish(imageDownloadRunnable)
+ eventBus.publishEvent(
+ QueueStateEvent(
+ QueueState(
+ runningCount(), pendingCount()
+ )
+ )
+ )
+ eventBus.publishEvent(ErrorCountEvent(ErrorCount(dataTransaction.countImagesInError())))
+ log.debug(
+ "Finished downloading ${imageDownloadRunnable.imageEntity.url}"
+ )
+ }.runAsync(imageDownloadRunnable)
}
private fun afterJobFinish(imageDownloadRunnable: ImageDownloadRunnable) {
lock.withLock {
- val image = imageDownloadRunnable.context.imageEntity
+ val image = imageDownloadRunnable.imageEntity
running[image.host]!!.remove(imageDownloadRunnable)
if (!isPending(image.postId) && !isRunning(
image.postId
- ) && !imageDownloadRunnable.context.stopped
+ ) && !imageDownloadRunnable.stopped
) {
dataTransaction.finishPost(image.postId, true)
}
diff --git a/vripper-core/src/main/kotlin/me/vripper/download/ImageDownloadContext.kt b/vripper-core/src/main/kotlin/me/vripper/download/ImageDownloadContext.kt
index 7231f24a..6fef9bb8 100644
--- a/vripper-core/src/main/kotlin/me/vripper/download/ImageDownloadContext.kt
+++ b/vripper-core/src/main/kotlin/me/vripper/download/ImageDownloadContext.kt
@@ -15,8 +15,6 @@ internal class ImageDownloadContext(val imageEntity: ImageEntity, val settings:
HttpClientContext.create().apply { cookieStore = BasicCookieStore() }
val requests = mutableListOf()
val postId = imageEntity.postIdRef
- var stopped = false
- var completed = false
fun cancelCoroutines() {
runBlocking {
diff --git a/vripper-core/src/main/kotlin/me/vripper/download/ImageDownloadRunnable.kt b/vripper-core/src/main/kotlin/me/vripper/download/ImageDownloadRunnable.kt
index 45a9f4a1..762f9475 100644
--- a/vripper-core/src/main/kotlin/me/vripper/download/ImageDownloadRunnable.kt
+++ b/vripper-core/src/main/kotlin/me/vripper/download/ImageDownloadRunnable.kt
@@ -1,5 +1,6 @@
package me.vripper.download
+import dev.failsafe.function.CheckedRunnable
import me.vripper.entities.ImageEntity
import me.vripper.entities.Status
import me.vripper.exception.DownloadException
@@ -13,7 +14,6 @@ import me.vripper.utilities.LoggerDelegate
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
import java.io.IOException
@@ -26,23 +26,23 @@ import kotlin.io.path.Path
import kotlin.io.path.pathString
internal class ImageDownloadRunnable(
- private val imageEntity: ImageEntity, val postRank: Int, private val settings: Settings
+ val imageEntity: ImageEntity, val postRank: Int, private val settings: Settings
) : KoinComponent, CheckedRunnable {
private val log by LoggerDelegate()
-
private val dataTransaction: DataTransaction by inject()
private val hosts: List = getKoin().getAll()
+ var completed = false
+ var stopped = false
- val context: ImageDownloadContext = ImageDownloadContext(imageEntity, settings)
+ private lateinit var context: ImageDownloadContext
- @Throws(DownloadException::class)
fun download() {
try {
imageEntity.status = Status.DOWNLOADING
imageEntity.downloaded = 0
dataTransaction.updateImage(imageEntity)
synchronized(imageEntity.postId.toString().intern()) {
- val post = dataTransaction.findPostById(context.postId).orElseThrow()
+ val post = dataTransaction.findPostById(context.postId)
if (post.status != Status.DOWNLOADING) {
post.status = Status.DOWNLOADING
dataTransaction.updatePost(post)
@@ -54,7 +54,7 @@ internal class ImageDownloadRunnable(
log.debug("Resolved name for ${imageEntity.url}: ${downloadedImage.name}")
log.debug("Downloaded image {} to {}", imageEntity.url, downloadedImage.path)
synchronized(imageEntity.postId.toString().intern()) {
- val post = dataTransaction.findPostById(context.postId).orElseThrow()
+ val post = dataTransaction.findPostById(context.postId)
val downloadDirectory = Path(post.downloadDirectory, post.folderName).pathString
checkImageTypeAndRename(
downloadDirectory, downloadedImage, imageEntity.index
@@ -70,7 +70,7 @@ internal class ImageDownloadRunnable(
dataTransaction.updateImage(imageEntity)
}
} catch (e: Exception) {
- if (context.stopped) {
+ if (stopped) {
return
}
imageEntity.status = Status.ERROR
@@ -119,19 +119,26 @@ internal class ImageDownloadRunnable(
}
}
- @Throws(Exception::class)
override fun run() {
+ context = ImageDownloadContext(imageEntity, settings)
try {
- if (context.stopped) {
+ if (stopped) {
return
}
download()
} finally {
- context.completed = true
+ completed = true
context.cancelCoroutines()
}
}
+ fun stop() {
+ stopped = true
+ context.requests.forEach { it.abort() }
+ context.cancelCoroutines()
+ dataTransaction.updateImage(context.imageEntity)
+ }
+
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
@@ -142,11 +149,4 @@ internal class ImageDownloadRunnable(
override fun hashCode(): Int {
return Objects.hash(imageEntity.id)
}
-
- fun stop() {
- context.requests.forEach { it.abort() }
- context.cancelCoroutines()
- context.stopped = true
- dataTransaction.updateImage(context.imageEntity)
- }
}
\ No newline at end of file
diff --git a/vripper-core/src/main/kotlin/me/vripper/host/Host.kt b/vripper-core/src/main/kotlin/me/vripper/host/Host.kt
index e8642f16..ebc9b7e4 100644
--- a/vripper-core/src/main/kotlin/me/vripper/host/Host.kt
+++ b/vripper-core/src/main/kotlin/me/vripper/host/Host.kt
@@ -106,7 +106,7 @@ internal abstract class Host(
return BufferedOutputStream(Files.newOutputStream(tempImage)).use { bos ->
val image = context.imageEntity
synchronized(image.postId.toString().intern()) {
- val post = dataTransaction.findPostById(context.postId).orElseThrow()
+ val post = dataTransaction.findPostById(context.postId)
val size = if (image.size < 0) {
response.entity.contentLength
} else {
@@ -134,7 +134,7 @@ internal abstract class Host(
}
}
while (response.entity.content.read(buffer)
- .also { read = it } != -1 && !context.stopped
+ .also { read = it } != -1
) {
bos.write(buffer, 0, read)
image.downloaded += read
diff --git a/vripper-core/src/main/kotlin/me/vripper/services/AppEndpointService.kt b/vripper-core/src/main/kotlin/me/vripper/services/AppEndpointService.kt
index 2056b46c..691da88a 100644
--- a/vripper-core/src/main/kotlin/me/vripper/services/AppEndpointService.kt
+++ b/vripper-core/src/main/kotlin/me/vripper/services/AppEndpointService.kt
@@ -1,7 +1,6 @@
package me.vripper.services
import kotlinx.coroutines.flow.*
-import kotlinx.coroutines.launch
import kotlinx.coroutines.time.sample
import kotlinx.serialization.json.Json
import me.vripper.download.DownloadService
@@ -13,9 +12,9 @@ import me.vripper.tasks.AddPostTask
import me.vripper.tasks.ThreadLookupTask
import me.vripper.utilities.ApplicationProperties
import me.vripper.utilities.ApplicationProperties.VRIPPER_DIR
-import me.vripper.utilities.GlobalScopeCoroutine
import me.vripper.utilities.LoggerDelegate
import me.vripper.utilities.PathUtils
+import me.vripper.utilities.executorService
import org.h2.jdbc.JdbcSQLNonTransientConnectionException
import java.sql.DriverManager
import java.time.Duration
@@ -59,17 +58,17 @@ internal class AppEndpointService(
threadId = m.group(1).toLong()
postId = m.group(4)?.toLong()
if (postId == null) {
- GlobalScopeCoroutine.launch {
+ executorService.submit(
ThreadLookupTask(
threadId, settingsService.settings
- ).run()
- }
+ )
+ )
} else {
- GlobalScopeCoroutine.launch {
+ executorService.submit(
AddPostTask(
listOf(ThreadPostId(threadId, postId))
- ).run()
- }
+ )
+ )
}
} else {
log.error("Invalid link $link, link is missing the threadId")
@@ -81,15 +80,13 @@ internal class AppEndpointService(
override suspend fun restartAll(posIds: List) {
lock.withLock {
- downloadService.restartAll(posIds.map { dataTransaction.findPostByPostId(it) }.filter { it.isPresent }
- .map { it.get() })
+ downloadService.restartAll(posIds.filter { dataTransaction.exists(it) }
+ .map { dataTransaction.findPostByPostId(it) })
}
}
override suspend fun download(posts: List) {
- GlobalScopeCoroutine.launch {
- AddPostTask(posts).run()
- }
+ executorService.submit(AddPostTask(posts))
}
override suspend fun stopAll(postIdList: List) {
@@ -185,16 +182,21 @@ internal class AppEndpointService(
}
override suspend fun rename(postId: Long, newName: String) {
- GlobalScopeCoroutine.launch {
+ executorService.submit {
synchronized(postId.toString().intern()) {
- dataTransaction.findPostByPostId(postId).ifPresent { post ->
- if (Path(post.downloadDirectory, post.folderName).exists()) {
- PathUtils.rename(
- dataTransaction.findImagesByPostId(postId), post.downloadDirectory, post.folderName, newName
- )
+ if (dataTransaction.exists(postId)) {
+ dataTransaction.findPostByPostId(postId).let { post ->
+ if (Path(post.downloadDirectory, post.folderName).exists()) {
+ PathUtils.rename(
+ dataTransaction.findImagesByPostId(postId),
+ post.downloadDirectory,
+ post.folderName,
+ newName
+ )
+ }
+ post.folderName = PathUtils.sanitize(newName)
+ dataTransaction.updatePost(post)
}
- post.folderName = PathUtils.sanitize(newName)
- dataTransaction.updatePost(post)
}
}
}
@@ -251,7 +253,7 @@ internal class AppEndpointService(
}
override suspend fun findPost(postId: Long): Post {
- return mapper(dataTransaction.findPostByPostId(postId).orElseThrow())
+ return mapper(dataTransaction.findPostByPostId(postId))
}
override suspend fun findImagesByPostId(postId: Long): List {
@@ -353,7 +355,7 @@ internal class AppEndpointService(
val addedAt = it.getTimestamp("ADDED_AT")
val folderName = it.getString("FOLDER_NAME") ?: ""
- val exists = dataTransaction.findPostByPostId(postId).isPresent
+ val exists = dataTransaction.exists(postId)
if (exists) {
continue
}
diff --git a/vripper-core/src/main/kotlin/me/vripper/services/DataTransaction.kt b/vripper-core/src/main/kotlin/me/vripper/services/DataTransaction.kt
index d24b7116..447dd16a 100644
--- a/vripper-core/src/main/kotlin/me/vripper/services/DataTransaction.kt
+++ b/vripper-core/src/main/kotlin/me/vripper/services/DataTransaction.kt
@@ -1,8 +1,10 @@
package me.vripper.services
+import com.github.benmanes.caffeine.cache.Caffeine
+import com.github.benmanes.caffeine.cache.LoadingCache
import me.vripper.data.repositories.ImageRepository
import me.vripper.data.repositories.MetadataRepository
-import me.vripper.data.repositories.PostDownloadStateRepository
+import me.vripper.data.repositories.PostRepository
import me.vripper.data.repositories.ThreadRepository
import me.vripper.entities.*
import me.vripper.event.*
@@ -12,12 +14,13 @@ import me.vripper.utilities.PathUtils.sanitize
import me.vripper.vgapi.PostItem
import org.jetbrains.exposed.sql.transactions.transaction
import java.util.*
+import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import kotlin.io.path.pathString
internal class DataTransaction(
private val settingsService: SettingsService,
- private val postDownloadStateRepository: PostDownloadStateRepository,
+ private val postRepository: PostRepository,
private val imageRepository: ImageRepository,
private val threadRepository: ThreadRepository,
private val metadataRepository: MetadataRepository,
@@ -25,15 +28,24 @@ internal class DataTransaction(
) {
private val nextRank = AtomicInteger(transaction { getQueuePosition() }?.plus(1) ?: 0)
+ private val postEntityIdCache: LoadingCache =
+ Caffeine.newBuilder().expireAfterAccess(5, TimeUnit.MINUTES).build { id ->
+ transaction { postRepository.findById(id) }
+ }
+
+ private val postPostIdCache: LoadingCache =
+ Caffeine.newBuilder().expireAfterAccess(5, TimeUnit.MINUTES).build { id ->
+ transaction { postRepository.findByPostId(id) }
+ }
private fun save(postEntities: List): List {
- return transaction { postDownloadStateRepository.save(postEntities) }
+ return transaction { postRepository.save(postEntities) }
}
fun saveAndNotify(postEntity: PostEntity, images: List) {
val savedPost = transaction {
val savedPost =
- postDownloadStateRepository.save(listOf(postEntity.copy(rank = nextRank.andIncrement))).first()
+ postRepository.save(listOf(postEntity.copy(rank = nextRank.andIncrement))).first()
save(images.map { it.copy(postIdRef = savedPost.id) })
savedPost
}
@@ -41,12 +53,18 @@ internal class DataTransaction(
}
fun updatePosts(postEntities: List) {
- transaction { postDownloadStateRepository.update(postEntities) }
+ transaction { postRepository.update(postEntities) }
+ postEntities.forEach { postEntity ->
+ postPostIdCache.put(postEntity.postId, postEntity)
+ postEntityIdCache.put(postEntity.id, postEntity)
+ }
eventBus.publishEvent(PostUpdateEvent(postEntities))
}
fun updatePost(postEntity: PostEntity) {
- transaction { postDownloadStateRepository.update(postEntity) }
+ transaction { postRepository.update(postEntity) }
+ postPostIdCache.put(postEntity.postId, postEntity)
+ postEntityIdCache.put(postEntity.id, postEntity)
eventBus.publishEvent(PostUpdateEvent(listOf(postEntity)))
}
@@ -75,7 +93,10 @@ internal class DataTransaction(
}
fun exists(postId: Long): Boolean {
- return transaction { postDownloadStateRepository.existByPostId(postId) }
+ if (postPostIdCache.getIfPresent(postId) != null) {
+ return true
+ }
+ return transaction { postRepository.existByPostId(postId) }
}
@Synchronized
@@ -128,7 +149,7 @@ internal class DataTransaction(
}
private fun getQueuePosition(): Int? {
- return postDownloadStateRepository.findMaxRank()
+ return postRepository.findMaxRank()
}
private fun save(imageEntities: List) {
@@ -136,7 +157,7 @@ internal class DataTransaction(
}
fun finishPost(postId: Long, automatic: Boolean = false) {
- val post = findPostByPostId(postId).orElseThrow()
+ val post = findPostByPostId(postId)
val imagesInErrorStatus = findByPostIdAndIsError(post.postId)
if (imagesInErrorStatus.isNotEmpty()) {
post.status = Status.ERROR
@@ -167,9 +188,13 @@ internal class DataTransaction(
transaction {
metadataRepository.deleteAllByPostId(postIds)
imageRepository.deleteAllByPostId(postIds)
- postDownloadStateRepository.deleteAll(postIds)
+ postRepository.deleteAll(postIds)
sortPostsByRank()
}
+ postIds.forEach { postId ->
+ postPostIdCache.get(postId)?.let { postEntityIdCache.invalidate(it.id) }
+ postPostIdCache.invalidate(postId)
+ }
eventBus.publishEvent(PostDeleteEvent(postIds = postIds))
eventBus.publishEvent(ErrorCountEvent(ErrorCount(countImagesInError())))
}
@@ -180,7 +205,7 @@ internal class DataTransaction(
}
fun clearCompleted(): List {
- val completed = transaction { postDownloadStateRepository.findCompleted() }
+ val completed = transaction { postRepository.findCompleted() }
remove(completed)
return completed
}
@@ -225,15 +250,15 @@ internal class DataTransaction(
}
fun setDownloadingToStopped() {
- transaction { postDownloadStateRepository.setDownloadingToStopped() }
+ transaction { postRepository.setDownloadingToStopped() }
}
fun findAllPosts(): List {
- return transaction { postDownloadStateRepository.findAll() }
+ return transaction { postRepository.findAll() }
}
- fun findPostById(id: Long): Optional {
- return transaction { postDownloadStateRepository.findById(id) }
+ fun findPostById(id: Long): PostEntity {
+ return postEntityIdCache.get(id) ?: throw NoSuchElementException("Post with id = $id does not exist")
}
fun findImagesByPostId(postId: Long): List {
@@ -262,8 +287,8 @@ internal class DataTransaction(
return transaction { imageRepository.countError() }
}
- fun findPostByPostId(postId: Long): Optional {
- return transaction { postDownloadStateRepository.findByPostId(postId) }
+ fun findPostByPostId(postId: Long): PostEntity {
+ return postPostIdCache.get(postId) ?: throw NoSuchElementException("Post with postId = $postId does not exist")
}
fun findThreadByThreadId(threadId: Long): Optional {
@@ -271,7 +296,7 @@ internal class DataTransaction(
}
fun findAllNonCompletedPostIds(): List {
- return transaction { postDownloadStateRepository.findAllNonCompletedPostIds() }
+ return transaction { postRepository.findAllNonCompletedPostIds() }
}
fun findMetadataByPostId(postId: Long): Optional {
diff --git a/vripper-core/src/main/kotlin/me/vripper/services/HTTPService.kt b/vripper-core/src/main/kotlin/me/vripper/services/HTTPService.kt
index d5f4d133..25372e62 100644
--- a/vripper-core/src/main/kotlin/me/vripper/services/HTTPService.kt
+++ b/vripper-core/src/main/kotlin/me/vripper/services/HTTPService.kt
@@ -14,7 +14,6 @@ import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder
import org.apache.hc.core5.pool.PoolConcurrencyPolicy
import org.apache.hc.core5.pool.PoolReusePolicy
-import org.apache.hc.core5.util.TimeValue
import org.apache.hc.core5.util.Timeout
internal class HTTPService(
@@ -41,8 +40,10 @@ internal class HTTPService(
buildConnectionPool()
buildClientBuilder()
coroutineScope.launch {
- pcm.closeIdle(TimeValue.ofSeconds(60))
- delay(15000)
+ while (isActive) {
+ pcm.closeExpired()
+ delay(60_000)
+ }
}
coroutineScope.launch {
eventBus
@@ -64,8 +65,8 @@ internal class HTTPService(
private fun buildConnectionPool() {
pcm = PoolingHttpClientConnectionManagerBuilder.create()
- .setPoolConcurrencyPolicy(PoolConcurrencyPolicy.STRICT)
- .setConnPoolPolicy(PoolReusePolicy.LIFO)
+ .setPoolConcurrencyPolicy(PoolConcurrencyPolicy.LAX)
+ .setConnPoolPolicy(PoolReusePolicy.FIFO)
.setDefaultConnectionConfig(cc)
.setMaxConnTotal(Int.MAX_VALUE)
.setMaxConnPerRoute(Int.MAX_VALUE)
@@ -83,7 +84,6 @@ internal class HTTPService(
cc = ConnectionConfig.custom()
.setConnectTimeout(Timeout.ofSeconds(connectionTimeout.toLong()))
.setSocketTimeout(Timeout.ofSeconds(connectionTimeout.toLong()))
- .setTimeToLive(TimeValue.ofMinutes(10))
.build()
}
diff --git a/vripper-core/src/main/kotlin/me/vripper/services/RetryPolicyService.kt b/vripper-core/src/main/kotlin/me/vripper/services/RetryPolicyService.kt
index 362edef6..84479d03 100644
--- a/vripper-core/src/main/kotlin/me/vripper/services/RetryPolicyService.kt
+++ b/vripper-core/src/main/kotlin/me/vripper/services/RetryPolicyService.kt
@@ -1,5 +1,6 @@
package me.vripper.services
+import dev.failsafe.RetryPolicy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -8,8 +9,6 @@ import kotlinx.coroutines.launch
import me.vripper.event.EventBus
import me.vripper.event.SettingsUpdateEvent
import me.vripper.utilities.LoggerDelegate
-import net.jodah.failsafe.RetryPolicy
-import net.jodah.failsafe.event.ExecutionAttemptedEvent
import java.time.temporal.ChronoUnit
internal class RetryPolicyService(
@@ -29,16 +28,10 @@ internal class RetryPolicyService(
}
}
- fun buildRetryPolicyForDownload(message: String): RetryPolicy {
- return RetryPolicy().withDelay(2, 5, ChronoUnit.SECONDS).withMaxAttempts(maxAttempts).onFailedAttempt {
- log.warn(message + "#${it.attemptCount} tries failed", it.lastFailure)
- }
- }
-
- fun buildGenericRetryPolicy(message: String): RetryPolicy {
- return RetryPolicy().withDelay(2, 5, ChronoUnit.SECONDS).withMaxAttempts(maxAttempts)
- .onFailedAttempt { e: ExecutionAttemptedEvent ->
- log.warn(message + "#${e.attemptCount} tries failed", e.lastFailure)
- }
+ fun buildRetryPolicy(message: String): RetryPolicy {
+ return RetryPolicy.builder().withDelay(2, 5, ChronoUnit.SECONDS).withMaxAttempts(maxAttempts)
+ .onFailedAttempt {
+ log.warn(message + "#${it.attemptCount} tries failed", it.lastException)
+ }.build()
}
}
\ No newline at end of file
diff --git a/vripper-core/src/main/kotlin/me/vripper/services/ThreadCacheService.kt b/vripper-core/src/main/kotlin/me/vripper/services/ThreadCacheService.kt
index 22f5b1a7..a906cb73 100644
--- a/vripper-core/src/main/kotlin/me/vripper/services/ThreadCacheService.kt
+++ b/vripper-core/src/main/kotlin/me/vripper/services/ThreadCacheService.kt
@@ -2,8 +2,11 @@ package me.vripper.services
import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.LoadingCache
-import kotlinx.coroutines.*
+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.vgapi.ThreadItem
@@ -25,9 +28,7 @@ internal class ThreadCacheService(val eventBus: EventBus) {
private val cache: LoadingCache =
Caffeine.newBuilder().expireAfterWrite(20, TimeUnit.MINUTES).build { threadId ->
- runBlocking {
- ThreadLookupAPIParser(threadId).parse()
- }
+ ThreadLookupAPIParser(threadId).parse()
}
@Throws(ExecutionException::class)
diff --git a/vripper-core/src/main/kotlin/me/vripper/services/VGAuthService.kt b/vripper-core/src/main/kotlin/me/vripper/services/VGAuthService.kt
index a0a5c83a..89bb9bc1 100644
--- a/vripper-core/src/main/kotlin/me/vripper/services/VGAuthService.kt
+++ b/vripper-core/src/main/kotlin/me/vripper/services/VGAuthService.kt
@@ -12,8 +12,8 @@ import me.vripper.event.VGUserLoginEvent
import me.vripper.exception.VripperException
import me.vripper.model.Settings
import me.vripper.tasks.LeaveThanksTask
-import me.vripper.utilities.GlobalScopeCoroutine
import me.vripper.utilities.LoggerDelegate
+import me.vripper.utilities.executorService
import org.apache.hc.client5.http.classic.methods.HttpPost
import org.apache.hc.client5.http.cookie.BasicCookieStore
import org.apache.hc.client5.http.cookie.Cookie
@@ -108,8 +108,8 @@ internal class VGAuthService(
}
fun leaveThanks(postEntity: PostEntity) {
- GlobalScopeCoroutine.launch {
- LeaveThanksTask(postEntity, authenticated, context).run()
- }
+ executorService.submit(
+ LeaveThanksTask(postEntity, authenticated, context)
+ )
}
}
\ No newline at end of file
diff --git a/vripper-core/src/main/kotlin/me/vripper/tasks/ThreadLookupTask.kt b/vripper-core/src/main/kotlin/me/vripper/tasks/ThreadLookupTask.kt
index afc5f386..a6b2addf 100644
--- a/vripper-core/src/main/kotlin/me/vripper/tasks/ThreadLookupTask.kt
+++ b/vripper-core/src/main/kotlin/me/vripper/tasks/ThreadLookupTask.kt
@@ -1,14 +1,13 @@
package me.vripper.tasks
-import kotlinx.coroutines.launch
import me.vripper.entities.ThreadEntity
import me.vripper.model.Settings
import me.vripper.model.ThreadPostId
import me.vripper.services.DataTransaction
import me.vripper.services.SettingsService
import me.vripper.services.ThreadCacheService
-import me.vripper.utilities.GlobalScopeCoroutine
import me.vripper.utilities.LoggerDelegate
+import me.vripper.utilities.executorService
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -34,15 +33,14 @@ internal class ThreadLookupTask(private val threadId: Long, private val settings
}
if (threadLookupResult.postItemList.size <= settings.downloadSettings.autoQueueThreshold) {
- GlobalScopeCoroutine.launch {
+ executorService.submit(
AddPostTask(threadLookupResult.postItemList.map {
ThreadPostId(
it.threadId, it.postId
)
- }).run()
- }
+ })
+ )
} else {
- try {
dataTransaction.save(
ThreadEntity(
title = threadLookupResult.title,
@@ -51,9 +49,6 @@ internal class ThreadLookupTask(private val threadId: Long, private val settings
total = threadLookupResult.postItemList.size
)
)
- } catch (e: Exception) {
- e.printStackTrace()
- }
}
}
} catch (e: Exception) {
diff --git a/vripper-core/src/main/kotlin/me/vripper/utilities/GlobalScope.kt b/vripper-core/src/main/kotlin/me/vripper/utilities/GlobalScope.kt
deleted file mode 100644
index b887f4a8..00000000
--- a/vripper-core/src/main/kotlin/me/vripper/utilities/GlobalScope.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package me.vripper.utilities
-
-import kotlinx.coroutines.CoroutineExceptionHandler
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.SupervisorJob
-
-private val log by LoggerDelegate()
-
-val errorHandler = CoroutineExceptionHandler { _, exception ->
- log.error("Unexpected error", exception)
-}
-
-val GlobalScopeCoroutine = CoroutineScope(SupervisorJob() + Dispatchers.IO + errorHandler)
\ No newline at end of file
diff --git a/vripper-core/src/main/kotlin/me/vripper/utilities/Utils.kt b/vripper-core/src/main/kotlin/me/vripper/utilities/Utils.kt
new file mode 100644
index 00000000..05d82fc6
--- /dev/null
+++ b/vripper-core/src/main/kotlin/me/vripper/utilities/Utils.kt
@@ -0,0 +1,6 @@
+package me.vripper.utilities
+
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+val executorService: ExecutorService = Executors.newVirtualThreadPerTaskExecutor()
\ No newline at end of file
diff --git a/vripper-core/src/main/kotlin/me/vripper/vgapi/PostLookupAPIParser.kt b/vripper-core/src/main/kotlin/me/vripper/vgapi/PostLookupAPIParser.kt
index ab09f515..ab43b8ac 100644
--- a/vripper-core/src/main/kotlin/me/vripper/vgapi/PostLookupAPIParser.kt
+++ b/vripper-core/src/main/kotlin/me/vripper/vgapi/PostLookupAPIParser.kt
@@ -1,5 +1,7 @@
package me.vripper.vgapi
+import dev.failsafe.Failsafe
+import dev.failsafe.function.CheckedSupplier
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.withPermit
import me.vripper.exception.DownloadException
@@ -11,8 +13,6 @@ import me.vripper.services.VGAuthService
import me.vripper.tasks.Tasks
import me.vripper.utilities.LoggerDelegate
import me.vripper.utilities.RequestLimit
-import net.jodah.failsafe.Failsafe
-import net.jodah.failsafe.function.CheckedSupplier
import org.apache.hc.client5.http.classic.methods.HttpGet
import org.apache.hc.core5.http.io.entity.EntityUtils
import org.apache.hc.core5.net.URIBuilder
@@ -42,9 +42,9 @@ internal class PostLookupAPIParser(private val threadId: Long, private val postI
log.debug("Requesting {}", httpGet)
Tasks.increment()
return try {
- Failsafe.with(retryPolicyService.buildGenericRetryPolicy("Failed to parse $httpGet: ")).onFailure {
+ Failsafe.with(retryPolicyService.buildRetryPolicy("Failed to parse $httpGet: ")).onFailure {
log.error(
- "Failed to process thread $threadId, post $postId", it.failure
+ "Failed to process thread $threadId, post $postId", it.exception
)
}.get(CheckedSupplier {
runBlocking {
diff --git a/vripper-core/src/main/kotlin/me/vripper/vgapi/ThreadLookupAPIParser.kt b/vripper-core/src/main/kotlin/me/vripper/vgapi/ThreadLookupAPIParser.kt
index b18b130f..bd7c050c 100644
--- a/vripper-core/src/main/kotlin/me/vripper/vgapi/ThreadLookupAPIParser.kt
+++ b/vripper-core/src/main/kotlin/me/vripper/vgapi/ThreadLookupAPIParser.kt
@@ -1,5 +1,7 @@
package me.vripper.vgapi
+import dev.failsafe.Failsafe
+import dev.failsafe.function.CheckedSupplier
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.withPermit
import me.vripper.exception.DownloadException
@@ -11,8 +13,6 @@ import me.vripper.services.VGAuthService
import me.vripper.tasks.Tasks
import me.vripper.utilities.LoggerDelegate
import me.vripper.utilities.RequestLimit
-import net.jodah.failsafe.Failsafe
-import net.jodah.failsafe.function.CheckedSupplier
import org.apache.hc.client5.http.classic.methods.HttpGet
import org.apache.hc.core5.http.io.entity.EntityUtils
import org.apache.hc.core5.net.URIBuilder
@@ -43,10 +43,10 @@ internal class ThreadLookupAPIParser(private val threadId: Long) : KoinComponent
log.debug("Requesting {}", httpGet)
Tasks.increment()
return try {
- Failsafe.with(retryPolicyService.buildGenericRetryPolicy("Failed to parse $httpGet: ")).onFailure {
+ Failsafe.with(retryPolicyService.buildRetryPolicy("Failed to parse $httpGet: ")).onFailure {
log.error(
"Failed to process thread $threadId",
- it.failure
+ it.exception
)
}.get(CheckedSupplier {
runBlocking {
diff --git a/vripper-gui/src/main/kotlin/me/vripper/gui/components/fragments/AboutFragment.kt b/vripper-gui/src/main/kotlin/me/vripper/gui/components/fragments/AboutFragment.kt
index a8e602fc..5580c3f2 100644
--- a/vripper-gui/src/main/kotlin/me/vripper/gui/components/fragments/AboutFragment.kt
+++ b/vripper-gui/src/main/kotlin/me/vripper/gui/components/fragments/AboutFragment.kt
@@ -23,8 +23,7 @@ class AboutFragment : Fragment("About") {
padding = Insets(15.0, 15.0, 15.0, 15.0)
spacing = 15.0
imageview("icons/64x64.png")
- vbox {
- spacing = 5.0
+ vbox(spacing = 5.0) {
text("VRipper") {
style {
fontWeight = FontWeight.BOLD
@@ -32,15 +31,29 @@ class AboutFragment : Fragment("About") {
}
}
text("Version ${ApplicationProperties.VERSION}")
- text("Developed by death-claw and VRipper working group")
- hyperlink("Home Page") {
- action {
- openLink("https://github.com/death-claw/vripper-project")
+ text("Developed by dev-claw and VRipper working group")
+ hbox(spacing = 5.0) {
+ hyperlink {
+ imageview("icons/github-mark.png").apply {
+ isPreserveRatio = true
+ fitHeight = 32.0
+ }
+ action {
+ openLink("https://github.com/dev-claw/vripper-project")
+ }
+ }
+ hyperlink {
+ imageview("icons/buymeacoffee-logo.png").apply {
+ isPreserveRatio = true
+ fitHeight = 32.0
+ }
+ action {
+ openLink("https://buymeacoffee.com/devclaw")
+ }
}
}
}
}
-
}
tab("System") {
form {
diff --git a/vripper-gui/src/main/resources/icons/buymeacoffee-logo.png b/vripper-gui/src/main/resources/icons/buymeacoffee-logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..d28f44254e90d08f9913470f97b8e08c3ebdebf1
GIT binary patch
literal 1455
zcmV;g1yK5lP)>bLL|bvR9gjBZO3Njzh%N#ik;RJ@WnCoi|5eCL
z;wDnXW~t?m&8I3gnacH4s_R!>trNXdXO6d1eWac~PM+0qclP9vdRpt?^wZRnS`R06
z;;h!gS)J0_IJ5StKR@30y?Qwx)6_2?zrx&me_xj*=ZE&{?QU`}l-56i{Y%J;CWGVHX2NNxTkFCY&<*GiYEk^Gh~&ms4k=93w@HY8pumR=o_^WWc0~q)3(KsR_aeMj9Ae
zNOD+9gHKeF+|XtT4XMGktWMSLd6sPMXx|8oa~6^z$>U^brDA9_0J0nex#0~V&R?nV
zO3=-_jf_zT+0o)6Zb%(Tf@0|`t!K14b!U?$cP6@e@u~)aW_Gb4RRlpNE&eX3r2tuw
z!~ygl%r7sB?w8yR6~a%cxCGO1fAW>(+fEOIXSQ)YlI0&9d1?X
z%_$47PPO1@Qfdq)ViVvPmEDnYT(saS#}uz)R=BxMM#$3Sc?HdGjT1`Jk8
z28yyJtxDDM2-y5f3yy*7GV@RmxM;vc{WryHla(1%&G6e&0|Wp+zXRKO0Mp3Y#hFU9A@9#ZaY*r>(7==*?p_AmHQ5#z2loL>ANlGSY&Vx$^a7Dc~WX1hSF>*~i
zT->Ngjv6A=I0e9rT7ae;oZ9uD{q4JRGjIPi(JI*hNzdxCRATJ3nj`tXvl#k-wn#a~
zEFGNc2-4*KJoViNceRSgT9S^yky;$Cf1-s|sf}r++dHq%H!F{}v_ghd(H$){zhLUZ
z*4~@5)K90^wZaISF-P65)_AH6
zI*MYzD<1qkr*w246LmBt^^IN1dagA(UR$F>%k?=5UZ(^0e*ulMjWSDaZT0{F002ov
JPDHLkV1fcQsZIa@
literal 0
HcmV?d00001
diff --git a/vripper-gui/src/main/resources/icons/github-mark.png b/vripper-gui/src/main/resources/icons/github-mark.png
new file mode 100644
index 0000000000000000000000000000000000000000..6cb3b705d018006a2bd4200ea94c9d5fb98b6f76
GIT binary patch
literal 6393
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
Hu0mjfZ$b4`
literal 0
HcmV?d00001