This commit is contained in:
2026-10-07 01:43:28 +07:00
parent 494b3fc471
commit e0caa0b409
23 changed files with 5264 additions and 963 deletions
@@ -146,3 +146,8 @@ func (s *FileService) DownloadFile(
}
return file, nil
}
// DeleteFile removes a stored file.
func (s *FileService) DeleteFile(ctx context.Context, filename string) error {
return s.fileStorage.Delete(ctx, filename)
}
@@ -0,0 +1,37 @@
package playlist_service
import "evening_detective_server/internal/repos/playlists_repo"
func mapPlaylist(value *playlists_repo.Playlist) *Playlist {
if value == nil {
return nil
}
result := &Playlist{ID: value.ID, Name: value.Name}
for _, track := range value.Tracks {
result.Tracks = append(result.Tracks, &Track{
ID: track.ID,
Name: track.Name,
Filename: track.Filename,
Position: track.Position,
})
}
return result
}
func mapMusicState(value *playlists_repo.MusicState) *MusicState {
if value == nil {
return nil
}
return &MusicState{
Playlist: mapPlaylist(value.Playlist),
TrackIndex: value.TrackIndex,
PositionMs: value.PositionMs,
Volume: value.Volume,
Loop: value.Loop,
Playback: value.Playback,
Version: value.Version,
StartedAt: value.StartedAt,
ScenarioID: value.ScenarioID,
GameID: value.GameID,
}
}
@@ -0,0 +1,32 @@
package playlist_service
import "time"
// Track describes an MP3 track in a playlist.
type Track struct {
ID int
Name string
Filename string
Position int
}
// Playlist describes a reusable playlist.
type Playlist struct {
ID int
Name string
Tracks []*Track
}
// MusicState describes the synchronized music state of a game.
type MusicState struct {
Playlist *Playlist
TrackIndex int
PositionMs int64
Volume int32
Loop bool
Playback int32
Version int64
StartedAt *time.Time
ScenarioID *int
GameID *int
}
@@ -0,0 +1,166 @@
package playlist_service
import (
"context"
"errors"
"strings"
"evening_detective_server/internal/modules/file_storage"
"evening_detective_server/internal/repos/playlists_repo"
"evening_detective_server/internal/services/file_service"
)
var (
ErrInvalidName = errors.New("некорректное имя плейлиста")
ErrInvalidTrack = errors.New("некорректный трек")
ErrPlaylistNotFound = errors.New("плейлист не найден")
ErrPlaylistInUse = errors.New("плейлист используется")
ErrTrackNotFound = errors.New("трек не найден")
ErrGameNotFound = errors.New("игра не найдена")
ErrScenarioNotFound = errors.New("сценарий не найден")
)
// Service implements playlist and game music use cases.
type Service struct {
repo *playlists_repo.Repo
files *file_service.FileService
}
// New creates a playlist service.
func New(repo *playlists_repo.Repo, files *file_service.FileService) *Service {
return &Service{repo: repo, files: files}
}
// List returns all playlists.
func (s *Service) List(ctx context.Context) ([]*Playlist, error) {
items, err := s.repo.List(ctx)
if err != nil {
return nil, mapError(err)
}
result := make([]*Playlist, 0, len(items))
for _, item := range items {
result = append(result, mapPlaylist(item))
}
return result, nil
}
// Get returns a playlist by ID.
func (s *Service) Get(ctx context.Context, id int) (*Playlist, error) {
item, err := s.repo.Get(ctx, id)
if err != nil {
return nil, mapError(err)
}
return mapPlaylist(item), nil
}
// Create validates and creates a playlist.
func (s *Service) Create(ctx context.Context, name string) (*Playlist, error) {
name = strings.TrimSpace(name)
if name == "" || len(name) > 255 {
return nil, ErrInvalidName
}
item, err := s.repo.Create(ctx, name)
if err != nil {
return nil, err
}
return mapPlaylist(item), nil
}
// Update validates and updates a playlist.
func (s *Service) Update(ctx context.Context, id int, name string) (*Playlist, error) {
name = strings.TrimSpace(name)
if id <= 0 || name == "" || len(name) > 255 {
return nil, ErrInvalidName
}
item, err := s.repo.Update(ctx, id, name)
if err != nil {
return nil, mapError(err)
}
return mapPlaylist(item), nil
}
// Delete removes a playlist.
func (s *Service) Delete(ctx context.Context, id int) error {
return mapError(s.repo.Delete(ctx, id))
}
// UploadTrack stores an MP3 file and adds it to a playlist.
func (s *Service) UploadTrack(ctx context.Context, playlistID int, name, filename string, data []byte) error {
if playlistID <= 0 || strings.TrimSpace(name) == "" || len(data) == 0 || !strings.HasSuffix(strings.ToLower(filename), ".mp3") {
return ErrInvalidTrack
}
if _, err := s.repo.Get(ctx, playlistID); err != nil {
return mapError(err)
}
stored, fileType, err := s.files.UploadFile(ctx, filename, data)
if err != nil || fileType != "audio" || !strings.HasSuffix(strings.ToLower(stored), ".mp3") {
return ErrInvalidTrack
}
if err := s.repo.AddTrack(ctx, playlistID, strings.TrimSpace(name), stored); err != nil {
_ = s.files.DeleteFile(ctx, stored)
return mapError(err)
}
return nil
}
// DeleteTrack removes a track and its stored file.
func (s *Service) DeleteTrack(ctx context.Context, playlistID, trackID int) error {
filename, err := s.repo.DeleteTrack(ctx, playlistID, trackID)
if err != nil {
return mapError(err)
}
if err := s.files.DeleteFile(ctx, filename); err != nil && !errors.Is(err, file_storage.ErrFileNotFound) {
return err
}
return nil
}
// SetScenario assigns a playlist to a scenario.
func (s *Service) SetScenario(ctx context.Context, scenarioID, playlistID int) error {
return mapError(s.repo.SetScenario(ctx, scenarioID, playlistID))
}
// SetGame updates the music state for a game.
func (s *Service) SetGame(
ctx context.Context,
gameID, playlistID, trackIndex int,
positionMs int64,
volume int32,
loop bool,
playback int32,
) (*MusicState, error) {
if trackIndex < 0 || positionMs < 0 || volume < 0 || volume > 100 || playback < 1 || playback > 3 {
return nil, ErrInvalidTrack
}
state, err := s.repo.SetMusic(ctx, gameID, playlistID, trackIndex, positionMs, volume, loop, playback)
if err != nil {
return nil, mapError(err)
}
return mapMusicState(state), nil
}
// GetGame returns the music state for a game.
func (s *Service) GetGame(ctx context.Context, gameID int) (*MusicState, error) {
state, err := s.repo.GetMusic(ctx, gameID)
if err != nil {
return nil, mapError(err)
}
return mapMusicState(state), nil
}
func mapError(err error) error {
switch {
case errors.Is(err, playlists_repo.ErrPlaylistNotFound):
return ErrPlaylistNotFound
case errors.Is(err, playlists_repo.ErrPlaylistInUse):
return ErrPlaylistInUse
case errors.Is(err, playlists_repo.ErrTrackNotFound):
return ErrTrackNotFound
case errors.Is(err, playlists_repo.ErrGameNotFound):
return ErrGameNotFound
case errors.Is(err, playlists_repo.ErrScenarioNotFound):
return ErrScenarioNotFound
default:
return err
}
}
@@ -13,6 +13,7 @@ type Scenario struct {
Name string
Description *string
Image *string
PlaylistID *int
Story *storytelling.Story
Author *users_service.User
Status string
@@ -34,6 +34,7 @@ func MapScenarioLight(
Name: o.Name,
Description: o.Description,
Image: mapImage(o.Image, domain),
PlaylistID: o.PlaylistID,
Story: nil,
Author: mapAuthor(o.Author),
Status: o.Status,