Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 56 additions & 46 deletions src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,29 +147,41 @@ async fn intake_remote_torrent(
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "download".to_string());
// Multi-file torrents offer per-file switches in the inline picker's
// right-sliding page.
match crate::torrent::torrent_file_list(&bytes) {
Ok((_, entries)) if entries.len() > 1 => {
add_card.open_torrent_picker(file_name, bytes, entries);
// Picker opened, not queued: return OpenedCard (202) so the
// extension keeps the browser copy until the user chooses.
Routed::OpenedCard
}
Ok(_) => {
if let Err(e) = manager
.enqueue_torrent_file_async(bytes, &stem, None, None)
.await
{
fail(e);
return Routed::Failed;
}
Routed::Queued
// Parse off the GTK thread: bencode decode and the info-hash hash are
// CPU work on up to 10 MB.
let parsed = gio::spawn_blocking(move || {
let list = crate::torrent::torrent_file_list(&bytes)?;
Ok::<_, String>((bytes, list))
})
.await;
let (bytes, (_tname, entries)) = match parsed {
Ok(Ok(data)) => data,
Ok(Err(e)) => {
fail(e);
return Routed::Failed;
}
Err(e) => {
tracing::warn!(error = ?e, "torrent parse worker failed");
fail(gettext("Could not read that .torrent file"));
return Routed::Failed;
}
};
// Multi-file torrents offer per-file switches in the inline picker's
// right-sliding page.
if entries.len() > 1 {
add_card.open_torrent_picker(file_name, bytes, entries);
// Picker opened, not queued: return OpenedCard (202) so the
// extension keeps the browser copy until the user chooses.
Routed::OpenedCard
} else {
if let Err(e) = manager
.enqueue_torrent_file_async(bytes, &stem, None, None)
.await
{
fail(e);
Routed::Failed
return Routed::Failed;
}
Routed::Queued
}
}

Expand Down Expand Up @@ -551,36 +563,34 @@ fn handle_open_files(s: Rc<State>, files: Vec<gio::File>) {
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| format!("{stem}.torrent"));
glib::spawn_future_local(async move {
let bytes =
gio::spawn_blocking(move || crate::torrent::read_torrent_bytes(&path))
.await
.ok()
.flatten();
let Some(bytes) = bytes else {
toasts.add_toast(adw::Toast::new(&gettext(
"Could not read that .torrent file",
)));
return;
};
// Multi-file torrents offer per-file switches in the
// inline picker's right-sliding page.
match crate::torrent::torrent_file_list(&bytes) {
Ok((_, entries)) if entries.len() > 1 => {
add_card.open_torrent_picker(file_name, bytes, entries);
}
Ok(_) => {
if let Err(e) = manager
.enqueue_torrent_file_async(bytes, &stem, None, None)
.await
{
toasts.add_toast(adw::Toast::new(&crate::ui_util::esc_markup(
&e,
)));
}
// Read and parse off the GTK thread: a large multi-file
// torrent must not stall the main loop.
let loaded =
gio::spawn_blocking(move || crate::torrent::load_torrent_file(&path))
.await;
let (bytes, _, entries) = match loaded {
Ok(Ok(data)) => data,
Ok(Err(e)) => {
toasts.add_toast(adw::Toast::new(&crate::ui_util::esc_markup(&e)));
return;
}
Err(e) => {
toasts.add_toast(adw::Toast::new(&crate::ui_util::esc_markup(&e)));
tracing::warn!(error = ?e, "torrent read/parse worker failed");
toasts.add_toast(adw::Toast::new(&gettext(
"Could not read that .torrent file",
)));
return;
}
};
// Multi-file torrents offer per-file switches in the
// inline picker's right-sliding page.
if entries.len() > 1 {
add_card.open_torrent_picker(file_name, bytes, entries);
} else if let Err(e) = manager
.enqueue_torrent_file_async(bytes, &stem, None, None)
.await
{
toasts.add_toast(adw::Toast::new(&crate::ui_util::esc_markup(&e)));
}
});
continue;
Expand Down
16 changes: 11 additions & 5 deletions src/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ struct RestoreSweepPlan {
dest_dirs: HashSet<String>,
state_keep: HashSet<(String, u64)>,
selections: Vec<(String, Vec<usize>)>,
session_keep: Option<HashSet<String>>,
session_urls: Option<Vec<String>>,
}

/// One validated queue entry awaiting the restore apply phase (module level so a mid-loop failure never leaves half-spawned engines).
Expand Down Expand Up @@ -4699,12 +4699,12 @@ impl DownloadManager {
.filter(|it| it.status() != DownloadStatus::Done)
.map(|it| (it.dest_dir(), it.id()))
.collect();
let session_keep = crate::torrent::session_handle().is_some().then(|| {
let session_urls = crate::torrent::session_handle().is_some().then(|| {
self.items()
.filter(|it| {
it.status() != DownloadStatus::Done && crate::torrent::is_torrent(&it.url())
})
.filter_map(|it| crate::torrent::info_hash_for_url(&it.url()))
.map(|it| it.url().to_string())
.collect()
});
RestoreSweepPlan {
Expand All @@ -4713,7 +4713,7 @@ impl DownloadManager {
dest_dirs,
state_keep,
selections,
session_keep,
session_urls,
}
}

Expand All @@ -4728,7 +4728,13 @@ impl DownloadManager {
}
crate::video::sweep_state_staging(&plan.state_keep);
crate::cookies::sweep_cookie_staging();
plan.session_keep
// Hashing reads each torrent archive: do it here, on the blocking pool,
// not while building the plan on the GTK thread.
plan.session_urls.map(|urls| {
urls.into_iter()
.filter_map(|url| crate::torrent::info_hash_for_url(&url))
.collect()
})
}

fn schedule_session_orphan_sweep(keep: Option<HashSet<String>>) {
Expand Down
25 changes: 14 additions & 11 deletions src/inline_add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1640,20 +1640,23 @@ fn wire_torrent_picker(
.basename()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|| "download.torrent".to_string());
let bytes = match file
.path()
.and_then(|p| crate::torrent::read_torrent_bytes(&p))
{
Some(b) => b,
None => {
set_field_error(&torrent_row, &gettext("Could not read that .torrent file"));
let Some(path) = file.path() else {
set_field_error(&torrent_row, &gettext("Could not read that .torrent file"));
return;
};
// Read and parse off the GTK thread: a large multi-file torrent
// must not stall the main loop.
let loaded =
gio::spawn_blocking(move || crate::torrent::load_torrent_file(&path)).await;
let (bytes, _, entries) = match loaded {
Ok(Ok(data)) => data,
Ok(Err(e)) => {
set_field_error(&torrent_row, &e);
return;
}
};
let (_tname, entries) = match crate::torrent::torrent_file_list(&bytes) {
Ok(v) => v,
Err(e) => {
set_field_error(&torrent_row, &e);
tracing::warn!(error = ?e, "torrent read/parse worker failed");
set_field_error(&torrent_row, &gettext("Could not read that .torrent file"));
return;
}
};
Expand Down
12 changes: 12 additions & 0 deletions src/torrent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,18 @@ pub(crate) fn read_torrent_bytes(path: &std::path::Path) -> Option<Vec<u8>> {
(buf.len() as u64 <= MAX_TORRENT_BYTES).then_some(buf)
}

/// Read and parse a `.torrent` file in one call, so UI paths can hand the
/// whole job to a blocking pool.
pub(crate) fn load_torrent_file(
path: &std::path::Path,
) -> Result<(Vec<u8>, String, Vec<TorrentFileEntry>), String> {
let Some(bytes) = read_torrent_bytes(path) else {
return Err(gettext("Could not read that .torrent file"));
};
let (name, entries) = torrent_file_list(&bytes)?;
Ok((bytes, name, entries))
}

/// Bounded read of the archived `.torrent` behind a queue pseudo-URL (`None` when missing, unreadable, or over the ceiling).
pub(crate) fn read_archive_bytes(url: &str) -> Option<Vec<u8>> {
read_torrent_bytes(&archive_path_for_url(url)?)
Expand Down
Loading