From 6ad49fe0e3eb1843b412d2c6e5c7b0bb1803851f Mon Sep 17 00:00:00 2001 From: mrleo1nid <43583385+mrleo1nid@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:18:26 +0300 Subject: [PATCH] fix(wave): switch to the new context right away when activity or mood changes Changing the activity or mood used to keep the current track playing and only replace the upcoming ones, so nothing seemed to happen. Now the first track of the new batch starts immediately (web and Android). The interrupted track is not reported to the wave as a skip and is recorded in history only if it played for at least 30 s, so a context change does not teach the recommender a false dislike. Co-Authored-By: Claude Opus 5.5 --- apps/mobile/src/player/controller.ts | 16 ++++++++++++++++ apps/web/src/player/player.ts | 26 ++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/apps/mobile/src/player/controller.ts b/apps/mobile/src/player/controller.ts index eb3c9d7..f51c396 100644 --- a/apps/mobile/src/player/controller.ts +++ b/apps/mobile/src/player/controller.ts @@ -90,6 +90,7 @@ class Controller { private lastSave = 0; private waveFeedback: { track_id: string; outcome: WaveOutcome }[] = []; private waveLoading = false; + private neutralSwitch = false; // следующая смена трека — из-за нового контекста волны private setupDone: Promise | null = null; private listeners = new Set<() => void>(); private state: PlayerState = { @@ -189,6 +190,14 @@ class Controller { if (!a) return; this.attempt = null; const pos = Math.round(lastPositionS * 1000); + if (this.neutralSwitch) { + this.neutralSwitch = false; + if (a.playedMs >= EARLY_SKIP_MS) { + this.events?.push({ type: 'skip', track_id: a.track.id, position_ms: pos, played_ms: Math.round(a.playedMs) }); + void this.events?.flush(); + } + return; + } const kind = ended ? 'play_end' : a.outcome(pos); this.events?.push({ type: kind, track_id: a.track.id, position_ms: pos, played_ms: Math.round(a.playedMs) }); void this.events?.flush(); @@ -316,6 +325,13 @@ class Controller { ); await this.replaceUpcoming(res.tracks); this.update({ wave: { ...wave, activity: res.activity, mood: res.mood, fallback: res.fallback } }); + // Новый контекст звучит сразу; прерванный трек — не скип для волны (как в вебе). + if (res.tracks.some(playable)) { + this.neutralSwitch = true; + await TrackPlayer.skipToNext().catch(() => { + this.neutralSwitch = false; + }); + } } private leaveWave() { diff --git a/apps/web/src/player/player.ts b/apps/web/src/player/player.ts index 8d47210..5594232 100644 --- a/apps/web/src/player/player.ts +++ b/apps/web/src/player/player.ts @@ -186,7 +186,10 @@ export class Player { void this.load(true); } - /** Сменить занятие / настроение на лету: текущий трек доигрывает, дальше — новая пачка. */ + /** + * Сменить занятие / настроение на лету: сразу играет первый трек новой пачки (docs/recommendations.md#6.7). + * Прерванный трек — не скип: волне сигнал не уходит, в историю он попадает, только если играл ≥ 30 с. + */ async setWaveContext(activity: string | null, mood: string | null) { const wave = this.state.wave; if (!wave) return this.startWave(activity, mood); @@ -197,10 +200,17 @@ export class Player { body: { activity, mood }, }), ); + const fresh = res.tracks.filter((t) => t.available && t.stream_url); this.queue.dropUpcoming(); - this.queue.append(res.tracks.filter((t) => t.available && t.stream_url)); + this.queue.append(fresh); this.update({ wave: { ...wave, activity: res.activity, mood: res.mood, fallback: res.fallback } }); - this.syncQueue(); + if (fresh.length && this.queue.next()) { + this.finishAttempt('skip', true); + this.syncQueue(); + void this.load(true); + } else { + this.syncQueue(); + } } stopWave() { @@ -395,11 +405,19 @@ export class Player { } /** Завершает текущую попытку: событие с реально прослушанным временем. */ - private finishAttempt(type: 'play_end' | 'skip') { + /** neutral — трек прерван сменой контекста волны: не отрицательный сигнал и без короткого «скипа» в событиях. */ + private finishAttempt(type: 'play_end' | 'skip', neutral = false) { const a = this.attempt; if (!a) return; this.attempt = null; const pos = Math.round(this.audio.currentTime * 1000); + if (neutral) { + if (a.playedMs >= EARLY_SKIP_MS) { + this.events.push({ type: 'skip', track_id: a.track.id, position_ms: pos, played_ms: Math.round(a.playedMs) }); + void this.events.flush(); + } + return; + } const kind = type === 'skip' ? a.outcome(pos) : 'play_end'; this.events.push({ type: kind, track_id: a.track.id, position_ms: pos, played_ms: Math.round(a.playedMs) }); void this.events.flush();