srctree

Andrew Kelley parent f0ebdbe2 283b001c
add musicbrainz example usage to fingerprint

README.md added: 214, removed: 26, total 188
@@ -29,6 +29,5 @@ zig build
* example that transcodes a list of input files
* update playlist example to support scripted inputs including pausing,
seeking, volume adjustment
* update fingerprint example to lookup in the musicbrainz API
* integration with https://coverartarchive.org/
* [picard tag mapping](https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html)
 
example/fingerprint.zig added: 214, removed: 26, total 188
@@ -2,6 +2,13 @@ const std = @import("std");
const player = @import("player");
const gpa = std.heap.raw_c_allocator;
 
pub const std_options: std.Options = .{
.log_level = switch (@import("builtin").mode) {
.Debug => .debug,
else => .info,
},
};
 
pub fn main() !void {
var arena_instance = std.heap.ArenaAllocator.init(gpa);
defer arena_instance.deinit();
@@ -40,19 +47,34 @@ pub fn main() !void {
 
std.log.info("acoustid.org politely asks applications to rate limit themselves to 3 requests per second, and this example code does not implement that for you", .{});
 
const acoustid = try player.acoustid.lookup(.{
.arena = arena,
.http_client = &http_client,
.api_key = acoustid_api_key,
.duration = chromaprint.duration,
.base64_fingerprint = encoded_fingerprint,
});
const acoustid = try player.acoustid.lookup(arena, &http_client, acoustid_api_key, chromaprint.duration, encoded_fingerprint);
 
for (acoustid.results) |result| {
for (result.recordings) |recording| {
std.log.info("acoustid result: recording={s} score={d}", .{
recording.id, result.score,
});
const response = try player.musicbrainz.lookup(arena, &http_client, recording.id);
std.log.info("musicbrainz: {s} - {s} [{s}]", .{
response.@"artist-credit"[0].name, response.title, response.@"first-release-date",
});
for (response.releases, 0..) |release, i| {
std.log.info("musicbrainz release[{d}] album: {s} ({s})", .{
i, release.title, release.media[0].format,
});
if (release.media[0].title.len != 0) {
std.log.info("musicbrainz release[{d}] media title: {s}", .{ i, release.media[0].title });
}
std.log.info("musicbrainz release[{d}] track {d}/{d} (disc {d})", .{
i,
release.media[0].@"track-offset" + 1,
release.media[0].@"track-count",
release.media[0].position,
});
}
for (response.relations) |relation| {
std.log.info("it's a cover of {s}", .{relation.work.id});
}
}
}
}
 
src/acoustid.zig added: 214, removed: 26, total 188
@@ -2,30 +2,26 @@ const std = @import("std");
const Allocator = std.mem.Allocator;
const player = @import("root.zig");
 
pub const LookupOptions = struct {
pub fn lookup(
arena: Allocator,
http_client: *std.http.Client,
api_key: []const u8,
duration: u32,
base64_fingerprint: []const u8,
};
 
pub fn lookup(options: LookupOptions) !Response {
) !Response {
var server_header_buffer: [16 * 1024]u8 = undefined;
var json_read_buffer: [16 * 1024]u8 = undefined;
var req = try options.http_client.open(.GET, .{
var req = try http_client.open(.GET, .{
.scheme = "https",
.host = "api.acoustid.org",
.path = "/v2/lookup",
.query = try std.fmt.allocPrint(options.arena, "client={s}&meta=recordingids&duration={d}&fingerprint={s}", .{
options.api_key, options.duration, options.base64_fingerprint,
.query = try std.fmt.allocPrint(arena, "client={s}&meta=recordingids&duration={d}&fingerprint={s}", .{
api_key, duration, base64_fingerprint,
}),
}, .{
.server_header_buffer = &server_header_buffer,
.headers = .{
// Used by MusicBrainz for rate-limiting and contacting us if
// there is misbehaving code in the wild.
.user_agent = .{ .override = "Groove Basin/dev.ec4c673 (https://codeberg.org/andrewrk/player)" },
.user_agent = .{ .override = player.http_user_agent },
},
.extra_headers = &.{
.{ .name = "accept", .value = "application/json" },
@@ -49,7 +45,7 @@ pub fn lookup(options: LookupOptions) !Response {
return error.HttpResponseNotJson;
 
const raw_json = json_read_buffer[0..(try req.readAll(&json_read_buffer))];
const response = try std.json.parseFromSliceLeaky(Response, options.arena, raw_json, .{
const response = try std.json.parseFromSliceLeaky(Response, arena, raw_json, .{
.ignore_unknown_fields = true,
});
if (!std.mem.eql(u8, response.status, "ok"))
 
filename was Deleted added: 214, removed: 26, total 188
@@ -0,0 +1,166 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const player = @import("root.zig");
 
pub fn lookup(
arena: Allocator,
http_client: *std.http.Client,
recording_id: []const u8,
) !Response {
var server_header_buffer: [16 * 1024]u8 = undefined;
var json_read_buffer: [16 * 1024]u8 = undefined;
var req = try http_client.open(.GET, .{
.scheme = "https",
.host = "musicbrainz.org",
.path = try std.fmt.allocPrint(arena, "/ws/2/recording/{s}", .{recording_id}),
.query = "inc=work-rels+artist-credits+releases+discids",
}, .{
.server_header_buffer = &server_header_buffer,
.headers = .{
.user_agent = .{ .override = player.http_user_agent },
},
.extra_headers = &.{
.{ .name = "accept", .value = "application/json" },
},
});
defer req.deinit();
 
try req.send(.{ .raw_uri = true });
try req.wait();
 
if (req.response.status != .ok)
return error.HttpRequestFailed;
 
const content_type = req.response.content_type orelse
return error.HttpResponseMissingContentType;
 
const mime_type_end = std.mem.indexOf(u8, content_type, ";") orelse content_type.len;
const mime_type = content_type[0..mime_type_end];
 
if (!std.ascii.eqlIgnoreCase(mime_type, "application/json"))
return error.HttpResponseNotJson;
 
const raw_json = json_read_buffer[0..(try req.readAll(&json_read_buffer))];
const response = try std.json.parseFromSliceLeaky(Response, arena, raw_json, .{
.ignore_unknown_fields = true,
});
 
return response;
}
 
pub const Response = struct {
//disambiguation: []u8,
title: []u8,
@"artist-credit": []ArtistCredit,
//id: []u8,
relations: []Relation,
@"first-release-date": []u8,
//length: f64,
releases: []Release,
//video: bool,
};
 
pub const ArtistCredit = struct {
//artist: Artist,
//joinphrase: []u8,
name: []u8,
};
 
pub const Artist = struct {
@"sort-name": []u8,
disambiguation: []u8,
@"type-id": []u8,
type: []u8,
id: []u8,
name: []u8,
};
 
pub const Relation = struct {
//attributes: [][]u8,
work: Work,
//"attribute-values": {},
//ended: bool,
//@"attribute-ids": AttributeIds,
//@"source-credit": []u8,
//@"target-credit": []u8,
//@"end": null,
//direction: []u8,
//@"type-id": []u8,
//type: []u8,
//@"begin": null,
//@"target-type": []u8,
};
 
pub const Release = struct {
//id: []u8,
//country: []u8,
//packaging: []u8,
//@"status-id": []u8,
//@"release-events": []ReleaseEvent,
//barcode: []u8,
//status: []u8,
//@"text-representation": TextRepresentation,
media: []Media,
//quality: []u8,
//disambiguation: []u8,
title: []u8,
//date: []u8,
//@"artist-credit": []ArtistCredit,
//@"packaging-id": []u8,
};
 
pub const Media = struct {
format: []u8,
position: u32,
//@"discs": []Disc,
@"track-count": u32,
//tracks: []Track,
@"track-offset": u32,
title: []u8,
//@"format-id": []u8,
};
 
pub const Track = struct {
title: []u8,
@"artist-credit": []ArtistCredit,
number: []u8,
length: f64,
position: u32,
id: []u8,
};
 
pub const Work = struct {
id: []u8,
type: []u8,
disambiguation: []u8,
@"type-id": []u8,
title: []u8,
languages: [][]u8,
iswcs: [][]u8,
//@"attributes": [],
language: []u8,
};
 
pub const AttributeIds = struct {
cover: []u8,
};
 
pub const ReleaseEvent = struct {
area: Area,
date: []u8,
};
 
pub const TextRepresentation = struct {
script: []u8,
language: []u8,
};
 
pub const Area = struct {
@"sort-name": []u8,
//@"type-id": null,
disambiguation: []u8,
@"iso-3166-1-codes": [][]u8,
//@"type": null,
name: []u8,
id: []u8,
};
 
src/root.zig added: 214, removed: 26, total 188
@@ -7,6 +7,11 @@ const chromaprint = @import("chromaprint.zig");
 
pub const SoundIo = @import("SoundIo.zig");
pub const acoustid = @import("acoustid.zig");
pub const musicbrainz = @import("musicbrainz.zig");
 
// Used by MusicBrainz for rate-limiting and contacting us if
// there is misbehaving code in the wild.
pub const http_user_agent = "Groove Basin/dev.ec4c673 (https://codeberg.org/andrewrk/player)";
 
var av_singleton_cache: struct {
abuffer: *const av.Filter,
@@ -21,8 +26,8 @@ pub const InitError = error{
};
 
pub fn init() InitError!void {
switch (builtin.mode) {
.Debug => av.LOG.DEBUG.set_level(),
switch (std.options.log_level) {
.debug => av.LOG.DEBUG.set_level(),
else => av.LOG.QUIET.set_level(),
}