Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,450 changes: 677 additions & 773 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ log = "0"
thiserror = "2"
unidecode = "0"
levenshtein = "1"
musicbrainz_rs_nova = { version = "0", features = ["async"] }
musicbrainz_rs = { version = "0.14", features = ["async", "rustls"] }
tokio = { version = "1", features = ["full"] }
id3 = { version = "1", optional = true }
metaflac = { version = "0", optional = true }
Expand Down
2 changes: 1 addition & 1 deletion src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
//! Caching for MusicBrainz API queries.

use chrono::{DateTime, Utc};
use musicbrainz_rs_nova::entity::{
use musicbrainz_rs::entity::{
release::Release as MusicBrainzRelease, release_group::ReleaseGroup as MusicBrainzReleaseGroup,
search::SearchResult as MusicBrainzSearchResult,
};
Expand Down
2 changes: 1 addition & 1 deletion src/cli/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
use crate::cache::Cache;
use crate::Config;
use clap::Parser;
use musicbrainz_rs_nova::entity::{
use musicbrainz_rs::entity::{
release::Release as MusicBrainzRelease, release_group::ReleaseGroup as MusicBrainzReleaseGroup,
search::SearchResult as MusicBrainzSearchResult,
};
Expand Down
16 changes: 9 additions & 7 deletions src/cli/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ use crate::release_candidate::{ReleaseCandidate, ReleaseCandidateCollection};
use crate::scanner::Scanner;
use crate::track::TrackLike;
use crate::util::FormattedDuration;
use crate::Cache;
use crate::{Config, TaggedFileCollection};
use clap::Parser;
use futures::StreamExt;
Expand All @@ -42,9 +41,9 @@ enum SelectionResult {
}

/// Select the release for the given track collection from the list of candidates.
async fn select_release<'a>(
async fn select_release(
config: &Config,
musicbrainz: &'a MusicBrainzClient<'a>,
musicbrainz: &MusicBrainzClient,
track_collection: TaggedFileCollection,
mut candidates: ReleaseCandidateCollection<MusicBrainzRelease>,
) -> crate::Result<SelectionResult> {
Expand Down Expand Up @@ -213,8 +212,12 @@ fn print_tracklist(release: &impl ReleaseLike) {
///
/// If the underlying [`walk_dir`] function encounters any form of I/O or other error, an error
/// variant will be returned.
pub async fn run(config: &Config, cache: Option<&Cache>, args: Args) -> crate::Result<()> {
let mut scanner = Scanner::scan(config.clone(), cache.cloned(), args.path);
pub async fn run(
config: &Config,
musicbrainz: &MusicBrainzClient,
args: Args,
) -> crate::Result<()> {
let mut scanner = Scanner::scan(config.clone(), musicbrainz.clone(), args.path);

let (importer_tx, mut importer_rx) = tokio::sync::mpsc::channel::<(
TaggedFileCollection,
Expand All @@ -240,7 +243,6 @@ pub async fn run(config: &Config, cache: Option<&Cache>, args: Args) -> crate::R
}
});

let musicbrainz = MusicBrainzClient::new(config, cache);
while let Some(result) = scanner.recv().await {
let (track_collection, candidates) = match result {
Ok(res) => res,
Expand All @@ -249,7 +251,7 @@ pub async fn run(config: &Config, cache: Option<&Cache>, args: Args) -> crate::R
continue;
}
};
match select_release(config, &musicbrainz, track_collection, candidates).await? {
match select_release(config, musicbrainz, track_collection, candidates).await? {
SelectionResult::Selected(track_collection, selected_candidate) => {
if let Err(err) = importer_tx
.send((track_collection, selected_candidate))
Expand Down
5 changes: 3 additions & 2 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ mod import;
mod ui;

use crate::{Cache, Config, PKG_NAME, PKG_VERSION, USER_AGENT};
use crate::musicbrainz::MusicBrainzClient;
use clap::{Parser, Subcommand};
use log::LevelFilter;
use simplelog::{ConfigBuilder as LogConfigBuilder, WriteLogger};
Expand Down Expand Up @@ -136,10 +137,10 @@ pub async fn main() -> crate::Result<()> {
//
// See this for details:
// - <https://musicbrainz.org/doc/MusicBrainz_API#Application_rate_limiting_and_identification>
musicbrainz_rs_nova::config::set_user_agent(USER_AGENT);
let musicbrainz = MusicBrainzClient::new(&config, Some(&cache), USER_AGENT);

match args.command {
Commands::Import(cmd_args) => import::run(&config, Some(&cache), cmd_args).await,
Commands::Import(cmd_args) => import::run(&config, &musicbrainz, cmd_args).await,
Commands::Config(cmd_args) => config::run(&config, Some(&cache), cmd_args),
Commands::Cache(cmd_args) => cache::run(&config, Some(&cache), cmd_args),
Commands::Analyze(cmd_args) => analyze::run(&config, Some(&cache), cmd_args),
Expand Down
8 changes: 7 additions & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pub enum ErrorType {
UnknownFileType,
/// A MusicBrainz API request failed.
#[error("API request failed: {0}")]
Request(#[from] musicbrainz_rs_nova::Error),
Request(#[from] Box<musicbrainz_rs::ApiEndpointError>),
/// A MusicBrainz API request failed.
#[error("MusicBrainz lookup failed")]
MusicBrainzLookupFailed(&'static str),
Expand All @@ -57,5 +57,11 @@ pub enum ErrorType {
TemplateFormattingFailed(#[from] handlebars::RenderError),
}

impl From<musicbrainz_rs::ApiEndpointError> for ErrorType {
fn from(err: musicbrainz_rs::ApiEndpointError) -> Self {
Self::Request(Box::new(err))
}
}

Comment on lines +60 to +65

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clippy complained about big errors, I couldn't find a better solution

/// Convenience type.
pub type Result<T> = std::result::Result<T, ErrorType>;
2 changes: 1 addition & 1 deletion src/media.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
//! Release media.

use crate::track::TrackLike;
use musicbrainz_rs_nova::entity::release::Media as MusicBrainzReleaseMedia;
use musicbrainz_rs::entity::release::Media as MusicBrainzReleaseMedia;
use std::borrow::Cow;

/// Represent a generic release, independent of the underlying source.
Expand Down
53 changes: 30 additions & 23 deletions src/musicbrainz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ use futures::{
future::TryFutureExt,
stream::{self, Stream, StreamExt},
};
pub use musicbrainz_rs_nova::entity::{
use musicbrainz_rs::client::MusicBrainzClient as UpstreamMusicBrainzClient;
pub use musicbrainz_rs::entity::{
release::Release as MusicBrainzRelease, release_group::ReleaseGroup as MusicBrainzReleaseGroup,
};
use musicbrainz_rs_nova::{
use musicbrainz_rs::{
entity::release::ReleaseSearchQuery as MusicBrainzReleaseSearchQuery, Fetch, Search,
};
use regex::Regex;
Expand All @@ -32,18 +33,24 @@ use std::borrow::Cow;
pub const VARIOUS_ARTISTS_ID: &str = "89ad4ac3-39f7-470e-963a-56509c546377";

/// Configurable MusicBrainz API client with caching support.
#[derive(Debug)]
pub struct MusicBrainzClient<'a> {
#[derive(Debug, Clone)]
pub struct MusicBrainzClient {
/// Configuration
config: &'a Config,
config: Config,
/// Cache
cache: Option<&'a Cache>,
cache: Option<Cache>,
/// Upstream MusicBrainz API client used for the actual HTTP requests.
upstream_client: UpstreamMusicBrainzClient,
}

impl<'a> MusicBrainzClient<'a> {
impl MusicBrainzClient {
/// Create a new MusicBrainz client.
pub fn new(config: &'a Config, cache: Option<&'a Cache>) -> Self {
Self { config, cache }
pub fn new(config: &Config, cache: Option<&Cache>, user_agent: &str) -> Self {
Self {
config: config.clone(),
cache: cache.cloned(),
upstream_client: UpstreamMusicBrainzClient::new(user_agent),
}
}

/// Find MusicBrainz Release information for the given (generic) Release.
Expand All @@ -55,7 +62,7 @@ impl<'a> MusicBrainzClient<'a> {
match self.find_release_by_id(release_id.into_owned()).await {
Ok(release) => {
let candidate =
ReleaseCandidate::with_base_release(release, base_release, self.config);
ReleaseCandidate::with_base_release(release, base_release, &self.config);
return Ok(vec![candidate]);
}
Err(err) => {
Expand All @@ -75,7 +82,7 @@ impl<'a> MusicBrainzClient<'a> {
.await?;
let heap = KeyedBinaryHeap::with_capacity(
similar_release_ids.len(),
|candidate: &ReleaseCandidate<MusicBrainzRelease>| candidate.distance(self.config),
|candidate: &ReleaseCandidate<MusicBrainzRelease>| candidate.distance(&self.config),
);
let heap = stream::iter(similar_release_ids)
.map(|release_id| self.find_release_by_id(release_id))
Expand All @@ -86,12 +93,12 @@ impl<'a> MusicBrainzClient<'a> {
};

let candidate =
ReleaseCandidate::with_base_release(release, base_release, self.config);
ReleaseCandidate::with_base_release(release, base_release, &self.config);

log::debug!(
"Release '{}' has distance to track collection: {}",
candidate.release().title,
candidate.distance(self.config),
candidate.distance(&self.config),
);
heap.push(candidate);
heap
Expand All @@ -112,7 +119,7 @@ impl<'a> MusicBrainzClient<'a> {
) -> crate::Result<Vec<String>> {
let search_query = build_search_query(base_release);
log::debug!("Querying MusicBrainz: {search_query}");
let response = if let Some(cached_response) = self.cache.and_then(|cache| cache.get_item((search_query.as_ref(), limit, offset))
let response = if let Some(cached_response) = self.cache.as_ref().and_then(|cache| cache.get_item((search_query.as_ref(), limit, offset))
.inspect_err(|err| {
log::debug!("Failed to get release search result for query {search_query} (limit {limit}) from cache: {err}");
})
Expand All @@ -122,14 +129,14 @@ impl<'a> MusicBrainzClient<'a> {
let response = MusicBrainzRelease::search(search_query.clone())
.limit(limit)
.offset(offset)
.execute()
.execute_with_client_async(&self.upstream_client)
.await?;
log::debug!(
"Found {} releases using query: {}",
response.entities.len(),
search_query
);
if let Some(cache) = self.cache {
if let Some(cache) = self.cache.as_ref() {
match cache.insert_item((search_query.as_ref(), limit, offset), &response) {
Ok(()) => {
log::debug!("Inserted release search {search_query:?} (limit: {limit}, offset: {offset}) into cache");
Expand All @@ -155,7 +162,7 @@ impl<'a> MusicBrainzClient<'a> {
&self,
release_group_id: String,
) -> crate::Result<MusicBrainzReleaseGroup> {
if let Some(release_group) = self.cache.and_then(|cache| {
if let Some(release_group) = self.cache.as_ref().and_then(|cache| {
cache
.get_item(release_group_id.as_ref())
.inspect_err(|err| {
Expand All @@ -169,11 +176,11 @@ impl<'a> MusicBrainzClient<'a> {
MusicBrainzReleaseGroup::fetch()
.id(&release_group_id)
.with_releases()
.execute()
.execute_with_client_async(&self.upstream_client)
.map_err(crate::Error::from)
.await
.inspect(|release_group| {
if let Some(cache) = self.cache {
if let Some(cache) = self.cache.as_ref() {
match cache.insert_item(release_group_id.as_ref(), release_group) {
Ok(()) => {
log::debug!("Inserted release group {release_group_id} into cache");
Expand Down Expand Up @@ -221,7 +228,7 @@ impl<'a> MusicBrainzClient<'a> {
&self,
release_id: String,
) -> crate::Result<MusicBrainzRelease> {
if let Some(release) = self.cache.and_then(|cache| {
if let Some(release) = self.cache.as_ref().and_then(|cache| {
cache
.get_item(release_id.as_ref())
.inspect_err(|err| {
Expand All @@ -245,11 +252,11 @@ impl<'a> MusicBrainzClient<'a> {
.with_work_level_relations()
.with_artist_relations()
.with_url_relations()
.execute()
.execute_with_client_async(&self.upstream_client)
.map_err(crate::Error::from)
.await
.inspect(|release| {
if let Some(cache) = self.cache {
if let Some(cache) = self.cache.as_ref() {
match cache.insert_item(release_id.as_ref(), release) {
Ok(()) => {
log::debug!("Inserted release {release_id} into cache");
Expand Down Expand Up @@ -449,7 +456,7 @@ mod tests {
use super::*;
use crate::release::ReleaseLike;
use crate::track::TrackLike;
use musicbrainz_rs_nova::entity::release::Release as MusicBrainzRelease;
use musicbrainz_rs::entity::release::Release as MusicBrainzRelease;

const MUSICBRAINZ_RELEASE_JSON: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
Expand Down
2 changes: 1 addition & 1 deletion src/pathformat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ mod tests {
use crate::Config;
use serde_json::json;

use musicbrainz_rs_nova::entity::release::Release as MusicBrainzRelease;
use musicbrainz_rs::entity::release::Release as MusicBrainzRelease;

const MUSICBRAINZ_RELEASE_JSON: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
Expand Down
11 changes: 7 additions & 4 deletions src/release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ use crate::musicbrainz;
use crate::track::TrackLike;
use crate::Config;
use itertools::Itertools;
use musicbrainz_rs_nova::entity::release::{Release as MusicBrainzRelease, ReleaseStatus};
use musicbrainz_rs_nova::entity::release_group::{
use musicbrainz_rs::entity::release::{Release as MusicBrainzRelease, ReleaseStatus};
use musicbrainz_rs::entity::release_group::{
ReleaseGroupPrimaryType, ReleaseGroupSecondaryType,
};
use std::borrow::Cow;
Expand Down Expand Up @@ -272,13 +272,16 @@ impl ReleaseLike for MusicBrainzRelease {

fn release_date(&self) -> Option<Cow<'_, str>> {
self.date
.map(|date| date.format("%Y-%m-%d").to_string())
.as_ref()
.map(ToString::to_string)
.map(Cow::from)
}

fn release_year(&self) -> Option<Cow<'_, str>> {
self.date
.map(|date| date.format("%Y").to_string())
.as_ref()
.and_then(musicbrainz_rs::entity::date_string::DateString::year)
.map(|year| year.to_string())
.map(Cow::from)
}

Expand Down
16 changes: 8 additions & 8 deletions src/scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ use crate::analyzer;
use crate::musicbrainz::{MusicBrainzClient, MusicBrainzRelease};
use crate::release_candidate::ReleaseCandidateCollection;
use crate::util::walk_dir;
use crate::Cache;
use crate::{Config, TaggedFile, TaggedFileCollection};
use futures::FutureExt;
use regex::RegexBuilder;
Expand Down Expand Up @@ -55,7 +54,11 @@ pub struct Scanner {

impl Scanner {
/// Create a scanner for the given path.
pub fn scan(config: Config, cache: Option<Cache>, path: PathBuf) -> Scanner {
pub fn scan(
config: Config,
musicbrainz: MusicBrainzClient,
path: PathBuf,
) -> Scanner {
log::info!("Starting scan of {}", path.display());

let (results_tx, results_rx) = tokio::sync::mpsc::channel(20);
Expand All @@ -77,7 +80,6 @@ impl Scanner {
// First, search the file system to find track paths.
for (path, tracks) in find_track_paths(path) {
let cloned_config = config.clone();
let cloned_config2 = config.clone();

// Some tracks were found, spawn individual tasks for analyzing the tracks in the
// threadpool. We keep track of the spawned task handles in a Vec, so that we
Expand All @@ -93,15 +95,13 @@ impl Scanner {

// When all handles are joined, make a collection out of it and search similar
// releases on MusicBrainz. The result is sent to the `results_tx` queue.
let cloned_cache = cache.clone();
let cloned_musicbrainz = musicbrainz.clone();
let results_tx = cloned_results_tx.clone();
let _matching_logic = pool_handle.spawn(async move {
let musicbrainz =
MusicBrainzClient::new(&cloned_config2, cloned_cache.as_ref());
if let Err(err) = results_tx
.send(
join_analysis_tasks_to_collection_and_find_release_candidates(
&musicbrainz,
&cloned_musicbrainz,
path,
handles,
)
Expand Down Expand Up @@ -213,7 +213,7 @@ fn analyze_tagged_file(config: &Config, tagged_file: TaggedFile) -> TaggedFile {
/// Join all analysis tasks, then create a TaggedFieCollection from it. Then find similar
/// candidates on MusicBrainz.
async fn join_analysis_tasks_to_collection_and_find_release_candidates(
musicbrainz: &MusicBrainzClient<'_>,
musicbrainz: &MusicBrainzClient,
path: PathBuf,
handles: JoinSet<TaggedFile>,
) -> ScanResult {
Expand Down
2 changes: 1 addition & 1 deletion src/taggedfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,7 @@ impl AnalyzedTrackMetadata for TaggedFileAnalyzedMetadata<'_> {
mod tests {
use super::*;
use crate::TaggedFileCollection;
use musicbrainz_rs_nova::entity::release::{
use musicbrainz_rs::entity::release::{
Release as MusicBrainzRelease, Track as MusicBrainzTrack,
};

Expand Down
Loading