rsynko_media/
application.rs1use crate::*;
2use alux_ext::ext;
3use itertools::Itertools;
4use rsynko_download::*;
5use std::fmt::Display;
6use std::path::{Path, PathBuf};
7use thiserror::Error;
8
9pub trait FormatSourceAlg<Source>: MediaSorts {
14 fn format_source(&self, format: &Self::Format) -> Option<Source>;
16}
17
18#[derive(Clone, Debug, PartialEq, Eq)]
20pub enum OutputTarget {
21 Path(PathBuf),
23 MediaId,
25 Title,
27}
28
29#[derive(Debug, Error)]
31pub enum ApplicationError<ExtractionError, FetchError, PublishError> {
32 #[error("{0}")]
34 Extraction(ExtractUrlError<ExtractionError>),
35 #[error("{0}")]
37 Media(MediaDownloadError<FetchError, PublishError>),
38}
39
40#[derive(Debug, Error)]
42pub enum MediaDownloadError<FetchError, PublishError> {
43 #[error("the extracted result is not a single media item")]
45 NotSingleMedia,
46 #[error("no directly retrievable format matches the download options")]
48 NoMatchingFormat,
49 #[error("the selected format states no location this interpreter can retrieve")]
51 UnlocatableFormat,
52 #[error("the initial downloader requires exactly one selected format")]
54 MultipleFormats,
55 #[error("{0}")]
57 Download(DownloadError<FetchError, PublishError>),
58}
59
60#[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 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#[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 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#[ext(name = MediaOutputExt)]
148pub impl<This> This
149where
150 This: MediaViewAlg + FormatViewAlg,
151{
152 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}