Compare commits

..

6 Commits

14 changed files with 272 additions and 106 deletions

View File

@@ -6,8 +6,10 @@
"Flatpak",
"geqnfr",
"hifi",
"libnotify",
"listenbrainz",
"playpause",
"prs",
"rescrobbler",
"scrobble",
"scrobbling",

View File

@@ -4,6 +4,10 @@ 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.7.1]
- Fixed mpris not being set up correctly due to capitalization of the instance name.
## [5.7.0]
- Renamed app to TIDAL Hi-Fi.
@@ -12,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added settings to customize the Discord rich presence information
- Discord settings are now also collapsible like the ListenBrainz ones are
- Restyled settings menu to include version number and useful links on the about page
![The new about page](./docs/images/new-about.png)
- The ListenBrainz integration has been extended with a configurable (5 seconds by default) delay in song reporting so that it doesn't spam the API when you are cycling through songs.
- Custom CSS now also applies to settings window
![Tokyo Night theme on settings window](./docs/images/customcss-menu.png)
## [5.6.0]

View File

@@ -51,10 +51,11 @@ The web version of [listen.tidal.com](https://listen.tidal.com) running in elect
- Songwhip.com integration (hotkey `ctrl + w`)
- Discord RPC integration (showing "now listening", "Browsing", etc)
- MPRIS integration
- UI + Json config (`~/.config/tidal-hifi/`, or `~/.var/app/com.mastermindzh.tidal-hifi/` for Flatpak)
## Contributions
To contribute you can use the standard GitHub features (issues, prs, etc) or join the discord server to talk with like-minded individuals.
To contribute you can use the standard GitHub features (issues, prs, etc.) or join the discord server to talk with like-minded individuals.
- ![Discord logo](./docs/images/discord.png) [Join the Discord server](https://discord.gg/yhNwf4v4He)

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

BIN
docs/images/new-about.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

6
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "TIDAL Hi-Fi",
"version": "5.6.0",
"version": "5.7.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "TIDAL Hi-Fi",
"version": "5.6.0",
"version": "5.7.1",
"license": "MIT",
"dependencies": {
"@electron/remote": "^2.0.10",
@@ -9340,4 +9340,4 @@
}
}
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "tidal-hifi",
"version": "5.6.0",
"version": "5.7.1",
"description": "Tidal on Electron with widevine(hifi) support",
"main": "ts-dist/main.js",
"scripts": {

View File

@@ -29,6 +29,7 @@ export const settings = {
enabled: "ListenBrainz.enabled",
api: "ListenBrainz.api",
token: "ListenBrainz.token",
delay: "ListenBrainz.delay",
},
flags: {
root: "flags",

View File

@@ -0,0 +1,31 @@
import fs from "fs";
import { settings } from "../../constants/settings";
import { settingsStore } from "../../scripts/settings";
import { Logger } from "../logger";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function addCustomCss(app: any) {
window.addEventListener("DOMContentLoaded", () => {
const selectedTheme = settingsStore.get<string, string>(settings.theme);
if (selectedTheme !== "none") {
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) {
Logger.alert("An error ocurred reading the theme file.", err, alert);
return;
}
const themeStyle = document.createElement("style");
themeStyle.innerHTML = data;
document.head.appendChild(themeStyle);
});
}
// read customCSS (it will override the theme)
const style = document.createElement("style");
style.innerHTML = settingsStore.get<string, string[]>(settings.customCSS).join("\n");
document.head.appendChild(style);
});
}

View File

