sorry for doing this.

This commit is contained in:
cryptofyre 2021-12-04 00:04:04 -06:00
parent 5fc82a8bc7
commit b3db294485
106 changed files with 86 additions and 259 deletions

View file

@ -1,163 +0,0 @@
const {app, protocol, dialog} = require("electron"),
{join, resolve} = require("path"),
{existsSync, createReadStream, unlink, rmSync} = require("fs"),
Store = require('electron-store');
module.exports = () => {
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Application Configuration Init
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
app.setPath("userData", join(app.getPath("appData"), app.name.replace(/\s/g, ''))) // Set Linux to use .cache instead of .config and remove the space as its annoying
// Set the Theme List based on css files in themes directory
app.userThemesPath = resolve(app.getPath('userData'), 'themes');
app.userPluginsPath = resolve(app.getPath('userData'), 'plugins');
let showIntro = false
const migrationFunctions = {
clearElectronPrefs: () => {
if (existsSync(resolve(app.getPath('userData'), 'preferences.json'))) {
unlink(resolve(app.getPath('userData'), 'preferences.json'), (err) => {
if (err) console.error(err)
})
}
if (existsSync(resolve(app.getPath('userData'), 'Preferences'))) {
unlink(resolve(app.getPath('userData'), 'Preferences'), (err) => {
if (err) console.error(err)
})
}
},
clearCache: () => {
if (existsSync(resolve(app.getPath('userData'), 'Cache'))) {
rmSync(resolve(app.getPath('userData'), 'Cache'), {recursive: true, force: true})
}
},
showDevelopmentMessage: () => {
app.whenReady().then(() => {
dialog.showMessageBox({
title: "Version under Development!",
message: "This version is under development. Expect bugs and issues whilst using the application.",
type: "warning"
})
})
}
}
const storeDefaults = {
general: {
storefront: "",
incognitoMode: false,
playbackNotifications: "minimized",
trayTooltipSongName: true,
startupPage: "browse",
discordRPC: "ame-title",
discordClearActivityOnPause: true,
lastfm: false,
lastfmRemoveFeaturingArtists: true,
lastfmNowPlaying: true,
analyticsEnabled: true,
lastfmScrobbleDelay: 30
},
visual: {
theme: "default",
frameType: "",
transparencyEffect: "",
transparencyTheme: "appearance-based",
transparencyDisableBlur: true,
transparencyMaximumRefreshRate: "",
streamerMode: false,
removeUpsell: true,
removeAppleLogo: true,
removeFooter: true,
removeScrollbars: true,
useOperatingSystemAccent: false,
scaling: 1,
yton: false,
mxmon: false,
mxmlanguage: "en"
},
audio: {
audioQuality: "auto",
seamlessAudioTransitions: false,
castingBitDepth: '16',
enableDLNA: false,
},
window: {
appStartupBehavior: "",
closeButtonMinimize: true,
alwaysOnTop: false
},
advanced: {
forceApplicationMode: "system",
hardwareAcceleration: true,
verboseLogging: false,
autoUpdaterBetaBuilds: false,
useBetaSite: true,
preventMediaKeyHijacking: false,
devToolsOnStartup: false,
allowMultipleInstances: false
},
tokens: {
lastfm: ""
}
}
const storeMigrations = {
'>=3.0.0': store => {
showIntro = true
},
'<=3.0.0': store => {
migrationFunctions.clearElectronPrefs()
migrationFunctions.clearCache()
migrationFunctions.showDevelopmentMessage()
}
}
app.cfg = new Store({
defaults: storeDefaults,
migrations: storeMigrations
})
app.cfg.watch = true;
app.isQuiting = false;
app.whenReady().then(() => {
protocol.registerFileProtocol('themes', (request, callback) => {
const url = request.url.substr(7)
callback({
path: join(app.userThemesPath, url.toLowerCase())
})
})
protocol.registerFileProtocol('ameres', (request, callback) => {
const url = request.url.substr(7)
callback(createReadStream(join(join(__dirname, '../'), url.toLowerCase())))
})
protocol.registerFileProtocol('plugin', (request, callback) => {
const url = request.url.substr(7)
callback({
path: join(app.userPluginsPath, url.toLowerCase())
})
})
})
const handlersFuncs = require('./handler'),
initFuncs = require('./init'),
loadFuncs = require('./load'),
utilsFuncs = require('./utils'),
winFuncs = require('./win'),
discordFuncs = require('./media/discordrpc'),
lastfmFuncs = require('./media/lastfm'),
mprisFuncs = require('./media/mpris');
return {
handler: handlersFuncs,
init: initFuncs,
load: loadFuncs,
utils: utilsFuncs,
win: winFuncs,
discord: discordFuncs,
lastfm: lastfmFuncs,
mpris: mprisFuncs,
wsapi: require('./wsapi'),
showOOBE: showIntro
};
}

View file

