Compare commits
50 Commits
Author | SHA1 | Date | |
---|---|---|---|
6e43cbb4d7 | |||
f95f13b44a | |||
f911564d8a | |||
db8a2c2741 | |||
000853414e | |||
53603c4cad | |||
0b595f920f | |||
81143af3fa | |||
|
8d1ac3be3b | ||
|
666e602c02 | ||
|
04ec850005 | ||
943d9b5bd8 | |||
|
755816c2b8 | ||
25afd05ad7 | |||
6e5024742a | |||
417afaab85 | |||
d225c0056b | |||
|
a75b0336db | ||
|
29465ce13a | ||
|
d333047269 | ||
|
712330f8f1 | ||
|
84fd35ce0e | ||
|
326038f262 | ||
a6c1d35a60 | |||
|
c09a4bc4a8 | ||
|
554cb12a01 | ||
2e31b5d913 | |||
2fd29c1b83 | |||
b2f27a2afe | |||
8e11fd7f09 | |||
17b2818b70 | |||
4ef76c262e | |||
fd0dae2762 | |||
aa59bdc6dd | |||
5b5b6ecb38 | |||
5983145857 | |||
0c7d579951 | |||
|
235d916749 | ||
|
2d9f268866 | ||
ae65e57e32 | |||
|
3f2d69f2f4 | ||
5ff2cc68d3 | |||
daabe5bdbb | |||
|
456727c0e0 | ||
|
ba50e0c095 | ||
|
312e90e8cb | ||
76769dfab3 | |||
|
565d32ae3d | ||
7be6f79040 | |||
|
f894c82b12 |
29
CHANGELOG.md
@@ -4,6 +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.11.0]
|
||||
|
||||
- Re-implemented the API, added support for duration/current in seconds & shuffle+repeat
|
||||
- made the original API "legacy" (still works the same)
|
||||
- Now using the correct HTTP verb for all new endpoints
|
||||
- Implemented TIDAL's universal links. All links are now universal.
|
||||
- Custom `tidal://` protocol fixed - By [TheRockYT](https://github.com/TheRockYT)
|
||||
- Global media shortcuts removed since TIDAL includes them by default - By [TheRockYT](https://github.com/TheRockYT)
|
||||
|
||||
- Fixes
|
||||
- [#390](https://github.com/Mastermindzh/tidal-hifi/issues/390)
|
||||
- [#376](https://github.com/Mastermindzh/tidal-hifi/issues/376)
|
||||
- [#383](https://github.com/Mastermindzh/tidal-hifi/issues/383)
|
||||
- [#393](https://github.com/Mastermindzh/tidal-hifi/issues/393)
|
||||
|
||||
## [5.10.0]
|
||||
|
||||
- TIDAL will now close the previous notification if a new one is sent whilst the old is still visible. [#364](https://github.com/Mastermindzh/tidal-hifi/pull/364)
|
||||
- Updated developer documentation to get started in README [#365](https://github.com/Mastermindzh/tidal-hifi/pull/365)
|
||||
- Links in the about window now open in the user's default browser. fixes [#360](https://github.com/Mastermindzh/tidal-hifi/issues/360)
|
||||
- Refactored "nowPlaying" code to always display the current state, even when the built-in UI is updated.
|
||||
- fixes [#351](https://github.com/Mastermindzh/tidal-hifi/issues/351)
|
||||
- fixes [#356](https://github.com/Mastermindzh/tidal-hifi/issues/356)
|
||||
- fixes [#370](https://github.com/Mastermindzh/tidal-hifi/issues/370)
|
||||
- Reverted to using old icon syntax with icons in the build directory. fixes [#350](https://github.com/Mastermindzh/tidal-hifi/issues/350)
|
||||
- Enabled wayland platform flags by default when launching through .desktop file
|
||||
- fixes [#273](https://github.com/Mastermindzh/tidal-hifi/issues/273)
|
||||
- fixes [#347](https://github.com/Mastermindzh/tidal-hifi/issues/347)
|
||||
|
||||
## [5.9.0]
|
||||
|
||||
- More Discord options:
|
||||
|
11
README.md
@@ -130,10 +130,13 @@ nix-env -iA nixpkgs.tidal-hifi
|
||||
|
||||
To install and work with the code on this project follow these steps:
|
||||
|
||||
- git clone [https://github.com/Mastermindzh/tidal-hifi.git](https://github.com/Mastermindzh/tidal-hifi.git)
|
||||
- cd TIDAL Hi-Fi
|
||||
- npm install
|
||||
- npm start
|
||||
- `git clone [https://github.com/Mastermindzh/tidal-hifi.git](https://github.com/Mastermindzh/tidal-hifi.git)`
|
||||
- `cd tidal-hifi`
|
||||
- `npm install`
|
||||
- `npm run watch` to watch for auto-reload of Typescript/SCSS changes.
|
||||
- `npm run compile` can be used to trigger it once
|
||||
- `npm watchStart` to auto watch for any updates files and reload Tidal Hi-Fi
|
||||
- `npm start` can be used to run Tidal Hi-Fi manually once
|
||||
|
||||
## Integrations
|
||||
|
||||
|
@@ -11,10 +11,16 @@ extraResources:
|
||||
- "themes/**"
|
||||
linux:
|
||||
category: AudioVideo
|
||||
icon: build/icons/256x256.png
|
||||
icon: build/icons
|
||||
target:
|
||||
- dir
|
||||
executableName: tidal-hifi
|
||||
executableArgs:
|
||||
[
|
||||
"--enable-features=UseOzonePlatform",
|
||||
"--ozone-platform-hint=auto",
|
||||
"--enable-features=WaylandWindowDecorations",
|
||||
]
|
||||
desktop:
|
||||
Encoding: UTF-8
|
||||
Name: TIDAL Hi-Fi
|
||||
|
BIN
build/icons/128x128.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
build/icons/16x16.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
build/icons/22x22.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
build/icons/24x24.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
build/icons/256x256 copy.png
Normal file
After Width: | Height: | Size: 9.0 KiB |
BIN
build/icons/32x32.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
build/icons/384x384.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
build/icons/48x48.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
build/icons/64x64.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
684
package-lock.json
generated
14
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tidal-hifi",
|
||||
"version": "5.9.0",
|
||||
"version": "5.11.0",
|
||||
"description": "Tidal on Electron with widevine(hifi) support",
|
||||
"main": "ts-dist/main.js",
|
||||
"scripts": {
|
||||
@@ -39,15 +39,15 @@
|
||||
"homepage": "https://github.com/Mastermindzh/tidal-hifi",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron/remote": "^2.1.1",
|
||||
"axios": "^1.6.5",
|
||||
"@electron/remote": "^2.1.2",
|
||||
"axios": "^1.6.8",
|
||||
"discord-rpc": "^4.0.1",
|
||||
"electron-store": "^8.1.0",
|
||||
"express": "^4.18.2",
|
||||
"hotkeys-js": "^3.13.5",
|
||||
"electron-store": "^8.2.0",
|
||||
"express": "^4.19.2",
|
||||
"hotkeys-js": "^3.13.7",
|
||||
"mpris-service": "^2.1.2",
|
||||
"request": "^2.88.2",
|
||||
"sass": "^1.70.0"
|
||||
"sass": "^1.75.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mastermindzh/prettier-config": "^1.0.0",
|
||||
|
@@ -13,4 +13,6 @@ export const globalEvents = {
|
||||
whip: "whip",
|
||||
log: "log",
|
||||
toggleFavorite: "toggleFavorite",
|
||||
toggleShuffle: "toggleShuffle",
|
||||
toggleRepeat: "toggleRepeat",
|
||||
};
|
||||
|
20
src/features/api/features/current.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import fs from "fs";
|
||||
import { mediaInfo } from "../../../scripts/mediaInfo";
|
||||
|
||||
export const addCurrentInfo = (expressApp: Router) => {
|
||||
expressApp.get("/current", (req, res) => res.json({ ...mediaInfo, artist: mediaInfo.artists }));
|
||||
expressApp.get("/current/image", getCurrentImage);
|
||||
};
|
||||
|
||||
export const getCurrentImage = (req: Request, res: Response) => {
|
||||
const 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");
|
||||
});
|
||||
};
|
36
src/features/api/features/player.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { BrowserWindow } from "electron";
|
||||
import { Router } from "express";
|
||||
import { globalEvents } from "../../../constants/globalEvents";
|
||||
import { settings } from "../../../constants/settings";
|
||||
import { MediaStatus } from "../../../models/mediaStatus";
|
||||
import { mediaInfo } from "../../../scripts/mediaInfo";
|
||||
import { settingsStore } from "../../../scripts/settings";
|
||||
import { handleWindowEvent } from "../helpers/handleWindowEvent";
|
||||
|
||||
export const addPlaybackControl = (expressApp: Router, mainWindow: BrowserWindow) => {
|
||||
const windowEvent = handleWindowEvent(mainWindow);
|
||||
const createRoute = (route: string) => `/player${route}`;
|
||||
|
||||
const createPlayerAction = (route: string, action: string) => {
|
||||
expressApp.post(createRoute(route), (req, res) => windowEvent(res, action));
|
||||
};
|
||||
|
||||
if (settingsStore.get(settings.playBackControl)) {
|
||||
createPlayerAction("/play", globalEvents.play);
|
||||
createPlayerAction("/favorite/toggle", globalEvents.toggleFavorite);
|
||||
createPlayerAction("/pause", globalEvents.pause);
|
||||
createPlayerAction("/next", globalEvents.next);
|
||||
createPlayerAction("/previous", globalEvents.previous);
|
||||
createPlayerAction("/shuffle/toggle", globalEvents.toggleShuffle);
|
||||
createPlayerAction("/repeat/toggle", globalEvents.toggleRepeat);
|
||||
|
||||
expressApp.post(createRoute("/playpause"), (req, res) => {
|
||||
if (mediaInfo.status === MediaStatus.playing) {
|
||||
windowEvent(res, globalEvents.pause);
|
||||
} else {
|
||||
windowEvent(res, globalEvents.play);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
12
src/features/api/helpers/handleWindowEvent.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { BrowserWindow } from "electron";
|
||||
import { Response } from "express";
|
||||
|
||||
/**
|
||||
* Shorthand to handle a fire and forget global event
|
||||
* @param {*} res
|
||||
* @param {*} action
|
||||
*/
|
||||
export const handleWindowEvent = (mainWindow: BrowserWindow) => (res: Response, action: string) => {
|
||||
mainWindow.webContents.send("globalEvent", action);
|
||||
res.sendStatus(200);
|
||||
};
|
31
src/features/api/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { BrowserWindow, dialog } from "electron";
|
||||
import express from "express";
|
||||
import { settings } from "../../constants/settings";
|
||||
import { settingsStore } from "../../scripts/settings";
|
||||
import { addCurrentInfo } from "./features/current";
|
||||
import { addPlaybackControl } from "./features/player";
|
||||
import { addLegacyApi } from "./legacy";
|
||||
|
||||
/**
|
||||
* Function to enable TIDAL Hi-Fi's express api
|
||||
*/
|
||||
export const startApi = (mainWindow: BrowserWindow) => {
|
||||
const expressApp = express();
|
||||
expressApp.get("/", (req, res) => res.send("Hello World!"));
|
||||
|
||||
// add features
|
||||
addLegacyApi(expressApp, mainWindow);
|
||||
addPlaybackControl(expressApp, mainWindow);
|
||||
addCurrentInfo(expressApp);
|
||||
|
||||
const port = settingsStore.get<string, number>(settings.apiSettings.port);
|
||||
const expressInstance = expressApp.listen(port, "127.0.0.1");
|
||||
expressInstance.on("error", function (e: { code: string }) {
|
||||
let message = e.code;
|
||||
if (e.code === "EADDRINUSE") {
|
||||
message = `Port ${port} in use.`;
|
||||
}
|
||||
|
||||
dialog.showErrorBox("Api failed to start.", message);
|
||||
});
|
||||
};
|
47
src/features/api/legacy.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { BrowserWindow } from "electron";
|
||||
import { Response, Router } from "express";
|
||||
import { globalEvents } from "../../constants/globalEvents";
|
||||
import { settings } from "../../constants/settings";
|
||||
import { MediaStatus } from "../../models/mediaStatus";
|
||||
import { mediaInfo } from "../../scripts/mediaInfo";
|
||||
import { settingsStore } from "../../scripts/settings";
|
||||
import { getCurrentImage } from "./features/current";
|
||||
|
||||
/**
|
||||
* The legacy API, this will not be maintained and probably has duplicate code :)
|
||||
* @param expressApp
|
||||
* @param mainWindow
|
||||
*/
|
||||
export const addLegacyApi = (expressApp: Router, mainWindow: BrowserWindow) => {
|
||||
expressApp.get("/image", getCurrentImage);
|
||||
|
||||
if (settingsStore.get(settings.playBackControl)) {
|
||||
addLegacyControls();
|
||||
}
|
||||
function addLegacyControls() {
|
||||
expressApp.get("/play", ({ res }) => handleGlobalEvent(res, globalEvents.play));
|
||||
expressApp.post("/favorite/toggle", (req, res) =>
|
||||
handleGlobalEvent(res, globalEvents.toggleFavorite)
|
||||
);
|
||||
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 === MediaStatus.playing) {
|
||||
handleGlobalEvent(res, globalEvents.pause);
|
||||
} else {
|
||||
handleGlobalEvent(res, globalEvents.play);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand to handle a fire and forget global event
|
||||
* @param {*} res
|
||||
* @param {*} action
|
||||
*/
|
||||
function handleGlobalEvent(res: Response, action: string) {
|
||||
mainWindow.webContents.send("globalEvent", action);
|
||||
res.sendStatus(200);
|
||||
}
|
||||
};
|
@@ -9,7 +9,6 @@ import { Logger } from "../logger";
|
||||
*/
|
||||
export function setDefaultFlags(app: App) {
|
||||
setFlag(app, "disable-seccomp-filter-sandbox");
|
||||
setFlag(app, "disable-features", "MediaSessionService");
|
||||
}
|
||||
|
||||
/**
|
||||
|
14
src/features/time/parse.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Convert a HH:MM:SS string (or variants such as MM:SS or SS) to plain seconds
|
||||
* @param duration in HH:MM:SS format
|
||||
* @returns number of seconds in duration
|
||||
*/
|
||||
export const convertDurationToSeconds = (duration: string) => {
|
||||
return duration
|
||||
.split(":")
|
||||
.reverse()
|
||||
.map((val) => Number(val))
|
||||
.reduce((previous, current, index) => {
|
||||
return index === 0 ? current : previous + current * Math.pow(60, index);
|
||||
}, 0);
|
||||
};
|
90
src/main.ts
@@ -1,17 +1,9 @@
|
||||
import { enable, initialize } from "@electron/remote/main";
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
components,
|
||||
globalShortcut,
|
||||
ipcMain,
|
||||
protocol,
|
||||
session,
|
||||
} from "electron";
|
||||
import { BrowserWindow, app, components, ipcMain, session } from "electron";
|
||||
import path from "path";
|
||||
import { globalEvents } from "./constants/globalEvents";
|
||||
import { mediaKeys } from "./constants/mediaKeys";
|
||||
import { settings } from "./constants/settings";
|
||||
import { startApi } from "./features/api";
|
||||
import { setDefaultFlags, setManagedFlagsFromSettings } from "./features/flags/flags";
|
||||
import {
|
||||
acquireInhibitorIfInactive,
|
||||
@@ -22,7 +14,6 @@ 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";
|
||||
import { addMenu } from "./scripts/menu";
|
||||
import {
|
||||
@@ -61,20 +52,31 @@ function syncMenuBarWithStore() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the current window is the main window
|
||||
* if singleInstance is requested.
|
||||
* If singleInstance isn't requested simply return true
|
||||
* @returns true if singInstance is not requested, otherwise true/false based on whether the current window is the main window
|
||||
* @returns true/false based on whether the current window is the main window
|
||||
*/
|
||||
function isMainInstanceOrMultipleInstancesAllowed() {
|
||||
if (settingsStore.get(settings.singleInstance)) {
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
function isMainInstance() {
|
||||
return app.requestSingleInstanceLock();
|
||||
}
|
||||
|
||||
if (!gotTheLock) {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* @returns true/false based on whether multiple instances are allowed
|
||||
*/
|
||||
function isMultipleInstancesAllowed() {
|
||||
return !settingsStore.get(settings.singleInstance);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param args the arguments passed to the app
|
||||
* @returns the custom protocol url if it exists, otherwise null
|
||||
*/
|
||||
function getCustomProtocolUrl(args: string[]) {
|
||||
const customProtocolArg = args.find((arg) => arg.startsWith(PROTOCOL_PREFIX));
|
||||
|
||||
if (!customProtocolArg) {
|
||||
return null;
|
||||
}
|
||||
return true;
|
||||
|
||||
return tidalUrl + "/" + customProtocolArg.substring(PROTOCOL_PREFIX.length + 3);
|
||||
}
|
||||
|
||||
function createWindow(options = { x: 0, y: 0, backgroundColor: "white" }) {
|
||||
@@ -98,8 +100,16 @@ function createWindow(options = { x: 0, y: 0, backgroundColor: "white" }) {
|
||||
registerHttpProtocols();
|
||||
syncMenuBarWithStore();
|
||||
|
||||
// load the Tidal website
|
||||
mainWindow.loadURL(tidalUrl);
|
||||
// find the custom protocol argument
|
||||
const customProtocolUrl = getCustomProtocolUrl(process.argv);
|
||||
|
||||
if (customProtocolUrl) {
|
||||
// load the url received from the custom protocol
|
||||
mainWindow.loadURL(customProtocolUrl);
|
||||
} else {
|
||||
// load the Tidal website
|
||||
mainWindow.loadURL(tidalUrl);
|
||||
}
|
||||
|
||||
if (settingsStore.get(settings.disableBackgroundThrottle)) {
|
||||
// prevent setInterval lag
|
||||
@@ -139,27 +149,32 @@ function createWindow(options = { x: 0, y: 0, backgroundColor: "white" }) {
|
||||
}
|
||||
|
||||
function registerHttpProtocols() {
|
||||
protocol.registerHttpProtocol(PROTOCOL_PREFIX, (request) => {
|
||||
mainWindow.loadURL(`${tidalUrl}/${request.url.substring(PROTOCOL_PREFIX.length + 3)}`);
|
||||
});
|
||||
if (!app.isDefaultProtocolClient(PROTOCOL_PREFIX)) {
|
||||
app.setAsDefaultProtocolClient(PROTOCOL_PREFIX);
|
||||
}
|
||||
}
|
||||
|
||||
function addGlobalShortcuts() {
|
||||
Object.keys(mediaKeys).forEach((key) => {
|
||||
globalShortcut.register(`${key}`, () => {
|
||||
mainWindow.webContents.send("globalEvent", `${(mediaKeys as any)[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", async () => {
|
||||
if (isMainInstanceOrMultipleInstancesAllowed()) {
|
||||
// check if the app is the main instance and multiple instances are not allowed
|
||||
if (isMainInstance() && !isMultipleInstancesAllowed()) {
|
||||
app.on("second-instance", (_, commandLine) => {
|
||||
const customProtocolUrl = getCustomProtocolUrl(commandLine);
|
||||
|
||||
if (customProtocolUrl) {
|
||||
mainWindow.loadURL(customProtocolUrl);
|
||||
}
|
||||
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||
mainWindow.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (isMainInstance() || isMultipleInstancesAllowed()) {
|
||||
await components.whenReady();
|
||||
|
||||
// Adblock
|
||||
@@ -174,12 +189,11 @@ app.on("ready", async () => {
|
||||
createWindow();
|
||||
addMenu(mainWindow);
|
||||
createSettingsWindow();
|
||||
addGlobalShortcuts();
|
||||
if (settingsStore.get(settings.trayIcon)) {
|
||||
addTray(mainWindow, { icon });
|
||||
refreshTray(mainWindow);
|
||||
}
|
||||
settingsStore.get(settings.api) && startExpress(mainWindow);
|
||||
settingsStore.get(settings.api) && startApi(mainWindow);
|
||||
settingsStore.get(settings.enableDiscord) && initRPC();
|
||||
} else {
|
||||
app.quit();
|
||||
|
@@ -8,7 +8,9 @@ export interface MediaInfo {
|
||||
status: MediaStatus;
|
||||
url: string;
|
||||
current: string;
|
||||
currentInSeconds?: number;
|
||||
duration: string;
|
||||
durationInSeconds?: number;
|
||||
image: string;
|
||||
favorite: boolean;
|
||||
}
|
||||
|
@@ -5,7 +5,9 @@ export interface Options {
|
||||
status: string;
|
||||
url: string;
|
||||
current: string;
|
||||
currentInSeconds: number;
|
||||
duration: string;
|
||||
durationInSeconds: number;
|
||||
"app-name": string;
|
||||
image: string;
|
||||
icon: string;
|
||||
|
@@ -432,14 +432,16 @@
|
||||
<img alt="tidal icon" class="about-section__icon" src="./icon.png" />
|
||||
<h4>TIDAL Hi-Fi</h4>
|
||||
<div class="about-section__version">
|
||||
<a href="https://github.com/Mastermindzh/tidal-hifi/releases/tag/5.9.0">5.9.0</a>
|
||||
<a target="_blank" rel="noopener"
|
||||
href="https://github.com/Mastermindzh/tidal-hifi/releases/tag/5.11.0">5.11.0</a>
|
||||
</div>
|
||||
<div class="about-section__links">
|
||||
<a href="https://github.com/mastermindzh/tidal-hifi/" class="about-section__button">Github <i
|
||||
class="fa fa-external-link"></i></a>
|
||||
<a href="https://github.com/Mastermindzh/tidal-hifi/issues" class="about-section__button">Report an issue <i
|
||||
class="fa fa-external-link"></i></a>
|
||||
<a href="https://github.com/Mastermindzh/tidal-hifi/graphs/contributors"
|
||||
<a target="_blank" rel="noopener" href="https://github.com/mastermindzh/tidal-hifi/"
|
||||
class="about-section__button">Github
|
||||
<i class="fa fa-external-link"></i></a>
|
||||
<a target="_blank" rel="noopener" href="https://github.com/Mastermindzh/tidal-hifi/issues"
|
||||
class="about-section__button">Report an issue <i class="fa fa-external-link"></i></a>
|
||||
<a target="_blank" rel="noopener" href="https://github.com/Mastermindzh/tidal-hifi/graphs/contributors"
|
||||
class="about-section__button">Contributors <i class="fa fa-external-link"></i></a>
|
||||
</div>
|
||||
</section>
|
||||
|
@@ -415,7 +415,6 @@ html {
|
||||
}
|
||||
|
||||
// file upload
|
||||
|
||||
.file-drop-area {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
@@ -12,6 +12,7 @@ import { StoreData } from "./features/listenbrainz/models/storeData";
|
||||
import { Logger } from "./features/logger";
|
||||
import { Songwhip } from "./features/songwhip/songwhip";
|
||||
import { addCustomCss } from "./features/theming/theming";
|
||||
import { convertDurationToSeconds } from "./features/time/parse";
|
||||
import { MediaStatus } from "./models/mediaStatus";
|
||||
import { Options } from "./models/options";
|
||||
import { downloadFile } from "./scripts/download";
|
||||
@@ -26,8 +27,10 @@ let player: Player;
|
||||
let currentPlayStatus = MediaStatus.paused;
|
||||
let currentListenBrainzDelayId: ReturnType<typeof setTimeout>;
|
||||
let scrobbleWaitingForDelay = false;
|
||||
let wasJustPausedOrResumed = false;
|
||||
|
||||
let currentlyPlaying = MediaStatus.paused;
|
||||
let currentMediaInfo: Options;
|
||||
let currentNotification: Electron.Notification;
|
||||
|
||||
const elements = {
|
||||
play: '*[data-test="play"]',
|
||||
@@ -184,7 +187,6 @@ function getUpdateFrequency() {
|
||||
* Play or pause the current song
|
||||
*/
|
||||
function playPause() {
|
||||
wasJustPausedOrResumed = true;
|
||||
const play = elements.get("play");
|
||||
|
||||
if (play) {
|
||||
@@ -317,6 +319,12 @@ function addIPCEventListeners() {
|
||||
case globalEvents.toggleFavorite:
|
||||
elements.click("favorite");
|
||||
break;
|
||||
case globalEvents.toggleShuffle:
|
||||
elements.click("shuffle");
|
||||
break;
|
||||
case globalEvents.toggleRepeat:
|
||||
elements.click("repeat");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -359,7 +367,13 @@ function updateMediaInfo(options: Options, notify: boolean) {
|
||||
currentMediaInfo = options;
|
||||
ipcRenderer.send(globalEvents.updateInfo, options);
|
||||
if (settingsStore.get(settings.notifications) && notify) {
|
||||
new Notification({ title: options.title, body: options.artists, icon: options.icon }).show();
|
||||
if (currentNotification) currentNotification.close();
|
||||
currentNotification = new Notification({
|
||||
title: options.title,
|
||||
body: options.artists,
|
||||
icon: options.icon,
|
||||
});
|
||||
currentNotification.show();
|
||||
}
|
||||
updateMpris(options);
|
||||
updateListenBrainz(options);
|
||||
@@ -486,23 +500,6 @@ function getTrackID() {
|
||||
return window.location;
|
||||
}
|
||||
|
||||
function updateMediaSession(options: Options) {
|
||||
if ("mediaSession" in navigator) {
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: options.title,
|
||||
artist: options.artists,
|
||||
album: options.album,
|
||||
artwork: [
|
||||
{
|
||||
src: options.icon,
|
||||
sizes: "640x640",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch for song changes and update title + notify
|
||||
*/
|
||||
@@ -515,10 +512,12 @@ setInterval(function () {
|
||||
const current = elements.getText("current");
|
||||
const currentStatus = getCurrentlyPlayingStatus();
|
||||
|
||||
const playStateChanged = currentStatus != currentlyPlaying;
|
||||
|
||||
// update info if song changed or was just paused/resumed
|
||||
if (titleOrArtistsChanged || wasJustPausedOrResumed) {
|
||||
if (wasJustPausedOrResumed) {
|
||||
wasJustPausedOrResumed = false;
|
||||
if (titleOrArtistsChanged || playStateChanged) {
|
||||
if (playStateChanged) {
|
||||
currentlyPlaying = currentStatus;
|
||||
}
|
||||
skipArtistsIfFoundInSkippedArtistsList(artistsArray);
|
||||
|
||||
@@ -531,7 +530,9 @@ setInterval(function () {
|
||||
status: currentStatus,
|
||||
url: getTrackURL(),
|
||||
current,
|
||||
currentInSeconds: convertDurationToSeconds(current),
|
||||
duration,
|
||||
durationInSeconds: convertDurationToSeconds(duration),
|
||||
"app-name": appName,
|
||||
image: "",
|
||||
icon: "",
|
||||
@@ -565,13 +566,13 @@ setInterval(function () {
|
||||
}
|
||||
}).then(() => {
|
||||
updateMediaInfo(options, titleOrArtistsChanged);
|
||||
if (titleOrArtistsChanged) {
|
||||
updateMediaSession(options);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// just update the time
|
||||
updateMediaInfo({ ...currentMediaInfo, ...{ current } }, false);
|
||||
updateMediaInfo(
|
||||
{ ...currentMediaInfo, ...{ current, currentInSeconds: convertDurationToSeconds(current) } },
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -3,18 +3,13 @@ import { app, ipcMain } from "electron";
|
||||
import { globalEvents } from "../constants/globalEvents";
|
||||
import { settings } from "../constants/settings";
|
||||
import { Logger } from "../features/logger";
|
||||
import { convertDurationToSeconds } from "../features/time/parse";
|
||||
import { MediaStatus } from "../models/mediaStatus";
|
||||
import { mediaInfo } from "./mediaInfo";
|
||||
import { settingsStore } from "./settings";
|
||||
|
||||
const clientId = "833617820704440341";
|
||||
|
||||
function timeToSeconds(timeArray: string[]) {
|
||||
const minutes = parseInt(timeArray[0]) * 1;
|
||||
const seconds = minutes * 60 + parseInt(timeArray[1]) * 1;
|
||||
return seconds;
|
||||
}
|
||||
|
||||
export let rpc: Client;
|
||||
|
||||
const observer = () => {
|
||||
@@ -59,7 +54,7 @@ const getActivity = (): Presence => {
|
||||
return { includeTimestamps, detailsPrefix, buttonText };
|
||||
}
|
||||
|
||||
function setPresenceFromMediaInfo(detailsPrefix: any, buttonText: any) {
|
||||
function setPresenceFromMediaInfo(detailsPrefix: string, buttonText: string) {
|
||||
if (mediaInfo.url) {
|
||||
presence.details = `${detailsPrefix}${mediaInfo.title}`;
|
||||
presence.state = mediaInfo.artists ? mediaInfo.artists : "unknown artist(s)";
|
||||
@@ -74,10 +69,10 @@ const getActivity = (): Presence => {
|
||||
}
|
||||
}
|
||||
|
||||
function includeTimeStamps(includeTimestamps: any) {
|
||||
function includeTimeStamps(includeTimestamps: boolean) {
|
||||
if (includeTimestamps) {
|
||||
const currentSeconds = timeToSeconds(mediaInfo.current.split(":"));
|
||||
const durationSeconds = timeToSeconds(mediaInfo.duration.split(":"));
|
||||
const currentSeconds = convertDurationToSeconds(mediaInfo.current);
|
||||
const durationSeconds = convertDurationToSeconds(mediaInfo.duration);
|
||||
const date = new Date();
|
||||
const now = (date.getTime() / 1000) | 0;
|
||||
const remaining = date.setSeconds(date.getSeconds() + (durationSeconds - currentSeconds));
|
||||
|
@@ -1,69 +0,0 @@
|
||||
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 { mediaInfo } from "./mediaInfo";
|
||||
import { settingsStore } from "./settings";
|
||||
|
||||
/**
|
||||
* Function to enable TIDAL Hi-Fi's express api
|
||||
*/
|
||||
|
||||
// expressModule.run = function (mainWindow)
|
||||
export const startExpress = (mainWindow: BrowserWindow) => {
|
||||
/**
|
||||
* Shorthand to handle a fire and forget global event
|
||||
* @param {*} res
|
||||
* @param {*} action
|
||||
*/
|
||||
function handleGlobalEvent(res: Response, action: string) {
|
||||
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, artist: mediaInfo.artists }));
|
||||
expressApp.get("/image", (req, res) => {
|
||||
const 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 (settingsStore.get(settings.playBackControl)) {
|
||||
expressApp.get("/play", (req, res) => handleGlobalEvent(res, globalEvents.play));
|
||||
expressApp.post("/favorite/toggle", (req, res) =>
|
||||
handleGlobalEvent(res, globalEvents.toggleFavorite)
|
||||
);
|
||||
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 === MediaStatus.playing) {
|
||||
handleGlobalEvent(res, globalEvents.pause);
|
||||
} else {
|
||||
handleGlobalEvent(res, globalEvents.play);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const port = settingsStore.get<string, number>(settings.apiSettings.port);
|
||||
|
||||
const expressInstance = expressApp.listen(port, "127.0.0.1");
|
||||
expressInstance.on("error", function (e: { code: string }) {
|
||||
let message = e.code;
|
||||
if (e.code === "EADDRINUSE") {
|
||||
message = `Port ${port} in use.`;
|
||||
}
|
||||
|
||||
dialog.showErrorBox("Api failed to start.", message);
|
||||
});
|
||||
};
|
@@ -9,7 +9,9 @@ export const mediaInfo = {
|
||||
status: MediaStatus.paused as string,
|
||||
url: "",
|
||||
current: "",
|
||||
currentInSeconds: 0,
|
||||
duration: "",
|
||||
durationInSeconds: 0,
|
||||
image: "tidal-hifi-icon",
|
||||
favorite: false,
|
||||
};
|
||||
@@ -19,10 +21,12 @@ export const updateMediaInfo = (arg: MediaInfo) => {
|
||||
mediaInfo.artists = propOrDefault(arg.artists);
|
||||
mediaInfo.album = propOrDefault(arg.album);
|
||||
mediaInfo.icon = propOrDefault(arg.icon);
|
||||
mediaInfo.url = propOrDefault(arg.url);
|
||||
mediaInfo.url = toUniversalUrl(propOrDefault(arg.url));
|
||||
mediaInfo.status = propOrDefault(arg.status);
|
||||
mediaInfo.current = propOrDefault(arg.current);
|
||||
mediaInfo.currentInSeconds = arg.currentInSeconds ?? 0;
|
||||
mediaInfo.duration = propOrDefault(arg.duration);
|
||||
mediaInfo.durationInSeconds = arg.durationInSeconds ?? 0;
|
||||
mediaInfo.image = propOrDefault(arg.image);
|
||||
mediaInfo.favorite = arg.favorite;
|
||||
};
|
||||
@@ -33,5 +37,18 @@ export const updateMediaInfo = (arg: MediaInfo) => {
|
||||
* @param {*} defaultValue defaults to ""
|
||||
*/
|
||||
function propOrDefault(prop: string, defaultValue = "") {
|
||||
return prop ? prop : defaultValue;
|
||||
return prop || defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append the universal link syntax (?u) to any url
|
||||
* @param url url to append the universal link syntax to
|
||||
* @returns url with `?u` appended, or the original value of url if falsy
|
||||
*/
|
||||
function toUniversalUrl(url: string) {
|
||||
if (url) {
|
||||
const queryParamsSet = url.indexOf("?");
|
||||
return queryParamsSet > -1 ? `${url}&u` : `${url}?u`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import Store from "electron-store";
|
||||
|
||||
import { BrowserWindow } from "electron";
|
||||
import { BrowserWindow, shell } from "electron";
|
||||
import path from "path";
|
||||
import { settings } from "../constants/settings";
|
||||
|
||||
@@ -134,6 +134,10 @@ export const createSettingsWindow = function () {
|
||||
|
||||
settingsWindow.loadURL(`file://${__dirname}/../pages/settings/settings.html`);
|
||||
|
||||
settingsWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
shell.openExternal(url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
settingsModule.settingsWindow = settingsWindow;
|
||||
};
|
||||
|
||||
|