diff --git a/packages/obsidian/src/watchlist/parse.ts b/packages/obsidian/src/watchlist/parse.ts new file mode 100644 index 0000000..29a2eb8 --- /dev/null +++ b/packages/obsidian/src/watchlist/parse.ts @@ -0,0 +1,36 @@ +export interface ParsedNote { + frontmatter: Record; + body: string; +} + +export function parseNote(content: string): ParsedNote { + const m = /^---\n([\s\S]*?)\n---([\s\S]*)$/.exec(content); + const frontmatter: Record = {}; + let body = content; + if (m) { + body = m[2]; + for (const line of m[1].split('\n')) { + const mm = /^([A-Za-z0-9_]+):\s*(.*)$/.exec(line); + if (mm) frontmatter[mm[1]] = mm[2].trim(); + } + } + return { frontmatter, body }; +} + +export function extractMyNotes(body: string): string { + const m = /##\s*My Notes\s*([\s\S]*)$/.exec(body ?? ''); + return m ? m[1].trim() : ''; +} + +function stripQuotes(s: string | undefined): string { + return (s ?? '').trim().replace(/^"|"$/g, ''); +} + +export function noteTmdbRef(fm: Record): { tmdbId: string; isMovie: boolean } | null { + const tid = stripQuotes(fm['tmdb_id']); + if (tid) return { tmdbId: tid, isMovie: stripQuotes(fm['media_type']) === 'Movie' }; + const rawId = stripQuotes(fm['id']); + const ds = (fm['dataSource'] ?? '').trim(); + if (rawId && ds.startsWith('TMDB')) return { tmdbId: rawId, isMovie: ds.includes('Movie') }; + return null; +} diff --git a/tests/watchlist-parse.test.ts b/tests/watchlist-parse.test.ts new file mode 100644 index 0000000..27c0f70 --- /dev/null +++ b/tests/watchlist-parse.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, test } from 'bun:test'; +import { parseNote, extractMyNotes, noteTmdbRef } from 'packages/obsidian/src/watchlist/parse'; + +const NOTE = `--- +type: watchlist_item +media_type: TV Series +tmdb_id: 84958 +last_air_date: 2023-11-09 +watch_status: Watched +--- + +# Loki + +## My Notes + +great finale +`; + +describe('parseNote', () => { + test('splits frontmatter and body', () => { + const { frontmatter, body } = parseNote(NOTE); + expect(frontmatter['tmdb_id']).toBe('84958'); + expect(frontmatter['media_type']).toBe('TV Series'); + expect(body).toContain('# Loki'); + }); + test('no frontmatter → empty fm, full body', () => { + const { frontmatter, body } = parseNote('# Just a heading'); + expect(Object.keys(frontmatter).length).toBe(0); + expect(body).toBe('# Just a heading'); + }); +}); + +describe('extractMyNotes', () => { + test('extracts trailing section', () => { + expect(extractMyNotes(parseNote(NOTE).body)).toBe('great finale'); + }); + test('missing section → empty', () => { + expect(extractMyNotes('# T\n\ncontent')).toBe(''); + }); +}); + +describe('noteTmdbRef', () => { + test('canonical note', () => { + expect(noteTmdbRef({ tmdb_id: '84958', media_type: 'TV Series' })).toEqual({ tmdbId: '84958', isMovie: false }); + expect(noteTmdbRef({ tmdb_id: '693134', media_type: 'Movie' })).toEqual({ tmdbId: '693134', isMovie: true }); + }); + test('quoted values stripped', () => { + expect(noteTmdbRef({ tmdb_id: '"84958"', media_type: '"TV Series"' })).toEqual({ tmdbId: '84958', isMovie: false }); + }); + test('raw Media DB note fallback (id + dataSource)', () => { + expect(noteTmdbRef({ id: '693134', dataSource: 'TMDBMovieAPI' })).toEqual({ tmdbId: '693134', isMovie: true }); + expect(noteTmdbRef({ id: '84958', dataSource: 'TMDBSeriesAPI' })).toEqual({ tmdbId: '84958', isMovie: false }); + }); + test('no id → null', () => { + expect(noteTmdbRef({ type: 'list' })).toBeNull(); + }); +});