@ -3,16 +3,28 @@ const {join, resolve} = require("path")
const getPort = require("get-port");
const express = require("express");
const path = require("path");
const windowStateKeeper = require("electron-window-state");
const CiderBase = {
const CiderWin = {
CreateBrowserWindow() {
// Set default window sizes
const mainWindowState = windowStateKeeper({
defaultWidth: 1024,
defaultHeight: 600
});
let win = null
const options = {
width: 1024,
height: 600,
icon: join(__dirname, `../icons/icon.ico`),
width: mainWindowState.width,
height: mainWindowState.height,
x: mainWindowState.x,
y: mainWindowState.y,
minWidth: 844,
minHeight: 410,
frame: false,
title: "Cider",
vibrancy: 'dark',
hasShadow: false,
webPreferences: {
@ -23,10 +35,13 @@ const CiderWin = {
allowRunningInsecureContent: true,
enableRemoteModule: true,
sandbox: true,
nativeWindowOpen: true
nativeWindowOpen: true,
contextIsolation: false,
preload: join(__dirname, '../preload/cider-preload.js')
}
}
CiderWin.InitWebServer()
CiderBase.InitWebServer()
if (process.platform === "darwin" || process.platform === "linux") {
win = new BrowserWindow(options)
} else {
@ -39,6 +54,31 @@ const CiderWin = {
win.on("closed", () => {
win = null
})
// Register listeners on Window to track size and position of the Window.
mainWindowState.manage(win);
// IPC stuff
ipcMain.on('close', () => { // listen for close event
win.close();
})
ipcMain.on('maximize', () => { // listen for maximize event
if (win.maximizable) {
win.maximize();
win.maximizable = false;
} else {
win.unmaximize();
win.maximizable = true;
}
})
ipcMain.on('minimize', () => { // listen for minimize event
win.minimize();
})
return win
},
async InitWebServer() {
const webRemotePort = await getPort({port : 9000});
@ -51,7 +91,7 @@ const CiderWin = {
webapp.listen(webRemotePort, function () {
console.log(`Web Remote listening on port ${webRemotePort}`);
});
}
},
}
module.exports = CiderWin;
module.exports = CiderBase;

File diff suppressed because it is too large Load diff

View file

@ -1,234 +0,0 @@
const {app, nativeTheme, nativeImage, Tray} = require("electron"),
{join, resolve} = require("path"),
os = require("os"),
{existsSync, readdirSync} = require("fs"),
regedit = require("regedit"),
{initAnalytics} = require('./utils');
initAnalytics();
const init = {
BaseInit: () => {
const censoredConfig = app.cfg.store;
censoredConfig.tokens = {};
console.log('---------------------------------------------------------------------')
console.log(`${app.getName()} has started.`);
console.log(`Version: ${app.getVersion()} | Electron Version: ${process.versions.electron}`)
console.log(`Type: ${os.type} | Release: ${os.release()} ${app.ame.utils.fetchOperatingSystem() ? `(${app.ame.utils.fetchOperatingSystem()})` : ""} | Platform: ${os.platform()}`)
console.log(`User Data Path: '${app.getPath('userData')}'`)
console.log(`Current Configuration: ${JSON.stringify(censoredConfig)}`)
console.log("---------------------------------------------------------------------")
if (app.cfg.get('general.analyticsEnabled') && app.isPackaged) console.log('[Sentry] Sentry logging is enabled, any errors you receive will be presented to the development team to fix for the next release.')
console.verbose('[InitializeBase] Started.');
// Disable CORS
app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors')
app.commandLine.appendSwitch('high-dpi-support', 'true')
if (process.platform === "win32") {
app.commandLine.appendSwitch('force-device-scale-factor', '1')
}
if (app.cfg.get('advanced.verboseLogging')) {
app.commandLine.appendSwitch('--enable-logging');
app.commandLine.appendSwitch('--log-file', join(app.getPath('userData'), 'logs', 'renderer.log'));
console.verbose(`[InitializeBase] Renderer logging setup at ${join(app.getPath('userData'), 'logs', 'renderer.log')}`);
}
// Media Key Hijacking
if (app.cfg.get('advanced.preventMediaKeyHijacking')) {
console.log("[Apple-Music-Electron] Hardware Media Key Handling disabled.")
app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling,MediaSessionService');
}
// GPU Hardware Acceleration
if (!app.cfg.get('advanced.hardwareAcceleration')) {
app.commandLine.appendSwitch('disable-gpu')
}
// Registry
if (process.platform === "win32") {
regedit.setExternalVBSLocation("resources/regedit/vbs")
}
// Sets the ModelId (For windows notifications)
if (process.platform === "win32") app.setAppUserModelId(app.getName());
// Disable the Media Session to allow MPRIS to be the primary service
if (process.platform === "linux") app.commandLine.appendSwitch('disable-features', 'MediaSessionService');
// Assign Default Variables
app.isQuiting = (app.isQuiting ? app.isQuiting : false);
app.win = '';
app.ipc = {
existingNotification: false
};
if (app.cfg.get('general.incognitoMode')) {
console.log("[Incognito] Incognito Mode enabled. DiscordRPC and LastFM updates are ignored.")
}
/* Protocols for Link Handling */
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient('ame', process.execPath, [resolve(process.argv[1])])
app.setAsDefaultProtocolClient('itms', process.execPath, [resolve(process.argv[1])])
app.setAsDefaultProtocolClient('itmss', process.execPath, [resolve(process.argv[1])])
app.setAsDefaultProtocolClient('musics', process.execPath, [resolve(process.argv[1])])
app.setAsDefaultProtocolClient('music', process.execPath, [resolve(process.argv[1])])
}
} else {
app.setAsDefaultProtocolClient('ame') // Custom AME Protocol
app.setAsDefaultProtocolClient('itms') // iTunes HTTP Protocol
app.setAsDefaultProtocolClient('itmss') // iTunes HTTPS Protocol
app.setAsDefaultProtocolClient('musics') // macOS Client Protocol
app.setAsDefaultProtocolClient('music') // macOS Client Protocol
}
app.on('open-url', (event, url) => {
event.preventDefault()
if (url.includes('ame://') || url.includes('itms://') || url.includes('itmss://') || url.includes('musics://') || url.includes('music://')) {
app.ame.handler.LinkHandler(url)
}
})
// Running the Application on Login
if (app.cfg.get('window.appStartupBehavior')) {
app.setLoginItemSettings({
openAtLogin: true,
args: [
'--process-start-args', `${app.cfg.get('window.appStartupBehavior') === 'hidden' ? "--hidden" : (app.cfg.get('window.appStartupBehavior') === 'minimized' ? "--minimized" : "")}`
]
})
}
// Set Max Listener
require('events').EventEmitter.defaultMaxListeners = Infinity;
},
LoggingInit: () => {
app.log = require("electron-log");
if (app.commandLine.hasSwitch('verbose')) {
app.verboseLaunched = true
}
app.log.transports.file.resolvePath = (vars) => {
return join(app.getPath('userData'), 'logs', vars.fileName);
}
Object.assign(console, app.log.functions);
console.verbose = () => {
};
if (app.cfg.get('advanced.verboseLogging') || app.verboseLaunched) {
console.verbose = app.log.debug
} else {
console.verbose = function (_data) {
return false
};
}
},
ThemeInstallation: () => {
const themesPath = join(app.getPath('userData'), "themes");
// Check if the themes folder exists and check permissions
if (existsSync(join(themesPath, 'README.md'))) {
console.verbose('[ThemeInstallation] Themes Directory Exists. Running Permission Check.')
app.ame.utils.permissionsCheck(themesPath, 'README.md')
} else {
app.ame.utils.updateThemes().catch(err => console.error(err))
}
// Save all the file names to array and log it
if (existsSync(themesPath)) {
console.log(`[InitializeTheme] Files found in Themes Directory: [${readdirSync(themesPath).join(', ')}]`)
}
// Set the default theme
if (app.cfg.get('advanced.forceApplicationMode')) {
nativeTheme.themeSource = app.cfg.get('advanced.forceApplicationMode')
}
},
PluginInstallation: () => {
if (!existsSync(resolve(app.getPath("userData"), "plugins"))) {
return;
}
// Check if the plugins folder exists and check permissions
app.pluginsEnabled = true;
console.log("[PluginInstallation][existsSync] Plugins folder exists!");
app.ame.utils.permissionsCheck(app.userPluginsPath, '/');
app.ame.utils.fetchPluginsListing();
// Save all the file names to array and log it
console.log(`[PluginInstallation] Files found in Plugins Directory: [${readdirSync(resolve(app.getPath("userData"), "plugins")).join(', ')}]`);
},
AppReady: () => {
console.verbose('[ApplicationReady] Started.');
app.pluginsEnabled = false;
// Run the Functions
init.ThemeInstallation()
init.PluginInstallation()
init.TrayInit()
app.ame.mpris.connect(); // M.P.R.I.S
app.ame.lastfm.authenticate(); // LastFM
app.ame.discord.connect(app.cfg.get('general.discordRPC') === 'ame-title' ? '911790844204437504' : '886578863147192350'); // Discord
app.isAuthorized = false;
app.isMiniplayerActive = false;
app.injectedCSS = {}
app.media = {status: false, playParams: {id: 'no-id-found'}};
/** wsapi */
// app.ame.wsapi.inAppUI()
/** wsapi */
},
TrayInit: () => {
console.verbose('[InitializeTray] Started.');
const winTray = nativeImage.createFromPath(join(__dirname, `../icons/icon.ico`)).resize({
width: 32,
height: 32
})
const macTray = nativeImage.createFromPath(join(__dirname, `../icons/icon.png`)).resize({
width: 20,
height: 20
})
const linuxTray = nativeImage.createFromPath(join(__dirname, `../icons/icon.png`)).resize({
width: 32,
height: 32
})
let trayIcon;
if (process.platform === "win32") {
trayIcon = winTray
} else if (process.platform === "linux") {
trayIcon = linuxTray
} else if (process.platform === "darwin") {
trayIcon = macTray
}
app.tray = new Tray(trayIcon)
app.tray.setToolTip(app.getName());
app.ame.win.SetContextMenu(true);
app.tray.on('double-click', () => {
if (typeof app.win.show === 'function') {
if (app.win.isVisible()) {
app.win.focus()
} else {
app.win.show()
}
}
})
}
}
module.exports = init

View file

@ -1,238 +0,0 @@
const {join, resolve} = require("path"),
{app, ipcMain, systemPreferences} = require("electron"),
{readFile, constants, chmodSync, existsSync, watch} = require("fs"),
{initAnalytics} = require('./utils');
initAnalytics();
module.exports = {
LoadCSS: function (path, theme, important) {
const fileName = path
if (theme) {
path = join(app.userThemesPath, path.toLowerCase());
} else {
path = join(join(__dirname, '../css/'), path)
}
// Check that the file exists
if (!existsSync(path)) {
console.warn(`[LoadCSS] ${path} not found.`)
return
}
// Remove previous inject (If there is one)
if (app.injectedCSS[path]) {
app.win.webContents.removeInsertedCSS(app.injectedCSS[fileName]).then(r => { if (r) console.error(r); });
}
// Get the CSS to inject
readFile(path, "utf-8", function (error, data) {
if (error) {
console.error(`[LoadCSS] Error while injecting: '${path}' - ${error}`)
try {
chmodSync(path, constants.S_IRUSR | constants.S_IWUSR);
} catch (err) {
console.error(`[LoadCSS] ${err}`)
}
} else {
let formattedData = data.replace(/\s{2,10}/g, ' ').trim();
app.win.webContents.insertCSS(formattedData, {cssOrigin: (important ? 'user' : 'author')}).then((key) => {
console.verbose(`[${theme ? 'LoadTheme' : 'LoadCSS'}] '${fileName}' successfully injected.`)
app.injectedCSS[fileName] = key
});
}
});
},
LoadJS: function (path, formatting = true) {
const fileName = path;
path = join(join(__dirname, '../js/'), path)
readFile(path, "utf-8", function (error, data) {
if (!error) {
try {
let formattedData = data
if (formatting) {
formattedData = data.replace(/\s{2,10}/g, ' ').trim();
}
app.win.webContents.executeJavaScript(formattedData).then(() => {
console.verbose(`[LoadJSFile] '${fileName}' successfully injected.`)
});
} catch (err) {
console.error(`[LoadJSFile] Error while injecting: '${fileName}' - Error: ${err}`)
}
} else {
console.error(`[LoadJSFile] Error while reading: '${fileName}' - Error: ${error}`)
}
});
},
LoadWebsite: function (win) {
if (!win) return;
app.storefront = app.cfg.get('general.storefront');
const urlBase = app.cfg.get('advanced.useBetaSite') ? 'https://beta.music.apple.com' : 'https://music.apple.com/' + app.cfg.get('general.storefront'),
urlFallback = `https://music.apple.com/`;
ipcMain.once('userAuthorized', (e, args) => {
app.isAuthorized = true
console.log(`[LoadWebsite] User Authenticated. Setting page to: ${args}`)
win.webContents.clearHistory()
})
win.loadURL(urlBase).then(() => {
app.ame.load.LoadJS('checkAuth.js')
}).catch((err) => {
win.loadURL(urlFallback).then(() => console.error(`[LoadWebsite] '${urlBase}' was unavailable, falling back to '${urlFallback}' | ${err}`))
})
},
LoadFiles: function () {
app.ame.load.LoadJS('settingsPage.js');
if (app.cfg.get('visual.removeAppleLogo')) {
app.win.webContents.insertCSS(`
@media only screen and (max-width: 483px) {
.web-navigation__nav-list {
margin-top: 50px;
}
}
}
`).catch((e) => console.error(e));
}
if (app.cfg.get('visual.useOperatingSystemAccent') && (process.platform === "win32" || process.platform === "darwin")) {
if (systemPreferences.getAccentColor()) {
const accent = '#' + systemPreferences.getAccentColor().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
})
}
} else {
app.ame.win.removeInsertedCSS('useOperatingSystemAccent')
}
/* Load Window Frame */
if (app.cfg.get('visual.frameType') === 'mac') {
app.ame.load.LoadJS('frame_macOS.js')
} else if ((app.cfg.get('visual.frameType') === 'mac-right')) {
app.ame.load.LoadJS('frame_Windows.js')
} else if (process.platform === 'darwin' && !app.cfg.get('visual.frameType')) {
app.ame.load.LoadJS('frame_macOS.js')
} else if (process.platform === 'win32' && !app.cfg.get('visual.frameType')) {
app.ame.load.LoadJS('frame_Windows.js')
if (app.win.isMaximized()) {
app.win.webContents.executeJavaScript(`if (document.querySelector("#maximize")) { document.querySelector("#maximize").classList.add("maxed"); }`).catch((e) => console.error(e));
}
}
const BackButtonChecks = (url) => {
if (!app.win.webContents.canGoBack()) return false
const backButtonBlacklist = [`*music.apple.com/*/listen-now*`, `*music.apple.com/*/browse*`, `*music.apple.com/*/radio*`, `*music.apple.com/*/search*`, `*music.apple.com/library/recently-added?l=*`, `*music.apple.com/library/albums?l=*`, `*music.apple.com/library/songs?l=*`, `*music.apple.com/library/made-for-you?l=*`, `*music.apple.com/library/recently-added`, `*music.apple.com/library/albums`, `*music.apple.com/library/made-for-you`, `*music.apple.com/library/songs*`, `*music.apple.com/library/artists/*`, `*music.apple.com/library/playlist/*`];
let blacklistPassed = true
backButtonBlacklist.forEach((item) => {
if (!blacklistPassed) return;
if (app.ame.utils.matchRuleShort(url, item) || url === item) {
blacklistPassed = false
}
});
return blacklistPassed
}
/* Load Back Button */
if (BackButtonChecks(app.win.webContents.getURL())) {
app.ame.load.LoadJS('backButton.js')
app.win.webContents.executeJavaScript(`document.body.setAttribute('back-button', 1)`)
} else { /* Removes the button if the check failed. */
app.win.webContents.executeJavaScript(`if (document.querySelector('#backButtonBar')) { document.getElementById('backButtonBar').remove() };`).catch((e) => console.error(e));
app.win.webContents.executeJavaScript(`document.body.removeAttribute('back-button')`)
}
/* Load the Startup JavaScript Function */
app.win.webContents.executeJavaScript('if (AMJavaScript) { AMJavaScript.LoadCustom(); }').catch((e) => console.error(e));
},
LoadOneTimeFiles: function () {
// Inject the custom stylesheet
app.ame.load.LoadCSS('custom-stylesheet.css')
app.ame.load.LoadCSS('ameframework.css')
// Inject Plugin Interaction
if (app.pluginsEnabled) {
app.ame.load.LoadJS('pluginSystem.js', false)
}
// Load this first so it doesn't stuck
app.ame.load.LoadJS('OpusMediaRecorder.umd.js')
app.ame.load.LoadJS('encoderWorker.umd.js')
// Lyrics
app.ame.load.LoadJS('lyrics.js')
// Vue Test
app.ame.load.LoadJS('vue.js')
app.ame.load.LoadJS('utils.js', false)
app.ame.load.LoadJS('tests.js', false)
// wsapi
app.ame.load.LoadJS('WSAPI_Interop.js', false)
// wsapi
// Bulk JavaScript Functions
app.ame.load.LoadJS('custom.js')
// Audio Manuipulation Stuff
app.ame.load.LoadJS('eq.js')
// Window Frames
if (app.cfg.get('visual.frameType') === 'mac') {
app.ame.load.LoadCSS('frame_macOS_emulation.css')
} else if (app.cfg.get('visual.frameType') === 'mac-right') {
app.ame.load.LoadCSS('frame_macOS_emulation_right.css')
} else if (process.platform === 'win32' && !app.cfg.get('visual.frameType')) {
app.ame.load.LoadCSS('frame_Windows.css')
}
// Set the settings variables if needed
if (app.cfg.get('visual.frameType') === 'mac' || app.cfg.get('visual.frameType') === 'mac-right') {
app.cfg.set('visual.removeUpsell', true);
app.cfg.set('visual.removeAppleLogo', true);
}
// Streamer Mode
if (app.cfg.get('visual.streamerMode')) {
app.ame.load.LoadCSS('streamerMode.css')
}
/* Remove the Scrollbar */
if (app.cfg.get('visual.removeScrollbars')) {
app.win.webContents.insertCSS('::-webkit-scrollbar { display: none; }').then()
} else {
app.ame.load.LoadCSS('macosScrollbar.css')
}
/* Inject the MusicKitInterop file */
app.win.webContents.executeJavaScript('MusicKitInterop.init()').catch((e) => console.error(e));
/* Watches for changes to a theme */
if (app.watcher) {
app.watcher.close();
console.verbose('[Watcher] Removed old watcher.')
}
if (existsSync(resolve(app.getPath('userData'), 'themes', `${app.cfg.get('visual.theme')}.css`)) && app.cfg.get('visual.theme') !== "default" && app.cfg.get('visual.theme')) {
app.watcher = watch(resolve(app.getPath('userData'), 'themes', `${app.cfg.get('visual.theme')}.css`), (event, fileName) => {
if (event === "change" && fileName === `${app.cfg.get('visual.theme')}.css`) {
app.win.webContents.executeJavaScript(`AMStyling.loadTheme("${app.cfg.get('visual.theme')}", true);`).catch((err) => console.error(err));
}
});
console.verbose(`[Watcher] Watching for changes: 'themes/${app.cfg.get('visual.theme')}.css'`)
}
}
}

