fix(wave): switch to the new context right away when activity or mood changes
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m38s
ci / ts / ts (packages + web) (push) Successful in 1m47s
release / release (версия и заметки) (push) Successful in 12s
release / android (APK в релиз) (push) Failing after 34m58s

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 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Opus 5.5 committed 2026-10-20 19:18:26 +03:00
1 parent 7cf741d82f
commit 6ad49fe0e3
2 files changed
+38 -4

No files matched your search

+22 -4
View File
@@ -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();