mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-10 22:32:17 +02:00
fix reportPlayback event chain (#2131)
- properly send stopped event on song change - properly send both starting and playing evento n song change instead of only starting
This commit is contained in:
@@ -1793,6 +1793,17 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (query.event === 'stop') {
|
||||
jfApiClient(apiClientProps).scrobbleStopped({
|
||||
body: {
|
||||
ItemId: query.id,
|
||||
PositionTicks: position,
|
||||
},
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
jfApiClient(apiClientProps).scrobbleProgress({
|
||||
body: {
|
||||
ItemId: query.id,
|
||||
|
||||
@@ -2326,35 +2326,49 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
}
|
||||
}
|
||||
|
||||
let state: 'paused' | 'playing' | 'starting' | 'stopped' = 'playing';
|
||||
const defaultParams = {
|
||||
ignoreScrobble: true,
|
||||
mediaId: query.id,
|
||||
mediaType: query.mediaType,
|
||||
playbackRate: query.playbackRate,
|
||||
positionMs: query.position ?? 0,
|
||||
};
|
||||
|
||||
const reportPlayback = (state: 'paused' | 'playing' | 'starting' | 'stopped') => {
|
||||
return ssApiClient(apiClientProps).reportPlayback({
|
||||
query: {
|
||||
...defaultParams,
|
||||
state,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const promises: Promise<any>[] = [];
|
||||
|
||||
switch (query.event) {
|
||||
case 'pause':
|
||||
state = 'paused';
|
||||
promises.push(reportPlayback('paused'));
|
||||
break;
|
||||
case 'start':
|
||||
state = 'starting';
|
||||
promises.push(reportPlayback('starting'));
|
||||
promises.push(reportPlayback('playing'));
|
||||
break;
|
||||
case 'stop':
|
||||
promises.push(reportPlayback('stopped'));
|
||||
break;
|
||||
case 'unpause':
|
||||
state = 'playing';
|
||||
promises.push(reportPlayback('playing'));
|
||||
break;
|
||||
default:
|
||||
state = 'playing';
|
||||
break;
|
||||
}
|
||||
|
||||
const res = await ssApiClient(apiClientProps).reportPlayback({
|
||||
query: {
|
||||
ignoreScrobble: true,
|
||||
mediaId: query.id,
|
||||
mediaType: query.mediaType,
|
||||
playbackRate: query.playbackRate,
|
||||
positionMs: query.position ?? 0,
|
||||
state,
|
||||
},
|
||||
});
|
||||
for (const promise of promises) {
|
||||
const res = await promise;
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to report playback');
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to report playback');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -67,8 +67,9 @@ Jellyfin progress APIs still use playback position (ticks), not listen time:
|
||||
- pause / unpause
|
||||
|
||||
Other events:
|
||||
- When the song changes: sends 'start' when the new track is playing;
|
||||
clears submission flag and listen accumulator for the new track.
|
||||
- When the song changes: sends 'stop' for the previous track; sends 'start'
|
||||
when the new track is playing; clears submission flag and listen accumulator
|
||||
for the new track.
|
||||
|
||||
- When the song is restarted (near 0 after 10s+): clears submission flag
|
||||
and listen accumulator.
|
||||
@@ -129,6 +130,7 @@ export const useScrobble = () => {
|
||||
|
||||
const previousSongRef = useRef<QueueSong | undefined>(undefined);
|
||||
const previousTimestampRef = useRef<number>(0);
|
||||
const stopPositionRef = useRef<number>(0);
|
||||
const lastProgressEventRef = useRef<number>(0);
|
||||
const lastSeekEventRef = useRef<number>(0);
|
||||
const songChangeTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
@@ -316,7 +318,10 @@ export const useScrobble = () => {
|
||||
) => {
|
||||
const currentSong = properties.song;
|
||||
const previousSong = previousSongRef.current;
|
||||
const previousPositionSec = stopPositionRef.current;
|
||||
const mediaType = currentSong?._itemType.includes('song') ? 'song' : 'podcast';
|
||||
const previousMediaType = previousSong?._itemType.includes('song') ? 'song' : 'podcast';
|
||||
const useTicksForPrevious = previousSong?._serverType === ServerType.JELLYFIN;
|
||||
|
||||
// Handle notifications
|
||||
if (scrobbleSettings?.notify && currentSong?.id) {
|
||||
@@ -352,6 +357,7 @@ export const useScrobble = () => {
|
||||
if (!isScrobbleEnabled || isPrivateModeEnabled) {
|
||||
previousSongRef.current = currentSong;
|
||||
previousTimestampRef.current = 0;
|
||||
stopPositionRef.current = 0;
|
||||
listenedMsRef.current = 0;
|
||||
lastListenSampleTimeRef.current = null;
|
||||
flushScrobbleDebug();
|
||||
@@ -395,10 +401,42 @@ export const useScrobble = () => {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Send stop scrobble for the track that was playing before the change
|
||||
if (previousSong?.id) {
|
||||
sendScrobble.mutate(
|
||||
{
|
||||
apiClientProps: { serverId: previousSong._serverId || '' },
|
||||
query: {
|
||||
albumId: previousSong.albumId,
|
||||
event: 'stop',
|
||||
id: previousSong.id,
|
||||
mediaType: previousMediaType,
|
||||
playbackRate: playbackRate,
|
||||
position: getPositionValue(
|
||||
previousPositionSec,
|
||||
useTicksForPrevious,
|
||||
),
|
||||
submission: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledStop, {
|
||||
category: LogCategory.SCROBBLE,
|
||||
meta: {
|
||||
id: previousSong.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
previousSongRef.current = currentSong;
|
||||
previousTimestampRef.current = 0;
|
||||
stopPositionRef.current = 0;
|
||||
flushScrobbleDebug();
|
||||
},
|
||||
[
|
||||
@@ -591,6 +629,7 @@ export const useScrobble = () => {
|
||||
isCurrentSongScrobbledRef.current = false;
|
||||
lastProgressEventRef.current = 0;
|
||||
previousTimestampRef.current = 0;
|
||||
stopPositionRef.current = 0;
|
||||
listenedMsRef.current = 0;
|
||||
lastListenSampleTimeRef.current = null;
|
||||
|
||||
@@ -625,6 +664,17 @@ export const useScrobble = () => {
|
||||
// Update previous timestamp on progress for use in status change handler
|
||||
const handleProgressUpdate = useCallback(
|
||||
(properties: { timestamp: number }, prev: { timestamp: number }) => {
|
||||
// Preserve last playback position when the playhead resets to the start
|
||||
// (song change can fire after progress already reports 0 for the new track).
|
||||
if (
|
||||
properties.timestamp < SCROBBLE_TRACK_BEGIN_SEC &&
|
||||
prev.timestamp >= SCROBBLE_TRACK_BEGIN_SEC
|
||||
) {
|
||||
stopPositionRef.current = prev.timestamp;
|
||||
} else {
|
||||
stopPositionRef.current = properties.timestamp;
|
||||
}
|
||||
|
||||
previousTimestampRef.current = properties.timestamp;
|
||||
handleScrobbleFromProgress(properties, prev);
|
||||
flushScrobbleDebug();
|
||||
|
||||
@@ -107,6 +107,7 @@ export const logMsg = {
|
||||
[LogCategory.SCROBBLE]: {
|
||||
scrobbledPause: 'Scrobbled a pause event',
|
||||
scrobbledStart: 'Scrobbled a start event',
|
||||
scrobbledStop: 'Scrobbled a stop event',
|
||||
scrobbledSubmission: 'Scrobbled a submission event',
|
||||
scrobbledTimeupdate: 'Scrobbled a timeupdate event',
|
||||
scrobbledUnpause: 'Scrobbled an unpause event',
|
||||
|
||||
@@ -1363,7 +1363,7 @@ export type ScrobbleArgs = BaseEndpointArgs & {
|
||||
|
||||
export type ScrobbleQuery = {
|
||||
albumId?: string;
|
||||
event?: 'pause' | 'start' | 'unpause';
|
||||
event?: 'pause' | 'start' | 'stop' | 'unpause';
|
||||
id: string;
|
||||
mediaType: 'podcast' | 'song';
|
||||
playbackRate: number;
|
||||
|
||||
Reference in New Issue
Block a user