export async function aggregateDailySkipMetrics() {
try {
const skippedTracks = await getSkippedTracks();
const dailyMetrics: Record<string, DailyMetrics> = {};
const statistics = await getStatistics();
// Process each skipped track
skippedTracks.forEach((track) => {
if (!track.skipEvents || track.skipEvents.length === 0) return;
// Process each skip event
track.skipEvents.forEach((event) => {
try {
// Skip events without timestamps
if (!event.timestamp) {
return;
}
// Create a safe date from the timestamp
const skipDate = createSafeDate(event.timestamp);
// Skip invalid dates
if (!skipDate) {
return;
}
// Format as YYYY-MM-DD for the daily key
const dateStr = skipDate.toISOString().split("T")[0];
const hourOfDay = skipDate.getHours();
// Initialize this day's metrics if needed
if (!dailyMetrics[dateStr]) {
dailyMetrics[dateStr] = {
date: dateStr,
listeningTimeMs: 0,
tracksPlayed: 0,
tracksSkipped: 0,
uniqueArtists: [],
uniqueTracks: [],
peakHour: hourOfDay,
sequentialSkips: 0,
skipsByType: {
preview: 0,
standard: 0,
near_end: 0,
auto: 0,
manual: 0,
},
};
}
// Update metrics
dailyMetrics[dateStr].tracksSkipped++;
// Add to unique tracks if not already included
// First convert to array if it's a Set
const uniqueTracks = Array.isArray(dailyMetrics[dateStr].uniqueTracks)
? dailyMetrics[dateStr].uniqueTracks
: Array.from(dailyMetrics[dateStr].uniqueTracks);
if (!uniqueTracks.includes(track.id)) {
(dailyMetrics[dateStr].uniqueTracks as string[]).push(track.id);
}
// Ensure skipsByType exists
if (!dailyMetrics[dateStr].skipsByType) {
dailyMetrics[dateStr].skipsByType = {
preview: 0,
standard: 0,
near_end: 0,
auto: 0,
manual: 0,
};
}
// Determine skip type based on progress
if (event.progress <= 0.1) {
dailyMetrics[dateStr].skipsByType.preview++;
} else if (event.progress >= 0.8) {
dailyMetrics[dateStr].skipsByType.near_end++;
} else {
dailyMetrics[dateStr].skipsByType.standard++;
}
// Track manual/auto skips
if (event.isManualSkip) {
dailyMetrics[dateStr].skipsByType.manual++;
} else {
dailyMetrics[dateStr].skipsByType.auto++;
}
} catch (err) {
console.error("Error processing skip event:", err);
}
});
});
// Store the daily metrics in a separate file
const dailyMetricsFilePath = join(
ensureStatisticsDir(),
"daily_skip_metrics.json",
);
writeJsonSync(dailyMetricsFilePath, dailyMetrics, { spaces: 2 });
// Also update the statistics object with the aggregated daily data
for (const [dateStr, metrics] of Object.entries(dailyMetrics)) {
if (!statistics.dailyMetrics[dateStr]) {
continue; // Skip dates not in the current statistics object
}
// Update with skip-specific metrics that may not be in the main statistics
if (!statistics.dailyMetrics[dateStr].skipsByType) {
statistics.dailyMetrics[dateStr].skipsByType = {
preview: 0,
standard: 0,
near_end: 0,
auto: 0,
manual: 0,
};
}
// Merge skip types
if (metrics.skipsByType) {
for (const [type, count] of Object.entries(metrics.skipsByType)) {
if (statistics.dailyMetrics[dateStr].skipsByType) {
(
statistics.dailyMetrics[dateStr].skipsByType as Record<
string,
number
>
)[type] =
((
statistics.dailyMetrics[dateStr].skipsByType as Record<
string,
number
>
)[type] || 0) + count;
}
}
}
// Update manual/auto counts
if (
statistics.dailyMetrics[dateStr].skipsByType &&
metrics.skipsByType &&
metrics.skipsByType.manual !== undefined &&
metrics.skipsByType.manual > 0
) {
statistics.dailyMetrics[dateStr].skipsByType.manual =
(statistics.dailyMetrics[dateStr].skipsByType.manual || 0) +
metrics.skipsByType.manual;
}
if (
statistics.dailyMetrics[dateStr].skipsByType &&
metrics.skipsByType &&
metrics.skipsByType.auto !== undefined &&
metrics.skipsByType.auto > 0
) {
statistics.dailyMetrics[dateStr].skipsByType.auto =
(statistics.dailyMetrics[dateStr].skipsByType.auto || 0) +
metrics.skipsByType.auto;
}
}
// Save updated statistics
await saveStatistics(statistics);
return dailyMetrics;
} catch (error) {
console.error("Error aggregating daily skip metrics:", error);
return {};
}
}
Aggregates daily listening metrics from track skip data
Processes raw skip events to generate comprehensive daily listening statistics:
The resulting metrics are stored both as a standalone file and integrated into the main statistics object for dashboard display.