Add Cinema1 video store
This commit is contained in:
parent
3f4fd9be5e
commit
450ee41971
15 changed files with 579 additions and 144 deletions
|
@ -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" ] }
|
||||
|
|
|
@ -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<StoreEntry> for AudioEntry {
|
||||
fn into(self) -> StoreEntry {
|
||||
StoreEntry::Audio(self)
|
||||
/// Store audio type
|
||||
pub enum AudioType {
|
||||
/// Music store
|
||||
Music
|
||||
}
|
||||
|
||||
impl std::convert::Into<StoreExtra> 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<Vec<AudioEntry>>;
|
||||
async fn search(&self, s: String) -> MuniteResult<Vec<StoreEntry>>;
|
||||
|
||||
/// Store type
|
||||
fn store(&self) -> Vec<AudioType>;
|
||||
}
|
||||
|
||||
macro_rules! audio_store_impl {
|
||||
($name:ty) => {
|
||||
#[async_trait::async_trait]
|
||||
impl StoreSearch for $name {
|
||||
async fn search(&self, s: String) -> MuniteResult<Vec<StoreEntry>> {
|
||||
(self as &(dyn AudioSearchStore + Sync))
|
||||
.search(s)
|
||||
.await
|
||||
}
|
||||
|
||||
fn store(&self) -> Vec<crate::StoreType> {
|
||||
(self as &(dyn AudioSearchStore + Sync)).store()
|
||||
.into_iter()
|
||||
.map(|x| crate::StoreType::Audio(x))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
#[async_trait::async_trait]
|
||||
impl<X: AudioSearchStore + Sync> StoreSearch for X {
|
||||
async fn search(&self, s: String) -> MuniteResult<Vec<StoreEntry>> {
|
||||
(self as &(dyn AudioSearchStore + Sync))
|
||||
.search(s)
|
||||
.await
|
||||
.map(
|
||||
|entries|
|
||||
entries
|
||||
.into_iter()
|
||||
.map(|e| e.into())
|
||||
.collect()
|
||||
)
|
||||
}
|
||||
|
||||
fn store(&self) -> Vec<crate::StoreType> {
|
||||
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}
|
||||
|
|
|
@ -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<Box<dyn AudioSearchStore>> {
|
||||
|
|
|
@ -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<u64>,
|
||||
/// Artist's name
|
||||
pub name: String,
|
||||
pub struct ArtistExtra {
|
||||
/// Artist's cover image
|
||||
pub image: Option<String>,
|
||||
/// Store name
|
||||
pub store: String,
|
||||
}
|
||||
|
||||
impl std::convert::Into<MusicEntry> for ArtistEntry {
|
||||
fn into(self) -> MusicEntry {
|
||||
MusicEntry::Artist(self)
|
||||
impl std::convert::Into<MusicExtra> 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<u64>,
|
||||
/// 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<MusicEntry> for ReleaseEntry {
|
||||
fn into(self) -> MusicEntry {
|
||||
MusicEntry::Release(self)
|
||||
impl std::convert::Into<MusicExtra> 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<u64>,
|
||||
/// 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<ReleaseEntry>,
|
||||
/// Store name
|
||||
pub store: String,
|
||||
pub release: Option<ReleaseExtra>,
|
||||
}
|
||||
|
||||
impl std::convert::Into<MusicEntry> for TrackEntry {
|
||||
fn into(self) -> MusicEntry {
|
||||
MusicEntry::Track(self)
|
||||
impl std::convert::Into<MusicExtra> 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<AudioEntry> for MusicEntry {
|
||||
fn into(self) -> AudioEntry {
|
||||
AudioEntry::Music(self)
|
||||
impl std::convert::Into<AudioExtra> for MusicExtra {
|
||||
fn into(self) -> AudioExtra {
|
||||
AudioExtra::Music(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::Into<crate::StoreExtra> for MusicExtra {
|
||||
fn into(self) -> crate::StoreExtra {
|
||||
AudioExtra::Music(self).into()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -126,22 +102,19 @@ impl std::convert::Into<AudioEntry> 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<Vec<MusicEntry>>;
|
||||
async fn search(&self, s: String) -> MuniteResult<Vec<StoreEntry>>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<X: MusicSearchStore + Sync> AudioSearchStore for X {
|
||||
async fn search(&self, s: String) -> MuniteResult<Vec<AudioEntry>> {
|
||||
async fn search(&self, s: String) -> MuniteResult<Vec<StoreEntry>> {
|
||||
(self as &(dyn MusicSearchStore + Sync))
|
||||
.search(s)
|
||||
.await
|
||||
.map(
|
||||
|entries|
|
||||
entries
|
||||
.into_iter()
|
||||
.map(|e| e.into())
|
||||
.collect()
|
||||
)
|
||||
}
|
||||
|
||||
fn store(&self) -> Vec<AudioType> {
|
||||
vec![AudioType::Music]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Box<dyn MusicSearchStore>> {
|
||||
|
|
|
@ -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<Vec<MusicEntry>> {
|
||||
async fn search(&self, s: String) -> crate::MuniteResult<Vec<StoreEntry>> {
|
||||
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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Vec<MusicEntry>> {
|
||||
async fn search(&self, s: String) -> crate::MuniteResult<Vec<StoreEntry>> {
|
||||
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<MusicEntry> for AnySearchResult {
|
||||
fn into(self) -> MusicEntry {
|
||||
impl std::convert::Into<StoreEntry> 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<f64>,
|
||||
}
|
||||
|
||||
impl std::convert::Into<ArtistEntry> for ArtistSearchResult {
|
||||
fn into(self) -> ArtistEntry {
|
||||
ArtistEntry {
|
||||
impl std::convert::Into<StoreEntry> 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<DownloadInfo>,
|
||||
}
|
||||
|
||||
impl std::convert::Into<ReleaseEntry> for ReleaseSearchResult {
|
||||
fn into(self) -> ReleaseEntry {
|
||||
ReleaseEntry {
|
||||
impl std::convert::Into<StoreEntry> 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<DownloadInfo>,
|
||||
}
|
||||
|
||||
impl std::convert::Into<TrackEntry> for TrackSearchResult {
|
||||
fn into(self) -> TrackEntry {
|
||||
TrackEntry {
|
||||
impl std::convert::Into<StoreEntry> 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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
73
munite-core/src/entry.rs
Normal file
73
munite-core/src/entry.rs
Normal file
|
@ -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<u64>,
|
||||
/// 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),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Box<dyn StoreSearch>> {
|
||||
|
|
|
@ -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<Vec<StoreEntry>>;
|
||||
|
||||
/// Store type
|
||||
fn store(&self) -> Vec<StoreType>;
|
||||
}
|
||||
|
|
142
munite-core/src/video/cinema1.rs
Normal file
142
munite-core/src/video/cinema1.rs
Normal file
|
@ -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<StoreEntry>, 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("<a> 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<Vec<StoreEntry>> {
|
||||
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<VideoType> {
|
||||
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);
|
||||
}
|
||||
}
|
178
munite-core/src/video/interface.rs
Normal file
178
munite-core/src/video/interface.rs
Normal file
|
@ -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<VideoInfoExtra>,
|
||||
}
|
||||
|
||||
/// 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<VideoInfoExtra> 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<VideoInfoExtra> 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<VideoInfoExtra> 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<StoreExtra> 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<Vec<StoreEntry>>;
|
||||
|
||||
/// Store type
|
||||
fn store(&self) -> Vec<VideoType>;
|
||||
}
|
||||
|
||||
macro_rules! video_store_impl {
|
||||
($name:ty) => {
|
||||
#[async_trait::async_trait]
|
||||
impl StoreSearch for $name {
|
||||
async fn search(&self, s: String) -> MuniteResult<Vec<StoreEntry>> {
|
||||
(self as &(dyn VideoSearchStore + Sync))
|
||||
.search(s)
|
||||
.await
|
||||
}
|
||||
|
||||
fn store(&self) -> Vec<crate::StoreType> {
|
||||
(self as &(dyn VideoSearchStore + Sync)).store()
|
||||
.into_iter()
|
||||
.map(|x| crate::StoreType::Video(x))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
#[async_trait::async_trait]
|
||||
impl<X: VideoSearchStore + Sync> StoreSearch for X {
|
||||
async fn search(&self, s: String) -> MuniteResult<Vec<StoreEntry>> {
|
||||
(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}
|
20
munite-core/src/video/mod.rs
Normal file
20
munite-core/src/video/mod.rs
Normal file
|
@ -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<Box<dyn VideoSearchStore>> {
|
||||
vec![
|
||||
Box::new(CinemaOne::default()),
|
||||
]
|
||||
}
|
||||
|
||||
/// All available audio SearchStores
|
||||
pub fn stores() -> Vec<Box<dyn crate::StoreSearch>> {
|
||||
vec![
|
||||
Box::new(CinemaOne::default()),
|
||||
]
|
||||
}
|
|
@ -25,6 +25,8 @@ pub enum StoreCategory {
|
|||
Audio(StandardSearch),
|
||||
/// Search all music stores
|
||||
Music(StandardSearch),
|
||||
/// Search all video stores
|
||||
Video(StandardSearch),
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue