Release 0.6.1

This commit is contained in:
hnzio committed 2026-10-25 18:18:23 +01:00
1 parent b80dded55d
commit d586fbd8ce
29 files changed
+398 -66

No files matched your search

+61 -30
View File
@@ -27,6 +27,7 @@ export type ShowTemplateSection = {
categoryId: string | null;
position: number;
explicitDurationSeconds: number | null;
twitchTags: string[];
};
export type ShowTemplate = {
@@ -106,6 +107,7 @@ export type ShowSection = {
categoryColor: string | null;
position: number;
explicitDurationSeconds: number | null;
twitchTags: string[];
durationSeconds: number;
usages: PieceUsage[];
};
@@ -144,6 +146,7 @@ export type ShowSectionInput = {
name: string;
categoryId: string | null;
explicitDurationSeconds: number | null;
twitchTags?: string[];
position?: number;
};
@@ -207,6 +210,7 @@ type SectionRow = {
category_color_snapshot: string | null;
position: number;
explicit_duration_seconds: number | null;
twitch_tags: string;
is_reserve_bank: number;
};
@@ -226,6 +230,7 @@ type UsageRow = {
transition_snapshot: string;
presentation_snapshot: string;
presentation_mode_snapshot: PresentationMode;
presentation_image_only_snapshot: number;
presentation_source_label_snapshot: string;
presentation_source_url_snapshot: string;
presentation_source_timecode_snapshot: number | null;
@@ -254,6 +259,14 @@ function validDuration(value: number | null): number | null {
return value;
}
function validTwitchTags(values: string[]) {
const tags = [...new Set(values.map((tag) => tag.trim()).filter(Boolean))];
if (tags.length > 10 || tags.some((tag) => tag.length > 25 || tag.includes(','))) {
throw new Error('INVALID_TWITCH_TAGS');
}
return tags;
}
function validPosition(value: number, maximum: number): number {
if (!Number.isInteger(value) || value < 0) throw new Error('INVALID_POSITION');
return Math.min(value, maximum);
@@ -277,7 +290,7 @@ function validPresentation(
imageUrl: string,
siteName: string
) {
if (!['text', 'link', 'video'].includes(mode)) throw new Error('INVALID_PRESENTATION');
if (!['text', 'image', 'link', 'video'].includes(mode)) throw new Error('INVALID_PRESENTATION');
const cleanUrl = cleanText(sourceUrl, 2_000, 'presentation_source');
const cleanImageUrl = cleanText(imageUrl, 2_000, 'presentation_image');
for (const value of [cleanUrl, cleanImageUrl]) {
@@ -289,7 +302,7 @@ function validPresentation(
throw new Error('INVALID_PRESENTATION');
}
}
if (mode !== 'text' && !cleanUrl) throw new Error('INVALID_PRESENTATION');
if ((mode === 'link' || mode === 'video') && !cleanUrl) throw new Error('INVALID_PRESENTATION');
if (
timecodeSeconds !== null &&
(!Number.isInteger(timecodeSeconds) || timecodeSeconds < 0 || timecodeSeconds > 86_400)
@@ -762,7 +775,7 @@ export function listShowTemplates(): ShowTemplate[] {
.prepare('SELECT id, name, description FROM show_templates ORDER BY name')
.all() as Omit<ShowTemplate, 'sections'>[];
const sectionStatement = database.prepare(
`SELECT id, name, category_id, position, explicit_duration_seconds
`SELECT id, name, category_id, position, explicit_duration_seconds, twitch_tags
FROM show_template_sections WHERE template_id = ? ORDER BY position`
);
return templates.map((template) => ({
@@ -774,13 +787,15 @@ export function listShowTemplates(): ShowTemplate[] {
category_id: string | null;
position: number;
explicit_duration_seconds: number | null;
twitch_tags: string;
}[]
).map((section) => ({
id: section.id,
name: section.name,
categoryId: section.category_id,
position: section.position,
explicitDurationSeconds: section.explicit_duration_seconds
explicitDurationSeconds: section.explicit_duration_seconds,
twitchTags: section.twitch_tags ? section.twitch_tags.split(',') : []
}))
}));
}
@@ -798,7 +813,7 @@ export function getShow(id: string): Show | null {
.prepare(
`SELECT s.id, s.name, s.category_id, c.name AS category, c.color AS category_color,
s.category_name_snapshot, s.category_color_snapshot,
s.position, s.explicit_duration_seconds, s.is_reserve_bank
s.position, s.explicit_duration_seconds, s.twitch_tags, s.is_reserve_bank
FROM show_sections s LEFT JOIN section_categories c ON c.id = s.category_id
WHERE s.show_id = ? ORDER BY s.position`
)
@@ -808,7 +823,9 @@ export function getShow(id: string): Show | null {
`SELECT u.id, u.show_section_id, u.original_piece_id, u.position, u.status,
u.duration_seconds, u.title_snapshot, u.output_title_snapshot, u.intro_snapshot, u.research_snapshot,
u.moderation_snapshot, u.interaction_snapshot, u.transition_snapshot, u.presentation_snapshot,
u.presentation_mode_snapshot, u.presentation_source_label_snapshot,
CASE WHEN u.presentation_image_only_snapshot = 1 THEN 'image' ELSE u.presentation_mode_snapshot END AS presentation_mode_snapshot,
u.presentation_image_only_snapshot,
u.presentation_source_label_snapshot,
u.presentation_source_url_snapshot, u.presentation_source_timecode_snapshot,
u.presentation_title_meta_snapshot, u.presentation_description_snapshot,
u.presentation_image_url_snapshot, u.presentation_site_name_snapshot,
@@ -841,6 +858,7 @@ export function getShow(id: string): Show | null {
: (row.category_color_snapshot ?? row.category_color),
position: row.position,
explicitDurationSeconds: row.explicit_duration_seconds,
twitchTags: row.twitch_tags ? row.twitch_tags.split(',') : [],
durationSeconds:
row.explicit_duration_seconds ??
usages.reduce((total, usage) => total + usage.durationSeconds, 0),
@@ -957,7 +975,7 @@ export function createShow(input: CreateShowInput): CreatedMutationResult {
const templateSections = input.templateId
? (database
.prepare(
`SELECT name, category_id, position, explicit_duration_seconds
`SELECT name, category_id, position, explicit_duration_seconds, twitch_tags
FROM show_template_sections WHERE template_id = ? ORDER BY position`
)
.all(input.templateId) as {
@@ -965,11 +983,12 @@ export function createShow(input: CreateShowInput): CreatedMutationResult {
category_id: string | null;
position: number;
explicit_duration_seconds: number | null;
twitch_tags: string;
}[])
: [];
const insertSection = database.prepare(
`INSERT INTO show_sections (id, show_id, name, category_id, position,
explicit_duration_seconds, is_reserve_bank) VALUES (?, ?, ?, ?, ?, ?, ?)`
explicit_duration_seconds, twitch_tags, is_reserve_bank) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
);
for (const section of templateSections) {
insertSection.run(
@@ -979,10 +998,11 @@ export function createShow(input: CreateShowInput): CreatedMutationResult {
section.category_id,
section.position,
section.explicit_duration_seconds,
section.twitch_tags,
0
);
}
insertSection.run(randomUUID(), id, 'Reservebank', null, templateSections.length, null, 1);
insertSection.run(randomUUID(), id, 'Reservebank', null, templateSections.length, null, '', 1);
});
create();
return { id, revision: 1, updatedAtUtc: now };
@@ -1033,7 +1053,7 @@ export function duplicateCompletedShow(
const sections = database
.prepare(
`SELECT id, name, category_id, category_name_snapshot, category_color_snapshot,
position, explicit_duration_seconds, is_reserve_bank
position, explicit_duration_seconds, twitch_tags, is_reserve_bank
FROM show_sections WHERE show_id = ? ORDER BY position`
)
.all(sourceShowId) as {
@@ -1044,12 +1064,13 @@ export function duplicateCompletedShow(
category_color_snapshot: string | null;
position: number;
explicit_duration_seconds: number | null;
twitch_tags: string;
is_reserve_bank: number;
}[];
const insertSection = database.prepare(
`INSERT INTO show_sections (id, show_id, name, category_id, category_name_snapshot,
category_color_snapshot, position, explicit_duration_seconds, is_reserve_bank)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
category_color_snapshot, position, explicit_duration_seconds, twitch_tags, is_reserve_bank)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
);
const usages = database.prepare(
'SELECT * FROM piece_usages WHERE show_section_id = ? ORDER BY position'
@@ -1062,8 +1083,8 @@ export function duplicateCompletedShow(
presentation_source_url_snapshot, presentation_source_timecode_snapshot,
presentation_title_meta_snapshot, presentation_description_snapshot,
presentation_image_url_snapshot, presentation_site_name_snapshot,
presentation_provider_snapshot, presentation_provider_id_snapshot, own_notes, is_reserve)
VALUES (?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
presentation_provider_snapshot, presentation_provider_id_snapshot, presentation_image_only_snapshot, own_notes, is_reserve)
VALUES (?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
);
for (const section of sections) {
const sectionId = randomUUID();
@@ -1076,6 +1097,7 @@ export function duplicateCompletedShow(
section.category_color_snapshot,
section.position,
section.explicit_duration_seconds,
section.twitch_tags,
section.is_reserve_bank
);
for (const usage of usages.all(section.id) as (UsageRow & { is_reserve: number })[]) {
@@ -1104,6 +1126,7 @@ export function duplicateCompletedShow(
usage.presentation_site_name_snapshot,
usage.presentation_provider_snapshot,
usage.presentation_provider_id_snapshot,
usage.presentation_image_only_snapshot,
usage.own_notes,
usage.is_reserve
);
@@ -1150,6 +1173,7 @@ export function addShowSection(
): CreatedMutationResult {
const name = cleanText(input.name, 200, 'section_name', true);
const duration = validDuration(input.explicitDurationSeconds);
const twitchTags = validTwitchTags(input.twitchTags ?? []);
const database = getDatabase();
requireCategory(database, input.categoryId);
return database.transaction(() => {
@@ -1174,9 +1198,9 @@ export function addShowSection(
database
.prepare(
`INSERT INTO show_sections (id, show_id, name, category_id, position,
explicit_duration_seconds, is_reserve_bank) VALUES (?, ?, ?, ?, ?, ?, 0)`
explicit_duration_seconds, twitch_tags, is_reserve_bank) VALUES (?, ?, ?, ?, ?, ?, ?, 0)`
)
.run(id, showId, name, input.categoryId, temporaryPosition, duration);
.run(id, showId, name, input.categoryId, temporaryPosition, duration, twitchTags.join(','));
visibleIds.splice(position, 0, id);
const reserveId = reserveSectionId(database, showId);
normalizePositions(database, 'show_sections', [...visibleIds, reserveId]);
@@ -1192,6 +1216,7 @@ export function updateShowSection(
): MutationResult {
const name = cleanText(input.name, 200, 'section_name', true);
const duration = validDuration(input.explicitDurationSeconds);
const twitchTags = validTwitchTags(input.twitchTags ?? []);
const database = getDatabase();
requireCategory(database, input.categoryId);
return database.transaction(() => {
@@ -1199,11 +1224,11 @@ export function updateShowSection(
sectionBelongsToShow(database, showId, sectionId);
database
.prepare(
`UPDATE show_sections SET name = ?, category_id = ?, explicit_duration_seconds = ?,
`UPDATE show_sections SET name = ?, category_id = ?, explicit_duration_seconds = ?, twitch_tags = ?,
category_name_snapshot = NULL, category_color_snapshot = NULL
WHERE id = ?`
)
.run(name, input.categoryId, duration, sectionId);
.run(name, input.categoryId, duration, twitchTags.join(','), sectionId);
return result;
})();
}
@@ -1275,7 +1300,8 @@ export function addPieceUsage(
const piece = database
.prepare(
`SELECT p.title, p.output_title, p.duration_seconds, p.intro, p.research_text, p.moderation_notes,
p.interaction_text, p.transition_text, p.presentation_text, p.status, p.presentation_mode,
p.interaction_text, p.transition_text, p.presentation_text, p.status,
CASE WHEN p.presentation_image_only = 1 THEN 'image' ELSE p.presentation_mode END AS presentation_mode,
COALESCE(ps.label, '') AS presentation_source_label,
COALESCE(ps.url, '') AS presentation_source_url,
ps.timecode_seconds AS presentation_source_timecode,
@@ -1329,12 +1355,12 @@ export function addPieceUsage(
`INSERT INTO piece_usages (id, show_section_id, original_piece_id, position, status,
duration_seconds, title_snapshot, output_title_snapshot, intro_snapshot, research_snapshot,
moderation_snapshot, interaction_snapshot, transition_snapshot, presentation_snapshot,
presentation_mode_snapshot, presentation_source_label_snapshot,
presentation_mode_snapshot, presentation_image_only_snapshot, presentation_source_label_snapshot,
presentation_source_url_snapshot, presentation_source_timecode_snapshot,
presentation_title_meta_snapshot, presentation_description_snapshot,
presentation_image_url_snapshot, presentation_site_name_snapshot,
presentation_provider_snapshot, presentation_provider_id_snapshot, own_notes, is_reserve)
VALUES (?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', ?)`
VALUES (?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', ?)`
)
.run(
id,
@@ -1350,7 +1376,8 @@ export function addPieceUsage(
piece.interaction_text,
piece.transition_text,
piece.presentation_text,
piece.presentation_mode,
piece.presentation_mode === 'image' ? 'text' : piece.presentation_mode,
piece.presentation_mode === 'image' ? 1 : 0,
piece.presentation_source_label,
piece.presentation_source_url,
piece.presentation_source_timecode,
@@ -1436,7 +1463,7 @@ export function duplicatePieceUsage(
.prepare(
`SELECT original_piece_id, status, duration_seconds, title_snapshot, output_title_snapshot, intro_snapshot,
research_snapshot, moderation_snapshot, interaction_snapshot, transition_snapshot, presentation_snapshot,
presentation_mode_snapshot, presentation_source_label_snapshot,
presentation_mode_snapshot, presentation_image_only_snapshot, presentation_source_label_snapshot,
presentation_source_url_snapshot, presentation_source_timecode_snapshot,
presentation_title_meta_snapshot, presentation_description_snapshot,
presentation_image_url_snapshot, presentation_site_name_snapshot,
@@ -1459,12 +1486,12 @@ export function duplicatePieceUsage(
`INSERT INTO piece_usages (id, show_section_id, original_piece_id, position, status,
duration_seconds, title_snapshot, output_title_snapshot, intro_snapshot, research_snapshot,
moderation_snapshot, interaction_snapshot, transition_snapshot, presentation_snapshot,
presentation_mode_snapshot, presentation_source_label_snapshot,
presentation_mode_snapshot, presentation_image_only_snapshot, presentation_source_label_snapshot,
presentation_source_url_snapshot, presentation_source_timecode_snapshot,
presentation_title_meta_snapshot, presentation_description_snapshot,
presentation_image_url_snapshot, presentation_site_name_snapshot,
presentation_provider_snapshot, presentation_provider_id_snapshot, own_notes, is_reserve)
VALUES (?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
VALUES (?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
)
.run(
id,
@@ -1481,6 +1508,7 @@ export function duplicatePieceUsage(
source.transition_snapshot,
source.presentation_snapshot,
source.presentation_mode_snapshot,
source.presentation_image_only_snapshot,
source.presentation_source_label_snapshot,
source.presentation_source_url_snapshot,
source.presentation_source_timecode_snapshot,
@@ -1512,7 +1540,8 @@ export function refreshPieceUsageFromOriginal(
const source = database
.prepare(
`SELECT p.title, p.output_title, p.duration_seconds, p.intro, p.research_text,
p.moderation_notes, p.interaction_text, p.transition_text, p.presentation_text, p.presentation_mode,
p.moderation_notes, p.interaction_text, p.transition_text, p.presentation_text,
CASE WHEN p.presentation_image_only = 1 THEN 'image' ELSE p.presentation_mode END AS presentation_mode,
COALESCE(ps.label, '') AS presentation_source_label,
COALESCE(ps.url, '') AS presentation_source_url,
ps.timecode_seconds AS presentation_source_timecode,
@@ -1551,7 +1580,7 @@ export function refreshPieceUsageFromOriginal(
.prepare(
`UPDATE piece_usages SET title_snapshot = ?, output_title_snapshot = ?, duration_seconds = ?, intro_snapshot = ?,
research_snapshot = ?, moderation_snapshot = ?, interaction_snapshot = ?, transition_snapshot = ?,
presentation_snapshot = ?, presentation_mode_snapshot = ?,
presentation_snapshot = ?, presentation_mode_snapshot = ?, presentation_image_only_snapshot = ?,
presentation_source_label_snapshot = ?, presentation_source_url_snapshot = ?,
presentation_source_timecode_snapshot = ?, presentation_title_meta_snapshot = ?,
presentation_description_snapshot = ?, presentation_image_url_snapshot = ?,
@@ -1568,7 +1597,8 @@ export function refreshPieceUsageFromOriginal(
source.interaction_text,
source.transition_text,
source.presentation_text,
source.presentation_mode,
source.presentation_mode === 'image' ? 'text' : source.presentation_mode,
source.presentation_mode === 'image' ? 1 : 0,
source.presentation_source_label,
source.presentation_source_url,
source.presentation_source_timecode,
@@ -1630,7 +1660,7 @@ export function updatePieceUsage(
.prepare(
`UPDATE piece_usages SET title_snapshot = ?, output_title_snapshot = ?, intro_snapshot = ?, research_snapshot = ?,
moderation_snapshot = ?, interaction_snapshot = COALESCE(?, interaction_snapshot), transition_snapshot = ?, presentation_snapshot = ?,
presentation_mode_snapshot = ?, presentation_source_label_snapshot = ?,
presentation_mode_snapshot = ?, presentation_image_only_snapshot = ?, presentation_source_label_snapshot = ?,
presentation_source_url_snapshot = ?, presentation_source_timecode_snapshot = ?,
presentation_title_meta_snapshot = ?, presentation_description_snapshot = ?,
presentation_image_url_snapshot = ?, presentation_site_name_snapshot = ?,
@@ -1646,7 +1676,8 @@ export function updatePieceUsage(
values.interaction,
values.transition,
values.presentation,
presentation.mode,
presentation.mode === 'image' ? 'text' : presentation.mode,
presentation.mode === 'image' ? 1 : 0,
presentation.sourceLabel,
presentation.sourceUrl,
presentation.timecodeSeconds,