Skip to main content

rsynko_media/
application.rs

1use crate::*;
2use alux_ext::ext;
3use itertools::Itertools;
4use rsynko_download::*;
5use std::fmt::Display;
6use std::path::{Path, PathBuf};
7use thiserror::Error;
8
9/// Specifies how one generic format denotes a retrievable source carrier.
10///
11/// A format states where its bytes rest as an observation, so an interpreter that cannot locate a
12/// format from what it observed says so rather than inventing a location.
13pub trait FormatSourceAlg<Source>: MediaSorts {
14    /// Defines the source consumed by resource retrieval for one format.
15    fn format_source(&self, format: &Self::Format) -> Option<Source>;
16}
17
18/// Selects how a final output path is denoted.
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub enum OutputTarget {
21    /// Uses an explicit final path.
22    Path(PathBuf),
23    /// Derives `MEDIA_ID.EXTENSION` in the current directory.
24    MediaId,
25    /// Derives `PORTABLE_TITLE.EXTENSION`, falling back to the media identity.
26    Title,
27}
28
29/// Denotes failure of an ordinary direct-resource media program.
30#[derive(Debug, Error)]
31pub enum ApplicationError<ExtractionError, FetchError, PublishError> {
32    /// Denotes extraction failure.
33    #[error("{0}")]
34    Extraction(ExtractUrlError<ExtractionError>),
35    /// Denotes failure of the shared direct-media program.
36    #[error("{0}")]
37    Media(MediaDownloadError<FetchError, PublishError>),
38}
39
40/// Denotes failure after extraction has produced a media description.
41#[derive(Debug, Error)]
42pub enum MediaDownloadError<FetchError, PublishError> {
43    /// Denotes a result not supported by the single-media program.
44    #[error("the extracted result is not a single media item")]
45    NotSingleMedia,
46    /// Denotes absence of a matching direct format.
47    #[error("no directly retrievable format matches the download options")]
48    NoMatchingFormat,
49    /// Denotes a selected format the interpreter cannot locate.
50    #[error("the selected format states no location this interpreter can retrieve")]
51    UnlocatableFormat,
52    /// Denotes a selection producing more than one resource.
53    #[error("the initial downloader requires exactly one selected format")]
54    MultipleFormats,
55    /// Denotes resource download failure.
56    #[error("{0}")]
57    Download(DownloadError<FetchError, PublishError>),
58}
59
60/// Derives selection, naming, and download for an already extracted result.
61#[ext(name = MediaDownloadExt)]
62pub impl<This, Source, FetchError, PublishError, Event, Progress> This
63where
64    This: ExtractionViewAlg
65        + MediaViewAlg
66        + FormatViewAlg
67        + FormatSelectionApplyAlg
68        + FormatSourceAlg<Source>
69        + FetchStreamAlg<Source, Error = FetchError>
70        + AtomicPublishAlg<Error = PublishError>
71        + DownloadObservationAlg<Event = Event, Progress = Progress>
72        + DownloadProgressAlg<Progress = Progress>
73        + DownloadReportAlg<Event = Event>,
74    FetchError: Display,
75    PublishError: Display,
76{
77    /// Downloads exactly one selected format from an extracted single-media result.
78    ///
79    /// # Errors
80    ///
81    /// Returns media-shape, selection, retrieval, or publication failure.
82    fn download_extraction(
83        &self,
84        extraction: This::Extraction,
85        selection: &This::Selection,
86        target: &OutputTarget,
87    ) -> Result<PathBuf, MediaDownloadError<FetchError, PublishError>> {
88        let media = self.as_media(extraction).ok_or(MediaDownloadError::NotSingleMedia)?;
89        let formats =
90            self.select_formats(self.media_formats(&media), selection).ok_or(MediaDownloadError::NoMatchingFormat)?;
91        let format = formats.into_iter().exactly_one().map_err(|_| MediaDownloadError::MultipleFormats)?;
92        let destination = self.media_output_path(&media, format, target);
93        let source = self.format_source(format).ok_or(MediaDownloadError::UnlocatableFormat)?;
94        self.download_resource(&source, &destination).map_err(MediaDownloadError::Download)?;
95        Ok(destination)
96    }
97}
98
99/// Derives ordinary extraction, format selection, destination naming, and download.
100#[ext(name = ApplicationExt)]
101pub impl<This, ExtractionError, FetchError, PublishError, Event, Progress> This
102where
103    This: ExtractionCatalogAlg
104        + ExtractionApplyAlg<Error = ExtractionError>
105        + ExtractionViewAlg
106        + MediaViewAlg
107        + FormatViewAlg
108        + FormatSelectionApplyAlg
109        + FormatSourceAlg<String>
110        + FetchStreamAlg<String, Error = FetchError>
111        + AtomicPublishAlg<Error = PublishError>
112        + DownloadObservationAlg<Event = Event, Progress = Progress>
113        + DownloadProgressAlg<Progress = Progress>
114        + DownloadReportAlg<Event = Event>,
115    FetchError: Display,
116    PublishError: Display,
117{
118    /// Downloads one URL through exactly one selected direct format.
119    ///
120    /// # Errors
121    ///
122    /// Returns extraction, selection, retrieval, or publication failure.
123    fn download_url(
124        &self,
125        url: &str,
126        selection: &This::Selection,
127        target: &OutputTarget,
128    ) -> Result<PathBuf, ApplicationError<ExtractionError, FetchError, PublishError>> {
129        let extraction = self.extract_url(url).map_err(ApplicationError::Extraction)?;
130        self.download_extraction(extraction, selection, target).map_err(ApplicationError::Media)
131    }
132}
133
134/// Derives a final path from media observations, the selected format, and the output target.
135#[ext(name = MediaOutputExt)]
136pub impl<This> This
137where
138    This: MediaViewAlg + FormatViewAlg,
139{
140    /// Defines the final path one media item publishes its selected format at.
141    fn media_output_path(&self, media: &This::Media, format: &This::Format, target: &OutputTarget) -> PathBuf {
142        match target {
143            OutputTarget::Path(path) => path.clone(),
144            OutputTarget::MediaId => media_id_path(self.media_id(media), self.format_text(format, FORMAT_EXTENSION)),
145            OutputTarget::Title => crate::portable_file_name(
146                self.media_title(media),
147                self.media_id(media),
148                self.format_text(format, FORMAT_EXTENSION),
149            ),
150        }
151    }
152}
153
154fn media_id_path(id: &str, extension: Option<&str>) -> PathBuf {
155    let safe_id: String = id
156        .chars()
157        .map(|character| match character {
158            '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
159            other => other,
160        })
161        .collect();
162    let extension = extension.unwrap_or("bin");
163    Path::new(&format!("{safe_id}.{extension}")).to_owned()
164}