mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 16:20:04 +00:00
Add TorrentDownloadPlugin
This commit is contained in:
@@ -51,3 +51,4 @@ templates
|
|||||||
/gameyfin/src/main/frontend/**/*.js
|
/gameyfin/src/main/frontend/**/*.js
|
||||||
/gameyfin/src/main/frontend/**/*.js.map
|
/gameyfin/src/main/frontend/**/*.js.map
|
||||||
/gameyfin/src/main/bundles/
|
/gameyfin/src/main/bundles/
|
||||||
|
/torrent_dotfiles/
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.google.devtools.ksp")
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
maven { setUrl("https://jitpack.io") }
|
||||||
|
maven { setUrl("https://repository.jboss.org") }
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
ksp("care.better.pf4j:pf4j-kotlin-symbol-processing:${rootProject.extra["pf4jKspVersion"]}")
|
||||||
|
|
||||||
|
// Torrent tracker & seeder
|
||||||
|
implementation("com.github.mpetazzoni:ttorrent:ttorrent-2.0") {
|
||||||
|
exclude(group = "org.slf4j")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Torrent file builder
|
||||||
|
implementation("com.github.atomashpolskiy:bt-core:1.10") {
|
||||||
|
exclude(group = "org.slf4j")
|
||||||
|
}
|
||||||
|
}
|
||||||
+205
@@ -0,0 +1,205 @@
|
|||||||
|
package de.grimsi.gameyfinplugins.torrentdownload
|
||||||
|
|
||||||
|
import bt.torrent.maker.TorrentBuilder
|
||||||
|
import com.turn.ttorrent.client.CommunicationManager
|
||||||
|
import com.turn.ttorrent.client.SelectorFactoryImpl
|
||||||
|
import com.turn.ttorrent.client.storage.FullyPieceStorageFactory
|
||||||
|
import com.turn.ttorrent.network.FirstAvailableChannel
|
||||||
|
import com.turn.ttorrent.tracker.TrackedTorrent
|
||||||
|
import com.turn.ttorrent.tracker.Tracker
|
||||||
|
import de.grimsi.gameyfin.pluginapi.core.config.ConfigMetadata
|
||||||
|
import de.grimsi.gameyfin.pluginapi.core.config.PluginConfigMetadata
|
||||||
|
import de.grimsi.gameyfin.pluginapi.core.config.PluginConfigValidationResult
|
||||||
|
import de.grimsi.gameyfin.pluginapi.core.wrapper.ConfigurableGameyfinPlugin
|
||||||
|
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 java.net.InetAddress
|
||||||
|
import java.net.URI
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import kotlin.io.path.*
|
||||||
|
|
||||||
|
class TorrentDownloadPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wrapper) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TORRENT_FILE_DIRECTORY = Path.of("torrent_dotfiles")
|
||||||
|
private lateinit var tracker: Tracker
|
||||||
|
private lateinit var communicationManager: CommunicationManager
|
||||||
|
|
||||||
|
private lateinit var plugin: TorrentDownloadPlugin
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
plugin = this
|
||||||
|
}
|
||||||
|
|
||||||
|
override val configMetadata: PluginConfigMetadata = listOf(
|
||||||
|
ConfigMetadata(
|
||||||
|
key = "trackerPort",
|
||||||
|
label = "Tracker Port",
|
||||||
|
description = "Which port the torrent tracker should use",
|
||||||
|
type = Int::class.java,
|
||||||
|
default = 6969
|
||||||
|
),
|
||||||
|
ConfigMetadata(
|
||||||
|
key = "clientPort",
|
||||||
|
label = "Seed Client Port",
|
||||||
|
description = "Which port the seed client should use",
|
||||||
|
type = Int::class.java,
|
||||||
|
default = 6881
|
||||||
|
),
|
||||||
|
ConfigMetadata(
|
||||||
|
key = "externalHost",
|
||||||
|
label = "Hostname/IP override",
|
||||||
|
description = "Overrides the external host (e.g., if behind NAT)",
|
||||||
|
type = String::class.java,
|
||||||
|
isRequired = false
|
||||||
|
),
|
||||||
|
ConfigMetadata(
|
||||||
|
key = "trackerSsl",
|
||||||
|
label = "Use SSL for tracker",
|
||||||
|
description = "Enables use of SSL for the torrent tracker",
|
||||||
|
type = Boolean::class.java,
|
||||||
|
default = false
|
||||||
|
),
|
||||||
|
ConfigMetadata(
|
||||||
|
key = "privateMode",
|
||||||
|
label = "Create torrents with private mode enabled",
|
||||||
|
description = "Enables private mode for the torrent tracker according to BEP-27",
|
||||||
|
type = Boolean::class.java,
|
||||||
|
default = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPathApi::class)
|
||||||
|
override fun start() {
|
||||||
|
// Currently Gameyfin does not support storing plugin state
|
||||||
|
// and since we can't associate the torrent files with a game path after a restart
|
||||||
|
// we just delete the directory on startup.
|
||||||
|
if (Files.exists(TORRENT_FILE_DIRECTORY)) {
|
||||||
|
TORRENT_FILE_DIRECTORY.deleteRecursively()
|
||||||
|
}
|
||||||
|
Files.createDirectories(TORRENT_FILE_DIRECTORY)
|
||||||
|
|
||||||
|
tracker = Tracker(config("trackerPort"), getTrackerUri().toString())
|
||||||
|
tracker.setAcceptForeignTorrents(false)
|
||||||
|
tracker.start(true)
|
||||||
|
|
||||||
|
val workingExecutor = Executors.newVirtualThreadPerTaskExecutor()
|
||||||
|
val validationExecutor = Executors.newVirtualThreadPerTaskExecutor()
|
||||||
|
val clientPort = config<Int>("clientPort")
|
||||||
|
communicationManager = CommunicationManager(workingExecutor, validationExecutor)
|
||||||
|
communicationManager.start(
|
||||||
|
arrayOf(getHostname()),
|
||||||
|
15,
|
||||||
|
getTrackerUri(),
|
||||||
|
SelectorFactoryImpl(),
|
||||||
|
FirstAvailableChannel(clientPort, clientPort)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
tracker.stop()
|
||||||
|
communicationManager.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun validateConfig(config: Map<String, String?>): PluginConfigValidationResult {
|
||||||
|
val configValidationResult = super.validateConfig(config)
|
||||||
|
if (!configValidationResult.isValid()) {
|
||||||
|
return configValidationResult
|
||||||
|
}
|
||||||
|
|
||||||
|
val errors = mutableMapOf<String, String>()
|
||||||
|
|
||||||
|
val trackerPort = config["trackerPort"]?.toIntOrNull()
|
||||||
|
if (trackerPort != null && trackerPort !in 1024..49151) {
|
||||||
|
errors["trackerPort"] = "Must be a valid port number between 1024 and 49151."
|
||||||
|
}
|
||||||
|
|
||||||
|
val externalHost = config["externalHost"]
|
||||||
|
if (externalHost != null) {
|
||||||
|
try {
|
||||||
|
InetAddress.getByName(externalHost)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
errors["externalHost"] = "Must be a valid hostname or IP address."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (errors.isEmpty()) {
|
||||||
|
PluginConfigValidationResult.VALID
|
||||||
|
} else {
|
||||||
|
PluginConfigValidationResult.INVALID(errors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTrackerUri(): URI {
|
||||||
|
val protocol = if (config("trackerSsl")) "https" else "http"
|
||||||
|
val host = getHostname().getCanonicalHostName()
|
||||||
|
val port = config<Int>("trackerPort")
|
||||||
|
val path = "announce"
|
||||||
|
|
||||||
|
return URI.create("$protocol://$host:$port/$path")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getHostname(): InetAddress {
|
||||||
|
return InetAddress.getByName(
|
||||||
|
optionalConfig("externalHost") ?: InetAddress.getLocalHost().hostAddress
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Extension
|
||||||
|
class TorrentDownloadProvider : DownloadProvider {
|
||||||
|
override fun download(path: Path): Download {
|
||||||
|
val torrentFile = createTorrentFile(path)
|
||||||
|
|
||||||
|
tracker.announce(TrackedTorrent.load(torrentFile.toFile()))
|
||||||
|
communicationManager.addTorrent(
|
||||||
|
torrentFile.toString(),
|
||||||
|
getRootPath(path).toString(),
|
||||||
|
FullyPieceStorageFactory.INSTANCE
|
||||||
|
)
|
||||||
|
|
||||||
|
return FileDownload(
|
||||||
|
data = torrentFile.inputStream(),
|
||||||
|
fileExtension = "torrent",
|
||||||
|
size = torrentFile.fileSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createTorrentFile(gameFilesPath: Path): Path {
|
||||||
|
val torrentFile =
|
||||||
|
TORRENT_FILE_DIRECTORY.resolve("${gameFilesPath.nameWithoutExtension}-${gameFilesPath.hashCode()}.torrent")
|
||||||
|
|
||||||
|
if (Files.exists(torrentFile)) {
|
||||||
|
return torrentFile
|
||||||
|
}
|
||||||
|
|
||||||
|
Files.createFile(torrentFile)
|
||||||
|
Files.write(torrentFile, torrentFileContent(gameFilesPath))
|
||||||
|
return torrentFile
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun torrentFileContent(gameFilesPath: Path): ByteArray {
|
||||||
|
return TorrentBuilder()
|
||||||
|
.numHashingThreads(Runtime.getRuntime().availableProcessors() * 2)
|
||||||
|
.createdBy(plugin.javaClass.name)
|
||||||
|
.addFile(gameFilesPath)
|
||||||
|
.rootPath(getRootPath(gameFilesPath))
|
||||||
|
.announce(plugin.getTrackerUri().toString())
|
||||||
|
.privateFlag(plugin.config("privateMode"))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getRootPath(gameFilesPath: Path): Path {
|
||||||
|
return if (gameFilesPath.isDirectory()) {
|
||||||
|
gameFilesPath
|
||||||
|
} else {
|
||||||
|
gameFilesPath.parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
Plugin-Version: 1.0.0-alpha1
|
||||||
|
Plugin-Class: de.grimsi.gameyfinplugins.torrentdownload.TorrentDownloadPlugin
|
||||||
|
Plugin-Id: de.grimsi.gameyfinplugins.torrentdownload
|
||||||
|
Plugin-Name: Torrent Download
|
||||||
|
Plugin-Description: Distributes games via a built-in torrent tracker.<br>
|
||||||
|
Users need to install a torrent client to be able to download the games.
|
||||||
|
Plugin-Short-Description: Download via Torrent
|
||||||
|
Plugin-Author: grimsi, Pfuenzle
|
||||||
|
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" fill="#000000" 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="M212,96a27.84,27.84,0,0,0-10.51,2L171,59.94A28,28,0,1,0,120,44a28.65,28.65,0,0,0,.15,2.94L73.68,66.3a28,28,0,1,0-28.6,44.83l1.85,46.38a28,28,0,1,0,32.74,41.42L128,212.47a28,28,0,1,0,49.13-18.79l27.21-42.75A28,28,0,1,0,212,96ZM71.19,104.36,113.72,129,72.26,161.22a28,28,0,0,0-9.34-4.35l-1.85-46.38A28,28,0,0,0,71.19,104.36ZM149.57,72a27.8,27.8,0,0,0,8.94-2L189,108.06a27.86,27.86,0,0,0-4.18,9.22l-46.57,2.22ZM82.09,173.85,124,141.26l15.94,47.83a28.2,28.2,0,0,0-7.6,8L84,183.53A28,28,0,0,0,82.09,173.85ZM156,184l-.89,0-16.18-48.53,46.65-2.22a27.94,27.94,0,0,0,5.28,9l-27.21,42.75A28,28,0,0,0,156,184ZM126.32,61.7A28.44,28.44,0,0,0,134,68.24l-11.3,47.45L79.23,90.52A28,28,0,0,0,80,84a28.65,28.65,0,0,0-.15-2.94Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
Reference in New Issue
Block a user