fix: fix bugs in MPRIS, and improve MPRIS (#1760)

Co-authored-by: JellyBrick <shlee1503@naver.com>
Co-authored-by: Totto <32566573+Totto16@users.noreply.github.com>
This commit is contained in:
JellyBrick
2024-02-20 20:50:55 +09:00
committed by GitHub
parent 8bd05f525d
commit d37cd2418c
15 changed files with 516 additions and 300 deletions

View File

@ -673,7 +673,9 @@ app.whenReady().then(async () => {
); );
} }
handleProtocol(command); const splited = decodeURIComponent(command).split(' ');
handleProtocol(splited.shift()!, splited);
return; return;
} }

View File

@ -6,7 +6,7 @@ import { t } from '@/i18n';
import { createPlugin } from '@/utils'; import { createPlugin } from '@/utils';
import promptOptions from '@/providers/prompt-options'; import promptOptions from '@/providers/prompt-options';
import { type AppElement, getDefaultProfile, type Permission, type Profile, type VideoData } from './types'; import { getDefaultProfile, type Permission, type Profile, type VideoData } from './types';
import { Queue } from './queue'; import { Queue } from './queue';
import { Connection, type ConnectionEventUnion } from './connection'; import { Connection, type ConnectionEventUnion } from './connection';
import { createHostPopup } from './ui/host'; import { createHostPopup } from './ui/host';
@ -19,6 +19,7 @@ import style from './style.css?inline';
import type { YoutubePlayer } from '@/types/youtube-player'; import type { YoutubePlayer } from '@/types/youtube-player';
import type { RendererContext } from '@/types/contexts'; import type { RendererContext } from '@/types/contexts';
import type { VideoDataChanged } from '@/types/video-data-changed'; import type { VideoDataChanged } from '@/types/video-data-changed';
import type { AppElement } from '@/types/queue';
type RawAccountData = { type RawAccountData = {
accountName: { accountName: {

View File

@ -4,8 +4,9 @@ import { mapQueueItem } from './utils';
import { t } from '@/i18n'; import { t } from '@/i18n';
import type { ConnectionEventUnion } from '@/plugins/music-together/connection'; import type { ConnectionEventUnion } from '@/plugins/music-together/connection';
import type { Profile, QueueElement, VideoData } from '../types'; import type { Profile, VideoData } from '../types';
import type { QueueItem } from '@/types/datahost-get-state'; import type { QueueItem } from '@/types/datahost-get-state';
import type { QueueElement } from '@/types/queue';
const getHeaderPayload = (() => { const getHeaderPayload = (() => {
let payload: { let payload: {

View File

@ -1,44 +1,3 @@
import type { YoutubePlayer } from '@/types/youtube-player';
import type { GetState, QueueItem } from '@/types/datahost-get-state';
type StoreState = GetState;
type Store = {
dispatch: (obj: {
type: string;
payload?: {
items?: QueueItem[];
};
}) => void;
getState: () => StoreState;
replaceReducer: (param1: unknown) => unknown;
subscribe: (callback: () => void) => unknown;
}
export type QueueElement = HTMLElement & {
dispatch(obj: {
type: string;
payload?: unknown;
}): void;
queue: QueueAPI;
};
export type QueueAPI = {
getItems(): unknown[];
store: {
store: Store,
};
continuation?: string;
autoPlaying?: boolean;
};
export type AppElement = HTMLElement & AppAPI;
export type AppAPI = {
queue_: QueueAPI;
playerApi_: YoutubePlayer;
openToast: (message: string) => void;
// TODO: Add more
};
export type Profile = { export type Profile = {
id: string; id: string;
handleId: string; handleId: string;

View File

@ -307,9 +307,9 @@ export default (
savedNotification?.close(); savedNotification?.close();
}); });
changeProtocolHandler((cmd) => { changeProtocolHandler((cmd, args) => {
if (Object.keys(songControls).includes(cmd)) { if (Object.keys(songControls).includes(cmd)) {
songControls[cmd as keyof typeof songControls](); songControls[cmd as keyof typeof songControls](args as never);
if ( if (
config().refreshOnPlayPause && config().refreshOnPlayPause &&
(cmd === 'pause' || (cmd === 'play' && !config().unpauseNotification)) (cmd === 'pause' || (cmd === 'play' && !config().unpauseNotification))

View File

@ -4,10 +4,10 @@ declare module '@jellybrick/mpris-service' {
import { interface as dbusInterface } from 'dbus-next'; import { interface as dbusInterface } from 'dbus-next';
interface RootInterfaceOptions { interface RootInterfaceOptions {
identity: string; identity?: string;
supportedUriSchemes: string[]; supportedUriSchemes?: string[];
supportedMimeTypes: string[]; supportedMimeTypes?: string[];
desktopEntry: string; desktopEntry?: string;
} }
export interface Track { export interface Track {
@ -35,6 +35,32 @@ declare module '@jellybrick/mpris-service' {
'xesam:userRating'?: number; 'xesam:userRating'?: number;
} }
export type PlayBackStatus = 'Playing' | 'Paused' | 'Stopped';
export type LoopStatus = 'None' | 'Track' | 'Playlist';
export const PLAYBACK_STATUS_PLAYING: 'Playing';
export const PLAYBACK_STATUS_PAUSED: 'Paused';
export const PLAYBACK_STATUS_STOPPED: 'Stopped';
export const LOOP_STATUS_NONE: 'None';
export const LOOP_STATUS_TRACK: 'Track';
export const LOOP_STATUS_PLAYLIST: 'Playlist';
export type Interfaces = 'player' | 'trackList' | 'playlists';
export interface AdditionalPlayerOptions {
name: string;
supportedInterfaces: Interfaces[];
}
export type PlayerOptions = RootInterfaceOptions & AdditionalPlayerOptions;
export interface Position {
trackId: string;
position: number;
}
declare class Player extends EventEmitter { declare class Player extends EventEmitter {
constructor(opts: { constructor(opts: {
name: string; name: string;
@ -43,18 +69,44 @@ declare module '@jellybrick/mpris-service' {
supportedInterfaces?: string[]; supportedInterfaces?: string[];
}); });
//RootInterface
on(event: 'quit', listener: () => void): this;
on(event: 'raise', listener: () => void): this;
on(
event: 'fullscreen',
listener: (fullscreenEnabled: boolean) => void,
): this;
emit(type: string, ...args: unknown[]): unknown;
name: string; name: string;
identity: string; identity: string;
fullscreen: boolean; fullscreen?: boolean;
supportedUriSchemes: string[]; supportedUriSchemes: string[];
supportedMimeTypes: string[]; supportedMimeTypes: string[];
canQuit: boolean; canQuit: boolean;
canRaise: boolean; canRaise: boolean;
canSetFullscreen: boolean; canSetFullscreen?: boolean;
desktopEntry?: string;
hasTrackList: boolean; hasTrackList: boolean;
desktopEntry: string;
playbackStatus: string; // PlayerInterface
loopStatus: string; on(event: 'next', listener: () => void): this;
on(event: 'previous', listener: () => void): this;
on(event: 'pause', listener: () => void): this;
on(event: 'playpause', listener: () => void): this;
on(event: 'stop', listener: () => void): this;
on(event: 'play', listener: () => void): this;
on(event: 'seek', listener: (offset: number) => void): this;
on(event: 'open', listener: ({ uri: string }) => void): this;
on(event: 'loopStatus', listener: (status: LoopStatus) => void): this;
on(event: 'rate', listener: () => void): this;
on(event: 'shuffle', listener: (enableShuffle: boolean) => void): this;
on(event: 'volume', listener: (newVolume: number) => void): this;
on(event: 'position', listener: (position: Position) => void): this;
playbackStatus: PlayBackStatus;
loopStatus: LoopStatus;
shuffle: boolean; shuffle: boolean;
metadata: Track; metadata: Track;
volume: number; volume: number;
@ -67,9 +119,40 @@ declare module '@jellybrick/mpris-service' {
rate: number; rate: number;
minimumRate: number; minimumRate: number;
maximumRate: number; maximumRate: number;
playlists: unknown[];
abstract getPosition(): number;
seeked(position: number): void;
// TracklistInterface
on(event: 'addTrack', listener: () => void): this;
on(event: 'removeTrack', listener: () => void): this;
on(event: 'goTo', listener: () => void): this;
tracks: Track[];
canEditTracks: boolean;
on(event: '*', a: unknown[]): this;
addTrack(track: string): void;
removeTrack(trackId: string): void;
// PlaylistsInterface
on(event: 'activatePlaylist', listener: () => void): this;
playlists: Playlist[];
activePlaylist: string; activePlaylist: string;
setPlaylists(playlists: Playlist[]): void;
setActivePlaylist(playlistId: string): void;
// Player methods
constructor(opts: PlayerOptions);
on(event: 'error', listener: (error: Error) => void): this;
init(opts: RootInterfaceOptions): void; init(opts: RootInterfaceOptions): void;
objectPath(subpath?: string): string; objectPath(subpath?: string): string;
@ -91,13 +174,6 @@ declare module '@jellybrick/mpris-service' {
setPlaylists(playlists: Track[]): void; setPlaylists(playlists: Track[]): void;
setActivePlaylist(playlistId: string): void; setActivePlaylist(playlistId: string): void;
static PLAYBACK_STATUS_PLAYING: 'Playing';
static PLAYBACK_STATUS_PAUSED: 'Paused';
static PLAYBACK_STATUS_STOPPED: 'Stopped';
static LOOP_STATUS_NONE: 'None';
static LOOP_STATUS_TRACK: 'Track';
static LOOP_STATUS_PLAYLIST: 'Playlist';
} }
interface MprisInterface extends dbusInterface.Interface { interface MprisInterface extends dbusInterface.Interface {

View File

@ -1,12 +1,27 @@
import { BrowserWindow, ipcMain } from 'electron'; import { BrowserWindow, ipcMain } from 'electron';
import MprisPlayer, { Track } from '@jellybrick/mpris-service'; import MprisPlayer, {
Track,
LoopStatus,
type PlayBackStatus,
type PlayerOptions,
PLAYBACK_STATUS_STOPPED,
PLAYBACK_STATUS_PAUSED,
PLAYBACK_STATUS_PLAYING,
LOOP_STATUS_NONE,
LOOP_STATUS_PLAYLIST,
LOOP_STATUS_TRACK,
type Position,
} from '@jellybrick/mpris-service';
import registerCallback, { type SongInfo } from '@/providers/song-info'; import registerCallback, { type SongInfo } from '@/providers/song-info';
import getSongControls from '@/providers/song-controls'; import getSongControls from '@/providers/song-controls';
import config from '@/config'; import config from '@/config';
import { LoggerPrefix } from '@/utils'; import { LoggerPrefix } from '@/utils';
import type { RepeatMode } from '@/types/datahost-get-state';
import type { QueueResponse } from '@/types/youtube-music-desktop-internal';
class YTPlayer extends MprisPlayer { class YTPlayer extends MprisPlayer {
/** /**
* @type {number} The current position in microseconds * @type {number} The current position in microseconds
@ -14,12 +29,7 @@ class YTPlayer extends MprisPlayer {
*/ */
private currentPosition: number; private currentPosition: number;
constructor(opts: { constructor(opts: PlayerOptions) {
name: string;
identity: string;
supportedMimeTypes?: string[];
supportedInterfaces?: string[];
}) {
super(opts); super(opts);
this.currentPosition = 0; this.currentPosition = 0;
@ -33,35 +43,38 @@ class YTPlayer extends MprisPlayer {
return this.currentPosition; return this.currentPosition;
} }
setLoopStatus(status: string) { setLoopStatus(status: LoopStatus) {
this.loopStatus = status; this.loopStatus = status;
} }
isPlaying(): boolean { isPlaying(): boolean {
return this.playbackStatus === YTPlayer.PLAYBACK_STATUS_PLAYING; return this.playbackStatus === PLAYBACK_STATUS_PLAYING;
} }
isPaused(): boolean { isPaused(): boolean {
return this.playbackStatus === YTPlayer.PLAYBACK_STATUS_PAUSED; return this.playbackStatus === PLAYBACK_STATUS_PAUSED;
} }
isStopped(): boolean { isStopped(): boolean {
return this.playbackStatus === YTPlayer.PLAYBACK_STATUS_STOPPED; return this.playbackStatus === PLAYBACK_STATUS_STOPPED;
} }
setPlaybackStatus(status: string) { setPlaybackStatus(status: PlayBackStatus) {
this.playbackStatus = status; this.playbackStatus = status;
} }
} }
function setupMPRIS() { function setupMPRIS() {
const instance = new YTPlayer({ const instance = new YTPlayer({
name: 'youtube-music', name: 'YoutubeMusic',
identity: 'YouTube Music', identity: 'YouTube Music',
supportedMimeTypes: ['audio/mpeg'], supportedMimeTypes: ['audio/mpeg'],
supportedInterfaces: ['player'], supportedInterfaces: ['player'],
}); });
instance.canRaise = true; instance.canRaise = true;
instance.canQuit = false;
instance.canSetFullscreen = true;
instance.supportedUriSchemes = ['http', 'https']; instance.supportedUriSchemes = ['http', 'https'];
instance.desktopEntry = 'youtube-music'; instance.desktopEntry = 'youtube-music';
return instance; return instance;
@ -73,21 +86,27 @@ function registerMPRIS(win: BrowserWindow) {
playPause, playPause,
next, next,
previous, previous,
volumeMinus10, setVolume,
volumePlus10,
shuffle, shuffle,
switchRepeat, switchRepeat,
setFullscreen,
requestFullscreenInformation,
requestQueueInformation,
} = songControls; } = songControls;
try { try {
let currentSongInfo: SongInfo | null = null; let currentSongInfo: SongInfo | null = null;
const secToMicro = (n: number) => Math.round(Number(n) * 1e6); const secToMicro = (n: number) => Math.round(Number(n) * 1e6);
const microToSec = (n: number) => Math.round(Number(n) / 1e6); const microToSec = (n: number) => Math.round(Number(n) / 1e6);
const seekTo = (event: { const correctId = (videoId: string) => {
trackId: string; return videoId.replace('-', '_MINUS_');
position: number; };
}) => {
if (event.trackId === currentSongInfo?.videoId) { const seekTo = (event: Position) => {
if (
currentSongInfo?.videoId &&
event.trackId.endsWith(correctId(currentSongInfo.videoId))
) {
win.webContents.send('ytmd:seek-to', microToSec(event.position ?? 0)); win.webContents.send('ytmd:seek-to', microToSec(event.position ?? 0));
} }
}; };
@ -101,6 +120,10 @@ function registerMPRIS(win: BrowserWindow) {
win.webContents.send('ytmd:setup-time-changed-listener', 'mpris'); win.webContents.send('ytmd:setup-time-changed-listener', 'mpris');
win.webContents.send('ytmd:setup-repeat-changed-listener', 'mpris'); win.webContents.send('ytmd:setup-repeat-changed-listener', 'mpris');
win.webContents.send('ytmd:setup-volume-changed-listener', 'mpris'); win.webContents.send('ytmd:setup-volume-changed-listener', 'mpris');
win.webContents.send('ytmd:setup-fullscreen-changed-listener', 'mpris');
win.webContents.send('ytmd:setup-autoplay-changed-listener', 'mpris');
requestFullscreenInformation();
requestQueueInformation();
}); });
ipcMain.on('ytmd:seeked', (_, t: number) => player.seeked(secToMicro(t))); ipcMain.on('ytmd:seeked', (_, t: number) => player.seeked(secToMicro(t)));
@ -109,29 +132,85 @@ function registerMPRIS(win: BrowserWindow) {
player.setPosition(secToMicro(t)); player.setPosition(secToMicro(t));
}); });
ipcMain.on('ytmd:repeat-changed', (_, mode: string) => { ipcMain.on('ytmd:repeat-changed', (_, mode: RepeatMode) => {
switch (mode) { switch (mode) {
case 'NONE': { case 'NONE': {
player.setLoopStatus(YTPlayer.LOOP_STATUS_NONE); player.setLoopStatus(LOOP_STATUS_NONE);
break; break;
} }
case 'ONE': { case 'ONE': {
player.setLoopStatus(YTPlayer.LOOP_STATUS_TRACK); player.setLoopStatus(LOOP_STATUS_TRACK);
break; break;
} }
case 'ALL': { case 'ALL': {
player.setLoopStatus(YTPlayer.LOOP_STATUS_PLAYLIST); player.setLoopStatus(LOOP_STATUS_PLAYLIST);
// No default // No default
break; break;
} }
} }
requestQueueInformation();
}); });
player.on('loopStatus', (status: string) => {
ipcMain.on('ytmd:fullscreen-changed', (_, changedTo: boolean) => {
if (player.fullscreen === undefined || !player.canSetFullscreen) {
return;
}
player.fullscreen =
changedTo !== undefined ? changedTo : !player.fullscreen;
});
ipcMain.on(
'ytmd:set-fullscreen',
(_, isFullscreen: boolean | undefined) => {
if (!player.canSetFullscreen || isFullscreen === undefined) {
return;
}
player.fullscreen = isFullscreen;
},
);
ipcMain.on(
'ytmd:fullscreen-changed-supported',
(_, isFullscreenSupported: boolean) => {
player.canSetFullscreen = isFullscreenSupported;
},
);
ipcMain.on('ytmd:autoplay-changed', (_) => {
requestQueueInformation();
});
ipcMain.on('ytmd:get-queue-response', (_, queue: QueueResponse) => {
if (!queue) {
return;
}
const currentPosition = queue.items?.findIndex((it) =>
it?.playlistPanelVideoRenderer?.selected ||
it?.playlistPanelVideoWrapperRenderer?.primaryRenderer?.playlistPanelVideoRenderer?.selected
) ?? 0;
player.canGoPrevious = currentPosition !== 0;
let hasNext: boolean;
if (queue.autoPlaying) {
hasNext = true;
} else if (player.loopStatus === LOOP_STATUS_PLAYLIST) {
hasNext = true;
} else {
// Example: currentPosition = 0, queue.items.length = 29 -> hasNext = true
hasNext = !!(currentPosition - (queue?.items?.length ?? 0 - 1));
}
player.canGoNext = hasNext;
});
player.on('loopStatus', (status: LoopStatus) => {
// SwitchRepeat cycles between states in that order // SwitchRepeat cycles between states in that order
const switches = [ const switches = [
YTPlayer.LOOP_STATUS_NONE, LOOP_STATUS_NONE,
YTPlayer.LOOP_STATUS_PLAYLIST, LOOP_STATUS_PLAYLIST,
YTPlayer.LOOP_STATUS_TRACK, LOOP_STATUS_TRACK,
]; ];
const currentIndex = switches.indexOf(player.loopStatus); const currentIndex = switches.indexOf(player.loopStatus);
const targetIndex = switches.indexOf(status); const targetIndex = switches.indexOf(status);
@ -142,33 +221,44 @@ function registerMPRIS(win: BrowserWindow) {
}); });
player.on('raise', () => { player.on('raise', () => {
if (!player.canRaise) {
return;
}
win.setSkipTaskbar(false); win.setSkipTaskbar(false);
win.show(); win.show();
}); });
player.on('fullscreen', (fullscreenEnabled: boolean) => {
setFullscreen(fullscreenEnabled);
});
player.on('play', () => { player.on('play', () => {
if (!player.isPlaying()) { if (!player.isPlaying()) {
player.setPlaybackStatus(YTPlayer.PLAYBACK_STATUS_PLAYING); player.setPlaybackStatus(PLAYBACK_STATUS_PLAYING);
playPause(); playPause();
} }
}); });
player.on('pause', () => { player.on('pause', () => {
if (player.playbackStatus !== YTPlayer.PLAYBACK_STATUS_PAUSED) { if (!player.isPaused()) {
player.setPlaybackStatus(YTPlayer.PLAYBACK_STATUS_PAUSED); player.setPlaybackStatus(PLAYBACK_STATUS_PAUSED);
playPause(); playPause();
} }
}); });
player.on('playpause', () => { player.on('playpause', () => {
player.setPlaybackStatus( player.setPlaybackStatus(
player.isPlaying() player.isPlaying() ? PLAYBACK_STATUS_PAUSED : PLAYBACK_STATUS_PLAYING,
? YTPlayer.PLAYBACK_STATUS_PAUSED
: YTPlayer.PLAYBACK_STATUS_PLAYING
); );
playPause(); playPause();
}); });
player.on('next', next); player.on('next', () => {
player.on('previous', previous); next();
});
player.on('previous', () => {
previous();
});
player.on('seek', seekBy); player.on('seek', seekBy);
player.on('position', seekTo); player.on('position', seekTo);
@ -176,10 +266,18 @@ function registerMPRIS(win: BrowserWindow) {
player.on('shuffle', (enableShuffle) => { player.on('shuffle', (enableShuffle) => {
if (enableShuffle) { if (enableShuffle) {
shuffle(); shuffle();
requestQueueInformation();
} }
}); });
player.on('open', (args: { uri: string }) => { player.on('open', (args: { uri: string }) => {
win.loadURL(args.uri); win.loadURL(args.uri).then(() => {
requestQueueInformation();
});
});
player.on('error', (error: Error) => {
console.error(LoggerPrefix, 'Error in MPRIS');
console.trace(error);
}); });
let mprisVolNewer = false; let mprisVolNewer = false;
@ -198,7 +296,7 @@ function registerMPRIS(win: BrowserWindow) {
} }
}); });
player.on('volume', (newVolume) => { player.on('volume', (newVolume: number) => {
if (config.plugins.isEnabled('precise-volume')) { if (config.plugins.isEnabled('precise-volume')) {
// With precise volume we can set the volume to the exact value. // With precise volume we can set the volume to the exact value.
const newVol = ~~(newVolume * 100); const newVol = ~~(newVolume * 100);
@ -208,31 +306,23 @@ function registerMPRIS(win: BrowserWindow) {
win.webContents.send('setVolume', newVol); win.webContents.send('setVolume', newVol);
} }
} else { } else {
// With keyboard shortcuts we can only change the volume in increments of 10, so round it. setVolume(newVolume * 100);
let deltaVolume = Math.round((newVolume - player.volume) * 10);
while (deltaVolume !== 0 && deltaVolume > 0) {
volumePlus10();
player.volume += 0.1;
deltaVolume--;
}
while (deltaVolume !== 0 && deltaVolume < 0) {
volumeMinus10();
player.volume -= 0.1;
deltaVolume++;
}
} }
}); });
registerCallback((songInfo) => { registerCallback((songInfo: SongInfo) => {
if (player) { if (player) {
const data: Track = { const data: Track = {
'mpris:length': secToMicro(songInfo.songDuration), 'mpris:length': secToMicro(songInfo.songDuration),
'mpris:artUrl': songInfo.imageSrc ?? undefined, ...(songInfo.imageSrc
? { 'mpris:artUrl': songInfo.imageSrc }
: undefined),
'xesam:title': songInfo.title, 'xesam:title': songInfo.title,
'xesam:url': songInfo.url, 'xesam:url': songInfo.url,
'xesam:artist': [songInfo.artist], 'xesam:artist': [songInfo.artist],
'mpris:trackid': songInfo.videoId, 'mpris:trackid': player.objectPath(
`Track/${correctId(songInfo.videoId)}`,
),
}; };
if (songInfo.album) { if (songInfo.album) {
data['xesam:album'] = songInfo.album; data['xesam:album'] = songInfo.album;
@ -241,22 +331,20 @@ function registerMPRIS(win: BrowserWindow) {
player.metadata = data; player.metadata = data;
const currentElapsedMicroSeconds = secToMicro(songInfo.elapsedSeconds ?? 0); const currentElapsedMicroSeconds = secToMicro(
songInfo.elapsedSeconds ?? 0,
);
player.setPosition(currentElapsedMicroSeconds); player.setPosition(currentElapsedMicroSeconds);
player.seeked(currentElapsedMicroSeconds); player.seeked(currentElapsedMicroSeconds);
player.setPlaybackStatus( player.setPlaybackStatus(
songInfo.isPaused ? songInfo.isPaused ? PLAYBACK_STATUS_PAUSED : PLAYBACK_STATUS_PLAYING,
YTPlayer.PLAYBACK_STATUS_PAUSED :
YTPlayer.PLAYBACK_STATUS_PLAYING
); );
} }
requestQueueInformation();
}); });
} catch (error) { } catch (error) {
console.error( console.error(LoggerPrefix, 'Error in MPRIS');
LoggerPrefix,
'Error in MPRIS'
);
console.trace(error); console.trace(error);
} }
} }

View File

@ -6,7 +6,7 @@ import getSongControls from './song-controls';
export const APP_PROTOCOL = 'youtubemusic'; export const APP_PROTOCOL = 'youtubemusic';
let protocolHandler: ((cmd: string) => void) | undefined; let protocolHandler: ((cmd: string, args: string[] | undefined) => void) | undefined;
export function setupProtocolHandler(win: BrowserWindow) { export function setupProtocolHandler(win: BrowserWindow) {
if (process.defaultApp && process.argv.length >= 2) { if (process.defaultApp && process.argv.length >= 2) {
@ -19,18 +19,18 @@ export function setupProtocolHandler(win: BrowserWindow) {
const songControls = getSongControls(win); const songControls = getSongControls(win);
protocolHandler = ((cmd: keyof typeof songControls) => { protocolHandler = ((cmd: keyof typeof songControls, args: string[] | undefined = undefined) => {
if (Object.keys(songControls).includes(cmd)) { if (Object.keys(songControls).includes(cmd)) {
songControls[cmd](); songControls[cmd](args as never);
} }
}) as (cmd: string) => void; }) as (cmd: string) => void;
} }
export function handleProtocol(cmd: string) { export function handleProtocol(cmd: string, args: string[] | undefined) {
protocolHandler?.(cmd); protocolHandler?.(cmd, args);
} }
export function changeProtocolHandler(f: (cmd: string) => void) { export function changeProtocolHandler(f: (cmd: string, args: string[] | undefined) => void) {
protocolHandler = f; protocolHandler = f;
} }

View File

@ -1,43 +1,82 @@
// This is used for to control the songs // This is used for to control the songs
import { BrowserWindow, ipcMain } from 'electron'; import { BrowserWindow } from 'electron';
// see protocol-handler.ts
type ArgsType<T> = T | string[] | undefined;
const parseNumberFromArgsType = (args: ArgsType<number>) => {
if (typeof args === 'number') {
return args;
} else if (Array.isArray(args)) {
return Number(args[0]);
} else {
return null;
}
};
const parseBooleanFromArgsType = (args: ArgsType<boolean>) => {
if (typeof args === 'boolean') {
return args;
} else if (Array.isArray(args)) {
return args[0] === 'true';
} else {
return null;
}
};
export default (win: BrowserWindow) => { export default (win: BrowserWindow) => {
const commands = { return {
// Playback // Playback
previous: () => win.webContents.send('ytmd:previous-video'), previous: () => win.webContents.send('ytmd:previous-video'),
next: () => win.webContents.send('ytmd:next-video'), next: () => win.webContents.send('ytmd:next-video'),
playPause: () => win.webContents.send('ytmd:toggle-play'), playPause: () => win.webContents.send('ytmd:toggle-play'),
like: () => win.webContents.send('ytmd:update-like', 'LIKE'), like: () => win.webContents.send('ytmd:update-like', 'LIKE'),
dislike: () => win.webContents.send('ytmd:update-like', 'DISLIKE'), dislike: () => win.webContents.send('ytmd:update-like', 'DISLIKE'),
go10sBack: () => win.webContents.send('ytmd:seek-by', -10), goBack: (seconds: ArgsType<number>) => {
go10sForward: () => win.webContents.send('ytmd:seek-by', 10), const secondsNumber = parseNumberFromArgsType(seconds);
go1sBack: () => win.webContents.send('ytmd:seek-by', -1), if (secondsNumber !== null) {
go1sForward: () => win.webContents.send('ytmd:seek-by', 1), win.webContents.send('ytmd:seek-by', -secondsNumber);
}
},
goForward: (seconds: ArgsType<number>) => {
const secondsNumber = parseNumberFromArgsType(seconds);
if (secondsNumber !== null) {
win.webContents.send('ytmd:seek-by', seconds);
}
},
shuffle: () => win.webContents.send('ytmd:shuffle'), shuffle: () => win.webContents.send('ytmd:shuffle'),
switchRepeat: (n = 1) => win.webContents.send('ytmd:switch-repeat', n), switchRepeat: (n: ArgsType<number> = 1) => {
const repeat = parseNumberFromArgsType(n);
if (repeat !== null) {
win.webContents.send('ytmd:switch-repeat', n);
}
},
// General // General
volumeMinus10: () => { setVolume: (volume: ArgsType<number>) => {
ipcMain.once('ytmd:get-volume-return', (_, volume) => { const volumeNumber = parseNumberFromArgsType(volume);
win.webContents.send('ytmd:update-volume', volume - 10); if (volumeNumber !== null) {
}); win.webContents.send('ytmd:update-volume', volume);
win.webContents.send('ytmd:get-volume'); }
}, },
volumePlus10: () => { setFullscreen: (isFullscreen: ArgsType<boolean>) => {
ipcMain.once('ytmd:get-volume-return', (_, volume) => { const isFullscreenValue = parseBooleanFromArgsType(isFullscreen);
win.webContents.send('ytmd:update-volume', volume + 10); if (isFullscreenValue !== null) {
}); win.setFullScreen(isFullscreenValue);
win.webContents.send('ytmd:get-volume'); win.webContents.send('ytmd:click-fullscreen-button', isFullscreenValue);
}
},
requestFullscreenInformation: () => {
win.webContents.send('ytmd:get-fullscreen');
},
requestQueueInformation: () => {
win.webContents.send('ytmd:get-queue');
}, },
fullscreen: () => win.webContents.send('ytmd:toggle-fullscreen'),
muteUnmute: () => win.webContents.send('ytmd:toggle-mute'), muteUnmute: () => win.webContents.send('ytmd:toggle-mute'),
search: () => win.webContents.sendInputEvent({ search: () => {
type: 'keyDown', win.webContents.sendInputEvent({
keyCode: '/', type: 'keyDown',
}), keyCode: '/',
}; });
return { },
...commands,
play: commands.playPause,
pause: commands.playPause,
}; };
}; };

View File

@ -62,11 +62,13 @@ export const setupRepeatChangedListener = singleton(() => {
// provided by YouTube Music // provided by YouTube Music
window.ipcRenderer.send( window.ipcRenderer.send(
'ytmd:repeat-changed', 'ytmd:repeat-changed',
document.querySelector< document
HTMLElement & { .querySelector<
getState: () => GetState; HTMLElement & {
} getState: () => GetState;
>('ytmusic-player-bar')?.getState().queue.repeatMode, }
>('ytmusic-player-bar')
?.getState().queue.repeatMode,
); );
}); });
@ -78,6 +80,46 @@ export const setupVolumeChangedListener = singleton((api: YoutubePlayer) => {
window.ipcRenderer.send('ytmd:volume-changed', api.getVolume()); window.ipcRenderer.send('ytmd:volume-changed', api.getVolume());
}); });
export const setupFullScreenChangedListener = singleton(() => {
const playerBar = document.querySelector('ytmusic-player-bar');
if (!playerBar) {
window.ipcRenderer.send('ytmd:fullscreen-changed-supported', false);
return;
}
const observer = new MutationObserver(() => {
window.ipcRenderer.send(
'ytmd:fullscreen-changed',
(
playerBar?.attributes.getNamedItem('player-fullscreened') ?? null
) !== null,
);
});
observer.observe(playerBar, {
attributes: true,
childList: false,
subtree: false,
});
});
export const setupAutoPlayChangedListener = singleton(() => {
const autoplaySlider = document.querySelector<HTMLInputElement>(
'.autoplay > tp-yt-paper-toggle-button',
);
const observer = new MutationObserver(() => {
window.ipcRenderer.send('ytmd:autoplay-changed');
});
observer.observe(autoplaySlider!, {
attributes: true,
childList: false,
subtree: false,
});
});
export default (api: YoutubePlayer) => { export default (api: YoutubePlayer) => {
window.ipcRenderer.on('ytmd:setup-time-changed-listener', () => { window.ipcRenderer.on('ytmd:setup-time-changed-listener', () => {
setupTimeChangedListener(); setupTimeChangedListener();
@ -91,6 +133,14 @@ export default (api: YoutubePlayer) => {
setupVolumeChangedListener(api); setupVolumeChangedListener(api);
}); });
window.ipcRenderer.on('ytmd:setup-fullscreen-changed-listener', () => {
setupFullScreenChangedListener();
});
window.ipcRenderer.on('ytmd:setup-autoplay-changed-listener', () => {
setupAutoPlayChangedListener();
});
window.ipcRenderer.on('ytmd:setup-seeked-listener', () => { window.ipcRenderer.on('ytmd:setup-seeked-listener', () => {
setupSeekedListener(); setupSeekedListener();
}); });
@ -155,13 +205,13 @@ export default (api: YoutubePlayer) => {
function sendSongInfo(videoData: VideoDataChangeValue) { function sendSongInfo(videoData: VideoDataChangeValue) {
const data = api.getPlayerResponse(); const data = api.getPlayerResponse();
data.videoDetails.album = data.videoDetails.album = (
( Object.entries(videoData).find(
Object.entries(videoData) ([, value]) => value && Object.hasOwn(value, 'playerOverlays'),
.find(([, value]) => value && Object.hasOwn(value, 'playerOverlays')) as [string, AlbumDetails | undefined] ) as [string, AlbumDetails | undefined]
)?.[1]?.playerOverlays?.playerOverlayRenderer?.browserMediaSession?.browserMediaSessionRenderer?.album?.runs?.at( )?.[1]?.playerOverlays?.playerOverlayRenderer?.browserMediaSession?.browserMediaSessionRenderer?.album?.runs?.at(
0, 0,
)?.text; )?.text;
data.videoDetails.elapsedSeconds = 0; data.videoDetails.elapsedSeconds = 0;
data.videoDetails.isPaused = false; data.videoDetails.isPaused = false;

View File

@ -120,7 +120,9 @@ const handleData = async (
songInfo.mediaType = MediaType.PodcastEpisode; songInfo.mediaType = MediaType.PodcastEpisode;
// HACK: Podcast's participant is not the artist // HACK: Podcast's participant is not the artist
if (!config.get('options.usePodcastParticipantAsArtist')) { if (!config.get('options.usePodcastParticipantAsArtist')) {
songInfo.artist = cleanupName(data.microformat.microformatDataRenderer.pageOwnerDetails.name); songInfo.artist = cleanupName(
data.microformat.microformatDataRenderer.pageOwnerDetails.name,
);
} }
break; break;
default: default:
@ -128,14 +130,13 @@ const handleData = async (
// HACK: This is a workaround for "podcast" types where "musicVideoType" doesn't exist. Google :facepalm: // HACK: This is a workaround for "podcast" types where "musicVideoType" doesn't exist. Google :facepalm:
if ( if (
!config.get('options.usePodcastParticipantAsArtist') && !config.get('options.usePodcastParticipantAsArtist') &&
( (data.responseContext.serviceTrackingParams
data.responseContext.serviceTrackingParams ?.at(0)
?.at(0) ?.params?.find((it) => it.key === 'ipcc')?.value ?? '1') != '0'
?.params
?.find((it) => it.key === 'ipcc')?.value ?? '1'
) != '0'
) { ) {
songInfo.artist = cleanupName(data.microformat.microformatDataRenderer.pageOwnerDetails.name); songInfo.artist = cleanupName(
data.microformat.microformatDataRenderer.pageOwnerDetails.name,
);
} }
break; break;
} }
@ -165,10 +166,12 @@ const registerProvider = (win: BrowserWindow) => {
// This will be called when the song-info-front finds a new request with song data // This will be called when the song-info-front finds a new request with song data
ipcMain.on('ytmd:video-src-changed', async (_, data: GetPlayerResponse) => { ipcMain.on('ytmd:video-src-changed', async (_, data: GetPlayerResponse) => {
const tempSongInfo = await dataMutex.runExclusive<SongInfo | null>(async () => { const tempSongInfo = await dataMutex.runExclusive<SongInfo | null>(
songInfo = await handleData(data, win); async () => {
return songInfo; songInfo = await handleData(data, win);
}); return songInfo;
},
);
if (tempSongInfo) { if (tempSongInfo) {
for (const c of callbacks) { for (const c of callbacks) {

View File

@ -15,6 +15,8 @@ import { loadI18n, setLanguage, t as i18t } from '@/i18n';
import type { PluginConfig } from '@/types/plugins'; import type { PluginConfig } from '@/types/plugins';
import type { YoutubePlayer } from '@/types/youtube-player'; import type { YoutubePlayer } from '@/types/youtube-player';
import type { QueueElement } from '@/types/queue';
import type { QueueResponse } from '@/types/youtube-music-desktop-internal';
let api: (Element & YoutubePlayer) | null = null; let api: (Element & YoutubePlayer) | null = null;
let isPluginLoaded = false; let isPluginLoaded = false;
@ -61,18 +63,56 @@ async function onApiLoaded() {
} }
}); });
window.ipcRenderer.on('ytmd:update-volume', (_, volume: number) => { window.ipcRenderer.on('ytmd:update-volume', (_, volume: number) => {
document.querySelector<HTMLElement & { updateVolume: (volume: number) => void }>('ytmusic-player-bar')?.updateVolume(volume); document
.querySelector<
HTMLElement & { updateVolume: (volume: number) => void }
>('ytmusic-player-bar')
?.updateVolume(volume);
}); });
window.ipcRenderer.on('ytmd:get-volume', (event) => {
event.sender.emit('ytmd:get-volume-return', api?.getVolume()); const isFullscreen = () => {
const isFullscreen =
document
.querySelector<HTMLElement>('ytmusic-player-bar')
?.attributes.getNamedItem('player-fullscreened') ?? null;
return isFullscreen !== null;
};
const clickFullscreenButton = (isFullscreenValue: boolean) => {
const fullscreen = isFullscreen();
if (isFullscreenValue === fullscreen) {
return;
}
if (fullscreen) {
document.querySelector<HTMLElement>('.exit-fullscreen-button')?.click();
} else {
document.querySelector<HTMLElement>('.fullscreen-button')?.click();
}
};
window.ipcRenderer.on('ytmd:get-fullscreen', (event) => {
event.sender.send('ytmd:set-fullscreen', isFullscreen());
}); });
window.ipcRenderer.on('ytmd:toggle-fullscreen', (_) => {
document.querySelector<HTMLElement & { toggleFullscreen: () => void }>('ytmusic-player-bar')?.toggleFullscreen(); window.ipcRenderer.on('ytmd:click-fullscreen-button', (_, fullscreen: boolean | undefined) => {
clickFullscreenButton(fullscreen ?? false);
}); });
window.ipcRenderer.on('ytmd:toggle-mute', (_) => { window.ipcRenderer.on('ytmd:toggle-mute', (_) => {
document.querySelector<HTMLElement & { onVolumeTap: () => void }>('ytmusic-player-bar')?.onVolumeTap(); document.querySelector<HTMLElement & { onVolumeTap: () => void }>('ytmusic-player-bar')?.onVolumeTap();
}); });
window.ipcRenderer.on('ytmd:get-queue', (event) => {
const queue = document.querySelector<QueueElement>('#queue');
event.sender.send('ytmd:get-queue-response', {
items: queue?.queue.getItems(),
autoPlaying: queue?.queue.autoPlaying,
continuation: queue?.queue.continuation,
} satisfies QueueResponse);
});
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);
@ -236,7 +276,9 @@ const initObserver = async () => {
// check document.documentElement is ready // check document.documentElement is ready
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => resolve(), { once: true }); document.addEventListener('DOMContentLoaded', () => resolve(), {
once: true,
});
} else { } else {
resolve(); resolve();
} }

View File

@ -1,3 +1,5 @@
import type { PlayerConfig } from '@/types/get-player-response';
export interface GetState { export interface GetState {
castStatus: CastStatus; castStatus: CastStatus;
entities: Entities; entities: Entities;
@ -32,17 +34,11 @@ export interface Download {
export interface Entities {} export interface Entities {}
export interface LikeStatus { export interface LikeStatus {
videos: Videos; videos: Record<string, LikeType>;
playlists: Entities; playlists: Entities;
} }
export interface Videos { export enum LikeType {
tNVTuUEeWP0: Kqp1PyPRBzA;
KQP1PyPrBzA: Kqp1PyPRBzA;
'o1iz4L-5zkQ': Kqp1PyPRBzA;
}
export enum Kqp1PyPRBzA {
Dislike = 'DISLIKE', Dislike = 'DISLIKE',
Indifferent = 'INDIFFERENT', Indifferent = 'INDIFFERENT',
Like = 'LIKE', Like = 'LIKE',
@ -195,14 +191,10 @@ export interface Target {
export interface CommandWatchEndpoint { export interface CommandWatchEndpoint {
videoId: string; videoId: string;
params: PurpleParams; params: string;
watchEndpointMusicSupportedConfigs: PurpleWatchEndpointMusicSupportedConfigs; watchEndpointMusicSupportedConfigs: PurpleWatchEndpointMusicSupportedConfigs;
} }
export enum PurpleParams {
WAEB = 'wAEB',
}
export interface PurpleWatchEndpointMusicSupportedConfigs { export interface PurpleWatchEndpointMusicSupportedConfigs {
watchEndpointMusicConfig: PurpleWatchEndpointMusicConfig; watchEndpointMusicConfig: PurpleWatchEndpointMusicConfig;
} }
@ -381,7 +373,7 @@ export enum SharePanelType {
export interface PurpleWatchEndpoint { export interface PurpleWatchEndpoint {
videoId: string; videoId: string;
playlistId: string; playlistId: string;
params: PurpleParams; params: string;
loggingContext: LoggingContext; loggingContext: LoggingContext;
watchEndpointMusicSupportedConfigs: PurpleWatchEndpointMusicSupportedConfigs; watchEndpointMusicSupportedConfigs: PurpleWatchEndpointMusicSupportedConfigs;
} }
@ -466,7 +458,7 @@ export interface FeedbackEndpoint {
} }
export interface PurpleLikeEndpoint { export interface PurpleLikeEndpoint {
status: Kqp1PyPRBzA; status: LikeType;
target: Target; target: Target;
actions?: LikeEndpointAction[]; actions?: LikeEndpointAction[];
} }
@ -488,7 +480,7 @@ export interface PurpleToggledServiceEndpoint {
} }
export interface FluffyLikeEndpoint { export interface FluffyLikeEndpoint {
status: Kqp1PyPRBzA; status: LikeType;
target: Target; target: Target;
} }
@ -690,7 +682,7 @@ export interface FluffyDefaultServiceEndpoint {
} }
export interface TentacledLikeEndpoint { export interface TentacledLikeEndpoint {
status: Kqp1PyPRBzA; status: LikeType;
target: AddToPlaylistEndpoint; target: AddToPlaylistEndpoint;
actions?: LikeEndpointAction[]; actions?: LikeEndpointAction[];
} }
@ -702,7 +694,7 @@ export interface FluffyToggledServiceEndpoint {
} }
export interface StickyLikeEndpoint { export interface StickyLikeEndpoint {
status: Kqp1PyPRBzA; status: LikeType;
target: AddToPlaylistEndpoint; target: AddToPlaylistEndpoint;
} }
@ -1185,81 +1177,6 @@ export interface PtrackingURLClass {
headers: HeaderElement[]; headers: HeaderElement[];
} }
export interface PlayerConfig {
audioConfig: AudioConfig;
streamSelectionConfig: StreamSelectionConfig;
mediaCommonConfig: MediaCommonConfig;
webPlayerConfig: WebPlayerConfig;
}
export interface AudioConfig {
loudnessDb: number;
perceptualLoudnessDb: number;
enablePerFormatLoudness: boolean;
}
export interface MediaCommonConfig {
dynamicReadaheadConfig: DynamicReadaheadConfig;
}
export interface DynamicReadaheadConfig {
maxReadAheadMediaTimeMs: number;
minReadAheadMediaTimeMs: number;
readAheadGrowthRateMs: number;
}
export interface StreamSelectionConfig {
maxBitrate: string;
}
export interface WebPlayerConfig {
useCobaltTvosDash: boolean;
webPlayerActionsPorting: WebPlayerActionsPorting;
gatewayExperimentGroup: string;
}
export interface WebPlayerActionsPorting {
subscribeCommand: SubscribeCommand;
unsubscribeCommand: UnsubscribeCommand;
addToWatchLaterCommand: AddToWatchLaterCommand;
removeFromWatchLaterCommand: RemoveFromWatchLaterCommand;
}
export interface AddToWatchLaterCommand {
clickTrackingParams: string;
playlistEditEndpoint: AddToWatchLaterCommandPlaylistEditEndpoint;
}
export interface AddToWatchLaterCommandPlaylistEditEndpoint {
playlistId: string;
actions: PurpleAction[];
}
export interface PurpleAction {
addedVideoId: string;
action: string;
}
export interface RemoveFromWatchLaterCommand {
clickTrackingParams: string;
playlistEditEndpoint: RemoveFromWatchLaterCommandPlaylistEditEndpoint;
}
export interface RemoveFromWatchLaterCommandPlaylistEditEndpoint {
playlistId: string;
actions: FluffyAction[];
}
export interface FluffyAction {
action: string;
removedVideoId: string;
}
export interface SubscribeCommand {
clickTrackingParams: string;
subscribeEndpoint: SubscribeEndpoint;
}
export interface Storyboards { export interface Storyboards {
playerStoryboardSpecRenderer: PlayerStoryboardSpecRenderer; playerStoryboardSpecRenderer: PlayerStoryboardSpecRenderer;
} }
@ -1384,7 +1301,7 @@ export interface PlayerOverlayRendererAction {
export interface LikeButtonRenderer { export interface LikeButtonRenderer {
target: Target; target: Target;
likeStatus: Kqp1PyPRBzA; likeStatus: LikeType;
trackingParams: string; trackingParams: string;
likesAllowed: boolean; likesAllowed: boolean;
serviceEndpoints: ServiceEndpoint[]; serviceEndpoints: ServiceEndpoint[];
@ -1396,13 +1313,14 @@ export interface ServiceEndpoint {
} }
export interface ServiceEndpointLikeEndpoint { export interface ServiceEndpointLikeEndpoint {
status: Kqp1PyPRBzA; status: LikeType;
target: Target; target: Target;
likeParams?: LikeParams; likeParams?: LikeParams;
dislikeParams?: LikeParams; dislikeParams?: LikeParams;
removeLikeParams?: LikeParams; removeLikeParams?: LikeParams;
} }
// TODO: Add more
export enum LikeParams { export enum LikeParams {
Oai3D = 'OAI%3D', Oai3D = 'OAI%3D',
} }
@ -1467,16 +1385,12 @@ export interface CurrentVideoEndpoint {
export interface CurrentVideoEndpointWatchEndpoint { export interface CurrentVideoEndpointWatchEndpoint {
videoId: string; videoId: string;
playlistId: PlaylistID; playlistId: string;
index: number; index: number;
playlistSetVideoId: string; playlistSetVideoId: string;
loggingContext: LoggingContext; loggingContext: LoggingContext;
} }
export enum PlaylistID {
RDAMVMrkaNKAvksDE = 'RDAMVMrkaNKAvksDE',
}
export interface PlayerPageWatchNextResponseResponseContext { export interface PlayerPageWatchNextResponseResponseContext {
serviceTrackingParams: ServiceTrackingParam[]; serviceTrackingParams: ServiceTrackingParam[];
} }
@ -1536,6 +1450,8 @@ export interface FlagEndpoint {
flagAction: string; flagAction: string;
} }
export type RepeatMode = 'NONE' | 'ONE' | 'ALL';
export interface Queue { export interface Queue {
automixItems: unknown[]; automixItems: unknown[];
autoplay: boolean; autoplay: boolean;
@ -1553,7 +1469,7 @@ export interface Queue {
nextQueueItemId: number; nextQueueItemId: number;
playbackContentMode: string; playbackContentMode: string;
queueContextParams: string; queueContextParams: string;
repeatMode: string; repeatMode: RepeatMode;
responsiveSignals: ResponsiveSignals; responsiveSignals: ResponsiveSignals;
selectedItemIndex: number; selectedItemIndex: number;
shuffleEnabled: boolean; shuffleEnabled: boolean;
@ -1642,23 +1558,15 @@ export interface PlaylistPanelVideoRendererNavigationEndpoint {
export interface FluffyWatchEndpoint { export interface FluffyWatchEndpoint {
videoId: string; videoId: string;
playlistId?: PlaylistID; playlistId?: string;
index: number; index: number;
params: FluffyParams; params: string;
playerParams?: PlayerParams; playerParams?: string;
playlistSetVideoId?: string; playlistSetVideoId?: string;
loggingContext?: LoggingContext; loggingContext?: LoggingContext;
watchEndpointMusicSupportedConfigs: FluffyWatchEndpointMusicSupportedConfigs; watchEndpointMusicSupportedConfigs: FluffyWatchEndpointMusicSupportedConfigs;
} }
export enum FluffyParams {
OAHyAQIIAQ3D3D = 'OAHyAQIIAQ%3D%3D',
}
export enum PlayerParams {
The8Aub = '8AUB',
}
export interface FluffyWatchEndpointMusicSupportedConfigs { export interface FluffyWatchEndpointMusicSupportedConfigs {
watchEndpointMusicConfig: FluffyWatchEndpointMusicConfig; watchEndpointMusicConfig: FluffyWatchEndpointMusicConfig;
} }

40
src/types/queue.ts Normal file
View File

@ -0,0 +1,40 @@
import type { YoutubePlayer } from '@/types/youtube-player';
import type { GetState, QueueItem } from '@/types/datahost-get-state';
type StoreState = GetState;
type Store = {
dispatch: (obj: {
type: string;
payload?: {
items?: QueueItem[];
};
}) => void;
getState: () => StoreState;
replaceReducer: (param1: unknown) => unknown;
subscribe: (callback: () => void) => unknown;
}
export type QueueElement = HTMLElement & {
dispatch(obj: {
type: string;
payload?: unknown;
}): void;
queue: QueueAPI;
};
export type QueueAPI = {
getItems(): QueueItem[];
store: {
store: Store,
};
continuation?: string;
autoPlaying?: boolean;
};
export type AppElement = HTMLElement & AppAPI;
export type AppAPI = {
queue_: QueueAPI;
playerApi_: YoutubePlayer;
openToast: (message: string) => void;
// TODO: Add more
};

View File

@ -0,0 +1,7 @@
import type { QueueItem } from '@/types/datahost-get-state';
export interface QueueResponse {
items?: QueueItem[];
autoPlaying?: boolean;
continuation?: string;
}