From 66d2f224edf5dd4529486f0b728fd7dcc9acd1b5 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Fri, 24 Nov 2023 15:36:37 +0000 Subject: [PATCH] Switch UI to display statuses better --- crates/librqbit/src/chunk_tracker.rs | 4 +- crates/librqbit/src/http_api.rs | 6 ++- crates/librqbit/src/session.rs | 4 +- .../src/torrent_state/live/stats/snapshot.rs | 2 +- crates/librqbit/webui/src/api.ts | 29 ++++++++++- crates/librqbit/webui/src/index.tsx | 49 ++++++++++++++----- crates/rqbit/src/main.rs | 12 ++--- 7 files changed, 80 insertions(+), 26 deletions(-) diff --git a/crates/librqbit/src/chunk_tracker.rs b/crates/librqbit/src/chunk_tracker.rs index 21207fc..ecaeab5 100644 --- a/crates/librqbit/src/chunk_tracker.rs +++ b/crates/librqbit/src/chunk_tracker.rs @@ -4,7 +4,7 @@ use tracing::{debug, info}; use crate::type_aliases::BF; -pub(crate) struct ChunkTracker { +pub struct ChunkTracker { // This forms the basis of a "queue" to pull from. // It's set to 1 if we need a piece, but the moment we start requesting a peer, // it's set to 0. @@ -51,7 +51,7 @@ fn compute_chunk_status(lengths: &Lengths, needed_pieces: &BF) -> BF { chunk_bf } -pub(crate) enum ChunkMarkingResult { +pub enum ChunkMarkingResult { PreviouslyCompleted, NotCompleted, Completed, diff --git a/crates/librqbit/src/http_api.rs b/crates/librqbit/src/http_api.rs index 87bb46c..2d4edc1 100644 --- a/crates/librqbit/src/http_api.rs +++ b/crates/librqbit/src/http_api.rs @@ -285,6 +285,7 @@ struct StatsResponse { error: Option, progress_bytes: u64, total_bytes: u64, + finished: bool, live: Option, } @@ -514,6 +515,7 @@ impl ApiInternal { state: "", error: None, progress_bytes: 0, + finished: false, live: None, }; @@ -526,11 +528,13 @@ impl ApiInternal { ManagedTorrentState::Paused(p) => { resp.state = "paused"; resp.progress_bytes = p.have_bytes; + resp.finished = p.have_bytes == resp.progress_bytes; } ManagedTorrentState::Live(l) => { resp.state = "live"; let live_stats = self.make_live_stats(l); - resp.progress_bytes = live_stats.snapshot.downloaded_and_checked_bytes; + resp.progress_bytes = live_stats.snapshot.have_bytes; + resp.finished = resp.progress_bytes == resp.total_bytes; resp.live = Some(live_stats); } ManagedTorrentState::Error(e) => { diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index f3af02c..2d1d656 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -438,7 +438,7 @@ impl Session { .as_ref() .map(|dht| dht.get_peers(handle.info_hash())) .transpose()?; - handle.start(Default::default(), peer_rx); - return Ok(()); + handle.start(Default::default(), peer_rx)?; + Ok(()) } } diff --git a/crates/librqbit/src/torrent_state/live/stats/snapshot.rs b/crates/librqbit/src/torrent_state/live/stats/snapshot.rs index 45dce43..6331b6a 100644 --- a/crates/librqbit/src/torrent_state/live/stats/snapshot.rs +++ b/crates/librqbit/src/torrent_state/live/stats/snapshot.rs @@ -1,4 +1,4 @@ -use std::time::{Duration, Instant}; +use std::time::Duration; use serde::Serialize; diff --git a/crates/librqbit/webui/src/api.ts b/crates/librqbit/webui/src/api.ts index 915bc10..e99026f 100644 --- a/crates/librqbit/webui/src/api.ts +++ b/crates/librqbit/webui/src/api.ts @@ -29,7 +29,7 @@ export interface ListTorrentsResponse { } // Interface for the Torrent Stats API response -export interface TorrentStats { +export interface LiveTorrentStats { snapshot: { have_bytes: number; downloaded_and_checked_bytes: number; @@ -69,6 +69,15 @@ export interface TorrentStats { } | null; } +export interface TorrentStats { + state: string, + error: string | null, + progress_bytes: number, + finished: boolean, + total_bytes: number, + live: LiveTorrentStats | null; +} + export interface ErrorDetails { id?: number, @@ -129,7 +138,7 @@ export const API = { return makeRequest('GET', `/torrents/${index}`); }, getTorrentStats: (index: number): Promise => { - return makeRequest('GET', `/torrents/${index}/stats`); + return makeRequest('GET', `/torrents/${index}/stats/v1`); }, uploadTorrent: (data: string | File, opts?: { @@ -144,5 +153,21 @@ export const API = { url += `&only_files=${opts.selectedFiles.join(',')}`; } return makeRequest('POST', url, data) + }, + + pause: (index: number): Promise => { + return makeRequest('POST', `/torrents/${index}/pause`); + }, + + start: (index: number): Promise => { + return makeRequest('POST', `/torrents/${index}/start`); + }, + + forget: (index: number): Promise => { + return makeRequest('POST', `/torrents/${index}/forget`); + }, + + delete: (index: number): Promise => { + return makeRequest('POST', `/torrents/${index}/delete`); } } \ No newline at end of file diff --git a/crates/librqbit/webui/src/index.tsx b/crates/librqbit/webui/src/index.tsx index 7baee31..40b219f 100644 --- a/crates/librqbit/webui/src/index.tsx +++ b/crates/librqbit/webui/src/index.tsx @@ -18,13 +18,33 @@ const AppContext = createContext(null); const TorrentRow: React.FC<{ id: number, detailsResponse: TorrentDetails, statsResponse: TorrentStats }> = ({ id, detailsResponse, statsResponse }) => { - const totalBytes = statsResponse?.snapshot?.total_bytes ?? 1; - const downloadedBytes = statsResponse?.snapshot?.have_bytes ?? 0; - const finished = totalBytes == downloadedBytes; - const downloadPercentage = (downloadedBytes / totalBytes) * 100; + const state = statsResponse?.state ?? ""; + + const totalBytes = statsResponse?.total_bytes ?? 1; + const progressBytes = statsResponse?.progress_bytes ?? 0; + const finished = statsResponse?.finished || false; + const progressPercentage = state == 'error' ? 100 : (progressBytes / totalBytes) * 100; + const isAnimated = (state == "initializing" || state == "live") && !finished; + const progressLabel = state == 'error' ? 'Error' : `${progressPercentage.toFixed(2)}%`; + + const getPeersString = (statsResponse: TorrentStats) => { + let peer_stats = statsResponse?.live?.snapshot.peer_stats; + if (!peer_stats) { + return ''; + } + return `${peer_stats.live} / ${peer_stats.seen}`; + } + + let classNames = []; + if (id % 2 == 0) { + classNames.push('bg-light'); + } + if (statsResponse?.error) { + classNames.push('bg-warning'); + } return ( - + {detailsResponse ?
@@ -36,11 +56,16 @@ const TorrentRow: React.FC<{ <> {`${formatBytes(totalBytes)} `} - + + { + statsResponse.error && ( +

{statsResponse.error}

+ ) + }
- {statsResponse.download_speed.human_readable} + {statsResponse.live?.download_speed.human_readable ?? "N/A"} {getCompletionETA(statsResponse)} - {`${statsResponse.snapshot.peer_stats.live} / ${statsResponse.snapshot.peer_stats.seen}`} + {getPeersString(statsResponse)} : } @@ -374,7 +399,7 @@ const RootContent = (props: { closeableError: ErrorDetails, otherError: ErrorDet }; function torrentIsDone(stats: TorrentStats): boolean { - return stats.snapshot.have_bytes == stats.snapshot.total_bytes; + return stats.finished; } function formatBytes(bytes: number): string { @@ -398,11 +423,11 @@ function getLargestFileName(torrentDetails: TorrentDetails): string { } function getCompletionETA(stats: TorrentStats): string { - if (stats.time_remaining && stats.time_remaining.duration) { - return formatSecondsToTime(stats.time_remaining.duration.secs); - } else { + let duration = stats?.live?.time_remaining?.duration?.secs; + if (duration == null) { return 'N/A'; } + return formatSecondsToTime(duration); } function formatSecondsToTime(seconds: number): string { diff --git a/crates/rqbit/src/main.rs b/crates/rqbit/src/main.rs index c46d0e2..e1b923b 100644 --- a/crates/rqbit/src/main.rs +++ b/crates/rqbit/src/main.rs @@ -238,12 +238,12 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> let stats_printer = |session: Arc| async move { loop { session.with_torrents(|torrents| { - for (idx, torrent) in torrents.iter().enumerate() { + for (idx, torrent) in torrents { let live = torrent.with_state(|s| { match s { ManagedTorrentState::Initializing(_) => info!("[{}] initializing", idx), ManagedTorrentState::Live(h) => return Some(h.clone()), - ManagedTorrentState::Error(_) | ManagedTorrentState::Paused(_) => {}, + _ => {}, }; None }); @@ -397,10 +397,11 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> .await { Ok(v) => match v { - AddTorrentResponse::AlreadyManaged(handle) => { + AddTorrentResponse::AlreadyManaged(id, handle) => { info!( - "torrent {:?} is already managed, downloaded to {:?}", + "torrent {:?} is already managed, id={}, downloaded to {:?}", handle.info_hash(), + id, handle.info().out_dir ); continue; @@ -426,7 +427,7 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> } continue; } - AddTorrentResponse::Added(handle) => { + AddTorrentResponse::Added(_, handle) => { added = true; handle } @@ -437,7 +438,6 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> } }; - http_api.add_torrent_handle(handle.clone()); handles.push(handle); }