fixed excel stuff
This commit is contained in:
@@ -3,6 +3,46 @@ import { db } from '$lib/server/db/index.js';
|
||||
import { audioFile } from '$lib/server/db/schema.js';
|
||||
import { eq, isNull } from 'drizzle-orm';
|
||||
import { uploadToS3, deleteFromS3, generateAudioS3Key } from '$lib/server/s3.js';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
async function getAudioDuration(buffer) {
|
||||
try {
|
||||
// Create temporary file
|
||||
const tempDir = os.tmpdir();
|
||||
const tempFilePath = path.join(tempDir, `audio_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`);
|
||||
|
||||
// Write buffer to temp file
|
||||
await fs.writeFile(tempFilePath, buffer);
|
||||
|
||||
try {
|
||||
// Use ffprobe to get duration
|
||||
const { stdout } = await execAsync(`ffprobe -v quiet -show_entries format=duration -of csv=p=0 "${tempFilePath}"`);
|
||||
const duration = parseFloat(stdout.trim());
|
||||
|
||||
// Clean up temp file
|
||||
await fs.unlink(tempFilePath);
|
||||
|
||||
return isNaN(duration) ? null : duration;
|
||||
} catch (error) {
|
||||
// Clean up temp file even if ffprobe fails
|
||||
try {
|
||||
await fs.unlink(tempFilePath);
|
||||
} catch {}
|
||||
|
||||
console.error('Error extracting audio duration:', error);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating temp file for duration extraction:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function load() {
|
||||
const audioFiles = await db.select({
|
||||
@@ -43,16 +83,19 @@ export const actions = {
|
||||
const s3Key = generateAudioS3Key(id, file.name);
|
||||
|
||||
try {
|
||||
// Extract duration before uploading
|
||||
const duration = await getAudioDuration(buffer);
|
||||
|
||||
// Upload to S3 first
|
||||
await uploadToS3(s3Key, buffer, file.type);
|
||||
|
||||
// Then save metadata to database (without blob data)
|
||||
// Then save metadata to database with calculated duration
|
||||
await db.insert(audioFile).values({
|
||||
id,
|
||||
filename: file.name,
|
||||
contentType: file.type,
|
||||
s3Key,
|
||||
duration: null, // Will be updated by client-side after upload
|
||||
duration,
|
||||
fileSize: file.size,
|
||||
createdAt: new Date()
|
||||
});
|
||||
|
||||
@@ -91,9 +91,6 @@
|
||||
if (response.ok && !responseText.includes('missing') && !responseText.includes('error')) {
|
||||
uploadProgress[index].status = 'completed';
|
||||
uploadProgress[index].progress = 100;
|
||||
|
||||
// Extract duration from uploaded file
|
||||
extractAndSaveDuration(file);
|
||||
} else {
|
||||
uploadProgress[index].status = 'error';
|
||||
uploadProgress[index].error = 'Upload failed';
|
||||
@@ -140,48 +137,6 @@
|
||||
uploadProgress = newProgress;
|
||||
}
|
||||
|
||||
async function extractAndSaveDuration(file) {
|
||||
try {
|
||||
const audio = new Audio();
|
||||
const url = URL.createObjectURL(file);
|
||||
|
||||
audio.src = url;
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
audio.addEventListener('loadedmetadata', () => {
|
||||
URL.revokeObjectURL(url);
|
||||
resolve();
|
||||
});
|
||||
audio.addEventListener('error', reject);
|
||||
});
|
||||
|
||||
if (audio.duration && !isNaN(audio.duration)) {
|
||||
// Find the file ID from the most recent upload response
|
||||
// For now, we'll need to match by filename - this is a limitation
|
||||
const matchingFile = data.audioFiles.find(af => af.filename === file.name);
|
||||
if (matchingFile) {
|
||||
await updateFileDuration(matchingFile.id, audio.duration);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error extracting duration:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateFileDuration(fileId, duration) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('fileId', fileId);
|
||||
formData.append('duration', duration.toString());
|
||||
|
||||
await fetch('?/updateDuration', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating duration:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function startEdit(audioFile) {
|
||||
editingAudioId = audioFile.id;
|
||||
@@ -456,7 +411,7 @@
|
||||
{formatFileSize(audio.fileSize || 0)}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-500">
|
||||
{audio.duration ? formatTime(audio.duration) : 'Loading...'}
|
||||
{audio.duration ? formatTime(audio.duration) : '--'}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-500">
|
||||
{new Date(audio.createdAt).toLocaleDateString()}
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
import { db } from '$lib/server/db/index.js';
|
||||
import { rating, participant, audioFile, inviteLink, overallRating } from '$lib/server/db/schema.js';
|
||||
import { eq, isNull, and, desc } from 'drizzle-orm';
|
||||
import { eq, isNull, and, desc, count } from 'drizzle-orm';
|
||||
|
||||
export async function load() {
|
||||
// Only get active (non-deleted) completed ratings with timeseries data
|
||||
const ratings = await db
|
||||
.select({
|
||||
rating,
|
||||
rating: {
|
||||
id: rating.id,
|
||||
participantId: rating.participantId,
|
||||
audioFileId: rating.audioFileId,
|
||||
timestamp: rating.timestamp,
|
||||
value: rating.value,
|
||||
isCompleted: rating.isCompleted,
|
||||
timeseriesData: rating.timeseriesData,
|
||||
createdAt: rating.createdAt
|
||||
},
|
||||
participant,
|
||||
audioFile,
|
||||
audioFile: {
|
||||
id: audioFile.id,
|
||||
filename: audioFile.filename,
|
||||
duration: audioFile.duration,
|
||||
fileSize: audioFile.fileSize,
|
||||
createdAt: audioFile.createdAt
|
||||
},
|
||||
inviteLink
|
||||
})
|
||||
.from(rating)
|
||||
@@ -30,8 +45,8 @@ export async function load() {
|
||||
))
|
||||
.orderBy(desc(rating.createdAt));
|
||||
|
||||
const ratingStats = await db
|
||||
.select()
|
||||
const ratingCountResult = await db
|
||||
.select({ count: count() })
|
||||
.from(rating)
|
||||
.innerJoin(participant, and(
|
||||
eq(rating.participantId, participant.id),
|
||||
@@ -42,12 +57,20 @@ export async function load() {
|
||||
isNull(rating.deletedAt) // Only active ratings
|
||||
));
|
||||
|
||||
const totalRatings = ratingCountResult[0]?.count || 0;
|
||||
|
||||
// Get overall ratings
|
||||
const overallRatings = await db
|
||||
.select({
|
||||
overallRating,
|
||||
participant,
|
||||
audioFile,
|
||||
audioFile: {
|
||||
id: audioFile.id,
|
||||
filename: audioFile.filename,
|
||||
duration: audioFile.duration,
|
||||
fileSize: audioFile.fileSize,
|
||||
createdAt: audioFile.createdAt
|
||||
},
|
||||
inviteLink
|
||||
})
|
||||
.from(overallRating)
|
||||
@@ -168,7 +191,7 @@ export async function load() {
|
||||
|
||||
return {
|
||||
ratings,
|
||||
totalRatings: ratingStats.length,
|
||||
totalRatings,
|
||||
timeseriesData,
|
||||
overallRatingsMap
|
||||
};
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
<div class="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="mb-2 text-2xl font-bold text-gray-900">Participant Ratings</h1>
|
||||
<p class="text-gray-600">View and analyze participant ratings data with timeseries visualization.</p>
|
||||
<p class="text-gray-600">View participant ratings data with timeseries visualization.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
|
||||
@@ -22,6 +22,7 @@ export async function GET() {
|
||||
eq(participant.inviteToken, inviteLink.token),
|
||||
isNull(inviteLink.deletedAt)
|
||||
))
|
||||
.where(isNull(rating.deletedAt))
|
||||
.orderBy(desc(rating.createdAt));
|
||||
|
||||
// Get overall ratings
|
||||
@@ -71,6 +72,7 @@ export async function GET() {
|
||||
timestamp: entry.rating.timestamp,
|
||||
value: entry.rating.value,
|
||||
isCompleted: entry.rating.isCompleted,
|
||||
timeseriesData: entry.rating.timeseriesData,
|
||||
createdAt: entry.rating.createdAt
|
||||
});
|
||||
|
||||
@@ -156,10 +158,9 @@ export async function GET() {
|
||||
const lastUpdated = participantData.ratings.length > 0 ?
|
||||
new Date(Math.max(...participantData.ratings.map(r => new Date(r.createdAt).getTime()))) : null;
|
||||
|
||||
// Format timeseries data as string
|
||||
const timeseriesString = participantData.ratings
|
||||
.map(rating => `${rating.timestamp.toFixed(2)}:${rating.value}`)
|
||||
.join('; ');
|
||||
// Use the stored timeseries data from the rating record
|
||||
const timeseriesData = participantData.ratings.find(r => r.isCompleted && r.timeseriesData)?.timeseriesData ||
|
||||
JSON.stringify(participantData.ratings.map(r => ({ timestamp: r.timestamp, value: r.value })));
|
||||
|
||||
worksheet.addRow([
|
||||
participantData.name,
|
||||
@@ -168,7 +169,7 @@ export async function GET() {
|
||||
participantData.ratings.length,
|
||||
durationCovered.toFixed(2),
|
||||
participantData.overallRating !== undefined ? participantData.overallRating : '-',
|
||||
timeseriesString,
|
||||
timeseriesData,
|
||||
lastUpdated ? lastUpdated.toLocaleString() : '-'
|
||||
]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user