From cb89a3e6f3c7e0829ca297c0ee26dcc73326a6e7 Mon Sep 17 00:00:00 2001 From: mitteneer Date: Mon, 28 Apr 2025 21:37:08 -0400 Subject: [PATCH 1/4] Switch dates from i128 to u64 I was making them unnecessarily large by accidentally storing them as microseconds instead of milliseconds. Might be able to get away with seconds in the future --- build.zig.zon | 4 ++-- src/app/jobs/process_scrobbles.zig | 2 +- src/app/views/scrobbles.zig | 2 +- src/app/views/upload.zig | 2 +- src/types.zig | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 0839430..c06a1ac 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -17,8 +17,8 @@ // internet connectivity. .dependencies = .{ .jetzig = .{ - .url = "https://github.com/jetzig-framework/jetzig/archive/86d82026ab574d4e5c3c6cc3817dda84b510001a.tar.gz", - .hash = "jetzig-0.0.0-IpAgLTkzDwDKmsY9MqM41EHDXWGkViiECa0lzV8xl17x", + .url = "https://github.com/jetzig-framework/jetzig/archive/1feb18fb74e626fe068ec67532318640a9cb83be.tar.gz", + .hash = "jetzig-0.0.0-IpAgLfMzDwDyAqZ05btcLDd9dfE_bxUbfOI_Wx7a19ed", }, .zeit = .{ .url = "https://github.com/rockorager/zeit/archive/refs/heads/main.tar.gz", diff --git a/src/app/jobs/process_scrobbles.zig b/src/app/jobs/process_scrobbles.zig index b2ae22e..8a9dadc 100644 --- a/src/app/jobs/process_scrobbles.zig +++ b/src/app/jobs/process_scrobbles.zig @@ -33,7 +33,7 @@ pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig .artists_track = track_artist_name_buffer, .album = item.getT(.string, "album") orelse "", .artists_album = album_artist_name_buffer, - .date = @as(u64, @bitCast(@as(i64, @truncate(item.getT(.integer, "date").? * 1000)))), + .date = @as(u64, @bitCast(@as(i64, @truncate(item.getT(.integer, "date").?)))), }; var id_prehash = std.ArrayList(u8).init(allocator); diff --git a/src/app/views/scrobbles.zig b/src/app/views/scrobbles.zig index 1ef4a9e..f9ab102 100644 --- a/src/app/views/scrobbles.zig +++ b/src/app/views/scrobbles.zig @@ -35,7 +35,7 @@ pub fn index(request: *jetzig.Request) !jetzig.View { try scrobble_view.put("album_name", scrobble.album_name); try scrobble_view.put("album_id", scrobble.album_id); var date = std.ArrayList(u8).init(request.allocator); - try (try zeit.instant(.{ .source = .{ .unix_timestamp = @divFloor(scrobble.date, 1_000_000) } })).time().strftime(date.writer(), "%d %b %Y, %H:%M"); + try (try zeit.instant(.{ .source = .{ .unix_timestamp = @divFloor(scrobble.date, 1_000) } })).time().strftime(date.writer(), "%d %b %Y, %H:%M"); try scrobble_view.put("date", date.items); } diff --git a/src/app/views/upload.zig b/src/app/views/upload.zig index 59442a8..1e69e94 100644 --- a/src/app/views/upload.zig +++ b/src/app/views/upload.zig @@ -111,7 +111,7 @@ pub fn post(request: *jetzig.Request) !jetzig.View { .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, + .date = @as(u64, @bitCast((try zeit.instant(.{ .source = .{ .iso8601 = scrobble.ts } })).unixTimestamp() * 1000)), }; const formatted_scrobble = if (rule_list) |rl| diff --git a/src/types.zig b/src/types.zig index f0268ed..09d87c7 100644 --- a/src/types.zig +++ b/src/types.zig @@ -2,7 +2,7 @@ pub const ImportedScrobble = struct { track: []const u8, artist: []const u8, album: []const u8 = "", - date: i128, + date: u64, }; pub const Scrobble = struct { @@ -10,7 +10,7 @@ pub const Scrobble = struct { artists_track: []const []const u8, album: []const u8 = "", artists_album: []const []const u8, - date: i128, + date: u64, }; // From lastfmstats.com From ae85f94ddb80c17dbe998ca2a6f46500d726422e Mon Sep 17 00:00:00 2001 From: mitteneer Date: Mon, 28 Apr 2025 23:01:34 -0400 Subject: [PATCH 2/4] Switch dates from u64 to i64 PostgreSQL only uses signed ints, so this makes things much easier --- src/app/jobs/process_scrobbles.zig | 2 +- src/app/views/upload.zig | 2 +- src/types.zig | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/jobs/process_scrobbles.zig b/src/app/jobs/process_scrobbles.zig index 8a9dadc..c6b25b1 100644 --- a/src/app/jobs/process_scrobbles.zig +++ b/src/app/jobs/process_scrobbles.zig @@ -33,7 +33,7 @@ pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig .artists_track = track_artist_name_buffer, .album = item.getT(.string, "album") orelse "", .artists_album = album_artist_name_buffer, - .date = @as(u64, @bitCast(@as(i64, @truncate(item.getT(.integer, "date").?)))), + .date = @as(i64, @truncate(item.getT(.integer, "date").?)), }; var id_prehash = std.ArrayList(u8).init(allocator); diff --git a/src/app/views/upload.zig b/src/app/views/upload.zig index 1e69e94..59442a8 100644 --- a/src/app/views/upload.zig +++ b/src/app/views/upload.zig @@ -111,7 +111,7 @@ pub fn post(request: *jetzig.Request) !jetzig.View { .track = scrobble.master_metadata_track_name.?, .album = scrobble.master_metadata_album_album_name.?, .artist = scrobble.master_metadata_album_artist_name.?, - .date = @as(u64, @bitCast((try zeit.instant(.{ .source = .{ .iso8601 = scrobble.ts } })).unixTimestamp() * 1000)), + .date = (try zeit.instant(.{ .source = .{ .iso8601 = scrobble.ts } })).unixTimestamp() * 1000, }; const formatted_scrobble = if (rule_list) |rl| diff --git a/src/types.zig b/src/types.zig index 09d87c7..704adc0 100644 --- a/src/types.zig +++ b/src/types.zig @@ -2,7 +2,7 @@ pub const ImportedScrobble = struct { track: []const u8, artist: []const u8, album: []const u8 = "", - date: u64, + date: i64, }; pub const Scrobble = struct { @@ -10,7 +10,7 @@ pub const Scrobble = struct { artists_track: []const []const u8, album: []const u8 = "", artists_album: []const []const u8, - date: u64, + date: i64, }; // From lastfmstats.com From 8138e5ccf25c14fe98236ebf9775fab3c41c1547 Mon Sep 17 00:00:00 2001 From: mitteneer Date: Mon, 28 Apr 2025 23:03:24 -0400 Subject: [PATCH 3/4] Create dateFmt function It's easier to keep the date as an epoch in PostgreSQL to do comparisons, but I always want to show it to the user as a formatted date --- src/date_fmt.zig | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/date_fmt.zig diff --git a/src/date_fmt.zig b/src/date_fmt.zig new file mode 100644 index 0000000..603a209 --- /dev/null +++ b/src/date_fmt.zig @@ -0,0 +1,8 @@ +const std = @import("std"); +const zeit = @import("zeit"); + +pub fn dateFmt(allocator: std.mem.Allocator, epoch: i64) ![]const u8 { + var date = std.ArrayList(u8).init(allocator); + try (try zeit.instant(.{ .source = .{ .unix_timestamp = @divFloor(epoch, 1_000) } })).time().strftime(date.writer(), "%d %b %Y, %H:%M"); + return date.items; +} From 78e416eeaf95c881e5f17e545a48726458422b4f Mon Sep 17 00:00:00 2001 From: mitteneer Date: Mon, 28 Apr 2025 23:06:21 -0400 Subject: [PATCH 4/4] Add more information to scrobbles views and refactor artists view --- src/app/views/artists.zig | 114 ++++++++++++++++++++++------- src/app/views/artists/get.zmpl | 11 ++- src/app/views/scrobbles.zig | 58 +++++++-------- src/app/views/scrobbles/index.zmpl | 6 ++ 4 files changed, 129 insertions(+), 60 deletions(-) diff --git a/src/app/views/artists.zig b/src/app/views/artists.zig index 9f707f0..9dacc08 100644 --- a/src/app/views/artists.zig +++ b/src/app/views/artists.zig @@ -1,6 +1,7 @@ const std = @import("std"); const jetzig = @import("jetzig"); const jetquery = @import("jetzig").jetquery; +const dateFmt = @import("../../date_fmt.zig").dateFmt; pub fn index(request: *jetzig.Request) !jetzig.View { var root = try request.data(.object); @@ -34,12 +35,34 @@ pub fn index(request: *jetzig.Request) !jetzig.View { pub fn get(id: []const u8, request: *jetzig.Request) !jetzig.View { var root = try request.data(.object); - var albums_view = try root.put("albums", .array); - var appears = try root.put("appears", .array); - const artist_name = try jetzig.database.Query(.Artist).find(id).select(.{ .id, .name }).execute(request.repo); - _ = try root.put("artist", artist_name.?.name); - const query = - \\SELECT albums.name, albums.id, COUNT(scrobbles) AS scrobbles + + const ArtistResult = struct { name: []const u8, id: i32, scrobbles: i64, rank: i64 }; + const AlbumsResult = struct { name: []const u8, id: i32, scrobbles: i64 }; + const ScrobbleResult = struct { name: []const u8, id: i32, date: i64 }; + + const artist_info_query = + \\SELECT * FROM + \\(SELECT *, ROW_NUMBER() OVER (ORDER BY scrobbles DESC) AS rank FROM + \\(SELECT artists.name AS name, artists.id AS id, COUNT(scrobbles) AS scrobbles + \\FROM albumsongsartists + \\INNER JOIN artists ON albumsongsartists.artist_id = artists.id + \\INNER JOIN albumsongs ON albumsongsartists.albumsong_id = albumsongs.id + \\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id + \\GROUP BY artists.id)) + \\WHERE id = $1 + ; + + var artist_jq_result = try request.repo.executeSql(artist_info_query, .{id}); + defer artist_jq_result.deinit(); + + if (try artist_jq_result.postgresql.result.next()) |artist_row| { + const artist = try artist_row.to(ArtistResult, .{ .dupe = true, .allocator = request.allocator }); + try root.put("artist", artist); + } + try artist_jq_result.drain(); + + const albums_query = + \\SELECT albums.name AS name, albums.id AS id, COUNT(scrobbles) AS scrobbles \\FROM artistalbums \\INNER JOIN albums ON albums.id = artistalbums.album_id \\INNER JOIN albumsongs ON albumsongs.album_id = albums.id @@ -49,8 +72,19 @@ pub fn get(id: []const u8, request: *jetzig.Request) !jetzig.View { \\ORDER BY scrobbles DESC ; + var albums_view = try root.put("albums", .array); + + var albums_jq_result = try request.repo.executeSql(albums_query, .{id}); + defer albums_jq_result.deinit(); + + while (try albums_jq_result.postgresql.result.next()) |album_row| { + const album = try album_row.to(AlbumsResult, .{ .dupe = true, .allocator = request.allocator }); + try albums_view.append(album); + } + //albums_jq_result.drain(); + const appears_query = - \\SELECT albums.name, albums.id, COUNT(scrobbles) AS scrobbles + \\SELECT albums.name AS name, albums.id AS id, COUNT(scrobbles) AS scrobbles \\FROM artistalbums \\INNER JOIN albums ON albums.id = artistalbums.album_id \\INNER JOIN albumsongs ON albumsongs.album_id = albums.id @@ -61,30 +95,60 @@ pub fn get(id: []const u8, request: *jetzig.Request) !jetzig.View { \\ORDER BY scrobbles DESC; ; - var albums_jq_result = try request.repo.executeSql(query, .{id}); - defer albums_jq_result.deinit(); - - const Album = struct { name: []const u8, id: i32, scrobbles: i64 }; - - while (try albums_jq_result.postgresql.result.next()) |album_row| { - const album = try album_row.to(Album, .{ .dupe = true, .allocator = request.allocator }); - var album_view = try albums_view.append(.object); - try album_view.put("name", album.name); - try album_view.put("url", album.id); - try album_view.put("scrobbles", album.scrobbles); - } + var appears_view = try root.put("appears", .array); var appears_jq_result = try request.repo.executeSql(appears_query, .{id}); defer appears_jq_result.deinit(); - while (try appears_jq_result.postgresql.result.next()) |album_row| { - const album = try album_row.to(Album, .{ .dupe = true, .allocator = request.allocator }); - var album_view = try appears.append(.object); - try album_view.put("name", album.name); - try album_view.put("url", album.id); - try album_view.put("scrobbles", album.scrobbles); + while (try appears_jq_result.postgresql.result.next()) |appears_row| { + const appears = try appears_row.to(AlbumsResult, .{ .dupe = true, .allocator = request.allocator }); + try appears_view.append(appears); } + //appears_jq_result.drain(); + + const first_last_songs_query = + \\(SELECT songs.name AS name, songs.id AS id, scrobbles.datetime AS date + \\FROM albumsongs + \\INNER JOIN songs ON songs.id = albumsongs.song_id + \\INNER JOIN scrobbles ON albumsongs.id = scrobbles.albumsong + \\INNER JOIN albumsongsartists ON albumsongsartists.albumsong_id = albumsongs.id + \\WHERE albumsongsartists.artist_id = $1 + \\ORDER BY scrobbles.datetime DESC + \\LIMIT 1) + \\ + \\UNION ALL + \\ + \\(SELECT songs.name AS name, songs.id AS id, scrobbles.datetime AS date + \\FROM albumsongs + \\INNER JOIN songs ON songs.id = albumsongs.song_id + \\INNER JOIN scrobbles ON albumsongs.id = scrobbles.albumsong + \\INNER JOIN albumsongsartists ON albumsongsartists.albumsong_id = albumsongs.id + \\WHERE albumsongsartists.artist_id = $1 + \\ORDER BY scrobbles.datetime ASC + \\LIMIT 1) + ; + + var first_last_songs_jq_result = try request.repo.executeSql(first_last_songs_query, .{id}); + defer first_last_songs_jq_result.deinit(); + + // These look backwards, but it's correct this way + if (try first_last_songs_jq_result.postgresql.result.next()) |last_song_row| { + const last_song = try last_song_row.to(ScrobbleResult, .{ .dupe = true, .allocator = request.allocator }); + try root.put("last_song_name", last_song.name); + try root.put("last_song_id", last_song.id); + try root.put("last_song_date", (try dateFmt(request.allocator, last_song.date))); + } + + if (try first_last_songs_jq_result.postgresql.result.next()) |first_song_row| { + const first_song = try first_song_row.to(ScrobbleResult, .{ .dupe = true, .allocator = request.allocator }); + try root.put("first_song_name", first_song.name); + try root.put("first_song_id", first_song.id); + try root.put("first_song_date", (try dateFmt(request.allocator, first_song.date))); + } + + try first_last_songs_jq_result.drain(); + return request.render(.ok); } diff --git a/src/app/views/artists/get.zmpl b/src/app/views/artists/get.zmpl index 2cc50cd..c272f79 100644 --- a/src/app/views/artists/get.zmpl +++ b/src/app/views/artists/get.zmpl @@ -5,7 +5,12 @@ @partial partials/header -

