From 5b55fdc33efdd9cc369da4e67eecbc6bf0df69c9 Mon Sep 17 00:00:00 2001 From: Naoki Kishi Date: Wed, 5 Aug 2020 10:14:01 +0900 Subject: [PATCH] =?UTF-8?q?Revert=20"=E6=9B=B2=E3=81=AESkip=E3=81=AE?= =?UTF-8?q?=E3=81=9F=E3=82=81=E3=81=AEAPI=E3=82=92=E5=AE=9F=E8=A3=85?= =?UTF-8?q?=E3=81=97=E3=81=9F"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api.md | 30 -- domain/entity/session.go | 8 +- domain/entity/session_test.go | 7 - domain/entity/sync_check_timer.go | 99 ++---- domain/entity/sync_check_timer_test.go | 69 ++++- domain/mock_spotify/player.go | 26 +- domain/spotify/player.go | 3 +- spotify/player.go | 32 +- spotify/player_test.go | 6 +- usecase/session.go | 27 +- usecase/session_state.go | 117 +------- usecase/session_test.go | 13 +- usecase/session_timer.go | 207 +++++-------- usecase/session_timer_test.go | 354 +++++++--------------- web/handler/session.go | 28 -- web/handler/session_state_test.go | 2 +- web/handler/session_test.go | 401 +------------------------ web/router.go | 1 - 18 files changed, 322 insertions(+), 1108 deletions(-) diff --git a/docs/api.md b/docs/api.md index 67419d6d..ba8bfc33 100644 --- a/docs/api.md +++ b/docs/api.md @@ -256,36 +256,6 @@ X-CSRF-Token: relaym [Spotify APIの不思議な挙動](sotify_api_problem.md) -## PUT /sessions/:id/next - -### 概要 - -指定したセッションを一曲進めます。 - -### リクエスト - -空 - -### レスポンス - -空 - -| code | 補足 | -| ----- | -------- | -| 202 | | - -非同期的にレスポンスを返すので、実際に状態が反映されたかWebSocketのメッセージか別のAPIリクエストを通して取得する必要があります。 - -### エラー - -| code | message | 補足 | -| ---- | -------- | -------- | -| 400 | session is not allowed to control by others | 作成者以外によるstateの操作が許可されていない | -| 400 | requested state is not allowed | 許可されていないstateへの変更(許可されているstateの変更は[PRD](prd.md)を参照) | -| 400 | next queue track not found | 次のキューが無いので次の曲に遷移できない | -| 403 | active device not found | アクティブなデバイスが存在しないので操作ができない | -| 404 | session not found | 指定されたidのセッションが存在しない | - ## POST /sessions/:id/queue ### 概要 diff --git a/domain/entity/session.go b/domain/entity/session.go index 994c2050..ca1c70c3 100644 --- a/domain/entity/session.go +++ b/domain/entity/session.go @@ -71,7 +71,7 @@ func (s *Session) MoveToPlay() error { // MoveToPause はセッションのStateTypeをPauseに状態遷移します。 func (s *Session) MoveToPause() error { - if s.StateType == Play || s.StateType == Pause || s.StateType == Stop { + if s.StateType == Play || s.StateType == Pause { s.StateType = Pause return nil } @@ -97,7 +97,6 @@ func (s *Session) IsCreator(userID string) bool { // GoNextTrack 次の曲の状態に進めます。 func (s *Session) GoNextTrack() error { - s.SetProgressWhenPaused(0 * time.Second) if len(s.QueueTracks) <= s.QueueHead+1 { s.QueueHead++ // https://github.com/camphor-/relaym-server/blob/master/docs/definition.md#%E7%8F%BE%E5%9C%A8%E5%AF%BE%E8%B1%A1%E3%81%AE%E6%9B%B2%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9-head s.StateType = Stop @@ -186,11 +185,6 @@ func (s *Session) canMoveFromStopToPlay() error { return nil } -// IsNextTrackExistWhenStateIsStop はstateがstopの時に次の曲が存在するかを調べます -func (s *Session) IsNextTrackExistWhenStateIsStop() bool { - return len(s.QueueTracks) > s.QueueHead -} - // IsPlaying は現在のStateTypeがPlayかどうか返します。 func (s *Session) IsPlaying() bool { return s.StateType == Play diff --git a/domain/entity/session_test.go b/domain/entity/session_test.go index 40922ecd..d27ed45a 100644 --- a/domain/entity/session_test.go +++ b/domain/entity/session_test.go @@ -180,13 +180,6 @@ func TestSession_MoveToPause(t *testing.T) { session: &Session{ StateType: Stop, }, - wantErr: false, - }, - { - name: "Archived", - session: &Session{ - StateType: Archived, - }, wantErr: true, }, } diff --git a/domain/entity/sync_check_timer.go b/domain/entity/sync_check_timer.go index 976e6970..b023945b 100644 --- a/domain/entity/sync_check_timer.go +++ b/domain/entity/sync_check_timer.go @@ -1,7 +1,6 @@ package entity import ( - "fmt" "sync" "time" @@ -11,10 +10,8 @@ import ( // SyncCheckTimer はSpotifyとの同期チェック用のタイマーです。タイマーが止まったことを確認するためのstopチャネルがあります。 // ref : http://okzk.hatenablog.com/entry/2015/12/01/001924 type SyncCheckTimer struct { - timer *time.Timer - isTimerExpired bool - stopCh chan struct{} - nextCh chan struct{} + timer *time.Timer + stopCh chan struct{} } // ExpireCh は指定設定された秒数経過したことを送るチャネルを返します。 @@ -27,44 +24,13 @@ func (s *SyncCheckTimer) StopCh() <-chan struct{} { return s.stopCh } -// NextCh は次の曲への遷移の指示を送るチャネルを返します。 -func (s *SyncCheckTimer) NextCh() <-chan struct{} { - return s.nextCh -} - -// MakeIsTimerExpiredTrue はisTimerExpiredをtrueに変更します -// <- s.ExpireCh でtimerから値を受け取った際に呼び出してください -func (s *SyncCheckTimer) MakeIsTimerExpiredTrue() { - s.isTimerExpired = true -} - -// newSyncCheckTimer はSyncCheckTimerを作成します -// この段階ではtimerには空のtimerがセットされており、SetTimerを使用して正しいtimerのセットを行う必要があります -func newSyncCheckTimer() *SyncCheckTimer { - timer := time.NewTimer(0) - //Expiredしたtimerを作成する - if !timer.Stop() { - <-timer.C - } - +func newSyncCheckTimer(d time.Duration) *SyncCheckTimer { return &SyncCheckTimer{ - stopCh: make(chan struct{}, 2), - nextCh: make(chan struct{}, 1), - isTimerExpired: true, - timer: timer, + timer: time.NewTimer(d), + stopCh: make(chan struct{}, 2), } } -// SetTimerはSyncCheckTimerにTimerをセットします -func (s *SyncCheckTimer) SetDuration(d time.Duration) { - if !s.timer.Stop() && !s.isTimerExpired { - <-s.timer.C - } - - s.isTimerExpired = false - s.timer.Reset(d) -} - // SyncCheckTimerManager はSpotifyとの同期チェック用のタイマーを一括して管理する構造体です。 type SyncCheckTimerManager struct { timers map[string]*SyncCheckTimer @@ -78,9 +44,9 @@ func NewSyncCheckTimerManager() *SyncCheckTimerManager { } } -// CreateExpiredTimer は与えられたセッションの同期チェック用のタイマーを作成します。 +// CreateTimer は与えられたセッションの同期チェック用のタイマーを作成します。 // 既存のタイマーが存在する場合はstopしてから新しいタイマーを作成します。 -func (m *SyncCheckTimerManager) CreateExpiredTimer(sessionID string) *SyncCheckTimer { +func (m *SyncCheckTimerManager) CreateTimer(sessionID string, d time.Duration) *SyncCheckTimer { logger := log.New() m.mu.Lock() defer m.mu.Unlock() @@ -94,21 +60,23 @@ func (m *SyncCheckTimerManager) CreateExpiredTimer(sessionID string) *SyncCheckT existing.timer.Stop() close(existing.stopCh) } - timer := newSyncCheckTimer() + timer := newSyncCheckTimer(d) m.timers[sessionID] = timer return timer } -// DeleteTimer は与えられたセッションのタイマーをマップから削除します。 -// 既にタイマーがExpireして、そのチャネルの値を取り出してしまった後にマップから削除したいときに使います。 -func (m *SyncCheckTimerManager) DeleteTimer(sessionID string) { +// StopTimer は与えられたセッションのタイマーを終了します。 +func (m *SyncCheckTimerManager) StopTimer(sessionID string) { logger := log.New() m.mu.Lock() defer m.mu.Unlock() - logger.Debugj(map[string]interface{}{"message": "delete timer", "sessionID": sessionID}) + logger.Debugj(map[string]interface{}{"message": "stop timer", "sessionID": sessionID}) if timer, ok := m.timers[sessionID]; ok { + if !timer.timer.Stop() { + <-timer.timer.C + } close(timer.stopCh) delete(m.timers, sessionID) return @@ -117,44 +85,33 @@ func (m *SyncCheckTimerManager) DeleteTimer(sessionID string) { logger.Debugj(map[string]interface{}{"message": "timer not existed", "sessionID": sessionID}) } -// GetTimer は与えられたセッションのタイマーを取得します。存在しない場合はfalseが返ります。 -func (m *SyncCheckTimerManager) GetTimer(sessionID string) (*SyncCheckTimer, bool) { - m.mu.Lock() - defer m.mu.Unlock() - - if existing, ok := m.timers[sessionID]; ok { - return existing, true - } - return nil, false -} - -// SendToNextCh は与えられたセッションのタイマーのNextChに通知を送ります -func (m *SyncCheckTimerManager) SendToNextCh(sessionID string) error { +// DeleteTimer は与えられたセッションのタイマーをマップから削除します。 +// StopTimerと異なり、タイマーのストップ処理は行いません。 +// 既にタイマーがExpireして、そのチャネルの値を取り出してしまった後にマップから削除したいときに使います。 +// <-timer.timer.Cを呼ぶと無限に待ちが発生してしまいます。(値を取り出すことは一生出来ないので) +func (m *SyncCheckTimerManager) DeleteTimer(sessionID string) { logger := log.New() m.mu.Lock() defer m.mu.Unlock() - logger.Debugj(map[string]interface{}{"message": "call next ch", "sessionID": sessionID}) + logger.Debugj(map[string]interface{}{"message": "delete timer", "sessionID": sessionID}) if timer, ok := m.timers[sessionID]; ok { - timer.nextCh <- struct{}{} - return nil + close(timer.stopCh) + delete(m.timers, sessionID) + return } - logger.Debugj(map[string]interface{}{"message": "timer not existed on SendToNextCh", "sessionID": sessionID}) - return fmt.Errorf("timer not existed") + logger.Debugj(map[string]interface{}{"message": "timer not existed", "sessionID": sessionID}) } -// IsTimerExpired は与えられたセッションのisTimerExpiredの値を返します -func (m *SyncCheckTimerManager) IsTimerExpired(sessionID string) (bool, error) { - logger := log.New() +// GetTimer は与えられたセッションのタイマーを取得します。存在しない場合はfalseが返ります。 +func (m *SyncCheckTimerManager) GetTimer(sessionID string) (*SyncCheckTimer, bool) { m.mu.Lock() defer m.mu.Unlock() if existing, ok := m.timers[sessionID]; ok { - return existing.isTimerExpired, nil + return existing, true } - - logger.Debugj(map[string]interface{}{"message": "timer not existed on IsRemainDuration", "sessionID": sessionID}) - return false, fmt.Errorf("timer not existed") + return nil, false } diff --git a/domain/entity/sync_check_timer_test.go b/domain/entity/sync_check_timer_test.go index bb29a531..ef8151e8 100644 --- a/domain/entity/sync_check_timer_test.go +++ b/domain/entity/sync_check_timer_test.go @@ -82,11 +82,10 @@ func TestSyncCheckTimer_StopCh(t *testing.T) { } } -func TestSyncCheckTimerManager_CreateExpiredTimer(t *testing.T) { +func TestSyncCheckTimerManager_CreateTimer(t *testing.T) { t.Parallel() - timer := newSyncCheckTimer() - timer.SetDuration(time.Second) + timer := newSyncCheckTimer(time.Second) tests := []struct { name string @@ -130,11 +129,67 @@ func TestSyncCheckTimerManager_CreateExpiredTimer(t *testing.T) { return } opts := []cmp.Option{cmp.AllowUnexported(SyncCheckTimer{}), cmpopts.IgnoreUnexported(time.Timer{})} - got := m.CreateExpiredTimer(tt.sessionID) - got.SetDuration(tt.d) - if !cmp.Equal(got, tt.want, opts...) { - t.Errorf("CreateExpiredTimer() diff=%v", cmp.Diff(tt.want, got, opts...)) + if got := m.CreateTimer(tt.sessionID, tt.d); !cmp.Equal(got, tt.want, opts...) { + t.Errorf("CreateTimer() diff=%v", cmp.Diff(tt.want, got, opts...)) + } + }) + } +} + +func TestSyncCheckTimerManager_StopTimer(t *testing.T) { + t.Parallel() + + timer := newSyncCheckTimer(time.Second) + timerForNotFound := newSyncCheckTimer(time.Second) + + tests := []struct { + name string + timer *SyncCheckTimer + timers map[string]*SyncCheckTimer + sessionID string + want map[string]*SyncCheckTimer + wantPanic bool + }{ + { + name: "存在するセッションのタイマーが削除される", + timer: timer, + timers: map[string]*SyncCheckTimer{"sessionID": timer}, + sessionID: "sessionID", + want: map[string]*SyncCheckTimer{}, + wantPanic: true, + }, + + { + name: "存在しないセッションの場合は何も起こらない", + timer: timerForNotFound, + timers: map[string]*SyncCheckTimer{"sessionID": timerForNotFound}, + sessionID: "not_found", + want: map[string]*SyncCheckTimer{"sessionID": timerForNotFound}, + wantPanic: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func() { + err := recover() + if (err != nil) != tt.wantPanic { + t.Errorf("StopTimer() wantPanic=%v, but err=%v", tt.wantPanic, err) + } + }() + + m := &SyncCheckTimerManager{ + timers: tt.timers, + mu: sync.Mutex{}, } + m.StopTimer(tt.sessionID) + + opts := []cmp.Option{cmp.AllowUnexported(SyncCheckTimer{}), cmpopts.IgnoreUnexported(time.Timer{})} + if !cmp.Equal(m.timers, tt.want, opts...) { + t.Errorf("StopTimer() diff=%v", cmp.Diff(tt.want, m.timers, opts...)) + } + + // 既に閉じられているchannelに対してcloseするとpanicが起こることを利用して正しくcloseされているかチェックする + close(tt.timer.stopCh) }) } } diff --git a/domain/mock_spotify/player.go b/domain/mock_spotify/player.go index c6ec8eec..cb47cfea 100644 --- a/domain/mock_spotify/player.go +++ b/domain/mock_spotify/player.go @@ -148,30 +148,16 @@ func (mr *MockPlayerMockRecorder) SetShuffleMode(ctx, on, deviceID interface{}) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetShuffleMode", reflect.TypeOf((*MockPlayer)(nil).SetShuffleMode), ctx, on, deviceID) } -// DeleteAllTracksInQueue mocks base method -func (m *MockPlayer) DeleteAllTracksInQueue(ctx context.Context, deviceID, trackURI string) error { +// SkipAllTracks mocks base method +func (m *MockPlayer) SkipAllTracks(ctx context.Context, deviceID, trackURI string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteAllTracksInQueue", ctx, deviceID, trackURI) + ret := m.ctrl.Call(m, "SkipAllTracks", ctx, deviceID, trackURI) ret0, _ := ret[0].(error) return ret0 } -// DeleteAllTracksInQueue indicates an expected call of DeleteAllTracksInQueue -func (mr *MockPlayerMockRecorder) DeleteAllTracksInQueue(ctx, deviceID, trackURI interface{}) *gomock.Call { +// SkipAllTracks indicates an expected call of SkipAllTracks +func (mr *MockPlayerMockRecorder) SkipAllTracks(ctx, deviceID, trackURI interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAllTracksInQueue", reflect.TypeOf((*MockPlayer)(nil).DeleteAllTracksInQueue), ctx, deviceID, trackURI) -} - -// GoNextTrack mocks base method -func (m *MockPlayer) GoNextTrack(ctx context.Context, deviceID string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GoNextTrack", ctx, deviceID) - ret0, _ := ret[0].(error) - return ret0 -} - -// GoNextTrack indicates an expected call of GoNextTrack -func (mr *MockPlayerMockRecorder) GoNextTrack(ctx, deviceID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GoNextTrack", reflect.TypeOf((*MockPlayer)(nil).GoNextTrack), ctx, deviceID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SkipAllTracks", reflect.TypeOf((*MockPlayer)(nil).SkipAllTracks), ctx, deviceID, trackURI) } diff --git a/domain/spotify/player.go b/domain/spotify/player.go index 6e2175d8..0f266bc6 100644 --- a/domain/spotify/player.go +++ b/domain/spotify/player.go @@ -19,6 +19,5 @@ type Player interface { Enqueue(ctx context.Context, trackURI string, deviceID string) error SetRepeatMode(ctx context.Context, on bool, deviceID string) error SetShuffleMode(ctx context.Context, on bool, deviceID string) error - DeleteAllTracksInQueue(ctx context.Context, deviceID string, trackURI string) error - GoNextTrack(ctx context.Context, deviceID string) error + SkipAllTracks(ctx context.Context, deviceID string, trackURI string) error } diff --git a/spotify/player.go b/spotify/player.go index af6a630a..65959241 100644 --- a/spotify/player.go +++ b/spotify/player.go @@ -43,10 +43,9 @@ func (c *Client) toDevice(device spotify.PlayerDevice) *entity.Device { } } -// GoNextTrack はユーザーが現在再生している曲を1曲skipします。 -// APIが非同期で処理がされるため、リクエストが返ってきてもskipが完了しているとは限りません。 +// skipAllTracks はユーザーのSpotifyに積まれている「次に再生される曲」「再生待ち」を全てskipします。 // プレミアム会員必須 -func (c *Client) GoNextTrack(ctx context.Context, deviceID string) error { +func (c *Client) SkipAllTracks(ctx context.Context, deviceID string, trackURI string) error { token, ok := service.GetTokenFromContext(ctx) if !ok { return errors.New("token not found") @@ -59,44 +58,21 @@ func (c *Client) GoNextTrack(ctx context.Context, deviceID string) error { opt = &spotify.PlayOptions{DeviceID: &spotifyID} } - err := cli.NextOpt(opt) - if convErr := c.convertPlayerError(err); convErr != nil { - return fmt.Errorf("spotify api: next: %w", convErr) - } - - return nil -} - -// DeleteAllTracksInQueue はユーザーのSpotifyに積まれている「次に再生される曲」「再生待ち」を全てskipします。 -// プレミアム会員必須 -func (c *Client) DeleteAllTracksInQueue(ctx context.Context, deviceID string, trackURI string) error { - token, ok := service.GetTokenFromContext(ctx) - if !ok { - return errors.New("token not found") - } - cli := c.auth.NewClient(token) - // PlayWithTracksで「再生待ち」を0曲にする if err := c.PlayWithTracks(ctx, deviceID, []string{trackURI}); err != nil { return fmt.Errorf("call play api with tracks %v: %w", trackURI, err) } - opt := &spotify.PlayOptions{DeviceID: nil} - if deviceID != "" { - spotifyID := spotify.ID(deviceID) - opt = &spotify.PlayOptions{DeviceID: &spotifyID} - } - skipOnceTime := 3 sleepTime := 300 * time.Millisecond for i := 1; ; i++ { err := cli.NextOpt(opt) + // SpotifyAPIを叩いてからSpotifyが曲をskipするのに時間がかかるため余計にAPIを叩かないように調節 if convErr := c.convertPlayerError(err); convErr != nil { return fmt.Errorf("spotify api: next: %w", convErr) } if i%skipOnceTime == 0 { - // SpotifyAPIを叩いてからSpotifyが曲をskipするのに時間がかかるため余計にAPIを叩かないように調節 time.Sleep(sleepTime) cpi, err := c.CurrentlyPlaying(ctx) if err != nil { @@ -260,7 +236,7 @@ func (c *Client) SetRepeatMode(ctx context.Context, on bool, deviceID string) er } // SetShuffleMode はシャッフルモードの設定を変更するAPIです。 -// APIが非同期で処理がされるため、リクエストが返ってきてもシャッフルモードの設定が完了しているとは限りません。 +// APIが非同期で処理がされるため、リクエストが返ってきてもリピートモードの設定が完了しているとは限りません。 // 設定が反映されたか確認するには CurrentlyPlaying() を叩く必要があります。 // プレミアム会員必須 func (c *Client) SetShuffleMode(ctx context.Context, on bool, deviceID string) error { diff --git a/spotify/player_test.go b/spotify/player_test.go index 3353b04e..1152c854 100644 --- a/spotify/player_test.go +++ b/spotify/player_test.go @@ -195,7 +195,7 @@ func TestClient_SetRepeatMode(t *testing.T) { } // テスト前にSpotify側で「次に再生される曲」を積んでください -func TestClient_DeleteAllTracksInQueueTracks(t *testing.T) { +func TestClient_SkipAllTracks(t *testing.T) { tests := []struct { name string wantErr string @@ -220,8 +220,8 @@ func TestClient_DeleteAllTracksInQueueTracks(t *testing.T) { } ctx := context.Background() ctx = service.SetTokenToContext(ctx, token) - if err := c.DeleteAllTracksInQueue(ctx, "", "spotify:track:5uQ0vKy2973Y9IUCd1wMEF"); err.Error() != tt.wantErr { - t.Errorf("DeleteAllTracksInQueue() error = %v, wantErr %v", err, tt.wantErr) + if err := c.SkipAllTracks(ctx, "", "spotify:track:5uQ0vKy2973Y9IUCd1wMEF"); err.Error() != tt.wantErr { + t.Errorf("SkipAllTracks() error = %v, wantErr %v", err, tt.wantErr) } }) } diff --git a/usecase/session.go b/usecase/session.go index 36cc88b8..cffec6ad 100644 --- a/usecase/session.go +++ b/usecase/session.go @@ -141,26 +141,17 @@ func (s *SessionUseCase) GetSession(ctx context.Context, sessionID string) (*ent return nil, nil, nil, fmt.Errorf("CurrentlyPlaying: %w", err) } - if !s.timerUC.existsTimer(sessionID) { - return entity.NewSessionWithUser(session, creator), tracks, cpi, nil - } - - isExpired, err := s.timerUC.isTimerExpired(sessionID) - - if isExpired { - return entity.NewSessionWithUser(session, creator), tracks, cpi, nil - } - - if err != nil { - return nil, nil, nil, fmt.Errorf("isTimerExpired: %w", err) - } + // timerが存在しない時はsyncCheckOffsetの時間なのでcpiのチェックは飛ばす + if _, isExist := s.timerUC.tm.GetTimer(sessionID); isExist { + if err := session.IsPlayingCorrectTrack(cpi); err != nil { + s.timerUC.deleteTimer(session.ID) + s.timerUC.handleInterrupt(session) - if err := session.IsPlayingCorrectTrack(cpi); err != nil { - s.timerUC.deleteTimer(session.ID) - s.timerUC.handleInterrupt(session) + if updateErr := s.sessionRepo.Update(ctx, session); updateErr != nil { + return nil, nil, nil, fmt.Errorf("update session id=%s: %v: %w", session.ID, err, updateErr) + } - if updateErr := s.sessionRepo.Update(ctx, session); updateErr != nil { - return nil, nil, nil, fmt.Errorf("update session id=%s: %v: %w", session.ID, err, updateErr) + return entity.NewSessionWithUser(session, creator), tracks, cpi, nil } } diff --git a/usecase/session_state.go b/usecase/session_state.go index 1fb929df..6136595e 100644 --- a/usecase/session_state.go +++ b/usecase/session_state.go @@ -25,117 +25,6 @@ func NewSessionStateUseCase(sessionRepo repository.Session, playerCli spotify.Pl return &SessionStateUseCase{sessionRepo: sessionRepo, playerCli: playerCli, pusher: pusher, timerUC: timerUC} } -// NextTrack は指定されたidのsessionを次の曲に進めます -func (s *SessionStateUseCase) NextTrack(ctx context.Context, sessionID string) error { - session, err := s.sessionRepo.FindByID(ctx, sessionID) - if err != nil { - return fmt.Errorf("find session id=%s: %w", sessionID, err) - } - - userID, _ := service.GetUserIDFromContext(ctx) - if !session.AllowToControlByOthers && !session.IsCreator(userID) { - return fmt.Errorf("not allowd to control session: %w", entity.ErrSessionNotAllowToControlOthers) - } - - switch session.StateType { - case entity.Play: - if err = s.nextTrackInPlay(ctx, session); err != nil { - return fmt.Errorf("go next track in play session id=%s: %w", session.ID, err) - } - case entity.Pause: - if err = s.nextTrackInPause(ctx, session); err != nil { - return fmt.Errorf("go next track in pause session id=%s: %w", session.ID, err) - } - case entity.Stop: - if err = s.nextTrackInStop(ctx, session); err != nil { - return fmt.Errorf("go next track in stop session id=%s: %w", session.ID, err) - } - case entity.Archived: - return fmt.Errorf("go next track: %w", entity.ErrChangeSessionStateNotPermit) - } - - return nil -} - -// nextTrackInPlay はsessionのstateがPLAYの時のnextTrackの処理を行います -func (s *SessionStateUseCase) nextTrackInPlay(ctx context.Context, session *entity.Session) error { - if err := s.playerCli.GoNextTrack(ctx, session.DeviceID); err != nil { - return fmt.Errorf("GoNextTrack: %w", err) - } - - // NextChを通してstartTrackEndTriggerに次の曲への遷移を通知 - if err := s.timerUC.sendToNextCh(session.ID); err != nil { - return fmt.Errorf("send to next ch: %w", err) - } - - return nil -} - -// nextTrackInPause はsessionのstateがPAUSEの時のnextTrackの処理を行います -func (s *SessionStateUseCase) nextTrackInPause(ctx context.Context, session *entity.Session) error { - if err := s.playerCli.GoNextTrack(ctx, session.DeviceID); err != nil { - return fmt.Errorf("GoNextTrack: %w", err) - } - - if err := session.GoNextTrack(); err != nil && errors.Is(err, entity.ErrSessionAllTracksFinished) { - s.timerUC.handleAllTrackFinish(session) - if err := s.sessionRepo.Update(ctx, session); err != nil { - return fmt.Errorf("update session id=%s: %w", session.ID, err) - } - return nil - } - - // GoNextTrackだけだと次の曲の再生が始まってしまう - if err := s.playerCli.Pause(ctx, session.DeviceID); err != nil { - return fmt.Errorf("call pause api: %w", err) - } - - if err := s.sessionRepo.Update(ctx, session); err != nil { - return fmt.Errorf("update session id=%s: %w", session.ID, err) - } - - track := session.TrackURIShouldBeAddedWhenHandleTrackEnd() - if track != "" { - if err := s.playerCli.Enqueue(ctx, track, session.DeviceID); err != nil { - return fmt.Errorf("enqueue error session id=%s: %w", session.ID, err) - } - } - - s.pusher.Push(&event.PushMessage{ - SessionID: session.ID, - Msg: entity.NewEventNextTrack(session.QueueHead), - }) - - return nil -} - -// nextTrackInStop はsessionのstateがSTOPの時のnextTrackの処理を行います -// stopToPlayで曲がResetされ、再度Spotifyのキューに積まれるため、Enqueueを行っていません -func (s *SessionStateUseCase) nextTrackInStop(ctx context.Context, session *entity.Session) error { - if !session.IsNextTrackExistWhenStateIsStop() { - return fmt.Errorf("nextTrackInStop: %w", entity.ErrNextQueueTrackNotFound) - } - - if err := session.GoNextTrack(); err != nil && errors.Is(err, entity.ErrSessionAllTracksFinished) { - s.timerUC.handleAllTrackFinish(session) - if err := s.sessionRepo.Update(ctx, session); err != nil { - return fmt.Errorf("update session id=%s: %w", session.ID, err) - } - return nil - } - - if err := s.sessionRepo.Update(ctx, session); err != nil { - return fmt.Errorf("update session id=%s: %w", session.ID, err) - } - - s.pusher.Push(&event.PushMessage{ - SessionID: session.ID, - Msg: entity.NewEventNextTrack(session.QueueHead), - }) - - return nil -} - // ChangeSessionState は与えられたセッションのstateを操作します。 func (s *SessionStateUseCase) ChangeSessionState(ctx context.Context, sessionID string, st entity.StateType) error { session, err := s.sessionRepo.FindByID(ctx, sessionID) @@ -225,8 +114,8 @@ func (s *SessionStateUseCase) stopToPlay(ctx context.Context, sess *entity.Sessi return fmt.Errorf(": %w", err) } - if err := s.playerCli.DeleteAllTracksInQueue(ctx, sess.DeviceID, trackURIs[0]); err != nil { - return fmt.Errorf("call DeleteAllTracksInQueue: %w", err) + if err := s.playerCli.SkipAllTracks(ctx, sess.DeviceID, trackURIs[0]); err != nil { + return fmt.Errorf("call SkipAllTracks: %w", err) } for i := 0; i < len(trackURIs); i++ { if i == 0 { @@ -254,7 +143,7 @@ func (s *SessionStateUseCase) pause(ctx context.Context, sess *entity.Session) e return fmt.Errorf("call pause api: %w", err) } - s.timerUC.deleteTimer(sess.ID) + s.timerUC.stopTimer(sess.ID) if err := sess.MoveToPause(); err != nil { return fmt.Errorf("move to pause id=%s: %w", sess.ID, err) diff --git a/usecase/session_test.go b/usecase/session_test.go index 126388c5..98120693 100644 --- a/usecase/session_test.go +++ b/usecase/session_test.go @@ -111,34 +111,37 @@ func (m *FakePlayer) CurrentlyPlaying(ctx context.Context) (*entity.CurrentPlayi }, nil } +// Play mocks base method func (m *FakePlayer) Play(ctx context.Context, deviceID string) error { return nil } +// PlayWithTracks mocks base method func (m *FakePlayer) PlayWithTracks(ctx context.Context, deviceID string, trackURIs []string) error { return nil } +// Pause mocks base method func (m *FakePlayer) Pause(ctx context.Context, deviceID string) error { return nil } +// Enqueue mocks base method func (m *FakePlayer) Enqueue(ctx context.Context, trackURI, deviceID string) error { return nil } +// SetRepeatMode mocks base method func (m *FakePlayer) SetRepeatMode(ctx context.Context, on bool, deviceID string) error { return nil } +// SetShuffleMode mocks base method func (m *FakePlayer) SetShuffleMode(ctx context.Context, on bool, deviceID string) error { return nil } -func (m *FakePlayer) DeleteAllTracksInQueue(ctx context.Context, deviceID, trackURI string) error { - return nil -} - -func (m *FakePlayer) GoNextTrack(ctx context.Context, deviceID string) error { +// SkipAllTracks mocks base method +func (m *FakePlayer) SkipAllTracks(ctx context.Context, deviceID, trackURI string) error { return nil } diff --git a/usecase/session_timer.go b/usecase/session_timer.go index 79647c47..b3c70fcb 100644 --- a/usecase/session_timer.go +++ b/usecase/session_timer.go @@ -13,8 +13,7 @@ import ( "github.com/camphor-/relaym-server/log" ) -var waitTimeAfterHandleTrackEnd = 7 * time.Second -var waitTimeAfterHandleSkipTrack = 300 * time.Millisecond +var waitTimeBeforeHandleTrackEnd = 7 * time.Second type SessionTimerUseCase struct { tm *entity.SyncCheckTimerManager @@ -32,47 +31,35 @@ func (s *SessionTimerUseCase) startTrackEndTrigger(ctx context.Context, sessionI logger := log.New() logger.Debugj(map[string]interface{}{"message": "start track end trigger", "sessionID": sessionID}) - // 曲の再生を待つ - waitTimer := time.NewTimer(5 * time.Second) - currentOperation := operationPlay + time.Sleep(5 * time.Second) // 曲の再生が始まるのを待つ + playingInfo, err := s.playerCli.CurrentlyPlaying(ctx) + if err != nil { + logger.Errorj(map[string]interface{}{ + "message": "startTrackEndTrigger: failed to get currently playing info", + "sessionID": sessionID, + "error": err.Error(), + }) + return + } + + // ぴったしのタイマーをセットすると、Spotifyでは次の曲の再生が始まってるのにRelaym側では次の曲に進んでおらず、 + // INTERRUPTになってしまう + remainDuration := playingInfo.Remain() - 2*time.Second + + logger.Infoj(map[string]interface{}{ + "message": "start timer", "sessionID": sessionID, "remainDuration": remainDuration.String(), + }) + + triggerAfterTrackEnd := s.tm.CreateTimer(sessionID, remainDuration) - triggerAfterTrackEnd := s.tm.CreateExpiredTimer(sessionID) for { select { - case <-waitTimer.C: - if err := s.handleWaitTimerExpired(ctx, sessionID, triggerAfterTrackEnd, currentOperation); err != nil { - return - } case <-triggerAfterTrackEnd.StopCh(): logger.Infoj(map[string]interface{}{"message": "stop timer", "sessionID": sessionID}) - waitTimer.Stop() - s.deleteTimer(sessionID) return - - case <-triggerAfterTrackEnd.NextCh(): - logger.Debugj(map[string]interface{}{"message": "call to move next track", "sessionID": sessionID}) - waitTimer.Stop() - nextTrack, err := s.handleTrackEnd(ctx, sessionID) - if err != nil { - if errors.Is(err, entity.ErrSessionPlayingDifferentTrack) { - logger.Infoj(map[string]interface{}{"message": "handleTrackEnd detects interrupt", "sessionID": sessionID, "error": err.Error()}) - return - } - logger.Errorj(map[string]interface{}{"message": "handleTrackEnd with error", "sessionID": sessionID, "error": err.Error()}) - return - } - if !nextTrack { - logger.Infoj(map[string]interface{}{"message": "no next track", "sessionID": sessionID}) - return - } - - waitTimer = time.NewTimer(waitTimeAfterHandleSkipTrack) - currentOperation = operationNextTrack - case <-triggerAfterTrackEnd.ExpireCh(): - triggerAfterTrackEnd.MakeIsTimerExpiredTrue() logger.Debugj(map[string]interface{}{"message": "trigger expired", "sessionID": sessionID}) - nextTrack, err := s.handleTrackEnd(ctx, sessionID) + timer, nextTrack, err := s.handleTrackEnd(ctx, sessionID) if err != nil { if errors.Is(err, entity.ErrSessionPlayingDifferentTrack) { logger.Infoj(map[string]interface{}{"message": "handleTrackEnd detects interrupt", "sessionID": sessionID, "error": err.Error()}) @@ -85,87 +72,29 @@ func (s *SessionTimerUseCase) startTrackEndTrigger(ctx context.Context, sessionI logger.Infoj(map[string]interface{}{"message": "no next track", "sessionID": sessionID}) return } - waitTimer = time.NewTimer(waitTimeAfterHandleTrackEnd) - currentOperation = operationNextTrack + triggerAfterTrackEnd = timer } } } -func (s *SessionTimerUseCase) handleWaitTimerExpired(ctx context.Context, sessionID string, triggerAfterTrackEnd *entity.SyncCheckTimer, currentOperation currentOperation) error { - logger := log.New() - - playingInfo, err := s.playerCli.CurrentlyPlaying(ctx) - if err != nil { - logger.Errorj(map[string]interface{}{ - "message": "startTrackEndTrigger: failed to get currently playing info", - "sessionID": sessionID, - "error": err.Error(), - }) - return fmt.Errorf("failed to get currently playing info") - } - - sess, err := s.sessionRepo.FindByID(ctx, sessionID) - if err != nil { - logger.Errorj(map[string]interface{}{ - "message": "startTrackEndTrigger: failed to get session", - "sessionID": sessionID, - "error": err.Error(), - }) - return fmt.Errorf("failed to get session from repo") - } - - if err := sess.IsPlayingCorrectTrack(playingInfo); err != nil { - s.handleInterrupt(sess) - if err := s.sessionRepo.Update(ctx, sess); err != nil { - logger.Errorj(map[string]interface{}{ - "message": "startTrackEndTrigger: failed to update session after handleInterrupt", - "sessionID": sessionID, - "error": err.Error(), - }) - return fmt.Errorf("failed to update session") - } - return fmt.Errorf("session interrupt") - } - - logger.Debugj(map[string]interface{}{"message": "currentOperation", "currentOperation": currentOperation}) - - switch currentOperation { - case operationNextTrack: - s.pusher.Push(&event.PushMessage{ - SessionID: sess.ID, - Msg: entity.NewEventNextTrack(sess.QueueHead), - }) - } - - // ぴったしのタイマーをセットすると、Spotifyでは次の曲の再生が始まってるのにRelaym側では次の曲に進んでおらず、 - // INTERRUPTになってしまう - remainDuration := playingInfo.Remain() - 2*time.Second - - logger.Infoj(map[string]interface{}{ - "message": "start timer", "sessionID": sessionID, "remainDuration": remainDuration.String(), - }) - - triggerAfterTrackEnd.SetDuration(remainDuration) - - return nil -} - // handleTrackEnd はある一曲の再生が終わったときの処理を行います。 -func (s *SessionTimerUseCase) handleTrackEnd(ctx context.Context, sessionID string) (bool, error) { +func (s *SessionTimerUseCase) handleTrackEnd(ctx context.Context, sessionID string) (*entity.SyncCheckTimer, bool, error) { + s.tm.DeleteTimer(sessionID) + time.Sleep(waitTimeBeforeHandleTrackEnd) triggerAfterTrackEndResponse, err := s.sessionRepo.DoInTx(ctx, s.handleTrackEndTx(sessionID)) if v, ok := triggerAfterTrackEndResponse.(*handleTrackEndResponse); ok { // これはトランザクションが失敗してRollbackしたとき if err != nil { - return false, fmt.Errorf("handle track end in transaction: %w", err) + return nil, false, fmt.Errorf("handle track end in transaction: %w", err) } - return v.nextTrack, v.err + return v.triggerAfterTrackEnd, v.nextTrack, v.err } // これはトランザクションが失敗してRollbackしたとき if err != nil { - return false, fmt.Errorf("handle track end in transaction: %w", err) + return nil, false, fmt.Errorf("handle track end in transaction: %w", err) } - return false, nil + return nil, false, nil } // handleTrackEndTx はINTERRUPTになってerrorを帰す場合もトランザクションをコミットして欲しいので、 @@ -175,7 +104,7 @@ func (s *SessionTimerUseCase) handleTrackEndTx(sessionID string) func(ctx contex return func(ctx context.Context) (_ interface{}, returnErr error) { sess, err := s.sessionRepo.FindByIDForUpdate(ctx, sessionID) if err != nil { - return &handleTrackEndResponse{nextTrack: false}, fmt.Errorf("find session id=%s: %v", sessionID, err) + return &handleTrackEndResponse{triggerAfterTrackEnd: nil, nextTrack: false}, fmt.Errorf("find session id=%s: %v", sessionID, err) } defer func() { @@ -188,25 +117,26 @@ func (s *SessionTimerUseCase) handleTrackEndTx(sessionID string) func(ctx contex } }() - // 曲の再生中にArchivedになった場合 if sess.StateType == entity.Archived { s.pusher.Push(&event.PushMessage{ - SessionID: sess.ID, + SessionID: sessionID, Msg: entity.EventArchived, }) return &handleTrackEndResponse{ - nextTrack: false, - err: nil, + triggerAfterTrackEnd: nil, + nextTrack: false, + err: nil, }, nil } if err := sess.GoNextTrack(); err != nil && errors.Is(err, entity.ErrSessionAllTracksFinished) { s.handleAllTrackFinish(sess) return &handleTrackEndResponse{ - nextTrack: false, - err: nil, + triggerAfterTrackEnd: nil, + nextTrack: false, + err: nil, }, nil } @@ -214,15 +144,44 @@ func (s *SessionTimerUseCase) handleTrackEndTx(sessionID string) func(ctx contex if track != "" { if err := s.playerCli.Enqueue(ctx, track, sess.DeviceID); err != nil { return &handleTrackEndResponse{ - nextTrack: false, - err: fmt.Errorf("call add queue api trackURI=%s: %w", track, err), + triggerAfterTrackEnd: nil, + nextTrack: false, + err: fmt.Errorf("call add queue api trackURI=%s: %w", track, err), + }, nil + } + } + + logger.Debugj(map[string]interface{}{"message": "next track", "sessionID": sessionID, "queueHead": sess.QueueHead}) + + playingInfo, err := s.playerCli.CurrentlyPlaying(ctx) + if err != nil { + if errors.Is(err, entity.ErrActiveDeviceNotFound) { + s.handleInterrupt(sess) + return &handleTrackEndResponse{ + triggerAfterTrackEnd: nil, + nextTrack: false, + err: err, }, nil } + return &handleTrackEndResponse{triggerAfterTrackEnd: nil, nextTrack: false, err: fmt.Errorf("get currently playing info id=%s: %v", sessionID, err)}, nil + } + + if err := sess.IsPlayingCorrectTrack(playingInfo); err != nil { + s.handleInterrupt(sess) + return &handleTrackEndResponse{triggerAfterTrackEnd: nil, nextTrack: false, err: fmt.Errorf("check whether playing correct track: %w", err)}, nil } - logger.Debugj(map[string]interface{}{"message": "next track", "sessionID": sess.ID, "queueHead": sess.QueueHead}) + s.pusher.Push(&event.PushMessage{ + SessionID: sessionID, + Msg: entity.NewEventNextTrack(sess.QueueHead), + }) + triggerAfterTrackEnd := s.tm.CreateTimer(sessionID, playingInfo.Remain()) + + logger.Infoj(map[string]interface{}{ + "message": "restart timer", "sessionID": sessionID, "remainDuration": playingInfo.Remain().String(), + }) - return &handleTrackEndResponse{nextTrack: true, err: nil}, nil + return &handleTrackEndResponse{triggerAfterTrackEnd: triggerAfterTrackEnd, nextTrack: true, err: nil}, nil } } @@ -254,26 +213,16 @@ func (s *SessionTimerUseCase) existsTimer(sessionID string) bool { return exists } -func (s *SessionTimerUseCase) deleteTimer(sessionID string) { - s.tm.DeleteTimer(sessionID) -} - -func (s *SessionTimerUseCase) isTimerExpired(sessionID string) (bool, error) { - return s.tm.IsTimerExpired(sessionID) +func (s *SessionTimerUseCase) stopTimer(sessionID string) { + s.tm.StopTimer(sessionID) } -func (s *SessionTimerUseCase) sendToNextCh(sessionID string) error { - return s.tm.SendToNextCh(sessionID) +func (s *SessionTimerUseCase) deleteTimer(sessionID string) { + s.tm.DeleteTimer(sessionID) } type handleTrackEndResponse struct { - nextTrack bool - err error + triggerAfterTrackEnd *entity.SyncCheckTimer + nextTrack bool + err error } - -type currentOperation string - -const ( - operationPlay currentOperation = "play" - operationNextTrack currentOperation = "NextTrack" -) diff --git a/usecase/session_timer_test.go b/usecase/session_timer_test.go index b687e812..439f8365 100644 --- a/usecase/session_timer_test.go +++ b/usecase/session_timer_test.go @@ -3,7 +3,6 @@ package usecase import ( "context" "testing" - "time" "github.com/camphor-/relaym-server/domain/entity" "github.com/camphor-/relaym-server/domain/event" @@ -23,6 +22,7 @@ func TestSessionTimerUseCase_handleTrackEndTx(t *testing.T) { prepareMockPusherFn func(m *mock_event.MockPusher) prepareMockUserRepoFn func(m *mock_repository.MockUser) prepareMockSessionRepoFn func(m *mock_repository.MockSession) + wantTriggerAfterTrackEnd bool wantNextTrack bool wantErr bool }{ @@ -63,15 +63,42 @@ func TestSessionTimerUseCase_handleTrackEndTx(t *testing.T) { }, }).Return(nil) }, - wantNextTrack: false, - wantErr: false, + wantTriggerAfterTrackEnd: false, + wantNextTrack: false, + wantErr: false, }, { name: "次の曲が存在するときはNEXTTRACKイベントが送られて、次の再生状態に遷移する", sessionID: "sessionID", prepareMockPlayerFn: func(m *mock_spotify.MockPlayer) { + m.EXPECT().CurrentlyPlaying(gomock.Any()).Return(&entity.CurrentPlayingInfo{ + Playing: true, + Progress: 10000000, + Track: &entity.Track{ + URI: "spotify:track:06QTSGUEgcmKwiEJ0IMPig", + ID: "06QTSGUEgcmKwiEJ0IMPig", + Name: "Borderland", + Duration: 213066000000, + Artists: []*entity.Artist{{Name: "MONOEYES"}}, + URL: "https://open.spotify.com/track/06QTSGUEgcmKwiEJ0IMPig", + Album: &entity.Album{ + Name: "Interstate 46 E.P.", + Images: []*entity.AlbumImage{ + { + URL: "https://i.scdn.co/image/ab67616d0000b273b48630d6efcebca2596120c4", + Height: 640, + Width: 640, + }, + }, + }, + }, + }, nil) }, prepareMockPusherFn: func(m *mock_event.MockPusher) { + m.EXPECT().Push(&event.PushMessage{ + SessionID: "sessionID", + Msg: entity.NewEventNextTrack(1), + }) }, prepareMockUserRepoFn: func(m *mock_repository.MockUser) {}, prepareMockSessionRepoFn: func(m *mock_repository.MockSession) { @@ -116,16 +143,43 @@ func TestSessionTimerUseCase_handleTrackEndTx(t *testing.T) { }, }).Return(nil) }, - wantNextTrack: true, - wantErr: false, + wantTriggerAfterTrackEnd: true, + wantNextTrack: true, + wantErr: false, }, { name: "次の曲が存在し、次に再生される曲の二曲先の曲が存在するときはNEXTTRACKイベントが送られて、次の再生状態に遷移し、同時に二曲先の曲がSpotifyのqueueに積まれる", sessionID: "sessionID", prepareMockPlayerFn: func(m *mock_spotify.MockPlayer) { + m.EXPECT().CurrentlyPlaying(gomock.Any()).Return(&entity.CurrentPlayingInfo{ + Playing: true, + Progress: 10000000, + Track: &entity.Track{ + URI: "spotify:track:06QTSGUEgcmKwiEJ0IMPig", + ID: "06QTSGUEgcmKwiEJ0IMPig", + Name: "Borderland", + Duration: 213066000000, + Artists: []*entity.Artist{{Name: "MONOEYES"}}, + URL: "https://open.spotify.com/track/06QTSGUEgcmKwiEJ0IMPig", + Album: &entity.Album{ + Name: "Interstate 46 E.P.", + Images: []*entity.AlbumImage{ + { + URL: "https://i.scdn.co/image/ab67616d0000b273b48630d6efcebca2596120c4", + Height: 640, + Width: 640, + }, + }, + }, + }, + }, nil) m.EXPECT().Enqueue(gomock.Any(), "spotify:track:3", "deviceID").Return(nil) }, prepareMockPusherFn: func(m *mock_event.MockPusher) { + m.EXPECT().Push(&event.PushMessage{ + SessionID: "sessionID", + Msg: entity.NewEventNextTrack(1), + }) }, prepareMockUserRepoFn: func(m *mock_repository.MockUser) {}, prepareMockSessionRepoFn: func(m *mock_repository.MockSession) { @@ -190,14 +244,43 @@ func TestSessionTimerUseCase_handleTrackEndTx(t *testing.T) { }, }).Return(nil) }, - wantNextTrack: true, - wantErr: false, + wantTriggerAfterTrackEnd: true, + wantNextTrack: true, + wantErr: false, }, { - name: "次の曲が存在するが、実際には違う曲が流れていた場合はINTERRUPTイベントが送られる", - sessionID: "sessionID", - prepareMockPlayerFn: func(m *mock_spotify.MockPlayer) {}, - prepareMockPusherFn: func(m *mock_event.MockPusher) {}, + name: "次の曲が存在するが、実際には違う曲が流れていた場合はINTERRUPTイベントが送られる", + sessionID: "sessionID", + prepareMockPlayerFn: func(m *mock_spotify.MockPlayer) { + m.EXPECT().CurrentlyPlaying(gomock.Any()).Return(&entity.CurrentPlayingInfo{ + Playing: true, + Progress: 10000000, + Track: &entity.Track{ + URI: "spotify:track:06QTSGUEgcmKwiEJ0IMPig", + ID: "06QTSGUEgcmKwiEJ0IMPig", + Name: "Borderland", + Duration: 213066000000, + Artists: []*entity.Artist{{Name: "MONOEYES"}}, + URL: "https://open.spotify.com/track/06QTSGUEgcmKwiEJ0IMPig", + Album: &entity.Album{ + Name: "Interstate 46 E.P.", + Images: []*entity.AlbumImage{ + { + URL: "https://i.scdn.co/image/ab67616d0000b273b48630d6efcebca2596120c4", + Height: 640, + Width: 640, + }, + }, + }, + }, + }, nil) + }, + prepareMockPusherFn: func(m *mock_event.MockPusher) { + m.EXPECT().Push(&event.PushMessage{ + SessionID: "sessionID", + Msg: entity.EventInterrupt, + }) + }, prepareMockUserRepoFn: func(m *mock_repository.MockUser) {}, prepareMockSessionRepoFn: func(m *mock_repository.MockSession) { m.EXPECT().FindByIDForUpdate(gomock.Any(), "sessionID").Return(&entity.Session{ @@ -225,7 +308,7 @@ func TestSessionTimerUseCase_handleTrackEndTx(t *testing.T) { Name: "name", CreatorID: "creatorID", DeviceID: "deviceID", - StateType: entity.Play, + StateType: entity.Stop, QueueHead: 1, QueueTracks: []*entity.QueueTrack{ { @@ -241,15 +324,21 @@ func TestSessionTimerUseCase_handleTrackEndTx(t *testing.T) { }, }).Return(nil) }, - wantNextTrack: true, - wantErr: false, + wantTriggerAfterTrackEnd: false, + wantNextTrack: false, + wantErr: true, }, { name: "次の曲が存在するが、デバイスがオフラインになっていた場合はINTERRUPTイベントが送られる", sessionID: "sessionID", prepareMockPlayerFn: func(m *mock_spotify.MockPlayer) { + m.EXPECT().CurrentlyPlaying(gomock.Any()).Return(nil, entity.ErrActiveDeviceNotFound) }, prepareMockPusherFn: func(m *mock_event.MockPusher) { + m.EXPECT().Push(&event.PushMessage{ + SessionID: "sessionID", + Msg: entity.EventInterrupt, + }) }, prepareMockUserRepoFn: func(m *mock_repository.MockUser) {}, prepareMockSessionRepoFn: func(m *mock_repository.MockSession) { @@ -278,7 +367,7 @@ func TestSessionTimerUseCase_handleTrackEndTx(t *testing.T) { Name: "name", CreatorID: "creatorID", DeviceID: "deviceID", - StateType: entity.Play, + StateType: entity.Stop, QueueHead: 1, QueueTracks: []*entity.QueueTrack{ { @@ -294,8 +383,9 @@ func TestSessionTimerUseCase_handleTrackEndTx(t *testing.T) { }, }).Return(nil) }, - wantNextTrack: true, - wantErr: false, + wantTriggerAfterTrackEnd: false, + wantNextTrack: false, + wantErr: true, }, { name: "呼び出された時点でsessionのstateがARCHIVEDになっていた時にはtimerをdeleteしてArchivedのイベントを送信する", @@ -351,8 +441,9 @@ func TestSessionTimerUseCase_handleTrackEndTx(t *testing.T) { }, }).Return(nil) }, - wantNextTrack: false, - wantErr: false, + wantTriggerAfterTrackEnd: false, + wantNextTrack: false, + wantErr: false, }, } for _, tt := range tests { @@ -368,10 +459,10 @@ func TestSessionTimerUseCase_handleTrackEndTx(t *testing.T) { mockSessionRepo := mock_repository.NewMockSession(ctrl) tt.prepareMockSessionRepoFn(mockSessionRepo) - tmpWaitTimeBeforeHandleTrackEnd := waitTimeAfterHandleTrackEnd - waitTimeAfterHandleTrackEnd = 0 + tmpWaitTimeBeforeHandleTrackEnd := waitTimeBeforeHandleTrackEnd + waitTimeBeforeHandleTrackEnd = 0 defer func() { - waitTimeAfterHandleTrackEnd = tmpWaitTimeBeforeHandleTrackEnd + waitTimeBeforeHandleTrackEnd = tmpWaitTimeBeforeHandleTrackEnd }() syncCheckTimerManager := entity.NewSyncCheckTimerManager() @@ -387,225 +478,12 @@ func TestSessionTimerUseCase_handleTrackEndTx(t *testing.T) { t.Errorf("handleTrackEnd() error = %v, wantErr %v", err, tt.wantErr) return } + if (gotHandleTrackEndResponse.triggerAfterTrackEnd != nil) != tt.wantTriggerAfterTrackEnd { + t.Errorf("handleTrackEnd() gotTriggerAfterTrackEnd = %v, want %v", gotHandleTrackEndResponse.triggerAfterTrackEnd, tt.wantTriggerAfterTrackEnd) + } if gotHandleTrackEndResponse.nextTrack != tt.wantNextTrack { t.Errorf("handleTrackEnd() gotNextTrack = %v, want %v", gotHandleTrackEndResponse.nextTrack, tt.wantNextTrack) } }) } } - -func TestSessionTimerUseCase_handleWaitTimerExpired(t *testing.T) { - tests := []struct { - name string - sessionID string - currentOperation currentOperation - prepareMockPlayerFn func(m *mock_spotify.MockPlayer) - prepareMockPusherFn func(m *mock_event.MockPusher) - prepareMockUserRepoFn func(m *mock_repository.MockUser) - prepareMockSessionRepoFn func(m *mock_repository.MockSession) - wantErr bool - }{ - { - name: "Spotifyとの同期が取れていることが確認されると、currentOperationがPlayの時はイベントは送信されない", - sessionID: "sessionID", - currentOperation: "Play", - prepareMockPlayerFn: func(m *mock_spotify.MockPlayer) { - m.EXPECT().CurrentlyPlaying(gomock.Any()).Return(&entity.CurrentPlayingInfo{ - Playing: true, - Progress: 10000000, - Track: &entity.Track{ - URI: "spotify:track:06QTSGUEgcmKwiEJ0IMPig", - ID: "06QTSGUEgcmKwiEJ0IMPig", - Name: "Borderland", - Duration: 213066000000, - Artists: []*entity.Artist{{Name: "MONOEYES"}}, - URL: "https://open.spotify.com/track/06QTSGUEgcmKwiEJ0IMPig", - Album: &entity.Album{ - Name: "Interstate 46 E.P.", - Images: []*entity.AlbumImage{ - { - URL: "https://i.scdn.co/image/ab67616d0000b273b48630d6efcebca2596120c4", - Height: 640, - Width: 640, - }, - }, - }, - }, - }, nil) - }, - prepareMockPusherFn: func(m *mock_event.MockPusher) {}, - prepareMockUserRepoFn: func(m *mock_repository.MockUser) {}, - prepareMockSessionRepoFn: func(m *mock_repository.MockSession) { - m.EXPECT().FindByID(gomock.Any(), "sessionID").Return(&entity.Session{ - ID: "sessionID", - Name: "name", - CreatorID: "creatorID", - DeviceID: "deviceID", - StateType: "PLAY", - QueueHead: 1, - QueueTracks: []*entity.QueueTrack{ - {Index: 0, URI: "spotify:track:5uQ0vKy2973Y9IUCd1wMEF"}, - {Index: 1, URI: "spotify:track:06QTSGUEgcmKwiEJ0IMPig"}, - }, - ExpiredAt: time.Time{}, - AllowToControlByOthers: false, - ProgressWhenPaused: 0, - }, nil) - }, - wantErr: false, - }, - { - name: "Spotifyとの同期が取れていることが確認されると、currentOperationがNextTrackの時はイベントは送信されない", - sessionID: "sessionID", - currentOperation: "NextTrack", - prepareMockPlayerFn: func(m *mock_spotify.MockPlayer) { - m.EXPECT().CurrentlyPlaying(gomock.Any()).Return(&entity.CurrentPlayingInfo{ - Playing: true, - Progress: 10000000, - Track: &entity.Track{ - URI: "spotify:track:06QTSGUEgcmKwiEJ0IMPig", - ID: "06QTSGUEgcmKwiEJ0IMPig", - Name: "Borderland", - Duration: 213066000000, - Artists: []*entity.Artist{{Name: "MONOEYES"}}, - URL: "https://open.spotify.com/track/06QTSGUEgcmKwiEJ0IMPig", - Album: &entity.Album{ - Name: "Interstate 46 E.P.", - Images: []*entity.AlbumImage{ - { - URL: "https://i.scdn.co/image/ab67616d0000b273b48630d6efcebca2596120c4", - Height: 640, - Width: 640, - }, - }, - }, - }, - }, nil) - }, - prepareMockPusherFn: func(m *mock_event.MockPusher) { - m.EXPECT().Push(&event.PushMessage{ - SessionID: "sessionID", - Msg: entity.NewEventNextTrack(1), - }) - }, - prepareMockUserRepoFn: func(m *mock_repository.MockUser) {}, - prepareMockSessionRepoFn: func(m *mock_repository.MockSession) { - m.EXPECT().FindByID(gomock.Any(), "sessionID").Return(&entity.Session{ - ID: "sessionID", - Name: "name", - CreatorID: "creatorID", - DeviceID: "deviceID", - StateType: "PLAY", - QueueHead: 1, - QueueTracks: []*entity.QueueTrack{ - {Index: 0, URI: "spotify:track:5uQ0vKy2973Y9IUCd1wMEF"}, - {Index: 1, URI: "spotify:track:06QTSGUEgcmKwiEJ0IMPig"}, - }, - ExpiredAt: time.Time{}, - AllowToControlByOthers: false, - ProgressWhenPaused: 0, - }, nil) - }, - wantErr: false, - }, - { - name: "Spotifyとの同期が取れていないとhandleInterruptが呼び出されErrorが返る", - sessionID: "sessionID", - currentOperation: "NextTrack", - prepareMockPlayerFn: func(m *mock_spotify.MockPlayer) { - m.EXPECT().CurrentlyPlaying(gomock.Any()).Return(&entity.CurrentPlayingInfo{ - Playing: true, - Progress: 10000000, - Track: &entity.Track{ - URI: "spotify:track:06QTSGUEgcmKwiEJ0IMPig", - ID: "06QTSGUEgcmKwiEJ0IMPig", - Name: "Borderland", - Duration: 213066000000, - Artists: []*entity.Artist{{Name: "MONOEYES"}}, - URL: "https://open.spotify.com/track/06QTSGUEgcmKwiEJ0IMPig", - Album: &entity.Album{ - Name: "Interstate 46 E.P.", - Images: []*entity.AlbumImage{ - { - URL: "https://i.scdn.co/image/ab67616d0000b273b48630d6efcebca2596120c4", - Height: 640, - Width: 640, - }, - }, - }, - }, - }, nil) - }, - prepareMockPusherFn: func(m *mock_event.MockPusher) { - m.EXPECT().Push(&event.PushMessage{ - SessionID: "sessionID", - Msg: entity.EventInterrupt, - }) - }, - prepareMockUserRepoFn: func(m *mock_repository.MockUser) {}, - prepareMockSessionRepoFn: func(m *mock_repository.MockSession) { - m.EXPECT().FindByID(gomock.Any(), "sessionID").Return(&entity.Session{ - ID: "sessionID", - Name: "name", - CreatorID: "creatorID", - DeviceID: "deviceID", - StateType: "PLAY", - QueueHead: 1, - QueueTracks: []*entity.QueueTrack{ - {Index: 0, URI: "spotify:track:5uQ0vKy2973Y9IUCd1wMEF"}, - {Index: 1, URI: "spotify:track:hogehogehogehogehogeho"}, - }, - ExpiredAt: time.Time{}, - AllowToControlByOthers: false, - ProgressWhenPaused: 0, - }, nil) - m.EXPECT().Update(gomock.Any(), &entity.Session{ - ID: "sessionID", - Name: "name", - CreatorID: "creatorID", - DeviceID: "deviceID", - StateType: "STOP", - QueueHead: 1, - QueueTracks: []*entity.QueueTrack{ - {Index: 0, URI: "spotify:track:5uQ0vKy2973Y9IUCd1wMEF"}, - {Index: 1, URI: "spotify:track:hogehogehogehogehogeho"}, - }, - ExpiredAt: time.Time{}, - AllowToControlByOthers: false, - ProgressWhenPaused: 0, - }).Return(nil) - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - mockPlayer := mock_spotify.NewMockPlayer(ctrl) - tt.prepareMockPlayerFn(mockPlayer) - mockPusher := mock_event.NewMockPusher(ctrl) - tt.prepareMockPusherFn(mockPusher) - mockUserRepo := mock_repository.NewMockUser(ctrl) - tt.prepareMockUserRepoFn(mockUserRepo) - mockSessionRepo := mock_repository.NewMockSession(ctrl) - tt.prepareMockSessionRepoFn(mockSessionRepo) - - tmpWaitTimeBeforeHandleTrackEnd := waitTimeAfterHandleTrackEnd - waitTimeAfterHandleTrackEnd = 0 - defer func() { - waitTimeAfterHandleTrackEnd = tmpWaitTimeBeforeHandleTrackEnd - }() - - syncCheckTimerManager := entity.NewSyncCheckTimerManager() - - s := NewSessionTimerUseCase(mockSessionRepo, mockPlayer, mockPusher, syncCheckTimerManager) - - triggerAfterTrackEnd := s.tm.CreateExpiredTimer(tt.sessionID) - - if err := s.handleWaitTimerExpired(context.Background(), tt.sessionID, triggerAfterTrackEnd, tt.currentOperation); (err != nil) != tt.wantErr { - t.Errorf("handleWaitTimerExpired() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} diff --git a/web/handler/session.go b/web/handler/session.go index fa1e5a9d..2894157e 100644 --- a/web/handler/session.go +++ b/web/handler/session.go @@ -102,34 +102,6 @@ func (h *SessionHandler) Enqueue(c echo.Context) error { return c.NoContent(http.StatusNoContent) } -// NextTrack は PUT /sessions/:id/next に対応するハンドラーです。 -func (h *SessionHandler) NextTrack(c echo.Context) error { - logger := log.New() - - ctx := c.Request().Context() - id := c.Param("id") - - if err := h.stateUC.NextTrack(ctx, id); err != nil { - switch { - case errors.Is(err, entity.ErrSessionNotAllowToControlOthers): - return echo.NewHTTPError(http.StatusBadRequest, entity.ErrSessionNotAllowToControlOthers.Error()) - case errors.Is(err, entity.ErrChangeSessionStateNotPermit): - return echo.NewHTTPError(http.StatusBadRequest, entity.ErrChangeSessionStateNotPermit.Error()) - case errors.Is(err, entity.ErrNextQueueTrackNotFound): - return echo.NewHTTPError(http.StatusBadRequest, entity.ErrNextQueueTrackNotFound.Error()) - case errors.Is(err, entity.ErrSessionNotFound): - logger.Debug(err) - return echo.NewHTTPError(http.StatusNotFound, entity.ErrSessionNotFound.Error()) - case errors.Is(err, entity.ErrActiveDeviceNotFound): - logger.Debug(err) - return echo.NewHTTPError(http.StatusForbidden, entity.ErrActiveDeviceNotFound.Error()) - } - logger.Errorj(map[string]interface{}{"message": "failed to move to next track", "error": err.Error()}) - return echo.NewHTTPError(http.StatusInternalServerError) - } - return c.NoContent(http.StatusAccepted) -} - // State は PUT /sessions/:id/state に対応するハンドラーです。 func (h *SessionHandler) State(c echo.Context) error { logger := log.New() diff --git a/web/handler/session_state_test.go b/web/handler/session_state_test.go index 87d79109..8f68500d 100644 --- a/web/handler/session_state_test.go +++ b/web/handler/session_state_test.go @@ -241,7 +241,7 @@ func TestSessionHandler_State_PLAY(t *testing.T) { prepareMockPlayerFn: func(m *mock_spotify.MockPlayer) { m.EXPECT().SetRepeatMode(gomock.Any(), false, "device_id").Return(nil) m.EXPECT().SetShuffleMode(gomock.Any(), false, "device_id").Return(nil) - m.EXPECT().DeleteAllTracksInQueue(gomock.Any(), "device_id", "spotify:track:5uQ0vKy2973Y9IUCd1wMEF").Return(nil) + m.EXPECT().SkipAllTracks(gomock.Any(), "device_id", "spotify:track:5uQ0vKy2973Y9IUCd1wMEF").Return(nil) m.EXPECT().PlayWithTracks(gomock.Any(), "device_id", []string{"spotify:track:5uQ0vKy2973Y9IUCd1wMEF"}).Return(nil) m.EXPECT().Enqueue(gomock.Any(), "spotify:track:49BRCNV7E94s7Q2FUhhT3w", "device_id").Return(nil) m.EXPECT().Enqueue(gomock.Any(), "spotify:track:3", "device_id").Return(nil) diff --git a/web/handler/session_test.go b/web/handler/session_test.go index 757612a0..08b2538c 100644 --- a/web/handler/session_test.go +++ b/web/handler/session_test.go @@ -828,401 +828,6 @@ func TestUserHandler_GetActiveDevices(t *testing.T) { } } -func TestUserHandler_NextTrack(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - sessionID string - userID string - addToTimerSessionID string - prepareMockPlayerCliFn func(m *mock_spotify.MockPlayer) - prepareMockSessionRepoFn func(m *mock_repository.MockSession) - prepareMockPusherFn func(m *mock_event.MockPusher) - prepareMockTrackCliFn func(m *mock_spotify.MockTrackClient) - prepareMockUserRepoFn func(m *mock_repository.MockUser) - wantErr bool - wantCode int - }{ - { - name: "STOPかつ次の曲が存在する時に次の曲にSTOPのまま遷移,202", - sessionID: "sessionID", - userID: "userID", - addToTimerSessionID: "sessionID", - prepareMockPlayerCliFn: func(m *mock_spotify.MockPlayer) {}, - prepareMockSessionRepoFn: func(m *mock_repository.MockSession) { - m.EXPECT().FindByID(gomock.Any(), "sessionID").Return( - &entity.Session{ - ID: "sessionID", - Name: "name", - CreatorID: "creatorID", - DeviceID: "deviceID", - StateType: "STOP", - QueueHead: 0, - QueueTracks: []*entity.QueueTrack{ - { - Index: 0, - URI: "spotify:track:track_uri1", - SessionID: "sessionID", - }, - { - Index: 1, - URI: "spotify:track:track_uri2", - SessionID: "sessionID", - }, - }, - ExpiredAt: time.Time{}, - AllowToControlByOthers: true, - ProgressWhenPaused: 0, - }, nil) - m.EXPECT().Update(gomock.Any(), &entity.Session{ - ID: "sessionID", - Name: "name", - CreatorID: "creatorID", - DeviceID: "deviceID", - StateType: "STOP", - QueueHead: 1, - QueueTracks: []*entity.QueueTrack{ - { - Index: 0, - URI: "spotify:track:track_uri1", - SessionID: "sessionID", - }, - { - Index: 1, - URI: "spotify:track:track_uri2", - SessionID: "sessionID", - }, - }, - ExpiredAt: time.Time{}, - AllowToControlByOthers: true, - ProgressWhenPaused: 0, - }).Return(nil) - }, - prepareMockPusherFn: func(m *mock_event.MockPusher) { - m.EXPECT().Push(&event.PushMessage{ - SessionID: "sessionID", - Msg: entity.NewEventNextTrack(1), - }) - }, - prepareMockTrackCliFn: func(m *mock_spotify.MockTrackClient) {}, - prepareMockUserRepoFn: func(m *mock_repository.MockUser) {}, - wantErr: false, - wantCode: http.StatusAccepted, - }, - { - name: "STOPかつ次の曲が存在しない時にErrNextQueueTrackNotFound,400", - sessionID: "sessionID", - userID: "userID", - addToTimerSessionID: "sessionID", - prepareMockPlayerCliFn: func(m *mock_spotify.MockPlayer) {}, - prepareMockSessionRepoFn: func(m *mock_repository.MockSession) { - m.EXPECT().FindByID(gomock.Any(), "sessionID").Return( - &entity.Session{ - ID: "sessionID", - Name: "name", - CreatorID: "creatorID", - DeviceID: "deviceID", - StateType: "STOP", - QueueHead: 2, - QueueTracks: []*entity.QueueTrack{ - { - Index: 0, - URI: "spotify:track:track_uri1", - SessionID: "sessionID", - }, - { - Index: 1, - URI: "spotify:track:track_uri2", - SessionID: "sessionID", - }, - }, - ExpiredAt: time.Time{}, - AllowToControlByOthers: true, - ProgressWhenPaused: 0, - }, nil) - }, - prepareMockPusherFn: func(m *mock_event.MockPusher) {}, - prepareMockTrackCliFn: func(m *mock_spotify.MockTrackClient) {}, - prepareMockUserRepoFn: func(m *mock_repository.MockUser) {}, - wantErr: true, - wantCode: http.StatusBadRequest, - }, - { - name: "Archivedの際はどのような状態でも400", - sessionID: "sessionID", - userID: "userID", - addToTimerSessionID: "sessionID", - prepareMockPlayerCliFn: func(m *mock_spotify.MockPlayer) {}, - prepareMockSessionRepoFn: func(m *mock_repository.MockSession) { - m.EXPECT().FindByID(gomock.Any(), "sessionID").Return( - &entity.Session{ - ID: "sessionID", - Name: "name", - CreatorID: "creatorID", - DeviceID: "deviceID", - StateType: "ARCHIVED", - QueueHead: 0, - QueueTracks: nil, - ExpiredAt: time.Time{}, - AllowToControlByOthers: true, - ProgressWhenPaused: 0, - }, nil) - }, - prepareMockPusherFn: func(m *mock_event.MockPusher) {}, - prepareMockTrackCliFn: func(m *mock_spotify.MockTrackClient) {}, - prepareMockUserRepoFn: func(m *mock_repository.MockUser) {}, - wantErr: true, - wantCode: http.StatusBadRequest, - }, - { - name: "Playかつ次の曲が存在する時に次の曲にPlayのまま遷移,202", - sessionID: "sessionID", - userID: "userID", - addToTimerSessionID: "sessionID", - prepareMockPlayerCliFn: func(m *mock_spotify.MockPlayer) { - m.EXPECT().GoNextTrack(gomock.Any(), "deviceID").Return(nil) - }, - prepareMockSessionRepoFn: func(m *mock_repository.MockSession) { - m.EXPECT().FindByID(gomock.Any(), "sessionID").Return( - &entity.Session{ - ID: "sessionID", - Name: "name", - CreatorID: "creatorID", - DeviceID: "deviceID", - StateType: "PLAY", - QueueHead: 0, - QueueTracks: nil, - ExpiredAt: time.Time{}, - AllowToControlByOthers: true, - ProgressWhenPaused: 0, - }, nil) - }, - prepareMockPusherFn: func(m *mock_event.MockPusher) {}, - prepareMockTrackCliFn: func(m *mock_spotify.MockTrackClient) {}, - prepareMockUserRepoFn: func(m *mock_repository.MockUser) {}, - wantErr: false, - wantCode: http.StatusAccepted, - }, - { - name: "Playかつ次の曲が存在しても、AllowToControlByOthersかつcreatorでない時,400", - sessionID: "sessionID", - userID: "userID", - addToTimerSessionID: "sessionID", - prepareMockPlayerCliFn: func(m *mock_spotify.MockPlayer) {}, - prepareMockSessionRepoFn: func(m *mock_repository.MockSession) { - m.EXPECT().FindByID(gomock.Any(), "sessionID").Return( - &entity.Session{ - ID: "sessionID", - Name: "name", - CreatorID: "creatorID", - DeviceID: "deviceID", - StateType: "PLAY", - QueueHead: 0, - QueueTracks: nil, - ExpiredAt: time.Time{}, - AllowToControlByOthers: false, - ProgressWhenPaused: 0, - }, nil) - }, - prepareMockPusherFn: func(m *mock_event.MockPusher) {}, - prepareMockTrackCliFn: func(m *mock_spotify.MockTrackClient) {}, - prepareMockUserRepoFn: func(m *mock_repository.MockUser) {}, - wantErr: true, - wantCode: http.StatusBadRequest, - }, - { - name: "Pauseかつ次の曲が存在すると次の曲に遷移し、202", - sessionID: "sessionID", - userID: "userID", - addToTimerSessionID: "sessionID", - prepareMockPlayerCliFn: func(m *mock_spotify.MockPlayer) { - m.EXPECT().GoNextTrack(gomock.Any(), "deviceID").Return(nil) - m.EXPECT().Pause(gomock.Any(), "deviceID").Return(nil) - }, - prepareMockSessionRepoFn: func(m *mock_repository.MockSession) { - m.EXPECT().FindByID(gomock.Any(), "sessionID").Return( - &entity.Session{ - ID: "sessionID", - Name: "name", - CreatorID: "creatorID", - DeviceID: "deviceID", - StateType: "PAUSE", - QueueHead: 0, - QueueTracks: []*entity.QueueTrack{ - { - Index: 0, - URI: "spotify:track:track_uri1", - SessionID: "sessionID", - }, - { - Index: 1, - URI: "spotify:track:track_uri2", - SessionID: "sessionID", - }, - }, - ExpiredAt: time.Time{}, - AllowToControlByOthers: true, - ProgressWhenPaused: 0, - }, nil) - m.EXPECT().Update(gomock.Any(), &entity.Session{ - ID: "sessionID", - Name: "name", - CreatorID: "creatorID", - DeviceID: "deviceID", - StateType: "PAUSE", - QueueHead: 1, - QueueTracks: []*entity.QueueTrack{ - { - Index: 0, - URI: "spotify:track:track_uri1", - SessionID: "sessionID", - }, - { - Index: 1, - URI: "spotify:track:track_uri2", - SessionID: "sessionID", - }, - }, - ExpiredAt: time.Time{}, - AllowToControlByOthers: true, - ProgressWhenPaused: 0, - }) - }, - prepareMockPusherFn: func(m *mock_event.MockPusher) { - m.EXPECT().Push(&event.PushMessage{ - SessionID: "sessionID", - Msg: entity.NewEventNextTrack(1), - }) - }, - prepareMockTrackCliFn: func(m *mock_spotify.MockTrackClient) {}, - prepareMockUserRepoFn: func(m *mock_repository.MockUser) {}, - wantErr: false, - wantCode: http.StatusAccepted, - }, - { - name: "Pauseかつ次の曲が3曲存在すると次の曲に遷移し、三曲先がEnqueueされ、202", - sessionID: "sessionID", - userID: "userID", - addToTimerSessionID: "sessionID", - prepareMockPlayerCliFn: func(m *mock_spotify.MockPlayer) { - m.EXPECT().GoNextTrack(gomock.Any(), "deviceID").Return(nil) - m.EXPECT().Pause(gomock.Any(), "deviceID").Return(nil) - m.EXPECT().Enqueue(gomock.Any(), "spotify:track:track_uri4", "deviceID").Return(nil) - }, - prepareMockSessionRepoFn: func(m *mock_repository.MockSession) { - m.EXPECT().FindByID(gomock.Any(), "sessionID").Return( - &entity.Session{ - ID: "sessionID", - Name: "name", - CreatorID: "creatorID", - DeviceID: "deviceID", - StateType: "PAUSE", - QueueHead: 0, - QueueTracks: []*entity.QueueTrack{ - { - Index: 0, - URI: "spotify:track:track_uri1", - SessionID: "sessionID", - }, - { - Index: 1, - URI: "spotify:track:track_uri2", - SessionID: "sessionID", - }, - { - Index: 2, - URI: "spotify:track:track_uri3", - SessionID: "sessionID", - }, - { - Index: 3, - URI: "spotify:track:track_uri4", - SessionID: "sessionID", - }, - }, - ExpiredAt: time.Time{}, - AllowToControlByOthers: true, - ProgressWhenPaused: 0, - }, nil) - m.EXPECT().Update(gomock.Any(), &entity.Session{ - ID: "sessionID", - Name: "name", - CreatorID: "creatorID", - DeviceID: "deviceID", - StateType: "PAUSE", - QueueHead: 1, - QueueTracks: []*entity.QueueTrack{ - { - Index: 0, - URI: "spotify:track:track_uri1", - SessionID: "sessionID", - }, - { - Index: 1, - URI: "spotify:track:track_uri2", - SessionID: "sessionID", - }, - { - Index: 2, - URI: "spotify:track:track_uri3", - SessionID: "sessionID", - }, - { - Index: 3, - URI: "spotify:track:track_uri4", - SessionID: "sessionID", - }, - }, - ExpiredAt: time.Time{}, - AllowToControlByOthers: true, - ProgressWhenPaused: 0, - }) - }, - prepareMockPusherFn: func(m *mock_event.MockPusher) { - m.EXPECT().Push(&event.PushMessage{ - SessionID: "sessionID", - Msg: entity.NewEventNextTrack(1), - }) - }, - prepareMockTrackCliFn: func(m *mock_spotify.MockTrackClient) {}, - prepareMockUserRepoFn: func(m *mock_repository.MockUser) {}, - wantErr: false, - wantCode: http.StatusAccepted, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - e := echo.New() - req := httptest.NewRequest(http.MethodPut, "/", nil) - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) - c.SetPath("/sessions/:id/devices") - c.SetParamNames("id") - c.SetParamValues(tt.sessionID) - c = setToContext(c, tt.userID, nil) - - // モックの準備 - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - h := newSessionHandlerForTest(t, ctrl, tt.prepareMockPlayerCliFn, tt.prepareMockTrackCliFn, - tt.prepareMockPusherFn, tt.prepareMockUserRepoFn, tt.prepareMockSessionRepoFn, tt.addToTimerSessionID) - - err := h.NextTrack(c) - if (err != nil) != tt.wantErr { - t.Errorf("NextTrack() error = %v, wantErr %v", err, tt.wantErr) - } - // ステータスコードのチェック - if er, ok := err.(*echo.HTTPError); (ok && er.Code != tt.wantCode) || (!ok && rec.Code != tt.wantCode) { - t.Errorf("NextTrack() code = %d, want = %d", rec.Code, tt.wantCode) - } - - }) - } -} - func convToPointer(given int64) *int64 { return &given } @@ -1251,11 +856,9 @@ func newSessionHandlerForTest( prepareMockSessionRepoFn(mockSessionRepo) syncCheckTimerManager := entity.NewSyncCheckTimerManager() if sessionID != "" { - timer := syncCheckTimerManager.CreateExpiredTimer(sessionID) - timer.SetDuration(5 * time.Minute) + syncCheckTimerManager.CreateTimer(sessionID, 5*time.Minute) } timerUC := usecase.NewSessionTimerUseCase(mockSessionRepo, mockPlayer, mockPusher, syncCheckTimerManager) uc := usecase.NewSessionUseCase(mockSessionRepo, mockUserRepo, mockPlayer, mockTrackCli, nil, mockPusher, timerUC) - stateUC := usecase.NewSessionStateUseCase(mockSessionRepo, mockPlayer, mockPusher, timerUC) - return &SessionHandler{uc: uc, stateUC: stateUC} + return &SessionHandler{uc: uc} } diff --git a/web/router.go b/web/router.go index 14694aec..59cde146 100644 --- a/web/router.go +++ b/web/router.go @@ -71,7 +71,6 @@ func NewServer(authUC *usecase.AuthUseCase, userUC *usecase.UserUseCase, session sessionWithCreatorToken.PUT("/devices", sessionHandler.SetDevice) sessionWithCreatorToken.POST("/queue", sessionHandler.Enqueue) sessionWithCreatorToken.PUT("/state", sessionHandler.State) - sessionWithCreatorToken.PUT("/next", sessionHandler.NextTrack) sessionWithCreatorToken.GET("/ws", wsHandler.WebSocket) return e }