Skip to main content

rsynko_media/
extraction.rs

1use crate::MediaSorts;
2use alux_ext::ext;
3use ambassador::delegatable_trait;
4use thiserror::Error;
5
6/// Provides the carriers and constructors for extraction results.
7#[delegatable_trait]
8pub trait ExtractionAlg: MediaSorts {
9    /// Defines a fully described media extraction.
10    fn media(
11        &self,
12        id: impl Into<String>,
13        metadata: Self::Metadata,
14        formats: impl IntoIterator<Item = Self::Format>,
15    ) -> Self::Extraction;
16    /// Defines a URL requiring another extraction step.
17    fn url_reference(
18        &self,
19        url: impl Into<String>,
20        extractor: Option<Self::Extractor>,
21        transparent: bool,
22    ) -> Self::Extraction;
23    /// Defines an ordered extraction collection.
24    fn extraction_collection(
25        &self,
26        id: Option<String>,
27        kind: CollectionKind,
28        metadata: Self::Metadata,
29        entries: impl IntoIterator<Item = Self::Extraction>,
30    ) -> Self::Extraction;
31}
32
33/// Specifies observation of what shape an extraction result denotes.
34#[delegatable_trait]
35pub trait ExtractionViewAlg: MediaSorts {
36    /// Observes the single media item an extraction denotes, exactly when it denotes one.
37    fn as_media(&self, extraction: Self::Extraction) -> Option<Self::Media>;
38}
39
40/// Specifies the observations one media item offers.
41#[delegatable_trait]
42pub trait MediaViewAlg: MediaSorts {
43    /// Observes the media identity.
44    fn media_id<'a>(&self, media: &'a Self::Media) -> &'a str;
45    /// Observes the media title exactly when one was extracted.
46    fn media_title<'a>(&self, media: &'a Self::Media) -> Option<&'a str>;
47    /// Observes the formats in extractor order.
48    fn media_formats<'a>(&self, media: &'a Self::Media) -> &'a [Self::Format];
49}
50
51/// Specifies an ordered catalog of extractor meanings.
52#[delegatable_trait]
53pub trait ExtractionCatalogAlg: MediaSorts {
54    /// Observes extractor keys in selection order.
55    fn extractor_keys(&self) -> impl Iterator<Item = &Self::Extractor>;
56
57    /// Observes whether one extractor accepts an input URL.
58    fn extractor_accepts(&self, extractor: &Self::Extractor, url: &str) -> bool;
59}
60
61/// Specifies application of one selected extractor.
62#[delegatable_trait]
63pub trait ExtractionApplyAlg: MediaSorts {
64    /// Denotes an extractor-specific failure.
65    type Error;
66
67    /// Applies a selected extractor to an accepted URL.
68    ///
69    /// # Errors
70    ///
71    /// Returns the selected extractor's error when extraction fails.
72    fn extract_with(&self, extractor: &Self::Extractor, url: &str) -> Result<Self::Extraction, Self::Error>;
73}
74
75/// Classifies an ordered extraction collection.
76#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77pub enum CollectionKind {
78    /// Denotes independently selectable entries.
79    Playlist,
80    /// Denotes entries comprising one logical media item.
81    MultiVideo,
82}
83
84/// Denotes failure to derive extraction from an input URL.
85#[derive(Debug, Error, PartialEq, Eq)]
86pub enum ExtractUrlError<ExtractorError> {
87    /// Denotes that no catalog entry accepts the URL.
88    #[error("no extractor accepts {url}")]
89    Unsupported {
90        /// Names the unsupported input.
91        url: String,
92    },
93    /// Denotes failure of the selected extractor.
94    #[error("selected extractor failed: {0}")]
95    Extractor(ExtractorError),
96}
97
98/// Derives ordered extractor choice and URL extraction from primitive capabilities.
99#[ext(name = ExtractionExt)]
100pub impl<This, Extractor, Extraction> This
101where
102    This: ExtractionCatalogAlg<Extractor = Extractor>
103        + ExtractionApplyAlg<Extractor = Extractor, Extraction = Extraction>,
104{
105    /// Chooses the first extractor accepting the URL.
106    fn choose_extractor(&self, url: &str) -> Option<&Extractor> {
107        self.extractor_keys().find(|extractor| self.extractor_accepts(extractor, url))
108    }
109
110    /// Extracts a URL through the first accepting extractor.
111    ///
112    /// # Errors
113    ///
114    /// Returns [`ExtractUrlError::Unsupported`] when no extractor accepts the URL, or wraps the
115    /// selected extractor's failure in [`ExtractUrlError::Extractor`].
116    fn extract_url(&self, url: &str) -> Result<Extraction, ExtractUrlError<This::Error>> {
117        let extractor =
118            self.choose_extractor(url).ok_or_else(|| ExtractUrlError::Unsupported { url: url.to_owned() })?;
119        self.extract_with(extractor, url).map_err(ExtractUrlError::Extractor)
120    }
121}