From b0ea2ca26b392831ea42ac25134889912f92f37a Mon Sep 17 00:00:00 2001 From: Ioannis Chiotakakos Date: Fri, 3 Jul 2026 12:26:59 +0300 Subject: [PATCH 1/5] Add .claude/launch.json describing the dev server (pnpm dev, port 9091, web UI + Transmission RPC) --- .claude/launch.json | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .claude/launch.json diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..4b3d9a0 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,24 @@ +{ + "version": 1, + "servers": [ + { + "id": "putiorr", + "name": "putiorr dev server", + "description": "put.io Transmission RPC bridge + web UI, running under Node --watch.", + "type": "node", + "command": "pnpm dev", + "cwd": ".", + "env": { + "PUTIORR_LISTEN_HOST": "0.0.0.0", + "PUTIORR_LISTEN_PORT": "9091" + }, + "port": 9091, + "url": "http://localhost:9091/", + "endpoints": { + "webUi": "http://localhost:9091/", + "transmissionRpc": "http://localhost:9091/transmission/rpc" + }, + "readyWhen": { "portOpen": 9091 } + } + ] +} From cae955dc8d9f2cf9c2dcf256174dc4b4c800583a Mon Sep 17 00:00:00 2001 From: Ioannis Chiotakakos Date: Fri, 3 Jul 2026 12:31:58 +0300 Subject: [PATCH 2/5] Use Claude Preview launch.json format (configurations/runtimeExecutable) for putiorr dev server on 9092 --- .claude/launch.json | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/.claude/launch.json b/.claude/launch.json index 4b3d9a0..21c3b19 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -1,24 +1,15 @@ { - "version": 1, - "servers": [ + "version": "0.0.1", + "configurations": [ { - "id": "putiorr", - "name": "putiorr dev server", - "description": "put.io Transmission RPC bridge + web UI, running under Node --watch.", - "type": "node", - "command": "pnpm dev", - "cwd": ".", + "name": "putiorr", + "runtimeExecutable": "pnpm", + "runtimeArgs": ["dev"], + "port": 9091, "env": { - "PUTIORR_LISTEN_HOST": "0.0.0.0", + "PUTIORR_LISTEN_HOST": "127.0.0.1", "PUTIORR_LISTEN_PORT": "9091" - }, - "port": 9091, - "url": "http://localhost:9091/", - "endpoints": { - "webUi": "http://localhost:9091/", - "transmissionRpc": "http://localhost:9091/transmission/rpc" - }, - "readyWhen": { "portOpen": 9091 } + } } ] } From 05e4f5bbb4cff9cc0394bdb08e9ac2d25ebd772c Mon Sep 17 00:00:00 2001 From: Ioannis Chiotakakos Date: Sat, 4 Jul 2026 16:56:48 +0300 Subject: [PATCH 3/5] Fix put.io Finishing status: use percent_done for COMPLETING, drop phantom completion_percent field (#39) --- src/putio/client.js | 1 - src/state/store.js | 12 +++--------- src/transfer/service.js | 2 -- src/web/constants.js | 2 +- src/web/downloads.js | 8 +++----- test/state-store.test.js | 14 +++++++------- 6 files changed, 14 insertions(+), 25 deletions(-) diff --git a/src/putio/client.js b/src/putio/client.js index b839335..32e1368 100644 --- a/src/putio/client.js +++ b/src/putio/client.js @@ -41,7 +41,6 @@ export function normalizeTransfer(transfer) { statusMessage: transfer.status_message ?? transfer.statusMessage ?? '', errorMessage: transfer.error_message ?? transfer.errorMessage ?? '', percentDone: Number(transfer.percent_done ?? transfer.percentDone ?? 0), - completionPercent: Number(transfer.completion_percent ?? transfer.completionPercent ?? 0), size: Number(transfer.size ?? 0), downloaded: Number(transfer.downloaded ?? 0), uploaded: Number(transfer.uploaded ?? 0), diff --git a/src/state/store.js b/src/state/store.js index e0024c4..6fe2d43 100644 --- a/src/state/store.js +++ b/src/state/store.js @@ -208,7 +208,6 @@ export class StateStore { lifecycle TEXT NOT NULL DEFAULT 'remote', putio_status TEXT NOT NULL DEFAULT 'UNKNOWN', percent_done INTEGER NOT NULL DEFAULT 0, - completion_percent INTEGER NOT NULL DEFAULT 0, total_size INTEGER NOT NULL DEFAULT 0, downloaded_ever INTEGER NOT NULL DEFAULT 0, uploaded_ever INTEGER NOT NULL DEFAULT 0, @@ -252,7 +251,6 @@ export class StateStore { this.ensureColumn('profiles', 'client_port', "TEXT NOT NULL DEFAULT '9091'"); this.ensureColumn('profiles', 'client_use_ssl', 'INTEGER NOT NULL DEFAULT 0'); this.ensureColumn('transfers', 'profile_id', 'INTEGER REFERENCES profiles(id) ON DELETE SET NULL'); - this.ensureColumn('transfers', 'completion_percent', 'INTEGER NOT NULL DEFAULT 0'); this.ensureColumn('transfer_files', 'download_speed', 'INTEGER NOT NULL DEFAULT 0'); this.migrateMagnetTransferHashes(); } @@ -579,11 +577,11 @@ export class StateStore { INSERT INTO transfers ( profile_id, putio_transfer_id, putio_file_id, save_parent_id, hash, name, source, source_type, category, download_dir, lifecycle, putio_status, - percent_done, completion_percent, total_size, downloaded_ever, uploaded_ever, + percent_done, total_size, downloaded_ever, uploaded_ever, download_speed, upload_speed, eta, error, error_string, created_at, updated_at ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); const result = stmt.run( input.profile_id ?? null, @@ -599,7 +597,6 @@ export class StateStore { input.lifecycle ?? 'remote', input.putio_status ?? 'UNKNOWN', input.percent_done ?? 0, - input.completion_percent ?? 0, input.total_size ?? input.size ?? 0, input.downloaded_ever ?? 0, input.uploaded_ever ?? 0, @@ -627,7 +624,6 @@ export class StateStore { lifecycle: input.lifecycle ?? existing.lifecycle, putio_status: input.putio_status ?? existing.putio_status, percent_done: input.percent_done ?? existing.percent_done, - completion_percent: input.completion_percent ?? existing.completion_percent, total_size: input.total_size ?? input.size ?? existing.total_size, downloaded_ever: input.downloaded_ever ?? existing.downloaded_ever, uploaded_ever: input.uploaded_ever ?? existing.uploaded_ever, @@ -642,7 +638,7 @@ export class StateStore { UPDATE transfers SET profile_id = ?, putio_transfer_id = ?, putio_file_id = ?, save_parent_id = ?, name = ?, source = ?, source_type = ?, category = ?, download_dir = ?, - lifecycle = ?, putio_status = ?, percent_done = ?, completion_percent = ?, total_size = ?, + lifecycle = ?, putio_status = ?, percent_done = ?, total_size = ?, downloaded_ever = ?, uploaded_ever = ?, download_speed = ?, upload_speed = ?, eta = ?, error = ?, error_string = ?, removed_at = NULL, updated_at = ? @@ -660,7 +656,6 @@ export class StateStore { merged.lifecycle, merged.putio_status, merged.percent_done, - merged.completion_percent, merged.total_size, merged.downloaded_ever, merged.uploaded_ever, @@ -689,7 +684,6 @@ export class StateStore { 'lifecycle', 'putio_status', 'percent_done', - 'completion_percent', 'total_size', 'downloaded_ever', 'uploaded_ever', diff --git a/src/transfer/service.js b/src/transfer/service.js index 0d44e54..c1dfb31 100644 --- a/src/transfer/service.js +++ b/src/transfer/service.js @@ -65,7 +65,6 @@ function putioTransferToStoreInput(transfer, fallback = {}) { lifecycle, putio_status: transfer.status, percent_done: transfer.percentDone, - completion_percent: transfer.completionPercent, total_size: transfer.size, downloaded_ever: transfer.downloaded, uploaded_ever: transfer.uploaded, @@ -606,7 +605,6 @@ export class TransferService { lifecycle: row.lifecycle, putioStatus: row.putio_status, putioProgress: Math.max(0, Math.min(100, Number(row.percent_done ?? 0))), - putioCompletion: Math.max(0, Math.min(100, Number(row.completion_percent ?? 0))), localProgress: Number(stats.total_size ?? 0) > 0 ? Math.round((Number(stats.downloaded_size ?? 0) / Number(stats.total_size)) * 100) : 0, diff --git a/src/web/constants.js b/src/web/constants.js index a6fe527..f94cedc 100644 --- a/src/web/constants.js +++ b/src/web/constants.js @@ -54,7 +54,7 @@ export const PUTIO_PHASE_LABELS = { WAITING: 'Queued on Put.io', PREPARING_DOWNLOAD: 'Preparing on Put.io', DOWNLOADING: 'Downloading on Put.io', - COMPLETING: 'Completing on Put.io', + COMPLETING: 'Finishing on Put.io', SEEDING: 'Ready on Put.io', COMPLETED: 'Ready on Put.io', ERROR: 'Put.io error', diff --git a/src/web/downloads.js b/src/web/downloads.js index 2df4bee..b384db8 100644 --- a/src/web/downloads.js +++ b/src/web/downloads.js @@ -609,16 +609,14 @@ export function canStartDownload(download) { // While a transfer is still `remote`, the local downloader has not started yet, so the // lifecycle word alone ("remote") reads as stalled. Surface the put.io phase instead — -// in particular COMPLETING (put.io finished the torrent and is copying it into storage), -// which reports percent_done=100 but its real progress in completion_percent. +// in particular COMPLETING (put.io finished the torrent and is copying it into storage). +// put.io exposes a single `percent_done` field that tracks whichever phase `status` +// names, so it carries the finishing progress during COMPLETING too. export function downloadStatusText(download) { const combinedProgress = clampPercent(download.combinedProgress); if (download.lifecycle !== 'remote') { return `${download.lifecycle} · ${combinedProgress}%`; } const phase = PUTIO_PHASE_LABELS[download.putioStatus] ?? 'On Put.io'; - if (download.putioStatus === 'COMPLETING') { - return `${phase} · ${clampPercent(download.putioCompletion)}%`; - } return `${phase} · ${clampPercent(download.putioProgress)}%`; } diff --git a/test/state-store.test.js b/test/state-store.test.js index 085acca..d47ef3b 100644 --- a/test/state-store.test.js +++ b/test/state-store.test.js @@ -38,28 +38,28 @@ test('createOrUpdateTransfer matches later remote updates by put.io id', () => { } }); -test('createOrUpdateTransfer persists put.io completion_percent across updates', () => { +test('createOrUpdateTransfer tracks percent_done through the COMPLETING phase', () => { const store = new StateStore(':memory:'); try { + // put.io reuses percent_done for whichever phase `status` names, so during + // COMPLETING it carries the finishing progress rather than the download %. const created = store.createOrUpdateTransfer({ putio_transfer_id: 11, hash: 'completinghash', name: 'Completing Transfer', putio_status: 'COMPLETING', - percent_done: 100, - completion_percent: 67, + percent_done: 67, }); - assert.equal(created.completion_percent, 67); + assert.equal(created.percent_done, 67); const updated = store.createOrUpdateTransfer({ putio_transfer_id: 11, hash: 'completinghash', putio_status: 'COMPLETING', - percent_done: 100, - completion_percent: 82, + percent_done: 82, }); assert.equal(updated.id, created.id); - assert.equal(updated.completion_percent, 82); + assert.equal(updated.percent_done, 82); } finally { store.close(); } From 61b1bc653c88731a6f35377cca90a6d57f11cc9b Mon Sep 17 00:00:00 2001 From: Ioannis Chiotakakos Date: Sat, 4 Jul 2026 17:43:05 +0300 Subject: [PATCH 4/5] Revert "Fix put.io Finishing status: use percent_done for COMPLETING, drop phantom completion_percent field (#39)" This reverts commit 05e4f5bbb4cff9cc0394bdb08e9ac2d25ebd772c. --- src/putio/client.js | 1 + src/state/store.js | 12 +++++++++--- src/transfer/service.js | 2 ++ src/web/constants.js | 2 +- src/web/downloads.js | 8 +++++--- test/state-store.test.js | 14 +++++++------- 6 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/putio/client.js b/src/putio/client.js index 32e1368..b839335 100644 --- a/src/putio/client.js +++ b/src/putio/client.js @@ -41,6 +41,7 @@ export function normalizeTransfer(transfer) { statusMessage: transfer.status_message ?? transfer.statusMessage ?? '', errorMessage: transfer.error_message ?? transfer.errorMessage ?? '', percentDone: Number(transfer.percent_done ?? transfer.percentDone ?? 0), + completionPercent: Number(transfer.completion_percent ?? transfer.completionPercent ?? 0), size: Number(transfer.size ?? 0), downloaded: Number(transfer.downloaded ?? 0), uploaded: Number(transfer.uploaded ?? 0), diff --git a/src/state/store.js b/src/state/store.js index 6fe2d43..e0024c4 100644 --- a/src/state/store.js +++ b/src/state/store.js @@ -208,6 +208,7 @@ export class StateStore { lifecycle TEXT NOT NULL DEFAULT 'remote', putio_status TEXT NOT NULL DEFAULT 'UNKNOWN', percent_done INTEGER NOT NULL DEFAULT 0, + completion_percent INTEGER NOT NULL DEFAULT 0, total_size INTEGER NOT NULL DEFAULT 0, downloaded_ever INTEGER NOT NULL DEFAULT 0, uploaded_ever INTEGER NOT NULL DEFAULT 0, @@ -251,6 +252,7 @@ export class StateStore { this.ensureColumn('profiles', 'client_port', "TEXT NOT NULL DEFAULT '9091'"); this.ensureColumn('profiles', 'client_use_ssl', 'INTEGER NOT NULL DEFAULT 0'); this.ensureColumn('transfers', 'profile_id', 'INTEGER REFERENCES profiles(id) ON DELETE SET NULL'); + this.ensureColumn('transfers', 'completion_percent', 'INTEGER NOT NULL DEFAULT 0'); this.ensureColumn('transfer_files', 'download_speed', 'INTEGER NOT NULL DEFAULT 0'); this.migrateMagnetTransferHashes(); } @@ -577,11 +579,11 @@ export class StateStore { INSERT INTO transfers ( profile_id, putio_transfer_id, putio_file_id, save_parent_id, hash, name, source, source_type, category, download_dir, lifecycle, putio_status, - percent_done, total_size, downloaded_ever, uploaded_ever, + percent_done, completion_percent, total_size, downloaded_ever, uploaded_ever, download_speed, upload_speed, eta, error, error_string, created_at, updated_at ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); const result = stmt.run( input.profile_id ?? null, @@ -597,6 +599,7 @@ export class StateStore { input.lifecycle ?? 'remote', input.putio_status ?? 'UNKNOWN', input.percent_done ?? 0, + input.completion_percent ?? 0, input.total_size ?? input.size ?? 0, input.downloaded_ever ?? 0, input.uploaded_ever ?? 0, @@ -624,6 +627,7 @@ export class StateStore { lifecycle: input.lifecycle ?? existing.lifecycle, putio_status: input.putio_status ?? existing.putio_status, percent_done: input.percent_done ?? existing.percent_done, + completion_percent: input.completion_percent ?? existing.completion_percent, total_size: input.total_size ?? input.size ?? existing.total_size, downloaded_ever: input.downloaded_ever ?? existing.downloaded_ever, uploaded_ever: input.uploaded_ever ?? existing.uploaded_ever, @@ -638,7 +642,7 @@ export class StateStore { UPDATE transfers SET profile_id = ?, putio_transfer_id = ?, putio_file_id = ?, save_parent_id = ?, name = ?, source = ?, source_type = ?, category = ?, download_dir = ?, - lifecycle = ?, putio_status = ?, percent_done = ?, total_size = ?, + lifecycle = ?, putio_status = ?, percent_done = ?, completion_percent = ?, total_size = ?, downloaded_ever = ?, uploaded_ever = ?, download_speed = ?, upload_speed = ?, eta = ?, error = ?, error_string = ?, removed_at = NULL, updated_at = ? @@ -656,6 +660,7 @@ export class StateStore { merged.lifecycle, merged.putio_status, merged.percent_done, + merged.completion_percent, merged.total_size, merged.downloaded_ever, merged.uploaded_ever, @@ -684,6 +689,7 @@ export class StateStore { 'lifecycle', 'putio_status', 'percent_done', + 'completion_percent', 'total_size', 'downloaded_ever', 'uploaded_ever', diff --git a/src/transfer/service.js b/src/transfer/service.js index c1dfb31..0d44e54 100644 --- a/src/transfer/service.js +++ b/src/transfer/service.js @@ -65,6 +65,7 @@ function putioTransferToStoreInput(transfer, fallback = {}) { lifecycle, putio_status: transfer.status, percent_done: transfer.percentDone, + completion_percent: transfer.completionPercent, total_size: transfer.size, downloaded_ever: transfer.downloaded, uploaded_ever: transfer.uploaded, @@ -605,6 +606,7 @@ export class TransferService { lifecycle: row.lifecycle, putioStatus: row.putio_status, putioProgress: Math.max(0, Math.min(100, Number(row.percent_done ?? 0))), + putioCompletion: Math.max(0, Math.min(100, Number(row.completion_percent ?? 0))), localProgress: Number(stats.total_size ?? 0) > 0 ? Math.round((Number(stats.downloaded_size ?? 0) / Number(stats.total_size)) * 100) : 0, diff --git a/src/web/constants.js b/src/web/constants.js index f94cedc..a6fe527 100644 --- a/src/web/constants.js +++ b/src/web/constants.js @@ -54,7 +54,7 @@ export const PUTIO_PHASE_LABELS = { WAITING: 'Queued on Put.io', PREPARING_DOWNLOAD: 'Preparing on Put.io', DOWNLOADING: 'Downloading on Put.io', - COMPLETING: 'Finishing on Put.io', + COMPLETING: 'Completing on Put.io', SEEDING: 'Ready on Put.io', COMPLETED: 'Ready on Put.io', ERROR: 'Put.io error', diff --git a/src/web/downloads.js b/src/web/downloads.js index b384db8..2df4bee 100644 --- a/src/web/downloads.js +++ b/src/web/downloads.js @@ -609,14 +609,16 @@ export function canStartDownload(download) { // While a transfer is still `remote`, the local downloader has not started yet, so the // lifecycle word alone ("remote") reads as stalled. Surface the put.io phase instead — -// in particular COMPLETING (put.io finished the torrent and is copying it into storage). -// put.io exposes a single `percent_done` field that tracks whichever phase `status` -// names, so it carries the finishing progress during COMPLETING too. +// in particular COMPLETING (put.io finished the torrent and is copying it into storage), +// which reports percent_done=100 but its real progress in completion_percent. export function downloadStatusText(download) { const combinedProgress = clampPercent(download.combinedProgress); if (download.lifecycle !== 'remote') { return `${download.lifecycle} · ${combinedProgress}%`; } const phase = PUTIO_PHASE_LABELS[download.putioStatus] ?? 'On Put.io'; + if (download.putioStatus === 'COMPLETING') { + return `${phase} · ${clampPercent(download.putioCompletion)}%`; + } return `${phase} · ${clampPercent(download.putioProgress)}%`; } diff --git a/test/state-store.test.js b/test/state-store.test.js index d47ef3b..085acca 100644 --- a/test/state-store.test.js +++ b/test/state-store.test.js @@ -38,28 +38,28 @@ test('createOrUpdateTransfer matches later remote updates by put.io id', () => { } }); -test('createOrUpdateTransfer tracks percent_done through the COMPLETING phase', () => { +test('createOrUpdateTransfer persists put.io completion_percent across updates', () => { const store = new StateStore(':memory:'); try { - // put.io reuses percent_done for whichever phase `status` names, so during - // COMPLETING it carries the finishing progress rather than the download %. const created = store.createOrUpdateTransfer({ putio_transfer_id: 11, hash: 'completinghash', name: 'Completing Transfer', putio_status: 'COMPLETING', - percent_done: 67, + percent_done: 100, + completion_percent: 67, }); - assert.equal(created.percent_done, 67); + assert.equal(created.completion_percent, 67); const updated = store.createOrUpdateTransfer({ putio_transfer_id: 11, hash: 'completinghash', putio_status: 'COMPLETING', - percent_done: 82, + percent_done: 100, + completion_percent: 82, }); assert.equal(updated.id, created.id); - assert.equal(updated.percent_done, 82); + assert.equal(updated.completion_percent, 82); } finally { store.close(); } From 5ac24b9f4f192d99128b5de9c7e0bf3cebacebec Mon Sep 17 00:00:00 2001 From: Ioannis Chiotakakos Date: Sat, 4 Jul 2026 17:43:51 +0300 Subject: [PATCH 5/5] Show put.io Finishing progress from completion_percent when percent_done is pinned at 100 (#39) --- src/web/downloads.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/web/downloads.js b/src/web/downloads.js index 2df4bee..cbdeb78 100644 --- a/src/web/downloads.js +++ b/src/web/downloads.js @@ -608,17 +608,21 @@ export function canStartDownload(download) { } // While a transfer is still `remote`, the local downloader has not started yet, so the -// lifecycle word alone ("remote") reads as stalled. Surface the put.io phase instead — -// in particular COMPLETING (put.io finished the torrent and is copying it into storage), -// which reports percent_done=100 but its real progress in completion_percent. +// lifecycle word alone ("remote") reads as stalled. Surface the put.io phase instead. +// put.io pins percent_done at 100 once the torrent is downloaded but keeps copying it +// into your storage — that copy's real progress lives in completion_percent, and put.io +// reports status SEEDING (not COMPLETING) throughout. So key the "Finishing" phase off +// completion_percent, not status, or a still-copying transfer reads as a stalled 100%. export function downloadStatusText(download) { const combinedProgress = clampPercent(download.combinedProgress); if (download.lifecycle !== 'remote') { return `${download.lifecycle} · ${combinedProgress}%`; } - const phase = PUTIO_PHASE_LABELS[download.putioStatus] ?? 'On Put.io'; - if (download.putioStatus === 'COMPLETING') { - return `${phase} · ${clampPercent(download.putioCompletion)}%`; + const putioProgress = clampPercent(download.putioProgress); + const completion = clampPercent(download.putioCompletion); + if (putioProgress >= 100 && completion < 100) { + return `Finishing on Put.io · ${completion}%`; } - return `${phase} · ${clampPercent(download.putioProgress)}%`; + const phase = PUTIO_PHASE_LABELS[download.putioStatus] ?? 'On Put.io'; + return `${phase} · ${putioProgress}%`; }