@@ -4,6 +4,7 @@ import fs from "fs";
import { globalEvents } from "../../constants/globalEvents";
import { settings } from "../../constants/settings";
import { Logger } from "../../features/logger";
import { addCustomCss } from "../../features/theming/theming";
import { settingsStore } from "./../../scripts/settings";
import { getOptions, getOptionsHeader, getThemeListFromDirectory } from "./theming";
@@ -44,10 +45,13 @@ let adBlock: HTMLInputElement,
enableListenBrainz: HTMLInputElement,
ListenBrainzAPI: HTMLInputElement,
ListenBrainzToken: HTMLInputElement,
listenbrainz_delay: HTMLInputElement,
enableWaylandSupport: HTMLInputElement,
discord_details_prefix: HTMLInputElement,
discord_button_text: HTMLInputElement;
addCustomCss(app);
function getThemeFiles() {
const selectElement = document.getElementById("themesList") as HTMLSelectElement;
const builtInThemes = getThemeListFromDirectory(process.resourcesPath);
@@ -75,6 +79,7 @@ function handleFileUploads() {
const fileMessage = document.getElementById("file-message");
fileMessage.innerText = "or drag and drop files here";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
document.getElementById("theme-files").addEventListener("change", function (e: any) {
Array.from(e.target.files).forEach((file: File) => {
const destination = `${app.getPath("userData")}/themes/${file.name}`;
@@ -128,6 +133,7 @@ function refreshSettings() {
enableListenBrainz.checked = settingsStore.get(settings.ListenBrainz.enabled);
ListenBrainzAPI.value = settingsStore.get(settings.ListenBrainz.api);
ListenBrainzToken.value = settingsStore.get(settings.ListenBrainz.token);
listenbrainz_delay.value = settingsStore.get(settings.ListenBrainz.delay);
discord_details_prefix.value = settingsStore.get(settings.discord.detailsPrefix);
discord_button_text.value = settingsStore.get(settings.discord.buttonText);
@@ -154,14 +160,6 @@ function hide() {
ipcRenderer.send(globalEvents.hideSettings);
}
/**
* Restart TIDAL Hi-Fi after changes
*/
function restart() {
app.relaunch();
app.exit();
}
/**
* Bind UI components to functions after DOMContentLoaded
*/
@@ -174,7 +172,6 @@ window.addEventListener("DOMContentLoaded", () => {
handleFileUploads();
document.getElementById("close").addEventListener("click", hide);
document.getElementById("restart").addEventListener("click", restart);
document.querySelectorAll(".external-link").forEach((elem) =>
elem.addEventListener("click", function (event) {
openExternal((event.target as HTMLElement).getAttribute("data-url"));
@@ -249,6 +246,7 @@ window.addEventListener("DOMContentLoaded", () => {
ListenBrainzAPI = get("ListenBrainzAPI");
ListenBrainzToken = get("ListenBrainzToken");
discord_details_prefix = get("discord_details_prefix");
listenbrainz_delay = get("listenbrainz_delay");
discord_button_text = get("discord_button_text");
refreshSettings();
@@ -278,8 +276,9 @@ window.addEventListener("DOMContentLoaded", () => {
settings.ListenBrainz.enabled,
switchesWithSettings.listenBrainz
);
addTextAreaListener(ListenBrainzAPI, settings.ListenBrainz.api);
addTextAreaListener(ListenBrainzToken, settings.ListenBrainz.token);
addInputListener(ListenBrainzAPI, settings.ListenBrainz.api);
addInputListener(ListenBrainzToken, settings.ListenBrainz.token);
addInputListener(listenbrainz_delay, settings.ListenBrainz.delay);
addInputListener(discord_details_prefix, settings.discord.detailsPrefix);
addInputListener(discord_button_text, settings.discord.buttonText);
});

View File

@@ -250,16 +250,22 @@
<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>
<input id="ListenBrainzAPI" type="text" class="text-input" name="ListenBrainzAPI" />
</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>
<input id="ListenBrainzToken" type="text" class="text-input" name="ListenBrainzToken" />
</div>
</div>
<textarea id="ListenBrainzToken" class="textarea" cols="1" rows="1" spellcheck="false"></textarea>
</div>
<div class="group__description">
<h4>ScrobbleDelay</h4>
<p>The delay (in ms) to send a song to ListenBrainz. Prevents spamming the API when you fast forward
immediately</p>
<input id="listenbrainz_delay" type="number" class="text-input" name="listenbrainz_delay" />
</div>
</div>
</section>

View File

@@ -1,6 +1,5 @@
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";
@@ -12,6 +11,7 @@ import {
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 { MediaStatus } from "./models/mediaStatus";
import { Options } from "./models/options";
import { downloadFile } from "./scripts/download";
@@ -24,6 +24,8 @@ const appName = "TIDAL Hi-Fi";
let currentSong = "";
let player: Player;
let currentPlayStatus = MediaStatus.paused;
let currentListenBrainzDelayId: ReturnType<typeof setTimeout>;
let scrobbleWaitingForDelay = false;
const elements = {
play: '*[data-test="play"]',
@@ -156,32 +158,6 @@ const elements = {
},
};
function addCustomCss() {
window.addEventListener("DOMContentLoaded", () => {
const selectedTheme = settingsStore.get<string, string>(settings.theme);
if (selectedTheme !== "none") {
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) {
Logger.alert("An error ocurred reading the theme file.", err, alert);
return;
}
const themeStyle = document.createElement("style");
themeStyle.innerHTML = data;
document.head.appendChild(themeStyle);
});
}
// read customCSS (it will override the theme)
const style = document.createElement("style");
style.innerHTML = settingsStore.get<string, string[]>(settings.customCSS).join("\n");
document.head.appendChild(style);
});
}
/**
* Get the update frequency from the store
* make sure it returns a number, if not use the default
@@ -378,6 +354,60 @@ function updateMediaInfo(options: Options, notify: boolean) {
}
}
function addMPRIS() {
if (process.platform === "linux" && settingsStore.get(settings.mpris)) {
try {
player = Player({
name: "tidal-hifi",
identity: "tidal-hifi",
supportedUriSchemes: ["file"],
supportedMimeTypes: [
"audio/mpeg",
"audio/flac",
"audio/x-flac",
"application/ogg",
"audio/wav",
],
supportedInterfaces: ["player"],
desktopEntry: "tidal-hifi",
});
// Events
const events = {
next: "next",
previous: "previous",
pause: "pause",
playpause: "playpause",
stop: "stop",
play: "play",
loopStatus: "repeat",
shuffle: "shuffle",
seek: "seek",
} as { [key: string]: string };
Object.keys(events).forEach(function (eventName) {
player.on(eventName, function () {
const eventValue = events[eventName];
switch (events[eventValue]) {
case events.playpause:
playPause();
break;
default:
elements.click(eventValue);
}
});
});
// Override get position function
player.getPosition = function () {
return convertDuration(elements.getText("current")) * 1000 * 1000;
};
player.on("quit", function () {
app.quit();
});
} catch (exception) {
Logger.log("MPRIS player api not working", exception);
}
}
}
function updateMpris(options: Options) {
if (player) {
player.metadata = {
@@ -395,6 +425,9 @@ function updateMpris(options: Options) {
}
}
/**
* Update the listenbrainz service with new data based on a few conditions
*/
function updateListenBrainz(options: Options) {
if (settingsStore.get(settings.ListenBrainz.enabled)) {
const oldData = ListenBrainzStore.get(ListenBrainzConstants.oldData) as StoreData;
@@ -402,12 +435,22 @@ function updateListenBrainz(options: Options) {
(!oldData && options.status === MediaStatus.playing) ||
(oldData && oldData.title !== options.title)
) {
ListenBrainz.scrobble(
options.title,
options.artists,
options.status,
convertDuration(options.duration)
);
if (!scrobbleWaitingForDelay) {
scrobbleWaitingForDelay = true;
clearTimeout(currentListenBrainzDelayId);
currentListenBrainzDelayId = setTimeout(
() => {
ListenBrainz.scrobble(
options.title,
options.artists,
options.status,
convertDuration(options.duration)
);
scrobbleWaitingForDelay = false;
},
settingsStore.get(settings.ListenBrainz.delay) ?? 0
);
}
}
}
}
@@ -528,60 +571,8 @@ setInterval(function () {
}
}, getUpdateFrequency());
if (process.platform === "linux" && settingsStore.get(settings.mpris)) {
try {
player = Player({
name: "TIDAL Hi-Fi",
identity: "TIDAL Hi-Fi",
supportedUriSchemes: ["file"],
supportedMimeTypes: [
"audio/mpeg",
"audio/flac",
"audio/x-flac",
"application/ogg",
"audio/wav",
],
supportedInterfaces: ["player"],
desktopEntry: "tidal-hifi",
});
// Events
const events = {
next: "next",
previous: "previous",
pause: "pause",
playpause: "playpause",
stop: "stop",
play: "play",
loopStatus: "repeat",
shuffle: "shuffle",
seek: "seek",
} as { [key: string]: string };
Object.keys(events).forEach(function (eventName) {
player.on(eventName, function () {
const eventValue = events[eventName];
switch (events[eventValue]) {
case events.playpause:
playPause();
break;
default:
elements.click(eventValue);
}
});
});
// Override get position function
player.getPosition = function () {
return convertDuration(elements.getText("current")) * 1000 * 1000;
};
player.on("quit", function () {
app.quit();
});
} catch (exception) {
console.log("player api not working");
}
}
addCustomCss();
addMPRIS();
addCustomCss(app);
addHotKeys();
addIPCEventListeners();
addFullScreenListeners();

View File

@@ -26,6 +26,7 @@ export const settingsStore = new Store({
enabled: false,
api: "https://api.listenbrainz.org",
token: "",
delay: 5000,
},
flags: {
disableHardwareMediaKeys: false,
@@ -53,6 +54,13 @@ export const settingsStore = new Store({
migrationStore.get("disableHardwareMediaKeys") ?? false
);
},
"5.7.0": (migrationStore) => {
console.log("running migrations for 5.7.0");
migrationStore.set(
settings.ListenBrainz.delay,
migrationStore.get(settings.ListenBrainz.delay) ?? 5000
);
},
},
});

View File

@@ -74,7 +74,7 @@ button.feedBell--kvAbD {
.container--PFTHk {
background-color: var(--right-queue-background);
}
.container--cl4MJ{
.container--cl4MJ {
background-color: var(--search-background);
}
.searchFieldHighlighted--Fitvs {
@@ -83,3 +83,122 @@ button.feedBell--kvAbD {
.searchField--EGBSq {
background-color: var(--search-background);
}
// Settings window styling
.settings-window {
color: var(--sidebar-menu-playlist-text);
}
.settings-window__wrapper {
background: var(--main-background);
box-shadow: inset 0 0 2px 0 var(--main-feed-button-background);
}
.settings-window__close-button:hover {
background: var(--main-feed-button-background);
}
.settings input:checked + label {
border-bottom: 2px solid var(--player-control-active-button);
color: var(--player-control-active-button);
}
.tabs::-webkit-scrollbar-thumb {
background-color: #404248;
box-shadow: inset 0 0 10px 2px var(--search-background);
}
.group {
border-bottom: 1px solid #333;
}
.group__description p {
color: var(--sidebar-menu-top-text);
}
.group__description .text-input {
border-bottom: solid 1px #333;
color: var(--sidebar-menu-top-text);
}
.group__description .text-input:focus {
border-color: var(--player-control-active-button);
color: var(--sidebar-menu-playlist-text);
}
.switch input:checked + .switch__slider {
background-color: var(--player-control-active-button);
}
.switch input:checked + .switch__slider::before {
background-color: var(--sidebar-menu-playlist-text);
}
.switch input:focus + .switch__slider {
box-shadow: inset 0 0 0 1px var(--player-control-active-button);
}
.switch__slider {
background-color: var(--search-background);
}
.switch__slider::before {
background-color: var(--sidebar-menu-playlist-text);
}
.textarea {
background: var(--search-background);
color: var(--sidebar-menu-top-text);
}
.textarea:focus {
border-color: var(--player-control-active-button);
color: var(--sidebar-menu-playlist-text);
}
.about-section__version a {
background-color: #404248;
color: var(--player-control-active-button);
}
.about-section__links a {
color: var(--sidebar-menu-playlist-text);
background-color: var(--search-background);
}
.about-section__links a i {
color: var(--sidebar-menu-playlist-text);
}
.about-section__links a:hover {
background-color: var(--player-control-favorite);
}
.footer__note {
color: var(--sidebar-menu-top-text);
}
.footer__button {
background: #404248;
color: var(--sidebar-menu-playlist-text);
}
.footer__button:hover {
background: #55585f;
}
.file-drop-area {
border: 1px dashed var(--sidebar-menu-top-text);
}
.file-drop-area.is-active {
background-color: #17171a;
}
.file-btn {
background-color: #17171a;
border: 1px solid var(--sidebar-menu-top-text);
}
.select-input {
border-bottom: solid 1px #333;
color: var(--sidebar-menu-top-text);
}
.select-input:focus {
border-color: var(--player-control-active-button);
color: var(--sidebar-menu-playlist-text);
}
.select-input option {
background-color: var(--search-background);
}
.select-input option:disabled {
color: var(--sidebar-menu-playlist-text);
}