Compare commits

..

20 Commits
0.1.0 ... 1.0.1

Author SHA1 Message Date
ef37478788 trying to fix the pipeline, have to test on master because apparantly the master branch always tries to release 2020-04-10 11:07:45 +02:00
c411c2cf85 Updated NPM packages to fix CVE-2020-7598 2020-04-10 10:45:24 +02:00
Steffen Sun Lyng
b6185c3e12 Fix/add readme for local snap install (#10)
* Added more explicit installation guide for snap

* changed a few details about snap installation

* Removed unwanted character
2020-04-09 17:02:49 +02:00
Luka Jankovic
c90902e9a9 RPM support and icns icon (#9)
* rpm support and a new icon

* removed unneeded dependency
2020-03-19 21:26:40 +01:00
e72b607f29 updated aur scripts 2020-02-02 10:17:19 +00:00
d05d8f48b6 v1.0 (#6) 2020-02-01 21:14:34 +01:00
Matthieu Le brazidec
30844cb51b Updated the express api to only listen on localhost (#5) 2020-02-01 20:55:05 +01:00
a59cc16f34 Update README.md 2020-01-18 12:10:49 +01:00
52b32c0783 added arch (AUR) PKGBUILD files
Package is visible on: https://aur.archlinux.org/packages/tidal-hifi-git/
2019-12-02 22:55:10 +01:00
José Augusto Bolina
0636c8b92f added individual build scripts. (#1)
* added individual build scripts.

- squashed + changed commit messages.
2019-12-01 23:02:59 +01:00
94ed652619 added a custom menu and enabled the menu by default 2019-11-03 20:22:59 +01:00
f8aa97c15e added settings 2019-11-03 18:52:15 +01:00
6ef4a0854d now using electron-store to save settings between launches 2019-11-03 11:18:01 +01:00
3d334cd19e removed pacman target because github actions' ubuntu vm can't build pacman images. 2019-11-01 22:57:25 +01:00
896ed577f6 installed express and added github (CI) actions 2019-11-01 22:52:08 +01:00
bb49c112db added player status and worked on getting the integrations working with i3: 9714b2fa1d 2019-10-30 23:42:08 +01:00
d7dab07845 added express endpoints, a settings service and a media info service 2019-10-30 22:49:04 +01:00
e5dd8cb87a added temp tray and renamed mediaKeysModule 2019-10-30 21:43:52 +01:00
f389f1d6a2 added new icons 2019-10-30 21:34:26 +01:00
a56d18e414 added media keys + notifications 2019-10-22 21:25:57 +02:00
36 changed files with 2379 additions and 231 deletions

37
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Build CI
on:
push:
branches-ignore:
- master
- develop
jobs:
build_on_linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 12
- run: npm install
- run: npm run build
build_on_mac:
runs-on: macOS-latest
steps:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 12
- run: npm install
- run: npm run build
build_on_win:
runs-on: windows-latest
steps:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 12
- run: npm install
- run: npm run build

49
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,49 @@
name: Release CI
on:
push:
branches:
- master
- develop
jobs:
build_on_linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 12
- run: npm install
- run: npm run build
- uses: actions/upload-artifact@master
with:
name: linux-builds
path: dist/
build_on_mac:
runs-on: macOS-latest
steps:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 12
- run: npm install
- run: npm run build
- uses: actions/upload-artifact@master
with:
name: mac-builds
path: dist/
build_on_win:
runs-on: windows-latest
steps:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 12
- run: npm install
- run: npm run build
- uses: actions/upload-artifact@master
with:
name: windows-builds
path: dist/

7
.gitignore vendored
View File

@@ -1,2 +1,9 @@
node_modules
dist
# ignore all build files except for the .desktop and PKGBUILD files
build/linux/arch/*
!build/linux/arch/PKGBUILD
!build/linux/arch/.SRCINFO
!build/linux/arch/tidal-hifi.desktop
!build/linux/arch/install.sh

View File

@@ -1,17 +1,23 @@
<img src = "./build/icon.png" height="50" style="float:right; margin-top: 29px;" />
# Tidal-hifi
<h1>
Tidal-hifi
<img src = "./build/icon.png" height="40" align="right" />
</h1>
The web version of [listen.tidal.com](listen.tidal.com) running in electron with hifi support thanks to widevine.
![tidal-hifi preview](./docs/preview.png)
## Table of contents
<!-- toc -->
- [Installation](#installation)
- [Using releases](#using-releases)
- [Snap install](#snap-install)
- [Using source](#using-source)
- [Why](#why)
- [features](#features)
- [Integrations](#integrations)
- [Why](#why)
- [Why not extend existing projects?](#why-not-extend-existing-projects)
- [Special thanks to..](#special-thanks-to)
@@ -23,6 +29,28 @@ The web version of [listen.tidal.com](listen.tidal.com) running in electron with
Various packaged versions of the software are available on the [releases](https://github.com/Mastermindzh/tidal-hifi/releases) tab.
#### Snap install
To install with `snap` you need to download the pre-packaged snap-package from this repository, found under releases:
1) Download:
```sh
wget <URI> #for instance: https://github.com/Mastermindzh/tidal-hifi/releases/download/1.0/tidal-hifi_1.0.0_amd64.snap
```
2) Install:
```sh
snap install --dangerous <path> #for instance: tidal-hifi_1.0.0_amd64.snap
```
### Arch Linux
Arch Linux users can use the AUR to install tidal-hifi:
```sh
trizen tidal-hifi
```
### Using source
To install and work with the code on this project follow these steps:
@@ -32,16 +60,28 @@ 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. (`ctrl+/`)
- Tray(/mini) 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)
### Known bugs
- [Last.fm login doesn't work](https://github.com/Mastermindzh/tidal-hifi/issues/4).
## Why
I moved from Spotify over to Tidal and found Linux support to be lacking.
When I started this project there weren't any Linux apps that offered Tidal's "hifi" options nor any scripts to control it.
## Integrations
- [i3 blocks config]() - My dotfiles where I use this app to fetch currently playing music
## Why not extend existing projects?
Whilst there are a handful of projects attempting to run Tidal on Electron they are all unappealing to me because of various reasons:
@@ -49,7 +89,7 @@ Whilst there are a handful of projects attempting to run Tidal on Electron they
- Lack of a maintainers/developers. (no hotfixes, no issues being handled etc)
- Most are simple web wrappers, not my cup of tea.
- Some are DE oriented. I want this to work on WM's too.
- None have widevine working at the moment and that is really the hardest part..
- None have widevine working at the moment
Sometimes it's just easier to start over, cover my own needs and then making it available to the public :)

BIN
assets/TIDAL.icns Executable file

Binary file not shown.

BIN
assets/icon-inverted.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

BIN
assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

View File

@@ -0,0 +1,6 @@
extends: ./build/electron-builder.yml
linux:
category: Audio
icon: ./assets/icon.png
target:
- deb

View File

@@ -0,0 +1,6 @@
extends: ./build/electron-builder.yml
linux:
category: Audio
icon: ./assets/icon.png
target:
- pacman

View File

@@ -0,0 +1,6 @@
extends: ./build/electron-builder.yml
linux:
category: Audio
icon: ./assets/TIDAL.icns
target:
- rpm

View File

@@ -0,0 +1,6 @@
extends: ./build/electron-builder.yml
linux:
category: Audio
icon: ./assets/icon.png
target:
- snap

View File

@@ -1,4 +1,6 @@
appId: com.rickvanlieshout.tidal-hifi
electronDownload:
mirror: https://github.com/castlabs/electron-releases/releases/download/v
snap:
plugs:
- default
@@ -6,9 +8,10 @@ snap:
linux:
category: Audio
target:
- pacman
# - pacman
- tar.gz
- deb
- rpm
- AppImage
- snap
mac:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 102 KiB

20
build/linux/arch/.SRCINFO Normal file
View File

@@ -0,0 +1,20 @@
pkgbase = tidal-hifi-git
pkgdesc = The web version of listen.tidal.com running in electron with hifi support thanks to widevine.
pkgver = 1.0
pkgrel = 2
url = https://github.com/Mastermindzh/tidal-hifi
arch = x86_64
license = custom:MIT
makedepends = npm
makedepends = git
depends = libxss
depends = nss
depends = gtk3
provides = tidal-hifi
source = https://github.com/Mastermindzh/tidal-hifi/archive/1.0.zip
source = tidal-hifi.desktop
sha512sums = 5b3af830c4043f90ebe9b988ee47214e8b21fb26451baad543e9cd1dcba8a7a1a6b5c751212fa6f9edc791ea9c40fb2122c08a1985c7d75152817b27348f1680
sha512sums = fa5fa918ea890baa5f500db3153a6eff3d63966528ffa3349acda3ea02fbecb1ea78a1ba1d23ef7402de2228fc0a483252e0b7e72c73cfb25ed401bedaf856f5
pkgname = tidal-hifi-git

56
build/linux/arch/PKGBUILD Normal file
View File

@@ -0,0 +1,56 @@
# Maintainer: Rick van Lieshout <info@rickvanlieshout.com>
_pkgname=tidal-hifi
pkgname="$_pkgname-git"
pkgver=1.0
pkgrel=2
pkgdesc="The web version of listen.tidal.com running in electron with hifi support thanks to widevine."
arch=("x86_64")
url="https://github.com/Mastermindzh/tidal-hifi"
license=("custom:MIT")
depends=("libxss" "nss" "gtk3")
makedepends=("npm" "git")
provides=("$_pkgname")
source=("https://github.com/Mastermindzh/tidal-hifi/archive/$pkgver.zip"
"${_pkgname}.desktop")
sha512sums=('5b3af830c4043f90ebe9b988ee47214e8b21fb26451baad543e9cd1dcba8a7a1a6b5c751212fa6f9edc791ea9c40fb2122c08a1985c7d75152817b27348f1680'
'fa5fa918ea890baa5f500db3153a6eff3d63966528ffa3349acda3ea02fbecb1ea78a1ba1d23ef7402de2228fc0a483252e0b7e72c73cfb25ed401bedaf856f5')
cdToPkg(){
cd "tidal-hifi-$pkgver"
}
prepare() {
cdToPkg
# install build dependencies
npm install
}
build() {
cdToPkg
# We are not using the systems Electron as we need castlab's Electron.
npx electron-builder --linux dir
}
package() {
cdToPkg
install -d "${pkgdir}/opt/${_pkgname}/" "${pkgdir}/usr/bin" "${pkgdir}/usr/share/doc" "${pkgdir}/usr/share/licenses"
cp -r dist/linux-unpacked/* "${pkgdir}/opt/${_pkgname}/"
chmod +x "${pkgdir}/opt/${_pkgname}/${_pkgname}"
ln -s "/opt/${_pkgname}/${_pkgname}" "${pkgdir}/usr/bin/${_pkgname}"
install -Dm 644 "build/icon.png" "${pkgdir}/usr/share/pixmaps/${_pkgname}.png"
install -Dm 644 "${srcdir}/${_pkgname}.desktop" "${pkgdir}/usr/share/applications/${_pkgname}.desktop"
install -Dm 644 "README.md" "${pkgdir}/usr/share/doc/${pkgname}/README.md"
install -Dm 644 "LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
ln -s "/opt/${_pkgname}/LICENSE.electron.txt" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE.electron.txt"
ln -s "/opt/${_pkgname}/LICENSES.chromium.html" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSES.chromium.html"
}

View File

@@ -0,0 +1,18 @@
#!/bin/bash
# Will generate a correctly formatted SRCINFO file
SCRIPT_DIST=".SRCINFO"
# generate SRCINFO
makepkg --printsrcinfo > $SCRIPT_DIST
# replace pkgbase with tidal-hifi-git
pkgName="tidal-hifi-git"
sed -i "1s/.*/pkgbase = $pkgName/" $SCRIPT_DIST
# replace pkgbase with tidal-hifi-git
sed -i '/^pkgname/ d' $SCRIPT_DIST
echo "pkgname = $pkgName" >> $SCRIPT_DIST
# remove double line breaks and replace with single line breaks
sed -i '/^$/N;/^\n$/D' $SCRIPT_DIST

View File

@@ -0,0 +1,12 @@
[Desktop Entry]
Encoding=UTF-8
Name=tidal-hifi
GenericName=tidal-hifi
Comment=The web version of listen.tidal.com running in electron with hifi support thanks to widevine.
Exec=tidal-hifi %u
Icon=tidal-hifi.png
StartupNotify=true
Terminal=false
Type=Application
Categories=Network;Application;Audio;Video
StartupWMClass=tidal-hifi

BIN
docs/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

1236
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,17 @@
{
"name": "tidal-hifi",
"version": "0.1.0",
"version": "1.0.1",
"description": "Tidal on Electron with widevine(hifi) support",
"main": "src/main.js",
"scripts": {
"start": "electron .",
"build": "electron-builder -c ./build/electron-builder.yml"
"build": "electron-builder --publish=never -c ./build/electron-builder.yml",
"build-deb": "electron-builder --publish=never -c ./build/electron-builder.deb.yml",
"build-rpm": "electron-builder --publish=never -c ./build/electron-builder.rpm.yml",
"build-snap": "electron-builder --publish=never -c ./build/electron-builder.snap.yml",
"build-arch": "npm run build-without-release -c ./build/electron-builder.pacman.yml",
"build-wl": "electron-builder --publish=never -c ./build/electron-builder.yml -wl",
"build-mac": "electron-builder --publish=never -c ./build/electron-builder.yml -m"
},
"keywords": [
"electron",
@@ -16,14 +22,18 @@
"author": "Rick van Lieshout <info@rickvanlieshout.com> (http://rickvanlieshout.com)",
"license": "MIT",
"dependencies": {
"hotkeys-js": "^3.7.1"
"electron-store": "^5.1.1",
"express": "^4.17.1",
"hotkeys-js": "^3.7.6",
"node-notifier": "^6.0.0",
"request": "^2.88.2"
},
"devDependencies": {
"@mastermindzh/prettier-config": "^1.0.0",
"electron": "https://github.com/castlabs/electron-releases#v6.0.12-wvvmp",
"electron": "git+https://github.com/castlabs/electron-releases.git#v6.1.0-wvvmp",
"electron-builder": "^21.2.0",
"electron-reload": "^1.5.0",
"prettier": "^1.18.2"
"prettier": "^2.0.4"
},
"prettier": "@mastermindzh/prettier-config"
}

View File

@@ -0,0 +1,15 @@
const globalEvents = {
play: "play",
pause: "pause",
playPause: "playPause",
next: "next",
previous: "previous",
updateInfo: "update-info",
hideSettings: "hideSettings",
showSettings: "showSettings",
updateStatus: "update-status",
storeChanged: "storeChanged",
error: "error",
};
module.exports = globalEvents;

View File

@@ -0,0 +1,9 @@
const globalEvents = require("./globalEvents");
const mediaKeys = {
MediaPlayPause: globalEvents.playPause,
MediaNextTrack: globalEvents.next,
MediaPreviousTrack: globalEvents.previous,
};
module.exports = mediaKeys;

27
src/constants/settings.js Normal file
View File

@@ -0,0 +1,27 @@
/**
* Object to type my settings file:
*
* notifications: true,
* api: true,
* apiSettings: {
* port: 47836,
* },
* windowBounds: { width: 800, height: 600 },
*/
const settings = {
notifications: "notifications",
api: "api",
menuBar: "menuBar",
playBackControl: "playBackControl",
apiSettings: {
root: "apiSettings",
port: "apiSettings.port",
},
windowBounds: {
root: "windowBounds",
width: "windowBounds.width",
height: "windowBounds.height",
},
};
module.exports = settings;

View File

@@ -0,0 +1,4 @@
module.exports = {
playing: "playing",
paused: "paused",
};

View File

@@ -1,7 +1,24 @@
const { app, BrowserWindow } = require("electron");
const { app, BrowserWindow, globalShortcut, ipcMain } = require("electron");
const {
settings,
store,
createSettingsWindow,
showSettingsWindow,
closeSettingsWindow,
hideSettingsWindow,
} = require("./scripts/settings");
const { addTray, refreshTray } = require("./scripts/tray");
const { addMenu } = require("./scripts/menu");
const path = require("path");
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");
/**
* Enable live reload in development builds
@@ -19,15 +36,18 @@ function createWindow(options = {}) {
y: options.y,
width: 1024,
height: 800,
icon,
tray: true,
backgroundColor: options.backgroundColor,
webPreferences: {
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);
@@ -37,24 +57,35 @@ 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();
store.set(settings.windowBounds.root, { width, height });
});
}
function addGlobalShortcuts() {
// globalShortcut.register("Control+A", () => {
// dialog.showErrorBox("test", "test");
// // mainWindow.webContents.send("getPlayInfo");
// });
Object.keys(mediaKeys).forEach((key) => {
globalShortcut.register(`${key}`, () => {
mainWindow.webContents.send("globalEvent", `${mediaKeys[key]}`);
});
});
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on("ready", () => {
// window with white backround
createWindow();
addMenu();
createSettingsWindow();
addGlobalShortcuts();
addTray({ icon });
refreshTray();
store.get(settings.api) && expressModule.run(mainWindow);
});
app.on("activate", function() {
@@ -64,3 +95,27 @@ app.on("activate", function() {
createWindow();
}
});
// IPC
ipcMain.on(globalEvents.updateInfo, (event, arg) => {
mediaInfoModule.update(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);
});

BIN
src/pages/settings/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

View File

@@ -0,0 +1,86 @@
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);
}
/**
* Open an url in the default browsers
*/
window.openExternal = function(url) {
const { shell } = require("electron");
shell.openExternal(url);
};
/**
* 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();
});
ipcRenderer.on("goToTab", (event, tab) => {
document.getElementById(tab).click();
});
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);
});

View File

@@ -0,0 +1,400 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
</head>
<body>
<div class="header">
<h1 class="title">Settings</h1>
<a href="javascript:hide();" class="exitWindow">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="18px" height="18px" viewBox="0 0 348.333 348.334">
<g>
<path fill="white" d="M336.559,68.611L231.016,174.165l105.543,105.549c15.699,15.705,15.699,41.145,0,56.85
c-7.844,7.844-18.128,11.769-28.407,11.769c-10.296,0-20.581-3.919-28.419-11.769L174.167,231.003L68.609,336.563
c-7.843,7.844-18.128,11.769-28.416,11.769c-10.285,0-20.563-3.919-28.413-11.769c-15.699-15.698-15.699-41.139,0-56.85
l105.54-105.549L11.774,68.611c-15.699-15.699-15.699-41.145,0-56.844c15.696-15.687,41.127-15.687,56.829,0l105.563,105.554
L279.721,11.767c15.705-15.687,41.139-15.687,56.832,0C352.258,27.466,352.258,52.912,336.559,68.611z" />
</svg>
</a>
</div>
<div class="body">
<div class="tabset">
<!-- Tab 1 -->
<input type="radio" name="tabset" id="tab1" checked />
<label for="tab1">General</label>
<!-- Tab 2 -->
<input type="radio" name="tabset" id="tab2" />
<label for="tab2">Api</label>
<!-- Tab 3 -->
<input type="radio" name="tabset" id="tab3" />
<label for="tab3">About</label>
<div class="tab-panels">
<section id="general" class="tab-panel">
<div class="section">
<h3>Playback</h3>
<div class="option">
<h4>Notifications</h4>
<p>
Whether to show a notification when a new song starts.
</p>
<label class="switch">
<input id="notifications" type="checkbox">
<span class="slider round"></span>
</label>
</div>
</div>
<div class="section">
<h3>UI</h3>
<div class="option">
<h4>Menubar</h4>
<p>
Show Tidal-hifi's menu bar
</p>
<label class="switch">
<input id="menuBar" type="checkbox">
<span class="slider round"></span>
</label>
</div>
</div>
</section>
<section id="api" class="tab-panel">
<div class="section">
<h3>Api</h3>
<p style="margin-bottom: 15px;">
Tidal-hifi has a web api built in to allow users to get current song information. You can optionally enable playback control as well.
<br />
<br />
<small>* api changes require a restart to update</small>
</p>
<div class="option">
<h4>Web API</h4>
<p>
Whether to enable the Tidal-hifi web api
</p>
<label class="switch">
<input id="apiCheckbox" type="checkbox">
<span class="slider round"></span>
</label>
</div>
<div class="option">
<h4 style="margin-bottom: 5px;">API port</h4>
<input id="port" type="text" class="freeTextInput" name="port">
</div>
<div class="option">
<h4>Playback control</h4>
<p>
Whether to enable playback control from the api
</p>
<label class="switch">
<input id="playBackControl" type="checkbox">
<span class="slider round"></span>
</label>
</div>
</div>
<button onClick="restart()">Restart Tidal-hifi</button>
</section>
<section id="general" class="tab-panel">
<div class="section">
<img style="width: 100px; height: auto; display: block; margin: 0 auto; margin-bottom: 20px; margin-top: 20px;" src = "./icon.png">
<p style="max-width: 350px; display:block; margin: 0 auto; text-align: center;">
<a href ="javascript:openExternal('https://github.com/Mastermindzh/tidal-hifi');">Tidal-hifi</a> is made by <a href ="javascript:openExternal('https://www.rickvanlieshout.com')">Rick van Lieshout</a>.<br />
It uses <a href="javascript:openExternal('https://castlabs.com/');">castlabs</a> versions of Electron for widevine support.
</p>
</div>
</section>
</div>
</div>
</div>
</body>
<style>
.header {
-webkit-user-select: none;
-webkit-app-region: drag;
}
.header a {
-webkit-app-region: no-drag;
}
* {
margin: 0%;
padding: 0%;
color: #ffffff;
font-weight: 400;
font-stretch: normal;
-webkit-font-smoothing: antialiased;
font-family: nationale, nationale-regular, Helvetica, sans-serif;
}
html,
body {
height: 100%;
background-color: black;
display: flex;
flex-direction: column;
}
h2 {
font-size: 1.2rem;
}
small {
font-style: italic;
color: #72777f;
}
.header {
background-color: #242528;
border-bottom: 1px solid #5a5a5a;
height: 50px;
}
.title {
float: left;
line-height: 50px;
margin-left: 15px;
}
.accent {
color: #0ff;
}
.exitWindow {
border: none;
text-decoration: none;
font-size: 1.4rem;
float: right;
margin-right: 15px;
height: 50px;
line-height: 50px;
}
.exitWindow svg {
height: 50px;
color: white;
}
.section {
padding-top: 10px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(246, 245, 255, .1);
}
.section .option {
margin-bottom: 15px;
}
.section .option p {
max-width: 75%;
float: left
}
.section .option label {
float: right;
}
.section:after,
.section .option:after {
content: "";
display: table;
clear: both;
}
.section h3 {
margin-bottom: 15px;
}
.section h4 {
font-size: 0.9rem;
}
.section p {
color: #72777f;
}
.bottom-border {
border-bottom: 1px solid #0ff;
}
.body {
padding: 15px;
flex: 1 1 auto;
position: relative;
overflow-y: auto;
}
.body::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
border-radius: 5px;
background-color: 2a2a2a;
}
.body::-webkit-scrollbar {
width: 10px;
background-color: #2a2a2a;
}
.body::-webkit-scrollbar-thumb {
border-radius: 10px;
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3);
box-shadow: inset 0 0 6px rgba(0, 0, 0, .3);
background-color: #5a5a5a;
}
/* Tabs */
.tabset > input[type="radio"] {
position: absolute;
left: -200vw;
}
.tabset .tab-panel {
display: none;
}
.tabset > input:first-child:checked ~ .tab-panels > .tab-panel:first-child,
.tabset > input:nth-child(3):checked ~ .tab-panels > .tab-panel:nth-child(2),
.tabset > input:nth-child(5):checked ~ .tab-panels > .tab-panel:nth-child(3) {
display: block;
}
.tabset > label {
position: relative;
display: inline-block;
padding: 15px 0px 10px;
border-bottom: 0;
cursor: pointer;
}
.tabset > input + label {
color: #e0e0e0;
margin-right: 30px;
}
.tabset > input:checked + label {
color: #0ff;
border-bottom: 2px solid #0ff;
}
.tab-panel {
padding: 10px 0;
}
/* switches */
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 28px;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(246, 245, 255, .1);
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 24px;
width: 24px;
left: 2px;
bottom: 2px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #0ff;
}
input:focus + .slider {
box-shadow: 0 0 1px #0ff;
}
input:checked + .slider:before {
-webkit-transform: translateX(22px);
-ms-transform: translateX(22px);
transform: translateX(22px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
/* input field */
input {
background: transparent;
border: 0;
border-bottom: 1px solid rgba(246, 245, 255, .1);
color: rgba(229, 238, 255, .6);
width: 100%;
display: block;
padding: 0 0 12px;
}
.freeTextInput:focus {
outline: none;
border-bottom: 1px solid #0ff;
}
/* buttons */
button{
border:none;
background:none;
align-items: center;
background-color: rgba(229,238,255,.2);
display: inline-flex;
justify-content: center;
border-radius: 12px;
height: 48px;
line-height: 49px;
padding: 0 24px;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: background .35s ease;
min-height: 0;
min-width: 0;
font-size: 1.14286rem;
font-family: nationale,nationale-regular,Helvetica,sans-serif;
margin-top: 10px;
cursor: pointer;
}
button:hover{
background-color: rgba(229,238,255,.3);
}
</style>
</html>

View File

@@ -1,6 +1,15 @@
const { setTitle, getTitle } = require("./scripts/window-functions");
const { dialog } = require("electron").remote;
const { store, settings } = require("./scripts/settings");
const { ipcRenderer } = require("electron");
const { app } = require("electron").remote;
const { downloadFile } = require("./scripts/download");
const statuses = require("./constants/statuses");
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"]',
@@ -18,19 +27,55 @@ const elements = {
block: '[class="blockButton"]',
account: '*[data-test^="profile-image-button"]',
settings: '*[data-test^="open-settings"]',
media: '*[data-test="current-media-imagery"]',
image: '*[class^="image--"]',
/**
* Get an element from the dom
* @param {*} key key in elements object to fetch
*/
get: function(key) {
return window.document.querySelector(this[key.toLowerCase()]);
},
getText: function(key) {
return this.get(key).textContent;
/**
* Get the icon of the current song
*/
getSongIcon: function() {
const figure = this.get("media");
if (figure) {
const mediaElement = figure.querySelector(this["image"]);
if (mediaElement) {
return mediaElement.src;
}
}
return "";
},
/**
* Shorthand function to get the text of a dom element
* @param {*} key key in elements object to fetch
*/
getText: function(key) {
const element = this.get(key);
return element ? element.textContent : "";
},
/**
* Shorthand function to click a dom element
* @param {*} key key in elements object to fetch
*/
click: function(key) {
this.get(key).click();
return this;
},
/**
* Shorthand function to focus a dom element
* @param {*} key key in elements object to fetch
*/
focus: function(key) {
return this.get(key).focus();
},
@@ -102,6 +147,10 @@ function addHotKeys() {
hotkeys.add("control+r", function() {
elements.click("repeat");
});
hotkeys.add("control+/", function() {
ipcRenderer.send(globalEvents.showSettings);
});
}
/**
@@ -141,25 +190,86 @@ function handleLogout() {
*/
function addIPCEventListeners() {
window.addEventListener("DOMContentLoaded", () => {
const { ipcRenderer } = require("electron");
ipcRenderer.on("getPlayInfo", (event, col) => {
alert(`${elements.getText("title")} - ${elements.getText("artists")}`);
ipcRenderer.on("globalEvent", (event, args) => {
switch (args) {
case globalEvents.playPause:
playPause();
break;
case globalEvents.next:
elements.click("next");
break;
case globalEvents.previous:
elements.click("previous");
break;
case globalEvents.play:
elements.click("play");
break;
case globalEvents.pause:
elements.click("pause");
break;
}
});
});
}
/**
* Update window title
* Update the current status of tidal (e.g playing or paused)
*/
function updateStatus() {
const play = elements.get("play");
let status = statuses.paused;
// if play button is NOT visible tidal is playing
if (!play) {
status = statuses.playing;
}
ipcRenderer.send(globalEvents.updateStatus, status);
}
/**
* Watch for song changes and update title + notify
*/
setInterval(function() {
const title = elements.getText("title");
const artists = elements.getText("artists");
const songDashArtistTitle = `${title} - ${artists}`;
updateStatus();
if (getTitle() !== songDashArtistTitle) {
setTitle(songDashArtistTitle);
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(globalEvents.updateInfo, options);
store.get(settings.notifications) && notifier.notify(options);
},
() => {}
);
}
}
}, 1000);
}, 200);
addHotKeys();
addIPCEventListeners();

26
src/scripts/download.js Normal file
View File

@@ -0,0 +1,26 @@
const download = {};
const request = require("request");
const fs = require("fs");
/**
* download and save a file
* @param {*} fileUrl url to download
* @param {*} targetPath path to save it at
*/
download.downloadFile = function(fileUrl, targetPath) {
return new Promise((resolve, reject) => {
var req = request({
method: "GET",
uri: fileUrl,
});
var out = fs.createWriteStream(targetPath);
req.pipe(out);
req.on("end", resolve);
req.on("error", reject);
});
};
module.exports = download;

70
src/scripts/express.js Normal file
View File

@@ -0,0 +1,70 @@
const express = require("express");
const { mediaInfo } = require("./mediaInfo");
const { store, settings } = require("./settings");
const globalEvents = require("./../constants/globalEvents");
const statuses = require("./../constants/statuses");
const expressModule = {};
const fs = require("fs");
let expressInstance;
/**
* Function to enable tidal-hifi's express api
*/
expressModule.run = function(mainWindow) {
/**
* Shorthand to handle a fire and forget global event
* @param {*} res
* @param {*} action
*/
function handleGlobalEvent(res, action) {
mainWindow.webContents.send("globalEvent", action);
res.sendStatus(200);
}
const expressApp = express();
expressApp.get("/", (req, res) => res.send("Hello World!"));
expressApp.get("/current", (req, res) => res.json(mediaInfo));
expressApp.get("/image", (req, res) => {
var stream = fs.createReadStream(mediaInfo.icon);
stream.on("open", function() {
res.set("Content-Type", "image/png");
stream.pipe(res);
});
stream.on("error", function() {
res.set("Content-Type", "text/plain");
res.status(404).end("Not found");
});
});
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, "127.0.0.1", () => {});
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;

40
src/scripts/mediaInfo.js Normal file
View File

@@ -0,0 +1,40 @@
const statuses = require("./../constants/statuses");
const mediaInfo = {
title: "",
artist: "",
icon: "",
status: statuses.paused,
};
const mediaInfoModule = {
mediaInfo,
};
/**
* Update artist and song info in the mediaInfo constant
*/
mediaInfoModule.update = function(arg) {
mediaInfo.title = propOrDefault(arg.title);
mediaInfo.artist = propOrDefault(arg.message);
mediaInfo.icon = propOrDefault(arg.icon);
};
/**
* Update tidal's status in the mediaInfo constant
*/
mediaInfoModule.updateStatus = function(status) {
if (Object.values(statuses).includes(status)) {
mediaInfo.status = status;
}
};
/**
* Return the property or a default value
* @param {*} prop property to check
* @param {*} defaultValue defaults to ""
*/
function propOrDefault(prop, defaultValue = "") {
return prop ? prop : defaultValue;
}
module.exports = mediaInfoModule;

107
src/scripts/menu.js Normal file
View File

@@ -0,0 +1,107 @@
const { Menu } = require("electron");
const { showSettingsWindow } = require("./settings");
const isMac = process.platform === "darwin";
const settingsMenuEntry = {
label: "Settings",
click() {
showSettingsWindow();
},
accelerator: "Control+/",
};
const mainMenu = [
...(isMac
? [
{
label: app.name,
submenu: [
{ role: "about" },
settingsMenuEntry,
{ type: "separator" },
{ role: "services" },
{ type: "separator" },
{ role: "hide" },
{ role: "hideothers" },
{ role: "unhide" },
{ type: "separator" },
{ role: "quit" },
],
},
]
: []),
// { role: 'fileMenu' }
{
label: "File",
submenu: [settingsMenuEntry, isMac ? { role: "close" } : { role: "quit" }],
},
// { role: 'editMenu' }
{
label: "Edit",
submenu: [
{ role: "undo" },
{ role: "redo" },
{ type: "separator" },
{ role: "cut" },
{ role: "copy" },
{ role: "paste" },
...(isMac
? [
{ role: "pasteAndMatchStyle" },
{ role: "delete" },
{ role: "selectAll" },
{ type: "separator" },
{
label: "Speech",
submenu: [{ role: "startspeaking" }, { role: "stopspeaking" }],
},
]
: [{ role: "delete" }, { type: "separator" }, { role: "selectAll" }]),
{ type: "separator" },
settingsMenuEntry,
],
},
// { role: 'viewMenu' }
{
label: "View",
submenu: [
{ role: "reload" },
{ role: "forcereload" },
{ type: "separator" },
{ role: "resetzoom" },
{ role: "zoomin" },
{ role: "zoomout" },
{ type: "separator" },
{ role: "togglefullscreen" },
],
},
// { role: 'windowMenu' }
{
label: "Window",
submenu: [
{ role: "minimize" },
...(isMac
? [{ type: "separator" }, { role: "front" }, { type: "separator" }, { role: "window" }]
: [{ role: "close" }]),
],
},
settingsMenuEntry,
{
label: "About",
click() {
showSettingsWindow("tab3");
},
},
];
const menuModule = { mainMenu };
menuModule.getMenu = function() {
return Menu.buildFromTemplate(mainMenu);
};
menuModule.addMenu = function() {
Menu.setApplicationMenu(menuModule.getMenu());
};
module.exports = menuModule;

70
src/scripts/settings.js Normal file
View File

@@ -0,0 +1,70 @@
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: true,
apiSettings: {
port: 47836,
},
windowBounds: { width: 800, height: 600 },
},
});
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(tab = "tab1") {
settingsWindow.webContents.send("goToTab", tab);
// 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;

19
src/scripts/tray.js Normal file
View File

@@ -0,0 +1,19 @@
const { Tray } = require("electron");
const { getMenu } = require("./menu");
const trayModule = {};
let tray;
trayModule.addTray = function(options = { icon: "" }) {
tray = new Tray(options.icon);
};
trayModule.refreshTray = function() {
tray.on("click", function(e) {
// do nothing on click
});
tray.setToolTip("Tidal-hifi");
tray.setContextMenu(getMenu());
};
module.exports = trayModule;