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"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = "0.1.61"
|
async-trait = "0.1.61"
|
||||||
reqwest = { version = "0.11.13", features = [ "json" ] }
|
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
|
/// A search result from an audio store
|
||||||
pub enum AudioEntry {
|
pub enum AudioExtra {
|
||||||
/// A search result from a music store
|
/// A search result from a music store
|
||||||
Music(super::music::MusicEntry),
|
Music(super::music::MusicExtra),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::convert::Into<StoreEntry> for AudioEntry {
|
/// Store audio type
|
||||||
fn into(self) -> StoreEntry {
|
pub enum AudioType {
|
||||||
StoreEntry::Audio(self)
|
/// 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 {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Music(x) => write!(f, "[MUSIC]{}", x),
|
Self::Music(x) => write!(f, "[MUSIC]{}", x),
|
||||||
|
@ -24,21 +30,46 @@ impl std::fmt::Display for AudioEntry {
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
pub trait AudioSearchStore {
|
pub trait AudioSearchStore {
|
||||||
/// Search the store for the given string `s`, returning the results of the search
|
/// 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]
|
#[async_trait::async_trait]
|
||||||
impl<X: AudioSearchStore + Sync> StoreSearch for X {
|
impl<X: AudioSearchStore + Sync> StoreSearch for X {
|
||||||
async fn search(&self, s: String) -> MuniteResult<Vec<StoreEntry>> {
|
async fn search(&self, s: String) -> MuniteResult<Vec<StoreEntry>> {
|
||||||
(self as &(dyn AudioSearchStore + Sync))
|
(self as &(dyn AudioSearchStore + Sync))
|
||||||
.search(s)
|
.search(s)
|
||||||
.await
|
.await
|
||||||
.map(
|
}
|
||||||
|entries|
|
|
||||||
entries
|
fn store(&self) -> Vec<crate::StoreType> {
|
||||||
.into_iter()
|
crate::StoreType::Audio((self as &(dyn AudioSearchStore + Sync)).store().map(|x| x.into()))
|
||||||
.map(|e| e.into())
|
|
||||||
.collect()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
audio_store_impl!{super::music::SevenDigital}
|
||||||
|
audio_store_impl!{super::music::ProStudioMasters}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
mod interface;
|
mod interface;
|
||||||
pub mod music;
|
pub mod music;
|
||||||
|
|
||||||
pub use interface::{AudioSearchStore, AudioEntry};
|
pub use interface::{AudioSearchStore, AudioExtra, AudioType};
|
||||||
|
|
||||||
/// All available AudioSearchStores
|
/// All available AudioSearchStores
|
||||||
pub fn all() -> Vec<Box<dyn AudioSearchStore>> {
|
pub fn all() -> Vec<Box<dyn AudioSearchStore>> {
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
use crate::MuniteResult;
|
use crate::MuniteResult;
|
||||||
use crate::audio::{AudioEntry, AudioSearchStore};
|
use crate::StoreEntry;
|
||||||
|
use crate::audio::{AudioExtra, AudioSearchStore, AudioType};
|
||||||
|
|
||||||
/// A search result from a music store
|
/// A search result from a music store
|
||||||
pub enum MusicEntry {
|
pub enum MusicExtra {
|
||||||
/// A search result for an artist
|
/// A search result for an artist
|
||||||
Artist(ArtistEntry),
|
Artist(ArtistExtra),
|
||||||
/// A search result for an album or single
|
/// A search result for an album or single
|
||||||
Release(ReleaseEntry),
|
Release(ReleaseExtra),
|
||||||
/// A search result for a track
|
/// 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 {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Artist(x) => write!(f, "[ARTIST]{}", x),
|
Self::Artist(x) => write!(f, "[ARTIST]{}", x),
|
||||||
|
@ -22,103 +23,78 @@ impl std::fmt::Display for MusicEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A search result for an artist
|
/// A search result for an artist
|
||||||
pub struct ArtistEntry {
|
pub struct ArtistExtra {
|
||||||
/// Artist's ID used by the store
|
|
||||||
pub id: Option<u64>,
|
|
||||||
/// Artist's name
|
|
||||||
pub name: String,
|
|
||||||
/// Artist's cover image
|
/// Artist's cover image
|
||||||
pub image: Option<String>,
|
pub image: Option<String>,
|
||||||
/// Store name
|
|
||||||
pub store: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::convert::Into<MusicEntry> for ArtistEntry {
|
impl std::convert::Into<MusicExtra> for ArtistExtra {
|
||||||
fn into(self) -> MusicEntry {
|
fn into(self) -> MusicExtra {
|
||||||
MusicEntry::Artist(self)
|
MusicExtra::Artist(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for ArtistEntry {
|
impl std::fmt::Display for ArtistExtra {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
if let Some(id) = self.id {
|
Ok(())
|
||||||
write!(f, "[{}]({}) `{}`", self.store, id, self.name)
|
|
||||||
} else {
|
|
||||||
write!(f, "[{}] `{}`", self.store, self.name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A search result for an album or single
|
/// A search result for an album or single
|
||||||
pub struct ReleaseEntry {
|
pub struct ReleaseExtra {
|
||||||
/// Release's ID used by the store
|
/// Release's artist name
|
||||||
pub id: Option<u64>,
|
pub artist_name: String,
|
||||||
/// Release's title
|
|
||||||
pub title: String,
|
|
||||||
/// Release's artist
|
/// Release's artist
|
||||||
pub artist: ArtistEntry,
|
pub artist: ArtistExtra,
|
||||||
/// Store name
|
|
||||||
pub store: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::convert::Into<MusicEntry> for ReleaseEntry {
|
impl std::convert::Into<MusicExtra> for ReleaseExtra {
|
||||||
fn into(self) -> MusicEntry {
|
fn into(self) -> MusicExtra {
|
||||||
MusicEntry::Release(self)
|
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 {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
if let Some(id) = self.id {
|
write!(f, "by {}", self.artist_name)
|
||||||
write!(f, "[{}]({}) `{}` by {}", self.store, id, self.title, self.artist)
|
|
||||||
} else {
|
|
||||||
write!(f, "[{}] `{}` by {}", self.store, self.title, self.artist)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A search result for a track
|
/// A search result for a track
|
||||||
pub struct TrackEntry {
|
pub struct TrackExtra {
|
||||||
/// Track's ID used by the store
|
/// Track's artist name
|
||||||
pub id: Option<u64>,
|
pub artist_name: String,
|
||||||
/// Track's title
|
|
||||||
pub title: String,
|
|
||||||
/// Track's artist
|
/// Track's artist
|
||||||
pub artist: ArtistEntry,
|
pub artist: ArtistExtra,
|
||||||
/// Track's release
|
/// Track's release
|
||||||
pub release: Option<ReleaseEntry>,
|
pub release: Option<ReleaseExtra>,
|
||||||
/// Store name
|
|
||||||
pub store: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::convert::Into<MusicEntry> for TrackEntry {
|
impl std::convert::Into<MusicExtra> for TrackExtra {
|
||||||
fn into(self) -> MusicEntry {
|
fn into(self) -> MusicExtra {
|
||||||
MusicEntry::Track(self)
|
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 {
|
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 {}", release)
|
||||||
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)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if let Some(release) = &self.release {
|
write!(f, "by {}", self.artist)
|
||||||
write!(f, "[{}] `{}` from {}", self.store, self.title, release)
|
|
||||||
} else {
|
|
||||||
write!(f, "[{}] `{}` by {}", self.store, self.title, self.artist)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::convert::Into<AudioEntry> for MusicEntry {
|
impl std::convert::Into<AudioExtra> for MusicExtra {
|
||||||
fn into(self) -> AudioEntry {
|
fn into(self) -> AudioExtra {
|
||||||
AudioEntry::Music(self)
|
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]
|
#[async_trait::async_trait]
|
||||||
pub trait MusicSearchStore {
|
pub trait MusicSearchStore {
|
||||||
/// Search the store for the given string `s`, returning the results of the search
|
/// 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]
|
#[async_trait::async_trait]
|
||||||
impl<X: MusicSearchStore + Sync> AudioSearchStore for X {
|
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))
|
(self as &(dyn MusicSearchStore + Sync))
|
||||||
.search(s)
|
.search(s)
|
||||||
.await
|
.await
|
||||||
.map(
|
}
|
||||||
|entries|
|
|
||||||
entries
|
fn store(&self) -> Vec<AudioType> {
|
||||||
.into_iter()
|
vec![AudioType::Music]
|
||||||
.map(|e| e.into())
|
|
||||||
.collect()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ pub use pro_studio_masters::ProStudioMasters;
|
||||||
|
|
||||||
mod interface;
|
mod interface;
|
||||||
|
|
||||||
pub use interface::{MusicEntry, MusicSearchStore, TrackEntry, ArtistEntry, ReleaseEntry};
|
pub use interface::{MusicExtra, MusicSearchStore, TrackExtra, ArtistExtra, ReleaseExtra};
|
||||||
|
|
||||||
/// All available MusicSearchStores
|
/// All available MusicSearchStores
|
||||||
pub fn all() -> Vec<Box<dyn MusicSearchStore>> {
|
pub fn all() -> Vec<Box<dyn MusicSearchStore>> {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use reqwest::{Client, RequestBuilder};
|
use reqwest::{Client, RequestBuilder};
|
||||||
use soup::{Soup, QueryBuilderExt, NodeExt};
|
use soup::{Soup, QueryBuilderExt, NodeExt};
|
||||||
|
|
||||||
use super::{MusicEntry, MusicSearchStore, ReleaseEntry, ArtistEntry};
|
use super::{MusicExtra, MusicSearchStore, ReleaseExtra, ArtistExtra};
|
||||||
use crate::MuniteError;
|
use crate::{MuniteError, StoreEntry, StorePrice, StoreCurrency};
|
||||||
|
|
||||||
/// ProStudioMasters store client
|
/// ProStudioMasters store client
|
||||||
pub struct ProStudioMasters {
|
pub struct ProStudioMasters {
|
||||||
|
@ -43,7 +43,7 @@ impl std::default::Default for ProStudioMasters {
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl MusicSearchStore for ProStudioMasters {
|
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 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 page = releases.text().await.map_err(|e| MuniteError::Http(e))?;
|
||||||
let mut output = Vec::new();
|
let mut output = Vec::new();
|
||||||
|
@ -55,20 +55,21 @@ impl MusicSearchStore for ProStudioMasters {
|
||||||
if let Some(id) = album.get("href") {
|
if let Some(id) = album.get("href") {
|
||||||
if let Some(artist) = album.tag("span").class("artist").find() {
|
if let Some(artist) = album.tag("span").class("artist").find() {
|
||||||
if let Some(title) = album.tag("span").class("title").find() {
|
if let Some(title) = album.tag("span").class("title").find() {
|
||||||
output.push(ReleaseEntry {
|
output.push(StoreEntry {
|
||||||
id: id.split('/')
|
id: id.split('/')
|
||||||
.last()
|
.last()
|
||||||
.map(|x| x.parse().ok())
|
.map(|x| x.parse().ok())
|
||||||
.flatten(),
|
.flatten(),
|
||||||
title: title.text().trim().to_owned(),
|
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(),
|
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 reqwest::{Client, RequestBuilder};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use super::{MusicEntry, MusicSearchStore, TrackEntry, ReleaseEntry, ArtistEntry};
|
use super::{MusicExtra, MusicSearchStore, TrackExtra, ReleaseExtra, ArtistExtra};
|
||||||
use crate::MuniteError;
|
use crate::{MuniteError, StoreEntry, StorePrice, StoreCurrency};
|
||||||
|
|
||||||
/// 7Digital store client
|
/// 7Digital store client
|
||||||
pub struct SevenDigital {
|
pub struct SevenDigital {
|
||||||
|
@ -66,7 +66,7 @@ impl std::default::Default for SevenDigital {
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl MusicSearchStore for SevenDigital {
|
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 tracks = self.search_tracks(&s).send();
|
||||||
let releases = self.search_releases(&s).send();
|
let releases = self.search_releases(&s).send();
|
||||||
let artists = self.search_artists(&s).send();
|
let artists = self.search_artists(&s).send();
|
||||||
|
@ -120,12 +120,12 @@ enum AnySearchResult {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::convert::Into<MusicEntry> for AnySearchResult {
|
impl std::convert::Into<StoreEntry> for AnySearchResult {
|
||||||
fn into(self) -> MusicEntry {
|
fn into(self) -> StoreEntry {
|
||||||
match self {
|
match self {
|
||||||
Self::artist{artist: x, ..} => MusicEntry::Artist(x.into()),
|
Self::artist{artist: x, ..} => x.into(),
|
||||||
Self::release{release: x, ..} => MusicEntry::Release(x.into()),
|
Self::release{release: x, ..} => x.into(),
|
||||||
Self::track{track: x, ..} => MusicEntry::Track(x.into()),
|
Self::track{track: x, ..} => x.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -143,13 +143,16 @@ struct ArtistSearchResult {
|
||||||
popularity: Option<f64>,
|
popularity: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::convert::Into<ArtistEntry> for ArtistSearchResult {
|
impl std::convert::Into<StoreEntry> for ArtistSearchResult {
|
||||||
fn into(self) -> ArtistEntry {
|
fn into(self) -> StoreEntry {
|
||||||
ArtistEntry {
|
StoreEntry {
|
||||||
id: Some(self.id),
|
id: Some(self.id),
|
||||||
name: self.name,
|
title: self.name,
|
||||||
image: Some(self.image),
|
extra: MusicExtra::Artist(ArtistExtra {
|
||||||
|
image: Some(self.image)
|
||||||
|
}).into(),
|
||||||
store: "7digital".to_owned(),
|
store: "7digital".to_owned(),
|
||||||
|
price: StorePrice { cents: 0, currency: StoreCurrency::Unknown }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -176,13 +179,19 @@ struct ReleaseSearchResult {
|
||||||
download: Option<DownloadInfo>,
|
download: Option<DownloadInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::convert::Into<ReleaseEntry> for ReleaseSearchResult {
|
impl std::convert::Into<StoreEntry> for ReleaseSearchResult {
|
||||||
fn into(self) -> ReleaseEntry {
|
fn into(self) -> StoreEntry {
|
||||||
ReleaseEntry {
|
StoreEntry {
|
||||||
id: Some(self.id),
|
id: Some(self.id),
|
||||||
title: self.title,
|
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(),
|
store: "7digital".to_owned(),
|
||||||
|
price: StorePrice { cents: 0, currency: StoreCurrency::Unknown }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -219,14 +228,25 @@ struct TrackSearchResult {
|
||||||
download: Option<DownloadInfo>,
|
download: Option<DownloadInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::convert::Into<TrackEntry> for TrackSearchResult {
|
impl std::convert::Into<StoreEntry> for TrackSearchResult {
|
||||||
fn into(self) -> TrackEntry {
|
fn into(self) -> StoreEntry {
|
||||||
TrackEntry {
|
StoreEntry {
|
||||||
id: Some(self.id),
|
id: Some(self.id),
|
||||||
title: self.title,
|
title: self.title,
|
||||||
release: Some(self.release.into()),
|
extra: MusicExtra::Track(TrackExtra {
|
||||||
artist: self.artist.into(),
|
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(),
|
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)]
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
pub mod audio;
|
pub mod audio;
|
||||||
|
pub mod video;
|
||||||
|
mod entry;
|
||||||
mod errors;
|
mod errors;
|
||||||
mod search;
|
mod search;
|
||||||
|
|
||||||
|
pub use entry::{StoreEntry, StoreExtra, StoreType, StorePrice, StoreCurrency};
|
||||||
pub use errors::{MuniteError, MuniteResult};
|
pub use errors::{MuniteError, MuniteResult};
|
||||||
|
|
||||||
pub use search::{StoreSearch, StoreEntry};
|
pub use search::StoreSearch;
|
||||||
|
|
||||||
/// All available SearchStores
|
/// All available SearchStores
|
||||||
pub fn stores() -> Vec<Box<dyn StoreSearch>> {
|
pub fn stores() -> Vec<Box<dyn StoreSearch>> {
|
||||||
|
|
|
@ -1,22 +1,12 @@
|
||||||
use super::MuniteResult;
|
use super::MuniteResult;
|
||||||
|
use super::{StoreEntry, StoreType};
|
||||||
/// 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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Store search client for a specific store
|
/// Store search client for a specific store
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
pub trait StoreSearch {
|
pub trait StoreSearch {
|
||||||
/// Search the store for the given string `s`, returning the results of the search
|
/// Search the store for the given string `s`, returning the results of the search
|
||||||
async fn search(&self, s: String) -> MuniteResult<Vec<StoreEntry>>;
|
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),
|
Audio(StandardSearch),
|
||||||
/// Search all music stores
|
/// Search all music stores
|
||||||
Music(StandardSearch),
|
Music(StandardSearch),
|
||||||
|
/// Search all video stores
|
||||||
|
Video(StandardSearch),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args, Debug)]
|
#[derive(Args, Debug)]
|
||||||
|
|
|
@ -17,6 +17,10 @@ async fn main() {
|
||||||
let stores = munite_core::audio::music::stores();
|
let stores = munite_core::audio::music::stores();
|
||||||
search_query(stores, s, args.max).await
|
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 {
|
for entry in results {
|
||||||
match entry {
|
match entry {
|
||||||
|
|
Loading…
Reference in a new issue