···11+import { $ } from "bun";
22+33+/**
44+ * Extracts creation date from audio file metadata using ffprobe
55+ * Falls back to file birth time (original creation) if no metadata found
66+ * @param filePath Path to audio file
77+ * @returns Date object or null if not found
88+ */
99+export async function extractAudioCreationDate(
1010+ filePath: string,
1111+): Promise<Date | null> {
1212+ try {
1313+ // Use ffprobe to extract creation_time metadata
1414+ // -v quiet: suppress verbose output
1515+ // -print_format json: output as JSON
1616+ // -show_entries format_tags: show all tags to search for date fields
1717+ const result =
1818+ await $`ffprobe -v quiet -print_format json -show_entries format_tags ${filePath}`.text();
1919+2020+ const metadata = JSON.parse(result);
2121+ const tags = metadata?.format?.tags || {};
2222+2323+ // Try multiple metadata fields that might contain creation date
2424+ const dateFields = [
2525+ tags.creation_time, // Standard creation_time
2626+ tags.date, // Common date field
2727+ tags.DATE, // Uppercase variant
2828+ tags.year, // Year field
2929+ tags.YEAR, // Uppercase variant
3030+ tags["com.apple.quicktime.creationdate"], // Apple QuickTime
3131+ tags.TDRC, // ID3v2 recording time
3232+ tags.TDRL, // ID3v2 release time
3333+ ];
3434+3535+ for (const dateField of dateFields) {
3636+ if (dateField) {
3737+ const date = new Date(dateField);
3838+ if (!Number.isNaN(date.getTime())) {
3939+ console.log(
4040+ `[AudioMetadata] Extracted creation date from metadata: ${date.toISOString()} from ${filePath}`,
4141+ );
4242+ return date;
4343+ }
4444+ }
4545+ }
4646+4747+ // Fallback: use file birth time (original creation time on filesystem)
4848+ // This preserves the original file creation date better than mtime
4949+ console.log(
5050+ `[AudioMetadata] No creation_time metadata found, using file birth time`,
5151+ );
5252+ const file = Bun.file(filePath);
5353+ const stat = await file.stat();
5454+ const date = new Date(stat.birthtime || stat.mtime);
5555+ console.log(
5656+ `[AudioMetadata] Using file birth time: ${date.toISOString()} from ${filePath}`,
5757+ );
5858+ return date;
5959+ } catch (error) {
6060+ console.error(
6161+ `[AudioMetadata] Failed to extract metadata from ${filePath}:`,
6262+ error instanceof Error ? error.message : "Unknown error",
6363+ );
6464+ return null;
6565+ }
6666+}
6767+6868+/**
6969+ * Gets day of week from a date (0 = Sunday, 6 = Saturday)
7070+ */
7171+export function getDayOfWeek(date: Date): number {
7272+ return date.getDay();
7373+}
7474+7575+/**
7676+ * Gets day name from a date
7777+ */
7878+export function getDayName(date: Date): string {
7979+ const days = [
8080+ "Sunday",
8181+ "Monday",
8282+ "Tuesday",
8383+ "Wednesday",
8484+ "Thursday",
8585+ "Friday",
8686+ "Saturday",
8787+ ];
8888+ return days[date.getDay()] || "Unknown";
8989+}
9090+9191+/**
9292+ * Checks if a meeting time label matches a specific day
9393+ * Labels like "Monday Lecture", "Tuesday Lab", "Wed Discussion" should match
9494+ */
9595+export function meetingTimeLabelMatchesDay(
9696+ label: string,
9797+ dayName: string,
9898+): boolean {
9999+ const lowerLabel = label.toLowerCase();
100100+ const lowerDay = dayName.toLowerCase();
101101+102102+ // Check for full day name
103103+ if (lowerLabel.includes(lowerDay)) {
104104+ return true;
105105+ }
106106+107107+ // Check for 3-letter abbreviations
108108+ const abbrev = dayName.slice(0, 3).toLowerCase();
109109+ if (lowerLabel.includes(abbrev)) {
110110+ return true;
111111+ }
112112+113113+ return false;
114114+}
115115+116116+/**
117117+ * Finds the best matching meeting time for a given date
118118+ * @param date Date from audio metadata
119119+ * @param meetingTimes Available meeting times for the class
120120+ * @returns Meeting time ID or null if no match
121121+ */
122122+export function findMatchingMeetingTime(
123123+ date: Date,
124124+ meetingTimes: Array<{ id: string; label: string }>,
125125+): string | null {
126126+ const dayName = getDayName(date);
127127+128128+ // Find meeting time that matches the day
129129+ const match = meetingTimes.find((mt) =>
130130+ meetingTimeLabelMatchesDay(mt.label, dayName),
131131+ );
132132+133133+ if (match) {
134134+ console.log(
135135+ `[AudioMetadata] Matched ${dayName} to meeting time: ${match.label}`,
136136+ );
137137+ return match.id;
138138+ }
139139+140140+ console.log(
141141+ `[AudioMetadata] No meeting time found matching ${dayName} in available options: ${meetingTimes.map((mt) => mt.label).join(", ")}`,
142142+ );
143143+ return null;
144144+}