Merge remote-tracking branch 'refs/remotes/origin/rawsql' into rawsql

This commit is contained in:
mitteneer 2025-06-11 09:28:14 -04:00
commit df8f01525e
18 changed files with 253 additions and 138 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).
**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
the functionality of the aforementioned inspirations.
@ -12,6 +11,35 @@ Zuletzt means "last" in German.
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:
- [ ] Entity statistics
- [x] See all artists under "/artists"
@ -60,6 +88,7 @@ Licensed under MIT.
- [ ] Rank songs
- [ ] Custom statistics[^7]
- [ ] "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).

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,9 +6,9 @@ pub fn up(repo: anytype) !void {
try repo.createTable(
"artistalbums",
&.{
t.primaryKey("id", .{}),
t.column("album_id", .integer, .{ .reference = .{ "albums", "id" } }),
t.column("artist_id", .integer, .{ .reference = .{ "artists", "id" } }),
t.primaryKey("id", .{ .type = .bigint }),
t.column("album_id", .bigint, .{ .reference = .{ "albums", "id" } }),
t.column("artist_id", .bigint, .{ .reference = .{ "artists", "id" } }),
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
const track_artist_count = item.getT(.array, "artists_track").?.count();
const album_artist_count = item.getT(.array, "artists_album").?.count();
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_id_buffer = try allocator.alloc(i32, track_artist_count);
var album_artist_id_buffer = try allocator.alloc(i32, album_artist_count);
const track_artists = item.getT(.array, "artists_track").?.items();
const album_artists = item.getT(.array, "artists_album").?.items();
var track_artist_name_buffer = try allocator.alloc([]const u8, track_artists.len);
var album_artist_name_buffer = try allocator.alloc([]const u8, album_artists.len);
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 = .{
.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").?)),
};
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);
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);
album_artist_name_buffer[i] = artist_name;
album_artist_id_buffer[i] = @as(i32, @bitCast(std.hash.Fnv1a_32.hash(artist_name)));
try id_prehash.appendSlice(artist_name);
album_artist_id_buffer[i] = @as(i64, @bitCast(std.hash.Fnv1a_32.hash(artist_name)));
try album_hash_string.appendSlice(artist_name);
}
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)));
try album_hash_string.appendSlice(scrobble.album);
try track_hash_string.appendSlice(scrobble.album);
const album_hash = @as(i64, @bitCast(std.hash.Fnv1a_32.hash(album_hash_string.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)
.findBy(.{ .album_id = album_id, .song_id = song_id })
var albumsong_id = try jetzig.database.Query(.Albumsong)
.find(album_hash ^ track_hash)
.select(.{.id}).execute(env.repo);
var ins_album = try jetzig.database.Query(.Album)
.find(album_id)
var album_id = try jetzig.database.Query(.Album)
.find(album_hash)
.select(.{.id}).execute(env.repo);
for (track_artist_name_buffer, track_artist_id_buffer) |artist_name, artist_id| {
var ins_artist = try jetzig.database.Query(.Artist)
.find(artist_id)
for (track_artist_name_buffer, track_artist_id_buffer) |scrobble_track_artist, track_artist_hash| {
var artist_id = try jetzig.database.Query(.Artist)
.find(track_artist_hash)
.select(.{.id}).execute(env.repo);
if (ins_artist == null)
ins_artist = try jetzig.database.Query(.Artist)
.insert(.{ .id = artist_id, .name = artist_name, .disambiguation = null })
if (artist_id == null)
artist_id = try jetzig.database.Query(.Artist)
.insert(.{ .id = track_artist_hash, .name = scrobble_track_artist, .disambiguation = null })
.returning(.{.id}).execute(env.repo);
if (albumsong == null) {
var ins_song = try jetzig.database.Query(.Song)
.find(song_id)
if (albumsong_id == null) {
var track_id = try jetzig.database.Query(.Song)
.find(track_hash)
.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 })
if (track_id == null)
track_id = try jetzig.database.Query(.Song)
.insert(.{ .id = track_hash, .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 })
if (album_id == null)
album_id = try jetzig.database.Query(.Album)
.insert(.{ .id = album_hash, .name = scrobble.album, .length = null })
.returning(.{.id}).execute(env.repo);
albumsong = try jetzig.database.Query(.Albumsong)
.insert(.{ .song_id = ins_song.?.id, .album_id = ins_album.?.id })
albumsong_id = try jetzig.database.Query(.Albumsong)
.insert(.{ .song_id = track_id.?.id, .album_id = album_id.?.id })
.returning(.{.id}).execute(env.repo);
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 {
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);
if (ins_albumsongartist == null)
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)
.findBy(.{ .album_id = ins_album.?.id, .artist_id = artist_id })
for (album_artist_name_buffer, album_artist_id_buffer) |scrobble_album_artist, album_artist_hash| {
const artistalbum_id = try jetzig.database.Query(.Artistalbum)
.findBy(.{ .album_id = album_id.?.id, .artist_id = album_artist_hash })
.select(.{.id}).execute(env.repo);
if (ins_artistalbum == null) {
var ins_artist = try jetzig.database.Query(.Artist)
.find(artist_id)
if (artistalbum_id == null) {
var artist_id = try jetzig.database.Query(.Artist)
.find(album_artist_hash)
.select(.{.id}).execute(env.repo);
if (ins_artist == null)
ins_artist = try jetzig.database.Query(.Artist)
.insert(.{ .id = artist_id, .name = artist_name, .disambiguation = null })
if (artist_id == null)
artist_id = try jetzig.database.Query(.Artist)
.insert(.{ .id = artist_id, .name = scrobble_album_artist, .disambiguation = null })
.returning(.{.id}).execute(env.repo);
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)
.insert(.{ .albumsong = albumsong.?.id, .datetime = scrobble.date }).execute(env.repo);
.insert(.{ .albumsong = albumsong_id.?.id, .datetime = scrobble.date }).execute(env.repo);
}
}
}

View file

@ -20,7 +20,7 @@ pub fn get(id: []const u8, request: *jetzig.Request) !jetzig.View {
const album = try queries.entityQueryResult(request, queries.loadQuery(.album, .entity_info), .{id});
try root.put("album", album);
const songs = try queries.entityQueryResult(request, queries.loadQuery(.album, .entity_items), .{id});
const songs = try queries.entityQueryResult(request, queries.loadQuery(.album, .get_songs), .{id});
try root.put("songs", songs);
const firstlast = try queries.entityQueryResult(request, queries.loadQuery(.album, .firstlast), .{id});

View file

@ -21,7 +21,7 @@ pub fn get(id: []const u8, request: *jetzig.Request) !jetzig.View {
const artist = try queries.entityQueryResult(request, queries.loadQuery(.artist, .entity_info), .{id});
try root.put("artist", artist);
const albums = try queries.entityQueryResult(request, queries.loadQuery(.artist, .entity_items), .{id});
const albums = try queries.entityQueryResult(request, queries.loadQuery(.artist, .get_albums), .{id});
try root.put("albums", albums);
const appears = try queries.entityQueryResult(request, queries.loadQuery(.artist, .appears), .{id});

View file

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

View file

@ -25,11 +25,11 @@ pub fn get(id: []const u8, request: *jetzig.Request) !jetzig.View {
const song = try queries.entityQueryResult(request, queries.loadQuery(.song, .entity_info), .{id});
try root.put("song", song);
const scrobbles = try queries.entityQueryResult(request, queries.loadQuery(.song, .entity_items), .{id});
const scrobbles = try queries.entityQueryResult(request, queries.loadQuery(.song, .get_scrobbles), .{id});
try root.put("scrobbles", scrobbles);
const appears = try queries.entityQueryResult(request, queries.loadQuery(.song, .appears), .{id});
try root.put("appears", appears);
const albums = try queries.entityQueryResult(request, queries.loadQuery(.song, .get_albums), .{id});
try root.put("albums", albums);
const firstlast = try queries.entityQueryResult(request, queries.loadQuery(.song, .firstlast), .{id});
try root.put("firstlast", firstlast);

View file

@ -9,12 +9,24 @@
</head>
<body>
@partial partials/header
<div style="text-align:center">
<h1>{{.song.song_name}}</h1>
<div>{{.song.scrobbles}} scrobbles ({{.song.rank}} place)</div>
@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:row;justify-content:space-evenly">
<div style="display:flex;flex-direction:column;align-self:left">
<div>{{.song.scrobbles}} scrobbles ({{.song.rank}} place)</div>
@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>
</html>

View file

@ -23,12 +23,6 @@ pub fn entityQueryResult(request: *jetzig.Request, query: GeneratedQuery, args:
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 mapper = result.mapper(UnifiedResult, .{ .dupe = true, .allocator = request.allocator });
@ -55,7 +49,7 @@ pub fn entityQueryResult(request: *jetzig.Request, query: GeneratedQuery, args:
}
const EntityType = enum { scrobble, song, album, artist };
const QueryTypeEnum = enum { firstlast, timescale, entities, entity_items, appears, entity_info, datestreak, entities_by_name };
const QueryTypeEnum = enum { firstlast, timescale, entities, get_songs, get_albums, get_scrobbles, appears, entity_info, datestreak, entities_by_name };
const GeneratedQuery = struct {
entity: EntityType,
@ -65,22 +59,22 @@ const GeneratedQuery = struct {
const UnifiedResult = struct {
album_name: ?[]const u8 = null,
album_id: ?i32 = null,
album_id: ?i64 = null,
song_name: ?[]const u8 = null,
song_id: ?i32 = null,
song_id: ?i64 = null,
artist_name: ?[]const u8 = null,
artist_id: ?i32 = null,
artist_id: ?i64 = null,
scrobbles: ?i64 = null,
date: ?[]const u8 = null,
};
const EntityInfoResult = struct {
album_name: ?[]const u8 = null,
album_id: ?i32 = null,
album_id: ?i64 = null,
song_name: ?[]const u8 = null,
song_id: ?i32 = null,
song_id: ?i64 = null,
artist_name: ?[]const u8 = null,
artist_id: ?i32 = null,
artist_id: ?i64 = null,
scrobbles: ?i64 = null,
date: ?[]const u8 = null,
rank: []const u8,
@ -247,20 +241,9 @@ pub fn loadQuery(entity: EntityType, query_type: QueryTypeEnum) GeneratedQuery {
},
.appears =>
//.ResultType = AppearsResult,
// Not sure how I feel about this one
switch (entity) {
.scrobble, .album => unreachable,
.song =>
\\SELECT albums.name AS album_name, albums.id AS album_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
\\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id
\\INNER JOIN albumsongsartists ON albumsongsartists.albumsong_id = albumsongs.id
\\WHERE albumsongs.song_id = $1
\\GROUP BY albums.id
\\ORDER BY scrobbles DESC;
,
.scrobble, .song, .album => unreachable,
.artist =>
\\SELECT albums.name AS album_name, albums.id AS album_id, COUNT(scrobbles) AS scrobbles
\\FROM artistalbums
@ -274,19 +257,8 @@ pub fn loadQuery(entity: EntityType, query_type: QueryTypeEnum) GeneratedQuery {
,
},
.entity_items =>
//.ResultType = EntityItemsResult,
switch (entity) {
.scrobble => unreachable,
.song =>
\\SELECT songs.name AS song_name, songs.id AS song_id, albums.name AS album_name, albums.id AS album_id, TO_CHAR(scrobbles.datetime, 'YYYY-MM-DD HH24:MI:SS') AS date
\\FROM albumsongs
\\INNER JOIN albums ON albums.id = albumsongs.album_id
\\INNER JOIN songs ON songs.id = albumsongs.song_id
\\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id
\\WHERE songs.id = $1
\\ORDER BY date ASC
,
.get_songs => switch (entity) {
.scrobble, .song => unreachable, // Might be able to use this with SongGroups?
.album =>
\\SELECT songs.name AS song_name, songs.id AS song_id, COUNT(scrobbles) AS scrobbles
\\FROM albumsongs
@ -297,6 +269,34 @@ pub fn loadQuery(entity: EntityType, query_type: QueryTypeEnum) GeneratedQuery {
\\ORDER BY scrobbles DESC
,
.artist =>
\\SELECT songs.name AS song_name, songs.id AS song_id, albums.name AS album_name, albums.id AS album_id, COUNT(scrobbles) AS scrobbles
\\FROM albumsongs
\\INNER JOIN songs ON songs.id = albumsongs.song_id
\\INNER JOIN albums ON albums.id = albumsongs.album_id
\\INNER JOIN albumsongsartists ON albumsongsartists.albumsong_id = albumsongs.id
\\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id
\\WHERE albumsongsartists.artist_id = $1
\\GROUP BY songs.id, albums.id
\\ORDER BY scrobbles DESC
,
},
.get_albums =>
//.ResultType = EntityItemsResult,
switch (entity) {
.scrobble, .album => unreachable, // Might be able to use this with ReleaseGroups?
.song =>
\\SELECT albums.name AS album_name, albums.id AS album_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
\\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id
\\INNER JOIN albumsongsartists ON albumsongsartists.albumsong_id = albumsongs.id
\\WHERE albumsongs.song_id = $1
\\GROUP BY albums.id
\\ORDER BY scrobbles DESC
,
.artist =>
\\SELECT albums.name AS album_name, albums.id AS album_id, COUNT(scrobbles) AS scrobbles
\\FROM artistalbums
\\INNER JOIN albums ON albums.id = artistalbums.album_id
@ -308,6 +308,37 @@ pub fn loadQuery(entity: EntityType, query_type: QueryTypeEnum) GeneratedQuery {
,
},
.get_scrobbles => switch (entity) {
.scrobble => unreachable,
.song =>
\\SELECT songs.name AS song_name, songs.id AS song_id, albums.name AS album_name, albums.id AS album_id, TO_CHAR(scrobbles.datetime, 'YYYY-MM-DD HH24:MI:SS') AS date
\\FROM albumsongs
\\INNER JOIN albums ON albums.id = albumsongs.album_id
\\INNER JOIN songs ON songs.id = albumsongs.song_id
\\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id
\\WHERE songs.id = $1
\\ORDER BY date ASC
,
.album =>
\\SELECT songs.name AS song_name, songs.id AS song_id, albums.name AS album_name, albums.id AS album_id, TO_CHAR(scrobbles.datetime, 'YYYY-MM-DD HH24:MI:SS') AS date
\\FROM albumsongs
\\INNER JOIN albums ON albums.id = albumsongs.album_id
\\INNER JOIN songs ON songs.id = albumsongs.song_id
\\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id
\\WHERE albums.id = $1
\\ORDER BY date ASC
,
.artist =>
\\SELECT songs.name AS song_name, songs.id AS song_id, albums.name AS album_name, albums.id AS album_id, TO_CHAR(scrobbles.datetime, 'YYYY-MM-DD HH24:MI:SS') AS date
\\FROM albumsongs
\\INNER JOIN albums ON albums.id = albumsongs.album_id
\\INNER JOIN songs ON songs.id = albumsongs.song_id
\\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id
\\WHERE artists.id = $1
\\ORDER BY date ASC
,
},
.entity_info => switch (entity) {
.scrobble => unreachable,
.song =>

View file

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