diff --git a/src/app/jobs/process_rule.zig b/src/app/jobs/process_rule.zig index f211dbe..217826a 100644 --- a/src/app/jobs/process_rule.zig +++ b/src/app/jobs/process_rule.zig @@ -35,12 +35,20 @@ pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig defer rules.deinit(); const file_content = try file_read.readToEndAlloc(allocator, 16_000_000); - const content: Data.Rules = try std.json.parseFromSliceLeaky(Data.Rules, allocator, file_content, .{}); - try rules.appendSlice(content.rules); - try rules.append(rule); file_read.close(); const file_write: std.fs.File = try std.fs.cwd().openFile("rules.json", .{ .mode = .write_only }); + if (file_content.len == 0) { + const out_rules = Data.Rules{ .rules = &[_]Data.Rule{rule} }; + const out = try std.json.stringifyAlloc(allocator, out_rules, .{}); + try file_write.writeAll(out); + file_write.close(); + return; + } + const content: Data.Rules = try std.json.parseFromSliceLeaky(Data.Rules, allocator, file_content, .{}); + try rules.appendSlice(content.rules); + try rules.append(rule); + const out_rules = Data.Rules{ .rules = rules.items }; const out = try std.json.stringifyAlloc(allocator, out_rules, .{}); diff --git a/src/app/jobs/process_scrobbles.zig b/src/app/jobs/process_scrobbles.zig index 4262fe5..87871c7 100644 --- a/src/app/jobs/process_scrobbles.zig +++ b/src/app/jobs/process_scrobbles.zig @@ -1,9 +1,7 @@ const std = @import("std"); const jetzig = @import("jetzig"); const jetquery = @import("jetzig").jetquery; -const Scrobble = @import("../../types.zig").LastFMScrobble; const lastfm = @import("../../types.zig").LastFM; -//const Rules = @import("../../types.zig").Rules; const Data = @import("../../types.zig"); const rules = @import("../../apply_rule.zig"); @@ -18,26 +16,50 @@ const rules = @import("../../apply_rule.zig"); // - environment: Enum of `{ production, development }`. pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig.jobs.JobEnv) !void { //_ = env; - _ = allocator; if (params.getT(.array, "scrobbles")) |scrobbles| { for (scrobbles.items()) |item| { - //const fixed_date: u32 = @as(u32, item.getT(.integer, "date").?); - const scrobble: Scrobble = .{ .track = item.getT(.string, "track").?, .artist = item.getT(.string, "artist").?, .album = item.getT(.string, "album") orelse "", .date = @as(u64, @bitCast(@as(i64, @truncate(item.getT(.integer, "date").? * 1000)))) }; + //var buffer: [256**4]u8 = undefined; + //var fba = std.heap.FixedBufferAllocator.init(&buffer); + //const alloc = fba.allocator(); - // Make hashes - //const album_hash = @as(i32, @bitCast(std.hash.Fnv1a_32.hash(scrobble.album))); - //const artist_hash = @as(i32, @bitCast(std.hash.Fnv1a_32.hash(scrobble.artist))); - //const song_hash = @as(i32, @bitCast(std.hash.Fnv1a_32.hash(scrobble.track))); + var alssu8 = std.ArrayList([]const u8).init(allocator); + defer alssu8.deinit(); - // Create a buffer to hold the metadata to hash. Numbers based on the title of a - // particularly long Sufjan Stevens song title, and we're gonna pray the metadata - // does not exceed three times it's length. - var buffer = [_]u8{undefined} ** (288 * 3); - const artist_id = @as(i32, @bitCast(std.hash.Fnv1a_32.hash(scrobble.artist))); - const album_prehash = try std.fmt.bufPrint(&buffer, "{s}{s}", .{ scrobble.artist, scrobble.album }); - const album_id = @as(i32, @bitCast(std.hash.Fnv1a_32.hash(album_prehash))); - const song_prehash = try std.fmt.bufPrint(&buffer, "{s}{s}{s}", .{ scrobble.artist, scrobble.album, scrobble.track }); - const song_id = @as(i32, @bitCast(std.hash.Fnv1a_32.hash(song_prehash))); + for (item.getT(.array, "artists_track").?.items()) |artist| { + try alssu8.append(try artist.coerce([]const u8)); + } + + const track_artists = try alssu8.toOwnedSlice(); + + for (item.getT(.array, "artists_album").?.items()) |artist| { + try alssu8.append(try artist.coerce([]const u8)); + } + + const album_artists = try alssu8.toOwnedSlice(); + + const scrobble: Data.Scrobble = .{ + .track = item.getT(.string, "track").?, + .artists_track = track_artists, + .album = item.getT(.string, "album") orelse "", + .artists_album = album_artists, + .date = @as(u64, @bitCast(@as(i64, @truncate(item.getT(.integer, "date").? * 1000)))), + }; + + var id_prehash = std.ArrayList(u8).init(allocator); + defer id_prehash.deinit(); + + var alartist = std.ArrayList(struct { name: []const u8, id: i32 }).init(allocator); + defer alartist.deinit(); + + for (scrobble.artists_track) |artist| { + //try id_prehash.appendSlice(artist); + try alartist.append(.{ .name = artist, .id = @as(i32, @bitCast(std.hash.Fnv1a_32.hash(artist))) }); + } + //const artist_id = @as(i32, @bitCast(std.hash.Fnv1a_32.hash(id_prehash.items))); + try id_prehash.appendSlice(scrobble.album); + const album_id = @as(i32, @bitCast(std.hash.Fnv1a_32.hash(id_prehash.items))); + try id_prehash.appendSlice(scrobble.track); + const song_id = @as(i32, @bitCast(std.hash.Fnv1a_32.hash(id_prehash.items))); // Make IDs // Song: Song hash XOR artist hash XOR album hash @@ -65,64 +87,121 @@ pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig //var album_id: i32 = @as(i32, @bitCast(std.hash.Fnv1a_32.hash(formed))); //const song_id = (song_hash ^ artist_hash ^ album_hash); + var albumsong = try jetzig.database.Query(.Albumsong) + .findBy(.{ + .album_id = album_id, + .song_id = song_id, + }) + .select(.{.id}) + .execute(env.repo); - var albumsong = try jetzig.database.Query(.Albumsong).findBy(.{ .album_id = album_id, .song_id = song_id }).select(.{.id}).execute(env.repo); - var ins_album = try jetzig.database.Query(.Album).find(album_id).select(.{.id}).execute(env.repo); + var ins_album = try jetzig.database.Query(.Album) + .find(album_id) + .select(.{.id}) + .execute(env.repo); - var ins_artist = try jetzig.database.Query(.Artist).find(artist_id).select(.{.id}).execute(env.repo); - if (ins_artist == null) ins_artist = try jetzig.database.Query(.Artist).insert(.{ .id = artist_id, .name = scrobble.artist, .disambiguation = null }).returning(.{.id}).execute(env.repo); + for (alartist.items) |artist| { + var ins_artist = try jetzig.database.Query(.Artist) + .find(artist.id) + .select(.{.id}) + .execute(env.repo); - if (albumsong == null) { - var ins_song = try jetzig.database.Query(.Song).find(song_id).select(.{.id}).execute(env.repo); - if (ins_song == null) ins_song = try jetzig.database.Query(.Song).insert(.{ .id = song_id, .name = scrobble.track, .length = null, .hidden = false }).returning(.{.id}).execute(env.repo); + if (ins_artist == null) ins_artist = try jetzig.database.Query(.Artist) + .insert(.{ + .id = artist.id, + .name = artist.name, + .disambiguation = null, + }) + .returning(.{.id}) + .execute(env.repo); - if (ins_album == null) { - ins_album = try jetzig.database.Query(.Album).insert(.{ .id = album_id, .name = scrobble.album, .length = null }).returning(.{.id}).execute(env.repo); - // I think there's still technically a bug here when you have a different artist but I'm not sure - try jetzig.database.Query(.Artistalbum).insert(.{ .artist_id = ins_artist.?.id, .album_id = ins_album.?.id }).execute(env.repo); + if (albumsong == null) { + var ins_song = try jetzig.database.Query(.Song) + .find(song_id) + .select(.{.id}) + .execute(env.repo); + + if (ins_song == null) ins_song = try jetzig.database.Query(.Song) + .insert(.{ + .id = song_id, + .name = scrobble.track, + .length = null, + .hidden = false, + }) + .returning(.{.id}) + .execute(env.repo); + + if (ins_album == null) { + ins_album = try jetzig.database.Query(.Album) + .insert(.{ + .id = album_id, + .name = scrobble.album, + .length = null, + }) + .returning(.{.id}) + .execute(env.repo); + // I think there's still technically a bug here when you have a different artist but I'm not sure + try jetzig.database.Query(.Artistalbum) + .insert(.{ + .artist_id = ins_artist.?.id, + .album_id = ins_album.?.id, + }) + .execute(env.repo); + } + + albumsong = try jetzig.database.Query(.Albumsong) + .insert(.{ + .song_id = ins_song.?.id, + .album_id = ins_album.?.id, + }) + .returning(.{.id}) + .execute(env.repo); + + try jetzig.database.Query(.Albumsongsartist) + .insert(.{ + .albumsong_id = albumsong.?.id, + .artist_id = ins_artist.?.id, + }) + .execute(env.repo); + } else { + const ins_albumsongartist = try jetzig.database.Query(.Albumsongsartist) + .findBy(.{ + .albumsong_id = albumsong.?.id, + .artist_id = ins_artist.?.id, + }) + .select(.{.id}) + .execute(env.repo); + + if (ins_albumsongartist == null) try jetzig.database.Query(.Albumsongsartist) + .insert(.{ + .albumsong_id = albumsong.?.id, + .artist_id = ins_artist.?.id, + }) + .execute(env.repo); + + const ins_artistalbum = try jetzig.database.Query(.Artistalbum) + .findBy(.{ + .album_id = ins_album.?.id, + .artist_id = ins_artist.?.id, + }) + .select(.{.id}) + .execute(env.repo); + + if (ins_artistalbum == null) try jetzig.database.Query(.Artistalbum) + .insert(.{ + .album_id = ins_album.?.id, + .artist_id = ins_artist.?.id, + }) + .execute(env.repo); } - - albumsong = try jetzig.database.Query(.Albumsong).insert(.{ .song_id = ins_song.?.id, .album_id = ins_album.?.id }).returning(.{.id}).execute(env.repo); - - try jetzig.database.Query(.Albumsongsartist).insert(.{ .albumsong_id = albumsong.?.id, .artist_id = ins_artist.?.id }).execute(env.repo); - } else { - const ins_albumsongartist = try jetzig.database.Query(.Albumsongsartist).findBy(.{ .albumsong_id = albumsong.?.id, .artist_id = ins_artist.?.id }).select(.{.id}).execute(env.repo); - if (ins_albumsongartist == null) try jetzig.database.Query(.Albumsongsartist).insert(.{ .albumsong_id = albumsong.?.id, .artist_id = ins_artist.?.id }).execute(env.repo); - - const ins_artistalbum = try jetzig.database.Query(.Artistalbum).findBy(.{ .album_id = ins_album.?.id, .artist_id = ins_artist.?.id }).select(.{.id}).execute(env.repo); - if (ins_artistalbum == null) try jetzig.database.Query(.Artistalbum).insert(.{ .album_id = ins_album.?.id, .artist_id = ins_artist.?.id }).execute(env.repo); } - //if (ins_artist_id == null) { - // ins_artist_id = try jetzig.database.Query(.Artist).insert(.{ .id = artist_id, .name = scrobble.artist, .disambiguation = null }).returning(.{.id}).execute(env.repo); - // try jetzig.database.Query(.Albumsongartist).insert(.{ .albumsong_id = albumsong_id, .artist_id = ins_artist_id }); - // try jetzig.database.Query(.Artistalbum).insert(.{ .artist_id = ins_artist_id, .album_id = ins_album_id }); - //} - - try jetzig.database.Query(.Scrobble).insert(.{ .albumsong = albumsong.?.id, .datetime = scrobble.date }).execute(env.repo); + try jetzig.database.Query(.Scrobble) + .insert(.{ + .albumsong = albumsong.?.id, + .datetime = scrobble.date, + }) + .execute(env.repo); } } - - // I would like to replicate this kind of functionality for several kinds of queries - // This one gives me all albums by Dream Theater (it also returns Dream Theater for - // each entry, but removing artists.name from the SELECT would remove that) - // - // SELECT - // artists.name, albums.name - // FROM - // "Albumartists" - // INNER JOIN artists - // ON "Albumartists".artist_id = artists.id - // INNER JOIN albums - // ON "Albumartists".album_id = albums.id - // WHERE artists.name = 'Dream Theater'; - - //const query = jetzig.database.Query(.Artist).include(.artistalbums, .{}); - //const results = try env.repo.all(query); - //defer env.repo.free(results); - //for (results) |result| { - // for (result.artistalbums) |artistalbum| { - // std.log.debug("{s}: {any}", .{ result.name, artistalbum.album_id }); - // } - //} } diff --git a/src/app/views/partials/_table.zmpl b/src/app/views/partials/_table.zmpl index 8ea394d..83c1cd4 100644 --- a/src/app/views/partials/_table.zmpl +++ b/src/app/views/partials/_table.zmpl @@ -10,7 +10,11 @@ @for (table_data) |value| { {{value.track}} - {{value.artist}} + + @for (value.get("artists").?) |artist| { + {{artist}} + } + {{value.album}} {{value.date}} diff --git a/src/app/views/rules/index.zmpl b/src/app/views/rules/index.zmpl index 038ce71..f97ef35 100644 --- a/src/app/views/rules/index.zmpl +++ b/src/app/views/rules/index.zmpl @@ -52,7 +52,8 @@ then diff --git a/src/app/views/upload.zig b/src/app/views/upload.zig index 032da9d..98f0009 100644 --- a/src/app/views/upload.zig +++ b/src/app/views/upload.zig @@ -1,7 +1,6 @@ const std = @import("std"); const jetzig = @import("jetzig"); const jetquery = @import("jetzig").jetquery; -const ScrobbleTypes = @import("../../types.zig"); const zeit = @import("zeit"); const rules = @import("../../apply_rule.zig"); const Data = @import("../../types.zig"); @@ -33,58 +32,70 @@ pub fn post(request: *jetzig.Request) !jetzig.View { var skipped_tracks: u64 = 0; var limited_tracks: u64 = 0; - const rule_file = try std.fs.cwd().openFile("rules.json", .{ .mode = .read_only }); + const rule_file = try (std.fs.cwd().openFile("rules.json", .{ .mode = .read_only }) catch |err| switch (err) { + error.FileNotFound => std.fs.cwd().createFile("rules.json", .{ .read = true }), + else => err, + }); + defer rule_file.close(); const file_content = try rule_file.readToEndAlloc(request.allocator, 16_000_000); - const rule_list = try std.json.parseFromSliceLeaky(Data.Rules, request.allocator, file_content, .{}); + const rule_list = std.json.parseFromSliceLeaky(Data.Rules, request.allocator, file_content, .{}) catch null; - // The only difference between a LastFM scrobble and a Spotify scrobble is the format. - // I've made a branches for each, because doing it all in one made the readability terrible, - // and formatting the date in particular was challenging. I could probably pull out the - // actual appending at some point, since that's the same process for each, but I'm not - // sure how to do that yet. switch (source) { 0 => { - const content: ScrobbleTypes.LastFM = try std.json.parseFromSliceLeaky(ScrobbleTypes.LastFM, request.allocator, file.content, .{}); + const content: Data.LastFM = try std.json.parseFromSliceLeaky(Data.LastFM, request.allocator, file.content, .{}); const before_limiting_date = if (before_limiter and params.get("b") != null) (try zeit.instant(.{ .source = .{ .iso8601 = params.get("b").?.string.value } })).unixTimestamp() * 1000 else 0; const after_limiting_date = if (after_limiter and params.get("a") != null) (try zeit.instant(.{ .source = .{ .iso8601 = params.get("a").?.string.value } })).unixTimestamp() * 1000 else 9_223_372_036_854_775_807; appends: for (content.scrobbles) |scrobble| { // We can short-circuit on the limiter bools if ((before_limiter or after_limiter) and (scrobble.date > before_limiting_date or scrobble.date < after_limiting_date)) continue :appends; - var value = try scrobbles_data.append(.object); - const formatted_scrobble = rules.applyScrobbleRule(scrobble, rule_list); + const formatted_scrobble = if (rule_list) |rl| + rules.applyScrobbleRule(request.allocator, scrobble, rl) + else + Data.Scrobble{ + .album = scrobble.album, + .artists_album = &[_][]const u8{scrobble.artist}, + .track = scrobble.track, + .artists_track = &[_][]const u8{scrobble.artist}, + .date = scrobble.date, + }; - // This is so unnecessary, probably useful once I start doing Spotify integration though - inline for (std.meta.fields(ScrobbleTypes.LastFMScrobble)) |f| { - try value.put(f.name, @as(f.type, @field(formatted_scrobble, f.name))); + var scrobble_view = try scrobbles_view.append(.object); + var artists = try scrobble_view.put("artists", .array); + + try scrobble_view.put("track", formatted_scrobble.track); + try scrobble_view.put("album", formatted_scrobble.album); + for (formatted_scrobble.artists_track) |artist| { + try artists.append(artist); } - // Note sure why this works for ZMPL, but not for jobs. - try scrobbles_view.append(formatted_scrobble); + try scrobble_view.put("date", formatted_scrobble.date); + + var scrobble_data = try scrobbles_data.append(.object); + var artists_album = try scrobble_data.put("artists_album", .array); + var artists_track = try scrobble_data.put("artists_track", .array); + + try scrobble_data.put("track", formatted_scrobble.track); + try scrobble_data.put("album", formatted_scrobble.album); + for (formatted_scrobble.artists_album) |artist| { + try artists_album.append(artist); + } + + for (formatted_scrobble.artists_track) |artist| { + try artists_track.append(artist); + } + try scrobble_data.put("date", formatted_scrobble.date); } }, 1 => { - const content: []ScrobbleTypes.SpotifyScrobble = try std.json.parseFromSliceLeaky([]ScrobbleTypes.SpotifyScrobble, request.allocator, file.content, .{}); + const content: []Data.SpotifyScrobble = try std.json.parseFromSliceLeaky([]Data.SpotifyScrobble, request.allocator, file.content, .{ .ignore_unknown_fields = true }); const before_limiting_date: zeit.Time = if (before_limiter) (try zeit.Time.fromISO8601(params.get("b").?.string.value)) else (try zeit.instant(.{})).time(); const after_limiting_date: zeit.Time = if (after_limiter) (try zeit.Time.fromISO8601(params.get("a").?.string.value)) else (try zeit.instant(.{ .source = .{ .unix_nano = 0 } })).time(); appends: for (content) |scrobble| { - // A LastFM Scrobble occurs when half a song has been played - // or the song plays for 4 minutes, whichever happens first. - // Spotify considers a song played if it plays for 30 seconds. - // Ideally, I would go with the LastFM convention, but Spotify - // history data only gives us so much information. I'm okay - // with the 30 second convention, but eventually I would prefer - // to get the song length from MusicBrainz and check if it meets - // the requirement. Until then, if it goes 30 seconds, or the - // reason_end field reads "trackdone", then it counts as a Scrobble. - // May consider giving user control to the minimum millisecond requirement. if (scrobble.ms_played < 30_000 and (scrobble.reason_end == null or !std.mem.eql(u8, scrobble.reason_end.?, "trackdone"))) { skipped_tracks += 1; continue :appends; } - // In the case where the artist is null, but there's other metadata, I could - // probably let the user edit it in themselves, although I'm not sure if that - // situation happens. if (scrobble.master_metadata_album_artist_name == null or scrobble.master_metadata_track_name == null) { skipped_tracks += 1; continue :appends; @@ -96,19 +107,42 @@ pub fn post(request: *jetzig.Request) !jetzig.View { continue :appends; } - // Turn SpotifyScrobble into a LastFM scrobble - const pre_formatted_scrobble: ScrobbleTypes.LastFMScrobble = .{ .track = scrobble.master_metadata_track_name.?, .album = scrobble.master_metadata_album_album_name.?, .artist = scrobble.master_metadata_album_artist_name.?, .date = (try zeit.instant(.{ .source = .{ .iso8601 = scrobble.ts } })).unixTimestamp() * 1000 }; + const pre_formatted_scrobble: Data.ImportedScrobble = .{ .track = scrobble.master_metadata_track_name.?, .album = scrobble.master_metadata_album_album_name.?, .artist = scrobble.master_metadata_album_artist_name.?, .date = (try zeit.instant(.{ .source = .{ .iso8601 = scrobble.ts } })).unixTimestamp() * 1000 }; + const formatted_scrobble = if (rule_list) |rl| + rules.applyScrobbleRule(request.allocator, pre_formatted_scrobble, rl) + else + Data.Scrobble{ + .album = pre_formatted_scrobble.album, + .artists_album = &[_][]const u8{pre_formatted_scrobble.artist}, + .track = pre_formatted_scrobble.track, + .artists_track = &[_][]const u8{pre_formatted_scrobble.artist}, + .date = pre_formatted_scrobble.date, + }; - const formatted_scrobble = rules.applyScrobbleRule(pre_formatted_scrobble, rule_list); + var scrobble_view = try scrobbles_view.append(.object); + var artists = try scrobble_view.put("artists", .array); - var value = try scrobbles_data.append(.object); - - // This is so unnecessary, probably useful once I start doing Spotify integration though - inline for (std.meta.fields(ScrobbleTypes.LastFMScrobble)) |f| { - try value.put(f.name, @as(f.type, @field(formatted_scrobble, f.name))); + try scrobble_view.put("track", formatted_scrobble.track); + try scrobble_view.put("album", formatted_scrobble.album); + for (formatted_scrobble.artists_track) |artist| { + try artists.append(artist); } - // Note sure why this works for ZMPL, but not for jobs. - try scrobbles_view.append(formatted_scrobble); + try scrobble_view.put("date", formatted_scrobble.date); + + var scrobble_data = try scrobbles_data.append(.object); + var artists_album = try scrobble_data.put("artists_album", .array); + var artists_track = try scrobble_data.put("artists_track", .array); + + try scrobble_data.put("track", formatted_scrobble.track); + try scrobble_data.put("album", formatted_scrobble.album); + for (formatted_scrobble.artists_album) |artist| { + try artists_album.append(artist); + } + + for (formatted_scrobble.artists_track) |artist| { + try artists_track.append(artist); + } + try scrobble_data.put("date", formatted_scrobble.date); } }, else => unreachable, diff --git a/src/apply_rule.zig b/src/apply_rule.zig index db6eb42..dd9f8bb 100644 --- a/src/apply_rule.zig +++ b/src/apply_rule.zig @@ -1,18 +1,32 @@ const std = @import("std"); -const Scrobble = @import("./types.zig").LastFMScrobble; const Rules = @import("./types.zig").Rules; +const Data = @import("./types.zig"); // Wrapper for containsAtLeast to make the switch below to work -fn containsAtLeastOne(haystack: []const u8, needle: []const u8) bool { +fn containsWrapper(haystack: []const u8, needle: []const u8) bool { return std.mem.containsAtLeast(u8, haystack, 1, needle); } -fn eqlDecomped(haystack: []const u8, needle: []const u8) bool { +fn eqlWrapper(haystack: []const u8, needle: []const u8) bool { return std.mem.eql(u8, haystack, needle); } -pub fn applyScrobbleRule(scrobble: Scrobble, rules: Rules) Scrobble { - var output_scrobble: Scrobble = scrobble; +pub fn applyScrobbleRule(allocator: std.mem.Allocator, scrobble: Data.ImportedScrobble, rules: Rules) Data.Scrobble { + var output_scrobble = Data.Scrobble{ + .track = scrobble.track, + .artists_track = &[_][]const u8{scrobble.artist}, + .album = scrobble.album, + .artists_album = &[_][]const u8{scrobble.artist}, + .date = scrobble.date, + }; + + //var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + //const gpalloc = gpa.allocator(); + + //var arena = std.heap.ArenaAllocator.init(gpalloc); + //defer arena.deinit(); + //const allocator = arena.allocator(); + for (rules.rules) |rule| { var match_found: bool = switch (rule.cond_req) { .any => false, @@ -20,8 +34,8 @@ pub fn applyScrobbleRule(scrobble: Scrobble, rules: Rules) Scrobble { }; for (rule.conditionals) |cond| { const match_fn: *const fn ([]const u8, []const u8) bool = switch (cond.match_cond) { - .is => eqlDecomped, - .contains => containsAtLeastOne, + .is => eqlWrapper, + .contains => containsWrapper, }; switch (rule.cond_req) { .any => switch (cond.match_on) { @@ -35,9 +49,22 @@ pub fn applyScrobbleRule(scrobble: Scrobble, rules: Rules) Scrobble { if (match_found) { for (rule.actions) |act| { switch (act.action) { - .add => {}, + .add => { + var al = std.ArrayList([]const u8).init(allocator); + switch (act.action_on) { + .album, .track => unreachable, + inline else => |on| { + // I have decided an error won't happen :) + al.appendSlice(@field(output_scrobble, @tagName(on))) catch unreachable; + al.append(act.action_txt) catch unreachable; + const artists = al.toOwnedSlice() catch unreachable; + @field(output_scrobble, @tagName(on)) = artists; + }, + } + }, .replace => switch (act.action_on) { - inline else => |on| @field(output_scrobble, @tagName(on)) = act.action_txt, + inline .album, .track => |on| @field(output_scrobble, @tagName(on)) = act.action_txt, + inline .artists_album, .artists_track => |on| @field(output_scrobble, @tagName(on)) = &[_][]const u8{act.action_txt}, }, } } diff --git a/src/types.zig b/src/types.zig index 0e385a5..f0268ed 100644 --- a/src/types.zig +++ b/src/types.zig @@ -1,51 +1,59 @@ -pub const LastFMScrobble = struct { +pub const ImportedScrobble = struct { track: []const u8, artist: []const u8, album: []const u8 = "", date: i128, }; +pub const Scrobble = struct { + track: []const u8, + artists_track: []const []const u8, + album: []const u8 = "", + artists_album: []const []const u8, + date: i128, +}; + // From lastfmstats.com -pub const LastFM = struct { username: []const u8, scrobbles: []LastFMScrobble }; +pub const LastFM = struct { username: []const u8, scrobbles: []ImportedScrobble }; // I derived whether or not these values were optional from searching // the respective fields for null in Vim, so there may be some fields // that can be optional that I haven't run into yet pub const SpotifyScrobble = struct { ts: []const u8, - username: []const u8, - platform: []const u8, + //username: []const u8, + //platform: []const u8, ms_played: u64, - conn_country: []const u8, - ip_addr_decrypted: ?[]const u8, - user_agent_decrypted: ?[]const u8, + //conn_country: []const u8, + //ip_addr_decrypted: ?[]const u8, + //user_agent_decrypted: ?[]const u8, master_metadata_track_name: ?[]const u8, master_metadata_album_artist_name: ?[]const u8, master_metadata_album_album_name: ?[]const u8, - spotify_track_uri: ?[]const u8, - episode_name: ?[]const u8, - episode_show_name: ?[]const u8, - spotify_episode_uri: ?[]const u8, + //spotify_track_uri: ?[]const u8, + //episode_name: ?[]const u8, + //episode_show_name: ?[]const u8, + //spotify_episode_uri: ?[]const u8, reason_start: []const u8, reason_end: ?[]const u8, - shuffle: bool, + //shuffle: bool, skipped: ?bool, - offline: bool, + //offline: bool, offline_timestamp: u64, - incognito_mode: ?bool, + //incognito_mode: ?bool, }; pub const Rule = struct { name: []const u8, cond_req: enum { any, all }, conditionals: []struct { - match_on: ScrobbleFields, + match_on: enum { artist, album, track }, match_cond: enum { is, contains }, match_txt: []const u8, }, actions: []struct { action: enum { replace, add }, - action_on: ScrobbleFields, + action_on: enum { artists_album, album, artists_track, track }, action_txt: []const u8, }, }; @@ -53,9 +61,3 @@ pub const Rule = struct { pub const Rules = struct { rules: []const Rule, }; - -pub const ScrobbleFields = enum { - artist, - album, - track, -};