[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: case-insensitive proxy handles spread/rest

FoxxMD (May 28, 2026, 2:44 PM UTC) 20834e81 d1e5b115

+46 -18
+12
src/backend/tests/utils/strings.test.ts
··· 133 133 expect(iObj.foo).eq('bar'); 134 134 }); 135 135 136 + it('Proxy object handles spread', function() { 137 + const myObj = {FOO: 'bar', WIDget: 'cool'}; 138 + 139 + const iObj = noCasePropObj(myObj); 140 + const { foo, ...rest } = iObj; 141 + 142 + expect(foo).is.not.undefined; 143 + expect(foo).eq('bar'); 144 + expect(rest.widget).is.not.undefined; 145 + expect(rest.widget).eq('cool'); 146 + }); 147 + 136 148 describe('Interpolation', function() { 137 149 138 150 it('interpolates values', function() {
+34 -18
src/backend/utils/DataUtils.ts
··· 15 15 16 16 const handler: ProxyHandler<object> = 17 17 { 18 + ownKeys(target) { 19 + return Reflect.ownKeys(target).map(x => typeof x === 'string' ? x.toLocaleLowerCase() : x); 20 + }, 21 + getOwnPropertyDescriptor(target, key) { 22 + return { 23 + ...Reflect.getOwnPropertyDescriptor(target, key), 24 + enumerable: true, 25 + configurable: true, 26 + }; 27 + }, 18 28 get: function (target, key) { 19 29 //console.log("key: " + key.toString()); 20 30 if (typeof key == "string") { ··· 65 75 return value; 66 76 } 67 77 78 + export type toLowerCase<T> = { 79 + [K in keyof T]: T[K] 80 + } 81 + 82 + export type LowercaseString<S extends string> = S extends `${infer Str}` 83 + ? `${Lowercase<Str>}` 84 + : S; 85 + 86 + /** https://medium.com/@vincent.dibon_78881/type-level-uppercase-and-lowercase-in-typescript-c18f574f4572 */ 87 + export type LowercaseKeys<T> = { 88 + [K in keyof T as LowercaseString<Extract<K, string>>]: T[K] 89 + }; 90 + 68 91 /** Return a Proxy of an object where keys can be accessed case-insensitive 69 92 * 70 - * https://stackoverflow.com/a/50102779/1469797 93 + * Note: objects from rest operator in spread are *always* lowercase 94 + * 95 + * @see https://stackoverflow.com/a/50102779/1469797 71 96 */ 72 - export const noCasePropObj = <T>(obj: T): T => { 73 - let newObj; 97 + export const noCasePropObj = <T extends object>(obj: T): LowercaseKeys<T> => { 74 98 75 - if (typeof obj == "object") { 76 - newObj = new Proxy({}, handler); 77 - // traverse the Original object converting string keys to upper case 78 - for (var key in obj) { 79 - if (typeof key == "string") { 80 - var objKey = key.toUpperCase(); 81 - if (!(key in newObj)) 82 - newObj[objKey] = checkAtomic(obj[key]); 83 - } 99 + const newObj = new Proxy<LowercaseKeys<T>>({} as LowercaseKeys<T>, handler); 100 + // traverse the Original object converting string keys to upper case 101 + for (var key in obj) { 102 + if (typeof key == "string") { 103 + var objKey = key.toUpperCase(); 104 + if (!(key in newObj)) 105 + newObj[objKey] = checkAtomic(obj[key]); 84 106 } 85 - } 86 - else if (Array.isArray(obj)) { 87 - // in an array of objects convert to upper case string keys within each row 88 - newObj = new Array(); 89 - for (var i = 0; i < obj.length; i++) 90 - newObj[i] = checkAtomic(obj[i]); 91 107 } 92 108 return newObj; // object with upper cased keys 93 109 }