Prep for tauri

This commit is contained in:
Igor Katson 2023-12-01 18:08:41 +00:00
parent ab7d0f68d9
commit 950ed816d1
No known key found for this signature in database
GPG key ID: B4EC22B66D61A3F5
6 changed files with 158 additions and 136 deletions

View file

@ -32,13 +32,13 @@ use crate::torrent_state::ManagedTorrentHandle;
// Public API // Public API
#[derive(Clone)] #[derive(Clone)]
pub struct HttpApi { pub struct HttpApi {
inner: Arc<ApiInternal>, inner: Arc<Api>,
} }
impl HttpApi { impl HttpApi {
pub fn new(session: Arc<Session>, rust_log_reload_tx: Option<UnboundedSender<String>>) -> Self { pub fn new(session: Arc<Session>, rust_log_reload_tx: Option<UnboundedSender<String>>) -> Self {
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<T> = std::result::Result<T, ApiError>; type Result<T> = std::result::Result<T, ApiError>;
#[derive(Serialize)] #[derive(Serialize)]
struct TorrentListResponseItem { pub struct TorrentListResponseItem {
id: usize, pub id: usize,
info_hash: String, pub info_hash: String,
} }
#[derive(Serialize)] #[derive(Serialize)]
struct TorrentListResponse { pub struct TorrentListResponse {
torrents: Vec<TorrentListResponseItem>, pub torrents: Vec<TorrentListResponseItem>,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -292,7 +292,7 @@ pub struct TorrentDetailsResponseFile {
} }
#[derive(Default, Serialize)] #[derive(Default, Serialize)]
struct EmptyJsonResponse {} pub struct EmptyJsonResponse {}
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct TorrentDetailsResponse { pub struct TorrentDetailsResponse {
@ -414,14 +414,14 @@ impl TorrentAddQueryParams {
} }
// Private HTTP API internals. Agnostic of web framework. // Private HTTP API internals. Agnostic of web framework.
struct ApiInternal { pub struct Api {
session: Arc<Session>, session: Arc<Session>,
rust_log_reload_tx: Option<UnboundedSender<String>>, rust_log_reload_tx: Option<UnboundedSender<String>>,
} }
type ApiState = Arc<ApiInternal>; type ApiState = Arc<Api>;
impl ApiInternal { impl Api {
pub fn new(session: Arc<Session>, rust_log_reload_tx: Option<UnboundedSender<String>>) -> Self { pub fn new(session: Arc<Session>, rust_log_reload_tx: Option<UnboundedSender<String>>) -> Self {
Self { Self {
session, session,
@ -429,13 +429,13 @@ impl ApiInternal {
} }
} }
fn mgr_handle(&self, idx: TorrentId) -> Result<ManagedTorrentHandle> { pub fn mgr_handle(&self, idx: TorrentId) -> Result<ManagedTorrentHandle> {
self.session self.session
.get(idx) .get(idx)
.ok_or(ApiError::torrent_not_found(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| { let items = self.session.with_torrents(|torrents| {
torrents torrents
.map(|(id, mgr)| TorrentListResponseItem { .map(|(id, mgr)| TorrentListResponseItem {
@ -447,14 +447,18 @@ impl ApiInternal {
TorrentListResponse { torrents: items } TorrentListResponse { torrents: items }
} }
fn api_torrent_details(&self, idx: TorrentId) -> Result<TorrentDetailsResponse> { pub fn api_torrent_details(&self, idx: TorrentId) -> Result<TorrentDetailsResponse> {
let handle = self.mgr_handle(idx)?; let handle = self.mgr_handle(idx)?;
let info_hash = handle.info().info_hash; let info_hash = handle.info().info_hash;
let only_files = handle.only_files(); let only_files = handle.only_files();
make_torrent_details(&info_hash, &handle.info().info, only_files.as_deref()) make_torrent_details(&info_hash, &handle.info().info, only_files.as_deref())
} }
fn api_peer_stats(&self, idx: TorrentId, filter: PeerStatsFilter) -> Result<PeerStatsSnapshot> { pub fn api_peer_stats(
&self,
idx: TorrentId,
filter: PeerStatsFilter,
) -> Result<PeerStatsSnapshot> {
let handle = self.mgr_handle(idx)?; let handle = self.mgr_handle(idx)?;
Ok(handle Ok(handle
.live() .live()
@ -462,7 +466,7 @@ impl ApiInternal {
.per_peer_stats_snapshot(filter)) .per_peer_stats_snapshot(filter))
} }
fn api_torrent_action_pause(&self, idx: TorrentId) -> Result<EmptyJsonResponse> { pub fn api_torrent_action_pause(&self, idx: TorrentId) -> Result<EmptyJsonResponse> {
let handle = self.mgr_handle(idx)?; let handle = self.mgr_handle(idx)?;
handle handle
.pause() .pause()
@ -471,7 +475,7 @@ impl ApiInternal {
Ok(Default::default()) Ok(Default::default())
} }
fn api_torrent_action_start(&self, idx: TorrentId) -> Result<EmptyJsonResponse> { pub fn api_torrent_action_start(&self, idx: TorrentId) -> Result<EmptyJsonResponse> {
let handle = self.mgr_handle(idx)?; let handle = self.mgr_handle(idx)?;
self.session self.session
.unpause(&handle) .unpause(&handle)
@ -480,21 +484,21 @@ impl ApiInternal {
Ok(Default::default()) Ok(Default::default())
} }
fn api_torrent_action_forget(&self, idx: TorrentId) -> Result<EmptyJsonResponse> { pub fn api_torrent_action_forget(&self, idx: TorrentId) -> Result<EmptyJsonResponse> {
self.session self.session
.delete(idx, false) .delete(idx, false)
.context("error forgetting torrent")?; .context("error forgetting torrent")?;
Ok(Default::default()) Ok(Default::default())
} }
fn api_torrent_action_delete(&self, idx: TorrentId) -> Result<EmptyJsonResponse> { pub fn api_torrent_action_delete(&self, idx: TorrentId) -> Result<EmptyJsonResponse> {
self.session self.session
.delete(idx, true) .delete(idx, true)
.context("error deleting torrent with files")?; .context("error deleting torrent with files")?;
Ok(Default::default()) Ok(Default::default())
} }
fn api_set_rust_log(&self, new_value: String) -> Result<EmptyJsonResponse> { pub fn api_set_rust_log(&self, new_value: String) -> Result<EmptyJsonResponse> {
let tx = self let tx = self
.rust_log_reload_tx .rust_log_reload_tx
.as_ref() .as_ref()
@ -553,7 +557,7 @@ impl ApiInternal {
Ok(response) Ok(response)
} }
fn api_dht_stats(&self) -> Result<DhtStats> { pub fn api_dht_stats(&self) -> Result<DhtStats> {
self.session self.session
.get_dht() .get_dht()
.as_ref() .as_ref()
@ -561,23 +565,23 @@ impl ApiInternal {
.ok_or(ApiError::dht_disabled()) .ok_or(ApiError::dht_disabled())
} }
fn api_dht_table(&self) -> Result<impl Serialize> { pub fn api_dht_table(&self) -> Result<impl Serialize> {
let dht = self.session.get_dht().ok_or(ApiError::dht_disabled())?; let dht = self.session.get_dht().ok_or(ApiError::dht_disabled())?;
Ok(dht.with_routing_table(|r| r.clone())) Ok(dht.with_routing_table(|r| r.clone()))
} }
fn api_stats_v0(&self, idx: TorrentId) -> Result<LiveStats> { pub fn api_stats_v0(&self, idx: TorrentId) -> Result<LiveStats> {
let mgr = self.mgr_handle(idx)?; let mgr = self.mgr_handle(idx)?;
let live = mgr.live().context("torrent not live")?; let live = mgr.live().context("torrent not live")?;
Ok(LiveStats::from(&*live)) Ok(LiveStats::from(&*live))
} }
fn api_stats_v1(&self, idx: TorrentId) -> Result<TorrentStats> { pub fn api_stats_v1(&self, idx: TorrentId) -> Result<TorrentStats> {
let mgr = self.mgr_handle(idx)?; let mgr = self.mgr_handle(idx)?;
Ok(mgr.stats()) Ok(mgr.stats())
} }
fn api_dump_haves(&self, idx: usize) -> Result<String> { pub fn api_dump_haves(&self, idx: usize) -> Result<String> {
let mgr = self.mgr_handle(idx)?; let mgr = self.mgr_handle(idx)?;
Ok(mgr.with_chunk_tracker(|chunks| format!("{:?}", chunks.get_have_pieces()))?) Ok(mgr.with_chunk_tracker(|chunks| format!("{:?}", chunks.get_have_pieces()))?)
} }

View file

@ -15,8 +15,7 @@
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="src/main.tsx"></script>
<script type="module" src="src/index.tsx"></script>
</body> </body>
</html> </html>

View file

@ -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<TorrentFile>;
}
export interface AddTorrentResponse {
id: number | null;
details: TorrentDetails;
seen_peers?: Array<string>;
}
export interface ListTorrentsResponse {
torrents: Array<TorrentId>;
}
// 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<ListTorrentsResponse>,
getTorrentDetails: (index: number) => Promise<TorrentDetails>,
getTorrentStats: (index: number) => Promise<TorrentStats>;
uploadTorrent: (data: string | File, opts?: {
listOnly?: boolean,
selectedFiles?: Array<number>,
unpopularTorrent?: boolean,
initialPeers?: Array<string>,
}) => Promise<AddTorrentResponse>;
pause: (index: number) => Promise<void>;
start: (index: number) => Promise<void>;
forget: (index: number) => Promise<void>;
delete: (index: number) => Promise<void>;
}

View file

@ -1,100 +1,8 @@
import { AddTorrentResponse, ErrorDetails, ListTorrentsResponse, RqbitAPI, TorrentDetails, TorrentStats } from "./api-types";
// Define API URL and base path // Define API URL and base path
const apiUrl = (window.origin === 'null' || window.origin === 'http://localhost:3031') ? 'http://localhost:3030' : ''; 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<TorrentFile>;
}
export interface AddTorrentResponse {
id: number | null;
details: TorrentDetails;
seen_peers?: Array<string>;
}
export interface ListTorrentsResponse {
torrents: Array<TorrentId>;
}
// 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<any> => { const makeRequest = async (method: string, path: string, data?: any): Promise<any> => {
console.log(method, path); console.log(method, path);
const url = apiUrl + path; const url = apiUrl + path;
@ -138,7 +46,7 @@ const makeRequest = async (method: string, path: string, data?: any): Promise<an
return result; return result;
} }
export const API = { export const API: RqbitAPI = {
listTorrents: (): Promise<ListTorrentsResponse> => makeRequest('GET', '/torrents'), listTorrents: (): Promise<ListTorrentsResponse> => makeRequest('GET', '/torrents'),
getTorrentDetails: (index: number): Promise<TorrentDetails> => { getTorrentDetails: (index: number): Promise<TorrentDetails> => {
return makeRequest('GET', `/torrents/${index}`); return makeRequest('GET', `/torrents/${index}`);

View file

@ -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(<StrictMode><RqbitWebUI /></StrictMode >);

View file

@ -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 ReactDOM from 'react-dom/client';
import { ProgressBar, Button, Container, Row, Col, Alert, Modal, Form, Spinner, Table } from 'react-bootstrap'; import { ProgressBar, Button, Container, Row, Col, Alert, Modal, Form, Spinner } from 'react-bootstrap';
import { AddTorrentResponse, TorrentDetails, TorrentFile, TorrentId, TorrentStats, ErrorDetails, API, STATE_INITIALIZING, STATE_LIVE, STATE_PAUSED, STATE_ERROR } from './api'; import { AddTorrentResponse, TorrentDetails, TorrentId, TorrentStats, ErrorDetails, STATE_INITIALIZING, STATE_LIVE, STATE_PAUSED, STATE_ERROR, RqbitAPI } from './api-types';
declare const API: RqbitAPI;
interface Error { interface Error {
text: string, text: string,
@ -308,7 +310,7 @@ const TorrentsList = (props: { torrents: Array<TorrentId>, loading: boolean }) =
</>; </>;
}; };
const Root = () => { export const RqbitWebUI = () => {
const [closeableError, setCloseableError] = useState<Error>(null); const [closeableError, setCloseableError] = useState<Error>(null);
const [otherError, setOtherError] = useState<Error>(null); const [otherError, setOtherError] = useState<Error>(null);
@ -694,12 +696,3 @@ function loopUntilSuccess<T>(callback: () => Promise<T>, interval: number): () =
return () => clearTimeout(timeoutId); return () => clearTimeout(timeoutId);
} }
// List all torrents on page load and set up auto-refresh
async function init(): Promise<void> {
const torrentsContainer = document.getElementById('app');
ReactDOM.createRoot(torrentsContainer).render(<StrictMode><Root /></StrictMode>);
}
// Call init function on page load
document.addEventListener('DOMContentLoaded', init);