Add general store functionality and BestBuy Canada

This commit is contained in:
NGnius (Graham) 2023-02-05 15:05:27 -05:00
parent 450ee41971
commit ea1e748399
13 changed files with 515 additions and 38 deletions

View file

@ -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<AudioType> = 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<StoreExtra> for AudioExtra {
@ -73,3 +97,16 @@ impl<X: AudioSearchStore + Sync> 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);
}
}

View file

@ -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"),
}
}
}

View file

@ -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<Box<dyn MusicSearchStore>> {

View file

@ -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);
}
}

View file

@ -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<StoreEntry> 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<StoreEntry> 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<StoreEntry> 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);
}
}

View file

@ -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<u64>,
pub id: Option<StoreId>,
/// 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),
}
}
}

View file

@ -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<Vec<StoreEntry>> {
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<StoreType> {
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<ProductInfo>
}
#[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<String>, // 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<StoreEntry> 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);
}
}

View file

@ -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<Vec<StoreEntry>>;
/// Store type
fn store(&self) -> Vec<StoreType>;
}
macro_rules! general_store_impl {
($name:ty) => {
#[async_trait::async_trait]
impl StoreSearch for $name {
async fn search(&self, s: String) -> MuniteResult<Vec<StoreEntry>> {
(self as &(dyn GeneralSearchStore + Sync))
.search(s, StoreType::General)
.await
}
fn store(&self) -> Vec<crate::StoreType> {
(self as &(dyn GeneralSearchStore)).store()
}
}
#[async_trait::async_trait]
impl VideoSearchStore for $name {
async fn search(&self, s: String) -> MuniteResult<Vec<StoreEntry>> {
(self as &(dyn GeneralSearchStore + Sync))
.search(s, StoreType::Video(VideoType::Any))
.await
}
fn store(&self) -> Vec<VideoType> {
(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<Vec<StoreEntry>> {
(self as &(dyn GeneralSearchStore + Sync))
.search(s, StoreType::Audio(AudioType::Any))
.await
}
fn store(&self) -> Vec<AudioType> {
(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}

View file

@ -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<Box<dyn GeneralSearchStore>> {
vec![
Box::new(BestBuyCa::default())
]
}
/// All available GeneralSearchStores
pub fn all_audio() -> Vec<Box<dyn crate::audio::AudioSearchStore>> {
vec![
Box::new(BestBuyCa::default())
]
}
/// All available GeneralSearchStores
pub fn all_video() -> Vec<Box<dyn crate::video::VideoSearchStore>> {
vec![
Box::new(BestBuyCa::default())
]
}
/// All available general SearchStores
pub fn stores() -> Vec<Box<dyn crate::StoreSearch>> {
vec![
Box::new(BestBuyCa::default())
]
}

View file

@ -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<Box<dyn StoreSearch>> {
audio::stores()
let mut all = audio::stores();
all.append(&mut video::stores());
all
}
#[cfg(test)]

View file

@ -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<Vec<StoreEntry>> {
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);
}
}

View file

@ -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<StoreExtra> for VideoExtra {
@ -176,3 +197,19 @@ impl<X: VideoSearchStore + Sync> 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);
}
}

View file

@ -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<Box<dyn munite_core::StoreSearch>>,
query: cli::StandardSearch,
async fn search_query<'a, I: 'a>(
stores: impl Iterator<Item=&'a I> + 'a,
//query: cli::StandardSearch,
max_results: Option<usize>,
) -> Vec<munite_core::MuniteResult<munite_core::StoreEntry>> {
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<Box<dyn Future<Output = MuniteResult<Vec<StoreEntry>>> + Send + 'a>>,
) -> Vec<MuniteResult<StoreEntry>> {
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();