Compare commits

...
Sign in to create a new pull request.

73 commits

Author SHA1 Message Date
acef5d8e49 Parse ScrobbleArray type rather than individual types using jsonParse
POC for parsing and data validation inside jsonParse rather than in the view. I believe this is necessarily slower (and actually doesn't work for Spotify scrobbles) but could be made to work/be faster if we implemented our own json parsing for each type, but I think that's too much work for too little gain (atm)
2025-06-14 16:29:16 -04:00
36873053bc Make new scrobble processing function
This uses the zmpl data as a hash map to check if we've already checked the db for some song/album/artist/etc. and now only checks once per entity/assoc. table entry to speed things up. Previously, for each scrobble, we checked if its metadata appeared in the respective table, regardless of whether or not we've scrobbled that albumsong before. So, a song like Starless had to be checked (at the time of writing) ~180 times, but is now only checked once. Similarly, Wilco was checked ~3000+ times, as Hurry Up, We're Dreaming was cheked ~700 times.

The only problem now is the way it was implemented. Obviously, copying and pasting those huge chunks of code isn't very nice looking. ATM, I don't really care, and I'm more happy about the overall speed increase, as well as the readability increase of the job. However, I don't want to leave it like that. The way I see it, I have two options: either create a funcion which does this, or I can do something even better, which is create a jsonParse function, which, if my thought process works, would remove the need for an intermediary source type, meaning we no longer need to switch on that type, which means we can just have one for loop that does everything, which would mean we just need to have that code in one place.

Also not entirely happy with the code concerning all the conversions to i64s and []const u8s, but I think I have to.
2025-06-11 20:08:13 -04:00
df8f01525e Merge remote-tracking branch 'refs/remotes/origin/rawsql' into rawsql 2025-06-11 09:28:14 -04:00
2f420bc5ce Testing with groups and htmx 2025-06-11 09:26:12 -04:00
6a1c822420 Add entities_by_name query
Will probably be used for disambiguation pages (among other things, but disambiguation pages are coming up soon)
2025-06-11 09:25:40 -04:00
a8a4ed27c4 Make process_scrobbles vars more readable, and change hashing 2025-06-11 09:22:38 -04:00
1e4a271b8d Update README 2025-06-11 09:22:06 -04:00
85552f39c1 Add Artistsongs table
Whether or not a song is covered, there was an original artist who originally performed the song. The only issue is that an Artistsongs table will almost be the exact same as the Albumsongsartists table, since most songs aren't covered. So, it may be better not to populate that table by default, and then if two albumsongs with different artists share the same song, then fill the Artistsongs table.
2025-06-09 21:45:41 -04:00
162341fb5f Convert ids to i64
The birthday paradox is a real problem with the size of our datasets. i64 is the largest numerical value we can use, and there's a 0.1% chance of collision with ~2,000,000 values, so I feel pretty comfortable with this
2025-06-09 21:41:52 -04:00
c8f2ef57c8 Add some tyling to songs view
This can (will) be easily replicated for the other views, I just first tested it on songs. I think this looks much nicer, and I'll probably roll with a layout similar to this for the other views, with some minor adjustments for each particular view.
2025-06-06 15:55:20 -04:00
3ef17fcd46 Split entity_items and appears query into more granular queries
We can be a bit more specific about the information we get this way
2025-06-06 14:28:15 -04:00
adcaff34ea Fix dumb appears query for albums 2025-06-02 00:13:27 -04:00
566edf1818 Include artist(s) name in album GET view
This also makes the entity_info struct very similar to the UnifiedResult struct, so we'll probably see a merge at some point. Would be nice if we used the fields from the entity_info result more commonly.
2025-05-31 15:48:30 -04:00
906ba6d2e5 Update header partial and remoev table partial
Long overdue
2025-05-31 14:47:52 -04:00
3777b818e3 Create view for groups
One of the largest components that makes zuletzt unique - implementing groups the way MusicBrainz has (release groups in particular). I thought for a while that I would just connect songs via a shared ID, but for remixes and such, I don't think they should be so tightly coupled. This also gives the user freedom for how they want to do the grouping (a remix can be included in the group if they choose to, or it may not). This will allow someone to see a combined scrobble number for an album with, for example, a regular release, a deluxe release, and an anniversary release, in addition to the individual releases. This will complicate SQL queries rather significantly I imagine, and I'm not sure what the interface for creating/deleting groups will be (although it will likely be easier when I have full use of TS), but it's a necessity for the project.
2025-05-31 14:45:45 -04:00
a314fd447d Fix LastFM API scrobble parsing when song is currently playing 2025-05-31 14:31:15 -04:00
c57bf18627 Update queries
Adds datestreak query, provides the number of songs/albums when relevant, and provides timescale with all years, regardless of the number of plays (defaults to 0)
2025-05-31 13:39:03 -04:00
d81681e698 Move scrobble rank from firstlast partial to view.
Eventually moving this to its own partial (probably)
2025-05-31 13:37:34 -04:00
62590fee37 Made queries.zig look significantly nicer
There's a little bit of weird stuff happening, but holy cannoli, that's so much easier to maintain and parse
2025-05-29 19:39:51 -04:00
d638fa66c5 Create GET function for a song view 2025-05-29 15:33:10 -04:00
3ff973e193 Use queries.zig in scrobbles view 2025-05-26 11:15:51 -04:00
f59eec79a8 Removed inline else from upload.zig
If I can figure out a way to get an array of a union instead of a union of arrays, we're in business to make this even better, but this is fine right now. The inline else was just a dumb way to keep the for on the outside
2025-05-26 11:15:19 -04:00
aab61631a3 Directly append complete_scrobble in upload.zig
Thanks bob :)
2025-05-25 16:16:18 -04:00
7f3778e82f Move SQL logic to separate function
Idk if this makes any sense, and I don't really like the code atm, but the view .zig files lookk nicer?
2025-05-24 13:59:28 -04:00
09f542e26e Add timescale partial
Bad name, idk what else to call it
2025-05-24 13:58:31 -04:00
1734e6a4bb Fix date formatting in scrobble view 2025-05-20 16:29:53 -04:00
d6a638bf27 Merge remote-tracking branch 'refs/remotes/origin/rawsql' into rawsql 2025-05-20 15:10:49 -04:00
a2a739bc9c Refactor upload.zig
I have been unhappy with the branches, but didn't quite know what to do about it. THis feels much nicer. Also fixes datetime stuff with jetquery.  The HTML element parsing isn't quite where I want it to be, but it works for the time being.
2025-05-20 15:07:51 -04:00
6494bbdf60 Remove Rules type 2025-05-20 09:33:15 -04:00
4c759433d2 Update README.md 2025-05-16 05:06:59 +00:00
614607ae71 Fix LastFM uploadig error
I figured it out; if you have a song currently being played, then it doesn't have a date
2025-05-16 01:05:32 -04:00
5697f95355 Fix LastFM uploading errors
Not sure which of these actually made it work, will probably work backwards at some point to reverse engineer it
2025-05-15 20:23:53 -04:00
89e98c7a47 Allow uploads from LastFM API
Very slow at the moment. Look into ways to speed this up
2025-05-15 20:23:12 -04:00
f69ffb2b37 Move upload.zig to the new table partial 2025-05-15 20:22:34 -04:00
52fefc9ba5 Create dateCompare function
Will eventually try to move away from zeit. Don't need all of it's functionality as long as SQL can format dates
2025-05-15 20:22:09 -04:00
4991bac9a4 Add LastFM scrobble type
In preparation for importing via LastFM api
2025-05-15 15:39:21 -04:00
c42b8d24dd Fix typo 2025-05-15 15:37:21 -04:00
365b9dbf11 Switch to using newtable partial for all tables
Will be renamed eventually, don't care right now. Also cleans up a lot of code I wasn't particularly happy about
2025-05-13 14:24:14 -04:00
153ea869e0 Work on making partials for views 2025-05-08 18:17:45 -04:00
4758885c68 Keep cleaning 2025-05-05 13:09:16 -04:00
9ffc45b207 Delete common_queries.md
No longer relevant
2025-05-05 11:15:30 -04:00
94cc6e3bd5 Remove unused views and functions 2025-05-05 11:06:52 -04:00
c574885f8d Get rid of unused views 2025-05-05 10:37:18 -04:00
762a4fd51e Create partial for view agnostic table 2025-05-02 10:00:47 -04:00
3345b20f1f Make an ordinal formatting funcrion
I am hungry
2025-04-29 00:38:20 -04:00
78e416eeaf Add more information to scrobbles views and refactor artists view 2025-04-28 23:06:21 -04:00
8138e5ccf2 Create dateFmt function
It's easier to keep the date as an epoch in PostgreSQL to do comparisons, but I always want to show it to the user as a formatted date
2025-04-28 23:03:24 -04:00
ae85f94ddb Switch dates from u64 to i64
PostgreSQL only uses signed ints, so this makes things much easier
2025-04-28 23:01:34 -04:00
cb89a3e6f3 Switch dates from i128 to u64
I was making them unnecessarily large by accidentally storing them as microseconds instead of milliseconds. Might be able to get away with seconds in the future
2025-04-28 21:37:08 -04:00
65136a44d6 Add more information to artists view, songs view, and format dates correctly in scrobbles view 2025-04-27 23:58:50 -04:00
01fe10f045 Fix limit on rule parameters and fix segfault in applyScrobbleRule
For sure this time
2025-04-27 16:27:03 -04:00
18d4df0a5c Fix albums not being hashed correctly
Also provides more actions for rules, but they don't seem to work...
2025-04-27 15:48:47 -04:00
5e58e81ca7 Fix album artist parsing in process_scrobbles 2025-04-27 14:28:39 -04:00
9df8f9ea12 Fix segfault in applyScrobbleRule
Thanks bob :)
2025-04-27 10:41:42 -04:00
be8c1191b0 Clean 2025-04-24 09:34:34 -04:00
0631ded115 Work on add artist action in rules
Really close to having it work, but there seems to be an error when uploading files, which causes particularly annoying problems on WSL when testing, so I'm commiting and trying on my desktop.
2025-04-23 19:32:32 -04:00
e9c72041a5 Allow multiple conditions in rules.
Scrobble processing appears noticeably slower (according to the logs), so I think rules are going to be something to optimize later. Fortunately, they shouldn't need to be applied too often
2025-04-22 13:50:39 -04:00
77170a1e28 Move Scrobble rule application to upload.zig
They couldn't see the changes made by rules after uploading Scrobbles which made it seem like the rules weren't being applied. Also makes Album rules easier to apply I believe.
2025-04-21 16:41:40 -04:00
87a2fe2d34 Complete preliminary find and replace rules
Tested by replacing AJR with John Van Derwood. Need to test on albums and artists, as well as matching on one piece of metadata, and replacing another
2025-04-21 12:23:20 -04:00
445ca45fa9 Begin rule application
The more I think about this, the more I think it's gonna be super slow and bad. There must bve a good way of doing this, but I'm not sure how...
2025-04-21 00:17:16 -04:00
baf9ef38a4 Simplify file creation branch of process_rule.zig
Still not quite where I want it, but definitely better than what it was
2025-04-19 15:36:51 -04:00
5383b69eb6 Allow reading and writing rules.json
I like the idea of letting the user write to a file themselves for rules, but I think this is going to significantly slow things down. Will probably switch to SQL table at some point. Also very hardcoded for my purposes. ALSO  the code looks bad, I think there must be a better way...
2025-04-19 15:01:30 -04:00
18cdb48b53 Begin rules 2025-04-18 21:29:00 -04:00
ff8cdabbf1 Cleanup 2025-04-17 15:28:00 -04:00
387493d3c0 Change typedef of prev_artist_infos
Feeling much better about my choices this time around
2025-04-17 15:17:10 -04:00
4d63844def Make artist retrieval apart of main query
This feels bad or wrong somehow, but it do be working tho
2025-04-17 15:05:44 -04:00
2c4af0b378 Include artist column for albums
I'm convinced there's a better way of doing this, but this is all I can think of right now
2025-04-17 14:05:15 -04:00
41ab0dc888 Remove artists column from views
I kinda just didn't want to deal with it while implementing the raw sql. Bringing it back is my next priority, but I want to do the searching in a nice way, and I'm not sure how to do that yet
2025-04-17 00:26:56 -04:00
27358fe217 Implement db searches using raw sql 2025-04-17 00:24:48 -04:00
09d4453665 Fix various issues with process_scrobbles
I use the ins_ variables an unnecessary amount I think, I need to take a closer look at it, and give them better names
2025-04-17 00:24:16 -04:00
3f69183b6f Create new Schema from migrations 2025-04-17 00:23:12 -04:00
64038079d8 Update process_scrobbles.zig to fit new db 2025-04-07 15:44:52 -04:00
0537ef7db2 she QUERY on my DATA so she's BASEd 2025-04-07 10:44:28 -04:00
117 changed files with 1773 additions and 1729 deletions

