178 lines
5.1 KiB
Go
178 lines
5.1 KiB
Go
package memory
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.threesix.ai/jordan/persona-community-2/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] = ©
|
|
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 ©, 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
|
|
}
|