Rewrite getIDs

I liked being able to capture all the information possible from MusicBrainz, but it's not necessary right now. This gives us a much cleaner interface, and less data to worry about. Other than figuring out how to implement retrieving all ids, really need to clean/rename files/functions/etc, then I think we can write more tests and starting looking at CAA, or go back to Zuletzt
This commit is contained in:
mitteneer 2025-03-25 00:08:27 -04:00
parent 4e95f395e2
commit 8028e5e42b
3 changed files with 312 additions and 22 deletions

79
src/StrippedEntities.zig Normal file
View file

@ -0,0 +1,79 @@
//! Types of MusicBrainz Entities described here: https://musicbrainz.org/doc/MusicBrainz_Entity
//! Fields based on JSON response fields
const Alias = @import("Alias.zig").Alias;
const std = @import("std");
pub const Artist = struct {
id: []const u8,
};
pub const ArtistCredit = struct {
name: []const u8,
artist: Artist,
};
pub const Recording = struct {
id: []const u8,
title: []const u8,
//length: u64 = null,
@"artist-credit": []ArtistCredit,
@"first-release-date": ?[]const u8 = null,
releases: []Release,
pub fn lessThan(context: void, a: Recording, b: Recording) bool {
_ = context; // Idk what this is but it's in the Zig docs
if (a.@"first-release-date") |adtstr| {
if (b.@"first-release-date") |bdtstr| {
const adtyr = std.fmt.parseInt(u32, adtstr[0..4], 10) catch 0; // Each `catch 0` is to avoid returning an error
const bdtyr = std.fmt.parseInt(u32, bdtstr[0..4], 10) catch 0;
if (adtyr != bdtyr) {
return adtyr < bdtyr;
} else if (adtstr.len >= 7) {
if (bdtstr.len < 7) return true; // a provides more information
const adtmn = std.fmt.parseInt(u32, adtstr[5..7], 10) catch 0;
const bdtmn = std.fmt.parseInt(u32, bdtstr[5..7], 10) catch 0;
if (adtmn != bdtmn) {
return adtmn < bdtmn;
} else if (adtstr.len == 10) {
if (bdtstr.len < 10) return true; // a provides more information
const adtdy = std.fmt.parseInt(u32, adtstr[8..10], 10) catch 0;
const bdtdy = std.fmt.parseInt(u32, bdtstr[8..10], 10) catch 0;
return adtdy < bdtdy;
} else return false; // Either b provides more information, or they're the same date
} else return false; // Either b provides more information, or they're the same date
} else return true; // b provides no information
} else return false; // a provides no information
}
};
pub const Release = struct {
id: []const u8,
title: []const u8,
@"artist-credit": []ArtistCredit,
date: ?[]const u8 = null,
pub fn lessThan(context: void, a: Release, b: Release) bool {
_ = context; // Idk what this is but it's in the Zig docs
if (a.date) |adtstr| {
if (b.date) |bdtstr| {
const adtyr = std.fmt.parseInt(u32, adtstr[0..4], 10) catch 0; // Each `catch 0` is to avoid returning an error
const bdtyr = std.fmt.parseInt(u32, bdtstr[0..4], 10) catch 0;
if (adtyr != bdtyr) {
return adtyr < bdtyr;
} else if (adtstr.len >= 7) {
if (bdtstr.len < 7) return true; // a provides more information
const adtmn = std.fmt.parseInt(u32, adtstr[5..7], 10) catch 0;
const bdtmn = std.fmt.parseInt(u32, bdtstr[5..7], 10) catch 0;
if (adtmn != bdtmn) {
return adtmn < bdtmn;
} else if (adtstr.len == 10) {
if (bdtstr.len < 10) return true; // a provides more information
const adtdy = std.fmt.parseInt(u32, adtstr[8..10], 10) catch 0;
const bdtdy = std.fmt.parseInt(u32, bdtstr[8..10], 10) catch 0;
return adtdy < bdtdy;
} else return false; // Either b provides more information, or they're the same date
} else return false; // Either b provides more information, or they're the same date
} else return true; // b provides no information
} else return false; // a provides no information
}
};

207
src/StrippedMBQ.zig Normal file
View file

