fix: address code review findings
- Preserve MBID and last_synced across periodic artist syncs (INSERT OR REPLACE was wiping them, forcing MusicBrainz re-resolution every cycle) - Exclude unmonitored artists from unnotified-release digest query - Require telegram.cron_schedule when telegram.enabled is true - Add same-origin CSRF defense to state-changing web POST routes - Skip WAL/busy_timeout pragmas for :memory: databases (no-op there) - Scan mbid as sql.NullString in GetAllArtistSettings to tolerate NULLs
This commit is contained in:
@@ -52,11 +52,24 @@ func TouchArtistSynced(db DBer, artistID string, syncedAt time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveArtistSettings inserts or replaces an artist_settings row.
|
||||
// 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 OR REPLACE INTO artist_settings (id, name, mbid, ignore_singles, ignore_compilations, monitored) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
settings.ID, settings.Name, settings.MBID, settings.IgnoreSingles, settings.IgnoreCompilations, settings.Monitored,
|
||||
_, err := db.Conn().Exec(`
|
||||
INSERT INTO artist_settings (id, name, mbid, ignore_singles, ignore_compilations, monitored, last_synced)
|
||||
VALUES (?, ?, ?, ?, ?, ?, (SELECT last_synced FROM artist_settings WHERE id = ?))
|
||||
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,
|
||||
monitored = excluded.monitored
|
||||
`,
|
||||
settings.ID, settings.Name, nullIfEmpty(settings.MBID), settings.IgnoreSingles, settings.IgnoreCompilations, settings.Monitored, settings.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("save artist settings: %w", err)
|
||||
@@ -64,6 +77,15 @@ func SaveArtistSettings(db *DB, settings *ArtistSettings) error {
|
||||
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
|
||||
}
|
||||
|
||||
// GetAllArtistSettings returns all rows from artist_settings.
|
||||
func GetAllArtistSettings(db *DB) ([]ArtistSettings, error) {
|
||||
rows, err := db.Conn().Query(
|
||||
@@ -77,9 +99,11 @@ func GetAllArtistSettings(db *DB) ([]ArtistSettings, error) {
|
||||
var results []ArtistSettings
|
||||
for rows.Next() {
|
||||
var s ArtistSettings
|
||||
if err := rows.Scan(&s.ID, &s.Name, &s.MBID, &s.IgnoreSingles, &s.IgnoreCompilations, &s.Monitored); err != nil {
|
||||
var mbid sql.NullString
|
||||
if err := rows.Scan(&s.ID, &s.Name, &mbid, &s.IgnoreSingles, &s.IgnoreCompilations, &s.Monitored); err != nil {
|
||||
return nil, fmt.Errorf("scan artist settings: %w", err)
|
||||
}
|
||||
s.MBID = mbid.String
|
||||
results = append(results, s)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
|
||||
@@ -51,18 +51,20 @@ func New(dbPath string) (*DB, error) {
|
||||
}
|
||||
if dbPath == ":memory:" {
|
||||
conn.SetMaxOpenConns(1)
|
||||
}
|
||||
} else {
|
||||
// Enable WAL mode for better concurrent read performance. WAL is a
|
||||
// no-op on in-memory databases (they always use the MEMORY journal), so
|
||||
// skip it there to avoid misleading configuration.
|
||||
if _, err := conn.Exec("PRAGMA journal_mode=WAL"); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("set WAL mode: %w", err)
|
||||
}
|
||||
|
||||
// Enable WAL mode for better concurrent read performance.
|
||||
if _, err := conn.Exec("PRAGMA journal_mode=WAL"); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("set WAL mode: %w", err)
|
||||
}
|
||||
|
||||
// Set busy timeout to handle concurrent write contention.
|
||||
if _, err := conn.Exec("PRAGMA busy_timeout=5000"); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("set busy timeout: %w", err)
|
||||
// Set busy timeout to handle concurrent write contention.
|
||||
if _, err := conn.Exec("PRAGMA busy_timeout=5000"); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("set busy timeout: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
db := &DB{conn: conn}
|
||||
|
||||
@@ -28,13 +28,16 @@ func IsNotificationSent(db *DB, rgid string) (bool, error) {
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// GetUnnotifiedReleases returns all external_release rows that have no entry in notifications_sent.
|
||||
// GetUnnotifiedReleases returns all external_release rows for monitored artists
|
||||
// that have no entry in notifications_sent. Releases belonging to unmonitored
|
||||
// artists are excluded so the digest honors the monitoring contract.
|
||||
func GetUnnotifiedReleases(db *DB) ([]ExternalRelease, error) {
|
||||
rows, err := db.Conn().Query(`
|
||||
SELECT e.rgid, e.artist_id, e.title, e.type, e.release_date, e.is_ignored
|
||||
FROM external_releases e
|
||||
JOIN artist_settings s ON e.artist_id = s.id
|
||||
LEFT JOIN notifications_sent n ON e.rgid = n.rgid
|
||||
WHERE n.rgid IS NULL AND e.is_ignored = 0
|
||||
WHERE s.monitored = 1 AND n.rgid IS NULL AND e.is_ignored = 0
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query unnotified releases: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user