Compare commits

..

33 Commits
5.3.0 ... 5.6.0

Author SHA1 Message Date
dc87b20ab8 Merge pull request #265 from Mastermindzh/feature/5.6.0
Feature/5.6.0
2023-08-12 15:42:27 +02:00
c7b3921514 Added app suspension inhibitors when music is playing. fixes #257 2023-08-12 15:02:23 +02:00
89f1ff4228 Create SECURITY.md 2023-08-12 14:30:10 +02:00
a0c73596e4 feat: added wayland support. fixes #262 #157 2023-08-07 20:48:29 +02:00
aa17d80450 added new quality names to readme + added neptune mention. fixes #261 2023-08-07 20:28:14 +02:00
5ea3972053 fixed errors with user theme files loading 2023-08-07 20:04:06 +02:00
4b81378423 fixed feature flag parsing & setting 2023-08-07 19:48:29 +02:00
c7931cf913 simplified logger 2023-08-07 19:45:44 +02:00
c6dff0b0e5 Merge pull request #260 from Mastermindzh/5.5.0
5.5.0
2023-07-31 21:13:39 +02:00
644beea2a6 fixed listenbrainz link 2023-07-31 21:13:24 +02:00
df1c45982b 5.5.0 docs, versions, etc 2023-07-31 15:49:29 +02:00
ec82aa8401 Merge pull request #258 from Mar0xy/master2
Add ListenBrainz implementation
2023-07-31 15:06:39 +02:00
586f7b595b various code improvements and some boyscout rule fixes :) 2023-07-31 13:43:32 +02:00
Mar0xy
de8a5a1b07 Fix bug where it does not run if condition 2023-07-31 12:14:06 +02:00
Mar0xy
38c1f05c35 Allow listenbrainz to be triggered on every play 2023-07-31 12:06:31 +02:00
Mar0xy
ed6f04b6d4 Fix bug where it does not unhide 2023-07-30 21:25:35 +02:00
Mar0xy
ffe8278c8c Fix complainy by sonarcloud 2023-07-30 21:22:38 +02:00
Mar0xy
e9434cc5ea Hide/Show ListenBrainz settings 2023-07-30 21:18:25 +02:00
Marie
d81912db0c Fix music_service domain 2023-07-30 11:46:27 +02:00
Mar0xy
c0110632e6 Seperate old ListenBrainz data from config 2023-07-30 10:42:32 +02:00
Mar0xy
3571289d28 Add ListenBrainz implementation 2023-07-30 02:38:38 +02:00
11cc209025 Merge pull request #255 from Mastermindzh/feature/5.4.0
Feature/5.4.0
2023-07-24 21:59:57 +02:00
f5ccbda7d9 set versions to 5.4.0 2023-07-24 12:04:08 +02:00
e8cf1783e8 chore(docs): updated README spelling 2023-07-23 23:59:45 +02:00
8037a73e57 Merge branch 'feature/5.4.0' of github.com:Mastermindzh/tidal-hifi into feature/5.4.0 2023-07-23 23:51:24 +02:00
45e191dae0 updated dependencies 2023-07-23 23:51:20 +02:00
f147536b12 updated dependencies 2023-07-23 23:43:28 +02:00
d03bb58afa removed windows builds from publishes 2023-07-23 23:20:01 +02:00
a39fef8d49 fix(hotkeys): Fixed bug with several hotkeys not working due to Tidal's HTML/css changes. fixes #250 2023-07-23 23:13:37 +02:00
41ca1d5a43 added songwhip 2023-07-23 23:12:18 +02:00
6969de8270 added several dev improvements 2023-07-23 23:07:19 +02:00
ad05b767d8 Merge pull request #245 from Mastermindzh/dependabot/npm_and_yarn/stylelint-15.10.1
chore(deps-dev): bump stylelint from 15.6.0 to 15.10.1
2023-07-09 00:38:31 +02:00
dependabot[bot]
6d873ce287 chore(deps-dev): bump stylelint from 15.6.0 to 15.10.1
Bumps [stylelint](https://github.com/stylelint/stylelint) from 15.6.0 to 15.10.1.
- [Release notes](https://github.com/stylelint/stylelint/releases)
- [Changelog](https://github.com/stylelint/stylelint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stylelint/stylelint/compare/15.6.0...15.10.1)

---
updated-dependencies:
- dependency-name: stylelint
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-07 22:30:49 +00:00
31 changed files with 2047 additions and 922 deletions

View File

@@ -13,4 +13,4 @@ steps:
commands:
- apt-get update && apt-get upgrade -y
- apt-get install -y libarchive-tools rpm
- npm run build
- npm run build-unpacked

View File

@@ -1,5 +1,9 @@
{
"root": true,
"env": {
"node": true,
"browser": true
},
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"

View File

@@ -1,10 +1,17 @@
{
"cSpell.words": [
"Brainz",
"Castlabs",
"flac",
"Flatpak",
"geqnfr",
"hifi",
"listenbrainz",
"playpause",
"rescrobbler",
"scrobble",
"scrobbling",
"Songwhip",
"trackid",
"tracklist",
"widevine",

View File

@@ -4,11 +4,35 @@ 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).
## [5.6.0]
- Added support for Wayland (on by default) fixes [#262](https://github.com/Mastermindzh/tidal-hifi/issues/262) and [#157](https://github.com/Mastermindzh/tidal-hifi/issues/157)
- Made it clear in the readme that this tidal-hifi client supports High & Max audio settings. fixes [#261](https://github.com/Mastermindzh/tidal-hifi/issues/261)
- Added app suspension inhibitors when music is playing. fixes [#257](https://github.com/Mastermindzh/tidal-hifi/issues/257)
- Fixed bug with theme files from user directory trying to load: "an error occurred reading the theme file"
- Fixed: config flags not being set correctly
- [DEV]:
- Logger is now static and will automatically call either ipcRenderer or ipcMain
## 5.5.0
- ListenBrainz integration added (thanks @Mar0xy)
## 5.4.0
- Removed Windows builds (from publishes) as they don't work anymore.
- Added [Songwhip](https://songwhip.com/) integration
- Fixed bug with several hotkeys not working due to Tidal's HTML/css changes
- [DEV]:
- added a logger to log into STDout
- added "watchStart" which will automatically restart electron when it detects a source code change
- added "listen.tidal.com-parsing-scripts" folder with a script to verify whether all elements (in the main preload.ts) are present on the page
## 5.3.0
- SPKChaosPhoenix updated the beautiful Tokyo Night theme:
![](./docs/images/tokyo-night.png)
![tidal with the tokyo night theme applied](./docs/images/tokyo-night.png)
## 5.2.0

View File

@@ -2,7 +2,7 @@
![GitHub release](https://img.shields.io/github/release/Mastermindzh/tidal-hifi.svg) [![github builds](https://github.com/mastermindzh/tidal-hifi/actions/workflows/build.yml/badge.svg)](https://github.com/Mastermindzh/tidal-hifi/actions) [![Build Status](https://ci.mastermindzh.tech/api/badges/Mastermindzh/tidal-hifi/status.svg)](https://ci.mastermindzh.tech/Mastermindzh/tidal-hifi) [![Discord logo](./docs/images/discord.png)](https://discord.gg/yhNwf4v4He)
The web version of [listen.tidal.com](https://listen.tidal.com) running in electron with hifi support thanks to widevine.
The web version of [listen.tidal.com](https://listen.tidal.com) running in electron with hifi (High & Max) support thanks to widevine.
![tidal-hifi preview](./docs/images/preview.png)
@@ -25,8 +25,9 @@ The web version of [listen.tidal.com](https://listen.tidal.com) running in elect
- [Nix](#nix)
- [Using source](#using-source)
- [Integrations](#integrations)
- [Known bugs](#known-bugs)
- [last.fm doesn't work out of the box. Use rescrobbler as a workaround](#lastfm-doesnt-work-out-of-the-box-use-rescrobbler-as-a-workaround)
- [Known bugs](#known-bugs)
- [last.fm doesn't work out of the box. Use rescrobbler as a workaround](#lastfm-doesnt-work-out-of-the-box-use-rescrobbler-as-a-workaround)
- [DRM not working on Windows](#drm-not-working-on-windows)
- [Special thanks to](#special-thanks-to)
- [Donations](#donations)
- [Images](#images)
@@ -37,15 +38,19 @@ The web version of [listen.tidal.com](https://listen.tidal.com) running in elect
## Features
- HiFi playback
- HiFi playback (High & Max settings)
- Notifications
- Custom [theming](./docs/theming.md)
- Custom hotkeys ([source](https://defkey.com/tidal-desktop-shortcuts))
- [Settings feature](./docs/images/settings.png) to disable certain functionality. (`ctrl+=` or `ctrl+0`)
- API for status and playback
- Disabled audio & visual ads, unlocked lyrics, suggested track, track info, and unlimited skips thanks to uBlockOrigin custom filters ([source](https://github.com/uBlockOrigin/uAssets/issues/17495))
- Custom [integrations](#integrations)
- [Settings feature](./docs/images/settings.png) to disable certain functionality. (`ctrl+=` or `ctrl+0`)
- AlbumArt in integrations ([best-effort](https://github.com/Mastermindzh/tidal-hifi/pull/88#pullrequestreview-840814847))
- Custom [integrations](#integrations)
- [ListenBrainz](https://listenbrainz.org/?redirect=false) integration
- Songwhip.com integration (hotkey `ctrl + w`)
- Discord RPC integration (showing "now listening", "Browsing", etc)
- MPRIS integration
## Contributions
@@ -56,8 +61,8 @@ To contribute you can use the standard GitHub features (issues, prs, etc) or joi
## Why did I create tidal-hifi?
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.
I made this app to support the highest quality audio available on the Linux platform. It used to be "hifi" but now is ["High & Max"](https://tidal.com/sound-quality).
### Why not extend existing projects?
@@ -65,10 +70,10 @@ Whilst there are a handful of projects attempting to run Tidal on Electron they
- Lack of 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
- Some are DE-oriented. I want this to work on WM's too.
- 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 :)
Sometimes it's just easier to start over, cover my own needs and after that making it available to the public :)
## Installation
@@ -131,27 +136,28 @@ To install and work with the code on this project follow these steps:
## Integrations
Tidal-hifi comes with several integrations out of the box.
tidal-hifi comes with several integrations out of the box.
You can find these in the settings menu (`ctrl + =` by default) under the "integrations" tab.
![integrations menu, showing a list of integrations](./docs/images/integrations.png)
It currently includes:
- MPRIS - MPRIS media player controls/status
- Discord - Shows what you're listening to on Discord.
Not included:
Integrations with other projects that are not included natively:
- [i3 blocks config](https://github.com/Mastermindzh/dotfiles/commit/9714b2fa1d670108ce811d5511fd3b7a43180647) - My dotfiles where I use this app to fetch currently playing music (direct commit)
- [neptune](https://github.com/uwu/neptune) third party plugins & theming
### Known bugs
## Known bugs
#### last.fm doesn't work out of the box. Use rescrobbler as a workaround
### last.fm doesn't work out of the box. Use rescrobbler as a workaround
The last.fm login doesn't work, as is evident from the following issue: [Last.fm login doesn't work](https://github.com/Mastermindzh/tidal-hifi/issues/4).
However, in that same issue you can read about a workaround using [rescrobbler](https://github.com/InputUsername/rescrobbled).
For now that will be the default workaround.
For now, that will be the default workaround.
### DRM not working on Windows
Most Windows users run into DRM issues when trying to use tidal-hifi.
Nothing I can do about that I'm afraid... Tidal is working on removing/changing DRM so when they finish with that we can give it another shot.
## Special thanks to

11
SECURITY.md Normal file
View File

@@ -0,0 +1,11 @@
# Security Policy
## Supported Versions
Only the very latest 😄.
## Reporting a Vulnerability
If you find a vulnerability just add it as an issue.
If there's an especially bad vulnerability that you don't want to make public just send me a private message (email, discord, wherever).

View File

@@ -0,0 +1,43 @@
// for some dumb reason `listen.tidal.com` has disabled console.log
// we can simply return an array with values though...
// run this on a playlist or mix page and observe the result
// NOTE: play & pause can't live together so one or the other will throw an error
(() => {
let elements = {
play: '*[data-test="play"]',
pause: '*[data-test="pause"]',
next: '*[data-test="next"]',
previous: 'button[data-test="previous"]',
title: '*[data-test^="footer-track-title"]',
artists: '*[data-test^="grid-item-detail-text-title-artist"]',
home: '*[data-test="menu--home"]',
back: '[title^="Back"]',
forward: '[title^="Next"]',
search: '[class^="searchField"]',
shuffle: '*[data-test="shuffle"]',
repeat: '*[data-test="repeat"]',
account: '*[data-test^="profile-image-button"]',
media: '*[data-test="current-media-imagery"]',
image: "img",
current: '*[data-test="current-time"]',
duration: '*[data-test="duration"]',
bar: '*[data-test="progress-bar"]',
footer: "#footerPlayer",
mediaItem: "[data-type='mediaItem']",
album_header_title: '.header-details [data-test="title"]',
currentlyPlaying: "[class^='isPlayingIcon'], [data-test-is-playing='true']",
album_name_cell: '[class^="album"]',
tracklist_row: '[data-test="tracklist-row"]',
volume: '*[data-test="volume"]',
};
let results = [];
Object.entries(elements).forEach(([key, value]) => {
const returnValue = document.querySelector(`${value}`);
if (!returnValue) {
results.push(`element ${key} not found`);
}
});
return results;
})();

1988
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,11 @@
{
"name": "tidal-hifi",
"version": "5.3.0",
"version": "5.6.0",
"description": "Tidal on Electron with widevine(hifi) support",
"main": "ts-dist/main.js",
"scripts": {
"start": "electron .",
"start": "electron --inspect=0.0.0.0:5858 .",
"watchStart": "nodemon dist -x \"npm run start\"",
"compile": "tsc && npm run sass-and-copy",
"watch": "tsc-watch --onSuccess \"npm run sass-and-copy\"",
"copy-files": "copyfiles -u 1 --exclude './src/**/*.ts' --exclude './src/**/*.scss' \"./src/**/*\" ts-dist",
@@ -29,41 +30,46 @@
"electron",
"hifi",
"widevine",
"linux"
"linux",
"drm",
"castlabs"
],
"author": "Rick van Lieshout <info@rickvanlieshout.com> (http://rickvanlieshout.com)",
"homepage": "https://github.com/Mastermindzh/tidal-hifi",
"license": "MIT",
"dependencies": {
"@electron/remote": "^2.0.9",
"@electron/remote": "^2.0.10",
"axios": "^1.4.0",
"discord-rpc": "^4.0.1",
"electron-store": "^8.1.0",
"express": "^4.18.2",
"hotkeys-js": "^3.10.2",
"hotkeys-js": "^3.11.2",
"mpris-service": "^2.1.2",
"request": "^2.88.2",
"sass": "^1.62.0"
"sass": "^1.64.1"
},
"devDependencies": {
"@mastermindzh/prettier-config": "^1.0.0",
"@types/discord-rpc": "^4.0.4",
"@types/discord-rpc": "^4.0.5",
"@types/express": "^4.17.17",
"@types/node": "^20.4.4",
"@types/request": "^2.48.8",
"@typescript-eslint/eslint-plugin": "^5.59.1",
"@typescript-eslint/parser": "^5.59.1",
"@typescript-eslint/eslint-plugin": "^6.1.0",
"@typescript-eslint/parser": "^6.1.0",
"copyfiles": "^2.4.1",
"electron": "git+https://github.com/castlabs/electron-releases.git#v24.1.2+wvcus",
"electron-builder": "^24.2.1",
"eslint": "^8.39.0",
"electron-builder": "^24.4.0",
"eslint": "^8.45.0",
"js-yaml": "^4.1.0",
"markdown-toc": "^1.2.0",
"prettier": "^2.8.8",
"stylelint": "^15.6.0",
"stylelint-config-standard": "^33.0.0",
"stylelint-config-standard-scss": "^9.0.0",
"stylelint-prettier": "^3.0.0",
"nodemon": "^3.0.1",
"prettier": "^3.0.0",
"stylelint": "^15.10.2",
"stylelint-config-standard": "^34.0.0",
"stylelint-config-standard-scss": "^10.0.0",
"stylelint-prettier": "^4.0.0",
"tsc-watch": "^6.0.4",
"typescript": "^5.0.4"
"typescript": "^5.1.6"
},
"prettier": "@mastermindzh/prettier-config"
}

View File

@@ -1,4 +1,9 @@
export const flags: { [key: string]: { flag: string; value?: string }[] } = {
gpuRasterization: [{ flag: "enable-gpu-rasterization", value: undefined }],
disableHardwareMediaKeys: [{ flag: "disable-features", value: "HardwareMediaKeyHandling" }],
enableWaylandSupport: [
{ flag: "enable-features", value: "UseOzonePlatform" },
{ flag: "ozone-platform-hint", value: "auto" },
{ flag: "enable-features", value: "WaylandWindowDecorations" },
],
};

View File

@@ -10,4 +10,6 @@ export const globalEvents = {
showSettings: "showSettings",
storeChanged: "storeChanged",
error: "error",
whip: "whip",
log: "log",
};

View File

@@ -20,10 +20,17 @@ export const settings = {
disableHardwareMediaKeys: "disableHardwareMediaKeys",
enableCustomHotkeys: "enableCustomHotkeys",
enableDiscord: "enableDiscord",
ListenBrainz: {
root: "ListenBrainz",
enabled: "ListenBrainz.enabled",
api: "ListenBrainz.api",
token: "ListenBrainz.token",
},
flags: {
root: "flags",
disableHardwareMediaKeys: "flags.disableHardwareMediaKeys",
gpuRasterization: "flags.gpuRasterization",
enableWaylandSupport: "flags.enableWaylandSupport",
},
menuBar: "menuBar",
minimizeOnClose: "minimizeOnClose",

View File

@@ -1,4 +0,0 @@
export const statuses = {
playing: "playing",
paused: "paused",
};

View File

@@ -0,0 +1,41 @@
import { App } from "electron";
import { flags } from "../../constants/flags";
import { settings } from "../../constants/settings";
import { settingsStore } from "../../scripts/settings";
import { Logger } from "../logger";
/**
* Set default Electron flags
*/
export function setDefaultFlags(app: App) {
setFlag(app, "disable-seccomp-filter-sandbox");
}
/**
* Set Tidal's managed flags from the user settings
* @param app
*/
export function setManagedFlagsFromSettings(app: App) {
const flagsFromSettings = settingsStore.get(settings.flags.root);
if (flagsFromSettings) {
for (const [key, value] of Object.entries(flagsFromSettings)) {
if (value) {
flags[key].forEach((flag) => {
Logger.log(`enabling command line option ${flag.flag} with value ${flag.value}`);
setFlag(app, flag.flag, flag.value);
});
}
}
}
}
/**
* Set a single flag for Electron
* @param app app to set it on
* @param flag flag name
* @param value value to be set for the flag
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function setFlag(app: App, flag: string, value?: any) {
app.commandLine.appendSwitch(flag, value);
}

View File

@@ -0,0 +1,65 @@
import { PowerSaveBlocker, powerSaveBlocker } from "electron";
import { Logger } from "../logger";
/**
* Start blocking idle/screen timeouts
* @param blocker optional instance of the powerSaveBlocker to use
* @returns id of current block
*/
export const acquireInhibitor = (blocker?: PowerSaveBlocker): number => {
const currentBlocker = blocker ?? powerSaveBlocker;
const blockId = currentBlocker.start("prevent-app-suspension");
Logger.log(`Started preventing app suspension with id: ${blockId}`);
return blockId;
};
/**
* Check whether there is a blocker active for the current id, if not start it.
* @param id id of inhibitor you want to check activity against
* @param blocker optional instance of the powerSaveBlocker to use
*/
export const acquireInhibitorIfInactive = (id: number, blocker?: PowerSaveBlocker): number => {
const currentBlocker = blocker ?? powerSaveBlocker;
if (!isInhibitorActive(id, currentBlocker)) {
return acquireInhibitor();
}
return id;
};
/**
* stop blocking idle/screen timeouts
* @param id id of inhibitor you want to check activity against
* @param blocker optional instance of the powerSaveBlocker to use
*/
export const releaseInhibitor = (id: number, blocker?: PowerSaveBlocker) => {
try {
const currentBlocker = blocker ?? powerSaveBlocker;
currentBlocker.stop(id);
Logger.log(`Released inhibitor with id: ${id}`);
} catch (error) {
Logger.log("Releasing inhibitor failed");
}
};
/**
* stop blocking idle/screen timeouts if a inhibitor is active
* @param id id of inhibitor you want to check activity against
* @param blocker optional instance of the powerSaveBlocker to use
*/
export const releaseInhibitorIfActive = (id: number, blocker?: PowerSaveBlocker) => {
const currentBlocker = blocker ?? powerSaveBlocker;
if (isInhibitorActive(id, currentBlocker)) {
releaseInhibitor(id, currentBlocker);
}
};
/**
* check whether the inhibitor is active
* @param id id of inhibitor you want to check activity against
* @param blocker optional instance of the powerSaveBlocker to use
*/
export const isInhibitorActive = (id: number, blocker?: PowerSaveBlocker) => {
const currentBlocker = blocker ?? powerSaveBlocker;
return currentBlocker.isStarted(id);
};

View File

@@ -0,0 +1,132 @@
import axios from "axios";
import Store from "electron-store";
import { settings } from "../../constants/settings";
import { MediaStatus } from "../../models/mediaStatus";
import { settingsStore } from "../../scripts/settings";
import { Logger } from "../logger";
import { StoreData } from "./models/storeData";
const ListenBrainzStore = new Store({ name: "listenbrainz" });
export const ListenBrainzConstants = {
oldData: "oldData",
};
export class ListenBrainz {
/**
* Create the object to store old information in the Store :)
* @param title
* @param artists
* @param duration
* @returns data passed along in an object + a "listenedAt" key with the current time
*/
private static constructStoreData(title: string, artists: string, duration: number): StoreData {
return {
listenedAt: Math.floor(new Date().getTime() / 1000),
title,
artists,
duration,
};
}
/**
* Call the ListenBrainz API and create playing now payload and scrobble old song
* @param title
* @param artists
* @param status
* @param duration
*/
public static async scrobble(
title: string,
artists: string,
status: string,
duration: number
): Promise<void> {
try {
if (status === MediaStatus.paused) {
return;
} else {
// Fetches the oldData required for scrobbling and proceeds to construct a playing_now data payload for the Playing Now area
const oldData = ListenBrainzStore.get(ListenBrainzConstants.oldData) as StoreData;
const playing_data = {
listen_type: "playing_now",
payload: [
{
track_metadata: {
additional_info: {
media_player: "Tidal Hi-Fi",
submission_client: "Tidal Hi-Fi",
music_service: "tidal.com",
duration: duration,
},
artist_name: artists,
track_name: title,
},
},
],
};
await axios.post(
`${settingsStore.get<string, string>(settings.ListenBrainz.api)}/1/submit-listens`,
playing_data,
{
headers: {
"Content-Type": "application/json",
Authorization: `Token ${settingsStore.get<string, string>(
settings.ListenBrainz.token
)}`,
},
}
);
if (!oldData) {
ListenBrainzStore.set(
ListenBrainzConstants.oldData,
this.constructStoreData(title, artists, duration)
);
} else {
if (oldData.title !== title) {
// This constructs the data required to scrobble the data after the song finishes
const scrobble_data = {
listen_type: "single",
payload: [
{
listened_at: oldData.listenedAt,
track_metadata: {
additional_info: {
media_player: "Tidal Hi-Fi",
submission_client: "Tidal Hi-Fi",
music_service: "listen.tidal.com",
duration: oldData.duration,
},
artist_name: oldData.artists,
track_name: oldData.title,
},
},
],
};
await axios.post(
`${settingsStore.get<string, string>(settings.ListenBrainz.api)}/1/submit-listens`,
scrobble_data,
{
headers: {
"Content-Type": "application/json",
Authorization: `Token ${settingsStore.get<string, string>(
settings.ListenBrainz.token
)}`,
},
}
);
ListenBrainzStore.set(
ListenBrainzConstants.oldData,
this.constructStoreData(title, artists, duration)
);
}
}
}
} catch (error) {
Logger.log(JSON.stringify(error));
}
}
}
export { ListenBrainzStore };

View File

@@ -0,0 +1,9 @@
/**
* Data saved for ListenBrainz
*/
export interface StoreData {
listenedAt: number;
title: string;
artists: string;
duration: number;
}

52
src/features/logger.ts Normal file
View File

@@ -0,0 +1,52 @@
import { IpcMain, ipcMain, IpcMainEvent, ipcRenderer } from "electron";
import { globalEvents } from "../constants/globalEvents";
export class Logger {
/**
* Subscribe to watch for logs from the IPC client
* @param ipcMain main thread IPC client so we can subscribe to events
*/
public static watch(ipcMain: IpcMain) {
ipcMain.on(
globalEvents.log,
(event: IpcMainEvent | { content: string; message: string }, message) => {
const { content, object } = message ?? event;
this.logToSTDOut(content, object);
}
);
}
/**
* Log content to STDOut
* @param content
* @param object js(on) object that will be prettyPrinted
*/
public static log(content: string, object: object = {}) {
if (ipcRenderer) {
ipcRenderer.send(globalEvents.log, { content, object });
} else {
ipcMain.emit(globalEvents.log, { content, object });
}
}
/**
* Log content to STDOut and use the provided alert function to alert
* @param content
* @param object js(on) object that will be prettyPrinted
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public static alert(content: string, object: any = {}, alert?: (msg: string) => void) {
Logger.log(content, object);
if (alert) {
alert(`${content} \n\nwith details: \n${JSON.stringify(object, null, 2)}`);
}
}
/**
* Log to STDOut
* @param content
* @param object
*/
private static logToSTDOut(content: string, object = {}) {
console.log(content, Object.keys(object).length > 0 ? JSON.stringify(object, null, 2) : "");
}
}

View File

@@ -0,0 +1,21 @@
import { ServiceLinks } from "./ServiceLinks";
export interface Artist {
type: string;
id: number;
path: string;
name: string;
sourceUrl: string;
sourceCountry: string;
url: string;
image: string;
createdAt: string;
updatedAt: string;
refreshedAt: string;
serviceIds: { [key: string]: string };
orchardId: string;
spotifyId: string;
links: { [key: string]: ServiceLinks[] };
linksCountries: string[];
description: string;
}

View File

@@ -0,0 +1,4 @@
export interface ServiceLinks {
link: string;
countries: string[];
}

View File

@@ -0,0 +1,27 @@
import { Artist } from "./Artist";
import { ServiceLinks } from "./ServiceLinks";
export interface WhippedResult {
status: string;
data: {
item: {
type: string;
id: number;
path: string;
name: string;
url: string;
sourceUrl: string;
sourceCountry: string;
releaseDate: string;
createdAt: string;
updatedAt: string;
refreshedAt: string;
image: string;
isrc: string;
isExplicit: boolean;
links: { [key: string]: ServiceLinks[] };
linksCountries: string[];
artists: Artist[];
};
};
}

View File

@@ -0,0 +1,32 @@
import { WhippedResult } from "./models/whip";
import axios from "axios";
export class Songwhip {
/**
* Call the songwhip API and create a shareable songwhip page
* @param currentUrl
* @returns
*/
public static async whip(currentUrl: string): Promise<WhippedResult> {
try {
const response = await axios.post("https://songwhip.com/api/songwhip/create", {
url: currentUrl,
// doesn't actually matter.. returns everything the same way anyway
country: "NL",
});
return response.data;
} catch (error) {
console.log(JSON.stringify(error));
}
}
/**
* Transform a songwhip response into a shareable url
* @param response
* @returns
*/
public static getWhipUrl(response: WhippedResult) {
return `https://songwhip.com${response.data.item.url}`;
}
}

View File

@@ -1,7 +1,7 @@
import { enable, initialize } from "@electron/remote/main";
import {
BrowserWindow,
app,
BrowserWindow,
components,
globalShortcut,
ipcMain,
@@ -9,9 +9,18 @@ import {
session,
} from "electron";
import path from "path";
import { flags } from "./constants/flags";
import { globalEvents } from "./constants/globalEvents";
import { mediaKeys } from "./constants/mediaKeys";
import { settings } from "./constants/settings";
import { setDefaultFlags, setManagedFlagsFromSettings } from "./features/flags/flags";
import {
acquireInhibitorIfInactive,
releaseInhibitorIfActive,
} from "./features/idleInhibitor/idleInhibitor";
import { Logger } from "./features/logger";
import { Songwhip } from "./features/songwhip/songwhip";
import { MediaInfo } from "./models/mediaInfo";
import { MediaStatus } from "./models/mediaStatus";
import { initRPC, rpc, unRPC } from "./scripts/discord";
import { startExpress } from "./scripts/express";
import { updateMediaInfo } from "./scripts/mediaInfo";
@@ -20,13 +29,12 @@ import {
closeSettingsWindow,
createSettingsWindow,
hideSettingsWindow,
showSettingsWindow,
settingsStore,
showSettingsWindow,
} from "./scripts/settings";
import { settings } from "./constants/settings";
import { addTray, refreshTray } from "./scripts/tray";
import { MediaInfo } from "./models/mediaInfo";
const tidalUrl = "https://listen.tidal.com";
let mainInhibitorId = -1;
initialize();
@@ -34,29 +42,11 @@ let mainWindow: BrowserWindow;
const icon = path.join(__dirname, "../assets/icon.png");
const PROTOCOL_PREFIX = "tidal";
setFlags();
function setFlags() {
const flagsFromSettings = settingsStore.get(settings.flags.root);
if (flagsFromSettings) {
for (const [key, value] of Object.entries(flags)) {
if (value) {
flags[key].forEach((flag) => {
console.log(`enabling command line switch ${flag.flag} with value ${flag.value}`);
app.commandLine.appendSwitch(flag.flag, flag.value);
});
}
}
}
/**
* Fix Display Compositor issue.
*/
app.commandLine.appendSwitch("disable-seccomp-filter-sandbox");
}
setDefaultFlags(app);
setManagedFlagsFromSettings(app);
/**
* Update the menuBarVisbility according to the store value
* Update the menuBarVisibility according to the store value
*
*/
function syncMenuBarWithStore() {
@@ -88,8 +78,8 @@ function createWindow(options = { x: 0, y: 0, backgroundColor: "white" }) {
mainWindow = new BrowserWindow({
x: options.x,
y: options.y,
width: settingsStore && settingsStore.get(settings.windowBounds.width),
height: settingsStore && settingsStore.get(settings.windowBounds.height),
width: settingsStore?.get(settings.windowBounds.width),
height: settingsStore?.get(settings.windowBounds.height),
icon,
backgroundColor: options.backgroundColor,
autoHideMenuBar: true,
@@ -122,6 +112,7 @@ function createWindow(options = { x: 0, y: 0, backgroundColor: "white" }) {
});
// Emitted when the window is closed.
mainWindow.on("closed", function () {
releaseInhibitorIfActive(mainInhibitorId);
closeSettingsWindow();
app.quit();
});
@@ -194,6 +185,12 @@ app.on("browser-window-created", (_, window) => {
// IPC
ipcMain.on(globalEvents.updateInfo, (_event, arg: MediaInfo) => {
updateMediaInfo(arg);
if (arg.status === MediaStatus.playing) {
mainInhibitorId = acquireInhibitorIfInactive(mainInhibitorId);
} else {
releaseInhibitorIfActive(mainInhibitorId);
mainInhibitorId = -1;
}
});
ipcMain.on(globalEvents.hideSettings, () => {
@@ -220,3 +217,9 @@ ipcMain.on(globalEvents.storeChanged, () => {
ipcMain.on(globalEvents.error, (event) => {
console.log(event);
});
ipcMain.handle(globalEvents.whip, async (event, url) => {
return Songwhip.whip(url);
});
Logger.watch(ipcMain);

View File

@@ -1,8 +1,9 @@
import remote, { app } from "@electron/remote";
import { app } from "@electron/remote";
import { ipcRenderer, shell } from "electron";
import fs from "fs";
import { globalEvents } from "../../constants/globalEvents";
import { settings } from "../../constants/settings";
import { Logger } from "../../features/logger";
import { settingsStore } from "./../../scripts/settings";
import { getOptions, getOptionsHeader, getThemeListFromDirectory } from "./theming";
@@ -25,7 +26,12 @@ let adBlock: HTMLInputElement,
skippedArtists: HTMLInputElement,
theme: HTMLSelectElement,
trayIcon: HTMLInputElement,
updateFrequency: HTMLInputElement;
updateFrequency: HTMLInputElement,
enableListenBrainz: HTMLInputElement,
ListenBrainzAPI: HTMLInputElement,
ListenBrainzToken: HTMLInputElement,
enableWaylandSupport: HTMLInputElement;
function getThemeFiles() {
const selectElement = document.getElementById("themesList") as HTMLSelectElement;
const builtInThemes = getThemeListFromDirectory(process.resourcesPath);
@@ -67,26 +73,34 @@ function handleFileUploads() {
* Sync the UI forms with the current settings
*/
function refreshSettings() {
adBlock.checked = settingsStore.get(settings.adBlock);
api.checked = settingsStore.get(settings.api);
customCSS.value = settingsStore.get<string, string[]>(settings.customCSS).join("\n");
disableBackgroundThrottle.checked = settingsStore.get(settings.disableBackgroundThrottle);
disableHardwareMediaKeys.checked = settingsStore.get(settings.flags.disableHardwareMediaKeys);
enableCustomHotkeys.checked = settingsStore.get(settings.enableCustomHotkeys);
enableDiscord.checked = settingsStore.get(settings.enableDiscord);
gpuRasterization.checked = settingsStore.get(settings.flags.gpuRasterization);
menuBar.checked = settingsStore.get(settings.menuBar);
minimizeOnClose.checked = settingsStore.get(settings.minimizeOnClose);
mpris.checked = settingsStore.get(settings.mpris);
notifications.checked = settingsStore.get(settings.notifications);
playBackControl.checked = settingsStore.get(settings.playBackControl);
port.value = settingsStore.get(settings.apiSettings.port);
singleInstance.checked = settingsStore.get(settings.singleInstance);
skipArtists.checked = settingsStore.get(settings.skipArtists);
theme.value = settingsStore.get(settings.theme);
skippedArtists.value = settingsStore.get<string, string[]>(settings.skippedArtists).join("\n");
trayIcon.checked = settingsStore.get(settings.trayIcon);
updateFrequency.value = settingsStore.get(settings.updateFrequency);
try {
adBlock.checked = settingsStore.get(settings.adBlock);
api.checked = settingsStore.get(settings.api);
customCSS.value = settingsStore.get<string, string[]>(settings.customCSS).join("\n");
disableBackgroundThrottle.checked = settingsStore.get(settings.disableBackgroundThrottle);
disableHardwareMediaKeys.checked = settingsStore.get(settings.flags.disableHardwareMediaKeys);
enableCustomHotkeys.checked = settingsStore.get(settings.enableCustomHotkeys);
enableDiscord.checked = settingsStore.get(settings.enableDiscord);
enableWaylandSupport.checked = settingsStore.get(settings.flags.enableWaylandSupport);
gpuRasterization.checked = settingsStore.get(settings.flags.gpuRasterization);
menuBar.checked = settingsStore.get(settings.menuBar);
minimizeOnClose.checked = settingsStore.get(settings.minimizeOnClose);
mpris.checked = settingsStore.get(settings.mpris);
notifications.checked = settingsStore.get(settings.notifications);
playBackControl.checked = settingsStore.get(settings.playBackControl);
port.value = settingsStore.get(settings.apiSettings.port);
singleInstance.checked = settingsStore.get(settings.singleInstance);
skipArtists.checked = settingsStore.get(settings.skipArtists);
theme.value = settingsStore.get(settings.theme);
skippedArtists.value = settingsStore.get<string, string[]>(settings.skippedArtists).join("\n");
trayIcon.checked = settingsStore.get(settings.trayIcon);
updateFrequency.value = settingsStore.get(settings.updateFrequency);
enableListenBrainz.checked = settingsStore.get(settings.ListenBrainz.enabled);
ListenBrainzAPI.value = settingsStore.get(settings.ListenBrainz.api);
ListenBrainzToken.value = settingsStore.get(settings.ListenBrainz.token);
} catch (error) {
Logger.log("Refreshing settings failed.", error);
}
}
/**
@@ -107,8 +121,8 @@ function hide() {
* Restart tidal-hifi after changes
*/
function restart() {
remote.app.relaunch();
remote.app.exit();
app.relaunch();
app.exit();
}
/**
@@ -137,6 +151,12 @@ window.addEventListener("DOMContentLoaded", () => {
} else {
settingsStore.set(key, source.value);
}
// Live update the view for ListenBrainz input, hide if disabled/show if enabled
if (source.value === "on" && source.id === "enableListenBrainz") {
source.checked
? document.getElementById("listenbrainz__options").removeAttribute("hidden")
: document.getElementById("listenbrainz__options").setAttribute("hidden", "true");
}
ipcRenderer.send(globalEvents.storeChanged);
});
}
@@ -170,6 +190,7 @@ window.addEventListener("DOMContentLoaded", () => {
disableHardwareMediaKeys = get("disableHardwareMediaKeys");
enableCustomHotkeys = get("enableCustomHotkeys");
enableDiscord = get("enableDiscord");
enableWaylandSupport = get("enableWaylandSupport");
gpuRasterization = get("gpuRasterization");
menuBar = get("menuBar");
minimizeOnClose = get("minimizeOnClose");
@@ -183,8 +204,14 @@ window.addEventListener("DOMContentLoaded", () => {
skippedArtists = get("skippedArtists");
singleInstance = get("singleInstance");
updateFrequency = get("updateFrequency");
enableListenBrainz = get("enableListenBrainz");
ListenBrainzAPI = get("ListenBrainzAPI");
ListenBrainzToken = get("ListenBrainzToken");
refreshSettings();
enableListenBrainz.checked
? document.getElementById("listenbrainz__options").removeAttribute("hidden")
: document.getElementById("listenbrainz__options").setAttribute("hidden", "true");
addInputListener(adBlock, settings.adBlock);
addInputListener(api, settings.api);
@@ -193,6 +220,7 @@ window.addEventListener("DOMContentLoaded", () => {
addInputListener(disableHardwareMediaKeys, settings.flags.disableHardwareMediaKeys);
addInputListener(enableCustomHotkeys, settings.enableCustomHotkeys);
addInputListener(enableDiscord, settings.enableDiscord);
addInputListener(enableWaylandSupport, settings.flags.enableWaylandSupport);
addInputListener(gpuRasterization, settings.flags.gpuRasterization);
addInputListener(menuBar, settings.menuBar);
addInputListener(minimizeOnClose, settings.minimizeOnClose);
@@ -206,4 +234,7 @@ window.addEventListener("DOMContentLoaded", () => {
addSelectListener(theme, settings.theme);
addInputListener(trayIcon, settings.trayIcon);
addInputListener(updateFrequency, settings.updateFrequency);
addInputListener(enableListenBrainz, settings.ListenBrainz.enabled);
addTextAreaListener(ListenBrainzAPI, settings.ListenBrainz.api);
addTextAreaListener(ListenBrainzToken, settings.ListenBrainz.token);
});

View File

@@ -212,6 +212,35 @@
</label>
</div>
</div>
<div class="group">
<p class="group__title">ListenBrainz</p>
<div class="group__option">
<div class="group__description">
<h4>Enable ListenBrainz</h4>
<p>Scrobble your listens directly to ListenBrainz.</p>
</div>
<label class="switch">
<input id="enableListenBrainz" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div id="listenbrainz__options" hidden="true">
<div class="group__option">
<div class="group__description">
<h4>ListenBrainz API Url</h4>
<p>There are multiple instances for ListenBrainz you can set the corresponding API url below.</p>
</div>
</div>
<textarea id="ListenBrainzAPI" class="textarea" cols="1" rows="1" spellcheck="false"></textarea>
<div class="group__option">
<div class="group__description">
<h4>ListenBrainz User Token</h4>
<p>Provide the user token you can get from the settings page.</p>
</div>
</div>
<textarea id="ListenBrainzToken" class="textarea" cols="1" rows="1" spellcheck="false"></textarea>
</div>
</div>
</section>
<section id="advanced-section" class="tabs__section">
@@ -229,44 +258,57 @@
<input id="updateFrequency" type="number" class="text-input" name="updateFrequency" />
</div>
</div>
<div class="group">
<p class="group__title">Flags</p>
<div class="group__option">
<div class="group__description">
<h4>Disable hardware built-in media keys</h4>
<p>
Also prevents certain desktop environments from recognizing the chrome MPRIS
client separately from the custom MPRIS client.
</p>
</div>
<label class="switch">
<input id="disableHardwareMediaKeys" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div class="group__option">
<div class="group__description">
<h4>Enable GPU rasterization</h4>
<p>Move a part of the rendering to the GPU for increased performance.</p>
</div>
<label class="switch">
<input id="gpuRasterization" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div class="group__option">
<div class="group__description">
<h4>Disable Background Throttling</h4>
<p>
Makes app more responsive while in the background, at the cost of performance.
</p>
</div>
<label class="switch">
<input id="disableBackgroundThrottle" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div class="group">
<p class="group__title">Flags</p>
<div class="group__option">
<div class="group__description">
<h4>Disable hardware built-in media keys</h4>
<p>
Also prevents certain desktop environments from recognizing the chrome MPRIS
client separately from the custom MPRIS client.
</p>
</div>
<label class="switch">
<input id="disableHardwareMediaKeys" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div class="group__option">
<div class="group__description">
<h4>Enable GPU rasterization</h4>
<p>Move a part of the rendering to the GPU for increased performance.</p>
</div>
<label class="switch">
<input id="gpuRasterization" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div class="group__option">
<div class="group__description">
<h4>Disable Background Throttling</h4>
<p>
Makes app more responsive while in the background, at the cost of performance.
</p>
</div>
<label class="switch">
<input id="disableBackgroundThrottle" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
<div class="group__option">
<div class="group__description">
<h4>Wayland support</h4>
<p>
Adds a couple of Electron flags to help Tidal-hifi run smoothly on the Wayland window system.
</p>
</div>
<label class="switch">
<input id="enableWaylandSupport" type="checkbox" />
<span class="switch__slider"></span>
</label>
</div>
</div>
</section>
<section id="theming-section" class="tabs__section">

View File

@@ -1,4 +1,5 @@
import fs from "fs";
import { Logger } from "../../features/logger";
const cssFilter = (file: string) => file.endsWith(".css");
const sort = (a: string, b: string) => a.toLowerCase().localeCompare(b.toLowerCase());
@@ -36,7 +37,7 @@ export const getThemeListFromDirectory = (directory: string): string[] => {
makeUserThemesDirectory(directory);
return fs.readdirSync(directory).filter(cssFilter).sort(sort);
} catch (err) {
console.error(err);
Logger.log(`Failed to get files from ${directory}`, err);
return [];
}
};
@@ -49,6 +50,6 @@ export const makeUserThemesDirectory = (directory: string) => {
try {
fs.mkdirSync(directory, { recursive: true });
} catch (err) {
console.error(err);
Logger.log(`Failed to make user theme directory: ${directory}`, err);
}
};

View File

@@ -1,21 +1,29 @@
import { Notification, app, dialog } from "@electron/remote";
import { ipcRenderer } from "electron";
import { app, dialog, Notification } from "@electron/remote";
import { clipboard, ipcRenderer } from "electron";
import fs from "fs";
import Player from "mpris-service";
import { globalEvents } from "./constants/globalEvents";
import { settings } from "./constants/settings";
import { statuses } from "./constants/statuses";
import {
ListenBrainz,
ListenBrainzConstants,
ListenBrainzStore,
} from "./features/listenbrainz/listenbrainz";
import { StoreData } from "./features/listenbrainz/models/storeData";
import { Logger } from "./features/logger";
import { Songwhip } from "./features/songwhip/songwhip";
import { MediaStatus } from "./models/mediaStatus";
import { Options } from "./models/options";
import { downloadFile } from "./scripts/download";
import { addHotkey } from "./scripts/hotkeys";
import { settingsStore } from "./scripts/settings";
import { setTitle } from "./scripts/window-functions";
const notificationPath = `${app.getPath("userData")}/notification.jpg`;
const appName = "Tidal Hifi";
let currentSong = "";
let player: Player;
let currentPlayStatus = statuses.paused;
let currentPlayStatus = MediaStatus.paused;
const elements = {
play: '*[data-test="play"]',
@@ -25,13 +33,12 @@ const elements = {
title: '*[data-test^="footer-track-title"]',
artists: '*[data-test^="grid-item-detail-text-title-artist"]',
home: '*[data-test="menu--home"]',
back: '[class^="backwardButton"]',
forward: '[class^="forwardButton"]',
back: '[title^="Back"]',
forward: '[title^="Next"]',
search: '[class^="searchField"]',
shuffle: '*[data-test="shuffle"]',
repeat: '*[data-test="repeat"]',
block: '[class="blockButton"]',
account: '*[data-test^="profile-image-button"]',
account: '*[class^="profileOptions"]',
settings: '*[data-test^="open-settings"]',
media: '*[data-test="current-media-imagery"]',
image: "img",
@@ -39,9 +46,10 @@ const elements = {
duration: '*[data-test="duration"]',
bar: '*[data-test="progress-bar"]',
footer: "#footerPlayer",
mediaItem: "[data-type='mediaItem']",
album_header_title: '.header-details [data-test="title"]',
playing_title: 'span[data-test="table-cell-title"].css-1vjc1xk',
album_name_cell: '[data-test="table-cell-album"]',
currentlyPlaying: "[class^='isPlayingIcon'], [data-test-is-playing='true']",
album_name_cell: '[class^="album"]',
tracklist_row: '[data-test="tracklist-row"]',
volume: '*[data-test="volume"]',
/**
@@ -104,8 +112,10 @@ const elements = {
window.location.href.includes("/playlist/") ||
window.location.href.includes("/mix/")
) {
if (currentPlayStatus === statuses.playing) {
const row = window.document.querySelector(this.playing_title).closest(this.tracklist_row);
if (currentPlayStatus === MediaStatus.playing) {
// find the currently playing element from the list (which might be in an album icon), traverse back up to the mediaItem (row) and select the album cell.
// document.querySelector("[class^='isPlayingIcon'], [data-test-is-playing='true']").closest('[data-type="mediaItem"]').querySelector('[class^="album"]').textContent
const row = window.document.querySelector(this.currentlyPlaying).closest(this.mediaItem);
if (row) {
return row.querySelector(this.album_name_cell).textContent;
}
@@ -148,12 +158,14 @@ const elements = {
function addCustomCss() {
window.addEventListener("DOMContentLoaded", () => {
const selectedTheme = settingsStore.get(settings.theme);
const selectedTheme = settingsStore.get<string, string>(settings.theme);
if (selectedTheme !== "none") {
const themeFile = `${process.resourcesPath}/${selectedTheme}`;
const userThemePath = `${app.getPath("userData")}/themes/${selectedTheme}`;
const resourcesThemePath = `${process.resourcesPath}/${selectedTheme}`;
const themeFile = fs.existsSync(userThemePath) ? userThemePath : resourcesThemePath;
fs.readFile(themeFile, "utf-8", (err, data) => {
if (err) {
alert("An error ocurred reading the theme file.");
Logger.alert("An error ocurred reading the theme file.", err, alert);
return;
}
@@ -175,7 +187,7 @@ function addCustomCss() {
* make sure it returns a number, if not use the default
*/
function getUpdateFrequency() {
const storeValue = settingsStore.get(settings.updateFrequency) as number;
const storeValue = settingsStore.get<string, number>(settings.updateFrequency);
const defaultValue = 500;
if (!isNaN(storeValue)) {
@@ -198,6 +210,11 @@ function playPause() {
}
}
/**
* Clears the old listenbrainz data on launch
*/
ListenBrainzStore.clear();
/**
* Add hotkeys for when tidal is focused
* Reflects the desktop hotkeys found on:
@@ -206,7 +223,10 @@ function playPause() {
function addHotKeys() {
if (settingsStore.get(settings.enableCustomHotkeys)) {
addHotkey("Control+p", function () {
elements.click("account").click("settings");
elements.click("account");
setTimeout(() => {
elements.click("settings");
}, 100);
});
addHotkey("Control+l", function () {
handleLogout();
@@ -232,6 +252,15 @@ function addHotKeys() {
addHotkey("control+r", function () {
elements.click("repeat");
});
addHotkey("control+w", async function () {
const result = await ipcRenderer.invoke(globalEvents.whip, getTrackURL());
const url = Songwhip.getWhipUrl(result);
clipboard.writeText(url);
new Notification({
title: `Successfully whipped: `,
body: `URL copied to clipboard: ${url}`,
}).show();
});
}
// always add the hotkey for the settings window
@@ -259,7 +288,7 @@ function handleLogout() {
defaultId: 2,
})
.then((result: { response: number }) => {
if (logoutOptions.indexOf("Yes, please") == result.response) {
if (logoutOptions.indexOf("Yes, please") === result.response) {
for (let i = 0; i < window.localStorage.length; i++) {
const key = window.localStorage.key(i);
if (key.startsWith("_TIDAL_activeSession")) {
@@ -301,6 +330,8 @@ function addIPCEventListeners() {
case globalEvents.pause:
elements.click("pause");
break;
default:
break;
}
});
});
@@ -315,9 +346,9 @@ function getCurrentlyPlayingStatus() {
// if pause button is visible tidal is playing
if (pause) {
status = statuses.playing;
status = MediaStatus.playing;
} else {
status = statuses.paused;
status = MediaStatus.paused;
}
return status;
}
@@ -342,19 +373,41 @@ function updateMediaInfo(options: Options, notify: boolean) {
if (settingsStore.get(settings.notifications) && notify) {
new Notification({ title: options.title, body: options.artists, icon: options.icon }).show();
}
if (player) {
player.metadata = {
...player.metadata,
...{
"xesam:title": options.title,
"xesam:artist": [options.artists],
"xesam:album": options.album,
"mpris:artUrl": options.image,
"mpris:length": convertDuration(options.duration) * 1000 * 1000,
"mpris:trackid": "/org/mpris/MediaPlayer2/track/" + getTrackID(),
},
};
player.playbackStatus = options.status == statuses.paused ? "Paused" : "Playing";
updateMpris(options);
updateListenBrainz(options);
}
}
function updateMpris(options: Options) {
if (player) {
player.metadata = {
...player.metadata,
...{
"xesam:title": options.title,
"xesam:artist": [options.artists],
"xesam:album": options.album,
"mpris:artUrl": options.image,
"mpris:length": convertDuration(options.duration) * 1000 * 1000,
"mpris:trackid": "/org/mpris/MediaPlayer2/track/" + getTrackID(),
},
};
player.playbackStatus = options.status === MediaStatus.paused ? "Paused" : "Playing";
}
}
function updateListenBrainz(options: Options) {
if (settingsStore.get(settings.ListenBrainz.enabled)) {
const oldData = ListenBrainzStore.get(ListenBrainzConstants.oldData) as StoreData;
if (
(!oldData && options.status === MediaStatus.playing) ||
(oldData && oldData.title !== options.title)
) {
ListenBrainz.scrobble(
options.title,
options.artists,
options.status,
convertDuration(options.duration)
);
}
}
}

View File

@@ -1,11 +1,11 @@
import { BrowserWindow, dialog } from "electron";
import express, { Response } from "express";
import fs from "fs";
import { settings } from "../constants/settings";
import { MediaStatus } from "../models/mediaStatus";
import { globalEvents } from "./../constants/globalEvents";
import { statuses } from "./../constants/statuses";
import { mediaInfo } from "./mediaInfo";
import { settingsStore } from "./settings";
import { settings } from "../constants/settings";
/**
* Function to enable tidal-hifi's express api
@@ -44,7 +44,7 @@ export const startExpress = (mainWindow: BrowserWindow) => {
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) {
if (mediaInfo.status === MediaStatus.playing) {
handleGlobalEvent(res, globalEvents.pause);
} else {
handleGlobalEvent(res, globalEvents.play);

View File

@@ -1,12 +1,12 @@
import { MediaInfo } from "../models/mediaInfo";
import { statuses } from "./../constants/statuses";
import { MediaStatus } from "../models/mediaStatus";
export const mediaInfo = {
title: "",
artists: "",
album: "",
icon: "",
status: statuses.paused,
status: MediaStatus.paused as string,
url: "",
current: "",
duration: "",

View File

@@ -18,9 +18,15 @@ export const settingsStore = new Store({
disableHardwareMediaKeys: false,
enableCustomHotkeys: false,
enableDiscord: false,
ListenBrainz: {
enabled: false,
api: "https://api.listenbrainz.org",
token: "",
},
flags: {
gpuRasterization: true,
disableHardwareMediaKeys: false,
enableWaylandSupport: true,
gpuRasterization: true,
},
menuBar: true,
minimizeOnClose: false,

View File

@@ -1,8 +1,9 @@
{
"compilerOptions": {
"typeRoots": ["src/types"],
"typeRoots": ["src/types", "node_modules/@types"],
"module": "commonjs",
"target": "ES6",
"lib": ["ES2020", "DOM"],
"noImplicitAny": true,
"sourceMap": true,
"allowJs": true,