package db import ( "database/sql" "errors" ) func (s *Store) UpsertSource(item Source) (Source, error) { now := Now() if item.SourceID == "" { item.SourceID = item.CategoryID + "-" + item.Name } if item.Method == "" { item.Method = "GET" } item.ProxyMode = normalizeProxyMode(firstNonEmpty(item.ProxyMode, "client_direct"), item.CategoryID, item.Name, item.APIURL) if item.URLTemplate == "" { item.URLTemplate = item.APIURL } if item.TimeoutMS <= 0 { item.TimeoutMS = 8000 } if item.RetryCount <= 0 { item.RetryCount = 1 } if item.CacheSeconds <= 0 { item.CacheSeconds = item.CheckIntervalSec } if item.CacheSeconds <= 0 { item.CacheSeconds = 300 } if item.CheckIntervalSec <= 0 { item.CheckIntervalSec = item.CacheSeconds } if item.SupportedFormats == "" { item.SupportedFormats = "[]" } if item.LastStatus == "" { item.LastStatus = "unknown" } if item.CategoryID == "" { item.CategoryID = "custom" } if item.CategoryName == "" { item.CategoryName = item.CategoryID } _, _ = s.exec(`INSERT INTO source_categories (category_id, name, enabled, ui_config, created_at, updated_at) VALUES (?, ?, 1, '{}', ?, ?) ON CONFLICT (category_id) DO UPDATE SET name = excluded.name, updated_at = excluded.updated_at`, item.CategoryID, item.CategoryName, now, now) conn, d := s.active() query := d.upsert("source_endpoints", []string{"category_id", "category_name", "source_id", "name", "description", "method", "api_url", "url_template", "thumbnail_url", "proxy_mode", "timeout_ms", "retry_count", "cache_seconds", "check_interval_sec", "enabled", "client_visible", "supported_formats", "last_status", "last_latency_ms", "last_checked_at", "last_error", "consecutive_failure", "created_at", "updated_at"}, []string{"source_id"}) if _, err := conn.Exec(d.rebind(query), item.CategoryID, item.CategoryName, item.SourceID, item.Name, item.Description, item.Method, item.APIURL, item.URLTemplate, item.ThumbnailURL, item.ProxyMode, item.TimeoutMS, item.RetryCount, item.CacheSeconds, item.CheckIntervalSec, boolInt(item.Enabled), boolInt(item.ClientVisible), item.SupportedFormats, item.LastStatus, item.LastLatencyMS, item.LastCheckedAt, item.LastError, item.ConsecutiveFailure, firstNonEmpty(item.CreatedAt, now), now); err != nil { s.markFailover(err) return Source{}, err } return s.GetSourceBySourceID(item.SourceID) } func (s *Store) GetSourceBySourceID(sourceID string) (Source, error) { item, err := scanSourceRow(s.queryRow(sourceSelectSQL()+` WHERE source_id = ?`, sourceID)) if errors.Is(err, sql.ErrNoRows) { return Source{}, errors.New("source not found") } return item, err } func (s *Store) ListSources(includeHidden bool) ([]Source, error) { where := "" args := []any{} if !includeHidden { where = " WHERE enabled = 1 AND client_visible = 1" } rows, err := s.query(sourceSelectSQL()+where+` ORDER BY category_id ASC, name ASC`, args...) if err != nil { return nil, err } defer rows.Close() items := []Source{} for rows.Next() { item, err := scanSourceRowsCurrent(rows) if err != nil { return nil, err } items = append(items, item) } return items, rows.Err() } func (s *Store) CountSources() (int, error) { var count int err := s.queryRow(`SELECT COUNT(*) FROM source_endpoints`).Scan(&count) return count, err } func (s *Store) DeleteSource(sourceID string) error { _, err := s.exec(`DELETE FROM source_endpoints WHERE source_id = ?`, sourceID) return err } func (s *Store) RecordSourceCheck(sourceDBID int64, status string, latency int, message string) error { now := Now() _, err := s.exec(`INSERT INTO endpoint_health_checks (source_db_id, status, latency_ms, error, checked_at) VALUES (?, ?, ?, ?, ?)`, sourceDBID, status, latency, sanitize(message), now) if err != nil { return err } if status == "ok" { _, err = s.exec(`UPDATE source_endpoints SET last_status = ?, last_latency_ms = ?, last_checked_at = ?, last_error = '', consecutive_failure = 0, updated_at = ? WHERE id = ?`, status, latency, now, now, sourceDBID) } else if status == "redirected" { _, err = s.exec(`UPDATE source_endpoints SET last_status = ?, last_latency_ms = ?, last_checked_at = ?, last_error = ?, consecutive_failure = 0, updated_at = ? WHERE id = ?`, status, latency, now, sanitize(message), now, sourceDBID) } else { _, err = s.exec(`UPDATE source_endpoints SET last_status = ?, last_latency_ms = ?, last_checked_at = ?, last_error = ?, consecutive_failure = consecutive_failure + 1, updated_at = ? WHERE id = ?`, status, latency, now, sanitize(message), now, sourceDBID) } return err } func (s *Store) RecordSourceCall(call SourceCall) error { if call.CreatedAt == "" { call.CreatedAt = Now() } _, err := s.exec(`INSERT INTO endpoint_call_logs (source_id, status, latency_ms, error, client, created_at) VALUES (?, ?, ?, ?, ?, ?)`, sanitize(call.SourceID), sanitize(call.Status), call.LatencyMS, sanitize(call.Error), sanitize(call.Client), call.CreatedAt) return err }