Release 0.6.1
This commit is contained in:
1 parent
b80dded55d
commit
d586fbd8ce
29 files changed
+398
-66
No files matched your search
+61
-30
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user