View file

@ -1,121 +0,0 @@
const {app} = require('electron'),
DiscordRPC = require('discord-rpc'),
{initAnalytics} = require('../utils');
initAnalytics();
module.exports = {
connect: function (clientId) {
app.discord = {isConnected: false};
if (!app.cfg.get('general.discordRPC')) return;
DiscordRPC.register(clientId) // Apparently needed for ask to join, join, spectate etc.
const client = new DiscordRPC.Client({ transport: "ipc" });
app.discord = Object.assign(client,{error: false, activityCache: null, isConnected: false});
// Login to Discord
app.discord.login({ clientId })
.then(() => {
app.discord.isConnected = true;
})
.catch((e) => console.error(`[DiscordRPC][connect] ${e}`));
app.discord.on('ready', () => {
console.log(`[DiscordRPC][connect] Successfully Connected to Discord. Authed for user: ${client.user.username} (${client.user.id})`);
if (app.discord.activityCache) {
client.setActivity(app.discord.activityCache).catch((e) => console.error(e));
app.discord.activityCache = null;
}
})
// Handles Errors
app.discord.on('error', err => {
console.error(`[DiscordRPC] ${err}`);
this.disconnect()
app.discord.isConnected = false;
});
},
disconnect: function () {
if (!app.cfg.get('general.discordRPC') || !app.discord.isConnected) return;
try {
app.discord.destroy().then(() => {
app.discord.isConnected = false;
console.verbose('[DiscordRPC][disconnect] Disconnected from discord.')
}).catch((e) => console.error(`[DiscordRPC][disconnect] ${e}`));
} catch (err) {
console.error(err)
}
},
updateActivity: function (attributes) {
if (!app.cfg.get('general.discordRPC') || app.cfg.get('general.incognitoMode')) return;
if (!app.discord.isConnected) {
this.connect()
}
if (!app.discord.isConnected) return;
console.verbose('[DiscordRPC][updateActivity] Updating Discord Activity.')
const listenURL = `https://applemusicelectron.com/p?id=${attributes.playParams.id}`
let ActivityObject = {
details: attributes.name,
state: `by ${attributes.artistName}`,
startTimestamp: attributes.startTime,
endTimestamp: attributes.endTime,
largeImageKey: ((app.cfg.get('general.discordRPC') === 'am-title') ? 'apple' : 'cider'),
largeImageText: attributes.albumName,
smallImageKey: (attributes.status ? 'play' : 'pause'),
smallImageText: (attributes.status ? 'Playing': 'Paused'),
instance: true,
buttons: [
{label: "Listen on Cider", url: listenURL},
]
};
console.verbose(`[LinkHandler] Listening URL has been set to: ${listenURL}`);
if (app.cfg.get('general.discordClearActivityOnPause')) {
delete ActivityObject.smallImageKey
delete ActivityObject.smallImageText
}
// Check all the values work
if (!((new Date(attributes.endTime)).getTime() > 0)) {
delete ActivityObject.startTimestamp
delete ActivityObject.endTimestamp
}
if (!attributes.artistName) {
delete ActivityObject.state
}
if (!ActivityObject.largeImageText || ActivityObject.largeImageText.length < 2) {
delete ActivityObject.largeImageText
}
// Clear if if needed
if (!attributes.status) {
if (app.cfg.get('general.discordClearActivityOnPause')) {
app.discord.clearActivity().catch((e) => console.error(`[DiscordRPC][clearActivity] ${e}`));
ActivityObject = null
} else {
delete ActivityObject.startTimestamp
delete ActivityObject.endTimestamp
ActivityObject.smallImageKey = 'pause'
ActivityObject.smallImageText = 'Paused'
}
}
if (ActivityObject) {
try {
console.verbose(`[DiscordRPC][setActivity] Setting activity to ${JSON.stringify(ActivityObject)}`);
app.discord.setActivity(ActivityObject)
} catch (err) {
console.error(`[DiscordRPC][setActivity] ${err}`)
}
}
},
}

