up
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
/* eslint-disable camelcase */
|
||||
// @ts-nocheck
|
||||
|
||||
export type PlaylistGroupEndMode =
|
||||
| "PLAYLIST_GROUP_END_MODE_REPEAT"
|
||||
| "PLAYLIST_GROUP_END_MODE_STOP"
|
||||
| "PLAYLIST_GROUP_END_MODE_NEXT";
|
||||
export type ScreenMode =
|
||||
| "SCREEN_MODE_UNSPECIFIED"
|
||||
| "SCREEN_MODE_COVER"
|
||||
@@ -208,17 +212,65 @@ export type DownloadFileReq = {
|
||||
filename: string | undefined;
|
||||
};
|
||||
|
||||
export type PlaylistTrack = {
|
||||
export type PlaylistGroup = {
|
||||
ID: number | undefined;
|
||||
name: string | undefined;
|
||||
filename: string | undefined;
|
||||
position: number | undefined;
|
||||
endMode: PlaylistGroupEndMode | undefined;
|
||||
};
|
||||
|
||||
export type SavePlaylistGroupReq = {
|
||||
playlistID: number | undefined;
|
||||
groupID: number | undefined;
|
||||
name: string | undefined;
|
||||
endMode: PlaylistGroupEndMode | undefined;
|
||||
};
|
||||
|
||||
export type SavePlaylistGroupRsp = {
|
||||
playlist: Playlist | undefined;
|
||||
};
|
||||
|
||||
export type Playlist = {
|
||||
ID: number | undefined;
|
||||
name: string | undefined;
|
||||
tracks: PlaylistTrack[] | undefined;
|
||||
groups: PlaylistGroup[] | undefined;
|
||||
};
|
||||
|
||||
export type PlaylistTrack = {
|
||||
ID: number | undefined;
|
||||
name: string | undefined;
|
||||
filename: string | undefined;
|
||||
position: number | undefined;
|
||||
groupId: number | undefined;
|
||||
};
|
||||
|
||||
export type DeletePlaylistGroupReq = {
|
||||
playlistID: number | undefined;
|
||||
groupID: number | undefined;
|
||||
};
|
||||
|
||||
export type DeletePlaylistGroupRsp = {
|
||||
playlist: Playlist | undefined;
|
||||
};
|
||||
|
||||
export type ReorderPlaylistGroupsReq = {
|
||||
playlistID: number | undefined;
|
||||
groupIDs: number[] | undefined;
|
||||
};
|
||||
|
||||
export type ReorderPlaylistGroupsRsp = {
|
||||
playlist: Playlist | undefined;
|
||||
};
|
||||
|
||||
export type SetPlaylistTrackGroupReq = {
|
||||
playlistID: number | undefined;
|
||||
trackID: number | undefined;
|
||||
groupID: number | undefined;
|
||||
};
|
||||
|
||||
export type SetPlaylistTrackGroupRsp = {
|
||||
playlist: Playlist | undefined;
|
||||
};
|
||||
|
||||
export type GetPlaylistsReq = {
|
||||
@@ -649,6 +701,7 @@ export type MusicState = {
|
||||
loop: boolean | undefined;
|
||||
playback: MusicPlaybackState | undefined;
|
||||
stateVersion: number | undefined;
|
||||
groupId: number | undefined;
|
||||
};
|
||||
|
||||
export type CreateScreenSessionReq = {
|
||||
@@ -695,6 +748,7 @@ export type SetGameMusicReq = {
|
||||
volume: number | undefined;
|
||||
loop: boolean | undefined;
|
||||
playback: MusicPlaybackState | undefined;
|
||||
groupId: number | undefined;
|
||||
};
|
||||
|
||||
export type SetGameMusicRsp = {
|
||||
@@ -704,6 +758,7 @@ export type SetGameMusicRsp = {
|
||||
export type AdvanceScreenMusicReq = {
|
||||
screenID: string | undefined;
|
||||
accessToken: string | undefined;
|
||||
expectedStateVersion: string | undefined;
|
||||
};
|
||||
|
||||
export type AdvanceScreenMusicRsp = {
|
||||
@@ -1429,6 +1484,10 @@ export interface EveningDetectiveServer {
|
||||
GetPermissions(request: GetPermissionsReq): Promise<GetPermissionsRsp>;
|
||||
UploadFile(request: UploadFileReq): Promise<UploadFileRsp>;
|
||||
DownloadFile(request: DownloadFileReq): Promise<googleapi_HttpBody>;
|
||||
SavePlaylistGroup(request: SavePlaylistGroupReq): Promise<SavePlaylistGroupRsp>;
|
||||
DeletePlaylistGroup(request: DeletePlaylistGroupReq): Promise<DeletePlaylistGroupRsp>;
|
||||
ReorderPlaylistGroups(request: ReorderPlaylistGroupsReq): Promise<ReorderPlaylistGroupsRsp>;
|
||||
SetPlaylistTrackGroup(request: SetPlaylistTrackGroupReq): Promise<SetPlaylistTrackGroupRsp>;
|
||||
GetPlaylists(request: GetPlaylistsReq): Promise<GetPlaylistsRsp>;
|
||||
CreatePlaylist(request: CreatePlaylistReq): Promise<CreatePlaylistRsp>;
|
||||
UpdatePlaylist(request: UpdatePlaylistReq): Promise<UpdatePlaylistRsp>;
|
||||
@@ -2066,6 +2125,92 @@ export function createEveningDetectiveServerClient(
|
||||
method: "DownloadFile",
|
||||
}) as Promise<googleapi_HttpBody>;
|
||||
},
|
||||
SavePlaylistGroup(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
if (!request.playlistID) {
|
||||
throw new Error("missing required field request.playlistID");
|
||||
}
|
||||
const path = `api/playlists/${request.playlistID}/groups`; // eslint-disable-line quotes
|
||||
const body = JSON.stringify(request);
|
||||
const queryParams: string[] = [];
|
||||
let uri = path;
|
||||
if (queryParams.length > 0) {
|
||||
uri += `?${queryParams.join("&")}`
|
||||
}
|
||||
return handler({
|
||||
path: uri,
|
||||
method: "POST",
|
||||
body,
|
||||
}, {
|
||||
service: "EveningDetectiveServer",
|
||||
method: "SavePlaylistGroup",
|
||||
}) as Promise<SavePlaylistGroupRsp>;
|
||||
},
|
||||
DeletePlaylistGroup(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
if (!request.playlistID) {
|
||||
throw new Error("missing required field request.playlistID");
|
||||
}
|
||||
if (!request.groupID) {
|
||||
throw new Error("missing required field request.groupID");
|
||||
}
|
||||
const path = `api/playlists/${request.playlistID}/groups/${request.groupID}`; // eslint-disable-line quotes
|
||||
const body = null;
|
||||
const queryParams: string[] = [];
|
||||
let uri = path;
|
||||
if (queryParams.length > 0) {
|
||||
uri += `?${queryParams.join("&")}`
|
||||
}
|
||||
return handler({
|
||||
path: uri,
|
||||
method: "DELETE",
|
||||
body,
|
||||
}, {
|
||||
service: "EveningDetectiveServer",
|
||||
method: "DeletePlaylistGroup",
|
||||
}) as Promise<DeletePlaylistGroupRsp>;
|
||||
},
|
||||
ReorderPlaylistGroups(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
if (!request.playlistID) {
|
||||
throw new Error("missing required field request.playlistID");
|
||||
}
|
||||
const path = `api/playlists/${request.playlistID}/groups/order`; // eslint-disable-line quotes
|
||||
const body = JSON.stringify(request);
|
||||
const queryParams: string[] = [];
|
||||
let uri = path;
|
||||
if (queryParams.length > 0) {
|
||||
uri += `?${queryParams.join("&")}`
|
||||
}
|
||||
return handler({
|
||||
path: uri,
|
||||
method: "PUT",
|
||||
body,
|
||||
}, {
|
||||
service: "EveningDetectiveServer",
|
||||
method: "ReorderPlaylistGroups",
|
||||
}) as Promise<ReorderPlaylistGroupsRsp>;
|
||||
},
|
||||
SetPlaylistTrackGroup(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
if (!request.playlistID) {
|
||||
throw new Error("missing required field request.playlistID");
|
||||
}
|
||||
if (!request.trackID) {
|
||||
throw new Error("missing required field request.trackID");
|
||||
}
|
||||
const path = `api/playlists/${request.playlistID}/tracks/${request.trackID}/group`; // eslint-disable-line quotes
|
||||
const body = JSON.stringify(request);
|
||||
const queryParams: string[] = [];
|
||||
let uri = path;
|
||||
if (queryParams.length > 0) {
|
||||
uri += `?${queryParams.join("&")}`
|
||||
}
|
||||
return handler({
|
||||
path: uri,
|
||||
method: "PUT",
|
||||
body,
|
||||
}, {
|
||||
service: "EveningDetectiveServer",
|
||||
method: "SetPlaylistTrackGroup",
|
||||
}) as Promise<SetPlaylistTrackGroupRsp>;
|
||||
},
|
||||
GetPlaylists(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
const path = `api/playlists`; // eslint-disable-line quotes
|
||||
const body = null;
|
||||
|
||||
+51
-1
@@ -67,6 +67,9 @@ async function fixture(t: TestContext, secure = true) {
|
||||
refreshCalls: 0,
|
||||
refreshResponse: null as (() => Promise<Response>) | null,
|
||||
advanceCalls: 0,
|
||||
advanceRequest: null as Record<string, unknown> | null,
|
||||
advanceResponse: null as Record<string, unknown> | null,
|
||||
advanceError: 0,
|
||||
resultError: false,
|
||||
teamsError: false,
|
||||
screenTeams: [{ ID: '2', name: 'Ястребы' }, { ID: '1', name: 'Агенты' }],
|
||||
@@ -140,7 +143,9 @@ async function fixture(t: TestContext, secure = true) {
|
||||
}
|
||||
if (path === '/api/screens/music/advance') {
|
||||
model.advanceCalls++
|
||||
return json({ state: objectMusic(3, 1) })
|
||||
model.advanceRequest = JSON.parse(String(init?.body)) as Record<string, unknown>
|
||||
if (model.advanceError) return json({}, model.advanceError)
|
||||
return json({ state: model.advanceResponse ?? objectMusic(3, 1) })
|
||||
}
|
||||
if (path === '/api/screens/teams') return model.teamsError ? json({}, 500) : json({ teams: model.screenTeams })
|
||||
if (path === '/api/screens/results') {
|
||||
@@ -258,6 +263,7 @@ async function fixture(t: TestContext, secure = true) {
|
||||
playback: body.playback,
|
||||
volume: body.volume,
|
||||
loop: body.loop,
|
||||
groupId: body.groupId,
|
||||
positionMs: body.positionMs,
|
||||
}
|
||||
return json({ state: model.remote.music })
|
||||
@@ -2240,3 +2246,47 @@ test('список команд обновляется, сохраняется
|
||||
await restored.select(null)
|
||||
assert.equal(c.state.teams, null)
|
||||
})
|
||||
|
||||
for (const action of ['volume', 'pause', 'resume', 'replay'] as const) {
|
||||
test(`группа сохраняется при команде ${action} нативного плеера`, async (t) => {
|
||||
const f = await fixture(t)
|
||||
await f.prepare()
|
||||
f.controller.output(true)
|
||||
f.model.remote.music = { ...objectMusic(2), groupId: 42 }
|
||||
await f.online.poll()
|
||||
await f.online.planCommand(action === 'volume' ? { action, channel: 'music', volume: 55 } : { action, channel: 'music' })
|
||||
assert.equal((f.model.remote.music as { groupId: number }).groupId, 42)
|
||||
await assert.rejects(f.online.planCommand({ action: 'next', channel: 'music' }), /активной группы/)
|
||||
await assert.rejects(f.online.planCommand({ action: 'track', channel: 'music', trackIndex: 1 }), /активной группы/)
|
||||
await assert.rejects(f.online.planCommand({ action: 'loop', channel: 'music', enabled: true }), /активной группы/)
|
||||
})
|
||||
}
|
||||
|
||||
test('повтор одного трека группы передаёт точную версию и перезапускает трек', async (t) => {
|
||||
const f = await fixture(t)
|
||||
await f.prepare()
|
||||
f.controller.output(true)
|
||||
const version = '9007199254740995'
|
||||
f.model.remote.music = { ...objectMusic(2), stateVersion: version, groupId: 42, positionMs: 0 }
|
||||
await f.online.poll()
|
||||
send(f.controller, { action: 'play', channel: 'music' })
|
||||
const before = f.controller.state.channels.music.operation
|
||||
f.model.advanceResponse = { ...objectMusic(3), stateVersion: '9007199254740996', groupId: 42, positionMs: 0 }
|
||||
await f.online.musicEnded({ session: f.controller.state.session, channel: 'music', operation: before, status: 'ended', position: 0.1, duration: 0.1 })
|
||||
assert.equal(f.model.advanceRequest?.expectedStateVersion, version)
|
||||
assert.ok(f.controller.state.channels.music.operation > before)
|
||||
assert.equal(f.controller.state.channels.music.trackIndex, 0)
|
||||
})
|
||||
|
||||
test('группа останавливается при потере связи вместо выхода в общий список', async (t) => {
|
||||
const f = await fixture(t)
|
||||
await f.prepare()
|
||||
f.controller.output(true)
|
||||
f.model.remote.music = { ...objectMusic(2), groupId: 42 }
|
||||
await f.online.poll()
|
||||
send(f.controller, { action: 'play', channel: 'music' })
|
||||
f.model.advanceError = 503
|
||||
await f.online.musicEnded({ session: f.controller.state.session, channel: 'music', operation: f.controller.state.channels.music.operation, status: 'ended', position: 4, duration: 4 })
|
||||
assert.equal(f.controller.state.channels.music.desired, 'stopped')
|
||||
assert.equal(f.controller.state.channels.music.trackIndex, 0)
|
||||
})
|
||||
|
||||
+16
-1
@@ -624,6 +624,14 @@ export class Online {
|
||||
async planCommand(intent: Intent) {
|
||||
if (this.busy) throw new Error('Подождите завершения текущего действия')
|
||||
const pkg = this.selectedGame()
|
||||
const activeMusic = this.received?.music
|
||||
// shortcut: native track navigation and completion controls have no group catalogue; use website group controls until it does.
|
||||
if (
|
||||
activeMusic?.groupId &&
|
||||
'channel' in intent && intent.channel === 'music' &&
|
||||
(intent.action === 'next' || intent.action === 'track' || intent.action === 'loop')
|
||||
)
|
||||
throw new Error('Треки и режим завершения активной группы меняются через сайт. Для обычного списка выберите весь плейлист.')
|
||||
const localVersion = ++this.localCommandVersion
|
||||
const localGeneration = this.generation
|
||||
const localSession = this.controller.state.session
|
||||
@@ -707,6 +715,7 @@ export class Online {
|
||||
await this.client(true, AbortSignal.timeout(5000)).SetGameMusic({
|
||||
gameID: pkg.package.game.id,
|
||||
playlistID: c.playlistId ?? 0,
|
||||
groupId: c.playlistId === activeMusic?.playlistId ? activeMusic.groupId : 0,
|
||||
trackIndex: c.trackIndex,
|
||||
positionMs: Math.max(0, Math.round(c.position * 1000)),
|
||||
volume: Math.round(c.volume),
|
||||
@@ -1673,6 +1682,7 @@ export class Online {
|
||||
await client.SetGameMusic({
|
||||
gameID: pkg.package.game.id,
|
||||
playlistID: id,
|
||||
groupId: 0,
|
||||
trackIndex: 0,
|
||||
positionMs: 0,
|
||||
volume: this.controller.state.channels.music.volume,
|
||||
@@ -1903,10 +1913,13 @@ export class Online {
|
||||
const result = await this.client(false, AbortSignal.timeout(5000)).AdvanceScreenMusic({
|
||||
screenID: this.screen.id,
|
||||
accessToken: this.screen.token,
|
||||
expectedStateVersion: this.received?.music?.version.toString(),
|
||||
})
|
||||
if (generation !== this.generation || report.session !== this.controller.state.session) return
|
||||
const music = parseRemote({ music: result.state }).music
|
||||
if (this.received) {
|
||||
if (music && this.received.music && music.version > this.received.music.version && music.trackId === this.received.music.trackId)
|
||||
this.musicApplied = null
|
||||
this.received = { ...this.received, music }
|
||||
await this.applyReceived(true)
|
||||
}
|
||||
@@ -1914,7 +1927,9 @@ export class Online {
|
||||
if (generation !== this.generation || report.session !== this.controller.state.session) return
|
||||
const pkg = this.selectedGame()?.package
|
||||
const playlist = pkg?.playlists.find((p) => p.id === c.playlistId)
|
||||
if (pkg && playlist) {
|
||||
if (this.received?.music?.groupId) {
|
||||
send(this.controller, { action: 'stop', channel: 'music' })
|
||||
} else if (pkg && playlist) {
|
||||
const nextIndex = c.trackIndex + 1
|
||||
const track =
|
||||
playlist.tracks[nextIndex < playlist.tracks.length ? nextIndex : c.loop ? 0 : -1]
|
||||
|
||||
@@ -79,6 +79,7 @@ export interface RemoteState {
|
||||
music: {
|
||||
version: bigint
|
||||
playlistId: number | null
|
||||
groupId: number
|
||||
trackId: number | null
|
||||
trackIds: number[]
|
||||
filenames: string[]
|
||||
@@ -156,6 +157,7 @@ export function parseRemote(value: unknown): RemoteState {
|
||||
const index = integer(m.trackIndex ?? 0, 0, Number.MAX_SAFE_INTEGER, 'state.music.trackIndex')
|
||||
music = {
|
||||
version: version(m.stateVersion, 'state.music.stateVersion'),
|
||||
groupId: integer(m.groupId ?? 0, 0, Number.MAX_SAFE_INTEGER, 'state.music.groupId'),
|
||||
playlistId: p ? integer(p.ID, 1, Number.MAX_SAFE_INTEGER, 'state.music.playlist.ID') : null,
|
||||
trackId: trackIds[index] ?? null,
|
||||
trackIds,
|
||||
|
||||
Reference in New Issue
Block a user