Add general store functionality and BestBuy Canada
This commit is contained in:
parent
450ee41971
commit
ea1e748399
13 changed files with 515 additions and 38 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>> {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
207
munite-core/src/general/best_buy_ca.rs
Normal file
207
munite-core/src/general/best_buy_ca.rs
Normal 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);
|
||||
}
|
||||
}
|
64
munite-core/src/general/interface.rs
Normal file
64
munite-core/src/general/interface.rs
Normal 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}
|
34
munite-core/src/general/mod.rs
Normal file
34
munite-core/src/general/mod.rs
Normal 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())
|
||||
]
|
||||
}
|
|
@ -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)]
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
58
src/main.rs
58
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<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();
|
||||
|
|
Loading…
Reference in a new issue