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(
73        &self,
74        extractor: &Self::Extractor,
75        url: &str,
76    ) -> Result<Self::Extraction, Self::Error>;
77}
78
79/// Classifies an ordered extraction collection.
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
81pub enum CollectionKind {
82    /// Denotes independently selectable entries.
83    Playlist,
84    /// Denotes entries comprising one logical media item.
85    MultiVideo,
86}
87
88/// Denotes failure to derive extraction from an input URL.
89#[derive(Debug, Error, PartialEq, Eq)]
90pub enum ExtractUrlError<ExtractorError> {
91    /// Denotes that no catalog entry accepts the URL.
92    #[error("no extractor accepts {url}")]
93    Unsupported {
94        /// Names the unsupported input.
95        url: String,
96    },
97    /// Denotes failure of the selected extractor.
98    #[error("selected extractor failed: {0}")]
99    Extractor(ExtractorError),
100}
101
102/// Derives ordered extractor choice and URL extraction from primitive capabilities.
103#[ext(name = ExtractionExt)]
104pub impl<This, Extractor, Extraction> This
105where
106    This: ExtractionCatalogAlg<Extractor = Extractor>
107        + ExtractionApplyAlg<Extractor = Extractor, Extraction = Extraction>,
108{
109    /// Chooses the first extractor accepting the URL.
110    fn choose_extractor(&self, url: &str) -> Option<&Extractor> {
111        self.extractor_keys()
112            .find(|extractor| self.extractor_accepts(extractor, url))
113    }
114
115    /// Extracts a URL through the first accepting extractor.
116    ///
117    /// # Errors
118    ///
119    /// Returns [`ExtractUrlError::Unsupported`] when no extractor accepts the URL, or wraps the
120    /// selected extractor's failure in [`ExtractUrlError::Extractor`].
121    fn extract_url(&self, url: &str) -> Result<Extraction, ExtractUrlError<This::Error>> {
122        let extractor = self
123            .choose_extractor(url)
124            .ok_or_else(|| ExtractUrlError::Unsupported {
125                url: url.to_owned(),
126            })?;
127        self.extract_with(extractor, url)
128            .map_err(ExtractUrlError::Extractor)
129    }
130}