@@ -100,15 +100,37 @@ pub fn main() anyerror!noreturn {
try loadDb(gpa, &db, install_directory, config.db_path);
{
var it = config.music_directory.handle.iterate();
try scanDir(
&db,
gpa,
var scan_arena = std.heap.ArenaAllocator.init(gpa);
defer scan_arena.deinit();
var scan: Scan = .{
.gpa = gpa,
.arena = scan_arena.allocator(),
.db = &db,
.mutex = .{},
.thread_pool = &thread_pool,
.wait_group = .{},
.new_count = 0,
};
defer {
scan.wait_group.wait();
std.log.info("finished scanning {d} new files", .{scan.new_count});
}
scan.mutex.lock();
defer scan.mutex.unlock();
var rcd: Scan.RefCountedDir = .{
.dir = config.music_directory.handle,
.count = 1,
.it = config.music_directory.handle.iterate(),
};
try scan.dir(
try db.addDirectory(gpa, .{
.parent = .none,
.basename = .empty,
}),
&it,
&rcd,
);
}
@@ -451,109 +473,168 @@ pub const Server = struct {
}
};
fn scanDir(db: *Db, gpa: Allocator, db_dir: Db.Path.Index, it: *std.fs.Dir.Iterator) anyerror!void {
while (try it.next()) |entry| switch (entry.kind) {
.directory => {
var sub_dir = it.dir.openDir(entry.name, .{ .iterate = true }) catch |err| {
std.log.err("while scanning, unable to open directory '{s}': {s}", .{
entry.name, @errorName(err),
});
continue;
};
defer sub_dir.close();
const basename = try db.getOrPutString(gpa, entry.name);
var sub_it = sub_dir.iterateAssumeFirstIteration();
try scanDir(
db,
gpa,
try db.addDirectory(gpa, .{
.parent = db_dir.toOptional(),
.basename = basename,
}),
&sub_it,
);
},
.sym_link => {
var sub_dir = it.dir.openDir(entry.name, .{ .iterate = true }) catch |err| {
std.log.err("while scanning, unable to open symlink '{s}' as a directory: {s}", .{
entry.name, @errorName(err),
});
continue;
};
defer sub_dir.close();
const basename = try db.getOrPutString(gpa, entry.name);
var sub_it = sub_dir.iterateAssumeFirstIteration();
try scanDir(
db,
gpa,
try db.addDirectory(gpa, .{
.parent = db_dir.toOptional(),
.basename = basename,
}),
&sub_it,
);
},
.file => {
const basename = try db.getOrPutString(gpa, entry.name);
const file_gop = try db.files.getOrPut(gpa, .{
.directory = db_dir,
.basename = basename,
.title = undefined,
.artist = undefined,
.album = undefined,
.track_number = undefined,
.composer = undefined,
.performer = undefined,
.duration = undefined,
});
if (file_gop.found_existing) continue;
db.files.lockPointers();
defer db.files.unlockPointers();
const Scan = struct {
gpa: Allocator,
arena: Allocator,
db: *Db,
mutex: std.Thread.Mutex,
thread_pool: *std.Thread.Pool,
wait_group: std.Thread.WaitGroup,
new_count: u32,
const basename_z = db.stringToSlice(basename);
std.log.debug("opening new file '{s}'", .{basename_z});
const file = player.File.open(it.dir, basename_z, basename_z) catch |err| {
std.log.warn("unable to open '{s}': {s}", .{ entry.name, @errorName(err) });
continue;
};
defer file.close();
const RefCountedDir = struct {
dir: std.fs.Dir,
count: u32,
it: std.fs.Dir.Iterator,
const metadata = try fileMetaData(gpa, db, file);
fn ref(rcd: *RefCountedDir) void {
_ = @atomicRmw(u32, &rcd.count, .Add, 1, .monotonic);
}
const album: Db.Album.Index = a: {
const gop = try db.albums.getOrPut(gpa, .{
.title = metadata.album,
.year = metadata.year,
.track_count = metadata.track_count,
.disc_number = metadata.disc_number,
.disc_count = metadata.disc_count,
.artist = Db.Album.ArtistIndex.init(metadata.album_artist, db, metadata.compilation),
});
break :a @enumFromInt(gop.index);
};
file_gop.key_ptr.* = .{
.directory = db_dir,
.basename = basename,
.title = metadata.title.unwrap() orelse basename,
.artist = metadata.artist,
.album = album,
.track_number = metadata.track_number,
.composer = metadata.composer,
.performer = metadata.performer,
.duration = metadata.duration,
};
},
else => {
std.log.info("ignoring {s} '{/}{s}'", .{
@tagName(entry.kind),
db.fmtPath(db.directory(db_dir)),
entry.name,
});
continue;
},
fn deref(rcd: *RefCountedDir) void {
switch (@atomicRmw(u32, &rcd.count, .Sub, 1, .monotonic)) {
0 => unreachable, // too many derefs
1 => rcd.dir.close(),
else => {},
}
}
};
}
/// Assumes DB lock is held.
fn dir(scan: *Scan, db_dir: Db.Path.Index, rcd_parent: *RefCountedDir) anyerror!void {
const db = scan.db;
const gpa = scan.gpa;
const arena = scan.arena;
while (try rcd_parent.it.next()) |entry| switch (entry.kind) {
.directory, .sym_link => {
const rcd = try arena.create(RefCountedDir);
var sub_dir = rcd_parent.it.dir.openDir(entry.name, .{ .iterate = true }) catch |err| {
std.log.err("while scanning, unable to open directory '{s}': {s}", .{
entry.name, @errorName(err),
});
continue;
};
rcd.* = .{
.dir = sub_dir,
.count = 1,
.it = sub_dir.iterateAssumeFirstIteration(),
};
defer rcd.deref();
const basename = try db.getOrPutString(gpa, entry.name);
const db_sub_dir = try db.addDirectory(gpa, .{
.parent = db_dir.toOptional(),
.basename = basename,
});
try dir(scan, db_sub_dir, rcd);
},
.file => {
const basename = try db.getOrPutString(gpa, entry.name);
const gop = try db.files.getOrPut(gpa, .{
.directory = db_dir,
.basename = basename,
.title = undefined,
.artist = undefined,
.album = undefined,
.track_number = undefined,
.composer = undefined,
.performer = undefined,
.duration = undefined,
.true_peak = undefined,
.integrated_loudness = undefined,
});
if (gop.found_existing) continue;
const basename_z = try arena.dupeZ(u8, entry.name);
scan.new_count += 1;
scan.mutex.unlock();
defer scan.mutex.lock();
rcd_parent.ref();
const file_index: Db.File.Index = @enumFromInt(gop.index);
scan.thread_pool.spawnWg(&scan.wait_group, analyzeFile, .{
scan, rcd_parent, basename_z, file_index,
});
},
else => {
std.log.info("ignoring {s} '{/}{s}'", .{
@tagName(entry.kind),
db.fmtPath(db.directory(db_dir)),
entry.name,
});
continue;
},
};
}
/// This function is executed in a thread pool.
fn analyzeFile(scan: *Scan, rcd: *RefCountedDir, basename_z: [:0]const u8, file_index: Db.File.Index) void {
const gpa = scan.gpa;
const db = scan.db;
defer rcd.deref();
// DB lock is not held here.
std.log.debug("opening new file '{s}'", .{basename_z});
const file = player.File.open(rcd.dir, basename_z, basename_z) catch |err| {
std.log.warn("unable to open '{s}': {s}", .{ basename_z, @errorName(err) });
return;
};
defer file.close();
const analysis = file.analyze(1920) catch |err| {
std.log.err("unable to analyze '{s}': {s}", .{ basename_z, @errorName(err) });
return;
};
scan.mutex.lock();
defer scan.mutex.unlock();
const metadata = fileMetaData(gpa, db, file) catch |err| {
std.log.err("unable to extract metadata from '{s}': {s}", .{ basename_z, @errorName(err) });
return;
};
const album: Db.Album.Index = a: {
const gop = db.albums.getOrPut(gpa, .{
.title = metadata.album,
.year = metadata.year,
.track_count = metadata.track_count,
.disc_number = metadata.disc_number,
.disc_count = metadata.disc_count,
.artist = Db.Album.ArtistIndex.init(metadata.album_artist, db, metadata.compilation),
}) catch |err| {
std.log.err("unable to add '{s}' to database: {s}", .{ basename_z, @errorName(err) });
return;
};
break :a @enumFromInt(gop.index);
};
const file_ptr = db.file(file_index);
const basename = file_ptr.basename;
file_ptr.* = .{
.directory = file_ptr.directory,
.basename = basename,
.title = metadata.title.unwrap() orelse basename,
.artist = metadata.artist,
.album = album,
.track_number = metadata.track_number,
.composer = metadata.composer,
.performer = metadata.performer,
.duration = .{
.sample_count = analysis.duration.sample_count,
.sample_rate = analysis.duration.sample_rate,
},
.true_peak = @floatCast(analysis.true_peak),
.integrated_loudness = @floatCast(analysis.integrated_loudness),
};
}
};
const FileMetaData = struct {
/// Zero sample rate indicates absence of duration metadata.
@@ -578,7 +659,7 @@ const FileMetaData = struct {
compilation: bool,
};
fn fileMetaData(gpa: Allocator, db: *Db, file: *player.File) !FileMetaData {
fn fileMetaData(gpa: Allocator, db: *Db, file: *player.File) Allocator.Error!FileMetaData {
const estimated_duration = file.estimatedDuration();
var result: FileMetaData = .{