Compare commits

..

41 Commits
0.1.0 ... 2.1.1

Author SHA1 Message Date
148d1746ad Merge pull request #42 from Mastermindzh/feature/improving-discord-integration-with-maroxy
Discord improvements with Mar0xy
2021-04-19 23:18:41 +02:00
ae51f9610c - The discord integration now doesn't send an update every 15 seconds it sends an update whenever the media info changes
- consolidated updating the media info changes with the status changes into a single global event
2021-04-19 23:08:19 +02:00
5eb3b8d95f released version 2.1.0 on the AUR version as well. 2021-04-19 21:31:12 +02:00
ebdae6bc88 bumped a few packages and added a changelog 2021-04-19 20:59:37 +02:00
Marie
ab25bf16b2 Add Discord RPC & fix artist in mediainfo (#40) 2021-04-19 20:43:25 +02:00
31670d0c2b updated aur info 2021-01-10 14:11:45 +01:00
9ca3d3b37d added some extra images, updated the TOC and bumped the ini dependency 2021-01-10 13:53:34 +01:00
fb9082e995 Changes some hotkeys around and fixed some bugs with the hotkeys. Fixes #34 and #35 2021-01-10 13:52:22 +01:00
87a4ff3fc5 Merge pull request #32 from Mastermindzh/feature/mpris-403-fix 2020-11-29 12:32:47 +01:00
1e5b7d61f5 added changelog etc 2020-11-29 12:31:58 +01:00
8177e6e3ca re-enabled mpris after the 403 has been fixed 2020-11-29 10:55:01 +01:00
9f26db22fc updated arch install files. fixes #27 2020-10-07 20:33:39 +02:00
e2ea4d13c4 added the option to disable the system tray icon and fixed the settings menu 2020-10-07 20:10:31 +02:00
c222113cf1 updated arch builds 2020-10-05 10:50:15 +02:00
bdab6c3a17 added homepage attribute for electron-builder.. 2020-10-05 10:38:34 +02:00
813beec863 having pkgbuild run the internal electron-builder 2020-10-05 10:17:27 +02:00
591d42b31a updated arch info 2020-10-04 12:00:19 +02:00
beacedd64b Develop (#25) 2020-10-04 11:52:08 +02:00
dependabot[bot]
e13af7a2d5 Bump yargs-parser from 13.1.1 to 13.1.2 (#21)
Bumps [yargs-parser](https://github.com/yargs/yargs-parser) from 13.1.1 to 13.1.2.
- [Release notes](https://github.com/yargs/yargs-parser/releases)
- [Changelog](https://github.com/yargs/yargs-parser/blob/master/docs/CHANGELOG-full.md)
- [Commits](https://github.com/yargs/yargs-parser/commits)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-09-14 09:12:06 +02:00
73cba7f761 Create stale.yml 2020-08-16 16:30:51 +02:00
71208bbf81 updated the aur info 2020-04-10 11:36:34 +02:00
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
40 changed files with 3359 additions and 745 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

49
CHANGELOG.md Normal file
View File

@@ -0,0 +1,49 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 2.1.1
- The discord integration now doesn't send an update every 15 seconds it sends an update whenever the media info changes
- consolidated updating the media info changes with the status changes into a single global event
## 2.1.0
- [Mar0xy](https://github.com/Mar0xy) added Discord integration.
- Several versions have been bumped to fix vulnerabilities
## 2.0.0
### Breaking changes
- Changed settings hotkey from "ctrl+/" to "ctrl+=" to avoid a conflict with the default Tidal hotkeys
## Other changes
- Added a setting to disable custom hotkeys
- Fixed the bug that the previous song hotkey would register 3 times. (Twice due to a duplicate block of code + once from the default tidal hotkey)
## 1.3.0
-- re-enabled mpris-service wit the electron downloader fixes
## 1.2.0
- Added the ability to disable the tray icon
## 1.1.1
Bugfixes:
- Arch AUR install failed before, it is fixed now by using the included build scripts
## 1.1.0
- updated to electron 8.0.0
- Added a beta-version of the mpris service
- Bugfixes:
- icon on gnome not showing in launcher
- app not remembering size on startup

View File

@@ -1,19 +1,31 @@
<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)
- [Arch Linux](#arch-linux)
- [Using source](#using-source)
- [Why](#why)
- [features](#features)
- [Integrations](#integrations)
- [Known bugs](#known-bugs)
- [Why](#why)
- [Why not extend existing projects?](#why-not-extend-existing-projects)
- [Special thanks to..](#special-thanks-to)
- [Special thanks to...](#special-thanks-to)
- [Buy me a coffee? Please don't](#buy-me-a-coffee-please-dont)
- [Images](#images)
- [settings window](#settings-window)
- [user setups](#user-setups)
<!-- tocstop -->
@@ -23,6 +35,30 @@ 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 +68,28 @@ To install and work with the code on this project follow these steps:
- npm install
- npm start
## features
- HiFi playback
- Notifications
- Custom hotkeys ([source](https://defkey.com/tidal-desktop-shortcuts))
- API for status and playback
- [Settings feature](./docs/settings.png) to disable certain functionality. (`ctrl+=`)
## 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,11 +97,27 @@ 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 :)
## Special thanks to..
## Special thanks to...
- [Castlabs](https://castlabs.com/)
For maintaining Electron with Widevine CDM installation, Verified Media Path (VMP), and persistent licenses (StorageID)
## Buy me a coffee? Please don't
Instead spend some money on a charity I care for: [kwf.nl](secure.kwf.nl/donation).
Inspired by [haydenjames' issue](https://github.com/Mastermindzh/tidal-hifi/issues/27#issuecomment-704198429)
## Images
### settings window
![settings window](./docs/settings-preview.png)
### user setups
Some of our users are kind enough to share their usage pictures.
If you want to see them or possibly even add one please do so in the following issue: [#3 - image thread](https://github.com/Mastermindzh/tidal-hifi/issues/3).

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,8 @@
appId: com.rickvanlieshout.tidal-hifi
electronVersion: 8.5.2
electronDownload:
version: 8.5.2-wvvmp
mirror: https://github.com/castlabs/electron-releases/releases/download/v
snap:
plugs:
- default
@@ -6,11 +10,26 @@ snap:
linux:
category: Audio
target:
- pacman
# - pacman
- tar.gz
- deb
- rpm
- AppImage
- snap
- freebsd
executableName: tidal-hifi
desktop:
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.
Icon: assets/icon.png
StartupNotify: true
Terminal: false
Type: Application
Categories: Network;Application;Audio;Video
StartupWMClass: tidal-hifi
X-PulseAudio-Properties: media.role=music
mac:
category: public.app-category.entertainment
win:

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 = 2.1.0
pkgrel = 1
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/2.1.0.zip
source = tidal-hifi.desktop
sha512sums = 30b50243e7033219351f7d7f318757e4590e5c78a7031762cf846ae3652b424ab3f54388cba44209e49434d8534edb198e1c65d0b53450b4367636ef0f4653e0
sha512sums = 35f38ac308b871c1822d7f6f760f2fb54c3748cf769822cb0f0dfb90f0f5754ba9316da5e903a0d2e9839de3a43ec76f238f3f2e44021956fa1da19142081349
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=2.1.0
pkgrel=1
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=('30b50243e7033219351f7d7f318757e4590e5c78a7031762cf846ae3652b424ab3f54388cba44209e49434d8534edb198e1c65d0b53450b4367636ef0f4653e0'
'35f38ac308b871c1822d7f6f760f2fb54c3748cf769822cb0f0dfb90f0f5754ba9316da5e903a0d2e9839de3a43ec76f238f3f2e44021956fa1da19142081349')
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.
npm run build-arch
}
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,13 @@
[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
X-PulseAudio-Properties=media.role=music

BIN
docs/settings-preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 KiB

BIN
docs/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

2292
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": "2.1.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": "electron-builder --publish=never -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",
@@ -14,16 +20,24 @@
"linux"
],
"author": "Rick van Lieshout <info@rickvanlieshout.com> (http://rickvanlieshout.com)",
"homepage": "https://github.com/Mastermindzh/tidal-hifi",
"license": "MIT",
"dependencies": {
"hotkeys-js": "^3.7.1"
"discord-rpc": "^3.2.0",
"electron-store": "^5.1.1",
"express": "^4.17.1",
"hotkeys-js": "^3.7.6",
"mpris-service": "^2.1.0",
"node-notifier": "^9.0.1",
"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#v8.5.2-wvvmp",
"electron-builder": "^21.2.0",
"electron-reload": "^1.5.0",
"prettier": "^1.18.2"
"prettier": "^2.0.4",
"dot-prop": ">=4.2.1"
},
"prettier": "@mastermindzh/prettier-config"
}

View File

@@ -0,0 +1,14 @@
const globalEvents = {
play: "play",
pause: "pause",
playPause: "playPause",
next: "next",
previous: "previous",
updateInfo: "update-info",
hideSettings: "hideSettings",
showSettings: "showSettings",
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;

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

@@ -0,0 +1,31 @@
/**
* 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",
},
mpris: "mpris",
enableCustomHotkeys: "enableCustomHotkeys",
trayIcon: "trayIcon",
enableDiscord: "enableDiscord",
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 discordModule = require("./scripts/discord");
const globalEvents = require("./constants/globalEvents");
let mainWindow;
let icon = path.join(__dirname, "../assets/icon.png");
/**
* Enable live reload in development builds
@@ -17,17 +34,20 @@ function createWindow(options = {}) {
mainWindow = new BrowserWindow({
x: options.x,
y: options.y,
width: 1024,
height: 800,
width: store && store.get(settings.windowBounds.width),
height: store && store.get(settings.windowBounds.height),
icon,
tray: true,
backgroundColor: options.backgroundColor,
webPreferences: {
affinity: "window",
preload: path.join(__dirname, "preload.js"),
plugins: true,
devTools: true, // I like tinkering, others might too
},
});
mainWindow.setMenuBarVisibility(false);
mainWindow.setMenuBarVisibility(store.get(settings.menuBar));
// load the Tidal website
mainWindow.loadURL(tidalUrl);
@@ -36,31 +56,68 @@ function createWindow(options = {}) {
mainWindow.webContents.once("did-finish-load", () => {});
// Emitted when the window is closed.
mainWindow.on("closed", function() {
mainWindow = null;
mainWindow.on("closed", function () {
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();
store.get(settings.trayIcon) && addTray({ icon }) && refreshTray();
store.get(settings.api) && expressModule.run(mainWindow);
store.get(settings.enableDiscord) && discordModule.initRPC();
});
app.on("activate", function() {
app.on("activate", function () {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) {
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.storeChanged, (event, arg) => {
mainWindow.setMenuBarVisibility(store.get(settings.menuBar));
if (store.get(settings.enableDiscord) && !discordModule.rpc) {
discordModule.initRPC();
} else if (!store.get(settings.enableDiscord) && discordModule.rpc) {
discordModule.unRPC();
}
});
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,98 @@
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);
trayIcon.checked = store.get(settings.trayIcon);
mpris.checked = store.get(settings.mpris);
enableCustomHotkeys.checked = store.get(settings.enableCustomHotkeys);
enableDiscord.checked = store.get(settings.enableDiscord);
}
/**
* 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", (_, tab) => {
document.getElementById(tab).click();
});
notifications = get("notifications");
playBackControl = get("playBackControl");
api = get("apiCheckbox");
port = get("port");
menuBar = get("menuBar");
trayIcon = get("trayIcon");
mpris = get("mprisCheckbox");
enableCustomHotkeys = get("enableCustomHotkeys");
enableDiscord = get("enableDiscord");
refreshSettings();
addInputListener(notifications, settings.notifications);
addInputListener(playBackControl, settings.playBackControl);
addInputListener(api, settings.api);
addInputListener(port, settings.apiSettings.port);
addInputListener(menuBar, settings.menuBar);
addInputListener(trayIcon, settings.trayIcon);
addInputListener(mpris, settings.mpris);
addInputListener(enableCustomHotkeys, settings.enableCustomHotkeys);
addInputListener(enableDiscord, settings.enableDiscord);
});

View File

@@ -0,0 +1,463 @@
<!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="general" checked />
<label for="general">General</label>
<!-- Tab 2 -->
<input type="radio" name="tabset" id="api" />
<label for="api">Api</label>
<!-- Integrations tab -->
<input type="radio" name="tabset" id="integrations" />
<label for="integrations">Integrations</label>
<!-- about tab -->
<input type="radio" name="tabset" id="about" />
<label for="about">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>
<div class="section">
<h3>System</h3>
<div class="option">
<h4>Tray icon</h4>
<p>
Show Tidal-hifi's tray icon<br />
</p>
<label class="switch">
<input id="trayIcon" type="checkbox">
<span class="slider round"></span>
</label>
</div>
<div class="option">
<h4>Hotkeys</h4>
<p>
Enables extra hotkeys to achieve feature parity with the <a href = "javascript:openExternal('https://defkey.com/tidal-desktop-shortcuts')">desktop apps</a><br />
</p>
<label class="switch">
<input id="enableCustomHotkeys" 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.
</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>
</section>
<section id="integrations" class="tab-panel">
<div class="section">
<h3>integrations</h3>
<p style="margin-bottom: 15px;">
Tidal-hifi is extensible trough the use of integrations. You can enable or disable integrations here
</p>
<div class="option">
<h4>mpris-player</h4>
<p>
Whether to enable the mpris media player controls for Linux systems
</p>
<label class="switch">
<input id="mprisCheckbox" type="checkbox">
<span class="slider round"></span>
</label>
</div>
<div class="option">
<h4>Discord RPC</h4>
<p>
Show what you're listening to on Discord
</p>
<label class="switch">
<input id="enableDiscord" type="checkbox">
<span class="slider round"></span>
</label>
</div>
</div>
</section>
<section id="about" 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>
<small>Some settings require a restart of Tidal-hifi. To do so click the button below:</small>
<button onClick="restart()" style ="width: 100%">Restart Tidal-hifi</button>
</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;
outline: none;
text-decoration: none;
font-size: 1.4rem;
float: right;
margin-right: 15px;
height: 50px;
line-height: 50px;
}
.exitWindow:focus {
border: none;
outline: none;
}
.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),
.tabset > input:nth-child(7):checked ~ .tab-panels > .tab-panel:nth-child(4)
{
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,17 @@
const { setTitle, getTitle } = require("./scripts/window-functions");
const { dialog } = require("electron").remote;
const { setTitle } = require("./scripts/window-functions");
const { dialog, process } = 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 = "";
let player;
let currentPlayStatus = statuses.paused;
const elements = {
play: '*[data-test="play"]',
@@ -8,7 +19,7 @@ const elements = {
next: '*[data-test="next"]',
previous: 'button[data-test="previous"]',
title: '*[data-test^="footer-track-title"]',
artists: '*[class^="mediaArtists"]',
artists: '*[data-test^="grid-item-detail-text-title-artist"]',
home: '*[data-test="menu--home"]',
back: '[class^="backwardButton"]',
forward: '[class^="forwardButton"]',
@@ -18,20 +29,57 @@ const elements = {
block: '[class="blockButton"]',
account: '*[data-test^="profile-image-button"]',
settings: '*[data-test^="open-settings"]',
media: '*[data-test="current-media-imagery"]',
image: "img",
url: 'a[href*="/track/"]',
get: function(key) {
/**
* 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 "";
},
click: function(key) {
/**
* 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;
},
focus: function(key) {
/**
* Shorthand function to focus a dom element
* @param {*} key key in elements object to fetch
*/
focus: function (key) {
return this.get(key).focus();
},
};
@@ -55,53 +103,40 @@ function playPause() {
* https://defkey.com/tidal-desktop-shortcuts
*/
function addHotKeys() {
hotkeys.add("Control+p", function() {
if (store.get(settings.enableCustomHotkeys)) {
hotkeys.add("Control+p", function () {
elements.click("account").click("settings");
});
hotkeys.add("Control+l", function() {
hotkeys.add("Control+l", function () {
handleLogout();
});
hotkeys.add("Control+h", function() {
hotkeys.add("Control+h", function () {
elements.click("home");
});
hotkeys.add("backspace", function() {
hotkeys.add("backspace", function () {
elements.click("back");
});
hotkeys.add("shift+backspace", function() {
hotkeys.add("shift+backspace", function () {
elements.click("forward");
});
hotkeys.add("control+f", function() {
elements.focus("search");
});
hotkeys.add("control+u", function() {
hotkeys.add("control+u", function () {
// reloading window without cache should show the update bar if applicable
window.location.reload(true);
});
hotkeys.add("control+left", function() {
elements.click("previous");
});
hotkeys.add("control+right", function() {
elements.click("next");
});
hotkeys.add("control+right", function() {
elements.click("next");
});
hotkeys.add("control+s", function() {
elements.click("shuffle");
});
hotkeys.add("control+r", function() {
hotkeys.add("control+r", function () {
elements.click("repeat");
});
}
// always add the hotkey for the settings window
hotkeys.add("control+=", function () {
ipcRenderer.send(globalEvents.showSettings);
});
}
/**
@@ -120,10 +155,10 @@ function handleLogout() {
buttons: logoutOptions,
defaultId: 2,
},
function(response) {
function (response) {
if (logoutOptions.indexOf("Yes, please") == response) {
for (i = 0; i < window.localStorage.length; i++) {
key = window.localStorage.key(i);
for (let i = 0; i < window.localStorage.length; i++) {
const key = window.localStorage.key(i);
if (key.startsWith("_TIDAL_activeSession")) {
window.localStorage.removeItem(key);
i = window.localStorage.length + 1;
@@ -141,25 +176,171 @@ 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)
*/
setInterval(function() {
function getCurrentlyPlayingStatus() {
let pause = elements.get("pause");
let status = undefined;
// if pause button is visible tidal is playing
if (pause) {
status = statuses.playing;
} else {
status = statuses.paused;
}
return status;
}
/**
* Update Tidal-hifi's media info
*
* @param {*} options
*/
function updateMediaInfo(options, notify) {
if (options) {
ipcRenderer.send(globalEvents.updateInfo, options);
store.get(settings.notifications) && notify && notifier.notify(options);
if (player) {
player.metadata = {
...player.metadata,
...{
"xesam:title": options.title,
"xesam:artist": [options.artists],
"mpris:artUrl": options.image,
},
};
player.playbackStatus = options.status == statuses.paused ? "Paused" : "Playing";
}
}
}
/**
* Watch for song changes and update title + notify
*/
setInterval(function () {
const title = elements.getText("title");
const url = elements.get("url").href.replace(/[^0-9]/g, "");
const artists = elements.getText("artists");
const songDashArtistTitle = `${title} - ${artists}`;
if (getTitle() !== songDashArtistTitle) {
const currentStatus = getCurrentlyPlayingStatus();
const options = {
title,
message: artists,
status: currentStatus,
url: `https://tidal.com/browse/track/${url}`,
};
const playStatusChanged = currentStatus !== currentPlayStatus;
const titleOrArtistChanged = currentSong !== songDashArtistTitle;
if (titleOrArtistChanged || playStatusChanged) {
// update title and play info with new info
setTitle(songDashArtistTitle);
currentSong = songDashArtistTitle;
currentPlayStatus = currentStatus;
const image = elements.getSongIcon();
new Promise((resolve) => {
if (image.startsWith("http")) {
downloadFile(image, notificationPath).then(
() => {
options.icon = notificationPath;
resolve();
},
() => {
// if the image can't be downloaded then continue without it
resolve();
}
}, 1000);
);
} else {
// if the image can't be found on the page continue without it
resolve();
}
}).then(
() => {
updateMediaInfo(options, titleOrArtistChanged);
},
() => {}
);
}
}, 200);
if (process.platform === "linux" && store.get(settings.mpris)) {
try {
const Player = require("mpris-service");
player = Player({
name: "tidal-hifi",
identity: "tidal-hifi",
supportedUriSchemes: ["file"],
supportedMimeTypes: [
"audio/mpeg",
"audio/flac",
"audio/x-flac",
"application/ogg",
"audio/wav",
],
supportedInterfaces: ["player"],
desktopEntry: "tidal-hifi",
});
// Events
var events = {
next: "next",
previous: "previous",
pause: "pause",
playpause: "playpause",
stop: "stop",
play: "play",
loopStatus: "repeat",
shuffle: "shuffle",
seek: "seek",
};
Object.keys(events).forEach(function (eventName) {
player.on(eventName, function () {
const eventValue = events[eventName];
switch (events[eventValue]) {
case events.playpause:
playPause();
break;
default:
elements.click(eventValue);
}
});
});
player.on("quit", function () {
app.quit();
});
} catch (exception) {
console.log("player api not working");
}
}
addHotKeys();
addIPCEventListeners();

57
src/scripts/discord.js Normal file
View File

@@ -0,0 +1,57 @@
const discordrpc = require("discord-rpc");
const { ipcMain } = require("electron");
const electron = require("electron");
const globalEvents = require("../constants/globalEvents");
const clientId = "833617820704440341";
const mediaInfoModule = require("./mediaInfo");
const discordModule = [];
let rpc;
const observer = (event, arg) => {
if (mediaInfoModule.mediaInfo.status == "paused" && rpc) {
rpc.setActivity(idleStatus);
} else if (rpc) {
rpc.setActivity({
...idleStatus,
...{
details: `Listening to ${mediaInfoModule.mediaInfo.title}`,
state: mediaInfoModule.mediaInfo.artist,
buttons: [{ label: "Play on Tidal", url: mediaInfoModule.mediaInfo.url }],
},
});
}
};
const idleStatus = {
details: `Browsing Tidal`,
largeImageKey: "tidal-hifi-icon",
largeImageText: `Tidal HiFi ${electron.app.getVersion()}`,
instance: false,
};
/**
* Set up the discord rpc and listen on globalEvents.updateInfo
*/
discordModule.initRPC = function () {
rpc = new discordrpc.Client({ transport: "ipc" });
rpc.login({ clientId }).catch(console.error);
discordModule.rpc = rpc;
rpc.on("ready", () => {
rpc.setActivity(idleStatus);
});
ipcMain.on(globalEvents.updateInfo, observer);
};
/**
* Remove any RPC connection with discord and remove the event listener on globalEvents.updateInfo
*/
discordModule.unRPC = function () {
rpc.clearActivity();
rpc.destroy();
rpc = false;
discordModule.rpc = rpc;
ipcMain.removeListener(globalEvents.updateInfo, observer);
};
module.exports = discordModule;

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;

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

@@ -0,0 +1,34 @@
const statuses = require("./../constants/statuses");
const mediaInfo = {
title: "",
artist: "",
icon: "",
status: statuses.paused,
url: "",
};
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);
mediaInfo.url = propOrDefault(arg.url);
mediaInfo.status = propOrDefault(arg.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("about");
},
},
];
const menuModule = { mainMenu };
menuModule.getMenu = function () {
return Menu.buildFromTemplate(mainMenu);
};
menuModule.addMenu = function () {
Menu.setApplicationMenu(menuModule.getMenu());
};
module.exports = menuModule;

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

@@ -0,0 +1,73 @@
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,
},
trayIcon: true,
mpris: false,
enableCustomHotkeys: false,
enableDiscord: false,
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 = "general") {
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;

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

@@ -0,0 +1,22 @@
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 () {
if (!tray) {
trayModule.addTray();
}
tray.on("click", function (e) {
// do nothing on click
});
tray.setToolTip("Tidal-hifi");
tray.setContextMenu(getMenu());
};
module.exports = trayModule;

18
stale.yml Normal file
View File

@@ -0,0 +1,18 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 30
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 2
# Issues with these labels will never be considered stale
exemptLabels:
- good first issue
- waiting for support
- security
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false