- Tags déclaratifs (max 10, 30 chars) via track_tags + tags - Genres normalisés (max 3) via track_genres + taxonomy - GET /api/v1/discover/genre/:genre, tag/:tag (browse chrono) - POST/DELETE follow genre/tag - Section feed "Nouvelles sorties dans vos genres" - Track update: SyncTrackTags, SyncTrackGenres via discover service - Frontend: discoverService, FeedPage by_genres, DiscoverPage - Migration 126_tags_genres_discover - MSW handlers for discover
93 lines
2.7 KiB
Go
93 lines
2.7 KiB
Go
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
|
|
}
|