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.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#[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 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#[ext(name = MediaOutputExt)]
136pub impl<This> This
137where
138 This: MediaViewAlg + FormatViewAlg,
139{
140 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}