Formatting
This commit is contained in:
+6
-3
@@ -45,8 +45,8 @@ pub fn normalize(s: &str) -> String {
|
||||
/// Escape special characters for MusicBrainz Lucene query syntax.
|
||||
pub fn escape_lucene(s: &str) -> String {
|
||||
let special = [
|
||||
'+', '-', '&', '|', '!', '(', ')', '{', '}', '[', ']', '^', '"', '~', '*', '?', ':',
|
||||
'\\', '/',
|
||||
'+', '-', '&', '|', '!', '(', ')', '{', '}', '[', ']', '^', '"', '~', '*', '?', ':', '\\',
|
||||
'/',
|
||||
];
|
||||
let mut result = String::with_capacity(s.len());
|
||||
for c in s.chars() {
|
||||
@@ -72,7 +72,10 @@ mod tests {
|
||||
fn test_normalize_strips_official_video() {
|
||||
assert_eq!(normalize("Time (Official Video)"), "time");
|
||||
assert_eq!(normalize("Money (Official Music Video)"), "money");
|
||||
assert_eq!(normalize("Comfortably Numb (Official Audio)"), "comfortably numb");
|
||||
assert_eq!(
|
||||
normalize("Comfortably Numb (Official Audio)"),
|
||||
"comfortably numb"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+1
-3
@@ -31,9 +31,7 @@ pub fn write_tags(
|
||||
) -> TagResult<()> {
|
||||
let path = Path::new(file_path);
|
||||
|
||||
let tagged_file = Probe::open(path)?
|
||||
.options(ParseOptions::default())
|
||||
.read()?;
|
||||
let tagged_file = Probe::open(path)?.options(ParseOptions::default()).read()?;
|
||||
|
||||
// Determine the tag type to use
|
||||
let tag_type = tagged_file
|
||||
|
||||
+4
-1
@@ -7,7 +7,10 @@ use shanty_db::Database;
|
||||
use shanty_tag::{MusicBrainzClient, TagConfig, run_tagging};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "shanty-tag", about = "Fill in missing metadata on music files via MusicBrainz")]
|
||||
#[command(
|
||||
name = "shanty-tag",
|
||||
about = "Fill in missing metadata on music files via MusicBrainz"
|
||||
)]
|
||||
struct Cli {
|
||||
/// Database URL. Defaults to sqlite://<XDG_DATA_HOME>/shanty/shanty.db?mode=rwc
|
||||
#[arg(long, env = "SHANTY_DATABASE_URL")]
|
||||
|
||||
+2
-8
@@ -30,9 +30,7 @@ pub fn build_query(track: &track::Model) -> Option<(String, String)> {
|
||||
|
||||
/// Parse "Artist - Title" from a filename, stripping extension and path.
|
||||
pub fn parse_filename(file_path: &str) -> Option<(String, String)> {
|
||||
let filename = std::path::Path::new(file_path)
|
||||
.file_stem()?
|
||||
.to_str()?;
|
||||
let filename = std::path::Path::new(file_path).file_stem()?.to_str()?;
|
||||
|
||||
// Try common "Artist - Title" pattern
|
||||
if let Some((artist, title)) = filename.split_once(" - ") {
|
||||
@@ -55,11 +53,7 @@ pub fn parse_filename(file_path: &str) -> Option<(String, String)> {
|
||||
/// Score a candidate recording against the track's known metadata.
|
||||
/// Returns a confidence value from 0.0 to 1.0.
|
||||
pub fn score_match(track: &track::Model, candidate: &RecordingMatch) -> f64 {
|
||||
let track_title = track
|
||||
.title
|
||||
.as_deref()
|
||||
.map(normalize)
|
||||
.unwrap_or_default();
|
||||
let track_title = track.title.as_deref().map(normalize).unwrap_or_default();
|
||||
let candidate_title = normalize(&candidate.title);
|
||||
|
||||
let track_artist = track
|
||||
|
||||
+20
-32
@@ -64,11 +64,7 @@ impl MusicBrainzClient {
|
||||
}
|
||||
|
||||
impl MetadataProvider for MusicBrainzClient {
|
||||
async fn search_recording(
|
||||
&self,
|
||||
artist: &str,
|
||||
title: &str,
|
||||
) -> TagResult<Vec<RecordingMatch>> {
|
||||
async fn search_recording(&self, artist: &str, title: &str) -> TagResult<Vec<RecordingMatch>> {
|
||||
let query = if artist.is_empty() {
|
||||
format!("recording:{}", escape_lucene(title))
|
||||
} else {
|
||||
@@ -78,7 +74,10 @@ impl MetadataProvider for MusicBrainzClient {
|
||||
escape_lucene(title)
|
||||
)
|
||||
};
|
||||
let url = format!("{BASE_URL}/recording/?query={}&fmt=json&limit=5", urlencoded(&query));
|
||||
let url = format!(
|
||||
"{BASE_URL}/recording/?query={}&fmt=json&limit=5",
|
||||
urlencoded(&query)
|
||||
);
|
||||
let resp: MbRecordingSearchResponse = self.get_json(&url).await?;
|
||||
|
||||
Ok(resp
|
||||
@@ -108,11 +107,7 @@ impl MetadataProvider for MusicBrainzClient {
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn search_release(
|
||||
&self,
|
||||
artist: &str,
|
||||
album: &str,
|
||||
) -> TagResult<Vec<ReleaseMatch>> {
|
||||
async fn search_release(&self, artist: &str, album: &str) -> TagResult<Vec<ReleaseMatch>> {
|
||||
let query = if artist.is_empty() {
|
||||
format!("release:{}", escape_lucene(album))
|
||||
} else {
|
||||
@@ -122,7 +117,10 @@ impl MetadataProvider for MusicBrainzClient {
|
||||
escape_lucene(album)
|
||||
)
|
||||
};
|
||||
let url = format!("{BASE_URL}/release/?query={}&fmt=json&limit=5", urlencoded(&query));
|
||||
let url = format!(
|
||||
"{BASE_URL}/release/?query={}&fmt=json&limit=5",
|
||||
urlencoded(&query)
|
||||
);
|
||||
let resp: MbReleaseSearchResponse = self.get_json(&url).await?;
|
||||
|
||||
Ok(resp
|
||||
@@ -144,9 +142,7 @@ impl MetadataProvider for MusicBrainzClient {
|
||||
}
|
||||
|
||||
async fn get_recording(&self, mbid: &str) -> TagResult<RecordingDetails> {
|
||||
let url = format!(
|
||||
"{BASE_URL}/recording/{mbid}?inc=artists+releases+genres&fmt=json"
|
||||
);
|
||||
let url = format!("{BASE_URL}/recording/{mbid}?inc=artists+releases+genres&fmt=json");
|
||||
let r: MbRecordingDetail = self.get_json(&url).await?;
|
||||
|
||||
let (artist_name, artist_mbid) = extract_artist_credit(&r.artist_credit);
|
||||
@@ -177,11 +173,7 @@ impl MetadataProvider for MusicBrainzClient {
|
||||
secondary_artists,
|
||||
})
|
||||
}
|
||||
async fn search_artist(
|
||||
&self,
|
||||
query: &str,
|
||||
limit: u32,
|
||||
) -> TagResult<Vec<ArtistSearchResult>> {
|
||||
async fn search_artist(&self, query: &str, limit: u32) -> TagResult<Vec<ArtistSearchResult>> {
|
||||
let url = format!(
|
||||
"{BASE_URL}/artist/?query={}&fmt=json&limit={limit}",
|
||||
urlencoded(&escape_lucene(query))
|
||||
@@ -207,9 +199,7 @@ impl MetadataProvider for MusicBrainzClient {
|
||||
artist_mbid: &str,
|
||||
limit: u32,
|
||||
) -> TagResult<Vec<DiscographyEntry>> {
|
||||
let url = format!(
|
||||
"{BASE_URL}/release/?artist={artist_mbid}&fmt=json&limit={limit}"
|
||||
);
|
||||
let url = format!("{BASE_URL}/release/?artist={artist_mbid}&fmt=json&limit={limit}");
|
||||
let resp: MbReleaseSearchResponse = self.get_json(&url).await?;
|
||||
|
||||
Ok(resp
|
||||
@@ -225,13 +215,8 @@ impl MetadataProvider for MusicBrainzClient {
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn get_release_tracks(
|
||||
&self,
|
||||
release_mbid: &str,
|
||||
) -> TagResult<Vec<ReleaseTrack>> {
|
||||
let url = format!(
|
||||
"{BASE_URL}/release/{release_mbid}?inc=recordings&fmt=json"
|
||||
);
|
||||
async fn get_release_tracks(&self, release_mbid: &str) -> TagResult<Vec<ReleaseTrack>> {
|
||||
let url = format!("{BASE_URL}/release/{release_mbid}?inc=recordings&fmt=json");
|
||||
let resp: MbReleaseDetail = self.get_json(&url).await?;
|
||||
|
||||
let mut tracks = Vec::new();
|
||||
@@ -270,7 +255,8 @@ impl MetadataProvider for MusicBrainzClient {
|
||||
primary_type: rg.primary_type,
|
||||
secondary_types: rg.secondary_types.unwrap_or_default(),
|
||||
first_release_date: rg.first_release_date,
|
||||
first_release_mbid: rg.releases
|
||||
first_release_mbid: rg
|
||||
.releases
|
||||
.and_then(|r| r.into_iter().next().map(|rel| rel.id)),
|
||||
})
|
||||
.collect())
|
||||
@@ -294,7 +280,9 @@ fn extract_artist_credit(credits: &Option<Vec<MbArtistCredit>>) -> (String, Opti
|
||||
/// Extract non-featuring secondary artists from MusicBrainz artist credits.
|
||||
/// Returns (name, mbid) pairs for collaborators that aren't "featuring" credits.
|
||||
fn extract_secondary_artists(credits: &Option<Vec<MbArtistCredit>>) -> Vec<(String, String)> {
|
||||
let Some(credits) = credits else { return vec![] };
|
||||
let Some(credits) = credits else {
|
||||
return vec![];
|
||||
};
|
||||
if credits.len() <= 1 {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
+15
-11
@@ -107,12 +107,16 @@ pub async fn tag_track(
|
||||
|
||||
// Upsert primary artist with MusicBrainz ID
|
||||
let artist_id = match &details.artist_mbid {
|
||||
Some(mbid) => {
|
||||
Some(queries::artists::upsert(conn, &details.artist, Some(mbid)).await?.id)
|
||||
}
|
||||
None => {
|
||||
Some(queries::artists::upsert(conn, &details.artist, None).await?.id)
|
||||
}
|
||||
Some(mbid) => Some(
|
||||
queries::artists::upsert(conn, &details.artist, Some(mbid))
|
||||
.await?
|
||||
.id,
|
||||
),
|
||||
None => Some(
|
||||
queries::artists::upsert(conn, &details.artist, None)
|
||||
.await?
|
||||
.id,
|
||||
),
|
||||
};
|
||||
|
||||
// Upsert secondary collaborator artists so they exist as separate library entries
|
||||
@@ -174,16 +178,16 @@ pub async fn tag_track(
|
||||
queries::tracks::update_metadata(conn, track.id, active).await?;
|
||||
|
||||
// Optionally write tags to file
|
||||
if config.write_tags {
|
||||
if let Err(e) = file_tags::write_tags(
|
||||
if config.write_tags
|
||||
&& let Err(e) = file_tags::write_tags(
|
||||
&track.file_path,
|
||||
&details,
|
||||
best_release.as_ref(),
|
||||
year,
|
||||
genre.as_deref(),
|
||||
) {
|
||||
tracing::warn!(id = track.id, path = %track.file_path, "failed to write file tags: {e}");
|
||||
}
|
||||
)
|
||||
{
|
||||
tracing::warn!(id = track.id, path = %track.file_path, "failed to write file tags: {e}");
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
|
||||
Reference in New Issue
Block a user