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,
-};