up
This commit is contained in:
@@ -259,6 +259,15 @@ export type UploadPlaylistTrackRsp = {
|
||||
playlist: Playlist | undefined;
|
||||
};
|
||||
|
||||
export type ReorderPlaylistTracksReq = {
|
||||
playlistID: number | undefined;
|
||||
trackIDs: number[] | undefined;
|
||||
};
|
||||
|
||||
export type ReorderPlaylistTracksRsp = {
|
||||
playlist: Playlist | undefined;
|
||||
};
|
||||
|
||||
export type DeletePlaylistTrackReq = {
|
||||
playlistID: number | undefined;
|
||||
trackID: number | undefined;
|
||||
@@ -697,6 +706,7 @@ export type BindScreenReq = {
|
||||
screenID: string | undefined;
|
||||
accessToken: string | undefined;
|
||||
gameID: number | undefined;
|
||||
initializeAudio: boolean | undefined;
|
||||
};
|
||||
|
||||
export type BindScreenRsp = {
|
||||
@@ -1395,6 +1405,7 @@ export interface EveningDetectiveServer {
|
||||
UpdatePlaylist(request: UpdatePlaylistReq): Promise<UpdatePlaylistRsp>;
|
||||
DeletePlaylist(request: DeletePlaylistReq): Promise<DeletePlaylistRsp>;
|
||||
UploadPlaylistTrack(request: UploadPlaylistTrackReq): Promise<UploadPlaylistTrackRsp>;
|
||||
ReorderPlaylistTracks(request: ReorderPlaylistTracksReq): Promise<ReorderPlaylistTracksRsp>;
|
||||
DeletePlaylistTrack(request: DeletePlaylistTrackReq): Promise<DeletePlaylistTrackRsp>;
|
||||
SetScenarioPlaylist(request: SetScenarioPlaylistReq): Promise<SetScenarioPlaylistRsp>;
|
||||
AddScenario(request: AddScenarioReq): Promise<AddScenarioRsp>;
|
||||
@@ -2119,6 +2130,26 @@ export function createEveningDetectiveServerClient(
|
||||
method: "UploadPlaylistTrack",
|
||||
}) as Promise<UploadPlaylistTrackRsp>;
|
||||
},
|
||||
ReorderPlaylistTracks(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}/tracks/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: "ReorderPlaylistTracks",
|
||||
}) as Promise<ReorderPlaylistTracksRsp>;
|
||||
},
|
||||
DeletePlaylistTrack(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
if (!request.playlistID) {
|
||||
throw new Error("missing required field request.playlistID");
|
||||
|
||||
@@ -163,7 +163,7 @@ test('музыка независима от основного канала, mu
|
||||
assert.equal(c.state.channels.main.position, 20)
|
||||
assert.equal(c.state.channels.music.desired, 'playing')
|
||||
assert.equal(c.state.channels.music.position, 15)
|
||||
assert.equal(c.state.channels.music.volume, 35)
|
||||
assert.equal(c.state.channels.music.volume, 100)
|
||||
send(c, { action: 'mute', enabled: false })
|
||||
assert.equal(c.state.channels.main.volume, 80)
|
||||
assert.throws(
|
||||
@@ -237,3 +237,23 @@ test('Range: полный файл, границы, суффикс и отказ
|
||||
for (const value of ['bytes=100-', 'bytes=20-10', 'bytes=-0', 'bytes=0-1,3-5', 'bytes=-', 'abc'])
|
||||
assert.equal(parseRange(value, 100), null)
|
||||
})
|
||||
|
||||
test('обновление порядка сохраняет текущий файл и операцию, следующий трек берётся из нового порядка', () => {
|
||||
const c = controller()
|
||||
const pkg = testPackage()
|
||||
c.selectPackage(pkg)
|
||||
send(c, { action: 'play', channel: 'music' })
|
||||
ack(c, 'music', 'playing', 23)
|
||||
const before = structuredClone(c.state.channels.music)
|
||||
pkg.package.playlists[0]!.tracks.reverse()
|
||||
c.selectPackage(pkg, true)
|
||||
const after = c.state.channels.music
|
||||
assert.equal(after.trackIndex, 1)
|
||||
assert.equal(after.source?.url, before.source?.url)
|
||||
assert.equal(after.operation, before.operation)
|
||||
assert.equal(after.position, 23)
|
||||
assert.equal(after.desired, 'playing')
|
||||
send(c, { action: 'loop', channel: 'music', enabled: true })
|
||||
send(c, { action: 'next', channel: 'music' })
|
||||
assert.equal(c.state.channels.music.trackIndex, 0)
|
||||
})
|
||||
|
||||
@@ -144,7 +144,7 @@ function initialChannel(): ChannelState {
|
||||
status: 'idle',
|
||||
position: 0,
|
||||
duration: null,
|
||||
volume: 35,
|
||||
volume: 100,
|
||||
error: '',
|
||||
playlistId: null,
|
||||
trackIndex: 0,
|
||||
@@ -268,6 +268,10 @@ export class Controller {
|
||||
if (preserve && catalog.key === this.state.catalog.key) {
|
||||
catalog.cover = this.state.catalog.cover
|
||||
catalog.background = this.state.catalog.background
|
||||
const music = this.state.channels.music
|
||||
const tracks = catalog.playlists.find((p) => p.id === music.playlistId)?.tracks
|
||||
const index = tracks?.findIndex((t) => t.source.url === music.source?.url) ?? -1
|
||||
if (index >= 0) music.trackIndex = index
|
||||
this.state.catalog = catalog
|
||||
this.broadcast()
|
||||
return
|
||||
|
||||
+15
-2
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
app,
|
||||
dialog,
|
||||
BrowserWindow,
|
||||
ipcMain,
|
||||
protocol,
|
||||
@@ -10,8 +11,8 @@ import {
|
||||
powerSaveBlocker,
|
||||
} from 'electron'
|
||||
import { createReadStream } from 'node:fs'
|
||||
import { stat } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { readFile, stat } from 'node:fs/promises'
|
||||
import { basename, join } from 'node:path'
|
||||
import { Readable } from 'node:stream'
|
||||
import { DiagnosticLog } from './diagnostics.js'
|
||||
import { Controller, parseCommand } from './controller.js'
|
||||
@@ -304,6 +305,18 @@ app
|
||||
},
|
||||
undefined,
|
||||
(failure) => diagnostics?.record('request-error', failure),
|
||||
async () => {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ['openFile'],
|
||||
filters: [{ name: 'MP3', extensions: ['mp3'] }],
|
||||
})
|
||||
const path = result.filePaths[0]
|
||||
if (result.canceled || !path) return null
|
||||
const info = await stat(path)
|
||||
if (!info.isFile() || !/\.mp3$/i.test(path) || !info.size || info.size > 64 * 1048576)
|
||||
throw new Error('Выберите MP3 размером до 64 МиБ')
|
||||
return { filename: basename(path), data: await readFile(path) }
|
||||
},
|
||||
)
|
||||
await online.initialize()
|
||||
let previousConnection = online.state.connection
|
||||
|
||||
+120
-4
@@ -34,6 +34,19 @@ async function fixture(t: TestContext, secure = true) {
|
||||
},
|
||||
}
|
||||
const model = {
|
||||
library: [
|
||||
{
|
||||
ID: 1,
|
||||
name: 'Музыка',
|
||||
tracks: [
|
||||
{ ID: 1, name: 'Один' },
|
||||
{ ID: 2, name: 'Два' },
|
||||
],
|
||||
},
|
||||
],
|
||||
playlistError: 0,
|
||||
upload: null as { filename: string; data: Buffer } | null,
|
||||
bindings: [] as Record<string, unknown>[],
|
||||
offline: false,
|
||||
server: 'https://server.test',
|
||||
introURL: 'https://media.test/intro.wav',
|
||||
@@ -233,7 +246,34 @@ async function fixture(t: TestContext, secure = true) {
|
||||
}
|
||||
return json({})
|
||||
}
|
||||
if (path.endsWith('/bind') || path.endsWith('/sound')) return json({})
|
||||
if (path.endsWith('/bind')) {
|
||||
model.bindings.push(JSON.parse(String(init?.body)))
|
||||
return json({})
|
||||
}
|
||||
if (path.endsWith('/sound')) return json({})
|
||||
if (path.startsWith('/api/playlists')) {
|
||||
if (model.playlistError) return json({}, model.playlistError)
|
||||
const data = init?.body ? JSON.parse(String(init.body)) : {}
|
||||
if (path === '/api/playlists') {
|
||||
if (init?.method === 'POST') model.library.push({ ID: 2, name: data.name, tracks: [] })
|
||||
return json({ playlists: model.library })
|
||||
}
|
||||
if (path === '/api/playlists/1' && init?.method === 'PUT') model.library[0]!.name = data.name
|
||||
if (path === '/api/playlists/2' && init?.method === 'DELETE') model.library.pop()
|
||||
if (path === '/api/playlists/1/tracks/order') {
|
||||
const tracks = model.library[0]!.tracks
|
||||
model.library[0]!.tracks = data.trackIDs.map((id: number) =>
|
||||
tracks.find((t) => t.ID === id),
|
||||
)
|
||||
}
|
||||
if (path === '/api/playlists/1/tracks' && init?.method === 'POST') {
|
||||
assert.equal(Buffer.from(data.data, 'base64').toString(), 'ID3test')
|
||||
model.library[0]!.tracks.push({ ID: 3, name: data.name })
|
||||
}
|
||||
if (path === '/api/playlists/1/tracks/3' && init?.method === 'DELETE')
|
||||
model.library[0]!.tracks.pop()
|
||||
return json({})
|
||||
}
|
||||
throw new Error(`Unexpected request ${path}`)
|
||||
}
|
||||
const store = new PackageStore(join(directory, 'packages'), {
|
||||
@@ -243,7 +283,15 @@ async function fixture(t: TestContext, secure = true) {
|
||||
})
|
||||
await store.initialize()
|
||||
const controller = new Controller(() => {})
|
||||
const online = new Online(join(directory, 'connection'), store, controller, secrets, fetchAPI)
|
||||
const online = new Online(
|
||||
join(directory, 'connection'),
|
||||
store,
|
||||
controller,
|
||||
secrets,
|
||||
fetchAPI,
|
||||
undefined,
|
||||
async () => model.upload,
|
||||
)
|
||||
await online.initialize()
|
||||
shutdown = () => online.close()
|
||||
const login = () =>
|
||||
@@ -721,7 +769,7 @@ test('вход без прав управления не сохраняет то
|
||||
test('назначение звука подтверждается сервером отдельно от локального mute и сбрасывается с сессией', async (t) => {
|
||||
const f = await fixture(t)
|
||||
await f.prepare()
|
||||
assert.equal(f.online.state.soundEnabled, null)
|
||||
assert.equal(f.online.state.soundEnabled, true)
|
||||
f.controller.output(true)
|
||||
f.model.remote.soundEnabled = false
|
||||
await f.online.poll()
|
||||
@@ -743,7 +791,7 @@ test('назначение звука подтверждается сервер
|
||||
await f.online.poll()
|
||||
assert.equal(f.online.state.soundEnabled, false)
|
||||
await f.online.act({ action: 'connect' })
|
||||
assert.equal(f.online.state.soundEnabled, null)
|
||||
assert.equal(f.online.state.soundEnabled, true)
|
||||
await f.online.poll()
|
||||
await f.online.act({ action: 'disconnect' })
|
||||
assert.equal(f.online.state.soundEnabled, null)
|
||||
@@ -1332,3 +1380,71 @@ test('без истории и медиа текст, таймер и музык
|
||||
assert.equal(f.online.snapshot().connection, 'connected')
|
||||
assert.equal(f.online.snapshot().preparation.error, '')
|
||||
})
|
||||
|
||||
test('явное подключение включает оба канала на 100%, дальнейшие команды и reconnect сохраняют власть сервера', async (t) => {
|
||||
const f = await fixture(t)
|
||||
await f.prepare()
|
||||
assert.equal(f.model.bindings[0]!.initializeAudio, true)
|
||||
assert.equal(f.controller.state.muted, false)
|
||||
assert.equal(f.controller.state.channels.main.volume, 100)
|
||||
assert.equal(f.controller.state.channels.music.volume, 100)
|
||||
f.controller.output(true)
|
||||
f.model.remote.volume = 27
|
||||
f.model.remote.soundEnabled = false
|
||||
await f.online.poll()
|
||||
assert.equal(f.controller.state.channels.main.volume, 27)
|
||||
assert.equal(f.controller.state.muted, true)
|
||||
f.model.offline = true
|
||||
await f.online.poll()
|
||||
f.model.offline = false
|
||||
await f.online.poll()
|
||||
assert.equal(f.model.bindings.length, 1)
|
||||
assert.equal(f.controller.state.channels.main.volume, 27)
|
||||
assert.equal(f.controller.state.muted, true)
|
||||
})
|
||||
|
||||
test('редактор музыки: CRUD, native MP3, порядок, валидация и конфликт не меняют текущий канал', async (t) => {
|
||||
const f = await fixture(t)
|
||||
await f.login()
|
||||
await f.online.act({ action: 'playlists' })
|
||||
assert.equal(f.online.snapshot().playlists[0]!.tracks.length, 2)
|
||||
const before = structuredClone(f.controller.state.channels.music)
|
||||
await f.online.act({ action: 'playlist-create', name: 'Новый' })
|
||||
assert.equal(f.online.state.playlists[1]!.name, 'Новый')
|
||||
await f.online.act({ action: 'playlist-rename', playlistId: 1, name: 'Новое имя' })
|
||||
await f.online.act({ action: 'playlist-reorder', playlistId: 1, trackIds: [2, 1] })
|
||||
assert.deepEqual(
|
||||
f.online.state.playlists[0]!.tracks.map((t) => t.id),
|
||||
[2, 1],
|
||||
)
|
||||
assert.deepEqual(f.controller.state.channels.music, before)
|
||||
await assert.rejects(
|
||||
f.online.act({ action: 'playlist-reorder', playlistId: 1, trackIds: [1, 1] }),
|
||||
/повторяться/,
|
||||
)
|
||||
await assert.rejects(
|
||||
f.online.act({ action: 'playlist-upload', playlistId: '../etc/passwd' }),
|
||||
/число|Некоррект/,
|
||||
)
|
||||
f.model.upload = { filename: 'wrong.wav', data: Buffer.from('ID3test') }
|
||||
await assert.rejects(f.online.act({ action: 'playlist-upload', playlistId: 1 }), /MP3/)
|
||||
f.model.upload = { filename: 'large.mp3', data: Buffer.alloc(64 * 1048576 + 1) }
|
||||
await assert.rejects(f.online.act({ action: 'playlist-upload', playlistId: 1 }), /64 МиБ/)
|
||||
f.model.upload = { filename: 'test.mp3', data: Buffer.from('ID3test') }
|
||||
await f.online.act({ action: 'playlist-upload', playlistId: 1 })
|
||||
assert.equal(f.online.state.playlists[0]!.tracks.length, 3)
|
||||
await f.online.act({ action: 'playlist-track-delete', playlistId: 1, trackId: 3 })
|
||||
await f.online.act({ action: 'playlist-delete', playlistId: 2 })
|
||||
assert.equal(f.online.state.playlists.length, 1)
|
||||
f.model.playlistError = 409
|
||||
await assert.rejects(
|
||||
f.online.act({ action: 'playlist-reorder', playlistId: 1, trackIds: [1, 2] }),
|
||||
/Состав треков изменился/,
|
||||
)
|
||||
assert.deepEqual(
|
||||
f.online.state.playlists[0]!.tracks.map((t) => t.id),
|
||||
[2, 1],
|
||||
)
|
||||
f.model.playlistError = 403
|
||||
await assert.rejects(f.online.act({ action: 'playlist-create', name: 'Нет прав' }), /Нет доступа/)
|
||||
})
|
||||
|
||||
+110
-3
@@ -72,6 +72,7 @@ export class Online {
|
||||
busy: false,
|
||||
canCancel: false,
|
||||
games: [],
|
||||
playlists: [],
|
||||
screenId: null,
|
||||
soundEnabled: null,
|
||||
connection: 'disabled',
|
||||
@@ -336,7 +337,8 @@ export class Online {
|
||||
if (
|
||||
music &&
|
||||
old &&
|
||||
JSON.stringify(old.music?.filenames) !== JSON.stringify(next.music?.filenames)
|
||||
old.music?.trackId === next.music?.trackId &&
|
||||
!this.musicSourceMatches(next)
|
||||
)
|
||||
base.music = null
|
||||
if (!music && old && JSON.stringify(old.resources) !== JSON.stringify(next.resources))
|
||||
@@ -813,6 +815,8 @@ export class Online {
|
||||
private onRequestFailure: NonNullable<
|
||||
import('./screen-package.js').PackageRequest['onRequestFailure']
|
||||
> = () => {},
|
||||
private chooseTrack: () => Promise<{ filename: string; data: Buffer } | null> = async () =>
|
||||
null,
|
||||
) {
|
||||
controller.onMusicEnded = (report) => {
|
||||
if (!this.screen) return false
|
||||
@@ -975,7 +979,9 @@ export class Online {
|
||||
}
|
||||
private async raw(server: string, path: string, init: RequestInit = {}): Promise<Response> {
|
||||
const signal = AbortSignal.any([
|
||||
AbortSignal.timeout(30_000),
|
||||
AbortSignal.timeout(
|
||||
init.method === 'POST' && /^\/api\/playlists\/\d+\/tracks$/.test(path) ? 120_000 : 30_000,
|
||||
),
|
||||
...(init.signal ? [init.signal] : []),
|
||||
])
|
||||
try {
|
||||
@@ -1072,6 +1078,12 @@ export class Online {
|
||||
parsePlan(result.plan)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof ApiError &&
|
||||
error.status === 400 &&
|
||||
['DeletePlaylist', 'DeletePlaylistTrack'].includes(meta.method)
|
||||
)
|
||||
error.message = 'Плейлист используется игрой или сценарием; удаление недоступно'
|
||||
throw requestErrorContext(error, meta.method, request.method, path)
|
||||
}
|
||||
})
|
||||
@@ -1101,6 +1113,16 @@ export class Online {
|
||||
private async perform(data: Record<string, unknown>) {
|
||||
const action = data.action as OnlineIntent['action']
|
||||
switch (action) {
|
||||
case 'playlists':
|
||||
return this.loadPlaylists()
|
||||
case 'playlist-create':
|
||||
case 'playlist-rename':
|
||||
case 'playlist-delete':
|
||||
case 'playlist-upload':
|
||||
case 'playlist-track-delete':
|
||||
case 'playlist-reorder':
|
||||
case 'playlist-use':
|
||||
return this.editPlaylist(data)
|
||||
case 'plan-save':
|
||||
return this.savePlan(data)
|
||||
case 'plan-refresh':
|
||||
@@ -1115,6 +1137,7 @@ export class Online {
|
||||
this.credentials = null
|
||||
this.state.authenticated = false
|
||||
this.state.games = []
|
||||
this.state.playlists = []
|
||||
await this.persistSecrets()
|
||||
this.state.server = server
|
||||
const result = await this.client(false, undefined, server).Login({ email, password })
|
||||
@@ -1149,6 +1172,7 @@ export class Online {
|
||||
this.credentials = null
|
||||
this.state.authenticated = false
|
||||
this.state.games = []
|
||||
this.state.playlists = []
|
||||
await this.persistSecrets()
|
||||
return
|
||||
case 'games':
|
||||
@@ -1253,7 +1277,16 @@ export class Online {
|
||||
token: token(result.accessToken),
|
||||
gameId,
|
||||
}
|
||||
await this.client().BindScreen({ screenID: s.id, accessToken: s.token, gameID: s.gameId })
|
||||
await this.client().BindScreen({
|
||||
screenID: s.id,
|
||||
accessToken: s.token,
|
||||
gameID: s.gameId,
|
||||
initializeAudio: true,
|
||||
})
|
||||
send(this.controller, { action: 'mute', enabled: false })
|
||||
send(this.controller, { action: 'volume', channel: 'main', volume: 100 })
|
||||
send(this.controller, { action: 'volume', channel: 'music', volume: 100 })
|
||||
this.state.soundEnabled = true
|
||||
this.screen = s
|
||||
this.state.screenId = s.id
|
||||
this.state.connection = 'waiting'
|
||||
@@ -1277,6 +1310,80 @@ export class Online {
|
||||
throw new Error('Неизвестное действие')
|
||||
}
|
||||
}
|
||||
private async loadPlaylists() {
|
||||
const result = await this.client().GetPlaylists({})
|
||||
if (!Array.isArray(result.playlists ?? [])) throw new Error('Некорректный список плейлистов')
|
||||
this.state.playlists = (result.playlists ?? []).map((value) => {
|
||||
const p = object(value)
|
||||
if (!Array.isArray(p.tracks ?? [])) throw new Error('Некорректный список треков')
|
||||
return {
|
||||
id: integer(p.ID, 1),
|
||||
name: text(p.name),
|
||||
tracks: ((p.tracks ?? []) as unknown[]).map((value) => {
|
||||
const t = object(value)
|
||||
return { id: integer(t.ID, 1), name: text(t.name) }
|
||||
}),
|
||||
}
|
||||
})
|
||||
}
|
||||
private async editPlaylist(data: Record<string, unknown>) {
|
||||
if (!this.credentials) throw new ApiError(401)
|
||||
const generation = this.generation
|
||||
const id = data.action === 'playlist-create' ? 0 : integer(data.playlistId, 1, 2147483647)
|
||||
const client = this.client()
|
||||
if (data.action === 'playlist-create' || data.action === 'playlist-rename') {
|
||||
const name = text(data.name).trim()
|
||||
if (!name || Buffer.byteLength(name) > 255) throw new Error('Имя плейлиста: от 1 до 255 байт')
|
||||
if (data.action === 'playlist-create') await client.CreatePlaylist({ name })
|
||||
else await client.UpdatePlaylist({ ID: id, name })
|
||||
} else if (data.action === 'playlist-delete') await client.DeletePlaylist({ ID: id })
|
||||
else if (data.action === 'playlist-track-delete')
|
||||
await client.DeletePlaylistTrack({
|
||||
playlistID: id,
|
||||
trackID: integer(data.trackId, 1, 2147483647),
|
||||
})
|
||||
else if (data.action === 'playlist-reorder') {
|
||||
if (!Array.isArray(data.trackIds)) throw new Error('Некорректный порядок треков')
|
||||
const ids = data.trackIds.map((value) => integer(value, 1, 2147483647))
|
||||
if (new Set(ids).size !== ids.length) throw new Error('Треки не должны повторяться')
|
||||
try {
|
||||
await client.ReorderPlaylistTracks({ playlistID: id, trackIDs: ids })
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError && error.status === 409)
|
||||
throw new Error(
|
||||
'Состав треков изменился на сервере. Обновите список и повторите перестановку',
|
||||
)
|
||||
throw error
|
||||
}
|
||||
} else if (data.action === 'playlist-upload') {
|
||||
const file = await this.chooseTrack()
|
||||
if (generation !== this.generation) throw new SessionChangedError()
|
||||
if (!file) return
|
||||
if (!/\.mp3$/i.test(file.filename) || !file.data.length || file.data.length > 64 * 1048576)
|
||||
throw new Error('Выберите MP3 размером до 64 МиБ')
|
||||
await client.UploadPlaylistTrack({
|
||||
playlistID: id,
|
||||
name: file.filename,
|
||||
filename: file.filename,
|
||||
data: file.data.toString('base64'),
|
||||
})
|
||||
} else if (data.action === 'playlist-use') {
|
||||
const pkg = this.planGame()
|
||||
await client.SetGameMusic({
|
||||
gameID: pkg.package.game.id,
|
||||
playlistID: id,
|
||||
trackIndex: 0,
|
||||
positionMs: 0,
|
||||
volume: this.controller.state.channels.music.volume,
|
||||
loop: this.controller.state.channels.music.loop,
|
||||
playback: 'MUSIC_PLAYBACK_STOPPED',
|
||||
})
|
||||
}
|
||||
await this.loadPlaylists()
|
||||
this.lastMaterials = -Infinity
|
||||
await this.refreshMaterials()
|
||||
await this.poll()
|
||||
}
|
||||
private async loadGames() {
|
||||
const result = await this.client().GetGames({})
|
||||
if (!Array.isArray(result.games ?? [])) throw new Error('Некорректный список игр')
|
||||
|
||||
@@ -122,6 +122,13 @@ export function requestErrorContext(
|
||||
AdvanceScreenMusic: 'Следующий трек',
|
||||
Login: 'Вход',
|
||||
Refresh: 'Обновление сессии',
|
||||
GetPlaylists: 'Список плейлистов',
|
||||
CreatePlaylist: 'Создание плейлиста',
|
||||
UpdatePlaylist: 'Название плейлиста',
|
||||
DeletePlaylist: 'Удаление плейлиста',
|
||||
UploadPlaylistTrack: 'Загрузка MP3',
|
||||
DeletePlaylistTrack: 'Удаление трека',
|
||||
ReorderPlaylistTracks: 'Порядок треков',
|
||||
GetMe: 'Данные пользователя',
|
||||
GetGames: 'Список игр',
|
||||
CreateScreenSession: 'Создание экрана',
|
||||
|
||||
Reference in New Issue
Block a user