View file

@ -1,155 +0,0 @@
const {app, Notification} = require('electron'),
fs = require('fs'),
{resolve} = require('path'),
sessionPath = resolve(app.getPath('userData'), 'session.json'),
apiCredentials = require('../../lfmApiCredentials.json'),
LastfmAPI = require('lastfmapi'),
{initAnalytics} = require('../utils');
initAnalytics();
const lfm = {
authenticateFromFile: function () {
let sessionData = require(sessionPath)
console.log("[LastFM][authenticateFromFile] Logging in with Session Info.")
app.lastfm.setSessionCredentials(sessionData.name, sessionData.key)
console.log("[LastFM][authenticateFromFile] Logged in.")
},
authenticate: function () {
if (app.cfg.get('tokens.lastfm')) {
app.cfg.set('general.lastfm', true);
}
if (!app.cfg.get('general.lastfm') || !app.cfg.get('tokens.lastfm')) {
app.cfg.set('general.lastfm', false);
return
}
const lfmAPI = new LastfmAPI({
'api_key': apiCredentials.key,
'secret': apiCredentials.secret
});
app.lastfm = Object.assign(lfmAPI, {cachedAttributes: false, cachedNowPlayingAttributes: false});
fs.stat(sessionPath, function (err) {
if (err) {
console.error("[LastFM][Session] Session file couldn't be opened or doesn't exist,", err)
console.log("[LastFM][Auth] Beginning authentication from configuration")
app.lastfm.authenticate(app.cfg.get('tokens.lastfm'), function (err, session) {
if (err) {
throw err;
}
console.log("[LastFM] Successfully obtained LastFM session info,", session); // {"name": "LASTFM_USERNAME", "key": "THE_USER_SESSION_KEY"}
console.log("[LastFM] Saving session info to disk.")
let tempData = JSON.stringify(session)
fs.writeFile(sessionPath, tempData, (err) => {
if (err)
console.log("[LastFM][fs]", err)
else {
console.log("[LastFM][fs] File was written successfully.")
lfm.authenticateFromFile()
new Notification({
title: app.getName(),
body: "Successfully logged into LastFM using Authentication Key."
}).show()
}
})
});
} else {
lfm.authenticateFromFile()
}
})
},
scrobbleSong: async function (attributes) {
await new Promise(resolve => setTimeout(resolve, app.cfg.get('general.lastfmScrobbleDelay') * 1000));
const currentAttributes = app.media;
if (!app.lastfm || app.lastfm.cachedAttributes === attributes || app.cfg.get('general.incognitoMode')) {
return
}
if (app.lastfm.cachedAttributes) {
if (app.lastfm.cachedAttributes.playParams.id === attributes.playParams.id) return;
}
if (currentAttributes.status && currentAttributes === attributes) {
if (fs.existsSync(sessionPath)) {
// Scrobble playing song.
if (attributes.status === true) {
app.lastfm.track.scrobble({
'artist': lfm.filterArtistName(attributes.artistName),
'track': attributes.name,
'album': attributes.albumName,
'albumArtist': this.filterArtistName(attributes.artistName),
'timestamp': new Date().getTime() / 1000
}, function (err, scrobbled) {
if (err) {
return console.error('[LastFM] An error occurred while scrobbling', err);
}
console.verbose('[LastFM] Successfully scrobbled: ', scrobbled);
});
app.lastfm.cachedAttributes = attributes
}
} else {
this.authenticate();
}
} else {
return console.verbose('[LastFM] Did not add ', attributes.name , '-' , lfm.filterArtistName(attributes.artistName), 'because now playing a other song.');
}
},
filterArtistName: function (artist) {
if (!app.cfg.get('general.lastfmRemoveFeaturingArtists')) return artist;
artist = artist.split(' ');
if (artist.includes('&')) {
artist.length = artist.indexOf('&');
}
if (artist.includes('and')) {
artist.length = artist.indexOf('and');
}
artist = artist.join(' ');
if (artist.includes(',')) {
artist = artist.split(',')
artist = artist[0]
}
return artist.charAt(0).toUpperCase() + artist.slice(1);
},
updateNowPlayingSong: function (attributes) {
if (!app.lastfm ||app.lastfm.cachedNowPlayingAttributes === attributes || app.cfg.get('general.incognitoMode') || !app.cfg.get('general.lastfmNowPlaying')) {
return
}
if (app.lastfm.cachedNowPlayingAttributes) {
if (app.lastfm.cachedNowPlayingAttributes.playParams.id === attributes.playParams.id) return;
}
if (fs.existsSync(sessionPath)) {
// update Now Playing
if (attributes.status === true) {
app.lastfm.track.updateNowPlaying({
'artist': lfm.filterArtistName(attributes.artistName),
'track': attributes.name,
'album': attributes.albumName,
'albumArtist': this.filterArtistName(attributes.artistName)
}, function (err, nowPlaying) {
if (err) {
return console.error('[LastFM] An error occurred while updating nowPlayingSong', err);
}
console.verbose('[LastFM] Successfully updated nowPlayingSong', nowPlaying);
});
app.lastfm.cachedNowPlayingAttributes = attributes
}
} else {
this.authenticate()
}
}
}
module.exports = lfm;

View file

@ -1,119 +0,0 @@
const {app} = require('electron'),
Player = require('mpris-service'),
{initAnalytics} = require('../utils');
initAnalytics();
// Remember to use playerctl when debugging this.
// I'm just putting this here as I keep forgetting the command.
module.exports = {
connect: function () {
if (process.platform !== "linux") {
app.mpris = {active: false}
return;
}
console.log('[MPRIS][connect] Initializing Connection.')
try {
app.mpris = Player({
name: 'AppleMusic',
identity: 'Apple Music',
supportedUriSchemes: [],
supportedMimeTypes: [],
supportedInterfaces: ['player']
});
app.mpris = Object.assign(app.mpris, { active: false, canQuit: true, canControl: true, canPause: true, canPlay: true, canGoNext: true })
} catch (err) {
app.mpris.active = false
console.error(`[MPRIS][connect] ${err}`)
return
}
let pos_atr = {durationInMillis: 0};
app.mpris.getPosition = function () {
const durationInMicro = pos_atr.durationInMillis * 1000;
const percentage = parseFloat(0) || 0;
return durationInMicro * percentage;
}
app.mpris.active = true
this.clearActivity()
this.stateHandler()
},
stateHandler: function () {
app.mpris.on('playpause', async () => {
app.win.webContents.executeJavaScript('MusicKitInterop.pausePlay()').catch(err => console.error(err))
});
app.mpris.on('play', async () => {
app.win.webContents.executeJavaScript('MusicKitInterop.pausePlay()').catch(err => console.error(err))
});
app.mpris.on('pause', async () => {
app.win.webContents.executeJavaScript('MusicKitInterop.pausePlay()').catch(err => console.error(err))
});
app.mpris.on('next', async () => {
app.win.webContents.executeJavaScript('MusicKitInterop.nextTrack()').catch(err => console.error(err))
});
app.mpris.on('previous', async () => {
app.win.webContents.executeJavaScript('MusicKitInterop.previousTrack()').catch(err => console.error(err))
});
},
updateActivity: function (attributes) {
if (!app.mpris.active) return;
console.verbose('[MPRIS][updateActivity] Updating Song Activity.')
const MetaData = {
'mpris:trackid': app.mpris.objectPath(`track/${attributes.playParams.id.replace(/[.]+/g, "")}`),
'mpris:length': attributes.durationInMillis * 1000, // In microseconds
'mpris:artUrl': (attributes.artwork.url.replace('/{w}x{h}bb', '/512x512bb')).replace('/2000x2000bb', '/35x35bb'),
'xesam:title': `${attributes.name}`,
'xesam:album': `${attributes.albumName}`,
'xesam:artist': [`${attributes.artistName}`,],
'xesam:genre': attributes.genreNames
}
if (app.mpris.metadata["mpris:trackid"] === MetaData["mpris:trackid"]) {
return
}
app.mpris.metadata = MetaData
},
updateState: function (attributes) {
if (!app.mpris.active) return;
console.verbose('[MPRIS][updateState] Updating Song Playback State.')
function setPlaybackIfNeeded(status) {
if (app.mpris.playbackStatus === status) {
return
}
app.mpris.playbackStatus = status;
}
switch (attributes.status) {
case true: // Playing
setPlaybackIfNeeded('Playing');
break;
case false: // Paused
setPlaybackIfNeeded('Paused');
break;
default: // Stopped
setPlaybackIfNeeded('Stopped');
break;
}
},
clearActivity: function () {
if (!app.mpris.active) return;
app.mpris.metadata = {'mpris:trackid': '/org/mpris/MediaPlayer2/TrackList/NoTrack'}
app.mpris.playbackStatus = 'Stopped';
},
}

