mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 10:31:47 +00:00
feat(plugin): add onPlayerApiReady hook
Co-authored-by: JellyBrick <shlee1503@naver.com>
This commit is contained in:
@ -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);
|
||||||
|
|||||||
@ -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));
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -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 });
|
},
|
||||||
}
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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 });
|
|
||||||
};
|
|
||||||
@ -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 });
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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
1
src/reset.d.ts
vendored
@ -13,7 +13,6 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface DocumentEventMap {
|
interface DocumentEventMap {
|
||||||
'apiLoaded': CustomEvent<YoutubePlayer>;
|
|
||||||
'audioCanPlay': CustomEvent<Compressor>;
|
'audioCanPlay': CustomEvent<Compressor>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user