diff --git a/README.md b/README.md index 9fb8d9f..263932d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

Tidal-hifi - +

The web version of [listen.tidal.com](listen.tidal.com) running in electron with hifi support thanks to widevine. @@ -14,6 +14,7 @@ The web version of [listen.tidal.com](listen.tidal.com) running in electron with - [Installation](#installation) - [Using releases](#using-releases) - [Using source](#using-source) +- [features](#features) - [Integrations](#integrations) - [Why](#why) - [Why not extend existing projects?](#why-not-extend-existing-projects) @@ -36,6 +37,15 @@ To install and work with the code on this project follow these steps: - npm install - npm start +## features + +- HiFi playback +- Notifications +- Shortcuts ([source](https://defkey.com/tidal-desktop-shortcuts)) +- API for status and playback +- [Settings feature](./docs/settings.png) to disable certain functionality. +- Tray player (coming soon) + ## Integrations - [i3 blocks config](https://github.com/Mastermindzh/dotfiles/commit/9714b2fa1d670108ce811d5511fd3b7a43180647) - My dotfiles where I use this app to fetch currently playing music (direct commit) diff --git a/docs/settings.png b/docs/settings.png new file mode 100644 index 0000000..ced236c Binary files /dev/null and b/docs/settings.png differ diff --git a/src/constants/globalEvents.js b/src/constants/globalEvents.js index f106b4c..191fd12 100644 --- a/src/constants/globalEvents.js +++ b/src/constants/globalEvents.js @@ -4,6 +4,12 @@ const globalEvents = { playPause: "playPause", next: "next", previous: "previous", + updateInfo: "update-info", + hideSettings: "hideSettings", + showSettings: "showSettings", + updateStatus: "update-status", + storeChanged: "storeChanged", + error: "error", }; module.exports = globalEvents; diff --git a/src/constants/settings.js b/src/constants/settings.js index 524e9bc..8a519c0 100644 --- a/src/constants/settings.js +++ b/src/constants/settings.js @@ -11,6 +11,8 @@ const settings = { notifications: "notifications", api: "api", + menuBar: "menuBar", + playBackControl: "playBackControl", apiSettings: { root: "apiSettings", port: "apiSettings.port", diff --git a/src/main.js b/src/main.js index 574304e..ff162f1 100644 --- a/src/main.js +++ b/src/main.js @@ -1,5 +1,12 @@ const { app, BrowserWindow, globalShortcut, ipcMain } = require("electron"); -const { settings, store } = require("./scripts/settings"); +const { + settings, + store, + createSettingsWindow, + showSettingsWindow, + closeSettingsWindow, + hideSettingsWindow, +} = require("./scripts/settings"); const { addTray, refreshTray } = require("./scripts/tray"); const path = require("path"); @@ -7,6 +14,7 @@ const tidalUrl = "https://listen.tidal.com"; const expressModule = require("./scripts/express"); const mediaKeys = require("./constants/mediaKeys"); const mediaInfoModule = require("./scripts/mediaInfo"); +const globalEvents = require("./constants/globalEvents"); let mainWindow; let icon = path.join(__dirname, "../assets/icon.png"); @@ -34,10 +42,11 @@ function createWindow(options = {}) { affinity: "window", preload: path.join(__dirname, "preload.js"), plugins: true, + devTools: !app.isPackaged, }, }); - mainWindow.setMenuBarVisibility(false); + mainWindow.setMenuBarVisibility(store.get(settings.menuBar)); // load the Tidal website mainWindow.loadURL(tidalUrl); @@ -47,7 +56,8 @@ function createWindow(options = {}) { // Emitted when the window is closed. mainWindow.on("closed", function() { - mainWindow = null; + closeSettingsWindow(); + app.quit(); }); mainWindow.on("resize", () => { let { width, height } = mainWindow.getBounds(); @@ -69,6 +79,7 @@ function addGlobalShortcuts() { // Some APIs can only be used after this event occurs. app.on("ready", () => { createWindow(); + createSettingsWindow(); addGlobalShortcuts(); addTray({ icon }); refreshTray(); @@ -85,10 +96,24 @@ app.on("activate", function() { // IPC -ipcMain.on("update-info", (event, arg) => { +ipcMain.on(globalEvents.updateInfo, (event, arg) => { mediaInfoModule.update(arg); }); -ipcMain.on("update-status", (event, arg) => { +ipcMain.on(globalEvents.hideSettings, (event, arg) => { + hideSettingsWindow(); +}); +ipcMain.on(globalEvents.showSettings, (event, arg) => { + showSettingsWindow(); +}); + +ipcMain.on(globalEvents.updateStatus, (event, arg) => { mediaInfoModule.updateStatus(arg); }); +ipcMain.on(globalEvents.storeChanged, (event, arg) => { + mainWindow.setMenuBarVisibility(store.get(settings.menuBar)); +}); + +ipcMain.on(globalEvents.error, (event, arg) => { + console.log(event); +}); diff --git a/src/pages/settings/preload.js b/src/pages/settings/preload.js new file mode 100644 index 0000000..35be590 --- /dev/null +++ b/src/pages/settings/preload.js @@ -0,0 +1,74 @@ +let notifications; +let playBackControl; +let api; +let port; +let menuBar; + +const { store, settings } = require("./../../scripts/settings"); +const { ipcRenderer } = require("electron"); +const globalEvents = require("./../../constants/globalEvents"); + +/** + * Sync the UI forms with the current settings + */ +function refreshSettings() { + notifications.checked = store.get(settings.notifications); + playBackControl.checked = store.get(settings.playBackControl); + api.checked = store.get(settings.api); + port.value = store.get(settings.apiSettings.port); + menuBar.checked = store.get(settings.menuBar); +} + +/** + * hide the settings window + */ +window.hide = function() { + ipcRenderer.send(globalEvents.hideSettings); +}; + +/** + * Restart tidal-hifi after changes + */ +window.restart = function() { + const remote = require("electron").remote; + remote.app.relaunch(); + remote.app.exit(0); +}; + +/** + * Bind UI components to functions after DOMContentLoaded + */ +window.addEventListener("DOMContentLoaded", () => { + function get(id) { + return document.getElementById(id); + } + + function addInputListener(source, key) { + source.addEventListener("input", function(event, data) { + if (this.value === "on") { + store.set(key, source.checked); + } else { + store.set(key, this.value); + } + ipcRenderer.send(globalEvents.storeChanged); + }); + } + + ipcRenderer.on("refreshData", () => { + refreshSettings(); + }); + + notifications = get("notifications"); + playBackControl = get("playBackControl"); + api = get("apiCheckbox"); + port = get("port"); + menuBar = get("menuBar"); + + refreshSettings(); + + addInputListener(notifications, settings.notifications); + addInputListener(playBackControl, settings.playBackControl); + addInputListener(api, settings.api); + addInputListener(port, settings.apiSettings.port); + addInputListener(menuBar, settings.menuBar); +}); diff --git a/src/pages/settings/settings.html b/src/pages/settings/settings.html new file mode 100644 index 0000000..4a668a7 --- /dev/null +++ b/src/pages/settings/settings.html @@ -0,0 +1,387 @@ + + + + + + + + + + +
+

Settings

+ + + + + + + + +
+
+
+ + + + + + + +
+
+
+

Playback

+
+

Notifications

+

+ Whether to show a notification when a new song starts. +

+ +
+
+
+

UI

+
+

Menubar

+

+ Show Tidal-hifi's menu bar +

+ +
+
+
+
+
+

Api

+

+ Tidal-hifi has a web api built in to allow users to get current song information. You can optionally enable playback control as well. +
+
+ * api changes require a restart to update +

+ +
+

Web API

+

+ Whether to enable the Tidal-hifi web api +

+ +
+
+

API port

+ +
+
+

Playback control

+

+ Whether to enable playback control from the api +

+ +
+
+ +
+ +
+
+
+ + + + + diff --git a/src/preload.js b/src/preload.js index ca78477..2fb6b47 100644 --- a/src/preload.js +++ b/src/preload.js @@ -9,6 +9,7 @@ const hotkeys = require("./scripts/hotkeys"); const globalEvents = require("./constants/globalEvents"); const notifier = require("node-notifier"); const notificationPath = `${app.getPath("userData")}/notification.jpg`; +let currentSong = ""; const elements = { play: '*[data-test="play"]', @@ -146,6 +147,10 @@ function addHotKeys() { hotkeys.add("control+r", function() { elements.click("repeat"); }); + + hotkeys.add("control+/", function() { + ipcRenderer.send(globalEvents.showSettings); + }); } /** @@ -185,10 +190,6 @@ function handleLogout() { */ function addIPCEventListeners() { window.addEventListener("DOMContentLoaded", () => { - ipcRenderer.on("getPlayInfo", () => { - alert(`${elements.getText("title")} - ${elements.getText("artists")}`); - }); - ipcRenderer.on("globalEvent", (event, args) => { switch (args) { case globalEvents.playPause: @@ -221,7 +222,7 @@ function updateStatus() { if (!play) { status = statuses.playing; } - ipcRenderer.send("update-status", status); + ipcRenderer.send(globalEvents.updateStatus, status); } /** @@ -237,33 +238,36 @@ setInterval(function() { if (getTitle() !== songDashArtistTitle) { setTitle(songDashArtistTitle); - const image = elements.getSongIcon(); + if (currentSong !== songDashArtistTitle) { + currentSong = songDashArtistTitle; + const image = elements.getSongIcon(); - const options = { - title, - message: artists, - }; - new Promise((resolve, reject) => { - if (image.startsWith("http")) { - downloadFile(image, notificationPath).then( - () => { - options.icon = notificationPath; - resolve(); - }, - () => { - reject(); - } - ); - } else { - reject(); - } - }).then( - () => { - ipcRenderer.send("update-info", options); - store.get(settings.notifications) && notifier.notify(options); - }, - () => {} - ); + const options = { + title, + message: artists, + }; + new Promise((resolve, reject) => { + if (image.startsWith("http")) { + downloadFile(image, notificationPath).then( + () => { + options.icon = notificationPath; + resolve(); + }, + () => { + reject(); + } + ); + } else { + reject(); + } + }).then( + () => { + ipcRenderer.send(globalEvents.updateInfo, options); + store.get(settings.notifications) && notifier.notify(options); + }, + () => {} + ); + } } }, 200); diff --git a/src/scripts/express.js b/src/scripts/express.js index 1bf0b46..b0a51d0 100644 --- a/src/scripts/express.js +++ b/src/scripts/express.js @@ -3,9 +3,10 @@ const { mediaInfo } = require("./mediaInfo"); const { store, settings } = require("./settings"); const globalEvents = require("./../constants/globalEvents"); const statuses = require("./../constants/statuses"); - const expressModule = {}; -var fs = require("fs"); +const fs = require("fs"); + +let expressInstance; /** * Function to enable tidal-hifi's express api @@ -24,17 +25,6 @@ expressModule.run = function(mainWindow) { const expressApp = express(); expressApp.get("/", (req, res) => res.send("Hello World!")); expressApp.get("/current", (req, res) => res.json(mediaInfo)); - expressApp.get("/play", (req, res) => handleGlobalEvent(res, globalEvents.play)); - expressApp.get("/pause", (req, res) => handleGlobalEvent(res, globalEvents.pause)); - expressApp.get("/next", (req, res) => handleGlobalEvent(res, globalEvents.next)); - expressApp.get("/previous", (req, res) => handleGlobalEvent(res, globalEvents.previous)); - expressApp.get("/playpause", (req, res) => { - if (mediaInfo.status == statuses.playing) { - handleGlobalEvent(res, globalEvents.pause); - } else { - handleGlobalEvent(res, globalEvents.play); - } - }); expressApp.get("/image", (req, res) => { var stream = fs.createReadStream(mediaInfo.icon); stream.on("open", function() { @@ -47,7 +37,34 @@ expressModule.run = function(mainWindow) { }); }); - expressApp.listen(store.get(settings.apiSettings.port), () => {}); + if (store.get(settings.playBackControl)) { + expressApp.get("/play", (req, res) => handleGlobalEvent(res, globalEvents.play)); + expressApp.get("/pause", (req, res) => handleGlobalEvent(res, globalEvents.pause)); + expressApp.get("/next", (req, res) => handleGlobalEvent(res, globalEvents.next)); + expressApp.get("/previous", (req, res) => handleGlobalEvent(res, globalEvents.previous)); + expressApp.get("/playpause", (req, res) => { + if (mediaInfo.status == statuses.playing) { + handleGlobalEvent(res, globalEvents.pause); + } else { + handleGlobalEvent(res, globalEvents.play); + } + }); + } + if (store.get(settings.api)) { + let port = store.get(settings.apiSettings.port); + + expressInstance = expressApp.listen(port, () => {}); + expressInstance.on("error", function(e) { + let message = e.code; + if (e.code === "EADDRINUSE") { + message = `Port ${port} in use.`; + } + const { dialog } = require("electron"); + dialog.showErrorBox("Api failed to start.", message); + }); + } else { + expressInstance = undefined; + } }; module.exports = expressModule; diff --git a/src/scripts/settings.js b/src/scripts/settings.js index a4ba748..5484fc1 100644 --- a/src/scripts/settings.js +++ b/src/scripts/settings.js @@ -1,13 +1,20 @@ const Store = require("electron-store"); const settings = require("./../constants/settings"); +const path = require("path"); +const { BrowserWindow } = require("electron"); + +let settingsWindow; const store = new Store({ defaults: { notifications: true, api: true, + playBackControl: true, + menuBar: false, apiSettings: { port: 47836, }, + windowBounds: { width: 800, height: 600 }, }, }); @@ -15,6 +22,47 @@ const store = new Store({ const settingsModule = { store, settings, + settingsWindow, +}; + +settingsModule.createSettingsWindow = function() { + settingsWindow = new BrowserWindow({ + width: 500, + height: 600, + show: false, + frame: false, + title: "Tidal-hifi - settings", + webPreferences: { + affinity: "window", + preload: path.join(__dirname, "../pages/settings/preload.js"), + plugins: true, + nodeIntegration: true, + }, + }); + + settingsWindow.on("close", (event) => { + if (settingsWindow != null) { + event.preventDefault(); + settingsWindow.hide(); + } + }); + + settingsWindow.loadURL(`file://${__dirname}/../pages/settings/settings.html`); + + settingsModule.settingsWindow = settingsWindow; +}; + +settingsModule.showSettingsWindow = function() { + // refresh data just before showing the window + settingsWindow.webContents.send("refreshData"); + settingsWindow.show(); +}; +settingsModule.hideSettingsWindow = function() { + settingsWindow.hide(); +}; + +settingsModule.closeSettingsWindow = function() { + settingsWindow = null; }; module.exports = settingsModule;