mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-18 00:16:40 +02:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 34314bdf46 | |||
| 9d53c53c54 | |||
| 8acd585630 | |||
| 1f5907716f | |||
| 99ae0c99c6 | |||
| a56253cd3a | |||
| a2cdce66bc | |||
| 7454832663 |
@@ -6,7 +6,7 @@ body:
|
|||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: check-duplicate
|
id: check-duplicate
|
||||||
attributes:
|
attributes:
|
||||||
label: I have already checked through the existing bug reports and found no duplicates
|
label: I have already checked through the existing (both open AND closed) bug reports and found no duplicates
|
||||||
options:
|
options:
|
||||||
- label: 'Yes'
|
- label: 'Yes'
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "1.12.0",
|
"version": "1.12.1",
|
||||||
"description": "A modern self-hosted music player.",
|
"description": "A modern self-hosted music player.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"subsonic",
|
"subsonic",
|
||||||
|
|||||||
+1
-1
@@ -335,7 +335,7 @@ if (isDevelopment) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const RESOURCES_PATH = app.isPackaged
|
const RESOURCES_PATH = app.isPackaged
|
||||||
? path.join(process.resourcesPath, 'assets')
|
? path.join(path.dirname(app.getAppPath()), 'assets')
|
||||||
: path.join(__dirname, '../../assets');
|
: path.join(__dirname, '../../assets');
|
||||||
|
|
||||||
const getAssetPath = (...paths: string[]): string => {
|
const getAssetPath = (...paths: string[]): string => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|||||||
|
|
||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
|
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
|
||||||
|
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
||||||
import {
|
import {
|
||||||
useIsRadioActive,
|
useIsRadioActive,
|
||||||
useRadioPlayer,
|
useRadioPlayer,
|
||||||
@@ -36,6 +37,7 @@ const DiscordStatusDisplayType = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type ActivityState = [QueueSong | undefined, number, PlayerStatus];
|
type ActivityState = [QueueSong | undefined, number, PlayerStatus];
|
||||||
|
type ActivityTrigger = 'initial' | 'interval' | 'seek' | 'status_change' | 'track_change';
|
||||||
|
|
||||||
const MAX_FIELD_LENGTH = 127;
|
const MAX_FIELD_LENGTH = 127;
|
||||||
const MAX_URL_LENGTH = 256;
|
const MAX_URL_LENGTH = 256;
|
||||||
@@ -64,22 +66,24 @@ export const useDiscordRpc = () => {
|
|||||||
const imageUrlRef = useRef<null | string | undefined>(imageUrl);
|
const imageUrlRef = useRef<null | string | undefined>(imageUrl);
|
||||||
const previousEnabledRef = useRef<boolean>(discordSettings.enabled);
|
const previousEnabledRef = useRef<boolean>(discordSettings.enabled);
|
||||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const previousActivityStateRef = useRef<ActivityState | null>(null);
|
const discordEnabledRef = useRef<boolean>(discordSettings.enabled);
|
||||||
|
const privateModeRef = useRef<boolean>(privateMode);
|
||||||
|
|
||||||
// Update imageUrl ref when it changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
imageUrlRef.current = imageUrl;
|
imageUrlRef.current = imageUrl;
|
||||||
}, [imageUrl]);
|
}, [imageUrl]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
discordEnabledRef.current = discordSettings.enabled;
|
||||||
|
}, [discordSettings.enabled]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
privateModeRef.current = privateMode;
|
||||||
|
}, [privateMode]);
|
||||||
|
|
||||||
const setActivity = useCallback(
|
const setActivity = useCallback(
|
||||||
async (current: ActivityState, previous: ActivityState) => {
|
async (current: ActivityState, trigger: ActivityTrigger) => {
|
||||||
// Check if track changed by comparing with previous state
|
|
||||||
const song = current[0];
|
const song = current[0];
|
||||||
const previousSong = previous[0];
|
|
||||||
const trackChangedByState =
|
|
||||||
song && previousSong
|
|
||||||
? song._uniqueId !== previousSong._uniqueId
|
|
||||||
: song !== previousSong;
|
|
||||||
const trackChanged = song ? lastUniqueId !== song._uniqueId : false;
|
const trackChanged = song ? lastUniqueId !== song._uniqueId : false;
|
||||||
|
|
||||||
const isPlayingRadio = isRadioActive && isRadioPlaying;
|
const isPlayingRadio = isRadioActive && isRadioPlaying;
|
||||||
@@ -103,6 +107,7 @@ export const useDiscordRpc = () => {
|
|||||||
meta: {
|
meta: {
|
||||||
reason,
|
reason,
|
||||||
status: current[2],
|
status: current[2],
|
||||||
|
trigger,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return discordRpc?.clearActivity();
|
return discordRpc?.clearActivity();
|
||||||
@@ -152,6 +157,7 @@ export const useDiscordRpc = () => {
|
|||||||
showAsListening: discordSettings.showAsListening,
|
showAsListening: discordSettings.showAsListening,
|
||||||
stationName: stationName || 'Radio',
|
stationName: stationName || 'Radio',
|
||||||
title,
|
title,
|
||||||
|
trigger,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
discordRpc?.setActivity(activity);
|
discordRpc?.setActivity(activity);
|
||||||
@@ -162,214 +168,177 @@ export const useDiscordRpc = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
if (trackChanged) {
|
||||||
1. If the song has just started, update status
|
logFn.debug(logMsg[LogCategory.EXTERNAL].discordRpcTrackChanged, {
|
||||||
2. If we jump more then 1.2 seconds from last state, update status to match
|
category: LogCategory.EXTERNAL,
|
||||||
3. If the current song id is completely different, update status
|
meta: {
|
||||||
4. If the player state changed, update status
|
artistName: song.artists?.[0]?.name,
|
||||||
*/
|
songId: song._uniqueId,
|
||||||
|
songName: song.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setlastUniqueId(song._uniqueId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reason = trigger;
|
||||||
|
const start = Math.round(Date.now() - current[1] * 1000);
|
||||||
|
const end = Math.round(start + song.duration);
|
||||||
|
|
||||||
|
const artists = song?.artists.map((artist) => artist.name).join(', ');
|
||||||
|
|
||||||
|
const statusDisplayMap = {
|
||||||
|
[DiscordDisplayType.ARTIST_NAME]: DiscordStatusDisplayType.STATE,
|
||||||
|
[DiscordDisplayType.FEISHIN]: DiscordStatusDisplayType.NAME,
|
||||||
|
[DiscordDisplayType.SONG_NAME]: DiscordStatusDisplayType.DETAILS,
|
||||||
|
};
|
||||||
|
|
||||||
|
const activity: SetActivity = {
|
||||||
|
details: truncate((song?.name && song.name.padEnd(2, ' ')) || 'Idle'),
|
||||||
|
instance: false,
|
||||||
|
largeImageKey: undefined,
|
||||||
|
largeImageText: truncate(
|
||||||
|
(song?.album && song.album.padEnd(2, ' ')) || 'Unknown album',
|
||||||
|
),
|
||||||
|
smallImageKey: undefined,
|
||||||
|
smallImageText: undefined,
|
||||||
|
state: truncate((artists && artists.padEnd(2, ' ')) || 'Unknown artist'),
|
||||||
|
statusDisplayType: statusDisplayMap[discordSettings.displayType],
|
||||||
|
// I would love to use the actual type as opposed to hardcoding to 2,
|
||||||
|
// but manually installing the discord-types package appears to break things
|
||||||
|
type: discordSettings.showAsListening ? 2 : 0,
|
||||||
|
};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
previous[1] === 0 ||
|
(discordSettings.linkType == DiscordLinkType.LAST_FM ||
|
||||||
Math.abs(current[1] - previous[1]) > 1.2 ||
|
discordSettings.linkType == DiscordLinkType.MBZ_LAST_FM) &&
|
||||||
trackChangedByState ||
|
song?.artistName
|
||||||
trackChanged ||
|
|
||||||
current[2] !== previous[2]
|
|
||||||
) {
|
) {
|
||||||
if (trackChangedByState || trackChanged) {
|
activity.stateUrl =
|
||||||
logFn.debug(logMsg[LogCategory.EXTERNAL].discordRpcTrackChanged, {
|
'https://www.last.fm/music/' + encodeURIComponent(song.artists[0].name);
|
||||||
category: LogCategory.EXTERNAL,
|
|
||||||
meta: {
|
const detailsUrl =
|
||||||
artistName: song.artists?.[0]?.name,
|
'https://www.last.fm/music/' +
|
||||||
songId: song._uniqueId,
|
encodeURIComponent(song.albumArtists[0].name) +
|
||||||
songName: song.name,
|
'/' +
|
||||||
},
|
encodeURIComponent(song.album || '_') +
|
||||||
});
|
'/' +
|
||||||
setlastUniqueId(song._uniqueId);
|
encodeURIComponent(song.name);
|
||||||
|
|
||||||
|
// The details URL has a max length, only set it if it doesn't exceed it
|
||||||
|
if (detailsUrl.length <= MAX_URL_LENGTH) {
|
||||||
|
activity.detailsUrl = detailsUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
discordSettings.linkType == DiscordLinkType.MBZ ||
|
||||||
|
discordSettings.linkType == DiscordLinkType.MBZ_LAST_FM
|
||||||
|
) {
|
||||||
|
if (song?.mbzTrackId) {
|
||||||
|
activity.detailsUrl = 'https://musicbrainz.org/track/' + song.mbzTrackId;
|
||||||
|
} else if (song?.mbzRecordingId) {
|
||||||
|
activity.detailsUrl =
|
||||||
|
'https://musicbrainz.org/recording/' + song.mbzRecordingId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current[2] === PlayerStatus.PLAYING) {
|
||||||
|
if (start && end) {
|
||||||
|
activity.startTimestamp = start;
|
||||||
|
activity.endTimestamp = end;
|
||||||
}
|
}
|
||||||
|
|
||||||
let reason: string;
|
if (discordSettings.showStateIcon) {
|
||||||
if (trackChangedByState || trackChanged) {
|
activity.smallImageKey = 'playing';
|
||||||
reason = 'track_changed';
|
|
||||||
} else if (previous[1] === 0) {
|
|
||||||
reason = 'song_started';
|
|
||||||
} else if (Math.abs(current[1] - previous[1]) > 1.2) {
|
|
||||||
reason = 'time_jump';
|
|
||||||
} else {
|
|
||||||
reason = 'player_state_changed';
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = Math.round(Date.now() - current[1] * 1000);
|
|
||||||
const end = Math.round(start + song.duration);
|
|
||||||
|
|
||||||
const artists = song?.artists.map((artist) => artist.name).join(', ');
|
|
||||||
|
|
||||||
const statusDisplayMap = {
|
|
||||||
[DiscordDisplayType.ARTIST_NAME]: DiscordStatusDisplayType.STATE,
|
|
||||||
[DiscordDisplayType.FEISHIN]: DiscordStatusDisplayType.NAME,
|
|
||||||
[DiscordDisplayType.SONG_NAME]: DiscordStatusDisplayType.DETAILS,
|
|
||||||
};
|
|
||||||
|
|
||||||
const activity: SetActivity = {
|
|
||||||
details: truncate((song?.name && song.name.padEnd(2, ' ')) || 'Idle'),
|
|
||||||
instance: false,
|
|
||||||
largeImageKey: undefined,
|
|
||||||
largeImageText: truncate(
|
|
||||||
(song?.album && song.album.padEnd(2, ' ')) || 'Unknown album',
|
|
||||||
),
|
|
||||||
smallImageKey: undefined,
|
|
||||||
smallImageText: undefined,
|
|
||||||
state: truncate((artists && artists.padEnd(2, ' ')) || 'Unknown artist'),
|
|
||||||
statusDisplayType: statusDisplayMap[discordSettings.displayType],
|
|
||||||
// I would love to use the actual type as opposed to hardcoding to 2,
|
|
||||||
// but manually installing the discord-types package appears to break things
|
|
||||||
type: discordSettings.showAsListening ? 2 : 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (
|
|
||||||
(discordSettings.linkType == DiscordLinkType.LAST_FM ||
|
|
||||||
discordSettings.linkType == DiscordLinkType.MBZ_LAST_FM) &&
|
|
||||||
song?.artistName
|
|
||||||
) {
|
|
||||||
activity.stateUrl =
|
|
||||||
'https://www.last.fm/music/' + encodeURIComponent(song.artists[0].name);
|
|
||||||
|
|
||||||
const detailsUrl =
|
|
||||||
'https://www.last.fm/music/' +
|
|
||||||
encodeURIComponent(song.albumArtists[0].name) +
|
|
||||||
'/' +
|
|
||||||
encodeURIComponent(song.album || '_') +
|
|
||||||
'/' +
|
|
||||||
encodeURIComponent(song.name);
|
|
||||||
|
|
||||||
// The details URL has a max length, only set it if it doesn't exceed it
|
|
||||||
if (detailsUrl.length <= MAX_URL_LENGTH) {
|
|
||||||
activity.detailsUrl = detailsUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
discordSettings.linkType == DiscordLinkType.MBZ ||
|
|
||||||
discordSettings.linkType == DiscordLinkType.MBZ_LAST_FM
|
|
||||||
) {
|
|
||||||
if (song?.mbzTrackId) {
|
|
||||||
activity.detailsUrl = 'https://musicbrainz.org/track/' + song.mbzTrackId;
|
|
||||||
} else if (song?.mbzRecordingId) {
|
|
||||||
activity.detailsUrl =
|
|
||||||
'https://musicbrainz.org/recording/' + song.mbzRecordingId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current[2] === PlayerStatus.PLAYING) {
|
|
||||||
if (start && end) {
|
|
||||||
activity.startTimestamp = start;
|
|
||||||
activity.endTimestamp = end;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (discordSettings.showStateIcon) {
|
|
||||||
activity.smallImageKey = 'playing';
|
|
||||||
activity.smallImageText = sentenceCase(current[2]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
activity.smallImageKey = 'paused';
|
|
||||||
activity.smallImageText = sentenceCase(current[2]);
|
activity.smallImageText = sentenceCase(current[2]);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
activity.smallImageKey = 'paused';
|
||||||
|
activity.smallImageText = sentenceCase(current[2]);
|
||||||
|
}
|
||||||
|
|
||||||
if (discordSettings.showServerImage && song) {
|
if (discordSettings.showServerImage && song) {
|
||||||
if (song._uniqueId === currentSong?._uniqueId && imageUrlRef.current) {
|
if (song._uniqueId === currentSong?._uniqueId && imageUrlRef.current) {
|
||||||
if (song._serverType === ServerType.JELLYFIN) {
|
if (song._serverType === ServerType.JELLYFIN) {
|
||||||
activity.largeImageKey = imageUrlRef.current;
|
activity.largeImageKey = imageUrlRef.current;
|
||||||
} else if (
|
} else if (
|
||||||
song._serverType === ServerType.NAVIDROME ||
|
song._serverType === ServerType.NAVIDROME ||
|
||||||
song._serverType === ServerType.SUBSONIC
|
song._serverType === ServerType.SUBSONIC
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const info = await api.controller.getAlbumInfo({
|
const info = await api.controller.getAlbumInfo({
|
||||||
apiClientProps: {
|
apiClientProps: {
|
||||||
forceRemoteUrl: true,
|
forceRemoteUrl: true,
|
||||||
serverId: song._serverId,
|
serverId: song._serverId,
|
||||||
},
|
},
|
||||||
query: { id: song.albumId },
|
query: { id: song.albumId },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (info.imageUrl) {
|
if (info.imageUrl) {
|
||||||
activity.largeImageKey = info.imageUrl;
|
activity.largeImageKey = info.imageUrl;
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
activity.largeImageKey === undefined &&
|
|
||||||
lastfmApiKey &&
|
|
||||||
song?.album &&
|
|
||||||
song?.albumArtists.length
|
|
||||||
) {
|
|
||||||
const albumInfo = await fetch(
|
|
||||||
`https://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key=${lastfmApiKey}&artist=${encodeURIComponent(song.albumArtists[0].name)}&album=${encodeURIComponent(song.album)}&format=json`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const albumInfoJson = await albumInfo.json();
|
|
||||||
|
|
||||||
if (albumInfoJson.album?.image?.[3]['#text']) {
|
|
||||||
activity.largeImageKey = albumInfoJson.album.image[3]['#text'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to default icon if not set
|
|
||||||
if (!activity.largeImageKey) {
|
|
||||||
activity.largeImageKey = 'icon';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize if needed
|
|
||||||
const isConnected = await discordRpc?.isConnected();
|
|
||||||
if (!isConnected) {
|
|
||||||
logFn.debug(logMsg[LogCategory.EXTERNAL].discordRpcInitialized, {
|
|
||||||
category: LogCategory.EXTERNAL,
|
|
||||||
meta: {
|
|
||||||
clientId: discordSettings.clientId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
previousEnabledRef.current = true;
|
|
||||||
|
|
||||||
await discordRpc?.initialize(discordSettings.clientId);
|
|
||||||
}
|
|
||||||
|
|
||||||
logFn.debug(logMsg[LogCategory.EXTERNAL].discordRpcSetActivity, {
|
|
||||||
category: LogCategory.EXTERNAL,
|
|
||||||
meta: {
|
|
||||||
albumName: song.album,
|
|
||||||
artistName: song.artists?.[0]?.name,
|
|
||||||
currentStatus: current[2],
|
|
||||||
currentTime: current[1],
|
|
||||||
displayType: discordSettings.displayType,
|
|
||||||
hasLargeImage: !!activity.largeImageKey,
|
|
||||||
hasTimestamps: !!(activity.startTimestamp && activity.endTimestamp),
|
|
||||||
previousStatus: previous[2],
|
|
||||||
previousTime: previous[1],
|
|
||||||
reason,
|
|
||||||
showAsListening: discordSettings.showAsListening,
|
|
||||||
songName: song.name,
|
|
||||||
trackChanged: trackChangedByState || trackChanged,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
discordRpc?.setActivity(activity);
|
|
||||||
} else {
|
|
||||||
logFn.debug(logMsg[LogCategory.EXTERNAL].discordRpcUpdateSkipped, {
|
|
||||||
category: LogCategory.EXTERNAL,
|
|
||||||
meta: {
|
|
||||||
currentStatus: current[2],
|
|
||||||
currentTime: current[1],
|
|
||||||
previousStatus: previous[2],
|
|
||||||
previousTime: previous[1],
|
|
||||||
timeDiff: Math.abs(current[1] - previous[1]),
|
|
||||||
trackChanged: trackChangedByState || trackChanged,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
activity.largeImageKey === undefined &&
|
||||||
|
lastfmApiKey &&
|
||||||
|
song?.album &&
|
||||||
|
song?.albumArtists.length
|
||||||
|
) {
|
||||||
|
const albumInfo = await fetch(
|
||||||
|
`https://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key=${lastfmApiKey}&artist=${encodeURIComponent(song.albumArtists[0].name)}&album=${encodeURIComponent(song.album)}&format=json`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const albumInfoJson = await albumInfo.json();
|
||||||
|
|
||||||
|
if (albumInfoJson.album?.image?.[3]['#text']) {
|
||||||
|
activity.largeImageKey = albumInfoJson.album.image[3]['#text'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to default icon if not set
|
||||||
|
if (!activity.largeImageKey) {
|
||||||
|
activity.largeImageKey = 'icon';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize if needed
|
||||||
|
const isConnected = await discordRpc?.isConnected();
|
||||||
|
if (!isConnected) {
|
||||||
|
logFn.debug(logMsg[LogCategory.EXTERNAL].discordRpcInitialized, {
|
||||||
|
category: LogCategory.EXTERNAL,
|
||||||
|
meta: {
|
||||||
|
clientId: discordSettings.clientId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
previousEnabledRef.current = true;
|
||||||
|
|
||||||
|
await discordRpc?.initialize(discordSettings.clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
logFn.debug(logMsg[LogCategory.EXTERNAL].discordRpcSetActivity, {
|
||||||
|
category: LogCategory.EXTERNAL,
|
||||||
|
meta: {
|
||||||
|
albumName: song.album,
|
||||||
|
artistName: song.artists?.[0]?.name,
|
||||||
|
currentStatus: current[2],
|
||||||
|
currentTime: current[1],
|
||||||
|
displayType: discordSettings.displayType,
|
||||||
|
hasLargeImage: !!activity.largeImageKey,
|
||||||
|
hasTimestamps: !!(activity.startTimestamp && activity.endTimestamp),
|
||||||
|
reason,
|
||||||
|
showAsListening: discordSettings.showAsListening,
|
||||||
|
songName: song.name,
|
||||||
|
trackChanged,
|
||||||
|
trigger,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
discordRpc?.setActivity(activity);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
discordSettings.showAsListening,
|
discordSettings.showAsListening,
|
||||||
@@ -390,7 +359,7 @@ export const useDiscordRpc = () => {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const debouncedSetActivity = useDebouncedCallback(setActivity, 500);
|
const debouncedSetActivity = useDebouncedCallback(setActivity, 1000);
|
||||||
|
|
||||||
// Quit Discord RPC if it was enabled and is now disabled
|
// Quit Discord RPC if it was enabled and is now disabled
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -409,95 +378,110 @@ export const useDiscordRpc = () => {
|
|||||||
}
|
}
|
||||||
}, [discordSettings.clientId, privateMode, discordSettings.enabled]);
|
}, [discordSettings.clientId, privateMode, discordSettings.enabled]);
|
||||||
|
|
||||||
|
const getCurrentActivityState = useCallback((): ActivityState => {
|
||||||
|
const state = usePlayerStore.getState();
|
||||||
|
return [
|
||||||
|
state.getCurrentSong(),
|
||||||
|
useTimestampStoreBase.getState().timestamp,
|
||||||
|
state.player.status,
|
||||||
|
];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clearRefreshInterval = useCallback(() => {
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
intervalRef.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const emitActivityUpdateRef = useRef<(next: ActivityState, trigger: ActivityTrigger) => void>(
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetRefreshInterval = useCallback(() => {
|
||||||
|
clearRefreshInterval();
|
||||||
|
intervalRef.current = setInterval(() => {
|
||||||
|
const current = getCurrentActivityState();
|
||||||
|
emitActivityUpdateRef.current(current, 'interval');
|
||||||
|
}, 15000);
|
||||||
|
}, [clearRefreshInterval, getCurrentActivityState]);
|
||||||
|
|
||||||
|
const emitActivityUpdate = useCallback(
|
||||||
|
(next: ActivityState, trigger: ActivityTrigger) => {
|
||||||
|
debouncedSetActivity(next, trigger);
|
||||||
|
resetRefreshInterval();
|
||||||
|
},
|
||||||
|
[debouncedSetActivity, resetRefreshInterval],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
emitActivityUpdateRef.current = emitActivityUpdate;
|
||||||
|
}, [emitActivityUpdate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!discordSettings.enabled || privateMode) {
|
if (!discordSettings.enabled || privateMode) {
|
||||||
|
clearRefreshInterval();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCurrentActivityState = (): ActivityState => {
|
|
||||||
const state = usePlayerStore.getState();
|
|
||||||
const currentSong = state.getCurrentSong();
|
|
||||||
const currentTime = useTimestampStoreBase.getState().timestamp;
|
|
||||||
const status = state.player.status;
|
|
||||||
return [currentSong, currentTime, status];
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetInterval = () => {
|
|
||||||
if (intervalRef.current) {
|
|
||||||
clearInterval(intervalRef.current);
|
|
||||||
}
|
|
||||||
intervalRef.current = setInterval(() => {
|
|
||||||
const current = getCurrentActivityState();
|
|
||||||
const previous = previousActivityStateRef.current || current;
|
|
||||||
debouncedSetActivity(current, previous);
|
|
||||||
previousActivityStateRef.current = current;
|
|
||||||
}, 15000);
|
|
||||||
};
|
|
||||||
|
|
||||||
resetInterval();
|
|
||||||
|
|
||||||
const initialState = getCurrentActivityState();
|
const initialState = getCurrentActivityState();
|
||||||
let previousUniqueId = initialState[0]?._uniqueId || '';
|
emitActivityUpdate(initialState, 'initial');
|
||||||
|
|
||||||
previousActivityStateRef.current = initialState;
|
|
||||||
|
|
||||||
// Set activity immediately when Discord RPC is enabled
|
|
||||||
debouncedSetActivity(initialState, initialState);
|
|
||||||
|
|
||||||
const unsubSongChange = usePlayerStore.subscribe(
|
|
||||||
(state): ActivityState => {
|
|
||||||
const currentSong = state.getCurrentSong();
|
|
||||||
const currentTime = useTimestampStoreBase.getState().timestamp;
|
|
||||||
const status = state.player.status;
|
|
||||||
|
|
||||||
return [currentSong, currentTime, status];
|
|
||||||
},
|
|
||||||
(current, previous) => {
|
|
||||||
const currentUniqueId = current[0]?._uniqueId || '';
|
|
||||||
const trackChanged = previousUniqueId !== currentUniqueId;
|
|
||||||
|
|
||||||
if (trackChanged && current[0]) {
|
|
||||||
resetInterval();
|
|
||||||
previousUniqueId = currentUniqueId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const activity: ActivityState = [
|
|
||||||
current[0] as QueueSong,
|
|
||||||
current[1] as number,
|
|
||||||
current[2] as PlayerStatus,
|
|
||||||
];
|
|
||||||
|
|
||||||
// Use the ref as the source of truth for previous state
|
|
||||||
const previousActivity: ActivityState =
|
|
||||||
previousActivityStateRef.current ||
|
|
||||||
(previous
|
|
||||||
? [
|
|
||||||
previous[0] as QueueSong,
|
|
||||||
previous[1] as number,
|
|
||||||
previous[2] as PlayerStatus,
|
|
||||||
]
|
|
||||||
: activity);
|
|
||||||
|
|
||||||
debouncedSetActivity(activity, previousActivity);
|
|
||||||
|
|
||||||
previousActivityStateRef.current = activity;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubSongChange();
|
clearRefreshInterval();
|
||||||
if (intervalRef.current) {
|
|
||||||
clearInterval(intervalRef.current);
|
|
||||||
intervalRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
debouncedSetActivity,
|
clearRefreshInterval,
|
||||||
discordSettings.clientId,
|
|
||||||
discordSettings.enabled,
|
discordSettings.enabled,
|
||||||
|
emitActivityUpdate,
|
||||||
|
getCurrentActivityState,
|
||||||
privateMode,
|
privateMode,
|
||||||
setActivity,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
usePlayerEvents(
|
||||||
|
{
|
||||||
|
onCurrentSongChange: ({ song }) => {
|
||||||
|
if (!discordEnabledRef.current || privateModeRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerState = usePlayerStore.getState();
|
||||||
|
const activityState: ActivityState = [
|
||||||
|
song,
|
||||||
|
useTimestampStoreBase.getState().timestamp,
|
||||||
|
playerState.player.status,
|
||||||
|
];
|
||||||
|
emitActivityUpdateRef.current(activityState, 'track_change');
|
||||||
|
},
|
||||||
|
onPlayerSeekToTimestamp: ({ timestamp }) => {
|
||||||
|
if (!discordEnabledRef.current || privateModeRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerState = usePlayerStore.getState();
|
||||||
|
const activityState: ActivityState = [
|
||||||
|
playerState.getCurrentSong(),
|
||||||
|
timestamp,
|
||||||
|
playerState.player.status,
|
||||||
|
];
|
||||||
|
emitActivityUpdateRef.current(activityState, 'seek');
|
||||||
|
},
|
||||||
|
onPlayerStatus: ({ status }) => {
|
||||||
|
if (!discordEnabledRef.current || privateModeRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerState = usePlayerStore.getState();
|
||||||
|
const activityState: ActivityState = [
|
||||||
|
playerState.getCurrentSong(),
|
||||||
|
useTimestampStoreBase.getState().timestamp,
|
||||||
|
status,
|
||||||
|
];
|
||||||
|
emitActivityUpdateRef.current(activityState, 'status_change');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const DiscordRpcHookInner = () => {
|
const DiscordRpcHookInner = () => {
|
||||||
|
|||||||
@@ -214,7 +214,14 @@ export const SidebarPlayQueue = () => {
|
|||||||
))}
|
))}
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
) : (
|
) : (
|
||||||
<Stack gap={0} h="100%" w="100%">
|
<Stack
|
||||||
|
gap={0}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
minHeight: 0,
|
||||||
|
}}
|
||||||
|
w="100%"
|
||||||
|
>
|
||||||
<PlayQueueListControls
|
<PlayQueueListControls
|
||||||
handleSearch={setSearch}
|
handleSearch={setSearch}
|
||||||
searchTerm={search}
|
searchTerm={search}
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ export function WebPlayer() {
|
|||||||
gaplessHandler({
|
gaplessHandler({
|
||||||
currentTime: e.playedSeconds,
|
currentTime: e.playedSeconds,
|
||||||
duration: getDuration(playerRef.current.player1().ref),
|
duration: getDuration(playerRef.current.player1().ref),
|
||||||
|
hasNextSong: Boolean(player2),
|
||||||
isFlac: false,
|
isFlac: false,
|
||||||
isTransitioning,
|
isTransitioning,
|
||||||
nextPlayer: playerRef.current.player2(),
|
nextPlayer: playerRef.current.player2(),
|
||||||
@@ -206,6 +207,7 @@ export function WebPlayer() {
|
|||||||
gaplessHandler({
|
gaplessHandler({
|
||||||
currentTime: e.playedSeconds,
|
currentTime: e.playedSeconds,
|
||||||
duration: getDuration(playerRef.current.player2().ref),
|
duration: getDuration(playerRef.current.player2().ref),
|
||||||
|
hasNextSong: Boolean(player1),
|
||||||
isFlac: false,
|
isFlac: false,
|
||||||
isTransitioning,
|
isTransitioning,
|
||||||
nextPlayer: playerRef.current.player1(),
|
nextPlayer: playerRef.current.player1(),
|
||||||
@@ -680,6 +682,7 @@ function exponentialEaseOut(t: number): number {
|
|||||||
function gaplessHandler(args: {
|
function gaplessHandler(args: {
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
|
hasNextSong: boolean;
|
||||||
isFlac: boolean;
|
isFlac: boolean;
|
||||||
isTransitioning: boolean | string;
|
isTransitioning: boolean | string;
|
||||||
nextPlayer: {
|
nextPlayer: {
|
||||||
@@ -688,7 +691,19 @@ function gaplessHandler(args: {
|
|||||||
};
|
};
|
||||||
setIsTransitioning: Dispatch<boolean | string>;
|
setIsTransitioning: Dispatch<boolean | string>;
|
||||||
}) {
|
}) {
|
||||||
const { currentTime, duration, isFlac, isTransitioning, nextPlayer, setIsTransitioning } = args;
|
const {
|
||||||
|
currentTime,
|
||||||
|
duration,
|
||||||
|
hasNextSong,
|
||||||
|
isFlac,
|
||||||
|
isTransitioning,
|
||||||
|
nextPlayer,
|
||||||
|
setIsTransitioning,
|
||||||
|
} = args;
|
||||||
|
|
||||||
|
if (!hasNextSong) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isTransitioning) {
|
if (!isTransitioning) {
|
||||||
if (currentTime > duration - 2) {
|
if (currentTime > duration - 2) {
|
||||||
|
|||||||
@@ -180,9 +180,6 @@ export const useScrobble = () => {
|
|||||||
|
|
||||||
const currentSong = usePlayerStore.getState().getCurrentSong();
|
const currentSong = usePlayerStore.getState().getCurrentSong();
|
||||||
const mediaType = currentSong?._itemType.includes('song') ? 'song' : 'podcast';
|
const mediaType = currentSong?._itemType.includes('song') ? 'song' : 'podcast';
|
||||||
const serverId = currentSong?._serverId;
|
|
||||||
const server = getServerById(serverId);
|
|
||||||
const hasPlaybackReport = hasFeature(server, ServerFeature.REPORT_PLAYBACK);
|
|
||||||
const useTicks = currentSong?._serverType === ServerType.JELLYFIN;
|
const useTicks = currentSong?._serverType === ServerType.JELLYFIN;
|
||||||
const currentStatus = usePlayerStore.getState().player.status;
|
const currentStatus = usePlayerStore.getState().player.status;
|
||||||
const currentTime = properties.timestamp;
|
const currentTime = properties.timestamp;
|
||||||
@@ -239,36 +236,36 @@ export const useScrobble = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send progress events every 10 seconds
|
// Send progress events every 10 seconds
|
||||||
if (hasPlaybackReport) {
|
// if (hasPlaybackReport) {
|
||||||
const timeSinceLastProgress = currentTime - lastProgressEventRef.current;
|
// const timeSinceLastProgress = currentTime - lastProgressEventRef.current;
|
||||||
if (timeSinceLastProgress >= 10) {
|
// if (timeSinceLastProgress >= 10) {
|
||||||
sendScrobble.mutate(
|
// sendScrobble.mutate(
|
||||||
{
|
// {
|
||||||
apiClientProps: { serverId: serverId || '' },
|
// apiClientProps: { serverId: serverId || '' },
|
||||||
query: {
|
// query: {
|
||||||
albumId: currentSong.albumId,
|
// albumId: currentSong.albumId,
|
||||||
event: 'timeupdate',
|
// event: 'timeupdate',
|
||||||
id: currentSong.id,
|
// id: currentSong.id,
|
||||||
mediaType: mediaType,
|
// mediaType: mediaType,
|
||||||
playbackRate,
|
// playbackRate,
|
||||||
position: getPositionValue(currentTime, useTicks),
|
// position: getPositionValue(currentTime, useTicks),
|
||||||
submission: false,
|
// submission: false,
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
onSuccess: () => {
|
// onSuccess: () => {
|
||||||
logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledTimeupdate, {
|
// logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledTimeupdate, {
|
||||||
category: LogCategory.SCROBBLE,
|
// category: LogCategory.SCROBBLE,
|
||||||
meta: {
|
// meta: {
|
||||||
id: currentSong.id,
|
// id: currentSong.id,
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
);
|
// );
|
||||||
lastProgressEventRef.current = currentTime;
|
// lastProgressEventRef.current = currentTime;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Check if we should submit scrobble based on listened time
|
// Check if we should submit scrobble based on listened time
|
||||||
if (!isCurrentSongScrobbledRef.current) {
|
if (!isCurrentSongScrobbledRef.current) {
|
||||||
|
|||||||
@@ -36,13 +36,12 @@ export const WindowSettings = memo(() => {
|
|||||||
if (!e) return;
|
if (!e) return;
|
||||||
|
|
||||||
// Platform.LINUX is used as the native frame option regardless of the actual platform
|
// Platform.LINUX is used as the native frame option regardless of the actual platform
|
||||||
const hasFrame = localSettings?.get('window_has_frame') as
|
const previousWindowBarStyle = settings.windowBarStyle;
|
||||||
| boolean
|
const isSwitchingToNative =
|
||||||
| undefined;
|
previousWindowBarStyle !== Platform.LINUX && e === Platform.LINUX;
|
||||||
const isSwitchingToFrame = !hasFrame && e === Platform.LINUX;
|
const isSwitchingFromNative =
|
||||||
const isSwitchingToNoFrame = hasFrame && e !== Platform.LINUX;
|
previousWindowBarStyle === Platform.LINUX && e !== Platform.LINUX;
|
||||||
|
const requireRestart = isSwitchingToNative || isSwitchingFromNative;
|
||||||
const requireRestart = isSwitchingToFrame || isSwitchingToNoFrame;
|
|
||||||
|
|
||||||
if (requireRestart) {
|
if (requireRestart) {
|
||||||
openRestartRequiredToast();
|
openRestartRequiredToast();
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ function calculateNextIndex(
|
|||||||
} else {
|
} else {
|
||||||
// Repeat none: move to next track, or pause if at the end
|
// Repeat none: move to next track, or pause if at the end
|
||||||
if (isLastTrack) {
|
if (isLastTrack) {
|
||||||
return { nextIndex: 0, shouldPause: true };
|
return { nextIndex: currentIndex, shouldPause: true };
|
||||||
} else {
|
} else {
|
||||||
return { nextIndex: currentIndex + 1, shouldPause: false };
|
return { nextIndex: currentIndex + 1, shouldPause: false };
|
||||||
}
|
}
|
||||||
@@ -939,10 +939,12 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
|||||||
const pauseOnNext = player.pauseOnNextSongEnd;
|
const pauseOnNext = player.pauseOnNextSongEnd;
|
||||||
const newStatus =
|
const newStatus =
|
||||||
shouldPause || pauseOnNext ? PlayerStatus.PAUSED : PlayerStatus.PLAYING;
|
shouldPause || pauseOnNext ? PlayerStatus.PAUSED : PlayerStatus.PLAYING;
|
||||||
|
const shouldKeepCurrentPlayer = newStatus === PlayerStatus.PAUSED;
|
||||||
|
const shouldSwapPlayer = !isRepeatOneSameTrack && !shouldKeepCurrentPlayer;
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.player.index = nextPlaybackIndex;
|
state.player.index = nextPlaybackIndex;
|
||||||
state.player.playerNum = newPlayerNum;
|
state.player.playerNum = shouldSwapPlayer ? newPlayerNum : player.playerNum;
|
||||||
setTimestampStore(0);
|
setTimestampStore(0);
|
||||||
state.player.status = newStatus;
|
state.player.status = newStatus;
|
||||||
|
|
||||||
@@ -999,7 +1001,7 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { player1, player2 } = getDualPlayerSongs(
|
const { player1, player2 } = getDualPlayerSongs(
|
||||||
newPlayerNum,
|
shouldSwapPlayer ? newPlayerNum : player.playerNum,
|
||||||
currentSong,
|
currentSong,
|
||||||
nextSong,
|
nextSong,
|
||||||
repeat,
|
repeat,
|
||||||
@@ -1009,7 +1011,7 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
|||||||
currentSong,
|
currentSong,
|
||||||
index: currentQueueIndex,
|
index: currentQueueIndex,
|
||||||
nextSong,
|
nextSong,
|
||||||
num: newPlayerNum,
|
num: shouldSwapPlayer ? newPlayerNum : player.playerNum,
|
||||||
player1,
|
player1,
|
||||||
player2,
|
player2,
|
||||||
previousSong,
|
previousSong,
|
||||||
|
|||||||
Reference in New Issue
Block a user