diff --git a/src/putio/client.js b/src/putio/client.js index b839335..3b3a08f 100644 --- a/src/putio/client.js +++ b/src/putio/client.js @@ -42,6 +42,8 @@ export function normalizeTransfer(transfer) { errorMessage: transfer.error_message ?? transfer.errorMessage ?? '', percentDone: Number(transfer.percent_done ?? transfer.percentDone ?? 0), completionPercent: Number(transfer.completion_percent ?? transfer.completionPercent ?? 0), + peers: Number(transfer.peers ?? 0), + availability: Number(transfer.availability ?? 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 f4d1eaa..13994c1 100644 --- a/src/state/store.js +++ b/src/state/store.js @@ -227,6 +227,9 @@ export class StateStore { download_dir TEXT NOT NULL DEFAULT '', lifecycle TEXT NOT NULL DEFAULT 'remote', putio_status TEXT NOT NULL DEFAULT 'UNKNOWN', + putio_status_message TEXT NOT NULL DEFAULT '', + putio_peers INTEGER NOT NULL DEFAULT 0, + putio_availability INTEGER NOT NULL DEFAULT 0, percent_done INTEGER NOT NULL DEFAULT 0, completion_percent INTEGER NOT NULL DEFAULT 0, total_size INTEGER NOT NULL DEFAULT 0, @@ -322,6 +325,9 @@ export class StateStore { 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('transfers', 'putio_status_message', "TEXT NOT NULL DEFAULT ''"); + this.ensureColumn('transfers', 'putio_peers', 'INTEGER NOT NULL DEFAULT 0'); + this.ensureColumn('transfers', 'putio_availability', 'INTEGER NOT NULL DEFAULT 0'); this.ensureColumn('transfer_files', 'download_speed', 'INTEGER NOT NULL DEFAULT 0'); this.migrateTransferAssociations(); this.migrateMagnetTransferHashes(); @@ -739,11 +745,12 @@ 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, + putio_status_message, putio_peers, putio_availability, percent_done, completion_percent, + total_size, downloaded_ever, uploaded_ever, download_speed, upload_speed, eta, error, error_string, created_at, updated_at ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( input.profile_id ?? null, input.putio_transfer_id ?? null, @@ -757,6 +764,9 @@ export class StateStore { input.download_dir ?? '', input.lifecycle ?? 'remote', input.putio_status ?? 'UNKNOWN', + input.putio_status_message ?? '', + input.putio_peers ?? 0, + input.putio_availability ?? 0, input.percent_done ?? 0, input.completion_percent ?? 0, input.total_size ?? input.size ?? 0, @@ -780,6 +790,9 @@ export class StateStore { source: input.source ?? remote.source, source_type: input.source_type ?? remote.source_type, putio_status: input.putio_status ?? remote.putio_status, + putio_status_message: input.putio_status_message ?? remote.putio_status_message, + putio_peers: input.putio_peers ?? remote.putio_peers, + putio_availability: input.putio_availability ?? remote.putio_availability, percent_done: input.percent_done ?? remote.percent_done, completion_percent: input.completion_percent ?? remote.completion_percent, total_size: input.total_size ?? input.size ?? remote.total_size, @@ -789,8 +802,9 @@ export class StateStore { this.db.prepare(` UPDATE transfers SET putio_transfer_id = ?, putio_file_id = ?, save_parent_id = ?, name = ?, - source = ?, source_type = ?, putio_status = ?, percent_done = ?, - completion_percent = ?, total_size = ?, uploaded_ever = ?, upload_speed = ?, + source = ?, source_type = ?, putio_status = ?, putio_status_message = ?, + putio_peers = ?, putio_availability = ?, percent_done = ?, completion_percent = ?, + total_size = ?, uploaded_ever = ?, upload_speed = ?, updated_at = ? WHERE id = ? `).run( @@ -801,6 +815,9 @@ export class StateStore { merged.source, merged.source_type, merged.putio_status, + merged.putio_status_message, + merged.putio_peers, + merged.putio_availability, merged.percent_done, merged.completion_percent, merged.total_size, @@ -873,6 +890,9 @@ export class StateStore { 'save_parent_id', 'name', 'putio_status', + 'putio_status_message', + 'putio_peers', + 'putio_availability', 'percent_done', 'completion_percent', 'uploaded_ever', @@ -926,6 +946,9 @@ export class StateStore { a.download_dir, a.lifecycle, r.putio_status, + r.putio_status_message, + r.putio_peers, + r.putio_availability, r.percent_done, r.completion_percent, COALESCE(a.total_size, r.total_size) AS total_size, diff --git a/src/transfer/service.js b/src/transfer/service.js index 86e769a..96daafd 100644 --- a/src/transfer/service.js +++ b/src/transfer/service.js @@ -79,6 +79,9 @@ function putioTransferToStoreInput(transfer, fallback = {}) { download_dir: fallback.download_dir, lifecycle, putio_status: transfer.status, + putio_status_message: transfer.statusMessage, + putio_peers: transfer.peers, + putio_availability: transfer.availability, percent_done: transfer.percentDone, completion_percent: transfer.completionPercent, total_size: transfer.size, @@ -775,6 +778,9 @@ export class TransferService { downloadAt: profile ? path.join(profile.download_at, row.category ?? '') : '', lifecycle: row.lifecycle, putioStatus: row.putio_status, + putioStatusMessage: row.putio_status_message, + putioPeers: row.putio_peers, + putioAvailability: row.putio_availability, 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 diff --git a/src/web/downloads.js b/src/web/downloads.js index 1ec99aa..a5b8ea9 100644 --- a/src/web/downloads.js +++ b/src/web/downloads.js @@ -95,6 +95,7 @@ export function createDownloadRow(download) {
${progressLine('Put.io', 0, 'putio-bar', 'putio-progress')} ${progressLine('Local', 0, 'local-bar', 'local-progress', 'local')} +
@@ -206,6 +207,10 @@ export function updateDownloadRow(row, download) { setText(startButton.querySelector('[data-role="start-label"]'), starting ? 'Starting' : 'Start'); setProgressValue(row, 'putio-bar', 'putio-progress', putioProgress); setProgressValue(row, 'local-bar', 'local-progress', localProgress); + const putioStatusMessage = putioStatusDetails(download); + const putioStatus = row.querySelector('[data-role="putio-status-message"]'); + setText(putioStatus, putioStatusMessage); + setHidden(putioStatus, !putioStatusMessage); populateFilePanel(row, download, fileItems); } @@ -751,3 +756,13 @@ export function downloadStatusText(download) { } return `${phase} ยท ${clampPercent(download.putioProgress)}%`; } + +export function putioStatusDetails(download) { + const message = String(download.putioStatusMessage ?? '').trim(); + if (message) return message; + if (download.lifecycle !== 'remote' || download.putioStatus !== 'DOWNLOADING') return ''; + + const peers = Math.max(0, Number(download.putioPeers ?? 0)); + const peerText = peers > 0 ? `${peers} peer${peers === 1 ? '' : 's'}` : 'No peers'; + return `${peerText} | availability: ${clampPercent(download.putioAvailability)}%`; +} diff --git a/src/web/styles/07-downloads.css b/src/web/styles/07-downloads.css index 99c165d..6445c43 100644 --- a/src/web/styles/07-downloads.css +++ b/src/web/styles/07-downloads.css @@ -277,6 +277,13 @@ gap: 9px; } +.putio-status-message { + margin-left: 54px; + color: var(--muted); + font-size: 11.5px; + overflow-wrap: anywhere; +} + .download-footer { display: flex; align-items: center; diff --git a/test/download-metrics.test.js b/test/download-metrics.test.js index 540c1cc..69f7bce 100644 --- a/test/download-metrics.test.js +++ b/test/download-metrics.test.js @@ -114,6 +114,31 @@ test('local download progress updates dashboard speed and ETA metrics', async () } }); +test('put.io refresh exposes the transfer status message on the dashboard', async () => { + const harness = await createHarness([{ + id: 70, + fileId: 80, + saveParentId: 42, + hash: 'putiostatushash', + name: 'Waiting Release', + status: 'DOWNLOADING', + statusMessage: 'Waiting for torrent details from the network...', + peers: 2, + availability: 11, + percentDone: 0, + }]); + try { + await harness.service.refreshRemoteTransfers(); + + const [download] = harness.service.listDownloads(); + assert.equal(download.putioStatusMessage, 'Waiting for torrent details from the network...'); + assert.equal(download.putioPeers, 2); + assert.equal(download.putioAvailability, 11); + } finally { + harness.store.close(); + } +}); + test('dashboard reports multi-file progress details', async () => { const harness = await createHarness(); try { diff --git a/test/putio-client.test.js b/test/putio-client.test.js index 14a085a..abc4ac0 100644 --- a/test/putio-client.test.js +++ b/test/putio-client.test.js @@ -124,6 +124,20 @@ test('PutioClient normalizes transfer and file endpoints', async () => { assert.equal(calls.at(-1).url, 'https://api.put.io/v2/files/delete'); }); +test('normalizeTransfer keeps Put.io status details', () => { + const transfer = normalizeTransfer({ + id: 22, + status: 'DOWNLOADING', + status_message: 'Waiting for torrent details from the network...', + peers: 2, + availability: 11, + }); + + assert.equal(transfer.statusMessage, 'Waiting for torrent details from the network...'); + assert.equal(transfer.peers, 2); + assert.equal(transfer.availability, 11); +}); + test('PutioClient handles folder creation and transfer edge cases', async () => { const missingFolder = createFetch([ { body: { files: [] } }, diff --git a/test/state-store.test.js b/test/state-store.test.js index a162302..af526f2 100644 --- a/test/state-store.test.js +++ b/test/state-store.test.js @@ -65,6 +65,36 @@ test('createOrUpdateTransfer persists put.io completion_percent across updates', } }); +test('createOrUpdateTransfer persists put.io status details across updates', () => { + const store = new StateStore(':memory:'); + try { + const created = store.createOrUpdateTransfer({ + putio_transfer_id: 12, + hash: 'statusmessagehash', + name: 'Waiting Transfer', + putio_status: 'DOWNLOADING', + putio_status_message: 'Waiting for torrent details from the network...', + putio_peers: 0, + putio_availability: 0, + }); + assert.equal(created.putio_status_message, 'Waiting for torrent details from the network...'); + + const updated = store.createOrUpdateTransfer({ + putio_transfer_id: 12, + hash: 'statusmessagehash', + putio_status_message: '', + putio_peers: 2, + putio_availability: 11, + }); + assert.equal(updated.id, created.id); + assert.equal(updated.putio_status_message, ''); + assert.equal(updated.putio_peers, 2); + assert.equal(updated.putio_availability, 11); + } finally { + store.close(); + } +}); + test('profile rows migrate local_path to downloadAt', async () => { const root = await mkdtemp(path.join(tmpdir(), 'putiorr-store-')); const dbPath = path.join(root, 'state.sqlite'); diff --git a/test/web-download-testids.test.js b/test/web-download-testids.test.js index b810f55..04b22e4 100644 --- a/test/web-download-testids.test.js +++ b/test/web-download-testids.test.js @@ -25,3 +25,11 @@ test('downloads UI exposes stable data-testid hooks for frontend tests', () => { assert.match(source, new RegExp(`data-testid=["']${testId}["']|['"]data-testid['"], ['"]${testId}['"]`)); } }); + +test('downloads UI renders Put.io status messages or peer availability below the progress bars', () => { + const downloadsJs = readFileSync(new URL('../src/web/downloads.js', import.meta.url), 'utf8'); + + assert.match(downloadsJs, /data-role="putio-status-message"/); + assert.match(downloadsJs, /\$\{peerText\} \| availability: \$\{clampPercent\(download\.putioAvailability\)\}%/); + assert.doesNotMatch(downloadsJs, /putioDownloaded|putioUploaded/); +});