@ -0,0 +1,207 @@
const std = @import("std");
const testing = std.testing;
//const Entities = @import("Entities.zig");
const Entities = @import("StrippedEntities.zig");
const Artist = Entities.Artist;
const Recording = Entities.Recording;
const Release = Entities.Release;
pub const Result = struct {
created: []const u8,
count: u32,
offset: u32,
recordings: []Recording,
// There are two ways of getting information about songs and albums: the canonical and original methods
// Canonical: The information provided is the information you want. If the song title/album title/artist
// name matches, return the respective ID. This will fail often when phrases like "Remastered" are in
// song/album title, or when multiple artists are listed in a scrobble, like "Billy Bragg, Wilco". I also
// haven't implemented case/diacritic insensitiv matching, so Canonical is sorta useless atm
// Original: Tries to find the oldest instance of the information listed, rather than what's explicitly
// stated. Thus, if you search for a song coming from a compilation album (say, Yesterday on the Beatles
// album "1"), it will instead give you the information from the album the song originally appeared on
// (in this case, the album "Help!").
pub fn getOriginalIDs(self: *const Result, smd: searchMetadata, pbo: parseOption) ?[]const u8 {
if (self.count == 0) return null;
switch (pbo) {
.song => |method| {
const recordings = self.recordings;
switch (method) {
.orig => std.mem.sort(Recording, recordings, {}, Recording.lessThan),
.canon => {},
}
for (recordings) |rc| {
if (std.mem.eql(u8, rc.title, smd.tn)) {
if (method == .orig) {
return rc.id;
} else {
const artist_match: bool = artist_search: for (rc.@"artist-credit") |ac| {
if (std.mem.eql(u8, ac.name, smd.an)) break :artist_search true;
} else false; // Does this even make sense?
const album_match: bool = album_search: for (rc.releases) |rl| {
if (std.mem.eql(u8, rl.title, smd.rn)) break :album_search true;
} else false;
if (artist_match and album_match) return rc.id;
}
}
}
return recordings[0].id;
},
.album => |method| {
const recordings = self.recordings;
// This sorting is probably not necessary
switch (method) {
.orig => std.mem.sort(Recording, recordings, {}, Recording.lessThan),
.canon => {},
}
song_loop: for (recordings) |rc| {
const releases = rc.releases;
switch (pbo.album) {
.orig => std.mem.sort(Release, releases, {}, Release.lessThan),
.canon => {
if (!std.mem.eql(u8, rc.title, smd.tn)) continue :song_loop;
for (rc.@"artist-credit") |ac| {
if (!std.mem.eql(u8, ac.name, smd.an)) continue :song_loop;
}
},
}
for (releases) |rl| {
if (std.mem.eql(u8, rl.title, smd.rn)) return rl.id;
}
}
return recordings[0].releases[0].id;
},
.artist => {
for (self.recordings) |rc| {
for (rc.@"artist-credit") |ac| {
if (std.mem.eql(u8, ac.name, smd.an)) return ac.artist.id;
}
}
return self.recordings[0].@"artist-credit"[0].artist.id;
},
.all => unreachable,
}
}
pub fn getIDs(self: *const Result, smd: searchMetadata, pbo: parseByOptions) ?searchIDs {
if (self.count == 0) return null;
const sorted_recordings = self.recordings;
std.mem.sort(Recording, sorted_recordings, {}, Recording.lessThan);
for (sorted_recordings) |rc| {
switch (pbo.parse) {
.song => {
const output = searchIDs{
.id = switch (pbo.specifyBy) {
.song => song_song: {
if (std.mem.eql(u8, rc.title, smd.tn)) {
break :song_song rc.id;
//break :rc_loop;
} else break :song_song "";
},
.album => song_album: {
for (rc.releases) |rl| {
if (std.mem.eql(u8, rl.title, smd.rn)) {
break :song_album rc.id;
//break :rc_loop;
}
}
break :song_album "";
},
.artist => song_artist: {
for (rc.@"artist-credit") |ac| {
if (std.mem.eql(u8, ac.name, smd.an)) {
break :song_artist rc.id;
//break :rc_loop;
}
}
break :song_artist "";
},
.all => unreachable, // I'll do this later
},
};
return output;
},
.album => {
const output = searchIDs{ .id = blk: {
const sorted_releases = rc.releases;
std.mem.sort(Release, sorted_releases, {}, Release.lessThan);
for (sorted_releases) |rl| {
switch (pbo.specifyBy) {
.song, .album => {
if (std.mem.eql(u8, rl.title, smd.rn)) break :blk rl.id;
},
.artist => {
for (rl.@"artist-credit") |ac| {
if (std.mem.eql(u8, ac.name, smd.an)) break :blk rl.id;
}
},
.all => unreachable,
}
}
break :blk "";
} };
return output;
},
.artist => {
const output = searchIDs{ .id = switch (pbo.specifyBy) {
.song, .artist => blk: {
for (rc.@"artist-credit") |ac| {
if (std.mem.eql(u8, ac.name, smd.an)) break :blk ac.artist.id;
}
break :blk "";
},
.album => blk: {
for (rc.releases) |rl| {
for (rl.@"artist-credit") |ac| {
if (std.mem.eql(u8, ac.name, smd.an)) break :blk ac.artist.id;
}
}
break :blk "";
},
.all => unreachable,
} };
return output;
},
.all => {
var output = searchIDs{ .ids = undefined };
if (self.getIDs(smd, .{ .parse = .song })) |ids| output.ids.song_id = ids.id;
if (self.getIDs(smd, .{ .parse = .album })) |ids| output.ids.album_id = ids.id;
if (self.getIDs(smd, .{ .parse = .artist })) |ids| output.ids.artist_id = ids.id;
return output;
},
}
}
return null;
}
};
pub const parseOption = union(enum) {
song: enum { canon, orig },
album: enum { canon, orig },
artist,
all,
};
pub const parseByOptions = struct { parse: parseOption = .song, specifyBy: parseOption = .song };
pub const searchMetadata = struct {
tn: []const u8,
rn: []const u8,
an: []const u8,
};
pub const searchIDs = union {
id: []const u8,
ids: struct {
artist_id: ?[]const u8,
album_id: ?[]const u8,
song_id: ?[]const u8,
},
};

