I'm not sure if this is a "good" use of an Arena Allocator, but I'm not getting any leaks, so maybe it's fine...
This commit is contained in:
Samuel Webb 2025-03-23 16:46:06 -04:00
parent 319c5aeae8
commit 4779e105d1
2 changed files with 217 additions and 255 deletions

View file

@ -7,118 +7,117 @@ const Recording = Entities.Recording;
const Release = Entities.Release;
pub const Result = struct {
created: ?[]const u8 = null,
count: ?u32 = null,
offset: ?u32 = null,
artists: ?[]Artist = null,
recordings: ?[]Recording = null,
@"error": ?[]const u8 = null,
help: ?[]const u8 = null,
created: []const u8,
count: u32,
offset: u32,
recordings: []Recording,
pub fn getIDs(self: *const Result, smd: searchMetadata, pbo: parseByOptions) ?searchIDs {
if (self.count) |count| {
if (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;
}
},
.album => song_album: {
if (rc.releases) |rls| {
for (rls) |rl| {
if (std.mem.eql(u8, rl.title, smd.rgn)) {
break :song_album rc.id;
//break :rc_loop;
}
}
} else {
std.log.err("Could not find any releases", .{});
}
},
.artist => song_artist: {
if (rc.@"artist-credit") |acs| {
for (acs) |ac| {
if (std.mem.eql(u8, ac.name, smd.an)) {
break :song_artist rc.id;
//break :rc_loop;
}
}
} else {
std.log.err("Could not find any artists", .{});
}
},
.all => unreachable, // I'll do this later
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 "";
},
};
return output;
},
.album => {
const output = searchIDs{ .id = blk: {
if (rc.releases) |rls| {
const sorted_releases = rls;
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.rgn)) break :blk rl.id;
},
.artist => {
if (rl.@"artist-credit") |acs| {
for (acs) |ac| {
if (std.mem.eql(u8, ac.name, smd.an)) break :blk rl.id;
}
}
},
.all => unreachable,
}
}
break :blk "";
} else break :blk "";
} };
return output;
},
.artist => {
const output = searchIDs{ .id = switch (pbo.specifyBy) {
.song, .artist => blk: {
if (rc.@"artist-credit") |acs| {
for (acs) |ac| {
if (std.mem.eql(u8, ac.name, smd.an)) break :blk ac.artist.id;
}
}
break :blk "";
},
.album => blk: {
.album => song_album: {
if (rc.releases) |rls| {
for (rls) |rl| {
if (std.mem.eql(u8, rl.title, smd.rn)) {
break :song_album rc.id;
//break :rc_loop;
}
}
break :song_album "";
} else {
std.log.err("Could not find any releases", .{});
break :song_album "";
}
},
.artist => song_artist: {
if (rc.@"artist-credit") |acs| {
for (acs) |ac| {
if (std.mem.eql(u8, ac.name, smd.an)) {
break :song_artist rc.id;
//break :rc_loop;
}
}
break :song_artist "";
} else {
std.log.err("Could not find any artists", .{});
break :song_artist "";
}
},
.all => unreachable, // I'll do this later
},
};
return output;
},
.album => {
const output = searchIDs{ .id = blk: {
if (rc.releases) |rls| {
const sorted_releases = rls;
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 => {
if (rl.@"artist-credit") |acs| {
for (acs) |ac| {
if (std.mem.eql(u8, ac.name, smd.an)) break :blk ac.artist.id;
if (std.mem.eql(u8, ac.name, smd.an)) break :blk rl.id;
}
}
},
.all => unreachable,
}
}
break :blk "";
} else break :blk "";
} };
return output;
},
.artist => {
const output = searchIDs{ .id = switch (pbo.specifyBy) {
.song, .artist => blk: {
if (rc.@"artist-credit") |acs| {
for (acs) |ac| {
if (std.mem.eql(u8, ac.name, smd.an)) break :blk ac.artist.id;
}
}
break :blk "";
},
.album => blk: {
if (rc.releases) |rls| {
for (rls) |rl| {
if (rl.@"artist-credit") |acs| {
for (acs) |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;
},
}
}
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;
@ -140,39 +139,27 @@ pub const searchMetadata = struct {
an: []const u8,
};
// Maybe just use a HashMap...
pub const searchMetadataEncoded = struct {
tn: []const u8,
rn: []const u8,
an: []const u8,
//pub fn deinit(self: *searchMetadata) void {
// self.alloc.free(self.tn.enc);
// self.alloc.free(self.rn.enc);
// self.alloc.free(self.an.enc);
//}
};
pub fn percentEncode(alloc: std.mem.Allocator, smd: searchMetadata) !searchMetadataEncoded {
var output: searchMetadataEncoded = undefined;
inline for (std.meta.fields(searchMetadata)) |k| {
var encoded = std.ArrayList(u8).init(alloc);
errdefer encoded.deinit();
for (@field(smd, k.name)) |v| {
if ((v >= 'A' and v <= 'Z') or (v >= 'a' and v <= 'z') or (v >= '0' and v <= '9') or v == '-' or v == '_' or v == '.' or v == '~') {
try encoded.append(v);
} else if (v == ' ') {
try encoded.append('+');
} else {
const hex = try std.fmt.allocPrint(alloc, "%{x}", .{v});
defer alloc.free(hex);
try encoded.appendSlice(hex);
}
}
@field(output, k.name) = try encoded.toOwnedSlice();
}
return output;
}
//pub fn percentEncode(alloc: std.mem.Allocator, smd: searchMetadata) !searchMetadata {
// var output: searchMetadata = undefined;
// inline for (std.meta.fields(searchMetadata)) |k| {
// var encoded = std.ArrayList(u8).init(alloc);
// errdefer encoded.deinit();
// for (@field(smd, k.name)) |v| {
// if ((v >= 'A' and v <= 'Z') or (v >= 'a' and v <= 'z') or (v >= '0' and v <= '9') or v == '-' or v == '_' or v == '.' or v == '~') {
// try encoded.append(v);
// } else if (v == ' ') {
// try encoded.append('+');
// } else {
// const hex = try std.fmt.allocPrint(alloc, "%{x}", .{v});
// defer alloc.free(hex);
// try encoded.appendSlice(hex);
// }
// }
// @field(output, k.name) = try encoded.toOwnedSlice();
// }
//
// return output;
//}
pub const searchIDs = union {
id: []const u8,
@ -183,26 +170,28 @@ pub const searchIDs = union {
},
};
test "pe_jethro_tull" {
const test_alloc = std.testing.allocator;
var jt = searchMetadata{ .alloc = test_alloc, .tn = .{ .dec = "Aqualung" }, .rn = .{ .dec = "Aqualung" }, .an = .{ .dec = "Jethro Tull" } };
try jt.percentEncode();
defer jt.deinit();
try testing.expect(std.mem.eql(u8, jt.an.enc, "Jethro+Tull"));
}
test "pe_aphex_twin" {
const test_alloc = std.testing.allocator;
var at = searchMetadata{ .alloc = test_alloc, .tn = .{ .dec = "#3" }, .rn = .{ .dec = "Selected Ambient Works Volume II" }, .an = .{ .dec = "Aphex Twin" } };
try at.percentEncode();
defer at.deinit();
try testing.expect(std.mem.eql(u8, at.tn.enc, "%233"));
}
test "pe_bon_iver" {
const test_alloc = std.testing.allocator;
var bi = searchMetadata{ .alloc = test_alloc, .tn = .{ .dec = "21 M♢♢N WATER" }, .rn = .{ .dec = "22, a Million" }, .an = .{ .dec = "Bon Iver" } };
try bi.percentEncode();
defer bi.deinit();
try testing.expect(std.mem.eql(u8, bi.tn.enc, "21+M%e2%99%a2%e2%99%a2N+WATER"));
}
//test "pe_jethro_tull" {
// const test_alloc = std.testing.allocator;
// const jt = searchMetadata{ .tn = "Aqualung ", .rn = "Aqualung", .an = "Jethro Tull" };
// const jt_enc = try percentEncode(test_alloc, jt);
// defer test_alloc.free(jt_enc);
// //defer jt.deinit();
// try testing.expect(std.mem.eql(u8, jt_enc.an, "Jethro+Tull"));
//}
//
//test "pe_aphex_twin" {
// const test_alloc = std.testing.allocator;
// var at = searchMetadata{ .alloc = test_alloc, .tn = .{ .dec = "#3" }, .rn = .{ .dec = "Selected Ambient Works Volume II" }, .an = .{ .dec = "Aphex Twin" } };
// try at.percentEncode();
// defer at.deinit();
// try testing.expect(std.mem.eql(u8, at.tn.enc, "%233"));
//}
//
//test "pe_bon_iver" {
// const test_alloc = std.testing.allocator;
// var bi = searchMetadata{ .alloc = test_alloc, .tn = .{ .dec = "21 M♢♢N WATER" }, .rn = .{ .dec = "22, a Million" }, .an = .{ .dec = "Bon Iver" } };
// try bi.percentEncode();
// defer bi.deinit();
// try testing.expect(std.mem.eql(u8, bi.tn.enc, "21+M%e2%99%a2%e2%99%a2N+WATER"));
//}
//

View file

@ -1,6 +1,5 @@
const std = @import("std");
const testing = std.testing;
//const zeit = @import("zeit");
const Entities = @import("Entities.zig");
const Alias = @import("Alias.zig");
const MBQ = @import("MusicBrainzQuery.zig");
@ -8,18 +7,31 @@ const Client = std.http.Client;
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 {
const encoded = try MBQ.percentEncode(alloc, smd);
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| {
var code = std.ArrayList(u8).init(alloc);
errdefer code.deinit();
for (@field(smd, k.name)) |v| {
if ((v >= 'A' and v <= 'Z') or (v >= 'a' and v <= 'z') or (v >= '0' and v <= '9') or v == '-' or v == '_' or v == '.' or v == '~') {
try code.append(v);
} else if (v == ' ') {
try code.append('+');
} else {
const hex = try std.fmt.allocPrint(alloc, "%{x}", .{v});
defer alloc.free(hex);
try code.appendSlice(hex);
}
}
@field(encoded, k.name) = try code.toOwnedSlice();
}
// Might consider making just one of these and passing it around
var client = Client{ .allocator = alloc };
defer client.deinit();
const query: []const u8 = try std.fmt.allocPrint(alloc, "https://musicbrainz.org/ws/2/recording/?query=\"{s}\"%20AND%20artist:\"{s}\"%20AND%20release:\"{s}\"&fmt=json", .{ encoded.tn, encoded.an, encoded.rn });
defer alloc.free(query);
var ar = std.ArrayList(u8).init(alloc);
errdefer ar.deinit();
const response: Client.FetchResult = try client.fetch(.{ .response_storage = .{ .dynamic = &ar }, .location = .{ .url = query }, .method = .GET, .headers = .{ .user_agent = .{ .override = user_agent } } });
@ -27,7 +39,6 @@ pub fn mbSearch(alloc: std.mem.Allocator, smd: MBQ.searchMetadata) !?MBQ.Result
0...299 => std.log.debug("Success", .{}),
300...399 => {
std.log.err("Redirected", .{});
return null;
},
400...511 => {
std.log.err("Get rekt {s}", .{ar.items});
@ -35,158 +46,120 @@ pub fn mbSearch(alloc: std.mem.Allocator, smd: MBQ.searchMetadata) !?MBQ.Result
},
512 => {
std.log.err("Need to login", .{});
return null;
},
else => unreachable,
}
const json = try std.json.parseFromSlice(MBQ.Result, alloc, ar.items, .{ .ignore_unknown_fields = true });
errdefer json.deinit();
const json = try std.json.parseFromSliceLeaky(MBQ.Result, alloc, ar.items, .{ .ignore_unknown_fields = true });
return json.value;
return json;
}
test "arid_via_recording" {
const test_alloc = std.testing.allocator;
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const test_alloc = arena.allocator();
const metadata = MBQ.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);
if (search_result) |rs| {
const id = rs.getIDs(metadata, .{ .parse = .artist, .specifyBy = .song });
std.Thread.sleep(1000000000);
const id = search_result.getIDs(metadata, .{ .parse = .artist, .specifyBy = .song });
if (id) |out| {
try testing.expect(std.mem.eql(u8, out.id, iatm_id));
}
std.Thread.sleep(1000000000);
if (id) |out| {
try testing.expect(std.mem.eql(u8, out.id, iatm_id));
}
}
test "arid_via_recording_multiple_artists_1" {
const test_alloc = std.testing.allocator;
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const test_alloc = arena.allocator();
const metadata = MBQ.searchMetadata{ .alloc = test_alloc, .tn = .{ .dec = "Roll Me Up And Smoke Me When I Die" }, .an = .{ .dec = "Lyle Lovett" }, .rn = .{ .dec = "Willie Nelson American Outlaw" } };
const metadata = MBQ.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";
var mb_result = std.ArrayList(u8).init(test_alloc);
defer mb_result.deinit();
const search_result = try mbSearch(test_alloc, metadata);
const search_result = try mbSearch(test_alloc, &mb_result, metadata);
const id = search_result.getIDs(metadata, .{ .parse = .artist, .specifyBy = .song });
if (search_result) |sr| {
const json = try std.json.parseFromSlice(MBQ.Result, test_alloc, sr, .{ .ignore_unknown_fields = true });
defer json.deinit();
std.Thread.sleep(1000000000);
const result: MBQ.Result = json.value;
const id = result.getIDs(metadata, .{ .parse = .artist, .specifyBy = .song });
std.Thread.sleep(1000000000);
if (id) |out| {
try testing.expect(std.mem.eql(u8, out.id, ll_id));
}
if (id) |out| {
try testing.expect(std.mem.eql(u8, out.id, ll_id));
}
}
test "rgid_via_recording" {
const test_alloc = std.testing.allocator;
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const test_alloc = arena.allocator();
const metadata = MBQ.searchMetadata{ .alloc = test_alloc, .tn = .{ .dec = "I Of The Storm" }, .rn = .{ .dec = "Beneath the Skin" }, .an = .{ .dec = "Of Monsters and Men" } };
const metadata = MBQ.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";
var mb_result = std.ArrayList(u8).init(test_alloc);
defer mb_result.deinit();
const search_result = try mbSearch(test_alloc, metadata);
const search_result = try mbSearch(test_alloc, &mb_result, metadata);
const id = search_result.getIDs(metadata, .{ .parse = .album, .specifyBy = .song });
if (search_result) |sr| {
const json = try std.json.parseFromSlice(MBQ.Result, test_alloc, sr, .{ .ignore_unknown_fields = true });
defer json.deinit();
std.Thread.sleep(1000000000);
const result: MBQ.Result = json.value;
const id = result.getIDs(metadata, .{ .parse = .album, .specifyBy = .song });
std.Thread.sleep(1000000000);
if (id) |out| {
try testing.expect(std.mem.eql(u8, out.id, bts_id));
}
if (id) |out| {
try testing.expect(std.mem.eql(u8, out.id, bts_id));
}
}
test "rgid_via_recording_multiple_artists_2" {
const test_alloc = std.testing.allocator;
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const test_alloc = arena.allocator();
const metadata = MBQ.searchMetadata{ .alloc = test_alloc, .tn = .{ .dec = "Hesitating Beauty" }, .rn = .{ .dec = "Mermaid Avenue" }, .an = .{ .dec = "Wilco" } };
const metadata = MBQ.searchMetadata{ .tn = "Hesitating Beauty", .rn = "Mermaid Avenue", .an = "Wilco" };
const wilco_id: []const u8 = "9ba73bf8-6c15-4bd2-8da1-e2292538f617";
var mb_result = std.ArrayList(u8).init(test_alloc);
defer mb_result.deinit();
const search_result = try mbSearch(test_alloc, metadata);
const search_result = try mbSearch(test_alloc, &mb_result, metadata);
const id = search_result.getIDs(metadata, .{ .parse = .album, .specifyBy = .song });
if (search_result) |sr| {
const json = try std.json.parseFromSlice(MBQ.Result, test_alloc, sr, .{ .ignore_unknown_fields = true });
defer json.deinit();
const result: MBQ.Result = json.value;
const id = result.getIDs(metadata, .{ .parse = .album, .specifyBy = .song });
std.Thread.sleep(1000000000);
if (id) |out| {
try testing.expect(std.mem.eql(u8, out.id, wilco_id));
}
std.Thread.sleep(1000000000);
if (id) |out| {
try testing.expect(std.mem.eql(u8, out.id, wilco_id));
}
//try testing.expect(false);
}
test "rid_aqualung" {
const test_alloc = std.testing.allocator;
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const test_alloc = arena.allocator();
const metadata = MBQ.searchMetadata{ .alloc = test_alloc, .tn = .{ .dec = "Aqualung" }, .rn = .{ .dec = "Aqualung" }, .an = .{ .dec = "Jethro Tull" } };
const metadata = MBQ.searchMetadata{ .tn = "Aqualung", .rn = "Aqualung", .an = "Jethro Tull" };
const aqualung_id: []const u8 = "2621d113-3a9f-41f9-a0f2-b9459f86e8e9";
var mb_result = std.ArrayList(u8).init(test_alloc);
defer mb_result.deinit();
const search_result = try mbSearch(test_alloc, metadata);
const search_result = try mbSearch(test_alloc, &mb_result, metadata);
const id = search_result.getIDs(metadata, .{ .parse = .song, .specifyBy = .song });
if (search_result) |sr| {
const json = try std.json.parseFromSlice(MBQ.Result, test_alloc, sr, .{ .ignore_unknown_fields = true });
defer json.deinit();
const result: MBQ.Result = json.value;
const id = result.getIDs(metadata, .{ .parse = .song, .specifyBy = .song });
if (id) |out| {
try testing.expect(std.mem.eql(u8, out.id, aqualung_id));
}
if (id) |out| {
try testing.expect(std.mem.eql(u8, out.id, aqualung_id));
}
}
test "rid_battery" {
const test_alloc = std.testing.allocator;
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const test_alloc = arena.allocator();
const metadata = MBQ.searchMetadata{ .alloc = test_alloc, .tn = .{ .dec = "Battery" }, .rn = .{ .dec = "Master of Puppets" }, .an = .{ .dec = "Metallica" } };
const metadata = MBQ.searchMetadata{ .tn = "Battery", .rn = "Master of Puppets", .an = "Metallica" };
const battery_id: []const u8 = "3bfda26a-49fa-4bc4-a4d6-8bbfa0767ab7";
var mb_result = std.ArrayList(u8).init(test_alloc);
defer mb_result.deinit();
const search_result = try mbSearch(test_alloc, metadata);
const search_result = try mbSearch(test_alloc, &mb_result, metadata);
const id = search_result.getIDs(metadata, .{ .parse = .song, .specifyBy = .song });
if (search_result) |sr| {
const json = try std.json.parseFromSlice(MBQ.Result, test_alloc, sr, .{ .ignore_unknown_fields = true });
defer json.deinit();
const result: MBQ.Result = json.value;
const id = result.getIDs(metadata, .{ .parse = .song, .specifyBy = .song });
if (id) |out| {
try testing.expect(std.mem.eql(u8, out.id, battery_id));
}
if (id) |out| {
try testing.expect(std.mem.eql(u8, out.id, battery_id));
}
}