Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2d683fc
[webui]: refactor a bit - move more stuff to torrent store, less API …
ikatson Jul 5, 2025
0f5b031
Starting to vibe code compact UI
ikatson Jul 5, 2025
c970df3
split up components
ikatson Jul 6, 2025
5ab1c98
remove a useless check
ikatson Jul 6, 2025
a82ecf6
web fmt
ikatson Jul 6, 2025
96ffd69
Saving
ikatson Jul 6, 2025
d6b7116
fixed sorting
ikatson Jul 6, 2025
96cd78d
continuing
ikatson Jul 6, 2025
45c1610
Nothing
ikatson Jul 6, 2025
978a8f8
Nothing
ikatson Jul 6, 2025
4c3b7d6
Reuse tab functionality between desktop and webui
ikatson Jul 11, 2025
728944a
Resizeable panes
ikatson Jul 11, 2025
8830c4e
Overview tab
ikatson Jul 11, 2025
56f41fc
Code is getting uglier
ikatson Jul 11, 2025
f526ce1
Continuing
ikatson Jul 12, 2025
e1229a2
Continuing
ikatson Jul 12, 2025
2359d72
remove duplcate stats call
ikatson Jul 12, 2025
d55ecea
Almost nothing - selected torrent in store
ikatson Jul 12, 2025
3688d4a
Cleaning up
ikatson Jul 13, 2025
d8739e6
Cleaning up
ikatson Jul 13, 2025
2cb03ef
continuing compact
ikatson Jul 13, 2025
b4af1ab
format
ikatson Jul 13, 2025
1861b7c
Starting to add up speed to peer table
ikatson Jul 13, 2025
74ac05a
Add upload speed indicator to peer table
ikatson Jul 13, 2025
42ac70c
Add a missing file
ikatson Jul 14, 2025
15d1b9f
live/mod.rs: store more snapshots in speed estimator (as it was calcu…
ikatson Jul 14, 2025
e768639
Remove locking in speed estimator
ikatson Jul 14, 2025
15257ef
Peer loading spinner
ikatson Jul 14, 2025
47124e7
Add multiactions in compact UI
ikatson Jul 14, 2025
8516027
compact by default on larger screens
ikatson Jul 14, 2025
5f31d13
fix react error
ikatson Jul 14, 2025
22bd3dd
Less rerenders
ikatson Jul 14, 2025
d54ef58
Reducing re-renders
ikatson Jul 14, 2025
bcba24d
Add an empty space to test CI
ikatson Aug 2, 2025
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
6 changes: 4 additions & 2 deletions crates/librqbit/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,8 @@ impl Session {
}
};

let (stats, down_updater, up_updater) = SessionStats::new();

