mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
Implement direct download via plugin
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
plugins {
|
||||
id("com.google.devtools.ksp")
|
||||
kotlin("plugin.serialization")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
ksp("care.better.pf4j:pf4j-kotlin-symbol-processing:${rootProject.extra["pf4jKspVersion"]}")
|
||||
|
||||
implementation("commons-io:commons-io:2.19.0")
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
package de.grimsi.gameyfin.plugins.directdownload
|
||||
|
||||
import de.grimsi.gameyfin.pluginapi.core.Configurable
|
||||
import de.grimsi.gameyfin.pluginapi.core.GameyfinPlugin
|
||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement
|
||||
import de.grimsi.gameyfin.pluginapi.download.Download
|
||||
import de.grimsi.gameyfin.pluginapi.download.DownloadProvider
|
||||
import de.grimsi.gameyfin.pluginapi.download.FileDownload
|
||||
import org.pf4j.Extension
|
||||
import org.pf4j.PluginWrapper
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.InputStream
|
||||
import java.io.PipedInputStream
|
||||
import java.io.PipedOutputStream
|
||||
import java.nio.file.*
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
import java.util.concurrent.BlockingQueue
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.zip.Deflater
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.extension
|
||||
import kotlin.io.path.fileSize
|
||||
import kotlin.io.path.isDirectory
|
||||
|
||||
class DirectDownloadPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper), Configurable {
|
||||
val log: Logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
enum class CompressionMode {
|
||||
NONE,
|
||||
FAST,
|
||||
BEST;
|
||||
|
||||
companion object {
|
||||
fun toDeflaterLevel(mode: CompressionMode): Int {
|
||||
return when (mode) {
|
||||
NONE -> Deflater.NO_COMPRESSION
|
||||
FAST -> Deflater.BEST_SPEED
|
||||
BEST -> Deflater.BEST_COMPRESSION
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val configMetadata: List<PluginConfigElement> = listOf(
|
||||
PluginConfigElement(
|
||||
key = "compressionMode",
|
||||
name = "Compression mode for generated ZIP files (\"none\", \"fast\", \"best\")",
|
||||
description = "Higher compression uses more CPU but saves bandwidth",
|
||||
)
|
||||
)
|
||||
|
||||
override var config: Map<String, String?> = emptyMap()
|
||||
|
||||
override fun validateConfig(config: Map<String, String?>): Boolean {
|
||||
return config["compressionMode"]?.let {
|
||||
try {
|
||||
CompressionMode.valueOf(it.uppercase())
|
||||
true
|
||||
} catch (_: IllegalArgumentException) {
|
||||
log.error("Invalid compression mode: $it")
|
||||
false
|
||||
}
|
||||
} ?: true
|
||||
}
|
||||
|
||||
@Extension
|
||||
class DirectDownloadProvider : DownloadProvider {
|
||||
companion object {
|
||||
private val END_OF_QUEUE = Pair<ZipEntry, Path>(ZipEntry("__END__"), Paths.get(""))
|
||||
}
|
||||
|
||||
override fun getDownloadSources(path: Path): Download {
|
||||
if (!path.exists()) throw IllegalArgumentException("Path $path does not exist")
|
||||
|
||||
return FileDownload(
|
||||
data = readContentAsSingleFile(path),
|
||||
fileExtension = if (path.isDirectory()) "zip" else path.extension,
|
||||
size = path.fileSize()
|
||||
)
|
||||
}
|
||||
|
||||
fun readContentAsSingleFile(path: Path): InputStream {
|
||||
if (path.isDirectory()) return zipFilesInPath(path)
|
||||
return Files.newInputStream(path, StandardOpenOption.READ)
|
||||
}
|
||||
|
||||
private fun zipFilesInPath(path: Path): InputStream {
|
||||
val pipedIn = PipedInputStream(64 * 1024)
|
||||
val pipedOut = PipedOutputStream(pipedIn)
|
||||
val queue: BlockingQueue<Pair<ZipEntry, Path>?> = LinkedBlockingQueue()
|
||||
|
||||
// Producer: walks the file tree and enqueues files
|
||||
Thread.startVirtualThread {
|
||||
try {
|
||||
Files.walkFileTree(path, object : SimpleFileVisitor<Path>() {
|
||||
override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
|
||||
val entry = ZipEntry(path.relativize(file).toString())
|
||||
queue.put(entry to file)
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
})
|
||||
} finally {
|
||||
queue.put(END_OF_QUEUE) // signal end
|
||||
}
|
||||
}
|
||||
|
||||
// Consumer: zips files in parallel, but writes entries in order
|
||||
Thread {
|
||||
ZipOutputStream(pipedOut).use { zos ->
|
||||
zos.setLevel(Deflater.NO_COMPRESSION)
|
||||
while (true) {
|
||||
val item = queue.take()
|
||||
if (item === END_OF_QUEUE || item == null) break
|
||||
val (entry, file) = item
|
||||
zos.putNextEntry(entry)
|
||||
Files.newInputStream(file, StandardOpenOption.READ).use { input ->
|
||||
input.copyTo(zos, 128 * 1024)
|
||||
}
|
||||
zos.closeEntry()
|
||||
}
|
||||
}
|
||||
pipedOut.close()
|
||||
}.start()
|
||||
|
||||
return pipedIn
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
Plugin-Version: 1.0.0-alpha1
|
||||
Plugin-Class: de.grimsi.gameyfin.plugins.directdownload.DirectDownloadPlugin
|
||||
Plugin-Id: de.grimsi.gameyfin.directdownload
|
||||
Plugin-Name: Direct Download
|
||||
Plugin-Description: Downloads games directly in the browser.<br>
|
||||
If the game is contained in a folder, it will pack the folder into a zip file on the fly.
|
||||
Plugin-Short-Description: Download games directly in the browser
|
||||
Plugin-Author: grimsi
|
||||
Plugin-License: MIT
|
||||
Plugin-Url: https://github.com/gameyfin
|
||||
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2332c8;stop-opacity:1"/>
|
||||
<stop offset="100%" style="stop-color:#6441a5;stop-opacity:1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="url(#grad1)"
|
||||
d="M74.34,85.66A8,8,0,0,1,85.66,74.34L120,108.69V24a8,8,0,0,1,16,0v84.69l34.34-34.35a8,8,0,0,1,11.32,11.32l-48,48a8,8,0,0,1-11.32,0ZM240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16H84.4a4,4,0,0,1,2.83,1.17L111,145A24,24,0,0,0,145,145l23.8-23.8A4,4,0,0,1,171.6,120H224A16,16,0,0,1,240,136Zm-40,32a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 764 B |
Reference in New Issue
Block a user