View file

@ -1,30 +0,0 @@
const {
BrowserWindow
} = require('electron');
const SplashScreen = {
win: null,
show: function () {
this.win = new BrowserWindow({
width: 300,
height: 300,
resizable: false,
show: true,
center: true,
transparent: true,
frame: false,
alwaysOnTop: true,
// skipTaskbar: true,
webPreferences: {
nodeIntegration: true
}
})
this.win.show()
this.win.loadFile('./resources/splash/index.html')
this.win.on("closed", () => {
this.win = null
})
}
}
module.exports = SplashScreen

View file

@ -1,395 +0,0 @@
const {app, nativeImage, nativeTheme, Notification, dialog} = require("electron"),
{existsSync, readFileSync, readdirSync, constants, access, writeFileSync, copyFileSync} = require("fs"),
{readdir, mkdir} = require("fs/promises"),
{join, resolve} = require("path"),
{autoUpdater} = require("electron-updater"),
os = require("os"),
rimraf = require("rimraf"),
chmod = require("chmodr"),
clone = require("git-clone/promise"),
trayIconDir = (nativeTheme.shouldUseDarkColors ? join(__dirname, `../icons/media/light/`) : join(__dirname, `../icons/media/dark/`)),
ElectronSentry = require("@sentry/electron");
const Utils = {
/* hexToRgb - Converts hex codes to rgb */
hexToRgb: (hex) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
},
/* matchRuleShort - Used for wildcards */
matchRuleShort: (str, rule) => {
var escapeRegex = (str) => str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
return new RegExp("^" + rule.split("*").map(escapeRegex).join(".*") + "$").test(str);
},
/* isVibrancySupported - Checks if the operating system support electron-acrylic-window (Windows 10 or greater) */
isVibrancySupported: () => {
return (process.platform === 'win32' && parseInt(os.release().split('.')[0]) >= 10)
},
/* isAcrylicSupported - Checks if the operating system supports the acrylic transparency affect (Windows RS3 (Redstone 3) 1709 or Greater) */
isAcrylicSupported: () => {
return (process.platform === 'win32' && parseInt(os.release().replace(/\./g, "").replace(',', '.')) >= 10016299)
},
/* fetchThemeMeta - Fetches the meta data associated to a theme */
fetchThemeMeta: (fileName) => {
const filePath = resolve(app.getPath("userData"), "themes", `${fileName}.css`);
let fileMeta = {
name: null,
author: null,
description: null,
transparency: {dark: null, light: null},
options: []
};
if (!existsSync(filePath)) return fileMeta;
const file = readFileSync(filePath, "utf8");
if (!file) return;
file.split(/\r?\n/).forEach((line) => {
if (line.includes("@name")) {
fileMeta.name = line.split("@name ")[1].trim();
}
if (line.includes("@author")) {
fileMeta.author = line.split("@author ")[1].trim();
}
if (line.includes("@description")) {
fileMeta.description = line.split("@description ")[1]
}
if (line.includes("@option")) {
var themeOption = line.split("@option ")[1].trim().split("|")
fileMeta.options.push({
key: themeOption[0],
name: themeOption[1],
defaultValue: themeOption[2]
})
}
if (line.includes("--lightTransparency")) {
fileMeta.transparency.light = line.split("--lightTransparency: ")[1].trim().split(' ')[0];
}
if (line.includes("--darkTransparency")) {
fileMeta.transparency.dark = line.split("--darkTransparency: ")[1].trim().split(' ')[0];
}
if (fileMeta.transparency.dark && fileMeta.transparency.light) {
fileMeta.transparency = nativeTheme.shouldUseDarkColors ? fileMeta.transparency.dark : fileMeta.transparency.light
}
if (!fileMeta.transparency.dark || !fileMeta.transparency.light) {
if (line.includes("--transparency")) {
fileMeta.transparency = line.split("--transparency: ")[1].split(' ')[0];
}
}
});
if (typeof fileMeta.transparency == "object") {
if (!fileMeta.transparency.dark || !fileMeta.transparency.light) {
fileMeta.transparency = false;
}
}
console.verbose(`[fetchThemeMeta] Returning ${JSON.stringify(fileMeta)}`);
return fileMeta
},
/* fetchTransparencyOptions - Fetches the transparency options */
fetchTransparencyOptions: () => {
if (process.platform === "darwin" && (!app.cfg.get('visual.transparencyEffect') || !Utils.isVibrancySupported())) {
app.transparency = true;
return "fullscreen-ui"
} else if (!app.cfg.get('visual.transparencyEffect') || !Utils.isVibrancySupported()) {
console.verbose(`[fetchTransparencyOptions] Vibrancy not created. Required options not met. (transparencyEffect: ${app.cfg.get('visual.transparencyEffect')} | isVibrancySupported: ${Utils.isVibrancySupported()})`);
app.transparency = false;
return false
} else if (process.platform === "win32" && app.cfg.get('visual.transparencyEffect') === "mica") {
return false
}
console.log('[fetchTransparencyOptions] Fetching Transparency Options')
let transparencyOptions = {
theme: null,
effect: app.cfg.get('visual.transparencyEffect'),
debug: app.cfg.get('advanced.verboseLogging'),
}
//------------------------------------------
// Disable on blur for acrylic
//------------------------------------------
if (app.cfg.get('visual.transparencyEffect') === 'acrylic') {
transparencyOptions.disableOnBlur = app.cfg.get('visual.transparencyDisableBlur');
}
//------------------------------------------
// Set the transparency theme
//------------------------------------------
if (app.cfg.get('visual.transparencyTheme') === 'appearance-based') {
if (app.cfg.get('visual.theme') && app.cfg.get('visual.theme') !== "default") {
transparencyOptions.theme = Utils.fetchThemeMeta(app.cfg.get('visual.theme')).transparency; /* Fetch the Transparency from the Themes Folder */
} else if ((!app.cfg.get('visual.theme') || app.cfg.get('visual.theme') === "default") && app.cfg.get('visual.transparencyEffect') === 'acrylic') {
transparencyOptions.theme = (nativeTheme.shouldUseDarkColors ? '#3C3C4307' : '#EBEBF507') /* Default Theme when Using Acrylic */
} else { // Fallback
transparencyOptions.theme = (nativeTheme.shouldUseDarkColors ? 'dark' : 'light')
}
} else {
transparencyOptions.theme = app.cfg.get('visual.transparencyTheme');
}
//------------------------------------------
// Set the refresh rate
//------------------------------------------
if (app.cfg.get('visual.transparencyMaximumRefreshRate')) {
transparencyOptions.useCustomWindowRefreshMethod = true
transparencyOptions.maximumRefreshRate = app.cfg.get('visual.transparencyMaximumRefreshRate')
}
app.transparency = true
console.log(`[fetchTransparencyOptions] Returning: ${JSON.stringify(transparencyOptions)}`)
return transparencyOptions
},
/* fetchThemesListing - Fetches the themes directory listing (Lists .css files) */
fetchThemesListing: () => {
if (!existsSync(resolve(app.getPath("userData"), "themes"))) return;
let themesFileNames = [], themesListing = {};
readdirSync(resolve(app.getPath("userData"), "themes")).forEach((value) => {
if (value.split('.').pop() === 'css') {
themesFileNames.push(value.split('.').shift())
}
});
// Get the Info
themesFileNames.forEach((themeFileName) => {
const themeData = Utils.fetchThemeMeta(themeFileName);
if (themeData && themeData.name && themeData.description && themeData.author) {
themesListing[themeFileName] = themeData;
}
})
return themesListing
},
/* fetchPluginsListing - Fetches the plugins directory listing (Lists .js files) */
fetchPluginsListing: () => {
if (!existsSync(resolve(app.getPath("userData"), "plugins"))) return;
let pluginsFileNames = [], pluginsListing = {};
readdirSync(resolve(app.getPath("userData"), "plugins")).forEach((value) => {
if (value.split('.').pop() === 'js') {
pluginsFileNames.push(value.split('.').shift())
}
});
console.log(pluginsFileNames)
return pluginsFileNames
},
/* fetchOperatingSystem - Fetches the operating system name */
fetchOperatingSystem: () => {
if (process.platform === "win32") {
const release = parseInt(os.release().replaceAll('.', ''))
if (release >= 10022000) {
return 'Windows 11'
} else if (release < 10022000 && release >= 10010240) {
return 'Windows 10'
}
}
},
/* updateThemes - Purges the themes directory and clones a fresh copy of the themes */
updateThemes: async () => {
if (app.watcher) {
app.watcher.close()
}
const tmpDir = join(os.tmpdir(), "ame-themes")
const themesDir = join(app.getPath("userData"), "themes")
if (existsSync(themesDir)) {
if (existsSync(tmpDir)) {
rimraf(tmpDir, [], async (err) => {
if (err) return err
await clone('https://github.com/Apple-Music-Electron/Apple-Music-Electron-Themes', tmpDir, [], (err) => console.log(err))
})
} else {
await mkdir(tmpDir, {recursive: true})
await clone('https://github.com/Apple-Music-Electron/Apple-Music-Electron-Themes', tmpDir, [], (err) => console.log(err))
}
// Base Line Directory Comparison
const updateList = await readdir(tmpDir);
const foundChanges = {};
for (const file of updateList) {
if (file.split('.').pop() === 'css' && file !== "Template.css") { // Reduces listing compare down to css files
console.verbose(`[compareDirectories] Comparing ${file}`)
if (!existsSync(join(themesDir, file))) {
copyFileSync(join(tmpDir, file), join(themesDir, file))
foundChanges[file] = 'added'
} else {
const updateFile = readFileSync(join(tmpDir, file));
const origFile = readFileSync(join(themesDir, file));
if (origFile.toString() !== updateFile.toString()) {
writeFileSync(join(themesDir, file), updateFile)
foundChanges[file] = 'updated'
}
}
}
}
return foundChanges
} else {
await mkdir(tmpDir, {recursive: true})
await clone('https://github.com/Apple-Music-Electron/Apple-Music-Electron-Themes', themesDir, [], (err) => console.log(err))
return {'initial': true}
}
},
/* permissionsCheck - Checks of the file can be read and written to, if it cannot be chmod -r is run on the directory */
permissionsCheck: (folder, file) => {
console.verbose(`[permissionsCheck] Running check on ${join(folder, file)}`)
access(join(folder, file), constants.R_OK | constants.W_OK, (err) => {
if (err) { // File cannot be read after cloning
console.error(`[permissionsCheck][access] ${err}`)
chmod(folder, 0o777, (err) => {
if (err) {
console.error(`[permissionsCheck][chmod] ${err} - Theme set to default to prevent application launch halt.`);
}
});
} else {
console.verbose('[permissionsCheck] Check passed.')
}
})
},
/* initAnalytics - Sentry Analytics */
initAnalytics: () => {
if (app.cfg.get('general.analyticsEnabled') && app.isPackaged) {
ElectronSentry.init({dsn: "https://20e1c34b19d54dfcb8231e3ef7975240@o954055.ingest.sentry.io/5903033"});
}
},
/* checkForUpdates - Checks for update using electron-updater (Part of electron-builder) */
checkForUpdates: (manual) => {
if (!app.isPackaged || process.env.NODE_ENV !== 'production') return;
autoUpdater.logger = require("electron-log");
autoUpdater.logger.transports.file.resolvePath = (vars) => {
return join(app.getPath('userData'), 'logs', vars.fileName);
}
autoUpdater.logger.transports.file.level = "info";
if (app.cfg.get('advanced.autoUpdaterBetaBuilds')) {
autoUpdater.allowPrerelease = true
autoUpdater.allowDowngrade = false
}
autoUpdater.on('update-not-available', () => {
if (manual === true) {
let bodyVer = `You are on the latest version. (v${app.getVersion()})`
new Notification({title: "Apple Music", body: bodyVer}).show()
}
})
autoUpdater.on('download-progress', (progress) => {
let convertedProgress = parseFloat(progress);
app.win.setProgressBar(convertedProgress)
})
autoUpdater.on("error", function (error) {
console.error(`[checkForUpdates] Error ${error}`)
});
autoUpdater.on('update-downloaded', (updateInfo) => {
console.warn('[checkForUpdates] New version downloaded. Starting user prompt.');
dialog.showMessageBox({
type: 'info',
title: 'Updates Available',
message: `Update was found and downloaded, would you like to install the update now?`,
details: updateInfo,
buttons: ['Sure', 'No']
}).then(({response}) => {
if (response === 0) {
const isSilent = true;
const isForceRunAfter = true;
autoUpdater.quitAndInstall(isSilent, isForceRunAfter);
} else {
updater.enabled = true
updater = null
}
})
})
autoUpdater.checkForUpdates()
.then(r => {
console.verbose(`[checkForUpdates] Check for updates completed. Response: ${r}`)
})
.catch(err => {
console.error(`[checkUpdates] An error occurred while checking for updates: ${err}`)
})
},
/* Media Controlling Functions (Pause/Play/Skip/Previous) */
media: {
pausePlay() {
console.verbose('[AppleMusic] pausePlay run.')
app.win.webContents.executeJavaScript("MusicKitInterop.pausePlay()").catch((err) => console.error(err))
},
nextTrack() {
console.verbose('[AppleMusic] nextTrack run.')
app.win.webContents.executeJavaScript("MusicKitInterop.nextTrack()").catch((err) => console.error(err))
},
previousTrack() {
console.verbose('[AppleMusic] previousTrack run.')
app.win.webContents.executeJavaScript("MusicKitInterop.previousTrack()").catch((err) => console.error(err))
}
},
/* Media-associated Icons (Used for Thumbar and TouchBar) */
icons: {
pause: nativeImage.createFromPath(join(trayIconDir, 'pause.png')).resize({width: 32, height: 32}),
play: nativeImage.createFromPath(join(trayIconDir, 'play.png')).resize({width: 32, height: 32}),
nextTrack: nativeImage.createFromPath(join(trayIconDir, 'next.png')).resize({width: 32, height: 32}),
previousTrack: nativeImage.createFromPath(join(trayIconDir, 'previous.png')).resize({width: 32, height: 32}),
inactive: {
play: nativeImage.createFromPath(join(trayIconDir, 'play-inactive.png')).resize({width: 32, height: 32}),
nextTrack: nativeImage.createFromPath(join(trayIconDir, 'next-inactive.png')).resize({
width: 32,
height: 32
}),
previousTrack: nativeImage.createFromPath(join(trayIconDir, 'previous-inactive.png')).resize({
width: 32,
height: 32
}),
}
}
}
Utils.initAnalytics()
module.exports = Utils;

