Better error display in UI
This commit is contained in:
parent
414b2c5f65
commit
4078eacf1d
7 changed files with 92 additions and 51 deletions
|
|
@ -23,6 +23,7 @@ use crate::http_api_error::{ApiError, ApiErrorExt};
|
||||||
use crate::peer_connection::PeerConnectionOptions;
|
use crate::peer_connection::PeerConnectionOptions;
|
||||||
use crate::session::{
|
use crate::session::{
|
||||||
AddTorrent, AddTorrentOptions, AddTorrentResponse, ListOnlyResponse, Session, TorrentId,
|
AddTorrent, AddTorrentOptions, AddTorrentResponse, ListOnlyResponse, Session, TorrentId,
|
||||||
|
SUPPORTED_SCHEMES,
|
||||||
};
|
};
|
||||||
use crate::torrent_state::peer::stats::snapshot::{PeerStatsFilter, PeerStatsSnapshot};
|
use crate::torrent_state::peer::stats::snapshot::{PeerStatsFilter, PeerStatsSnapshot};
|
||||||
use crate::torrent_state::stats::{LiveStats, TorrentStats};
|
use crate::torrent_state::stats::{LiveStats, TorrentStats};
|
||||||
|
|
@ -88,10 +89,29 @@ impl HttpApi {
|
||||||
Query(params): Query<TorrentAddQueryParams>,
|
Query(params): Query<TorrentAddQueryParams>,
|
||||||
data: Bytes,
|
data: Bytes,
|
||||||
) -> Result<impl IntoResponse> {
|
) -> Result<impl IntoResponse> {
|
||||||
|
let is_url = params.is_url;
|
||||||
let opts = params.into_add_torrent_options();
|
let opts = params.into_add_torrent_options();
|
||||||
let add = match String::from_utf8(data.to_vec()) {
|
let data = data.to_vec();
|
||||||
Ok(s) => AddTorrent::Url(s.into()),
|
let add = match is_url {
|
||||||
Err(e) => AddTorrent::TorrentFileBytes(e.into_bytes().into()),
|
Some(true) => AddTorrent::Url(
|
||||||
|
String::from_utf8(data)
|
||||||
|
.context("invalid utf-8 for passed URL")?
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
Some(false) => AddTorrent::TorrentFileBytes(data.into()),
|
||||||
|
|
||||||
|
// Guess the format.
|
||||||
|
None if SUPPORTED_SCHEMES
|
||||||
|
.iter()
|
||||||
|
.any(|s| data.starts_with(s.as_bytes())) =>
|
||||||
|
{
|
||||||
|
AddTorrent::Url(
|
||||||
|
String::from_utf8(data)
|
||||||
|
.context("invalid utf-8 for passed URL")?
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => AddTorrent::TorrentFileBytes(data.into()),
|
||||||
};
|
};
|
||||||
state.api_add_torrent(add, Some(opts)).await.map(axum::Json)
|
state.api_add_torrent(add, Some(opts)).await.map(axum::Json)
|
||||||
}
|
}
|
||||||
|
|
@ -366,6 +386,8 @@ pub struct TorrentAddQueryParams {
|
||||||
pub peer_connect_timeout: Option<u64>,
|
pub peer_connect_timeout: Option<u64>,
|
||||||
pub peer_read_write_timeout: Option<u64>,
|
pub peer_read_write_timeout: Option<u64>,
|
||||||
pub initial_peers: Option<InitialPeers>,
|
pub initial_peers: Option<InitialPeers>,
|
||||||
|
// Will force interpreting the content as a URL.
|
||||||
|
pub is_url: Option<bool>,
|
||||||
pub list_only: Option<bool>,
|
pub list_only: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ impl std::fmt::Display for ApiError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match &self.kind {
|
match &self.kind {
|
||||||
ApiErrorKind::TorrentNotFound(idx) => write!(f, "torrent {idx} not found"),
|
ApiErrorKind::TorrentNotFound(idx) => write!(f, "torrent {idx} not found"),
|
||||||
ApiErrorKind::Other(err) => write!(f, "{err:#}"),
|
ApiErrorKind::Other(err) => write!(f, "{err:?}"),
|
||||||
ApiErrorKind::DhtDisabled => write!(f, "DHT is disabled"),
|
ApiErrorKind::DhtDisabled => write!(f, "DHT is disabled"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -150,14 +150,14 @@ pub struct Session {
|
||||||
async fn torrent_from_url(url: &str) -> anyhow::Result<TorrentMetaV1Owned> {
|
async fn torrent_from_url(url: &str) -> anyhow::Result<TorrentMetaV1Owned> {
|
||||||
let response = reqwest::get(url)
|
let response = reqwest::get(url)
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("error downloading torrent metadata from {url}"))?;
|
.context("error downloading torrent metadata")?;
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
anyhow::bail!("GET {} returned {}", url, response.status())
|
anyhow::bail!("GET {} returned {}", url, response.status())
|
||||||
}
|
}
|
||||||
let b = response
|
let b = response
|
||||||
.bytes()
|
.bytes()
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("error reading repsonse body from {url}"))?;
|
.with_context(|| format!("error reading response body from {url}"))?;
|
||||||
torrent_from_bytes(&b).context("error decoding torrent")
|
torrent_from_bytes(&b).context("error decoding torrent")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
10
crates/librqbit/webui/dist/assets/index.js
vendored
10
crates/librqbit/webui/dist/assets/index.js
vendored
File diff suppressed because one or more lines are too long
2
crates/librqbit/webui/dist/manifest.json
vendored
2
crates/librqbit/webui/dist/manifest.json
vendored
|
|
@ -4,7 +4,7 @@
|
||||||
"src": "assets/logo.svg"
|
"src": "assets/logo.svg"
|
||||||
},
|
},
|
||||||
"index.html": {
|
"index.html": {
|
||||||
"file": "assets/index-75fed916.js",
|
"file": "assets/index-3dee43e7.js",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
"src": "index.html"
|
"src": "index.html"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,9 @@ export const API = {
|
||||||
if (opts.initialPeers) {
|
if (opts.initialPeers) {
|
||||||
url += `&initial_peers=${opts.initialPeers.join(',')}`;
|
url += `&initial_peers=${opts.initialPeers.join(',')}`;
|
||||||
}
|
}
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
url += '&is_url=true';
|
||||||
|
}
|
||||||
return makeRequest('POST', url, data)
|
return makeRequest('POST', url, data)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -356,8 +356,8 @@ const ErrorDetails = (props: { details: ErrorDetails }) => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return <>
|
return <>
|
||||||
{details.status && <strong>{details.status} {details.statusText}: </strong>}
|
<p>{details.status && <strong>{details.status} {details.statusText}</strong>}</p>
|
||||||
{details.text}
|
<pre>{details.text}</pre>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -438,7 +438,13 @@ const MagnetInput = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UploadButton variant='primary' buttonText="Add Torrent from Magnet / URL" onClick={onClick} data={magnet} resetData={() => setMagnet(null)} />
|
<UploadButton
|
||||||
|
variant='primary'
|
||||||
|
buttonText="Add Torrent from Magnet / URL"
|
||||||
|
onClick={onClick}
|
||||||
|
data={magnet}
|
||||||
|
resetData={() => setMagnet(null)}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -463,7 +469,13 @@ const FileInput = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<input type="file" ref={inputRef} accept=".torrent" onChange={onFileChange} className='d-none' />
|
<input type="file" ref={inputRef} accept=".torrent" onChange={onFileChange} className='d-none' />
|
||||||
<UploadButton variant='secondary' buttonText="Upload .torrent File" onClick={onClick} data={file} resetData={reset} />
|
<UploadButton
|
||||||
|
variant='secondary'
|
||||||
|
buttonText="Upload .torrent File"
|
||||||
|
onClick={onClick}
|
||||||
|
data={file}
|
||||||
|
resetData={reset}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -474,7 +486,7 @@ const FileSelectionModal = (props: {
|
||||||
listTorrentResponse: AddTorrentResponse,
|
listTorrentResponse: AddTorrentResponse,
|
||||||
listTorrentError: Error,
|
listTorrentError: Error,
|
||||||
listTorrentLoading: boolean,
|
listTorrentLoading: boolean,
|
||||||
data: string | File
|
data: string | File,
|
||||||
}) => {
|
}) => {
|
||||||
let { show, onHide, listTorrentResponse, listTorrentError, listTorrentLoading, data } = props;
|
let { show, onHide, listTorrentResponse, listTorrentError, listTorrentLoading, data } = props;
|
||||||
|
|
||||||
|
|
@ -516,46 +528,50 @@ const FileSelectionModal = (props: {
|
||||||
).finally(() => setUploading(false));
|
).finally(() => setUploading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getBody = () => {
|
||||||
|
if (listTorrentLoading) {
|
||||||
|
return <Spinner />;
|
||||||
|
} else if (listTorrentError) {
|
||||||
|
return <ErrorComponent error={listTorrentError}></ErrorComponent>;
|
||||||
|
} else if (listTorrentResponse) {
|
||||||
|
return <Form>
|
||||||
|
<fieldset className='mb-5'>
|
||||||
|
<legend>Pick the files to download</legend>
|
||||||
|
{listTorrentResponse.details.files.map((file, index) => (
|
||||||
|
<Form.Group key={index} controlId={`check-${index}`}>
|
||||||
|
<Form.Check
|
||||||
|
type="checkbox"
|
||||||
|
label={`${file.name} (${formatBytes(file.length)})`}
|
||||||
|
checked={selectedFiles.includes(index)}
|
||||||
|
onChange={() => handleToggleFile(index)}>
|
||||||
|
</Form.Check>
|
||||||
|
</Form.Group>
|
||||||
|
))}
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Other options</legend>
|
||||||
|
|
||||||
|
<Form.Group controlId='unpopular-torrent'>
|
||||||
|
<Form.Check
|
||||||
|
type="checkbox"
|
||||||
|
label="Increase timeouts"
|
||||||
|
checked={unpopularTorrent}
|
||||||
|
onChange={() => setUnpopularTorrent(!unpopularTorrent)}>
|
||||||
|
</Form.Check>
|
||||||
|
<small id="emailHelp" className="form-text text-muted">This might be useful for unpopular torrents with few peers. It will slow down fast torrents though.</small>
|
||||||
|
</Form.Group>
|
||||||
|
</fieldset>
|
||||||
|
</Form >
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal show={show} onHide={clear} size='lg'>
|
<Modal show={show} onHide={clear} size='lg'>
|
||||||
<Modal.Header closeButton>
|
<Modal.Header closeButton>
|
||||||
<Modal.Title>Add torrent</Modal.Title>
|
<Modal.Title>Add torrent</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<Form>
|
{getBody()}
|
||||||
<fieldset className='mb-5'>
|
|
||||||
<legend>Pick the files to download</legend>
|
|
||||||
{listTorrentLoading ? <Spinner />
|
|
||||||
: listTorrentError ? <ErrorComponent error={listTorrentError}></ErrorComponent> :
|
|
||||||
<>
|
|
||||||
{listTorrentResponse?.details.files.map((file, index) => (
|
|
||||||
<Form.Group key={index} controlId={`check-${index}`}>
|
|
||||||
<Form.Check
|
|
||||||
type="checkbox"
|
|
||||||
label={`${file.name} (${formatBytes(file.length)})`}
|
|
||||||
checked={selectedFiles.includes(index)}
|
|
||||||
onChange={() => handleToggleFile(index)}>
|
|
||||||
</Form.Check>
|
|
||||||
</Form.Group>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
</fieldset>
|
|
||||||
<fieldset>
|
|
||||||
<legend>Other options</legend>
|
|
||||||
|
|
||||||
<Form.Group controlId='unpopular-torrent'>
|
|
||||||
<Form.Check
|
|
||||||
type="checkbox"
|
|
||||||
label="Increase timeouts"
|
|
||||||
checked={unpopularTorrent}
|
|
||||||
onChange={() => setUnpopularTorrent(!unpopularTorrent)}>
|
|
||||||
</Form.Check>
|
|
||||||
<small id="emailHelp" className="form-text text-muted">This might be useful for unpopular torrents with few peers.</small>
|
|
||||||
</Form.Group>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
</Form>
|
|
||||||
<ErrorComponent error={uploadError} />
|
<ErrorComponent error={uploadError} />
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue