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
89            .as_media(extraction)
90            .ok_or(MediaDownloadError::NotSingleMedia)?;
91        let formats = self
92            .select_formats(self.media_formats(&media), selection)
93            .ok_or(MediaDownloadError::NoMatchingFormat)?;
94        let format = formats
95            .into_iter()
96            .exactly_one()
97            .map_err(|_| MediaDownloadError::MultipleFormats)?;
98        let destination = self.media_output_path(&media, format, target);
99        let source = self
100            .format_source(format)
101            .ok_or(MediaDownloadError::UnlocatableFormat)?;
102        self.download_resource(&source, &destination)
103            .map_err(MediaDownloadError::Download)?;
104        Ok(destination)
105    }
106}
107
108/// Derives ordinary extraction, format selection, destination naming, and download.
109#[ext(name = ApplicationExt)]
110pub impl<This, ExtractionError, FetchError, PublishError, Event, Progress> This
111where
112    This: ExtractionCatalogAlg
113        + ExtractionApplyAlg<Error = ExtractionError>
114        + ExtractionViewAlg
115        + MediaViewAlg
116        + FormatViewAlg
117        + FormatSelectionApplyAlg
118        + FormatSourceAlg<String>
119        + FetchStreamAlg<String, Error = FetchError>
120        + AtomicPublishAlg<Error = PublishError>
121        + DownloadObservationAlg<Event = Event, Progress = Progress>
122        + DownloadProgressAlg<Progress = Progress>
123        + DownloadReportAlg<Event = Event>,
124    FetchError: Display,
125    PublishError: Display,
126{
127    /// Downloads one URL through exactly one selected direct format.
128    ///
129    /// # Errors
130    ///
131    /// Returns extraction, selection, retrieval, or publication failure.
132    fn download_url(
133        &self,
134        url: &str,
135        selection: &This::Selection,
136        target: &OutputTarget,
137    ) -> Result<PathBuf, ApplicationError<ExtractionError, FetchError, PublishError>> {
138        let extraction = self
139            .extract_url(url)
140            .map_err(ApplicationError::Extraction)?;
141        self.download_extraction(extraction, selection, target)
142            .map_err(ApplicationError::Media)
143    }
144}
145
146/// Derives a final path from media observations, the selected format, and the output target.
147#[ext(name = MediaOutputExt)]
148pub impl<This> This
149where
150    This: MediaViewAlg + FormatViewAlg,
151{
152    /// Defines the final path one media item publishes its selected format at.
153    fn media_output_path(
154        &self,
155        media: &This::Media,
156        format: &This::Format,
157        target: &OutputTarget,
158    ) -> PathBuf {
159        match target {
160            OutputTarget::Path(path) => path.clone(),
161            OutputTarget::MediaId => media_id_path(
162                self.media_id(media),
163                self.format_text(format, FORMAT_EXTENSION),
164            ),
165            OutputTarget::Title => crate::portable_file_name(
166                self.media_title(media),
167                self.media_id(media),
168                self.format_text(format, FORMAT_EXTENSION),
169            ),
170        }
171    }
172}
173
174fn media_id_path(id: &str, extension: Option<&str>) -> PathBuf {
175    let safe_id: String = id
176        .chars()
177        .map(|character| match character {
178            '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
179            other => other,
180        })
181        .collect();
182    let extension = extension.unwrap_or("bin");
183    Path::new(&format!("{safe_id}.{extension}")).to_owned()
184}