Not Exported
Array of tracks with skip event data
Array of detected skip streak patterns meeting confidence thresholds
// Detect skip streak patterns
const tracks = await getSkippedTracks();
const streakPatterns = detectSkipStreakPatterns(tracks);
// Example result:
// {
// type: "skip_streak",
// confidence: 0.75,
// description: "Often skips 4 tracks in a row",
// details: {
// streakCount: 12,
// avgStreakLength: 4.2,
// recentStreaks: [...]
// }
// }
function detectSkipStreakPatterns(
skippedTracks: SkippedTrack[],
): DetectedPattern[] {
const patterns: DetectedPattern[] = [];
// We need to reconstruct the timeline of skips
const allSkipEvents: {
timestamp: Date;
track: string;
artist: string;
context?: Record<string, unknown>;
}[] = [];
// Collect all skip events
skippedTracks.forEach((track) => {
if (!track.skipEvents || track.skipEvents.length === 0) return;
track.skipEvents.forEach((event) => {
allSkipEvents.push({
timestamp: new Date(event.timestamp),
track: track.name,
artist: track.artist,
context: event.context as Record<string, unknown>,
});
});
});
// Sort skip events by timestamp
allSkipEvents.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
// Find skip streaks (consecutive skips with little time between them)
const skipStreaks: {
startTime: Date;
endTime: Date;
tracks: string[];
artists: string[];
contexts: Record<string, unknown>[];
duration: number;
}[] = [];
let currentStreak: {
tracks: string[];
artists: string[];
contexts: Record<string, unknown>[];
events: {
timestamp: Date;
track: string;
artist: string;
context?: Record<string, unknown>;
}[];
} | null = null;
// Look for consecutive skips
for (let i = 0; i < allSkipEvents.length; i++) {
const event = allSkipEvents[i];
// If no current streak or it's been more than 30 seconds since last skip
if (
!currentStreak ||
(i > 0 &&
event.timestamp.getTime() - allSkipEvents[i - 1].timestamp.getTime() >
30000)
) {
// If we had a streak, check if it's valid and save it
if (
currentStreak &&
currentStreak.events.length >= PATTERN_THRESHOLDS.STREAK_THRESHOLD
) {
skipStreaks.push({
startTime: currentStreak.events[0].timestamp,
endTime:
currentStreak.events[currentStreak.events.length - 1].timestamp,
tracks: currentStreak.tracks,
artists: currentStreak.artists,
contexts: currentStreak.contexts.filter(
(c) => c !== undefined,
) as Record<string, unknown>[],
duration:
(currentStreak.events[
currentStreak.events.length - 1
].timestamp.getTime() -
currentStreak.events[0].timestamp.getTime()) /
1000, // in seconds
});
}
// Start new streak
currentStreak = {
tracks: [event.track],
artists: [event.artist],
contexts: event.context ? [event.context] : [],
events: [event],
};
} else {
// Continue current streak
currentStreak.tracks.push(event.track);
currentStreak.artists.push(event.artist);
if (event.context) currentStreak.contexts.push(event.context);
currentStreak.events.push(event);
}
}
// Check final streak
if (
currentStreak &&
currentStreak.events.length >= PATTERN_THRESHOLDS.STREAK_THRESHOLD
) {
skipStreaks.push({
startTime: currentStreak.events[0].timestamp,
endTime: currentStreak.events[currentStreak.events.length - 1].timestamp,
tracks: currentStreak.tracks,
artists: currentStreak.artists,
contexts: currentStreak.contexts.filter((c) => c !== undefined) as Record<
string,
unknown
>[],
duration:
(currentStreak.events[
currentStreak.events.length - 1
].timestamp.getTime() -
currentStreak.events[0].timestamp.getTime()) /
1000, // in seconds
});
}
// If we have enough streaks, create a pattern
if (skipStreaks.length >= 3) {
// Calculate average streak length
const avgStreakLength =
skipStreaks.reduce((sum, streak) => sum + streak.tracks.length, 0) /
skipStreaks.length;
// Calculate confidence based on number of streaks and their length
const confidence = Math.min(
0.9,
(skipStreaks.length / 10) *
(avgStreakLength / PATTERN_THRESHOLDS.STREAK_THRESHOLD),
);
if (confidence >= PATTERN_THRESHOLDS.CONFIDENCE_THRESHOLD) {
patterns.push({
type: PatternType.SKIP_STREAK,
confidence,
description: `Often skips ${Math.round(avgStreakLength)} tracks in a row`,
occurrences: skipStreaks.length,
relatedItems: skipStreaks
.slice(0, 3)
.map(
(s) =>
`${s.tracks.length} tracks on ${s.startTime.toLocaleDateString()}`,
),
details: {
streakCount: skipStreaks.length,
avgStreakLength,
longestStreak: Math.max(...skipStreaks.map((s) => s.tracks.length)),
recentStreaks: skipStreaks.slice(-5).map((s) => ({
date: s.startTime.toISOString(),
tracks: s.tracks.length,
duration: s.duration,
})),
},
firstDetected: skipStreaks[0].startTime.toISOString(),
lastDetected: skipStreaks[skipStreaks.length - 1].endTime.toISOString(),
});
}
}
return patterns;
}
Detects patterns of consecutive rapid skips (skip streaks)
Analyzes the chronological sequence of skip events to identify periods when the user rapidly skips multiple tracks in succession. This reveals potentially important behavioral patterns related to browsing, mood, or content quality.
The multi-step detection algorithm:
Skip streak patterns can indicate: