Begin album reviews

Album reviews would ideally allow you to rate tracks at the same time, so we'll have to work on that next. Also, disambiguation pages are becoming more and more necessary (Little Talks in inaccessible atm) Preferably, we start working on the `INDEX` for `/ratings` as well, and maybe use a unified language for these things (is it review, rating, rating_text, score,...?)
This commit is contained in:
mitteneer 2025-06-24 00:05:25 -04:00
parent 9f27fad235
commit 0b7efc3420
6 changed files with 98 additions and 22 deletions

View file

@ -4,6 +4,7 @@ const jetquery = @import("jetzig").jetquery;
const TableRow = @import("../../types.zig").TableRow; const TableRow = @import("../../types.zig").TableRow;
const HyperlinkData = @import("../../types.zig").HyperlinkData; const HyperlinkData = @import("../../types.zig").HyperlinkData;
const queries = @import("../../queries.zig"); const queries = @import("../../queries.zig");
const decode = @import("../../date_fmt.zig").urlDecode;
pub fn index(request: *jetzig.Request) !jetzig.View { pub fn index(request: *jetzig.Request) !jetzig.View {
var root = try request.data(.object); var root = try request.data(.object);
@ -15,19 +16,47 @@ pub fn index(request: *jetzig.Request) !jetzig.View {
} }
pub fn get(id: []const u8, request: *jetzig.Request) !jetzig.View { pub fn get(id: []const u8, request: *jetzig.Request) !jetzig.View {
const parse_err = blk: {
const rdr_id = std.fmt.parseInt(i64, id, 10) catch |err| break :blk err;
const album = try jetzig.database.Query(.Album).find(rdr_id).execute(request.repo);
if (album == null) break :blk error.InvalidCharacter;
var name = std.ArrayList(u8).init(request.allocator);
try name.appendSlice("http://127.0.0.1:8080/albums/");
try name.appendSlice(album.?.name);
return request.redirect(try name.toOwnedSlice(), .found);
};
const id_int = switch (parse_err) {
error.Overflow => return request.fail(.not_found),
error.InvalidCharacter => blk: {
const rn = try decode(request.allocator, id);
std.log.debug("{s}", .{rn});
const songs = try jetzig.database.Query(.Album).where(.{ .name = rn }).all(request.repo);
if (songs.len == 0) return request.fail(.not_found);
if (songs.len > 1) return request.redirect("http://127.0.0.1:8080", .found);
break :blk songs[0].id;
},
};
var root = try request.data(.object); var root = try request.data(.object);
const album = try queries.entityQueryResult(request, queries.loadQuery(.album, .entity_info), .{id}); const album = try queries.entityQueryResult(request, queries.loadQuery(.album, .entity_info), .{id_int});
try root.put("album", album); try root.put("album", album);
const songs = try queries.entityQueryResult(request, queries.loadQuery(.album, .get_songs), .{id}); const scrobbles = try queries.entityQueryResult(request, queries.loadQuery(.song, .get_scrobbles), .{id_int});
try root.put("scrobbles", scrobbles);
const songs = try queries.entityQueryResult(request, queries.loadQuery(.album, .get_songs), .{id_int});
try root.put("songs", songs); try root.put("songs", songs);
const firstlast = try queries.entityQueryResult(request, queries.loadQuery(.album, .firstlast), .{id}); const firstlast = try queries.entityQueryResult(request, queries.loadQuery(.album, .firstlast), .{id_int});
try root.put("firstlast", firstlast); try root.put("firstlast", firstlast);
const timescale = try queries.entityQueryResult(request, queries.loadQuery(.album, .timescale), .{id}); const timescale = try queries.entityQueryResult(request, queries.loadQuery(.album, .timescale), .{id_int});
try root.put("yearly", timescale); try root.put("yearly", timescale);
const ratings = try queries.entityQueryResult(request, queries.loadQuery(.song, .get_ratings), .{id_int});
try root.put("reviews", ratings);
return request.render(.ok); return request.render(.ok);
} }

View file

@ -1,22 +1,48 @@
@zig { @zig {
const ColumnChoices = []const enum{song, album, artist, artistlist, scrobbles, date}; const ColumnChoices = []const enum{song, album, artist, artistlist, scrobbles, date};
const columns: ColumnChoices = &.{.song, .scrobbles}; const columns: ColumnChoices = &.{.song, .scrobbles};
const reviews = try zmpl.coerceArray(".reviews");
} }
<html> <html>
<head> <head>
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.5/dist/htmx.min.js" integrity="sha384-t4DxZSyQK+0Uv4jzy5B0QyHyWQD2GFURUmxKMBVww9+e2EJ0ei/vCvv7+79z0fkr" crossorigin="anonymous"></script>
<meta charset="UTF-8"> <meta charset="UTF-8">
</head> </head>
<body> <body>
@partial partials/header @partial partials/header
<div style="text-align:center">
<h1>{{.album.album_name}}</h1> <h1>{{.album.album_name}}</h1>
<h2><a href="/artists/{{.album.artist_id}}">{{.album.artist_name}}</a></h2> <h2><a href="/artists/{{.album.artist_id}}">{{.album.artist_name}}</a></h2>
<div>{{.album.scrobbles}} scrobbles ({{.album.rank}} place)</div> </div>
<div>{{.album.song_num}} songs</div>
@partial partials/firstlast_listens(firstlast: .firstlast) <div style="display:flex;flex-direction:row;justify-content:space-evenly">
<h3>Yearly Performance</h3> <div style="display:flex;flex-direction:column;align-self:left">
@partial partials/timescale(range: .yearly) <div>{{.album.scrobbles}} scrobbles ({{.album.rank}} place)</div>
<h2>Songs</h2> <div>{{.album.song_num}} songs</div>
@partial partials/newtable(T: ColumnChoices, table_data: .songs, columns: columns) @partial partials/firstlast_listens(firstlast: .firstlast)
<h3>Yearly Performance</h3>
@partial partials/timescale(range: .yearly)
<h2>Songs</h2>
@partial partials/newtable(T: ColumnChoices, table_data: .songs, columns: columns)
</div>
<div style="display:flex;flex-direction:column;align-self:right">
<h2>Rating</h2>
<div id="review-container">
@zig {
if (reviews.len == 0) {
<form>
<input type="number" name="score" id="score" style="width:50px;height:30px">
<textarea name="review" id="review" style="width:350px;height:100px"></textarea>
<button hx-post="/ratings/albums" hx-vals='{"album_id":"{{.album.album_id}}"}' hx-target="#review-container" style="width:50px;height:30px">Post</button>
</form>
} else {
for (reviews) |review| {
<b>{{review.score}}</b>: {{review.review}} ({{review.date}})
}
}
}
</div>
</div>
</body> </body>
</html> </html>

View file

@ -0,0 +1,14 @@
const std = @import("std");
const jetzig = @import("jetzig");
pub fn post(request: *jetzig.Request) !jetzig.View {
var root = try request.data(.object);
const params = try request.params();
const id = params.getT(.integer, "album_id").?;
const score = if (params.getT(.integer, "score")) |score| @as(i16, @truncate(score)) else null;
const review = params.getT(.string, "review");
try jetzig.database.Query(.Albumrating).insert(.{ .album = id, .rating = score, .rating_text = review, .date = @divFloor(request.start_time, 1_000) }).execute(request.repo);
try root.put("score", score);
try root.put("review", review);
return request.render(.created);
}

View file

@ -0,0 +1 @@
<b>{{.score}}</b>: {{.review}} (Today)

View file

@ -28,17 +28,17 @@
<h2>Rating</h2> <h2>Rating</h2>
<div id="review-container"> <div id="review-container">
@zig { @zig {
if (reviews.len == 0) { if (reviews.len == 0) {
<form> <form>
<input type="number" name="score" id="score" style="width:50px;height:30px"> <input type="number" name="score" id="score" style="width:50px;height:30px">
<textarea name="review" id="review" style="width:350px;height:100px"></textarea> <textarea name="review" id="review" style="width:350px;height:100px"></textarea>
<button hx-post="/ratings/songs" hx-vals='{"song_id":"{{.song.song_id}}"}' hx-target="#review-container" style="width:50px;height:30px">Post</button> <button hx-post="/ratings/songs" hx-vals='{"song_id":"{{.song.song_id}}"}' hx-target="#review-container" style="width:50px;height:30px">Post</button>
</form> </form>
} else { } else {
for (reviews) |review| { for (reviews) |review| {
<b>{{review.score}}</b>: {{review.review}} ({{review.date}}) <b>{{review.score}}</b>: {{review.review}} ({{review.date}})
} }
} }
} }
</div> </div>
</div> </div>

View file

@ -446,6 +446,12 @@ pub fn loadQuery(entity: EntityType, query_type: QueryTypeEnum) GeneratedQuery {
\\WHERE song = $1 \\WHERE song = $1
\\ORDER BY date DESC; \\ORDER BY date DESC;
, ,
.album =>
\\SELECT rating AS score, rating_text AS review, TO_CHAR(date, 'YYY-MM-DD') AS date
\\FROM albumratings
\\WHERE album = $1
\\ORDER BY date DESC;
,
else => unreachable, else => unreachable,
}, },
}, },