mirror of
https://github.com/echore/vault-autopilot.git
synced 2026-07-22 08:34:00 +00:00
feat(clip): carry published_at into note frontmatter with upsert backfill
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
9d0a8b9a28
commit
abd47cb079
4 changed files with 53 additions and 4 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import { ClipPayload, HookPayload, KeyframePayload, ScreenshotPayload, ThumbnailPayload } from './server';
|
||||
import { ClipRule, PluginSettings, ScreenshotClipRule, ThumbnailClipRule } from './types';
|
||||
import { sanitize, buildVideoEmbed, extractVideoId, detectPlatform, videoKey } from './util';
|
||||
import { buildAnchor, mergeSection, coverSection, hookSection, keyframeSection, screenshotSection, VideoNoteMeta, NewSection, headingLabel, sopBlock } from './video-note';
|
||||
import { buildAnchor, ensurePublished, mergeSection, coverSection, hookSection, keyframeSection, screenshotSection, VideoNoteMeta, NewSection, headingLabel, sopBlock } from './video-note';
|
||||
import { t } from './i18n';
|
||||
|
||||
export interface VaultOps {
|
||||
|
|
@ -54,8 +54,12 @@ async function upsertVideoNote(
|
|||
const existing = meta.videoId ? await findNoteByVideoId(meta.videoId, folder, vaultOps) : null;
|
||||
if (existing) {
|
||||
const { content, skipped } = mergeSection(existing.content, section);
|
||||
if (skipped) return { notePath: existing.path, notice: t('notice.sectionExists', { section: headingLabel(section.kind) }) };
|
||||
await vaultOps.modify(existing.path, content);
|
||||
if (skipped) {
|
||||
const patched = ensurePublished(existing.content, meta.published);
|
||||
if (patched !== existing.content) await vaultOps.modify(existing.path, patched);
|
||||
return { notePath: existing.path, notice: t('notice.sectionExists', { section: headingLabel(section.kind) }) };
|
||||
}
|
||||
await vaultOps.modify(existing.path, ensurePublished(content, meta.published));
|
||||
return { notePath: existing.path };
|
||||
}
|
||||
const { content } = mergeSection(buildAnchor(meta), section);
|
||||
|
|
@ -198,6 +202,7 @@ async function handleThumbnail(
|
|||
videoUrl: payload.video_url,
|
||||
title: payload.title,
|
||||
channel: payload.channel,
|
||||
published: payload.published_at,
|
||||
};
|
||||
return upsertVideoNote(meta, section, vaultOps, rule.outputFolder);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ export type ThumbnailPayload = {
|
|||
channel_handle?: string | null;
|
||||
views?: string | null;
|
||||
captured_at: string;
|
||||
published_at?: string;
|
||||
};
|
||||
|
||||
export type ClipPayload =
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export interface VideoNoteMeta {
|
|||
videoUrl: string;
|
||||
title: string;
|
||||
channel?: string | null;
|
||||
published?: string | null;
|
||||
}
|
||||
|
||||
export interface NewSection {
|
||||
|
|
@ -112,11 +113,24 @@ export function buildAnchor(meta: VideoNoteMeta): string {
|
|||
`video_id: "${meta.videoId}"`, `video_url: "${meta.videoUrl}"`,
|
||||
`title: "${meta.title}"`,
|
||||
...(meta.channel ? [`channel: "${meta.channel}"`] : []),
|
||||
...(meta.published ? [`published: ${meta.published}`] : []),
|
||||
`dimensions: []`, `analyzed_at: ${today}`, `tags: []`, `depth: normal`, `---`,
|
||||
].join('\n');
|
||||
return `${fm}\n\n# ${meta.title}\n`;
|
||||
}
|
||||
|
||||
// Backfill path: notes created by hook/keyframe (or an older version) have no
|
||||
// `published` — patch it into the frontmatter the next time a thumbnail clip
|
||||
// for the same video arrives with one.
|
||||
export function ensurePublished(content: string, published?: string | null): string {
|
||||
if (!published || !content.startsWith('---\n')) return content;
|
||||
const end = content.indexOf('\n---', 4);
|
||||
if (end === -1) return content;
|
||||
const fm = content.slice(0, end);
|
||||
if (/^published:/m.test(fm)) return content;
|
||||
return `${fm}\npublished: ${published}${content.slice(end)}`;
|
||||
}
|
||||
|
||||
interface ParsedSection { kind: SectionKind; startSeconds: number; text: string; }
|
||||
|
||||
function kindOf(heading: string): SectionKind | null {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { buildAnchor, mergeSection, coverSection, hookSection, keyframeSection, screenshotSection, VideoNoteMeta } from '../src/video-note';
|
||||
import { buildAnchor, ensurePublished, mergeSection, coverSection, hookSection, keyframeSection, screenshotSection, VideoNoteMeta } from '../src/video-note';
|
||||
import { setLanguage } from '../src/i18n';
|
||||
|
||||
beforeEach(() => setLanguage('zh'));
|
||||
|
|
@ -183,3 +183,32 @@ describe('bilingual headings', () => {
|
|||
expect(note).not.toContain('dimensions: [动效, Motion]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('published frontmatter', () => {
|
||||
const base = { platform: 'youtube', videoId: 'x', videoUrl: 'https://y/x', title: 'T' };
|
||||
|
||||
test('buildAnchor writes published when meta carries it', () => {
|
||||
expect(buildAnchor({ ...base, published: '2026-07-10' })).toContain('published: 2026-07-10');
|
||||
});
|
||||
|
||||
test('buildAnchor omits published when absent', () => {
|
||||
expect(buildAnchor(base)).not.toContain('published:');
|
||||
});
|
||||
|
||||
test('ensurePublished inserts into existing frontmatter', () => {
|
||||
const content = '---\ntype: video\ntitle: "T"\n---\n\n# T\n';
|
||||
const out = ensurePublished(content, '2026-07-10');
|
||||
expect(out).toContain('published: 2026-07-10');
|
||||
expect(out.indexOf('published:')).toBeLessThan(out.indexOf('\n---\n'));
|
||||
});
|
||||
|
||||
test('ensurePublished is a no-op when already present', () => {
|
||||
const content = '---\ntype: video\npublished: 2026-01-01\n---\n\n# T\n';
|
||||
expect(ensurePublished(content, '2026-07-10')).toBe(content);
|
||||
});
|
||||
|
||||
test('ensurePublished is a no-op without a date or frontmatter', () => {
|
||||
expect(ensurePublished('---\ntype: video\n---\n', undefined)).toBe('---\ntype: video\n---\n');
|
||||
expect(ensurePublished('no frontmatter', '2026-07-10')).toBe('no frontmatter');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue