Prep for tauri
This commit is contained in:
parent
ab7d0f68d9
commit
950ed816d1
6 changed files with 158 additions and 136 deletions
|
|
@ -32,13 +32,13 @@ use crate::torrent_state::ManagedTorrentHandle;
|
|||
// Public API
|
||||
#[derive(Clone)]
|
||||
pub struct HttpApi {
|
||||
inner: Arc<ApiInternal>,
|
||||
inner: Arc<Api>,
|
||||
}
|
||||
|
||||
impl HttpApi {
|
||||
pub fn new(session: Arc<Session>, rust_log_reload_tx: Option<UnboundedSender<String>>) -> 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>;
|
||||
|
||||
#[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<TorrentListResponseItem>,
|
||||
pub struct TorrentListResponse {
|
||||
pub torrents: Vec<TorrentListResponseItem>,
|
||||
}
|
||||
|
||||
#[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<Session>,
|
||||
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 {
|
||||
Self {
|
||||
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
|
||||
.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<TorrentDetailsResponse> {
|
||||
pub fn api_torrent_details(&self, idx: TorrentId) -> Result<TorrentDetailsResponse> {
|
||||
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<PeerStatsSnapshot> {
|
||||
pub fn api_peer_stats(
|
||||
&self,
|
||||
idx: TorrentId,
|
||||
filter: PeerStatsFilter,
|
||||
) -> Result<PeerStatsSnapshot> {
|
||||
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<EmptyJsonResponse> {
|
||||
pub fn api_torrent_action_pause(&self, idx: TorrentId) -> Result<EmptyJsonResponse> {
|
||||
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<EmptyJsonResponse> {
|
||||
pub fn api_torrent_action_start(&self, idx: TorrentId) -> Result<EmptyJsonResponse> {
|
||||
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<EmptyJsonResponse> {
|
||||
pub fn api_torrent_action_forget(&self, idx: TorrentId) -> Result<EmptyJsonResponse> {
|
||||
self.session
|
||||
.delete(idx, false)
|
||||
.context("error forgetting torrent")?;
|
||||
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
|
||||
.delete(idx, true)
|
||||
.context("error deleting torrent with files")?;
|
||||
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
|
||||
.rust_log_reload_tx
|
||||
.as_ref()
|
||||
|
|
@ -553,7 +557,7 @@ impl ApiInternal {
|
|||
Ok(response)
|
||||
}
|
||||
|
||||
fn api_dht_stats(&self) -> Result<DhtStats> {
|
||||
pub fn api_dht_stats(&self) -> Result<DhtStats> {
|
||||
self.session
|
||||
.get_dht()
|
||||
.as_ref()
|
||||
|
|
@ -561,23 +565,23 @@ impl ApiInternal {
|
|||
.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())?;
|
||||
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 live = mgr.live().context("torrent not 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)?;
|
||||
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)?;
|
||||
Ok(mgr.with_chunk_tracker(|chunks| format!("{:?}", chunks.get_have_pieces()))?)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,7 @@
|
|||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<script type="module" src="src/index.tsx"></script>
|
||||
<script type="module" src="src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
109
crates/librqbit/webui/src/api-types.ts
Normal file
109
crates/librqbit/webui/src/api-types.ts
Normal 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>;
|
||||
}
|
||||
|
|
@ -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<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> => {
|
||||
console.log(method, path);
|
||||
const url = apiUrl + path;
|
||||
|
|
@ -138,7 +46,7 @@ const makeRequest = async (method: string, path: string, data?: any): Promise<an
|
|||
return result;
|
||||
}
|
||||
|
||||
export const API = {
|
||||
export const API: RqbitAPI = {
|
||||
listTorrents: (): Promise<ListTorrentsResponse> => makeRequest('GET', '/torrents'),
|
||||
getTorrentDetails: (index: number): Promise<TorrentDetails> => {
|
||||
return makeRequest('GET', `/torrents/${index}`);
|
||||
|
|
|
|||
9
crates/librqbit/webui/src/main.tsx
Normal file
9
crates/librqbit/webui/src/main.tsx
Normal 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 >);
|
||||
|
|
@ -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<TorrentId>, loading: boolean }) =
|
|||
</>;
|
||||
};
|
||||
|
||||
const Root = () => {
|
||||
export const RqbitWebUI = () => {
|
||||
const [closeableError, setCloseableError] = useState<Error>(null);
|
||||
const [otherError, setOtherError] = useState<Error>(null);
|
||||
|
||||
|
|
@ -693,13 +695,4 @@ function loopUntilSuccess<T>(callback: () => Promise<T>, interval: number): () =
|
|||
scheduleNext(0);
|
||||
|
||||
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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue