diff --git a/munite-core/src/audio/interface.rs b/munite-core/src/audio/interface.rs index f5d57c4..9de6e08 100644 --- a/munite-core/src/audio/interface.rs +++ b/munite-core/src/audio/interface.rs @@ -7,9 +7,33 @@ pub enum AudioExtra { } /// Store audio type +#[derive(Eq)] pub enum AudioType { /// Music store - Music + Music, + /// Matches any audio type (not to be used by stores) + Any +} + +// nighytly-only +//const ANY_DISCRIMINANT: std::mem::Discriminant = std::mem::discriminant(&AudioType::Any); + +impl std::cmp::PartialEq for AudioType { + fn eq(&self, other: &Self) -> bool { + let self_d = std::mem::discriminant(self); + let other_d = std::mem::discriminant(other); + #[allow(non_snake_case)] + let ANY_DISCRIMINANT = std::mem::discriminant(&Self::Any); + self_d == other_d || self_d == ANY_DISCRIMINANT || other_d == ANY_DISCRIMINANT + } + + fn ne(&self, other: &Self) -> bool { + let self_d = std::mem::discriminant(self); + let other_d = std::mem::discriminant(other); + #[allow(non_snake_case)] + let ANY_DISCRIMINANT = std::mem::discriminant(&Self::Any); + self_d != other_d && self_d != ANY_DISCRIMINANT && other_d != ANY_DISCRIMINANT + } } impl std::convert::Into for AudioExtra { @@ -73,3 +97,16 @@ impl StoreSearch for X { audio_store_impl!{super::music::SevenDigital} audio_store_impl!{super::music::ProStudioMasters} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn audiotype_eq_test() { + assert!(AudioType::Music == AudioType::Any); + assert!(AudioType::Any == AudioType::Music); + assert!(AudioType::Any == AudioType::Any); + assert!(AudioType::Music == AudioType::Music); + } +} diff --git a/munite-core/src/audio/music/interface.rs b/munite-core/src/audio/music/interface.rs index bcfaa3a..90c2cf5 100644 --- a/munite-core/src/audio/music/interface.rs +++ b/munite-core/src/audio/music/interface.rs @@ -10,6 +10,8 @@ pub enum MusicExtra { Release(ReleaseExtra), /// A search result for a track Track(TrackExtra), + /// A search result for a physical album with limited information + Physical(MusicMediumType), } impl std::fmt::Display for MusicExtra { @@ -18,6 +20,27 @@ impl std::fmt::Display for MusicExtra { Self::Artist(x) => write!(f, "[ARTIST]{}", x), Self::Release(x) => write!(f, "[RELEASE]{}", x), Self::Track(x) => write!(f, "[TRACK]{}", x), + Self::Physical(x) => write!(f, "[{}]", x), + } + } +} + +/// Physical music media +pub enum MusicMediumType { + /// Compact Disc + CD, + /// Vinyl LP + Vinyl, + /// Any cassette form factor + Cassette, +} + +impl std::fmt::Display for MusicMediumType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::CD => write!(f, "CD"), + Self::Vinyl => write!(f, "Vinyl"), + Self::Cassette => write!(f, "Cassette"), } } } diff --git a/munite-core/src/audio/music/mod.rs b/munite-core/src/audio/music/mod.rs index 2b33087..7ee200d 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::{MusicExtra, MusicSearchStore, TrackExtra, ArtistExtra, ReleaseExtra}; +pub use interface::{MusicExtra, MusicSearchStore, TrackExtra, ArtistExtra, ReleaseExtra, MusicMediumType}; /// 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 27d7c2b..b811129 100644 --- a/munite-core/src/audio/music/pro_studio_masters.rs +++ b/munite-core/src/audio/music/pro_studio_masters.rs @@ -2,7 +2,7 @@ use reqwest::{Client, RequestBuilder}; use soup::{Soup, QueryBuilderExt, NodeExt}; use super::{MusicExtra, MusicSearchStore, ReleaseExtra, ArtistExtra}; -use crate::{MuniteError, StoreEntry, StorePrice, StoreCurrency}; +use crate::{MuniteError, StoreEntry, StorePrice, StoreCurrency, StoreId}; /// ProStudioMasters store client pub struct ProStudioMasters { @@ -59,7 +59,8 @@ impl MusicSearchStore for ProStudioMasters { id: id.split('/') .last() .map(|x| x.parse().ok()) - .flatten(), + .flatten() + .map(|x| StoreId::Id(x)), title: title.text().trim().to_owned(), store: "ProStudioMasters".to_owned(), extra: MusicExtra::Release(ReleaseExtra { @@ -89,6 +90,6 @@ mod tests { let store = ProStudioMasters::new(); let results = store.search("test".to_owned()).await; let results = results.expect("Search results query failed"); - assert_eq!(results.len(), 144); + assert_ne!(results.len(), 0); } } diff --git a/munite-core/src/audio/music/seven_digital.rs b/munite-core/src/audio/music/seven_digital.rs index e8c4460..bb94f3d 100644 --- a/munite-core/src/audio/music/seven_digital.rs +++ b/munite-core/src/audio/music/seven_digital.rs @@ -2,7 +2,7 @@ use reqwest::{Client, RequestBuilder}; use serde::Deserialize; use super::{MusicExtra, MusicSearchStore, TrackExtra, ReleaseExtra, ArtistExtra}; -use crate::{MuniteError, StoreEntry, StorePrice, StoreCurrency}; +use crate::{MuniteError, StoreEntry, StorePrice, StoreCurrency, StoreId}; /// 7Digital store client pub struct SevenDigital { @@ -146,7 +146,7 @@ struct ArtistSearchResult { impl std::convert::Into for ArtistSearchResult { fn into(self) -> StoreEntry { StoreEntry { - id: Some(self.id), + id: Some(StoreId::Id(self.id)), title: self.name, extra: MusicExtra::Artist(ArtistExtra { image: Some(self.image) @@ -182,7 +182,7 @@ struct ReleaseSearchResult { impl std::convert::Into for ReleaseSearchResult { fn into(self) -> StoreEntry { StoreEntry { - id: Some(self.id), + id: Some(StoreId::Id(self.id)), title: self.title, extra: MusicExtra::Release(ReleaseExtra { artist_name: self.artist.name, @@ -231,7 +231,7 @@ struct TrackSearchResult { impl std::convert::Into for TrackSearchResult { fn into(self) -> StoreEntry { StoreEntry { - id: Some(self.id), + id: Some(StoreId::Id(self.id)), title: self.title, extra: MusicExtra::Track(TrackExtra { artist_name: self.artist.name.to_owned(), @@ -265,11 +265,11 @@ mod tests { #[tokio::test] async fn interface_test_7digital() { - let key = std::env::var("SEVENDIGITAL_OATH_KEY").expect("missing 7Digital oath key in environment"); + let key = std::env::var("SEVENDIGITAL_OATH_KEY").unwrap_or_else(|_| String::from("7drfpc993qp5")); let shop_id = 1069; // Canada let store = SevenDigital::new(shop_id, key); let results = store.search("test".to_owned()).await; let results = results.expect("Search results query failed"); - assert_eq!(results.len(), 33); + assert_ne!(results.len(), 0); } } diff --git a/munite-core/src/entry.rs b/munite-core/src/entry.rs index c7aea2c..cdbf74f 100644 --- a/munite-core/src/entry.rs +++ b/munite-core/src/entry.rs @@ -4,20 +4,25 @@ pub enum StoreExtra { Audio(crate::audio::AudioExtra), /// Extra search information from a video store Video(crate::video::VideoExtra), + /// No extra search information available + Nothing, } /// Store type +#[derive(PartialEq, Eq)] pub enum StoreType { /// Audio store Audio(crate::audio::AudioType), /// Video store Video(crate::video::VideoType), + /// General store + General, } /// A search result from a store pub struct StoreEntry { /// Entry ID used by the store - pub id: Option, + pub id: Option, /// Entry title pub title: String, /// Store name @@ -28,6 +33,34 @@ pub struct StoreEntry { pub price: StorePrice, } +/// A store identifier +pub enum StoreId { + /// SKU string + Sku(String), + /// Numerical ID + Id(u64), +} + +impl StoreId { + /// Parse a raw store id into number or sku variant + pub fn from_parse(input: String) -> Self { + if let Ok(id) = input.parse() { + Self::Id(id) + } else { + Self::Sku(input) + } + } +} + +impl std::fmt::Display for StoreId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Sku(s) => write!(f, "{}", s), + Self::Id(id) => write!(f, "{}", id), + } + } +} + /// Store price information pub struct StorePrice { /// Price of store result, in 100ths of a dollar @@ -49,13 +82,14 @@ impl std::fmt::Display for StoreExtra { match self { Self::Audio(x) => write!(f, "[AUDIO]{}", x), Self::Video(x) => write!(f, "[VIDEO]{}", x), + Self::Nothing => write!(f, ""), } } } impl std::fmt::Display for StoreEntry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if let Some(id) = self.id { + 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) @@ -66,8 +100,8 @@ impl std::fmt::Display for StoreEntry { 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), + StoreCurrency::Cad => write!(f, "${}.{:02} CAD", self.cents / 100, self.cents % 100), + StoreCurrency::Unknown => write!(f, "?{}.{:02}?", self.cents / 100, self.cents % 100), } } } diff --git a/munite-core/src/general/best_buy_ca.rs b/munite-core/src/general/best_buy_ca.rs new file mode 100644 index 0000000..c12496c --- /dev/null +++ b/munite-core/src/general/best_buy_ca.rs @@ -0,0 +1,207 @@ +use reqwest::{Client, RequestBuilder}; +use serde::Deserialize; + +use super::GeneralSearchStore; +use crate::audio::{AudioExtra, music::{MusicExtra, MusicMediumType}}; +use crate::video::{VideoExtra, VideoInfoExtra, MovieExtra, VideoFormat, TvSeriesExtra}; +use crate::{MuniteError, StoreEntry, StorePrice, StoreCurrency, StoreType, StoreId, StoreExtra}; + +/// BestBuy Canada store client +pub struct BestBuyCa { + client: Client, +} + +impl BestBuyCa { + /// Create a new client for a BestBuy store + pub fn new() -> Self { + Self { + client: Client::new(), + } + } + + fn apply_search_query(&self, r: RequestBuilder, query: &str, page: u64, page_size: u64, path: &str, sort_by: &str, sort_direction: &str) -> RequestBuilder { + r + .query(&[ + ("page", &page.to_string() as &str), + ("path", path), + ("pageSize", &page_size.to_string() as &str), + ("query", query), + ("sortBy", sort_by), + ("sortDir", sort_direction), + ("exp", "search_abtesting_5050_conversion:b") + ]) + //.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") + //.header("Accept", "application/json, text/javascript, */*; q=0.01") + } + + fn search_store(&self, query: &str, page: u64, page_size: u64, path: &str, sort_by: &str, sort_direction: &str) -> RequestBuilder { + self.apply_search_query( + self.client.get("https://www.bestbuy.ca/api/v2/json/search"), + query, + page, + page_size, + path, + sort_by, + sort_direction, + ) + } +} + +impl std::default::Default for BestBuyCa { + fn default() -> Self { + Self::new() + } +} + +const MAX_PAGE_SIZE: u64 = 100; // Anything larger is clamped back down to this value + +#[async_trait::async_trait] +impl GeneralSearchStore for BestBuyCa { + async fn search(&self, s: String, category: StoreType) -> crate::MuniteResult> { + let result = match category { + StoreType::General => self.search_store(&s, 1, MAX_PAGE_SIZE, "", "relevance", "desc").send().await, + StoreType::Video(_) => self.search_store(&s, 1, MAX_PAGE_SIZE, "category:Movies & Music;category:Movies & TV Shows", "relevance", "desc").send().await, + StoreType::Audio(_) => self.search_store(&s, 1, MAX_PAGE_SIZE, "category:Movies & Music;category:Music", "relevance", "desc").send().await, + }.map_err(|e| MuniteError::Http(e))?; + let json_data: SearchResultsResponse = result.json().await.map_err(|e| MuniteError::Http(e))?; + let mut output = Vec::with_capacity(json_data.products.len()); + for entry in json_data.products { + output.push(entry.into()); + } + Ok(output) + } + + fn store(&self) -> Vec { + vec![ + StoreType::General, + StoreType::Audio(crate::audio::AudioType::Music), + StoreType::Video(crate::video::VideoType::TvSeries), + StoreType::Video(crate::video::VideoType::Movie), + ] + } +} + +// API datatypes + +#[allow(non_snake_case, dead_code)] +#[derive(Deserialize)] +struct SearchResultsResponse { + currentPage: u64, + total: u64, + totalPages: u64, + pageSize: u64, + products: Vec +} + +#[allow(non_snake_case, dead_code)] +#[derive(Deserialize)] +struct ProductInfo { + sku: String, + name: String, + shortDescription: String, + productUrl: String, + regularPrice: f64, + salePrice: f64, + categoryName: String, + seoText: String, + categoryIds: Vec, // numbers as strings +} + +fn seo_contains(seo: &str, word: &str) -> bool { + seo.contains(&format!("-{}-", word)) + || seo.starts_with(&format!("{}-", word)) + || seo.ends_with(&format!("-{}", word)) +} + +fn product_extras(prod: &ProductInfo) -> StoreExtra { + // some category ids: + // 20404 is Movies & TV Shows + // 20404a is Blu-ray movies + // 20404b is DVD movies + // 20527 is DVD TV Shows + // 27659 is Blu-ray TV shows + // 27645 is Blu-ray action movies + // 20002 is Movies & Music + // 20554 is Music + // 20986 is Rock Music + if prod.categoryIds.contains(&"20002".to_owned()) { + // is audio or video + if prod.categoryIds.contains(&"20554".to_owned()) { + // is music + if seo_contains(&prod.seoText, "vinyl") { + StoreExtra::Audio(AudioExtra::Music(MusicExtra::Physical(MusicMediumType::Vinyl))) + } else if seo_contains(&prod.seoText, "compact-discs") { + StoreExtra::Audio(AudioExtra::Music(MusicExtra::Physical(MusicMediumType::CD))) + } else { + StoreExtra::Nothing + } + } else if prod.categoryIds.contains(&"20404".to_owned()) { + // is video + // NOTE: 4K Blu-rays are NOT a different category, they're grouped in with regular blu-rays + if prod.categoryIds.contains(&"20404a".to_owned()) { + // Blu-ray movie + StoreExtra::Video(VideoExtra { + format: if seo_contains(&prod.seoText, "4k") { VideoFormat::BlurayUhd } else { VideoFormat::Bluray }, + info: Some(VideoInfoExtra::Movie(MovieExtra { })), + }) + } else if prod.categoryIds.contains(&"20404b".to_owned()) { + // DVD movie + StoreExtra::Video(VideoExtra { + format: VideoFormat::Dvd, + info: Some(VideoInfoExtra::Movie(MovieExtra { })), + }) + } else if prod.categoryIds.contains(&"27659".to_owned()) { + // Blu-ray TV show + StoreExtra::Video(VideoExtra { + format: if seo_contains(&prod.seoText, "4k") { VideoFormat::BlurayUhd } else { VideoFormat::Bluray }, + info: Some(VideoInfoExtra::TvSeries(TvSeriesExtra { })), + }) + } else if prod.categoryIds.contains(&"20527".to_owned()) { + // DVD TV show + StoreExtra::Video(VideoExtra { + format: VideoFormat::Dvd, + info: Some(VideoInfoExtra::TvSeries(TvSeriesExtra { })), + }) + } else { + StoreExtra::Nothing + } + } else { + StoreExtra::Nothing + } + } else { + StoreExtra::Nothing + } +} + +impl std::convert::Into for ProductInfo { + fn into(self) -> StoreEntry { + let extra_info = product_extras(&self); + StoreEntry { + id: Some(StoreId::Sku(self.sku)), + title: self.name, + store: "BestBuy.ca".to_owned(), + extra: extra_info, + price: StorePrice { + cents: (self.salePrice * 100.0) as _, + currency: StoreCurrency::Cad, + }, + } + } +} + +// Tests + +#[cfg(test)] +mod tests { + use super::*; + use crate::StoreSearch; + + #[tokio::test] + async fn interface_test_bestbuyca() { + let store = BestBuyCa::new(); + let results = (&store as &dyn StoreSearch).search("test".to_owned()).await; + let results = results.expect("Search results query failed"); + assert_ne!(results.len(), 0); + } +} diff --git a/munite-core/src/general/interface.rs b/munite-core/src/general/interface.rs new file mode 100644 index 0000000..553c0e5 --- /dev/null +++ b/munite-core/src/general/interface.rs @@ -0,0 +1,64 @@ +use crate::{MuniteResult, StoreEntry, StoreSearch, StoreType}; +use crate::audio::{AudioType, AudioSearchStore}; +use crate::video::{VideoType, VideoSearchStore}; + +/// Store with no specific goods functionality +#[async_trait::async_trait] +pub trait GeneralSearchStore { + /// Search the store for the given string `s`, returning the results of the search + async fn search(&self, s: String, category: StoreType) -> MuniteResult>; + + /// Store type + fn store(&self) -> Vec; +} + +macro_rules! general_store_impl { + ($name:ty) => { + #[async_trait::async_trait] + impl StoreSearch for $name { + async fn search(&self, s: String) -> MuniteResult> { + (self as &(dyn GeneralSearchStore + Sync)) + .search(s, StoreType::General) + .await + } + + fn store(&self) -> Vec { + (self as &(dyn GeneralSearchStore)).store() + } + } + + #[async_trait::async_trait] + impl VideoSearchStore for $name { + async fn search(&self, s: String) -> MuniteResult> { + (self as &(dyn GeneralSearchStore + Sync)) + .search(s, StoreType::Video(VideoType::Any)) + .await + } + + fn store(&self) -> Vec { + (self as &(dyn GeneralSearchStore)).store() + .into_iter() + .filter_map(|x| if let StoreType::Video(v) = x { Some(v) } else {None}) + .collect() + } + } + + #[async_trait::async_trait] + impl AudioSearchStore for $name { + async fn search(&self, s: String) -> MuniteResult> { + (self as &(dyn GeneralSearchStore + Sync)) + .search(s, StoreType::Audio(AudioType::Any)) + .await + } + + fn store(&self) -> Vec { + (self as &(dyn GeneralSearchStore)).store() + .into_iter() + .filter_map(|x| if let StoreType::Audio(a) = x { Some(a) } else {None}) + .collect() + } + } + } +} + +general_store_impl!{super::BestBuyCa} diff --git a/munite-core/src/general/mod.rs b/munite-core/src/general/mod.rs new file mode 100644 index 0000000..0ea61d2 --- /dev/null +++ b/munite-core/src/general/mod.rs @@ -0,0 +1,34 @@ +//! General store functionality +mod interface; +mod best_buy_ca; + +pub use interface::GeneralSearchStore; +pub use best_buy_ca::BestBuyCa; + +/// All available GeneralSearchStores +pub fn all() -> Vec> { + vec![ + Box::new(BestBuyCa::default()) + ] +} + +/// All available GeneralSearchStores +pub fn all_audio() -> Vec> { + vec![ + Box::new(BestBuyCa::default()) + ] +} + +/// All available GeneralSearchStores +pub fn all_video() -> Vec> { + vec![ + Box::new(BestBuyCa::default()) + ] +} + +/// All available general SearchStores +pub fn stores() -> Vec> { + vec![ + Box::new(BestBuyCa::default()) + ] +} diff --git a/munite-core/src/lib.rs b/munite-core/src/lib.rs index f428701..557d85f 100644 --- a/munite-core/src/lib.rs +++ b/munite-core/src/lib.rs @@ -1,20 +1,25 @@ //! Core functionality for Munite, a united media search client #![warn(missing_docs)] +// nightly-only +//#![feature(const_discriminant)] pub mod audio; +pub mod general; pub mod video; mod entry; mod errors; mod search; -pub use entry::{StoreEntry, StoreExtra, StoreType, StorePrice, StoreCurrency}; +pub use entry::{StoreEntry, StoreExtra, StoreType, StorePrice, StoreCurrency, StoreId}; pub use errors::{MuniteError, MuniteResult}; pub use search::StoreSearch; /// All available SearchStores pub fn stores() -> Vec> { - audio::stores() + let mut all = audio::stores(); + all.append(&mut video::stores()); + all } #[cfg(test)] diff --git a/munite-core/src/video/cinema1.rs b/munite-core/src/video/cinema1.rs index 0deeff6..cd936d9 100644 --- a/munite-core/src/video/cinema1.rs +++ b/munite-core/src/video/cinema1.rs @@ -2,7 +2,7 @@ use reqwest::{Client, RequestBuilder}; use soup::{Soup, QueryBuilderExt, NodeExt}; use super::{VideoExtra, /*TvSeriesExtra, MovieExtra, VideoBoxSetExtra, VideoInfoExtra,*/ VideoType, VideoFormat}; -use crate::{MuniteError, StoreEntry, StorePrice, StoreCurrency}; +use crate::{MuniteError, StoreEntry, StorePrice, StoreCurrency, StoreId}; /// Cinema1 store client pub struct CinemaOne { @@ -65,7 +65,8 @@ impl CinemaOne { .split('-') .last() .map(|x| x.replace(".html", "").parse().ok()) - .flatten(), + .flatten() + .map(|x| StoreId::Id(x)), title: title.text().trim().to_owned(), store: "Cinema1".to_owned(), extra: VideoExtra { @@ -82,6 +83,7 @@ impl CinemaOne { } } + #[allow(dead_code)] fn next_page_exists(soup: &Soup) -> bool { soup.class("pages-item-next").find().is_some() } @@ -96,11 +98,14 @@ impl std::default::Default for CinemaOne { #[async_trait::async_trait] impl super::VideoSearchStore for CinemaOne { async fn search(&self, s: String) -> crate::MuniteResult> { - let mut page = 1; + 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); + Self::process_page(&mut output, &soup_data); + // Disabled since it's too slow + /*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); @@ -113,7 +118,7 @@ impl super::VideoSearchStore for CinemaOne { 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) } @@ -133,10 +138,10 @@ mod tests { use crate::video::VideoSearchStore; #[tokio::test] - async fn interface_test_prostudiomasters() { + async fn interface_test_cinema1() { 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); + assert_ne!(results.len(), 0); } } diff --git a/munite-core/src/video/interface.rs b/munite-core/src/video/interface.rs index c1bdb1e..45ba59f 100644 --- a/munite-core/src/video/interface.rs +++ b/munite-core/src/video/interface.rs @@ -107,11 +107,32 @@ impl std::fmt::Display for VideoBoxSetExtra { } /// Store video type +#[derive(Eq)] pub enum VideoType { /// TV store TvSeries, /// Movie store Movie, + /// Matches any video type (not to be used by stores) + Any +} + +impl std::cmp::PartialEq for VideoType { + fn eq(&self, other: &Self) -> bool { + let self_d = std::mem::discriminant(self); + let other_d = std::mem::discriminant(other); + #[allow(non_snake_case)] + let ANY_DISCRIMINANT = std::mem::discriminant(&Self::Any); + self_d == other_d || self_d == ANY_DISCRIMINANT || other_d == ANY_DISCRIMINANT + } + + fn ne(&self, other: &Self) -> bool { + let self_d = std::mem::discriminant(self); + let other_d = std::mem::discriminant(other); + #[allow(non_snake_case)] + let ANY_DISCRIMINANT = std::mem::discriminant(&Self::Any); + self_d != other_d && self_d != ANY_DISCRIMINANT && other_d != ANY_DISCRIMINANT + } } impl std::convert::Into for VideoExtra { @@ -176,3 +197,19 @@ impl StoreSearch for X { */ video_store_impl!{super::CinemaOne} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn videotype_eq_test() { + assert!(VideoType::TvSeries == VideoType::Any); + assert!(VideoType::Any == VideoType::TvSeries); + assert!(VideoType::Movie == VideoType::Any); + assert!(VideoType::Any == VideoType::Movie); + assert!(VideoType::Any == VideoType::Any); + assert!(VideoType::TvSeries == VideoType::TvSeries); + assert!(VideoType::Movie == VideoType::Movie); + } +} diff --git a/src/main.rs b/src/main.rs index 8d6c83d..ebccaeb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,25 +1,52 @@ mod cli; +use core::future::Future; +use core::pin::Pin; +use std::iter::Iterator; + +use munite_core::{StoreEntry, MuniteResult}; + #[tokio::main] async fn main() { let args = cli::CliArgs::get(); + //let general_stores = munite_core::general::stores(); let results = match args.category { cli::StoreCategory::All(s) => { let stores = munite_core::stores(); - search_query(stores, s, args.max).await + search_query(stores.iter(), args.max, |store| store.search(s.query.clone())).await }, cli::StoreCategory::Audio(s) => { - let stores = munite_core::audio::stores(); - search_query(stores, s, args.max).await + let stores = munite_core::audio::all(); + let general_stores = munite_core::general::all_audio(); + search_query(stores.iter().chain(general_stores.iter()), + args.max, + |store| store.search(s.query.clone()), + ).await }, cli::StoreCategory::Music(s) => { - let stores = munite_core::audio::music::stores(); - search_query(stores, s, args.max).await + let stores = munite_core::audio::all(); + let general_stores = munite_core::general::all_audio(); + search_query(stores.iter() + .chain(general_stores.iter()) + .filter(|x| + x.store() + .iter() + .any(|x| x == &munite_core::audio::AudioType::Music) + ), + args.max, + |store| store.search(s.query.clone()), + ).await }, cli::StoreCategory::Video(s) => { - let stores = munite_core::video::stores(); - search_query(stores, s, args.max).await + let stores = munite_core::video::all(); + let general_stores = munite_core::general::all_video(); + search_query( + stores.iter() + .chain(general_stores.iter()), + args.max, + |store| store.search(s.query.clone()), + ).await }, }; for entry in results { @@ -30,14 +57,17 @@ async fn main() { } } -async fn search_query( - stores: Vec>, - query: cli::StandardSearch, +async fn search_query<'a, I: 'a>( + stores: impl Iterator + 'a, + //query: cli::StandardSearch, max_results: Option, -) -> Vec> { - let mut awaitables = Vec::with_capacity(stores.len()); - for store in stores.iter() { - awaitables.push(store.search(query.query.clone())); + gen: impl Fn(&'a I) -> Pin>> + Send + 'a>>, +) -> Vec> { + let size_hint = stores.size_hint(); + let mut awaitables = Vec::with_capacity(size_hint.1.unwrap_or(size_hint.0)); + for store in stores { + //awaitables.push(store.search(query.query.clone())); + awaitables.push(gen(store)); } let responses = futures_util::future::join_all(awaitables).await; let mut result = Vec::new();