Implement plugin state (closes #603) (#622)

This commit is contained in:
Simon
2025-07-11 20:23:39 +02:00
committed by GitHub
parent a407471814
commit c4c39a8dd3
7 changed files with 92 additions and 28 deletions
+1
View File
@@ -52,3 +52,4 @@ out/
/app/src/main/frontend/**/*.js.map /app/src/main/frontend/**/*.js.map
/app/src/main/frontend/generated/ /app/src/main/frontend/generated/
/torrent_dotfiles/ /torrent_dotfiles/
*.state.json
+6
View File
@@ -1,5 +1,7 @@
import com.vanniktech.maven.publish.SonatypeHost import com.vanniktech.maven.publish.SonatypeHost
val jacksonVersion = "2.19.1"
plugins { plugins {
kotlin("jvm") kotlin("jvm")
`java-library` `java-library`
@@ -11,6 +13,10 @@ group = "org.gameyfin"
dependencies { dependencies {
// PF4J (shared) // PF4J (shared)
api("org.pf4j:pf4j:${rootProject.extra["pf4jVersion"]}") api("org.pf4j:pf4j:${rootProject.extra["pf4jVersion"]}")
// JSON serialization
compileOnly("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
} }
mavenPublishing { mavenPublishing {
@@ -1,7 +1,14 @@
package org.gameyfin.pluginapi.core.wrapper package org.gameyfin.pluginapi.core.wrapper
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import org.pf4j.Plugin import org.pf4j.Plugin
import org.pf4j.PluginWrapper import org.pf4j.PluginWrapper
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.createFile
import kotlin.io.path.exists
import kotlin.io.path.fileSize
/** /**
* Abstract base class for all Gameyfin plugins. * Abstract base class for all Gameyfin plugins.
@@ -25,20 +32,17 @@ abstract class GameyfinPlugin(wrapper: PluginWrapper) : Plugin(wrapper) {
* Supported logo file formats. * Supported logo file formats.
*/ */
val SUPPORTED_LOGO_FORMATS: List<String> = listOf("png", "jpg", "jpeg", "gif", "svg", "webp") val SUPPORTED_LOGO_FORMATS: List<String> = listOf("png", "jpg", "jpeg", "gif", "svg", "webp")
/**
* Reference to the current plugin instance.
*/
lateinit var plugin: GameyfinPlugin
private set
} }
/** /**
* Initializes the plugin and sets the static plugin reference. * State file for the plugin, used to persist plugin-specific state.
*/ */
init { val stateFile: Path = Path.of("${wrapper.pluginId}.state.json")
plugin = this
} /**
* JSON serializer for serializing and deserializing plugin state.
*/
val objectMapper: ObjectMapper = ObjectMapper().registerModule(KotlinModule.Builder().build())
/** /**
* Checks if the plugin contains a logo file in any supported format. * Checks if the plugin contains a logo file in any supported format.
@@ -73,4 +77,18 @@ abstract class GameyfinPlugin(wrapper: PluginWrapper) : Plugin(wrapper) {
return null return null
} }
inline fun <reified T> loadState(): T? {
if (!stateFile.exists() || stateFile.fileSize() == 0L) return null
return Files.newBufferedReader(stateFile).use {
objectMapper.readValue(it.readText(), T::class.java)
}
}
inline fun <reified T> saveState(state: T) {
if (!stateFile.exists()) stateFile.createFile()
Files.newBufferedWriter(stateFile).use {
it.write(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(state))
}
}
} }
+1
View File
@@ -21,6 +21,7 @@ subprojects {
apply(plugin = "org.jetbrains.kotlin.jvm") apply(plugin = "org.jetbrains.kotlin.jvm")
dependencies { dependencies {
compileOnly(kotlin("stdlib"))
compileOnly(project(":plugin-api")) compileOnly(project(":plugin-api"))
} }
@@ -33,6 +33,8 @@ class TorrentDownloadPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin
private lateinit var communicationManager: CommunicationManager private lateinit var communicationManager: CommunicationManager
private lateinit var plugin: TorrentDownloadPlugin private lateinit var plugin: TorrentDownloadPlugin
private lateinit var state: TorrentDownloadPluginState
} }
init { init {
@@ -70,14 +72,8 @@ class TorrentDownloadPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin
) )
) )
@OptIn(ExperimentalPathApi::class)
override fun start() { 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) Files.createDirectories(TORRENT_FILE_DIRECTORY)
tracker = Tracker(config("trackerPort"), getTrackerUri().toString()) tracker = Tracker(config("trackerPort"), getTrackerUri().toString())
@@ -95,6 +91,27 @@ class TorrentDownloadPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin
SelectorFactoryImpl(), SelectorFactoryImpl(),
FirstAvailableChannel(clientPort, clientPort) FirstAvailableChannel(clientPort, clientPort)
) )
state = loadState<TorrentDownloadPluginState>() ?: TorrentDownloadPluginState()
state.torrentFilesMetadata.forEach {
// Check if the torrent and game files exist and
// that the game files have not been modified since the torrent file was created
if (Files.exists(it.torrentFile) && Files.exists(it.gameFile) &&
it.gameFile.getLastModifiedTime().toInstant().isBefore(it.torrentFile.getLastModifiedTime().toInstant())
) {
tracker.announce(TrackedTorrent.load(it.torrentFile.toFile()))
communicationManager.addTorrent(
it.torrentFile.toString(),
getRootPath(it.gameFile).toString(),
FullyPieceStorageFactory.INSTANCE
)
} else {
state.torrentFilesMetadata.remove(it)
}
}
saveState(state)
} }
override fun stop() { override fun stop() {
@@ -146,6 +163,14 @@ class TorrentDownloadPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin
) )
} }
private fun getRootPath(gameFilesPath: Path): Path {
return if (gameFilesPath.isDirectory()) {
gameFilesPath
} else {
gameFilesPath.parent
}
}
@Extension(ordinal = 2) @Extension(ordinal = 2)
class TorrentDownloadProvider : DownloadProvider { class TorrentDownloadProvider : DownloadProvider {
private val log = LoggerFactory.getLogger(TorrentDownloadProvider::class.java) private val log = LoggerFactory.getLogger(TorrentDownloadProvider::class.java)
@@ -180,10 +205,19 @@ class TorrentDownloadPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin
tracker.announce(TrackedTorrent.load(torrentFile.toFile())) tracker.announce(TrackedTorrent.load(torrentFile.toFile()))
communicationManager.addTorrent( communicationManager.addTorrent(
torrentFile.toString(), torrentFile.toString(),
getRootPath(gameFilesPath).toString(), plugin.getRootPath(gameFilesPath).toString(),
FullyPieceStorageFactory.INSTANCE FullyPieceStorageFactory.INSTANCE
) )
state.torrentFilesMetadata.add(
TorrentFileMetadata(
torrentFile = torrentFile,
gameFile = gameFilesPath
)
)
plugin.saveState(state)
return torrentFile return torrentFile
} }
@@ -192,18 +226,10 @@ class TorrentDownloadPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin
.numHashingThreads(Runtime.getRuntime().availableProcessors() * 2) .numHashingThreads(Runtime.getRuntime().availableProcessors() * 2)
.createdBy(plugin.javaClass.name) .createdBy(plugin.javaClass.name)
.addFile(gameFilesPath) .addFile(gameFilesPath)
.rootPath(getRootPath(gameFilesPath)) .rootPath(plugin.getRootPath(gameFilesPath))
.announce(plugin.getTrackerUri().toString()) .announce(plugin.getTrackerUri().toString())
.privateFlag(plugin.config("privateMode")) .privateFlag(plugin.config("privateMode"))
.build() .build()
} }
private fun getRootPath(gameFilesPath: Path): Path {
return if (gameFilesPath.isDirectory()) {
gameFilesPath
} else {
gameFilesPath.parent
}
}
} }
} }
@@ -0,0 +1,12 @@
package org.gameyfin.plugins.download.torrent
import java.nio.file.Path
data class TorrentDownloadPluginState(
val torrentFilesMetadata: MutableList<TorrentFileMetadata> = mutableListOf()
)
data class TorrentFileMetadata(
val torrentFile: Path,
val gameFile: Path
)
@@ -1,4 +1,4 @@
Plugin-Version: 1.0.0.beta1 Plugin-Version: 1.0.0.beta2
Plugin-Class: org.gameyfin.plugins.download.torrent.TorrentDownloadPlugin Plugin-Class: org.gameyfin.plugins.download.torrent.TorrentDownloadPlugin
Plugin-Id: org.gameyfin.plugins.download.torrent Plugin-Id: org.gameyfin.plugins.download.torrent
Plugin-Name: Torrent Download Plugin-Name: Torrent Download