From 690765752029273778b46a2f85f7282e69259a5f Mon Sep 17 00:00:00 2001 From: alexandrump Date: Tue, 8 Sep 2026 00:42:16 +0200 Subject: [PATCH] Add category-aware save_path and completed-by-category endpoint - save_path in torrent info now includes the query category, matching what Radarr/Sonarr each request for their own category, so imports resolve to per-app subfolders via Remote Path Mapping. - New /internal/completed-by-category plain-text endpoint exposing the real stored hash->category mapping (not the request filter echo), for the amule incoming-folder mover to sort completed downloads by category without needing a JSON parser. --- .../main/kotlin/amarr/torrent/TorrentApi.kt | 7 ++++++ .../kotlin/amarr/torrent/TorrentService.kt | 25 +++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/amarr/torrent/TorrentApi.kt b/app/src/main/kotlin/amarr/torrent/TorrentApi.kt index 6db3d72..7cc7649 100644 --- a/app/src/main/kotlin/amarr/torrent/TorrentApi.kt +++ b/app/src/main/kotlin/amarr/torrent/TorrentApi.kt @@ -73,6 +73,13 @@ fun Application.torrentApi(amuleClient: AmuleClient, categoryStore: CategoryStor val response = listOf(service.getFile(hash)) call.respond(response) } + get("/internal/completed-by-category") { + // Plain "category\tfileName" lines (no JSON) so the incoming-folder mover, + // running in a separate container with no JSON tooling, can parse it with + // plain shell text processing. + val lines = service.getCompletedByCategory().joinToString("\n") { (category, name) -> "$category\t$name" } + call.respondText(lines) + } get("/api/v2/torrents/properties") { val hash = call.request.queryParameters["hash"]!! call.application.log.debug("Received get properties request with hash: {}", hash) diff --git a/app/src/main/kotlin/amarr/torrent/TorrentService.kt b/app/src/main/kotlin/amarr/torrent/TorrentService.kt index 63913ea..dd673df 100644 --- a/app/src/main/kotlin/amarr/torrent/TorrentService.kt +++ b/app/src/main/kotlin/amarr/torrent/TorrentService.kt @@ -37,7 +37,7 @@ class TorrentService( name = dl.fileName!!, size = dl.sizeFull!!, total_size = dl.sizeFull!!, - save_path = finishedPath, + save_path = if (category != null) "$finishedPath/$category" else finishedPath, downloaded = dl.sizeDone!!, progress = dl.sizeDone!!.toDouble() / dl.sizeFull!!.toDouble(), priority = dl.downPrio.toInt(), @@ -66,7 +66,7 @@ class TorrentService( name = dl.fileName!!, size = dl.sizeFull!!, total_size = dl.sizeFull!!, - save_path = finishedPath, + save_path = if (category != null) "$finishedPath/$category" else finishedPath, dlspeed = 0, downloaded = dl.sizeFull!!, progress = 1.0, @@ -79,6 +79,27 @@ class TorrentService( } } + /** + * Returns (category, fileName) pairs for fully-downloaded files that were added + * with a category. Used by the incoming-folder mover to know where to file + * each completed download, since [getTorrentInfo]'s reported category is just + * an echo of the caller's filter, not the file's real stored category. + */ + fun getCompletedByCategory(): List> { + val downloadingFiles = amuleClient.getDownloadQueue().getOrThrow() + val sharedFiles = amuleClient.getSharedFiles().getOrThrow() + val downloadingFilesHashSet = downloadingFiles.map { it.fileHashHexString }.toHashSet() + + val allFiles = sharedFiles.filterNot { downloadingFilesHashSet.contains(it.fileHashHexString) } + + return allFiles.mapNotNull { dl -> + val hash = dl.fileHashHexString ?: return@mapNotNull null + val name = dl.fileName ?: return@mapNotNull null + val category = categoryStore.getCategory(hash) ?: return@mapNotNull null + category to name + } + } + private fun computeEta(speed: Long, sizeFull: Long, sizeDone: Long): Int { val remainingBytes = sizeFull - sizeDone return if (speed == 0L) 8640000 else Math.min((remainingBytes / speed).toInt(), 8640000)