View file

@ -1,407 +0,0 @@
const {app, Menu, Notification, TouchBar, BrowserWindow} = require("electron"),
{TouchBarButton, TouchBarLabel, TouchBarSpacer} = TouchBar,
{join} = require("path"),
windowStateKeeper = require("electron-window-state"),
{initAnalytics} = require('./utils');
initAnalytics();
module.exports = {
SetApplicationMenu: () => {
if (process.platform !== "darwin") return;
Menu.setApplicationMenu(Menu.buildFromTemplate([
{
label: app.getName(),
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
]
},
{
label: 'View',
submenu: [
{ role: 'reload' },
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
{ type: 'separator' },
{ role: 'togglefullscreen' }
]
},
{
label: 'Window',
role: 'window',
submenu: [
{ role: 'minimize' },
{ role: 'zoom' },
{ type: 'separator' },
{ role: 'front' },
{ type: 'separator' },
{ role: 'window' }
]
},
{
label: 'Support',
role: 'help',
submenu: [
{
label: 'Discord',
click() {
require("shell").openExternal("https://discord.gg/CezHYdXHEM")
}
},
{
label: 'GitHub Wiki',
click() {
require("shell").openExternal("https://github.com/Apple-Music-Electron/Apple-Music-Electron/wiki")
}
},
{ type: 'separator' },
{
label: 'View License',
click() {
require("shell").openExternal("https://github.com/Apple-Music-Electron/Apple-Music-Electron/blob/master/LICENSE")
}
},
{ type: 'separator' },
{
label: 'Toggle Developer Tools',
accelerator: 'Option+CommandOrControl+I',
click() {
app.win.webContents.openDevTools()
}
},
{
label: 'Open Configuration File in Editor',
click() {
app.cfg.openInEditor()
}
}
]
},
]));
},
SetContextMenu: (visibility) => {
if (visibility) {
app.tray.setContextMenu(Menu.buildFromTemplate([
{
label: 'Check for Updates',
click: function () {
app.ame.utils.checkForUpdates(true)
}
},
{
label: 'Minimize to Tray',
click: function () {
if (typeof app.win.hide === 'function') {
app.win.hide();
}
}
},
{
label: 'Quit',
click: function () {
app.quit();
}
}
]));
} else {
app.tray.setContextMenu(Menu.buildFromTemplate([
{
label: 'Check for Updates',
click: function () {
app.ame.utils.checkForUpdates(true)
}
},
{
label: `Show ${app.getName()}`,
click: function () {
if (typeof app.win.show === 'function') {
app.win.show();
}
}
},
{
label: 'Quit',
click: function () {
app.quit();
}
}
]));
}
return true
},
SetTaskList: () => {
if (process.platform !== "win32") return;
app.setUserTasks([
{
program: process.execPath,
arguments: '--force-quit',
iconPath: process.execPath,
iconIndex: 0,
title: `Quit ${app.getName()}`
}
]);
return true
},
SetButtons: () => {
if (process.platform === 'win32') { // Set the Windows Thumbnail Toolbar Buttons
if (app.media.playParams.id !== 'no-id-found') {
app.win.setThumbarButtons([
{
tooltip: 'Previous',
icon: app.ame.utils.icons.previousTrack,
click() {
app.ame.utils.media.previousTrack()
}
},
{
tooltip: app.media.status ? 'Pause' : 'Play',
icon: app.media.status ? app.ame.utils.icons.pause : app.ame.utils.icons.play,
click() {
app.ame.utils.media.pausePlay()
}
},
{
tooltip: 'Next',
icon: app.ame.utils.icons.nextTrack,
click() {
app.ame.utils.media.nextTrack()
}
}
]);
} else {
app.win.setThumbarButtons([
{
tooltip: 'Previous',
icon: app.ame.utils.icons.inactive.previousTrack,
flags: ["disabled"]
},
{
tooltip: 'Play',
icon: app.ame.utils.icons.inactive.play,
flags: ["disabled"]
},
{
tooltip: 'Next',
icon: app.ame.utils.icons.inactive.nextTrack,
flags: ["disabled"]
}
]);
}
} else if (process.platform === 'darwin') { // Set the macOS Touchbar
if (!app.media || app.media.playParams.id === 'no-id-found') return;
const nextTrack = new TouchBarButton({
icon: app.ame.utils.icons.nextTrack,
click: () => {
app.ame.utils.media.nextTrack()
}
})
const previousTrack = new TouchBarButton({
icon: app.ame.utils.icons.previousTrack,
click: () => {
app.ame.utils.media.previousTrack()
}
})
const playPause = new TouchBarButton({
icon: app.media.status ? app.ame.utils.icons.pause : app.ame.utils.icons.play,
click: () => {
app.ame.utils.media.pausePlay()
}
})
const trackInfo = new TouchBarLabel({
label: app.media.name ? `${app.media.name} by ${app.media.artistName}` : `Nothing is Playing`
})
const touchBar = new TouchBar({
items: [
previousTrack,
playPause,
nextTrack,
new TouchBarSpacer({size: 'flexible'}),
trackInfo,
new TouchBarSpacer({size: 'flexible'})
]
})
app.win.setTouchBar(touchBar)
}
},
SetTrayTooltip: (attributes) => {
if (!app.cfg.get('general.trayTooltipSongName')) return;
console.verbose(`[UpdateTooltip] Updating Tooltip for ${attributes.name} to ${attributes.status}`)
if (attributes.status === true) {
app.tray.setToolTip(`Playing ${attributes.name} by ${attributes.artistName} on ${attributes.albumName}`);
} else {
app.tray.setToolTip(`Paused ${attributes.name} by ${attributes.artistName} on ${attributes.albumName}`);
}
},
CreateNotification: (attributes) => {
if (!Notification.isSupported() || !(app.cfg.get('general.playbackNotifications') || app.cfg.get('general.playbackNotifications') === 'minimized')) return;
if (app.cfg.get('general.playbackNotifications') === "minimized" && !(!app.win.isVisible() || app.win.isMinimized())) {
return;
}
console.verbose(`[CreateNotification] Notification Generating | Function Parameters: SongName: ${attributes.name} | Artist: ${attributes.artistName} | Album: ${attributes.albumName}`)
if (app.ipc.existingNotification) {
console.log("[CreateNotification] Existing Notification Found - Removing. ")
app.ipc.existingNotification.close()
app.ipc.existingNotification = false
}
const NOTIFICATION_OBJECT = {
title: attributes.name,
body: `${attributes.artistName} - ${attributes.albumName}`,
silent: true,
icon: join(__dirname, '../icons/icon.png'),
actions: [{
type: 'button',
text: 'Skip'
}]
}
app.ipc.existingNotification = new Notification(NOTIFICATION_OBJECT)
app.ipc.existingNotification.show()
app.ipc.existingNotification.addListener('action', (_event) => {
app.ame.utils.media.nextTrack()
});
},
CreateBrowserWindow: () => {
console.log('[CreateBrowserWindow] Initializing Browser Window Creation.')
// Set default window sizes
const mainWindowState = windowStateKeeper({
defaultWidth: 1024,
defaultHeight: 600
});
const options = {
icon: join(__dirname, `../icons/icon.ico`),
width: mainWindowState.width,
height: mainWindowState.height,
x: mainWindowState.x,
y: mainWindowState.y,
minWidth: (app.cfg.get('visual.streamerMode') ? 400 : 300),
minHeight: ((app.cfg.get('visual.frameType') === 'mac' || app.cfg.get('visual.frameType') === 'mac-right') ? (app.cfg.get('visual.streamerMode')? 55 : 300) : (app.cfg.get('visual.streamerMode') ? 115 : 300)),
frame: (process.platform !== 'win32' && !(app.cfg.get('visual.frameType') === 'mac' || app.cfg.get('visual.frameType') === 'mac-right')),
title: app.getName(),
resizable: true,
// Enables DRM
webPreferences: {
plugins: true,
preload: join(__dirname, '../js/MusicKitInterop.js'),
allowRunningInsecureContent: true,
nodeIntegration: false,
nodeIntegrationInWorker: false,
contextIsolation: false,
webSecurity: true,
sandbox: true,
nativeWindowOpen: true
}
};
// Fetch the transparency options
const transparencyOptions = app.ame.utils.fetchTransparencyOptions()
if (process.platform === 'darwin' && !app.cfg.get('visual.frameType')) { // macOS Frame and transparency
options.titleBarStyle = 'hidden'
options.titleBarOverlay = true
options.frame = true
options.trafficLightPosition = {x: 20, y: 20}
options.transparent = (app.transparency && transparencyOptions)
}
// Create the Browser Window
console.log('[CreateBrowserWindow] Creating BrowserWindow.')
let win;
if (process.platform === "darwin" || process.platform === "linux") {
win = new BrowserWindow(options)
} else {
const {BrowserWindow} = require("electron-acrylic-window");
if (app.transparency && transparencyOptions) {
console.log('[CreateBrowserWindow] Setting Vibrancy')
options.vibrancy = transparencyOptions
}
win = new BrowserWindow(options)
}
// Set the transparency
if (app.transparency && transparencyOptions && process.platform === "darwin") {
console.log('[CreateBrowserWindow] Setting Vibrancy')
win.setVibrancy(transparencyOptions)
}
// alwaysOnTop
if (!app.cfg.get('window.alwaysOnTop')) {
win.setAlwaysOnTop(false)
} else {
win.setAlwaysOnTop(true)
}
win.setMenuBarVisibility(false); // Hide that nasty menu bar
if (app.cfg.get('advanced.devToolsOnStartup')) win.webContents.openDevTools({mode: 'detach'}); // Enables Detached DevTools
// Register listeners on Window to track size and position of the Window.
mainWindowState.manage(win);
// Load the Website
app.ame.load.LoadWebsite(win)
return win
},
HandleBrowserWindow: () => {
// Detect if the application has been opened with --minimized
if (app.commandLine.hasSwitch('minimized') || process.argv.includes('--minimized')) {
console.log("[Apple-Music-Electron] Application opened with '--minimized'");
if (typeof app.win.minimize === 'function') {
app.win.minimize();
}
}
// Detect if the application has been opened with --hidden
if (app.commandLine.hasSwitch('hidden') || process.argv.includes('--hidden')) {
console.log("[Apple-Music-Electron] Application opened with '--hidden'");
if (typeof app.win.hide === 'function') {
app.win.hide()
}
}
},
removeInsertedCSS: (index) => {
if (app.injectedCSS[index]) {
app.win.webContents.removeInsertedCSS(app.injectedCSS[index]).then(r => { if (r) { console.error(r); }});
}
}
}

