Compare commits

...

8 Commits

Author SHA1 Message Date
jeffvli 34314bdf46 update to v1.12.1 2026-05-28 02:06:41 -07:00
jeffvli 9d53c53c54 fix queue end handling to prevent repeat 2026-05-28 02:06:07 -07:00
jeffvli 8acd585630 clean up discord rpc implementation with usePlayerEvents 2026-05-28 01:50:29 -07:00
jeffvli 1f5907716f disable interval-based timeupdate scrobbles 2026-05-28 00:59:30 -07:00
jeffvli 99ae0c99c6 fix restart requirement logic when switching windowbar style 2026-05-28 00:46:07 -07:00
jeffvli a56253cd3a fix queue height when using web windowbar style (#2068) 2026-05-28 00:46:06 -07:00
Kendall Garner a2cdce66bc chore(github): update bug report template 2026-05-27 21:20:08 -07:00
Jonathan Grotelüschen 7454832663 fix: set RESOURCES_PATH relative to app.getAppPath() (#2064)
When running the app with system electron, process.resourcesPath points
to the resources folder of the system electron, not the one from
feishin.
2026-05-27 20:01:54 -07:00
9 changed files with 334 additions and 330 deletions
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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();
+6 -4
View File
@@ -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,