Release 0.5.1
This commit is contained in:
1 parent
5a9d7735d4
commit
fb24d171ae
36 files changed
+5099
-263
No files matched your search
+190
-3
@@ -4,6 +4,11 @@ import path from 'node:path';
|
||||
import { fileTypeFromBuffer } from 'file-type';
|
||||
import { getDatabase } from './database.js';
|
||||
import { getServerPaths } from './config.js';
|
||||
import {
|
||||
detectVideoProvider,
|
||||
fetchPresentationMetadata,
|
||||
type PresentationMode
|
||||
} from './presentation.js';
|
||||
|
||||
export const PIECE_STATUSES = ['inbox', 'preparing', 'ready', 'archived'] as const;
|
||||
export type PieceStatus = (typeof PIECE_STATUSES)[number];
|
||||
@@ -27,6 +32,18 @@ export type PieceInput = {
|
||||
moderationNotes: string;
|
||||
transitionText: string;
|
||||
presentationText: string;
|
||||
presentationMode: PresentationMode;
|
||||
presentationSourcePosition: number | null;
|
||||
presentationTitle: string;
|
||||
presentationDescription: string;
|
||||
presentationImageUrl: string;
|
||||
presentationSiteName: string;
|
||||
presentationProvider: string;
|
||||
presentationProviderId: string;
|
||||
presentationTitleManual: boolean;
|
||||
presentationDescriptionManual: boolean;
|
||||
presentationImageManual: boolean;
|
||||
presentationMetadataFetchedAtUtc: string | null;
|
||||
topicIds: string[];
|
||||
categoryIds: string[];
|
||||
sources: PieceSourceInput[];
|
||||
@@ -164,6 +181,25 @@ function validatePieceInput(input: PieceInput): PieceInput {
|
||||
}
|
||||
return { label, url, timecodeSeconds: source.timecodeSeconds };
|
||||
});
|
||||
if (!['text', 'link', 'video'].includes(input.presentationMode)) {
|
||||
throw new Error('INVALID_PRESENTATION');
|
||||
}
|
||||
const sourcePosition = input.presentationSourcePosition;
|
||||
if (
|
||||
sourcePosition !== null &&
|
||||
(!Number.isInteger(sourcePosition) || sourcePosition < 0 || sourcePosition >= sources.length)
|
||||
) {
|
||||
throw new Error('INVALID_PRESENTATION');
|
||||
}
|
||||
if (input.presentationMode !== 'text' && sourcePosition === null) {
|
||||
throw new Error('INVALID_PRESENTATION');
|
||||
}
|
||||
const presentationImageUrl = cleanText(input.presentationImageUrl, 2_000, 'presentation_image');
|
||||
if (presentationImageUrl && !validUrl(presentationImageUrl)) {
|
||||
throw new Error('INVALID_PRESENTATION');
|
||||
}
|
||||
const detected =
|
||||
sourcePosition === null ? null : detectVideoProvider(sources[sourcePosition].url);
|
||||
return {
|
||||
title,
|
||||
summary: cleanText(input.summary, 2_000, 'summary'),
|
||||
@@ -175,6 +211,22 @@ function validatePieceInput(input: PieceInput): PieceInput {
|
||||
moderationNotes: cleanText(input.moderationNotes, 50_000, 'moderation'),
|
||||
transitionText: cleanText(input.transitionText, 50_000, 'transition'),
|
||||
presentationText: cleanText(input.presentationText, 50_000, 'presentation'),
|
||||
presentationMode: input.presentationMode,
|
||||
presentationSourcePosition: sourcePosition,
|
||||
presentationTitle: cleanText(input.presentationTitle, 300, 'presentation_title'),
|
||||
presentationDescription: cleanText(
|
||||
input.presentationDescription,
|
||||
1_000,
|
||||
'presentation_description'
|
||||
),
|
||||
presentationImageUrl,
|
||||
presentationSiteName: cleanText(input.presentationSiteName, 200, 'presentation_site'),
|
||||
presentationProvider: detected?.provider ?? '',
|
||||
presentationProviderId: detected?.id ?? '',
|
||||
presentationTitleManual: Boolean(input.presentationTitleManual),
|
||||
presentationDescriptionManual: Boolean(input.presentationDescriptionManual),
|
||||
presentationImageManual: Boolean(input.presentationImageManual),
|
||||
presentationMetadataFetchedAtUtc: input.presentationMetadataFetchedAtUtc || null,
|
||||
topicIds,
|
||||
categoryIds,
|
||||
sources
|
||||
@@ -298,6 +350,18 @@ type PieceRow = {
|
||||
moderation_notes: string;
|
||||
transition_text: string;
|
||||
presentation_text: string;
|
||||
presentation_mode: PresentationMode;
|
||||
presentation_source_position: number | null;
|
||||
presentation_title: string;
|
||||
presentation_description: string;
|
||||
presentation_image_url: string;
|
||||
presentation_site_name: string;
|
||||
presentation_provider: string;
|
||||
presentation_provider_id: string;
|
||||
presentation_title_manual: number;
|
||||
presentation_description_manual: number;
|
||||
presentation_image_manual: number;
|
||||
presentation_metadata_fetched_at_utc: string | null;
|
||||
revision: number;
|
||||
created_at_utc: string;
|
||||
updated_at_utc: string;
|
||||
@@ -308,7 +372,12 @@ export function getPiece(id: string): PieceEditor | null {
|
||||
const row = database
|
||||
.prepare(
|
||||
`SELECT id, title, summary, format_id, status, duration_seconds, intro, research_text,
|
||||
moderation_notes, transition_text, presentation_text, revision, created_at_utc, updated_at_utc
|
||||
moderation_notes, transition_text, presentation_text, presentation_mode,
|
||||
presentation_source_position, presentation_title, presentation_description,
|
||||
presentation_image_url, presentation_site_name, presentation_provider,
|
||||
presentation_provider_id, presentation_title_manual, presentation_description_manual,
|
||||
presentation_image_manual, presentation_metadata_fetched_at_utc,
|
||||
revision, created_at_utc, updated_at_utc
|
||||
FROM pieces WHERE id = ?`
|
||||
)
|
||||
.get(id) as PieceRow | undefined;
|
||||
@@ -325,6 +394,18 @@ export function getPiece(id: string): PieceEditor | null {
|
||||
moderationNotes: row.moderation_notes,
|
||||
transitionText: row.transition_text,
|
||||
presentationText: row.presentation_text,
|
||||
presentationMode: row.presentation_mode,
|
||||
presentationSourcePosition: row.presentation_source_position,
|
||||
presentationTitle: row.presentation_title,
|
||||
presentationDescription: row.presentation_description,
|
||||
presentationImageUrl: row.presentation_image_url,
|
||||
presentationSiteName: row.presentation_site_name,
|
||||
presentationProvider: row.presentation_provider,
|
||||
presentationProviderId: row.presentation_provider_id,
|
||||
presentationTitleManual: row.presentation_title_manual === 1,
|
||||
presentationDescriptionManual: row.presentation_description_manual === 1,
|
||||
presentationImageManual: row.presentation_image_manual === 1,
|
||||
presentationMetadataFetchedAtUtc: row.presentation_metadata_fetched_at_utc,
|
||||
revision: row.revision,
|
||||
createdAtUtc: row.created_at_utc,
|
||||
updatedAtUtc: row.updated_at_utc,
|
||||
@@ -405,6 +486,11 @@ export function savePiece(id: string, expectedRevision: number, rawInput: PieceI
|
||||
.prepare(
|
||||
`UPDATE pieces SET title = ?, summary = ?, format_id = ?, status = ?, duration_seconds = ?,
|
||||
intro = ?, research_text = ?, moderation_notes = ?, transition_text = ?, presentation_text = ?,
|
||||
presentation_mode = ?, presentation_source_position = ?, presentation_title = ?,
|
||||
presentation_description = ?, presentation_image_url = ?, presentation_site_name = ?,
|
||||
presentation_provider = ?, presentation_provider_id = ?, presentation_title_manual = ?,
|
||||
presentation_description_manual = ?, presentation_image_manual = ?,
|
||||
presentation_metadata_fetched_at_utc = ?,
|
||||
updated_at_utc = ?, revision = revision + 1, archived_from_status = NULL
|
||||
WHERE id = ? AND revision = ? AND status != 'archived'`
|
||||
)
|
||||
@@ -419,6 +505,18 @@ export function savePiece(id: string, expectedRevision: number, rawInput: PieceI
|
||||
input.moderationNotes,
|
||||
input.transitionText,
|
||||
input.presentationText,
|
||||
input.presentationMode,
|
||||
input.presentationSourcePosition,
|
||||
input.presentationTitle,
|
||||
input.presentationDescription,
|
||||
input.presentationImageUrl,
|
||||
input.presentationSiteName,
|
||||
input.presentationProvider,
|
||||
input.presentationProviderId,
|
||||
input.presentationTitleManual ? 1 : 0,
|
||||
input.presentationDescriptionManual ? 1 : 0,
|
||||
input.presentationImageManual ? 1 : 0,
|
||||
input.presentationMetadataFetchedAtUtc,
|
||||
updatedAtUtc,
|
||||
id,
|
||||
expectedRevision
|
||||
@@ -477,6 +575,18 @@ export function createPiece(template: PieceTemplateId = 'blank'): string {
|
||||
moderationNotes: defaults.moderationNotes ?? '',
|
||||
transitionText: '',
|
||||
presentationText: defaults.presentationText ?? '',
|
||||
presentationMode: 'text',
|
||||
presentationSourcePosition: null,
|
||||
presentationTitle: '',
|
||||
presentationDescription: '',
|
||||
presentationImageUrl: '',
|
||||
presentationSiteName: '',
|
||||
presentationProvider: '',
|
||||
presentationProviderId: '',
|
||||
presentationTitleManual: false,
|
||||
presentationDescriptionManual: false,
|
||||
presentationImageManual: false,
|
||||
presentationMetadataFetchedAtUtc: null,
|
||||
topicIds: [],
|
||||
categoryIds: defaults.categoryIds ?? [],
|
||||
sources: []
|
||||
@@ -582,8 +692,13 @@ export async function duplicatePiece(sourceId: string): Promise<string> {
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO pieces (id, title, summary, format_id, status, duration_seconds, intro,
|
||||
research_text, moderation_notes, transition_text, presentation_text, created_at_utc,
|
||||
updated_at_utc, revision) VALUES (?, ?, ?, ?, 'preparing', ?, ?, ?, ?, ?, ?, ?, ?, 1)`
|
||||
research_text, moderation_notes, transition_text, presentation_text, presentation_mode,
|
||||
presentation_source_position, presentation_title, presentation_description,
|
||||
presentation_image_url, presentation_site_name, presentation_provider,
|
||||
presentation_provider_id, presentation_title_manual,
|
||||
presentation_description_manual, presentation_image_manual,
|
||||
presentation_metadata_fetched_at_utc, created_at_utc, updated_at_utc, revision)
|
||||
VALUES (?, ?, ?, ?, 'preparing', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`
|
||||
)
|
||||
.run(
|
||||
id,
|
||||
@@ -596,6 +711,18 @@ export async function duplicatePiece(sourceId: string): Promise<string> {
|
||||
source.moderationNotes,
|
||||
source.transitionText,
|
||||
source.presentationText,
|
||||
source.presentationMode,
|
||||
source.presentationSourcePosition,
|
||||
source.presentationTitle,
|
||||
source.presentationDescription,
|
||||
source.presentationImageUrl,
|
||||
source.presentationSiteName,
|
||||
source.presentationProvider,
|
||||
source.presentationProviderId,
|
||||
source.presentationTitleManual ? 1 : 0,
|
||||
source.presentationDescriptionManual ? 1 : 0,
|
||||
source.presentationImageManual ? 1 : 0,
|
||||
source.presentationMetadataFetchedAtUtc,
|
||||
now,
|
||||
now
|
||||
);
|
||||
@@ -627,6 +754,66 @@ export async function duplicatePiece(sourceId: string): Promise<string> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadPiecePresentationMetadata(
|
||||
id: string,
|
||||
expectedRevision: number,
|
||||
metadataLoader = fetchPresentationMetadata
|
||||
) {
|
||||
if (!Number.isInteger(expectedRevision) || expectedRevision < 1) {
|
||||
throw new Error('INVALID_REVISION');
|
||||
}
|
||||
const database = getDatabase();
|
||||
const piece = database
|
||||
.prepare(
|
||||
`SELECT p.revision, p.presentation_mode, p.presentation_source_position,
|
||||
p.presentation_title_manual, p.presentation_description_manual,
|
||||
p.presentation_image_manual, s.url
|
||||
FROM pieces p LEFT JOIN piece_sources s ON s.piece_id = p.id
|
||||
AND s.position = p.presentation_source_position
|
||||
WHERE p.id = ? AND p.status != 'archived'`
|
||||
)
|
||||
.get(id) as
|
||||
| {
|
||||
revision: number;
|
||||
presentation_mode: PresentationMode;
|
||||
presentation_source_position: number | null;
|
||||
presentation_title_manual: number;
|
||||
presentation_description_manual: number;
|
||||
presentation_image_manual: number;
|
||||
url: string | null;
|
||||
}
|
||||
| undefined;
|
||||
if (!piece) throw new Error('NOT_FOUND');
|
||||
if (piece.revision !== expectedRevision) throw new Error('CONFLICT');
|
||||
if (piece.presentation_mode === 'text' || !piece.url) throw new Error('INVALID_PRESENTATION');
|
||||
const metadata = await metadataLoader(piece.url);
|
||||
const now = new Date().toISOString();
|
||||
const result = database
|
||||
.prepare(
|
||||
`UPDATE pieces SET
|
||||
presentation_title = CASE WHEN presentation_title_manual = 1 THEN presentation_title ELSE ? END,
|
||||
presentation_description = CASE WHEN presentation_description_manual = 1 THEN presentation_description ELSE ? END,
|
||||
presentation_image_url = CASE WHEN presentation_image_manual = 1 THEN presentation_image_url ELSE ? END,
|
||||
presentation_site_name = ?, presentation_provider = ?, presentation_provider_id = ?,
|
||||
presentation_metadata_fetched_at_utc = ?, updated_at_utc = ?, revision = revision + 1
|
||||
WHERE id = ? AND revision = ? AND status != 'archived'`
|
||||
)
|
||||
.run(
|
||||
metadata.title,
|
||||
metadata.description,
|
||||
metadata.imageUrl,
|
||||
metadata.siteName,
|
||||
metadata.provider,
|
||||
metadata.providerId,
|
||||
now,
|
||||
now,
|
||||
id,
|
||||
expectedRevision
|
||||
);
|
||||
if (result.changes !== 1) throw new Error('CONFLICT');
|
||||
return { revision: expectedRevision + 1, updatedAtUtc: now };
|
||||
}
|
||||
|
||||
export async function uploadPieceImage(
|
||||
pieceId: string,
|
||||
expectedRevision: number,
|
||||
|
||||
Reference in new issue
Block a user