use crate::*;
use alux_ext::ext;
use itertools::Itertools;
use rsynko_download::*;
use std::fmt::Display;
use std::path::{Path, PathBuf};
use thiserror::Error;
pub trait FormatSourceAlg<Source>: MediaSorts {
fn format_source(&self, format: &Self::Format) -> Option<Source>;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum OutputTarget {
Path(PathBuf),
MediaId,
Title,
}
#[derive(Debug, Error)]
pub enum ApplicationError<ExtractionError, FetchError, PublishError> {
#[error("{0}")]
Extraction(ExtractUrlError<ExtractionError>),
#[error("{0}")]
Media(MediaDownloadError<FetchError, PublishError>),
}
#[derive(Debug, Error)]
pub enum MediaDownloadError<FetchError, PublishError> {
#[error("the extracted result is not a single media item")]
NotSingleMedia,
#[error("no directly retrievable format matches the download options")]
NoMatchingFormat,
#[error("the selected format states no location this interpreter can retrieve")]
UnlocatableFormat,
#[error("the initial downloader requires exactly one selected format")]
MultipleFormats,
#[error("{0}")]
Download(DownloadError<FetchError, PublishError>),
}
#[ext(name = MediaDownloadExt)]
pub impl<This, Source, FetchError, PublishError, Event, Progress> This
where
This: ExtractionViewAlg
+ MediaViewAlg
+ FormatViewAlg
+ FormatSelectionApplyAlg
+ FormatSourceAlg<Source>
+ FetchStreamAlg<Source, Error = FetchError>
+ AtomicPublishAlg<Error = PublishError>
+ DownloadObservationAlg<Event = Event, Progress = Progress>
+ DownloadProgressAlg<Progress = Progress>
+ DownloadReportAlg<Event = Event>,
FetchError: Display,
PublishError: Display,
{
fn download_extraction(
&self,
extraction: This::Extraction,
selection: &This::Selection,
target: &OutputTarget,
) -> Result<PathBuf, MediaDownloadError<FetchError, PublishError>> {
let media = self.as_media(extraction).ok_or(MediaDownloadError::NotSingleMedia)?;
let formats =
self.select_formats(self.media_formats(&media), selection).ok_or(MediaDownloadError::NoMatchingFormat)?;
let format = formats.into_iter().exactly_one().map_err(|_| MediaDownloadError::MultipleFormats)?;
let destination = self.media_output_path(&media, format, target);
let source = self.format_source(format).ok_or(MediaDownloadError::UnlocatableFormat)?;
self.download_resource(&source, &destination).map_err(MediaDownloadError::Download)?;
Ok(destination)
}
}
#[ext(name = ApplicationExt)]
pub impl<This, ExtractionError, FetchError, PublishError, Event, Progress> This
where
This: ExtractionCatalogAlg
+ ExtractionApplyAlg<Error = ExtractionError>
+ ExtractionViewAlg
+ MediaViewAlg
+ FormatViewAlg
+ FormatSelectionApplyAlg
+ FormatSourceAlg<String>
+ FetchStreamAlg<String, Error = FetchError>
+ AtomicPublishAlg<Error = PublishError>
+ DownloadObservationAlg<Event = Event, Progress = Progress>
+ DownloadProgressAlg<Progress = Progress>
+ DownloadReportAlg<Event = Event>,
FetchError: Display,
PublishError: Display,
{
fn download_url(
&self,
url: &str,
selection: &This::Selection,
target: &OutputTarget,
) -> Result<PathBuf, ApplicationError<ExtractionError, FetchError, PublishError>> {
let extraction = self.extract_url(url).map_err(ApplicationError::Extraction)?;
self.download_extraction(extraction, selection, target).map_err(ApplicationError::Media)
}
}
#[ext(name = MediaOutputExt)]
pub impl<This> This
where
This: MediaViewAlg + FormatViewAlg,
{
fn media_output_path(&self, media: &This::Media, format: &This::Format, target: &OutputTarget) -> PathBuf {
match target {
OutputTarget::Path(path) => path.clone(),
OutputTarget::MediaId => media_id_path(self.media_id(media), self.format_text(format, FORMAT_EXTENSION)),
OutputTarget::Title => crate::portable_file_name(
self.media_title(media),
self.media_id(media),
self.format_text(format, FORMAT_EXTENSION),
),
}
}
}
fn media_id_path(id: &str, extension: Option<&str>) -> PathBuf {
let safe_id: String = id
.chars()
.map(|character| match character {
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
other => other,
})
.collect();
let extension = extension.unwrap_or("bin");
Path::new(&format!("{safe_id}.{extension}")).to_owned()
}