persona-community-5/services/persona-api/internal/adapter/memory/album.go
jordan f5f3229364
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Add components: service/persona-api, worker/media-worker, app-react/creator-ui
2026-02-24 07:40:04 +00:00

178 lines
5.1 KiB
Go

package memory
import (
"context"
"fmt"
"sync"
"time"
"git.threesix.ai/jordan/persona-community-5/pkg/album"
)
// AlbumRepository is an in-memory implementation of port.AlbumRepository.
// Used in standalone dev mode (no DATABASE_URL). Not safe for persistence across restarts.
type AlbumRepository struct {
mu sync.RWMutex
albums map[album.AlbumID]*album.Album
}
// NewAlbumRepository creates an in-memory album repository.
func NewAlbumRepository() *AlbumRepository {
return &AlbumRepository{
albums: make(map[album.AlbumID]*album.Album),
}
}
// Create persists a new album. The caller must set ID, Name, SubjectDesc, Shots before calling.
func (r *AlbumRepository) Create(ctx context.Context, a *album.Album) error {
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now().UTC()
a.CreatedAt = now
a.UpdatedAt = now
copy := *a
r.albums[a.ID] = &copy
return nil
}
// Get returns an album by ID and userID. Returns error if not found or wrong user.
func (r *AlbumRepository) Get(ctx context.Context, id album.AlbumID, userID string) (*album.Album, error) {
r.mu.RLock()
defer r.mu.RUnlock()
a, ok := r.albums[id]
if !ok || a.UserID != userID {
return nil, fmt.Errorf("album not found: %s", id)
}
copy := *a
shots := make([]album.Shot, len(a.Shots))
copy.Shots = shots
for i, s := range a.Shots {
shots[i] = s
}
return &copy, nil
}
// List returns all albums for a user, ordered by CreatedAt DESC.
func (r *AlbumRepository) List(ctx context.Context, userID string) ([]album.Album, error) {
r.mu.RLock()
defer r.mu.RUnlock()
var result []album.Album
for _, a := range r.albums {
if a.UserID != userID {
continue
}
copy := *a
shots := make([]album.Shot, len(a.Shots))
for i, s := range a.Shots {
shots[i] = s
}
copy.Shots = shots
result = append(result, copy)
}
// Sort by CreatedAt DESC (simple insertion sort — in-memory is small).
for i := 1; i < len(result); i++ {
for j := i; j > 0 && result[j].CreatedAt.After(result[j-1].CreatedAt); j-- {
result[j], result[j-1] = result[j-1], result[j]
}
}
return result, nil
}
// Delete removes an album by ID and userID.
func (r *AlbumRepository) Delete(ctx context.Context, id album.AlbumID, userID string) error {
r.mu.Lock()
defer r.mu.Unlock()
a, ok := r.albums[id]
if !ok || a.UserID != userID {
return fmt.Errorf("album not found: %s", id)
}
delete(r.albums, id)
return nil
}
// UpdateAnchor stores the generated anchor URL.
func (r *AlbumRepository) UpdateAnchor(ctx context.Context, id album.AlbumID, userID, anchorURL, anchorJobID string) error {
r.mu.Lock()
defer r.mu.Unlock()
a, ok := r.albums[id]
if !ok || a.UserID != userID {
return fmt.Errorf("album not found: %s", id)
}
a.AnchorURL = anchorURL
a.AnchorJobID = anchorJobID
a.UpdatedAt = time.Now().UTC()
return nil
}
// UpdateShot stores the generated image URL and status for a specific shot.
func (r *AlbumRepository) UpdateShot(ctx context.Context, id album.AlbumID, userID string, shotIndex int, imageURL string, status album.ShotStatus, shotError string) error {
r.mu.Lock()
defer r.mu.Unlock()
a, ok := r.albums[id]
if !ok || a.UserID != userID {
return fmt.Errorf("album not found: %s", id)
}
if shotIndex < 0 || shotIndex >= len(a.Shots) {
return fmt.Errorf("shot index out of range: %d", shotIndex)
}
now := time.Now().UTC()
a.Shots[shotIndex].ImageURL = imageURL
a.Shots[shotIndex].Status = status
a.Shots[shotIndex].Error = shotError
if status == album.ShotComplete {
a.Shots[shotIndex].GeneratedAt = &now
}
a.UpdatedAt = now
return nil
}
// ResetShot clears a shot back to pending.
func (r *AlbumRepository) ResetShot(ctx context.Context, id album.AlbumID, userID string, shotIndex int) error {
r.mu.Lock()
defer r.mu.Unlock()
a, ok := r.albums[id]
if !ok || a.UserID != userID {
return fmt.Errorf("album not found: %s", id)
}
if shotIndex < 0 || shotIndex >= len(a.Shots) {
return fmt.Errorf("shot index out of range: %d", shotIndex)
}
a.Shots[shotIndex].ImageURL = ""
a.Shots[shotIndex].JobID = ""
a.Shots[shotIndex].Status = album.ShotPending
a.Shots[shotIndex].Error = ""
a.Shots[shotIndex].GeneratedAt = nil
a.UpdatedAt = time.Now().UTC()
return nil
}
// UpdateAnchorJobID stores the anchor job ID when the anchor generation is enqueued.
func (r *AlbumRepository) UpdateAnchorJobID(ctx context.Context, id album.AlbumID, userID, jobID string) error {
r.mu.Lock()
defer r.mu.Unlock()
a, ok := r.albums[id]
if !ok || a.UserID != userID {
return fmt.Errorf("album not found: %s", id)
}
a.AnchorJobID = jobID
a.UpdatedAt = time.Now().UTC()
return nil
}
// UpdateShotJobID stores the job ID for a shot when its generation is enqueued.
func (r *AlbumRepository) UpdateShotJobID(ctx context.Context, id album.AlbumID, userID string, shotIndex int, jobID string) error {
r.mu.Lock()
defer r.mu.Unlock()
a, ok := r.albums[id]
if !ok || a.UserID != userID {
return fmt.Errorf("album not found: %s", id)
}
if shotIndex < 0 || shotIndex >= len(a.Shots) {
return fmt.Errorf("shot index out of range: %d", shotIndex)
}
a.Shots[shotIndex].JobID = jobID
a.Shots[shotIndex].Status = album.ShotGenerating
a.UpdatedAt = time.Now().UTC()
return nil
}