From 58669a0abc5d9915361ba09fd430afa80940559c Mon Sep 17 00:00:00 2001 From: mbrucedogs Date: Tue, 22 Jul 2025 11:09:42 -0500 Subject: [PATCH] Signed-off-by: mbrucedogs --- functions/execute-samples.txt | 4 ++ functions/lib/index.js | 88 ++++++++++----------------- functions/lib/index.js.map | 2 +- functions/src/index.ts | 110 +++++++++++----------------------- 4 files changed, 70 insertions(+), 134 deletions(-) create mode 100644 functions/execute-samples.txt diff --git a/functions/execute-samples.txt b/functions/execute-samples.txt new file mode 100644 index 0000000..b2827a8 --- /dev/null +++ b/functions/execute-samples.txt @@ -0,0 +1,4 @@ + Invoke-WebRequest -Uri "https://us-central1-firebase-herse.cloudfunctions.net/recalculateTopPlayed" ` +>> -Method POST ` +>> -Headers @{"Content-Type"="application/json"} ` +>> -Body '{"data": {"controllerName": "bsully5150"}}' \ No newline at end of file diff --git a/functions/lib/index.js b/functions/lib/index.js index 2c25625..1e2f35c 100644 --- a/functions/lib/index.js +++ b/functions/lib/index.js @@ -27,25 +27,20 @@ exports.updateTopPlayedOnHistoryChange = functions.database console.log('No history data found, skipping TopPlayed update'); return; } - // Aggregate history items by artist + title combination + // Aggregate history items by path const aggregation = {}; Object.values(historyData).forEach((song) => { const historySong = song; - if (historySong && historySong.artist && historySong.title) { - // Create a unique key based on artist and title (case-insensitive) - // Replace invalid Firebase key characters with underscores - const sanitizedArtist = String(historySong.artist || '').toLowerCase().trim().replace(/[.#$/[\]]/g, '_'); - const sanitizedTitle = String(historySong.title || '').toLowerCase().trim().replace(/[.#$/[\]]/g, '_'); - const key = `${sanitizedArtist}_${sanitizedTitle}`; - if (aggregation[key]) { - // Increment count for existing song - aggregation[key].count += historySong.count || 1; + if (historySong && historySong.path) { + const path = historySong.path; + if (aggregation[path]) { + aggregation[path].count += historySong.count || 1; } else { - // Create new entry - aggregation[key] = { + aggregation[path] = { artist: historySong.artist, title: historySong.title, + path: historySong.path, count: historySong.count || 1 }; } @@ -53,26 +48,17 @@ exports.updateTopPlayedOnHistoryChange = functions.database }); // Convert aggregation to array, sort by count (descending), and take top 100 const sortedSongs = Object.entries(aggregation) - .map(([key, songData]) => ({ - key, + .map(([, songData]) => ({ artist: songData.artist, title: songData.title, + path: songData.path, count: songData.count })) - .sort((a, b) => b.count - a.count) // Sort by count descending - .slice(0, 100); // Take only top 100 - // Convert back to object format for Firebase - const topPlayedData = {}; - sortedSongs.forEach((song) => { - topPlayedData[song.key] = { - artist: song.artist, - title: song.title, - count: song.count - }; - }); - // Update the topPlayed collection - await controllerRef.child('topPlayed').set(topPlayedData); - console.log(`Successfully updated TopPlayed for controller ${controllerName} with ${Object.keys(topPlayedData).length} unique songs`); + .sort((a, b) => b.count - a.count) + .slice(0, 100); + // Write as an array so Firebase uses numeric keys + await controllerRef.child('topPlayed').set(sortedSongs); + console.log(`Successfully updated TopPlayed for controller ${controllerName} with ${sortedSongs.length} unique songs (by path)`); } catch (error) { console.error('Error updating TopPlayed:', error); @@ -83,7 +69,7 @@ exports.updateTopPlayedOnHistoryChange = functions.database * Alternative function that can be called manually to recalculate TopPlayed * This is useful for initial setup or data migration */ -exports.recalculateTopPlayed = functions.https.onCall(async (data, context) => { +exports.recalculateTopPlayed = functions.https.onCall(async (data) => { const { controllerName } = data; if (!controllerName) { throw new functions.https.HttpsError('invalid-argument', 'controllerName is required'); @@ -100,25 +86,20 @@ exports.recalculateTopPlayed = functions.https.onCall(async (data, context) => { await controllerRef.child('topPlayed').set({}); return { success: true, message: 'No history data found, TopPlayed cleared' }; } - // Aggregate history items by artist + title combination + // Aggregate history items by path const aggregation = {}; Object.values(historyData).forEach((song) => { const historySong = song; - if (historySong && historySong.artist && historySong.title) { - // Create a unique key based on artist and title (case-insensitive) - // Replace invalid Firebase key characters with underscores - const sanitizedArtist = String(historySong.artist || '').toLowerCase().trim().replace(/[.#$/[\]]/g, '_'); - const sanitizedTitle = String(historySong.title || '').toLowerCase().trim().replace(/[.#$/[\]]/g, '_'); - const key = `${sanitizedArtist}_${sanitizedTitle}`; - if (aggregation[key]) { - // Increment count for existing song - aggregation[key].count += historySong.count || 1; + if (historySong && historySong.path) { + const path = historySong.path; + if (aggregation[path]) { + aggregation[path].count += historySong.count || 1; } else { - // Create new entry - aggregation[key] = { + aggregation[path] = { artist: historySong.artist, title: historySong.title, + path: historySong.path, count: historySong.count || 1 }; } @@ -126,30 +107,21 @@ exports.recalculateTopPlayed = functions.https.onCall(async (data, context) => { }); // Convert aggregation to array, sort by count (descending), and take top 100 const sortedSongs = Object.entries(aggregation) - .map(([key, songData]) => ({ - key, + .map(([, songData]) => ({ artist: songData.artist, title: songData.title, + path: songData.path, count: songData.count })) - .sort((a, b) => b.count - a.count) // Sort by count descending - .slice(0, 100); // Take only top 100 - // Convert back to object format for Firebase - const topPlayedData = {}; - sortedSongs.forEach((song) => { - topPlayedData[song.key] = { - artist: song.artist, - title: song.title, - count: song.count - }; - }); - // Update the topPlayed collection - await controllerRef.child('topPlayed').set(topPlayedData); - console.log(`Successfully recalculated TopPlayed for controller ${controllerName} with ${Object.keys(topPlayedData).length} unique songs`); + .sort((a, b) => b.count - a.count) + .slice(0, 100); + // Write as an array so Firebase uses numeric keys + await controllerRef.child('topPlayed').set(sortedSongs); + console.log(`Successfully recalculated TopPlayed for controller ${controllerName} with ${sortedSongs.length} unique songs (by path)`); return { success: true, message: `TopPlayed recalculated successfully`, - songCount: Object.keys(topPlayedData).length + songCount: sortedSongs.length }; } catch (error) { diff --git a/functions/lib/index.js.map b/functions/lib/index.js.map index d06c3c1..9e4e22f 100644 --- a/functions/lib/index.js.map +++ b/functions/lib/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/src/index.ts b/functions/src/index.ts index 820b96c..4e57568 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -19,17 +19,11 @@ interface HistorySong { key?: string; } -interface TopPlayed { - artist: string; - title: string; - count: number; - key?: string; -} - -interface HistoryAggregation { - [key: string]: { +interface HistoryAggregationByPath { + [path: string]: { artist: string; title: string; + path: string; count: number; }; } @@ -59,26 +53,20 @@ export const updateTopPlayedOnHistoryChange = functions.database return; } - // Aggregate history items by artist + title combination - const aggregation: HistoryAggregation = {}; + // Aggregate history items by path + const aggregation: HistoryAggregationByPath = {}; Object.values(historyData).forEach((song: unknown) => { const historySong = song as HistorySong; - if (historySong && historySong.artist && historySong.title) { - // Create a unique key based on artist and title (case-insensitive) - // Replace invalid Firebase key characters with underscores - const sanitizedArtist = String(historySong.artist || '').toLowerCase().trim().replace(/[.#$/[\]]/g, '_'); - const sanitizedTitle = String(historySong.title || '').toLowerCase().trim().replace(/[.#$/[\]]/g, '_'); - const key = `${sanitizedArtist}_${sanitizedTitle}`; - - if (aggregation[key]) { - // Increment count for existing song - aggregation[key].count += historySong.count || 1; + if (historySong && historySong.path) { + const path = historySong.path; + if (aggregation[path]) { + aggregation[path].count += historySong.count || 1; } else { - // Create new entry - aggregation[key] = { + aggregation[path] = { artist: historySong.artist, title: historySong.title, + path: historySong.path, count: historySong.count || 1 }; } @@ -87,30 +75,19 @@ export const updateTopPlayedOnHistoryChange = functions.database // Convert aggregation to array, sort by count (descending), and take top 100 const sortedSongs = Object.entries(aggregation) - .map(([key, songData]) => ({ - key, + .map(([, songData]) => ({ artist: songData.artist, title: songData.title, + path: songData.path, count: songData.count })) - .sort((a, b) => b.count - a.count) // Sort by count descending - .slice(0, 100); // Take only top 100 + .sort((a, b) => b.count - a.count) + .slice(0, 100); - // Convert back to object format for Firebase - const topPlayedData: { [key: string]: TopPlayed } = {}; + // Write as an array so Firebase uses numeric keys + await controllerRef.child('topPlayed').set(sortedSongs); - sortedSongs.forEach((song) => { - topPlayedData[song.key] = { - artist: song.artist, - title: song.title, - count: song.count - }; - }); - - // Update the topPlayed collection - await controllerRef.child('topPlayed').set(topPlayedData); - - console.log(`Successfully updated TopPlayed for controller ${controllerName} with ${Object.keys(topPlayedData).length} unique songs`); + console.log(`Successfully updated TopPlayed for controller ${controllerName} with ${sortedSongs.length} unique songs (by path)`); } catch (error) { console.error('Error updating TopPlayed:', error); @@ -122,7 +99,7 @@ export const updateTopPlayedOnHistoryChange = functions.database * Alternative function that can be called manually to recalculate TopPlayed * This is useful for initial setup or data migration */ -export const recalculateTopPlayed = functions.https.onCall(async (data, context) => { +export const recalculateTopPlayed = functions.https.onCall(async (data) => { const { controllerName } = data; if (!controllerName) { @@ -145,26 +122,20 @@ export const recalculateTopPlayed = functions.https.onCall(async (data, context) return { success: true, message: 'No history data found, TopPlayed cleared' }; } - // Aggregate history items by artist + title combination - const aggregation: HistoryAggregation = {}; + // Aggregate history items by path + const aggregation: HistoryAggregationByPath = {}; Object.values(historyData).forEach((song: unknown) => { const historySong = song as HistorySong; - if (historySong && historySong.artist && historySong.title) { - // Create a unique key based on artist and title (case-insensitive) - // Replace invalid Firebase key characters with underscores - const sanitizedArtist = String(historySong.artist || '').toLowerCase().trim().replace(/[.#$/[\]]/g, '_'); - const sanitizedTitle = String(historySong.title || '').toLowerCase().trim().replace(/[.#$/[\]]/g, '_'); - const key = `${sanitizedArtist}_${sanitizedTitle}`; - - if (aggregation[key]) { - // Increment count for existing song - aggregation[key].count += historySong.count || 1; + if (historySong && historySong.path) { + const path = historySong.path; + if (aggregation[path]) { + aggregation[path].count += historySong.count || 1; } else { - // Create new entry - aggregation[key] = { + aggregation[path] = { artist: historySong.artist, title: historySong.title, + path: historySong.path, count: historySong.count || 1 }; } @@ -173,35 +144,24 @@ export const recalculateTopPlayed = functions.https.onCall(async (data, context) // Convert aggregation to array, sort by count (descending), and take top 100 const sortedSongs = Object.entries(aggregation) - .map(([key, songData]) => ({ - key, + .map(([, songData]) => ({ artist: songData.artist, title: songData.title, + path: songData.path, count: songData.count })) - .sort((a, b) => b.count - a.count) // Sort by count descending - .slice(0, 100); // Take only top 100 + .sort((a, b) => b.count - a.count) + .slice(0, 100); - // Convert back to object format for Firebase - const topPlayedData: { [key: string]: TopPlayed } = {}; + // Write as an array so Firebase uses numeric keys + await controllerRef.child('topPlayed').set(sortedSongs); - sortedSongs.forEach((song) => { - topPlayedData[song.key] = { - artist: song.artist, - title: song.title, - count: song.count - }; - }); - - // Update the topPlayed collection - await controllerRef.child('topPlayed').set(topPlayedData); - - console.log(`Successfully recalculated TopPlayed for controller ${controllerName} with ${Object.keys(topPlayedData).length} unique songs`); + console.log(`Successfully recalculated TopPlayed for controller ${controllerName} with ${sortedSongs.length} unique songs (by path)`); return { success: true, message: `TopPlayed recalculated successfully`, - songCount: Object.keys(topPlayedData).length + songCount: sortedSongs.length }; } catch (error) {