import type { AppleMusicContainer, AppleMusicPlaybackEvent, AppleMusicTrackHistory, } from "../types"; import * as Papaparse from "papaparse"; import { existsSync } from "fs"; const trackCache = new Map< string, { originId: string | null; artist: string } >(); type OverridesMap = Record; let overrides: OverridesMap = {}; if (existsSync("artist_overrides.json")) { const rawOverrides = await Bun.file("artist_overrides.json") .json() as OverridesMap; overrides = Object.fromEntries( Object.entries(rawOverrides).map(( [key, value], ) => [key.toLowerCase(), value]), ); } export async function parseCSV(dataExportPath: string, fileName: string) { const cachedPath = `cache/${fileName}.json`; if (existsSync(cachedPath)) { return await Bun.file(cachedPath).json(); } const csv = await Bun.file(`${dataExportPath}/${fileName}.csv`).text(); const parsed = Papaparse.parse(csv, { header: true, }); const data = parsed.data; await Bun.write(cachedPath, JSON.stringify(data)); return data; } export function isEligible(event: AppleMusicPlaybackEvent): boolean { // Must be a play end event if (event["Event Type"] !== "PLAY_END") { return false; } // Check end reason - should be a natural completion or user skip to next track const endReason = event["End Reason Type"]; const validEndReasons = [ "NATURAL_END_OF_TRACK", "TRACK_SKIPPED_FORWARDS", "MANUALLY_SELECTED_PLAYBACK_OF_A_DIFF_ITEM", ]; if (!validEndReasons.includes(endReason)) { return false; } // Get media duration in milliseconds const mediaDuration = parseInt(event["Media Duration In Milliseconds"]); // Track must be longer than 30 seconds if (isNaN(mediaDuration) || mediaDuration <= 30000) { return false; } // Calculate actual play duration const playDuration = parseInt(event["Play Duration Milliseconds"]); if (isNaN(playDuration)) { return false; } // Last.fm scrobbling rules: // Track must be played for at least 4 minutes (240000ms) OR 50% of duration const minimumDuration = Math.min(240000, mediaDuration * 0.5); if (playDuration < minimumDuration) { return false; } return true; } export function buildTrackCache(trackHistory: AppleMusicTrackHistory[]): void { trackCache.clear(); for (const track of trackHistory) { const [artist, name] = (track["Track Description"] || "").split(" - "); if (name) { const normalizedName = name.toLowerCase(); // Only store first occurrence (like original code) if (!trackCache.has(normalizedName)) { trackCache.set(normalizedName, { originId: track["Track Identifier"] === "0" ? null : track["Track Identifier"], artist: artist!, }); } } } } export function getExtraSongData( trackName: string, ): { originId: string | null; artist: string; } | null { const normalizedTrackName = trackName.toLowerCase(); const cached = trackCache.get(normalizedTrackName); if (cached) { return cached; } if (overrides[normalizedTrackName]) { return { originId: null, artist: overrides[normalizedTrackName], }; } return null; }