@goscribe/server 1.1.7 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +43 -0
- package/check-difficulty.cjs +14 -0
- package/check-questions.cjs +14 -0
- package/db-summary.cjs +22 -0
- package/dist/routers/auth.js +1 -1
- package/mcq-test.cjs +36 -0
- package/package.json +10 -2
- package/prisma/migrations/20260413143206_init/migration.sql +873 -0
- package/prisma/schema.prisma +485 -292
- package/src/context.ts +4 -1
- package/src/lib/activity_human_description.test.ts +28 -0
- package/src/lib/activity_human_description.ts +239 -0
- package/src/lib/activity_log_service.test.ts +37 -0
- package/src/lib/activity_log_service.ts +353 -0
- package/src/lib/ai-session.ts +194 -112
- package/src/lib/constants.ts +14 -0
- package/src/lib/email.ts +230 -0
- package/src/lib/env.ts +23 -6
- package/src/lib/inference.ts +3 -3
- package/src/lib/logger.ts +26 -9
- package/src/lib/notification-service.test.ts +106 -0
- package/src/lib/notification-service.ts +677 -0
- package/src/lib/prisma.ts +6 -1
- package/src/lib/pusher.ts +90 -6
- package/src/lib/retry.ts +61 -0
- package/src/lib/storage.ts +2 -2
- package/src/lib/stripe.ts +39 -0
- package/src/lib/subscription_service.ts +722 -0
- package/src/lib/usage_service.ts +74 -0
- package/src/lib/worksheet-generation.test.ts +31 -0
- package/src/lib/worksheet-generation.ts +139 -0
- package/src/lib/workspace-access.ts +13 -0
- package/src/routers/_app.ts +11 -0
- package/src/routers/admin.ts +710 -0
- package/src/routers/annotations.ts +227 -0
- package/src/routers/auth.ts +432 -33
- package/src/routers/copilot.ts +719 -0
- package/src/routers/flashcards.ts +207 -80
- package/src/routers/members.ts +280 -80
- package/src/routers/notifications.ts +142 -0
- package/src/routers/payment.ts +448 -0
- package/src/routers/podcast.ts +133 -108
- package/src/routers/studyguide.ts +80 -74
- package/src/routers/worksheets.ts +300 -80
- package/src/routers/workspace.ts +538 -328
- package/src/scripts/purge-deleted-users.ts +167 -0
- package/src/server.ts +140 -12
- package/src/services/flashcard-progress.service.ts +52 -43
- package/src/trpc.ts +184 -5
- package/test-generate.js +30 -0
- package/test-ratio.cjs +9 -0
- package/zod-test.cjs +22 -0
- package/prisma/migrations/20250826124819_add_worksheet_difficulty_and_estimated_time/migration.sql +0 -213
- package/prisma/migrations/20250826133236_add_worksheet_question_progress/migration.sql +0 -31
- package/prisma/seed.mjs +0 -135
- package/src/routers/meetingsummary.ts +0 -416
package/src/routers/podcast.ts
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { TRPCError } from '@trpc/server';
|
|
3
|
-
import { router, authedProcedure } from '../trpc.js';
|
|
3
|
+
import { router, authedProcedure, verifiedProcedure,limitedProcedure } from '../trpc.js';
|
|
4
4
|
import { v4 as uuidv4 } from 'uuid';
|
|
5
5
|
import inference from '../lib/inference.js';
|
|
6
6
|
import { uploadToSupabase, generateSignedUrl, deleteFromSupabase } from '../lib/storage.js';
|
|
7
7
|
import PusherService from '../lib/pusher.js';
|
|
8
8
|
import { aiSessionService } from '../lib/ai-session.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
STUDY_GUIDE: 'STUDY_GUIDE',
|
|
14
|
-
FLASHCARD_SET: 'FLASHCARD_SET',
|
|
15
|
-
} as const;
|
|
9
|
+
import { ArtifactType } from '../lib/constants.js';
|
|
10
|
+
import { workspaceAccessFilter } from '../lib/workspace-access.js';
|
|
11
|
+
import { logger } from '../lib/logger.js';
|
|
12
|
+
import { notifyArtifactFailed, notifyArtifactReady } from '../lib/notification-service.js';
|
|
16
13
|
|
|
17
14
|
// Podcast segment schema
|
|
18
15
|
const podcastSegmentSchema = z.object({
|
|
@@ -76,11 +73,11 @@ export const podcast = router({
|
|
|
76
73
|
// Check if workspace exists
|
|
77
74
|
|
|
78
75
|
if (!workspace) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
79
|
-
|
|
76
|
+
|
|
80
77
|
const artifacts = await ctx.db.artifact.findMany({
|
|
81
|
-
where: {
|
|
82
|
-
workspaceId: input.workspaceId,
|
|
83
|
-
type: ArtifactType.PODCAST_EPISODE
|
|
78
|
+
where: {
|
|
79
|
+
workspaceId: input.workspaceId,
|
|
80
|
+
type: ArtifactType.PODCAST_EPISODE
|
|
84
81
|
},
|
|
85
82
|
include: {
|
|
86
83
|
versions: {
|
|
@@ -93,10 +90,10 @@ export const podcast = router({
|
|
|
93
90
|
},
|
|
94
91
|
orderBy: { updatedAt: 'desc' },
|
|
95
92
|
});
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
|
|
94
|
+
logger.debug(`Found ${artifacts.length} podcast artifacts`);
|
|
98
95
|
artifacts.forEach((artifact, i) => {
|
|
99
|
-
|
|
96
|
+
logger.debug(` Podcast ${i + 1}: "${artifact.title}" - ${artifact.podcastSegments.length} segments`);
|
|
100
97
|
});
|
|
101
98
|
|
|
102
99
|
// Transform to include segments with fresh signed URLs
|
|
@@ -109,7 +106,7 @@ export const podcast = router({
|
|
|
109
106
|
if (artifact.imageObjectKey) {
|
|
110
107
|
objectUrl = await generateSignedUrl(artifact.imageObjectKey, 24);
|
|
111
108
|
}
|
|
112
|
-
|
|
109
|
+
|
|
113
110
|
// Generate fresh signed URLs for all segments
|
|
114
111
|
const segmentsWithUrls = await Promise.all(
|
|
115
112
|
artifact.podcastSegments.map(async (segment) => {
|
|
@@ -126,7 +123,7 @@ export const podcast = router({
|
|
|
126
123
|
order: segment.order,
|
|
127
124
|
};
|
|
128
125
|
} catch (error) {
|
|
129
|
-
|
|
126
|
+
logger.error(`Failed to generate signed URL for segment ${segment.id}:`, error);
|
|
130
127
|
return {
|
|
131
128
|
id: segment.id,
|
|
132
129
|
title: segment.title,
|
|
@@ -154,10 +151,10 @@ export const podcast = router({
|
|
|
154
151
|
let metadata = null;
|
|
155
152
|
if (latestVersion) {
|
|
156
153
|
try {
|
|
157
|
-
|
|
154
|
+
logger.debug(JSON.stringify(latestVersion.data))
|
|
158
155
|
metadata = podcastMetadataSchema.parse(latestVersion.data);
|
|
159
156
|
} catch (error) {
|
|
160
|
-
|
|
157
|
+
logger.error('Failed to parse podcast metadata:', error);
|
|
161
158
|
}
|
|
162
159
|
}
|
|
163
160
|
|
|
@@ -188,10 +185,10 @@ export const podcast = router({
|
|
|
188
185
|
.input(z.object({ episodeId: z.string() }))
|
|
189
186
|
.query(async ({ ctx, input }) => {
|
|
190
187
|
const episode = await ctx.db.artifact.findFirst({
|
|
191
|
-
where: {
|
|
188
|
+
where: {
|
|
192
189
|
id: input.episodeId,
|
|
193
190
|
type: ArtifactType.PODCAST_EPISODE,
|
|
194
|
-
workspace:
|
|
191
|
+
workspace: workspaceAccessFilter(ctx.session.user.id)
|
|
195
192
|
},
|
|
196
193
|
include: {
|
|
197
194
|
versions: {
|
|
@@ -204,23 +201,23 @@ export const podcast = router({
|
|
|
204
201
|
},
|
|
205
202
|
});
|
|
206
203
|
|
|
207
|
-
|
|
204
|
+
logger.debug(JSON.stringify(episode))
|
|
208
205
|
|
|
209
206
|
if (!episode) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
210
|
-
|
|
207
|
+
|
|
211
208
|
const latestVersion = episode.versions[0];
|
|
212
209
|
if (!latestVersion) throw new TRPCError({ code: 'NOT_FOUND', message: 'No version found' });
|
|
213
|
-
|
|
214
|
-
|
|
210
|
+
|
|
211
|
+
logger.debug(JSON.stringify(latestVersion))
|
|
215
212
|
try {
|
|
216
213
|
const metadata = podcastMetadataSchema.parse(latestVersion.data);
|
|
217
214
|
} catch (error) {
|
|
218
|
-
|
|
215
|
+
logger.error('Failed to parse podcast metadata:', error);
|
|
219
216
|
}
|
|
220
217
|
const metadata = podcastMetadataSchema.parse(latestVersion.data);
|
|
221
218
|
|
|
222
219
|
const imageUrl = episode.imageObjectKey ? await generateSignedUrl(episode.imageObjectKey, 24) : null;
|
|
223
|
-
|
|
220
|
+
|
|
224
221
|
|
|
225
222
|
// Generate fresh signed URLs for all segments
|
|
226
223
|
const segmentsWithUrls = await Promise.all(
|
|
@@ -240,7 +237,7 @@ export const podcast = router({
|
|
|
240
237
|
order: segment.order,
|
|
241
238
|
};
|
|
242
239
|
} catch (error) {
|
|
243
|
-
|
|
240
|
+
logger.error(`Failed to generate signed URL for segment ${segment.id}:`, error);
|
|
244
241
|
return {
|
|
245
242
|
id: segment.id,
|
|
246
243
|
title: segment.title,
|
|
@@ -267,7 +264,7 @@ export const podcast = router({
|
|
|
267
264
|
};
|
|
268
265
|
})
|
|
269
266
|
);
|
|
270
|
-
|
|
267
|
+
|
|
271
268
|
return {
|
|
272
269
|
id: episode.id,
|
|
273
270
|
title: metadata.title, // Use title from version metadata
|
|
@@ -282,7 +279,7 @@ export const podcast = router({
|
|
|
282
279
|
}),
|
|
283
280
|
|
|
284
281
|
// Generate podcast episode from text input
|
|
285
|
-
generateEpisode:
|
|
282
|
+
generateEpisode: limitedProcedure
|
|
286
283
|
.input(z.object({
|
|
287
284
|
workspaceId: z.string(),
|
|
288
285
|
podcastData: podcastInputSchema,
|
|
@@ -292,33 +289,33 @@ export const podcast = router({
|
|
|
292
289
|
where: { id: input.workspaceId, ownerId: ctx.session.user.id },
|
|
293
290
|
});
|
|
294
291
|
if (!workspace) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
295
|
-
|
|
296
|
-
// Emit podcast generation start notification
|
|
297
|
-
await PusherService.emitTaskComplete(input.workspaceId, 'podcast_generation_start', {
|
|
298
|
-
title: input.podcastData.title
|
|
299
|
-
});
|
|
300
292
|
|
|
301
|
-
|
|
293
|
+
// Emit podcast generation start notification
|
|
294
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'podcast_generation_start', {
|
|
295
|
+
title: input.podcastData.title
|
|
296
|
+
});
|
|
302
297
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
298
|
+
const BEGIN_PODCAST_GENERATION_MESSAGE = 'Structuring podcast contents...';
|
|
299
|
+
|
|
300
|
+
const newArtifact = await ctx.db.artifact.create({
|
|
301
|
+
data: {
|
|
302
|
+
title: '----',
|
|
303
|
+
type: ArtifactType.PODCAST_EPISODE,
|
|
304
|
+
generating: true,
|
|
305
|
+
generatingMetadata: {
|
|
306
|
+
message: BEGIN_PODCAST_GENERATION_MESSAGE,
|
|
307
|
+
},
|
|
308
|
+
workspace: {
|
|
309
|
+
connect: {
|
|
310
|
+
id: input.workspaceId,
|
|
315
311
|
}
|
|
316
312
|
}
|
|
317
|
-
}
|
|
313
|
+
}
|
|
314
|
+
});
|
|
318
315
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
316
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
|
|
317
|
+
message: BEGIN_PODCAST_GENERATION_MESSAGE,
|
|
318
|
+
});
|
|
322
319
|
|
|
323
320
|
try {
|
|
324
321
|
|
|
@@ -332,9 +329,9 @@ export const podcast = router({
|
|
|
332
329
|
);
|
|
333
330
|
|
|
334
331
|
if (!structureResult.success || !structureResult.structure) {
|
|
335
|
-
throw new TRPCError({
|
|
336
|
-
code: 'INTERNAL_SERVER_ERROR',
|
|
337
|
-
message: 'Failed to generate podcast structure'
|
|
332
|
+
throw new TRPCError({
|
|
333
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
334
|
+
message: 'Failed to generate podcast structure'
|
|
338
335
|
});
|
|
339
336
|
}
|
|
340
337
|
|
|
@@ -366,7 +363,7 @@ export const podcast = router({
|
|
|
366
363
|
}
|
|
367
364
|
});
|
|
368
365
|
|
|
369
|
-
await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
|
|
366
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
|
|
370
367
|
message: `Generating podcast image...`,
|
|
371
368
|
});
|
|
372
369
|
|
|
@@ -409,7 +406,7 @@ export const podcast = router({
|
|
|
409
406
|
}
|
|
410
407
|
});
|
|
411
408
|
|
|
412
|
-
await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
|
|
409
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
|
|
413
410
|
message: `Generating audio for segment ${i + 1} of ${structure.segments.length}...`,
|
|
414
411
|
});
|
|
415
412
|
|
|
@@ -444,47 +441,56 @@ export const podcast = router({
|
|
|
444
441
|
|
|
445
442
|
} catch (audioError) {
|
|
446
443
|
const errorMessage = audioError instanceof Error ? audioError.message : 'Unknown error';
|
|
447
|
-
|
|
444
|
+
logger.error(`❌ Error generating audio for segment ${i + 1}:`, {
|
|
448
445
|
title: segment.title,
|
|
449
446
|
error: errorMessage,
|
|
450
447
|
stack: audioError instanceof Error ? audioError.stack : undefined,
|
|
451
448
|
});
|
|
452
|
-
|
|
449
|
+
|
|
453
450
|
// Track failed segment
|
|
454
451
|
failedSegments.push({
|
|
455
452
|
index: i + 1,
|
|
456
453
|
title: segment.title || `Segment ${i + 1}`,
|
|
457
454
|
error: errorMessage,
|
|
458
455
|
});
|
|
459
|
-
|
|
460
|
-
await PusherService.emitTaskComplete(input.workspaceId, 'podcast_segment_error', {
|
|
456
|
+
|
|
457
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'podcast_segment_error', {
|
|
461
458
|
segmentIndex: i + 1,
|
|
462
459
|
segmentTitle: segment.title || `Segment ${i + 1}`,
|
|
463
460
|
error: errorMessage,
|
|
464
461
|
successfulSegments: segments.length,
|
|
465
462
|
failedSegments: failedSegments.length,
|
|
466
463
|
});
|
|
467
|
-
|
|
464
|
+
|
|
468
465
|
// Continue with other segments even if one fails
|
|
469
466
|
}
|
|
470
467
|
}
|
|
471
|
-
|
|
468
|
+
|
|
472
469
|
// Check if any segments were successfully generated
|
|
473
470
|
if (segments.length === 0) {
|
|
474
|
-
|
|
475
|
-
await PusherService.emitError(input.workspaceId,
|
|
476
|
-
`Failed to generate any segments. ${failedSegments.length} segment(s) failed.`,
|
|
471
|
+
logger.error('No segments were successfully generated');
|
|
472
|
+
await PusherService.emitError(input.workspaceId,
|
|
473
|
+
`Failed to generate any segments. ${failedSegments.length} segment(s) failed.`,
|
|
477
474
|
'podcast'
|
|
478
475
|
);
|
|
479
|
-
|
|
476
|
+
|
|
480
477
|
// Cleanup the artifact
|
|
478
|
+
await notifyArtifactFailed(ctx.db, {
|
|
479
|
+
userId: ctx.session.user.id,
|
|
480
|
+
workspaceId: input.workspaceId,
|
|
481
|
+
artifactType: ArtifactType.PODCAST_EPISODE,
|
|
482
|
+
artifactId: newArtifact.id,
|
|
483
|
+
title: input.podcastData.title,
|
|
484
|
+
message: `Failed to generate any audio segments. All ${failedSegments.length} attempts failed.`,
|
|
485
|
+
}).catch(() => {});
|
|
486
|
+
|
|
481
487
|
await ctx.db.artifact.delete({
|
|
482
488
|
where: { id: newArtifact.id },
|
|
483
489
|
});
|
|
484
|
-
|
|
485
|
-
throw new TRPCError({
|
|
486
|
-
code: 'INTERNAL_SERVER_ERROR',
|
|
487
|
-
message: `Failed to generate any audio segments. All ${failedSegments.length} attempts failed.`
|
|
490
|
+
|
|
491
|
+
throw new TRPCError({
|
|
492
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
493
|
+
message: `Failed to generate any audio segments. All ${failedSegments.length} attempts failed.`
|
|
488
494
|
});
|
|
489
495
|
}
|
|
490
496
|
|
|
@@ -500,7 +506,7 @@ export const podcast = router({
|
|
|
500
506
|
}
|
|
501
507
|
});
|
|
502
508
|
|
|
503
|
-
await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
|
|
509
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
|
|
504
510
|
message: `Preparing podcast summary...`,
|
|
505
511
|
});
|
|
506
512
|
|
|
@@ -527,7 +533,7 @@ export const podcast = router({
|
|
|
527
533
|
Podcast Title: ${structure.episodeTitle}
|
|
528
534
|
Segments: ${JSON.stringify(segments.map(s => ({ title: s.title, keyPoints: s.keyPoints })))}`;
|
|
529
535
|
|
|
530
|
-
const summaryResponse = await inference(summaryPrompt);
|
|
536
|
+
const summaryResponse = await inference([{ role: "user", content: summaryPrompt }]);
|
|
531
537
|
const summaryContent: string = summaryResponse.choices[0].message.content || '';
|
|
532
538
|
|
|
533
539
|
let episodeSummary;
|
|
@@ -539,8 +545,8 @@ export const podcast = router({
|
|
|
539
545
|
}
|
|
540
546
|
episodeSummary = JSON.parse(jsonMatch[0]);
|
|
541
547
|
} catch (parseError) {
|
|
542
|
-
|
|
543
|
-
await PusherService.emitTaskComplete(input.workspaceId, 'podcast_summary_error', {
|
|
548
|
+
logger.error('Failed to parse summary response:', summaryContent);
|
|
549
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'podcast_summary_error', {
|
|
544
550
|
error: 'Failed to parse summary response'
|
|
545
551
|
});
|
|
546
552
|
episodeSummary = {
|
|
@@ -555,13 +561,13 @@ export const podcast = router({
|
|
|
555
561
|
}
|
|
556
562
|
|
|
557
563
|
// Emit summary generation completion notification
|
|
558
|
-
await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
|
|
564
|
+
await PusherService.emitTaskComplete(input.workspaceId, 'podcast_info', {
|
|
559
565
|
message: `Podcast summary generated.`,
|
|
560
566
|
});
|
|
561
567
|
|
|
562
568
|
// Step 4: Create artifact and initial version
|
|
563
569
|
const episodeTitle = structure.episodeTitle || input.podcastData.title;
|
|
564
|
-
|
|
570
|
+
|
|
565
571
|
await ctx.db.artifact.update({
|
|
566
572
|
where: {
|
|
567
573
|
id: newArtifact.id,
|
|
@@ -574,7 +580,7 @@ export const podcast = router({
|
|
|
574
580
|
createdById: ctx.session.user.id,
|
|
575
581
|
},
|
|
576
582
|
});
|
|
577
|
-
|
|
583
|
+
|
|
578
584
|
const createdSegments = await ctx.db.podcastSegment.createMany({
|
|
579
585
|
data: segments.map(segment => ({
|
|
580
586
|
artifactId: newArtifact.id,
|
|
@@ -591,7 +597,7 @@ export const podcast = router({
|
|
|
591
597
|
},
|
|
592
598
|
})),
|
|
593
599
|
});
|
|
594
|
-
|
|
600
|
+
|
|
595
601
|
const metadata = {
|
|
596
602
|
title: episodeTitle,
|
|
597
603
|
description: input.podcastData.description,
|
|
@@ -621,7 +627,14 @@ export const podcast = router({
|
|
|
621
627
|
});
|
|
622
628
|
|
|
623
629
|
// Emit podcast generation completion notification
|
|
624
|
-
await PusherService.emitPodcastComplete(input.workspaceId,
|
|
630
|
+
await PusherService.emitPodcastComplete(input.workspaceId, newArtifact);
|
|
631
|
+
await notifyArtifactReady(ctx.db, {
|
|
632
|
+
userId: ctx.session.user.id,
|
|
633
|
+
workspaceId: input.workspaceId,
|
|
634
|
+
artifactId: newArtifact.id,
|
|
635
|
+
artifactType: ArtifactType.PODCAST_EPISODE,
|
|
636
|
+
title: metadata.title,
|
|
637
|
+
}).catch(() => {});
|
|
625
638
|
|
|
626
639
|
return {
|
|
627
640
|
id: newArtifact.id,
|
|
@@ -633,7 +646,19 @@ export const podcast = router({
|
|
|
633
646
|
|
|
634
647
|
} catch (error) {
|
|
635
648
|
|
|
636
|
-
|
|
649
|
+
logger.error('Error generating podcast episode:', error);
|
|
650
|
+
|
|
651
|
+
await notifyArtifactFailed(ctx.db, {
|
|
652
|
+
userId: ctx.session.user.id,
|
|
653
|
+
workspaceId: input.workspaceId,
|
|
654
|
+
artifactType: ArtifactType.PODCAST_EPISODE,
|
|
655
|
+
artifactId: newArtifact.id,
|
|
656
|
+
title: input.podcastData.title,
|
|
657
|
+
message:
|
|
658
|
+
error instanceof Error
|
|
659
|
+
? error.message
|
|
660
|
+
: 'Podcast generation failed.',
|
|
661
|
+
}).catch(() => {});
|
|
637
662
|
|
|
638
663
|
await ctx.db.artifact.delete({
|
|
639
664
|
where: {
|
|
@@ -641,9 +666,9 @@ export const podcast = router({
|
|
|
641
666
|
},
|
|
642
667
|
});
|
|
643
668
|
await PusherService.emitError(input.workspaceId, `Failed to generate podcast episode: ${error instanceof Error ? error.message : 'Unknown error'}`, 'podcast');
|
|
644
|
-
throw new TRPCError({
|
|
645
|
-
code: 'INTERNAL_SERVER_ERROR',
|
|
646
|
-
message: `Failed to generate podcast episode: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
669
|
+
throw new TRPCError({
|
|
670
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
671
|
+
message: `Failed to generate podcast episode: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
647
672
|
});
|
|
648
673
|
}
|
|
649
674
|
}),
|
|
@@ -660,10 +685,10 @@ export const podcast = router({
|
|
|
660
685
|
.input(z.object({ episodeId: z.string() }))
|
|
661
686
|
.query(async ({ ctx, input }) => {
|
|
662
687
|
const episode = await ctx.db.artifact.findFirst({
|
|
663
|
-
where: {
|
|
688
|
+
where: {
|
|
664
689
|
id: input.episodeId,
|
|
665
690
|
type: ArtifactType.PODCAST_EPISODE,
|
|
666
|
-
workspace:
|
|
691
|
+
workspace: workspaceAccessFilter(ctx.session.user.id)
|
|
667
692
|
},
|
|
668
693
|
include: {
|
|
669
694
|
versions: {
|
|
@@ -675,14 +700,14 @@ export const podcast = router({
|
|
|
675
700
|
},
|
|
676
701
|
},
|
|
677
702
|
});
|
|
678
|
-
|
|
703
|
+
|
|
679
704
|
if (!episode) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
680
|
-
|
|
705
|
+
|
|
681
706
|
const latestVersion = episode.versions[0];
|
|
682
707
|
if (!latestVersion) throw new TRPCError({ code: 'NOT_FOUND', message: 'No version found' });
|
|
683
708
|
|
|
684
709
|
const metadata = podcastMetadataSchema.parse(latestVersion.data);
|
|
685
|
-
|
|
710
|
+
|
|
686
711
|
return {
|
|
687
712
|
segments: episode.podcastSegments.map(s => ({
|
|
688
713
|
id: s.id,
|
|
@@ -711,10 +736,10 @@ export const podcast = router({
|
|
|
711
736
|
}))
|
|
712
737
|
.mutation(async ({ ctx, input }) => {
|
|
713
738
|
const episode = await ctx.db.artifact.findFirst({
|
|
714
|
-
where: {
|
|
739
|
+
where: {
|
|
715
740
|
id: input.episodeId,
|
|
716
741
|
type: ArtifactType.PODCAST_EPISODE,
|
|
717
|
-
workspace:
|
|
742
|
+
workspace: workspaceAccessFilter(ctx.session.user.id)
|
|
718
743
|
},
|
|
719
744
|
include: {
|
|
720
745
|
versions: {
|
|
@@ -723,14 +748,14 @@ export const podcast = router({
|
|
|
723
748
|
},
|
|
724
749
|
},
|
|
725
750
|
});
|
|
726
|
-
|
|
751
|
+
|
|
727
752
|
if (!episode) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
728
|
-
|
|
753
|
+
|
|
729
754
|
const latestVersion = episode.versions[0];
|
|
730
755
|
if (!latestVersion) throw new TRPCError({ code: 'NOT_FOUND', message: 'No version found' });
|
|
731
756
|
|
|
732
757
|
const metadata = podcastMetadataSchema.parse(latestVersion.data);
|
|
733
|
-
|
|
758
|
+
|
|
734
759
|
// Update metadata
|
|
735
760
|
if (input.title) metadata.title = input.title;
|
|
736
761
|
if (input.description) metadata.description = input.description;
|
|
@@ -746,7 +771,7 @@ export const podcast = router({
|
|
|
746
771
|
createdById: ctx.session.user.id,
|
|
747
772
|
},
|
|
748
773
|
});
|
|
749
|
-
|
|
774
|
+
|
|
750
775
|
// Update the artifact with basic info for listing/searching
|
|
751
776
|
return ctx.db.artifact.update({
|
|
752
777
|
where: { id: input.episodeId },
|
|
@@ -763,10 +788,10 @@ export const podcast = router({
|
|
|
763
788
|
.input(z.object({ episodeId: z.string() }))
|
|
764
789
|
.mutation(async ({ ctx, input }) => {
|
|
765
790
|
const episode = await ctx.db.artifact.findFirst({
|
|
766
|
-
where: {
|
|
791
|
+
where: {
|
|
767
792
|
id: input.episodeId,
|
|
768
793
|
type: ArtifactType.PODCAST_EPISODE,
|
|
769
|
-
workspace:
|
|
794
|
+
workspace: workspaceAccessFilter(ctx.session.user.id)
|
|
770
795
|
},
|
|
771
796
|
include: {
|
|
772
797
|
versions: {
|
|
@@ -775,12 +800,12 @@ export const podcast = router({
|
|
|
775
800
|
},
|
|
776
801
|
},
|
|
777
802
|
});
|
|
778
|
-
|
|
803
|
+
|
|
779
804
|
if (!episode) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
780
805
|
|
|
781
806
|
try {
|
|
782
807
|
// Emit episode deletion start notification
|
|
783
|
-
await PusherService.emitTaskComplete(episode.workspaceId, 'podcast_deletion_start', {
|
|
808
|
+
await PusherService.emitTaskComplete(episode.workspaceId, 'podcast_deletion_start', {
|
|
784
809
|
episodeId: input.episodeId,
|
|
785
810
|
episodeTitle: episode.title || 'Untitled Episode'
|
|
786
811
|
});
|
|
@@ -796,7 +821,7 @@ export const podcast = router({
|
|
|
796
821
|
try {
|
|
797
822
|
await deleteFromSupabase(segment.objectKey);
|
|
798
823
|
} catch (error) {
|
|
799
|
-
|
|
824
|
+
logger.error(`Failed to delete audio file ${segment.objectKey}:`, error);
|
|
800
825
|
}
|
|
801
826
|
}
|
|
802
827
|
}
|
|
@@ -817,7 +842,7 @@ export const podcast = router({
|
|
|
817
842
|
});
|
|
818
843
|
|
|
819
844
|
// Emit episode deletion completion notification
|
|
820
|
-
await PusherService.emitTaskComplete(episode.workspaceId, 'podcast_deletion_complete', {
|
|
845
|
+
await PusherService.emitTaskComplete(episode.workspaceId, 'podcast_deletion_complete', {
|
|
821
846
|
episodeId: input.episodeId,
|
|
822
847
|
episodeTitle: episode.title || 'Untitled Episode'
|
|
823
848
|
});
|
|
@@ -825,11 +850,11 @@ export const podcast = router({
|
|
|
825
850
|
return true;
|
|
826
851
|
|
|
827
852
|
} catch (error) {
|
|
828
|
-
|
|
853
|
+
logger.error('Error deleting episode:', error);
|
|
829
854
|
await PusherService.emitError(episode.workspaceId, `Failed to delete episode: ${error instanceof Error ? error.message : 'Unknown error'}`, 'podcast');
|
|
830
|
-
throw new TRPCError({
|
|
831
|
-
code: 'INTERNAL_SERVER_ERROR',
|
|
832
|
-
message: 'Failed to delete episode'
|
|
855
|
+
throw new TRPCError({
|
|
856
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
857
|
+
message: 'Failed to delete episode'
|
|
833
858
|
});
|
|
834
859
|
}
|
|
835
860
|
}),
|
|
@@ -839,10 +864,10 @@ export const podcast = router({
|
|
|
839
864
|
.input(z.object({ segmentId: z.string() }))
|
|
840
865
|
.query(async ({ ctx, input }) => {
|
|
841
866
|
const segment = await ctx.db.podcastSegment.findFirst({
|
|
842
|
-
where: {
|
|
867
|
+
where: {
|
|
843
868
|
id: input.segmentId,
|
|
844
869
|
artifact: {
|
|
845
|
-
workspace:
|
|
870
|
+
workspace: workspaceAccessFilter(ctx.session.user.id)
|
|
846
871
|
}
|
|
847
872
|
},
|
|
848
873
|
include: {
|
|
@@ -858,7 +883,7 @@ export const podcast = router({
|
|
|
858
883
|
try {
|
|
859
884
|
audioUrl = await generateSignedUrl(segment.objectKey, 24); // 24 hours
|
|
860
885
|
} catch (error) {
|
|
861
|
-
|
|
886
|
+
logger.error(`Failed to generate signed URL for segment ${segment.id}:`, error);
|
|
862
887
|
}
|
|
863
888
|
}
|
|
864
889
|
|