diff --git a/.gitignore b/.gitignore index 4991fba..056d6b6 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,4 @@ out/ /app/src/main/frontend/**/*.js.map /app/src/main/frontend/generated/ /torrent_dotfiles/ +*.state.json diff --git a/plugin-api/build.gradle.kts b/plugin-api/build.gradle.kts index 561d209..42b0243 100644 --- a/plugin-api/build.gradle.kts +++ b/plugin-api/build.gradle.kts @@ -1,5 +1,7 @@ import com.vanniktech.maven.publish.SonatypeHost +val jacksonVersion = "2.19.1" + plugins { kotlin("jvm") `java-library` @@ -11,6 +13,10 @@ group = "org.gameyfin" dependencies { // PF4J (shared) 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 { diff --git a/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/wrapper/GameyfinPlugin.kt b/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/wrapper/GameyfinPlugin.kt index ffef149..b2c86cf 100644 --- a/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/wrapper/GameyfinPlugin.kt +++ b/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/wrapper/GameyfinPlugin.kt @@ -1,7 +1,14 @@ 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.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. @@ -25,20 +32,17 @@ abstract class GameyfinPlugin(wrapper: PluginWrapper) : Plugin(wrapper) { * Supported logo file formats. */ val SUPPORTED_LOGO_FORMATS: List = 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 { - plugin = this - } + val stateFile: Path = Path.of("${wrapper.pluginId}.state.json") + + /** + * 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. @@ -73,4 +77,18 @@ abstract class GameyfinPlugin(wrapper: PluginWrapper) : Plugin(wrapper) { return null } + + inline fun loadState(): T? { + if (!stateFile.exists() || stateFile.fileSize() == 0L) return null + return Files.newBufferedReader(stateFile).use { + objectMapper.readValue(it.readText(), T::class.java) + } + } + + inline fun saveState(state: T) { + if (!stateFile.exists()) stateFile.createFile() + Files.newBufferedWriter(stateFile).use { + it.write(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(state)) + } + } } \ No newline at end of file diff --git a/plugins/build.gradle.kts b/plugins/build.gradle.kts index 6c6b866..66eb35f 100644 --- a/plugins/build.gradle.kts +++ b/plugins/build.gradle.kts @@ -21,6 +21,7 @@ subprojects { apply(plugin = "org.jetbrains.kotlin.jvm") dependencies { + compileOnly(kotlin("stdlib")) compileOnly(project(":plugin-api")) } diff --git a/plugins/torrentdownload/src/main/kotlin/org/gameyfin/plugins/download/torrent/TorrentDownloadPlugin.kt b/plugins/torrentdownload/src/main/kotlin/org/gameyfin/plugins/download/torrent/TorrentDownloadPlugin.kt index 6428a38..d3cc38d 100644 --- a/plugins/torrentdownload/src/main/kotlin/org/gameyfin/plugins/download/torrent/TorrentDownloadPlugin.kt +++ b/plugins/torrentdownload/src/main/kotlin/org/gameyfin/plugins/download/torrent/TorrentDownloadPlugin.kt @@ -33,6 +33,8 @@ class TorrentDownloadPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin private lateinit var communicationManager: CommunicationManager private lateinit var plugin: TorrentDownloadPlugin + + private lateinit var state: TorrentDownloadPluginState } init { @@ -70,14 +72,8 @@ class TorrentDownloadPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin ) ) - @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()) @@ -95,6 +91,27 @@ class TorrentDownloadPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin SelectorFactoryImpl(), FirstAvailableChannel(clientPort, clientPort) ) + + state = loadState() ?: 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() { @@ -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) class TorrentDownloadProvider : DownloadProvider { private val log = LoggerFactory.getLogger(TorrentDownloadProvider::class.java) @@ -180,10 +205,19 @@ class TorrentDownloadPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin tracker.announce(TrackedTorrent.load(torrentFile.toFile())) communicationManager.addTorrent( torrentFile.toString(), - getRootPath(gameFilesPath).toString(), + plugin.getRootPath(gameFilesPath).toString(), FullyPieceStorageFactory.INSTANCE ) + state.torrentFilesMetadata.add( + TorrentFileMetadata( + torrentFile = torrentFile, + gameFile = gameFilesPath + ) + ) + + plugin.saveState(state) + return torrentFile } @@ -192,18 +226,10 @@ class TorrentDownloadPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin .numHashingThreads(Runtime.getRuntime().availableProcessors() * 2) .createdBy(plugin.javaClass.name) .addFile(gameFilesPath) - .rootPath(getRootPath(gameFilesPath)) + .rootPath(plugin.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 - } - } } } \ No newline at end of file diff --git a/plugins/torrentdownload/src/main/kotlin/org/gameyfin/plugins/download/torrent/TorrentDownloadPluginState.kt b/plugins/torrentdownload/src/main/kotlin/org/gameyfin/plugins/download/torrent/TorrentDownloadPluginState.kt new file mode 100644 index 0000000..2db9e65 --- /dev/null +++ b/plugins/torrentdownload/src/main/kotlin/org/gameyfin/plugins/download/torrent/TorrentDownloadPluginState.kt @@ -0,0 +1,12 @@ +package org.gameyfin.plugins.download.torrent + +import java.nio.file.Path + +data class TorrentDownloadPluginState( + val torrentFilesMetadata: MutableList = mutableListOf() +) + +data class TorrentFileMetadata( + val torrentFile: Path, + val gameFile: Path +) diff --git a/plugins/torrentdownload/src/main/resources/MANIFEST.MF b/plugins/torrentdownload/src/main/resources/MANIFEST.MF index f0e9015..53d7c09 100644 --- a/plugins/torrentdownload/src/main/resources/MANIFEST.MF +++ b/plugins/torrentdownload/src/main/resources/MANIFEST.MF @@ -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-Id: org.gameyfin.plugins.download.torrent Plugin-Name: Torrent Download