• Aggregates daily listening metrics from track skip data

    Processes raw skip events to generate comprehensive daily listening statistics:

    1. Analyzes each track skip event by timestamp
    2. Groups events by calendar day (YYYY-MM-DD)
    3. Categorizes skip types (preview, standard, near-end)
    4. Tracks manual vs. automatic skips
    5. Counts unique tracks and artists per day
    6. Identifies listening patterns like sequential skips
    7. Calculates listening times and peak hours

    The resulting metrics are stored both as a standalone file and integrated into the main statistics object for dashboard display.

    Returns Promise<Record<string, DailyMetrics>>

    // Process and update daily metrics
    await aggregateDailySkipMetrics();
    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 {};
    }
    }