fixed excel stuff

This commit is contained in:
2025-07-25 15:37:12 +02:00
parent a39cf21ae8
commit 46ba0fe0e1
5 changed files with 83 additions and 61 deletions

View File

@@ -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()
});

View File

@@ -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()}

View File

@@ -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),
@@ -41,13 +56,21 @@ export async function load() {
eq(rating.isCompleted, true),
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
};

View File

@@ -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

View File

@@ -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() : '-'
]);
});