mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-15 04:11:47 +00:00
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:
@ -673,7 +673,9 @@ app.whenReady().then(async () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleProtocol(command);
|
const splited = decodeURIComponent(command).split(' ');
|
||||||
|
|
||||||
|
handleProtocol(splited.shift()!, splited);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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: {
|
||||||
|
|||||||
@ -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: {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
110
src/plugins/shortcuts/mpris-service.d.ts
vendored
110
src/plugins/shortcuts/mpris-service.d.ts
vendored
@ -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 {
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
40
src/types/queue.ts
Normal 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
|
||||||
|
};
|
||||||
7
src/types/youtube-music-desktop-internal.ts
Normal file
7
src/types/youtube-music-desktop-internal.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import type { QueueItem } from '@/types/datahost-get-state';
|
||||||
|
|
||||||
|
export interface QueueResponse {
|
||||||
|
items?: QueueItem[];
|
||||||
|
autoPlaying?: boolean;
|
||||||
|
continuation?: string;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user