Pass back peers from Web UI when adding a magnet in attempt to speed it up

This commit is contained in:
Igor Katson 2023-12-01 09:30:23 +00:00
parent 21b1bd9e7d
commit f337ab1837
No known key found for this signature in database
GPG key ID: B4EC22B66D61A3F5
8 changed files with 77 additions and 27 deletions

View file

@ -11,6 +11,7 @@ use librqbit_core::id20::Id20;
use librqbit_core::torrent_metainfo::TorrentMetaV1Info; use librqbit_core::torrent_metainfo::TorrentMetaV1Info;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::net::SocketAddr; use std::net::SocketAddr;
use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
@ -281,6 +282,7 @@ pub struct TorrentDetailsResponse {
pub struct ApiAddTorrentResponse { pub struct ApiAddTorrentResponse {
pub id: Option<usize>, pub id: Option<usize>,
pub details: TorrentDetailsResponse, pub details: TorrentDetailsResponse,
pub seen_peers: Option<Vec<SocketAddr>>,
} }
pub struct OnlyFiles(Vec<usize>); pub struct OnlyFiles(Vec<usize>);
@ -324,6 +326,36 @@ impl<'de> Deserialize<'de> for OnlyFiles {
} }
} }
pub struct InitialPeers(pub Vec<SocketAddr>);
impl<'de> Deserialize<'de> for InitialPeers {
fn deserialize<D>(deserializer: D) -> std::prelude::v1::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
let string = String::deserialize(deserializer)?;
let mut addrs = Vec::new();
for addr_str in string.split(',') {
addrs.push(SocketAddr::from_str(addr_str).map_err(D::Error::custom)?);
}
Ok(InitialPeers(addrs))
}
}
impl Serialize for InitialPeers {
fn serialize<S>(&self, serializer: S) -> std::prelude::v1::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0
.iter()
.map(|s| s.to_string())
.join(",")
.serialize(serializer)
}
}
#[derive(Serialize, Deserialize, Default)] #[derive(Serialize, Deserialize, Default)]
pub struct TorrentAddQueryParams { pub struct TorrentAddQueryParams {
pub overwrite: Option<bool>, pub overwrite: Option<bool>,
@ -333,6 +365,7 @@ pub struct TorrentAddQueryParams {
pub only_files: Option<OnlyFiles>, pub only_files: Option<OnlyFiles>,
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 list_only: Option<bool>, pub list_only: Option<bool>,
} }
@ -345,6 +378,7 @@ impl TorrentAddQueryParams {
output_folder: self.output_folder, output_folder: self.output_folder,
sub_folder: self.sub_folder, sub_folder: self.sub_folder,
list_only: self.list_only.unwrap_or(false), list_only: self.list_only.unwrap_or(false),
initial_peers: self.initial_peers.map(|i| i.0),
peer_opts: Some(PeerConnectionOptions { peer_opts: Some(PeerConnectionOptions {
connect_timeout: self.peer_connect_timeout.map(Duration::from_secs), connect_timeout: self.peer_connect_timeout.map(Duration::from_secs),
read_write_timeout: self.peer_read_write_timeout.map(Duration::from_secs), read_write_timeout: self.peer_read_write_timeout.map(Duration::from_secs),
@ -471,8 +505,10 @@ impl ApiInternal {
info_hash, info_hash,
info, info,
only_files, only_files,
seen_peers,
}) => ApiAddTorrentResponse { }) => ApiAddTorrentResponse {
id: None, id: None,
seen_peers: Some(seen_peers),
details: make_torrent_details(&info_hash, &info, only_files.as_deref()) details: make_torrent_details(&info_hash, &info, only_files.as_deref())
.context("error making torrent details")?, .context("error making torrent details")?,
}, },
@ -486,6 +522,7 @@ impl ApiInternal {
ApiAddTorrentResponse { ApiAddTorrentResponse {
id: Some(id), id: Some(id),
details, details,
seen_peers: None,
} }
} }
}; };

View file

@ -192,7 +192,7 @@ pub struct AddTorrentOptions {
pub sub_folder: Option<String>, pub sub_folder: Option<String>,
pub peer_opts: Option<PeerConnectionOptions>, pub peer_opts: Option<PeerConnectionOptions>,
pub force_tracker_interval: Option<Duration>, pub force_tracker_interval: Option<Duration>,
pub initial_peers: Option<Vec<SocketAddr>>,
// This is used to restore the session. // This is used to restore the session.
pub preferred_id: Option<usize>, pub preferred_id: Option<usize>,
} }
@ -201,6 +201,7 @@ pub struct ListOnlyResponse {
pub info_hash: Id20, pub info_hash: Id20,
pub info: TorrentMetaV1Info<ByteString>, pub info: TorrentMetaV1Info<ByteString>,
pub only_files: Option<Vec<usize>>, pub only_files: Option<Vec<usize>>,
pub seen_peers: Vec<SocketAddr>,
} }
pub enum AddTorrentResponse { pub enum AddTorrentResponse {
@ -642,6 +643,7 @@ impl Session {
info_hash, info_hash,
info, info,
only_files, only_files,
seen_peers: initial_peers,
})); }));
} }