View file

@ -1,321 +0,0 @@
const ws = require('ws');
const http = require('http');
const WebSocketServer = ws.Server;
const WebSocket = ws.WebSocket;
const url = require('url');
const fs = require('fs');
const path = require('path');
const port = process.argv[2] || 9000;
const express = require('express');
const router = express.Router();
const getPort = require('get-port');
const {
ipcMain,
app,
BrowserWindow
} = require('electron');
const wsapi = {
standardResponse: function (status, data, message, type = "generic") {
this.status = status;
this.message = message;
this.data = data;
this.type = type;
},
port: 26369,
wss: null,
clients: [],
createId() {
// create random guid
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0,
v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
},
async InitWebSockets () {
ipcMain.on('wsapi-updatePlaybackState', (event, arg) => {
wsapi.updatePlaybackState(arg);
})
ipcMain.on('wsapi-returnQueue', (event, arg) => {
wsapi.returnQueue(JSON.parse(arg));
});
ipcMain.on('wsapi-returnSearch', (event, arg) => {
wsapi.returnSearch(JSON.parse(arg));
});
ipcMain.on('wsapi-returnSearchLibrary', (event, arg) => {
wsapi.returnSearchLibrary(JSON.parse(arg));
});
ipcMain.on('wsapi-returnDynamic', (event, arg, type) => {
wsapi.returnDynamic(JSON.parse(arg), type);
});
ipcMain.on('wsapi-returnMusicKitApi', (event, arg, method) => {
wsapi.returnMusicKitApi(JSON.parse(arg), method);
});
ipcMain.on('wsapi-returnLyrics', (event, arg) => {
wsapi.returnLyrics(JSON.parse(arg));
});
var safeport = await getPort({port : 26369});
wss = new WebSocketServer({
port: safeport,
perMessageDeflate: {
zlibDeflateOptions: {
// See zlib defaults.
chunkSize: 1024,
memLevel: 7,
level: 3
},
zlibInflateOptions: {
chunkSize: 10 * 1024
},
// Other options settable:
clientNoContextTakeover: true, // Defaults to negotiated value.
serverNoContextTakeover: true, // Defaults to negotiated value.
serverMaxWindowBits: 10, // Defaults to negotiated value.
// Below options specified as default values.
concurrencyLimit: 10, // Limits zlib concurrency for perf.
threshold: 1024 // Size (in bytes) below which messages
// should not be compressed if context takeover is disabled.
}
})
console.log(`WebSocketServer started on port: ${safeport}`);
const defaultResponse = new wsapi.standardResponse(0, {}, "OK");
wss.on('connection', function connection(ws) {
ws.id = wsapi.createId();
console.log(`Client ${ws.id} connected`)
wsapi.clients.push(ws);
ws.on('message', function incoming(message) {
});
// ws on message
ws.on('message', function incoming(message) {
let data = JSON.parse(message);
let response = new wsapi.standardResponse(0, {}, "OK");;
if (data.action) {
data.action.toLowerCase();
}
switch (data.action) {
default:
response.message = "Action not found";
break;
case "identify":
response.message = "Thanks for identifying!"
response.data = {
id: ws.id
}
ws.identity = {
name: data.name,
author: data.author,
description: data.description,
version: data.version
}
break;
case "play-next":
app.win.webContents.executeJavaScript(`wsapi.playNext(\`${data.type}\`,\`${data.id}\`)`);
response.message = "Play Next";
break;
case "play-later":
app.win.webContents.executeJavaScript(`wsapi.playLater(\`${data.type}\`,\`${data.id}\`)`);
response.message = "Play Later";
break;
case "quick-play":
app.win.webContents.executeJavaScript(`wsapi.quickPlay(\`${data.term}\`)`);
response.message = "Quick Play";
break;
case "get-lyrics":
app.win.webContents.executeJavaScript(`wsapi.getLyrics()`);
break;
case "shuffle":
app.win.webContents.executeJavaScript(`wsapi.toggleShuffle()`);
break;
case "set-shuffle":
if(data.shuffle == true) {
app.win.webContents.executeJavaScript(`MusicKit.getInstance().shuffleMode = 1`);
}else{
app.win.webContents.executeJavaScript(`MusicKit.getInstance().shuffleMode = 0`);
}
break;
case "repeat":
app.win.webContents.executeJavaScript(`wsapi.toggleRepeat()`);
break;
case "seek":
app.win.webContents.executeJavaScript(`MusicKit.getInstance().seekToTime(${parseFloat(data.time)})`);
response.message = "Seek";
break;
case "pause":
app.win.webContents.executeJavaScript(`MusicKit.getInstance().pause()`);
response.message = "Paused";
break;
case "play":
app.win.webContents.executeJavaScript(`MusicKit.getInstance().play()`);
response.message = "Playing";
break;
case "stop":
app.win.webContents.executeJavaScript(`MusicKit.getInstance().stop()`);
response.message = "Stopped";
break;
case "volume":
app.win.webContents.executeJavaScript(`MusicKit.getInstance().volume = ${parseFloat(data.volume)}`);
response.message = "Volume";
break;
case "mute":
app.win.webContents.executeJavaScript(`MusicKit.getInstance().mute()`);
response.message = "Muted";
break;
case "unmute":
app.win.webContents.executeJavaScript(`MusicKit.getInstance().unmute()`);
response.message = "Unmuted";
break;
case "next":
app.win.webContents.executeJavaScript(`MusicKit.getInstance().skipToNextItem()`);
response.message = "Next";
break;
case "previous":
app.win.webContents.executeJavaScript(`MusicKit.getInstance().skipToPreviousItem()`);
response.message = "Previous";
break;
case "musickit-api":
app.win.webContents.executeJavaScript(`wsapi.musickitApi(\`${data.method}\`, \`${data.id}\`, ${JSON.stringify(data.params)})`);
break;
case "musickit-library-api":
break;
case "set-autoplay":
app.win.webContents.executeJavaScript(`wsapi.setAutoplay(${data.autoplay})`);
break;
case "queue-move":
app.win.webContents.executeJavaScript(`wsapi.moveQueueItem(${data.from},${data.to})`);
break;
case "get-queue":
app.win.webContents.executeJavaScript(`wsapi.getQueue()`);
break;
case "search":
if (!data.limit) {
data.limit = 10;
}
app.win.webContents.executeJavaScript(`wsapi.search(\`${data.term}\`, \`${data.limit}\`)`);
break;
case "library-search":
if (!data.limit) {
data.limit = 10;
}
app.win.webContents.executeJavaScript(`wsapi.searchLibrary(\`${data.term}\`, \`${data.limit}\`)`);
break;
case "show-window":
app.win.show()
break;
case "hide-window":
app.win.hide()
break;
case "play-mediaitem":
app.win.webContents.executeJavaScript(`wsapi.playTrackById(${data.id}, \`${data.kind}\`)`);
response.message = "Playing track";
break;
case "get-status":
response.data = {
isAuthorized: true
};
response.message = "Status";
break;
case "get-currentmediaitem":
app.win.webContents.executeJavaScript(`wsapi.getPlaybackState()`);
break;
}
ws.send(JSON.stringify(response));
});
ws.on('close', function close() {
// remove client from list
wsapi.clients.splice(wsapi.clients.indexOf(ws), 1);
console.log(`Client ${ws.id} disconnected`);
});
ws.send(JSON.stringify(defaultResponse));
});
},
sendToClient(id) {
// replace the clients.forEach with a filter to find the client that requested
},
win: null,
inAppUI() {
// create a browserwindow and load "localhost:8090"
this.win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
});
this.win.loadURL(`http://localhost:${this.webRemotePort}`);
this.win.show()
this.win.on('closed', () => {
this.win = null;
});
},
updatePlaybackState(attr) {
const response = new wsapi.standardResponse(0, attr, "OK", "playbackStateUpdate");
wsapi.clients.forEach(function each(client) {
client.send(JSON.stringify(response));
});
},
returnMusicKitApi(results, method) {
const response = new wsapi.standardResponse(0, results, "OK", `musickitapi.${method}`);
wsapi.clients.forEach(function each(client) {
client.send(JSON.stringify(response));
});
},
returnDynamic(results, type) {
const response = new wsapi.standardResponse(0, results, "OK", type);
wsapi.clients.forEach(function each(client) {
client.send(JSON.stringify(response));
});
},
returnLyrics(results) {
const response = new wsapi.standardResponse(0, results, "OK", "lyrics");
wsapi.clients.forEach(function each(client) {
client.send(JSON.stringify(response));
});
},
returnSearch(results) {
const response = new wsapi.standardResponse(0, results, "OK", "searchResults");
wsapi.clients.forEach(function each(client) {
client.send(JSON.stringify(response));
});
},
returnSearchLibrary(results) {
const response = new wsapi.standardResponse(0, results, "OK", "searchResultsLibrary");
wsapi.clients.forEach(function each(client) {
client.send(JSON.stringify(response));
});
},
returnQueue(queue) {
const response = new wsapi.standardResponse(0, queue, "OK", "queue");
wsapi.clients.forEach(function each(client) {
client.send(JSON.stringify(response));
});
},
webRemotePort: 8090,
async InitWebServer() {
const webRemotePort = await getPort({port : wsapi.webRemotePort});
// Web Remote
// express server that will serve static files in the "../web-remote" folder
const webapp = express();
const webRemotePath = path.join(__dirname, '../web-remote');
webapp.use(express.static(webRemotePath));
webapp.get('/', function (req, res) {
res.sendFile(path.join(webRemotePath, 'index.html'));
});
webapp.listen(webRemotePort, function () {
console.log(`Web Remote listening on port ${webRemotePort}`);
});
}
}
module.exports = wsapi