馃 distributed transcription service thistle.dunkirk.sh
at main 3.9 kB view raw
1import { $ } from "bun"; 2 3/** 4 * Extracts creation date from audio file metadata using ffprobe 5 * Falls back to file birth time (original creation) if no metadata found 6 * @param filePath Path to audio file 7 * @returns Date object or null if not found 8 */ 9export async function extractAudioCreationDate( 10 filePath: string, 11): Promise<Date | null> { 12 try { 13 // Use ffprobe to extract creation_time metadata 14 // -v quiet: suppress verbose output 15 // -print_format json: output as JSON 16 // -show_entries format_tags: show all tags to search for date fields 17 const result = 18 await $`ffprobe -v quiet -print_format json -show_entries format_tags ${filePath}`.text(); 19 20 const metadata = JSON.parse(result); 21 const tags = metadata?.format?.tags || {}; 22 23 // Try multiple metadata fields that might contain creation date 24 const dateFields = [ 25 tags.creation_time, // Standard creation_time 26 tags.date, // Common date field 27 tags.DATE, // Uppercase variant 28 tags.year, // Year field 29 tags.YEAR, // Uppercase variant 30 tags["com.apple.quicktime.creationdate"], // Apple QuickTime 31 tags.TDRC, // ID3v2 recording time 32 tags.TDRL, // ID3v2 release time 33 ]; 34 35 for (const dateField of dateFields) { 36 if (dateField) { 37 const date = new Date(dateField); 38 if (!Number.isNaN(date.getTime())) { 39 console.log( 40 `[AudioMetadata] Extracted creation date from metadata: ${date.toISOString()} from ${filePath}`, 41 ); 42 return date; 43 } 44 } 45 } 46 47 // Fallback: use file birth time (original creation time on filesystem) 48 // This preserves the original file creation date better than mtime 49 console.log( 50 `[AudioMetadata] No creation_time metadata found, using file birth time`, 51 ); 52 const file = Bun.file(filePath); 53 const stat = await file.stat(); 54 const date = new Date(stat.birthtime || stat.mtime); 55 console.log( 56 `[AudioMetadata] Using file birth time: ${date.toISOString()} from ${filePath}`, 57 ); 58 return date; 59 } catch (error) { 60 console.error( 61 `[AudioMetadata] Failed to extract metadata from ${filePath}:`, 62 error instanceof Error ? error.message : "Unknown error", 63 ); 64 return null; 65 } 66} 67 68/** 69 * Gets day of week from a date (0 = Sunday, 6 = Saturday) 70 */ 71export function getDayOfWeek(date: Date): number { 72 return date.getDay(); 73} 74 75/** 76 * Gets day name from a date 77 */ 78export function getDayName(date: Date): string { 79 const days = [ 80 "Sunday", 81 "Monday", 82 "Tuesday", 83 "Wednesday", 84 "Thursday", 85 "Friday", 86 "Saturday", 87 ]; 88 return days[date.getDay()] || "Unknown"; 89} 90 91/** 92 * Checks if a meeting time label matches a specific day 93 * Labels like "Monday Lecture", "Tuesday Lab", "Wed Discussion" should match 94 */ 95export function meetingTimeLabelMatchesDay( 96 label: string, 97 dayName: string, 98): boolean { 99 const lowerLabel = label.toLowerCase(); 100 const lowerDay = dayName.toLowerCase(); 101 102 // Check for full day name 103 if (lowerLabel.includes(lowerDay)) { 104 return true; 105 } 106 107 // Check for 3-letter abbreviations 108 const abbrev = dayName.slice(0, 3).toLowerCase(); 109 if (lowerLabel.includes(abbrev)) { 110 return true; 111 } 112 113 return false; 114} 115 116/** 117 * Finds the best matching meeting time for a given date 118 * @param date Date from audio metadata 119 * @param meetingTimes Available meeting times for the class 120 * @returns Meeting time ID or null if no match 121 */ 122export function findMatchingMeetingTime( 123 date: Date, 124 meetingTimes: Array<{ id: string; label: string }>, 125): string | null { 126 const dayName = getDayName(date); 127 128 // Find meeting time that matches the day 129 const match = meetingTimes.find((mt) => 130 meetingTimeLabelMatchesDay(mt.label, dayName), 131 ); 132 133 if (match) { 134 console.log( 135 `[AudioMetadata] Matched ${dayName} to meeting time: ${match.label}`, 136 ); 137 return match.id; 138 } 139 140 console.log( 141 `[AudioMetadata] No meeting time found matching ${dayName} in available options: ${meetingTimes.map((mt) => mt.label).join(", ")}`, 142 ); 143 return null; 144}