Compare commits

...

5 commits

Author SHA1 Message Date
a8a4ed27c4 Make process_scrobbles vars more readable, and change hashing 2025-06-11 09:22:38 -04:00
1e4a271b8d Update README 2025-06-11 09:22:06 -04:00
85552f39c1 Add Artistsongs table
Whether or not a song is covered, there was an original artist who originally performed the song. The only issue is that an Artistsongs table will almost be the exact same as the Albumsongsartists table, since most songs aren't covered. So, it may be better not to populate that table by default, and then if two albumsongs with different artists share the same song, then fill the Artistsongs table.
2025-06-09 21:45:41 -04:00
162341fb5f Convert ids to i64
The birthday paradox is a real problem with the size of our datasets. i64 is the largest numerical value we can use, and there's a 0.1% chance of collision with ~2,000,000 values, so I feel pretty comfortable with this
2025-06-09 21:41:52 -04:00
c8f2ef57c8 Add some tyling to songs view
This can (will) be easily replicated for the other views, I just first tested it on songs. I think this looks much nicer, and I'll probably roll with a layout similar to this for the other views, with some minor adjustments for each particular view.
2025-06-06 15:55:20 -04:00
15 changed files with 184 additions and 106 deletions

View file

@ -3,7 +3,6 @@
Inspired by [Last.fm](https://last.fm), [Maloja](https://github.com/krateng/maloja), and [Lastfmstats.com](https://www.lastfmstats.com). Inspired by [Last.fm](https://last.fm), [Maloja](https://github.com/krateng/maloja), and [Lastfmstats.com](https://www.lastfmstats.com).
**Z**uletzt is written with [**Z**ig](https://github.com/ziglang/zig) and [Jetzig](https://github.com/jetzig-framework/jetzig) as a means of learning the **Z**uletzt is written with [**Z**ig](https://github.com/ziglang/zig) and [Jetzig](https://github.com/jetzig-framework/jetzig) as a means of learning the
language, reintroducing myself to programming, and combining language, reintroducing myself to programming, and combining
the functionality of the aforementioned inspirations. the functionality of the aforementioned inspirations.
@ -12,6 +11,35 @@ Zuletzt means "last" in German.
Licensed under MIT. Licensed under MIT.
## Usage
Zuletzt allows uploads of Scrobbles at the `/upload` page, where you
can import Scrobbles from a Spotify data export, Last.FM data export (a `.json`
file from lastfmstats.com), or by providing a Last.FM username and connecting
to Last.FM directly.
Zuletzt will not make any assumptions about the data, and only change metadata when asked to by a rule. Two albums will be considered the same if:
- They share the same title (case/diacritic sensitive)
- The album artist(s) are the same
Zuletzt allows you to list multiple artists under an album using rules, but
does not try to automatically split artists along common delimiters. For
example, there's no way to know that "Mermaid Avenue" by "Billy Bragg, Wilco"
is performed by two artists, while "Ants From Up There" by "Black Country, New
Road" is performed by one artist. Thus, a rule needs to be made to tell Zuletzt
"Mermaid Avenue" is performed by "Billy Bragg" and "Wilco".
Two songs will only be considered the same if:
- They share the same title (case/diacritic sensitive)
- They appear on the same album
If two or more songs with the same spelling appear on an album, they are
necessarily grouped under the same name, as there is no way to differentiate
them (see "Once In Royal David's City" on Sufjan Stevens's "Songs For
Christmas", for example). Every artist that performs on those songs with
receive attribution for the combined song.
If two artists have the same name, they are necessarily listed as the same artist, but can be separated with a rule, or after the fact, with a disambiguation string.
## To-Do List: ## To-Do List:
- [ ] Entity statistics - [ ] Entity statistics
- [x] See all artists under "/artists" - [x] See all artists under "/artists"
@ -60,6 +88,7 @@ Licensed under MIT.
- [ ] Rank songs - [ ] Rank songs
- [ ] Custom statistics[^7] - [ ] Custom statistics[^7]
- [ ] "Playlists"[^8] - [ ] "Playlists"[^8]
- [ ] First launch setup
[^1]: I do not intend to exactly replicate all the statistics Lastfmstats.com provides, but I would at least like to give the user the option to see those kinds of statistics, or generate them themselves (see 7). [^1]: I do not intend to exactly replicate all the statistics Lastfmstats.com provides, but I would at least like to give the user the option to see those kinds of statistics, or generate them themselves (see 7).

View file

@ -4,7 +4,7 @@ pub const Album = jetquery.Model(
@This(), @This(),
"albums", "albums",
struct { struct {
id: i32, id: i64,
name: []const u8, name: []const u8,
length: ?f32, length: ?f32,
created_at: jetquery.DateTime, created_at: jetquery.DateTime,
@ -22,9 +22,9 @@ pub const Albumsong = jetquery.Model(
@This(), @This(),
"albumsongs", "albumsongs",
struct { struct {
id: i32, id: i64,
song_id: i32, song_id: i64,
album_id: i32, album_id: i64,
created_at: jetquery.DateTime, created_at: jetquery.DateTime,
updated_at: jetquery.DateTime, updated_at: jetquery.DateTime,
}, },
@ -32,9 +32,7 @@ pub const Albumsong = jetquery.Model(
.relations = .{ .relations = .{
.song = jetquery.belongsTo(.Song, .{}), .song = jetquery.belongsTo(.Song, .{}),
.album = jetquery.belongsTo(.Album, .{}), .album = jetquery.belongsTo(.Album, .{}),
.scrobbles = jetquery.hasMany(.Scrobble, .{ .scrobbles = jetquery.hasMany(.Scrobble, .{ .foreign_key = "albumsong" }),
.foreign_key = "albumsong",
}),
.albumsongsartists = jetquery.hasMany(.Albumsongsartist, .{}), .albumsongsartists = jetquery.hasMany(.Albumsongsartist, .{}),
}, },
}, },
@ -44,9 +42,9 @@ pub const Albumsongsartist = jetquery.Model(
@This(), @This(),
"albumsongsartists", "albumsongsartists",
struct { struct {
id: i32, id: i64,
albumsong_id: i32, albumsong_id: i64,
artist_id: i32, artist_id: i64,
created_at: jetquery.DateTime, created_at: jetquery.DateTime,
updated_at: jetquery.DateTime, updated_at: jetquery.DateTime,
}, },
@ -62,9 +60,9 @@ pub const Artistalbum = jetquery.Model(
@This(), @This(),
"artistalbums", "artistalbums",
struct { struct {
id: i32, id: i64,
album_id: i32, album_id: i64,
artist_id: i32, artist_id: i64,
created_at: jetquery.DateTime, created_at: jetquery.DateTime,
updated_at: jetquery.DateTime, updated_at: jetquery.DateTime,
}, },
@ -80,7 +78,7 @@ pub const Artist = jetquery.Model(
@This(), @This(),
"artists", "artists",
struct { struct {
id: i32, id: i64,
name: []const u8, name: []const u8,
disambiguation: ?[]const u8, disambiguation: ?[]const u8,
created_at: jetquery.DateTime, created_at: jetquery.DateTime,
@ -90,6 +88,7 @@ pub const Artist = jetquery.Model(
.relations = .{ .relations = .{
.albumsongsartists = jetquery.hasMany(.Albumsongsartist, .{}), .albumsongsartists = jetquery.hasMany(.Albumsongsartist, .{}),
.artistalbums = jetquery.hasMany(.Artistalbum, .{}), .artistalbums = jetquery.hasMany(.Artistalbum, .{}),
.artistsongs = jetquery.hasMany(.Artistsong, .{}),
}, },
}, },
); );
@ -98,17 +97,15 @@ pub const Scrobble = jetquery.Model(
@This(), @This(),
"scrobbles", "scrobbles",
struct { struct {
id: i32, id: i64,
albumsong: i32, albumsong: i64,
datetime: jetquery.DateTime, datetime: jetquery.DateTime,
created_at: jetquery.DateTime, created_at: jetquery.DateTime,
updated_at: jetquery.DateTime, updated_at: jetquery.DateTime,
}, },
.{ .{
.relations = .{ .relations = .{
.albumsong = jetquery.belongsTo(.Albumsong, .{ .albumsong = jetquery.belongsTo(.Albumsong, .{ .foreign_key = "albumsong" }),
.foreign_key = "albumsong",
}),
}, },
}, },
); );
@ -117,7 +114,7 @@ pub const Song = jetquery.Model(
@This(), @This(),
"songs", "songs",
struct { struct {
id: i32, id: i64,
name: []const u8, name: []const u8,
length: ?f32, length: ?f32,
hidden: bool, hidden: bool,
@ -127,6 +124,25 @@ pub const Song = jetquery.Model(
.{ .{
.relations = .{ .relations = .{
.albumsongs = jetquery.hasMany(.Albumsong, .{}), .albumsongs = jetquery.hasMany(.Albumsong, .{}),
.artistsongs = jetquery.hasMany(.Artistsong, .{}),
},
},
);
pub const Artistsong = jetquery.Model(
@This(),
"artistsongs",
struct {
id: i64,
artist_id: i64,
song_id: i64,
created_at: jetquery.DateTime,
updated_at: jetquery.DateTime,
},
.{
.relations = .{
.artist = jetquery.belongsTo(.Artist, .{}),
.song = jetquery.belongsTo(.Song, .{}),
}, },
}, },
); );

View file

@ -6,7 +6,7 @@ pub fn up(repo: anytype) !void {
try repo.createTable( try repo.createTable(
"songs", "songs",
&.{ &.{
t.primaryKey("id", .{}), t.primaryKey("id", .{ .type = .bigint }),
t.column("name", .string, .{}), t.column("name", .string, .{}),
t.column("length", .float, .{ .optional = true }), t.column("length", .float, .{ .optional = true }),
t.column("hidden", .boolean, .{}), t.column("hidden", .boolean, .{}),

View file

@ -6,7 +6,7 @@ pub fn up(repo: anytype) !void {
try repo.createTable( try repo.createTable(
"albums", "albums",
&.{ &.{
t.primaryKey("id", .{}), t.primaryKey("id", .{ .type = .bigint }),
t.column("name", .string, .{}), t.column("name", .string, .{}),
t.column("length", .float, .{ .optional = true }), t.column("length", .float, .{ .optional = true }),
t.timestamps(.{}), t.timestamps(.{}),

View file

@ -6,9 +6,9 @@ pub fn up(repo: anytype) !void {
try repo.createTable( try repo.createTable(
"albumsongs", "albumsongs",
&.{ &.{
t.primaryKey("id", .{}), t.primaryKey("id", .{ .type = .bigint }),
t.column("song_id", .integer, .{ .reference = .{ "songs", "id" } }), t.column("song_id", .bigint, .{ .reference = .{ "songs", "id" } }),
t.column("album_id", .integer, .{ .reference = .{ "albums", "id" } }), t.column("album_id", .bigint, .{ .reference = .{ "albums", "id" } }),
t.timestamps(.{}), t.timestamps(.{}),
}, },
.{}, .{},

View file

@ -6,8 +6,8 @@ pub fn up(repo: anytype) !void {
try repo.createTable( try repo.createTable(
"scrobbles", "scrobbles",
&.{ &.{
t.primaryKey("id", .{}), t.primaryKey("id", .{ .type = .bigint }),
t.column("albumsong", .integer, .{ .reference = .{ "albumsongs", "id" } }), t.column("albumsong", .bigint, .{ .reference = .{ "albumsongs", "id" } }),
t.column("datetime", .datetime, .{}), t.column("datetime", .datetime, .{}),
t.timestamps(.{}), t.timestamps(.{}),
}, },

View file

@ -6,7 +6,7 @@ pub fn up(repo: anytype) !void {
try repo.createTable( try repo.createTable(
"artists", "artists",
&.{ &.{
t.primaryKey("id", .{}), t.primaryKey("id", .{ .type = .bigint }),
t.column("name", .string, .{}), t.column("name", .string, .{}),
t.column("disambiguation", .string, .{ .optional = true }), t.column("disambiguation", .string, .{ .optional = true }),
t.timestamps(.{}), t.timestamps(.{}),

View file

@ -6,9 +6,9 @@ pub fn up(repo: anytype) !void {
try repo.createTable( try repo.createTable(
"albumsongsartists", "albumsongsartists",
&.{ &.{
t.primaryKey("id", .{}), t.primaryKey("id", .{ .type = .bigint }),
t.column("albumsong_id", .integer, .{ .reference = .{ "albumsongs", "id" } }), t.column("albumsong_id", .bigint, .{ .reference = .{ "albumsongs", "id" } }),
t.column("artist_id", .integer, .{ .reference = .{ "artists", "id" } }), t.column("artist_id", .bigint, .{ .reference = .{ "artists", "id" } }),
t.timestamps(.{}), t.timestamps(.{}),
}, },
.{}, .{},

View file

@ -6,9 +6,9 @@ pub fn up(repo: anytype) !void {
try repo.createTable( try repo.createTable(
"artistalbums", "artistalbums",
&.{ &.{
t.primaryKey("id", .{}), t.primaryKey("id", .{ .type = .bigint }),
t.column("album_id", .integer, .{ .reference = .{ "albums", "id" } }), t.column("album_id", .bigint, .{ .reference = .{ "albums", "id" } }),
t.column("artist_id", .integer, .{ .reference = .{ "artists", "id" } }), t.column("artist_id", .bigint, .{ .reference = .{ "artists", "id" } }),
t.timestamps(.{}), t.timestamps(.{}),
}, },
.{}, .{},

View file

@ -0,0 +1,20 @@
const std = @import("std");
const jetquery = @import("jetquery");
const t = jetquery.schema.table;
pub fn up(repo: anytype) !void {
try repo.createTable(
"artistsongs",
&.{
t.primaryKey("id", .{ .type = .bigint }),
t.column("artist_id", .bigint, .{ .reference = .{ "artists", "id" } }),
t.column("song_id", .bigint, .{ .reference = .{ "songs", "id" } }),
t.timestamps(.{}),
},
.{},
);
}
pub fn down(repo: anytype) !void {
try repo.dropTable("artistsongs", .{});
}

View file

@ -20,12 +20,13 @@ pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig
// Probably want to include artist name here, but not sure how to yet // Probably want to include artist name here, but not sure how to yet
const track_artist_count = item.getT(.array, "artists_track").?.count(); const track_artists = item.getT(.array, "artists_track").?.items();
const album_artist_count = item.getT(.array, "artists_album").?.count(); const album_artists = item.getT(.array, "artists_album").?.items();
var track_artist_name_buffer = try allocator.alloc([]const u8, track_artist_count);
var album_artist_name_buffer = try allocator.alloc([]const u8, album_artist_count); var track_artist_name_buffer = try allocator.alloc([]const u8, track_artists.len);
var track_artist_id_buffer = try allocator.alloc(i32, track_artist_count); var album_artist_name_buffer = try allocator.alloc([]const u8, album_artists.len);
var album_artist_id_buffer = try allocator.alloc(i32, album_artist_count); var track_artist_id_buffer = try allocator.alloc(i64, track_artists.len);
var album_artist_id_buffer = try allocator.alloc(i64, album_artists.len);
const scrobble: Data.Scrobble = .{ const scrobble: Data.Scrobble = .{
.track = item.getT(.string, "track").?, .track = item.getT(.string, "track").?,
@ -35,95 +36,99 @@ pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig
.date = @as(i64, @truncate(item.getT(.integer, "date").?)), .date = @as(i64, @truncate(item.getT(.integer, "date").?)),
}; };
var id_prehash = std.ArrayList(u8).init(allocator); var album_hash_string = std.ArrayList(u8).init(allocator);
var track_hash_string = std.ArrayList(u8).init(allocator);
for (item.getT(.array, "artists_track").?.items(), 0..track_artist_count) |artist, i| { // I theoretically don't need this for loop
for (track_artists, 0..track_artists.len) |artist, i| {
const artist_name = try artist.coerce([]const u8); const artist_name = try artist.coerce([]const u8);
track_artist_name_buffer[i] = artist_name; track_artist_name_buffer[i] = artist_name;
track_artist_id_buffer[i] = @as(i32, @bitCast(std.hash.Fnv1a_32.hash(artist_name))); track_artist_id_buffer[i] = @as(i64, @bitCast(std.hash.Fnv1a_64.hash(artist_name)));
} }
for (item.getT(.array, "artists_album").?.items(), 0..album_artist_count) |artist, i| { for (album_artists, 0..album_artists.len) |artist, i| {
const artist_name = try artist.coerce([]const u8); const artist_name = try artist.coerce([]const u8);
album_artist_name_buffer[i] = artist_name; album_artist_name_buffer[i] = artist_name;
album_artist_id_buffer[i] = @as(i32, @bitCast(std.hash.Fnv1a_32.hash(artist_name))); album_artist_id_buffer[i] = @as(i64, @bitCast(std.hash.Fnv1a_32.hash(artist_name)));
try id_prehash.appendSlice(artist_name); try album_hash_string.appendSlice(artist_name);
} }
try id_prehash.appendSlice(scrobble.album); try album_hash_string.appendSlice(scrobble.album);
const album_id = @as(i32, @bitCast(std.hash.Fnv1a_32.hash(id_prehash.items))); try track_hash_string.appendSlice(scrobble.album);
try id_prehash.appendSlice(scrobble.track); const album_hash = @as(i64, @bitCast(std.hash.Fnv1a_32.hash(album_hash_string.items)));
const song_id = @as(i32, @bitCast(std.hash.Fnv1a_32.hash(id_prehash.items))); try track_hash_string.appendSlice(scrobble.track);
const track_hash = @as(i64, @bitCast(std.hash.Fnv1a_32.hash(track_hash_string.items)));
var albumsong = try jetzig.database.Query(.Albumsong) var albumsong_id = try jetzig.database.Query(.Albumsong)
.findBy(.{ .album_id = album_id, .song_id = song_id }) .find(album_hash ^ track_hash)
.select(.{.id}).execute(env.repo); .select(.{.id}).execute(env.repo);
var ins_album = try jetzig.database.Query(.Album) var album_id = try jetzig.database.Query(.Album)
.find(album_id) .find(album_hash)
.select(.{.id}).execute(env.repo); .select(.{.id}).execute(env.repo);
for (track_artist_name_buffer, track_artist_id_buffer) |artist_name, artist_id| { for (track_artist_name_buffer, track_artist_id_buffer) |scrobble_track_artist, track_artist_hash| {
var ins_artist = try jetzig.database.Query(.Artist) var artist_id = try jetzig.database.Query(.Artist)
.find(artist_id) .find(track_artist_hash)
.select(.{.id}).execute(env.repo); .select(.{.id}).execute(env.repo);
if (ins_artist == null) if (artist_id == null)
ins_artist = try jetzig.database.Query(.Artist) artist_id = try jetzig.database.Query(.Artist)
.insert(.{ .id = artist_id, .name = artist_name, .disambiguation = null }) .insert(.{ .id = track_artist_hash, .name = scrobble_track_artist, .disambiguation = null })
.returning(.{.id}).execute(env.repo); .returning(.{.id}).execute(env.repo);
if (albumsong == null) { if (albumsong_id == null) {
var ins_song = try jetzig.database.Query(.Song) var track_id = try jetzig.database.Query(.Song)
.find(song_id) .find(track_hash)
.select(.{.id}).execute(env.repo); .select(.{.id}).execute(env.repo);
if (ins_song == null) if (track_id == null)
ins_song = try jetzig.database.Query(.Song) track_id = try jetzig.database.Query(.Song)
.insert(.{ .id = song_id, .name = scrobble.track, .length = null, .hidden = false }) .insert(.{ .id = track_hash, .name = scrobble.track, .length = null, .hidden = false })
.returning(.{.id}).execute(env.repo); .returning(.{.id}).execute(env.repo);
if (ins_album == null) if (album_id == null)
ins_album = try jetzig.database.Query(.Album) album_id = try jetzig.database.Query(.Album)
.insert(.{ .id = album_id, .name = scrobble.album, .length = null }) .insert(.{ .id = album_hash, .name = scrobble.album, .length = null })
.returning(.{.id}).execute(env.repo); .returning(.{.id}).execute(env.repo);
albumsong = try jetzig.database.Query(.Albumsong) albumsong_id = try jetzig.database.Query(.Albumsong)
.insert(.{ .song_id = ins_song.?.id, .album_id = ins_album.?.id }) .insert(.{ .song_id = track_id.?.id, .album_id = album_id.?.id })
.returning(.{.id}).execute(env.repo); .returning(.{.id}).execute(env.repo);
try jetzig.database.Query(.Albumsongsartist) try jetzig.database.Query(.Albumsongsartist)
.insert(.{ .albumsong_id = albumsong.?.id, .artist_id = ins_artist.?.id }).execute(env.repo); .insert(.{ .albumsong_id = albumsong_id.?.id, .artist_id = artist_id.?.id }).execute(env.repo);
} else { } else {
const ins_albumsongartist = try jetzig.database.Query(.Albumsongsartist) const ins_albumsongartist = try jetzig.database.Query(.Albumsongsartist)
.findBy(.{ .albumsong_id = albumsong.?.id, .artist_id = ins_artist.?.id }) .findBy(.{ .albumsong_id = albumsong_id.?.id, .artist_id = artist_id.?.id })
.select(.{.id}).execute(env.repo); .select(.{.id}).execute(env.repo);
if (ins_albumsongartist == null) if (ins_albumsongartist == null)
try jetzig.database.Query(.Albumsongsartist) try jetzig.database.Query(.Albumsongsartist)
.insert(.{ .albumsong_id = albumsong.?.id, .artist_id = ins_artist.?.id }).execute(env.repo); .insert(.{ .albumsong_id = albumsong_id.?.id, .artist_id = artist_id.?.id }).execute(env.repo);
} }
} }
for (album_artist_name_buffer, album_artist_id_buffer) |artist_name, artist_id| {
const ins_artistalbum = try jetzig.database.Query(.Artistalbum) for (album_artist_name_buffer, album_artist_id_buffer) |scrobble_album_artist, album_artist_hash| {
.findBy(.{ .album_id = ins_album.?.id, .artist_id = artist_id }) const artistalbum_id = try jetzig.database.Query(.Artistalbum)
.findBy(.{ .album_id = album_id.?.id, .artist_id = album_artist_hash })
.select(.{.id}).execute(env.repo); .select(.{.id}).execute(env.repo);
if (ins_artistalbum == null) { if (artistalbum_id == null) {
var ins_artist = try jetzig.database.Query(.Artist) var artist_id = try jetzig.database.Query(.Artist)
.find(artist_id) .find(album_artist_hash)
.select(.{.id}).execute(env.repo); .select(.{.id}).execute(env.repo);
if (ins_artist == null) if (artist_id == null)
ins_artist = try jetzig.database.Query(.Artist) artist_id = try jetzig.database.Query(.Artist)
.insert(.{ .id = artist_id, .name = artist_name, .disambiguation = null }) .insert(.{ .id = artist_id, .name = scrobble_album_artist, .disambiguation = null })
.returning(.{.id}).execute(env.repo); .returning(.{.id}).execute(env.repo);
try jetzig.database.Query(.Artistalbum) try jetzig.database.Query(.Artistalbum)
.insert(.{ .album_id = ins_album.?.id, .artist_id = ins_artist.?.id }).execute(env.repo); .insert(.{ .album_id = album_id.?.id, .artist_id = artist_id.?.id }).execute(env.repo);
} }
} }
try jetzig.database.Query(.Scrobble) try jetzig.database.Query(.Scrobble)
.insert(.{ .albumsong = albumsong.?.id, .datetime = scrobble.date }).execute(env.repo); .insert(.{ .albumsong = albumsong_id.?.id, .datetime = scrobble.date }).execute(env.repo);
} }
} }
} }

View file

@ -1,5 +1,6 @@
@args T: type, table_data: *ZmplValue, columns: T @args T: type, table_data: *ZmplValue, columns: T
<div>
<table> <table>
<thead> <thead>
<tr> <tr>
@ -71,3 +72,4 @@
} }
</tbody> </tbody>
</table> </table>
</div>

View file

@ -9,12 +9,24 @@
</head> </head>
<body> <body>
@partial partials/header @partial partials/header
<div style="text-align:center">
<h1>{{.song.song_name}}</h1> <h1>{{.song.song_name}}</h1>
<div>{{.song.scrobbles}} scrobbles ({{.song.rank}} place)</div> </div>
@partial partials/firstlast_listens(firstlast: .firstlast)
<h3>Yearly Performance</h3> <div style="display:flex;flex-direction:row;justify-content:space-evenly">
@partial partials/timescale(range: .yearly) <div style="display:flex;flex-direction:column;align-self:left">
<h2>Scrobbles</h2> <div>{{.song.scrobbles}} scrobbles ({{.song.rank}} place)</div>
@partial partials/newtable(T: ColumnChoices, table_data: .scrobbles, columns: columns) @partial partials/firstlast_listens(firstlast: .firstlast)
<h3>Yearly Performance</h3>
@partial partials/timescale(range: .yearly)
<h2>Scrobbles</h2>
@partial partials/newtable(T: ColumnChoices, table_data: .scrobbles, columns: columns)
</div>
<div style="display:flex;flex-direction:column;align-self:right">
<h2>Rating</h2>
<input type="text">
<input type="button">
</div>
</div>
</body> </body>
</html> </html>

View file

@ -23,12 +23,6 @@ pub fn entityQueryResult(request: *jetzig.Request, query: GeneratedQuery, args:
return out.get("entity_info").?; return out.get("entity_info").?;
} }
//if (query.query_type == .datestreak) {
// var out: *jetzig.Data.Value = try Data.object();
// const entity = try (try result.next()).?.to(struct { name: []const u8, id: i32, scrobbles: i64, rank: []const u8 }, .{ .dupe = true, .allocator = request.allocator });
//
//}
var out: *jetzig.Data.Value = try Data.array(); var out: *jetzig.Data.Value = try Data.array();
var mapper = result.mapper(UnifiedResult, .{ .dupe = true, .allocator = request.allocator }); var mapper = result.mapper(UnifiedResult, .{ .dupe = true, .allocator = request.allocator });
@ -65,22 +59,22 @@ const GeneratedQuery = struct {
const UnifiedResult = struct { const UnifiedResult = struct {
album_name: ?[]const u8 = null, album_name: ?[]const u8 = null,
album_id: ?i32 = null, album_id: ?i64 = null,
song_name: ?[]const u8 = null, song_name: ?[]const u8 = null,
song_id: ?i32 = null, song_id: ?i64 = null,
artist_name: ?[]const u8 = null, artist_name: ?[]const u8 = null,
artist_id: ?i32 = null, artist_id: ?i64 = null,
scrobbles: ?i64 = null, scrobbles: ?i64 = null,
date: ?[]const u8 = null, date: ?[]const u8 = null,
}; };
const EntityInfoResult = struct { const EntityInfoResult = struct {
album_name: ?[]const u8 = null, album_name: ?[]const u8 = null,
album_id: ?i32 = null, album_id: ?i64 = null,
song_name: ?[]const u8 = null, song_name: ?[]const u8 = null,
song_id: ?i32 = null, song_id: ?i64 = null,
artist_name: ?[]const u8 = null, artist_name: ?[]const u8 = null,
artist_id: ?i32 = null, artist_id: ?i64 = null,
scrobbles: ?i64 = null, scrobbles: ?i64 = null,
date: ?[]const u8 = null, date: ?[]const u8 = null,
rank: []const u8, rank: []const u8,

View file

@ -133,5 +133,5 @@ pub const TableRow = struct {
pub const HyperlinkData = struct { pub const HyperlinkData = struct {
name: []const u8, name: []const u8,
id: i32, id: i64,
}; };