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.
This commit is contained in:
alexandrump
2026-09-08 00:42:16 +02:00
parent 42c70c5d1b
commit 6907657520
2 changed files with 30 additions and 2 deletions
@@ -73,6 +73,13 @@ fun Application.torrentApi(amuleClient: AmuleClient, categoryStore: CategoryStor
val response = listOf(service.getFile(hash)) val response = listOf(service.getFile(hash))
call.respond(response) 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") { get("/api/v2/torrents/properties") {
val hash = call.request.queryParameters["hash"]!! val hash = call.request.queryParameters["hash"]!!
call.application.log.debug("Received get properties request with hash: {}", hash) call.application.log.debug("Received get properties request with hash: {}", hash)
@@ -37,7 +37,7 @@ class TorrentService(
name = dl.fileName!!, name = dl.fileName!!,
size = dl.sizeFull!!, size = dl.sizeFull!!,
total_size = dl.sizeFull!!, total_size = dl.sizeFull!!,
save_path = finishedPath, save_path = if (category != null) "$finishedPath/$category" else finishedPath,
downloaded = dl.sizeDone!!, downloaded = dl.sizeDone!!,
progress = dl.sizeDone!!.toDouble() / dl.sizeFull!!.toDouble(), progress = dl.sizeDone!!.toDouble() / dl.sizeFull!!.toDouble(),
priority = dl.downPrio.toInt(), priority = dl.downPrio.toInt(),
@@ -66,7 +66,7 @@ class TorrentService(
name = dl.fileName!!, name = dl.fileName!!,
size = dl.sizeFull!!, size = dl.sizeFull!!,
total_size = dl.sizeFull!!, total_size = dl.sizeFull!!,
save_path = finishedPath, save_path = if (category != null) "$finishedPath/$category" else finishedPath,
dlspeed = 0, dlspeed = 0,
downloaded = dl.sizeFull!!, downloaded = dl.sizeFull!!,
progress = 1.0, 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<Pair<String, String>> {
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 { private fun computeEta(speed: Long, sizeFull: Long, sizeDone: Long): Int {
val remainingBytes = sizeFull - sizeDone val remainingBytes = sizeFull - sizeDone
return if (speed == 0L) 8640000 else Math.min((remainingBytes / speed).toInt(), 8640000) return if (speed == 0L) 8640000 else Math.min((remainingBytes / speed).toInt(), 8640000)