Compare commits
No commits in common. "hijack-jsonParse" and "main" have entirely different histories.
hijack-jso
...
main
117 changed files with 1729 additions and 1773 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -6,5 +6,4 @@ static/
|
|||
src/app/database/data.db-journal
|
||||
src/app/database/old_migrations/
|
||||
src/lib
|
||||
src/app/scripts/
|
||||
rules.json
|
||||
src/app/scripts/
|
||||
38
README.md
38
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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = .{
|
||||
|
|
|
|||
164
common_queries.md
Normal file
164
common_queries.md
Normal file
|
|
@ -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;
|
||||
```
|
||||
|
|
@ -15,7 +15,7 @@ pub const database = .{
|
|||
.port = 5432,
|
||||
.username = "postgres",
|
||||
.password = "postgres",
|
||||
.database = "zuletzt_rsql",
|
||||
.database = "zuletzt_dev",
|
||||
.pool_size = 16,
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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, .{}),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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(.{}),
|
||||
|
|
@ -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", .{});
|
||||
}
|
||||
|
|
@ -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(.{}),
|
||||
},
|
||||
.{},
|
||||
|
|
@ -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", .{});
|
||||
}
|
||||
|
|
@ -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", .{});
|
||||
}
|
||||
|
|
@ -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", .{});
|
||||
}
|
||||
|
|
@ -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(.{}),
|
||||
},
|
||||
.{},
|
||||
|
|
@ -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, .{}),
|
||||
|
|
@ -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", .{});
|
||||
}
|
||||
|
|
@ -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", .{});
|
||||
}
|
||||
|
|
@ -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", .{});
|
||||
}
|
||||
|
|
@ -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", .{});
|
||||
}
|
||||
|
|
@ -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", .{});
|
||||
}
|
||||
|
|
@ -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", .{});
|
||||
}
|
||||
|
|
@ -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", .{});
|
||||
}
|
||||
|
|
@ -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", .{});
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 });
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
}
|
||||
65
src/app/middleware/DemoMiddleware.zig
Normal file
65
src/app/middleware/DemoMiddleware.zig
Normal file
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,19 @@
|
|||
@zig {
|
||||
const ColumnChoices = []const enum{song, album, artist, artistlist, scrobbles, date};
|
||||
const columns: ColumnChoices = &.{.song, .scrobbles};
|
||||
}
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
@partial partials/header
|
||||
<h1>{{.album.album_name}}</h1>
|
||||
<h2><a href="/artists/{{.album.artist_id}}">{{.album.artist_name}}</a></h2>
|
||||
<div>{{.album.scrobbles}} scrobbles ({{.album.rank}} place)</div>
|
||||
<div>{{.album.song_num}} songs</div>
|
||||
@partial partials/firstlast_listens(firstlast: .firstlast)
|
||||
<h3>Yearly Performance</h3>
|
||||
@partial partials/timescale(range: .yearly)
|
||||
<h2>Songs</h2>
|
||||
@partial partials/newtable(T: ColumnChoices, table_data: .songs, columns: columns)
|
||||
<h1>{{.album}}</h1>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
@for (.songs) |song| {
|
||||
<tr>
|
||||
<td class=cell><a href="/songs/{{song.url}}">{{song.name}}</a></td>
|
||||
<td class=cell>{{song.scrobbles}}</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,15 +1,40 @@
|
|||
@zig {
|
||||
const ColumnChoices = []const enum{song, album, artist, artistlist, scrobbles, date};
|
||||
const columns: ColumnChoices = &.{.album, .artistlist, .scrobbles};
|
||||
}
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<link href="https://cdn.jsdelivr.net/npm/simple-datatables@latest/dist/style.css" rel="stylesheet" type="text/css">
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
@partial partials/header
|
||||
<h1>Albums</h1>
|
||||
@partial partials/newtable(T: ColumnChoices, table_data: .albums, columns: columns)
|
||||
<table id="myTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Artist(s)</th>
|
||||
<th>Scrobbles</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</tbody>
|
||||
@for (.albums) |album| {
|
||||
<tr>
|
||||
<td class=cell><a href="/albums/{{album.url}}">{{album.name}}</a></td>
|
||||
<td class=cell>
|
||||
@for (album.get("artist_info").?) |ai| {
|
||||
<a href="/artists/{{ai.id}}">{{ai.name}}</a>
|
||||
}
|
||||
</td>
|
||||
<td class=cell>{{album.scrobbles}}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<script src="https://cdn.jsdelivr.net/npm/simple-datatables@latest" type="text/javascript"></script>
|
||||
<script>
|
||||
const dataTable = new simpleDatatables.DataTable("#myTable", {
|
||||
searchable: false,
|
||||
perPage: 50,
|
||||
perPageSelect: [25,50,100],
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
3
src/app/views/artists/edit.zmpl
Normal file
3
src/app/views/artists/edit.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,28 +1,31 @@
|
|||
@zig {
|
||||
const ColumnChoices = []const enum{song, album, artist, artistlist, scrobbles, date};
|
||||
const columns: ColumnChoices = &.{.album, .scrobbles};
|
||||
}
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<link href="https://cdn.jsdelivr.net/npm/simple-datatables@latest/dist/style.css" rel="stylesheet" type="text/css">
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
@partial partials/header
|
||||
<h1>{{.artist.artist_name}}</h1>
|
||||
<div>
|
||||
<div>{{.artist.scrobbles}} scrobbles ({{.artist.rank}} place)</div>
|
||||
<div>{{.artist.song_num}} songs</div>
|
||||
<div>{{.artist.album_num}} albums</div>
|
||||
</div>
|
||||
@partial partials/timescale(range: .yearly)
|
||||
<br>
|
||||
@partial partials/firstlast_listens(firstlast: .firstlast)
|
||||
<h2>Albums</h2>
|
||||
@partial partials/newtable(T: ColumnChoices, table_data: .albums, columns: columns)
|
||||
|
||||
<h2>Albums Featured On</h2>
|
||||
@partial partials/newtable(T: ColumnChoices, table_data: .appears, columns: columns)
|
||||
|
||||
<h1>{{.artist}}</h1>
|
||||
<table id="myTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th><th>Scrobbles</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (.albums) |album| {
|
||||
<tr>
|
||||
<td class=cell><a href="/albums/{{album.url}}">{{album.name}}</a></td>
|
||||
<td class=cell>{{album.scrobbles}}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<script src="https://cdn.jsdelivr.net/npm/simple-datatables@latest" type="text/javascript"></script>
|
||||
<script>
|
||||
const dataTable = new simpleDatatables.DataTable("#myTable", {
|
||||
searchable: false,
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,15 +1,34 @@
|
|||
@zig {
|
||||
const ColumnChoices = []const enum{song, album, artist, artistlist, scrobbles, date};
|
||||
const columns: ColumnChoices = &.{.artist, .scrobbles};
|
||||
}
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<link href="https://cdn.jsdelivr.net/npm/simple-datatables@latest/dist/style.css" rel="stylesheet" type="text/css">
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
@partial partials/header
|
||||
<h1>Artists</h1>
|
||||
@partial partials/newtable(T: ColumnChoices, table_data: .artists, columns: columns)
|
||||
<table id="myTable" class='table'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Scrobbles</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (.artists) |artist| {
|
||||
<tr>
|
||||
<td class=cell><a href="/artists/{{artist.url}}">{{artist.name}}</a></td>
|
||||
<td class=cell>{{artist.scrobbles}}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<script src="https://cdn.jsdelivr.net/npm/simple-datatables@latest" type="text/javascript"></script>
|
||||
<script>
|
||||
const dataTable = new simpleDatatables.DataTable("#myTable", {
|
||||
searchable: true,
|
||||
perPage: 50,
|
||||
perPageSelect: [25,50,100],
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
3
src/app/views/artists/new.zmpl
Normal file
3
src/app/views/artists/new.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/artists/patch.zmpl
Normal file
3
src/app/views/artists/patch.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/artists/post.zmpl
Normal file
3
src/app/views/artists/post.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/artists/put.zmpl
Normal file
3
src/app/views/artists/put.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
3
src/app/views/collection/delete.zmpl
Normal file
3
src/app/views/collection/delete.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/collection/patch.zmpl
Normal file
3
src/app/views/collection/patch.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/collection/post.zmpl
Normal file
3
src/app/views/collection/post.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/collection/put.zmpl
Normal file
3
src/app/views/collection/put.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
3
src/app/views/concerts/delete.zmpl
Normal file
3
src/app/views/concerts/delete.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/concerts/patch.zmpl
Normal file
3
src/app/views/concerts/patch.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/concerts/post.zmpl
Normal file
3
src/app/views/concerts/post.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/concerts/put.zmpl
Normal file
3
src/app/views/concerts/put.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<head>
|
||||
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4" integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
@partial partials/header
|
||||
|
||||
<h1>Merge Songs</h1>
|
||||
<form hx-get="/songs" hx-target="#response-div">
|
||||
<label>Song name <input name="s" type="text"></label>
|
||||
</form>
|
||||
<div id="response-div"></div>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
3
src/app/views/lists/delete.zmpl
Normal file
3
src/app/views/lists/delete.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/lists/patch.zmpl
Normal file
3
src/app/views/lists/patch.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/lists/put.zmpl
Normal file
3
src/app/views/lists/put.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
@args firstlast: *ZmplValue
|
||||
|
||||
@zig {
|
||||
const songs = firstlast.items(.array);
|
||||
}
|
||||
|
||||
<div>
|
||||
First listen: <a href="/songs/{{songs[0].song.id}}">{{songs[0].song.name}}</a> ({{songs[0].date}})
|
||||
<br>
|
||||
Most recent listen: <a href="/songs/{{songs[1].song.id}}">{{songs[1].song.name}}</a> ({{songs[1].date}})
|
||||
</div>
|
||||
|
|
@ -1,11 +1,7 @@
|
|||
<a class="header-link" href="/">Zuletzt</a>
|
||||
<a class="header-link" href="/artists">Artists</a>
|
||||
<a class="header-link" href="/albums">Albums</a>
|
||||
<a class="header-link" href="/songs">Songs</a>
|
||||
<a class="header-link" href="/scrobbles">Scrobbles</a>
|
||||
<a class="header-link" href="/concerts">Concerts</a>
|
||||
<a class="header-link" href="/collection">Collection</a>
|
||||
<a class="header-link" href="/ratings">Ratings</a>
|
||||
<a class="header-link" href="/lists">Lists</a>
|
||||
<a class="header-link" href="/groups">Groups</a>
|
||||
<hr>
|
||||
0
src/app/views/partials/_history.zmpl
Normal file
0
src/app/views/partials/_history.zmpl
Normal file
|
|
@ -1,75 +0,0 @@
|
|||
@args T: type, table_data: *ZmplValue, columns: T
|
||||
|
||||
<div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@zig {
|
||||
for (columns) |header| {
|
||||
switch (header) {
|
||||
.song => {
|
||||
<th>Song</th>
|
||||
},
|
||||
.album => {
|
||||
<th>Album</th>
|
||||
},
|
||||
.artist => {
|
||||
<th>Artist</th>
|
||||
},
|
||||
.artistlist => {
|
||||
<th>Artist(s)</th>
|
||||
},
|
||||
.scrobbles => {
|
||||
<th>Scrobbles</th>
|
||||
},
|
||||
.date => {
|
||||
<th>Date</th>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@zig {
|
||||
const array = table_data.items(.array);
|
||||
for (array) |row| {
|
||||
<tr>
|
||||
for (columns) |header| {
|
||||
switch (header) {
|
||||
.song => {
|
||||
<td class=cell>
|
||||
<a href="/songs/{{row.song.id}}">{{row.song.name}}</a>
|
||||
</td>
|
||||
},
|
||||
.album => {
|
||||
<td class=cell>
|
||||
<a href="/albums/{{row.album.id}}">{{row.album.name}}</a>
|
||||
</td>
|
||||
},
|
||||
.artist => {
|
||||
<td class=cell>
|
||||
<a href="/artists/{{row.artist.id}}">{{row.artist.name}}</a>
|
||||
</td>
|
||||
},
|
||||
.artistlist => {
|
||||
<td class=cell>
|
||||
@for (row.get("artistlist").?) |artist| {
|
||||
<a href="/artists/{{artist.id}}">{{artist.name}}</a>
|
||||
}
|
||||
</td>
|
||||
},
|
||||
.scrobbles => {
|
||||
<td class=cell>{{row.scrobbles}}</td>
|
||||
},
|
||||
.date =>{
|
||||
<td class=cell>{{row.date}}</td>
|
||||
}
|
||||
}
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
0
src/app/views/partials/_random.zmpl
Normal file
0
src/app/views/partials/_random.zmpl
Normal file
0
src/app/views/partials/_recent.zmpl
Normal file
0
src/app/views/partials/_recent.zmpl
Normal file
18
src/app/views/partials/_table.zmpl
Normal file
18
src/app/views/partials/_table.zmpl
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
@args table_data: *ZmplValue, table_headers: *ZmplValue
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
@for (table_headers) |text| {
|
||||
<th>{{text}}</th>
|
||||
}
|
||||
</tr>
|
||||
|
||||
@for (table_data) |value| {
|
||||
<tr>
|
||||
<td class=cell>{{value.track}}</td>
|
||||
<td class=cell>{{value.artist}}</td>
|
||||
<td class=cell>{{value.album}}</td>
|
||||
<td class=cell>{{value.date}}</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
@args range: *ZmplValue
|
||||
|
||||
<div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Year</th>
|
||||
<th>Scrobbles</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (range) |itm| {
|
||||
<tr>
|
||||
<td>{{itm.date}}:</td>
|
||||
<td>{{itm.scrobbles}}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
3
src/app/views/ratings/delete.zmpl
Normal file
3
src/app/views/ratings/delete.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/ratings/patch.zmpl
Normal file
3
src/app/views/ratings/patch.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/ratings/put.zmpl
Normal file
3
src/app/views/ratings/put.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
3
src/app/views/rules/delete.zmpl
Normal file
3
src/app/views/rules/delete.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/rules/edit.zmpl
Normal file
3
src/app/views/rules/edit.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/rules/get.zmpl
Normal file
3
src/app/views/rules/get.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,62 +1,3 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
@partial partials/header
|
||||
<h1>Rules</h1>
|
||||
Rules allow you change the default Scrobble import behavior based on provided criteria.
|
||||
Add a rule below.
|
||||
<br><br>
|
||||
<form action="/rules" enctype="multipart/form-data" method="POST">
|
||||
<label for="rule-title">Rule Name:</label>
|
||||
<input type="text" name="rule-title" id="rule-title">
|
||||
<br>
|
||||
Match
|
||||
<select name="cond-req" id="cond-req">
|
||||
<option value="any">any</option>
|
||||
<option value="all">all</option>
|
||||
</select>
|
||||
conditonals.
|
||||
<br>
|
||||
If
|
||||
@for (0..5) |i| {
|
||||
<select name="match-on{{i}}" id="match-on{{i}}">
|
||||
<option value="artist">artist</option>
|
||||
<option value="album">album</option>
|
||||
<option value="track">song</option>
|
||||
</select>
|
||||
<select name="match-cond{{i}}" id="match-cond{{i}}">
|
||||
<option value="is">is</option>
|
||||
<option value="contains">contains</option>
|
||||
<option value="matches">matches regex</option>
|
||||
</select>
|
||||
<input type="text" name="match-txt{{i}}" id="match-txt{{i}}">
|
||||
<label for="case-sens">Toggle case sensitivity</label>
|
||||
<input type="checkbox" name="case-sens{{i}}" id="case-sens{{i}}">
|
||||
<label for="accent-sens">Toggle diacritic sensitivity</label>
|
||||
<input type="checkbox" name="accent-sens{{i}}" id="accent-sens{{i}}">
|
||||
<br>
|
||||
}
|
||||
then
|
||||
@for (0..5) |i| {
|
||||
<select name="action{{i}}" id="action{{i}}">
|
||||
<option value="replace">replace</option>
|
||||
<option value="add">add</option>
|
||||
</select>
|
||||
<select name="action-on{{i}}" id="action-on{{i}}">
|
||||
<option value="artists_track">artist (song)</option>
|
||||
<option value="artists_album">artist (album)</option>
|
||||
<option value="album">album</option>
|
||||
<option value="track">song</option>
|
||||
</select>
|
||||
with
|
||||
<input type="text" name="action-txt{{i}}" id="action-txt{{i}}">
|
||||
<br>
|
||||
}
|
||||
<button type="submit" value="Submit">Submit</button>
|
||||
</form>
|
||||
|
||||
Current rules:
|
||||
|
||||
</html>
|
||||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
|
|||
3
src/app/views/rules/new.zmpl
Normal file
3
src/app/views/rules/new.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/rules/patch.zmpl
Normal file
3
src/app/views/rules/patch.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/rules/put.zmpl
Normal file
3
src/app/views/rules/put.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
3
src/app/views/scrobbles/delete.zmpl
Normal file
3
src/app/views/scrobbles/delete.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/scrobbles/get.zmpl
Normal file
3
src/app/views/scrobbles/get.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,15 +1,42 @@
|
|||
@zig {
|
||||
const ColumnChoices = []const enum{song, album, artist, artistlist, scrobbles, date};
|
||||
const columns: ColumnChoices = &.{.song, .artistlist, .album, .date};
|
||||
}
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<link href="https://cdn.jsdelivr.net/npm/simple-datatables@latest/dist/style.css" rel="stylesheet" type="text/css">
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
@partial partials/header
|
||||
<h1>Scrobbles</h1>
|
||||
@partial partials/newtable(T: ColumnChoices, table_data: .scrobbles, columns: columns)
|
||||
<table id="myTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Song</th>
|
||||
<th>Artist(s)</th>
|
||||
<th>Album</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (.scrobbles) |scrobble| {
|
||||
<tr>
|
||||
<td class=cell><a href="/songs/{{scrobble.song_id}}">{{scrobble.song_name}}</a></td>
|
||||
<td class=cell>
|
||||
@for (scrobble.get("artist_info").?) |ai| {
|
||||
<a href="/artists/{{ai.id}}">{{ai.name}}</a>
|
||||
}
|
||||
</td>
|
||||
<td class=cell><a href="/albums/{{scrobble.album_id}}">{{scrobble.album_name}}</a></td>
|
||||
<td class=cell>{{scrobble.date}}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<script src="https://cdn.jsdelivr.net/npm/simple-datatables@latest" type="text/javascript"></script>
|
||||
<script>
|
||||
const dataTable = new simpleDatatables.DataTable("#myTable", {
|
||||
searchable: true,
|
||||
perPage: 50,
|
||||
perPageSelect: [25,50,100],
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
3
src/app/views/scrobbles/patch.zmpl
Normal file
3
src/app/views/scrobbles/patch.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/scrobbles/post.zmpl
Normal file
3
src/app/views/scrobbles/post.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/scrobbles/put.zmpl
Normal file
3
src/app/views/scrobbles/put.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -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);
|
||||
}
|
||||
3
src/app/views/search/delete.zmpl
Normal file
3
src/app/views/search/delete.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/search/edit.zmpl
Normal file
3
src/app/views/search/edit.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/search/get.zmpl
Normal file
3
src/app/views/search/get.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/search/index.zmpl
Normal file
3
src/app/views/search/index.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/search/new.zmpl
Normal file
3
src/app/views/search/new.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/search/patch.zmpl
Normal file
3
src/app/views/search/patch.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/search/post.zmpl
Normal file
3
src/app/views/search/post.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/search/put.zmpl
Normal file
3
src/app/views/search/put.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
3
src/app/views/songs/delete.zmpl
Normal file
3
src/app/views/songs/delete.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
3
src/app/views/songs/edit.zmpl
Normal file
3
src/app/views/songs/edit.zmpl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,32 +1,3 @@
|
|||
@zig {
|
||||
const ColumnChoices = []const enum{song, album, artist, artistlist, scrobbles, date};
|
||||
const columns: ColumnChoices = &.{.song, .artistlist, .album, .date};
|
||||
}
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
@partial partials/header
|
||||
<div style="text-align:center">
|
||||
<h1>{{.song.song_name}}</h1>
|
||||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;flex-direction:row;justify-content:space-evenly">
|
||||
<div style="display:flex;flex-direction:column;align-self:left">
|
||||
<div>{{.song.scrobbles}} scrobbles ({{.song.rank}} place)</div>
|
||||
@partial partials/firstlast_listens(firstlast: .firstlast)
|
||||
<h3>Yearly Performance</h3>
|
||||
@partial partials/timescale(range: .yearly)
|
||||
<h2>Scrobbles</h2>
|
||||
@partial partials/newtable(T: ColumnChoices, table_data: .scrobbles, columns: columns)
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;align-self:right">
|
||||
<h2>Rating</h2>
|
||||
<input type="text">
|
||||
<input type="button">
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,17 +1,40 @@
|
|||
@zig {
|
||||
const ColumnChoices = []const enum{song, album, artist, artistlist, scrobbles, date};
|
||||
const columns: ColumnChoices = &.{.song, .album, .artistlist, .scrobbles};
|
||||
}
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<link href="https://cdn.jsdelivr.net/npm/simple-datatables@latest/dist/style.css" rel="stylesheet" type="text/css">
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
@if (! $.htmx)
|
||||
@partial partials/header
|
||||
<h1>Songs</h1>
|
||||
@end
|
||||
@partial partials/newtable(T: ColumnChoices, table_data: .songs, columns: columns)
|
||||
<table id="myTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Artists(s)</th>
|
||||
<th>Scrobbles</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (.songs) |song| {
|
||||
<tr>
|
||||
<td class=cell><a href="/songs/{{song.url}}">{{song.name}}</a></td>
|
||||
<td class=cell>
|
||||
@for (song.get("artist_info").?) |ai| {
|
||||
<a href="/artists/{{ai.id}}">{{ai.name}}</a>
|
||||
}
|
||||
</td>
|
||||
<td class=cell>{{song.scrobbles}}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<script src="https://cdn.jsdelivr.net/npm/simple-datatables@latest" type="text/javascript"></script>
|
||||
<script>
|
||||
const dataTable = new simpleDatatables.DataTable("#myTable", {
|
||||
searchable: true,
|
||||
perPage: 50,
|
||||
perPageSelect: [25,50,100],
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue