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:
parent
e9c72041a5
commit
0631ded115
7 changed files with 300 additions and 145 deletions
|
|
@ -35,12 +35,20 @@ pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig
|
||||||
defer rules.deinit();
|
defer rules.deinit();
|
||||||
|
|
||||||
const file_content = try file_read.readToEndAlloc(allocator, 16_000_000);
|
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();
|
file_read.close();
|
||||||
|
|
||||||
const file_write: std.fs.File = try std.fs.cwd().openFile("rules.json", .{ .mode = .write_only });
|
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_rules = Data.Rules{ .rules = rules.items };
|
||||||
const out = try std.json.stringifyAlloc(allocator, out_rules, .{});
|
const out = try std.json.stringifyAlloc(allocator, out_rules, .{});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const jetzig = @import("jetzig");
|
const jetzig = @import("jetzig");
|
||||||
const jetquery = @import("jetzig").jetquery;
|
const jetquery = @import("jetzig").jetquery;
|
||||||
const Scrobble = @import("../../types.zig").LastFMScrobble;
|
|
||||||
const lastfm = @import("../../types.zig").LastFM;
|
const lastfm = @import("../../types.zig").LastFM;
|
||||||
//const Rules = @import("../../types.zig").Rules;
|
|
||||||
const Data = @import("../../types.zig");
|
const Data = @import("../../types.zig");
|
||||||
const rules = @import("../../apply_rule.zig");
|
const rules = @import("../../apply_rule.zig");
|
||||||
|
|
||||||
|
|
@ -18,26 +16,50 @@ const rules = @import("../../apply_rule.zig");
|
||||||
// - environment: Enum of `{ production, development }`.
|
// - environment: Enum of `{ production, development }`.
|
||||||
pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig.jobs.JobEnv) !void {
|
pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig.jobs.JobEnv) !void {
|
||||||
//_ = env;
|
//_ = env;
|
||||||
_ = allocator;
|
|
||||||
if (params.getT(.array, "scrobbles")) |scrobbles| {
|
if (params.getT(.array, "scrobbles")) |scrobbles| {
|
||||||
for (scrobbles.items()) |item| {
|
for (scrobbles.items()) |item| {
|
||||||
//const fixed_date: u32 = @as(u32, item.getT(.integer, "date").?);
|
//var buffer: [256**4]u8 = undefined;
|
||||||
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 fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||||
|
//const alloc = fba.allocator();
|
||||||
|
|
||||||
// Make hashes
|
var alssu8 = std.ArrayList([]const u8).init(allocator);
|
||||||
//const album_hash = @as(i32, @bitCast(std.hash.Fnv1a_32.hash(scrobble.album)));
|
defer alssu8.deinit();
|
||||||
//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)));
|
|
||||||
|
|
||||||
// Create a buffer to hold the metadata to hash. Numbers based on the title of a
|
for (item.getT(.array, "artists_track").?.items()) |artist| {
|
||||||
// particularly long Sufjan Stevens song title, and we're gonna pray the metadata
|
try alssu8.append(try artist.coerce([]const u8));
|
||||||
// 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 track_artists = try alssu8.toOwnedSlice();
|
||||||
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)));
|
for (item.getT(.array, "artists_album").?.items()) |artist| {
|
||||||
const song_prehash = try std.fmt.bufPrint(&buffer, "{s}{s}{s}", .{ scrobble.artist, scrobble.album, scrobble.track });
|
try alssu8.append(try artist.coerce([]const u8));
|
||||||
const song_id = @as(i32, @bitCast(std.hash.Fnv1a_32.hash(song_prehash)));
|
}
|
||||||
|
|
||||||
|
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
|
// Make IDs
|
||||||
// Song: Song hash XOR artist hash XOR album hash
|
// 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)));
|
//var album_id: i32 = @as(i32, @bitCast(std.hash.Fnv1a_32.hash(formed)));
|
||||||
//const song_id = (song_hash ^ artist_hash ^ album_hash);
|
//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)
|
||||||
var ins_album = try jetzig.database.Query(.Album).find(album_id).select(.{.id}).execute(env.repo);
|
.find(album_id)
|
||||||
|
.select(.{.id})
|
||||||
|
.execute(env.repo);
|
||||||
|
|
||||||
var ins_artist = try jetzig.database.Query(.Artist).find(artist_id).select(.{.id}).execute(env.repo);
|
for (alartist.items) |artist| {
|
||||||
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);
|
var ins_artist = try jetzig.database.Query(.Artist)
|
||||||
|
.find(artist.id)
|
||||||
|
.select(.{.id})
|
||||||
|
.execute(env.repo);
|
||||||
|
|
||||||
if (albumsong == null) {
|
if (ins_artist == null) ins_artist = try jetzig.database.Query(.Artist)
|
||||||
var ins_song = try jetzig.database.Query(.Song).find(song_id).select(.{.id}).execute(env.repo);
|
.insert(.{
|
||||||
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);
|
.id = artist.id,
|
||||||
|
.name = artist.name,
|
||||||
|
.disambiguation = null,
|
||||||
|
})
|
||||||
|
.returning(.{.id})
|
||||||
|
.execute(env.repo);
|
||||||
|
|
||||||
if (ins_album == null) {
|
if (albumsong == null) {
|
||||||
ins_album = try jetzig.database.Query(.Album).insert(.{ .id = album_id, .name = scrobble.album, .length = null }).returning(.{.id}).execute(env.repo);
|
var ins_song = try jetzig.database.Query(.Song)
|
||||||
// I think there's still technically a bug here when you have a different artist but I'm not sure
|
.find(song_id)
|
||||||
try jetzig.database.Query(.Artistalbum).insert(.{ .artist_id = ins_artist.?.id, .album_id = ins_album.?.id }).execute(env.repo);
|
.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) {
|
try jetzig.database.Query(.Scrobble)
|
||||||
// ins_artist_id = try jetzig.database.Query(.Artist).insert(.{ .id = artist_id, .name = scrobble.artist, .disambiguation = null }).returning(.{.id}).execute(env.repo);
|
.insert(.{
|
||||||
// try jetzig.database.Query(.Albumsongartist).insert(.{ .albumsong_id = albumsong_id, .artist_id = ins_artist_id });
|
.albumsong = albumsong.?.id,
|
||||||
// try jetzig.database.Query(.Artistalbum).insert(.{ .artist_id = ins_artist_id, .album_id = ins_album_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 });
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,11 @@
|
||||||
@for (table_data) |value| {
|
@for (table_data) |value| {
|
||||||
<tr>
|
<tr>
|
||||||
<td class=cell>{{value.track}}</td>
|
<td class=cell>{{value.track}}</td>
|
||||||
<td class=cell>{{value.artist}}</td>
|
<td class=cell>
|
||||||
|
@for (value.get("artists").?) |artist| {
|
||||||
|
{{artist}}
|
||||||
|
}
|
||||||
|
</td>
|
||||||
<td class=cell>{{value.album}}</td>
|
<td class=cell>{{value.album}}</td>
|
||||||
<td class=cell>{{value.date}}</td>
|
<td class=cell>{{value.date}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,8 @@ then
|
||||||
<option value="add">add</option>
|
<option value="add">add</option>
|
||||||
</select>
|
</select>
|
||||||
<select name="action-on" id="action-on">
|
<select name="action-on" id="action-on">
|
||||||
<option value="artist">artist</option>
|
<option value="artists_track">artist (song)</option>
|
||||||
|
<option value="artists_album">artist (album)</option>
|
||||||
<option value="album">album</option>
|
<option value="album">album</option>
|
||||||
<option value="track">song</option>
|
<option value="track">song</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const jetzig = @import("jetzig");
|
const jetzig = @import("jetzig");
|
||||||
const jetquery = @import("jetzig").jetquery;
|
const jetquery = @import("jetzig").jetquery;
|
||||||
const ScrobbleTypes = @import("../../types.zig");
|
|
||||||
const zeit = @import("zeit");
|
const zeit = @import("zeit");
|
||||||
const rules = @import("../../apply_rule.zig");
|
const rules = @import("../../apply_rule.zig");
|
||||||
const Data = @import("../../types.zig");
|
const Data = @import("../../types.zig");
|
||||||
|
|
@ -33,58 +32,70 @@ pub fn post(request: *jetzig.Request) !jetzig.View {
|
||||||
var skipped_tracks: u64 = 0;
|
var skipped_tracks: u64 = 0;
|
||||||
var limited_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();
|
defer rule_file.close();
|
||||||
const file_content = try rule_file.readToEndAlloc(request.allocator, 16_000_000);
|
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) {
|
switch (source) {
|
||||||
0 => {
|
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 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;
|
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| {
|
appends: for (content.scrobbles) |scrobble| {
|
||||||
// We can short-circuit on the limiter bools
|
// 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;
|
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
|
var scrobble_view = try scrobbles_view.append(.object);
|
||||||
inline for (std.meta.fields(ScrobbleTypes.LastFMScrobble)) |f| {
|
var artists = try scrobble_view.put("artists", .array);
|
||||||
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 scrobble_view.put("date", formatted_scrobble.date);
|
||||||
try scrobbles_view.append(formatted_scrobble);
|
|
||||||
|
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 => {
|
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 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();
|
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| {
|
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"))) {
|
if (scrobble.ms_played < 30_000 and (scrobble.reason_end == null or !std.mem.eql(u8, scrobble.reason_end.?, "trackdone"))) {
|
||||||
skipped_tracks += 1;
|
skipped_tracks += 1;
|
||||||
continue :appends;
|
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) {
|
if (scrobble.master_metadata_album_artist_name == null or scrobble.master_metadata_track_name == null) {
|
||||||
skipped_tracks += 1;
|
skipped_tracks += 1;
|
||||||
continue :appends;
|
continue :appends;
|
||||||
|
|
@ -96,19 +107,42 @@ pub fn post(request: *jetzig.Request) !jetzig.View {
|
||||||
continue :appends;
|
continue :appends;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Turn SpotifyScrobble into a LastFM scrobble
|
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 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 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);
|
try scrobble_view.put("track", formatted_scrobble.track);
|
||||||
|
try scrobble_view.put("album", formatted_scrobble.album);
|
||||||
// This is so unnecessary, probably useful once I start doing Spotify integration though
|
for (formatted_scrobble.artists_track) |artist| {
|
||||||
inline for (std.meta.fields(ScrobbleTypes.LastFMScrobble)) |f| {
|
try artists.append(artist);
|
||||||
try value.put(f.name, @as(f.type, @field(formatted_scrobble, f.name)));
|
|
||||||
}
|
}
|
||||||
// Note sure why this works for ZMPL, but not for jobs.
|
try scrobble_view.put("date", formatted_scrobble.date);
|
||||||
try scrobbles_view.append(formatted_scrobble);
|
|
||||||
|
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,
|
else => unreachable,
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,32 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Scrobble = @import("./types.zig").LastFMScrobble;
|
|
||||||
const Rules = @import("./types.zig").Rules;
|
const Rules = @import("./types.zig").Rules;
|
||||||
|
const Data = @import("./types.zig");
|
||||||
|
|
||||||
// Wrapper for containsAtLeast to make the switch below to work
|
// 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);
|
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);
|
return std.mem.eql(u8, haystack, needle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn applyScrobbleRule(scrobble: Scrobble, rules: Rules) Scrobble {
|
pub fn applyScrobbleRule(allocator: std.mem.Allocator, scrobble: Data.ImportedScrobble, rules: Rules) Data.Scrobble {
|
||||||
var output_scrobble: Scrobble = 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| {
|
for (rules.rules) |rule| {
|
||||||
var match_found: bool = switch (rule.cond_req) {
|
var match_found: bool = switch (rule.cond_req) {
|
||||||
.any => false,
|
.any => false,
|
||||||
|
|
@ -20,8 +34,8 @@ pub fn applyScrobbleRule(scrobble: Scrobble, rules: Rules) Scrobble {
|
||||||
};
|
};
|
||||||
for (rule.conditionals) |cond| {
|
for (rule.conditionals) |cond| {
|
||||||
const match_fn: *const fn ([]const u8, []const u8) bool = switch (cond.match_cond) {
|
const match_fn: *const fn ([]const u8, []const u8) bool = switch (cond.match_cond) {
|
||||||
.is => eqlDecomped,
|
.is => eqlWrapper,
|
||||||
.contains => containsAtLeastOne,
|
.contains => containsWrapper,
|
||||||
};
|
};
|
||||||
switch (rule.cond_req) {
|
switch (rule.cond_req) {
|
||||||
.any => switch (cond.match_on) {
|
.any => switch (cond.match_on) {
|
||||||
|
|
@ -35,9 +49,22 @@ pub fn applyScrobbleRule(scrobble: Scrobble, rules: Rules) Scrobble {
|
||||||
if (match_found) {
|
if (match_found) {
|
||||||
for (rule.actions) |act| {
|
for (rule.actions) |act| {
|
||||||
switch (act.action) {
|
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) {
|
.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},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,59 @@
|
||||||
pub const LastFMScrobble = struct {
|
pub const ImportedScrobble = struct {
|
||||||
track: []const u8,
|
track: []const u8,
|
||||||
artist: []const u8,
|
artist: []const u8,
|
||||||
album: []const u8 = "",
|
album: []const u8 = "",
|
||||||
date: i128,
|
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
|
// 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
|
// I derived whether or not these values were optional from searching
|
||||||
// the respective fields for null in Vim, so there may be some fields
|
// the respective fields for null in Vim, so there may be some fields
|
||||||
// that can be optional that I haven't run into yet
|
// that can be optional that I haven't run into yet
|
||||||
pub const SpotifyScrobble = struct {
|
pub const SpotifyScrobble = struct {
|
||||||
ts: []const u8,
|
ts: []const u8,
|
||||||
username: []const u8,
|
//username: []const u8,
|
||||||
platform: []const u8,
|
//platform: []const u8,
|
||||||
ms_played: u64,
|
ms_played: u64,
|
||||||
conn_country: []const u8,
|
//conn_country: []const u8,
|
||||||
ip_addr_decrypted: ?[]const u8,
|
//ip_addr_decrypted: ?[]const u8,
|
||||||
user_agent_decrypted: ?[]const u8,
|
//user_agent_decrypted: ?[]const u8,
|
||||||
master_metadata_track_name: ?[]const u8,
|
master_metadata_track_name: ?[]const u8,
|
||||||
master_metadata_album_artist_name: ?[]const u8,
|
master_metadata_album_artist_name: ?[]const u8,
|
||||||
master_metadata_album_album_name: ?[]const u8,
|
master_metadata_album_album_name: ?[]const u8,
|
||||||
spotify_track_uri: ?[]const u8,
|
//spotify_track_uri: ?[]const u8,
|
||||||
episode_name: ?[]const u8,
|
//episode_name: ?[]const u8,
|
||||||
episode_show_name: ?[]const u8,
|
//episode_show_name: ?[]const u8,
|
||||||
spotify_episode_uri: ?[]const u8,
|
//spotify_episode_uri: ?[]const u8,
|
||||||
reason_start: []const u8,
|
reason_start: []const u8,
|
||||||
reason_end: ?[]const u8,
|
reason_end: ?[]const u8,
|
||||||
shuffle: bool,
|
//shuffle: bool,
|
||||||
skipped: ?bool,
|
skipped: ?bool,
|
||||||
offline: bool,
|
//offline: bool,
|
||||||
offline_timestamp: u64,
|
offline_timestamp: u64,
|
||||||
incognito_mode: ?bool,
|
//incognito_mode: ?bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Rule = struct {
|
pub const Rule = struct {
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
cond_req: enum { any, all },
|
cond_req: enum { any, all },
|
||||||
conditionals: []struct {
|
conditionals: []struct {
|
||||||
match_on: ScrobbleFields,
|
match_on: enum { artist, album, track },
|
||||||
match_cond: enum { is, contains },
|
match_cond: enum { is, contains },
|
||||||
match_txt: []const u8,
|
match_txt: []const u8,
|
||||||
},
|
},
|
||||||
actions: []struct {
|
actions: []struct {
|
||||||
action: enum { replace, add },
|
action: enum { replace, add },
|
||||||
action_on: ScrobbleFields,
|
action_on: enum { artists_album, album, artists_track, track },
|
||||||
action_txt: []const u8,
|
action_txt: []const u8,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -53,9 +61,3 @@ pub const Rule = struct {
|
||||||
pub const Rules = struct {
|
pub const Rules = struct {
|
||||||
rules: []const Rule,
|
rules: []const Rule,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const ScrobbleFields = enum {
|
|
||||||
artist,
|
|
||||||
album,
|
|
||||||
track,
|
|
||||||
};
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue