A tool to scrobble tracks from your Apple Music data export to Teal.fm.
at main 2.8 kB view raw
1import { getCache, updateCache } from "./utils/cache"; 2import { 3 constructRecord, 4 createRecord, 5 getLastTimestamp, 6} from "./utils/atproto"; 7import { 8 buildTrackCache, 9 getExtraSongData, 10 isEligible, 11 parseCSV, 12} from "./utils/data"; 13import consola from "consola"; 14import { lookupTrack } from "./utils/musicbrainz"; 15import type { AppleMusicPlaybackEvent } from "./types"; 16 17const logger = consola.withTag("AM -> TEAL.FM"); 18 19const dataExportPath = "/Users/bryceoliver/Desktop/Apple Music Activity/"; 20const playActivity = await parseCSV( 21 dataExportPath, 22 "Apple Music Play Activity", 23) as AppleMusicPlaybackEvent[]; 24 25const dailyTrackHistory = await parseCSV( 26 dataExportPath, 27 "Apple Music - Play History Daily Tracks", 28); 29 30buildTrackCache(dailyTrackHistory); 31 32const lastTimestamp = new Date(await getLastTimestamp()); 33logger.info("Last Teal.fm record timestamp: ", lastTimestamp.toDateString()); 34 35const progress = await getCache(); 36 37const eligible = playActivity.filter((event) => 38 isEligible(event) && new Date(event["Event End Timestamp"]) > lastTimestamp 39); 40 41const notYetImported = eligible.filter((event) => 42 !progress.some((p) => 43 p.songName === event["Song Name"] && 44 p.timestamp === event["Event End Timestamp"] 45 ) 46); 47 48logger.info(`Importing ${notYetImported.length.toLocaleString()} tracks`); 49 50const events = notYetImported 51 .sort( 52 (a, b) => 53 new Date(a["Event End Timestamp"]).getTime() - 54 new Date(b["Event End Timestamp"]).getTime(), 55 ); 56 57const missingTrackData = [ 58 ...new Set( 59 events.filter((x) => getExtraSongData(x["Song Name"]) == null).map(( 60 x, 61 ) => x["Song Name"]), 62 ), 63]; 64 65if (missingTrackData.length > 0) { 66 logger.warn( 67 "Missing track data! Please create an artist_overrides.json file to specify the artist for the following tracks", 68 ); 69 console.log(missingTrackData.join(", ")); 70 process.exit(1); 71} 72 73for (const event of events) { 74 const details = getExtraSongData(event["Song Name"])!; 75 logger.log(`Resolving track ${event["Song Name"]} by ${details.artist}..`); 76 77 const track = await lookupTrack( 78 details.artist, 79 event["Album Name"], 80 event["Song Name"], 81 )!; 82 83 if (!track) { 84 logger.error("Failed to resolve track."); 85 process.exit(1); 86 } 87 88 logger.success( 89 `Resolved track to recording "${track.recording.title}" and release "${track.release.title}"!`, 90 ); 91 92 const record = await constructRecord( 93 event["Event End Timestamp"], 94 event["Song Name"], 95 track.recording, 96 track.release, 97 details.originId, 98 ); 99 100 console.log(record); 101 102 await updateCache(event); 103 await createRecord(record); 104}