Compare commits
3 Commits
d09557d953
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b39dd6cc8e | ||
|
|
cbd0243516 | ||
|
|
d358b79a6b |
@@ -7,6 +7,7 @@ description = "Online music search for Shanty"
|
||||
repository = "ssh://connor@git.rcjohnstone.com:2222/Shanty/search.git"
|
||||
|
||||
[dependencies]
|
||||
shanty-data = { path = "../shanty-data" }
|
||||
shanty-db = { path = "../shanty-db" }
|
||||
shanty-tag = { path = "../shanty-tag" }
|
||||
sea-orm = { version = "1", features = ["sqlx-sqlite", "runtime-tokio-native-tls"] }
|
||||
|
||||
@@ -15,6 +15,12 @@ pub enum SearchError {
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl From<shanty_data::DataError> for SearchError {
|
||||
fn from(e: shanty_data::DataError) -> Self {
|
||||
SearchError::Provider(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<shanty_tag::TagError> for SearchError {
|
||||
fn from(e: shanty_tag::TagError) -> Self {
|
||||
SearchError::Provider(e.to_string())
|
||||
|
||||
20
src/main.rs
20
src/main.rs
@@ -82,7 +82,10 @@ async fn main() -> anyhow::Result<()> {
|
||||
} else if results.is_empty() {
|
||||
println!("No artists found.");
|
||||
} else {
|
||||
println!("{:<40} {:<8} {:<5} {}", "NAME", "COUNTRY", "SCORE", "DISAMBIGUATION");
|
||||
println!(
|
||||
"{:<40} {:<8} {:<5} DISAMBIGUATION",
|
||||
"NAME", "COUNTRY", "SCORE"
|
||||
);
|
||||
for r in &results {
|
||||
println!(
|
||||
"{:<40} {:<8} {:<5} {}",
|
||||
@@ -104,7 +107,10 @@ async fn main() -> anyhow::Result<()> {
|
||||
} else if results.is_empty() {
|
||||
println!("No albums found.");
|
||||
} else {
|
||||
println!("{:<35} {:<25} {:<6} {:<5}", "TITLE", "ARTIST", "YEAR", "SCORE");
|
||||
println!(
|
||||
"{:<35} {:<25} {:<6} {:<5}",
|
||||
"TITLE", "ARTIST", "YEAR", "SCORE"
|
||||
);
|
||||
for r in &results {
|
||||
println!(
|
||||
"{:<35} {:<25} {:<6} {:<5}",
|
||||
@@ -126,13 +132,19 @@ async fn main() -> anyhow::Result<()> {
|
||||
} else if results.is_empty() {
|
||||
println!("No tracks found.");
|
||||
} else {
|
||||
println!("{:<35} {:<25} {:<25} {:<5}", "TITLE", "ARTIST", "ALBUM", "SCORE");
|
||||
println!(
|
||||
"{:<35} {:<25} {:<25} {:<5}",
|
||||
"TITLE", "ARTIST", "ALBUM", "SCORE"
|
||||
);
|
||||
for r in &results {
|
||||
println!(
|
||||
"{:<35} {:<25} {:<25} {:<5}",
|
||||
truncate(&r.title, 35),
|
||||
truncate(&r.artist, 25),
|
||||
r.album.as_deref().map(|a| truncate(a, 25)).unwrap_or_default(),
|
||||
r.album
|
||||
.as_deref()
|
||||
.map(|a| truncate(a, 25))
|
||||
.unwrap_or_default(),
|
||||
r.score,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,30 +1,34 @@
|
||||
use shanty_tag::provider::MetadataProvider;
|
||||
use shanty_tag::MusicBrainzClient;
|
||||
use shanty_data::MetadataFetcher;
|
||||
use shanty_data::MusicBrainzFetcher;
|
||||
use shanty_data::http::RateLimiter;
|
||||
|
||||
use crate::error::SearchResult;
|
||||
use crate::provider::{
|
||||
AlbumResult, ArtistResult, Discography, DiscographyEntry, SearchProvider, TrackResult,
|
||||
};
|
||||
|
||||
/// MusicBrainz implementation of `SearchProvider`, wrapping shanty-tag's client.
|
||||
/// MusicBrainz implementation of `SearchProvider`, wrapping shanty-data's fetcher.
|
||||
pub struct MusicBrainzSearch {
|
||||
client: MusicBrainzClient,
|
||||
client: MusicBrainzFetcher,
|
||||
}
|
||||
|
||||
impl MusicBrainzSearch {
|
||||
pub fn new() -> SearchResult<Self> {
|
||||
let client = MusicBrainzClient::new()
|
||||
let client = MusicBrainzFetcher::new()
|
||||
.map_err(|e| crate::error::SearchError::Provider(e.to_string()))?;
|
||||
Ok(Self { client })
|
||||
}
|
||||
|
||||
/// Create with a shared rate limiter (to coordinate with other MB clients).
|
||||
pub fn with_limiter(limiter: RateLimiter) -> SearchResult<Self> {
|
||||
let client = MusicBrainzFetcher::with_limiter(limiter)
|
||||
.map_err(|e| crate::error::SearchError::Provider(e.to_string()))?;
|
||||
Ok(Self { client })
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchProvider for MusicBrainzSearch {
|
||||
async fn search_artist(
|
||||
&self,
|
||||
query: &str,
|
||||
limit: u32,
|
||||
) -> SearchResult<Vec<ArtistResult>> {
|
||||
async fn search_artist(&self, query: &str, limit: u32) -> SearchResult<Vec<ArtistResult>> {
|
||||
let results = self.client.search_artist(query, limit).await?;
|
||||
Ok(results
|
||||
.into_iter()
|
||||
@@ -55,7 +59,11 @@ impl SearchProvider for MusicBrainzSearch {
|
||||
title: r.title,
|
||||
artist: r.artist,
|
||||
artist_id: r.artist_mbid,
|
||||
year: r.date.as_deref().and_then(|d| d.split('-').next()).map(String::from),
|
||||
year: r
|
||||
.date
|
||||
.as_deref()
|
||||
.and_then(|d| d.split('-').next())
|
||||
.map(String::from),
|
||||
track_count: r.track_count,
|
||||
score: r.score,
|
||||
})
|
||||
@@ -85,10 +93,7 @@ impl SearchProvider for MusicBrainzSearch {
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn get_discography(
|
||||
&self,
|
||||
artist_id: &str,
|
||||
) -> SearchResult<Discography> {
|
||||
async fn get_discography(&self, artist_id: &str) -> SearchResult<Discography> {
|
||||
let releases = self.client.get_artist_releases(artist_id, 100).await?;
|
||||
|
||||
// Try to get the artist name from the first release, or use the MBID
|
||||
|
||||
@@ -87,7 +87,10 @@ impl SearchProvider for MockSearch {
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_release_groups(&self, _artist_id: &str) -> SearchResult<Vec<shanty_search::ReleaseGroupResult>> {
|
||||
async fn get_release_groups(
|
||||
&self,
|
||||
_artist_id: &str,
|
||||
) -> SearchResult<Vec<shanty_search::ReleaseGroupResult>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
@@ -104,14 +107,20 @@ async fn test_search_artist() {
|
||||
#[tokio::test]
|
||||
async fn test_search_artist_no_results() {
|
||||
let provider = MockSearch;
|
||||
let results = provider.search_artist("Nonexistent Band", 10).await.unwrap();
|
||||
let results = provider
|
||||
.search_artist("Nonexistent Band", 10)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(results.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_search_album() {
|
||||
let provider = MockSearch;
|
||||
let results = provider.search_album("Dark Side", Some("Pink Floyd"), 10).await.unwrap();
|
||||
let results = provider
|
||||
.search_album("Dark Side", Some("Pink Floyd"), 10)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].title, "The Dark Side of the Moon");
|
||||
}
|
||||
@@ -119,10 +128,16 @@ async fn test_search_album() {
|
||||
#[tokio::test]
|
||||
async fn test_search_track() {
|
||||
let provider = MockSearch;
|
||||
let results = provider.search_track("Time", Some("Pink Floyd"), 10).await.unwrap();
|
||||
let results = provider
|
||||
.search_track("Time", Some("Pink Floyd"), 10)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].title, "Time");
|
||||
assert_eq!(results[0].album.as_deref(), Some("The Dark Side of the Moon"));
|
||||
assert_eq!(
|
||||
results[0].album.as_deref(),
|
||||
Some("The Dark Side of the Moon")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -162,18 +177,25 @@ async fn test_cache_roundtrip() {
|
||||
}];
|
||||
|
||||
// Cache miss
|
||||
let cached: Option<Vec<ArtistResult>> =
|
||||
cache::get_cached(Some(db.conn()), "test:artist:query").await.unwrap();
|
||||
let cached: Option<Vec<ArtistResult>> = cache::get_cached(Some(db.conn()), "test:artist:query")
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(cached.is_none());
|
||||
|
||||
// Store
|
||||
cache::set_cached(Some(db.conn()), "test:artist:query", "musicbrainz", &results)
|
||||
.await
|
||||
.unwrap();
|
||||
cache::set_cached(
|
||||
Some(db.conn()),
|
||||
"test:artist:query",
|
||||
"musicbrainz",
|
||||
&results,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Cache hit
|
||||
let cached: Option<Vec<ArtistResult>> =
|
||||
cache::get_cached(Some(db.conn()), "test:artist:query").await.unwrap();
|
||||
let cached: Option<Vec<ArtistResult>> = cache::get_cached(Some(db.conn()), "test:artist:query")
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(cached.is_some());
|
||||
assert_eq!(cached.unwrap()[0].name, "Cached Artist");
|
||||
}
|
||||
@@ -181,8 +203,7 @@ async fn test_cache_roundtrip() {
|
||||
#[tokio::test]
|
||||
async fn test_cache_none_conn() {
|
||||
// With no DB connection, caching is a no-op
|
||||
let cached: Option<Vec<ArtistResult>> =
|
||||
cache::get_cached(None, "anything").await.unwrap();
|
||||
let cached: Option<Vec<ArtistResult>> = cache::get_cached(None, "anything").await.unwrap();
|
||||
assert!(cached.is_none());
|
||||
|
||||
// set_cached with None conn should not error
|
||||
|
||||
Reference in New Issue
Block a user