package feed import ( "context" "encoding/base64" "fmt" "strconv" "strings" "time" "github.com/google/uuid" "go.uber.org/zap" "gorm.io/gorm" "veza-backend-api/internal/core/discover" "veza-backend-api/internal/models" ) // Service provides the chronological tracks feed from followed users (v0.10.0 F210) // v0.10.1: Optional by_genres section from discover service type Service struct { db *gorm.DB logger *zap.Logger discoverService *discover.Service } // NewService creates a new feed service func NewService(db *gorm.DB, logger *zap.Logger) *Service { return &Service{db: db, logger: logger} } // SetDiscoverService sets the discover service for by_genres section (v0.10.1 F355) func (s *Service) SetDiscoverService(d *discover.Service) { s.discoverService = d } // GetDiscoverService returns the discover service (for handler access) func (s *Service) GetDiscoverService() *discover.Service { return s.discoverService } // GetTracksFeed returns tracks from users that the viewer follows, chronological order, cursor pagination. // Only includes completed, public tracks. Requires userID (authenticated). func (s *Service) GetTracksFeed(ctx context.Context, userID uuid.UUID, limit int, cursor string) ([]*models.Track, string, error) { if limit <= 0 { limit = 20 } if limit > 50 { limit = 50 } query := s.db.WithContext(ctx).Model(&models.Track{}). Joins("INNER JOIN follows ON follows.followed_id = tracks.creator_id AND follows.follower_id = ?", userID). Where("tracks.status = ?", models.TrackStatusCompleted). Where("tracks.is_public = ?", true) var cursorCreatedAt int64 var cursorID uuid.UUID if cursor != "" { decoded, err := base64.RawURLEncoding.DecodeString(cursor) if err == nil { parts := strings.SplitN(string(decoded), "|", 2) if len(parts) == 2 { if ts, err := strconv.ParseInt(parts[0], 10, 64); err == nil { cursorCreatedAt = ts } if uid, err := uuid.Parse(parts[1]); err == nil { cursorID = uid } } } } if cursor != "" && (cursorCreatedAt != 0 || cursorID != uuid.Nil) { query = query.Where("(tracks.created_at, tracks.id) < (?, ?)", time.Unix(0, cursorCreatedAt), cursorID) } query = query.Order("tracks.created_at DESC, tracks.id DESC").Limit(limit + 1) var tracks []*models.Track if err := query.Preload("User").Find(&tracks).Error; err != nil { return nil, "", fmt.Errorf("failed to get feed tracks: %w", err) } var nextCursor string if len(tracks) > limit { last := tracks[limit-1] nextCursor = base64.RawURLEncoding.EncodeToString([]byte( fmt.Sprintf("%d|%s", last.CreatedAt.UnixNano(), last.ID.String()))) tracks = tracks[:limit] } return tracks, nextCursor, nil }