Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/putio/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
31 changes: 27 additions & 4 deletions src/state/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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(
Expand All @@ -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,
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions src/transfer/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions src/web/downloads.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export function createDownloadRow(download) {
<div class="progress-group">
${progressLine('Put.io', 0, 'putio-bar', 'putio-progress')}
${progressLine('Local', 0, 'local-bar', 'local-progress', 'local')}
<div class="putio-status-message" data-role="putio-status-message" hidden></div>
</div>
<div class="download-actions">
<div class="download-speed-metric">
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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)}%`;
}
7 changes: 7 additions & 0 deletions src/web/styles/07-downloads.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
25 changes: 25 additions & 0 deletions test/download-metrics.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
14 changes: 14 additions & 0 deletions test/putio-client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [] } },
Expand Down
30 changes: 30 additions & 0 deletions test/state-store.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
8 changes: 8 additions & 0 deletions test/web-download-testids.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/);
});
Loading