mirror of
https://github.com/dev-claw/vripper-project.git
synced 2026-08-19 08:35:41 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ba422eee1 | ||
|
|
e80d704b03 | ||
|
|
562c36bf28 |
@@ -15,8 +15,8 @@
|
||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<revision>6.6.4</revision>
|
||||
<app-version>6.6.4</app-version>
|
||||
<revision>6.7.0</revision>
|
||||
<app-version>6.7.0</app-version>
|
||||
<kotlin.version>2.2.20</kotlin.version>
|
||||
<slf4j.version>2.0.16</slf4j.version>
|
||||
<logback.version>1.5.12</logback.version>
|
||||
|
||||
@@ -76,7 +76,7 @@ val coreModule = module {
|
||||
MetadataService(get(), get())
|
||||
}
|
||||
single {
|
||||
AcidimgHost(get())
|
||||
AcidimgHost()
|
||||
} bind Host::class
|
||||
single {
|
||||
DPicMeHost()
|
||||
|
||||
@@ -2,7 +2,6 @@ package me.vripper.host
|
||||
|
||||
import me.vripper.exception.HostException
|
||||
import me.vripper.exception.XpathException
|
||||
import me.vripper.services.HTTPService
|
||||
import me.vripper.services.download.ImageDownloadRunnable.Context
|
||||
import me.vripper.utilities.HtmlUtils
|
||||
import me.vripper.utilities.LoggerDelegate
|
||||
@@ -12,9 +11,7 @@ import org.apache.hc.client5.http.entity.UrlEncodedFormEntity
|
||||
import org.apache.hc.core5.http.message.BasicNameValuePair
|
||||
import org.w3c.dom.Node
|
||||
|
||||
internal class AcidimgHost(
|
||||
private val httpService: HTTPService,
|
||||
) : Host("acidimg.cc", 0) {
|
||||
internal class AcidimgHost : Host("acidimg.cc", 0) {
|
||||
private val log by LoggerDelegate()
|
||||
|
||||
@Throws(HostException::class)
|
||||
|
||||
@@ -31,7 +31,7 @@ internal abstract class Host(
|
||||
) : KoinComponent {
|
||||
private val log by LoggerDelegate()
|
||||
|
||||
private val httpService: HTTPService by inject()
|
||||
protected val httpService: HTTPService by inject()
|
||||
private val dataAccessService: DataAccessService by inject()
|
||||
private val downloadSpeedService: DownloadSpeedService by inject()
|
||||
|
||||
@@ -61,7 +61,7 @@ internal abstract class Host(
|
||||
val imageMimeType = getImageMimeType(headers)
|
||||
val downloadedImage = if (imageMimeType != null) {
|
||||
// a direct link, awesome
|
||||
val downloadedImage = fetch(context.imageEntity.url, context.imageEntity.url, context) {
|
||||
val downloadedImage = fetch(context.imageEntity.url, context) {
|
||||
handleImageDownload(it, context)
|
||||
}
|
||||
DownloadedImage(getDefaultImageName(context.imageEntity.url), downloadedImage.first, downloadedImage.second)
|
||||
@@ -84,7 +84,7 @@ internal abstract class Host(
|
||||
private fun downloadByHost(context: Context): DownloadedImage {
|
||||
val resolvedImage = resolve(context)
|
||||
val downloadImage: Pair<Path, ImageMimeType> =
|
||||
fetch(resolvedImage.second, context.imageEntity.url, context) {
|
||||
fetch(resolvedImage.second, context) {
|
||||
handleImageDownload(it, context)
|
||||
}
|
||||
return DownloadedImage(resolvedImage.first, downloadImage.first, downloadImage.second)
|
||||
@@ -152,6 +152,7 @@ internal abstract class Host(
|
||||
|
||||
fun head(context: Context): Array<Header> {
|
||||
val httpHead = HttpHead(context.imageEntity.url).also {
|
||||
it.addHeader("Referer", "https://vipergirls.to/")
|
||||
it.setAbsoluteRequestUri(true)
|
||||
context.requests.add(it)
|
||||
}
|
||||
@@ -169,8 +170,8 @@ internal abstract class Host(
|
||||
|
||||
fun <T> fetch(
|
||||
url: String,
|
||||
referer: String,
|
||||
context: Context,
|
||||
referer: String = "https://vipergirls.to/",
|
||||
transformer: (ClassicHttpResponse) -> T
|
||||
): T {
|
||||
val httpGet =
|
||||
@@ -191,7 +192,7 @@ internal abstract class Host(
|
||||
url: String,
|
||||
context: Context
|
||||
): Document {
|
||||
return fetch(url, url, context) {
|
||||
return fetch(url, context) {
|
||||
HtmlUtils.clean(it.entity.content)
|
||||
}.also {
|
||||
if (log.isDebugEnabled) {
|
||||
|
||||
@@ -8,7 +8,6 @@ import me.vripper.utilities.LoggerDelegate
|
||||
import me.vripper.utilities.XpathUtils
|
||||
import org.apache.hc.client5.http.impl.cookie.BasicClientCookie
|
||||
import org.w3c.dom.Node
|
||||
import java.sql.Date
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.util.*
|
||||
@@ -24,15 +23,13 @@ internal class ImageBamHost : Host("imagebam.com", 2) {
|
||||
val doc = try {
|
||||
log.debug(String.format("Looking for xpath expression %s in %s", CONTINUE_XPATH, context.imageEntity.url))
|
||||
if (XpathUtils.getAsNode(document, CONTINUE_XPATH) != null) {
|
||||
val clientCookie = BasicClientCookie("nsfw_inter", "1")
|
||||
clientCookie.domain = "www.imagebam.com"
|
||||
clientCookie.path = "/"
|
||||
clientCookie.expiryDate =
|
||||
Date.from(
|
||||
LocalDateTime.now().plusDays(3).atZone(ZoneId.systemDefault()).toInstant()
|
||||
)
|
||||
context.httpContext.cookieStore.addCookie(clientCookie)
|
||||
fetch(context.imageEntity.url, context.imageEntity.url, context) {
|
||||
val sfwCookie = BasicClientCookie("sfw_inter", "1")
|
||||
sfwCookie.domain = "www.imagebam.com"
|
||||
sfwCookie.path = "/"
|
||||
sfwCookie.setExpiryDate(LocalDateTime.now().plusDays(3).atZone(ZoneId.systemDefault()).toInstant())
|
||||
|
||||
context.httpContext.cookieStore.addCookie(sfwCookie)
|
||||
fetch(context.imageEntity.url, context) {
|
||||
HtmlUtils.clean(it.entity.content)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -26,7 +26,7 @@ internal class ImageVenueHost : Host("imagevenue.com", 4) {
|
||||
)
|
||||
if (XpathUtils.getAsNode(document, CONTINUE_BUTTON_XPATH) != null) {
|
||||
// Button detected. No need to actually click it, just make the call again.
|
||||
fetch(context.imageEntity.url, context.imageEntity.url, context) {
|
||||
fetch(context.imageEntity.url, context) {
|
||||
HtmlUtils.clean(it.entity.content)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -3,7 +3,12 @@ package me.vripper.host
|
||||
import me.vripper.entities.ImageEntity
|
||||
import me.vripper.exception.HostException
|
||||
import me.vripper.services.download.ImageDownloadRunnable
|
||||
import me.vripper.utilities.HtmlUtils
|
||||
import me.vripper.utilities.LoggerDelegate
|
||||
import me.vripper.utilities.XpathUtils
|
||||
import org.apache.hc.client5.http.classic.methods.HttpPost
|
||||
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity
|
||||
import org.apache.hc.core5.http.message.BasicNameValuePair
|
||||
|
||||
|
||||
internal class ImxHost : Host("imx.to", 8) {
|
||||
@@ -15,15 +20,66 @@ internal class ImxHost : Host("imx.to", 8) {
|
||||
context: ImageDownloadRunnable.Context
|
||||
): Pair<String, String> {
|
||||
log.debug("Resolving name and image url for ${context.imageEntity.url}")
|
||||
val imgTitle = String.format("IMG_%04d", context.imageEntity.index + 1)
|
||||
val imgTitle = getTitle(context).ifEmpty {
|
||||
getDefaultImageName(context.imageEntity.thumbUrl)
|
||||
}
|
||||
val imgUrl = findPattern(context.imageEntity)
|
||||
return Pair(
|
||||
imgTitle.ifEmpty { getDefaultImageName(imgUrl) }, imgUrl
|
||||
imgTitle, imgUrl
|
||||
)
|
||||
}
|
||||
|
||||
private fun findPattern(image: ImageEntity): String = image.thumbUrl
|
||||
.replace("http:", "https:")
|
||||
.replace("upload/small/", "u/i/")
|
||||
.replace("u/t/", "u/i/")
|
||||
private fun getTitle(context: ImageDownloadRunnable.Context): String {
|
||||
return try {
|
||||
val httpsUrl = context.imageEntity.url.replace("http:", "https:")
|
||||
val document = fetchDocument(httpsUrl, context)
|
||||
var value: String? = null
|
||||
log.debug("Looking for xpath expression $CONTINUE_BUTTON_XPATH in $httpsUrl")
|
||||
val contDiv = XpathUtils.getAsNode(document, CONTINUE_BUTTON_XPATH)
|
||||
?: throw HostException("$CONTINUE_BUTTON_XPATH cannot be found")
|
||||
val node = contDiv.attributes.getNamedItem("value")
|
||||
if (node != null) {
|
||||
value = node.textContent
|
||||
}
|
||||
log.debug("Click button found for $httpsUrl")
|
||||
val httpPost: HttpPost = HttpPost(httpsUrl).also {
|
||||
it.entity = UrlEncodedFormEntity(listOf(BasicNameValuePair("imgContinue", value)))
|
||||
}.also { context.requests.add(it) }
|
||||
log.debug("Requesting {}", httpPost)
|
||||
val doc = httpService.client.execute(
|
||||
httpPost, context.httpContext
|
||||
) { response ->
|
||||
log.debug("Cleaning response for {}", httpPost)
|
||||
HtmlUtils.clean(response.entity.content)
|
||||
}
|
||||
|
||||
log.debug("Looking for xpath expression $IMG_XPATH in $httpsUrl")
|
||||
val imgNode = XpathUtils.getAsNode(doc, IMG_XPATH)
|
||||
|
||||
log.debug("Resolving name for $httpsUrl")
|
||||
val imgTitle = imgNode?.attributes?.getNamedItem("alt")?.textContent?.trim() ?: ""
|
||||
return imgTitle
|
||||
} catch (_: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
private fun findPattern(image: ImageEntity): String {
|
||||
val url = image.thumbUrl
|
||||
.replace("http:", "https:")
|
||||
return if (url.startsWith("https://image.imx.to/u/t/")) {
|
||||
"https://image.imx.to/u/i/" + url.replace("https://image.imx.to/u/t/", "")
|
||||
} else if (url.startsWith("https://t.imx.to/t/")) {
|
||||
"https://image.imx.to/u/i/" + url.replace("https://t.imx.to/t/", "")
|
||||
} else if (url.startsWith("https://imx.to/upload/small/")) {
|
||||
"https://image.imx.to/u/i/" + url.replace("https://imx.to/upload/small/", "")
|
||||
} else {
|
||||
throw HostException("Cannot find pattern for url ${image.thumbUrl}")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTINUE_BUTTON_XPATH = "//*[@name='imgContinue']"
|
||||
private const val IMG_XPATH = "//img[@class='centred']"
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ internal class PimpandhostHost : Host("pimpandhost.com", 9) {
|
||||
} catch (e: Exception) {
|
||||
throw HostException(e)
|
||||
}
|
||||
val doc = fetch(newUrl, context.imageEntity.url, context) {
|
||||
val doc = fetch(newUrl, context) {
|
||||
HtmlUtils.clean(it.entity.content)
|
||||
}
|
||||
val imgNode: Node = try {
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
package me.vripper.host
|
||||
|
||||
import me.vripper.exception.HostException
|
||||
import me.vripper.exception.XpathException
|
||||
import me.vripper.services.download.ImageDownloadRunnable
|
||||
import me.vripper.utilities.LoggerDelegate
|
||||
import me.vripper.utilities.XpathUtils
|
||||
import org.w3c.dom.Node
|
||||
import java.util.*
|
||||
|
||||
internal class PostImgHost : Host("postimg.cc", 13) {
|
||||
private val log by LoggerDelegate()
|
||||
@@ -15,44 +12,22 @@ internal class PostImgHost : Host("postimg.cc", 13) {
|
||||
override fun resolve(
|
||||
context: ImageDownloadRunnable.Context
|
||||
): Pair<String, String> {
|
||||
val document = fetchDocument(context.imageEntity.url, context)
|
||||
val titleNode = try {
|
||||
log.debug(String.format("Looking for xpath expression %s in %s", TITLE_XPATH, context.imageEntity.url))
|
||||
XpathUtils.getAsNode(document, TITLE_XPATH)
|
||||
} catch (e: XpathException) {
|
||||
throw HostException(e)
|
||||
} ?: throw HostException(
|
||||
String.format(
|
||||
"Xpath '%s' cannot be found in '%s'",
|
||||
TITLE_XPATH,
|
||||
context.imageEntity.url
|
||||
)
|
||||
)
|
||||
val urlNode = try {
|
||||
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, context.imageEntity.url))
|
||||
XpathUtils.getAsNode(document, IMG_XPATH)
|
||||
} catch (e: XpathException) {
|
||||
throw HostException(e)
|
||||
} ?: throw HostException(
|
||||
val document = fetchDocument(context.imageEntity.url.replace("http:", "https:"), context)
|
||||
log.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, context.imageEntity.url))
|
||||
val node = XpathUtils.getAsNode(document, IMG_XPATH) ?: throw HostException(
|
||||
String.format(
|
||||
"Xpath '%s' cannot be found in '%s'",
|
||||
IMG_XPATH,
|
||||
context.imageEntity.url
|
||||
)
|
||||
)
|
||||
return try {
|
||||
log.debug(String.format("Resolving name and image url for %s", context.imageEntity.url))
|
||||
val imgTitle = Optional.ofNullable(titleNode)
|
||||
.map { node: Node -> node.textContent.trim { it <= ' ' } }
|
||||
.orElseGet { getDefaultImageName(context.imageEntity.url) }
|
||||
Pair(imgTitle, urlNode.attributes.getNamedItem("href").textContent.trim { it <= ' ' })
|
||||
} catch (e: Exception) {
|
||||
throw HostException("Unexpected error occurred", e)
|
||||
}
|
||||
return Pair(
|
||||
node.attributes.getNamedItem("alt").textContent.trim(),
|
||||
node.attributes.getNamedItem("src").textContent.trim()
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TITLE_XPATH = "//span[contains(@class,'imagename')]"
|
||||
private const val IMG_XPATH = "//a[@id='download']"
|
||||
private const val IMG_XPATH = "//img[contains(@class,'img-fluid')]"
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import me.vripper.data.repositories.ThreadRepository
|
||||
import me.vripper.entities.*
|
||||
import me.vripper.event.*
|
||||
import me.vripper.model.ErrorCount
|
||||
import me.vripper.utilities.LoggerDelegate
|
||||
import me.vripper.utilities.PathUtils
|
||||
import me.vripper.utilities.PathUtils.sanitize
|
||||
import me.vripper.vgapi.PostItem
|
||||
@@ -26,6 +27,8 @@ internal class DataAccessService(
|
||||
private val eventBus: EventBus,
|
||||
) {
|
||||
|
||||
private val log by LoggerDelegate()
|
||||
|
||||
private val postEntityIdCache: LoadingCache<Long, PostEntity> =
|
||||
Caffeine.newBuilder().expireAfterAccess(5, TimeUnit.MINUTES).build { id ->
|
||||
transaction { postRepository.findById(id) }
|
||||
@@ -40,9 +43,10 @@ internal class DataAccessService(
|
||||
val savedPost =
|
||||
postRepository.save(listOf(postEntity)).first()
|
||||
save(images.map { it.copy(postEntityId = savedPost.id) })
|
||||
// Publish event inside transaction for consistency
|
||||
eventBus.publishEvent(PostCreateEvent(listOf(savedPost)))
|
||||
savedPost
|
||||
}
|
||||
eventBus.publishEvent(PostCreateEvent(listOf(savedPost)))
|
||||
return savedPost
|
||||
}
|
||||
|
||||
@@ -51,27 +55,34 @@ internal class DataAccessService(
|
||||
postEntities.forEach { postEntity ->
|
||||
postEntityIdCache.put(postEntity.id, postEntity)
|
||||
}
|
||||
// Log and publish event after transaction commit but before returning
|
||||
log.debug("[{}] Publishing event: PostUpdateEvent for {} posts", System.currentTimeMillis(), postEntities.size)
|
||||
eventBus.publishEvent(PostUpdateEvent(postEntities))
|
||||
}
|
||||
|
||||
fun updatePost(postEntity: PostEntity) {
|
||||
transaction { postRepository.update(postEntity) }
|
||||
postEntityIdCache.put(postEntity.id, postEntity)
|
||||
// Log and publish event after transaction commit but before returning
|
||||
log.debug("[{}] Publishing event: PostUpdateEvent for post {}", System.currentTimeMillis(), postEntity.id)
|
||||
eventBus.publishEvent(PostUpdateEvent(listOf(postEntity)))
|
||||
}
|
||||
|
||||
fun save(threadEntity: ThreadEntity) {
|
||||
val savedThread = transaction { threadRepository.save(threadEntity) }
|
||||
log.debug("[{}] Publishing event: ThreadCreateEvent for thread {}", System.currentTimeMillis(), savedThread.id)
|
||||
eventBus.publishEvent(ThreadCreateEvent(savedThread))
|
||||
}
|
||||
|
||||
fun update(threadEntity: ThreadEntity) {
|
||||
transaction { threadRepository.update(threadEntity) }
|
||||
log.debug("[{}] Publishing event: ThreadUpdateEvent for thread {}", System.currentTimeMillis(), threadEntity.id)
|
||||
eventBus.publishEvent(ThreadUpdateEvent(threadEntity))
|
||||
}
|
||||
|
||||
fun updateImages(imageEntities: List<ImageEntity>) {
|
||||
transaction { imageRepository.update(imageEntities) }
|
||||
log.debug("[{}] Publishing event: ImageEvent for {} images", System.currentTimeMillis(), imageEntities.size)
|
||||
eventBus.publishEvent(ImageEvent(imageEntities))
|
||||
}
|
||||
|
||||
@@ -81,6 +92,7 @@ internal class DataAccessService(
|
||||
imageRepository.update(imageEntity)
|
||||
}
|
||||
}
|
||||
log.debug("[{}] Publishing event: ImageEvent for image {}", System.currentTimeMillis(), imageEntity.id)
|
||||
eventBus.publishEvent(ImageEvent(listOf(imageEntity)))
|
||||
}
|
||||
|
||||
@@ -183,12 +195,15 @@ internal class DataAccessService(
|
||||
postEntityIdCache.get(postEntityId)?.let { postEntityIdCache.invalidate(it.id) }
|
||||
postEntityIdCache.invalidate(postEntityId)
|
||||
}
|
||||
log.debug("[{}] Publishing event: PostDeleteEvent for {} posts", System.currentTimeMillis(), postEntityIds.size)
|
||||
eventBus.publishEvent(PostDeleteEvent(postEntityIds = postEntityIds))
|
||||
log.debug("[{}] Publishing event: ErrorCountEvent after post deletion", System.currentTimeMillis())
|
||||
eventBus.publishEvent(ErrorCountEvent(ErrorCount(countImagesInError())))
|
||||
}
|
||||
|
||||
fun removeThread(threadId: Long) {
|
||||
transaction { threadRepository.deleteByThreadId(threadId) }
|
||||
log.debug("[{}] Publishing event: ThreadDeleteEvent for thread {}", System.currentTimeMillis(), threadId)
|
||||
eventBus.publishEvent(ThreadDeleteEvent(threadId))
|
||||
}
|
||||
|
||||
@@ -216,11 +231,17 @@ internal class DataAccessService(
|
||||
|
||||
fun saveMetadata(metadataEntity: MetadataEntity) {
|
||||
transaction { metadataRepository.save(metadataEntity) }
|
||||
log.debug(
|
||||
"[{}] Publishing event: MetadataUpdateEvent for post {}",
|
||||
System.currentTimeMillis(),
|
||||
metadataEntity.postIdRef
|
||||
)
|
||||
eventBus.publishEvent(MetadataUpdateEvent(metadataEntity))
|
||||
}
|
||||
|
||||
fun clearQueueLinks() {
|
||||
transaction { threadRepository.deleteAll() }
|
||||
log.debug("[{}] Publishing event: ThreadClearEvent", System.currentTimeMillis())
|
||||
eventBus.publishEvent(ThreadClearEvent())
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import me.vripper.event.DownloadSpeedEvent
|
||||
import me.vripper.event.EventBus
|
||||
import me.vripper.event.QueueStateEvent
|
||||
import me.vripper.model.DownloadSpeed
|
||||
import me.vripper.utilities.LoggerDelegate
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
internal class DownloadSpeedService(
|
||||
@@ -16,6 +17,7 @@ internal class DownloadSpeedService(
|
||||
const val DOWNLOAD_POLL_RATE = 2500
|
||||
}
|
||||
|
||||
private val log by LoggerDelegate()
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val bytesCount = AtomicLong(0)
|
||||
private var job: Job? = null
|
||||
@@ -32,7 +34,13 @@ internal class DownloadSpeedService(
|
||||
while (isActive) {
|
||||
delay(DOWNLOAD_POLL_RATE.toLong())
|
||||
val newValue = bytesCount.getAndSet(0)
|
||||
eventBus.publishEvent(DownloadSpeedEvent(DownloadSpeed(((newValue * 1000) / DOWNLOAD_POLL_RATE))))
|
||||
val speed = DownloadSpeed(((newValue * 1000) / DOWNLOAD_POLL_RATE))
|
||||
log.debug(
|
||||
"[{}] Publishing event: DownloadSpeedEvent({})",
|
||||
System.currentTimeMillis(),
|
||||
speed
|
||||
)
|
||||
eventBus.publishEvent(DownloadSpeedEvent(speed))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +48,7 @@ internal class DownloadSpeedService(
|
||||
job?.cancel()
|
||||
coroutineScope.launch {
|
||||
delay(DOWNLOAD_POLL_RATE + 500L)
|
||||
log.debug("[{}] Publishing event: DownloadSpeedEvent(0)", System.currentTimeMillis())
|
||||
eventBus.publishEvent(DownloadSpeedEvent(DownloadSpeed(0L)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,11 +29,14 @@ internal class VGAuthService(
|
||||
fun authenticate(settings: Settings) {
|
||||
if (!settings.viperSettings.login) {
|
||||
log.debug("Authentication option is disabled")
|
||||
authenticated = false
|
||||
loggedUser = ""
|
||||
synchronized(this) {
|
||||
authenticated = false
|
||||
loggedUser = ""
|
||||
}
|
||||
synchronized(vgCookies) {
|
||||
vgCookies.clear()
|
||||
}
|
||||
log.debug("[{}] Publishing event: VGUserLoginEvent", System.currentTimeMillis())
|
||||
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
|
||||
return
|
||||
}
|
||||
@@ -41,11 +44,14 @@ internal class VGAuthService(
|
||||
val password = settings.viperSettings.password
|
||||
if (username.isEmpty() || password.isEmpty()) {
|
||||
log.error("Cannot authenticate with ViperGirls credentials, username or password is empty")
|
||||
authenticated = false
|
||||
loggedUser = ""
|
||||
synchronized(this) {
|
||||
authenticated = false
|
||||
loggedUser = ""
|
||||
}
|
||||
synchronized(vgCookies) {
|
||||
vgCookies.clear()
|
||||
}
|
||||
log.debug("[{}] Publishing event: VGUserLoginEvent (empty credentials)", System.currentTimeMillis())
|
||||
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
|
||||
return
|
||||
}
|
||||
@@ -82,6 +88,7 @@ internal class VGAuthService(
|
||||
"Failed to authenticate user with {}, missing vg_userid/vg_password cookie",
|
||||
settings.viperSettings.host
|
||||
)
|
||||
log.debug("[${System.currentTimeMillis()}] Publishing event: VGUserLoginEvent (missing cookies)")
|
||||
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
|
||||
return
|
||||
}
|
||||
@@ -98,15 +105,21 @@ internal class VGAuthService(
|
||||
log.error(
|
||||
"Failed to authenticate user with " + settings.viperSettings.host, e
|
||||
)
|
||||
authenticated = false
|
||||
loggedUser = ""
|
||||
synchronized(this) {
|
||||
authenticated = false
|
||||
loggedUser = ""
|
||||
}
|
||||
log.debug("[{}] Publishing event: VGUserLoginEvent (exception)", System.currentTimeMillis())
|
||||
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
|
||||
return
|
||||
}
|
||||
authenticated = true
|
||||
loggedUser = username
|
||||
synchronized(this) {
|
||||
authenticated = true
|
||||
loggedUser = username
|
||||
}
|
||||
log.debug("[{}] Publishing event: VGUserLoginEvent for user {}", System.currentTimeMillis(), username)
|
||||
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
|
||||
log.info("Successfully logged in as: $loggedUser")
|
||||
log.info("Successfully logged in as: {}", loggedUser)
|
||||
}
|
||||
|
||||
fun leaveThanks(postEntity: PostEntity) {
|
||||
|
||||
@@ -54,12 +54,20 @@ internal class DownloadService(
|
||||
}
|
||||
|
||||
fun stop(postEntityIds: List<Long> = emptyList()) {
|
||||
if (postEntityIds.isNotEmpty()) {
|
||||
stopInternal(postEntityIds)
|
||||
eventBus.publishEvent(StoppedEvent(postEntityIds))
|
||||
} else {
|
||||
stopAll()
|
||||
eventBus.publishEvent(StoppedEvent(listOf(-1)))
|
||||
downloadManagerLock.withLock {
|
||||
if (postEntityIds.isNotEmpty()) {
|
||||
stopInternal(postEntityIds)
|
||||
log.debug(
|
||||
"[{}] Publishing event: StoppedEvent for {} posts",
|
||||
System.currentTimeMillis(),
|
||||
postEntityIds.size
|
||||
)
|
||||
eventBus.publishEvent(StoppedEvent(postEntityIds))
|
||||
} else {
|
||||
stopAll()
|
||||
log.debug("[{}] Publishing event: StoppedEvent for all posts", System.currentTimeMillis())
|
||||
eventBus.publishEvent(StoppedEvent(listOf(-1)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,24 +133,20 @@ internal class DownloadService(
|
||||
}
|
||||
|
||||
private fun stopAll() {
|
||||
downloadManagerLock.withLock {
|
||||
queueManager.clearPending()
|
||||
queueManager.clearRunning()
|
||||
dataAccessService.findAllNonCompletedPostEntityIds().forEach {
|
||||
dataAccessService.stopImagesByPostEntityIdAndIsNotCompleted(it)
|
||||
dataAccessService.finishPost(it)
|
||||
}
|
||||
queueManager.clearPending()
|
||||
queueManager.clearRunning()
|
||||
dataAccessService.findAllNonCompletedPostEntityIds().forEach {
|
||||
dataAccessService.stopImagesByPostEntityIdAndIsNotCompleted(it)
|
||||
dataAccessService.finishPost(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopInternal(postEntityIds: List<Long>) {
|
||||
downloadManagerLock.withLock {
|
||||
postEntityIds.forEach {
|
||||
queueManager.clearPending(it)
|
||||
queueManager.clearRunning(it)
|
||||
dataAccessService.stopImagesByPostEntityIdAndIsNotCompleted(it)
|
||||
dataAccessService.finishPost(it)
|
||||
}
|
||||
postEntityIds.forEach {
|
||||
queueManager.clearPending(it)
|
||||
queueManager.clearRunning(it)
|
||||
dataAccessService.stopImagesByPostEntityIdAndIsNotCompleted(it)
|
||||
dataAccessService.finishPost(it)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,19 +66,21 @@ internal class QueueManager(
|
||||
}
|
||||
|
||||
fun accept(accepted: List<ImageQueueElement>) {
|
||||
pending.forEach {
|
||||
it.removeAll(accepted)
|
||||
downloadManagerLock.withLock {
|
||||
pending.forEach {
|
||||
it.removeAll(accepted)
|
||||
}
|
||||
pending.removeIf { it.isEmpty() }
|
||||
accepted.map {
|
||||
ImageDownloadRunnable(
|
||||
dataAccessService.findImageById(it.imageEntityId).orElseThrow(), settingsService.settings.copy()
|
||||
)
|
||||
}.forEach {
|
||||
launch(it)
|
||||
running.add(it)
|
||||
}
|
||||
reportQueueState()
|
||||
}
|
||||
pending.removeIf { it.isEmpty() }
|
||||
accepted.map {
|
||||
ImageDownloadRunnable(
|
||||
dataAccessService.findImageById(it.imageEntityId).orElseThrow(), settingsService.settings.copy()
|
||||
)
|
||||
}.forEach {
|
||||
launch(it)
|
||||
running.add(it)
|
||||
}
|
||||
reportQueueState()
|
||||
}
|
||||
|
||||
fun pending(): List<ImageQueueElement> {
|
||||
@@ -121,8 +123,8 @@ internal class QueueManager(
|
||||
}
|
||||
}
|
||||
}
|
||||
reportQueueState()
|
||||
}
|
||||
reportQueueState()
|
||||
}
|
||||
|
||||
fun getQueueState(): QueueState {
|
||||
@@ -146,8 +148,6 @@ internal class QueueManager(
|
||||
dataAccessService.updateImage(image)
|
||||
}.onComplete {
|
||||
afterJobFinish(runnable)
|
||||
reportQueueState()
|
||||
eventBus.publishEvent(ErrorCountEvent(ErrorCount(dataAccessService.countImagesInError())))
|
||||
log.debug(
|
||||
"Finished downloading ${runnable.context.imageEntity.url}"
|
||||
)
|
||||
@@ -161,12 +161,21 @@ internal class QueueManager(
|
||||
if (!isPending(image.postEntityId) && !isRunning(image.postEntityId) && !imageDownloadRunnable.stopped) {
|
||||
dataAccessService.finishPost(image.postEntityId, true)
|
||||
}
|
||||
reportQueueState()
|
||||
log.debug("[{}] Event published: ErrorCountEvent after job finish", System.currentTimeMillis())
|
||||
eventBus.publishEvent(ErrorCountEvent(ErrorCount(dataAccessService.countImagesInError())))
|
||||
downloadManagerCondition.signal()
|
||||
}
|
||||
}
|
||||
|
||||
private fun reportQueueState() {
|
||||
val queueState = getQueueState()
|
||||
log.debug(
|
||||
"[{}] Publishing event: QueueStateEvent(running={}, remaining={})",
|
||||
System.currentTimeMillis(),
|
||||
queueState.running,
|
||||
queueState.remaining
|
||||
)
|
||||
eventBus.publishEvent(QueueStateEvent(queueState))
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ class PostInfoView : View() {
|
||||
private val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private val imagesTableView: ImagesTableView by inject()
|
||||
private val postModel: PostModel = PostModel(
|
||||
-1, -1, "", 0.0, "", "", 0, 0, "", "", "*", "", "", "", emptyList(), emptyList(), "", 0
|
||||
-1, -1, "", 0.0, "", "", 0, 0, "", "", -1, "", "", "", emptyList(), emptyList(), "", 0
|
||||
)
|
||||
|
||||
override val root = tabpane()
|
||||
@@ -89,7 +89,7 @@ class PostInfoView : View() {
|
||||
this.total = 0
|
||||
this.hosts = ""
|
||||
this.addedOn = ""
|
||||
this.order = "*"
|
||||
this.order = -1
|
||||
this.path = ""
|
||||
this.folderName = ""
|
||||
this.progressCount = ""
|
||||
|
||||
@@ -203,7 +203,38 @@ class PostsTableView : View() {
|
||||
}
|
||||
}
|
||||
cellFactory = Callback {
|
||||
TextFieldTableCell<PostModel?, String?>().apply { alignment = Pos.CENTER_LEFT }
|
||||
object : TableCell<PostModel, Number>() {
|
||||
override fun updateItem(item: Number?, empty: Boolean) {
|
||||
super.updateItem(item, empty)
|
||||
text = when {
|
||||
empty -> null
|
||||
item == null -> null
|
||||
item.toLong() == -1L -> "*" // Display "*" for -1
|
||||
else -> item.toString() // Display the actual value
|
||||
}
|
||||
alignment = Pos.CENTER_LEFT
|
||||
}
|
||||
}
|
||||
}
|
||||
comparator = Comparator { a, b ->
|
||||
when {
|
||||
a == null && b == null -> 0
|
||||
a == null -> 1 // Nulls to bottom
|
||||
b == null -> -1
|
||||
a.toLong() == -1L && b.toLong() == -1L -> 0 // Both -1, equal
|
||||
a.toLong() == -1L -> {
|
||||
// -1 always goes to bottom
|
||||
// In ascending: return 1 (a is greater, goes last)
|
||||
// In descending: return -1 (a is smaller, still goes last in reverse)
|
||||
if (sortType == TableColumn.SortType.ASCENDING) 1 else -1
|
||||
}
|
||||
|
||||
b.toLong() == -1L -> {
|
||||
if (sortType == TableColumn.SortType.ASCENDING) -1 else 1
|
||||
}
|
||||
|
||||
else -> a.toLong().compareTo(b.toLong()) // Normal numeric comparison
|
||||
}
|
||||
}
|
||||
}
|
||||
column("Preview", PostModel::previewListProperty) {
|
||||
@@ -496,11 +527,11 @@ class PostsTableView : View() {
|
||||
val rank = queueState.rank.find { it.postEntityId == post.id }
|
||||
if (rank != null) {
|
||||
runLater {
|
||||
post.order = rank.rank.toString()
|
||||
post.order = rank.rank
|
||||
}
|
||||
} else if (post.order != "*") {
|
||||
} else if (post.order != -1L) {
|
||||
runLater {
|
||||
post.order = "*"
|
||||
post.order = -1L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ class PostController : Controller() {
|
||||
post.total,
|
||||
post.hosts.joinToString(separator = ", "),
|
||||
post.addedOn.format(dateTimeFormatter),
|
||||
"*",
|
||||
-1,
|
||||
post.getDownloadFolder(),
|
||||
post.folderName,
|
||||
progressCount(post.total, post.done, post.downloaded),
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package me.vripper.gui.model
|
||||
|
||||
import javafx.beans.property.SimpleDoubleProperty
|
||||
import javafx.beans.property.SimpleIntegerProperty
|
||||
import javafx.beans.property.SimpleListProperty
|
||||
import javafx.beans.property.SimpleStringProperty
|
||||
import javafx.beans.property.*
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
import tornadofx.getValue
|
||||
@@ -20,7 +17,7 @@ class PostModel(
|
||||
total: Int,
|
||||
hosts: String,
|
||||
addedOn: String,
|
||||
order: String,
|
||||
order: Long,
|
||||
path: String,
|
||||
folderName: String,
|
||||
progressCount: String,
|
||||
@@ -54,8 +51,8 @@ class PostModel(
|
||||
val addedOnProperty = SimpleStringProperty(addedOn)
|
||||
var addedOn: String by addedOnProperty
|
||||
|
||||
val orderProperty = SimpleStringProperty(order)
|
||||
var order: String by orderProperty
|
||||
val orderProperty = SimpleLongProperty(order)
|
||||
var order: Long by orderProperty
|
||||
|
||||
val pathProperty = SimpleStringProperty(path)
|
||||
var path: String by pathProperty
|
||||
|
||||
@@ -8,7 +8,7 @@ fun openFileDirectory(path: String) {
|
||||
if(os.contains("Windows")) {
|
||||
Shell32.INSTANCE.ShellExecuteW(null, WString("open"), WString(path), null, null, 1)
|
||||
} else if(os.contains("Linux")) {
|
||||
Runtime.getRuntime().exec("xdg-open $path")
|
||||
Runtime.getRuntime().exec(arrayOf("xdg-open", path))
|
||||
} else if(os.contains("Mac")) {
|
||||
Runtime.getRuntime().exec("open -R $path")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user