diff --git a/packages/app/src/components/content/BookGrid.vue b/packages/app/src/components/content/BookGrid.vue index f213e305..df62105e 100644 --- a/packages/app/src/components/content/BookGrid.vue +++ b/packages/app/src/components/content/BookGrid.vue @@ -88,10 +88,6 @@ -
- {{ book.title }} -
diff --git a/packages/app/src/composables/useContentPanel.ts b/packages/app/src/composables/useContentPanel.ts index 724206c5..339648d2 100644 --- a/packages/app/src/composables/useContentPanel.ts +++ b/packages/app/src/composables/useContentPanel.ts @@ -711,23 +711,26 @@ export function useContentPanel() { const patterns: { re: RegExp; titleIdx: number; authorIdx: number }[] = [ // "Title" by Author { re: /["""]([^"""]{2,80})["""]\s+by\s+([A-Z][^,\n\.]{1,50}?)(?:\s*[,()\n\.]|$)/gi, titleIdx: 1, authorIdx: 2 }, - // **Title** by Author - { re: /\*\*([^*]{2,80})\*\*\s+by\s+([A-Z][^,\n\.]{1,50}?)(?:\s*[,()\n\.]|$)/g, titleIdx: 1, authorIdx: 2 }, + // **Title** by/—/– Author (with optional italic on author) + { re: /\*\*([^*]{2,80})\*\*\s+(?:by|—|–)\s+\*?([A-Z][^*\n]{1,50}?)\*?(?:\s*[,()*\n]|$)/g, titleIdx: 1, authorIdx: 2 }, // - Title — Author or Title by Author (in list) - { re: /(?:^|\n)\s*(?:\d+\.\s*|[-•]\s*)\*{0,2}([^*\n\-–—]{2,80}?)\*{0,2}\s+(?:by|—|–)\s+([A-Z][^,\n]{1,50}?)(?:\s*[\n(,.]|$)/gm, titleIdx: 1, authorIdx: 2 }, + { re: /(?:^|\n)\s*(?:\d+\.\s*|[-•]\s*)\*{0,2}([^*\n\-–—]{2,80}?)\*{0,2}\s+(?:by|—|–)\s+\*?([A-Z][^*\n]{1,50}?)\*?(?:\s*[,()*\n]|$)/gm, titleIdx: 1, authorIdx: 2 }, ] for (const { re, titleIdx, authorIdx } of patterns) { let m: RegExpExecArray | null const rx = new RegExp(re.source, re.flags) while ((m = rx.exec(text)) !== null) { - const title = m[titleIdx].trim().replace(/^\*\*|\*\*$/g, '') + const title = m[titleIdx].trim().replace(/^\*\*|\*\*$/g, '').replace(/^\[|\]$/g, '') const author = m[authorIdx].trim().replace(/^\*\*|\*\*$/g, '') if (title.length < 2 || author.length < 2) continue // Skip if it looks like a film/song/podcast tag if (/\[\[(film|song|podcast|book)(_ext)?:/.test(title)) continue // Skip numbers-only if (/^\d{4}$/.test(title) || /^\d{4}$/.test(author)) continue + // Skip markdown links [text](url) — these are source references, not books + const afterMatch = text.substring(m.index + m[0].length, m.index + m[0].length + 200) + if (/^\s*\]\s*\(https?:/.test(afterMatch) || /\]\(https?:\/\//.test(m[0])) continue const key = `${title.toLowerCase()}|${author.toLowerCase()}` if (seen.has(key)) continue seen.add(key) @@ -763,11 +766,16 @@ export function useContentPanel() { if (externalBooks.length > 0) return externalBooks // Only do fallback pattern matching if the query or response looks book-related if (!isBookQuery(userQuery) && !isBookLikeResponse(text)) return [] - // Skip if other content types are already tagged - if (extractFilmIds(text).length > 0 || /\[\[film_ext:/.test(text)) return [] - if (extractSongIds(text).length > 0 || /\[\[song_ext:/.test(text)) return [] + // Skip if other content types are the primary content (not just supplementary refs) + // When user explicitly asked about books, film/song tags are just cross-references + if (!isBookQuery(userQuery)) { + if (extractFilmIds(text).length > 0 || /\[\[film_ext:/.test(text)) return [] + if (extractSongIds(text).length > 0 || /\[\[song_ext:/.test(text)) return [] + } if (isNewsLikeResponse(text)) return [] - return extractBooksFromPatterns(text) + // Strip sources/references section at the end to avoid matching markdown links as books + const cleanText = text.replace(/\n---\n\s*(?:Sources|References|Links):?\s*\n[\s\S]*$/i, '') + return extractBooksFromPatterns(cleanText) } function extractExternalTVSeries(text: string): TVSeries[] {