const std = @import("std"); const fs = std.fs; const mem = std.mem; const TrackInfo = @import("trackinfo.zig"); const TrackMap = std.StringHashMap(TrackInfo); fn parse_musicdef(allocator: mem.Allocator, tracks: *TrackMap, file: []const u8) !void { var lumps = mem.tokenizeSequence(u8, file, "Lump "); while (lumps.next()) |lump| { // Parse lump and add it to the tracklist const info = try TrackInfo.parseLump(allocator, lump); try tracks.put(try allocator.dupe(u8, info.id), info); } } const forbidden_chars = "<>:\"/\\|?*!"; fn slugify(allocator: mem.Allocator, name: ?[]const u8, fallback: []const u8) ![]const u8 { const notNullName = name orelse fallback; // Make copy of name var copiedName = try allocator.alloc(u8, notNullName.len); for (0..copiedName.len) |i| { if (std.mem.indexOfScalar(u8, forbidden_chars, notNullName[i])) |_| { copiedName[i] = '-'; } else { copiedName[i] = notNullName[i]; } } return copiedName; } pub fn main() !void { // Get allocator var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const allocator = gpa.allocator(); defer _ = gpa.deinit(); // Get args const args = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, args); if (args.len < 2) { std.debug.print("Usage: {s} \n", .{args[0]}); std.process.exit(1); } // Extract folder from argv const folder = args[1]; // Convert folder to absolute const dir = try fs.cwd().openDir(folder, .{ .iterate = true, }); // Prepare hashmap for storing song locations var trackLocations = std.StringHashMap([]const u8).init(allocator); defer trackLocations.deinit(); var trackInfos = TrackMap.init(allocator); defer trackInfos.deinit(); var arenaAllocator = std.heap.ArenaAllocator.init(allocator); defer arenaAllocator.deinit(); const keyAllocator = arenaAllocator.allocator(); // Iter through every file inside the directory (recursive) var iter = try dir.walk(allocator); defer iter.deinit(); while (try iter.next()) |entry| { // Skip non-files if (entry.kind != .file) { continue; } // Remove extension const extIndex = mem.indexOf(u8, entry.basename, "."); const filename = if (extIndex) |index| entry.basename[0..index] else entry.basename; // Check if it's a music definition file (and parse it if so) if (mem.eql(u8, filename, "MUSICDEF")) { const filedata = try dir.readFileAlloc(allocator, entry.path, 1048576); defer allocator.free(filedata); try parse_musicdef(keyAllocator, &trackInfos, filedata); } else { // Save file to hashmap of resolved files try trackLocations.put(try keyAllocator.dupe(u8, filename), try keyAllocator.dupe(u8, entry.path)); } } // Make target dirs const outputDir = try fs.cwd().makeOpenPath("target", .{}); const originalsDir = try outputDir.makeOpenPath("original", .{}); const othersDir = try outputDir.makeOpenPath("others", .{}); // Print detected songs var tracks = trackInfos.valueIterator(); while (tracks.next()) |trackInfo| { std.debug.print("- [{s}] {?s} ({?s}, {?s})\n", .{ trackInfo.id, trackInfo.title, trackInfo.author, trackInfo.source, }); // Extract all the qualifying IDs var ids = mem.splitScalar(u8, trackInfo.id, ','); while (ids.next()) |id| { const actualID = mem.trimLeft(u8, id, "\\"); // Map to track location var soundIDBuffer: [10]u8 = undefined; const soundID = try std.fmt.bufPrint(&soundIDBuffer, "O_{s}", .{actualID}); const location = trackLocations.get(soundID) orelse { std.debug.panic("{s} has no track assigned", .{soundID}); }; // Copy file over, dir depending on originality (I-I mean, I'm not trying to be mean) const sourceDirName = try slugify(allocator, trackInfo.source, "unknown"); defer allocator.free(sourceDirName); const targetDir = if (trackInfo.is_original) originalsDir else try othersDir.makeOpenPath(sourceDirName, .{}); const author = try slugify(allocator, trackInfo.author, "unknown"); defer allocator.free(author); const title = try slugify(allocator, trackInfo.title, trackInfo.id); defer allocator.free(title); const filename = try std.fmt.allocPrint(allocator, "{s} - {s}.ogg", .{ author, title }); defer allocator.free(filename); try dir.copyFile(location, targetDir, filename, .{}); } } }