From 450ee41971d802698e0167a5f250e57ada64e6d7 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sat, 4 Feb 2023 16:51:46 -0500 Subject: [PATCH] Add Cinema1 video store --- munite-core/Cargo.toml | 2 - munite-core/src/audio/interface.rs | 61 ++++-- munite-core/src/audio/mod.rs | 2 +- munite-core/src/audio/music/interface.rs | 127 +++++-------- munite-core/src/audio/music/mod.rs | 2 +- .../src/audio/music/pro_studio_masters.rs | 23 +-- munite-core/src/audio/music/seven_digital.rs | 64 ++++--- munite-core/src/entry.rs | 73 +++++++ munite-core/src/lib.rs | 5 +- munite-core/src/search.rs | 18 +- munite-core/src/video/cinema1.rs | 142 ++++++++++++++ munite-core/src/video/interface.rs | 178 ++++++++++++++++++ munite-core/src/video/mod.rs | 20 ++ src/cli.rs | 2 + src/main.rs | 4 + 15 files changed, 579 insertions(+), 144 deletions(-) create mode 100644 munite-core/src/entry.rs create mode 100644 munite-core/src/video/cinema1.rs create mode 100644 munite-core/src/video/interface.rs create mode 100644 munite-core/src/video/mod.rs diff --git a/munite-core/Cargo.toml b/munite-core/Cargo.toml index 717d609..68fc470 100644 --- a/munite-core/Cargo.toml +++ b/munite-core/Cargo.toml @@ -3,8 +3,6 @@ name = "munite-core" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] async-trait = "0.1.61" reqwest = { version = "0.11.13", features = [ "json" ] } diff --git a/munite-core/src/audio/interface.rs b/munite-core/src/audio/interface.rs index 8ce6163..f5d57c4 100644 --- a/munite-core/src/audio/interface.rs +++ b/munite-core/src/audio/interface.rs @@ -1,18 +1,24 @@ -use crate::{MuniteResult, StoreEntry, StoreSearch}; +use crate::{MuniteResult, StoreEntry, StoreSearch, StoreExtra}; /// A search result from an audio store -pub enum AudioEntry { +pub enum AudioExtra { /// A search result from a music store - Music(super::music::MusicEntry), + Music(super::music::MusicExtra), } -impl std::convert::Into for AudioEntry { - fn into(self) -> StoreEntry { - StoreEntry::Audio(self) +/// Store audio type +pub enum AudioType { + /// Music store + Music +} + +impl std::convert::Into for AudioExtra { + fn into(self) -> StoreExtra { + StoreExtra::Audio(self) } } -impl std::fmt::Display for AudioEntry { +impl std::fmt::Display for AudioExtra { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Music(x) => write!(f, "[MUSIC]{}", x), @@ -24,21 +30,46 @@ impl std::fmt::Display for AudioEntry { #[async_trait::async_trait] pub trait AudioSearchStore { /// Search the store for the given string `s`, returning the results of the search - async fn search(&self, s: String) -> MuniteResult>; + async fn search(&self, s: String) -> MuniteResult>; + + /// Store type + fn store(&self) -> Vec; } +macro_rules! audio_store_impl { + ($name:ty) => { + #[async_trait::async_trait] + impl StoreSearch for $name { + async fn search(&self, s: String) -> MuniteResult> { + (self as &(dyn AudioSearchStore + Sync)) + .search(s) + .await + } + + fn store(&self) -> Vec { + (self as &(dyn AudioSearchStore + Sync)).store() + .into_iter() + .map(|x| crate::StoreType::Audio(x)) + .collect() + } + } + } +} + +/* #[async_trait::async_trait] impl StoreSearch for X { async fn search(&self, s: String) -> MuniteResult> { (self as &(dyn AudioSearchStore + Sync)) .search(s) .await - .map( - |entries| - entries - .into_iter() - .map(|e| e.into()) - .collect() - ) + } + + fn store(&self) -> Vec { + crate::StoreType::Audio((self as &(dyn AudioSearchStore + Sync)).store().map(|x| x.into())) } } +*/ + +audio_store_impl!{super::music::SevenDigital} +audio_store_impl!{super::music::ProStudioMasters} diff --git a/munite-core/src/audio/mod.rs b/munite-core/src/audio/mod.rs index d2c8324..5fa8baa 100644 --- a/munite-core/src/audio/mod.rs +++ b/munite-core/src/audio/mod.rs @@ -2,7 +2,7 @@ mod interface; pub mod music; -pub use interface::{AudioSearchStore, AudioEntry}; +pub use interface::{AudioSearchStore, AudioExtra, AudioType}; /// All available AudioSearchStores pub fn all() -> Vec> { diff --git a/munite-core/src/audio/music/interface.rs b/munite-core/src/audio/music/interface.rs index f3ceb80..bcfaa3a 100644 --- a/munite-core/src/audio/music/interface.rs +++ b/munite-core/src/audio/music/interface.rs @@ -1,17 +1,18 @@ use crate::MuniteResult; -use crate::audio::{AudioEntry, AudioSearchStore}; +use crate::StoreEntry; +use crate::audio::{AudioExtra, AudioSearchStore, AudioType}; /// A search result from a music store -pub enum MusicEntry { +pub enum MusicExtra { /// A search result for an artist - Artist(ArtistEntry), + Artist(ArtistExtra), /// A search result for an album or single - Release(ReleaseEntry), + Release(ReleaseExtra), /// A search result for a track - Track(TrackEntry), + Track(TrackExtra), } -impl std::fmt::Display for MusicEntry { +impl std::fmt::Display for MusicExtra { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Artist(x) => write!(f, "[ARTIST]{}", x), @@ -22,103 +23,78 @@ impl std::fmt::Display for MusicEntry { } /// A search result for an artist -pub struct ArtistEntry { - /// Artist's ID used by the store - pub id: Option, - /// Artist's name - pub name: String, +pub struct ArtistExtra { /// Artist's cover image pub image: Option, - /// Store name - pub store: String, } -impl std::convert::Into for ArtistEntry { - fn into(self) -> MusicEntry { - MusicEntry::Artist(self) +impl std::convert::Into for ArtistExtra { + fn into(self) -> MusicExtra { + MusicExtra::Artist(self) } } -impl std::fmt::Display for ArtistEntry { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if let Some(id) = self.id { - write!(f, "[{}]({}) `{}`", self.store, id, self.name) - } else { - write!(f, "[{}] `{}`", self.store, self.name) - } +impl std::fmt::Display for ArtistExtra { + fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Ok(()) } } /// A search result for an album or single -pub struct ReleaseEntry { - /// Release's ID used by the store - pub id: Option, - /// Release's title - pub title: String, +pub struct ReleaseExtra { + /// Release's artist name + pub artist_name: String, /// Release's artist - pub artist: ArtistEntry, - /// Store name - pub store: String, + pub artist: ArtistExtra, } -impl std::convert::Into for ReleaseEntry { - fn into(self) -> MusicEntry { - MusicEntry::Release(self) +impl std::convert::Into for ReleaseExtra { + fn into(self) -> MusicExtra { + MusicExtra::Release(self) } } -impl std::fmt::Display for ReleaseEntry { +impl std::fmt::Display for ReleaseExtra { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if let Some(id) = self.id { - write!(f, "[{}]({}) `{}` by {}", self.store, id, self.title, self.artist) - } else { - write!(f, "[{}] `{}` by {}", self.store, self.title, self.artist) - } + write!(f, "by {}", self.artist_name) } } /// A search result for a track -pub struct TrackEntry { - /// Track's ID used by the store - pub id: Option, - /// Track's title - pub title: String, +pub struct TrackExtra { + /// Track's artist name + pub artist_name: String, /// Track's artist - pub artist: ArtistEntry, + pub artist: ArtistExtra, /// Track's release - pub release: Option, - /// Store name - pub store: String, + pub release: Option, } -impl std::convert::Into for TrackEntry { - fn into(self) -> MusicEntry { - MusicEntry::Track(self) +impl std::convert::Into for TrackExtra { + fn into(self) -> MusicExtra { + MusicExtra::Track(self) } } -impl std::fmt::Display for TrackEntry { +impl std::fmt::Display for TrackExtra { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if let Some(id) = self.id { - - if let Some(release) = &self.release { - write!(f, "[{}]({}) `{}` from {}", self.store, id, self.title, release) - } else { - write!(f, "[{}]({}) `{}` by {}", self.store, id, self.title, self.artist) - } + if let Some(release) = &self.release { + write!(f, "from {}", release) } else { - if let Some(release) = &self.release { - write!(f, "[{}] `{}` from {}", self.store, self.title, release) - } else { - write!(f, "[{}] `{}` by {}", self.store, self.title, self.artist) - } + write!(f, "by {}", self.artist) } } } -impl std::convert::Into for MusicEntry { - fn into(self) -> AudioEntry { - AudioEntry::Music(self) +impl std::convert::Into for MusicExtra { + fn into(self) -> AudioExtra { + AudioExtra::Music(self) + } +} + +impl std::convert::Into for MusicExtra { + fn into(self) -> crate::StoreExtra { + AudioExtra::Music(self).into() } } @@ -126,22 +102,19 @@ impl std::convert::Into for MusicEntry { #[async_trait::async_trait] pub trait MusicSearchStore { /// Search the store for the given string `s`, returning the results of the search - async fn search(&self, s: String) -> MuniteResult>; + async fn search(&self, s: String) -> MuniteResult>; } #[async_trait::async_trait] impl AudioSearchStore for X { - async fn search(&self, s: String) -> MuniteResult> { + async fn search(&self, s: String) -> MuniteResult> { (self as &(dyn MusicSearchStore + Sync)) .search(s) .await - .map( - |entries| - entries - .into_iter() - .map(|e| e.into()) - .collect() - ) + } + + fn store(&self) -> Vec { + vec![AudioType::Music] } } diff --git a/munite-core/src/audio/music/mod.rs b/munite-core/src/audio/music/mod.rs index b36ac50..2b33087 100644 --- a/munite-core/src/audio/music/mod.rs +++ b/munite-core/src/audio/music/mod.rs @@ -8,7 +8,7 @@ pub use pro_studio_masters::ProStudioMasters; mod interface; -pub use interface::{MusicEntry, MusicSearchStore, TrackEntry, ArtistEntry, ReleaseEntry}; +pub use interface::{MusicExtra, MusicSearchStore, TrackExtra, ArtistExtra, ReleaseExtra}; /// All available MusicSearchStores pub fn all() -> Vec> { diff --git a/munite-core/src/audio/music/pro_studio_masters.rs b/munite-core/src/audio/music/pro_studio_masters.rs index 13d7fe0..27d7c2b 100644 --- a/munite-core/src/audio/music/pro_studio_masters.rs +++ b/munite-core/src/audio/music/pro_studio_masters.rs @@ -1,8 +1,8 @@ use reqwest::{Client, RequestBuilder}; use soup::{Soup, QueryBuilderExt, NodeExt}; -use super::{MusicEntry, MusicSearchStore, ReleaseEntry, ArtistEntry}; -use crate::MuniteError; +use super::{MusicExtra, MusicSearchStore, ReleaseExtra, ArtistExtra}; +use crate::{MuniteError, StoreEntry, StorePrice, StoreCurrency}; /// ProStudioMasters store client pub struct ProStudioMasters { @@ -43,7 +43,7 @@ impl std::default::Default for ProStudioMasters { #[async_trait::async_trait] impl MusicSearchStore for ProStudioMasters { - async fn search(&self, s: String) -> crate::MuniteResult> { + async fn search(&self, s: String) -> crate::MuniteResult> { let releases = self.search_releases(&s).send().await.map_err(|e| MuniteError::Http(e))?; let page = releases.text().await.map_err(|e| MuniteError::Http(e))?; let mut output = Vec::new(); @@ -55,20 +55,21 @@ impl MusicSearchStore for ProStudioMasters { if let Some(id) = album.get("href") { if let Some(artist) = album.tag("span").class("artist").find() { if let Some(title) = album.tag("span").class("title").find() { - output.push(ReleaseEntry { + output.push(StoreEntry { id: id.split('/') .last() .map(|x| x.parse().ok()) .flatten(), title: title.text().trim().to_owned(), - artist: ArtistEntry { - id: None, - name: artist.text().trim().to_owned(), - image: None, - store: "ProStudioMasters".to_owned(), - }, store: "ProStudioMasters".to_owned(), - }.into()); + extra: MusicExtra::Release(ReleaseExtra { + artist_name: artist.text().trim().to_owned(), + artist: ArtistExtra { + image: None, + }, + }).into(), + price: StorePrice { cents: 0, currency: StoreCurrency::Unknown }, + }); } } } diff --git a/munite-core/src/audio/music/seven_digital.rs b/munite-core/src/audio/music/seven_digital.rs index 7abd543..e8c4460 100644 --- a/munite-core/src/audio/music/seven_digital.rs +++ b/munite-core/src/audio/music/seven_digital.rs @@ -1,8 +1,8 @@ use reqwest::{Client, RequestBuilder}; use serde::Deserialize; -use super::{MusicEntry, MusicSearchStore, TrackEntry, ReleaseEntry, ArtistEntry}; -use crate::MuniteError; +use super::{MusicExtra, MusicSearchStore, TrackExtra, ReleaseExtra, ArtistExtra}; +use crate::{MuniteError, StoreEntry, StorePrice, StoreCurrency}; /// 7Digital store client pub struct SevenDigital { @@ -66,7 +66,7 @@ impl std::default::Default for SevenDigital { #[async_trait::async_trait] impl MusicSearchStore for SevenDigital { - async fn search(&self, s: String) -> crate::MuniteResult> { + async fn search(&self, s: String) -> crate::MuniteResult> { let tracks = self.search_tracks(&s).send(); let releases = self.search_releases(&s).send(); let artists = self.search_artists(&s).send(); @@ -120,12 +120,12 @@ enum AnySearchResult { }, } -impl std::convert::Into for AnySearchResult { - fn into(self) -> MusicEntry { +impl std::convert::Into for AnySearchResult { + fn into(self) -> StoreEntry { match self { - Self::artist{artist: x, ..} => MusicEntry::Artist(x.into()), - Self::release{release: x, ..} => MusicEntry::Release(x.into()), - Self::track{track: x, ..} => MusicEntry::Track(x.into()), + Self::artist{artist: x, ..} => x.into(), + Self::release{release: x, ..} => x.into(), + Self::track{track: x, ..} => x.into(), } } } @@ -143,13 +143,16 @@ struct ArtistSearchResult { popularity: Option, } -impl std::convert::Into for ArtistSearchResult { - fn into(self) -> ArtistEntry { - ArtistEntry { +impl std::convert::Into for ArtistSearchResult { + fn into(self) -> StoreEntry { + StoreEntry { id: Some(self.id), - name: self.name, - image: Some(self.image), + title: self.name, + extra: MusicExtra::Artist(ArtistExtra { + image: Some(self.image) + }).into(), store: "7digital".to_owned(), + price: StorePrice { cents: 0, currency: StoreCurrency::Unknown } } } } @@ -176,13 +179,19 @@ struct ReleaseSearchResult { download: Option, } -impl std::convert::Into for ReleaseSearchResult { - fn into(self) -> ReleaseEntry { - ReleaseEntry { +impl std::convert::Into for ReleaseSearchResult { + fn into(self) -> StoreEntry { + StoreEntry { id: Some(self.id), title: self.title, - artist: self.artist.into(), + extra: MusicExtra::Release(ReleaseExtra { + artist_name: self.artist.name, + artist: ArtistExtra { + image: Some(self.artist.image), + } + }).into(), store: "7digital".to_owned(), + price: StorePrice { cents: 0, currency: StoreCurrency::Unknown } } } } @@ -219,14 +228,25 @@ struct TrackSearchResult { download: Option, } -impl std::convert::Into for TrackSearchResult { - fn into(self) -> TrackEntry { - TrackEntry { +impl std::convert::Into for TrackSearchResult { + fn into(self) -> StoreEntry { + StoreEntry { id: Some(self.id), title: self.title, - release: Some(self.release.into()), - artist: self.artist.into(), + extra: MusicExtra::Track(TrackExtra { + artist_name: self.artist.name.to_owned(), + artist: ArtistExtra { + image: Some(self.artist.image), + }, + release: Some(ReleaseExtra { + artist_name: self.release.artist.name.to_owned(), + artist: ArtistExtra { + image: Some(self.release.artist.image), + }, + }), + }).into(), store: "7digital".to_owned(), + price: StorePrice { cents: 0, currency: StoreCurrency::Unknown } } } } diff --git a/munite-core/src/entry.rs b/munite-core/src/entry.rs new file mode 100644 index 0000000..c7aea2c --- /dev/null +++ b/munite-core/src/entry.rs @@ -0,0 +1,73 @@ +/// Extra entry information for a search result +pub enum StoreExtra { + /// Extra search information from an audio store + Audio(crate::audio::AudioExtra), + /// Extra search information from a video store + Video(crate::video::VideoExtra), +} + +/// Store type +pub enum StoreType { + /// Audio store + Audio(crate::audio::AudioType), + /// Video store + Video(crate::video::VideoType), +} + +/// A search result from a store +pub struct StoreEntry { + /// Entry ID used by the store + pub id: Option, + /// Entry title + pub title: String, + /// Store name + pub store: String, + /// Extra information + pub extra: StoreExtra, + /// Price information + pub price: StorePrice, +} + +/// Store price information +pub struct StorePrice { + /// Price of store result, in 100ths of a dollar + pub cents: u64, + /// Price currency + pub currency: StoreCurrency, +} + +/// Price type +pub enum StoreCurrency { + /// Canadian Dollars ($) + Cad, + /// Unknown currency (?) + Unknown, +} + +impl std::fmt::Display for StoreExtra { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Audio(x) => write!(f, "[AUDIO]{}", x), + Self::Video(x) => write!(f, "[VIDEO]{}", x), + } + } +} + +impl std::fmt::Display for StoreEntry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(id) = self.id { + write!(f, "{} ({}:{}) {} ({})", self.title, self.store, id, self.extra, self.price) + } else { + write!(f, "{} ({}) {} ({})", self.title, self.store, self.extra, self.price) + } + } +} + +impl std::fmt::Display for StorePrice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.currency { + StoreCurrency::Cad => write!(f, "${}.{} CAD", self.cents / 100, self.cents % 100), + StoreCurrency::Unknown => write!(f, "?{}.{}?", self.cents / 100, self.cents % 100), + } + } +} diff --git a/munite-core/src/lib.rs b/munite-core/src/lib.rs index e3ecf6a..f428701 100644 --- a/munite-core/src/lib.rs +++ b/munite-core/src/lib.rs @@ -2,12 +2,15 @@ #![warn(missing_docs)] pub mod audio; +pub mod video; +mod entry; mod errors; mod search; +pub use entry::{StoreEntry, StoreExtra, StoreType, StorePrice, StoreCurrency}; pub use errors::{MuniteError, MuniteResult}; -pub use search::{StoreSearch, StoreEntry}; +pub use search::StoreSearch; /// All available SearchStores pub fn stores() -> Vec> { diff --git a/munite-core/src/search.rs b/munite-core/src/search.rs index f8c7b68..eaa7d40 100644 --- a/munite-core/src/search.rs +++ b/munite-core/src/search.rs @@ -1,22 +1,12 @@ use super::MuniteResult; - -/// A search result from a store -pub enum StoreEntry { - /// A search result from an audio store - Audio(crate::audio::AudioEntry), -} - -impl std::fmt::Display for StoreEntry { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Audio(x) => write!(f, "[AUDIO]{}", x), - } - } -} +use super::{StoreEntry, StoreType}; /// Store search client for a specific store #[async_trait::async_trait] pub trait StoreSearch { /// Search the store for the given string `s`, returning the results of the search async fn search(&self, s: String) -> MuniteResult>; + + /// Store type + fn store(&self) -> Vec; } diff --git a/munite-core/src/video/cinema1.rs b/munite-core/src/video/cinema1.rs new file mode 100644 index 0000000..0deeff6 --- /dev/null +++ b/munite-core/src/video/cinema1.rs @@ -0,0 +1,142 @@ +use reqwest::{Client, RequestBuilder}; +use soup::{Soup, QueryBuilderExt, NodeExt}; + +use super::{VideoExtra, /*TvSeriesExtra, MovieExtra, VideoBoxSetExtra, VideoInfoExtra,*/ VideoType, VideoFormat}; +use crate::{MuniteError, StoreEntry, StorePrice, StoreCurrency}; + +/// Cinema1 store client +pub struct CinemaOne { + client: Client, +} + +impl CinemaOne { + /// Create a new client for the Cinema1 store + pub fn new() -> Self { + Self { + client: Client::new(), + } + } + + fn apply_search_query(&self, r: RequestBuilder, query: &str, page: &str) -> RequestBuilder { + r + .query(&[ + ("p", page), + ("q", query), + ("product_list_limit", "36") + ]) + //.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8") + //.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; rv:108.0) Gecko/20100101 Firefox/108.0") + } + + fn search_releases(&self, query: &str, page: usize) -> RequestBuilder { + self.apply_search_query( + self.client.get("https://www.cinema1.ca/catalogsearch/result/index/"), + query, + &page.to_string(), + ) + } + + fn process_page(results: &mut Vec, soup: &Soup) { + for item in soup + .tag("div") + .class("product-item-info") + .find_all() { + //dbg!(item.display()); + if let Some(title) = item.tag("strong").class("product-item-name").find() { + if let Some(url) = title.tag("a").class("product-item-link").find() { + if let Some(price) = item.tag("span").class("price").find() { + if let Some(formats) = item.tag("div").class("format-toggle").find() { + if let Some(format) = formats.tag("li").class("active").find() { + let format_info = match format.text().trim() as &str { + "4K" => VideoFormat::BlurayUhd, + "3D BLU-RAY" => VideoFormat::Bluray3d, + "BLU-RAY" => VideoFormat::Bluray, + "DVD" => VideoFormat::Dvd, + x => panic!("Unrecognized format {}", x) + }; + let price: f64 = price.text().replace("$", "").trim().parse().unwrap(); + let price_info = StorePrice { + cents: (price * 100.0) as _, + currency: StoreCurrency::Cad, + }; + results.push(StoreEntry { + id: url.get("href") + .expect(" tag without href attribute") + .split('-') + .last() + .map(|x| x.replace(".html", "").parse().ok()) + .flatten(), + title: title.text().trim().to_owned(), + store: "Cinema1".to_owned(), + extra: VideoExtra { + format: format_info, + info: None, // TODO + }.into(), + price: price_info, + }); + } + } + } + } + } + } + } + + fn next_page_exists(soup: &Soup) -> bool { + soup.class("pages-item-next").find().is_some() + } +} + +impl std::default::Default for CinemaOne { + fn default() -> Self { + Self::new() + } +} + +#[async_trait::async_trait] +impl super::VideoSearchStore for CinemaOne { + async fn search(&self, s: String) -> crate::MuniteResult> { + let mut page = 1; + let mut output = Vec::new(); + let releases = self.search_releases(&s, page).send().await.map_err(|e| MuniteError::Http(e))?; + let page_html = releases.text().await.map_err(|e| MuniteError::Http(e))?; + let mut next_page_ok = { + let soup_data = Soup::new(&page_html); + let next_page_ok = Self::next_page_exists(&soup_data); + Self::process_page(&mut output, &soup_data); + next_page_ok + }; + while next_page_ok { + page += 1; + let releases = self.search_releases(&s, page).send().await.map_err(|e| MuniteError::Http(e))?; + let page_html = releases.text().await.map_err(|e| MuniteError::Http(e))?; + let soup_data = Soup::new(&page_html); + next_page_ok = Self::next_page_exists(&soup_data); + Self::process_page(&mut output, &soup_data); + } + Ok(output) + } + + fn store(&self) -> Vec { + vec![ + VideoType::TvSeries, + VideoType::Movie, + ] + } +} + +// Tests + +#[cfg(test)] +mod tests { + use super::*; + use crate::video::VideoSearchStore; + + #[tokio::test] + async fn interface_test_prostudiomasters() { + let store = CinemaOne::new(); + let results = store.search("test".to_owned()).await; + let results = results.expect("Search results query failed"); + assert_eq!(results.len(), 144); + } +} diff --git a/munite-core/src/video/interface.rs b/munite-core/src/video/interface.rs new file mode 100644 index 0000000..c1bdb1e --- /dev/null +++ b/munite-core/src/video/interface.rs @@ -0,0 +1,178 @@ +use crate::{MuniteResult, StoreEntry, StoreSearch, StoreExtra}; + +/// A search result from a video store +pub struct VideoExtra { + /// Video format + pub format: VideoFormat, + /// Additional video informatio + pub info: Option, +} + +/// A search result from a video store +pub enum VideoInfoExtra { + /// A search result for a TV series + TvSeries(TvSeriesExtra), + /// A search result for a movie + Movie(MovieExtra), + /// A search results for a box set of movies or TV series + Set(VideoBoxSetExtra), +} + +impl std::fmt::Display for VideoInfoExtra { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::TvSeries(x) => write!(f, "[TV]{}", x), + Self::Movie(x) => write!(f, "[MOVIE]{}", x), + Self::Set(x) => write!(f, "[BOXSET]{}", x), + } + } +} + +/// Video format +pub enum VideoFormat { + /// Non-physical media + Digital, + /// 4K UHD Blu-ray disc + BlurayUhd, + /// 3D Blu-ray disc + Bluray3d, + /// Standard Blu-ray disc + Bluray, + /// DVD + Dvd, +} + +impl std::fmt::Display for VideoFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Digital => write!(f, "DIGITAL"), + Self::BlurayUhd => write!(f, "4K-BD"), + Self::Bluray3d => write!(f, "3D-BD"), + Self::Bluray => write!(f, "BD"), + Self::Dvd => write!(f, "DVD"), + } + } +} + +/// A search result for a TV series +pub struct TvSeriesExtra { + +} + + +impl std::convert::Into for TvSeriesExtra { + fn into(self) -> VideoInfoExtra { + VideoInfoExtra::TvSeries(self) + } +} + +impl std::fmt::Display for TvSeriesExtra { + fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Ok(()) + } +} + +/// A search result for a movie +pub struct MovieExtra { + +} + +impl std::convert::Into for MovieExtra { + fn into(self) -> VideoInfoExtra { + VideoInfoExtra::Movie(self) + } +} + +impl std::fmt::Display for MovieExtra { + fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Ok(()) + } +} + +/// A search result for a movie or TV series box set +pub struct VideoBoxSetExtra { + +} + +impl std::convert::Into for VideoBoxSetExtra { + fn into(self) -> VideoInfoExtra { + VideoInfoExtra::Set(self) + } +} + +impl std::fmt::Display for VideoBoxSetExtra { + fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Ok(()) + } +} + +/// Store video type +pub enum VideoType { + /// TV store + TvSeries, + /// Movie store + Movie, +} + +impl std::convert::Into for VideoExtra { + fn into(self) -> StoreExtra { + StoreExtra::Video(self) + } +} + +impl std::fmt::Display for VideoExtra { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.info { + Some(info) => write!(f, "[{}]{}", self.format, info), + None => write!(f, "[{}]", self.format) + } + + } +} + +/// Store with audio-specific functionality +#[async_trait::async_trait] +pub trait VideoSearchStore { + /// Search the store for the given string `s`, returning the results of the search + async fn search(&self, s: String) -> MuniteResult>; + + /// Store type + fn store(&self) -> Vec; +} + +macro_rules! video_store_impl { + ($name:ty) => { + #[async_trait::async_trait] + impl StoreSearch for $name { + async fn search(&self, s: String) -> MuniteResult> { + (self as &(dyn VideoSearchStore + Sync)) + .search(s) + .await + } + + fn store(&self) -> Vec { + (self as &(dyn VideoSearchStore + Sync)).store() + .into_iter() + .map(|x| crate::StoreType::Video(x)) + .collect() + } + } + } +} + +/* +#[async_trait::async_trait] +impl StoreSearch for X { + async fn search(&self, s: String) -> MuniteResult> { + (self as &(dyn VideoSearchStore + Sync)) + .search(s) + .await + } + + fn store(&self) -> crate::StoreType { + crate::StoreType::Audio((self as &(dyn VideoSearchStore + Sync)).store()) + } +} +*/ + +video_store_impl!{super::CinemaOne} diff --git a/munite-core/src/video/mod.rs b/munite-core/src/video/mod.rs new file mode 100644 index 0000000..7c68526 --- /dev/null +++ b/munite-core/src/video/mod.rs @@ -0,0 +1,20 @@ +//! Video store functionality +mod interface; +mod cinema1; + +pub use interface::{VideoSearchStore, VideoExtra, VideoType, TvSeriesExtra, MovieExtra, VideoBoxSetExtra, VideoInfoExtra, VideoFormat}; +pub use cinema1::CinemaOne; + +/// All available VideoSearchStores +pub fn all() -> Vec> { + vec![ + Box::new(CinemaOne::default()), + ] +} + +/// All available audio SearchStores +pub fn stores() -> Vec> { + vec![ + Box::new(CinemaOne::default()), + ] +} diff --git a/src/cli.rs b/src/cli.rs index d11bfd1..f4a723c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -25,6 +25,8 @@ pub enum StoreCategory { Audio(StandardSearch), /// Search all music stores Music(StandardSearch), + /// Search all video stores + Video(StandardSearch), } #[derive(Args, Debug)] diff --git a/src/main.rs b/src/main.rs index ca0181a..8d6c83d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,10 @@ async fn main() { let stores = munite_core::audio::music::stores(); search_query(stores, s, args.max).await }, + cli::StoreCategory::Video(s) => { + let stores = munite_core::video::stores(); + search_query(stores, s, args.max).await + }, }; for entry in results { match entry {