3
.gitignore vendored
View file

@ -6,4 +6,5 @@ static/
src/app/database/data.db-journal
src/app/database/old_migrations/
src/lib
src/app/scripts/
src/app/scripts/
rules.json

View file

@ -3,7 +3,6 @@
Inspired by [Last.fm](https://last.fm), [Maloja](https://github.com/krateng/maloja), and [Lastfmstats.com](https://www.lastfmstats.com).
**Z**uletzt is written with [**Z**ig](https://github.com/ziglang/zig) and [Jetzig](https://github.com/jetzig-framework/jetzig) as a means of learning the
language, reintroducing myself to programming, and combining
the functionality of the aforementioned inspirations.
@ -12,17 +11,49 @@ 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"
- [ ] Include respective artist(s)
- [x] Include respective artist(s)
- [ ] Include respective album[^10]
- [x] Include number of plays
- [ ] Create disambiguation pages
@ -38,7 +69,7 @@ Licensed under MIT.
- [ ] Import from Discogs[^2]
- [ ] Import listening history
- [x] From Lastfmstats.com (.json file)[^3]
- [ ] From Last.fm (authentication)
- [x] From Last.fm (authentication)
- [x] From Spotify (.json file)
- [ ] From other streaming services[^4]
- [ ] "Unofficial scrobbles"[^9]
@ -57,6 +88,7 @@ Licensed under MIT.
- [ ] Rank songs
- [ ] Custom statistics[^7]
- [ ] "Playlists"[^8]
- [ ] First launch setup
[^1]: I do not intend to exactly replicate all the statistics Lastfmstats.com provides, but I would at least like to give the user the option to see those kinds of statistics, or generate them themselves (see 7).

View file

@ -17,12 +17,12 @@
// internet connectivity.
.dependencies = .{
.jetzig = .{
.url = "https://github.com/jetzig-framework/jetzig/archive/2c52792217b9441ed5e91d67e7ec5a8959285307.tar.gz",
.hash = "jetzig-0.0.0-IpAgLbMeDwDYRqWu0OJixGbcDUoUbfAN0xGe1xsYRHTj",
.url = "https://github.com/jetzig-framework/jetzig/archive/695664bc10e6e73389ee2fe7fbcaedc27fa8adc9.tar.gz",
.hash = "jetzig-0.0.0-IpAgLSFeDwBEhfEaDBo0JqhRbvQA3ScbWjssBqhJHFmb",
},
.zeit = .{
.url = "https://github.com/rockorager/zeit/archive/refs/heads/main.tar.gz",
.hash = "zeit-0.6.0-5I6bk5daAgC-P60TjxRqW0bYknfCGxJp-03eS9UjGrO7",
.url = "https://github.com/rockorager/zeit/archive/refs/tags/v0.6.0.tar.gz",
.hash = "zeit-0.0.0-5I6bk_pZAgB03N1p1GmVOZ--gOFwwQSRKj1UXb5tnaKS",
},
},
.paths = .{

View file

@ -1,164 +0,0 @@
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;
```

View file

@ -15,7 +15,7 @@ pub const database = .{
.port = 5432,
.username = "postgres",
.password = "postgres",
.database = "zuletzt_dev",
.database = "zuletzt_rsql",
.pool_size = 16,
},

View file

@ -4,7 +4,7 @@ pub const Album = jetquery.Model(
@This(),
"albums",
struct {
id: i32,
id: i64,
name: []const u8,
length: ?f32,
created_at: jetquery.DateTime,
@ -12,91 +12,19 @@ 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, .{}),
.albumartists = jetquery.hasMany(.Albumartist, .{}),
.artistalbums = jetquery.hasMany(.Artistalbum, .{}),
},
},
);
pub const Alias = jetquery.Model(
pub const Albumsong = jetquery.Model(
@This(),
"aliases",
"albumsongs",
struct {
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,
id: i64,
song_id: i64,
album_id: i64,
created_at: jetquery.DateTime,
updated_at: jetquery.DateTime,
},
@ -104,27 +32,80 @@ pub const Rating = 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: i32,
song_id: i32,
album_id: i32,
date: jetquery.DateTime,
id: i64,
albumsong: i64,
datetime: jetquery.DateTime,
created_at: jetquery.DateTime,
updated_at: jetquery.DateTime,
},
.{
.relations = .{
.song = jetquery.belongsTo(.Song, .{}),
.album = jetquery.belongsTo(.Album, .{}),
.scrobbleartists = jetquery.hasMany(.Scrobbleartist, .{}),
.albumsong = jetquery.belongsTo(.Albumsong, .{ .foreign_key = "albumsong" }),
},
},
);
@ -133,7 +114,7 @@ pub const Song = jetquery.Model(
@This(),
"songs",
struct {
id: i32,
id: i64,
name: []const u8,
length: ?f32,
hidden: bool,
@ -142,84 +123,26 @@ 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 Albumartist = jetquery.Model(
pub const Artistsong = jetquery.Model(
@This(),
"Albumartists",
"artistsongs",
struct {
id: i32,
album_id: i32,
artist_id: i32,
id: i64,
artist_id: i64,
song_id: i64,
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, .{}),
},
},
);

View file

@ -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(
"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", .{});
}

View file

@ -1,19 +0,0 @@
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", .{});
}

View file

@ -1,19 +0,0 @@
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", .{});
}

View file

@ -1,21 +0,0 @@
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", .{});
}

View file

@ -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(
"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", .{});
}

View file

@ -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(
"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", .{});
}

View file

@ -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", .{}),
t.column("album_id", .integer, .{}),
t.column("song_id", .integer, .{}),
t.timestamps(.{}),
},
.{},
);
}
pub fn down(repo: anytype) !void {
try repo.dropTable("Albumsongs", .{});
}

View file

@ -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(
"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", .{});
}

View file

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

View file

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

View file

@ -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", .{ .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", .{});
}

View file

@ -7,9 +7,8 @@ pub fn up(repo: anytype) !void {
"scrobbles",
&.{
t.primaryKey("id", .{}),
t.column("song_id", .integer, .{}),
t.column("album_id", .integer, .{}),
t.column("date", .datetime, .{}),
t.column("albumsong", .bigint, .{ .reference = .{ "albumsongs", "id" } }),
t.column("datetime", .datetime, .{}),
t.timestamps(.{}),
},
.{},

View file

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

View file

@ -0,0 +1,20 @@
const std = @import("std");
const jetquery = @import("jetquery");
const t = jetquery.schema.table;
pub fn up(repo: anytype) !void {
try repo.createTable(
"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", .{});
}

View file

@ -0,0 +1,20 @@
const std = @import("std");
const jetquery = @import("jetquery");
const t = jetquery.schema.table;
pub fn up(repo: anytype) !void {
try repo.createTable(
"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", .{});
}

View file

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

View file

@ -0,0 +1,53 @@
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;
}

View file

@ -1,8 +1,8 @@
const std = @import("std");
const jetzig = @import("jetzig");
const jetquery = @import("jetzig").jetquery;
const Scrobble = @import("../../types.zig").LastFMScrobble;
const lastfm = @import("../../types.zig").LastFM;
const Data = @import("../../types.zig");
const rules = @import("../../apply_rule.zig");
// 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,118 +14,121 @@ const lastfm = @import("../../types.zig").LastFM;
// - 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)))) };
// 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)));
// Probably want to include artist name here, but not sure how to yet
// 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)));
const track_artists = item.getT(.array, "artists_track").?.items();
const album_artists = item.getT(.array, "artists_album").?.items();
// 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.)
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);
// 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.
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").?)),
};
// 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.
var album_hash_string = std.ArrayList(u8).init(allocator);
var track_hash_string = std.ArrayList(u8).init(allocator);
//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;
// 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)));
}
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;
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 (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);
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);
}
}
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);
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);
}
}
// 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 });
// }
//}
}

View file

@ -0,0 +1,131 @@
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);
// }
// }
//}
}

View file

@ -1,65 +0,0 @@
/// 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);
}

View file

@ -1,155 +1,33 @@
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);
for (albums) |album| {
var album_view = try albums_view.append(.object);
const albums = try queries.entityQueryResult(request, queries.loadQuery(.album, .entities), .{});
try root.put("albums", albums);
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 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);
}
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);
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);
}

View file

@ -1,19 +1,22 @@
@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}}</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>
<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)
</body>
</html>

View file

@ -1,40 +1,15 @@
@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>
<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>
@partial partials/newtable(T: ColumnChoices, table_data: .albums, columns: columns)
</body>
</html>

View file

@ -1,141 +1,37 @@
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);
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);
}
const artists = try queries.entityQueryResult(request, queries.loadQuery(.artist, .entities), .{});
try root.put("artists", artists);
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 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);
}
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);
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);
}

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,31 +1,28 @@
@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}}</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>
<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)
</body>
</html>

View file

@ -1,34 +1,15 @@
@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>
<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>
@partial partials/newtable(T: ColumnChoices, table_data: .artists, columns: columns)
</body>
</html>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -11,26 +11,3 @@ 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);
}

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -11,26 +11,3 @@ 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);
}

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -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, "/search", .{});
const response = try app.request(.GET, "/groups", .{});
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, "/search/example-id", .{});
const response = try app.request(.GET, "/groups/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, "/search/new", .{});
const response = try app.request(.GET, "/groups/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, "/search/example-id/edit", .{});
const response = try app.request(.GET, "/groups/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, "/search", .{});
const response = try app.request(.POST, "/groups", .{});
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, "/search/example-id", .{});
const response = try app.request(.PUT, "/groups/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, "/search/example-id", .{});
const response = try app.request(.PATCH, "/groups/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, "/search/example-id", .{});
const response = try app.request(.DELETE, "/groups/example-id", .{});
try response.expectStatus(.ok);
}

View file

@ -0,0 +1,11 @@
<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>

View file

@ -16,21 +16,3 @@ 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);
}

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -0,0 +1,11 @@
@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>

View file

@ -1,7 +1,11 @@
<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>

View file

@ -0,0 +1,75 @@
@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>

View file

@ -1,18 +0,0 @@
@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>

View file

@ -0,0 +1,20 @@
@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>

View file

@ -16,21 +16,3 @@ 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);
}

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -4,101 +4,38 @@ 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);
}

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +1,62 @@
<div>
<span>Content goes here</span>
</div>
<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>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,64 +1,12 @@
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);
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);
}
const scrobbles = try queries.entityQueryResult(request, queries.loadQuery(.scrobble, .entities), .{});
try root.put("scrobbles", scrobbles);
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);
}

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,42 +1,15 @@
@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>
<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>
@partial partials/newtable(T: ColumnChoices, table_data: .scrobbles, columns: columns)
</body>
</html>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,129 +1,40 @@
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);
for (songs) |song| {
var song_view = try songs_view.append(.object);
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);
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 {
_ = id;
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);
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);
}

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +0,0 @@
<div>
<span>Content goes here</span>
</div>

View file

@ -1,3 +1,32 @@
<div>
<span>Content goes here</span>
@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>
<div style="display:flex;flex-direction:row;justify-content:space-evenly">
<div style="display:flex;flex-direction:column;align-self:left">
<div>{{.song.scrobbles}} scrobbles ({{.song.rank}} place)</div>
@partial partials/firstlast_listens(firstlast: .firstlast)
<h3>Yearly Performance</h3>
@partial partials/timescale(range: .yearly)
<h2>Scrobbles</h2>
@partial partials/newtable(T: ColumnChoices, table_data: .scrobbles, columns: columns)
</div>
<div style="display:flex;flex-direction:column;align-self:right">
<h2>Rating</h2>
<input type="text">
<input type="button">
</div>
</div>
</body>
</html>

View file

@ -1,40 +1,17 @@
@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>
<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>
@end
@partial partials/newtable(T: ColumnChoices, table_data: .songs, columns: columns)
</body>
</html>

Some files were not shown because too many files have changed in this diff Show more