@@ -5,6 +5,7 @@ const fatal = std.zig.fatal;
const Config = @import("Config.zig");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const Db = @import("Db.zig");
const usage =
\\Usage: groovebasin [options]
@@ -152,229 +153,6 @@ pub fn main() anyerror!noreturn {
@panic("TODO");
}
pub const Db = struct {
files: std.ArrayListUnmanaged(File),
directories: std.ArrayListUnmanaged(Path),
albums: std.AutoArrayHashMapUnmanaged(Album, void),
string_bytes: std.ArrayListUnmanaged(u8),
/// Used for finding the index inside `string_bytes`.
string_table: std.HashMapUnmanaged(
u32,
void,
std.hash_map.StringIndexContext,
std.hash_map.default_max_load_percentage,
) = .{},
/// Points inside `string_bytes`.
pub const String = enum(u32) {
/// Zero length string.
empty,
_,
pub fn toOptional(i: String) OptionalString {
return @enumFromInt(@intFromEnum(i));
}
};
pub const OptionalString = enum(u32) {
/// Zero length string.
empty,
/// Null.
none = std.math.maxInt(u32),
_,
pub fn unwrap(i: OptionalString) ?String {
if (i == .none) return null;
return @enumFromInt(@intFromEnum(i));
}
};
pub const Path = extern struct {
parent: OptionalIndex,
basename: String,
pub const Index = enum(u16) {
_,
pub fn toOptional(i: Index) OptionalIndex {
return @enumFromInt(@intFromEnum(i));
}
};
pub const OptionalIndex = enum(u16) {
none = std.math.maxInt(u16),
_,
pub fn unwrap(i: OptionalIndex) ?Index {
if (i == .none) return null;
return @enumFromInt(@intFromEnum(i));
}
};
fn format(
context: struct { path: Path, db: *const Db },
comptime format_string: []const u8,
unused_options: std.fmt.FormatOptions,
writer: anytype,
) !void {
const trailing = format_string.len == 1 and format_string[0] == '/';
_ = unused_options;
if (context.path.parent.unwrap()) |parent| {
try format(.{ .path = context.db.directory(parent), .db = context.db }, "", .{}, writer);
try writer.writeByte(std.fs.path.sep);
}
const basename = context.db.stringToSlice(context.path.basename);
if (basename.len > 0) {
try writer.writeAll(basename);
if (trailing) try writer.writeByte('/');
}
}
};
pub const Album = extern struct {
title: OptionalString,
artist: ArtistIndex,
year: i16,
track_count: i16,
disc_number: i16,
disc_count: i16,
/// If it is not one of these special values, it is a `String`.
pub const ArtistIndex = enum(u32) {
various = std.math.maxInt(u32) - 1,
none = std.math.maxInt(u32),
_,
pub fn init(i: OptionalString, db: *const Db, is_compilation: bool) ArtistIndex {
if (is_compilation)
return .various;
const name = db.optStringToSlice(i) orelse return .none;
if (std.ascii.eqlIgnoreCase(name, "various") or
std.ascii.eqlIgnoreCase(name, "various artists"))
{
return .various;
}
return @enumFromInt(@intFromEnum(i));
}
};
pub const Index = enum(u16) {
_,
pub fn toOptional(i: Index) OptionalIndex {
return @enumFromInt(@intFromEnum(i));
}
};
pub const OptionalIndex = enum(u16) {
none = std.math.maxInt(u16),
_,
};
};
pub const Duration = extern struct {
sample_count: u32,
sample_rate: u32,
pub fn fromPlayer(d: player.Duration) Duration {
return .{
.sample_count = d.sample_count,
.sample_rate = d.sample_rate,
};
}
};
pub const File = extern struct {
directory: Path.Index,
artist: OptionalString,
basename: String,
title: String,
album: Album.Index,
composer: OptionalString,
performer: OptionalString,
pub const Index = enum(u16) {
_,
};
};
pub fn deinit(db: *Db, gpa: Allocator) void {
db.files.deinit(gpa);
db.directories.deinit(gpa);
db.string_bytes.deinit(gpa);
db.string_table.deinit(gpa);
db.* = undefined;
}
pub fn getOrPutString(
db: *Db,
gpa: Allocator,
slice: []const u8,
) Allocator.Error!String {
try db.string_bytes.ensureUnusedCapacity(gpa, slice.len + 1);
db.string_bytes.appendSliceAssumeCapacity(slice);
db.string_bytes.appendAssumeCapacity(0);
return db.getOrPutTrailingString(gpa, slice.len);
}
pub fn getOrPutTrailingString(
db: *Db,
gpa: Allocator,
len: usize,
) Allocator.Error!String {
const string_bytes = &db.string_bytes;
const str_index: u32 = @intCast(string_bytes.items.len - len - 1);
const key: []const u8 = string_bytes.items[str_index..][0..len :0];
const gop = try db.string_table.getOrPutContextAdapted(gpa, key, std.hash_map.StringIndexAdapter{
.bytes = string_bytes,
}, std.hash_map.StringIndexContext{
.bytes = string_bytes,
});
if (gop.found_existing) {
string_bytes.shrinkRetainingCapacity(str_index);
return @enumFromInt(gop.key_ptr.*);
} else {
gop.key_ptr.* = str_index;
return @enumFromInt(str_index);
}
}
pub fn stringToSlice(db: *const Db, string: String) [:0]const u8 {
const slice = db.string_bytes.items[@intFromEnum(string)..];
const end = std.mem.indexOfScalar(u8, slice, 0).?;
return slice[0..end :0];
}
pub fn optStringToSlice(db: *const Db, optional_string: OptionalString) ?[:0]const u8 {
const string = optional_string.unwrap() orelse return null;
const slice = db.string_bytes.items[@intFromEnum(string)..];
const end = std.mem.indexOfScalar(u8, slice, 0).?;
return slice[0..end :0];
}
pub fn addDirectory(db: *Db, gpa: Allocator, path: Path) Allocator.Error!Path.Index {
try db.directories.append(gpa, path);
return @enumFromInt(db.directories.items.len - 1);
}
pub fn directory(db: *const Db, i: Path.Index) Path {
return db.directories.items[@intFromEnum(i)];
}
pub fn addFile(db: *Db, gpa: Allocator, new_file: File) Allocator.Error!File.Index {
try db.files.append(gpa, new_file);
return @enumFromInt(db.directories.items.len - 1);
}
pub fn fmtPath(db: *const Db, p: Path) std.fmt.Formatter(Path.format) {
return .{ .data = .{ .path = p, .db = db } };
}
};
const FileMetaData = struct {
/// Zero sample rate indicates absence of duration metadata.
duration: Db.Duration,
@@ -399,8 +177,13 @@ const FileMetaData = struct {
};
fn fileMetaData(gpa: Allocator, db: *Db, file: *player.File) !FileMetaData {
const estimated_duration = file.estimatedDuration();
var result: FileMetaData = .{
.duration = Db.Duration.fromPlayer(file.estimatedDuration()),
.duration = .{
.sample_count = estimated_duration.sample_count,
.sample_rate = estimated_duration.sample_rate,
},
.title = .none,
.artist = .none,
.composer = .none,