187 lines
6.0 KiB
Go
187 lines
6.0 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"git.threesix.ai/jordan/persona-community-3/pkg/album"
|
|
"git.threesix.ai/jordan/persona-community-3/pkg/logging"
|
|
"git.threesix.ai/jordan/persona-community-3/pkg/queue"
|
|
"git.threesix.ai/jordan/persona-community-3/services/persona-api/internal/port"
|
|
)
|
|
|
|
// AlbumService handles album creation, retrieval, and generation orchestration.
|
|
// All generation is async: service enqueues jobs and returns immediately.
|
|
// Results arrive via SSE on the user:<userId> channel.
|
|
type AlbumService struct {
|
|
albums port.AlbumRepository
|
|
queue queue.Producer
|
|
logger *logging.Logger
|
|
}
|
|
|
|
// NewAlbumService creates a new AlbumService.
|
|
func NewAlbumService(albums port.AlbumRepository, q queue.Producer, logger *logging.Logger) *AlbumService {
|
|
return &AlbumService{
|
|
albums: albums,
|
|
queue: q,
|
|
logger: logger.WithComponent("AlbumService"),
|
|
}
|
|
}
|
|
|
|
// Create creates a new album with the given shots and persists it.
|
|
// Shots are provided as ShotTemplate slices (Label + Direction).
|
|
func (s *AlbumService) Create(ctx context.Context, userID, name, subjectDesc string, shots []album.ShotTemplate) (*album.Album, error) {
|
|
if name == "" {
|
|
return nil, fmt.Errorf("album name is required")
|
|
}
|
|
if subjectDesc == "" {
|
|
return nil, fmt.Errorf("subject description is required")
|
|
}
|
|
if len(shots) == 0 {
|
|
return nil, fmt.Errorf("at least one shot is required")
|
|
}
|
|
if len(shots) > 20 {
|
|
return nil, fmt.Errorf("maximum 20 shots per album")
|
|
}
|
|
|
|
shotList := make([]album.Shot, len(shots))
|
|
for i, tmpl := range shots {
|
|
shotList[i] = album.Shot{
|
|
Index: i,
|
|
Label: tmpl.Label,
|
|
Direction: tmpl.Direction,
|
|
Status: album.ShotPending,
|
|
}
|
|
}
|
|
|
|
a := &album.Album{
|
|
ID: album.AlbumID("alb_" + generateID()),
|
|
UserID: userID,
|
|
Name: name,
|
|
SubjectDesc: subjectDesc,
|
|
Shots: shotList,
|
|
}
|
|
|
|
if err := s.albums.Create(ctx, a); err != nil {
|
|
return nil, fmt.Errorf("create album: %w", err)
|
|
}
|
|
|
|
s.logger.Info("album created", "album_id", string(a.ID), "user_id", userID, "shots", len(shotList))
|
|
return a, nil
|
|
}
|
|
|
|
// Get returns an album by ID, enforcing user ownership.
|
|
func (s *AlbumService) Get(ctx context.Context, id album.AlbumID, userID string) (*album.Album, error) {
|
|
a, err := s.albums.Get(ctx, id, userID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("album not found: %w", err)
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
// List returns all albums for a user.
|
|
func (s *AlbumService) List(ctx context.Context, userID string) ([]album.Album, error) {
|
|
return s.albums.List(ctx, userID)
|
|
}
|
|
|
|
// Delete removes an album. Does NOT delete stored images.
|
|
func (s *AlbumService) Delete(ctx context.Context, id album.AlbumID, userID string) error {
|
|
return s.albums.Delete(ctx, id, userID)
|
|
}
|
|
|
|
// GenerateAnchor enqueues an anchor generation job for an album.
|
|
// Returns the job ID. Result arrives via album_anchor_complete SSE event.
|
|
func (s *AlbumService) GenerateAnchor(ctx context.Context, id album.AlbumID, userID string) (string, error) {
|
|
a, err := s.albums.Get(ctx, id, userID)
|
|
if err != nil {
|
|
return "", fmt.Errorf("album not found: %w", err)
|
|
}
|
|
|
|
jobID, err := s.queue.Enqueue(ctx, "generate_anchor", map[string]any{
|
|
"albumId": string(a.ID),
|
|
"userId": userID,
|
|
"subjectDesc": a.SubjectDesc,
|
|
})
|
|
if err != nil {
|
|
return "", fmt.Errorf("enqueue anchor job: %w", err)
|
|
}
|
|
|
|
if err := s.albums.UpdateAnchorJobID(ctx, id, userID, jobID); err != nil {
|
|
s.logger.Warn("failed to persist anchor job ID", "error", err, "album_id", string(id))
|
|
}
|
|
|
|
s.logger.Info("anchor generation enqueued", "album_id", string(id), "job_id", jobID)
|
|
return jobID, nil
|
|
}
|
|
|
|
// GenerateAllShots enqueues generation jobs for all pending shots.
|
|
// Returns 422 if the album has no anchor yet (shots require an anchor reference).
|
|
func (s *AlbumService) GenerateAllShots(ctx context.Context, id album.AlbumID, userID string) ([]string, error) {
|
|
a, err := s.albums.Get(ctx, id, userID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("album not found: %w", err)
|
|
}
|
|
if a.AnchorURL == "" {
|
|
return nil, album.ErrAnchorRequired
|
|
}
|
|
|
|
var jobIDs []string
|
|
for _, shot := range a.Shots {
|
|
if shot.Status != album.ShotPending && shot.Status != album.ShotFailed {
|
|
continue
|
|
}
|
|
jobID, err := s.enqueueShotJob(ctx, a, shot.Index)
|
|
if err != nil {
|
|
s.logger.Error("failed to enqueue shot", "error", err, "album_id", string(id), "shot_index", shot.Index)
|
|
continue
|
|
}
|
|
jobIDs = append(jobIDs, jobID)
|
|
}
|
|
|
|
s.logger.Info("shots enqueued", "album_id", string(id), "count", len(jobIDs))
|
|
return jobIDs, nil
|
|
}
|
|
|
|
// GenerateShot enqueues a generation job for a single shot (for regeneration).
|
|
func (s *AlbumService) GenerateShot(ctx context.Context, id album.AlbumID, userID string, shotIndex int) (string, error) {
|
|
a, err := s.albums.Get(ctx, id, userID)
|
|
if err != nil {
|
|
return "", fmt.Errorf("album not found: %w", err)
|
|
}
|
|
if a.AnchorURL == "" {
|
|
return "", fmt.Errorf("anchor must be generated before shots")
|
|
}
|
|
if shotIndex < 0 || shotIndex >= len(a.Shots) {
|
|
return "", fmt.Errorf("shot index out of range: %d", shotIndex)
|
|
}
|
|
return s.enqueueShotJob(ctx, a, shotIndex)
|
|
}
|
|
|
|
// ResetShot clears a shot back to pending so it can be regenerated.
|
|
func (s *AlbumService) ResetShot(ctx context.Context, id album.AlbumID, userID string, shotIndex int) error {
|
|
return s.albums.ResetShot(ctx, id, userID, shotIndex)
|
|
}
|
|
|
|
// enqueueShotJob is the internal helper that enqueues a single shot generation job.
|
|
func (s *AlbumService) enqueueShotJob(ctx context.Context, a *album.Album, shotIndex int) (string, error) {
|
|
shot := a.Shots[shotIndex]
|
|
jobID, err := s.queue.Enqueue(ctx, "generate_shot", map[string]any{
|
|
"albumId": string(a.ID),
|
|
"userId": a.UserID,
|
|
"shotIndex": shotIndex,
|
|
"anchorUrl": a.AnchorURL,
|
|
"subjectDesc": a.SubjectDesc,
|
|
"direction": shot.Direction,
|
|
})
|
|
if err != nil {
|
|
return "", fmt.Errorf("enqueue shot job: %w", err)
|
|
}
|
|
|
|
if err := s.albums.UpdateShotJobID(ctx, a.ID, a.UserID, shotIndex, jobID); err != nil {
|
|
s.logger.Warn("failed to persist shot job ID", "error", err, "shot_index", shotIndex)
|
|
}
|
|
|
|
return jobID, nil
|
|
}
|
|
|