diff --git a/crates/librqbit/src/http_api.rs b/crates/librqbit/src/http_api.rs index 9fa1a27..35012cb 100644 --- a/crates/librqbit/src/http_api.rs +++ b/crates/librqbit/src/http_api.rs @@ -32,13 +32,13 @@ use crate::torrent_state::ManagedTorrentHandle; // Public API #[derive(Clone)] pub struct HttpApi { - inner: Arc, + inner: Arc, } impl HttpApi { pub fn new(session: Arc, rust_log_reload_tx: Option>) -> Self { Self { - inner: Arc::new(ApiInternal::new(session, rust_log_reload_tx)), + inner: Arc::new(Api::new(session, rust_log_reload_tx)), } } @@ -274,14 +274,14 @@ impl HttpApi { type Result = std::result::Result; #[derive(Serialize)] -struct TorrentListResponseItem { - id: usize, - info_hash: String, +pub struct TorrentListResponseItem { + pub id: usize, + pub info_hash: String, } #[derive(Serialize)] -struct TorrentListResponse { - torrents: Vec, +pub struct TorrentListResponse { + pub torrents: Vec, } #[derive(Serialize, Deserialize)] @@ -292,7 +292,7 @@ pub struct TorrentDetailsResponseFile { } #[derive(Default, Serialize)] -struct EmptyJsonResponse {} +pub struct EmptyJsonResponse {} #[derive(Serialize, Deserialize)] pub struct TorrentDetailsResponse { @@ -414,14 +414,14 @@ impl TorrentAddQueryParams { } // Private HTTP API internals. Agnostic of web framework. -struct ApiInternal { +pub struct Api { session: Arc, rust_log_reload_tx: Option>, } -type ApiState = Arc; +type ApiState = Arc; -impl ApiInternal { +impl Api { pub fn new(session: Arc, rust_log_reload_tx: Option>) -> Self { Self { session, @@ -429,13 +429,13 @@ impl ApiInternal { } } - fn mgr_handle(&self, idx: TorrentId) -> Result { + pub fn mgr_handle(&self, idx: TorrentId) -> Result { self.session .get(idx) .ok_or(ApiError::torrent_not_found(idx)) } - fn api_torrent_list(&self) -> TorrentListResponse { + pub fn api_torrent_list(&self) -> TorrentListResponse { let items = self.session.with_torrents(|torrents| { torrents .map(|(id, mgr)| TorrentListResponseItem { @@ -447,14 +447,18 @@ impl ApiInternal { TorrentListResponse { torrents: items } } - fn api_torrent_details(&self, idx: TorrentId) -> Result { + pub fn api_torrent_details(&self, idx: TorrentId) -> Result { let handle = self.mgr_handle(idx)?; let info_hash = handle.info().info_hash; let only_files = handle.only_files(); make_torrent_details(&info_hash, &handle.info().info, only_files.as_deref()) } - fn api_peer_stats(&self, idx: TorrentId, filter: PeerStatsFilter) -> Result { + pub fn api_peer_stats( + &self, + idx: TorrentId, + filter: PeerStatsFilter, + ) -> Result { let handle = self.mgr_handle(idx)?; Ok(handle .live() @@ -462,7 +466,7 @@ impl ApiInternal { .per_peer_stats_snapshot(filter)) } - fn api_torrent_action_pause(&self, idx: TorrentId) -> Result { + pub fn api_torrent_action_pause(&self, idx: TorrentId) -> Result { let handle = self.mgr_handle(idx)?; handle .pause() @@ -471,7 +475,7 @@ impl ApiInternal { Ok(Default::default()) } - fn api_torrent_action_start(&self, idx: TorrentId) -> Result { + pub fn api_torrent_action_start(&self, idx: TorrentId) -> Result { let handle = self.mgr_handle(idx)?; self.session .unpause(&handle) @@ -480,21 +484,21 @@ impl ApiInternal { Ok(Default::default()) } - fn api_torrent_action_forget(&self, idx: TorrentId) -> Result { + pub fn api_torrent_action_forget(&self, idx: TorrentId) -> Result { self.session .delete(idx, false) .context("error forgetting torrent")?; Ok(Default::default()) } - fn api_torrent_action_delete(&self, idx: TorrentId) -> Result { + pub fn api_torrent_action_delete(&self, idx: TorrentId) -> Result { self.session .delete(idx, true) .context("error deleting torrent with files")?; Ok(Default::default()) } - fn api_set_rust_log(&self, new_value: String) -> Result { + pub fn api_set_rust_log(&self, new_value: String) -> Result { let tx = self .rust_log_reload_tx .as_ref() @@ -553,7 +557,7 @@ impl ApiInternal { Ok(response) } - fn api_dht_stats(&self) -> Result { + pub fn api_dht_stats(&self) -> Result { self.session .get_dht() .as_ref() @@ -561,23 +565,23 @@ impl ApiInternal { .ok_or(ApiError::dht_disabled()) } - fn api_dht_table(&self) -> Result { + pub fn api_dht_table(&self) -> Result { let dht = self.session.get_dht().ok_or(ApiError::dht_disabled())?; Ok(dht.with_routing_table(|r| r.clone())) } - fn api_stats_v0(&self, idx: TorrentId) -> Result { + pub fn api_stats_v0(&self, idx: TorrentId) -> Result { let mgr = self.mgr_handle(idx)?; let live = mgr.live().context("torrent not live")?; Ok(LiveStats::from(&*live)) } - fn api_stats_v1(&self, idx: TorrentId) -> Result { + pub fn api_stats_v1(&self, idx: TorrentId) -> Result { let mgr = self.mgr_handle(idx)?; Ok(mgr.stats()) } - fn api_dump_haves(&self, idx: usize) -> Result { + pub fn api_dump_haves(&self, idx: usize) -> Result { let mgr = self.mgr_handle(idx)?; Ok(mgr.with_chunk_tracker(|chunks| format!("{:?}", chunks.get_have_pieces()))?) } diff --git a/crates/librqbit/webui/index.html b/crates/librqbit/webui/index.html index 9f68944..7499433 100644 --- a/crates/librqbit/webui/index.html +++ b/crates/librqbit/webui/index.html @@ -15,8 +15,7 @@
- - + \ No newline at end of file diff --git a/crates/librqbit/webui/src/api-types.ts b/crates/librqbit/webui/src/api-types.ts new file mode 100644 index 0000000..c504849 --- /dev/null +++ b/crates/librqbit/webui/src/api-types.ts @@ -0,0 +1,109 @@ +// Interface for the Torrent API response +export interface TorrentId { + id: number; + info_hash: string; +} + +export interface TorrentFile { + name: string; + length: number; + included: boolean; +} + +// Interface for the Torrent Details API response +export interface TorrentDetails { + info_hash: string, + files: Array; +} + +export interface AddTorrentResponse { + id: number | null; + details: TorrentDetails; + seen_peers?: Array; +} + +export interface ListTorrentsResponse { + torrents: Array; +} + +// Interface for the Torrent Stats API response +export interface LiveTorrentStats { + 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; + }; + all_time_download_speed: { + mbps: number; + human_readable: string; + }; + time_remaining: { + human_readable: string; + duration?: { + secs: number, + } + } | null; +} + +export const STATE_INITIALIZING = 'initializing'; +export const STATE_PAUSED = 'paused'; +export const STATE_LIVE = 'live'; +export const STATE_ERROR = 'error'; + +export interface TorrentStats { + state: 'initializing' | 'paused' | 'live' | 'error', + error: string | null, + progress_bytes: number, + finished: boolean, + total_bytes: number, + live: LiveTorrentStats | null; +} + + +export interface ErrorDetails { + id?: number, + method?: string, + path?: string, + status?: number, + statusText?: string, + text: string, +}; + +export interface RqbitAPI { + listTorrents: () => Promise, + getTorrentDetails: (index: number) => Promise, + getTorrentStats: (index: number) => Promise; + uploadTorrent: (data: string | File, opts?: { + listOnly?: boolean, + selectedFiles?: Array, + unpopularTorrent?: boolean, + initialPeers?: Array, + }) => Promise; + + pause: (index: number) => Promise; + start: (index: number) => Promise; + forget: (index: number) => Promise; + delete: (index: number) => Promise; +} \ No newline at end of file diff --git a/crates/librqbit/webui/src/api.ts b/crates/librqbit/webui/src/api.ts index 2926a82..56b89ce 100644 --- a/crates/librqbit/webui/src/api.ts +++ b/crates/librqbit/webui/src/api.ts @@ -1,100 +1,8 @@ +import { AddTorrentResponse, ErrorDetails, ListTorrentsResponse, RqbitAPI, TorrentDetails, TorrentStats } from "./api-types"; + // Define API URL and base path const apiUrl = (window.origin === 'null' || window.origin === 'http://localhost:3031') ? 'http://localhost:3030' : ''; -// Interface for the Torrent API response -export interface TorrentId { - id: number; - info_hash: string; -} - -export interface TorrentFile { - name: string; - length: number; - included: boolean; -} - -// Interface for the Torrent Details API response -export interface TorrentDetails { - info_hash: string, - files: Array; -} - -export interface AddTorrentResponse { - id: number | null; - details: TorrentDetails; - seen_peers?: Array; -} - -export interface ListTorrentsResponse { - torrents: Array; -} - -// Interface for the Torrent Stats API response -export interface LiveTorrentStats { - 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; - }; - all_time_download_speed: { - mbps: number; - human_readable: string; - }; - time_remaining: { - human_readable: string; - duration?: { - secs: number, - } - } | null; -} - -export const STATE_INITIALIZING = 'initializing'; -export const STATE_PAUSED = 'paused'; -export const STATE_LIVE = 'live'; -export const STATE_ERROR = 'error'; - -export interface TorrentStats { - state: 'initializing' | 'paused' | 'live' | 'error', - error: string | null, - progress_bytes: number, - finished: boolean, - total_bytes: number, - live: LiveTorrentStats | null; -} - - -export interface ErrorDetails { - id?: number, - method?: string, - path?: string, - status?: number, - statusText?: string, - text: string, -}; - - const makeRequest = async (method: string, path: string, data?: any): Promise => { console.log(method, path); const url = apiUrl + path; @@ -138,7 +46,7 @@ const makeRequest = async (method: string, path: string, data?: any): Promise => makeRequest('GET', '/torrents'), getTorrentDetails: (index: number): Promise => { return makeRequest('GET', `/torrents/${index}`); diff --git a/crates/librqbit/webui/src/main.tsx b/crates/librqbit/webui/src/main.tsx new file mode 100644 index 0000000..e7eb463 --- /dev/null +++ b/crates/librqbit/webui/src/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from "react"; +import ReactDOM from 'react-dom/client'; +import { RqbitWebUI } from "./rqbit-web"; +import { API } from "./api"; + +globalThis.API = API; + +const torrentsContainer = document.getElementById('app'); +ReactDOM.createRoot(torrentsContainer).render(); diff --git a/crates/librqbit/webui/src/index.tsx b/crates/librqbit/webui/src/rqbit-web.tsx similarity index 97% rename from crates/librqbit/webui/src/index.tsx rename to crates/librqbit/webui/src/rqbit-web.tsx index 224dfa0..599a621 100644 --- a/crates/librqbit/webui/src/index.tsx +++ b/crates/librqbit/webui/src/rqbit-web.tsx @@ -1,7 +1,9 @@ -import { MouseEventHandler, StrictMode, createContext, useContext, useEffect, useRef, useState } from 'react'; +import { StrictMode, createContext, useContext, useEffect, useRef, useState } from 'react'; import ReactDOM from 'react-dom/client'; -import { ProgressBar, Button, Container, Row, Col, Alert, Modal, Form, Spinner, Table } from 'react-bootstrap'; -import { AddTorrentResponse, TorrentDetails, TorrentFile, TorrentId, TorrentStats, ErrorDetails, API, STATE_INITIALIZING, STATE_LIVE, STATE_PAUSED, STATE_ERROR } from './api'; +import { ProgressBar, Button, Container, Row, Col, Alert, Modal, Form, Spinner } from 'react-bootstrap'; +import { AddTorrentResponse, TorrentDetails, TorrentId, TorrentStats, ErrorDetails, STATE_INITIALIZING, STATE_LIVE, STATE_PAUSED, STATE_ERROR, RqbitAPI } from './api-types'; + +declare const API: RqbitAPI; interface Error { text: string, @@ -308,7 +310,7 @@ const TorrentsList = (props: { torrents: Array, loading: boolean }) = ; }; -const Root = () => { +export const RqbitWebUI = () => { const [closeableError, setCloseableError] = useState(null); const [otherError, setOtherError] = useState(null); @@ -693,13 +695,4 @@ function loopUntilSuccess(callback: () => Promise, interval: number): () = scheduleNext(0); return () => clearTimeout(timeoutId); -} - -// List all torrents on page load and set up auto-refresh -async function init(): Promise { - const torrentsContainer = document.getElementById('app'); - ReactDOM.createRoot(torrentsContainer).render(); -} - -// Call init function on page load -document.addEventListener('DOMContentLoaded', init); \ No newline at end of file +} \ No newline at end of file