mirror of
https://github.com/Mastermindzh/tidal-hifi.git
synced 2025-05-15 15:22:58 +02:00
166 lines
4.9 KiB
TypeScript
166 lines
4.9 KiB
TypeScript
import { getTidalReduxStore, ReduxState, RepeatMode, TidalReduxStore } from "./redux";
|
|
import { createStore } from "zustand/vanilla";
|
|
import { ipcRenderer } from "electron";
|
|
import { globalEvents } from "../constants/globalEvents";
|
|
import equal from "fast-deep-equal";
|
|
import { TidalState } from "../models/tidalState";
|
|
|
|
export const $tidalState = createStore<TidalState>(() => ({
|
|
status: "Stopped",
|
|
repeat: "Off",
|
|
shuffle: false,
|
|
}));
|
|
|
|
export let reduxStore: TidalReduxStore | undefined;
|
|
|
|
export function playPause() {
|
|
if (!reduxStore) return;
|
|
|
|
const state = $tidalState.getState();
|
|
if (state.status === "Playing") {
|
|
reduxStore.dispatch({ type: "playbackControls/PAUSE" });
|
|
} else {
|
|
reduxStore.dispatch({ type: "playbackControls/PLAY" });
|
|
}
|
|
}
|
|
export function next() {
|
|
if (!reduxStore) return;
|
|
reduxStore.dispatch({ type: "playbackControls/SKIP_NEXT" });
|
|
}
|
|
export function previous() {
|
|
if (!reduxStore) return;
|
|
reduxStore.dispatch({ type: "playbackControls/SKIP_PREVIOUS" });
|
|
}
|
|
export function pause() {
|
|
if (!reduxStore) return;
|
|
reduxStore.dispatch({ type: "playbackControls/PAUSE" });
|
|
}
|
|
export function play() {
|
|
if (!reduxStore) return;
|
|
reduxStore.dispatch({ type: "playbackControls/PLAY" });
|
|
}
|
|
export function stop() {
|
|
if (!reduxStore) return;
|
|
reduxStore.dispatch({ type: "playbackControls/STOP" });
|
|
}
|
|
export function toggleRepeat() {
|
|
if (!reduxStore) return;
|
|
reduxStore.dispatch({ type: "playQueue/TOGGLE_REPEAT_MODE" });
|
|
}
|
|
export function toggleShuffle() {
|
|
if (!reduxStore) return;
|
|
reduxStore.dispatch({ type: "playQueue/TOGGLE_SHUFFLE" });
|
|
}
|
|
export function favoriteCurrentTrack() {
|
|
if (!reduxStore) return;
|
|
const track = $tidalState.getState().currentTrack;
|
|
if (!track) return;
|
|
|
|
reduxStore.dispatch({
|
|
type: "content/TOGGLE_FAVORITE_ITEMS",
|
|
payload: {
|
|
from: "heart",
|
|
items: [{ itemId: track.id, itemType: "track" }],
|
|
moduleId: undefined,
|
|
},
|
|
});
|
|
}
|
|
|
|
export const coverArtPaths = new Map<string, Promise<string>>();
|
|
|
|
(async () => {
|
|
while (!reduxStore) {
|
|
try {
|
|
reduxStore = getTidalReduxStore();
|
|
} catch (e) {
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
}
|
|
}
|
|
|
|
// Update currentTime
|
|
let rawCurrentTime: ReduxState["playbackControls"] = reduxStore.getState().playbackControls;
|
|
setInterval(() => {
|
|
const state = $tidalState.getState();
|
|
const track = state.currentTrack;
|
|
if (!track) return;
|
|
const oldCurrentTime = track.current;
|
|
let newCurrentTime: number;
|
|
|
|
if (state.status === "Playing") {
|
|
newCurrentTime = Math.trunc(
|
|
rawCurrentTime.latestCurrentTime +
|
|
Math.abs(rawCurrentTime.latestCurrentTimeSyncTimestamp - Date.now()) / 1000
|
|
);
|
|
} else {
|
|
newCurrentTime = rawCurrentTime.latestCurrentTime;
|
|
}
|
|
if (newCurrentTime !== oldCurrentTime) {
|
|
$tidalState.setState({
|
|
...state,
|
|
currentTrack: {
|
|
...track,
|
|
current: newCurrentTime,
|
|
},
|
|
});
|
|
}
|
|
}, 1000);
|
|
|
|
reduxStore.subscribe(async () => {
|
|
const state = reduxStore.getState();
|
|
rawCurrentTime = state.playbackControls;
|
|
const currentItem = getCurrentTrack(state);
|
|
let track: TidalState["currentTrack"];
|
|
if (currentItem) {
|
|
const imageId =
|
|
currentItem.type === "track" ? currentItem.item.album.cover : currentItem.item.imageId;
|
|
const coverUrl = `https://resources.tidal.com/images/${imageId.replace(
|
|
/-/g,
|
|
"/"
|
|
)}/640x640.jpg`;
|
|
if (!coverArtPaths.has(coverUrl)) {
|
|
coverArtPaths.set(
|
|
coverUrl,
|
|
ipcRenderer.invoke(globalEvents.downloadCover, imageId, coverUrl).catch(() => "") // ignore errors if the cover can't be downloaded
|
|
);
|
|
}
|
|
track = {
|
|
id: currentItem.item.id,
|
|
title: currentItem.item.title,
|
|
album: currentItem.type === "track" ? currentItem.item.album.title : undefined,
|
|
artists: currentItem.item.artists.map((artist) => artist.name),
|
|
current: state.playbackControls.latestCurrentTime,
|
|
duration: currentItem.item.duration,
|
|
url: currentItem.item.url,
|
|
image: coverUrl,
|
|
};
|
|
}
|
|
const oldState = $tidalState.getState();
|
|
const newState: TidalState = {
|
|
status: playbackStatusMap[state.playbackControls.playbackState] ?? "Stopped",
|
|
repeat: repeatModeMap[state.playQueue.repeatMode] ?? "Off",
|
|
shuffle: state.playQueue.shuffleModeEnabled,
|
|
currentTrack: track,
|
|
};
|
|
if (!equal(oldState, newState)) {
|
|
$tidalState.setState(newState);
|
|
}
|
|
});
|
|
})();
|
|
|
|
function getCurrentTrack(state: ReduxState) {
|
|
return state.content.mediaItems[state.playbackControls.mediaProduct?.productId];
|
|
}
|
|
|
|
const playbackStatusMap = {
|
|
PLAYING: "Playing",
|
|
NOT_PLAYING: "Paused",
|
|
IDLE: "Stopped",
|
|
STALLED: "Stopped",
|
|
} as const;
|
|
|
|
const repeatModeMap = {
|
|
[RepeatMode.REPEAT_OFF]: "Off",
|
|
[RepeatMode.REPEAT_ALL]: "All",
|
|
[RepeatMode.REPEAT_SINGLE]: "Single",
|
|
} as const;
|