mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
Add TorrentDownloadPlugin
This commit is contained in:
@@ -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