orchard/resources/functions/handler.js
2021-11-19 17:02:28 -06:00

1467 lines
57 KiB
JavaScript

const {
app,
Menu,
ipcMain,
shell,
dialog,
Notification,
BrowserWindow,
systemPreferences,
nativeTheme,
clipboard
} = require('electron'),
{join, resolve} = require('path'),
{readFile, readFileSync, writeFile, existsSync, watch} = require('fs'),
os = require('os'),
mdns = require('mdns-js'),
Client = require('node-ssdp').Client,
express = require('express'),
audioClient = require('castv2-client').Client,
MediaRendererClient = require('upnp-mediarenderer-client'),
DefaultMediaReceiver = require('castv2-client').DefaultMediaReceiver,
getPort = require('get-port'),
{Stream} = require('stream'),
regedit = require('regedit'),
WaveFile = require('wavefile').WaveFile,
{initAnalytics} = require('./utils');
initAnalytics();
const handler = {
LaunchHandler: () => {
// Version Fetch
if (app.commandLine.hasSwitch('version') || app.commandLine.hasSwitch('v')) {
console.log(app.getVersion())
app.exit()
}
// Verbose Check
if (app.commandLine.hasSwitch('verbose')) {
console.log("[Apple-Music-Electron] User has launched the application with --verbose");
}
// Log File Location
if (app.commandLine.hasSwitch('log') || app.commandLine.hasSwitch('l')) {
console.log(join(app.getPath('userData'), 'logs'))
app.exit()
}
},
InstanceHandler: () => {
console.verbose('[InstanceHandler] Started.')
app.on('second-instance', (_e, argv) => {
console.warn(`[InstanceHandler][SecondInstanceHandler] Second Instance Started with args: [${argv.join(', ')}]`)
// Checks if first instance is authorized and if second instance has protocol args
argv.forEach((value) => {
if (value.includes('ame://') || value.includes('itms://') || value.includes('itmss://') || value.includes('musics://') || value.includes('music://')) {
console.warn(`[InstanceHandler][SecondInstanceHandler] Found Protocol!`)
handler.LinkHandler(value);
}
})
if (argv.includes("--force-quit")) {
console.warn('[InstanceHandler][SecondInstanceHandler] Force Quit found. Quitting App.');
app.isQuiting = true
app.quit()
} else if (app.win && !app.cfg.get('advanced.allowMultipleInstances')) { // If a Second Instance has Been Started
console.warn('[InstanceHandler][SecondInstanceHandler] Showing window.');
app.win.show()
app.win.focus()
}
})
if (!app.requestSingleInstanceLock() && !app.cfg.get('advanced.allowMultipleInstances')) {
console.warn("[InstanceHandler] Existing Instance is Blocking Second Instance.");
app.quit();
app.isQuiting = true
}
},
PlaybackStateHandler: () => {
console.verbose('[playbackStateDidChange] Started.');
ipcMain.on('playbackStateDidChange', (_event, a) => {
console.verbose('[handler] playbackStateDidChange received.');
app.media = a;
app.ame.win.SetButtons()
app.ame.win.SetTrayTooltip(a)
app.ame.discord.updateActivity(a)
app.ame.lastfm.scrobbleSong(a)
app.ame.lastfm.updateNowPlayingSong(a)
app.ame.mpris.updateState(a)
});
},
MediaStateHandler: () => {
console.verbose('[MediaStateHandler] Started.');
ipcMain.on('nowPlayingItemDidChange', (_event, a) => {
console.verbose('[handler] nowPlayingItemDidChange received.');
app.media = a;
app.ame.win.CreateNotification(a);
app.ame.mpris.updateActivity(a);
if (app.cfg.get('audio.seamlessAudioTransitions')) {
app.ame.win.SetButtons()
app.ame.win.SetTrayTooltip(a)
app.ame.discord.updateActivity(a)
app.ame.lastfm.scrobbleSong(a)
app.ame.lastfm.updateNowPlayingSong(a)
app.ame.mpris.updateState(a)
}
});
},
WindowStateHandler: () => {
console.verbose('[WindowStateHandler] Started.');
app.win.webContents.setWindowOpenHandler(({url}) => {
shell.openExternal(url).then(() => console.log(`[WindowStateHandler] User has opened ${url} which has been redirected to browser.`));
return {
action: 'deny'
}
})
let incognitoNotification;
app.win.webContents.on('did-finish-load', () => {
console.verbose('[did-finish-load] Completed.');
app.ame.load.LoadOneTimeFiles();
app.win.webContents.setZoomFactor(parseFloat(app.cfg.get("visual.scaling")))
if (app.cfg.get('general.incognitoMode') && !incognitoNotification) {
incognitoNotification = new Notification({
title: 'Incognito Mode Enabled',
body: `Listening activity is hidden.`,
icon: join(__dirname, '../icons/icon.png')
})
incognitoNotification.show()
}
});
app.win.webContents.on('did-fail-load', (event, errCode, errDesc, url, mainFrame) => {
console.error(`Error Code: ${errCode}\nLoading: ${url}\n${errDesc}`)
if (mainFrame) {
app.exit()
}
});
// Windows specific: Handles window states
// Needed because Aero Snap events do not send the same way as clicking the frame buttons.
if (process.platform === "win32" && app.cfg.get('visual.frameType') !== 'mac' || app.cfg.get('visual.frameType') !== 'mac-right') {
var WND_STATE = {
MINIMIZED: 0,
NORMAL: 1,
MAXIMIZED: 2,
FULL_SCREEN: 3
}
var wndState = WND_STATE.NORMAL
app.win.on("resize", (_event) => {
const isMaximized = app.win.isMaximized()
const isMinimized = app.win.isMinimized()
const isFullScreen = app.win.isFullScreen()
const state = wndState;
if (isMinimized && state !== WND_STATE.MINIMIZED) {
wndState = WND_STATE.MINIMIZED
} else if (isFullScreen && state !== WND_STATE.FULL_SCREEN) {
wndState = WND_STATE.FULL_SCREEN
} else if (isMaximized && state !== WND_STATE.MAXIMIZED) {
wndState = WND_STATE.MAXIMIZED
app.win.webContents.executeJavaScript(`document.querySelector("#maximize").classList.add("maxed")`)
} else if (state !== WND_STATE.NORMAL) {
wndState = WND_STATE.NORMAL
app.win.webContents.executeJavaScript(`document.querySelector("#maximize").classList.remove("maxed")`)
}
})
}
app.win.on('unresponsive', () => {
dialog.showMessageBox({
message: `${app.getName()} has become unresponsive`,
title: 'Do you want to try forcefully reloading the app?',
buttons: ['Yes', 'Quit', 'No'],
cancelId: 1
}).then(({response}) => {
if (response === 0) {
app.win.contents.forcefullyCrashRenderer()
app.win.contents.reload()
} else if (response === 1) {
console.log("[WindowStateHandler] Application has become unresponsive and has been closed.")
app.exit();
}
})
})
app.win.on('page-title-updated', (event, title) => {
console.verbose(`[page-title-updated] Title updated Running necessary files. ('${title}')`)
app.ame.load.LoadFiles();
})
app.win.on('close', (event) => {
if (app.isMiniplayerActive && !app.isQuiting) {
ipcMain.emit("set-miniplayer", false);
event.preventDefault()
} else if ((app.cfg.get('window.closeButtonMinimize') || process.platform === "darwin") && !app.isQuiting) {
app.win.hide()
app.ame.win.SetContextMenu(false)
event.preventDefault()
} else {
app.win.destroy()
if (app.lyrics.mxmWin) {
app.lyrics.mxmWin.destroy();
}
if (app.lyrics.neteaseWin) {
app.lyrics.neteaseWin.destroy();
}
if (app.lyrics.ytWin) {
app.lyrics.ytWin.destroy();
}
}
})
app.win.on('maximize', (e) => {
if (app.isMiniplayerActive) {
e.preventDefault()
}
})
app.win.on('show', () => {
app.ame.win.SetContextMenu(true)
app.ame.win.SetButtons()
if (app.win.isVisible()) {
app.win.focus()
}
});
app.win.on('hide', () => {
app.ame.win.SetContextMenu(false)
if (app.pluginsEnabled) {
app.win.webContents.executeJavaScript(`_plugins.execute('OnHide')`)
}
});
},
SettingsHandler: () => {
console.verbose('[SettingsHandler] Started.');
let DialogMessage = false,
storedChanges = [],
handledConfigs = [];
systemPreferences.on('accent-color-changed', (event, color) => {
if (color && app.cfg.get('visual.useOperatingSystemAccent') && (process.platform === "win32" || process.platform === "darwin")) {
const accent = '#' + color.slice(0, -2)
app.win.webContents.insertCSS(`
:root {
--keyColor: ${accent} !important;
--systemAccentBG: ${accent} !important;
--systemAccentBG-pressed: rgba(${app.ame.utils.hexToRgb(accent).r}, ${app.ame.utils.hexToRgb(accent).g}, ${app.ame.utils.hexToRgb(accent).b}, 0.75) !important;
--keyColor-rgb: ${app.ame.utils.hexToRgb(accent).r} ${app.ame.utils.hexToRgb(accent).g} ${app.ame.utils.hexToRgb(accent).b} !important;
}`).then((key) => {
app.injectedCSS['useOperatingSystemAccent'] = key
})
}
})
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Restart Required Configuration Handling
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
app.cfg.onDidAnyChange((newConfig, oldConfig) => {
let currentChanges = [];
for (const [categoryTitle, categoryContents] of Object.entries(newConfig)) {
if (categoryContents !== oldConfig[categoryTitle]) { // This has gotten the changed category
for (const [settingTitle, settingValue] of Object.entries(newConfig[categoryTitle])) {
if (JSON.stringify(settingValue) !== JSON.stringify(oldConfig[categoryTitle][settingTitle])) {
currentChanges.push(`${categoryTitle}.${settingTitle}`)
if (!storedChanges.includes(`${categoryTitle}.${settingTitle}`)) {
storedChanges.push(`${categoryTitle}.${settingTitle}`)
}
}
}
}
}
console.verbose(`[SettingsHandler] Found changes: ${currentChanges} | Total Changes: ${storedChanges}`);
if (!DialogMessage && !handledConfigs.includes(currentChanges[0])) {
DialogMessage = dialog.showMessageBox({
title: "Relaunch Required",
message: "A relaunch is required in order for the settings you have changed to apply.",
type: "warning",
buttons: ['Relaunch Now', 'Relaunch Later']
}).then(({response}) => {
if (response === 0) {
app.relaunch()
app.quit()
}
})
}
})
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Individually Handled Configuration Options
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
handledConfigs.push('advanced.devToolsOnStartup', 'general.storefront', 'tokens.lastfm', 'window.closeButtonMinimize') // Stuff for the restart to just ignore
// Theme Changes
handledConfigs.push('visual.theme');
app.cfg.onDidChange('visual.theme', (newValue, _oldValue) => {
app.win.webContents.executeJavaScript(`AMStyling.loadTheme("${(newValue === 'default' || !newValue) ? '' : newValue}");`).catch((e) => console.error(e));
if (app.watcher) {
app.watcher.close();
console.verbose('[Watcher] Removed old watcher.')
}
if (existsSync(resolve(app.getPath('userData'), 'themes', `${newValue}.css`)) && newValue !== "default" && newValue) {
app.watcher = watch(resolve(app.getPath('userData'), 'themes', `${newValue}.css`), (event, fileName) => {
if (event === "change" && fileName === `${newValue}.css`) {
app.win.webContents.executeJavaScript(`AMStyling.loadTheme("${newValue}", true);`).catch((err) => console.error(err));
}
});
console.verbose(`[Watcher] Watching for changes: 'themes/${newValue}.css'`)
}
const updatedVibrancy = app.ame.utils.fetchTransparencyOptions();
if (app.transparency && updatedVibrancy && process.platform !== 'darwin') app.win.setVibrancy(updatedVibrancy);
})
// Transparency Changes
handledConfigs.push('visual.transparencyEffect', 'visual.transparencyTheme', 'visual.transparencyDisableBlur', 'visual.transparencyMaximumRefreshRate');
app.cfg.onDidChange('visual.transparencyEffect' || 'visual.transparencyTheme' || 'visual.transparencyDisableBlur' || 'visual.transparencyMaximumRefreshRate', (_newValue, _oldValue) => {
const updatedVibrancy = app.ame.utils.fetchTransparencyOptions()
if (app.cfg.get("visual.transparencyEffect") === "mica" && process.platform !== 'darwin') {
app.win.webContents.executeJavaScript(`AMStyling.setMica(true);`).catch((e) => console.error(e));
app.transparency = false;
app.win.setVibrancy();
} else {
app.win.webContents.executeJavaScript(`AMStyling.setMica(false);`).catch((e) => console.error(e));
}
if (app.transparency && updatedVibrancy && process.platform !== 'darwin') {
app.win.setVibrancy(updatedVibrancy);
app.win.webContents.executeJavaScript(`AMStyling.setTransparency(true);`).catch((e) => console.error(e));
} else {
app.win.setVibrancy();
app.win.webContents.executeJavaScript(`AMStyling.setTransparency(false);`).catch((e) => console.error(e));
}
})
// Reload scripts
handledConfigs.push('visual.removeUpsell', 'visual.removeAppleLogo', 'visual.removeFooter', 'visual.useOperatingSystemAccent');
app.cfg.onDidChange('visual.removeUpsell', (newValue, _oldValue) => {
app.ame.load.LoadFiles();
})
app.cfg.onDidChange('visual.removeAppleLogo', (newValue, _oldValue) => {
app.ame.load.LoadFiles();
})
app.cfg.onDidChange('visual.removeFooter', (newValue, _oldValue) => {
app.ame.load.LoadFiles();
})
app.cfg.onDidChange('visual.useOperatingSystemAccent', (newValue, _oldValue) => {
if (!newValue) {
app.ame.win.removeInsertedCSS('useOperatingSystemAccent')
} else {
app.ame.load.LoadFiles();
}
})
// DiscordRPC
handledConfigs.push('general.discordRPC', 'general.discordClearActivityOnPause');
app.cfg.onDidChange('general.discordRPC', (newValue, _oldValue) => {
if (newValue && !app.discord.isConnected) {
app.ame.discord.connect();
} else {
app.ame.discord.disconnect();
}
})
// IncognitoMode Changes
handledConfigs.push('general.incognitoMode');
app.cfg.onDidChange('general.incognitoMode', (newValue, _oldValue) => {
if (newValue) {
console.log("[Incognito] Incognito Mode enabled. DiscordRPC and LastFM updates are ignored.")
}
})
// Scaling Changes
handledConfigs.push('visual.scaling');
app.cfg.onDidChange('visual.scaling', (newValue, _oldValue) => {
app.win.webContents.setZoomFactor(parseFloat(newValue))
});
// Mode Changes
handledConfigs.push('advanced.forceApplicationMode');
app.cfg.onDidChange('advanced.forceApplicationMode', (newValue, _oldValue) => {
nativeTheme.themeSource = newValue;
});
},
RendererListenerHandlers: () => {
// Showing the OOBE on first launch
ipcMain.on('showOOBE', (event) => {
event.returnValue = app.ame.showOOBE;
app.ame.showOOBE = false
})
// Themes Listing Update
ipcMain.handle('updateThemesListing', (_event) => {
return app.ame.utils.fetchThemesListing();
})
// Plugins Listing Update
ipcMain.handle('fetchPluginsListing', (_event) => {
return app.ame.utils.fetchPluginsListing();
})
// Get OS
ipcMain.handle('fetchOperatingSystem', () => {
return process.platform
})
// Acrylic Check
ipcMain.handle('isAcrylicSupported', (_event) => {
return app.ame.utils.isAcrylicSupported();
})
// Electron-Store Renderer Handling for Getting Values
ipcMain.handle('getStoreValue', (event, key, defaultValue) => {
return (defaultValue ? app.cfg.get(key, true) : app.cfg.get(key));
});
// Electron-Store Renderer Handling for Setting Values
ipcMain.handle('setStoreValue', (event, key, value) => {
app.cfg.set(key, value);
});
ipcMain.handle('themeFileExists', (event, fileName) => {
return existsSync(resolve(app.getPath('userData'), 'themes', `${fileName}.css`))
});
// Copy Log File
ipcMain.on('copyLogFile', (event) => {
const data = readFileSync(app.log.transports.file.getFile().path, {encoding: 'utf8', flag: 'r'});
clipboard.writeText(data)
event.returnValue = true
});
// Electron-Store Renderer Handling for Getting Configuration
ipcMain.on('getStore', (event) => {
event.returnValue = app.cfg.store
})
// Electron-Store Renderer Handling for Setting Configuration
ipcMain.on('setStore', (event, store) => {
app.cfg.store = store
})
// Update Themes
ipcMain.handle('updateThemes', () => {
return app.ame.utils.updateThemes()
});
// Authorization (This needs to be cleaned up a bit, an alternative to reload() would be good )
ipcMain.on('authorizationStatusDidChange', (_event, authorized) => {
console.log(`authorization updated. status: ${authorized}`)
app.win.reload()
app.ame.load.LoadFiles()
app.isAuthorized = (authorized === 3)
})
// Window Navigation - Minimize
ipcMain.on('minimize', () => { // listen for minimize event
if (typeof app.win.minimize === 'function') {
app.win.minimize()
}
});
// Window Navigation - Maximize
ipcMain.on('maximize', () => { // listen for maximize event and perform restore/maximize depending on window state
if (app.win.isMaximized()) {
app.win.unmaximize()
if (process.platform !== "win32") {
app.win.webContents.executeJavaScript(`document.querySelector("#maximize").classList.remove("maxed")`)
}
} else {
app.win.maximize()
if (process.platform !== "win32") {
app.win.webContents.executeJavaScript(`document.querySelector("#maximize").classList.add("maxed")`)
}
}
})
// Window Navigation - Close
ipcMain.on('close', () => { // listen for close event
app.win.close();
})
// Window Navigation - Back
ipcMain.on('back', () => { // listen for back event
if (app.win.webContents.canGoBack()) {
app.win.webContents.goBack()
}
})
// Window Navigation - Resize
ipcMain.on("resize-window", (event, width, height) => {
app.win.setSize(width, height)
})
// miniPlayer
const minSize = app.win.getMinimumSize()
ipcMain.on("set-miniplayer", (event, val) => {
if (val) {
app.isMiniplayerActive = true;
app.win.setSize(300, 300);
app.win.setMinimumSize(300, 55);
app.win.setMaximumSize(300, 300);
app.win.maximizable = false;
app.win.webContents.executeJavaScript("_miniPlayer.setMiniPlayer(true)").catch((e) => console.error(e));
if (app.win.isMaximized) {
app.win.unmaximize();
}
} else {
app.isMiniplayerActive = false;
app.win.setMaximumSize(9999, 9999);
app.win.setMinimumSize(minSize[0], minSize[1]);
app.win.setSize(1024, 600);
app.win.maximizable = true;
app.win.webContents.executeJavaScript("_miniPlayer.setMiniPlayer(false)").catch((e) => console.error(e));
}
})
ipcMain.on("show-miniplayer-menu", () => {
const menuOptions = [{
type: "checkbox",
label: "Always On Top",
click: () => {
if (app.win.isAlwaysOnTop()) {
app.win.setAlwaysOnTop(false, 'screen')
} else {
app.win.setAlwaysOnTop(true, 'screen')
}
},
checked: app.win.isAlwaysOnTop()
}, {
label: "Exit Mini Player",
click: () => {
ipcMain.emit("set-miniplayer", false)
}
},
]
const menu = Menu.buildFromTemplate(menuOptions)
menu.popup(app.win)
})
ipcMain.on("alwaysOnTop", (event, val) => {
if (val) {
app.win.setAlwaysOnTop(true, 'screen')
} else {
app.win.setAlwaysOnTop(false, 'screen')
}
})
ipcMain.on("load-plugin", (event, plugin) => {
let path = join(app.userPluginsPath, plugin.toLowerCase() + ".js")
readFile(path, "utf-8", (error, data) => {
if (!error) {
try {
app.win.webContents.executeJavaScript(data).then(() => {
console.verbose(`[Plugins] Injected Plugin`)
})
} catch (err) {
console.error(`[Plugins] error injecting plugin: ${path} - Error: ${err}`)
}
} else {
console.error(`[Plugins] error reading plugin: ${path} - Error: ${error}`)
}
})
})
// Get Wallpaper
ipcMain.on("get-wallpaper", (event) => {
function base64_encode(file) {
const bitmap = readFileSync(file)
return `data:image/png;base64,${Buffer.from(bitmap).toString('base64')}`
}
regedit.list(`HKCU\\Control Panel\\Desktop\\`, (err, result) => {
var path = (result['HKCU\\Control Panel\\Desktop\\\\']['values']['WallPaper']['value'])
event.returnValue = base64_encode(path)
})
})
ipcMain.on("get-wallpaper-style", (event) => {
regedit.list(`HKCU\\Control Panel\\Desktop\\`, (err, result) => {
var value = (result['HKCU\\Control Panel\\Desktop\\\\']['values']['WallpaperStyle']['value'])
event.returnValue = parseInt(value)
})
})
// Set BrowserWindow zoom factor
ipcMain.on("set-zoom-factor", (event, factor) => {
app.win.webContents.setZoomFactor(factor)
})
},
LinkHandler: (startArgs) => {
if (!startArgs || !app.win || !app.isAuthorized) return;
if (String(startArgs).includes('auth')) {
let authURI = String(startArgs).split('/auth/')[1]
if (authURI.startsWith('lastfm')) { // If we wanted more auth options
const authKey = authURI.split('lastfm?token=')[1];
app.cfg.set('general.lastfm', true);
app.cfg.set('tokens.lastfm', authKey);
app.win.webContents.send('LastfmAuthenticated', authKey);
app.ame.lastfm.authenticate()
}
} else {
if (!app.isAuthorized) return
const formattedSongID = startArgs.replace('ame://', '').replace('/', '');
console.warn(`[LinkHandler] Attempting to load song id: ${formattedSongID}`);
// setQueue can be done with album, song, url, playlist id
app.win.webContents.executeJavaScript(`
MusicKit.getInstance().setQueue({ song: '${formattedSongID}'}).then(function(queue) {
MusicKit.getInstance().play();
});
`).catch((err) => console.error(err));
}
},
LyricsHandler: () => {
app.lyrics = {
neteaseWin: null,
mxmWin: null,
ytWin: null,
artworkURL: '',
savedLyric: ''
}
app.lyrics.neteaseWin = new BrowserWindow({
width: 1,
height: 1,
show: false,
autoHideMenuBar: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
app.lyrics.mxmWin = new BrowserWindow({
width: 1,
height: 1,
show: false,
autoHideMenuBar: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
},
});
app.lyrics.ytWin = new BrowserWindow({
width: 1,
height: 1,
show: false,
autoHideMenuBar: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
},
});
ipcMain.on('YTTranslation', function (event, track, artist, lang) {
try {
if (app.lyrics.ytWin == null) {
app.lyrics.ytWin = new BrowserWindow({
width: 1,
height: 1,
show: false,
autoHideMenuBar: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
}
});
} else {
app.lyrics.ytWin.webContents.send('ytcors', track, artist, lang);
}
if (!app.lyrics.ytWin.webContents.getURL().includes('youtube.html')) {
app.lyrics.ytWin.loadFile(join(__dirname, '../lyrics/youtube.html'));
app.lyrics.ytWin.webContents.on('did-finish-load', () => {
app.lyrics.ytWin.webContents.send('ytcors', track, artist, lang);
});
}
app.lyrics.ytWin.on('closed', () => {
app.lyrics.ytWin = null
});
} catch (e) {
console.error(e)
}
});
ipcMain.on('MXMTranslation', function (event, track, artist, lang, time) {
try {
if (app.lyrics.mxmWin == null) {
app.lyrics.mxmWin = new BrowserWindow({
width: 1,
height: 1,
show: false,
autoHideMenuBar: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
}
});
} else {
app.lyrics.mxmWin.webContents.send('mxmcors', track, artist, lang, time);
}
// try{
// const cookie = { url: 'https://apic-desktop.musixmatch.com/', name: 'x-mxm-user-id', value: '' }
// app.lyrics.mxmWin.webContents.session.defaultSession.cookies.set(cookie);
// } catch (e){}
if (!app.lyrics.mxmWin.webContents.getURL().includes('musixmatch.html')) {
app.lyrics.mxmWin.loadFile(join(__dirname, '../lyrics/musixmatch.html'));
app.lyrics.mxmWin.webContents.on('did-finish-load', () => {
app.lyrics.mxmWin.webContents.send('mxmcors', track, artist, lang, time);
});
}
app.lyrics.mxmWin.on('closed', () => {
app.lyrics.mxmWin = null
});
} catch (e) {
console.error(e)
}
});
ipcMain.on('NetEaseLyricsHandler', function (event, data) {
try {
if (app.lyrics.neteaseWin == null) {
app.lyrics.neteaseWin = new BrowserWindow({
width: 100,
height: 100,
show: false,
autoHideMenuBar: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
}
});
app.lyrics.neteaseWin.webContents.on('did-finish-load', () => {
app.lyrics.neteaseWin.webContents.send('neteasecors', data);
});
} else {
app.lyrics.neteaseWin.webContents.on('did-finish-load', () => {
app.lyrics.neteaseWin.webContents.send('neteasecors', data);
});
}
app.lyrics.neteaseWin.loadFile(join(__dirname, '../lyrics/netease.html'));
app.lyrics.neteaseWin.on('closed', () => {
app.lyrics.neteaseWin = null
});
} catch (e) {
console.log(e);
app.lyrics.savedLyric = '[00:00] Instrumental. / Lyrics not found.';
app.win.send('truelyrics', '[00:00] Instrumental. / Lyrics not found.');
}
});
ipcMain.on('LyricsHandler', function (event, data, artworkURL) {
app.win.send('truelyrics', data);
app.win.send('albumart', artworkURL);
app.lyrics.savedLyric = data;
app.lyrics.albumart = artworkURL;
});
ipcMain.on('updateMiniPlayerArt', function (event, artworkURL) {
app.lyrics.albumart = artworkURL;
})
ipcMain.on('LyricsHandlerNE', function (event, data) {
app.win.send('truelyrics', data);
app.lyrics.savedLyric = data;
});
ipcMain.on('LyricsHandlerTranslation', function (event, data) {
app.win.send('lyricstranslation', data);
});
ipcMain.on('LyricsTimeUpdate', function (event, data) {
app.win.send('ProgressTimeUpdate', data);
});
ipcMain.on('LyricsUpdate', function (event, data, artworkURL) {
app.win.send('truelyrics', data);
app.win.send('albumart', artworkURL);
app.lyrics.savedLyric = data;
app.lyrics.albumart = artworkURL;
});
ipcMain.on('LyricsMXMFailed', function (_event, _data) {
app.win.send('backuplyrics', '');
console.log("mxm failed");
});
ipcMain.on('LyricsYTFailed', function (_event, _data) {
app.win.send('backuplyricsMV', '');
});
ipcMain.on('ProgressTimeUpdateFromLyrics', function (event, data) {
app.win.webContents.executeJavaScript(`MusicKit.getInstance().seekToTime('${data}')`).catch((e) => console.error(e));
});
},
AudioHandler: () => {
ipcMain.on('muteAudio', function (event, mute) {
app.win.webContents.setAudioMuted(mute);
});
if (process.platform === "win32") {
const EAstream = new Stream.PassThrough();
let ao;
const portAudio = require('naudiodon');
console.log(portAudio.getDevices());
ipcMain.on('getAudioDevices', function (_event) {
for (let id = 0; id < portAudio.getDevices().length; id++) {
if (portAudio.getDevices()[id].maxOutputChannels > 0)
app.win.webContents.executeJavaScript(`console.log('id:','${id}','${portAudio.getDevices()[id].name}','outputChannels:','${portAudio.getDevices()[id].maxOutputChannels}','preferedSampleRate','${portAudio.getDevices()[id].defaultSampleRate}','nativeFormats','${portAudio.getDevices()[id].hostAPIName}')`);
}
})
ipcMain.on('enableExclusiveAudio', function (event, id) {
ao = new portAudio.AudioIO({
outOptions: {
channelCount: 2,
sampleFormat: portAudio.SampleFormat24Bit,
sampleRate: 48000,
maxQueue: 100,
deviceId: id,
highwaterMark: 1024, // Use -1 or omit the deviceId to select the default device
closeOnError: false // Close the stream if an audio error is detected, if set false then just log the error
}
});
// Create a stream to pipe into the AudioOutput
// Note that this does not strip the WAV header so a click will be heard at the beginning
EAstream.pipe(ao);
EAstream.once('data', (_data) => {
ao.start();
})
// Start piping data and start streaming
})
ipcMain.on('disableExclusiveAudio', function (_event, _data) {
if (ao) {
ao.quit();
}
})
app.win.on('quit', () => {
if (ao) {
ao.quit();
}
})
// mix the channels
function interleave(leftChannel, rightChannel) {
var length = leftChannel.length + rightChannel.length;
var result = new Float32Array(length);
var inputIndex = 0;
for (var index = 0; index < length;) {
result[index++] = leftChannel[inputIndex];
result[index++] = rightChannel[inputIndex];
inputIndex++;
}
return result;
}
ipcMain.on('changeAudioMode', function (_event, _mode) {
console.log(portAudio.getHostAPIs());
});
console.log(portAudio.getHostAPIs());
ipcMain.on('writePCM', function (event, buffer) {
// writeFile(join(app.getPath('userData'), 'buffertest5.raw'), Buffer.from(buffer,'binary').slice(44),{flag: 'a+'}, function (err) {
// if (err) throw err;
// console.log('It\'s saved!');
// });
// do anything with stereo pcm here
// buffer = Buffer.from(new Int8Array(interleave(Float32Array.from(leftpcm), Float32Array.from(rightpcm)).buffer));
EAstream.write(Buffer.from(buffer).slice(44));
});
ipcMain.on('writeChunks', function (event, blob) {
writeFile(join(app.getPath('userData'), 'buffertest.raw'), Buffer.from(blob, 'binary'), {flag: 'a+'}, function (err) {
if (err) throw err;
console.log('It\'s saved!');
});
})
}
},
GoogleCastHandler: () => {
const devices = [],
castDevices = [];
let GCRunning = false,
GCBuffer,
expectedConnections = 0,
currentConnections = 0,
activeConnections = [],
requests = [],
GCstream = new Stream.PassThrough(),
connectedHosts = {},
port = false,
server = false,
bufcount = 0,
bufcount2 = 0,
headerSent = false;
const audioserver = express();
audioserver.get('/', playData.bind(this));
function playData(req, res) {
try{if(app.cfg.get('audio.castingBitDepth') == "24")
headerSent = false;} catch (e){}
console.log("Device requested: /");
req.connection.setTimeout(Number.MAX_SAFE_INTEGER);
requests.push({req: req, res: res});
const pos = requests.length - 1;
req.on("close", () => {
console.info("CLOSED", requests.length);
requests.splice(pos, 1);
console.info("CLOSED", requests.length);
headerSent = false;
});
GCstream.on('data', (data) => {
try {
res.write(data);
} catch (ex) {
console.log("Dead", ex);
}
})
}
audioserver.get('/a.wav', playData2.bind(this));
function playData2(req, res) {
console.log("Device requested: /a.wav");
req.connection.setTimeout(Number.MAX_SAFE_INTEGER);
try{if(app.cfg.get('audio.castingBitDepth') == "24")
headerSent = false;} catch (e){}
res.setHeader('Accept-Ranges', 'bytes')
res.setHeader('Connection', 'keep-alive')
res.setHeader('Content-Type', 'audio/wav')
res.setHeader('Access-Control-Allow-Origin', '*');
res.statusCode = 200;
res.setHeader('transferMode.dlna.org', 'Streaming');
res.setHeader(
'contentFeatures.dlna.org',
'DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000'
);
requests.push({req: req, res: res});
const pos = requests.length - 1;
req.on("close", () => {
console.info("CLOSED", requests.length);
requests.splice(pos, 1);
console.info("CLOSED", requests.length);
headerSent = false;
});
GCstream.on('data', (data) => {
try {
res.write(data);
} catch (ex) {
console.log("Dead", ex);
}
})
}
ipcMain.on('writeOPUS', function (event, buffer) {
const pcm = Buffer.from(buffer, 'binary').slice(44); //stereo, 48k, 16signed in 8bit buffer
// Pipe it to something else (i.e. stdout)
// writeFile(join(app.getPath('userData'), 'buffertest3.raw'), Encoder.,{flag: 'a+'}, function (err) {
// if (err) throw err;
// console.log('It\'s saved!');
// });
// //GCstream.write(mp3Tmp);
})
ipcMain.on('writeWAV', function (event, pcm, extremeAudio) {
let pcmData;
if (extremeAudio === '24') {
pcmData = Buffer.from(pcm, 'binary').slice(44);
if (!headerSent) {
const header = Buffer.from(pcm, 'binary').slice(0, 44)
header.writeUInt32LE(2147483600, 4)
header.writeUInt32LE(2147483600 + 44 - 8, 40)
GCstream.write(Buffer.concat([header, pcmData]));
headerSent = true;
console.log('done');
} else {
GCstream.write(pcmData);
}
} else {
//sample down to 16 (default)
let wav = new WaveFile(Buffer.from(pcm, 'binary'));
wav.toBitDepth("16");
var newpcm = wav.toBuffer();
pcmData = Buffer.from(newpcm, 'binary').slice(44);
if (!headerSent) {
const header = Buffer.from(newpcm, 'binary').slice(0, 44)
header.writeUInt32LE(2147483600, 4)
header.writeUInt32LE(2147483600 + 44 - 8, 40)
GCstream.write(Buffer.concat([header, pcmData]));
headerSent = true;
console.log('done');
} else {
GCstream.write(pcmData);
}
}
}
);
function getServiceDescription(url, address) {
const request = require('request');
request.get(url, (error, response, body) => {
if (!error && response.statusCode === 200) {
parseServiceDescription(body, address, url);
}
});
}
function ondeviceup(host, name, location, type) {
if (castDevices.findIndex((item) => item.host === host && item.name === name && item.location === location && item.type === type) === -1) {
castDevices.push({
name: name,
host: host,
location: location,
type: type
});
if (devices.indexOf(host) === -1) {
devices.push(host);
}
if (name) {
app.win.webContents.executeJavaScript(`console.log('deviceFound','ip: ${host} name:${name}')`).catch(err => console.error(err));
console.log("deviceFound", host, name);
}
} else {
app.win.webContents.executeJavaScript(`console.log('deviceFound (added)','ip: ${host} name:${name}')`).catch(err => console.error(err));
console.log("deviceFound (added)", host, name);
}
}
function broadcastRemote() {
const myString = getIp();
const encoded = new Buffer(myString).toString('base64');
var x = mdns.tcp('ame-lg-client');
let server2 = mdns.createAdvertisement(x, '3839', { name: encoded });
server2.start();
let ssdpRemoteServer = express();
ssdpRemoteServer.listen(3840, () => {
});
ssdpRemoteServer.get('/', (req, res) => {
res.header("Content-Type", "application/xml");
data = `<?xml version="1.0"?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<URLBase>${'http://' + getIp()}</URLBase>
<device>
<deviceType>urn:schemas-upnp-org:device:MediaRenderer:1</deviceType>
<!-- The friendlyName element is the best place to put
the title to display in the Physical Web Browser -->
<friendlyName>AME Remote</friendlyName>
<manufacturer>${encoded}</manufacturer>
<manufacturerURL>http://applemusicelectron.com</manufacturerURL>
<modelDescription>AME</modelDescription>
<modelName>AME</modelName>
<modelNumber>3.0</modelNumber>
<modelURL>${'http://' + getIp()}</modelURL>
<serialNumber>manufacturer's serial number</serialNumber>
<UDN>uuid:75ebacfb-e890-4a21-a913-9a16858e9270</UDN>
<UPC>Universal Product Code</UPC>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
<serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
<SCPDURL></SCPDURL>
<controlURL></controlURL>
<eventSubURL></eventSubURL>
</service>
</serviceList>
</device>
</root>
`
res.status(200).send(data);
});
let SSDP = require('node-ssdp').Server
, server = new SSDP({
location : 'http://' + getIp() + ':3840',
allowWildcards : true,
adInterval : 5000,
})
;
// server.addUSN('upnp:rootdevice');
server.addUSN('urn:schemas-upnp-org:device:MediaRenderer:1');
server.addUSN('urn:schemas-upnp-org:service:AVTransport:1');
server.start().catch(e => {
console.log('Failed to start server:', e)
})
.then(() => {
console.log('Server started.')
})
process.on('exit', function(){
server.stop() // advertise shutting down and stop listening
})
}
function searchForGCDevices() {
try {
let browser = mdns.createBrowser(mdns.tcp('googlecast'));
browser.on('ready', browser.discover);
browser.on('update', (service) => {
if (service.addresses && service.fullname) {
ondeviceup(service.addresses[0], service.fullname.substring(0, service.fullname.indexOf("._googlecast")) + " " + (service.type[0].description ?? ""), '', 'googlecast');
}
});
// also do a SSDP/UPnP search
let ssdpBrowser = new Client();
ssdpBrowser.on('response', (headers, statusCode, rinfo) => {
var location = getLocation(headers);
if (location != null) {
getServiceDescription(location, rinfo.address);
}
});
function getLocation(headers) {
let location = null;
if (headers["LOCATION"] != null ){location = headers["LOCATION"]}
else if (headers["Location"] != null ){location = headers["Location"]}
return location;
}
ssdpBrowser.search('urn:dial-multiscreen-org:device:dial:1');
// actual upnp devices
if (app.cfg.get("audio.enableDLNA")) {
let ssdpBrowser2 = new Client();
ssdpBrowser2.on('response', (headers, statusCode, rinfo) => {
var location = getLocation(headers);
if (location != null) {
getServiceDescription(location, rinfo.address);
}
});
ssdpBrowser2.search('urn:schemas-upnp-org:device:MediaRenderer:1');
}
} catch (e) {
console.log('Search GC err', e);
}
}
function setupGCServer() {
return new Promise((resolve, reject) => {
getPort()
.then(port2 => {
port = port2;
server = audioserver.listen(port, () => {
console.info('Example app listening at http://%s:%s', getIp(), port);
});
GCRunning = true;
resolve()
})
.catch(reject);
});
}
broadcastRemote();
function parseServiceDescription(body, address, url) {
const parseString = require('xml2js').parseString;
parseString(body, (err, result) => {
if (!err && result && result.root && result.root.device) {
const device = result.root.device[0];
console.log('device', device);
let devicetype = 'googlecast';
console.log()
if (device.deviceType && device.deviceType.toString() === 'urn:schemas-upnp-org:device:MediaRenderer:1') {
devicetype = 'upnp';
}
ondeviceup(address, device.friendlyName.toString(), url, devicetype);
}
});
}
function loadMedia(client, song, artist, album, albumart, cb) {
const u = 'http://' + getIp() + ':' + server.address().port + '/';
client.launch(DefaultMediaReceiver, (err, player) => {
if (err) {
console.log(err);
return;
}
let media = {
// Here you can plug an URL to any mp4, webm, mp3 or jpg file with the proper contentType.
contentId: u,
contentType: 'audio/wav',
streamType: 'LIVE', // or LIVE
// Title and cover displayed while buffering
metadata: {
type: 0,
metadataType: 3,
title: song ?? "",
albumName: album ?? "",
artist: artist ?? "",
images: [
{url: albumart ?? ""}]
}
};
// ipcMain.on('setupNewTrack', function (event, song, artist, album, albumart) {
// try {
// let newmedia = {
// // Here you can plug an URL to any mp4, webm, mp3 or jpg file with the proper contentType.
// contentId: u,
// contentType: 'audio/wav',
// streamType: 'LIVE', // or LIVE
// // Title and cover displayed while buffering
// metadata: {
// type: 0,
// metadataType: 3,
// title: song ?? "",
// albumName: album ?? '',
// artist: artist ?? '',
// images: [
// {url: albumart ?? ''}]
// }
// };
// headerSent = false;
// player.queueUpdate(newmedia, {
// autoplay: true
// }, (err, status) => {
// console.log('media loaded playerState=%s', status);
// });
// } catch (e) {
// console.log('GCerror', e)
// }
// });
player.on('status', status => {
console.log('status broadcast playerState=%s', status);
});
console.log('app "%s" launched, loading media %s ...', player, media);
player.load(media, {
autoplay: true
}, (err, status) => {
console.log('media loaded playerState=%s', status);
});
client.getStatus((x, status) => {
if (status && status.volume) {
client.volume = status.volume.level;
client.muted = status.volume.muted;
client.stepInterval = status.volume.stepInterval;
}
})
});
}
function getIp() {
let ip = false;
let alias = 0;
let ifaces = os.networkInterfaces();
for (var dev in ifaces) {
ifaces[dev].forEach(details => {
if (details.family === 'IPv4') {
if (!/(loopback|vmware|internal|hamachi|vboxnet|virtualbox)/gi.test(dev + (alias ? ':' + alias : ''))) {
if (details.address.substring(0, 8) === '192.168.' ||
details.address.substring(0, 7) === '172.16.' ||
details.address.substring(0, 3) === '10.'
) {
ip = details.address;
++alias;
}
}
}
});
}
return ip;
}
function stream(device, song, artist, album, albumart) {
let castMode = 'googlecast';
let UPNPDesc = '';
castMode = device.type;
UPNPDesc = device.location;
let client;
if (castMode === 'googlecast') {
let client = new audioClient();
client.volume = 100;
client.stepInterval = 0.5;
client.muted = false;
client.connect(device.host, () => {
console.log('connected, launching app ...', 'http://' + getIp() + ':' + server.address().port + '/');
if (!connectedHosts[device.host]) {
connectedHosts[device.host] = client;
activeConnections.push(client);
}
loadMedia(client, song, artist, album, albumart);
});
client.on('close', () => {
console.info("Client Closed");
for (let i = activeConnections.length - 1; i >= 0; i--) {
if (activeConnections[i] === client) {
activeConnections.splice(i, 1);
return;
}
}
});
client.on('error', err => {
console.log('Error: %s', err.message);
client.close();
delete connectedHosts[device.host];
});
} else {
// upnp devices
try {
client = new MediaRendererClient(UPNPDesc);
const options = {
autoplay: true,
contentType: 'audio/x-wav',
dlnaFeatures: 'DLNA.ORG_PN=-;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01700000000000000000000000000000',
metadata: {
title: 'Apple Music Electron',
creator: 'Streaming ...',
type: 'audio', // can be 'video', 'audio' or 'image'
// url: 'http://' + getIp() + ':' + server.address().port + '/',
// protocolInfo: 'DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000;
}
};
client.load('http://' + getIp() + ':' + server.address().port + '/a.wav', options, function (err, _result) {
if (err) throw err;
console.log('playing ...');
});
} catch (e) {
}
}
}
ipcMain.on('getKnownCastDevices', function (event) {
event.returnValue = castDevices
});
ipcMain.on('performGCCast', function (event, device, song, artist, album, albumart) {
setupGCServer().then(function () {
app.win.webContents.setAudioMuted(true);
console.log(device);
stream(device, song, artist, album, albumart);
})
});
ipcMain.on('getChromeCastDevices', function (_event, _data) {
searchForGCDevices();
});
ipcMain.on('stopGCast', function (_event) {
app.win.webContents.setAudioMuted(false);
GCRunning = false;
expectedConnections = 0;
currentConnections = 0;
activeConnections = [];
requests = [];
GCstream = new Stream.PassThrough();
connectedHosts = {};
port = false;
server = false;
bufcount = 0;
bufcount2 = 0;
headerSent = false;
})
}
}
module.exports = handler