View file

@ -4,12 +4,14 @@ const Entities = @import("Entities.zig");
const Alias = @import("Alias.zig");
const MBQ = @import("MusicBrainzQuery.zig");
const Client = std.http.Client;
const SE = @import("StrippedEntities.zig");
const SMBQ = @import("StrippedMBQ.zig");
pub const user_agent: []const u8 = "ZuletztMBClient/0.0.1 (swebbguy@gmail.com)";
pub fn mbSearch(alloc: std.mem.Allocator, smd: MBQ.searchMetadata) !MBQ.Result {
var encoded: MBQ.searchMetadata = undefined;
inline for (std.meta.fields(MBQ.searchMetadata)) |k| {
pub fn mbSearch(alloc: std.mem.Allocator, smd: SMBQ.searchMetadata) !SMBQ.Result {
var encoded: SMBQ.searchMetadata = undefined;
inline for (std.meta.fields(SMBQ.searchMetadata)) |k| {
var code = std.ArrayList(u8).init(alloc);
errdefer code.deinit();
for (@field(smd, k.name)) |v| {
@ -51,7 +53,7 @@ pub fn mbSearch(alloc: std.mem.Allocator, smd: MBQ.searchMetadata) !MBQ.Result {
else => unreachable,
}
const json = try std.json.parseFromSliceLeaky(MBQ.Result, alloc, ar.items, .{ .ignore_unknown_fields = true });
const json = try std.json.parseFromSliceLeaky(SMBQ.Result, alloc, ar.items, .{ .ignore_unknown_fields = true });
return json;
}
@ -61,17 +63,19 @@ test "arid_via_recording" {
defer arena.deinit();
const test_alloc = arena.allocator();
const metadata = MBQ.searchMetadata{ .tn = "Veni veni Emmanuel", .an = "iamthemorning", .rn = "Counting the Ghosts" };
const metadata = SMBQ.searchMetadata{ .tn = "Veni veni Emmanuel", .an = "iamthemorning", .rn = "Counting the Ghosts" };
const iatm_id: []const u8 = "5854a6de-af8f-4b99-8710-cb47d6436a19";
const search_result = try mbSearch(test_alloc, metadata);
const id = search_result.getIDs(metadata, .{ .parse = .artist, .specifyBy = .song });
const id = search_result.getOriginalIDs(metadata, .artist);
std.Thread.sleep(1000000000);
if (id) |out| {
try testing.expect(std.mem.eql(u8, out.id, iatm_id));
try testing.expect(std.mem.eql(u8, out, iatm_id));
} else {
std.log.err("No results", .{});
}
}
@ -80,17 +84,17 @@ test "arid_via_recording_multiple_artists_1" {
defer arena.deinit();
const test_alloc = arena.allocator();
const metadata = MBQ.searchMetadata{ .tn = "Roll Me Up And Smoke Me When I Die", .an = "Lyle Lovett", .rn = "Willie Nelson American Outlaw" };
const metadata = SMBQ.searchMetadata{ .tn = "Roll Me Up And Smoke Me When I Die", .an = "Lyle Lovett", .rn = "Willie Nelson American Outlaw" };
const ll_id: []const u8 = "7241e3ed-5ad4-4849-94df-6858ea833472";
const search_result = try mbSearch(test_alloc, metadata);
const id = search_result.getIDs(metadata, .{ .parse = .artist, .specifyBy = .song });
const id = search_result.getOriginalIDs(metadata, .artist);
std.Thread.sleep(1000000000);
if (id) |out| {
try testing.expect(std.mem.eql(u8, out.id, ll_id));
try testing.expect(std.mem.eql(u8, out, ll_id));
}
}
@ -99,17 +103,17 @@ test "rgid_via_recording" {
defer arena.deinit();
const test_alloc = arena.allocator();
const metadata = MBQ.searchMetadata{ .tn = "I Of The Storm", .rn = "Beneath the Skin", .an = "Of Monsters and Men" };
const metadata = SMBQ.searchMetadata{ .tn = "I Of The Storm", .rn = "Beneath the Skin", .an = "Of Monsters and Men" };
const bts_id: []const u8 = "4f4f5b98-45ac-4b47-addb-66b501473bd8";
const search_result = try mbSearch(test_alloc, metadata);
const id = search_result.getIDs(metadata, .{ .parse = .album, .specifyBy = .song });
const id = search_result.getOriginalIDs(metadata, .{ .album = .orig });
std.Thread.sleep(1000000000);
if (id) |out| {
try testing.expect(std.mem.eql(u8, out.id, bts_id));
try testing.expect(std.mem.eql(u8, out, bts_id));
}
}
@ -118,16 +122,16 @@ test "rgid_via_recording_multiple_artists_2" {
defer arena.deinit();
const test_alloc = arena.allocator();
const metadata = MBQ.searchMetadata{ .tn = "Hesitating Beauty", .rn = "Mermaid Avenue", .an = "Wilco" };
const metadata = SMBQ.searchMetadata{ .tn = "Hesitating Beauty", .rn = "Mermaid Avenue", .an = "Wilco" };
const wilco_id: []const u8 = "9ba73bf8-6c15-4bd2-8da1-e2292538f617";
const search_result = try mbSearch(test_alloc, metadata);
const id = search_result.getIDs(metadata, .{ .parse = .album, .specifyBy = .song });
const id = search_result.getOriginalIDs(metadata, .{ .album = .orig });
std.Thread.sleep(1000000000);
if (id) |out| {
try testing.expect(std.mem.eql(u8, out.id, wilco_id));
try testing.expect(std.mem.eql(u8, out, wilco_id));
}
}
@ -136,15 +140,15 @@ test "rid_aqualung" {
defer arena.deinit();
const test_alloc = arena.allocator();
const metadata = MBQ.searchMetadata{ .tn = "Aqualung", .rn = "Aqualung", .an = "Jethro Tull" };
const metadata = SMBQ.searchMetadata{ .tn = "Aqualung", .rn = "Aqualung", .an = "Jethro Tull" };
const aqualung_id: []const u8 = "2621d113-3a9f-41f9-a0f2-b9459f86e8e9";
const search_result = try mbSearch(test_alloc, metadata);
const id = search_result.getIDs(metadata, .{ .parse = .song, .specifyBy = .song });
const id = search_result.getOriginalIDs(metadata, .{ .song = .orig });
if (id) |out| {
try testing.expect(std.mem.eql(u8, out.id, aqualung_id));
try testing.expect(std.mem.eql(u8, out, aqualung_id));
}
}
@ -153,14 +157,14 @@ test "rid_battery" {
defer arena.deinit();
const test_alloc = arena.allocator();
const metadata = MBQ.searchMetadata{ .tn = "Battery", .rn = "Master of Puppets", .an = "Metallica" };
const metadata = SMBQ.searchMetadata{ .tn = "Battery", .rn = "Master of Puppets", .an = "Metallica" };
const battery_id: []const u8 = "3bfda26a-49fa-4bc4-a4d6-8bbfa0767ab7";
const search_result = try mbSearch(test_alloc, metadata);
const id = search_result.getIDs(metadata, .{ .parse = .song, .specifyBy = .song });
const id = search_result.getOriginalIDs(metadata, .{ .song = .orig });
if (id) |out| {
try testing.expect(std.mem.eql(u8, out.id, battery_id));
try testing.expect(std.mem.eql(u8, out, battery_id));
}
}