mirror of
https://github.com/Mastermindzh/tidal-hifi.git
synced 2024-11-25 23:03:52 +01:00
feat: reworked the api, added duration/current in seconds + shuffle & repeat
This commit is contained in:
parent
000853414e
commit
db8a2c2741
@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [next]
|
## [next]
|
||||||
|
|
||||||
|
- 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.
|
- Implemented TIDAL's universal links. All links are now universal.
|
||||||
- Custom `tidal://` protocol fixed - By [TheRockYT](https://github.com/TheRockYT)
|
- Custom `tidal://` protocol fixed - By [TheRockYT](https://github.com/TheRockYT)
|
||||||
- Global media shortcuts removed since TIDAL includes them by default - By [TheRockYT]
|
- Global media shortcuts removed since TIDAL includes them by default - By [TheRockYT]
|
||||||
@ -14,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- [#390](https://github.com/Mastermindzh/tidal-hifi/issues/390)
|
- [#390](https://github.com/Mastermindzh/tidal-hifi/issues/390)
|
||||||
- [#376](https://github.com/Mastermindzh/tidal-hifi/issues/376)
|
- [#376](https://github.com/Mastermindzh/tidal-hifi/issues/376)
|
||||||
- [#383](https://github.com/Mastermindzh/tidal-hifi/issues/383)
|
- [#383](https://github.com/Mastermindzh/tidal-hifi/issues/383)
|
||||||
|
- [#393](https://github.com/Mastermindzh/tidal-hifi/issues/393)
|
||||||
|
|
||||||
## [5.10.0]
|
## [5.10.0]
|
||||||
|
|
||||||
|
@ -13,4 +13,6 @@ export const globalEvents = {
|
|||||||
whip: "whip",
|
whip: "whip",
|
||||||
log: "log",
|
log: "log",
|
||||||
toggleFavorite: "toggleFavorite",
|
toggleFavorite: "toggleFavorite",
|
||||||
|
toggleShuffle: "toggleShuffle",
|
||||||
|
toggleRepeat: "toggleRepeat",
|
||||||
};
|
};
|
||||||
|
20
src/features/api/features/current.ts
Normal file
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
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
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
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
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);
|
||||||
|
}
|
||||||
|
};
|
14
src/features/time/parse.ts
Normal file
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);
|
||||||
|
};
|
21
src/main.ts
21
src/main.ts
@ -1,14 +1,9 @@
|
|||||||
import { enable, initialize } from "@electron/remote/main";
|
import { enable, initialize } from "@electron/remote/main";
|
||||||
import {
|
import { BrowserWindow, app, components, ipcMain, session } from "electron";
|
||||||
app,
|
|
||||||
BrowserWindow,
|
|
||||||
components,
|
|
||||||
ipcMain,
|
|
||||||
session,
|
|
||||||
} from "electron";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { globalEvents } from "./constants/globalEvents";
|
import { globalEvents } from "./constants/globalEvents";
|
||||||
import { settings } from "./constants/settings";
|
import { settings } from "./constants/settings";
|
||||||
|
import { startApi } from "./features/api";
|
||||||
import { setDefaultFlags, setManagedFlagsFromSettings } from "./features/flags/flags";
|
import { setDefaultFlags, setManagedFlagsFromSettings } from "./features/flags/flags";
|
||||||
import {
|
import {
|
||||||
acquireInhibitorIfInactive,
|
acquireInhibitorIfInactive,
|
||||||
@ -19,7 +14,6 @@ import { Songwhip } from "./features/songwhip/songwhip";
|
|||||||
import { MediaInfo } from "./models/mediaInfo";
|
import { MediaInfo } from "./models/mediaInfo";
|
||||||
import { MediaStatus } from "./models/mediaStatus";
|
import { MediaStatus } from "./models/mediaStatus";
|
||||||
import { initRPC, rpc, unRPC } from "./scripts/discord";
|
import { initRPC, rpc, unRPC } from "./scripts/discord";
|
||||||
import { startExpress } from "./scripts/express";
|
|
||||||
import { updateMediaInfo } from "./scripts/mediaInfo";
|
import { updateMediaInfo } from "./scripts/mediaInfo";
|
||||||
import { addMenu } from "./scripts/menu";
|
import { addMenu } from "./scripts/menu";
|
||||||
import {
|
import {
|
||||||
@ -61,7 +55,7 @@ function syncMenuBarWithStore() {
|
|||||||
* @returns 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 isMainInstance() {
|
function isMainInstance() {
|
||||||
return app.requestSingleInstanceLock();
|
return app.requestSingleInstanceLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -82,7 +76,7 @@ function getCustomProtocolUrl(args: string[]) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return tidalUrl + "/" + customProtocolArg.substring(PROTOCOL_PREFIX.length + 3)
|
return tidalUrl + "/" + customProtocolArg.substring(PROTOCOL_PREFIX.length + 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createWindow(options = { x: 0, y: 0, backgroundColor: "white" }) {
|
function createWindow(options = { x: 0, y: 0, backgroundColor: "white" }) {
|
||||||
@ -109,7 +103,7 @@ function createWindow(options = { x: 0, y: 0, backgroundColor: "white" }) {
|
|||||||
// find the custom protocol argument
|
// find the custom protocol argument
|
||||||
const customProtocolUrl = getCustomProtocolUrl(process.argv);
|
const customProtocolUrl = getCustomProtocolUrl(process.argv);
|
||||||
|
|
||||||
if(customProtocolUrl) {
|
if (customProtocolUrl) {
|
||||||
// load the url received from the custom protocol
|
// load the url received from the custom protocol
|
||||||
mainWindow.loadURL(customProtocolUrl);
|
mainWindow.loadURL(customProtocolUrl);
|
||||||
} else {
|
} else {
|
||||||
@ -164,10 +158,9 @@ function registerHttpProtocols() {
|
|||||||
// initialization and is ready to create browser windows.
|
// initialization and is ready to create browser windows.
|
||||||
// Some APIs can only be used after this event occurs.
|
// Some APIs can only be used after this event occurs.
|
||||||
app.on("ready", async () => {
|
app.on("ready", async () => {
|
||||||
|
|
||||||
// check if the app is the main instance and multiple instances are not allowed
|
// check if the app is the main instance and multiple instances are not allowed
|
||||||
if (isMainInstance() && !isMultipleInstancesAllowed()) {
|
if (isMainInstance() && !isMultipleInstancesAllowed()) {
|
||||||
app.on('second-instance', (_, commandLine) => {
|
app.on("second-instance", (_, commandLine) => {
|
||||||
const customProtocolUrl = getCustomProtocolUrl(commandLine);
|
const customProtocolUrl = getCustomProtocolUrl(commandLine);
|
||||||
|
|
||||||
if (customProtocolUrl) {
|
if (customProtocolUrl) {
|
||||||
@ -200,7 +193,7 @@ app.on("ready", async () => {
|
|||||||
addTray(mainWindow, { icon });
|
addTray(mainWindow, { icon });
|
||||||
refreshTray(mainWindow);
|
refreshTray(mainWindow);
|
||||||
}
|
}
|
||||||
settingsStore.get(settings.api) && startExpress(mainWindow);
|
settingsStore.get(settings.api) && startApi(mainWindow);
|
||||||
settingsStore.get(settings.enableDiscord) && initRPC();
|
settingsStore.get(settings.enableDiscord) && initRPC();
|
||||||
} else {
|
} else {
|
||||||
app.quit();
|
app.quit();
|
||||||
|
@ -8,7 +8,9 @@ export interface MediaInfo {
|
|||||||
status: MediaStatus;
|
status: MediaStatus;
|
||||||
url: string;
|
url: string;
|
||||||
current: string;
|
current: string;
|
||||||
|
currentInSeconds?: number;
|
||||||
duration: string;
|
duration: string;
|
||||||
|
durationInSeconds?: number;
|
||||||
image: string;
|
image: string;
|
||||||
favorite: boolean;
|
favorite: boolean;
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,9 @@ export interface Options {
|
|||||||
status: string;
|
status: string;
|
||||||
url: string;
|
url: string;
|
||||||
current: string;
|
current: string;
|
||||||
|
currentInSeconds: number;
|
||||||
duration: string;
|
duration: string;
|
||||||
|
durationInSeconds: number;
|
||||||
"app-name": string;
|
"app-name": string;
|
||||||
image: string;
|
image: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
|
@ -12,6 +12,7 @@ import { StoreData } from "./features/listenbrainz/models/storeData";
|
|||||||
import { Logger } from "./features/logger";
|
import { Logger } from "./features/logger";
|
||||||
import { Songwhip } from "./features/songwhip/songwhip";
|
import { Songwhip } from "./features/songwhip/songwhip";
|
||||||
import { addCustomCss } from "./features/theming/theming";
|
import { addCustomCss } from "./features/theming/theming";
|
||||||
|
import { convertDurationToSeconds } from "./features/time/parse";
|
||||||
import { MediaStatus } from "./models/mediaStatus";
|
import { MediaStatus } from "./models/mediaStatus";
|
||||||
import { Options } from "./models/options";
|
import { Options } from "./models/options";
|
||||||
import { downloadFile } from "./scripts/download";
|
import { downloadFile } from "./scripts/download";
|
||||||
@ -318,6 +319,12 @@ function addIPCEventListeners() {
|
|||||||
case globalEvents.toggleFavorite:
|
case globalEvents.toggleFavorite:
|
||||||
elements.click("favorite");
|
elements.click("favorite");
|
||||||
break;
|
break;
|
||||||
|
case globalEvents.toggleShuffle:
|
||||||
|
elements.click("shuffle");
|
||||||
|
break;
|
||||||
|
case globalEvents.toggleRepeat:
|
||||||
|
elements.click("repeat");
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -523,7 +530,9 @@ setInterval(function () {
|
|||||||
status: currentStatus,
|
status: currentStatus,
|
||||||
url: getTrackURL(),
|
url: getTrackURL(),
|
||||||
current,
|
current,
|
||||||
|
currentInSeconds: convertDurationToSeconds(current),
|
||||||
duration,
|
duration,
|
||||||
|
durationInSeconds: convertDurationToSeconds(duration),
|
||||||
"app-name": appName,
|
"app-name": appName,
|
||||||
image: "",
|
image: "",
|
||||||
icon: "",
|
icon: "",
|
||||||
@ -560,7 +569,10 @@ setInterval(function () {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// just update the time
|
// 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 { globalEvents } from "../constants/globalEvents";
|
||||||
import { settings } from "../constants/settings";
|
import { settings } from "../constants/settings";
|
||||||
import { Logger } from "../features/logger";
|
import { Logger } from "../features/logger";
|
||||||
|
import { convertDurationToSeconds } from "../features/time/parse";
|
||||||
import { MediaStatus } from "../models/mediaStatus";
|
import { MediaStatus } from "../models/mediaStatus";
|
||||||
import { mediaInfo } from "./mediaInfo";
|
import { mediaInfo } from "./mediaInfo";
|
||||||
import { settingsStore } from "./settings";
|
import { settingsStore } from "./settings";
|
||||||
|
|
||||||
const clientId = "833617820704440341";
|
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;
|
export let rpc: Client;
|
||||||
|
|
||||||
const observer = () => {
|
const observer = () => {
|
||||||
@ -59,7 +54,7 @@ const getActivity = (): Presence => {
|
|||||||
return { includeTimestamps, detailsPrefix, buttonText };
|
return { includeTimestamps, detailsPrefix, buttonText };
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPresenceFromMediaInfo(detailsPrefix: any, buttonText: any) {
|
function setPresenceFromMediaInfo(detailsPrefix: string, buttonText: string) {
|
||||||
if (mediaInfo.url) {
|
if (mediaInfo.url) {
|
||||||
presence.details = `${detailsPrefix}${mediaInfo.title}`;
|
presence.details = `${detailsPrefix}${mediaInfo.title}`;
|
||||||
presence.state = mediaInfo.artists ? mediaInfo.artists : "unknown artist(s)";
|
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) {
|
if (includeTimestamps) {
|
||||||
const currentSeconds = timeToSeconds(mediaInfo.current.split(":"));
|
const currentSeconds = convertDurationToSeconds(mediaInfo.current);
|
||||||
const durationSeconds = timeToSeconds(mediaInfo.duration.split(":"));
|
const durationSeconds = convertDurationToSeconds(mediaInfo.duration);
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
const now = (date.getTime() / 1000) | 0;
|
const now = (date.getTime() / 1000) | 0;
|
||||||
const remaining = date.setSeconds(date.getSeconds() + (durationSeconds - currentSeconds));
|
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,
|
status: MediaStatus.paused as string,
|
||||||
url: "",
|
url: "",
|
||||||
current: "",
|
current: "",
|
||||||
|
currentInSeconds: 0,
|
||||||
duration: "",
|
duration: "",
|
||||||
|
durationInSeconds: 0,
|
||||||
image: "tidal-hifi-icon",
|
image: "tidal-hifi-icon",
|
||||||
favorite: false,
|
favorite: false,
|
||||||
};
|
};
|
||||||
@ -22,7 +24,9 @@ export const updateMediaInfo = (arg: MediaInfo) => {
|
|||||||
mediaInfo.url = toUniversalUrl(propOrDefault(arg.url));
|
mediaInfo.url = toUniversalUrl(propOrDefault(arg.url));
|
||||||
mediaInfo.status = propOrDefault(arg.status);
|
mediaInfo.status = propOrDefault(arg.status);
|
||||||
mediaInfo.current = propOrDefault(arg.current);
|
mediaInfo.current = propOrDefault(arg.current);
|
||||||
|
mediaInfo.currentInSeconds = arg.currentInSeconds ?? 0;
|
||||||
mediaInfo.duration = propOrDefault(arg.duration);
|
mediaInfo.duration = propOrDefault(arg.duration);
|
||||||
|
mediaInfo.durationInSeconds = arg.durationInSeconds ?? 0;
|
||||||
mediaInfo.image = propOrDefault(arg.image);
|
mediaInfo.image = propOrDefault(arg.image);
|
||||||
mediaInfo.favorite = arg.favorite;
|
mediaInfo.favorite = arg.favorite;
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user