1use crate::{DownloadEvent, DownloadOptions, Format, FormatCatalog, FormatChoice, role_accepts};
2use crate::{RsyncSyntax, SyncCommand};
3use rsynko_manager::*;
4use rsynko_media::portable_file_name;
5use rsynko_rsync::{
6 RsyncEndpointExt, SyncCommandExt, SyncCommandViewAlg, SyncMode, SyncProfile, sync_profile,
7};
8use rsynko_ui::{DiscoveryState, FormatChoiceViewAlg, FormatLabelExt, FormatRolesExt};
9use std::ops::Not;
10use std::path::{Path, PathBuf};
11use std::time::Duration;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub struct QueueId(pub u64);
16
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
19pub struct MemoryManager;
20
21#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct SourceRequest {
24 pub source: String,
26 pub output: Option<PathBuf>,
28 pub options: DownloadOptions,
30}
31
32impl SourceRequest {
33 #[must_use]
35 pub fn new(
36 source: impl Into<String>,
37 output: Option<PathBuf>,
38 options: DownloadOptions,
39 ) -> Self {
40 Self {
41 source: source.into(),
42 output,
43 options,
44 }
45 }
46}
47
48impl ManagerSorts for MemoryManager {
49 type Id = ();
50 type Source = SourceRequest;
51 type Options = DownloadOptions;
52 type Output = Option<PathBuf>;
53 type Format = Format;
54 type Change = PlannedChange;
55 type Entry = ();
56 type Downloads = ManagerState;
57}
58
59impl ManagerSorts for ManagerState {
60 type Id = QueueId;
61 type Source = SourceRequest;
62 type Options = DownloadOptions;
63 type Output = Option<PathBuf>;
64 type Format = Format;
65 type Change = PlannedChange;
66 type Entry = QueueEntry;
67 type Downloads = ();
68}
69
70impl DownloadsAlg for MemoryManager {
71 fn empty_downloads(&self) -> Self::Downloads {
72 ManagerState::downloads()
73 }
74}
75
76impl SourceRequestAlg for MemoryManager {
77 fn source(
78 &self,
79 input: impl Into<String>,
80 output: Self::Output,
81 options: Self::Options,
82 ) -> Self::Source {
83 SourceRequest::new(input, output, options)
84 }
85}
86
87impl MediaOptionsAlg for MemoryManager {
88 fn progressive(&self) -> Self::Options {
89 DownloadOptions::progressive()
90 }
91
92 fn audio(&self) -> Self::Options {
93 DownloadOptions::audio()
94 }
95
96 fn video(&self) -> Self::Options {
97 DownloadOptions::video()
98 }
99}
100
101impl SourceRecognitionAlg for MemoryManager {
102 fn recognizes_source(&self, line: &str) -> bool {
103 recognized(line.trim())
104 }
105}
106
107impl SubmissionAlg for MemoryManager {
108 fn submitted(&self, line: &str) -> Self::Source {
109 submitted(line)
110 }
111}
112
113impl OutputChoiceAlg for MemoryManager {
114 fn suggested_output(&self) -> Self::Output {
115 None
116 }
117
118 fn exact_output(&self, path: impl Into<PathBuf>) -> Self::Output {
119 Some(path.into())
120 }
121}
122
123impl SourceRequestAlg for ManagerState {
124 fn source(
125 &self,
126 input: impl Into<String>,
127 output: Self::Output,
128 options: Self::Options,
129 ) -> Self::Source {
130 SourceRequest::new(input, output, options)
131 }
132}
133
134impl MediaOptionsAlg for ManagerState {
135 fn progressive(&self) -> Self::Options {
136 DownloadOptions::progressive()
137 }
138
139 fn audio(&self) -> Self::Options {
140 DownloadOptions::audio()
141 }
142
143 fn video(&self) -> Self::Options {
144 DownloadOptions::video()
145 }
146}
147
148impl SourceRecognitionAlg for ManagerState {
149 fn recognizes_source(&self, line: &str) -> bool {
150 recognized(line.trim())
151 }
152}
153
154impl SubmissionAlg for ManagerState {
155 fn submitted(&self, line: &str) -> Self::Source {
156 submitted(line)
157 }
158}
159
160impl OutputChoiceAlg for ManagerState {
161 fn suggested_output(&self) -> Self::Output {
162 None
163 }
164
165 fn exact_output(&self, path: impl Into<PathBuf>) -> Self::Output {
166 Some(path.into())
167 }
168}
169
170#[derive(Clone, Debug, PartialEq, Eq)]
172pub struct TransferState {
173 phase: TransferPhase,
174 destination: Option<PathBuf>,
175 downloaded: u64,
176 total: Option<u64>,
177 elapsed: Duration,
178 terminal: Option<DownloadEvent>,
179 summary: Option<String>,
180 detail: Option<String>,
181 pause_supported: bool,
182 resume_phase: Option<TransferPhase>,
183}
184
185impl TransferState {
186 fn ready() -> Self {
187 Self {
188 phase: TransferPhase::Ready,
189 destination: None,
190 downloaded: 0,
191 total: None,
192 elapsed: Duration::ZERO,
193 terminal: None,
194 summary: None,
195 detail: None,
196 pause_supported: false,
197 resume_phase: None,
198 }
199 }
200
201 #[must_use]
203 pub const fn phase(&self) -> TransferPhase {
204 self.phase
205 }
206
207 #[must_use]
209 pub fn destination(&self) -> Option<&Path> {
210 self.destination.as_deref()
211 }
212
213 #[must_use]
215 pub const fn downloaded(&self) -> u64 {
216 self.downloaded
217 }
218
219 #[must_use]
221 pub const fn total(&self) -> Option<u64> {
222 self.total
223 }
224
225 #[must_use]
227 pub const fn elapsed(&self) -> Duration {
228 self.elapsed
229 }
230
231 #[must_use]
233 pub const fn terminal(&self) -> Option<&DownloadEvent> {
234 self.terminal.as_ref()
235 }
236
237 #[must_use]
239 pub fn summary(&self) -> Option<&str> {
240 self.summary.as_deref()
241 }
242
243 #[must_use]
245 pub fn detail(&self) -> Option<&str> {
246 self.detail.as_deref()
247 }
248}
249
250impl TransferViewAlg for TransferState {
251 fn transferred(&self) -> u64 {
252 self.downloaded
253 }
254
255 fn transfer_total(&self) -> Option<u64> {
256 self.total
257 }
258
259 fn transfer_elapsed(&self) -> Duration {
260 self.elapsed
261 }
262
263 fn transfer_complete(&self) -> bool {
264 self.phase == TransferPhase::Complete
265 }
266
267 fn transfer_destination(&self) -> Option<&Path> {
268 self.destination()
269 }
270
271 fn transfer_summary(&self) -> Option<&str> {
272 self.summary()
273 }
274
275 fn transfer_detail(&self) -> Option<&str> {
276 self.detail()
277 }
278}
279
280#[derive(Clone, Debug, PartialEq)]
282pub struct QueueEntry {
283 id: QueueId,
284 request: SourceRequest,
285 transfer: TransferState,
286 formats: FormatCatalog,
287 media_id: Option<String>,
288 title: Option<String>,
289 output_custom: bool,
290 log: Vec<String>,
291 rehearsal: Rehearsal,
292}
293
294#[derive(Clone, Debug, PartialEq, Eq)]
296pub enum Rehearsal {
297 Unrehearsed,
299 Rehearsing,
301 Reported(Vec<PlannedChange>),
303 Failed(String),
305}
306
307#[derive(Clone, Debug, PartialEq, Eq, derive_new::new)]
309pub struct PlannedChange {
310 pub path: String,
312 pub kind: ChangeKind,
314 pub size: Option<u64>,
316}
317
318impl PlannedChangeAlg for PlannedChange {
319 fn change_path(&self) -> &str {
320 &self.path
321 }
322
323 fn change_kind(&self) -> ChangeKind {
324 self.kind
325 }
326
327 fn change_size(&self) -> Option<u64> {
328 self.size
329 }
330}
331
332impl QueueEntry {
333 #[must_use]
335 pub const fn id(&self) -> QueueId {
336 self.id
337 }
338
339 #[must_use]
341 pub fn source(&self) -> &str {
342 &self.request.source
343 }
344
345 #[must_use]
347 pub fn output(&self) -> Option<&Path> {
348 self.request.output.as_deref()
349 }
350
351 #[must_use]
353 pub fn title(&self) -> Option<&str> {
354 self.title.as_deref()
355 }
356
357 #[must_use]
359 pub const fn transfer(&self) -> &TransferState {
360 &self.transfer
361 }
362
363 #[must_use]
365 pub const fn options(&self) -> &DownloadOptions {
366 &self.request.options
367 }
368
369 #[must_use]
371 pub const fn formats(&self) -> &FormatCatalog {
372 &self.formats
373 }
374
375 #[must_use]
377 pub fn is_editable(&self) -> bool {
378 self.transfer.phase == TransferPhase::Ready
379 }
380
381 #[must_use]
383 pub fn detail_controls(&self) -> Vec<DetailControl> {
384 let rehearsed = matches!(self.rehearsal, Rehearsal::Unrehearsed)
385 .not()
386 .then_some(DetailControl::Report);
387 let rehearsal = self
388 .request
389 .options
390 .dry_run()
391 .is_some()
392 .then_some(DetailControl::DryRun);
393 let commanded = self
395 .stated_command()
396 .is_some()
397 .then_some(DetailControl::Command);
398 match self.transfer.phase {
399 TransferPhase::Ready => [
400 (self.output_naming() == OutputNaming::Stated).then_some(DetailControl::Input),
402 Some(DetailControl::Output),
403 (self.request.options.media_streams().is_some()
405 || self.request.options.profile().is_some())
406 .then_some(DetailControl::Format),
407 commanded,
408 rehearsed,
409 Some(DetailControl::Log),
410 rehearsal,
411 Some(DetailControl::Duplicate),
412 Some(DetailControl::Delete),
413 ]
414 .into_iter()
415 .flatten()
416 .collect(),
417 TransferPhase::Failed => [
419 commanded,
420 rehearsed,
421 Some(DetailControl::Log),
422 Some(DetailControl::Restart),
423 Some(DetailControl::Duplicate),
424 Some(DetailControl::Delete),
425 ]
426 .into_iter()
427 .flatten()
428 .collect(),
429 TransferPhase::Rehearsing
430 | TransferPhase::Waiting
431 | TransferPhase::Extracting
432 | TransferPhase::Downloading
433 | TransferPhase::Paused
434 | TransferPhase::Publishing
435 | TransferPhase::Complete => [
436 commanded,
437 rehearsed,
438 Some(DetailControl::Log),
439 Some(DetailControl::Duplicate),
440 Some(DetailControl::Delete),
441 ]
442 .into_iter()
443 .flatten()
444 .collect(),
445 }
446 }
447
448 #[must_use]
450 pub fn space_action(&self) -> Option<SpaceAction> {
451 match self.transfer.phase {
452 TransferPhase::Ready if self.output().is_some() => Some(match self.dry_run() {
453 Some(true) => SpaceAction::Rehearse,
454 Some(false) | None => SpaceAction::Start,
455 }),
456 TransferPhase::Extracting | TransferPhase::Downloading
457 if self.transfer.pause_supported =>
458 {
459 Some(SpaceAction::Pause)
460 }
461 TransferPhase::Paused => Some(SpaceAction::Resume),
462 TransferPhase::Ready
463 | TransferPhase::Waiting
464 | TransferPhase::Rehearsing
465 | TransferPhase::Extracting
466 | TransferPhase::Downloading
467 | TransferPhase::Publishing
468 | TransferPhase::Complete
469 | TransferPhase::Failed => None,
470 }
471 }
472
473 #[must_use]
480 pub fn transfer_command(&self) -> Option<SyncCommand> {
481 let dry_run = self.request.options.dry_run()?;
482 let destination = self.output()?;
483 let mode = SyncMode::transfer()
484 .rehearsed(dry_run)
485 .profiled(self.request.options.profile()?);
486 Some(RsyncSyntax.transfer_command(
487 &RsyncSyntax.read_endpoint(&self.request.source),
488 &RsyncSyntax.read_endpoint(&destination.display().to_string()),
489 mode,
490 ))
491 }
492
493 #[must_use]
495 pub fn stated_command(&self) -> Option<String> {
496 let command = self.transfer_command()?;
497 Some(format!(
498 "{} {}",
499 RsyncSyntax.command_program(&command),
500 RsyncSyntax
501 .command_arguments(&command)
502 .collect::<Vec<_>>()
503 .join(" ")
504 ))
505 }
506
507 pub fn log(&self) -> impl DoubleEndedIterator<Item = &str> {
509 self.log.iter().map(String::as_str)
510 }
511
512 fn note(&mut self, note: impl Into<String>) {
514 self.log.push(note.into());
515 }
516
517 fn forget_rehearsal(&mut self) {
522 if matches!(self.rehearsal, Rehearsal::Unrehearsed) {
523 return;
524 }
525 self.rehearsal = Rehearsal::Unrehearsed;
526 self.note("the report was forgotten: it no longer states what would happen");
527 }
528
529 #[must_use]
531 pub fn label(&self) -> &str {
532 self.title
533 .as_deref()
534 .filter(|title| !title.is_empty())
535 .unwrap_or(&self.request.source)
536 }
537}
538
539impl TransferViewAlg for QueueEntry {
540 fn transferred(&self) -> u64 {
541 self.transfer.transferred()
542 }
543
544 fn transfer_total(&self) -> Option<u64> {
545 self.transfer.transfer_total()
546 }
547
548 fn transfer_elapsed(&self) -> Duration {
549 self.transfer.transfer_elapsed()
550 }
551
552 fn transfer_complete(&self) -> bool {
553 self.transfer.transfer_complete()
554 }
555
556 fn transfer_destination(&self) -> Option<&Path> {
557 self.transfer.transfer_destination()
558 }
559
560 fn transfer_summary(&self) -> Option<&str> {
561 self.transfer.transfer_summary()
562 }
563
564 fn transfer_detail(&self) -> Option<&str> {
565 self.transfer.transfer_detail()
566 }
567}
568
569impl QueueEntryAlg for QueueEntry {
570 fn label(&self) -> &str {
571 self.label()
572 }
573
574 fn source(&self) -> &str {
575 self.source()
576 }
577
578 fn phase(&self) -> TransferPhase {
579 self.transfer.phase()
580 }
581
582 fn dry_run(&self) -> Option<bool> {
583 self.request.options.dry_run()
584 }
585
586 fn output_naming(&self) -> OutputNaming {
587 if self.request.options.profile().is_some() {
589 OutputNaming::Stated
590 } else {
591 OutputNaming::Portable
592 }
593 }
594
595 fn stated_command(&self) -> Option<String> {
596 self.stated_command()
597 }
598
599 fn output(&self) -> Option<&Path> {
600 self.output()
601 }
602
603 fn is_editable(&self) -> bool {
604 self.is_editable()
605 }
606
607 fn detail_controls(&self) -> Vec<DetailControl> {
608 self.detail_controls()
609 }
610
611 fn download_log(&self) -> impl Iterator<Item = &str> {
612 self.log()
613 }
614
615 fn space_action(&self) -> Option<SpaceAction> {
616 self.space_action()
617 }
618}
619
620impl FormatChoiceViewAlg for QueueEntry {
621 type Format = Format;
622
623 fn discovery(&self) -> DiscoveryState<'_> {
624 match &self.formats {
625 FormatCatalog::Unknown => DiscoveryState::Unrequested,
626 FormatCatalog::Waiting => DiscoveryState::Waiting,
627 FormatCatalog::Inspecting => DiscoveryState::Inspecting,
628 FormatCatalog::Available(_) => DiscoveryState::Described,
629 FormatCatalog::Failed(message) => DiscoveryState::Failed(message),
630 }
631 }
632
633 fn described_formats(&self) -> impl Iterator<Item = &Self::Format> {
634 self.formats.available().unwrap_or_default().iter()
635 }
636}
637
638impl RehearsalViewAlg for QueueEntry {
639 type Change = PlannedChange;
640
641 fn rehearsal(&self) -> RehearsalState<'_> {
642 match &self.rehearsal {
643 Rehearsal::Unrehearsed => RehearsalState::Unrehearsed,
644 Rehearsal::Rehearsing => RehearsalState::Rehearsing,
645 Rehearsal::Reported(_) => RehearsalState::Reported,
646 Rehearsal::Failed(message) => RehearsalState::Failed(message),
647 }
648 }
649
650 fn planned_changes(&self) -> impl Iterator<Item = &Self::Change> {
651 match &self.rehearsal {
652 Rehearsal::Reported(changes) => changes.as_slice(),
653 Rehearsal::Unrehearsed | Rehearsal::Rehearsing | Rehearsal::Failed(_) => &[],
654 }
655 .iter()
656 }
657}
658
659impl RequestOptionsAlg for QueueEntry {
660 fn performer(&self) -> Performer {
661 self.request.options.performer()
662 }
663
664 fn media_streams(&self) -> Option<MediaStreams> {
665 self.request.options.media_streams()
666 }
667
668 fn chosen_choice(&self) -> Option<&str> {
669 if let Some(profile) = self.request.options.profile() {
671 return Some(sync_profile::to(profile));
672 }
673 match self.request.options.format() {
674 FormatChoice::Best => None,
675 FormatChoice::Id(id) => Some(id),
676 }
677 }
678
679 fn selectable_choices(&self) -> impl Iterator<Item = &str> {
680 let ways = self
681 .request
682 .options
683 .profile()
684 .map(|_| sync_profile::ALL)
685 .unwrap_or_default()
686 .iter()
687 .copied()
688 .map(sync_profile::to)
689 .collect::<Vec<_>>();
690 ways.into_iter().chain(
691 self.formats
692 .available()
693 .unwrap_or_default()
694 .iter()
695 .map(|format| format.id.as_str()),
696 )
697 }
698
699 fn choice_summary(&self, choice: &str) -> Option<&str> {
700 sync_profile::from(choice).map(SyncProfile::summary)
703 }
704}
705
706#[derive(Clone, Debug, PartialEq)]
708pub struct ManagerState {
709 page: ManagerPage<QueueId>,
710 queue: Vec<QueueEntry>,
711 selected: Option<QueueId>,
712 detail_control: Option<DetailControl>,
713 draft: String,
714 draft_cursor: usize,
715 output_draft: String,
716 output_draft_cursor: usize,
717 input_draft: String,
718 input_draft_cursor: usize,
719 message: Option<String>,
720 exit_requested: bool,
721 next_id: u64,
722}
723
724impl ManagerState {
725 #[must_use]
727 pub const fn downloads() -> Self {
728 Self {
729 page: ManagerPage::Collection,
730 queue: Vec::new(),
731 selected: None,
732 detail_control: None,
733 draft: String::new(),
734 draft_cursor: 0,
735 output_draft: String::new(),
736 output_draft_cursor: 0,
737 input_draft: String::new(),
738 input_draft_cursor: 0,
739 message: None,
740 exit_requested: false,
741 next_id: 0,
742 }
743 }
744
745 #[must_use]
747 pub const fn page(&self) -> ManagerPage<QueueId> {
748 self.page
749 }
750
751 #[must_use]
753 pub fn queue(&self) -> &[QueueEntry] {
754 &self.queue
755 }
756
757 #[must_use]
759 pub const fn selected_id(&self) -> Option<QueueId> {
760 self.selected
761 }
762
763 #[must_use]
765 pub const fn detail_control(&self) -> Option<DetailControl> {
766 self.detail_control
767 }
768
769 #[must_use]
771 pub fn selected(&self) -> Option<&QueueEntry> {
772 let selected = self.selected?;
773 self.entry(selected)
774 }
775
776 #[must_use]
778 pub fn entry(&self, id: QueueId) -> Option<&QueueEntry> {
779 self.queue.iter().find(|entry| entry.id == id)
780 }
781
782 #[must_use]
784 pub fn draft(&self) -> &str {
785 &self.draft
786 }
787
788 #[must_use]
790 pub fn output_draft(&self) -> &str {
791 &self.output_draft
792 }
793
794 #[must_use]
796 pub fn message(&self) -> Option<&str> {
797 self.message.as_deref()
798 }
799
800 pub fn waiting(&self) -> impl Iterator<Item = &QueueEntry> {
802 self.queue
803 .iter()
804 .filter(|entry| entry.transfer.phase == TransferPhase::Waiting)
805 }
806
807 #[must_use]
809 pub fn first_waiting_format_catalog(&self) -> Option<&QueueEntry> {
810 self.queue
811 .iter()
812 .find(|entry| entry.formats == FormatCatalog::Waiting)
813 }
814
815 #[must_use]
817 pub fn has_active_transfer(&self) -> bool {
818 self.queue.iter().any(|entry| {
819 matches!(
820 entry.transfer.phase,
821 TransferPhase::Extracting
822 | TransferPhase::Downloading
823 | TransferPhase::Paused
824 | TransferPhase::Publishing
825 )
826 })
827 }
828}
829
830impl DownloadCollectionAlg for ManagerState {
831 fn add_sources(mut self, sources: impl IntoIterator<Item = Self::Source>) -> Self {
832 let requests: Vec<Self::Source> = sources.into_iter().collect();
833 self.apply_manager_event(ManagerIntentOp::AddSources { requests });
834 self
835 }
836}
837
838impl NavigationStateAlg for ManagerState {
839 fn page(&self) -> ManagerPage<Self::Id> {
840 self.page
841 }
842
843 fn set_page(&mut self, page: ManagerPage<Self::Id>) {
844 self.page = page;
845 }
846}
847
848impl QueueCatalogAlg for ManagerState {
849 fn queue_ids(&self) -> impl Iterator<Item = Self::Id> {
850 self.queue.iter().map(|entry| entry.id)
851 }
852
853 fn selected_queue_id(&self) -> Option<Self::Id> {
854 self.selected
855 }
856
857 fn set_selected_queue_id(&mut self, selected: Option<Self::Id>) {
858 self.selected = selected;
859 }
860
861 fn queue_entry(&self, id: Self::Id) -> Option<&Self::Entry> {
862 self.entry(id)
863 }
864}
865
866impl QueueAppendAlg for ManagerState {
867 fn append_sources(&mut self, requests: Vec<Self::Source>) -> Vec<Self::Id> {
868 requests
869 .into_iter()
870 .map(|request| {
871 let id = QueueId(self.next_id);
872 self.next_id = self.next_id.saturating_add(1);
873 let output_custom = request.output.is_some();
874 let request_source = request.source.clone();
875 let formats = if request.options.performer() == Performer::Retrieval {
876 FormatCatalog::Waiting
877 } else {
878 FormatCatalog::Unknown
879 };
880 self.queue.push(QueueEntry {
881 id,
882 request,
883 transfer: TransferState::ready(),
884 formats,
885 media_id: None,
886 title: None,
887 output_custom,
888 log: vec![format!("added {}", request_source)],
889 rehearsal: Rehearsal::Unrehearsed,
890 });
891 refresh_default_output(self.queue.last_mut().expect("the request was appended"));
892 id
893 })
894 .collect()
895 }
896}
897
898impl QueueRemoveAlg for ManagerState {
899 fn remove_queue_entry(&mut self, id: Self::Id) {
900 self.queue.retain(|entry| entry.id != id);
901 }
902}
903
904impl QueuePauseAlg for ManagerState {
905 fn toggle_queue_pause(&mut self, id: Self::Id) {
906 let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id) else {
907 return;
908 };
909 match entry.space_action() {
910 Some(SpaceAction::Pause) => {
911 entry.transfer.resume_phase = Some(entry.transfer.phase);
912 entry.transfer.phase = TransferPhase::Paused;
913 }
914 Some(SpaceAction::Resume) => {
915 entry.transfer.phase = entry
916 .transfer
917 .resume_phase
918 .take()
919 .unwrap_or(TransferPhase::Downloading);
920 }
921 Some(SpaceAction::Start | SpaceAction::Rehearse) | None => {}
923 }
924 }
925}
926
927impl QueueDuplicateAlg for ManagerState {
928 fn duplicate_queue_entry(&mut self, id: Self::Id) -> Option<Self::Id> {
929 let entry = self.queue.iter().find(|entry| entry.id == id)?.clone();
930 let source = entry.source().to_owned();
931 let options = entry.request.options.clone().rehearsing();
933 let output = entry
936 .request
937 .options
938 .profile()
939 .and(entry.request.output.clone());
940 let stated = output.is_some();
941 let formats = match entry.formats {
942 FormatCatalog::Available(formats) => FormatCatalog::Available(formats),
943 FormatCatalog::Unknown
944 | FormatCatalog::Waiting
945 | FormatCatalog::Inspecting
946 | FormatCatalog::Failed(_) => FormatCatalog::Unknown,
947 };
948 let duplicate = QueueId(self.next_id);
949 self.next_id = self.next_id.saturating_add(1);
950 self.queue.push(QueueEntry {
951 id: duplicate,
952 request: SourceRequest::new(source, output, options),
953 transfer: TransferState::ready(),
954 formats,
955 media_id: entry.media_id,
956 title: entry.title,
957 output_custom: stated,
958 log: vec![format!("duplicated from download {}", entry.id.0)],
959 rehearsal: Rehearsal::Unrehearsed,
960 });
961 refresh_default_output(self.queue.last_mut().expect("duplicate was appended"));
962 Some(duplicate)
963 }
964}
965
966impl QueueFormatEditAlg for ManagerState {
967 fn cycle_queue_format(&mut self, id: Self::Id, forward: bool) {
968 let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id) else {
969 return;
970 };
971 if !entry.is_editable() {
972 return;
973 }
974 if entry.request.options.profile().is_some() {
976 let ways = sync_profile::ALL;
977 let current = entry
978 .request
979 .options
980 .profile()
981 .and_then(|profile| ways.iter().position(|way| *way == profile))
982 .unwrap_or(0);
983 let next = if forward {
984 (current + 1) % ways.len()
985 } else if current == 0 {
986 ways.len() - 1
987 } else {
988 current - 1
989 };
990 entry.request.options = entry.request.options.clone().with_profile(ways[next]);
991 entry.forget_rehearsal();
992 return;
993 }
994 let offered = entry
997 .request
998 .options
999 .media_streams()
1000 .map(|_| entry.offered_streams())
1001 .unwrap_or_default();
1002 let choices: Vec<DownloadOptions> = offered
1003 .into_iter()
1004 .map(|streams| entry.request.options.clone().with_media_streams(streams))
1005 .chain(
1006 entry
1007 .formats
1008 .available()
1009 .unwrap_or_default()
1010 .iter()
1011 .map(|format| {
1012 entry
1013 .request
1014 .options
1015 .clone()
1016 .with_format(FormatChoice::Id(format.id.clone()))
1017 }),
1018 )
1019 .collect();
1020 let current = choices
1021 .iter()
1022 .position(|choice| *choice == entry.request.options)
1023 .unwrap_or(0);
1024 let next = if forward {
1025 (current + 1) % choices.len()
1026 } else if current == 0 {
1027 choices.len() - 1
1028 } else {
1029 current - 1
1030 };
1031 entry.request.options = choices[next].clone();
1032 refresh_default_output(entry);
1033 }
1034}
1035
1036impl FormatCatalogStateAlg for ManagerState {
1037 fn request_format_catalog(&mut self, id: Self::Id) {
1038 if let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id)
1039 && entry.is_editable()
1040 && entry.request.options.performer() == Performer::Retrieval
1042 && matches!(
1043 entry.formats,
1044 FormatCatalog::Unknown | FormatCatalog::Failed(_)
1045 )
1046 {
1047 entry.formats = FormatCatalog::Waiting;
1048 }
1049 }
1050
1051 fn apply_format_catalog_event(&mut self, id: Self::Id, event: FormatDiscoveryOp<Self::Format>) {
1052 let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id) else {
1053 return;
1054 };
1055 entry.formats = match event {
1056 FormatDiscoveryOp::Started {} => {
1057 entry.note("inspecting source formats");
1058 FormatCatalog::Inspecting
1059 }
1060 FormatDiscoveryOp::Available { formats } => {
1061 entry.note(format!("extraction described {} formats", formats.len()));
1062 for format in &formats {
1063 entry.note(described_format(format));
1064 }
1065 FormatCatalog::Available(formats)
1066 }
1067 FormatDiscoveryOp::Failed { message } => {
1068 entry.note(format!("format inspection failed: {message}"));
1069 FormatCatalog::Failed(message)
1070 }
1071 };
1072 if matches!(entry.request.options.format(), FormatChoice::Best)
1077 && let Some(asked) = entry.request.options.media_streams()
1078 {
1079 let offered = entry.offered_streams();
1080 if let Some(carried) = offered.first().copied() {
1081 if !offered.contains(&asked) {
1082 entry.request.options =
1083 entry.request.options.clone().with_media_streams(carried);
1084 }
1085 } else if let Some(named) = entry
1086 .formats
1087 .available()
1088 .and_then(|described| described.first())
1089 .map(|format| format.id.clone())
1090 {
1091 entry.request.options = entry
1092 .request
1093 .options
1094 .clone()
1095 .with_format(FormatChoice::Id(named));
1096 }
1097 }
1098 refresh_default_output(entry);
1099 }
1100}
1101
1102impl OutputDraftAlg for ManagerState {
1103 fn output_draft(&self) -> &str {
1104 &self.output_draft
1105 }
1106
1107 fn set_output_draft(&mut self, draft: String) {
1108 self.output_draft_cursor = draft.len();
1109 self.output_draft = draft;
1110 }
1111}
1112
1113impl InputDraftAlg for ManagerState {
1114 fn input_draft(&self) -> &str {
1115 &self.input_draft
1116 }
1117
1118 fn set_input_draft(&mut self, draft: String) {
1119 self.input_draft_cursor = draft.len();
1120 self.input_draft = draft;
1121 }
1122}
1123
1124impl QueueSourceAlg for ManagerState {
1125 fn set_queue_source(&mut self, id: Self::Id, source: String) {
1126 if let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id)
1127 && entry.is_editable()
1128 {
1129 entry.note(format!("input changed to {source}"));
1130 entry.request.source = source;
1131 entry.forget_rehearsal();
1132 refresh_default_output(entry);
1133 }
1134 }
1135}
1136
1137impl QueueOutputAlg for ManagerState {
1138 fn set_queue_output(&mut self, id: Self::Id, output: PathBuf) {
1139 if let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id)
1140 && entry.is_editable()
1141 {
1142 entry.request.output = Some(output);
1143 entry.output_custom = true;
1144 entry.forget_rehearsal();
1145 }
1146 }
1147}
1148
1149impl DownloadLogAlg for ManagerState {
1150 fn note_download(&mut self, id: Self::Id, note: String) {
1151 if let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id) {
1152 entry.note(note);
1153 }
1154 }
1155}
1156
1157impl SourceMetadataAlg for ManagerState {
1158 fn apply_source_metadata(&mut self, id: Self::Id, media_id: String, title: Option<String>) {
1159 if let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id) {
1160 entry.note(match &title {
1161 Some(title) => format!("extracted {media_id}: {title}"),
1162 None => format!("extracted {media_id}"),
1163 });
1164 entry.media_id = Some(media_id);
1165 entry.title = title;
1166 refresh_default_output(entry);
1167 }
1168 }
1169}
1170
1171fn refresh_default_output(entry: &mut QueueEntry) {
1172 if entry.output_custom {
1173 return;
1174 }
1175 if entry.request.options.performer() == Performer::Program {
1179 let source = RsyncSyntax.read_endpoint(&entry.request.source);
1180 entry.request.output = Some(PathBuf::from(RsyncSyntax.endpoint_name(&source)));
1181 return;
1182 }
1183 let Some(media_id) = entry.media_id.as_deref() else {
1184 return;
1185 };
1186 entry.request.output = Some(portable_file_name(
1187 entry.title.as_deref(),
1188 media_id,
1189 selected_extension(entry),
1190 ));
1191}
1192
1193fn selected_extension(entry: &QueueEntry) -> Option<&str> {
1194 let formats = entry.formats.available()?;
1195 match entry.request.options.format() {
1196 FormatChoice::Best => formats
1197 .iter()
1198 .rev()
1199 .find(|format| {
1200 entry
1201 .request
1202 .options
1203 .media_streams()
1204 .is_some_and(|streams| role_accepts(streams, format))
1205 })
1206 .and_then(Format::extension),
1207 FormatChoice::Id(id) => formats
1208 .iter()
1209 .find(|format| format.id == *id)
1210 .and_then(Format::extension),
1211 }
1212}
1213
1214impl TransferStateAlg for ManagerState {
1215 fn set_waiting(&mut self, id: Self::Id) {
1216 if let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id)
1217 && entry.is_editable()
1218 && entry.output().is_some()
1219 {
1220 entry.transfer = TransferState::ready();
1221 entry.transfer.phase = TransferPhase::Waiting;
1222 }
1223 }
1224
1225 fn restart_waiting(&mut self, id: Self::Id) {
1226 if let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id)
1227 && entry.transfer.phase == TransferPhase::Failed
1228 {
1229 entry.transfer = TransferState::ready();
1230 entry.transfer.phase = TransferPhase::Waiting;
1231 }
1232 }
1233
1234 fn apply_transfer_event(&mut self, id: Self::Id, event: TransferObservationOp) {
1235 let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id) else {
1236 return;
1237 };
1238 event.interpret(&mut EntryObservation(entry));
1239 }
1240}
1241
1242struct EntryObservation<'a>(&'a mut QueueEntry);
1246
1247impl TransferObservationInterpreter for EntryObservation<'_> {
1248 fn pause_capability(&mut self, supported: bool) {
1249 self.0.transfer.pause_supported = supported;
1250 }
1251
1252 fn started(&mut self) {
1253 self.0.note("retrieval started");
1254 self.0.transfer.phase = TransferPhase::Extracting;
1255 }
1256
1257 fn elapsed(&mut self, elapsed: Duration) {
1258 self.0.transfer.elapsed = elapsed;
1259 }
1260
1261 fn progress(&mut self, destination: PathBuf, downloaded: u64, total: Option<u64>) {
1262 let observed_phase = if total.is_some_and(|total| downloaded >= total) {
1263 TransferPhase::Publishing
1264 } else {
1265 TransferPhase::Downloading
1266 };
1267 if self.0.transfer.phase == TransferPhase::Paused {
1268 self.0.transfer.resume_phase = Some(observed_phase);
1269 } else {
1270 self.0.transfer.phase = observed_phase;
1271 }
1272 self.0.transfer.destination = Some(destination);
1273 self.0.transfer.downloaded = downloaded;
1274 self.0.transfer.total = total;
1275 }
1276
1277 fn completed(&mut self, destination: PathBuf, bytes: u64) {
1278 self.0.note(format!(
1279 "published {} ({bytes} bytes)",
1280 destination.display()
1281 ));
1282 self.0.transfer.phase = TransferPhase::Complete;
1283 self.0.transfer.terminal = Some(DownloadEvent::Succeeded { destination, bytes });
1284 }
1285
1286 fn failed(&mut self, destination: PathBuf, message: String) {
1287 self.0.note(format!(
1288 "retrieval failed at {}: {message}",
1289 destination.display()
1290 ));
1291 self.0.transfer.phase = TransferPhase::Failed;
1292 self.0.transfer.terminal = Some(DownloadEvent::Failed {
1293 destination,
1294 message,
1295 });
1296 }
1297
1298 fn program_failed(&mut self, summary: String, detail: String) {
1299 self.0.note(format!("{summary}: {detail}"));
1300 self.0.transfer.phase = TransferPhase::Failed;
1301 self.0.transfer.summary = Some(summary);
1302 self.0.transfer.detail = Some(detail);
1303 }
1304}
1305
1306impl ManagerState {
1307 pub fn transfer_request(&mut self, id: QueueId) {
1312 if let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id) {
1313 entry.request.options = DownloadOptions::transfer();
1314 }
1315 }
1316}
1317
1318impl QueueDryRunAlg for ManagerState {
1319 fn set_queue_dry_run(&mut self, id: Self::Id, dry_run: bool) {
1320 if let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id)
1321 && entry.is_editable()
1322 {
1323 entry.request.options.set_dry_run(dry_run);
1324 entry.note(if dry_run {
1325 "dry run enabled: the next run will state what it would do and change nothing"
1326 } else {
1327 "dry run disabled: the next run will perform the transfer"
1328 });
1329 }
1330 }
1331}
1332
1333impl RehearsalStateAlg for ManagerState {
1334 fn apply_rehearsal_event(&mut self, id: Self::Id, event: RehearsalObservationOp<Self::Change>) {
1335 let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id) else {
1336 return;
1337 };
1338 match event {
1339 RehearsalObservationOp::Started {} => {
1340 entry.note("rehearsing what the transfer would do");
1341 entry.transfer.phase = TransferPhase::Rehearsing;
1342 entry.rehearsal = Rehearsal::Rehearsing;
1343 }
1344 RehearsalObservationOp::Reported { changes } => {
1345 let altered = changes.iter().filter(|change| change.kind.alters()).count();
1346 entry.note(format!(
1347 "the rehearsal stated {altered} of {} paths would change",
1348 changes.len()
1349 ));
1350 entry.transfer.phase = TransferPhase::Ready;
1352 entry.rehearsal = Rehearsal::Reported(changes);
1353 }
1354 RehearsalObservationOp::Failed { message } => {
1355 entry.note(format!("rehearsal failed: {message}"));
1356 entry.transfer.phase = TransferPhase::Ready;
1357 entry.rehearsal = Rehearsal::Failed(message);
1358 }
1359 }
1360 }
1361}
1362
1363impl DetailSelectionAlg for ManagerState {
1364 fn selected_detail_control(&self) -> Option<DetailControl> {
1365 self.detail_control
1366 }
1367
1368 fn set_selected_detail_control(&mut self, control: Option<DetailControl>) {
1369 self.detail_control = control;
1370 }
1371}
1372
1373impl DraftStateAlg for ManagerState {
1374 fn draft(&self) -> &str {
1375 &self.draft
1376 }
1377
1378 fn set_draft(&mut self, draft: String) {
1379 self.draft_cursor = draft.len();
1380 self.draft = draft;
1381 }
1382}
1383
1384impl TextEditorStateAlg for ManagerState {
1385 fn active_text_editor(&self) -> Option<(&str, usize)> {
1386 match self.page {
1387 ManagerPage::AddSources => Some((&self.draft, self.draft_cursor)),
1388 ManagerPage::Output(_) => Some((&self.output_draft, self.output_draft_cursor)),
1389 ManagerPage::Input(_) => Some((&self.input_draft, self.input_draft_cursor)),
1390 ManagerPage::Collection
1391 | ManagerPage::Details(_)
1392 | ManagerPage::Formats(_)
1393 | ManagerPage::Log(_)
1394 | ManagerPage::Report(_)
1395 | ManagerPage::Command(_) => None,
1396 }
1397 }
1398
1399 fn set_active_text_editor(&mut self, text: String, cursor: usize) {
1400 let mut cursor = cursor.min(text.len());
1401 while !text.is_char_boundary(cursor) {
1402 cursor = cursor.saturating_sub(1);
1403 }
1404 match self.page {
1405 ManagerPage::AddSources => {
1406 self.draft = text;
1407 self.draft_cursor = cursor;
1408 }
1409 ManagerPage::Output(_) => {
1410 self.output_draft = text;
1411 self.output_draft_cursor = cursor;
1412 }
1413 ManagerPage::Input(_) => {
1414 self.input_draft = text;
1415 self.input_draft_cursor = cursor;
1416 }
1417 ManagerPage::Collection
1418 | ManagerPage::Details(_)
1419 | ManagerPage::Formats(_)
1420 | ManagerPage::Log(_)
1421 | ManagerPage::Report(_)
1422 | ManagerPage::Command(_) => {}
1423 }
1424 }
1425}
1426
1427impl ManagerStatusAlg for ManagerState {
1428 fn manager_message(&self) -> Option<&str> {
1429 self.message()
1430 }
1431
1432 fn set_manager_message(&mut self, message: Option<String>) {
1433 self.message = message;
1434 }
1435}
1436
1437impl SafeExitAlg for ManagerState {
1438 fn exit_requested(&self) -> bool {
1439 self.exit_requested
1440 }
1441
1442 fn request_safe_exit(&mut self) {
1443 self.exit_requested = true;
1444 }
1445}
1446
1447fn submitted(line: &str) -> SourceRequest {
1454 let trimmed = line.trim();
1455 if recognized(trimmed) {
1458 return SourceRequest::new(trimmed, None, DownloadOptions::progressive());
1459 }
1460 if let Some((source, destination)) = RsyncSyntax.read_transfer(line) {
1461 return SourceRequest::new(
1462 RsyncSyntax.endpoint_text(&source),
1463 Some(PathBuf::from(RsyncSyntax.endpoint_text(&destination))),
1464 DownloadOptions::transfer(),
1465 );
1466 }
1467 SourceRequest::new(trimmed, None, DownloadOptions::transfer())
1468}
1469
1470const RETRIEVED_SCHEMES: [&str; 3] = ["https://", "http://", "fixture://"];
1476
1477fn recognized(line: &str) -> bool {
1483 RETRIEVED_SCHEMES
1484 .iter()
1485 .any(|scheme| line.starts_with(scheme))
1486}
1487
1488fn described_format(format: &Format) -> String {
1490 format!(" {}", format.format_label())
1491}