feat(plugin): add onPlayerApiReady hook

Co-authored-by: JellyBrick <shlee1503@naver.com>
This commit is contained in:
Su-Yong
2023-11-12 01:51:26 +09:00
parent 2097f42efb
commit a4f4ecb569
22 changed files with 273 additions and 291 deletions

View File

@ -112,7 +112,7 @@ const initHook = (win: BrowserWindow) => {
const oldConfig = oldPluginConfigList[id] as PluginBaseConfig; const oldConfig = oldPluginConfigList[id] as PluginBaseConfig;
const config = deepmerge(pluginBuilders[id as keyof PluginBuilderList].config, newPluginConfig) as PluginBaseConfig; const config = deepmerge(pluginBuilders[id as keyof PluginBuilderList].config, newPluginConfig) as PluginBaseConfig;
if (config.enabled !== oldConfig.enabled) { if (config.enabled !== oldConfig?.enabled) {
if (config.enabled) { if (config.enabled) {
win.webContents.send('plugin:enable', id); win.webContents.send('plugin:enable', id);
ipcMain.emit('plugin:enable', id); ipcMain.emit('plugin:enable', id);

View File

@ -63,15 +63,23 @@ export default builder.createRenderer(() => {
} }
} }
let playerPage: HTMLElement | null = null;
let navBarBackground: HTMLElement | null = null;
let ytmusicPlayerBar: HTMLElement | null = null;
let playerBarBackground: HTMLElement | null = null;
let sidebarBig: HTMLElement | null = null;
let sidebarSmall: HTMLElement | null = null;
let ytmusicAppLayout: HTMLElement | null = null;
return { return {
onLoad() { onLoad() {
const playerPage = document.querySelector<HTMLElement>('#player-page'); playerPage = document.querySelector<HTMLElement>('#player-page');
const navBarBackground = document.querySelector<HTMLElement>('#nav-bar-background'); navBarBackground = document.querySelector<HTMLElement>('#nav-bar-background');
const ytmusicPlayerBar = document.querySelector<HTMLElement>('ytmusic-player-bar'); ytmusicPlayerBar = document.querySelector<HTMLElement>('ytmusic-player-bar');
const playerBarBackground = document.querySelector<HTMLElement>('#player-bar-background'); playerBarBackground = document.querySelector<HTMLElement>('#player-bar-background');
const sidebarBig = document.querySelector<HTMLElement>('#guide-wrapper'); sidebarBig = document.querySelector<HTMLElement>('#guide-wrapper');
const sidebarSmall = document.querySelector<HTMLElement>('#mini-guide-background'); sidebarSmall = document.querySelector<HTMLElement>('#mini-guide-background');
const ytmusicAppLayout = document.querySelector<HTMLElement>('#layout'); ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
const observer = new MutationObserver((mutationsList) => { const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) { for (const mutation of mutationsList) {
@ -91,39 +99,38 @@ export default builder.createRenderer(() => {
if (playerPage) { if (playerPage) {
observer.observe(playerPage, { attributes: true }); observer.observe(playerPage, { attributes: true });
} }
},
onPlayerApiReady(playerApi) {
const fastAverageColor = new FastAverageColor();
document.addEventListener('apiLoaded', (apiEvent) => { playerApi.addEventListener('videodatachange', (name: string) => {
const fastAverageColor = new FastAverageColor(); if (name === 'dataloaded') {
const playerResponse = playerApi.getPlayerResponse();
apiEvent.detail.addEventListener('videodatachange', (name: string) => { const thumbnail = playerResponse?.videoDetails?.thumbnail?.thumbnails?.at(0);
if (name === 'dataloaded') { if (thumbnail) {
const playerResponse = apiEvent.detail.getPlayerResponse(); fastAverageColor.getColorAsync(thumbnail.url)
const thumbnail = playerResponse?.videoDetails?.thumbnail?.thumbnails?.at(0); .then((albumColor) => {
if (thumbnail) { if (albumColor) {
fastAverageColor.getColorAsync(thumbnail.url) [hue, saturation, lightness] = hexToHSL(albumColor.hex);
.then((albumColor) => { changeElementColor(playerPage, hue, saturation, lightness - 30);
if (albumColor) { changeElementColor(navBarBackground, hue, saturation, lightness - 15);
[hue, saturation, lightness] = hexToHSL(albumColor.hex); changeElementColor(ytmusicPlayerBar, hue, saturation, lightness - 15);
changeElementColor(playerPage, hue, saturation, lightness - 30); changeElementColor(playerBarBackground, hue, saturation, lightness - 15);
changeElementColor(navBarBackground, hue, saturation, lightness - 15); changeElementColor(sidebarBig, hue, saturation, lightness - 15);
changeElementColor(ytmusicPlayerBar, hue, saturation, lightness - 15); if (ytmusicAppLayout?.hasAttribute('player-page-open')) {
changeElementColor(playerBarBackground, hue, saturation, lightness - 15); changeElementColor(sidebarSmall, hue, saturation, lightness - 30);
changeElementColor(sidebarBig, hue, saturation, lightness - 15);
if (ytmusicAppLayout?.hasAttribute('player-page-open')) {
changeElementColor(sidebarSmall, hue, saturation, lightness - 30);
}
const ytRightClickList = document.querySelector<HTMLElement>('tp-yt-paper-listbox');
changeElementColor(ytRightClickList, hue, saturation, lightness - 15);
} else {
if (playerPage) {
playerPage.style.backgroundColor = '#000000';
}
} }
}) const ytRightClickList = document.querySelector<HTMLElement>('tp-yt-paper-listbox');
.catch((e) => console.error(e)); changeElementColor(ytRightClickList, hue, saturation, lightness - 15);
} } else {
if (playerPage) {
playerPage.style.backgroundColor = '#000000';
}
}
})
.catch((e) => console.error(e));
} }
}); }
}); });
} }
}; };

View File

@ -5,8 +5,6 @@ import builder from './index';
import { ElementFromHtml } from '../utils/renderer'; import { ElementFromHtml } from '../utils/renderer';
import { YoutubePlayer } from '../../types/youtube-player'; import { YoutubePlayer } from '../../types/youtube-player';
import type { ConfigType } from '../../config/dynamic';
interface LanguageOptions { interface LanguageOptions {
displayName: string; displayName: string;
id: string | null; id: string | null;
@ -82,30 +80,24 @@ export default builder.createRenderer(({ getConfig, setConfig }) => {
} }
}; };
const listener = ({ detail }: {
detail: YoutubePlayer;
}) => {
api = detail;
$('.right-controls-buttons').append(captionsSettingsButton);
captionTrackList = api.getOption<LanguageOptions[]>('captions', 'tracklist') ?? [];
$('video').addEventListener('srcChanged', videoChangeListener);
captionsSettingsButton.addEventListener('click', captionsButtonClickListener);
};
const removeListener = () => { const removeListener = () => {
$('.right-controls-buttons').removeChild(captionsSettingsButton); $('.right-controls-buttons').removeChild(captionsSettingsButton);
$<YoutubePlayer & HTMLElement>('#movie_player').unloadModule('captions'); $<YoutubePlayer & HTMLElement>('#movie_player').unloadModule('captions');
document.removeEventListener('apiLoaded', listener);
}; };
return { return {
async onLoad() { async onLoad() {
config = await getConfig(); config = await getConfig();
},
onPlayerApiReady(playerApi) {
api = playerApi;
document.addEventListener('apiLoaded', listener, { once: true, passive: true }); $('.right-controls-buttons').append(captionsSettingsButton);
captionTrackList = api.getOption<LanguageOptions[]>('captions', 'tracklist') ?? [];
$('video').addEventListener('srcChanged', videoChangeListener);
captionsSettingsButton.addEventListener('click', captionsButtonClickListener);
}, },
onUnload() { onUnload() {
removeListener(); removeListener();

View File

@ -85,7 +85,7 @@ export default builder.createRenderer(({ getConfig, invoke }) => {
}); });
// Exit just before the end for the transition // Exit just before the end for the transition
const transitionBeforeEnd = async () => { const transitionBeforeEnd = () => {
if ( if (
video.currentTime >= video.duration - config.secondsBeforeEnd video.currentTime >= video.duration - config.secondsBeforeEnd
&& isReadyToCrossfade() && isReadyToCrossfade()
@ -140,14 +140,11 @@ export default builder.createRenderer(({ getConfig, invoke }) => {
}; };
return { return {
onLoad() { async onLoad() {
document.addEventListener('apiLoaded', async () => { config = await getConfig();
config = await getConfig(); },
onApiLoaded(); onPlayerApiReady() {
}, { onApiLoaded();
once: true,
passive: true,
});
}, },
onConfigChange(newConfig) { onConfigChange(newConfig) {
config = newConfig; config = newConfig;

View File

@ -5,7 +5,7 @@ import type { YoutubePlayer } from '../../types/youtube-player';
export default builder.createRenderer(({ getConfig }) => { export default builder.createRenderer(({ getConfig }) => {
let config: Awaited<ReturnType<typeof getConfig>>; let config: Awaited<ReturnType<typeof getConfig>>;
let apiEvent: CustomEvent<YoutubePlayer>; let apiEvent: YoutubePlayer;
const timeUpdateListener = (e: Event) => { const timeUpdateListener = (e: Event) => {
if (e.target instanceof HTMLVideoElement) { if (e.target instanceof HTMLVideoElement) {
@ -15,27 +15,25 @@ export default builder.createRenderer(({ getConfig }) => {
const eventListener = async (name: string) => { const eventListener = async (name: string) => {
if (config.applyOnce) { if (config.applyOnce) {
apiEvent.detail.removeEventListener('videodatachange', eventListener); apiEvent.removeEventListener('videodatachange', eventListener);
} }
if (name === 'dataloaded') { if (name === 'dataloaded') {
apiEvent.detail.pauseVideo(); apiEvent.pauseVideo();
document.querySelector<HTMLVideoElement>('video')?.addEventListener('timeupdate', timeUpdateListener, { once: true }); document.querySelector<HTMLVideoElement>('video')?.addEventListener('timeupdate', timeUpdateListener, { once: true });
} }
}; };
return { return {
async onLoad() { async onPlayerApiReady(api) {
config = await getConfig(); config = await getConfig();
document.addEventListener('apiLoaded', (api) => { apiEvent = api;
apiEvent = api;
apiEvent.detail.addEventListener('videodatachange', eventListener); apiEvent.addEventListener('videodatachange', eventListener);
}, { once: true, passive: true });
}, },
onUnload() { onUnload() {
apiEvent.detail.removeEventListener('videodatachange', eventListener); apiEvent.removeEventListener('videodatachange', eventListener);
}, },
onConfigChange(newConfig) { onConfigChange(newConfig) {
config = newConfig; config = newConfig;

View File

@ -14,35 +14,35 @@ const downloadButton = ElementFromHtml(downloadHTML);
let doneFirstLoad = false; let doneFirstLoad = false;
export default builder.createRenderer(({ invoke, on }) => { export default builder.createRenderer(({ invoke, on }) => {
const menuObserver = new MutationObserver(() => {
if (!menu) {
menu = getSongMenu();
if (!menu) {
return;
}
}
if (menu.contains(downloadButton)) {
return;
}
const menuUrl = document.querySelector<HTMLAnchorElement>('tp-yt-paper-listbox [tabindex="-1"] #navigation-endpoint')?.href;
if (!menuUrl?.includes('watch?') && doneFirstLoad) {
return;
}
menu.prepend(downloadButton);
progress = document.querySelector('#ytmcustom-download');
if (doneFirstLoad) {
return;
}
setTimeout(() => doneFirstLoad ||= true, 500);
});
return { return {
onLoad() { onLoad() {
const menuObserver = new MutationObserver(() => {
if (!menu) {
menu = getSongMenu();
if (!menu) {
return;
}
}
if (menu.contains(downloadButton)) {
return;
}
const menuUrl = document.querySelector<HTMLAnchorElement>('tp-yt-paper-listbox [tabindex="-1"] #navigation-endpoint')?.href;
if (!menuUrl?.includes('watch?') && doneFirstLoad) {
return;
}
menu.prepend(downloadButton);
progress = document.querySelector('#ytmcustom-download');
if (doneFirstLoad) {
return;
}
setTimeout(() => doneFirstLoad ||= true, 500);
});
window.download = () => { window.download = () => {
let videoUrl = getSongMenu() let videoUrl = getSongMenu()
// Selector of first button which is always "Start Radio" // Selector of first button which is always "Start Radio"
@ -64,13 +64,6 @@ export default builder.createRenderer(({ invoke, on }) => {
invoke('download-song', videoUrl); invoke('download-song', videoUrl);
}; };
document.addEventListener('apiLoaded', () => {
menuObserver.observe(document.querySelector('ytmusic-popup-container')!, {
childList: true,
subtree: true,
});
}, { once: true, passive: true });
on('downloader-feedback', (feedback: string) => { on('downloader-feedback', (feedback: string) => {
if (progress) { if (progress) {
progress.innerHTML = feedback || 'Download'; progress.innerHTML = feedback || 'Download';
@ -78,6 +71,12 @@ export default builder.createRenderer(({ invoke, on }) => {
console.warn('Cannot update progress'); console.warn('Cannot update progress');
} }
}); });
} },
onPlayerApiReady() {
menuObserver.observe(document.querySelector('ytmusic-popup-container')!, {
childList: true,
subtree: true,
});
},
}; };
}); });

View File

@ -41,10 +41,7 @@ const exponentialVolume = () => {
}; };
export default builder.createRenderer(() => ({ export default builder.createRenderer(() => ({
onLoad() { onPlayerApiReady() {
return document.addEventListener('apiLoaded', exponentialVolume, { exponentialVolume();
once: true,
passive: true,
});
}, },
})); }));

View File

@ -182,15 +182,14 @@ export default builder.createRenderer(({ getConfig, invoke, on }) => {
updateMenu(); updateMenu();
}); });
} }
},
// Increases the right margin of Navbar background when the scrollbar is visible to avoid blocking it (z-index doesn't affect it) // Increases the right margin of Navbar background when the scrollbar is visible to avoid blocking it (z-index doesn't affect it)
document.addEventListener('apiLoaded', () => { onPlayerApiReady() {
const htmlHeadStyle = document.querySelector('head > div > style'); const htmlHeadStyle = document.querySelector('head > div > style');
if (htmlHeadStyle) { if (htmlHeadStyle) {
// HACK: This is a hack to remove the scrollbar width // HACK: This is a hack to remove the scrollbar width
htmlHeadStyle.innerHTML = htmlHeadStyle.innerHTML.replace('html::-webkit-scrollbar {width: var(--ytmusic-scrollbar-width);', 'html::-webkit-scrollbar {'); htmlHeadStyle.innerHTML = htmlHeadStyle.innerHTML.replace('html::-webkit-scrollbar {width: var(--ytmusic-scrollbar-width);', 'html::-webkit-scrollbar {');
} }
}, { once: true, passive: true }); },
}
}; };
}); });

View File

@ -211,7 +211,7 @@ export default (
songControls = getSongControls(win); songControls = getSongControls(win);
let currentSeconds = 0; let currentSeconds = 0;
on('apiLoaded', () => send('setupTimeChangedListener')); on('ytmd:player-api-loaded', () => send('setupTimeChangedListener'));
on('timeChanged', (t: number) => { on('timeChanged', (t: number) => {
currentSeconds = t; currentSeconds = t;

View File

@ -133,11 +133,27 @@ const listenForToggle = () => {
}); });
}; };
function observeMenu(options: PictureInPicturePluginConfig) {
useNativePiP = options.useNativePiP; export default builder.createRenderer(({ getConfig }) => {
document.addEventListener( return {
'apiLoaded', async onLoad() {
() => { const config = await getConfig();
useNativePiP = config.useNativePiP;
if (config.hotkey) {
const hotkeyEvent = toKeyEvent(config.hotkey);
window.addEventListener('keydown', (event) => {
if (
keyEventAreEqual(event, hotkeyEvent)
&& !$<HTMLElement & { opened: boolean }>('ytmusic-search-box')?.opened
) {
togglePictureInPicture();
}
});
}
},
onPlayerApiReady() {
listenForToggle(); listenForToggle();
cloneButton('.player-minimize-button')?.addEventListener('click', async () => { cloneButton('.player-minimize-button')?.addEventListener('click', async () => {
@ -154,28 +170,5 @@ function observeMenu(options: PictureInPicturePluginConfig) {
subtree: true, subtree: true,
}); });
}, },
{ once: true, passive: true },
);
}
export default builder.createRenderer(({ getConfig }) => {
return {
async onLoad() {
const config = await getConfig();
observeMenu(config);
if (config.hotkey) {
const hotkeyEvent = toKeyEvent(config.hotkey);
window.addEventListener('keydown', (event) => {
if (
keyEventAreEqual(event, hotkeyEvent)
&& !$<HTMLElement & { opened: boolean }>('ytmusic-search-box')?.opened
) {
togglePictureInPicture();
}
});
}
}
}; };
}); });

View File

@ -116,12 +116,10 @@ function forcePlaybackRate(e: Event) {
export default builder.createRenderer(() => { export default builder.createRenderer(() => {
return { return {
onLoad() { onPlayerApiReady() {
document.addEventListener('apiLoaded', () => { observePopupContainer();
observePopupContainer(); observeVideo();
observeVideo(); setupWheelListener();
setupWheelListener();
}, { once: true, passive: true });
}, },
onUnload() { onUnload() {
const video = $<HTMLVideoElement>('video'); const video = $<HTMLVideoElement>('video');

View File

@ -258,13 +258,13 @@ export default builder.createRenderer(async ({ on, getConfig, setConfig }) => {
return { return {
onLoad() { onLoad() {
overrideListener(); overrideListener();
},
onPlayerApiReady(playerApi) {
api = playerApi;
document.addEventListener('apiLoaded', (e) => { on('changeVolume', (toIncrease: boolean) => changeVolume(toIncrease));
api = e.detail; on('setVolume', (value: number) => setVolume(value));
on('changeVolume', (toIncrease: boolean) => changeVolume(toIncrease)); firstRun();
on('setVolume', (value: number) => setVolume(value));
firstRun();
}, { once: true, passive: true });
}, },
onConfigChange(config) { onConfigChange(config) {
options = config; options = config;

View File

@ -32,19 +32,19 @@ export default builder.createRenderer(({ invoke }) => {
api.setPlaybackQualityRange(newQuality); api.setPlaybackQualityRange(newQuality);
api.setPlaybackQuality(newQuality); api.setPlaybackQuality(newQuality);
}); });
} };
function setup(event: CustomEvent<YoutubePlayer>) {
api = event.detail;
function setup() {
$('.top-row-buttons.ytmusic-player')?.prepend(qualitySettingsButton); $('.top-row-buttons.ytmusic-player')?.prepend(qualitySettingsButton);
qualitySettingsButton.addEventListener('click', chooseQuality); qualitySettingsButton.addEventListener('click', chooseQuality);
} }
return { return {
onLoad() { onPlayerApiReady(playerApi) {
document.addEventListener('apiLoaded', setup, { once: true, passive: true }); api = playerApi;
setup();
}, },
onUnload() { onUnload() {
$('.top-row-buttons.ytmusic-player')?.removeChild(qualitySettingsButton); $('.top-row-buttons.ytmusic-player')?.removeChild(qualitySettingsButton);

View File

@ -32,7 +32,7 @@ function registerMPRIS(win: BrowserWindow) {
const player = setupMPRIS(); const player = setupMPRIS();
ipcMain.handle('apiLoaded', () => { ipcMain.on('ytmd:player-api-loaded', () => {
win.webContents.send('setupSeekedListener', 'mpris'); win.webContents.send('setupSeekedListener', 'mpris');
win.webContents.send('setupTimeChangedListener', 'mpris'); win.webContents.send('setupTimeChangedListener', 'mpris');
win.webContents.send('setupRepeatChangedListener', 'mpris'); win.webContents.send('setupRepeatChangedListener', 'mpris');

View File

@ -29,15 +29,14 @@ export default builder.createRenderer(({ on }) => {
on('sponsorblock-skip', (_, segments: Segment[]) => { on('sponsorblock-skip', (_, segments: Segment[]) => {
currentSegments = segments; currentSegments = segments;
}); });
},
onPlayerApiReady() {
const video = document.querySelector<HTMLVideoElement>('video');
if (!video) return;
document.addEventListener('apiLoaded', () => { video.addEventListener('timeupdate', timeUpdateListener);
const video = document.querySelector<HTMLVideoElement>('video'); // Reset segments on song end
if (!video) return; video.addEventListener('emptied', resetSegments);
video.addEventListener('timeupdate', timeUpdateListener);
// Reset segments on song end
video.addEventListener('emptied', resetSegments);
}, { once: true, passive: true });
}, },
onUnload() { onUnload() {
const video = document.querySelector<HTMLVideoElement>('video'); const video = document.querySelector<HTMLVideoElement>('video');

View File

@ -54,7 +54,7 @@ const post = (data: Data) => {
export default builder.createMain(({ send, handle, on }) => { export default builder.createMain(({ send, handle, on }) => {
return { return {
onLoad() { onLoad() {
on('apiLoaded', () => send('setupTimeChangedListener')); on('ytmd:player-api-loaded', () => send('setupTimeChangedListener'));
on('timeChanged', (t: number) => { on('timeChanged', (t: number) => {
if (!data.title) { if (!data.title) {
return; return;

View File

@ -2,6 +2,7 @@ import type {
BrowserWindow, BrowserWindow,
MenuItemConstructorOptions, MenuItemConstructorOptions,
} from 'electron'; } from 'electron';
import type { YoutubePlayer } from '../../types/youtube-player';
export type PluginBaseConfig = { export type PluginBaseConfig = {
enabled: boolean; enabled: boolean;
@ -11,7 +12,9 @@ export type BasePlugin<Config extends PluginBaseConfig> = {
onUnload?: () => void; onUnload?: () => void;
onConfigChange?: (newConfig: Config) => void; onConfigChange?: (newConfig: Config) => void;
} }
export type RendererPlugin<Config extends PluginBaseConfig> = BasePlugin<Config>; export type RendererPlugin<Config extends PluginBaseConfig> = BasePlugin<Config> & {
onPlayerApiReady?: (api: YoutubePlayer) => void;
};
export type MainPlugin<Config extends PluginBaseConfig> = Omit<BasePlugin<Config>, 'onLoad' | 'onUnload'> & { export type MainPlugin<Config extends PluginBaseConfig> = Omit<BasePlugin<Config>, 'onLoad' | 'onUnload'> & {
onLoad?: (window: BrowserWindow) => void; onLoad?: (window: BrowserWindow) => void;
onUnload?: (window: BrowserWindow) => void; onUnload?: (window: BrowserWindow) => void;

View File

@ -22,8 +22,8 @@ export default builder.createRenderer(({ getConfig }) => {
const switchButtonDiv = ElementFromHtml(buttonTemplate); const switchButtonDiv = ElementFromHtml(buttonTemplate);
function setup(e: CustomEvent<YoutubePlayer>) { function setup(playerApi: YoutubePlayer) {
api = e.detail; api = playerApi;
player = document.querySelector<(HTMLElement & { videoMode_: boolean; })>('ytmusic-player'); player = document.querySelector<(HTMLElement & { videoMode_: boolean; })>('ytmusic-player');
video = document.querySelector<HTMLVideoElement>('video'); video = document.querySelector<HTMLVideoElement>('video');
@ -194,13 +194,11 @@ export default builder.createRenderer(({ getConfig }) => {
document.querySelector('ytmusic-player')?.removeAttribute('has-av-switcher'); document.querySelector('ytmusic-player')?.removeAttribute('has-av-switcher');
return; return;
} }
default:
case 'custom': {
document.addEventListener('apiLoaded', setup, { once: true, passive: true });
}
} }
}, },
onPlayerApiReady(playerApi) {
if (config.mode !== 'native' && config.mode != 'disabled') setup(playerApi);
},
onConfigChange(newConfig) { onConfigChange(newConfig) {
config = newConfig; config = newConfig;

View File

@ -1,6 +0,0 @@
export const setupSongControls = () => {
document.addEventListener('apiLoaded', (event) => {
window.ipcRenderer.on('seekTo', (_, t: number) => event.detail.seekTo(t));
window.ipcRenderer.on('seekBy', (_, t: number) => event.detail.seekBy(t));
}, { once: true, passive: true });
};

View File

@ -73,74 +73,72 @@ export const setupVolumeChangedListener = singleton((api: YoutubePlayer) => {
window.ipcRenderer.send('volumeChanged', api.getVolume()); window.ipcRenderer.send('volumeChanged', api.getVolume());
}); });
export default () => { export default (api: YoutubePlayer) => {
document.addEventListener('apiLoaded', (apiEvent) => { window.ipcRenderer.on('setupTimeChangedListener', () => {
window.ipcRenderer.on('setupTimeChangedListener', () => { setupTimeChangedListener();
setupTimeChangedListener(); });
});
window.ipcRenderer.on('setupRepeatChangedListener', () => { window.ipcRenderer.on('setupRepeatChangedListener', () => {
setupRepeatChangedListener(); setupRepeatChangedListener();
}); });
window.ipcRenderer.on('setupVolumeChangedListener', () => { window.ipcRenderer.on('setupVolumeChangedListener', () => {
setupVolumeChangedListener(apiEvent.detail); setupVolumeChangedListener(api);
}); });
window.ipcRenderer.on('setupSeekedListener', () => { window.ipcRenderer.on('setupSeekedListener', () => {
setupSeekedListener(); setupSeekedListener();
}); });
const playPausedHandler = (e: Event, status: string) => { const playPausedHandler = (e: Event, status: string) => {
if (e.target instanceof HTMLVideoElement && Math.round(e.target.currentTime) > 0) { if (e.target instanceof HTMLVideoElement && Math.round(e.target.currentTime) > 0) {
window.ipcRenderer.send('playPaused', { window.ipcRenderer.send('playPaused', {
isPaused: status === 'pause', isPaused: status === 'pause',
elapsedSeconds: Math.floor(e.target.currentTime), elapsedSeconds: Math.floor(e.target.currentTime),
}); });
}
};
const playPausedHandlers = {
playing: (e: Event) => playPausedHandler(e, 'playing'),
pause: (e: Event) => playPausedHandler(e, 'pause'),
};
const waitingEvent = new Set<string>();
// Name = "dataloaded" and abit later "dataupdated"
api.addEventListener('videodatachange', (name: string, videoData) => {
if (name === 'dataupdated' && waitingEvent.has(videoData.videoId)) {
waitingEvent.delete(videoData.videoId);
sendSongInfo(videoData);
} else if (name === 'dataloaded') {
const video = $<HTMLVideoElement>('video');
video?.dispatchEvent(srcChangedEvent);
for (const status of ['playing', 'pause'] as const) { // for fix issue that pause event not fired
video?.addEventListener(status, playPausedHandlers[status]);
} }
};
const playPausedHandlers = { waitingEvent.add(videoData.videoId);
playing: (e: Event) => playPausedHandler(e, 'playing'), }
pause: (e: Event) => playPausedHandler(e, 'pause'), });
};
const waitingEvent = new Set<string>(); const video = $('video')!;
// Name = "dataloaded" and abit later "dataupdated" for (const status of ['playing', 'pause'] as const) {
apiEvent.detail.addEventListener('videodatachange', (name: string, videoData) => { video.addEventListener(status, playPausedHandlers[status]);
if (name === 'dataupdated' && waitingEvent.has(videoData.videoId)) { }
waitingEvent.delete(videoData.videoId);
sendSongInfo(videoData);
} else if (name === 'dataloaded') {
const video = $<HTMLVideoElement>('video');
video?.dispatchEvent(srcChangedEvent);
for (const status of ['playing', 'pause'] as const) { // for fix issue that pause event not fired function sendSongInfo(videoData: VideoDataChangeValue) {
video?.addEventListener(status, playPausedHandlers[status]); const data = api.getPlayerResponse();
}
waitingEvent.add(videoData.videoId); data.videoDetails.album = videoData?.Hd?.playerOverlays?.playerOverlayRenderer?.browserMediaSession?.browserMediaSessionRenderer?.album.runs?.at(0)?.text;
} data.videoDetails.elapsedSeconds = 0;
}); data.videoDetails.isPaused = false;
const video = $('video')!; // HACK: This is a workaround for "podcast" type video. GREAT JOB GOOGLE.
for (const status of ['playing', 'pause'] as const) { if (data.playabilityStatus.transportControlsConfig) {
video.addEventListener(status, playPausedHandlers[status]); data.videoDetails.author = data.microformat.microformatDataRenderer.pageOwnerDetails.name;
} }
function sendSongInfo(videoData: VideoDataChangeValue) { window.ipcRenderer.send('video-src-changed', data);
const data = apiEvent.detail.getPlayerResponse(); }
data.videoDetails.album = videoData?.Hd?.playerOverlays?.playerOverlayRenderer?.browserMediaSession?.browserMediaSessionRenderer?.album.runs?.at(0)?.text;
data.videoDetails.elapsedSeconds = 0;
data.videoDetails.isPaused = false;
// HACK: This is a workaround for "podcast" type video. GREAT JOB GOOGLE.
if (data.playabilityStatus.transportControlsConfig) {
data.videoDetails.author = data.microformat.microformatDataRenderer.pageOwnerDetails.name;
}
window.ipcRenderer.send('video-src-changed', data);
}
}, { once: true, passive: true });
}; };

View File

@ -6,22 +6,23 @@ import { pluginBuilders } from 'virtual:PluginBuilders';
import { PluginBaseConfig, PluginBuilder, RendererPluginFactory } from './plugins/utils/builder'; import { PluginBaseConfig, PluginBuilder, RendererPluginFactory } from './plugins/utils/builder';
import { startingPages } from './providers/extracted-data'; import { startingPages } from './providers/extracted-data';
import { setupSongControls } from './providers/song-controls-front';
import setupSongInfo from './providers/song-info-front'; import setupSongInfo from './providers/song-info-front';
import { import {
forceLoadRendererPlugin, forceLoadRendererPlugin,
forceUnloadRendererPlugin, forceUnloadRendererPlugin,
getAllLoadedRendererPlugins, getAllLoadedRendererPlugins, getLoadedRendererPlugin,
loadAllRendererPlugins, loadAllRendererPlugins,
registerRendererPlugin registerRendererPlugin
} from './loader/renderer'; } from './loader/renderer';
import { YoutubePlayer } from './types/youtube-player';
let api: Element | null = null; let api: (Element & YoutubePlayer) | null = null;
function listenForApiLoad() { function listenForApiLoad() {
api = document.querySelector('#movie_player'); api = document.querySelector('#movie_player');
if (api) { if (api) {
onApiLoaded(); onApiLoaded();
return; return;
} }
@ -29,6 +30,7 @@ function listenForApiLoad() {
api = document.querySelector('#movie_player'); api = document.querySelector('#movie_player');
if (api) { if (api) {
observer.disconnect(); observer.disconnect();
onApiLoaded(); onApiLoaded();
} }
}); });
@ -41,6 +43,12 @@ interface YouTubeMusicAppElement extends HTMLElement {
} }
function onApiLoaded() { function onApiLoaded() {
window.ipcRenderer.on('seekTo', (_, t: number) => api!.seekTo(t));
window.ipcRenderer.on('seekBy', (_, t: number) => api!.seekBy(t));
// Inject song-info provider
setupSongInfo(api!);
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);
@ -66,10 +74,14 @@ function onApiLoaded() {
); );
}, },
{ passive: true }, { passive: true },
);! );
document.dispatchEvent(new CustomEvent('apiLoaded', { detail: api })); Object.values(getAllLoadedRendererPlugins())
window.ipcRenderer.send('apiLoaded'); .forEach((plugin) => {
plugin.onPlayerApiReady?.(api!);
});
window.ipcRenderer.send('ytmd:player-api-loaded');
// Navigate to "Starting page" // Navigate to "Starting page"
const startingPage: string = window.mainConfig.get('options.startingPage'); const startingPage: string = window.mainConfig.get('options.startingPage');
@ -112,8 +124,13 @@ function onApiLoaded() {
window.ipcRenderer.on('plugin:unload', (_event, id: keyof PluginBuilderList) => { window.ipcRenderer.on('plugin:unload', (_event, id: keyof PluginBuilderList) => {
forceUnloadRendererPlugin(id); forceUnloadRendererPlugin(id);
}); });
window.ipcRenderer.on('plugin:enable', (_event, id: keyof PluginBuilderList) => { window.ipcRenderer.on('plugin:enable', async (_event, id: keyof PluginBuilderList) => {
forceLoadRendererPlugin(id); await forceLoadRendererPlugin(id);
if (api) {
const plugin = getLoadedRendererPlugin(id);
if (plugin) plugin.onPlayerApiReady?.(api);
}
}); });
window.ipcRenderer.on('config-changed', (_event, id: string, newConfig: PluginBaseConfig) => { window.ipcRenderer.on('config-changed', (_event, id: string, newConfig: PluginBaseConfig) => {
@ -122,12 +139,6 @@ function onApiLoaded() {
if (plugin) plugin.onConfigChange?.(newConfig); if (plugin) plugin.onConfigChange?.(newConfig);
}); });
// Inject song-info provider
setupSongInfo();
// Inject song-controls
setupSongControls();
// Wait for complete load of YouTube api // Wait for complete load of YouTube api
listenForApiLoad(); listenForApiLoad();

1
src/reset.d.ts vendored
View File

@ -13,7 +13,6 @@ declare global {
} }
interface DocumentEventMap { interface DocumentEventMap {
'apiLoaded': CustomEvent<YoutubePlayer>;
'audioCanPlay': CustomEvent<Compressor>; 'audioCanPlay': CustomEvent<Compressor>;
} }