Track ID or detailed SkipInfo object
Optional
trackName: stringTrack name (when using individual parameters)
Optional
artistName: stringArtist name (when using individual parameters)
Timestamp when track was skipped (when using individual parameters)
Optional
progress: numberProgress percentage at which the track was skipped (when using individual parameters)
Promise that resolves when the skip has been recorded
// Record a skip with detailed information
const skipInfo = {
id: 'spotify:track:1234567890',
name: 'Track Name',
artist: 'Artist Name',
album: 'Album Name',
skippedAt: Date.now(),
playDuration: 45000,
trackDuration: 180000,
playPercentage: 0.25,
deviceName: 'My Device',
skipType: 'standard',
isManualSkip: true,
confidence: 0.85,
reason: 'Track skipped at 25%, below threshold of 70%',
isInLibrary: true,
context: {
type: 'playlist',
name: 'My Playlist',
id: 'playlist:1234567890'
}
};
await recordSkippedTrack(skipInfo);
// Record a skip with basic information
await recordSkippedTrack(
'spotify:track:1234567890', // Track ID
'Track Name', // Track name
'Artist Name', // Artist name
Date.now(), // Current timestamp
0.25 // 25% progress
);
export async function recordSkippedTrack(
trackIdOrInfo: string | SkipInfo,
trackName?: string,
artistName?: string,
skippedAt: number = Date.now(),
progress?: number,
): Promise<void> {
// Get existing skipped tracks data - placing outside of try/catch to handle this error differently
let skippedTracks;
try {
skippedTracks = await getSkippedTracks();
} catch (error) {
// If getSkippedTracks fails, log the error and exit early without saving
store.saveLog(`Failed to get skipped tracks: ${error}`, "ERROR");
return; // Critical: Exit the function here to prevent proceeding with save
}
try {
// Handle both parameter styles
let trackId: string;
let skipEvent: {
timestamp: string;
progress: number;
skipType?: string;
isManual?: boolean;
context?: {
type: string;
uri?: string;
name?: string;
id?: string;
};
};
let trackAlbum: string | undefined;
let trackArtist: string;
let trackNameToUse: string;
let skippedAtToUse: number;
let isInLibrary: boolean | undefined;
// Check if first parameter is an object (enhanced skip info)
if (typeof trackIdOrInfo === "object") {
const skipInfo = trackIdOrInfo;
trackId = skipInfo.id;
trackNameToUse = skipInfo.name;
trackArtist = skipInfo.artist;
trackAlbum = skipInfo.album;
skippedAtToUse = skipInfo.skippedAt;
isInLibrary = skipInfo.isInLibrary;
skipEvent = {
timestamp: skipInfo.skippedAt.toString(),
progress: skipInfo.playPercentage,
skipType: skipInfo.skipType,
isManual: skipInfo.isManualSkip,
context: skipInfo.context,
};
} else {
// Original parameter style
trackId = trackIdOrInfo;
trackNameToUse = trackName || "";
trackArtist = artistName || "";
skippedAtToUse = skippedAt;
skipEvent = {
timestamp: skippedAt.toString(),
progress: progress || 0,
};
}
// Check if track exists in the skipped tracks list
const existingTrackIndex = skippedTracks.findIndex(
(track) => track.id === trackId,
);
const skippedAtStr = skippedAtToUse.toString();
if (existingTrackIndex >= 0) {
// Update existing entry
const track = skippedTracks[existingTrackIndex];
track.skipCount = (track.skipCount || 0) + 1;
track.lastSkipped = skippedAtStr;
// Update isInLibrary status if it's provided
if (isInLibrary !== undefined) {
track.isInLibrary = isInLibrary;
}
// Add to skipTimestamps array if it exists
const extendedTrack = track as ExtendedSkippedTrack;
if (!extendedTrack.skipTimestamps) {
extendedTrack.skipTimestamps = [];
}
extendedTrack.skipTimestamps.push(skippedAtStr);
// Cast to extended type to handle skipEvents
const trackWithEvents = track as SkippedTrackWithEvents;
if (!Array.isArray(trackWithEvents.skipEvents)) {
trackWithEvents.skipEvents = [];
}
trackWithEvents.skipEvents.push(skipEvent);
// Update skip type counts
if (!trackWithEvents.skipTypes) {
trackWithEvents.skipTypes = {};
}
const skipTypeKey = skipEvent.skipType || "standard";
trackWithEvents.skipTypes[skipTypeKey] =
(trackWithEvents.skipTypes[skipTypeKey] || 0) + 1;
// Update manual/automatic skip counts
if (skipEvent.isManual !== undefined) {
if (skipEvent.isManual) {
trackWithEvents.manualSkipCount =
(trackWithEvents.manualSkipCount || 0) + 1;
} else {
trackWithEvents.autoSkipCount =
(trackWithEvents.autoSkipCount || 0) + 1;
}
}
// Update time-of-day data
if (!trackWithEvents.timeOfDayData) {
trackWithEvents.timeOfDayData = Array(24).fill(0);
}
const skipHour = new Date(parseInt(skippedAtStr)).getHours();
trackWithEvents.timeOfDayData[skipHour] += 1;
// Update context data if available
if (skipEvent.context) {
// Store the last context
trackWithEvents.lastContext = skipEvent.context;
// Initialize context stats if needed
if (!trackWithEvents.contextStats) {
trackWithEvents.contextStats = {
total: 0,
contexts: {},
};
}
const contextType = skipEvent.context.type;
const contextId =
skipEvent.context.id || skipEvent.context.uri || "unknown";
const contextName = skipEvent.context.name || "Unknown";
// Increment total count
trackWithEvents.contextStats.total += 1;
// Initialize and update specific context
if (!trackWithEvents.contextStats.contexts[contextId]) {
trackWithEvents.contextStats.contexts[contextId] = {
type: contextType,
name: contextName,
count: 0,
};
}
trackWithEvents.contextStats.contexts[contextId].count += 1;
}
} else {
// Create new entry with the base SkippedTrack properties
// and add the skipEvents array as an extension
const newTrack: SkippedTrackWithEvents = {
id: trackId,
name: trackNameToUse,
artist: trackArtist,
skipCount: 1,
notSkippedCount: 0, // Initialize to 0 for a new track
lastSkipped: skippedAtStr,
skipTimestamps: [skippedAtStr],
skipEvents: [skipEvent],
// Initialize skip type tracking
skipTypes: {
[skipEvent.skipType || "standard"]: 1,
},
// Initialize manual/automatic counts if available
...(skipEvent.isManual !== undefined && {
manualSkipCount: skipEvent.isManual ? 1 : 0,
autoSkipCount: skipEvent.isManual ? 0 : 1,
}),
// Initialize time-of-day data
timeOfDayData: (() => {
const hourArray = Array(24).fill(0);
const skipHour = new Date(parseInt(skippedAtStr)).getHours();
hourArray[skipHour] = 1;
return hourArray;
})(),
// Set isInLibrary status if provided
...(isInLibrary !== undefined && { isInLibrary }),
};
// Add album if available (from enhanced info)
if (trackAlbum) {
newTrack.album = trackAlbum;
}
// Add context information if available
if (skipEvent.context) {
newTrack.lastContext = skipEvent.context;
const contextType = skipEvent.context.type;
const contextId =
skipEvent.context.id || skipEvent.context.uri || "unknown";
const contextName = skipEvent.context.name || "Unknown";
newTrack.contextStats = {
total: 1,
contexts: {
[contextId]: {
type: contextType,
name: contextName,
count: 1,
},
},
};
}
skippedTracks.push(newTrack);
}
// Save updated data
await store.saveSkippedTracks(skippedTracks);
store.saveLog(
`Recorded skip for "${trackNameToUse}" by ${trackArtist} - Skip type: ${skipEvent.skipType || "standard"}`,
"INFO",
);
} catch (error) {
store.saveLog(`Failed to record skipped track: ${error}`, "ERROR");
}
}
Records a track skip in the application's history
Stores detailed information about a skipped track in the application's persistent storage, including comprehensive metadata about the skip event. This function supports two parameter styles:
The function implements sophisticated storage logic: