Add Cinema1 video store

This commit is contained in:
NGnius (Graham) 2023-02-04 16:51:46 -05:00
parent 3f4fd9be5e
commit 450ee41971
15 changed files with 579 additions and 144 deletions

View file

@ -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" ] }

View file

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

View file

@ -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>> {

View file

@ -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]
}
}

View file

@ -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>> {

View file

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

View file

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

View file

@ -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>> {

View file

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

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

View 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}

View 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()),
]
}

View file

@ -25,6 +25,8 @@ pub enum StoreCategory {
Audio(StandardSearch),
/// Search all music stores
Music(StandardSearch),
/// Search all video stores
Video(StandardSearch),
}
#[derive(Args, Debug)]

View file

@ -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 {