[READ-ONLY] Mirror of https://github.com/FoxxMD/multi-scrobbler. Scrobble plays from multiple sources to multiple clients docs.multi-scrobbler.app
deezer docker jellyfin koito lastfm listenbrainz maloja mopidy mpris music music-assistant plex scrobble self-hosted spotify subsonic tautulli youtube-music
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(ui): track virtual play key by uid rather than index

Should prevent pop-in and false update-indicator triggering

FoxxMD (Jul 2, 2026, 9:32 PM UTC) 5c6edefc bf7fb4f5

+40 -4
+24 -1
src/client/components/playActivity/ListParts.tsx
··· 7 7 import { QueryPlaysOpts, QueryPlaysOptsJson } from '../../../backend/common/database/drizzle/repositories/PlayRepository.js'; 8 8 import { VscDebugRestart } from 'react-icons/vsc'; 9 9 import { sortByNewestDate } from '../../../core/PlayUtils.js'; 10 + import { getAllIndexes } from '../../../core/DataUtils.js'; 10 11 11 12 dayjs.extend(doy); 12 13 ··· 106 107 } 107 108 108 109 export const generateFlatItems = (data: PlayApiCommon[]) => { 110 + // ensure there are no duplicates 111 + // this may happen if a play is "bumped" from one "page" to another, based on offset, 112 + // when new plays are inserted out of order (playedAt) 113 + const allIds: string[] = []; 114 + const dupes: string[] = []; 115 + for(const d of data) { 116 + if(allIds.includes(d.uid)) { 117 + console.warn(`Duplicate ID detected ${d.uid}`); 118 + dupes.push(d.uid); 119 + } else { 120 + allIds.push(d.uid); 121 + } 122 + } 123 + for(const uid of dupes) { 124 + const indexes = getAllIndexes(data, (d) => d.uid === uid); 125 + // keep only the first one since its likely the freshest 126 + const oldIndexes = indexes.slice(1); 127 + for(const old of oldIndexes) { 128 + data.splice(old, 1); 129 + } 130 + } 131 + 109 132 const groups = generateGroupPlays(data); 110 133 groups.sort((a, b) => sortByNewestDate(a.date, b.date)); 111 134 return groups.map((x) => { 112 135 x.plays.sort((a, b) => sortByNewestDate(a.playedAt, b.playedAt)); 113 - return [{count: x.plays.length, date: x.date, uid: x.date.toISOString()}, ...x.plays]; 136 + return [{count: x.plays.length, date: x.date, uid: `${x.date.toISOString()}-${x.plays.length}`}, ...x.plays]; 114 137 }).flat(1); 115 138 }
+2 -2
src/client/components/playActivity/VirtualListDynamic.tsx
··· 56 56 }, [data]); 57 57 58 58 const getItemKey = useCallback((index) => { 59 - return items[index].uid; 59 + return items[index] !== undefined ? items[index].uid : index; 60 60 },[items]); 61 61 62 62 const parentRef = React.useRef(null) ··· 65 65 const virtualizer = useVirtualizer({ 66 66 count: items.length + 1, 67 67 getScrollElement: () => parentRef.current, 68 - //getItemKey: getItemKey, 68 + getItemKey: getItemKey, 69 69 estimateSize: () => 85, 70 70 directDomUpdates: true, 71 71 //directDomUpdatesMode: 'position',
+14 -1
src/core/DataUtils.ts
··· 122 122 } 123 123 }); 124 124 return cloned as unknown as U; 125 - }; 125 + }; 126 + 127 + /** 128 + * Get indexes of all elements in array that make the function return true 129 + * 130 + * @see https://stackoverflow.com/a/20798567/1469797 131 + */ 132 + export const getAllIndexes = <T>(arr: T[], truthyFunc: (val: T) => boolean) => { 133 + var indexes = [], i: number; 134 + for(i = 0; i < arr.length; i++) 135 + if (truthyFunc(arr[i])) 136 + indexes.push(i); 137 + return indexes; 138 + }