View file

@ -29,6 +29,7 @@ use tracing::debug;
use tracing::error; use tracing::error;
use tracing::error_span; use tracing::error_span;
use tracing::warn; use tracing::warn;
use tracing::trace;
use url::Url; use url::Url;
use crate::chunk_tracker::ChunkTracker; use crate::chunk_tracker::ChunkTracker;
@ -207,6 +208,7 @@ impl ManagedTorrent {
{ {
let live: Arc<TorrentStateLive> = let live: Arc<TorrentStateLive> =
live.upgrade().context("no longer live")?; live.upgrade().context("no longer live")?;
trace!("adding {} initial peers", initial_peers.len());
for peer in initial_peers { for peer in initial_peers {
live.add_peer_if_not_seen(peer).context("torrent closed")?; live.add_peer_if_not_seen(peer).context("torrent closed")?;
} }

File diff suppressed because one or more lines are too long

View file

@ -4,7 +4,7 @@
"src": "assets/logo.svg" "src": "assets/logo.svg"
}, },
"index.html": { "index.html": {
"file": "assets/index-b3c336f4.js", "file": "assets/index-75fed916.js",
"isEntry": true, "isEntry": true,
"src": "index.html" "src": "index.html"
} }

View file

@ -22,6 +22,7 @@ export interface TorrentDetails {
export interface AddTorrentResponse { export interface AddTorrentResponse {
id: number | null; id: number | null;
details: TorrentDetails; details: TorrentDetails;
seen_peers?: Array<string>;
} }
export interface ListTorrentsResponse { export interface ListTorrentsResponse {
@ -147,7 +148,10 @@ export const API = {
}, },
uploadTorrent: (data: string | File, opts?: { uploadTorrent: (data: string | File, opts?: {
listOnly?: boolean, selectedFiles?: Array<number>, unpopularTorrent?: boolean, listOnly?: boolean,
selectedFiles?: Array<number>,
unpopularTorrent?: boolean,
initialPeers?: Array<string>,
}): Promise<AddTorrentResponse> => { }): Promise<AddTorrentResponse> => {
opts = opts || {}; opts = opts || {};
let url = '/torrents?&overwrite=true'; let url = '/torrents?&overwrite=true';
@ -160,6 +164,9 @@ export const API = {
if (opts.unpopularTorrent) { if (opts.unpopularTorrent) {
url += '&peer_connect_timeout=20&peer_read_write_timeout=60'; url += '&peer_connect_timeout=20&peer_read_write_timeout=60';
} }
if (opts.initialPeers) {
url += `&initial_peers=${opts.initialPeers.join(',')}`;
}
return makeRequest('POST', url, data) return makeRequest('POST', url, data)
}, },

View file

@ -378,11 +378,11 @@ const ErrorComponent = (props: { error: Error, remove?: () => void }) => {
const UploadButton = ({ buttonText, onClick, data, resetData, variant }) => { const UploadButton = ({ buttonText, onClick, data, resetData, variant }) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [fileList, setFileList] = useState([]); const [listTorrentResponse, setListTorrentResponse] = useState<AddTorrentResponse>(null);
const [fileListError, setFileListError] = useState(null); const [listTorrentError, setListTorrentError] = useState<Error>(null);
const ctx = useContext(AppContext); const ctx = useContext(AppContext);
const showModal = data !== null || fileListError !== null; const showModal = data !== null || listTorrentError !== null;
// Get the torrent file list if there's data. // Get the torrent file list if there's data.
useEffect(() => { useEffect(() => {
@ -394,9 +394,9 @@ const UploadButton = ({ buttonText, onClick, data, resetData, variant }) => {
setLoading(true); setLoading(true);
try { try {
const response = await API.uploadTorrent(data, { listOnly: true }); const response = await API.uploadTorrent(data, { listOnly: true });
setFileList(response.details.files); setListTorrentResponse(response);
} catch (e) { } catch (e) {
setFileListError({ text: 'Error uploading torrent', details: e }); setListTorrentError({ text: 'Error listing torrent files', details: e });
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -406,8 +406,8 @@ const UploadButton = ({ buttonText, onClick, data, resetData, variant }) => {
const clear = () => { const clear = () => {
resetData(); resetData();
setFileListError(null); setListTorrentError(null);
setFileList([]); setListTorrentResponse(null);
setLoading(false); setLoading(false);
} }
@ -420,10 +420,10 @@ const UploadButton = ({ buttonText, onClick, data, resetData, variant }) => {
<FileSelectionModal <FileSelectionModal
show={showModal} show={showModal}
onHide={clear} onHide={clear}
fileListError={fileListError} listTorrentError={listTorrentError}
fileList={fileList} listTorrentResponse={listTorrentResponse}
data={data} data={data}
fileListLoading={loading} listTorrentLoading={loading}
/> />
</> </>
); );
@ -471,12 +471,12 @@ const FileInput = () => {
const FileSelectionModal = (props: { const FileSelectionModal = (props: {
show: boolean, show: boolean,
onHide: () => void, onHide: () => void,
fileList: Array<TorrentFile>, listTorrentResponse: AddTorrentResponse,
fileListError: Error, listTorrentError: Error,
fileListLoading: boolean, listTorrentLoading: boolean,
data: string | File data: string | File
}) => { }) => {
let { show, onHide, fileList, fileListError, fileListLoading, data } = props; let { show, onHide, listTorrentResponse, listTorrentError, listTorrentLoading, data } = props;
const [selectedFiles, setSelectedFiles] = useState([]); const [selectedFiles, setSelectedFiles] = useState([]);
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
@ -485,8 +485,8 @@ const FileSelectionModal = (props: {
const ctx = useContext(AppContext); const ctx = useContext(AppContext);
useEffect(() => { useEffect(() => {
setSelectedFiles(fileList.map((_, id) => id)); setSelectedFiles(listTorrentResponse ? listTorrentResponse.details.files.map((_, id) => id) : []);
}, [fileList]); }, [listTorrentResponse]);
const clear = () => { const clear = () => {
onHide(); onHide();
@ -505,7 +505,8 @@ const FileSelectionModal = (props: {
const handleUpload = async () => { const handleUpload = async () => {
setUploading(true); setUploading(true);
API.uploadTorrent(data, { selectedFiles, unpopularTorrent }).then(() => { let initialPeers = listTorrentResponse.seen_peers ? listTorrentResponse.seen_peers.slice(0, 32) : null;
API.uploadTorrent(data, { selectedFiles, unpopularTorrent, initialPeers }).then(() => {
onHide(); onHide();
ctx.refreshTorrents(); ctx.refreshTorrents();
}, },
@ -518,16 +519,16 @@ const FileSelectionModal = (props: {
return ( return (
<Modal show={show} onHide={clear} size='lg'> <Modal show={show} onHide={clear} size='lg'>
<Modal.Header closeButton> <Modal.Header closeButton>
{!!fileListError || <Modal.Title>Add torrent</Modal.Title>} <Modal.Title>Add torrent</Modal.Title>
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>
<Form> <Form>
<fieldset className='mb-5'> <fieldset className='mb-5'>
<legend>Pick the files to download</legend> <legend>Pick the files to download</legend>
{fileListLoading ? <Spinner /> {listTorrentLoading ? <Spinner />
: fileListError ? <ErrorComponent error={fileListError}></ErrorComponent> : : listTorrentError ? <ErrorComponent error={listTorrentError}></ErrorComponent> :
<> <>
{fileList.map((file, index) => ( {listTorrentResponse?.details.files.map((file, index) => (
<Form.Group key={index} controlId={`check-${index}`}> <Form.Group key={index} controlId={`check-${index}`}>
<Form.Check <Form.Check
type="checkbox" type="checkbox"
@ -559,7 +560,7 @@ const FileSelectionModal = (props: {
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
{uploading && <Spinner />} {uploading && <Spinner />}
<Button variant="primary" onClick={handleUpload} disabled={fileListLoading || uploading || selectedFiles.length == 0}> <Button variant="primary" onClick={handleUpload} disabled={listTorrentLoading || uploading || selectedFiles.length == 0}>
OK OK
</Button> </Button>
<Button variant="secondary" onClick={clear}> <Button variant="secondary" onClick={clear}>

View file

@ -437,7 +437,7 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()>
) )
.await .await
{ {
Ok(ApiAddTorrentResponse { id, details }) => { Ok(ApiAddTorrentResponse { id, details, .. }) => {
if let Some(id) = id { if let Some(id) = id {
info!("{} added to the server with index {}. Query {}/torrents/{}/(stats/haves) for details", details.info_hash, id, http_api_url, id) info!("{} added to the server with index {}. Query {}/torrents/{}/(stats/haves) for details", details.info_hash, id, http_api_url, id)
} }
@ -509,6 +509,7 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()>
info_hash: _, info_hash: _,
info, info,
only_files, only_files,
..
}) => { }) => {
for (idx, (filename, len)) in for (idx, (filename, len)) in
info.iter_filenames_and_lengths()?.enumerate() info.iter_filenames_and_lengths()?.enumerate()