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
#[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()))?)
}

View file

@ -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>

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
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}`);

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 { 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);
}