Compare commits
105 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f451868af | |||
| 2a42e07df0 | |||
| b0f7884f84 | |||
| 280cba2f9a | |||
| 682eebc951 | |||
| cd8c798bd4 | |||
| 6aac0bff2b | |||
| 902fcd4447 | |||
| 8af6341f95 | |||
| c95ac51e05 | |||
| 6fe885132a | |||
| 0dec52af01 | |||
| 851aec3a97 | |||
| 15e72ea326 | |||
| 7b1fc6dd71 | |||
| da9934ae1e | |||
| 12722f282d | |||
| b0727e77e1 | |||
| f9718f3a37 | |||
| 9fa90ff129 | |||
| 5739f89e0d | |||
| 29041044e7 | |||
| 0b7efc3420 | |||
| 9f27fad235 | |||
| 6f6aaecb8f | |||
| 996022fe5f | |||
| b7e625dd98 | |||
| f292368947 | |||
| 93da50652a | |||
| 77a9c24dab | |||
| 9c90c683c6 | |||
| 2d7d2835fd | |||
| 0b07947b8a | |||
| 36873053bc | |||
| df8f01525e | |||
| 2f420bc5ce | |||
| 6a1c822420 | |||
| a8a4ed27c4 | |||
| 1e4a271b8d | |||
| 85552f39c1 | |||
| 162341fb5f | |||
| c8f2ef57c8 | |||
| 3ef17fcd46 | |||
| adcaff34ea | |||
| 566edf1818 | |||
| 906ba6d2e5 | |||
| 3777b818e3 | |||
| a314fd447d | |||
| c57bf18627 | |||
| d81681e698 | |||
| 62590fee37 | |||
| d638fa66c5 | |||
| 3ff973e193 | |||
| f59eec79a8 | |||
| aab61631a3 | |||
| 7f3778e82f | |||
| 09f542e26e | |||
| 1734e6a4bb | |||
| d6a638bf27 | |||
| a2a739bc9c | |||
| 6494bbdf60 | |||
| 4c759433d2 | |||
| 614607ae71 | |||
| 5697f95355 | |||
| 89e98c7a47 | |||
| f69ffb2b37 | |||
| 52fefc9ba5 | |||
| 4991bac9a4 | |||
| c42b8d24dd | |||
| 365b9dbf11 | |||
| 153ea869e0 | |||
| 4758885c68 | |||
| 9ffc45b207 | |||
| 94cc6e3bd5 | |||
| c574885f8d | |||
| 762a4fd51e | |||
| 3345b20f1f | |||
| 78e416eeaf | |||
| 8138e5ccf2 | |||
| ae85f94ddb | |||
| cb89a3e6f3 | |||
| 65136a44d6 | |||
| 01fe10f045 | |||
| 18d4df0a5c | |||
| 5e58e81ca7 | |||
| 9df8f9ea12 | |||
| be8c1191b0 | |||
| 0631ded115 | |||
| e9c72041a5 | |||
| 77170a1e28 | |||
| 87a2fe2d34 | |||
| 445ca45fa9 | |||
| baf9ef38a4 | |||
| 5383b69eb6 | |||
| 18cdb48b53 | |||
| ff8cdabbf1 | |||
| 387493d3c0 | |||
| 4d63844def | |||
| 2c4af0b378 | |||
| 41ab0dc888 | |||
| 27358fe217 | |||
| 09d4453665 | |||
| 3f69183b6f | |||
| 64038079d8 | |||
| 0537ef7db2 |
127 changed files with 2528 additions and 1773 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -7,3 +7,4 @@ src/app/database/data.db-journal
|
|||
src/app/database/old_migrations/
|
||||
src/lib
|
||||
src/app/scripts/
|
||||
rules.json
|
||||
131
README.md
131
README.md
|
|
@ -1,28 +1,85 @@
|
|||
# Zuletzt
|
||||
**Zuletzt** gives you the statistics of your music listening habits.
|
||||
|
||||
Inspired by [Last.fm](https://last.fm), [Maloja](https://github.com/krateng/maloja), and [Lastfmstats.com](https://www.lastfmstats.com).
|
||||
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.
|
||||
**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.
|
||||
|
||||
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.
|
||||
|
||||
## Quirks
|
||||
Zuletzt does not assume any two songs are the same song unless they
|
||||
share the exact same metadata. However, there are plenty of situations where a
|
||||
song might appear on more than one album (consider a greatest hits album).
|
||||
Thus, a song which was played on one album 30 times, and also played on a
|
||||
different album 20 times, would not receive the credit of being played a total
|
||||
of 50 times. To resolve this, Zuletzt lets the user specify that these two
|
||||
songs are the same. This is, however, different from SongGroups. SongGroups,
|
||||
while superficially providing very similar functionality, does not permanently
|
||||
combine the statistics of the two songs, but will show their combined
|
||||
statistics anyways. This is useful if, for example, one song is a remix of
|
||||
another - they are, in reality, different songs, but there is a clear
|
||||
connection between them, and it may be interesting to see what their combined
|
||||
statistics are. The decision to merge songs or make a SongGroup, or neither, is
|
||||
left to the user, but the general thought is:
|
||||
- If they're the *exact* same song, merge them, and the data becomes more
|
||||
accurate for that song
|
||||
- If one is somehow remixed/covered/altered in some way, make a SongGroup, and
|
||||
see the combined info *as if* you had merged them.
|
||||
|
||||
## 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 +95,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]
|
||||
|
|
@ -49,7 +106,8 @@ Licensed under MIT.
|
|||
- [ ] Genres
|
||||
- [ ] Owned
|
||||
- [ ] Holiday
|
||||
- [ ] [MusicBrainz integration](https://musicbrainz.org/doc/libmusicbrainz)[^11]
|
||||
- [ ] [MusicBrainz
|
||||
integration](https://musicbrainz.org/doc/libmusicbrainz)[^11]
|
||||
- [ ] Concerts
|
||||
- [ ] Import from Setlist.fm[^5]
|
||||
- [ ] Ratings
|
||||
|
|
@ -57,28 +115,63 @@ 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).
|
||||
[^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).
|
||||
|
||||
[^2]: I do not intend to provide the level of granularity that Discogs provides, but a simple toggle that means "I own some version of this release" is all that is necessary.
|
||||
[^2]: I do not intend to provide the level of granularity that Discogs
|
||||
provides, but a simple toggle that means "I own some version of this release"
|
||||
is all that is necessary.
|
||||
|
||||
[^3]: I have not investigated any other service for downloading your listening history from Last.fm, but providing the listening history as a JSON rather than a CSV is highly preferred. I may eventually provide my own way of downloading Last.fm data as a JSON, but I would prefer to allow users to enter their username, or authenticate, and avoid needing to upload a file altogether.
|
||||
[^3]: I have not investigated any other service for downloading your listening
|
||||
history from Last.fm, but providing the listening history as a JSON rather than
|
||||
a CSV is highly preferred. I may eventually provide my own way of downloading
|
||||
Last.fm data as a JSON, but I would prefer to allow users to enter their
|
||||
username, or authenticate, and avoid needing to upload a file altogether.
|
||||
|
||||
[^4]: I only intend to allow imports from Last.fm and Spotify at the moment because those are the only data sources I currently rely on. To that extent, I imagine I could import from other sources as well fairly easily, although I do not know what their data dumps look like.
|
||||
[^4]: I only intend to allow imports from Last.fm and Spotify at the moment
|
||||
because those are the only data sources I currently rely on. To that extent, I
|
||||
imagine I could import from other sources as well fairly easily, although I do
|
||||
not know what their data dumps look like.
|
||||
|
||||
[^5]: I only intend to allow imports from Setlist.fm at the moment because that is the only data source I currently rely on.
|
||||
[^5]: I only intend to allow imports from Setlist.fm at the moment because that
|
||||
is the only data source I currently rely on.
|
||||
|
||||
[^6]: RYM has the most data, and once it has an API, will be the only user-driven review site that *has* an API. In this context, "integration" simply means displaying the critic score and user score next to the album. You will be able to write reviews and ranks songs/albums(/artists?), but not for them to be published to RYM.
|
||||
[^6]: RYM has the most data, and once it has an API, will be the only
|
||||
user-driven review site that *has* an API. In this context, "integration"
|
||||
simply means displaying the critic score and user score next to the album. You
|
||||
will be able to write reviews and ranks songs/albums(/artists?), but not for
|
||||
them to be published to RYM.
|
||||
|
||||
[^7]: I envision something akin to the Custom Reports from [Actual Budget](https://github.com/actualbudget/actual) that will allow users to create their own ways of rating/ranking songs/albums, and view their listening habits.
|
||||
[^7]: I envision something akin to the Custom Reports from [Actual
|
||||
Budget](https://github.com/actualbudget/actual) that will allow users to create
|
||||
their own ways of rating/ranking songs/albums, and view their listening habits.
|
||||
|
||||
[^8]: Misleading title, but same functionality as "Lists" on AlbumOfTheYear, although I would like to allow albums and songs to appear on the same list.
|
||||
[^8]: Misleading title, but same functionality as "Lists" on AlbumOfTheYear,
|
||||
although I would like to allow albums and songs to appear on the same list.
|
||||
|
||||
[^9]: This is a working title, but I have sources (iPods) that provide a play count, but no play dates, so I can't list them among my usual Scrobbles. However, I would still like to display that information along with everything else, so I would like to provide a way of entering this data into a separate category that can be toggled to display alongside "official" Scrobbles.
|
||||
[^9]: This is a working title, but I have sources (iPods) that provide a play
|
||||
count, but no play dates, so I can't list them among my usual Scrobbles.
|
||||
However, I would still like to display that information along with everything
|
||||
else, so I would like to provide a way of entering this data into a separate
|
||||
category that can be toggled to display alongside "official" Scrobbles.
|
||||
|
||||
[^10]: Would probably select the album with the most scrobbles
|
||||
|
||||
[^11]: I probably don't understand it well enough, but it appears that I should be able to do this using `@cImport` and/or `translate-c` on the original MusicBrainz source, but it's not all clear to me on how that would work yet. This is a necessary step for what I have planned however, so we'll see where it goes. **Update 3/25/25:** A Zig implementation for what Zuletzt requires (and *only* what Zuletzt requires) has been (mostly) written.
|
||||
[^11]: I probably don't understand it well enough, but it appears that I should
|
||||
be able to do this using `@cImport` and/or `translate-c` on the original
|
||||
MusicBrainz source, but it's not all clear to me on how that would work yet.
|
||||
This is a necessary step for what I have planned however, so we'll see where it
|
||||
goes. **Update 3/25/25:** A Zig implementation for what Zuletzt requires (and
|
||||
*only* what Zuletzt requires) has been (mostly) written.
|
||||
|
||||
## Contributing
|
||||
I am a math student who is interested in programming. I will not be writing quality code. That said, Zuletzt is something that, at the moment, I am very excited about making, and using to relearn some things about programming. Unless contributions are given in the form of code review, or some kind of constructive criticism, it's not likely that I accept pull requests. The project is, however, licensed under the MIT License, so feel free to do what you like with it in your own way.
|
||||
I am a math student who is interested in programming. I will
|
||||
not be writing quality code. That said, Zuletzt is something that, at the
|
||||
moment, I am very excited about making, and using to relearn some things about
|
||||
programming. Unless contributions are given in the form of code review, or some
|
||||
kind of constructive criticism, it's not likely that I accept pull requests.
|
||||
The project is, however, licensed under the MIT License, so feel free to do
|
||||
what you like with it in your own way.
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ pub fn build(b: *std.Build) !void {
|
|||
.optimize = optimize,
|
||||
});
|
||||
|
||||
exe.use_llvm = true;
|
||||
|
||||
// Example dependency:
|
||||
//
|
||||
const zig_time_dep = b.dependency("zeit", .{});
|
||||
|
|
|
|||
|
|
@ -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 = .{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
```
|
||||
|
|
@ -15,7 +15,7 @@ pub const database = .{
|
|||
.port = 5432,
|
||||
.username = "postgres",
|
||||
.password = "postgres",
|
||||
.database = "zuletzt_dev",
|
||||
.database = "zuletzt_rsql",
|
||||
.pool_size = 16,
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
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,83 @@ 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(
|
||||
pub const Albumrating = jetquery.Model(
|
||||
@This(),
|
||||
"Scrobbleartists",
|
||||
"albumratings",
|
||||
struct {
|
||||
id: i32,
|
||||
scrobble_id: i32,
|
||||
artist_id: i32,
|
||||
album: i64,
|
||||
rating: ?i16,
|
||||
rating_text: ?[]const u8,
|
||||
date: jetquery.DateTime,
|
||||
created_at: jetquery.DateTime,
|
||||
updated_at: jetquery.DateTime,
|
||||
},
|
||||
.{
|
||||
.relations = .{
|
||||
.scrobble = jetquery.belongsTo(.Scrobble, .{}),
|
||||
.artist = jetquery.belongsTo(.Artist, .{}),
|
||||
.album = jetquery.belongsTo(.Album, .{ .foreign_key = "album" }),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
pub const Artistrating = jetquery.Model(
|
||||
@This(),
|
||||
"artistratings",
|
||||
struct {
|
||||
id: i32,
|
||||
artist: i64,
|
||||
rating: ?i16,
|
||||
rating_text: ?[]const u8,
|
||||
date: jetquery.DateTime,
|
||||
created_at: jetquery.DateTime,
|
||||
updated_at: jetquery.DateTime,
|
||||
},
|
||||
.{
|
||||
.relations = .{
|
||||
.artist = jetquery.belongsTo(.Artist, .{ .foreign_key = "artist" }),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
pub const Songrating = jetquery.Model(
|
||||
@This(),
|
||||
"songratings",
|
||||
struct {
|
||||
id: i32,
|
||||
song: i64,
|
||||
rating: ?i16,
|
||||
rating_text: ?[]const u8,
|
||||
date: jetquery.DateTime,
|
||||
created_at: jetquery.DateTime,
|
||||
updated_at: jetquery.DateTime,
|
||||
},
|
||||
.{
|
||||
.relations = .{
|
||||
.albumsong = jetquery.belongsTo(.Albumsong, .{ .foreign_key = "song" }),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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", .{});
|
||||
}
|
||||
|
|
@ -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", .{});
|
||||
}
|
||||
|
|
@ -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", .{});
|
||||
}
|
||||
|
|
@ -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", .{});
|
||||
}
|
||||
|
|
@ -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", .{});
|
||||
}
|
||||
|
|
@ -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", .{});
|
||||
}
|
||||
|
|
@ -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", .{});
|
||||
}
|
||||
|
|
@ -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, .{}),
|
||||
|
|
@ -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(.{}),
|
||||
|
|
@ -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", .{});
|
||||
}
|
||||
|
|
@ -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(.{}),
|
||||
},
|
||||
.{},
|
||||
|
|
@ -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(.{}),
|
||||
},
|
||||
.{},
|
||||
|
|
@ -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", .{});
|
||||
}
|
||||
|
|
@ -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", .{});
|
||||
}
|
||||
|
|
@ -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", .{});
|
||||
}
|
||||
|
|
@ -4,11 +4,12 @@ const t = jetquery.schema.table;
|
|||
|
||||
pub fn up(repo: anytype) !void {
|
||||
try repo.createTable(
|
||||
"ratings",
|
||||
"songratings",
|
||||
&.{
|
||||
t.primaryKey("id", .{}),
|
||||
t.column("reference_id", .integer, .{}),
|
||||
t.column("score", .float, .{}),
|
||||
t.column("song", .bigint, .{ .reference = .{ "songs", "id" } }),
|
||||
t.column("rating", .smallint, .{ .optional = true }),
|
||||
t.column("rating_text", .text, .{ .optional = true }),
|
||||
t.column("date", .datetime, .{}),
|
||||
t.timestamps(.{}),
|
||||
},
|
||||
|
|
@ -17,5 +18,5 @@ pub fn up(repo: anytype) !void {
|
|||
}
|
||||
|
||||
pub fn down(repo: anytype) !void {
|
||||
try repo.dropTable("ratings", .{});
|
||||
try repo.dropTable("songratings", .{});
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
const std = @import("std");
|
||||
const jetquery = @import("jetquery");
|
||||
const t = jetquery.schema.table;
|
||||
|
||||
pub fn up(repo: anytype) !void {
|
||||
try repo.createTable(
|
||||
"albumratings",
|
||||
&.{
|
||||
t.primaryKey("id", .{}),
|
||||
t.column("album", .bigint, .{ .reference = .{ "albums", "id" } }),
|
||||
t.column("rating", .smallint, .{ .optional = true }),
|
||||
t.column("rating_text", .text, .{ .optional = true }),
|
||||
t.column("date", .datetime, .{}),
|
||||
t.timestamps(.{}),
|
||||
},
|
||||
.{},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn down(repo: anytype) !void {
|
||||
try repo.dropTable("albumratings", .{});
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
const std = @import("std");
|
||||
const jetquery = @import("jetquery");
|
||||
const t = jetquery.schema.table;
|
||||
|
||||
pub fn up(repo: anytype) !void {
|
||||
try repo.createTable(
|
||||
"artistratings",
|
||||
&.{
|
||||
t.primaryKey("id", .{}),
|
||||
t.column("artist", .bigint, .{ .reference = .{ "artists", "id" } }),
|
||||
t.column("rating", .smallint, .{ .optional = true }),
|
||||
t.column("rating_text", .text, .{ .optional = true }),
|
||||
t.column("date", .datetime, .{}),
|
||||
t.timestamps(.{}),
|
||||
},
|
||||
.{},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn down(repo: anytype) !void {
|
||||
try repo.dropTable("artistratings", .{});
|
||||
}
|
||||
31
src/app/jobs/add_album.zig
Normal file
31
src/app/jobs/add_album.zig
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
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;
|
||||
const artists = params.getT(.array, "artists").?.items();
|
||||
const album_id = try (params.get("album_hash").?).coerce(u64);
|
||||
|
||||
for (artists) |artist| {
|
||||
const artist_name = try artist.coerce([]const u8);
|
||||
const artist_id = std.hash.Fnv1a_64.hash(artist_name);
|
||||
const paired = @as(i64, @bitCast(@mod(@divFloor((artist_id +% album_id) *% (artist_id +% album_id +% 1), 2) +% album_id, std.math.maxInt(u64))));
|
||||
const aa_query = try jetzig.database.Query(.Artistalbum)
|
||||
.find(paired).execute(env.repo);
|
||||
|
||||
if (aa_query == null) {
|
||||
try jetzig.database.Query(.Artistalbum)
|
||||
.insert(.{ .id = paired, .artist_id = @as(i64, @bitCast(artist_id)), .album_id = @as(i64, @bitCast(album_id)) })
|
||||
.execute(env.repo);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/app/jobs/add_artist.zig
Normal file
25
src/app/jobs/add_artist.zig
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
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;
|
||||
const artist = params.getT(.string, "artist").?;
|
||||
const id = @as(i64, @bitCast(std.hash.Fnv1a_64.hash(artist)));
|
||||
const artist_query = try jetzig.database.Query(.Artist)
|
||||
.find(id).execute(env.repo);
|
||||
|
||||
if (artist_query == null) {
|
||||
try jetzig.database.Query(.Artist)
|
||||
.insert(.{ .id = id, .name = artist })
|
||||
.execute(env.repo);
|
||||
}
|
||||
}
|
||||
34
src/app/jobs/add_song.zig
Normal file
34
src/app/jobs/add_song.zig
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
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;
|
||||
//const album = params.getT(.string, "album").?;
|
||||
const as_id = try (params.get("as_hash").?).coerce(u64);
|
||||
const album_artists = params.getT(.array, "album_artists").?.items();
|
||||
// Will use this eventually, but not now
|
||||
// const track_artists = params.getT(.array,"track_artists");
|
||||
|
||||
for (album_artists) |artist| {
|
||||
const artist_name = try artist.coerce([]const u8);
|
||||
const artist_id = std.hash.Fnv1a_64.hash(artist_name);
|
||||
const asa_id = @as(i64, @bitCast(@mod(@divFloor((as_id +% artist_id) *% (as_id +% artist_id +% 1), 2) +% artist_id, std.math.maxInt(u64))));
|
||||
const asa_query = try jetzig.database.Query(.Albumsongsartist)
|
||||
.find(asa_id).execute(env.repo);
|
||||
|
||||
if (asa_query == null) {
|
||||
try jetzig.database.Query(.Albumsongsartist)
|
||||
.insert(.{ .id = asa_id, .albumsong_id = @as(i64, @bitCast(as_id)), .artist_id = @as(i64, @bitCast(artist_id)) })
|
||||
.execute(env.repo);
|
||||
}
|
||||
}
|
||||
}
|
||||
53
src/app/jobs/process_rule.zig
Normal file
53
src/app/jobs/process_rule.zig
Normal 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;
|
||||
}
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
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;
|
||||
|
||||
// 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;
|
||||
//_ = 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)));
|
||||
|
||||
// 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)));
|
||||
|
||||
// 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.)
|
||||
|
||||
// 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.
|
||||
|
||||
// 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_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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
const scr_id = try jetzig.database.Query(.Scrobble).insert(.{ .song_id = song_id, .album_id = album_id, .date = scrobble.date }).returning(.{.id}).execute(env.repo);
|
||||
defer env.repo.free(scr_id);
|
||||
try jetzig.database.Query(.Scrobbleartist).insert(.{ .scrobble_id = scr_id.?.id, .artist_id = artist_id }).execute(env.repo);
|
||||
}
|
||||
}
|
||||
|
||||
// I would like to replicate this kind of functionality for several kinds of queries
|
||||
// This one gives me all albums by Dream Theater (it also returns Dream Theater for
|
||||
// each entry, but removing artists.name from the SELECT would remove that)
|
||||
//
|
||||
// SELECT
|
||||
// artists.name, albums.name
|
||||
// FROM
|
||||
// "Albumartists"
|
||||
// INNER JOIN artists
|
||||
// ON "Albumartists".artist_id = artists.id
|
||||
// INNER JOIN albums
|
||||
// ON "Albumartists".album_id = albums.id
|
||||
// WHERE artists.name = 'Dream Theater';
|
||||
|
||||
//const query = jetzig.database.Query(.Artist).include(.artistalbums, .{});
|
||||
//const results = try env.repo.all(query);
|
||||
//defer env.repo.free(results);
|
||||
//for (results) |result| {
|
||||
// for (result.artistalbums) |artistalbum| {
|
||||
// std.log.debug("{s}: {any}", .{ result.name, artistalbum.album_id });
|
||||
// }
|
||||
//}
|
||||
}
|
||||
131
src/app/jobs/process_scrobbles2.zig
Normal file
131
src/app/jobs/process_scrobbles2.zig
Normal 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);
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -1,155 +1,62 @@
|
|||
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");
|
||||
const decode = @import("../../date_fmt.zig").urlDecode;
|
||||
|
||||
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 id_int = blk: {
|
||||
const rn = try decode(request.allocator, id);
|
||||
// Try to find the song by name
|
||||
const queried_albums = try jetzig.database.Query(.Album).select(.{.id}).where(.{ .name = rn }).all(request.repo);
|
||||
if (queried_albums.len == 0) {
|
||||
// Either we've been given an id in the db, or the song doesn't exist
|
||||
break :blk std.fmt.parseInt(i64, id, 10) catch return request.fail(.not_found);
|
||||
} else if (queried_albums.len == 1) {
|
||||
// It can only be one song
|
||||
break :blk queried_albums[0].id;
|
||||
} else {
|
||||
// It could be a variety of songs
|
||||
const albums = try queries.entityQueryResult(request, comptime queries.loadQuery(.album, .entities_by_name), .{rn});
|
||||
try root.put("name", rn);
|
||||
try root.put("albums", albums);
|
||||
try root.put("disambiguation", true);
|
||||
return request.render(.ok);
|
||||
}
|
||||
};
|
||||
const album = try queries.entityQueryResult(request, comptime queries.loadQuery(.album, .entity_info), .{id_int});
|
||||
try root.put("album", album);
|
||||
|
||||
const scrobbles = try queries.entityQueryResult(request, comptime queries.loadQuery(.song, .get_scrobbles), .{id_int});
|
||||
try root.put("scrobbles", scrobbles);
|
||||
|
||||
const songs = try queries.entityQueryResult(request, comptime queries.loadQuery(.album, .get_songs), .{id_int});
|
||||
try root.put("songs", songs);
|
||||
|
||||
const firstlast = try queries.entityQueryResult(request, comptime queries.loadQuery(.album, .firstlast), .{id_int});
|
||||
try root.put("firstlast", firstlast);
|
||||
|
||||
const timescale = try queries.entityQueryResult(request, comptime queries.loadQuery(.album, .timescale), .{id_int});
|
||||
try root.put("yearly", timescale);
|
||||
|
||||
const ratings = try queries.entityQueryResult(request, comptime queries.loadQuery(.song, .get_ratings), .{id_int});
|
||||
try root.put("reviews", ratings);
|
||||
|
||||
//const peak = try queries.entityQueryResult(request, comptime queries.loadQuery(.album, .peak), .{id_int});
|
||||
//try root.put("peak", peak);
|
||||
|
||||
return request.render(.ok);
|
||||
}
|
||||
|
||||
pub fn new(request: *jetzig.Request) !jetzig.View {
|
||||
return request.render(.ok);
|
||||
}
|
||||
|
||||
pub fn edit(id: []const u8, request: *jetzig.Request) !jetzig.View {
|
||||
_ = id;
|
||||
return request.render(.ok);
|
||||
}
|
||||
|
||||
pub fn post(request: *jetzig.Request) !jetzig.View {
|
||||
return request.render(.created);
|
||||
}
|
||||
|
||||
pub fn put(id: []const u8, request: *jetzig.Request) !jetzig.View {
|
||||
_ = id;
|
||||
return request.render(.ok);
|
||||
}
|
||||
|
||||
pub fn patch(id: []const u8, request: *jetzig.Request) !jetzig.View {
|
||||
_ = id;
|
||||
return request.render(.ok);
|
||||
}
|
||||
|
||||
pub fn delete(id: []const u8, request: *jetzig.Request) !jetzig.View {
|
||||
_ = id;
|
||||
return request.render(.ok);
|
||||
}
|
||||
|
||||
test "index" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const response = try app.request(.GET, "/album", .{});
|
||||
try response.expectStatus(.ok);
|
||||
}
|
||||
|
||||
test "get" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const response = try app.request(.GET, "/album/example-id", .{});
|
||||
try response.expectStatus(.ok);
|
||||
}
|
||||
|
||||
test "new" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const response = try app.request(.GET, "/album/new", .{});
|
||||
try response.expectStatus(.ok);
|
||||
}
|
||||
|
||||
test "edit" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const response = try app.request(.GET, "/album/example-id/edit", .{});
|
||||
try response.expectStatus(.ok);
|
||||
}
|
||||
|
||||
test "post" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const response = try app.request(.POST, "/album", .{});
|
||||
try response.expectStatus(.created);
|
||||
}
|
||||
|
||||
test "put" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const response = try app.request(.PUT, "/album/example-id", .{});
|
||||
try response.expectStatus(.ok);
|
||||
}
|
||||
|
||||
test "patch" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const response = try app.request(.PATCH, "/album/example-id", .{});
|
||||
try response.expectStatus(.ok);
|
||||
}
|
||||
|
||||
test "delete" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const response = try app.request(.DELETE, "/album/example-id", .{});
|
||||
try response.expectStatus(.ok);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,61 @@
|
|||
@zig {
|
||||
const ColumnChoices = []const enum{song, album, artist, artistlist, scrobbles, date};
|
||||
const columns: ColumnChoices = &.{.song, .scrobbles};
|
||||
const dis_columns: ColumnChoices = &.{.album, .artistlist, .scrobbles};
|
||||
}
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.5/dist/htmx.min.js" integrity="sha384-t4DxZSyQK+0Uv4jzy5B0QyHyWQD2GFURUmxKMBVww9+e2EJ0ei/vCvv7+79z0fkr" crossorigin="anonymous"></script>
|
||||
<meta charset="UTF-8">
|
||||
</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>
|
||||
@if ($.disambiguation)
|
||||
<h1>{{.name}} (disambiguation)</h1>
|
||||
@partial partials/newtable(T: ColumnChoices, table_data: .albums, columns: dis_columns)
|
||||
@else
|
||||
|
||||
@zig {
|
||||
const reviews = try zmpl.coerceArray(".reviews");
|
||||
}
|
||||
</table>
|
||||
<div style="text-align:center">
|
||||
<h1>{{.album.album_name}}</h1>
|
||||
<h2><a href="/artists/{{.album.artist_id}}">{{.album.artist_name}}</a></h2>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;flex-direction:row;justify-content:space-evenly">
|
||||
<div style="display:flex;flex-direction:column;align-self:left">
|
||||
@if ($.album.is_tie)
|
||||
<div>{{.album.scrobbles}} scrobbles ({{.album.rank}} place, tied)</div>
|
||||
@else
|
||||
<div>{{.album.scrobbles}} scrobbles ({{.album.rank}} place)</div>
|
||||
@end
|
||||
<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)
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;align-self:right">
|
||||
<h2>Rating</h2>
|
||||
<div id="review-container">
|
||||
@zig {
|
||||
if (reviews.len == 0) {
|
||||
<form>
|
||||
<input type="number" name="score" id="score" style="width:50px;height:30px">
|
||||
<textarea name="review" id="review" style="width:350px;height:100px"></textarea>
|
||||
<button hx-post="/ratings/albums" hx-vals='{"album_id":"{{.album.album_id}}"}' hx-target="#review-container" style="width:50px;height:30px">Post</button>
|
||||
</form>
|
||||
} else {
|
||||
for (reviews) |review| {
|
||||
<b>{{review.score}}</b>: {{review.review}} ({{review.date}})
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@end
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,141 +1,61 @@
|
|||
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");
|
||||
const decode = @import("../../date_fmt.zig").urlDecode;
|
||||
|
||||
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 id_int = blk: {
|
||||
const rn = try decode(request.allocator, id);
|
||||
// Try to find the song by name
|
||||
const queried_artists = try jetzig.database.Query(.Artist).select(.{.id}).where(.{ .name = rn }).all(request.repo);
|
||||
if (queried_artists.len == 0) {
|
||||
// Either we've been given an id in the db, or the song doesn't exist
|
||||
break :blk std.fmt.parseInt(i64, id, 10) catch return request.fail(.not_found);
|
||||
} else if (queried_artists.len == 1) {
|
||||
// It can only be one song
|
||||
break :blk queried_artists[0].id;
|
||||
} else {
|
||||
// It could be a variety of songs
|
||||
const artists = try queries.entityQueryResult(request, comptime queries.loadQuery(.artist, .entities_by_name), .{rn});
|
||||
try root.put("name", rn);
|
||||
try root.put("artists", artists);
|
||||
try root.put("disambiguation", true);
|
||||
return request.render(.ok);
|
||||
}
|
||||
};
|
||||
|
||||
const artist = try queries.entityQueryResult(request, comptime queries.loadQuery(.artist, .entity_info), .{id_int});
|
||||
try root.put("artist", artist);
|
||||
|
||||
const albums = try queries.entityQueryResult(request, comptime queries.loadQuery(.artist, .get_albums), .{id_int});
|
||||
try root.put("albums", albums);
|
||||
|
||||
const appears = try queries.entityQueryResult(request, comptime queries.loadQuery(.artist, .appears), .{id_int});
|
||||
try root.put("appears", appears);
|
||||
|
||||
const firstlast = try queries.entityQueryResult(request, comptime queries.loadQuery(.artist, .firstlast), .{id_int});
|
||||
try root.put("firstlast", firstlast);
|
||||
|
||||
const timescale = try queries.entityQueryResult(request, comptime queries.loadQuery(.artist, .timescale), .{id_int});
|
||||
try root.put("yearly", timescale);
|
||||
|
||||
//const peak = try queries.entityQueryResult(request, comptime queries.loadQuery(.artist, .peak), .{id_int});
|
||||
//try root.put("peak", peak);
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,31 +1,39 @@
|
|||
@zig {
|
||||
const ColumnChoices = []const enum{song, album, artist, artistlist, scrobbles, date};
|
||||
const columns: ColumnChoices = &.{.album, .scrobbles};
|
||||
const dis_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>{{.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>
|
||||
@if ($.disambiguation)
|
||||
<h1>{{.name}} (disambiguation)</h1>
|
||||
@partial partials/newtable(T: ColumnChoices, table_data: .artists, columns: dis_columns)
|
||||
@else
|
||||
|
||||
<h1>{{.artist.artist_name}}</h1>
|
||||
<div>
|
||||
@if ($.artist.is_tie)
|
||||
<div>{{.artist.scrobbles}} scrobbles ({{.artist.rank}} place, tied)</div>
|
||||
@else
|
||||
<div>{{.artist.scrobbles}} scrobbles ({{.artist.rank}} place)</div>
|
||||
@end
|
||||
<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)
|
||||
|
||||
@end
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -43,7 +43,7 @@ test "index" {
|
|||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const response = try app.request(.GET, "/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);
|
||||
}
|
||||
11
src/app/views/groups/index.zmpl
Normal file
11
src/app/views/groups/index.zmpl
Normal 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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
11
src/app/views/partials/_firstlast_listens.zmpl
Normal file
11
src/app/views/partials/_firstlast_listens.zmpl
Normal 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>
|
||||
|
|
@ -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>
|
||||
75
src/app/views/partials/_newtable.zmpl
Normal file
75
src/app/views/partials/_newtable.zmpl
Normal 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>
|
||||
|
|
@ -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>
|
||||
20
src/app/views/partials/_timescale.zmpl
Normal file
20
src/app/views/partials/_timescale.zmpl
Normal 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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
14
src/app/views/ratings/albums.zig
Normal file
14
src/app/views/ratings/albums.zig
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
pub fn post(request: *jetzig.Request) !jetzig.View {
|
||||
var root = try request.data(.object);
|
||||
const params = try request.params();
|
||||
const id = params.getT(.integer, "album_id").?;
|
||||
const score = if (params.getT(.integer, "score")) |score| @as(i16, @truncate(score)) else null;
|
||||
const review = params.getT(.string, "review");
|
||||
try jetzig.database.Query(.Albumrating).insert(.{ .album = id, .rating = score, .rating_text = review, .date = @divFloor(request.start_time, 1_000) }).execute(request.repo);
|
||||
try root.put("score", score);
|
||||
try root.put("review", review);
|
||||
|
||||
return request.render(.created);
|
||||
}
|
||||
1
src/app/views/ratings/albums/post.zmpl
Normal file
1
src/app/views/ratings/albums/post.zmpl
Normal file
|
|
@ -0,0 +1 @@
|
|||
<b>{{.score}}</b>: {{.review}} (Today)
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
14
src/app/views/ratings/songs.zig
Normal file
14
src/app/views/ratings/songs.zig
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
pub fn post(request: *jetzig.Request) !jetzig.View {
|
||||
var root = try request.data(.object);
|
||||
const params = try request.params();
|
||||
const id = params.getT(.integer, "song_id").?;
|
||||
const score = if (params.getT(.integer, "score")) |score| @as(i16, @truncate(score)) else null;
|
||||
const review = params.getT(.string, "review");
|
||||
try jetzig.database.Query(.Songrating).insert(.{ .song = id, .rating = score, .rating_text = review, .date = @divFloor(request.start_time, 1_000) }).execute(request.repo);
|
||||
try root.put("score", score);
|
||||
try root.put("review", review);
|
||||
|
||||
return request.render(.created);
|
||||
}
|
||||
1
src/app/views/ratings/songs/post.zmpl
Normal file
1
src/app/views/ratings/songs/post.zmpl
Normal file
|
|
@ -0,0 +1 @@
|
|||
<b>{{.score}}</b>: {{.review}} (Today)
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue