diff --git a/.gitignore b/.gitignore
index 76f5b26..70b0239 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,5 +6,4 @@ static/
src/app/database/data.db-journal
src/app/database/old_migrations/
src/lib
-src/app/scripts/
-rules.json
\ No newline at end of file
+src/app/scripts/
\ No newline at end of file
diff --git a/README.md b/README.md
index 06f45c2..b62b576 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,7 @@
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.
@@ -11,49 +12,17 @@ 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"
- [ ] List all songs on artist page, with respective album
- [x] List all albums on artist page
- [x] Include number of plays for each
- - [x] List albums features on
- [x] See all albums under "/albums"
- [x] See all songs from album
- [x] Include number of plays
- - [x] Include name of artist(s)
- - [ ] Include artists features on each song
- [x] See all songs under "/songs"
- - [x] Include respective artist(s)
+ - [ ] Include respective artist(s)
- [ ] Include respective album[^10]
- [x] Include number of plays
- [ ] Create disambiguation pages
@@ -69,7 +38,7 @@ If two artists have the same name, they are necessarily listed as the same artis
- [ ] Import from Discogs[^2]
- [ ] Import listening history
- [x] From Lastfmstats.com (.json file)[^3]
- - [x] From Last.fm (authentication)
+ - [ ] From Last.fm (authentication)
- [x] From Spotify (.json file)
- [ ] From other streaming services[^4]
- [ ] "Unofficial scrobbles"[^9]
@@ -88,7 +57,6 @@ If two artists have the same name, they are necessarily listed as the same artis
- [ ] 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).
diff --git a/build.zig.zon b/build.zig.zon
index 5db05e3..823b42c 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -17,12 +17,12 @@
// internet connectivity.
.dependencies = .{
.jetzig = .{
- .url = "https://github.com/jetzig-framework/jetzig/archive/695664bc10e6e73389ee2fe7fbcaedc27fa8adc9.tar.gz",
- .hash = "jetzig-0.0.0-IpAgLSFeDwBEhfEaDBo0JqhRbvQA3ScbWjssBqhJHFmb",
+ .url = "https://github.com/jetzig-framework/jetzig/archive/2c52792217b9441ed5e91d67e7ec5a8959285307.tar.gz",
+ .hash = "jetzig-0.0.0-IpAgLbMeDwDYRqWu0OJixGbcDUoUbfAN0xGe1xsYRHTj",
},
.zeit = .{
- .url = "https://github.com/rockorager/zeit/archive/refs/tags/v0.6.0.tar.gz",
- .hash = "zeit-0.0.0-5I6bk_pZAgB03N1p1GmVOZ--gOFwwQSRKj1UXb5tnaKS",
+ .url = "https://github.com/rockorager/zeit/archive/refs/heads/main.tar.gz",
+ .hash = "zeit-0.6.0-5I6bk5daAgC-P60TjxRqW0bYknfCGxJp-03eS9UjGrO7",
},
},
.paths = .{
diff --git a/common_queries.md b/common_queries.md
new file mode 100644
index 0000000..833d2c0
--- /dev/null
+++ b/common_queries.md
@@ -0,0 +1,164 @@
+Get all albums from specified artist:
+```sql
+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 = {ARTIST};
+```
+
+Get all songs from specified artist:
+```sql
+SELECT artists.name, songs.name
+FROM "Songartists"
+INNER JOIN artists
+ON "Songartists".artist_id = artists.id
+INNER JOIN songs
+ON "Songartists".song_id = songs.id
+WHERE artists.name = {ARTIST};
+```
+
+Get all songs from any album of the specified name:
+```sql
+SELECT songs.name
+FROM "Albumsongs"
+INNER JOIN albums
+ON "Albumsongs".album_id = albums.id
+INNER JOIN songs
+ON "Albumsongs".song_id = songs.id
+WHERE albums.name = {ALBUM};
+```
+
+Sort all songs by plays (does not list artist or album):
+```sql
+SELECT songs.name, COUNT(scrobbles.song_id) AS scount
+FROM songs, scrobbles
+WHERE songs.id = scrobbles.song_id
+GROUP BY songs.id
+ORDER BY scount DESC;
+```
+
+Sort all songs by plays, and include artist:
+```sql
+SELECT songs.name, artists.name, COUNT(scrobbles.song_id) AS scount
+FROM scrobbles, "Songartists"
+INNER JOIN artists
+ON "Songartists".artist_id = artists.id
+INNER JOIN songs
+ON "Songartists".song_id = songs.id
+WHERE songs.id = scrobbles.song_id
+GROUP BY songs.id, artists.id
+ORDER BY scount DESC;
+```
+
+Sort all songs by plays, and include artist and album:
+```sql
+SELECT songs.name, artists.name, albums.name, COUNT(scrobbles.song_id) AS scount
+FROM scrobbles CROSS JOIN "Songartists" CROSS JOIN "Albumsongs"
+INNER JOIN artists
+ON "Songartists".artist_id = artists.id
+INNER JOIN songs
+ON "Songartists".song_id = songs.id AND "Albumsongs".song_id = songs.id
+INNER JOIN albums
+ON "Albumsongs".album_id = albums.id
+WHERE songs.id = scrobbles.song_id
+GROUP BY songs.id, artists.id, albums.id
+ORDER BY scount DESC;
+```
+
+Sort all albums by plays, and include artist:
+```sql
+SELECT albums.name, artists.name, COUNT(scrobbles.album_id) AS scount
+FROM scrobbles, "Albumartists"
+INNER JOIN albums
+ON "Albumartists".album_id = albums.id
+INNER JOIN artists
+ON "Albumartists".artist_id = artists.id
+WHERE albums.id = scrobbles.album_id
+GROUP BY artists.id, albums.id
+ORDER BY scount DESC;
+```
+
+Sort all artists by plays:
+```sql
+SELECT artists.name, COUNT(scrobbles.id) AS scount
+FROM artists, "Scrobbleartists"
+INNER JOIN scrobbles
+ON scrobbles.id = "Scrobbleartists".scrobble_id
+WHERE "Scrobbleartists".artist_id = artists.id
+GROUP BY artists.id
+ORDER BY scount DESC;
+```
+
+Sort all artists by alphabetical order, and include the first time you listened to that artist:
+```sql
+SELECT artists.name, MIN(scrobbles.date)
+FROM "Scrobbleartists"
+INNER JOIN artists
+ON "Scrobbleartists".artist_id = artists.id
+INNER JOIN scrobbles
+ON "Scrobbleartists".scrobble_id = scrobbles.id
+GROUP BY artists.id
+ORDER BY artists.name ASC;
+```
+
+Sort all songs by alphabetical order, and include the first time you listened to that song:
+```sql
+SELECT songs.name, MIN(scrobbles.date)
+FROM scrobbles
+INNER JOIN songs
+ON scrobbles.song_id = songs.id
+GROUP BY songs.id
+ORDER BY songs.name ASC;
+```
+
+Sort all albums by alphabetical order, and include the first time you listened to that album:
+```sql
+SELECT albums.name, MIN(scrobbles.date)
+FROM scrobbles
+INNER JOIN albums
+ON scrobbles.album_id = albums.id
+GROUP BY albums.id
+ORDER BY albums.name ASC;
+```
+
+Select all songs by specified artists, include the number of plays of each song, and sort by plays:
+```sql
+SELECT songs.name, COUNT(scrobbles.song_id) as count
+FROM songs, "Scrobbleartists"
+INNER JOIN artists
+ON "Scrobbleartists".artist_id = artists.id
+INNER JOIN scrobbles
+ON "Scrobbleartists".scrobble_id = scrobbles.id
+WHERE songs.id = scrobbles.song_id AND artists.name = {ARTIST}
+GROUP BY songs.id
+ORDER BY count DESC;
+```
+
+Select all albums by specified artist, include the number of plays of each album, and sort by plays:
+```sql
+SELECT albums.name, COUNT(scrobbles.song_id) as count
+FROM albums, "Scrobbleartists"
+INNER JOIN artists
+ON "Scrobbleartists".artist_id = artists.id
+INNER JOIN scrobbles
+ON "Scrobbleartists".scrobble_id = scrobbles.id
+WHERE albums.id = scrobbles.album_id AND artists.name = {ARTIST}
+GROUP BY albums.id
+ORDER BY count DESC;
+```
+
+Select all songs from an album specified by an ID, and sort by plays
+```sql
+SELECT songs.name, COUNT(scrobbles.song_id) AS count
+FROM "Albumsongs"
+INNER JOIN songs
+ON songs.id = "Albumsongs".song_id
+INNER JOIN scrobbles
+ON scrobbles.song_id = "Albumsongs".song_id
+WHERE "Albumsongs".album_id = {ALBUM_ID}
+GROUP BY songs.id
+ORDER BY count DESC;
+```
\ No newline at end of file
diff --git a/config/database.zig b/config/database.zig
index 4042ff0..361129d 100644
--- a/config/database.zig
+++ b/config/database.zig
@@ -15,7 +15,7 @@ pub const database = .{
.port = 5432,
.username = "postgres",
.password = "postgres",
- .database = "zuletzt_rsql",
+ .database = "zuletzt_dev",
.pool_size = 16,
},
diff --git a/src/app/database/Schema.zig b/src/app/database/Schema.zig
index 0a30a19..231c283 100644
--- a/src/app/database/Schema.zig
+++ b/src/app/database/Schema.zig
@@ -4,7 +4,7 @@ pub const Album = jetquery.Model(
@This(),
"albums",
struct {
- id: i64,
+ id: i32,
name: []const u8,
length: ?f32,
created_at: jetquery.DateTime,
@@ -12,19 +12,91 @@ pub const Album = jetquery.Model(
},
.{
.relations = .{
+ .masteralbum = jetquery.belongsTo(.Masteralbum, .{}),
+ .scrobbles = jetquery.hasMany(.Scrobble, .{}),
+ .ratings = jetquery.hasMany(.Rating, .{}),
+ .aliases = jetquery.hasMany(.Alias, .{}),
.albumsongs = jetquery.hasMany(.Albumsong, .{}),
- .artistalbums = jetquery.hasMany(.Artistalbum, .{}),
+ .albumartists = jetquery.hasMany(.Albumartist, .{}),
},
},
);
-pub const Albumsong = jetquery.Model(
+pub const Alias = jetquery.Model(
@This(),
- "albumsongs",
+ "aliases",
struct {
- id: i64,
- song_id: i64,
- album_id: i64,
+ id: i32,
+ reference_id: i32,
+ alias: []const u8,
+ created_at: jetquery.DateTime,
+ updated_at: jetquery.DateTime,
+ },
+ .{},
+);
+
+pub const Artist = jetquery.Model(
+ @This(),
+ "artists",
+ struct {
+ id: i32,
+ name: []const u8,
+ descriptive_string: []const u8,
+ created_at: jetquery.DateTime,
+ updated_at: jetquery.DateTime,
+ },
+ .{
+ .relations = .{
+ .scrobbleartists = jetquery.hasMany(.Scrobbleartist, .{}),
+ .aliases = jetquery.hasMany(.Alias, .{}),
+ .artistsongs = jetquery.hasMany(.Songartist, .{}),
+ .mastersongs = jetquery.hasMany(.Mastersong, .{}),
+ .artistalbums = jetquery.hasMany(.Albumartist, .{}),
+ .masteralbums = jetquery.hasMany(.Masteralbum, .{}),
+ },
+ },
+);
+
+pub const Masteralbum = jetquery.Model(
+ @This(),
+ "masteralbums",
+ struct {
+ id: i32,
+ name: []const u8,
+ created_at: jetquery.DateTime,
+ updated_at: jetquery.DateTime,
+ },
+ .{
+ .relations = .{
+ .albums = jetquery.hasMany(.Album, .{}),
+ },
+ },
+);
+
+pub const Mastersong = jetquery.Model(
+ @This(),
+ "mastersongs",
+ struct {
+ id: i32,
+ name: []const u8,
+ created_at: jetquery.DateTime,
+ updated_at: jetquery.DateTime,
+ },
+ .{
+ .relations = .{
+ .songs = jetquery.hasMany(.Song, .{}),
+ },
+ },
+);
+
+pub const Rating = jetquery.Model(
+ @This(),
+ "ratings",
+ struct {
+ id: i32,
+ reference_id: i32,
+ score: f32,
+ date: jetquery.DateTime,
created_at: jetquery.DateTime,
updated_at: jetquery.DateTime,
},
@@ -32,80 +104,27 @@ pub const Albumsong = jetquery.Model(
.relations = .{
.song = jetquery.belongsTo(.Song, .{}),
.album = jetquery.belongsTo(.Album, .{}),
- .scrobbles = jetquery.hasMany(.Scrobble, .{ .foreign_key = "albumsong" }),
- .albumsongsartists = jetquery.hasMany(.Albumsongsartist, .{}),
- },
- },
-);
-
-pub const Albumsongsartist = jetquery.Model(
- @This(),
- "albumsongsartists",
- struct {
- id: i64,
- albumsong_id: i64,
- artist_id: i64,
- created_at: jetquery.DateTime,
- updated_at: jetquery.DateTime,
- },
- .{
- .relations = .{
- .albumsong = jetquery.belongsTo(.Albumsong, .{}),
.artist = jetquery.belongsTo(.Artist, .{}),
},
},
);
-pub const Artistalbum = jetquery.Model(
- @This(),
- "artistalbums",
- struct {
- id: i64,
- album_id: i64,
- artist_id: i64,
- created_at: jetquery.DateTime,
- updated_at: jetquery.DateTime,
- },
- .{
- .relations = .{
- .album = jetquery.belongsTo(.Album, .{}),
- .artist = jetquery.belongsTo(.Artist, .{}),
- },
- },
-);
-
-pub const Artist = jetquery.Model(
- @This(),
- "artists",
- struct {
- id: i64,
- name: []const u8,
- disambiguation: ?[]const u8,
- created_at: jetquery.DateTime,
- updated_at: jetquery.DateTime,
- },
- .{
- .relations = .{
- .albumsongsartists = jetquery.hasMany(.Albumsongsartist, .{}),
- .artistalbums = jetquery.hasMany(.Artistalbum, .{}),
- .artistsongs = jetquery.hasMany(.Artistsong, .{}),
- },
- },
-);
-
pub const Scrobble = jetquery.Model(
@This(),
"scrobbles",
struct {
- id: i64,
- albumsong: i64,
- datetime: jetquery.DateTime,
+ id: i32,
+ song_id: i32,
+ album_id: i32,
+ date: jetquery.DateTime,
created_at: jetquery.DateTime,
updated_at: jetquery.DateTime,
},
.{
.relations = .{
- .albumsong = jetquery.belongsTo(.Albumsong, .{ .foreign_key = "albumsong" }),
+ .song = jetquery.belongsTo(.Song, .{}),
+ .album = jetquery.belongsTo(.Album, .{}),
+ .scrobbleartists = jetquery.hasMany(.Scrobbleartist, .{}),
},
},
);
@@ -114,7 +133,7 @@ pub const Song = jetquery.Model(
@This(),
"songs",
struct {
- id: i64,
+ id: i32,
name: []const u8,
length: ?f32,
hidden: bool,
@@ -123,26 +142,84 @@ pub const Song = jetquery.Model(
},
.{
.relations = .{
+ .mastersong = jetquery.belongsTo(.Mastersong, .{}),
+ .scrobbles = jetquery.hasMany(.Scrobble, .{}),
+ .ratings = jetquery.hasMany(.Rating, .{}),
+ .aliases = jetquery.hasMany(.Alias, .{}),
+ .songartists = jetquery.hasMany(.Songartist, .{}),
.albumsongs = jetquery.hasMany(.Albumsong, .{}),
- .artistsongs = jetquery.hasMany(.Artistsong, .{}),
},
},
);
-pub const Artistsong = jetquery.Model(
+pub const Albumartist = jetquery.Model(
@This(),
- "artistsongs",
+ "Albumartists",
struct {
- id: i64,
- artist_id: i64,
- song_id: i64,
+ id: i32,
+ album_id: i32,
+ artist_id: i32,
created_at: jetquery.DateTime,
updated_at: jetquery.DateTime,
},
.{
.relations = .{
+ .album = jetquery.belongsTo(.Album, .{}),
.artist = jetquery.belongsTo(.Artist, .{}),
+ },
+ },
+);
+
+pub const Songartist = jetquery.Model(
+ @This(),
+ "Songartists",
+ struct {
+ id: i32,
+ song_id: i32,
+ artist_id: i32,
+ created_at: jetquery.DateTime,
+ updated_at: jetquery.DateTime,
+ },
+ .{
+ .relations = .{
+ .song = jetquery.belongsTo(.Song, .{}),
+ .artist = jetquery.belongsTo(.Artist, .{}),
+ },
+ },
+);
+
+pub const Albumsong = jetquery.Model(
+ @This(),
+ "Albumsongs",
+ struct {
+ id: i32,
+ album_id: i32,
+ song_id: i32,
+ created_at: jetquery.DateTime,
+ updated_at: jetquery.DateTime,
+ },
+ .{
+ .relations = .{
+ .album = jetquery.belongsTo(.Album, .{}),
.song = jetquery.belongsTo(.Song, .{}),
},
},
);
+
+pub const Scrobbleartist = jetquery.Model(
+ @This(),
+ "Scrobbleartists",
+ struct {
+ id: i32,
+ scrobble_id: i32,
+ artist_id: i32,
+ created_at: jetquery.DateTime,
+ updated_at: jetquery.DateTime,
+ },
+ .{
+ .relations = .{
+ .scrobble = jetquery.belongsTo(.Scrobble, .{}),
+ .artist = jetquery.belongsTo(.Artist, .{}),
+ },
+ },
+);
diff --git a/src/app/database/migrations/2025-04-07_14-31-45_create_albums.zig b/src/app/database/migrations/2025-02-17_22-38-40_create_albums.zig
similarity index 89%
rename from src/app/database/migrations/2025-04-07_14-31-45_create_albums.zig
rename to src/app/database/migrations/2025-02-17_22-38-40_create_albums.zig
index 86b6184..d706cfe 100644
--- a/src/app/database/migrations/2025-04-07_14-31-45_create_albums.zig
+++ b/src/app/database/migrations/2025-02-17_22-38-40_create_albums.zig
@@ -6,7 +6,7 @@ pub fn up(repo: anytype) !void {
try repo.createTable(
"albums",
&.{
- t.primaryKey("id", .{ .type = .bigint }),
+ t.primaryKey("id", .{}),
t.column("name", .string, .{}),
t.column("length", .float, .{ .optional = true }),
t.timestamps(.{}),
diff --git a/src/app/database/migrations/2025-02-17_22-38-41_create_aliases.zig b/src/app/database/migrations/2025-02-17_22-38-41_create_aliases.zig
new file mode 100644
index 0000000..11cbb70
--- /dev/null
+++ b/src/app/database/migrations/2025-02-17_22-38-41_create_aliases.zig
@@ -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(
+ "aliases",
+ &.{
+ t.primaryKey("id", .{}),
+ t.column("reference_id", .integer, .{}),
+ t.column("alias", .string, .{}),
+ t.timestamps(.{}),
+ },
+ .{},
+ );
+}
+
+pub fn down(repo: anytype) !void {
+ try repo.dropTable("aliases", .{});
+}
diff --git a/src/app/database/migrations/2025-04-07_14-38-02_create_artists.zig b/src/app/database/migrations/2025-02-17_22-38-42_create_artists.zig
similarity index 74%
rename from src/app/database/migrations/2025-04-07_14-38-02_create_artists.zig
rename to src/app/database/migrations/2025-02-17_22-38-42_create_artists.zig
index 97c5bfe..2c92de4 100644
--- a/src/app/database/migrations/2025-04-07_14-38-02_create_artists.zig
+++ b/src/app/database/migrations/2025-02-17_22-38-42_create_artists.zig
@@ -6,9 +6,9 @@ pub fn up(repo: anytype) !void {
try repo.createTable(
"artists",
&.{
- t.primaryKey("id", .{ .type = .bigint }),
+ t.primaryKey("id", .{}),
t.column("name", .string, .{}),
- t.column("disambiguation", .string, .{ .optional = true }),
+ t.column("descriptive_string", .string, .{}),
t.timestamps(.{}),
},
.{},
diff --git a/src/app/database/migrations/2025-02-17_22-38-43_create_masteralbums.zig b/src/app/database/migrations/2025-02-17_22-38-43_create_masteralbums.zig
new file mode 100644
index 0000000..69e82de
--- /dev/null
+++ b/src/app/database/migrations/2025-02-17_22-38-43_create_masteralbums.zig
@@ -0,0 +1,19 @@
+const std = @import("std");
+const jetquery = @import("jetquery");
+const t = jetquery.schema.table;
+
+pub fn up(repo: anytype) !void {
+ try repo.createTable(
+ "masteralbums",
+ &.{
+ t.primaryKey("id", .{}),
+ t.column("name", .string, .{}),
+ t.timestamps(.{}),
+ },
+ .{},
+ );
+}
+
+pub fn down(repo: anytype) !void {
+ try repo.dropTable("masteralbums", .{});
+}
diff --git a/src/app/database/migrations/2025-02-17_22-38-44_create_mastersongs.zig b/src/app/database/migrations/2025-02-17_22-38-44_create_mastersongs.zig
new file mode 100644
index 0000000..050a467
--- /dev/null
+++ b/src/app/database/migrations/2025-02-17_22-38-44_create_mastersongs.zig
@@ -0,0 +1,19 @@
+const std = @import("std");
+const jetquery = @import("jetquery");
+const t = jetquery.schema.table;
+
+pub fn up(repo: anytype) !void {
+ try repo.createTable(
+ "mastersongs",
+ &.{
+ t.primaryKey("id", .{}),
+ t.column("name", .string, .{}),
+ t.timestamps(.{}),
+ },
+ .{},
+ );
+}
+
+pub fn down(repo: anytype) !void {
+ try repo.dropTable("mastersongs", .{});
+}
diff --git a/src/app/database/migrations/2025-02-17_22-38-45_create_ratings.zig b/src/app/database/migrations/2025-02-17_22-38-45_create_ratings.zig
new file mode 100644
index 0000000..d7ac939
--- /dev/null
+++ b/src/app/database/migrations/2025-02-17_22-38-45_create_ratings.zig
@@ -0,0 +1,21 @@
+const std = @import("std");
+const jetquery = @import("jetquery");
+const t = jetquery.schema.table;
+
+pub fn up(repo: anytype) !void {
+ try repo.createTable(
+ "ratings",
+ &.{
+ t.primaryKey("id", .{}),
+ t.column("reference_id", .integer, .{}),
+ t.column("score", .float, .{}),
+ t.column("date", .datetime, .{}),
+ t.timestamps(.{}),
+ },
+ .{},
+ );
+}
+
+pub fn down(repo: anytype) !void {
+ try repo.dropTable("ratings", .{});
+}
diff --git a/src/app/database/migrations/2025-04-07_14-35-53_create_scrobbles.zig b/src/app/database/migrations/2025-02-17_22-38-46_create_scrobbles.zig
similarity index 72%
rename from src/app/database/migrations/2025-04-07_14-35-53_create_scrobbles.zig
rename to src/app/database/migrations/2025-02-17_22-38-46_create_scrobbles.zig
index c3bcd12..9764d99 100644
--- a/src/app/database/migrations/2025-04-07_14-35-53_create_scrobbles.zig
+++ b/src/app/database/migrations/2025-02-17_22-38-46_create_scrobbles.zig
@@ -7,8 +7,9 @@ pub fn up(repo: anytype) !void {
"scrobbles",
&.{
t.primaryKey("id", .{}),
- t.column("albumsong", .bigint, .{ .reference = .{ "albumsongs", "id" } }),
- t.column("datetime", .datetime, .{}),
+ t.column("song_id", .integer, .{}),
+ t.column("album_id", .integer, .{}),
+ t.column("date", .datetime, .{}),
t.timestamps(.{}),
},
.{},
diff --git a/src/app/database/migrations/2025-04-07_14-31-14_create_songs.zig b/src/app/database/migrations/2025-02-17_22-38-47_create_songs.zig
similarity index 89%
rename from src/app/database/migrations/2025-04-07_14-31-14_create_songs.zig
rename to src/app/database/migrations/2025-02-17_22-38-47_create_songs.zig
index e8ae1d6..9a52b6b 100644
--- a/src/app/database/migrations/2025-04-07_14-31-14_create_songs.zig
+++ b/src/app/database/migrations/2025-02-17_22-38-47_create_songs.zig
@@ -6,7 +6,7 @@ pub fn up(repo: anytype) !void {
try repo.createTable(
"songs",
&.{
- t.primaryKey("id", .{ .type = .bigint }),
+ t.primaryKey("id", .{}),
t.column("name", .string, .{}),
t.column("length", .float, .{ .optional = true }),
t.column("hidden", .boolean, .{}),
diff --git a/src/app/database/migrations/2025-02-19_18-03-51_create_albumartists.zig b/src/app/database/migrations/2025-02-19_18-03-51_create_albumartists.zig
new file mode 100644
index 0000000..b0e4f54
--- /dev/null
+++ b/src/app/database/migrations/2025-02-19_18-03-51_create_albumartists.zig
@@ -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(
+ "Albumartists",
+ &.{
+ t.primaryKey("id", .{}),
+ t.column("album_id", .integer, .{}),
+ t.column("artist_id", .integer, .{}),
+ t.timestamps(.{}),
+ },
+ .{},
+ );
+}
+
+pub fn down(repo: anytype) !void {
+ try repo.dropTable("Albumartists", .{});
+}
diff --git a/src/app/database/migrations/2025-02-19_18-04-22_create_songsartists.zig b/src/app/database/migrations/2025-02-19_18-04-22_create_songsartists.zig
new file mode 100644
index 0000000..a509d7a
--- /dev/null
+++ b/src/app/database/migrations/2025-02-19_18-04-22_create_songsartists.zig
@@ -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(
+ "Songartists",
+ &.{
+ t.primaryKey("id", .{}),
+ t.column("song_id", .integer, .{}),
+ t.column("artist_id", .integer, .{}),
+ t.timestamps(.{}),
+ },
+ .{},
+ );
+}
+
+pub fn down(repo: anytype) !void {
+ try repo.dropTable("Songartists", .{});
+}
diff --git a/src/app/database/migrations/2025-02-19_18-46-48_create_albumsongs.zig b/src/app/database/migrations/2025-02-19_18-46-48_create_albumsongs.zig
new file mode 100644
index 0000000..1865c2e
--- /dev/null
+++ b/src/app/database/migrations/2025-02-19_18-46-48_create_albumsongs.zig
@@ -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(
+ "Albumsongs",
+ &.{
+ t.primaryKey("id", .{}),
+ t.column("album_id", .integer, .{}),
+ t.column("song_id", .integer, .{}),
+ t.timestamps(.{}),
+ },
+ .{},
+ );
+}
+
+pub fn down(repo: anytype) !void {
+ try repo.dropTable("Albumsongs", .{});
+}
diff --git a/src/app/database/migrations/2025-02-21_14-24-31_create_scrobbleartists.zig b/src/app/database/migrations/2025-02-21_14-24-31_create_scrobbleartists.zig
new file mode 100644
index 0000000..2125a87
--- /dev/null
+++ b/src/app/database/migrations/2025-02-21_14-24-31_create_scrobbleartists.zig
@@ -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(
+ "Scrobbleartists",
+ &.{
+ t.primaryKey("id", .{}),
+ t.column("scrobble_id", .integer, .{}),
+ t.column("artist_id", .integer, .{}),
+ t.timestamps(.{}),
+ },
+ .{},
+ );
+}
+
+pub fn down(repo: anytype) !void {
+ try repo.dropTable("Scrobbleartists", .{});
+}
diff --git a/src/app/database/migrations/2025-04-07_14-34-39_create_albumsongs.zig b/src/app/database/migrations/2025-04-07_14-34-39_create_albumsongs.zig
deleted file mode 100644
index 96c3063..0000000
--- a/src/app/database/migrations/2025-04-07_14-34-39_create_albumsongs.zig
+++ /dev/null
@@ -1,20 +0,0 @@
-const std = @import("std");
-const jetquery = @import("jetquery");
-const t = jetquery.schema.table;
-
-pub fn up(repo: anytype) !void {
- try repo.createTable(
- "albumsongs",
- &.{
- t.primaryKey("id", .{ .type = .bigint }),
- t.column("song_id", .bigint, .{ .reference = .{ "songs", "id" } }),
- t.column("album_id", .bigint, .{ .reference = .{ "albums", "id" } }),
- t.timestamps(.{}),
- },
- .{},
- );
-}
-
-pub fn down(repo: anytype) !void {
- try repo.dropTable("albumsongs", .{});
-}
diff --git a/src/app/database/migrations/2025-04-07_14-39-09_create_albumsongsartists.zig b/src/app/database/migrations/2025-04-07_14-39-09_create_albumsongsartists.zig
deleted file mode 100644
index 3355196..0000000
--- a/src/app/database/migrations/2025-04-07_14-39-09_create_albumsongsartists.zig
+++ /dev/null
@@ -1,20 +0,0 @@
-const std = @import("std");
-const jetquery = @import("jetquery");
-const t = jetquery.schema.table;
-
-pub fn up(repo: anytype) !void {
- try repo.createTable(
- "albumsongsartists",
- &.{
- t.primaryKey("id", .{ .type = .bigint }),
- t.column("albumsong_id", .bigint, .{ .reference = .{ "albumsongs", "id" } }),
- t.column("artist_id", .bigint, .{ .reference = .{ "artists", "id" } }),
- t.timestamps(.{}),
- },
- .{},
- );
-}
-
-pub fn down(repo: anytype) !void {
- try repo.dropTable("albumsongsartists", .{});
-}
diff --git a/src/app/database/migrations/2025-04-07_14-40-17_create_artistalbums.zig b/src/app/database/migrations/2025-04-07_14-40-17_create_artistalbums.zig
deleted file mode 100644
index 3c3ea7f..0000000
--- a/src/app/database/migrations/2025-04-07_14-40-17_create_artistalbums.zig
+++ /dev/null
@@ -1,20 +0,0 @@
-const std = @import("std");
-const jetquery = @import("jetquery");
-const t = jetquery.schema.table;
-
-pub fn up(repo: anytype) !void {
- try repo.createTable(
- "artistalbums",
- &.{
- t.primaryKey("id", .{ .type = .bigint }),
- t.column("album_id", .bigint, .{ .reference = .{ "albums", "id" } }),
- t.column("artist_id", .bigint, .{ .reference = .{ "artists", "id" } }),
- t.timestamps(.{}),
- },
- .{},
- );
-}
-
-pub fn down(repo: anytype) !void {
- try repo.dropTable("artistalbums", .{});
-}
diff --git a/src/app/database/migrations/2025-06-06_19-46-32_create_artistsongs.zig b/src/app/database/migrations/2025-06-06_19-46-32_create_artistsongs.zig
deleted file mode 100644
index 7d0a6c1..0000000
--- a/src/app/database/migrations/2025-06-06_19-46-32_create_artistsongs.zig
+++ /dev/null
@@ -1,20 +0,0 @@
-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", .{});
-}
diff --git a/src/app/jobs/process_rule.zig b/src/app/jobs/process_rule.zig
deleted file mode 100644
index 6beb880..0000000
--- a/src/app/jobs/process_rule.zig
+++ /dev/null
@@ -1,53 +0,0 @@
-const std = @import("std");
-const jetzig = @import("jetzig");
-const Data = @import("../../types.zig");
-
-pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig.jobs.JobEnv) !void {
- _ = env;
-
- const rule = try std.json.parseFromSliceLeaky(Data.Rule, allocator, try params.toJson(), .{ .ignore_unknown_fields = true });
-
- const file_read: std.fs.File = std.fs.cwd().openFile("rules.json", .{}) catch |read_err| switch (read_err) {
- error.FileNotFound => {
- const file = std.fs.cwd().createFile("rules.json", .{ .read = true, .exclusive = true }) catch |write_err| switch (write_err) {
- error.PathAlreadyExists => unreachable,
- else => {
- std.log.debug("{any} while writing file", .{write_err});
- return;
- },
- };
- const out_rules = &[_]Data.Rule{rule};
- const out = try std.json.stringifyAlloc(allocator, out_rules, .{});
- try file.writeAll(out);
- file.close();
- return;
- },
- else => {
- std.log.debug("{any} while reading file", .{read_err});
- return;
- },
- };
-
- var rules = std.ArrayList(Data.Rule).init(allocator);
-
- const file_content = try file_read.readToEndAlloc(allocator, 16_000_000);
- file_read.close();
-
- 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.Rule{rule};
- const out = try std.json.stringifyAlloc(allocator, out_rules, .{});
- try file_write.writeAll(out);
- file_write.close();
- return;
- }
- const content: []Data.Rule = try std.json.parseFromSliceLeaky([]Data.Rule, allocator, file_content, .{});
- try rules.appendSlice(content);
- try rules.append(rule);
-
- const out = try std.json.stringifyAlloc(allocator, rules.items, .{});
-
- try file_write.writeAll(out);
- file_write.close();
- return;
-}
diff --git a/src/app/jobs/process_scrobbles.zig b/src/app/jobs/process_scrobbles.zig
index 770bde7..1314465 100644
--- a/src/app/jobs/process_scrobbles.zig
+++ b/src/app/jobs/process_scrobbles.zig
@@ -1,8 +1,8 @@
const std = @import("std");
const jetzig = @import("jetzig");
const jetquery = @import("jetzig").jetquery;
-const Data = @import("../../types.zig");
-const rules = @import("../../apply_rule.zig");
+const Scrobble = @import("../../types.zig").LastFMScrobble;
+const lastfm = @import("../../types.zig").LastFM;
// The `run` function for a job is invoked every time the job is processed by a queue worker
// (or by the Jetzig server if the job is processed in-line).
@@ -14,121 +14,118 @@ const rules = @import("../../apply_rule.zig");
// - logger: Logger attached to the same stream as the Jetzig server.
// - environment: Enum of `{ production, development }`.
pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig.jobs.JobEnv) !void {
+ _ = allocator;
//_ = env;
+
if (params.getT(.array, "scrobbles")) |scrobbles| {
for (scrobbles.items()) |item| {
+ //const fixed_date: u32 = @as(u32, item.getT(.integer, "date").?);
+ 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)))) };
- // Probably want to include artist name here, but not sure how to yet
+ // Make hashes
+ //const album_hash = @as(i32, @bitCast(std.hash.Fnv1a_32.hash(scrobble.album)));
+ //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)));
- const track_artists = item.getT(.array, "artists_track").?.items();
- const album_artists = item.getT(.array, "artists_album").?.items();
+ // Create a buffer to hold the metadata to hash. Numbers based on the title of a
+ // particularly long Sufjan Stevens song title, and we're gonna pray the metadata
+ // 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 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)));
+ const song_prehash = try std.fmt.bufPrint(&buffer, "{s}{s}{s}", .{ scrobble.artist, scrobble.album, scrobble.track });
+ const song_id = @as(i32, @bitCast(std.hash.Fnv1a_32.hash(song_prehash)));
- 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);
+ // Make IDs
+ // Song: Song hash XOR artist hash XOR album hash
+ // This way, if two songs share a name, then
+ // the IDs also depend on the hash of the album
+ // they're on, as well as the artist name. As far
+ // as I can tell, this is only as issue for Sufjan
+ // Steven's `Songs for Christmas`. (In practice.
+ // In reality, there are albums with several untitled
+ // songs (Selected Ambient Works Vol. II by Aphex Twin,
+ // ( ) by Sigur Ros, ...) that have working titles
+ // in their place.)
- const scrobble: Data.Scrobble = .{
- .track = item.getT(.string, "track").?,
- .artists_track = track_artist_name_buffer,
- .album = item.getT(.string, "album") orelse "",
- .artists_album = album_artist_name_buffer,
- .date = @as(i64, @truncate(item.getT(.integer, "date").?)),
- };
+ // Album: If the album is not self-titled, then
+ // album hash XOR artist hash. This way, if two
+ // artists have an album of the same name, then
+ // the IDs also depend on the hash of the artist
+ // name. As far as I can tell, this is only an
+ // issue for Weezer.
- var album_hash_string = std.ArrayList(u8).init(allocator);
- var track_hash_string = std.ArrayList(u8).init(allocator);
+ // Artist: Artist hash. If two artists have the same name,
+ // then a descriptive string can be provided to
+ // differentiate after the fact, or in a rule.
- // 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(i64, @bitCast(std.hash.Fnv1a_64.hash(artist_name)));
+ //var album_id: i32 = @as(i32, @bitCast(std.hash.Fnv1a_32.hash(formed)));
+ //const song_id = (song_hash ^ artist_hash ^ album_hash);
+
+ // Inserts
+ const album_insert = jetzig.database.Query(.Album).insert(.{ .id = album_id, .name = scrobble.album, .length = null });
+ const artist_insert = jetzig.database.Query(.Artist).insert(.{ .id = artist_id, .name = scrobble.artist, .descriptive_string = "" });
+ const song_insert = jetzig.database.Query(.Song).insert(.{ .id = song_id, .name = scrobble.track, .length = null, .hidden = false });
+
+ // Checks
+ const album_check = try jetzig.database.Query(.Album).find(album_id).execute(env.repo);
+ const artist_check = try jetzig.database.Query(.Artist).find(artist_id).execute(env.repo);
+ const song_check = try jetzig.database.Query(.Song).find(song_id).execute(env.repo);
+
+ // I think there must be a better way to do this next part
+ // There are very few situations where artist_check is null
+ // but song_check/album is not. Also yes, the order of these
+ // checks is weird, I didn't put a lot of thought into it
+ var associative_table_flags: [3]bool = [3]bool{ true, true, true };
+
+ if (album_check == null) {
+ try env.repo.execute(album_insert);
+ try jetzig.database.Query(.Albumartist).insert(.{ .album_id = album_id, .artist_id = artist_id }).execute(env.repo);
+ associative_table_flags[0] = false;
+ try jetzig.database.Query(.Albumsong).insert(.{ .album_id = album_id, .song_id = song_id }).execute(env.repo);
+ associative_table_flags[1] = false;
}
- 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(i64, @bitCast(std.hash.Fnv1a_64.hash(artist_name)));
- try album_hash_string.appendSlice(artist_name);
+ if (artist_check == null) {
+ try env.repo.execute(artist_insert);
+ if (associative_table_flags[0]) try jetzig.database.Query(.Albumartist).insert(.{ .album_id = album_id, .artist_id = artist_id }).execute(env.repo);
+ try jetzig.database.Query(.Songartist).insert(.{ .song_id = song_id, .artist_id = artist_id }).execute(env.repo);
+ associative_table_flags[2] = false;
}
- try album_hash_string.appendSlice(scrobble.album);
- try track_hash_string.appendSlice(scrobble.album);
- const album_hash = @as(i64, @bitCast(std.hash.Fnv1a_64.hash(album_hash_string.items)));
- try track_hash_string.appendSlice(scrobble.track);
- const track_hash = @as(i64, @bitCast(std.hash.Fnv1a_64.hash(track_hash_string.items)));
-
- var albumsong_id = try jetzig.database.Query(.Albumsong)
- .find(album_hash ^ track_hash)
- .select(.{.id}).execute(env.repo);
-
- 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) |scrobble_track_artist, track_artist_hash| {
- var artist_id = try jetzig.database.Query(.Artist)
- .find(track_artist_hash)
- .select(.{.id}).execute(env.repo);
-
- 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_id == null) {
- var track_id = try jetzig.database.Query(.Song)
- .find(track_hash)
- .select(.{.id}).execute(env.repo);
-
- 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 (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_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.?.id, .artist_id = artist_id.?.id }).execute(env.repo);
- } else {
- const ins_albumsongartist = try jetzig.database.Query(.Albumsongsartist)
- .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.?.id, .artist_id = artist_id.?.id }).execute(env.repo);
- }
+ if (song_check == null) {
+ try env.repo.execute(song_insert);
+ if (associative_table_flags[1]) try jetzig.database.Query(.Albumsong).insert(.{ .album_id = album_id, .song_id = song_id }).execute(env.repo);
+ if (associative_table_flags[2]) try jetzig.database.Query(.Songartist).insert(.{ .song_id = song_id, .artist_id = artist_id }).execute(env.repo);
}
- 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 (artistalbum_id == null) {
- var artist_id = try jetzig.database.Query(.Artist)
- .find(album_artist_hash)
- .select(.{.id}).execute(env.repo);
- if (artist_id == null)
- artist_id = try jetzig.database.Query(.Artist)
- .insert(.{ .id = album_artist_hash, .name = scrobble_album_artist, .disambiguation = null })
- .returning(.{.id}).execute(env.repo);
- try jetzig.database.Query(.Artistalbum)
- .insert(.{ .album_id = album_id.?.id, .artist_id = artist_id.?.id }).execute(env.repo);
- }
- }
-
- try jetzig.database.Query(.Scrobble)
- .insert(.{ .albumsong = albumsong_id.?.id, .datetime = scrobble.date }).execute(env.repo);
+ const scr_id = try jetzig.database.Query(.Scrobble).insert(.{ .song_id = song_id, .album_id = album_id, .date = scrobble.date }).returning(.{.id}).execute(env.repo);
+ defer env.repo.free(scr_id);
+ try jetzig.database.Query(.Scrobbleartist).insert(.{ .scrobble_id = scr_id.?.id, .artist_id = artist_id }).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 });
+ // }
+ //}
}
diff --git a/src/app/jobs/process_scrobbles2.zig b/src/app/jobs/process_scrobbles2.zig
deleted file mode 100644
index 8b3ae66..0000000
--- a/src/app/jobs/process_scrobbles2.zig
+++ /dev/null
@@ -1,131 +0,0 @@
-const std = @import("std");
-const jetzig = @import("jetzig");
-
-// The `run` function for a job is invoked every time the job is processed by a queue worker
-// (or by the Jetzig server if the job is processed in-line).
-//
-// Arguments:
-// * allocator: Arena allocator for use during the job execution process.
-// * params: Params assigned to a job (from a request, values added to response data).
-// * env: Provides the following fields:
-// - logger: Logger attached to the same stream as the Jetzig server.
-// - environment: Enum of `{ production, development }`.
-pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig.jobs.JobEnv) !void {
- _ = allocator;
-
- for (params.getT(.object, "tracks").?.items()) |track| {
- const id = try std.fmt.parseInt(i64, track.key, 10);
-
- const track_query = try jetzig.database.Query(.Song)
- .find(id).execute(env.repo);
-
- if (track_query == null) {
- const name = try track.value.coerce([]const u8);
- try jetzig.database.Query(.Song)
- .insert(.{ .id = id, .name = name, .length = null, .hidden = false })
- .execute(env.repo);
- }
- }
-
- for (params.getT(.object, "albums").?.items()) |album| {
- const id = try std.fmt.parseInt(i64, album.key, 10);
-
- const album_query = try jetzig.database.Query(.Album)
- .find(id).execute(env.repo);
-
- if (album_query == null) {
- const name = try album.value.coerce([]const u8);
- try jetzig.database.Query(.Album)
- .insert(.{ .id = id, .name = name, .length = null })
- .execute(env.repo);
- }
- }
-
- for (params.getT(.object, "artists").?.items()) |artist| {
- const id = try std.fmt.parseInt(i64, artist.key, 10);
-
- const artist_query = try jetzig.database.Query(.Artist)
- .find(id).execute(env.repo);
-
- if (artist_query == null) {
- const name = try artist.value.coerce([]const u8);
- try jetzig.database.Query(.Artist)
- .insert(.{ .id = id, .name = name })
- .execute(env.repo);
- }
- }
-
- for (params.getT(.object, "albumsongs").?.items()) |as| {
- const id = try std.fmt.parseInt(i64, as.key, 10);
-
- const as_query = try jetzig.database.Query(.Albumsong)
- .find(id).execute(env.repo);
-
- if (as_query == null) {
- const track_id = @as(i64, @intCast(as.value.getT(.integer, "song").?));
- const album_id = @as(i64, @intCast(as.value.getT(.integer, "album").?));
- try jetzig.database.Query(.Albumsong)
- .insert(.{ .id = id, .song_id = track_id, .album_id = album_id })
- .execute(env.repo);
- }
-
- const scrobbles = as.value.getT(.array, "scrobbles").?;
- for (scrobbles.items()) |date| {
- try jetzig.database.Query(.Scrobble).insert(.{ .albumsong = id, .datetime = date })
- .execute(env.repo);
- }
- }
-
- for (params.getT(.object, "artistalbums").?.items()) |aa| {
- const id = try std.fmt.parseInt(i64, aa.key, 10);
-
- const aa_query = try jetzig.database.Query(.Artistalbum)
- .find(id).execute(env.repo);
-
- if (aa_query == null) {
- const artist_id = @as(i64, @intCast(aa.value.getT(.integer, "artist").?));
- const album_id = @as(i64, @intCast(aa.value.getT(.integer, "album").?));
- try jetzig.database.Query(.Artistalbum)
- .insert(.{ .id = id, .artist_id = artist_id, .album_id = album_id })
- .execute(env.repo);
- }
- }
-
- for (params.getT(.object, "albumsongsartists").?.items()) |asa| {
- const id = try std.fmt.parseInt(i64, asa.key, 10);
-
- const asa_query = try jetzig.database.Query(.Albumsongsartist)
- .find(id).execute(env.repo);
-
- if (asa_query == null) {
- const albumsong_id = @as(i64, @intCast(asa.value.getT(.integer, "albumsong").?));
- const artist_id = @as(i64, @intCast(asa.value.getT(.integer, "artist").?));
- try jetzig.database.Query(.Albumsongsartist)
- .insert(.{ .id = id, .albumsong_id = albumsong_id, .artist_id = artist_id })
- .execute(env.repo);
- }
- }
-
- //for ((params.getT(.object, "albumsongsartists")).?.items(.object)) |asa| {
- // const id = try std.fmt.parseInt(i64, asa.key, 10);
- // const albumsong_id = asa.value.getT(.integer, "albumsong");
- // const track_artist_id = asa.value.getT(.integer, "artist");
-
- // const albumsongartist = try jetzig.database.Query(.Albumsongsartist)
- // .find(id)
- // .select(.{.id}).execute(env.repo);
-
- // if (albumsongartist == null) {
- // var artist_id = try jetzig.database.Query(.Artist)
- // .find(track_artist_id)
- // .select(.{.id}).execute(env.repo);
- //
- // if (artist_id == null) {
- // const artist = params.chain(.{"artists",})
- // artist_id = try jetzig.database.Query(.Artist)
- // .insert(.{ .id = track_artist_hash, .name = scrobble_track_artist, .disambiguation = null })
- // .execute(env.repo);
- // }
- // }
- //}
-}
diff --git a/src/app/middleware/DemoMiddleware.zig b/src/app/middleware/DemoMiddleware.zig
new file mode 100644
index 0000000..a6758d2
--- /dev/null
+++ b/src/app/middleware/DemoMiddleware.zig
@@ -0,0 +1,65 @@
+/// Demo middleware. Assign middleware by declaring `pub const middleware` in the
+/// `jetzig_options` defined in your application's `src/main.zig`.
+///
+/// Middleware is called before and after the request, providing full access to the active
+/// request, allowing you to execute any custom code for logging, tracking, inserting response
+/// headers, etc.
+///
+/// This middleware is configured in the demo app's `src/main.zig`:
+///
+/// ```
+/// pub const jetzig_options = struct {
+/// pub const middleware: []const type = &.{@import("app/middleware/DemoMiddleware.zig")};
+/// };
+/// ```
+const std = @import("std");
+const jetzig = @import("jetzig");
+
+/// Define any custom data fields you want to store here. Assigning to these fields in the `init`
+/// function allows you to access them in various middleware callbacks defined below, where they
+/// can also be modified.
+my_custom_value: []const u8,
+
+const Self = @This();
+
+/// Initialize middleware.
+pub fn init(request: *jetzig.http.Request) !*Self {
+ var middleware = try request.allocator.create(Self);
+ middleware.my_custom_value = "initial value";
+ return middleware;
+}
+
+/// Invoked immediately after the request is received but before it has started processing.
+/// Any calls to `request.render` or `request.redirect` will prevent further processing of the
+/// request, including any other middleware in the chain.
+pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void {
+ try request.server.logger.DEBUG(
+ "[DemoMiddleware:afterRequest] my_custom_value: {s}",
+ .{self.my_custom_value},
+ );
+ self.my_custom_value = @tagName(request.method);
+}
+
+/// Invoked immediately before the response renders to the client.
+/// The response can be modified here if needed.
+pub fn beforeResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
+ try request.server.logger.DEBUG(
+ "[DemoMiddleware:beforeResponse] my_custom_value: {s}, response status: {s}",
+ .{ self.my_custom_value, @tagName(response.status_code) },
+ );
+}
+
+/// Invoked immediately after the response has been finalized and sent to the client.
+/// Response data can be accessed for logging, but any modifications will have no impact.
+pub fn afterResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
+ _ = self;
+ _ = response;
+ try request.server.logger.DEBUG("[DemoMiddleware:afterResponse] response completed", .{});
+}
+
+/// Invoked after `afterResponse` is called. Use this function to do any clean-up.
+/// Note that `request.allocator` is an arena allocator, so any allocations are automatically
+/// freed before the next request starts processing.
+pub fn deinit(self: *Self, request: *jetzig.http.Request) void {
+ request.allocator.destroy(self);
+}
diff --git a/src/app/views/albums.zig b/src/app/views/albums.zig
index 03046b3..634f414 100644
--- a/src/app/views/albums.zig
+++ b/src/app/views/albums.zig
@@ -1,33 +1,155 @@
const std = @import("std");
const jetzig = @import("jetzig");
const jetquery = @import("jetzig").jetquery;
-const TableRow = @import("../../types.zig").TableRow;
-const HyperlinkData = @import("../../types.zig").HyperlinkData;
-const queries = @import("../../queries.zig");
pub fn index(request: *jetzig.Request) !jetzig.View {
var root = try request.data(.object);
+ var albums_view = try root.put("albums", .array);
+ const albums = try jetzig.database.Query(.Album)
+ .select(.{ .id, .name })
+ .include(.albumartists, .{ .select = .{.artist_id} })
+ .include(.scrobbles, .{ .select = .{.id} })
+ .orderBy(.{ .name = .asc })
+ .all(request.repo);
+ //const albums = try request.repo.all(query);
- const albums = try queries.entityQueryResult(request, queries.loadQuery(.album, .entities), .{});
- try root.put("albums", albums);
+ for (albums) |album| {
+ var album_view = try albums_view.append(.object);
+ var artist_infos = try album_view.put("artist_info", .array);
+ for (album.albumartists) |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 album_view.put("name", album.name);
+ try album_view.put("url", album.id);
+ try album_view.put("scrobbles", (album.scrobbles).len);
+ }
return request.render(.ok);
}
pub fn get(id: []const u8, request: *jetzig.Request) !jetzig.View {
+ const album = try jetzig.database.Query(.Album)
+ .find(id)
+ .select(.{ .id, .name })
+ .execute(request.repo);
var root = try request.data(.object);
+ try root.put("album", album.?.name);
+ var songs_view = try root.put("songs", .array);
+ const query = jetzig.database.Query(.Albumsong)
+ .select(.{.id})
+ .include(.song, .{ .select = .{ .name, .id } })
+ .join(.inner, .album)
+ .where(.{ .album = .{ .id = id } });
- 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, .get_songs), .{id});
- try root.put("songs", songs);
-
- const firstlast = try queries.entityQueryResult(request, queries.loadQuery(.album, .firstlast), .{id});
- try root.put("firstlast", firstlast);
-
- const timescale = try queries.entityQueryResult(request, queries.loadQuery(.album, .timescale), .{id});
- try root.put("yearly", timescale);
-
+ const songs = try request.repo.all(query);
+ for (songs) |song| {
+ const scrobbles = try jetzig.database.Query(.Scrobble)
+ .where(.{ .song_id = song.song.id })
+ .count()
+ .execute(request.repo);
+ var song_view = try songs_view.append(.object);
+ try song_view.put("name", song.song.name);
+ try song_view.put("url", song.song.id);
+ try song_view.put("scrobbles", scrobbles);
+ }
return request.render(.ok);
}
+
+pub fn new(request: *jetzig.Request) !jetzig.View {
+ return request.render(.ok);
+}
+
+pub fn edit(id: []const u8, request: *jetzig.Request) !jetzig.View {
+ _ = id;
+ return request.render(.ok);
+}
+
+pub fn post(request: *jetzig.Request) !jetzig.View {
+ return request.render(.created);
+}
+
+pub fn put(id: []const u8, request: *jetzig.Request) !jetzig.View {
+ _ = id;
+ return request.render(.ok);
+}
+
+pub fn patch(id: []const u8, request: *jetzig.Request) !jetzig.View {
+ _ = id;
+ return request.render(.ok);
+}
+
+pub fn delete(id: []const u8, request: *jetzig.Request) !jetzig.View {
+ _ = id;
+ return request.render(.ok);
+}
+
+test "index" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.GET, "/album", .{});
+ try response.expectStatus(.ok);
+}
+
+test "get" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.GET, "/album/example-id", .{});
+ try response.expectStatus(.ok);
+}
+
+test "new" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.GET, "/album/new", .{});
+ try response.expectStatus(.ok);
+}
+
+test "edit" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.GET, "/album/example-id/edit", .{});
+ try response.expectStatus(.ok);
+}
+
+test "post" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.POST, "/album", .{});
+ try response.expectStatus(.created);
+}
+
+test "put" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.PUT, "/album/example-id", .{});
+ try response.expectStatus(.ok);
+}
+
+test "patch" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.PATCH, "/album/example-id", .{});
+ try response.expectStatus(.ok);
+}
+
+test "delete" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.DELETE, "/album/example-id", .{});
+ try response.expectStatus(.ok);
+}
diff --git a/src/app/views/groups/delete.zmpl b/src/app/views/albums/delete.zmpl
similarity index 100%
rename from src/app/views/groups/delete.zmpl
rename to src/app/views/albums/delete.zmpl
diff --git a/src/app/views/groups/edit.zmpl b/src/app/views/albums/edit.zmpl
similarity index 100%
rename from src/app/views/groups/edit.zmpl
rename to src/app/views/albums/edit.zmpl
diff --git a/src/app/views/albums/get.zmpl b/src/app/views/albums/get.zmpl
index 7e089e7..8084038 100644
--- a/src/app/views/albums/get.zmpl
+++ b/src/app/views/albums/get.zmpl
@@ -1,22 +1,19 @@
-@zig {
- const ColumnChoices = []const enum{song, album, artist, artistlist, scrobbles, date};
- const columns: ColumnChoices = &.{.song, .scrobbles};
-}
-
@partial partials/header
-{{.album.album_name}}
-
-{{.album.scrobbles}} scrobbles ({{.album.rank}} place)
-{{.album.song_num}} songs
-@partial partials/firstlast_listens(firstlast: .firstlast)
-Yearly Performance
-@partial partials/timescale(range: .yearly)
-Songs
-@partial partials/newtable(T: ColumnChoices, table_data: .songs, columns: columns)
+{{.album}}
+
+
+Name
+@for (.songs) |song| {
+
+ {{song.name}}
+ {{song.scrobbles}}
+
+}
+
\ No newline at end of file
diff --git a/src/app/views/albums/index.zmpl b/src/app/views/albums/index.zmpl
index 43ff1de..2c259a6 100644
--- a/src/app/views/albums/index.zmpl
+++ b/src/app/views/albums/index.zmpl
@@ -1,15 +1,40 @@
-@zig {
- const ColumnChoices = []const enum{song, album, artist, artistlist, scrobbles, date};
- const columns: ColumnChoices = &.{.album, .artistlist, .scrobbles};
-}
-
+
@partial partials/header
Albums
-@partial partials/newtable(T: ColumnChoices, table_data: .albums, columns: columns)
+
+
+
+Name
+Artist(s)
+Scrobbles
+
+
+
+@for (.albums) |album| {
+
+ {{album.name}}
+
+ @for (album.get("artist_info").?) |ai| {
+ {{ai.name}}
+ }
+
+ {{album.scrobbles}}
+
+}
+
+
+
+
\ No newline at end of file
diff --git a/src/app/views/groups/new.zmpl b/src/app/views/albums/new.zmpl
similarity index 100%
rename from src/app/views/groups/new.zmpl
rename to src/app/views/albums/new.zmpl
diff --git a/src/app/views/groups/patch.zmpl b/src/app/views/albums/patch.zmpl
similarity index 100%
rename from src/app/views/groups/patch.zmpl
rename to src/app/views/albums/patch.zmpl
diff --git a/src/app/views/groups/post.zmpl b/src/app/views/albums/post.zmpl
similarity index 100%
rename from src/app/views/groups/post.zmpl
rename to src/app/views/albums/post.zmpl
diff --git a/src/app/views/groups/put.zmpl b/src/app/views/albums/put.zmpl
similarity index 100%
rename from src/app/views/groups/put.zmpl
rename to src/app/views/albums/put.zmpl
diff --git a/src/app/views/artists.zig b/src/app/views/artists.zig
index 3ec8eba..78058a7 100644
--- a/src/app/views/artists.zig
+++ b/src/app/views/artists.zig
@@ -1,37 +1,141 @@
const std = @import("std");
const jetzig = @import("jetzig");
const jetquery = @import("jetzig").jetquery;
-const TableRow = @import("../../types.zig").TableRow;
-const dateFmt = @import("../../date_fmt.zig").dateFmt;
-const ordinalFmt = @import("../../ordinal_fmt.zig").ordinalFmt;
-const queries = @import("../../queries.zig");
pub fn index(request: *jetzig.Request) !jetzig.View {
var root = try request.data(.object);
- const artists = try queries.entityQueryResult(request, queries.loadQuery(.artist, .entities), .{});
-
- try root.put("artists", artists);
+ var artists_view = try root.put("artists", .array);
+ const artists = try jetzig.database.Query(.Artist)
+ .select(.{ .id, .name })
+ .include(.scrobbleartists, .{ .select = .{.id} })
+ .orderBy(.{ .name = .asc })
+ .all(request.repo);
+ for (artists) |artist| {
+ var artist_view = try artists_view.append(.object);
+ try artist_view.put("name", artist.name);
+ try artist_view.put("url", artist.id);
+ try artist_view.put("scrobbles", (artist.scrobbleartists).len);
+ }
return request.render(.ok);
}
pub fn get(id: []const u8, request: *jetzig.Request) !jetzig.View {
+ const artist = try jetzig.database.Query(.Artist)
+ .find(id)
+ .select(.{ .id, .name })
+ .execute(request.repo);
var root = try request.data(.object);
+ try root.put("artist", artist.?.name);
+ var albums_view = try root.put("albums", .array);
+ const query = jetzig.database.Query(.Albumartist)
+ .select(.{.id})
+ .include(.album, .{ .select = .{ .name, .id } })
+ .join(.inner, .artist)
+ .where(.{ .artist = .{ .id = id } });
- 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, .get_albums), .{id});
- try root.put("albums", albums);
-
- const appears = try queries.entityQueryResult(request, queries.loadQuery(.artist, .appears), .{id});
- try root.put("appears", appears);
-
- const firstlast = try queries.entityQueryResult(request, queries.loadQuery(.artist, .firstlast), .{id});
- try root.put("firstlast", firstlast);
-
- const timescale = try queries.entityQueryResult(request, queries.loadQuery(.artist, .timescale), .{id});
- try root.put("yearly", timescale);
-
+ const albums = try request.repo.all(query);
+ for (albums) |album| {
+ const scrobbles = try jetzig.database.Query(.Scrobble)
+ .where(.{ .album_id = album.album.id })
+ .count()
+ .execute(request.repo);
+ var album_view = try albums_view.append(.object);
+ try album_view.put("name", album.album.name);
+ try album_view.put("url", album.album.id);
+ try album_view.put("scrobbles", scrobbles);
+ }
return request.render(.ok);
}
+
+pub fn new(request: *jetzig.Request) !jetzig.View {
+ return request.render(.ok);
+}
+
+pub fn edit(id: []const u8, request: *jetzig.Request) !jetzig.View {
+ _ = id;
+ return request.render(.ok);
+}
+
+pub fn post(request: *jetzig.Request) !jetzig.View {
+ return request.render(.created);
+}
+
+pub fn put(id: []const u8, request: *jetzig.Request) !jetzig.View {
+ _ = id;
+ return request.render(.ok);
+}
+
+pub fn patch(id: []const u8, request: *jetzig.Request) !jetzig.View {
+ _ = id;
+ return request.render(.ok);
+}
+
+pub fn delete(id: []const u8, request: *jetzig.Request) !jetzig.View {
+ _ = id;
+ return request.render(.ok);
+}
+
+test "index" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.GET, "/artist", .{});
+ try response.expectStatus(.ok);
+}
+
+test "get" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.GET, "/artist/example-id", .{});
+ try response.expectStatus(.ok);
+}
+
+test "new" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.GET, "/artist/new", .{});
+ try response.expectStatus(.ok);
+}
+
+test "edit" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.GET, "/artist/example-id/edit", .{});
+ try response.expectStatus(.ok);
+}
+
+test "post" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.POST, "/artist", .{});
+ try response.expectStatus(.created);
+}
+
+test "put" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.PUT, "/artist/example-id", .{});
+ try response.expectStatus(.ok);
+}
+
+test "patch" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.PATCH, "/artist/example-id", .{});
+ try response.expectStatus(.ok);
+}
+
+test "delete" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.DELETE, "/artist/example-id", .{});
+ try response.expectStatus(.ok);
+}
diff --git a/src/app/views/groups/get.zmpl b/src/app/views/artists/delete.zmpl
similarity index 100%
rename from src/app/views/groups/get.zmpl
rename to src/app/views/artists/delete.zmpl
diff --git a/src/app/views/artists/edit.zmpl b/src/app/views/artists/edit.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/artists/edit.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/artists/get.zmpl b/src/app/views/artists/get.zmpl
index 9f1ff07..911d2c3 100644
--- a/src/app/views/artists/get.zmpl
+++ b/src/app/views/artists/get.zmpl
@@ -1,28 +1,31 @@
-@zig {
- const ColumnChoices = []const enum{song, album, artist, artistlist, scrobbles, date};
- const columns: ColumnChoices = &.{.album, .scrobbles};
-}
-
+
@partial partials/header
-{{.artist.artist_name}}
-
-
{{.artist.scrobbles}} scrobbles ({{.artist.rank}} place)
-
{{.artist.song_num}} songs
-
{{.artist.album_num}} albums
-
-@partial partials/timescale(range: .yearly)
-
-@partial partials/firstlast_listens(firstlast: .firstlast)
-Albums
-@partial partials/newtable(T: ColumnChoices, table_data: .albums, columns: columns)
-
-Albums Featured On
-@partial partials/newtable(T: ColumnChoices, table_data: .appears, columns: columns)
-
+{{.artist}}
+
+
+
+Name Scrobbles
+
+
+
+@for (.albums) |album| {
+
+ {{album.name}}
+ {{album.scrobbles}}
+
+}
+
+
+
+
\ No newline at end of file
diff --git a/src/app/views/artists/index.zmpl b/src/app/views/artists/index.zmpl
index 0648c91..6854e07 100644
--- a/src/app/views/artists/index.zmpl
+++ b/src/app/views/artists/index.zmpl
@@ -1,15 +1,34 @@
-@zig {
- const ColumnChoices = []const enum{song, album, artist, artistlist, scrobbles, date};
- const columns: ColumnChoices = &.{.artist, .scrobbles};
-}
-
+
@partial partials/header
Artists
-@partial partials/newtable(T: ColumnChoices, table_data: .artists, columns: columns)
+
+
+
+Name
+Scrobbles
+
+
+
+@for (.artists) |artist| {
+
+ {{artist.name}}
+ {{artist.scrobbles}}
+
+}
+
+
+
+
\ No newline at end of file
diff --git a/src/app/views/artists/new.zmpl b/src/app/views/artists/new.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/artists/new.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/artists/patch.zmpl b/src/app/views/artists/patch.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/artists/patch.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/artists/post.zmpl b/src/app/views/artists/post.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/artists/post.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/artists/put.zmpl b/src/app/views/artists/put.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/artists/put.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/collection.zig b/src/app/views/collection.zig
index 1b0b502..8125efd 100644
--- a/src/app/views/collection.zig
+++ b/src/app/views/collection.zig
@@ -11,3 +11,26 @@ pub fn get(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig
_ = id;
return request.render(.ok);
}
+
+pub fn post(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
+ _ = data;
+ return request.render(.created);
+}
+
+pub fn put(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
+ _ = data;
+ _ = id;
+ return request.render(.ok);
+}
+
+pub fn patch(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
+ _ = data;
+ _ = id;
+ return request.render(.ok);
+}
+
+pub fn delete(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
+ _ = data;
+ _ = id;
+ return request.render(.ok);
+}
diff --git a/src/app/views/collection/delete.zmpl b/src/app/views/collection/delete.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/collection/delete.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/collection/patch.zmpl b/src/app/views/collection/patch.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/collection/patch.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/collection/post.zmpl b/src/app/views/collection/post.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/collection/post.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/collection/put.zmpl b/src/app/views/collection/put.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/collection/put.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/concerts.zig b/src/app/views/concerts.zig
index 1b0b502..8125efd 100644
--- a/src/app/views/concerts.zig
+++ b/src/app/views/concerts.zig
@@ -11,3 +11,26 @@ pub fn get(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig
_ = id;
return request.render(.ok);
}
+
+pub fn post(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
+ _ = data;
+ return request.render(.created);
+}
+
+pub fn put(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
+ _ = data;
+ _ = id;
+ return request.render(.ok);
+}
+
+pub fn patch(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
+ _ = data;
+ _ = id;
+ return request.render(.ok);
+}
+
+pub fn delete(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
+ _ = data;
+ _ = id;
+ return request.render(.ok);
+}
diff --git a/src/app/views/concerts/delete.zmpl b/src/app/views/concerts/delete.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/concerts/delete.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/concerts/patch.zmpl b/src/app/views/concerts/patch.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/concerts/patch.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/concerts/post.zmpl b/src/app/views/concerts/post.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/concerts/post.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/concerts/put.zmpl b/src/app/views/concerts/put.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/concerts/put.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/groups/index.zmpl b/src/app/views/groups/index.zmpl
deleted file mode 100644
index 1522924..0000000
--- a/src/app/views/groups/index.zmpl
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-@partial partials/header
-
-Merge Songs
-
-
\ No newline at end of file
diff --git a/src/app/views/lists.zig b/src/app/views/lists.zig
index aecf0dc..8125efd 100644
--- a/src/app/views/lists.zig
+++ b/src/app/views/lists.zig
@@ -16,3 +16,21 @@ pub fn post(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
_ = data;
return request.render(.created);
}
+
+pub fn put(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
+ _ = data;
+ _ = id;
+ return request.render(.ok);
+}
+
+pub fn patch(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
+ _ = data;
+ _ = id;
+ return request.render(.ok);
+}
+
+pub fn delete(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
+ _ = data;
+ _ = id;
+ return request.render(.ok);
+}
diff --git a/src/app/views/lists/delete.zmpl b/src/app/views/lists/delete.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/lists/delete.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/lists/patch.zmpl b/src/app/views/lists/patch.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/lists/patch.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/lists/put.zmpl b/src/app/views/lists/put.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/lists/put.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/partials/_firstlast_listens.zmpl b/src/app/views/partials/_firstlast_listens.zmpl
deleted file mode 100644
index 3cf3ac9..0000000
--- a/src/app/views/partials/_firstlast_listens.zmpl
+++ /dev/null
@@ -1,11 +0,0 @@
-@args firstlast: *ZmplValue
-
-@zig {
- const songs = firstlast.items(.array);
-}
-
-
\ No newline at end of file
diff --git a/src/app/views/partials/_header.zmpl b/src/app/views/partials/_header.zmpl
index 3f9089d..bf80e53 100644
--- a/src/app/views/partials/_header.zmpl
+++ b/src/app/views/partials/_header.zmpl
@@ -1,11 +1,7 @@
-
-
-
-
\ No newline at end of file
diff --git a/src/app/views/partials/_history.zmpl b/src/app/views/partials/_history.zmpl
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/views/partials/_newtable.zmpl b/src/app/views/partials/_newtable.zmpl
deleted file mode 100644
index f9dc618..0000000
--- a/src/app/views/partials/_newtable.zmpl
+++ /dev/null
@@ -1,75 +0,0 @@
-@args T: type, table_data: *ZmplValue, columns: T
-
-
-
-
-
-@zig {
- for (columns) |header| {
- switch (header) {
- .song => {
- Song
- },
- .album => {
- Album
- },
- .artist => {
- Artist
- },
- .artistlist => {
- Artist(s)
- },
- .scrobbles => {
- Scrobbles
- },
- .date => {
- Date
- }
- }
- }
-}
-
-
-
-@zig {
- const array = table_data.items(.array);
- for (array) |row| {
-
- for (columns) |header| {
- switch (header) {
- .song => {
-
- {{row.song.name}}
-
- },
- .album => {
-
- {{row.album.name}}
-
- },
- .artist => {
-
- {{row.artist.name}}
-
- },
- .artistlist => {
-
- @for (row.get("artistlist").?) |artist| {
- {{artist.name}}
- }
-
- },
- .scrobbles => {
- {{row.scrobbles}}
- },
- .date =>{
- {{row.date}}
- }
- }
- }
-
- }
-}
-
-
-
\ No newline at end of file
diff --git a/src/app/views/partials/_random.zmpl b/src/app/views/partials/_random.zmpl
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/views/partials/_recent.zmpl b/src/app/views/partials/_recent.zmpl
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/views/partials/_table.zmpl b/src/app/views/partials/_table.zmpl
new file mode 100644
index 0000000..8ea394d
--- /dev/null
+++ b/src/app/views/partials/_table.zmpl
@@ -0,0 +1,18 @@
+@args table_data: *ZmplValue, table_headers: *ZmplValue
+
+
+
+@for (table_headers) |text| {
+ {{text}}
+}
+
+
+ @for (table_data) |value| {
+
+ {{value.track}}
+ {{value.artist}}
+ {{value.album}}
+ {{value.date}}
+
+ }
+
\ No newline at end of file
diff --git a/src/app/views/partials/_timescale.zmpl b/src/app/views/partials/_timescale.zmpl
deleted file mode 100644
index 24ef925..0000000
--- a/src/app/views/partials/_timescale.zmpl
+++ /dev/null
@@ -1,20 +0,0 @@
-@args range: *ZmplValue
-
-
-
-
-
- Year
- Scrobbles
-
-
-
- @for (range) |itm| {
-
- {{itm.date}}:
- {{itm.scrobbles}}
-
- }
-
-
-
\ No newline at end of file
diff --git a/src/app/views/ratings.zig b/src/app/views/ratings.zig
index aecf0dc..8125efd 100644
--- a/src/app/views/ratings.zig
+++ b/src/app/views/ratings.zig
@@ -16,3 +16,21 @@ pub fn post(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
_ = data;
return request.render(.created);
}
+
+pub fn put(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
+ _ = data;
+ _ = id;
+ return request.render(.ok);
+}
+
+pub fn patch(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
+ _ = data;
+ _ = id;
+ return request.render(.ok);
+}
+
+pub fn delete(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
+ _ = data;
+ _ = id;
+ return request.render(.ok);
+}
diff --git a/src/app/views/ratings/delete.zmpl b/src/app/views/ratings/delete.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/ratings/delete.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/ratings/patch.zmpl b/src/app/views/ratings/patch.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/ratings/patch.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/ratings/put.zmpl b/src/app/views/ratings/put.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/ratings/put.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/rules.zig b/src/app/views/rules.zig
index eb85a72..a2a222e 100644
--- a/src/app/views/rules.zig
+++ b/src/app/views/rules.zig
@@ -4,38 +4,101 @@ const jetzig = @import("jetzig");
pub fn index(request: *jetzig.Request) !jetzig.View {
return request.render(.ok);
}
+
+pub fn get(id: []const u8, request: *jetzig.Request) !jetzig.View {
+ _ = id;
+ return request.render(.ok);
+}
+
+pub fn new(request: *jetzig.Request) !jetzig.View {
+ return request.render(.ok);
+}
+
+pub fn edit(id: []const u8, request: *jetzig.Request) !jetzig.View {
+ _ = id;
+ return request.render(.ok);
+}
+
pub fn post(request: *jetzig.Request) !jetzig.View {
- const params = try request.params();
-
- std.log.debug("{s}", .{try params.toJson()});
-
- var job = try request.job("process_rule");
-
- _ = try job.params.put("name", params.get("rule-title"));
- _ = try job.params.put("cond_req", params.get("cond-req"));
-
- var conditionals = try job.params.put("conditionals", .array);
- inline for (0..5) |i| {
- if (!std.mem.eql(u8, "", params.getT(.string, comptime std.fmt.comptimePrint("match-txt{}", .{i})).?)) {
- //if (params.getT(.string, comptime std.fmt.comptimePrint("match-txt{}", .{i})) != null) {
- var cond = try conditionals.append(.object);
- try cond.put("match_on", params.get(comptime std.fmt.comptimePrint("match-on{}", .{i})));
- try cond.put("match_cond", params.get(comptime std.fmt.comptimePrint("match-cond{}", .{i})));
- try cond.put("match_txt", params.get(comptime std.fmt.comptimePrint("match-txt{}", .{i})));
- }
- }
-
- var actions = try job.params.put("actions", .array);
- inline for (0..5) |i| {
- if (!std.mem.eql(u8, "", params.getT(.string, comptime std.fmt.comptimePrint("action-txt{}", .{i})).?)) {
- //if (params.getT(.string, comptime std.fmt.comptimePrint("action-txt{}", .{i})) != null) {
- var act = try actions.append(.object);
- try act.put("action", params.get(comptime std.fmt.comptimePrint("action{}", .{i})));
- try act.put("action_on", params.get(comptime std.fmt.comptimePrint("action-on{}", .{i})));
- try act.put("action_txt", params.get(comptime std.fmt.comptimePrint("action-txt{}", .{i})));
- }
- }
- try job.schedule();
-
return request.render(.created);
}
+
+pub fn put(id: []const u8, request: *jetzig.Request) !jetzig.View {
+ _ = id;
+ return request.render(.ok);
+}
+
+pub fn patch(id: []const u8, request: *jetzig.Request) !jetzig.View {
+ _ = id;
+ return request.render(.ok);
+}
+
+pub fn delete(id: []const u8, request: *jetzig.Request) !jetzig.View {
+ _ = id;
+ return request.render(.ok);
+}
+
+
+test "index" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.GET, "/rules", .{});
+ try response.expectStatus(.ok);
+}
+
+test "get" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.GET, "/rules/example-id", .{});
+ try response.expectStatus(.ok);
+}
+
+test "new" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.GET, "/rules/new", .{});
+ try response.expectStatus(.ok);
+}
+
+test "edit" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.GET, "/rules/example-id/edit", .{});
+ try response.expectStatus(.ok);
+}
+
+test "post" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.POST, "/rules", .{});
+ try response.expectStatus(.created);
+}
+
+test "put" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.PUT, "/rules/example-id", .{});
+ try response.expectStatus(.ok);
+}
+
+test "patch" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.PATCH, "/rules/example-id", .{});
+ try response.expectStatus(.ok);
+}
+
+test "delete" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.DELETE, "/rules/example-id", .{});
+ try response.expectStatus(.ok);
+}
diff --git a/src/app/views/rules/delete.zmpl b/src/app/views/rules/delete.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/rules/delete.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/rules/edit.zmpl b/src/app/views/rules/edit.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/rules/edit.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/rules/get.zmpl b/src/app/views/rules/get.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/rules/get.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/rules/index.zmpl b/src/app/views/rules/index.zmpl
index 5661f97..76457d0 100644
--- a/src/app/views/rules/index.zmpl
+++ b/src/app/views/rules/index.zmpl
@@ -1,62 +1,3 @@
-
-
-
-
-
-@partial partials/header
-Rules
-Rules allow you change the default Scrobble import behavior based on provided criteria.
-Add a rule below.
-
-
-
-Current rules:
-
-
\ No newline at end of file
+
+ Content goes here
+
diff --git a/src/app/views/rules/new.zmpl b/src/app/views/rules/new.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/rules/new.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/rules/patch.zmpl b/src/app/views/rules/patch.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/rules/patch.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/rules/put.zmpl b/src/app/views/rules/put.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/rules/put.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/scrobbles.zig b/src/app/views/scrobbles.zig
index 754a7c8..ebda828 100644
--- a/src/app/views/scrobbles.zig
+++ b/src/app/views/scrobbles.zig
@@ -1,12 +1,64 @@
const std = @import("std");
const jetzig = @import("jetzig");
-const queries = @import("../../queries.zig");
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);
+ for (scrobbles) |scrobble| {
+ var scrobble_view = try scrobbles_view.append(.object);
- const scrobbles = try queries.entityQueryResult(request, queries.loadQuery(.scrobble, .entities), .{});
- try root.put("scrobbles", scrobbles);
+ 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);
+}
+
+pub fn get(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
+ _ = id;
+ _ = data;
+ return request.render(.ok);
+}
+
+pub fn post(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
+ _ = data;
+ return request.render(.created);
+}
+
+pub fn put(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
+ _ = data;
+ _ = id;
+ return request.render(.ok);
+}
+
+pub fn patch(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
+ _ = data;
+ _ = id;
+ return request.render(.ok);
+}
+
+pub fn delete(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
+ _ = data;
+ _ = id;
return request.render(.ok);
}
diff --git a/src/app/views/scrobbles/delete.zmpl b/src/app/views/scrobbles/delete.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/scrobbles/delete.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/scrobbles/get.zmpl b/src/app/views/scrobbles/get.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/scrobbles/get.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/scrobbles/index.zmpl b/src/app/views/scrobbles/index.zmpl
index c3d6759..377f50f 100644
--- a/src/app/views/scrobbles/index.zmpl
+++ b/src/app/views/scrobbles/index.zmpl
@@ -1,15 +1,42 @@
-@zig {
- const ColumnChoices = []const enum{song, album, artist, artistlist, scrobbles, date};
- const columns: ColumnChoices = &.{.song, .artistlist, .album, .date};
-}
-
+
@partial partials/header
Scrobbles
-@partial partials/newtable(T: ColumnChoices, table_data: .scrobbles, columns: columns)
+
+
+
\ No newline at end of file
diff --git a/src/app/views/scrobbles/patch.zmpl b/src/app/views/scrobbles/patch.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/scrobbles/patch.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/scrobbles/post.zmpl b/src/app/views/scrobbles/post.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/scrobbles/post.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/scrobbles/put.zmpl b/src/app/views/scrobbles/put.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/scrobbles/put.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/groups.zig b/src/app/views/search.zig
similarity index 81%
rename from src/app/views/groups.zig
rename to src/app/views/search.zig
index 85505c8..be5f7e0 100644
--- a/src/app/views/groups.zig
+++ b/src/app/views/search.zig
@@ -43,7 +43,7 @@ test "index" {
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
defer app.deinit();
- const response = try app.request(.GET, "/groups", .{});
+ const response = try app.request(.GET, "/search", .{});
try response.expectStatus(.ok);
}
@@ -51,7 +51,7 @@ test "get" {
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
defer app.deinit();
- const response = try app.request(.GET, "/groups/example-id", .{});
+ const response = try app.request(.GET, "/search/example-id", .{});
try response.expectStatus(.ok);
}
@@ -59,7 +59,7 @@ test "new" {
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
defer app.deinit();
- const response = try app.request(.GET, "/groups/new", .{});
+ const response = try app.request(.GET, "/search/new", .{});
try response.expectStatus(.ok);
}
@@ -67,7 +67,7 @@ test "edit" {
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
defer app.deinit();
- const response = try app.request(.GET, "/groups/example-id/edit", .{});
+ const response = try app.request(.GET, "/search/example-id/edit", .{});
try response.expectStatus(.ok);
}
@@ -75,7 +75,7 @@ test "post" {
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
defer app.deinit();
- const response = try app.request(.POST, "/groups", .{});
+ const response = try app.request(.POST, "/search", .{});
try response.expectStatus(.created);
}
@@ -83,7 +83,7 @@ test "put" {
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
defer app.deinit();
- const response = try app.request(.PUT, "/groups/example-id", .{});
+ const response = try app.request(.PUT, "/search/example-id", .{});
try response.expectStatus(.ok);
}
@@ -91,7 +91,7 @@ test "patch" {
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
defer app.deinit();
- const response = try app.request(.PATCH, "/groups/example-id", .{});
+ const response = try app.request(.PATCH, "/search/example-id", .{});
try response.expectStatus(.ok);
}
@@ -99,6 +99,6 @@ test "delete" {
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
defer app.deinit();
- const response = try app.request(.DELETE, "/groups/example-id", .{});
+ const response = try app.request(.DELETE, "/search/example-id", .{});
try response.expectStatus(.ok);
}
diff --git a/src/app/views/search/delete.zmpl b/src/app/views/search/delete.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/search/delete.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/search/edit.zmpl b/src/app/views/search/edit.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/search/edit.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/search/get.zmpl b/src/app/views/search/get.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/search/get.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/search/index.zmpl b/src/app/views/search/index.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/search/index.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/search/new.zmpl b/src/app/views/search/new.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/search/new.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/search/patch.zmpl b/src/app/views/search/patch.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/search/patch.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/search/post.zmpl b/src/app/views/search/post.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/search/post.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/search/put.zmpl b/src/app/views/search/put.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/search/put.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/songs.zig b/src/app/views/songs.zig
index b634534..6c134b1 100644
--- a/src/app/views/songs.zig
+++ b/src/app/views/songs.zig
@@ -1,40 +1,129 @@
const std = @import("std");
const jetzig = @import("jetzig");
-const queries = @import("../../queries.zig");
pub fn index(request: *jetzig.Request) !jetzig.View {
var root = try request.data(.object);
+ var songs_view = try root.put("songs", .array);
+ const songs = try jetzig.database.Query(.Song)
+ .select(.{ .id, .name })
+ .include(.songartists, .{ .select = .{.artist_id} })
+ .include(.scrobbles, .{ .select = .{.id} })
+ .orderBy(.{ .name = .asc })
+ .all(request.repo);
- const htmx_query = (try request.queryParams()).getT(.string, "s");
-
- try root.put("htmx", htmx_query != null);
-
- const songs = if (htmx_query) |name|
- try queries.entityQueryResult(request, queries.loadQuery(.song, .entities_by_name), .{name})
- else
- try queries.entityQueryResult(request, queries.loadQuery(.song, .entities), .{});
-
- try root.put("songs", songs);
+ for (songs) |song| {
+ var song_view = try songs_view.append(.object);
+ var artist_infos = try song_view.put("artist_info", .array);
+ for (song.songartists) |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 song_view.put("name", song.name);
+ try song_view.put("url", song.id);
+ try song_view.put("scrobbles", (song.scrobbles).len);
+ }
return request.render(.ok);
}
pub fn get(id: []const u8, request: *jetzig.Request) !jetzig.View {
- var root = try request.data(.object);
-
- 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, .get_scrobbles), .{id});
- try root.put("scrobbles", scrobbles);
-
- 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);
-
- const timescale = try queries.entityQueryResult(request, queries.loadQuery(.song, .timescale), .{id});
- try root.put("yearly", timescale);
+ _ = id;
return request.render(.ok);
}
+
+pub fn new(request: *jetzig.Request) !jetzig.View {
+ return request.render(.ok);
+}
+
+pub fn edit(id: []const u8, request: *jetzig.Request) !jetzig.View {
+ _ = id;
+ return request.render(.ok);
+}
+
+pub fn post(request: *jetzig.Request) !jetzig.View {
+ return request.render(.created);
+}
+
+pub fn put(id: []const u8, request: *jetzig.Request) !jetzig.View {
+ _ = id;
+ return request.render(.ok);
+}
+
+pub fn patch(id: []const u8, request: *jetzig.Request) !jetzig.View {
+ _ = id;
+ return request.render(.ok);
+}
+
+pub fn delete(id: []const u8, request: *jetzig.Request) !jetzig.View {
+ _ = id;
+ return request.render(.ok);
+}
+
+test "index" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.GET, "/song", .{});
+ try response.expectStatus(.ok);
+}
+
+test "get" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.GET, "/song/example-id", .{});
+ try response.expectStatus(.ok);
+}
+
+test "new" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.GET, "/song/new", .{});
+ try response.expectStatus(.ok);
+}
+
+test "edit" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.GET, "/song/example-id/edit", .{});
+ try response.expectStatus(.ok);
+}
+
+test "post" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.POST, "/song", .{});
+ try response.expectStatus(.created);
+}
+
+test "put" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.PUT, "/song/example-id", .{});
+ try response.expectStatus(.ok);
+}
+
+test "patch" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.PATCH, "/song/example-id", .{});
+ try response.expectStatus(.ok);
+}
+
+test "delete" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.DELETE, "/song/example-id", .{});
+ try response.expectStatus(.ok);
+}
diff --git a/src/app/views/songs/delete.zmpl b/src/app/views/songs/delete.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/songs/delete.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/songs/edit.zmpl b/src/app/views/songs/edit.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/songs/edit.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/songs/get.zmpl b/src/app/views/songs/get.zmpl
index 1ceddef..76457d0 100644
--- a/src/app/views/songs/get.zmpl
+++ b/src/app/views/songs/get.zmpl
@@ -1,32 +1,3 @@
-@zig {
- const ColumnChoices = []const enum{song, album, artist, artistlist, scrobbles, date};
- const columns: ColumnChoices = &.{.song, .artistlist, .album, .date};
-}
-
-
-
-
-
-
-@partial partials/header
-
-
{{.song.song_name}}
+
+ Content goes here
-
-
-
-
{{.song.scrobbles}} scrobbles ({{.song.rank}} place)
- @partial partials/firstlast_listens(firstlast: .firstlast)
-
Yearly Performance
- @partial partials/timescale(range: .yearly)
-
Scrobbles
- @partial partials/newtable(T: ColumnChoices, table_data: .scrobbles, columns: columns)
-
-
-
Rating
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/app/views/songs/index.zmpl b/src/app/views/songs/index.zmpl
index 862b753..a0a1128 100644
--- a/src/app/views/songs/index.zmpl
+++ b/src/app/views/songs/index.zmpl
@@ -1,17 +1,40 @@
-@zig {
- const ColumnChoices = []const enum{song, album, artist, artistlist, scrobbles, date};
- const columns: ColumnChoices = &.{.song, .album, .artistlist, .scrobbles};
-}
-
+
- @if (! $.htmx)
@partial partials/header
Songs
- @end
-@partial partials/newtable(T: ColumnChoices, table_data: .songs, columns: columns)
+
+
+
+Name
+Artists(s)
+Scrobbles
+
+
+
+@for (.songs) |song| {
+
+ {{song.name}}
+
+ @for (song.get("artist_info").?) |ai| {
+ {{ai.name}}
+ }
+
+ {{song.scrobbles}}
+
+}
+
+
+
+
\ No newline at end of file
diff --git a/src/app/views/songs/new.zmpl b/src/app/views/songs/new.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/songs/new.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/songs/patch.zmpl b/src/app/views/songs/patch.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/songs/patch.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/songs/post.zmpl b/src/app/views/songs/post.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/songs/post.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/songs/put.zmpl b/src/app/views/songs/put.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/songs/put.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/upload.zig b/src/app/views/upload.zig
index f712ba4..53f3b27 100644
--- a/src/app/views/upload.zig
+++ b/src/app/views/upload.zig
@@ -1,158 +1,135 @@
const std = @import("std");
const jetzig = @import("jetzig");
const jetquery = @import("jetzig").jetquery;
+const ScrobbleTypes = @import("../../types.zig");
const zeit = @import("zeit");
-const rules = @import("../../apply_rule.zig");
-const Data = @import("../../types.zig");
-const Utils = @import("../../date_fmt.zig");
-const Client = std.http.Client;
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
_ = data;
return request.render(.ok);
}
+pub fn get(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
+ _ = data;
+ _ = id;
+ return request.render(.ok);
+}
+
pub fn post(request: *jetzig.Request) !jetzig.View {
var root = try request.data(.object);
- const params = try request.params();
- 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,
- });
+ if (try request.file("upload")) |file| {
+ const params = try request.params();
+ const source = try std.fmt.parseInt(u8, params.get("t").?.string.value, 10); // This param is required in HTML
+ const before_limiter: bool = if (params.get("bbool")) |_| true else false;
+ const after_limiter: bool = if (params.get("abool")) |_| true else false;
- defer rule_file.close();
- const rule_file_content = try rule_file.readToEndAlloc(request.allocator, 16_000_000);
- const rule_list = std.json.parseFromSliceLeaky([]Data.Rule, request.allocator, rule_file_content, .{}) catch null;
- var job = try request.job("process_scrobbles2");
- const source = params.getT(.integer, "t").?; // This param is required in HTML
+ var scrobbles_view = try root.put("scrobbles", .array);
+ var job = try request.job("process_scrobbles");
+ var scrobbles_data = try job.params.put("scrobbles", .array);
- const latest_date = if (params.getT(.boolean, "adv-opt")) |_| blk: {
- const date = params.getT(.string, "latest-date").?;
- break :blk try zeit.Time.fromISO8601(date);
- } else (try zeit.instant(.{ .source = .now })).time();
+ var skipped_tracks: u64 = 0;
+ var limited_tracks: u64 = 0;
- const earliest_date = if (params.getT(.boolean, "adv-opt")) |_| blk: {
- const date = params.getT(.string, "earliest-date").?;
- break :blk try zeit.Time.fromISO8601(date);
- } else (try zeit.instant(.{ .source = .{ .unix_timestamp = 0 } })).time();
+ // 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) {
+ 0 => {
+ const content: ScrobbleTypes.LastFM = try std.json.parseFromSliceLeaky(ScrobbleTypes.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 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| {
+ // 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;
+ var value = try scrobbles_data.append(.object);
- const earliest_timestamp = earliest_date.instant().unixTimestamp();
- const latest_timestamp = latest_date.instant().unixTimestamp();
+ // This is so unnecessary, probably useful once I start doing Spotify integration though
+ inline for (std.meta.fields(ScrobbleTypes.LastFMScrobble)) |f| {
+ try value.put(f.name, @as(f.type, @field(scrobble, f.name)));
+ }
+ // Note sure why this works for ZMPL, but not for jobs.
+ try scrobbles_view.append(scrobble);
+ }
+ },
+ 1 => {
+ const content: []ScrobbleTypes.SpotifyScrobble = try std.json.parseFromSliceLeaky([]ScrobbleTypes.SpotifyScrobble, request.allocator, file.content, .{});
+ 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();
+ 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"))) {
+ skipped_tracks += 1;
+ 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) {
+ skipped_tracks += 1;
+ continue :appends;
+ }
- var view_params = try root.put("scrobbles", .array);
+ const iso_ts = try zeit.Time.fromISO8601(scrobble.ts);
+ if ((before_limiter or after_limiter) and (iso_ts.after(before_limiting_date) or iso_ts.before(after_limiting_date))) {
+ limited_tracks += 1;
+ continue :appends;
+ }
- const imported_scrobbles = switch (source) {
- 0, 1 => (try std.json.parseFromSliceLeaky(Data.ScrobbleArray, request.allocator, if (try request.file("upload")) |file| file.content else unreachable, .{ .ignore_unknown_fields = true })).scrobbles,
- 2 => blk: {
- const user_agent: []const u8 = "Zuletzt/0.0.1";
- var client = Client{ .allocator = request.allocator };
- var lastfm_response_buffer = std.ArrayList(u8).init(request.allocator);
- var scrobble_buffer = std.ArrayList(Data.Scrobble).init(request.allocator);
+ // Turn SpotifyScrobble into a LastFM scrobble
+ const 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 username = if (params.getT(.string, "username")) |un| un else "VAOTM";
+ var value = try scrobbles_data.append(.object);
- const mp_query: []const u8 = try std.fmt.allocPrint(request.allocator, "https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user={s}&api_key=b0c410a48a6078a651e0832699e3cd41&from={}&to={}&page={}&limit=1000&format=json", .{ username, earliest_timestamp, latest_timestamp, 0 });
- const mp_r = try client.fetch(.{ .response_storage = .{ .dynamic = &lastfm_response_buffer }, .location = .{ .url = mp_query }, .method = .GET, .headers = .{ .user_agent = .{ .override = user_agent } } });
- std.log.debug("Max page query: {}", .{mp_r});
- const parsed_mp_response = try std.json.parseFromSliceLeaky(Data.LastFMWeb, request.allocator, (try lastfm_response_buffer.toOwnedSlice()), .{ .ignore_unknown_fields = true });
-
- var page: usize = 1;
- const max_pages: usize = try std.fmt.parseInt(usize, parsed_mp_response.recenttracks.@"@attr".totalPages, 10);
-
- while (true) : (page += 1) {
- if (page > max_pages) break;
- const query: []const u8 = try std.fmt.allocPrint(request.allocator, "https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user={s}&api_key=b0c410a48a6078a651e0832699e3cd41&from={}&to={}&page={}&limit=1000&format=json", .{ username, earliest_timestamp, latest_timestamp, page });
- const r = try client.fetch(.{ .response_storage = .{ .dynamic = &lastfm_response_buffer }, .location = .{ .url = query }, .method = .GET, .headers = .{ .user_agent = .{ .override = user_agent } } });
- std.log.debug("{}: {}", .{ page, r });
- const response_string = try lastfm_response_buffer.toOwnedSlice();
- const parsed_lastfm_response = try std.json.parseFromSliceLeaky(Data.ScrobbleArray, request.allocator, response_string, .{ .ignore_unknown_fields = true });
- try scrobble_buffer.appendSlice(parsed_lastfm_response.scrobbles);
- }
-
- break :blk scrobble_buffer.items;
- },
- else => unreachable,
- };
-
- var artists = try job.params.put("artists", .object);
- var albums = try job.params.put("albums", .object);
- var tracks = try job.params.put("tracks", .object);
- var artistalbums = try job.params.put("artistalbums", .object);
- var albumsongs = try job.params.put("albumsongs", .object);
- var albumsongsartists = try job.params.put("albumsongsartists", .object);
-
- for (imported_scrobbles) |scrobble| {
- if (scrobble.date > latest_timestamp * 1_000_000 or scrobble.date < earliest_timestamp * 1_000_000) continue;
- const complete_scrobble = if (rule_list) |rl| try rules.applyScrobbleRule(request.allocator, scrobble, rl) else scrobble;
-
- const row = try Utils.scrobbleToRow(request.allocator, complete_scrobble);
-
- try view_params.append(row);
- var stored_artist_hashes = std.ArrayList(u64).init(request.allocator);
-
- var album_hash_string = std.ArrayList(u8).init(request.allocator);
- for (complete_scrobble.artists_album) |artist| {
- try album_hash_string.appendSlice(artist);
- const artist_hash = std.hash.Fnv1a_64.hash(artist);
- try stored_artist_hashes.append(artist_hash);
- const signed_hash_string = try std.fmt.allocPrint(request.allocator, "{}", .{@as(i64, @bitCast(artist_hash))});
- if (artists.get(signed_hash_string) == null) try artists.put(signed_hash_string, artist);
- }
-
- try album_hash_string.appendSlice(complete_scrobble.album);
- const album_hash = std.hash.Fnv1a_64.hash(album_hash_string.items);
- const signed_album_hash_string = try std.fmt.allocPrint(request.allocator, "{}", .{@as(i64, @bitCast(album_hash))});
- if (albums.get(signed_album_hash_string) == null) try albums.put(signed_album_hash_string, complete_scrobble.album);
-
- for (stored_artist_hashes.items) |artist_hash| {
- const artistalbum_hash = pair(artist_hash, album_hash);
- const signed_artistalbums_hash_string = try std.fmt.allocPrint(request.allocator, "{}", .{@as(i64, @bitCast(artistalbum_hash))});
- if (tracks.get(signed_artistalbums_hash_string) == null) {
- var artistalbum = try artistalbums.put(signed_artistalbums_hash_string, .object);
- try artistalbum.put("artist", @as(i64, @bitCast(artist_hash)));
- try artistalbum.put("album", @as(i64, @bitCast(album_hash)));
- }
- }
-
- var track_hash_string = std.ArrayList(u8).init(request.allocator);
- try track_hash_string.appendSlice(complete_scrobble.album);
- try track_hash_string.appendSlice(complete_scrobble.track);
- const track_hash = std.hash.Fnv1a_64.hash(track_hash_string.items);
- const signed_track_hash_string = try std.fmt.allocPrint(request.allocator, "{}", .{@as(i64, @bitCast(track_hash))});
- if (tracks.get(signed_track_hash_string) == null) try tracks.put(signed_track_hash_string, complete_scrobble.track);
-
- const albumsong_hash = pair(album_hash, track_hash);
- const signed_albumsong_hash_string = try std.fmt.allocPrint(request.allocator, "{}", .{@as(i64, @bitCast(albumsong_hash))});
- if (albumsongs.get(signed_albumsong_hash_string)) |albumsong| {
- var albumsong_scrobbles = albumsong.get("scrobbles");
- try albumsong_scrobbles.?.append(complete_scrobble.date);
- } else {
- var albumsong = try albumsongs.put(signed_albumsong_hash_string, .object);
- try albumsong.put("album", @as(i64, @bitCast(album_hash)));
- try albumsong.put("song", @as(i64, @bitCast(track_hash)));
- var albumsong_scrobbles = try albumsong.put("scrobbles", .array);
- try albumsong_scrobbles.append(complete_scrobble.date);
- }
-
- for (complete_scrobble.artists_track) |artist| {
- const artist_hash = std.hash.Fnv1a_64.hash(artist);
- const signed_artist_hash_string = try std.fmt.allocPrint(request.allocator, "{}", .{@as(i64, @bitCast(artist_hash))});
- if (artists.get(signed_artist_hash_string) == null) try artists.put(signed_artist_hash_string, artist);
- const albumsongsartist_hash = pair(albumsong_hash, artist_hash);
- const signed_albumsongsartist_hash_string = try std.fmt.allocPrint(request.allocator, "{}", .{albumsongsartist_hash});
- if (albumsongsartists.get(signed_albumsongsartist_hash_string) == null) {
- var albumsongartist = try albumsongsartists.put(signed_albumsongsartist_hash_string, .object);
- try albumsongartist.put("albumsong", @as(i64, @bitCast(albumsong_hash)));
- try albumsongartist.put("artist", @as(i64, @bitCast(artist_hash)));
- }
+ // This is so unnecessary, probably useful once I start doing Spotify integration though
+ inline for (std.meta.fields(ScrobbleTypes.LastFMScrobble)) |f| {
+ 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 scrobbles_view.append(formatted_scrobble);
+ }
+ },
+ else => unreachable,
}
+ try job.schedule();
+ std.log.debug("Skipped {} tracks", .{skipped_tracks});
+ std.log.debug("Filtered {} tracks", .{limited_tracks});
}
- try job.schedule();
+
+ var upload_table = try root.put("upload_table", .array);
+ try upload_table.append("Track");
+ try upload_table.append("Artist");
+ try upload_table.append("Album");
+ try upload_table.append("Date");
return request.render(.created);
}
-fn pair(a: u64, b: u64) u64 {
- return @divFloor((a +% b) *% (a +% b +% 1) +% b, 2);
+pub fn put(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
+ _ = data;
+ _ = id;
+ return request.render(.ok);
+}
+
+pub fn patch(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
+ _ = data;
+ _ = id;
+ return request.render(.ok);
+}
+
+pub fn delete(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
+ _ = data;
+ _ = id;
+ return request.render(.ok);
}
diff --git a/src/app/views/upload/delete.zmpl b/src/app/views/upload/delete.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/upload/delete.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/upload/get.zmpl b/src/app/views/upload/get.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/upload/get.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/upload/index.zmpl b/src/app/views/upload/index.zmpl
index 420e08c..9043a5b 100644
--- a/src/app/views/upload/index.zmpl
+++ b/src/app/views/upload/index.zmpl
@@ -9,18 +9,13 @@
diff --git a/src/app/views/upload/patch.zmpl b/src/app/views/upload/patch.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/upload/patch.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/app/views/upload/post.zmpl b/src/app/views/upload/post.zmpl
index 91c5347..176f094 100644
--- a/src/app/views/upload/post.zmpl
+++ b/src/app/views/upload/post.zmpl
@@ -1,16 +1,15 @@
-@zig {
- const ColumnChoices = []const enum{song, album, artist, artistlist, scrobbles, date};
- const columns: ColumnChoices = &.{.song, .artistlist, .album, .date};
-}
-
+
@partial partials/header
File Uploaded Successfully
+
Scrobbles Added
-@partial partials/newtable(T: ColumnChoices, table_data: .scrobbles, columns: columns)
+
+@partial partials/table(table_data: .scrobbles, table_headers: .upload_table, table_context: .context)
+
\ No newline at end of file
diff --git a/src/app/views/upload/put.zmpl b/src/app/views/upload/put.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/src/app/views/upload/put.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/apply_rule.zig b/src/apply_rule.zig
deleted file mode 100644
index fc4c0bc..0000000
--- a/src/apply_rule.zig
+++ /dev/null
@@ -1,71 +0,0 @@
-const std = @import("std");
-const Rule = @import("./types.zig").Rule;
-const Data = @import("./types.zig");
-
-// Wrapper for containsAtLeast to make the switch below to work
-fn containsWrapper(haystack: []const u8, needle: []const u8) bool {
- return std.mem.containsAtLeast(u8, haystack, 1, needle);
-}
-
-fn eqlWrapper(haystack: []const u8, needle: []const u8) bool {
- return std.mem.eql(u8, haystack, needle);
-}
-
-pub fn applyScrobbleRule(allocator: std.mem.Allocator, scrobble: Data.Scrobble, rules: []Rule) !Data.Scrobble {
- var output_scrobble = scrobble;
-
- for (rules) |rule| {
- var match_found: bool = switch (rule.cond_req) {
- .any => false,
- .all => true,
- };
- for (rule.conditionals) |cond| {
- const match_fn: *const fn ([]const u8, []const u8) bool = switch (cond.match_cond) {
- .is => eqlWrapper,
- .contains => containsWrapper,
- };
- switch (rule.cond_req) {
- .any => switch (cond.match_on) {
- inline .album, .track => |on| match_found = match_found or match_fn(@field(scrobble, @tagName(on)), cond.match_txt),
- inline .artists_album, .artists_track => |on| {
- for (@field(scrobble, @tagName(on))) |artist| match_found = match_found or match_fn(artist, cond.match_txt);
- },
- },
- .all => switch (cond.match_on) {
- inline .album, .track => |on| match_found = match_found and match_fn(@field(scrobble, @tagName(on)), cond.match_txt),
- inline .artists_album, .artists_track => |on| {
- for (@field(scrobble, @tagName(on))) |artist| match_found = match_found and match_fn(artist, cond.match_txt);
- },
- },
- }
- }
- if (match_found) {
- for (rule.actions) |act| {
- switch (act.action) {
- .add => {
- var al = std.ArrayList([]const u8).init(allocator);
- switch (act.action_on) {
- .album, .track => unreachable,
- inline else => |on| {
- try al.appendSlice(@field(output_scrobble, @tagName(on)));
- try al.append(act.action_txt);
- const list = try al.toOwnedSlice();
- @field(output_scrobble, @tagName(on)) = list;
- },
- }
- },
- .replace => switch (act.action_on) {
- inline .album, .track => |on| @field(output_scrobble, @tagName(on)) = act.action_txt,
- inline .artists_album, .artists_track => |on| {
- const artist = try allocator.alloc([]const u8, 1);
- artist[0] = act.action_txt;
- @field(output_scrobble, @tagName(on)) = artist;
- },
- },
- }
- }
- }
- }
-
- return output_scrobble;
-}
diff --git a/src/date_fmt.zig b/src/date_fmt.zig
deleted file mode 100644
index 7e44da7..0000000
--- a/src/date_fmt.zig
+++ /dev/null
@@ -1,35 +0,0 @@
-const std = @import("std");
-const zeit = @import("zeit");
-const Data = @import("types.zig");
-
-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_000) } })).time().strftime(date.writer(), "%d %b %Y, %H:%M");
- return date.items;
-}
-
-pub fn dateCompare(self: *[]const u8, earliest: []const u8, latest: []const u8) bool {
- const range = if (self.len == 19) 4 else 2;
- const time = std.fmt.parseInt(u32, self[0..range], 10) catch 0;
- const etime = std.fmt.parseInt(u32, earliest[0..range], 10) catch 0;
- const ltime = std.fmt.parseInt(u32, latest[0..range], 10) catch 0;
-
- if (time != etime and time != ltime) {
- return (time > etime and time < ltime);
- } else {
- return dateCompare(self[range + 1 ..], earliest[range + 1 ..], latest[range + 1 ..]);
- }
-}
-
-pub fn scrobbleToRow(allocator: std.mem.Allocator, scrobble: Data.Scrobble) !Data.TableRow {
- var artistlist = std.ArrayList(Data.HyperlinkData).init(allocator);
- for (scrobble.artists_track) |a| {
- try artistlist.append(Data.HyperlinkData{ .name = a, .id = 0 });
- }
- return Data.TableRow{
- .song = .{ .name = scrobble.track, .id = 0 },
- .artistlist = artistlist.items,
- .album = .{ .name = scrobble.album, .id = 0 },
- .date = try dateFmt(allocator, scrobble.date),
- };
-}
diff --git a/src/main.zig b/src/main.zig
index cdc9bca..1cab920 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -14,13 +14,10 @@ pub const jetzig_options = struct {
// htmx middleware skips layouts when `HX-Target` header is present and issues
// `HX-Redirect` instead of a regular HTTP redirect when `request.redirect` is called.
jetzig.middleware.HtmxMiddleware,
- // Demo middleware included with new projects. Remove once you are familiar with Jetzig's
- // middleware system.
+ // Demo middleware included with new projects. Remove once you are familiar with Jetzig's
+ // middleware system.
};
- // This is currently the largest number of parameters one can have in a rule
- pub const max_multipart_form_fields = 42;
-
// Maximum bytes to allow in request body.
pub const max_bytes_request_body: usize = std.math.pow(usize, 2, 24);
diff --git a/src/ordinal_fmt.zig b/src/ordinal_fmt.zig
deleted file mode 100644
index 6b126ab..0000000
--- a/src/ordinal_fmt.zig
+++ /dev/null
@@ -1,16 +0,0 @@
-const std = @import("std");
-//const log = std.math.log10;
-
-pub fn ordinalFmt(allocator: std.mem.Allocator, ord: isize) ![]const u8 {
- const buff = try allocator.alloc(u8, 3 + @as(usize, @intFromFloat(@floor(@log10(@as(f64, @floatFromInt(ord)))))));
- const ind: []const u8 = switch (@mod(ord, 100)) {
- 11, 12, 13 => "th",
- else => switch (@mod(ord, 10)) {
- 1 => "st",
- 2 => "nd",
- 3 => "rd",
- else => "th",
- },
- };
- return std.fmt.bufPrint(buff, "{}{s}", .{ ord, ind });
-}
diff --git a/src/queries.zig b/src/queries.zig
deleted file mode 100644
index fcc5f07..0000000
--- a/src/queries.zig
+++ /dev/null
@@ -1,440 +0,0 @@
-// Probably the worst code in the project
-const jetzig = @import("jetzig");
-const TableRow = @import("types.zig").TableRow;
-const HyperlinkData = @import("types.zig").HyperlinkData;
-const std = @import("std");
-
-pub fn entityQueryResult(request: *jetzig.Request, query: GeneratedQuery, args: anytype) !*jetzig.Data.Value {
- //var result = try request.repo.executeSql(query.query, args);
- //
- var result = try request.repo.connection.?.postgresql.connection.queryOpts(query.query, args, .{ .allocator = request.allocator, .column_names = true });
-
- defer result.deinit();
-
- var Data = jetzig.Data.init(request.allocator);
-
- var artist_list = std.ArrayList(HyperlinkData).init(request.allocator);
-
- if (query.query_type == .entity_info) {
- var out: *jetzig.Data.Value = try Data.object();
- const entity = try (try result.next()).?.to(EntityInfoResult, .{ .dupe = true, .allocator = request.allocator, .map = .name });
- try out.put("entity_info", entity);
- try result.drain();
- return out.get("entity_info").?;
- }
-
- var out: *jetzig.Data.Value = try Data.array();
- var mapper = result.mapper(UnifiedResult, .{ .dupe = true, .allocator = request.allocator });
-
- blk: while (try mapper.next()) |entity| {
- if (entity.artist_id) |_| {
- const last_artist = artist_list.getLastOrNull();
- try artist_list.append(.{ .name = entity.artist_name.?, .id = entity.artist_id.? });
- if (last_artist) |la| {
- if (la.id == entity.artist_id) continue :blk;
- }
- }
-
- try out.append(TableRow{
- .artist = if (entity.artist_id) |_| .{ .id = entity.artist_id.?, .name = entity.artist_name.? } else null,
- .album = if (entity.album_id) |_| .{ .id = entity.album_id.?, .name = entity.album_name.? } else null,
- .song = if (entity.song_id) |_| .{ .id = entity.song_id.?, .name = entity.song_name.? } else null,
- .artistlist = if (artist_list.getLastOrNull()) |_| try artist_list.toOwnedSlice() else null,
- .scrobbles = if (entity.scrobbles) |scrobbles| scrobbles else null,
- .date = if (entity.date) |date| date else null,
- });
- }
-
- return out;
-}
-
-const EntityType = enum { scrobble, song, album, artist };
-const QueryTypeEnum = enum { firstlast, timescale, entities, get_songs, get_albums, get_scrobbles, appears, entity_info, datestreak, entities_by_name };
-
-const GeneratedQuery = struct {
- entity: EntityType,
- query_type: QueryTypeEnum,
- query: []const u8,
-};
-
-const UnifiedResult = struct {
- album_name: ?[]const u8 = null,
- album_id: ?i64 = null,
- song_name: ?[]const u8 = null,
- song_id: ?i64 = null,
- artist_name: ?[]const u8 = null,
- artist_id: ?i64 = null,
- scrobbles: ?i64 = null,
- date: ?[]const u8 = null,
-};
-
-const EntityInfoResult = struct {
- album_name: ?[]const u8 = null,
- album_id: ?i64 = null,
- song_name: ?[]const u8 = null,
- song_id: ?i64 = null,
- artist_name: ?[]const u8 = null,
- artist_id: ?i64 = null,
- scrobbles: ?i64 = null,
- date: ?[]const u8 = null,
- rank: []const u8,
- song_num: ?i64 = null,
- album_num: ?i64 = null,
-};
-
-pub fn loadQuery(entity: EntityType, query_type: QueryTypeEnum) GeneratedQuery {
- return GeneratedQuery{
- .entity = entity,
- .query_type = query_type,
- .query = switch (query_type) {
- .firstlast =>
- //.ResultType = FirstlastResult,
- switch (entity) {
- .scrobble => unreachable,
- .song =>
- \\(SELECT songs.name AS song_name, songs.id AS song_id, TO_CHAR(scrobbles.datetime,'YYYY-MM-DD HH24:MI:SS') AS date
- \\FROM albumsongs
- \\INNER JOIN songs ON songs.id = albumsongs.song_id
- \\INNER JOIN scrobbles ON albumsongs.id = scrobbles.albumsong
- \\WHERE albumsongs.song_id = $1
- \\ORDER BY scrobbles.datetime ASC
- \\LIMIT 1)
- \\
- \\UNION ALL
- \\
- \\(SELECT songs.name AS song_name, songs.id AS song_id, TO_CHAR(scrobbles.datetime, 'YYYY-MM-DD HH24:MI:SS') AS date
- \\FROM albumsongs
- \\INNER JOIN songs ON songs.id = albumsongs.song_id
- \\INNER JOIN scrobbles ON albumsongs.id = scrobbles.albumsong
- \\WHERE albumsongs.song_id = $1
- \\ORDER BY scrobbles.datetime DESC
- \\LIMIT 1)
- ,
-
- .album =>
- \\(SELECT songs.name AS song_name, songs.id AS song_id, TO_CHAR(scrobbles.datetime, 'YYYY-MM-DD HH24:MI:SS') AS date
- \\FROM albumsongs
- \\INNER JOIN songs ON songs.id = albumsongs.song_id
- \\INNER JOIN scrobbles ON albumsongs.id = scrobbles.albumsong
- \\INNER JOIN albums ON albums.id = albumsongs.album_id
- \\WHERE albums.id = $1
- \\ORDER BY scrobbles.datetime ASC
- \\LIMIT 1)
- \\
- \\UNION ALL
- \\
- \\(SELECT songs.name AS song_name, songs.id AS song_id, TO_CHAR(scrobbles.datetime, 'YYYY-MM-DD HH24:MI:SS') AS date
- \\FROM albumsongs
- \\INNER JOIN songs ON songs.id = albumsongs.song_id
- \\INNER JOIN scrobbles ON albumsongs.id = scrobbles.albumsong
- \\INNER JOIN albums ON albums.id = albumsongs.album_id
- \\WHERE albums.id = $1
- \\ORDER BY scrobbles.datetime DESC
- \\LIMIT 1)
- ,
-
- .artist =>
- \\(SELECT songs.name AS song_name, songs.id AS song_id, TO_CHAR(scrobbles.datetime, 'YYYY-MM-DD HH24:MI:SS') 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)
- \\
- \\UNION ALL
- \\
- \\(SELECT songs.name AS song_name, songs.id AS song_id, TO_CHAR(scrobbles.datetime, 'YYYY-MM-DD HH24:MI:SS') 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)
- ,
- },
-
- .timescale =>
- //.ResultType = TimescaleResult,
- switch (entity) {
- .scrobble => unreachable,
- .song =>
- \\SELECT TO_CHAR(date_trunc('year', datetime), 'YYYY') AS date, COUNT(*) as scrobbles
- \\FROM scrobbles
- \\INNER JOIN albumsongs ON albumsongs.id = scrobbles.albumsong
- \\INNER JOIN songs ON songs.id = albumsongs.song_id
- \\WHERE songs.id = $1
- \\GROUP BY date
- \\ORDER BY date ASC;
- ,
-
- .album =>
- \\SELECT TO_CHAR(date_trunc('year', datetime), 'YYYY') AS date, COUNT(*) as scrobbles
- \\FROM scrobbles
- \\INNER JOIN albumsongs ON albumsongs.id = scrobbles.albumsong
- \\INNER JOIN albums ON albums.id = albumsongs.album_id
- \\WHERE albums.id = $1
- \\GROUP BY date
- \\ORDER BY date ASC;
- ,
-
- .artist =>
- \\SELECT y.year AS date, COALESCE(DT.scrobbles, 0) AS scrobbles
- \\FROM (SELECT GENERATE_SERIES(2016,date_part('year',now())::int)::text) AS y(year)
- \\LEFT JOIN (SELECT TO_CHAR(date_trunc('year', datetime), 'YYYY') AS date, COUNT(*) as scrobbles
- \\FROM scrobbles
- \\INNER JOIN albumsongs ON albumsongs.id = scrobbles.albumsong
- \\INNER JOIN albumsongsartists ON albumsongsartists.albumsong_id = albumsongs.id
- \\INNER JOIN artists ON artists.id = albumsongsartists.artist_id
- \\WHERE artists.id = $1
- \\GROUP BY date
- \\ORDER BY date ASC) AS DT
- \\ON DT.date = y.year;
- ,
- },
-
- .entities =>
- //.ResultType = EntitiesResult,
- switch (entity) {
- .scrobble =>
- \\SELECT songs.name AS song_name, songs.id AS song_id, albums.name AS album_name, albums.id AS album_id, artists.name AS artist_name, artists.id AS artist_id, TO_CHAR(scrobbles.datetime, 'YYYY-MM-DD HH24:MI:SS') AS date
- \\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
- ,
- .song =>
- \\SELECT songs.name AS song_name, songs.id AS song_id, albums.name AS album_name, albums.id AS album_id, artists.name AS artist_name, artists.id AS artist_id, COUNT(scrobbles) AS scrobbles
- \\FROM albumsongs
- \\INNER JOIN songs ON albumsongs.song_id = songs.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
- \\INNER JOIN albums ON albums.id = albumsongs.album_id
- \\GROUP BY songs.id, albums.id, artists.id
- \\ORDER BY scrobbles DESC, songs.name ASC
- ,
- .album =>
- \\SELECT albums.name AS album_name, albums.id AS album_id, artists.name AS artist_name, artists.id AS artist_id, COUNT(scrobbles) AS scrobbles
- \\FROM albumsongs
- \\INNER JOIN albums ON albumsongs.album_id = albums.id
- \\INNER JOIN scrobbles ON albumsongs.id = scrobbles.albumsong
- \\INNER JOIN artistalbums ON artistalbums.album_id = albums.id
- \\INNER JOIN artists ON artists.id = artistalbums.artist_id
- \\GROUP BY albums.id, artists.id
- \\ORDER BY scrobbles DESC
- ,
- .artist =>
- \\SELECT artists.name AS artist_name, artists.id AS artist_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
- \\ORDER BY scrobbles DESC;
- ,
- },
-
- .appears =>
- // Not sure how I feel about this one
- switch (entity) {
- .scrobble, .song, .album => unreachable,
- .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
- \\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 albumsongsartists.artist_id = $1 AND artistalbums.artist_id != $1
- \\GROUP BY albums.id
- \\ORDER BY scrobbles DESC;
- ,
- },
-
- .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
- \\INNER JOIN songs ON albumsongs.song_id = songs.id
- \\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id
- \\WHERE albumsongs.album_id = $1
- \\GROUP BY songs.id
- \\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
- \\INNER JOIN albumsongs ON albumsongs.album_id = albums.id
- \\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id
- \\WHERE artistalbums.artist_id = $1
- \\GROUP BY albums.id
- \\ORDER BY scrobbles DESC
- ,
- },
-
- .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 =>
- \\SELECT * FROM
- \\(SELECT *, TO_CHAR(ROW_NUMBER() OVER (ORDER BY scrobbles DESC),'FM99999th') AS rank FROM
- \\(SELECT songs.name AS song_name, songs.id AS song_id, COUNT(scrobbles) AS scrobbles
- \\FROM albumsongs
- \\INNER JOIN songs ON albumsongs.song_id = songs.id
- \\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id
- \\GROUP BY songs.id))
- \\WHERE song_id = $1
- ,
- .album =>
- \\SELECT album_name, t.album_id, artists.name AS artist_name, artists.id AS artist_id, song_num, scrobbles, rank FROM
- \\(SELECT *, TO_CHAR(ROW_NUMBER() OVER (ORDER BY scrobbles DESC),'FM9999th') AS rank FROM
- \\(SELECT albums.name AS album_name, albums.id AS album_id, COUNT(DISTINCT songs.id) AS song_num, COUNT(scrobbles) AS scrobbles
- \\FROM albumsongs
- \\INNER JOIN albums ON albumsongs.album_id = albums.id
- \\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id
- \\INNER JOIN songs ON songs.id = albumsongs.song_id
- \\GROUP BY albums.id)) AS t
- \\INNER JOIN artistalbums ON artistalbums.album_id = t.album_id
- \\INNER JOIN artists ON artists.id = artistalbums.artist_id
- \\WHERE t.album_id = $1
- ,
- .artist =>
- \\SELECT * FROM
- \\(SELECT *, TO_CHAR(ROW_NUMBER() OVER (ORDER BY scrobbles DESC),'FM9999th') AS rank FROM
- \\(SELECT artists.name AS artist_name, artists.id AS artist_id, COUNT(scrobbles) AS scrobbles, COUNT(DISTINCT albums.id) AS album_num, COUNT(DISTINCT songs.id) AS song_num
- \\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
- \\INNER JOIN albums ON albums.id = albumsongs.album_id
- \\INNER JOIN songs ON songs.id = albumsongs.song_id
- \\GROUP BY artists.id))
- \\WHERE artist_id = $1
- ,
- },
-
- .datestreak => switch (entity) {
- .song =>
- \\SELECT maxseq AS streak, FORMAT('%s - %s', ds, de) AS date FROM (SELECT MAX(numdays) AS maxseq, ds, de
- \\FROM (SELECT grp, MIN(datetime::date) AS ds, MAX(datetime::date) AS de,
- \\COUNT(DISTINCT datetime::date) AS numdays
- \\FROM (SELECT scrobbles.datetime,
- \\((datetime::date - '1970-01-01'::date) - DENSE_RANK() OVER (PARTITION BY songs.id ORDER BY datetime::date)) AS grp
- \\FROM scrobbles INNER JOIN albumsongs ON albumsongs.id = albumsong INNER JOIN songs ON songs.id = albumsongs.song_id
- \\WHERE songs.id = $1) scrobbles
- \\GROUP BY grp
- \\) scrobbles
- \\GROUP BY ds, de)
- \\ORDER BY maxseq DESC;
- ,
- .artist =>
- \\SELECT maxseq AS streak, FORMAT('%s - %s' ,ds,de) AS date FROM (SELECT MAX(numdays) AS maxseq, ds, de
- \\FROM (SELECT grp, MIN(datetime::date) AS ds, MAX(datetime::date) AS de,
- \\COUNT(DISTINCT datetime::date) AS numdays
- \\FROM (SELECT scrobbles.datetime,
- \\((scrobbles.datetime::date - '1970-01-01'::date) - DENSE_RANK() OVER (PARTITION BY artists.id ORDER BY scrobbles.datetime::date)) AS grp
- \\FROM scrobbles INNER JOIN albumsongsartists ON albumsongsartists.albumsong_id = scrobbles.albumsong INNER JOIN artists ON artists.id = albumsongsartists.artist_id
- \\WHERE artists.id = $1) scrobbles
- \\GROUP BY grp
- \\) scrobbles
- \\GROUP BY ds, de)
- \\ORDER BY maxseq DESC;
- ,
- .album =>
- \\SELECT maxseq AS streak, FORMAT('%s - %s', ds, de) AS date FROM (SELECT MAX(numdays) AS maxseq, ds, de
- \\FROM (SELECT grp, MIN(datetime::date) AS ds, MAX(datetime::date) AS de,
- \\COUNT(DISTINCT datetime::date) AS numdays
- \\FROM (SELECT scrobbles.datetime,
- \\((datetime::date - '1970-01-01'::date) - DENSE_RANK() OVER (PARTITION BY albums.id ORDER BY datetime::date)) AS grp FROM scrobbles INNER JOIN albumsongs ON albumsongs.id = albumsong INNER JOIN albums ON albums.id = albumsongs.album_id
- \\WHERE albums.id = $1) scrobbles
- \\GROUP BY grp
- \\) scrobbles
- \\GROUP BY ds, de)
- \\ORDER BY maxseq DESC;
- ,
- .scrobble => unreachable,
- },
- .entities_by_name => switch (entity) {
- .song =>
- \\SELECT songs.name AS song_name, songs.id AS song_id, albums.name AS album_name, albums.id AS album_id, artists.name AS artist_name, artists.id AS artist_id, COUNT(scrobbles) AS scrobbles
- \\FROM albumsongs
- \\INNER JOIN songs ON albumsongs.song_id = songs.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
- \\INNER JOIN albums ON albums.id = albumsongs.album_id
- \\WHERE LOWER(songs.name) LIKE LOWER($1)
- \\GROUP BY songs.id, albums.id, artists.id
- \\ORDER BY songs.name ASC, scrobbles DESC;
- ,
- else => unreachable,
- },
- },
- };
-}
diff --git a/src/types.zig b/src/types.zig
index 222d898..55bca1c 100644
--- a/src/types.zig
+++ b/src/types.zig
@@ -1,213 +1,38 @@
-const std = @import("std");
const zeit = @import("zeit");
-pub const ImportedScrobbles = union(ScrobbleSources) {
- LastFMStats: []IgnorantScrobble,
- LastFMWeb: []LastFMWebScrobble,
- Spotify: []SpotifyScrobble,
-};
-
-const Litmus = struct {
- username: ?[]const u8 = null,
- ts: ?[]const u8 = null,
- recenttracks: ?struct {
- track: []LastFMWebScrobble,
- @"@attr": LastFMWebQueryInfo,
- } = null,
-};
-
-const ScrobbleSources = enum {
- LastFMStats,
- LastFMWeb,
- Spotify,
-};
-
-pub const IgnorantScrobble = struct {
+pub const LastFMScrobble = struct {
track: []const u8,
artist: []const u8,
- album: []const u8 = "Not Provided",
- //albumId: []const u8,
- date: i64,
-};
-
-pub const Scrobble = struct {
- track: []const u8,
- artists_track: []const []const u8,
album: []const u8 = "",
- artists_album: []const []const u8,
- date: i64,
-};
-
-pub const ScrobbleArray = struct {
- scrobbles: []Scrobble,
-
- // This is an abuse of the jsonParse function. I don't like the idea of doing it, but I really like the results
- // (or at least I will, assuming it works)
- pub fn jsonParse(allocator: std.mem.Allocator, source: *std.json.Scanner, options: std.json.ParseOptions) !ScrobbleArray {
- while (try source.peekNextTokenType() != .end_of_document) try source.skipValue();
- const litmus_test = try std.json.parseFromSliceLeaky(Litmus, allocator, source.input, .{ .ignore_unknown_fields = true });
-
- if (litmus_test.username != null) { // LastFMStats
- const lastfm_file = try std.json.parseFromSliceLeaky(LastFMStats, allocator, source.input, options);
- var scrobble_buffer = try allocator.alloc(Scrobble, lastfm_file.scrobbles.len);
- for (lastfm_file.scrobbles, 0..lastfm_file.scrobbles.len) |scrobble, i| {
- const artist = try allocator.alloc([]const u8, 1);
- artist[0] = scrobble.artist;
- scrobble_buffer[i] = Scrobble{
- .album = scrobble.album,
- .artists_album = artist,
- .track = scrobble.track,
- .artists_track = artist,
- .date = scrobble.date * 1_000,
- };
- }
- return ScrobbleArray{ .scrobbles = scrobble_buffer };
- }
-
- if (litmus_test.ts != null) { // Spotify
- const spotify = try std.json.parseFromSliceLeaky([]SpotifyScrobble, allocator, source.input, options);
- var scrobble_buffer = try allocator.alloc(Scrobble, spotify.len);
- for (spotify, 0..spotify.len) |scrobble, i| {
- const artist = try allocator.alloc([]const u8, 1);
- artist[0] = scrobble.master_metadata_album_artist_name.?;
- scrobble_buffer[i] = Scrobble{
- .album = scrobble.master_metadata_album_album_name.?,
- .artists_album = artist,
- .track = scrobble.master_metadata_track_name.?,
- .artists_track = artist,
- .date = (zeit.instant(.{ .source = .{ .iso8601 = scrobble.ts } }) catch return error.OutOfMemory).unixTimestamp() * 1_000_000,
- };
- }
- return ScrobbleArray{ .scrobbles = scrobble_buffer };
- }
-
- if (litmus_test.recenttracks != null) { // LastFM API
- const lastfm_web = try std.json.parseFromSliceLeaky(LastFMWeb, allocator, source.input, options);
- var scrobble_buffer = try allocator.alloc(Scrobble, lastfm_web.recenttracks.track.len);
- for (lastfm_web.recenttracks.track, 0..lastfm_web.recenttracks.track.len) |scrobble, i| {
- const artist = try allocator.alloc([]const u8, 1);
- artist[0] = scrobble.artist.@"#text";
- scrobble_buffer[i] = Scrobble{
- .album = if (scrobble.album) |album| album.@"#text" else "Not Provided",
- .artists_album = artist,
- .track = scrobble.name,
- .artists_track = artist,
- .date = (std.fmt.parseInt(i64, scrobble.date.?.uts, 10) catch return error.OutOfMemory) * 1_000_000,
- };
- }
- return ScrobbleArray{ .scrobbles = scrobble_buffer };
- }
-
- return error.UnexpectedToken;
- }
+ date: i128,
};
// From lastfmstats.com
-pub const LastFMStats = struct { username: []const u8, scrobbles: []IgnorantScrobble };
+pub const LastFM = struct { username: []const u8, scrobbles: []LastFMScrobble };
// I derived whether or not these values were optional from searching
// the respective fields for null in Vim, so there may be some fields
// that can be optional that I haven't run into yet
pub const SpotifyScrobble = struct {
ts: []const u8,
- //username: []const u8,
- //platform: []const u8,
+ username: []const u8,
+ platform: []const u8,
ms_played: u64,
- //conn_country: []const u8,
- //ip_addr_decrypted: ?[]const u8,
- //user_agent_decrypted: ?[]const u8,
+ conn_country: []const u8,
+ ip_addr_decrypted: ?[]const u8,
+ user_agent_decrypted: ?[]const u8,
master_metadata_track_name: ?[]const u8,
master_metadata_album_artist_name: ?[]const u8,
master_metadata_album_album_name: ?[]const u8,
- //spotify_track_uri: ?[]const u8,
- //episode_name: ?[]const u8,
- //episode_show_name: ?[]const u8,
- //spotify_episode_uri: ?[]const u8,
+ spotify_track_uri: ?[]const u8,
+ episode_name: ?[]const u8,
+ episode_show_name: ?[]const u8,
+ spotify_episode_uri: ?[]const u8,
reason_start: []const u8,
reason_end: ?[]const u8,
- //shuffle: bool,
+ shuffle: bool,
skipped: ?bool,
- //offline: bool,
+ offline: bool,
offline_timestamp: u64,
- //incognito_mode: ?bool,
-};
-
-pub const LastFMWeb = struct {
- recenttracks: struct {
- track: []LastFMWebScrobble,
- @"@attr": LastFMWebQueryInfo,
- },
-};
-
-pub const LastFMWebHyperlinkData = struct {
- mbid: []const u8,
- @"#text": []const u8,
-};
-
-pub const LastFMWebScrobble = struct {
- artist: LastFMWebHyperlinkData,
- album: ?LastFMWebHyperlinkData = null,
- name: []const u8,
- mbid: ?[]const u8 = null,
- image: []struct {
- size: []const u8,
- @"#text": []const u8,
- },
- date: ?struct {
- uts: []const u8,
- @"#text": []const u8,
- } = null,
- @"@attr": ?struct {
- nowplaying: []const u8,
- } = null,
- url: []const u8,
-};
-
-pub const LastFMWebQueryInfo = struct {
- perPage: []const u8,
- totalPages: []const u8,
- page: []const u8,
- user: []const u8,
- total: []const u8,
-};
-
-pub const Rule = struct {
- name: []const u8,
- cond_req: enum { any, all },
- conditionals: []struct {
- match_on: enum { artists_album, artists_track, album, track },
- match_cond: enum { is, contains },
- match_txt: []const u8,
- },
- actions: []struct {
- action: enum { replace, add },
- action_on: enum { artists_album, album, artists_track, track },
- action_txt: []const u8,
- },
-};
-
-// Can't import types in .zmpl files, so defining this here
-// doesn't really do much (except maybe in the .zig file for views?)
-//pub const HeaderTypes = []enum {
-// song,
-// album,
-// artist,
-// artistlist,
-// scrobbles,
-// date,
-//};
-//
-
-pub const TableRow = struct {
- song: ?HyperlinkData = null,
- album: ?HyperlinkData = null,
- artist: ?HyperlinkData = null,
- artistlist: ?[]HyperlinkData = null,
- scrobbles: ?i64 = null,
- date: ?[]const u8 = null,
-};
-
-pub const HyperlinkData = struct {
- name: []const u8,
- id: i64,
+ incognito_mode: ?bool,
};