mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-15 16:04:19 +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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (query.event === 'stop') {
|
||||||
|
jfApiClient(apiClientProps).scrobbleStopped({
|
||||||
|
body: {
|
||||||
|
ItemId: query.id,
|
||||||
|
PositionTicks: position,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
jfApiClient(apiClientProps).scrobbleProgress({
|
jfApiClient(apiClientProps).scrobbleProgress({
|
||||||
body: {
|
body: {
|
||||||
ItemId: query.id,
|
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) {
|
switch (query.event) {
|
||||||
case 'pause':
|
case 'pause':
|
||||||
state = 'paused';
|
promises.push(reportPlayback('paused'));
|
||||||
break;
|
break;
|
||||||
case 'start':
|
case 'start':
|
||||||
state = 'starting';
|
promises.push(reportPlayback('starting'));
|
||||||
|
promises.push(reportPlayback('playing'));
|
||||||
|
break;
|
||||||
|
case 'stop':
|
||||||
|
promises.push(reportPlayback('stopped'));
|
||||||
break;
|
break;
|
||||||
case 'unpause':
|
case 'unpause':
|
||||||
state = 'playing';
|
promises.push(reportPlayback('playing'));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
state = 'playing';
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await ssApiClient(apiClientProps).reportPlayback({
|
for (const promise of promises) {
|
||||||
query: {
|
const res = await promise;
|
||||||
ignoreScrobble: true,
|
|
||||||
mediaId: query.id,
|
|
||||||
mediaType: query.mediaType,
|
|
||||||
playbackRate: query.playbackRate,
|
|
||||||
positionMs: query.position ?? 0,
|
|
||||||
state,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error('Failed to report playback');
|
throw new Error('Failed to report playback');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -67,8 +67,9 @@ Jellyfin progress APIs still use playback position (ticks), not listen time:
|
|||||||
- pause / unpause
|
- pause / unpause
|
||||||
|
|
||||||
Other events:
|
Other events:
|
||||||
- When the song changes: sends 'start' when the new track is playing;
|
- When the song changes: sends 'stop' for the previous track; sends 'start'
|
||||||
clears submission flag and listen accumulator for the new track.
|
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
|
- When the song is restarted (near 0 after 10s+): clears submission flag
|
||||||
and listen accumulator.
|
and listen accumulator.
|
||||||
@@ -129,6 +130,7 @@ export const useScrobble = () => {
|
|||||||
|
|
||||||
const previousSongRef = useRef<QueueSong | undefined>(undefined);
|
const previousSongRef = useRef<QueueSong | undefined>(undefined);
|
||||||
const previousTimestampRef = useRef<number>(0);
|
const previousTimestampRef = useRef<number>(0);
|
||||||
|
const stopPositionRef = useRef<number>(0);
|
||||||
const lastProgressEventRef = useRef<number>(0);
|
const lastProgressEventRef = useRef<number>(0);
|
||||||
const lastSeekEventRef = useRef<number>(0);
|
const lastSeekEventRef = useRef<number>(0);
|
||||||
const songChangeTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
const songChangeTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||||
@@ -316,7 +318,10 @@ export const useScrobble = () => {
|
|||||||
) => {
|
) => {
|
||||||
const currentSong = properties.song;
|
const currentSong = properties.song;
|
||||||
const previousSong = previousSongRef.current;
|
const previousSong = previousSongRef.current;
|
||||||
|
const previousPositionSec = stopPositionRef.current;
|
||||||
const mediaType = currentSong?._itemType.includes('song') ? 'song' : 'podcast';
|
const mediaType = currentSong?._itemType.includes('song') ? 'song' : 'podcast';
|
||||||
|
const previousMediaType = previousSong?._itemType.includes('song') ? 'song' : 'podcast';
|
||||||
|
const useTicksForPrevious = previousSong?._serverType === ServerType.JELLYFIN;
|
||||||
|
|
||||||
// Handle notifications
|
// Handle notifications
|
||||||
if (scrobbleSettings?.notify && currentSong?.id) {
|
if (scrobbleSettings?.notify && currentSong?.id) {
|
||||||
@@ -352,6 +357,7 @@ export const useScrobble = () => {
|
|||||||
if (!isScrobbleEnabled || isPrivateModeEnabled) {
|
if (!isScrobbleEnabled || isPrivateModeEnabled) {
|
||||||
previousSongRef.current = currentSong;
|
previousSongRef.current = currentSong;
|
||||||
previousTimestampRef.current = 0;
|
previousTimestampRef.current = 0;
|
||||||
|
stopPositionRef.current = 0;
|
||||||
listenedMsRef.current = 0;
|
listenedMsRef.current = 0;
|
||||||
lastListenSampleTimeRef.current = null;
|
lastListenSampleTimeRef.current = null;
|
||||||
flushScrobbleDebug();
|
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);
|
}, 2000);
|
||||||
|
|
||||||
previousSongRef.current = currentSong;
|
previousSongRef.current = currentSong;
|
||||||
previousTimestampRef.current = 0;
|
previousTimestampRef.current = 0;
|
||||||
|
stopPositionRef.current = 0;
|
||||||
flushScrobbleDebug();
|
flushScrobbleDebug();
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@@ -591,6 +629,7 @@ export const useScrobble = () => {
|
|||||||
isCurrentSongScrobbledRef.current = false;
|
isCurrentSongScrobbledRef.current = false;
|
||||||
lastProgressEventRef.current = 0;
|
lastProgressEventRef.current = 0;
|
||||||
previousTimestampRef.current = 0;
|
previousTimestampRef.current = 0;
|
||||||
|
stopPositionRef.current = 0;
|
||||||
listenedMsRef.current = 0;
|
listenedMsRef.current = 0;
|
||||||
lastListenSampleTimeRef.current = null;
|
lastListenSampleTimeRef.current = null;
|
||||||
|
|
||||||
@@ -625,6 +664,17 @@ export const useScrobble = () => {
|
|||||||
// Update previous timestamp on progress for use in status change handler
|
// Update previous timestamp on progress for use in status change handler
|
||||||
const handleProgressUpdate = useCallback(
|
const handleProgressUpdate = useCallback(
|
||||||
(properties: { timestamp: number }, prev: { timestamp: number }) => {
|
(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;
|
previousTimestampRef.current = properties.timestamp;
|
||||||
handleScrobbleFromProgress(properties, prev);
|
handleScrobbleFromProgress(properties, prev);
|
||||||
flushScrobbleDebug();
|
flushScrobbleDebug();
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ export const logMsg = {
|
|||||||
[LogCategory.SCROBBLE]: {
|
[LogCategory.SCROBBLE]: {
|
||||||
scrobbledPause: 'Scrobbled a pause event',
|
scrobbledPause: 'Scrobbled a pause event',
|
||||||
scrobbledStart: 'Scrobbled a start event',
|
scrobbledStart: 'Scrobbled a start event',
|
||||||
|
scrobbledStop: 'Scrobbled a stop event',
|
||||||
scrobbledSubmission: 'Scrobbled a submission event',
|
scrobbledSubmission: 'Scrobbled a submission event',
|
||||||
scrobbledTimeupdate: 'Scrobbled a timeupdate event',
|
scrobbledTimeupdate: 'Scrobbled a timeupdate event',
|
||||||
scrobbledUnpause: 'Scrobbled an unpause event',
|
scrobbledUnpause: 'Scrobbled an unpause event',
|
||||||
|
|||||||
@@ -1363,7 +1363,7 @@ export type ScrobbleArgs = BaseEndpointArgs & {
|
|||||||
|
|
||||||
export type ScrobbleQuery = {
|
export type ScrobbleQuery = {
|
||||||
albumId?: string;
|
albumId?: string;
|
||||||
event?: 'pause' | 'start' | 'unpause';
|
event?: 'pause' | 'start' | 'stop' | 'unpause';
|
||||||
id: string;
|
id: string;
|
||||||
mediaType: 'podcast' | 'song';
|
mediaType: 'podcast' | 'song';
|
||||||
playbackRate: number;
|
playbackRate: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user