···
} from "./lib/transcription";
95
-
extractAudioCreationDate,
} from "./lib/audio-metadata";
100
+
deletePendingRecording,
101
+
getEnrolledUserCount,
102
+
getPendingRecordings,
103
+
getUserVoteForMeeting,
104
+
markAsAutoSubmitted,
107
+
} from "./lib/voting";
···
let creationDate: Date | null = null;
2116
-
// Try client-provided timestamp first (most accurate - from original file)
2125
+
// Use client-provided timestamp (from File.lastModified)
const timestamp = Number.parseInt(fileTimestampStr, 10);
if (!Number.isNaN(timestamp)) {
creationDate = new Date(timestamp);
2122
-
`[Upload] Using client-provided file timestamp: ${creationDate.toISOString()}`,
2131
+
`[Upload] Using file timestamp: ${creationDate.toISOString()}`,
2127
-
// Fallback: extract from audio file metadata
2128
-
if (!creationDate) {
2129
-
// Save file temporarily
2130
-
const tempId = crypto.randomUUID();
2131
-
const fileExtension = file.name.split(".").pop()?.toLowerCase();
2132
-
const tempFilename = `temp-${tempId}.${fileExtension}`;
2133
-
const tempPath = `./uploads/${tempFilename}`;
2135
-
await Bun.write(tempPath, file);
2138
-
creationDate = await extractAudioCreationDate(tempPath);
2140
-
// Clean up temp file
2142
-
await Bun.$`rm ${tempPath}`.quiet();
2144
-
// Ignore cleanup errors
2153
-
message: "Could not extract creation date from audio file",
2140
+
message: "Could not extract creation date from file",
···
2184
+
"/api/transcriptions/:id/meeting-time": {
2185
+
PATCH: async (req) => {
2187
+
const user = requireAuth(req);
2188
+
const transcriptionId = req.params.id;
2190
+
const body = await req.json();
2191
+
const meetingTimeId = body.meeting_time_id;
2193
+
if (!meetingTimeId) {
2194
+
return Response.json(
2195
+
{ error: "meeting_time_id required" },
2200
+
// Verify transcription ownership
2201
+
const transcription = db
2203
+
{ id: string; user_id: number; class_id: string | null },
2205
+
>("SELECT id, user_id, class_id FROM transcriptions WHERE id = ?")
2206
+
.get(transcriptionId);
2208
+
if (!transcription) {
2209
+
return Response.json(
2210
+
{ error: "Transcription not found" },
2215
+
if (transcription.user_id !== user.id && user.role !== "admin") {
2216
+
return Response.json({ error: "Forbidden" }, { status: 403 });
2219
+
// Verify meeting time belongs to the class
2220
+
if (transcription.class_id) {
2221
+
const meetingTime = db
2222
+
.query<{ id: string }, [string, string]>(
2223
+
"SELECT id FROM meeting_times WHERE id = ? AND class_id = ?",
2225
+
.get(meetingTimeId, transcription.class_id);
2227
+
if (!meetingTime) {
2228
+
return Response.json(
2231
+
"Meeting time does not belong to the class for this transcription",
2238
+
// Update meeting time
2240
+
"UPDATE transcriptions SET meeting_time_id = ? WHERE id = ?",
2241
+
[meetingTimeId, transcriptionId],
2244
+
return Response.json({
2246
+
message: "Meeting time updated successfully",
2249
+
return handleError(error);
2253
+
"/api/classes/:classId/meetings/:meetingTimeId/recordings": {
2254
+
GET: async (req) => {
2256
+
const user = requireAuth(req);
2257
+
const classId = req.params.classId;
2258
+
const meetingTimeId = req.params.meetingTimeId;
2260
+
// Verify user is enrolled in the class
2261
+
const enrolled = isUserEnrolledInClass(user.id, classId);
2262
+
if (!enrolled && user.role !== "admin") {
2263
+
return Response.json(
2264
+
{ error: "Not enrolled in this class" },
2269
+
// Get user's section for filtering (admins see all)
2270
+
const userSection =
2271
+
user.role === "admin" ? null : getUserSection(user.id, classId);
2273
+
const recordings = getPendingRecordings(
2278
+
const totalUsers = getEnrolledUserCount(classId);
2279
+
const userVote = getUserVoteForMeeting(
2285
+
// Check if any recording should be auto-submitted
2286
+
const winningId = checkAutoSubmit(
2292
+
return Response.json({
2294
+
total_users: totalUsers,
2295
+
user_vote: userVote,
2296
+
vote_threshold: Math.ceil(totalUsers * 0.4),
2297
+
winning_recording_id: winningId,
2300
+
return handleError(error);
2304
+
"/api/recordings/:id/vote": {
2305
+
POST: async (req) => {
2307
+
const user = requireAuth(req);
2308
+
const recordingId = req.params.id;
2310
+
// Verify user is enrolled in the recording's class
2311
+
const recording = db
2313
+
{ class_id: string; meeting_time_id: string; status: string },
2316
+
"SELECT class_id, meeting_time_id, status FROM transcriptions WHERE id = ?",
2318
+
.get(recordingId);
2321
+
return Response.json(
2322
+
{ error: "Recording not found" },
2327
+
if (recording.status !== "pending") {
2328
+
return Response.json(
2329
+
{ error: "Can only vote on pending recordings" },
2334
+
const enrolled = isUserEnrolledInClass(user.id, recording.class_id);
2335
+
if (!enrolled && user.role !== "admin") {
2336
+
return Response.json(
2337
+
{ error: "Not enrolled in this class" },
2342
+
// Remove existing vote for this meeting time
2343
+
const existingVote = getUserVoteForMeeting(
2345
+
recording.class_id,
2346
+
recording.meeting_time_id,
2348
+
if (existingVote) {
2349
+
removeVote(existingVote, user.id);
2353
+
const success = voteForRecording(recordingId, user.id);
2355
+
// Get user's section for auto-submit check
2356
+
const userSection =
2357
+
user.role === "admin"
2359
+
: getUserSection(user.id, recording.class_id);
2361
+
// Check if auto-submit threshold reached
2362
+
const winningId = checkAutoSubmit(
2363
+
recording.class_id,
2364
+
recording.meeting_time_id,
2368
+
markAsAutoSubmitted(winningId);
2369
+
// Start transcription
2370
+
const winningRecording = db
2371
+
.query<{ filename: string }, [string]>(
2372
+
"SELECT filename FROM transcriptions WHERE id = ?",
2375
+
if (winningRecording) {
2376
+
whisperService.startTranscription(
2378
+
winningRecording.filename,
2383
+
return Response.json({
2385
+
winning_recording_id: winningId,
2388
+
return handleError(error);
2392
+
"/api/recordings/:id": {
2393
+
DELETE: async (req) => {
2395
+
const user = requireAuth(req);
2396
+
const recordingId = req.params.id;
2398
+
const success = deletePendingRecording(
2401
+
user.role === "admin",
2405
+
return Response.json(
2406
+
{ error: "Cannot delete this recording" },
2411
+
return new Response(null, { status: 204 });
2413
+
return handleError(error);
···
const formData = await req.formData();
const file = formData.get("audio") as File;
const classId = formData.get("class_id") as string | null;
2339
-
const meetingTimeId = formData.get("meeting_time_id") as
const sectionId = formData.get("section_id") as string | null;
if (!file) throw ValidationErrors.missingField("audio");
···
const uploadDir = "./uploads";
await Bun.write(`${uploadDir}/${filename}`, file);
2409
-
// Auto-detect meeting time from audio metadata if class provided and no meeting_time_id
2410
-
let finalMeetingTimeId = meetingTimeId;
2411
-
if (classId && !meetingTimeId) {
2413
-
// Extract creation date from audio file
2414
-
const creationDate = await extractAudioCreationDate(
2415
-
`${uploadDir}/${filename}`,
2418
-
if (creationDate) {
2419
-
// Get meeting times for this class
2420
-
const meetingTimes = getMeetingTimesForClass(classId);
2422
-
if (meetingTimes.length > 0) {
2423
-
// Find matching meeting time based on day of week
2424
-
const matchedId = findMatchingMeetingTime(
2430
-
finalMeetingTimeId = matchedId;
2431
-
const dayName = getDayName(creationDate);
2433
-
`[Upload] Auto-detected meeting time for ${dayName} (${creationDate.toISOString()}) -> ${matchedId}`,
2436
-
const dayName = getDayName(creationDate);
2438
-
`[Upload] No meeting time matches ${dayName}, leaving unassigned`,
2444
-
// Non-fatal: just log and continue without auto-detection
2446
-
"[Upload] Failed to auto-detect meeting time:",
2447
-
error instanceof Error ? error.message : "Unknown error",
2452
-
// Create database record
2626
+
// Create database record (without meeting_time_id - will be set later via PATCH)
"INSERT INTO transcriptions (id, user_id, class_id, meeting_time_id, section_id, filename, original_filename, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
2459
-
finalMeetingTimeId,
2633
+
null, // meeting_time_id will be set via PATCH endpoint
···
2473
-
meeting_time_id: finalMeetingTimeId,
message: "Upload successful",