mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-16 12:42:06 +00:00
feat(api-server): Add queue api (#2767)
This commit is contained in:
@ -5,15 +5,20 @@ import { ipcMain } from 'electron';
|
|||||||
import getSongControls from '@/providers/song-controls';
|
import getSongControls from '@/providers/song-controls';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AddSongToQueueSchema,
|
||||||
AuthHeadersSchema,
|
AuthHeadersSchema,
|
||||||
type ResponseSongInfo,
|
|
||||||
SongInfoSchema,
|
|
||||||
SeekSchema,
|
|
||||||
GoForwardScheme,
|
|
||||||
GoBackSchema,
|
GoBackSchema,
|
||||||
SwitchRepeatSchema,
|
GoForwardScheme,
|
||||||
SetVolumeSchema,
|
MoveSongInQueueSchema,
|
||||||
|
QueueParamsSchema,
|
||||||
|
SearchSchema,
|
||||||
|
SeekSchema,
|
||||||
SetFullscreenSchema,
|
SetFullscreenSchema,
|
||||||
|
SetQueueIndexSchema,
|
||||||
|
SetVolumeSchema,
|
||||||
|
SongInfoSchema,
|
||||||
|
SwitchRepeatSchema,
|
||||||
|
type ResponseSongInfo,
|
||||||
} from '../scheme';
|
} from '../scheme';
|
||||||
|
|
||||||
import type { RepeatMode } from '@/types/datahost-get-state';
|
import type { RepeatMode } from '@/types/datahost-get-state';
|
||||||
@ -22,6 +27,7 @@ import type { BackendContext } from '@/types/contexts';
|
|||||||
import type { APIServerConfig } from '../../config';
|
import type { APIServerConfig } from '../../config';
|
||||||
import type { HonoApp } from '../types';
|
import type { HonoApp } from '../types';
|
||||||
import type { QueueResponse } from '@/types/youtube-music-desktop-internal';
|
import type { QueueResponse } from '@/types/youtube-music-desktop-internal';
|
||||||
|
import type { Context } from 'hono';
|
||||||
|
|
||||||
const API_VERSION = 'v1';
|
const API_VERSION = 'v1';
|
||||||
|
|
||||||
@ -297,7 +303,8 @@ const routes = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
queueInfo: createRoute({
|
oldQueueInfo: createRoute({
|
||||||
|
deprecated: true,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: `/api/${API_VERSION}/queue-info`,
|
path: `/api/${API_VERSION}/queue-info`,
|
||||||
summary: 'get current queue info',
|
summary: 'get current queue info',
|
||||||
@ -316,7 +323,8 @@ const routes = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
songInfo: createRoute({
|
oldSongInfo: createRoute({
|
||||||
|
deprecated: true,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: `/api/${API_VERSION}/song-info`,
|
path: `/api/${API_VERSION}/song-info`,
|
||||||
summary: 'get current song info',
|
summary: 'get current song info',
|
||||||
@ -335,6 +343,164 @@ const routes = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
songInfo: createRoute({
|
||||||
|
method: 'get',
|
||||||
|
path: `/api/${API_VERSION}/song`,
|
||||||
|
summary: 'get current song info',
|
||||||
|
description: 'Get the current song info',
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Success',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: SongInfoSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
204: {
|
||||||
|
description: 'No song info',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
queueInfo: createRoute({
|
||||||
|
method: 'get',
|
||||||
|
path: `/api/${API_VERSION}/queue`,
|
||||||
|
summary: 'get current queue info',
|
||||||
|
description: 'Get the current queue info',
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Success',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: z.object({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
204: {
|
||||||
|
description: 'No queue info',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
addSongToQueue: createRoute({
|
||||||
|
method: 'post',
|
||||||
|
path: `/api/${API_VERSION}/queue`,
|
||||||
|
summary: 'add song to queue',
|
||||||
|
description: 'Add a song to the queue',
|
||||||
|
request: {
|
||||||
|
headers: AuthHeadersSchema,
|
||||||
|
body: {
|
||||||
|
description: 'video id of the song to add',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: AddSongToQueueSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
204: {
|
||||||
|
description: 'Success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
moveSongInQueue: createRoute({
|
||||||
|
method: 'patch',
|
||||||
|
path: `/api/${API_VERSION}/queue/{index}`,
|
||||||
|
summary: 'move song in queue',
|
||||||
|
description: 'Move a song in the queue',
|
||||||
|
request: {
|
||||||
|
headers: AuthHeadersSchema,
|
||||||
|
params: QueueParamsSchema,
|
||||||
|
body: {
|
||||||
|
description: 'index to move the song to',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: MoveSongInQueueSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
204: {
|
||||||
|
description: 'Success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
removeSongFromQueue: createRoute({
|
||||||
|
method: 'delete',
|
||||||
|
path: `/api/${API_VERSION}/queue/{index}`,
|
||||||
|
summary: 'remove song from queue',
|
||||||
|
description: 'Remove a song from the queue',
|
||||||
|
request: {
|
||||||
|
headers: AuthHeadersSchema,
|
||||||
|
params: QueueParamsSchema,
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
204: {
|
||||||
|
description: 'Success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
setQueueIndex: createRoute({
|
||||||
|
method: 'patch',
|
||||||
|
path: `/api/${API_VERSION}/queue`,
|
||||||
|
summary: 'set queue index',
|
||||||
|
description: 'Set the current index of the queue',
|
||||||
|
request: {
|
||||||
|
headers: AuthHeadersSchema,
|
||||||
|
body: {
|
||||||
|
description: 'index to move the song to',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: SetQueueIndexSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
204: {
|
||||||
|
description: 'Success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
clearQueue: createRoute({
|
||||||
|
method: 'delete',
|
||||||
|
path: `/api/${API_VERSION}/queue`,
|
||||||
|
summary: 'clear queue',
|
||||||
|
description: 'Clear the queue',
|
||||||
|
responses: {
|
||||||
|
204: {
|
||||||
|
description: 'Success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
search: createRoute({
|
||||||
|
method: 'post',
|
||||||
|
path: `/api/${API_VERSION}/search`,
|
||||||
|
summary: 'search for a song',
|
||||||
|
description: 'search for a song',
|
||||||
|
request: {
|
||||||
|
headers: AuthHeadersSchema,
|
||||||
|
body: {
|
||||||
|
description: 'search query',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: SearchSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Success',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: z.object({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const register = (
|
export const register = (
|
||||||
@ -464,7 +630,26 @@ export const register = (
|
|||||||
ctx.status(200);
|
ctx.status(200);
|
||||||
return ctx.json({ state: fullscreen });
|
return ctx.json({ state: fullscreen });
|
||||||
});
|
});
|
||||||
app.openapi(routes.queueInfo, async (ctx) => {
|
|
||||||
|
const songInfo = (ctx: Context) => {
|
||||||
|
const info = songInfoGetter();
|
||||||
|
|
||||||
|
if (!info) {
|
||||||
|
ctx.status(204);
|
||||||
|
return ctx.body(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = { ...info };
|
||||||
|
delete body.image;
|
||||||
|
|
||||||
|
ctx.status(200);
|
||||||
|
return ctx.json(body satisfies ResponseSongInfo);
|
||||||
|
};
|
||||||
|
app.openapi(routes.oldSongInfo, songInfo);
|
||||||
|
app.openapi(routes.songInfo, songInfo);
|
||||||
|
|
||||||
|
// Queue
|
||||||
|
const queueInfo = async (ctx: Context) => {
|
||||||
const queueResponsePromise = new Promise<QueueResponse>((resolve) => {
|
const queueResponsePromise = new Promise<QueueResponse>((resolve) => {
|
||||||
ipcMain.once('ytmd:get-queue-response', (_, queue: QueueResponse) => {
|
ipcMain.once('ytmd:get-queue-response', (_, queue: QueueResponse) => {
|
||||||
return resolve(queue);
|
return resolve(queue);
|
||||||
@ -482,19 +667,50 @@ export const register = (
|
|||||||
|
|
||||||
ctx.status(200);
|
ctx.status(200);
|
||||||
return ctx.json(info);
|
return ctx.json(info);
|
||||||
|
};
|
||||||
|
app.openapi(routes.oldQueueInfo, queueInfo);
|
||||||
|
app.openapi(routes.queueInfo, queueInfo);
|
||||||
|
|
||||||
|
app.openapi(routes.addSongToQueue, (ctx) => {
|
||||||
|
const { videoId } = ctx.req.valid('json');
|
||||||
|
controller.addSongToQueue(videoId);
|
||||||
|
|
||||||
|
ctx.status(204);
|
||||||
|
return ctx.body(null);
|
||||||
});
|
});
|
||||||
app.openapi(routes.songInfo, (ctx) => {
|
app.openapi(routes.moveSongInQueue, (ctx) => {
|
||||||
const info = songInfoGetter();
|
const index = Number(ctx.req.param('index'));
|
||||||
|
const { toIndex } = ctx.req.valid('json');
|
||||||
|
controller.moveSongInQueue(index, toIndex);
|
||||||
|
|
||||||
if (!info) {
|
ctx.status(204);
|
||||||
ctx.status(204);
|
return ctx.body(null);
|
||||||
return ctx.body(null);
|
});
|
||||||
}
|
app.openapi(routes.removeSongFromQueue, (ctx) => {
|
||||||
|
const index = Number(ctx.req.param('index'));
|
||||||
|
controller.removeSongFromQueue(index);
|
||||||
|
|
||||||
const body = { ...info };
|
ctx.status(204);
|
||||||
delete body.image;
|
return ctx.body(null);
|
||||||
|
});
|
||||||
|
app.openapi(routes.setQueueIndex, (ctx) => {
|
||||||
|
const { index } = ctx.req.valid('json');
|
||||||
|
controller.setQueueIndex(index);
|
||||||
|
|
||||||
|
ctx.status(204);
|
||||||
|
return ctx.body(null);
|
||||||
|
});
|
||||||
|
app.openapi(routes.clearQueue, (ctx) => {
|
||||||
|
controller.clearQueue();
|
||||||
|
|
||||||
|
ctx.status(204);
|
||||||
|
return ctx.body(null);
|
||||||
|
});
|
||||||
|
app.openapi(routes.search, async (ctx) => {
|
||||||
|
const { query } = ctx.req.valid('json');
|
||||||
|
const response = await controller.search(query);
|
||||||
|
|
||||||
ctx.status(200);
|
ctx.status(200);
|
||||||
return ctx.json(body satisfies ResponseSongInfo);
|
return ctx.json(response as object);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,3 +6,5 @@ export * from './go-forward';
|
|||||||
export * from './switch-repeat';
|
export * from './switch-repeat';
|
||||||
export * from './set-volume';
|
export * from './set-volume';
|
||||||
export * from './set-fullscreen';
|
export * from './set-fullscreen';
|
||||||
|
export * from './queue';
|
||||||
|
export * from './search';
|
||||||
|
|||||||
15
src/plugins/api-server/backend/scheme/queue.ts
Normal file
15
src/plugins/api-server/backend/scheme/queue.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { z } from '@hono/zod-openapi';
|
||||||
|
|
||||||
|
export const QueueParamsSchema = z.object({
|
||||||
|
index: z.coerce.number().int().nonnegative(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AddSongToQueueSchema = z.object({
|
||||||
|
videoId: z.string(),
|
||||||
|
});
|
||||||
|
export const MoveSongInQueueSchema = z.object({
|
||||||
|
toIndex: z.number(),
|
||||||
|
});
|
||||||
|
export const SetQueueIndexSchema = z.object({
|
||||||
|
index: z.number().int().nonnegative(),
|
||||||
|
});
|
||||||
5
src/plugins/api-server/backend/scheme/search.ts
Normal file
5
src/plugins/api-server/backend/scheme/search.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { z } from '@hono/zod-openapi';
|
||||||
|
|
||||||
|
export const SearchSchema = z.object({
|
||||||
|
query: z.string(),
|
||||||
|
});
|
||||||
@ -1,5 +1,5 @@
|
|||||||
// This is used for to control the songs
|
// This is used for to control the songs
|
||||||
import { BrowserWindow } from 'electron';
|
import { BrowserWindow, ipcMain } from 'electron';
|
||||||
|
|
||||||
// see protocol-handler.ts
|
// see protocol-handler.ts
|
||||||
type ArgsType<T> = T | string[] | undefined;
|
type ArgsType<T> = T | string[] | undefined;
|
||||||
@ -24,6 +24,16 @@ const parseBooleanFromArgsType = (args: ArgsType<boolean>) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const parseStringFromArgsType = (args: ArgsType<string>) => {
|
||||||
|
if (typeof args === 'string') {
|
||||||
|
return args;
|
||||||
|
} else if (Array.isArray(args)) {
|
||||||
|
return args[0];
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default (win: BrowserWindow) => {
|
export default (win: BrowserWindow) => {
|
||||||
return {
|
return {
|
||||||
// Playback
|
// Playback
|
||||||
@ -80,11 +90,49 @@ export default (win: BrowserWindow) => {
|
|||||||
win.webContents.send('ytmd:get-queue');
|
win.webContents.send('ytmd:get-queue');
|
||||||
},
|
},
|
||||||
muteUnmute: () => win.webContents.send('ytmd:toggle-mute'),
|
muteUnmute: () => win.webContents.send('ytmd:toggle-mute'),
|
||||||
search: () => {
|
openSearchBox: () => {
|
||||||
win.webContents.sendInputEvent({
|
win.webContents.sendInputEvent({
|
||||||
type: 'keyDown',
|
type: 'keyDown',
|
||||||
keyCode: '/',
|
keyCode: '/',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
// Queue
|
||||||
|
addSongToQueue: (videoId: string) => {
|
||||||
|
const videoIdValue = parseStringFromArgsType(videoId);
|
||||||
|
if (videoIdValue === null) return;
|
||||||
|
|
||||||
|
win.webContents.send('ytmd:add-to-queue', videoIdValue);
|
||||||
|
},
|
||||||
|
moveSongInQueue: (
|
||||||
|
fromIndex: ArgsType<number>,
|
||||||
|
toIndex: ArgsType<number>,
|
||||||
|
) => {
|
||||||
|
const fromIndexValue = parseNumberFromArgsType(fromIndex);
|
||||||
|
const toIndexValue = parseNumberFromArgsType(toIndex);
|
||||||
|
if (fromIndexValue === null || toIndexValue === null) return;
|
||||||
|
|
||||||
|
win.webContents.send('ytmd:move-in-queue', fromIndexValue, toIndexValue);
|
||||||
|
},
|
||||||
|
removeSongFromQueue: (index: ArgsType<number>) => {
|
||||||
|
const indexValue = parseNumberFromArgsType(index);
|
||||||
|
if (indexValue === null) return;
|
||||||
|
|
||||||
|
win.webContents.send('ytmd:remove-from-queue', indexValue);
|
||||||
|
},
|
||||||
|
setQueueIndex: (index: ArgsType<number>) => {
|
||||||
|
const indexValue = parseNumberFromArgsType(index);
|
||||||
|
if (indexValue === null) return;
|
||||||
|
|
||||||
|
win.webContents.send('ytmd:set-queue-index', indexValue);
|
||||||
|
},
|
||||||
|
clearQueue: () => win.webContents.send('ytmd:clear-queue'),
|
||||||
|
|
||||||
|
search: (query: string) =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
ipcMain.once('ytmd:search-results', (_, result) => {
|
||||||
|
resolve(result as string);
|
||||||
|
});
|
||||||
|
win.webContents.send('ytmd:search', query);
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
100
src/renderer.ts
100
src/renderer.ts
@ -43,6 +43,13 @@ async function listenForApiLoad() {
|
|||||||
|
|
||||||
interface YouTubeMusicAppElement extends HTMLElement {
|
interface YouTubeMusicAppElement extends HTMLElement {
|
||||||
navigate(page: string): void;
|
navigate(page: string): void;
|
||||||
|
networkManager: {
|
||||||
|
fetch: (url: string, data: unknown) => Promise<unknown>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchBoxElement extends HTMLElement {
|
||||||
|
getSearchboxStats(): unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onApiLoaded() {
|
async function onApiLoaded() {
|
||||||
@ -159,6 +166,99 @@ async function onApiLoaded() {
|
|||||||
} satisfies QueueResponse);
|
} satisfies QueueResponse);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.ipcRenderer.on('ytmd:add-to-queue', (_, videoId: string) => {
|
||||||
|
const queue = document.querySelector<QueueElement>('#queue');
|
||||||
|
const app = document.querySelector<YouTubeMusicAppElement>('ytmusic-app');
|
||||||
|
if (!app) return;
|
||||||
|
|
||||||
|
const store = queue?.queue.store.store;
|
||||||
|
if (!store) return;
|
||||||
|
|
||||||
|
app.networkManager
|
||||||
|
.fetch('/music/get_queue', {
|
||||||
|
queueContextParams: store.getState().queue.queueContextParams,
|
||||||
|
queueInsertPosition: 'INSERT_AT_END',
|
||||||
|
videoIds: [videoId],
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
if (
|
||||||
|
result &&
|
||||||
|
typeof result === 'object' &&
|
||||||
|
'queueDatas' in result &&
|
||||||
|
Array.isArray(result.queueDatas)
|
||||||
|
) {
|
||||||
|
queue?.dispatch({
|
||||||
|
type: 'ADD_ITEMS',
|
||||||
|
payload: {
|
||||||
|
nextQueueItemId: store.getState().queue.nextQueueItemId,
|
||||||
|
index: store.getState().queue.items.length ?? 0,
|
||||||
|
items: result.queueDatas
|
||||||
|
.map((it) =>
|
||||||
|
typeof it === 'object' && it && 'content' in it
|
||||||
|
? it.content
|
||||||
|
: null,
|
||||||
|
)
|
||||||
|
.filter(Boolean),
|
||||||
|
shuffleEnabled: false,
|
||||||
|
shouldAssignIds: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
window.ipcRenderer.on(
|
||||||
|
'ytmd:move-in-queue',
|
||||||
|
(_, fromIndex: number, toIndex: number) => {
|
||||||
|
const queue = document.querySelector<QueueElement>('#queue');
|
||||||
|
queue?.dispatch({
|
||||||
|
type: 'MOVE_ITEM',
|
||||||
|
payload: {
|
||||||
|
fromIndex,
|
||||||
|
toIndex,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
window.ipcRenderer.on('ytmd:remove-from-queue', (_, index: number) => {
|
||||||
|
const queue = document.querySelector<QueueElement>('#queue');
|
||||||
|
queue?.dispatch({
|
||||||
|
type: 'REMOVE_ITEM',
|
||||||
|
payload: index,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
window.ipcRenderer.on('ytmd:set-queue-index', (_, index: number) => {
|
||||||
|
const queue = document.querySelector<QueueElement>('#queue');
|
||||||
|
queue?.dispatch({
|
||||||
|
type: 'SET_INDEX',
|
||||||
|
payload: index,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
window.ipcRenderer.on('ytmd:clear-queue', () => {
|
||||||
|
const queue = document.querySelector<QueueElement>('#queue');
|
||||||
|
queue?.queue.store.store.dispatch({
|
||||||
|
type: 'SET_PLAYER_PAGE_INFO',
|
||||||
|
payload: { open: false },
|
||||||
|
});
|
||||||
|
queue?.dispatch({
|
||||||
|
type: 'CLEAR',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
window.ipcRenderer.on('ytmd:search', async (_, query: string) => {
|
||||||
|
const app = document.querySelector<YouTubeMusicAppElement>('ytmusic-app');
|
||||||
|
const searchBox =
|
||||||
|
document.querySelector<SearchBoxElement>('ytmusic-search-box');
|
||||||
|
|
||||||
|
if (!app || !searchBox) return;
|
||||||
|
|
||||||
|
const result = await app.networkManager.fetch('/search', {
|
||||||
|
query,
|
||||||
|
suggestStats: searchBox.getSearchboxStats(),
|
||||||
|
});
|
||||||
|
|
||||||
|
window.ipcRenderer.send('ytmd:search-results', result);
|
||||||
|
});
|
||||||
|
|
||||||
const video = document.querySelector('video')!;
|
const video = document.querySelector('video')!;
|
||||||
const audioContext = new AudioContext();
|
const audioContext = new AudioContext();
|
||||||
const audioSource = audioContext.createMediaElementSource(video);
|
const audioSource = audioContext.createMediaElementSource(video);
|
||||||
|
|||||||
@ -3,12 +3,7 @@ import type { GetState, QueueItem } from '@/types/datahost-get-state';
|
|||||||
|
|
||||||
type StoreState = GetState;
|
type StoreState = GetState;
|
||||||
type Store = {
|
type Store = {
|
||||||
dispatch: (obj: {
|
dispatch: (obj: { type: string; payload?: unknown }) => void;
|
||||||
type: string;
|
|
||||||
payload?: {
|
|
||||||
items?: QueueItem[];
|
|
||||||
};
|
|
||||||
}) => void;
|
|
||||||
|
|
||||||
getState: () => StoreState;
|
getState: () => StoreState;
|
||||||
replaceReducer: (param1: unknown) => unknown;
|
replaceReducer: (param1: unknown) => unknown;
|
||||||
|
|||||||
Reference in New Issue
Block a user