Switch UI to display statuses better
This commit is contained in:
parent
876afbf41b
commit
66d2f224ed
7 changed files with 80 additions and 26 deletions
|
|
@ -4,7 +4,7 @@ use tracing::{debug, info};
|
||||||
|
|
||||||
use crate::type_aliases::BF;
|
use crate::type_aliases::BF;
|
||||||
|
|
||||||
pub(crate) struct ChunkTracker {
|
pub struct ChunkTracker {
|
||||||
// This forms the basis of a "queue" to pull from.
|
// 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 1 if we need a piece, but the moment we start requesting a peer,
|
||||||
// it's set to 0.
|
// it's set to 0.
|
||||||
|
|
@ -51,7 +51,7 @@ fn compute_chunk_status(lengths: &Lengths, needed_pieces: &BF) -> BF {
|
||||||
chunk_bf
|
chunk_bf
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) enum ChunkMarkingResult {
|
pub enum ChunkMarkingResult {
|
||||||
PreviouslyCompleted,
|
PreviouslyCompleted,
|
||||||
NotCompleted,
|
NotCompleted,
|
||||||
Completed,
|
Completed,
|
||||||
|
|
|
||||||
|
|
@ -285,6 +285,7 @@ struct StatsResponse {
|
||||||
error: Option<String>,
|
error: Option<String>,
|
||||||
progress_bytes: u64,
|
progress_bytes: u64,
|
||||||
total_bytes: u64,
|
total_bytes: u64,
|
||||||
|
finished: bool,
|
||||||
live: Option<LiveStats>,
|
live: Option<LiveStats>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -514,6 +515,7 @@ impl ApiInternal {
|
||||||
state: "",
|
state: "",
|
||||||
error: None,
|
error: None,
|
||||||
progress_bytes: 0,
|
progress_bytes: 0,
|
||||||
|
finished: false,
|
||||||
live: None,
|
live: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -526,11 +528,13 @@ impl ApiInternal {
|
||||||
ManagedTorrentState::Paused(p) => {
|
ManagedTorrentState::Paused(p) => {
|
||||||
resp.state = "paused";
|
resp.state = "paused";
|
||||||
resp.progress_bytes = p.have_bytes;
|
resp.progress_bytes = p.have_bytes;
|
||||||
|
resp.finished = p.have_bytes == resp.progress_bytes;
|
||||||
}
|
}
|
||||||
ManagedTorrentState::Live(l) => {
|
ManagedTorrentState::Live(l) => {
|
||||||
resp.state = "live";
|
resp.state = "live";
|
||||||
let live_stats = self.make_live_stats(l);
|
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);
|
resp.live = Some(live_stats);
|
||||||
}
|
}
|
||||||
ManagedTorrentState::Error(e) => {
|
ManagedTorrentState::Error(e) => {
|
||||||
|
|
|
||||||
|
|
@ -438,7 +438,7 @@ impl Session {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|dht| dht.get_peers(handle.info_hash()))
|
.map(|dht| dht.get_peers(handle.info_hash()))
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
handle.start(Default::default(), peer_rx);
|
handle.start(Default::default(), peer_rx)?;
|
||||||
return Ok(());
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::time::{Duration, Instant};
|
use std::time::Duration;
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export interface ListTorrentsResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface for the Torrent Stats API response
|
// Interface for the Torrent Stats API response
|
||||||
export interface TorrentStats {
|
export interface LiveTorrentStats {
|
||||||
snapshot: {
|
snapshot: {
|
||||||
have_bytes: number;
|
have_bytes: number;
|
||||||
downloaded_and_checked_bytes: number;
|
downloaded_and_checked_bytes: number;
|
||||||
|
|
@ -69,6 +69,15 @@ export interface TorrentStats {
|
||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TorrentStats {
|
||||||
|
state: string,
|
||||||
|
error: string | null,
|
||||||
|
progress_bytes: number,
|
||||||
|
finished: boolean,
|
||||||
|
total_bytes: number,
|
||||||
|
live: LiveTorrentStats | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface ErrorDetails {
|
export interface ErrorDetails {
|
||||||
id?: number,
|
id?: number,
|
||||||
|
|
@ -129,7 +138,7 @@ export const API = {
|
||||||
return makeRequest('GET', `/torrents/${index}`);
|
return makeRequest('GET', `/torrents/${index}`);
|
||||||
},
|
},
|
||||||
getTorrentStats: (index: number): Promise<TorrentStats> => {
|
getTorrentStats: (index: number): Promise<TorrentStats> => {
|
||||||
return makeRequest('GET', `/torrents/${index}/stats`);
|
return makeRequest('GET', `/torrents/${index}/stats/v1`);
|
||||||
},
|
},
|
||||||
|
|
||||||
uploadTorrent: (data: string | File, opts?: {
|
uploadTorrent: (data: string | File, opts?: {
|
||||||
|
|
@ -144,5 +153,21 @@ export const API = {
|
||||||
url += `&only_files=${opts.selectedFiles.join(',')}`;
|
url += `&only_files=${opts.selectedFiles.join(',')}`;
|
||||||
}
|
}
|
||||||
return makeRequest('POST', url, data)
|
return makeRequest('POST', url, data)
|
||||||
|
},
|
||||||
|
|
||||||
|
pause: (index: number): Promise<void> => {
|
||||||
|
return makeRequest('POST', `/torrents/${index}/pause`);
|
||||||
|
},
|
||||||
|
|
||||||
|
start: (index: number): Promise<void> => {
|
||||||
|
return makeRequest('POST', `/torrents/${index}/start`);
|
||||||
|
},
|
||||||
|
|
||||||
|
forget: (index: number): Promise<void> => {
|
||||||
|
return makeRequest('POST', `/torrents/${index}/forget`);
|
||||||
|
},
|
||||||
|
|
||||||
|
delete: (index: number): Promise<void> => {
|
||||||
|
return makeRequest('POST', `/torrents/${index}/delete`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -18,13 +18,33 @@ const AppContext = createContext<ContextType>(null);
|
||||||
const TorrentRow: React.FC<{
|
const TorrentRow: React.FC<{
|
||||||
id: number, detailsResponse: TorrentDetails, statsResponse: TorrentStats
|
id: number, detailsResponse: TorrentDetails, statsResponse: TorrentStats
|
||||||
}> = ({ id, detailsResponse, statsResponse }) => {
|
}> = ({ id, detailsResponse, statsResponse }) => {
|
||||||
const totalBytes = statsResponse?.snapshot?.total_bytes ?? 1;
|
const state = statsResponse?.state ?? "";
|
||||||
const downloadedBytes = statsResponse?.snapshot?.have_bytes ?? 0;
|
|
||||||
const finished = totalBytes == downloadedBytes;
|
const totalBytes = statsResponse?.total_bytes ?? 1;
|
||||||
const downloadPercentage = (downloadedBytes / totalBytes) * 100;
|
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 (
|
return (
|
||||||
<Row className={`${id % 2 == 0 ? 'bg-light' : ''}`}>
|
<Row className={classNames.join(' ')}>
|
||||||
<Column size={4} label="Name">
|
<Column size={4} label="Name">
|
||||||
{detailsResponse ?
|
{detailsResponse ?
|
||||||
<div className='text-truncate'>
|
<div className='text-truncate'>
|
||||||
|
|
@ -36,11 +56,16 @@ const TorrentRow: React.FC<{
|
||||||
<>
|
<>
|
||||||
<Column label="Size">{`${formatBytes(totalBytes)} `}</Column>
|
<Column label="Size">{`${formatBytes(totalBytes)} `}</Column>
|
||||||
<Column size={2} label="Progress">
|
<Column size={2} label="Progress">
|
||||||
<ProgressBar now={downloadPercentage} label={`${downloadPercentage.toFixed(2)}% `} animated={!finished} />
|
<ProgressBar now={progressPercentage} label={progressLabel} animated={isAnimated} className={state == 'error' ? 'bg-danger' : ''} />
|
||||||
|
{
|
||||||
|
statsResponse.error && (
|
||||||
|
<p>{statsResponse.error}</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
</Column>
|
</Column>
|
||||||
<Column size={2} label="Down Speed">{statsResponse.download_speed.human_readable}</Column>
|
<Column size={2} label="Down Speed">{statsResponse.live?.download_speed.human_readable ?? "N/A"}</Column>
|
||||||
<Column label="ETA">{getCompletionETA(statsResponse)}</Column>
|
<Column label="ETA">{getCompletionETA(statsResponse)}</Column>
|
||||||
<Column size={2} label="Peers">{`${statsResponse.snapshot.peer_stats.live} / ${statsResponse.snapshot.peer_stats.seen}`}</Column >
|
<Column size={2} label="Peers">{getPeersString(statsResponse)}</Column >
|
||||||
</>
|
</>
|
||||||
: <Column label="Loading stats" size={8}><Spinner /></Column>
|
: <Column label="Loading stats" size={8}><Spinner /></Column>
|
||||||
}
|
}
|
||||||
|
|
@ -374,7 +399,7 @@ const RootContent = (props: { closeableError: ErrorDetails, otherError: ErrorDet
|
||||||
};
|
};
|
||||||
|
|
||||||
function torrentIsDone(stats: TorrentStats): boolean {
|
function torrentIsDone(stats: TorrentStats): boolean {
|
||||||
return stats.snapshot.have_bytes == stats.snapshot.total_bytes;
|
return stats.finished;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatBytes(bytes: number): string {
|
function formatBytes(bytes: number): string {
|
||||||
|
|
@ -398,11 +423,11 @@ function getLargestFileName(torrentDetails: TorrentDetails): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCompletionETA(stats: TorrentStats): string {
|
function getCompletionETA(stats: TorrentStats): string {
|
||||||
if (stats.time_remaining && stats.time_remaining.duration) {
|
let duration = stats?.live?.time_remaining?.duration?.secs;
|
||||||
return formatSecondsToTime(stats.time_remaining.duration.secs);
|
if (duration == null) {
|
||||||
} else {
|
|
||||||
return 'N/A';
|
return 'N/A';
|
||||||
}
|
}
|
||||||
|
return formatSecondsToTime(duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSecondsToTime(seconds: number): string {
|
function formatSecondsToTime(seconds: number): string {
|
||||||
|
|
|
||||||
|
|
@ -238,12 +238,12 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()>
|
||||||
let stats_printer = |session: Arc<Session>| async move {
|
let stats_printer = |session: Arc<Session>| async move {
|
||||||
loop {
|
loop {
|
||||||
session.with_torrents(|torrents| {
|
session.with_torrents(|torrents| {
|
||||||
for (idx, torrent) in torrents.iter().enumerate() {
|
for (idx, torrent) in torrents {
|
||||||
let live = torrent.with_state(|s| {
|
let live = torrent.with_state(|s| {
|
||||||
match s {
|
match s {
|
||||||
ManagedTorrentState::Initializing(_) => info!("[{}] initializing", idx),
|
ManagedTorrentState::Initializing(_) => info!("[{}] initializing", idx),
|
||||||
ManagedTorrentState::Live(h) => return Some(h.clone()),
|
ManagedTorrentState::Live(h) => return Some(h.clone()),
|
||||||
ManagedTorrentState::Error(_) | ManagedTorrentState::Paused(_) => {},
|
_ => {},
|
||||||
};
|
};
|
||||||
None
|
None
|
||||||
});
|
});
|
||||||
|
|
@ -397,10 +397,11 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()>
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(v) => match v {
|
Ok(v) => match v {
|
||||||
AddTorrentResponse::AlreadyManaged(handle) => {
|
AddTorrentResponse::AlreadyManaged(id, handle) => {
|
||||||
info!(
|
info!(
|
||||||
"torrent {:?} is already managed, downloaded to {:?}",
|
"torrent {:?} is already managed, id={}, downloaded to {:?}",
|
||||||
handle.info_hash(),
|
handle.info_hash(),
|
||||||
|
id,
|
||||||
handle.info().out_dir
|
handle.info().out_dir
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -426,7 +427,7 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()>
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
AddTorrentResponse::Added(handle) => {
|
AddTorrentResponse::Added(_, handle) => {
|
||||||
added = true;
|
added = true;
|
||||||
handle
|
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);
|
handles.push(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue