package database import ( "database/sql" "fmt" "strings" "time" _ "github.com/mattn/go-sqlite3" ) // joinSecondaryTypes renders a slice of secondary types as a comma-separated // string for storage in the secondary_types TEXT column (empty when none). func joinSecondaryTypes(types []string) string { return strings.Join(types, ",") } // splitSecondaryTypes parses the comma-separated secondary_types column back // into a slice. A NULL/empty column yields an empty slice. func splitSecondaryTypes(s string) []string { if s == "" { return nil } parts := strings.Split(s, ",") out := make([]string, 0, len(parts)) for _, p := range parts { if p != "" { out = append(out, p) } } return out } // GetExternalRelease retrieves an external_release row by RGID. func GetExternalRelease(db *DB, rgid string) (*ExternalRelease, error) { var r ExternalRelease var cachedAt sql.NullTime var secondaryTypes sql.NullString err := db.Conn().QueryRow( "SELECT rgid, artist_id, title, type, release_date, is_ignored, cached_at, secondary_types FROM external_releases WHERE rgid = ?", rgid, ).Scan(&r.RGID, &r.ArtistID, &r.Title, &r.Type, &r.ReleaseDate, &r.IsIgnored, &cachedAt, &secondaryTypes) if err != nil { return nil, fmt.Errorf("get external release: %w", err) } if cachedAt.Valid { r.CachedAt = cachedAt.Time } if secondaryTypes.Valid { r.SecondaryTypes = splitSecondaryTypes(secondaryTypes.String) } return &r, nil } // SaveExternalRelease inserts or replaces an external_release row. func SaveExternalRelease(db *DB, release *ExternalRelease) error { var cachedAt interface{} if !release.CachedAt.IsZero() { cachedAt = release.CachedAt } _, err := db.Conn().Exec( "INSERT OR REPLACE INTO external_releases (rgid, artist_id, title, type, release_date, is_ignored, cached_at, secondary_types) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", release.RGID, release.ArtistID, release.Title, release.Type, release.ReleaseDate, release.IsIgnored, cachedAt, joinSecondaryTypes(release.SecondaryTypes), ) if err != nil { return fmt.Errorf("save external release: %w", err) } return nil } // GetExternalReleasesByArtist returns all external_release rows for a given artist_id. func GetExternalReleasesByArtist(db *DB, artistID string) ([]ExternalRelease, error) { rows, err := db.Conn().Query( "SELECT rgid, artist_id, title, type, release_date, is_ignored, cached_at, secondary_types FROM external_releases WHERE artist_id = ?", artistID, ) if err != nil { return nil, fmt.Errorf("query external releases by artist: %w", err) } defer rows.Close() var results []ExternalRelease for rows.Next() { var r ExternalRelease var cachedAt sql.NullTime var secondaryTypes sql.NullString if err := rows.Scan(&r.RGID, &r.ArtistID, &r.Title, &r.Type, &r.ReleaseDate, &r.IsIgnored, &cachedAt, &secondaryTypes); err != nil { return nil, fmt.Errorf("scan external release: %w", err) } if cachedAt.Valid { r.CachedAt = cachedAt.Time } if secondaryTypes.Valid { r.SecondaryTypes = splitSecondaryTypes(secondaryTypes.String) } results = append(results, r) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("iterate external releases: %w", err) } return results, nil } // GetIgnoredReleases returns all external_release rows where is_ignored = 1. func GetIgnoredReleases(db *DB) ([]ExternalRelease, error) { rows, err := db.Conn().Query( "SELECT rgid, artist_id, title, type, release_date, is_ignored, cached_at, secondary_types FROM external_releases WHERE is_ignored = 1", ) if err != nil { return nil, fmt.Errorf("query ignored releases: %w", err) } defer rows.Close() var results []ExternalRelease for rows.Next() { var r ExternalRelease var cachedAt sql.NullTime var secondaryTypes sql.NullString if err := rows.Scan(&r.RGID, &r.ArtistID, &r.Title, &r.Type, &r.ReleaseDate, &r.IsIgnored, &cachedAt, &secondaryTypes); err != nil { return nil, fmt.Errorf("scan ignored release: %w", err) } if cachedAt.Valid { r.CachedAt = cachedAt.Time } if secondaryTypes.Valid { r.SecondaryTypes = splitSecondaryTypes(secondaryTypes.String) } results = append(results, r) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("iterate ignored releases: %w", err) } return results, nil } // SetReleaseIgnored updates the is_ignored flag for a given RGID. func SetReleaseIgnored(db *DB, rgid string, ignored bool) error { result, err := db.Conn().Exec( "UPDATE external_releases SET is_ignored = ? WHERE rgid = ?", ignored, rgid, ) if err != nil { return fmt.Errorf("set release ignored: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("rows affected: %w", err) } if rowsAffected == 0 { return fmt.Errorf("release not found: %s", rgid) } return nil } // GetExternalReleasesByArtistWithCache returns cached external_release rows for a given artist_id // that are within the specified TTL. func GetExternalReleasesByArtistWithCache(db *DB, artistID string, ttl time.Duration) ([]ExternalRelease, error) { // cached_at is a TEXT DATETIME column serialized by the driver in the // "2006-01-02 15:04:05" UTC layout. Compare against an explicitly // formatted cutoff string in the same layout so the lexicographic // comparison does not depend on the driver's time serialization behavior. const layout = "2006-01-02 15:04:05" cutoff := time.Now().UTC().Add(-ttl).Format(layout) rows, err := db.Conn().Query( "SELECT rgid, artist_id, title, type, release_date, is_ignored, cached_at, secondary_types FROM external_releases WHERE artist_id = ? AND cached_at >= ?", artistID, cutoff, ) if err != nil { return nil, fmt.Errorf("query cached external releases: %w", err) } defer rows.Close() var results []ExternalRelease for rows.Next() { var r ExternalRelease var releaseType sql.NullString var releaseDate sql.NullString var cachedAt sql.NullTime var secondaryTypes sql.NullString if err := rows.Scan(&r.RGID, &r.ArtistID, &r.Title, &releaseType, &releaseDate, &r.IsIgnored, &cachedAt, &secondaryTypes); err != nil { return nil, fmt.Errorf("scan cached external release: %w", err) } if releaseType.Valid { r.Type = releaseType.String } if releaseDate.Valid { r.ReleaseDate = releaseDate.String } if cachedAt.Valid { r.CachedAt = cachedAt.Time } if secondaryTypes.Valid { r.SecondaryTypes = splitSecondaryTypes(secondaryTypes.String) } results = append(results, r) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("iterate cached external releases: %w", err) } return results, nil }