diff --git a/crates/librqbit/webui/app.js b/crates/librqbit/webui/app.js index 73f141e..b9757b0 100644 --- a/crates/librqbit/webui/app.js +++ b/crates/librqbit/webui/app.js @@ -1,6 +1,5 @@ // Define API URL and base path const apiUrl = window.origin == 'null' ? 'http://localhost:3030' : ''; - // Helper function for making API requests (async/await) async function makeRequest(method, path, data) { const url = apiUrl + path; @@ -45,13 +44,6 @@ async function makeRequest(method, path, data) { return Promise.reject(`Error: ${error.message}`); } } -// Helper function to display the API response -function displayResult(result) { - const outputDiv = document.getElementById('output'); - if (outputDiv) { - outputDiv.innerHTML = `
${result}
`; - } -} // Function to get detailed information about a torrent (async/await) async function getTorrentDetails(index) { return makeRequest('GET', `/torrents/${index}`); @@ -65,50 +57,57 @@ async function displayTorrents() { try { const response = await makeRequest('GET', '/torrents'); const torrents = response.torrents; - // Create a container for all torrents using Bootstrap classes - const torrentsContainer = document.createElement('div'); - torrentsContainer.classList.add('d-flex', 'flex-column', 'torrents-container'); - for (const torrent of torrents) { - const detailsResponse = await getTorrentDetails(torrent.id); - const statsResponse = await getTorrentStats(torrent.id); - const totalBytes = detailsResponse.files.reduce((total, file) => total + file.length, 0); + // Get the torrents container + const torrentsContainer = document.getElementById('output'); + // Array to hold promises for torrent details and stats + const promises = torrents.map(async (torrent) => { + const detailsPromise = getTorrentDetails(torrent.id); + const statsPromise = getTorrentStats(torrent.id); + const [detailsResponse, statsResponse] = await Promise.all([detailsPromise, statsPromise]); + const totalBytes = statsResponse.snapshot.total_bytes; const downloadedBytes = statsResponse.snapshot.have_bytes; // Calculate download percentage const downloadPercentage = (downloadedBytes / totalBytes) * 100; - // Create a container for each torrent using Bootstrap classes - const torrentContainer = document.createElement('div'); - torrentContainer.classList.add('torrent-container', 'd-flex', 'flex-row', 'p-3', 'bg-light', 'rounded', 'mb-3'); // Display basic information about the torrent const largestFileName = getLargestFileName(detailsResponse); const downloadSpeed = statsResponse.download_speed.human_readable; const eta = getCompletionETA(statsResponse); - // Create and append divs for concise information as columns - const nameColumn = createColumn('Name', largestFileName); - const sizeColumn = createColumn('Size', `${formatBytesToGB(totalBytes)} GB`); - const progressColumn = createColumnWithProgressBar('Progress', downloadPercentage); - const downloadSpeedColumn = createColumn('Download Speed', downloadSpeed); - const etaColumn = createColumn('ETA', eta); - // Append columns to the torrent container - torrentContainer.appendChild(nameColumn); - torrentContainer.appendChild(sizeColumn); - torrentContainer.appendChild(progressColumn); - torrentContainer.appendChild(downloadSpeedColumn); - torrentContainer.appendChild(etaColumn); - // Append the torrent container to the torrentsContainer - torrentsContainer.appendChild(torrentContainer); - } - // Replace the old content with the new one - const outputDiv = document.getElementById('output'); - outputDiv.replaceChildren(torrentsContainer); + const peers = `${statsResponse.snapshot.peer_stats.live} / ${statsResponse.snapshot.peer_stats.seen}`; + // Check if the torrent row already exists + let torrentRow = document.getElementById(`torrent-${torrent.id}`); + if (!torrentRow) { + console.log("not found!"); + // If the torrent row doesn't exist, create a new one + torrentRow = document.createElement('div'); + torrentRow.id = `torrent-${torrent.id}`; + torrentRow.classList.add('torrent-row', 'd-flex', 'flex-row', 'p-3', 'bg-light', 'rounded', 'mb-3'); + // Append the new torrent row to the torrentsContainer + torrentsContainer.appendChild(torrentRow); + } + // Create a detached element to replace torrentRow.innerHTML atomically + const newTorrentRow = document.createElement('div'); + newTorrentRow.classList.add('torrent-row', 'd-flex', 'flex-row', 'p-3', 'bg-light', 'rounded', 'mb-3'); + newTorrentRow.appendChild(createColumn('Name', largestFileName, 'name-column')); + newTorrentRow.appendChild(createColumn('Size', `${formatBytesToGB(totalBytes)} GB`, 'size-column')); + newTorrentRow.appendChild(createColumnWithProgressBar('Progress', downloadPercentage)); + newTorrentRow.appendChild(createColumn('Download Speed', downloadSpeed, 'download-speed-column')); + newTorrentRow.appendChild(createColumn('ETA', eta, 'eta-column')); + newTorrentRow.appendChild(createColumn('Peers', peers, 'peers-column')); + // Replace torrentRow.innerHTML with the new content + torrentRow.replaceChildren(newTorrentRow); + }); + // Wait for all promises to resolve + await Promise.all(promises); } catch (error) { console.error(error); + // Handle errors as needed } } // Function to create a column div -function createColumn(label, value) { +function createColumn(label, value, columnClass) { const columnDiv = document.createElement('div'); - columnDiv.classList.add('me-3', 'p-2'); + columnDiv.classList.add(columnClass, 'me-3', 'p-2'); columnDiv.innerHTML = `

