Object with artist-level skip metrics
export async function aggregateArtistSkipMetrics() {
try {
const skippedTracks = await getSkippedTracks();
const statistics = await getStatistics();
// Create a map to hold artist metrics
const artistMetrics: Record<
string,
{
artistId?: string;
artistName: string;
totalSkips: number;
uniqueTracksSkipped: string[];
skipsByType: Record<string, number>;
manualSkips: number;
autoSkips: number;
skipRatio: number;
averagePlayPercentage: number;
mostSkippedTrack?: {
id: string;
name: string;
skipCount: number;
};
}
> = {};
// Process each skipped track
skippedTracks.forEach((track) => {
// Use artist name as identifier since artist ID might not always be available
const artistKey = track.artist.toLowerCase();
// Initialize this artist's metrics if needed
if (!artistMetrics[artistKey]) {
artistMetrics[artistKey] = {
artistName: track.artist,
totalSkips: 0,
uniqueTracksSkipped: [],
skipsByType: {
preview: 0,
standard: 0,
near_end: 0,
manual: 0,
auto: 0,
},
manualSkips: 0,
autoSkips: 0,
skipRatio: 0,
averagePlayPercentage: 0,
};
}
// Update metrics
artistMetrics[artistKey].totalSkips += track.skipCount || 0;
// Add to unique tracks if not already included
if (!artistMetrics[artistKey].uniqueTracksSkipped.includes(track.id)) {
artistMetrics[artistKey].uniqueTracksSkipped.push(track.id);
}
// Update artist ID if available in statistics
if (statistics.artistMetrics && statistics.artistMetrics[track.id]) {
const artistId = statistics.artistMetrics[track.id].id;
if (artistId) {
artistMetrics[artistKey].artistId = artistId;
}
}
// Add skip type information if available
if (track.skipTypes) {
Object.entries(track.skipTypes).forEach(([type, count]) => {
// Add the skip count to the existing count or initialize if not present
artistMetrics[artistKey].skipsByType[type] =
(artistMetrics[artistKey].skipsByType[type] || 0) + count;
});
}
// Add manual/auto skip information if available
if (track.manualSkipCount) {
artistMetrics[artistKey].manualSkips += track.manualSkipCount;
}
if (track.autoSkipCount) {
artistMetrics[artistKey].autoSkips += track.autoSkipCount;
}
// Track most skipped track for this artist
if (
!artistMetrics[artistKey].mostSkippedTrack ||
(track.skipCount || 0) >
artistMetrics[artistKey].mostSkippedTrack.skipCount
) {
artistMetrics[artistKey].mostSkippedTrack = {
id: track.id,
name: track.name,
skipCount: track.skipCount || 0,
};
}
// Calculate average play percentage if we have events with progress data
if (track.skipEvents && track.skipEvents.length > 0) {
const totalProgress = track.skipEvents.reduce(
(sum, event) => sum + event.progress,
0,
);
const averageProgress = totalProgress / track.skipEvents.length;
// Update running average for the artist
const currentTotal =
artistMetrics[artistKey].averagePlayPercentage *
artistMetrics[artistKey].uniqueTracksSkipped.length;
const newTotal = currentTotal + averageProgress;
artistMetrics[artistKey].averagePlayPercentage =
newTotal / artistMetrics[artistKey].uniqueTracksSkipped.length;
}
});
// Calculate skip ratios using total track play counts from statistics
for (const [, metrics] of Object.entries(artistMetrics)) {
const artistName = metrics.artistName;
// Find artist in statistics to get total plays
let totalPlays = 0;
// Look for matching artist in statistics
for (const artistId in statistics.artistMetrics) {
const artistStats = statistics.artistMetrics[artistId];
if (artistStats.name.toLowerCase() === artistName.toLowerCase()) {
totalPlays = artistStats.tracksPlayed || 0;
// Update artist ID if we didn't have it before
if (!metrics.artistId) {
metrics.artistId = artistId;
}
break;
}
}
// Calculate skip ratio
if (totalPlays > 0) {
metrics.skipRatio = metrics.totalSkips / totalPlays;
}
}
// Store the artist metrics in a separate file
const artistMetricsFilePath = join(
ensureStatisticsDir(),
"artist_skip_metrics.json",
);
writeJsonSync(artistMetricsFilePath, artistMetrics, { spaces: 2 });
return artistMetrics;
} catch (error) {
console.error("Error aggregating artist skip metrics:", error);
return {};
}
}
Aggregates skip data by artist