Work on add artist action in rules

Really close to having it work, but there seems to be an error when uploading files, which causes particularly annoying problems on WSL when testing, so I'm commiting and trying on my desktop.
This commit is contained in:
mitteneer 2025-04-23 19:32:32 -04:00
parent e9c72041a5
commit 0631ded115
7 changed files with 300 additions and 145 deletions

View file

@ -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,