feat: implement Live/Remix filtering and add CI/CD pipeline
Some checks failed
Build and Push Docker Image / build (pull_request) Failing after 38s
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
This commit is contained in:
@@ -7,24 +7,18 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// DBer is the minimal query interface satisfied by both *sql.DB and *sql.Tx,
|
||||
// so callers can run statements inside or outside a transaction.
|
||||
type DBer interface {
|
||||
Exec(query string, args ...interface{}) (sql.Result, error)
|
||||
}
|
||||
|
||||
// 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
|
||||
s ArtistSettings
|
||||
mbid sql.NullString
|
||||
lastSynced sql.NullTime
|
||||
)
|
||||
err := db.Conn().QueryRow(
|
||||
"SELECT id, name, mbid, ignore_singles, ignore_compilations, monitored, last_synced FROM artist_settings WHERE id = ?",
|
||||
"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.Monitored, &lastSynced)
|
||||
).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
|
||||
@@ -60,17 +54,19 @@ func TouchArtistSynced(db DBer, artistID string, syncedAt time.Time) error {
|
||||
// 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, monitored, last_synced)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
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.Monitored, nullIfEmptyTime(settings.LastSynced),
|
||||
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)
|
||||
@@ -99,7 +95,7 @@ func nullIfEmptyTime(t time.Time) interface{} {
|
||||
// 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, monitored, last_synced FROM artist_settings",
|
||||
"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)
|
||||
@@ -111,7 +107,7 @@ func GetAllArtistSettings(db *DB) ([]ArtistSettings, error) {
|
||||
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.Monitored, &lastSynced); err != nil {
|
||||
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
|
||||
@@ -127,7 +123,7 @@ func GetAllArtistSettings(db *DB) ([]ArtistSettings, error) {
|
||||
}
|
||||
|
||||
// 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".
|
||||
// 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")
|
||||
@@ -164,6 +160,18 @@ func UpdateArtistSettings(db *DB, id string, updates map[string]interface{}) err
|
||||
}
|
||||
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 += ", "
|
||||
|
||||
@@ -15,6 +15,12 @@ type DB struct {
|
||||
conn *sql.DB
|
||||
}
|
||||
|
||||
// DBer is the minimal query interface satisfied by both *sql.DB and *sql.Tx,
|
||||
// so callers can run statements inside or outside a transaction.
|
||||
type DBer interface {
|
||||
Exec(query string, args ...interface{}) (sql.Result, error)
|
||||
}
|
||||
|
||||
// ErrArtistNotFound is returned by artist lookups when no row matches the given
|
||||
// ID. It is a sentinel so callers (e.g. the web UI) can distinguish "missing"
|
||||
// from other errors.
|
||||
@@ -164,6 +170,11 @@ func (db *DB) migrate() error {
|
||||
name: "009_add_last_synced_to_artist_settings",
|
||||
sql: `ALTER TABLE artist_settings ADD COLUMN last_synced DATETIME;`,
|
||||
},
|
||||
{
|
||||
name: "010_add_ignore_live_ignore_remix_to_artist_settings",
|
||||
sql: `ALTER TABLE artist_settings ADD COLUMN ignore_live BOOLEAN DEFAULT 0;
|
||||
ALTER TABLE artist_settings ADD COLUMN ignore_remix BOOLEAN DEFAULT 0;`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, m := range migrations {
|
||||
@@ -215,6 +226,8 @@ type ArtistSettings struct {
|
||||
MBID string `json:"mbid"`
|
||||
IgnoreSingles bool `json:"ignore_singles"`
|
||||
IgnoreCompilations bool `json:"ignore_compilations"`
|
||||
IgnoreLive bool `json:"ignore_live"`
|
||||
IgnoreRemix bool `json:"ignore_remix"`
|
||||
Monitored bool `json:"monitored"`
|
||||
LastSynced time.Time `json:"last_synced"`
|
||||
}
|
||||
|
||||
@@ -395,9 +395,9 @@ func TestSecondaryTypesRoundTrip(t *testing.T) {
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
in []string
|
||||
want []string
|
||||
name string
|
||||
in []string
|
||||
want []string
|
||||
}{
|
||||
{"empty", nil, nil},
|
||||
{"single", []string{"Compilation"}, []string{"Compilation"}},
|
||||
@@ -406,10 +406,10 @@ func TestSecondaryTypesRoundTrip(t *testing.T) {
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
r := &ExternalRelease{
|
||||
RGID: "rgid-" + c.name,
|
||||
ArtistID: "artist-1",
|
||||
Title: "Title " + c.name,
|
||||
Type: "Album",
|
||||
RGID: "rgid-" + c.name,
|
||||
ArtistID: "artist-1",
|
||||
Title: "Title " + c.name,
|
||||
Type: "Album",
|
||||
SecondaryTypes: c.in,
|
||||
}
|
||||
if err := SaveExternalRelease(db, r); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user