Skip to content

Commit ec39056

Browse files
author
houssemko
committed
Stop-and-keep live HLS captures instead of cancelling
1 parent 71fdc9c commit ec39056

6 files changed

Lines changed: 336 additions & 46 deletions

File tree

‎po/grab.pot‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1000,4 +1000,12 @@ msgstr ""
10001000

10011001
#: src/video.rs:333
10021002
msgid "video-only"
1003-
msgstr ""
1003+
msgstr ""
1004+
1005+
#: src/window.rs:104
1006+
msgid "Stop recording"
1007+
msgstr ""
1008+
1009+
#: src/window.rs:101
1010+
msgid "Stop"
1011+
msgstr ""

‎src/download.rs‎

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1704,6 +1704,11 @@ pub struct DownloadManager {
17041704
/// dropped) from pause/park/cancel paths so the worker stops its
17051705
/// extractor streams promptly; the pump tail also drops them.
17061706
video_abort: RefCell<HashMap<u64, tokio::sync::oneshot::Sender<()>>>,
1707+
/// Rows currently capturing a live stream. Pause/cancel/park only
1708+
/// signal these (no task abort, no status preset): the worker
1709+
/// finalizes the partial and its message drives the row to Done.
1710+
/// Set per attempt in `spawn_video`; dropped on terminal messages.
1711+
live_rows: RefCell<std::collections::HashSet<u64>>,
17071712
/// Set by shutdown(): stale engine futures must not re-persist or
17081713
/// re-mark rows once the authoritative shutdown persist has run.
17091714
draining: Cell<bool>,
@@ -1743,6 +1748,7 @@ impl DownloadManager {
17431748
server_mtime: RefCell::new(HashMap::new()),
17441749
video_sources: RefCell::new(HashMap::new()),
17451750
video_abort: RefCell::new(HashMap::new()),
1751+
live_rows: RefCell::new(std::collections::HashSet::new()),
17461752
draining: Cell::new(false),
17471753
});
17481754
// Live preferences: raising the download limit must wake queued
@@ -1844,6 +1850,12 @@ impl DownloadManager {
18441850
self.video_sources.borrow().get(&id).cloned()
18451851
}
18461852

