Some checks failed
Build and Push Docker Image / build (pull_request) Failing after 38s
This commit includes: 1. Live/Remix Filtering Feature: - Added ignore_live and ignore_remix columns to artist_settings table (migration 010) - Updated ArtistSettings struct with IgnoreLive and IgnoreRemix fields - Modified SaveArtistSettings and UpdateArtistSettings to handle new fields - Extended FilterOptions struct with IgnoreLive and IgnoreRemix - Updated ApplyTypeToggles and ApplyTypeTogglesToReleaseGroups to filter Live/Remix types - Added toggleIgnoreLive and toggleIgnoreRemix handlers in web layer - Updated ArtistData view model and artist.html template with new toggle UI - Comprehensive test coverage for all new functionality 2. CI/CD Pipeline with Gitea Actions: - Added .gitea/workflows/docker-build.yml for automated Docker builds - Workflow triggers on pushes to main/master and tags, plus PRs - Runs Go tests before building - Builds and pushes multi-architecture Docker images to gitea.mrixs.me - Includes caching for faster subsequent builds - Proper tagging strategy (branch, semver, SHA) - CI-CD-GUIDE.md documentation 3. Cleanup: - Removed temporary build artifacts and coverage files
203 lines
6.1 KiB
Go
203 lines
6.1 KiB
Go
package database
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// GetArtistSettings retrieves an artist_settings row by ID.
|
|
// Returns sql.ErrNoRows if the artist is not found.
|
|
func GetArtistSettings(db *DB, id string) (*ArtistSettings, error) {
|
|
var (
|
|
s ArtistSettings
|
|
mbid sql.NullString
|
|
lastSynced sql.NullTime
|
|
)
|
|
err := db.Conn().QueryRow(
|
|
"SELECT id, name, mbid, ignore_singles, ignore_compilations, ignore_live, ignore_remix, monitored, last_synced FROM artist_settings WHERE id = ?",
|
|
id,
|
|
).Scan(&s.ID, &s.Name, &mbid, &s.IgnoreSingles, &s.IgnoreCompilations, &s.IgnoreLive, &s.IgnoreRemix, &s.Monitored, &lastSynced)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrArtistNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
s.MBID = mbid.String
|
|
if lastSynced.Valid {
|
|
s.LastSynced = lastSynced.Time
|
|
}
|
|
return &s, nil
|
|
}
|
|
|
|
// TouchArtistSynced records that the artist was synced at the given time. It
|
|
// is used by the MusicBrainz pipeline to mark a successful sync (even one that
|
|
// found zero release groups) so the cache TTL is honoured.
|
|
func TouchArtistSynced(db DBer, artistID string, syncedAt time.Time) error {
|
|
_, err := db.Exec(
|
|
"UPDATE artist_settings SET last_synced = ? WHERE id = ?",
|
|
FormatCachedAt(syncedAt), artistID,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("touch artist synced: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SaveArtistSettings inserts or updates an artist_settings row. Columns not
|
|
// present in the struct's intended set are preserved on conflict rather than
|
|
// reset to their zero value: mbid and last_synced are carried over from the
|
|
// existing row when the caller does not supply new values. This protects the
|
|
// MusicBrainz-resolution cache and the sync TTL markers from being wiped on
|
|
// every periodic artist sync.
|
|
func SaveArtistSettings(db *DB, settings *ArtistSettings) error {
|
|
_, err := db.Conn().Exec(`
|
|
INSERT INTO artist_settings (id, name, mbid, ignore_singles, ignore_compilations, ignore_live, ignore_remix, monitored, last_synced)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(id) DO UPDATE SET
|
|
name = excluded.name,
|
|
mbid = COALESCE(excluded.mbid, artist_settings.mbid),
|
|
ignore_singles = excluded.ignore_singles,
|
|
ignore_compilations = excluded.ignore_compilations,
|
|
ignore_live = excluded.ignore_live,
|
|
ignore_remix = excluded.ignore_remix,
|
|
monitored = excluded.monitored,
|
|
last_synced = COALESCE(excluded.last_synced, artist_settings.last_synced)
|
|
`,
|
|
settings.ID, settings.Name, nullIfEmpty(settings.MBID), settings.IgnoreSingles, settings.IgnoreCompilations, settings.IgnoreLive, settings.IgnoreRemix, settings.Monitored, nullIfEmptyTime(settings.LastSynced),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("save artist settings: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// nullIfEmpty returns nil for an empty string so COALESCE-preserving columns
|
|
// (e.g. mbid) keep their existing value when the caller supplies no new one.
|
|
func nullIfEmpty(s string) interface{} {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
return s
|
|
}
|
|
|
|
// nullIfEmptyTime returns nil for zero time so COALESCE-preserving columns
|
|
// (e.g. last_synced) keep their existing value when the caller supplies no new one.
|
|
func nullIfEmptyTime(t time.Time) interface{} {
|
|
if t.IsZero() {
|
|
return nil
|
|
}
|
|
return t
|
|
}
|
|
|
|
// GetAllArtistSettings returns all rows from artist_settings.
|
|
func GetAllArtistSettings(db *DB) ([]ArtistSettings, error) {
|
|
rows, err := db.Conn().Query(
|
|
"SELECT id, name, mbid, ignore_singles, ignore_compilations, ignore_live, ignore_remix, monitored, last_synced FROM artist_settings",
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query all artist settings: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var results []ArtistSettings
|
|
for rows.Next() {
|
|
var s ArtistSettings
|
|
var mbid sql.NullString
|
|
var lastSynced sql.NullTime
|
|
if err := rows.Scan(&s.ID, &s.Name, &mbid, &s.IgnoreSingles, &s.IgnoreCompilations, &s.IgnoreLive, &s.IgnoreRemix, &s.Monitored, &lastSynced); err != nil {
|
|
return nil, fmt.Errorf("scan artist settings: %w", err)
|
|
}
|
|
s.MBID = mbid.String
|
|
if lastSynced.Valid {
|
|
s.LastSynced = lastSynced.Time
|
|
}
|
|
results = append(results, s)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("iterate artist settings: %w", err)
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// UpdateArtistSettings updates specific fields of an artist_settings row by ID.
|
|
// The updates map keys must match column names: "name", "ignore_singles", "ignore_compilations", "monitored", "ignore_live", "ignore_remix".
|
|
func UpdateArtistSettings(db *DB, id string, updates map[string]interface{}) error {
|
|
if len(updates) == 0 {
|
|
return fmt.Errorf("no updates provided")
|
|
}
|
|
|
|
// Build the query using a fixed set of allowed columns to avoid dynamic SQL.
|
|
const baseQuery = "UPDATE artist_settings SET"
|
|
|
|
var args []interface{}
|
|
setClause := ""
|
|
for col, val := range updates {
|
|
switch col {
|
|
case "name":
|
|
if setClause != "" {
|
|
setClause += ", "
|
|
}
|
|
setClause += "name = ?"
|
|
args = append(args, val)
|
|
case "mbid":
|
|
if setClause != "" {
|
|
setClause += ", "
|
|
}
|
|
setClause += "mbid = ?"
|
|
args = append(args, val)
|
|
case "ignore_singles":
|
|
if setClause != "" {
|
|
setClause += ", "
|
|
}
|
|
setClause += "ignore_singles = ?"
|
|
args = append(args, val)
|
|
case "ignore_compilations":
|
|
if setClause != "" {
|
|
setClause += ", "
|
|
}
|
|
setClause += "ignore_compilations = ?"
|
|
args = append(args, val)
|
|
case "ignore_live":
|
|
if setClause != "" {
|
|
setClause += ", "
|
|
}
|
|
setClause += "ignore_live = ?"
|
|
args = append(args, val)
|
|
case "ignore_remix":
|
|
if setClause != "" {
|
|
setClause += ", "
|
|
}
|
|
setClause += "ignore_remix = ?"
|
|
args = append(args, val)
|
|
case "monitored":
|
|
if setClause != "" {
|
|
setClause += ", "
|
|
}
|
|
setClause += "monitored = ?"
|
|
args = append(args, val)
|
|
default:
|
|
return fmt.Errorf("unknown column: %s", col)
|
|
}
|
|
}
|
|
|
|
args = append(args, id)
|
|
query := fmt.Sprintf("%s %s WHERE id = ?", baseQuery, setClause)
|
|
result, err := db.Conn().Exec(query, args...)
|
|
if err != nil {
|
|
return fmt.Errorf("update artist settings: %w", err)
|
|
}
|
|
|
|
rowsAffected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("rows affected: %w", err)
|
|
}
|
|
if rowsAffected == 0 {
|
|
return fmt.Errorf("artist not found: %s", id)
|
|
}
|
|
|
|
return nil
|
|
}
|