let session = Arc::new(Self {
persistence,
bitv_factory,
Expand All @@ -711,7 +713,7 @@ impl Session {
reqwest_client,
connector: stream_connector,
root_span: opts.root_span,
stats: Arc::new(SessionStats::new()),
stats: Arc::new(stats),
concurrent_initialize_semaphore: Arc::new(tokio::sync::Semaphore::new(
opts.concurrent_init_limit.unwrap_or(3),
)),
Expand Down Expand Up @@ -810,7 +812,7 @@ impl Session {
}
}

session.start_speed_estimator_updater();
session.start_speed_estimator_updater(down_updater, up_updater);

Ok(session)
}
Expand Down
42 changes: 22 additions & 20 deletions crates/librqbit/src/session_stats/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::{
};

use anyhow::Context;
use librqbit_core::speed_estimator::SpeedEstimator;
use librqbit_core::speed_estimator::{SpeedEstimator, Updater};
use snapshot::SessionStatsSnapshot;
use tracing::debug_span;

Expand All @@ -28,25 +28,29 @@ pub struct SessionStats {
}

impl SessionStats {
pub fn new() -> Self {
SessionStats {
counters: Default::default(),
peers: Default::default(),
down_speed_estimator: SpeedEstimator::new(5),
up_speed_estimator: SpeedEstimator::new(5),
startup_time: Instant::now(),
}
}
}

impl Default for SessionStats {
fn default() -> Self {
Self::new()
pub fn new() -> (Self, Updater, Updater) {
let (down, down_upd) = SpeedEstimator::new(5);
let (up, up_upd) = SpeedEstimator::new(5);
(
SessionStats {
counters: Default::default(),
peers: Default::default(),
down_speed_estimator: down,
up_speed_estimator: up,
startup_time: Instant::now(),
},
down_upd,
up_upd,
)
}
}

impl Session {
pub(crate) fn start_speed_estimator_updater(self: &Arc<Self>) {
pub(crate) fn start_speed_estimator_updater(
self: &Arc<Self>,
mut down_upd: Updater,
mut up_upd: Updater,
) {
self.spawn(
debug_span!(parent: self.rs(), "speed_estimator"),
"speed_estimator",
Expand All @@ -61,10 +65,8 @@ impl Session {
let now = Instant::now();
let fetched = s.stats.counters.fetched_bytes.load(Ordering::Relaxed);
let uploaded = s.stats.counters.uploaded_bytes.load(Ordering::Relaxed);
s.stats
.down_speed_estimator
.add_snapshot(fetched, None, now);
s.stats.up_speed_estimator.add_snapshot(uploaded, None, now);
down_upd.add_snapshot(fetched, None, now);
up_upd.add_snapshot(uploaded, None, now);
}
}
},
Expand Down
14 changes: 5 additions & 9 deletions crates/librqbit/src/torrent_state/live/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,8 @@ impl TorrentStateLive {
.upgrade()
.context("session is dead, cannot start torrent")?;
let session_stats = session.stats.clone();
let down_speed_estimator = SpeedEstimator::default();
let up_speed_estimator = SpeedEstimator::default();
let (down_speed_estimator, mut down_se_updater) = SpeedEstimator::new(10);
let (up_speed_estimator, mut up_se_updater) = SpeedEstimator::new(10);

let have_bytes = paused.chunk_tracker.get_hns().have_bytes;
let lengths = *paused.chunk_tracker.get_lengths();
Expand Down Expand Up @@ -308,13 +308,9 @@ impl TorrentStateLive {
let stats = state.stats_snapshot();
let fetched = stats.fetched_bytes;
let remaining = state.locked.read().get_chunks()?.get_remaining_bytes();
state
.down_speed_estimator
.add_snapshot(fetched, Some(remaining), now);
state
.up_speed_estimator
.add_snapshot(stats.uploaded_bytes, None, now);
tokio::time::sleep(Duration::from_millis(100)).await;
down_se_updater.add_snapshot(fetched, Some(remaining), now);
up_se_updater.add_snapshot(stats.uploaded_bytes, None, now);
tokio::time::sleep(Duration::from_secs(500)).await;
}
}
},
Expand Down
57 changes: 46 additions & 11 deletions crates/librqbit/webui/src/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,33 @@
export interface TorrentId {
id: number;
info_hash: string;
name: string;
output_folder: string;
}

export interface PeerCounters {
incoming_connections: number;
fetched_bytes: number;
uploaded_bytes: number;
total_time_connecting_ms: number;
connection_attempts: number;
connections: number;
errors: number;
fetched_chunks: number;
downloaded_and_checked_pieces: number;
total_piece_download_ms: number;
times_stolen_from_me: number;
times_i_stole: number;
}

export interface PeerStats {
counters: PeerCounters;
state: string;
conn_kind: string | null;
}

export interface PeerStatsSnapshot {
peers: Record<string, PeerStats>;
}

export interface TorrentFile {
Expand All @@ -26,6 +53,10 @@ export interface TorrentDetails {
files: Array<TorrentFile>;
}

export interface TorrentIdWithStats extends TorrentId {
stats: TorrentStats;
}

export interface AddTorrentResponse {
id: number | null;
details: TorrentDetails;
Expand All @@ -34,7 +65,7 @@ export interface AddTorrentResponse {
}

export interface ListTorrentsResponse {
torrents: Array<TorrentId>;
torrents: Array<TorrentIdWithStats>;
}

export interface Speed {
Expand Down Expand Up @@ -203,25 +234,29 @@ export interface JSONLogLine {
}

export interface RqbitAPI {
getPlaylistUrl: (index: number) => string | null;
getStreamLogsUrl: () => string | null;
listTorrents: () => Promise<ListTorrentsResponse>;
getTorrentDetails: (index: number) => Promise<TorrentDetails>;
getTorrentStats: (index: number) => Promise<TorrentStats>;
getTorrentStreamUrl: (

getTorrentPeerStats: (
index: number,
file_id: number,
filename?: string | null,
) => string | null;
state?: "all" | "live",
) => Promise<PeerStatsSnapshot>;
stats: () => Promise<SessionStats>;
uploadTorrent: (
data: string | File,
data: File | string,
opts?: AddTorrentOptions,
) => Promise<AddTorrentResponse>;

pause: (index: number) => Promise<void>;
updateOnlyFiles: (index: number, files: number[]) => Promise<void>;
pause: (index: number) => Promise<void>;
start: (index: number) => Promise<void>;
forget: (index: number) => Promise<void>;
delete: (index: number) => Promise<void>;
stats: () => Promise<SessionStats>;
getTorrentStreamUrl: (
index: number,
file_id: number,
filename?: string | null,
) => string;
getPlaylistUrl: (index: number) => string;
getStreamLogsUrl: () => string;
}
103 changes: 103 additions & 0 deletions crates/librqbit/webui/src/components/CompactTorrentRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { useMemo } from "react";
import { TorrentIdWithStats, STATE_INITIALIZING } from "../api-types";
import { formatBytes } from "../helper/formatBytes";
import { getCompletionETA } from "../helper/getCompletionETA";
import { useTorrentStore } from "../stores/torrentStore";
import { ProgressBar } from "./ProgressBar";
import { StatusIcon } from "./StatusIcon";

export const CompactTorrentRow: React.FC<{
torrent: TorrentIdWithStats;
}> = ({ torrent }) => {
const selected = useTorrentStore((state) =>
state.selectedTorrentIds.includes(torrent.id),
);
const setSelectedTorrentId = useTorrentStore(
(state) => state.setSelectedTorrentId,
);
const toggleSelectedTorrentId = useTorrentStore(
(state) => state.toggleSelectedTorrentId,
);
const statsResponse = torrent.stats;
const error = statsResponse?.error ?? null;
const finished = statsResponse?.finished || false;
const totalBytes = statsResponse?.total_bytes ?? 1;
const progressBytes = statsResponse?.progress_bytes ?? 0;
const progressPercentage = error
? 100
: totalBytes == 0
? 100
: (progressBytes / totalBytes) * 100;

const formatPeersString = () => {
let peer_stats = statsResponse?.live?.snapshot.peer_stats;
if (!peer_stats) {
return "";
}
return `${peer_stats.live} / ${peer_stats.seen}`;
};

const onClick = useMemo(() => {
return (event: React.MouseEvent<HTMLTableRowElement>) => {
event.stopPropagation();
event.preventDefault();
if (event.metaKey || event.ctrlKey || event.shiftKey) {
toggleSelectedTorrentId(torrent.id);
} else {
setSelectedTorrentId(torrent.id);
}
};
}, [torrent.id]);

return (
<tr
className={`cursor-pointer ${selected ? "bg-gray-200 dark:bg-slate-700" : ""}`}
onClick={onClick}
>
<td className="px-2 py-1 whitespace-nowrap text-xs">{torrent.id}</td>
<td className="px-2 py-1 whitespace-nowrap">
<StatusIcon
className="w-4 h-4"
error={!!error}
live={!!statsResponse?.live}
finished={finished}
/>
</td>
<td className="px-2 py-1 text-left text-xs text-gray-900 text-ellipsis overflow-hidden dark:text-slate-200">
{torrent.name}
</td>
<td className="px-2 py-1 whitespace-nowrap text-xs">
{error ? (
<span className="text-red-500 text-xs">Error</span>
) : (
<ProgressBar
now={progressPercentage}
label={error}
variant={
statsResponse?.state == STATE_INITIALIZING
? "warn"
: finished
? "success"
: "info"
}
/>
)}
</td>
<td className="px-2 py-1 whitespace-nowrap text-xs">
{statsResponse.live?.download_speed.human_readable}
</td>
<td className="px-2 py-1 whitespace-nowrap text-xs">
{statsResponse.live?.upload_speed.human_readable}
</td>
<td className="px-2 py-1 whitespace-nowrap text-xs">
{getCompletionETA(statsResponse)}
</td>
<td className="px-2 py-1 whitespace-nowrap text-xs">
{formatPeersString()}
</td>
<td className="px-2 py-1 whitespace-nowrap text-xs">
{formatBytes(totalBytes)}
</td>
</tr>
);
};
12 changes: 6 additions & 6 deletions crates/librqbit/webui/src/components/FileListInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ type FileTree = {

const newFileTree = (
torrentDetails: TorrentDetails,
stats: TorrentStats | null
stats: TorrentStats | null,
): FileTree => {
const newFileTreeInner = (
name: string,
id: string,
files: TorrentFileForCheckbox[],
depth: number
depth: number,
): FileTree => {
let directFiles: TorrentFileForCheckbox[] = [];
let groups: FileTree[] = [];
Expand All @@ -54,7 +54,7 @@ const newFileTree = (

let sortedGroupsByName = sortBy(
Object.entries(groupsByName),
([k, _]) => k
([k, _]) => k,
);

let childId = 0;
Expand Down Expand Up @@ -87,7 +87,7 @@ const newFileTree = (
};
})
.filter((f) => f !== null),
0
0,
);
};

Expand Down Expand Up @@ -177,7 +177,7 @@ const FileTreeComponent: React.FC<{
label={`${
tree.name ? tree.name + ", " : ""
} ${getTotalSelectedFiles()} files, ${formatBytes(
getTotalSelectedBytes()
getTotalSelectedBytes(),
)}`}
name={tree.id}
onChange={handleToggleTree}
Expand Down Expand Up @@ -253,7 +253,7 @@ export const FileListInput: React.FC<{
}) => {
let fileTree = useMemo(
() => newFileTree(torrentDetails, torrentStats),
[torrentDetails, torrentStats]
[torrentDetails, torrentStats],
);

return (
Expand Down
6 changes: 4 additions & 2 deletions crates/librqbit/webui/src/components/LogLine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,15 @@ export const LogLine: React.FC<{ line: JSONLogLine }> = React.memo(
</span>

<span className="m-1">
{parsed.spans?.map((span, i) => <LogSpan key={i} span={span} />)}
{parsed.spans?.map((span, i) => (
<LogSpan key={i} span={span} />
))}
</span>
<span className="m-1 text-slate-500 dark:text-slate-400">
{parsed.target}
</span>
<Fields fields={parsed.fields} />
</p>
);
}
},
);
Loading
Loading