1853+
/// Whether the row is currently capturing a live stream (stop-and-keep
1854+
/// applies: pausing/cancelling finalizes instead of discarding).
1855+
pub fn is_live_video(&self, id: u64) -> bool {
1856+
self.live_rows.borrow().contains(&id)
1857+
}
1858+
18471859
/// Validate, dedupe and queue a download, starting it when a slot is free.
18481860
///
18491861
/// # Errors
@@ -1967,9 +1979,7 @@ impl DownloadManager {
19671979
page_url: &str,
19681980
dest_dir: Option<&str>,
19691981
filename: Option<&str>,
1970-
quality: &str,
1971-
audio_only: bool,
1972-
video_format_id: Option<&str>,
1982+
choices: crate::video::VideoChoices,
19731983
) -> Result<DownloadItem, String> {
19741984
let url = normalize_url(page_url)?;
19751985
if !crate::video::is_video_page(&url) {
@@ -1992,7 +2002,7 @@ impl DownloadManager {
19922002
})
19932003
});
19942004
let item = DownloadItem::new(self.alloc_id(), &url, &name, &dir);
1995-
item.set_detail(if audio_only {
2005+
item.set_detail(if choices.audio_only {
19962006
gettext("Waiting to resolve audio…")
19972007
} else {
19982008
gettext("Waiting to resolve media…")
@@ -2003,9 +2013,10 @@ impl DownloadManager {
20032013
page_url: url,
20042014
media_url: None,
20052015
expires_at: None,
2006-
quality: quality.to_string(),
2007-
audio_only,
2008-
video_format_id: video_format_id.map(|s| s.to_string()),
2016+
quality: choices.quality,
2017+
audio_only: choices.audio_only,
2018+
is_live: choices.is_live,
2019+
video_format_id: choices.video_format_id,
20092020
},
20102021
);
20112022
Ok(self.insert(item))
@@ -2182,11 +2193,19 @@ impl DownloadManager {
21822193
page_url,
21832194
quality,
21842195
audio_only,
2196+
is_live,
21852197
video_format_id,
21862198
..
21872199
}) = self.video_source(item.id())
21882200
{
2189-
return self.spawn_video(item, page_url, quality, audio_only, video_format_id);
2201+
return self.spawn_video(
2202+
item,
2203+
page_url,
2204+
quality,
2205+
audio_only,
2206+
video_format_id,
2207+
is_live,
2208+
);
21902209
}
21912210
let connections = (opts.connections.max(1) as usize).min(16);
21922211
let timeout = Duration::from_secs(opts.timeout.max(1) as u64);
@@ -2580,6 +2599,7 @@ impl DownloadManager {
25802599
}
25812600
this.running.borrow_mut().remove(&id);
25822601
this.video_abort.borrow_mut().remove(&id);
2602+
this.live_rows.borrow_mut().remove(&id);
25832603
if this.draining.get() {
25842604
return;
25852605
}
@@ -2673,6 +2693,7 @@ impl DownloadManager {
26732693
quality: String,
26742694
audio_only: bool,
26752695
video_format_id: Option<String>,
2696+
is_live: bool,
26762697
) {
26772698
let id = item.id();
26782699
let generation = self.epoch.borrow().get(&id).cloned().unwrap_or(0) + 1;
@@ -2681,6 +2702,13 @@ impl DownloadManager {
26812702
let (abort_tx, abort_rx) = tokio::sync::oneshot::channel();
26822703
// Overwrite any stale sender: its task is dead or guarded stale.
26832704
self.video_abort.borrow_mut().insert(id, abort_tx);
2705+
// Live rows finalize in the worker on stop: track them so
2706+
// pause/cancel/park signal without aborting or presetting.
2707+
if is_live {
2708+
self.live_rows.borrow_mut().insert(id);
2709+
} else {
2710+
self.live_rows.borrow_mut().remove(&id);
2711+
}
26842712
let opts = DownloadOptions::from_settings(&self.settings);
26852713
let job = crate::video::VideoJob {
26862714
item_id: id,
@@ -2692,6 +2720,7 @@ impl DownloadManager {
26922720
timeout_secs: opts.timeout.max(1) as u64,
26932721
user_agent: opts.user_agent.clone(),
26942722
video_format_id,
2723+
is_live,
26952724
cookies_browser: self.settings.cookies_browser(),
26962725
};
26972726
let handle = tokio_rt().spawn(async move {
@@ -2751,6 +2780,14 @@ impl DownloadManager {
27512780

27522781
/// Pause a running download, freeing its slot for the next queued item.
27532782
pub fn pause(self: &Rc<Self>, id: u64) {
2783+
if self.live_rows.borrow().contains(&id) {
2784+
// Live captures finalize in the worker: signal it and let
2785+
// its Finished flip the row to Done. Aborting the task or
2786+
// presetting Paused would drop the recording or strand it.
2787+
// The pump tail frees the slot on completion.
2788+
self.stop_video_worker(id);
2789+
return;
2790+
}
27542791
self.stop_video_worker(id);
27552792
if let Some(handle) = self.running.borrow().get(&id) {
27562793
handle.abort();
@@ -2864,8 +2901,13 @@ impl DownloadManager {
28642901

28652902
/// Stop the engine for `id`, keeping file, bitmap and progress, and
28662903
/// mark it queued. Unlike pause the row yields its slot; unlike cancel
2867-
/// nothing is deleted and progress is kept.
2904+
/// nothing is deleted and progress is kept. Live rows only signal:
2905+
/// the worker finalizes and its message completes the row.
28682906
fn park(&self, id: u64) {
2907+
if self.live_rows.borrow().contains(&id) {
2908+
self.stop_video_worker(id);
2909+
return;
2910+
}
28692911
self.stop_video_worker(id);
28702912
if let Some(handle) = self.running.borrow().get(&id) {
28712913
handle.abort();
@@ -2927,11 +2969,17 @@ impl DownloadManager {
29272969
}
29282970

29292971
fn cancel_inner(&self, id: u64, keep_partial: bool) {
2972+
// Live rows keep what's recorded (Stop, not Cancel): signal the
2973+
// worker and skip the task abort plus the Cancelled preset, so
2974+
// its Finished still lands. Discard via explicit row removal.
2975+
let live = self.live_rows.borrow().contains(&id);
29302976
self.stop_video_worker(id);
2931-
if let Some(handle) = self.running.borrow().get(&id) {
2932-
handle.abort();
2977+
if !live {
2978+
if let Some(handle) = self.running.borrow().get(&id) {
2979+
handle.abort();
2980+
}
2981+
self.running.borrow_mut().remove(&id);
29332982
}
2934-
self.running.borrow_mut().remove(&id);
29352983
self.pending_names.borrow_mut().remove(&id);
29362984
let had_segments = self.segment_state.borrow_mut().remove(&id).is_some();
29372985
self.torrent_pieces.borrow_mut().remove(&id);
@@ -2948,8 +2996,10 @@ impl DownloadManager {
29482996
if crate::torrent::is_torrent(&item.url()) && item.status() != DownloadStatus::Done {
29492997
crate::torrent::forget_download(id);
29502998
}
2951-
item.set_status(DownloadStatus::Cancelled);
2952-
item.set_detail(item.status().label());
2999+
if !live {
3000+
item.set_status(DownloadStatus::Cancelled);
3001+
item.set_detail(item.status().label());
3002+
}
29533003
}
29543004
}
29553005

‎src/download_tests.rs‎

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -640,9 +640,12 @@ fn enqueue_video_spawns_and_fails_without_tools() {
640640
"https://www.youtube.com/watch?v=gXtp6C-3JKo",
641641
Some(&dest),
642642
Some("My Video.mp4"),
643-
"1080p",
644-
false,
645-
None,
643+
crate::video::VideoChoices {
644+
quality: "1080p".to_string(),
645+
audio_only: false,
646+
video_format_id: None,
647+
is_live: false,
648+
},
646649
)
647650
.expect("video enqueue");
648651
let id = item.id();
@@ -676,9 +679,12 @@ fn enqueue_video_rejects_direct_url() {
676679
"https://example.com/f.iso",
677680
None,
678681
None,
679-
"1080p",
680-
false,
681-
None
682+
crate::video::VideoChoices {
683+
quality: "1080p".to_string(),
684+
audio_only: false,
685+
video_format_id: None,
686+
is_live: false,
687+
},
682688
)
683689
.is_err()
684690
);
@@ -701,9 +707,12 @@ fn video_source_survives_restore_and_retry() {
701707
"https://vimeo.com/123456",
702708
Some(&dest),
703709
Some("Clip.mp4"),
704-
"720p",
705-
true,
706-
None,
710+
crate::video::VideoChoices {
711+
quality: "720p".to_string(),
712+
audio_only: true,
713+
video_format_id: None,
714+
is_live: false,
715+
},
707716
)
708717
.expect("video enqueue");
709718
// Persisted with the Page marker (not silently dropped).
@@ -752,6 +761,7 @@ fn mismatched_video_source_dropped_on_restore() {
752761
expires_at: None,
753762
quality: "1080p".into(),
754763
audio_only: false,
764+
is_live: false,
755765
video_format_id: None,
756766
}),
757767
}],
@@ -787,9 +797,12 @@ fn unremove_restores_video_source() {
787797
"https://vimeo.com/123456",
788798
Some(&dest),
789799
Some("Clip.mp4"),
790-
"720p",
791-
false,
792-
None,
800+
crate::video::VideoChoices {
801+
quality: "720p".to_string(),
802+
audio_only: false,
803+
video_format_id: None,
804+
is_live: false,
805+
},
793806
)
794807
.expect("video enqueue");
795808
let id = item.id();
@@ -835,9 +848,12 @@ fn queue_file_never_carries_cookies() {
835848
"https://vimeo.com/123456",
836849
Some("/tmp/dl"),
837850
Some("Clip.mp4"),
838-
"1080p",
839-
false,
840-
None,
851+
crate::video::VideoChoices {
852+
quality: "1080p".to_string(),
853+
audio_only: false,
854+
video_format_id: None,
855+
is_live: false,
856+
},
841857
)
842858
.expect("video enqueue");
843859
// Drain the spawned worker (fails fast without tools) so no woken pump

0 commit comments

Comments
 (0)