{{.artist}}

+

{{.artist.name}}

+{{.artist.scrobbles}} scrobbles ({{.artist.rank}}th place) +
+First listen: {{.first_song_name}} ({{.first_song_date}}) +
+Most recent listen: {{.last_song_name}} ({{.last_song_date}})

Albums

@@ -16,7 +21,7 @@ @for (.albums) |album| { - + } @@ -32,7 +37,7 @@ @for (.appears) |album| { - + } diff --git a/src/app/views/scrobbles.zig b/src/app/views/scrobbles.zig index f9ab102..6ee80d8 100644 --- a/src/app/views/scrobbles.zig +++ b/src/app/views/scrobbles.zig @@ -5,31 +5,42 @@ const zeit = @import("zeit"); pub fn index(request: *jetzig.Request) !jetzig.View { var root = try request.data(.object); var scrobbles_view = try root.put("scrobbles", .array); - //const query = jetzig.database.Query(.Scrobble) - // .select(.{ .id, .date }) - // .include(.song, .{ .select = .{ .id, .name } }) - // .include(.album, .{ .select = .{ .id, .name } }) - // .include(.scrobbleartists, .{ .select = .{.artist_id} }) - // .orderBy(.{ .date = .desc }); - //const scrobbles = try request.repo.all(query); const query = - \\SELECT songs.name, songs.id, albums.name, albums.id, scrobbles.datetime + \\SELECT songs.name, songs.id, albums.name, albums.id, artists.name, artists.id, scrobbles.id, scrobbles.datetime \\FROM albumsongs \\INNER JOIN songs ON songs.id = albumsongs.song_id \\INNER JOIN albums ON albums.id = albumsongs.album_id \\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id + \\INNER JOIN albumsongsartists ON albumsongsartists.albumsong_id = albumsongs.id + \\INNER JOIN artists ON artists.id = albumsongsartists.artist_id \\ORDER BY scrobbles.datetime ASC ; - var scrobbles_js_result = try request.repo.executeSql(query, .{}); - defer scrobbles_js_result.deinit(); + var scrobbles_jq_result = try request.repo.executeSql(query, .{}); + defer scrobbles_jq_result.deinit(); - const Scrobble = struct { song_name: []const u8, song_id: i32, album_name: []const u8, album_id: i32, date: i64 }; + const Scrobble = struct { song_name: []const u8, song_id: i32, album_name: []const u8, album_id: i32, artist_name: []const u8, artist_id: i32, s_id: i32, date: i64 }; - while (try scrobbles_js_result.postgresql.result.next()) |scrobble_row| { + var prev_s_id: ?i32 = null; + var prev_artist_infos: ?*jetzig.zmpl.Data.Value = null; + + blk: while (try scrobbles_jq_result.postgresql.result.next()) |scrobble_row| { const scrobble = try scrobble_row.to(Scrobble, .{ .dupe = true, .allocator = request.allocator }); + if (scrobble.s_id == prev_s_id) { + var artist_info = try prev_artist_infos.?.append(.object); + try artist_info.put("name", scrobble.artist_name); + try artist_info.put("url", scrobble.artist_id); + continue :blk; + } + // Not appending the scrobble directly because we don't want the unix timestamp or scrobble id var scrobble_view = try scrobbles_view.append(.object); + + var artist_infos = try scrobble_view.put("artist_info", .array); + var artist_info = try artist_infos.append(.object); + try artist_info.put("name", scrobble.artist_name); + try artist_info.put("url", scrobble.artist_id); + try scrobble_view.put("song_name", scrobble.song_name); try scrobble_view.put("song_id", scrobble.song_id); try scrobble_view.put("album_name", scrobble.album_name); @@ -37,28 +48,11 @@ pub fn index(request: *jetzig.Request) !jetzig.View { var date = std.ArrayList(u8).init(request.allocator); try (try zeit.instant(.{ .source = .{ .unix_timestamp = @divFloor(scrobble.date, 1_000) } })).time().strftime(date.writer(), "%d %b %Y, %H:%M"); try scrobble_view.put("date", date.items); + + prev_artist_infos = artist_infos; + prev_s_id = scrobble.s_id; } - //for (scrobbles) |scrobble| { - // var scrobble_view = try scrobbles_view.append(.object); - - // var artist_infos = try scrobble_view.put("artist_info", .array); - // for (scrobble.scrobbleartists) |artist| { - // var artist_info = try artist_infos.append(.object); - // const artist_data = try jetzig.database.Query(.Artist) - // .find(artist.artist_id) - // .select(.{ .id, .name }) - // .execute(request.repo); - // try artist_info.put("name", artist_data.?.name); - // try artist_info.put("id", artist_data.?.id); - // } - - // try scrobble_view.put("song_name", scrobble.song.name); - // try scrobble_view.put("song_id", scrobble.song.id); - // try scrobble_view.put("album_name", scrobble.album.name); - // try scrobble_view.put("album_id", scrobble.album.id); - // try scrobble_view.put("date", scrobble.date); - //} return request.render(.ok); } diff --git a/src/app/views/scrobbles/index.zmpl b/src/app/views/scrobbles/index.zmpl index c8e49be..b298cb6 100644 --- a/src/app/views/scrobbles/index.zmpl +++ b/src/app/views/scrobbles/index.zmpl @@ -10,6 +10,7 @@ + @@ -18,6 +19,11 @@ @for (.scrobbles) |scrobble| { +
{{album.name}}{{album.name}} {{album.scrobbles}}
{{album.name}}{{album.name}} {{album.scrobbles}}
SongArtist(s) Album Date
{{scrobble.song_name}} + @for (scrobble.get("artist_info").?) |ai| { + {{ai.name}} + } + {{scrobble.album_name}} {{scrobble.date}}