commit 2f27b711f4c63c65133c76519d09cbd38002ca6b
parent e5f4ff39a63baf26b1c2418fae2eab9871378a87
Author: brookjeynes <me@brookjeynes.dev>
Date: Wed, 17 Jun 2026 08:57:17 +1000
refactor: add release_type
Task-id: tqszpqrlzkvqrqkz
Signed-off-by: brookjeynes <me@brookjeynes.dev>
Diffstat:
14 files changed, 119 insertions(+), 22 deletions(-)
diff --git a/internal/db/db.go b/internal/db/db.go
@@ -95,6 +95,8 @@ func New(ctx context.Context, dbPath string) (*DB, error) {
title text not null,
latin_title text,
release_date text not null,
+ -- album, single, ep, other
+ type text not null,
created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
unique(title)
diff --git a/internal/db/release.go b/internal/db/release.go
@@ -12,10 +12,10 @@ func Release(e Execer, id domain.ReleaseID) (domain.Release, error) {
var releaseDateStr string
err := e.QueryRow(`
- select id, musicbrainz_release_id, title, latin_title, release_date
+ select id, musicbrainz_release_id, title, latin_title, release_date, type
from releases
where id = ?
- `, id).Scan(&release.ID, &release.MusicBrainzReleaseID, &release.Title, &release.LatinTitle, &releaseDateStr)
+ `, id).Scan(&release.ID, &release.MusicBrainzReleaseID, &release.Title, &release.LatinTitle, &releaseDateStr, &release.Type)
if err != nil {
return domain.Release{}, fmt.Errorf("failed to get release: %w", err)
}
@@ -155,12 +155,13 @@ func UpdateRelease(e Execer, release domain.EditRelease) error {
result, err := e.Exec(`
update releases
set
+ type = ?,
musicbrainz_release_id = ?,
title = ?,
latin_title = ?,
release_date = ?
where id = ?
- `, release.MusicBrainzReleaseID, release.Title, release.LatinTitle, release.ReleaseDate.Format(time.RFC3339), release.ID)
+ `, release.Type, release.MusicBrainzReleaseID, release.Title, release.LatinTitle, release.ReleaseDate.Format(time.RFC3339), release.ID)
if err != nil {
return fmt.Errorf("failed to update release: %w", err)
}
@@ -225,14 +226,15 @@ func UpdateRelease(e Execer, release domain.EditRelease) error {
func InsertRelease(e Execer, release domain.NewRelease) error {
result, err := e.Exec(`
insert into releases (
+ type,
musicbrainz_release_id,
title,
latin_title,
release_date,
created_at
)
- values (?, ?, ?, ?, ?)
- `, release.MusicBrainzReleaseID, release.Title, release.LatinTitle, release.ReleaseDate.Format(time.RFC3339), time.Now().Format(time.RFC3339))
+ values (?, ?, ?, ?, ?, ?)
+ `, release.Type, release.MusicBrainzReleaseID, release.Title, release.LatinTitle, release.ReleaseDate.Format(time.RFC3339), time.Now().Format(time.RFC3339))
if err != nil {
return fmt.Errorf("failed to insert release: %w", err)
}
@@ -270,7 +272,7 @@ func SearchRelease(e Execer, releaseName string) ([]domain.Release, error) {
wildcard := "%" + releaseName + "%"
rows, err := e.Query(`
- select id, musicbrainz_release_id, title, latin_title, release_date
+ select id, musicbrainz_release_id, title, latin_title, release_date, type
from releases
where title like ? collate nocase
or latin_title like ? collate nocase
@@ -289,7 +291,7 @@ func SearchRelease(e Execer, releaseName string) ([]domain.Release, error) {
var release domain.Release
var releaseDateStr string
- if err := rows.Scan(&release.ID, &release.MusicBrainzReleaseID, &release.Title, &release.LatinTitle, &releaseDateStr); err != nil {
+ if err := rows.Scan(&release.ID, &release.MusicBrainzReleaseID, &release.Title, &release.LatinTitle, &releaseDateStr, &release.Type); err != nil {
return nil, fmt.Errorf("failed to scan release row: %w", err)
}
diff --git a/internal/domain/release.go b/internal/domain/release.go
@@ -2,6 +2,22 @@ package domain
import "time"
+type ReleaseType string
+
+const (
+ ReleaseTypeAlbum ReleaseType = "album"
+ ReleaseTypeSingle ReleaseType = "single"
+ ReleaseTypeEP ReleaseType = "ep"
+ ReleaseTypeOther ReleaseType = "other"
+)
+
+var ReleaseTypes map[ReleaseType]struct{} = map[ReleaseType]struct{}{
+ ReleaseTypeAlbum: {},
+ ReleaseTypeSingle: {},
+ ReleaseTypeEP: {},
+ ReleaseTypeOther: {},
+}
+
type ReleaseID int64
type NewRelease struct {
@@ -11,6 +27,7 @@ type NewRelease struct {
LatinTitle *string
ReleaseDate time.Time
Artists []NewReleaseArtist
+ Type ReleaseType
}
type NewReleaseArtist struct {
@@ -27,6 +44,7 @@ type EditRelease struct {
ReleaseDate time.Time
Artists []NewReleaseArtist
Songs []EditReleaseSong
+ Type ReleaseType
}
type EditReleaseSong struct {
@@ -42,6 +60,7 @@ type Release struct {
ReleaseDate time.Time
Artists []ReleaseArtist
Songs []ReleaseSong
+ Type ReleaseType
}
type ReleaseArtist struct {
diff --git a/internal/server/edit-release.go b/internal/server/edit-release.go
@@ -14,7 +14,8 @@ import (
)
type EditReleaseForm struct {
- Title string `schema:"release"`
+ Title string `schema:"title"`
+ Type domain.ReleaseType `schema:"type"`
LatinTitle string `schema:"latin-title"`
ArtistID []domain.ArtistID `schema:"artist-id"`
SongID []domain.SongID `schema:"song-id"`
@@ -25,6 +26,14 @@ type EditReleaseForm struct {
func parseEditReleaseForm(form EditReleaseForm) (domain.EditRelease, error) {
var release domain.EditRelease
+ // type
+ if form.Type == "" {
+ return release, fmt.Errorf("Type must not be empty")
+ }
+ if _, ok := domain.ReleaseTypes[form.Type]; !ok {
+ return release, fmt.Errorf("Invalid type selected")
+ }
+
// title
if !domain.NoTrailingOrLeadingSpaceRegex.MatchString(form.Title) {
return release, fmt.Errorf("Title name must not be empty or have leading/trailing whitespace")
@@ -112,6 +121,7 @@ func parseEditReleaseForm(form EditReleaseForm) (domain.EditRelease, error) {
ReleaseDate: releaseDate,
Artists: artists,
Songs: songs,
+ Type: form.Type,
}
return release, nil
diff --git a/internal/server/new-release.go b/internal/server/new-release.go
@@ -13,7 +13,8 @@ import (
)
type NewReleaseForm struct {
- Title string `schema:"release"`
+ Title string `schema:"title"`
+ Type domain.ReleaseType `schema:"type"`
LatinTitle string `schema:"latin-title"`
ArtistID []domain.ArtistID `schema:"artist-id"`
ReleaseDate string `schema:"release-date"`
@@ -23,6 +24,14 @@ type NewReleaseForm struct {
func parseNewReleaseForm(form NewReleaseForm) (domain.NewRelease, error) {
var release domain.NewRelease
+ // type
+ if form.Type == "" {
+ return release, fmt.Errorf("Type must not be empty")
+ }
+ if _, ok := domain.ReleaseTypes[form.Type]; !ok {
+ return release, fmt.Errorf("Invalid type selected")
+ }
+
// title
if !domain.NoTrailingOrLeadingSpaceRegex.MatchString(form.Title) {
return release, fmt.Errorf("Title name must not be empty or have leading/trailing whitespace")
@@ -91,6 +100,7 @@ func parseNewReleaseForm(form NewReleaseForm) (domain.NewRelease, error) {
LatinTitle: latinTitle,
ReleaseDate: releaseDate,
Artists: artists,
+ Type: form.Type,
}
return release, nil
diff --git a/internal/server/search-release.go b/internal/server/search-release.go
@@ -27,7 +27,7 @@ func (ss *ServerState) SearchRelease(w http.ResponseWriter, r *http.Request) {
return
}
- release := r.URL.Query().Get("release")
+ release := r.URL.Query().Get("title")
results, err := db.SearchRelease(ss.db, release)
if err != nil {
logger.Error("failed to query db for releases", "err", err)
diff --git a/internal/server/ui/views/artist/artist.css b/internal/server/ui/views/artist/artist.css
@@ -0,0 +1,5 @@
+.release-title {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
diff --git a/internal/server/ui/views/artist/artist.templ b/internal/server/ui/views/artist/artist.templ
@@ -7,7 +7,7 @@ import (
)
templ Artist(params ArtistParams) {
- @base.Base(base.BaseParams{Title: params.Artist.Name, PageStylesheets: []string{}}) {
+ @base.Base(base.BaseParams{Title: params.Artist.Name, PageStylesheets: []string{"artist.css"}}) {
<article>
<header>
<hgroup>
@@ -40,9 +40,13 @@ templ Artist(params ArtistParams) {
}
</a>
</h3>
- <time aria-label="Release date" datetime={ release.ReleaseDate.Format(time.DateOnly) }>
- { release.ReleaseDate.Format("Jan 2, 2006") }
- </time>
+ <div>
+ <span>{ release.Type }</span>
+ ยท
+ <time aria-label="Release date" datetime={ release.ReleaseDate.Format(time.DateOnly) }>
+ { release.ReleaseDate.Format("Jan 2, 2006") }
+ </time>
+ </div>
</header>
<section aria-label="Track List">
<ol>
@@ -63,8 +67,8 @@ templ Artist(params ArtistParams) {
}
</ol>
</section>
- <section aria-label="Singles">
- <h2>Singles</h2>
+ <section aria-label="Songs">
+ <h2>Songs</h2>
<ul>
for _, song := range params.Singles {
<li>
diff --git a/internal/server/ui/views/edit-release/edit-release.templ b/internal/server/ui/views/edit-release/edit-release.templ
@@ -17,16 +17,46 @@ templ EditRelease(params EditReleaseParams) {
hx-swap="none"
hx-disable-elt="#save-release-btn"
>
+ <fieldset>
+ <legend>Type</legend>
+ <label for="type">type:</label>
+ <select name="type" required>
+ <option
+ if params.Release.Type == "album" {
+ selected
+ }
+ value="album"
+ >album</option>
+ <option
+ if params.Release.Type == "single" {
+ selected
+ }
+ value="single"
+ >single</option>
+ <option
+ if params.Release.Type == "ep" {
+ selected
+ }
+ value="ep"
+ >ep</option>
+ <option
+ if params.Release.Type == "other" {
+ selected
+ }
+ value="other"
+ >other</option>
+ </select>
+ </fieldset>
<fieldset
x-data="{ showLatinTitle: false }"
x-init="showLatinTitle = isNonAscii($refs.releaseTitle.value)"
>
- <legend>Release</legend>
+ <legend>Title</legend>
<label>
title:
<input
x-ref="releaseTitle"
- name="release"
+ name="title"
type="text"
value={ params.Release.Title }
pattern={ domain.NoTrailingOrLeadingSpaceRegex.String() }
diff --git a/internal/server/ui/views/new-release/new-release.templ b/internal/server/ui/views/new-release/new-release.templ
@@ -16,8 +16,18 @@ templ NewRelease(params NewReleaseParams) {
hx-swap="none"
hx-disable-elt="#add-release-btn"
>
+ <fieldset>
+ <legend>Type</legend>
+ <label for="type">type:</label>
+ <select name="type" required>
+ <option value="album">album</option>
+ <option value="single">single</option>
+ <option value="ep">ep</option>
+ <option value="other">other</option>
+ </select>
+ </fieldset>
<fieldset x-data="{ showLatinTitle: false }">
- <legend>Release</legend>
+ <legend>Title</legend>
<label>
title:
<input
@@ -25,7 +35,7 @@ templ NewRelease(params NewReleaseParams) {
hx-trigger="input changed delay:500ms"
hx-target="#similar-releases"
hx-swap="innerHTML"
- name="release"
+ name="title"
type="text"
pattern={ domain.NoTrailingOrLeadingSpaceRegex.String() }
required
diff --git a/internal/server/ui/views/new-song/new-song.templ b/internal/server/ui/views/new-song/new-song.templ
@@ -56,7 +56,7 @@ templ NewSong(params NewSongParams) {
hx-trigger="input changed delay:500ms"
hx-target="next .picker-options"
hx-swap="outerHTML"
- name="release"
+ name="title"
autocomplete="off"
:required="!selected && id > 0"
/>
diff --git a/internal/server/ui/views/release/release.templ b/internal/server/ui/views/release/release.templ
@@ -63,6 +63,10 @@ templ Release(params ReleaseParams) {
{ params.Release.ReleaseDate.Format("Jan 2, 2006") }
</time>
</dd>
+ <dt>Type</dt>
+ <dd>
+ <span>{ params.Release.Type }</span>
+ </dd>
if params.Release.MusicBrainzReleaseID != nil {
<dt>MusicBrainz release ID</dt>
<dd>
diff --git a/scripts/build-assets.sh b/scripts/build-assets.sh
@@ -15,6 +15,7 @@ cp static/vendor/*.min.js static/files/js/
minify internal/server/ui/style.css -o static/files/style.css
minify internal/server/toast/toast.css -o static/files/toast.css
minify internal/server/ui/views/song/song.css -o static/files/song.css
+minify internal/server/ui/views/artist/artist.css -o static/files/artist.css
minify internal/server/ui/views/release/release.css -o static/files/release.css
minify internal/server/ui/views/new-release/new-release.css -o static/files/new-release.css
minify internal/server/ui/views/new-song/new-song.css -o static/files/new-song.css
diff --git a/tasks/add_release_types.txt b/tasks/add_release_types.txt
@@ -2,7 +2,7 @@
id: tqszpqrlzkvqrqkz
[x] "album" is renamed to "release"
-[ ] follow musicbrainz modal of "album", "single", "ep", "other"
+[x] follow musicbrainz modal of "album", "single", "ep", "other"
[ ] songs must have an explicit release attached to them.
[ ] new song page has a "create as single" which will
- create a song