${label}

${value}

`; return columnDiv; } diff --git a/crates/librqbit/webui/app.ts b/crates/librqbit/webui/app.ts index 59b04ab..e66fad4 100644 --- a/crates/librqbit/webui/app.ts +++ b/crates/librqbit/webui/app.ts @@ -20,17 +20,41 @@ interface TorrentDetails { interface TorrentStats { snapshot: { have_bytes: number; + downloaded_and_checked_bytes: number; + downloaded_and_checked_pieces: number; + fetched_bytes: number; + uploaded_bytes: number; + initially_needed_bytes: number; remaining_bytes: number; + total_bytes: number; + total_piece_download_ms: number; + peer_stats: { + queued: number; + connecting: number; + live: number; + seen: number; + dead: number; + not_needed: number; + }; + }; + average_piece_download_time: { + secs: number; + nanos: number; }; download_speed: { mbps: number; human_readable: string; }; - time_remaining?: { + all_time_download_speed: { + mbps: number; human_readable: string; }; + time_remaining: { + human_readable: string; + } | null; } + // Interface for the API error response interface ApiError { status: number; @@ -82,14 +106,6 @@ async function makeRequest(method: string, path: string, data?: any): Promise${result}`; - } -} - // Function to get detailed information about a torrent (async/await) async function getTorrentDetails(index: number): Promise { return makeRequest('GET', `/torrents/${index}`); @@ -106,13 +122,8 @@ async function displayTorrents() { const response = await makeRequest('GET', '/torrents'); const torrents = response.torrents; - // Create a container for all torrents using Bootstrap classes - const torrentsContainer = document.createElement('div'); - torrentsContainer.id = 'torrents-container'; - torrentsContainer.classList.add('d-flex', 'flex-column', 'torrents-container'); - - // Map to store existing elements - const existingElementsMap = new Map(); + // Get the torrents container + const torrentsContainer = document.getElementById('output'); // Array to hold promises for torrent details and stats const promises = torrents.map(async (torrent) => { @@ -120,64 +131,63 @@ async function displayTorrents() { const statsPromise = getTorrentStats(torrent.id); const [detailsResponse, statsResponse] = await Promise.all([detailsPromise, statsPromise]); - const totalBytes = detailsResponse.files.reduce((total, file) => total + file.length, 0); + const totalBytes = statsResponse.snapshot.total_bytes; const downloadedBytes = statsResponse.snapshot.have_bytes; // Calculate download percentage const downloadPercentage = (downloadedBytes / totalBytes) * 100; - // Check if the torrent container already exists - let torrentContainer = existingElementsMap.get(torrent.id); - - if (!torrentContainer) { - torrentContainer = document.createElement('div'); - torrentContainer.id = `torrent-${torrent.id}`; - torrentContainer.classList.add('torrent-container', 'd-flex', 'flex-row', 'p-3', 'bg-light', 'rounded', 'mb-3'); - existingElementsMap.set(torrent.id, torrentContainer); - } - // Display basic information about the torrent const largestFileName = getLargestFileName(detailsResponse); const downloadSpeed = statsResponse.download_speed.human_readable; const eta = getCompletionETA(statsResponse); + const peers = `${statsResponse.snapshot.peer_stats.live} / ${statsResponse.snapshot.peer_stats.seen}`; - // Update or append columns to the torrent container - torrentContainer.innerHTML = ''; // Clear existing content - torrentContainer.appendChild(createColumn('Name', largestFileName, 'name-column')); - torrentContainer.appendChild(createColumn('Size', `${formatBytesToGB(totalBytes)} GB`, 'size-column')); - torrentContainer.appendChild(createColumnWithProgressBar('Progress', downloadPercentage, 'progress-column')); - torrentContainer.appendChild(createColumn('Download Speed', downloadSpeed, 'download-speed-column')); - torrentContainer.appendChild(createColumn('ETA', eta, 'eta-column')); + // Check if the torrent row already exists + let torrentRow = document.getElementById(`torrent-${torrent.id}`); - // Append the torrent container to the torrentsContainer - torrentsContainer.appendChild(torrentContainer); + if (!torrentRow) { + console.log("not found!"); + // If the torrent row doesn't exist, create a new one + torrentRow = document.createElement('div'); + torrentRow.id = `torrent-${torrent.id}`; + torrentRow.classList.add('torrent-row', 'd-flex', 'flex-row', 'p-3', 'bg-light', 'rounded', 'mb-3'); + + // Append the new torrent row to the torrentsContainer + torrentsContainer.appendChild(torrentRow); + } + + // Create a detached element to replace torrentRow.innerHTML atomically + const newTorrentRow = document.createElement('div'); + newTorrentRow.classList.add('torrent-row', 'd-flex', 'flex-row', 'p-3', 'bg-light', 'rounded', 'mb-3'); + newTorrentRow.appendChild(createColumn('Name', largestFileName, 'name-column')); + newTorrentRow.appendChild(createColumn('Size', `${formatBytesToGB(totalBytes)} GB`, 'size-column')); + newTorrentRow.appendChild(createColumnWithProgressBar('Progress', downloadPercentage,)); + newTorrentRow.appendChild(createColumn('Download Speed', downloadSpeed, 'download-speed-column')); + newTorrentRow.appendChild(createColumn('ETA', eta, 'eta-column')); + newTorrentRow.appendChild(createColumn('Peers', peers, 'peers-column')); + + // Replace torrentRow.innerHTML with the new content + torrentRow.replaceChildren(newTorrentRow); }); - // Replace the old content with loading spinners - const outputDiv = document.getElementById('output') || document.createElement('div'); - outputDiv.id = 'output'; - outputDiv.innerHTML = `
Loading...
`; - // Wait for all promises to resolve await Promise.all(promises); - - // Replace the loading spinners with the new content - outputDiv.replaceChildren(torrentsContainer); } catch (error) { console.error(error); // Handle errors as needed } } - // Function to create a column div -function createColumn(label: string, value: string): HTMLDivElement { +function createColumn(label: string, value: string, columnClass: string): HTMLDivElement { const columnDiv = document.createElement('div'); - columnDiv.classList.add('me-3', 'p-2'); + columnDiv.classList.add(columnClass, 'me-3', 'p-2'); columnDiv.innerHTML = `

${label}

${value}

`; return columnDiv; } + // Function to create a column div with a progress bar function createColumnWithProgressBar(label: string, percentage: number): HTMLDivElement { const columnDiv = document.createElement('div');