From abe5f8ec3c37fd73a3bda8ad7700e8b40207520a Mon Sep 17 00:00:00 2001 From: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> Date: Sat, 27 Jan 2024 11:42:09 +0100 Subject: [PATCH] feat: include book details in export (#291) * feat: include book details in export Closes: https://github.com/OGKevin/obsidian-kobo-highlights-import/issues/109 * fix: broken test --- README.md | 44 ++++++++++------- src/database/Highlight.ts | 16 ++++++- src/database/interfaces.ts | 22 +++++++++ src/database/repository.test.ts | 38 +++++++++++++++ src/database/repository.ts | 38 ++++++++++++++- src/modal/ExtractHighlightsModal.ts | 4 +- src/template/template.test.ts | 71 ++++++++++++++++++++++------ src/template/template.ts | 73 +++++++++++++++++++++++++++-- 8 files changed, 267 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index fcfb15d..cbcb597 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ This plugin aims to make highlight import from Kobo devices easier. - [Obsidian Kobo Highlight Importer](#obsidian-kobo-highlight-importer) - [How to use](#how-to-use) - [Templating](#templating) - - [Examples](#examples) - [Variables](#variables) - [Highlight markers](#highlight-markers) - [Helping Screenshots](#helping-screenshots) @@ -29,31 +28,44 @@ Once installed, the steps to import your highlights directly into the vault are: The default template is: ```markdown -# {{title}} +--- +title: {{Title}} +author: {{Author}} +publisher: {{Publisher}} +dateLastRead: {{DateLastRead}} +readStatus: {{ReadStatus}} +percentRead: {{PercentRead}} +isbn: {{ISBN}} +series: {{Series}} +seriesNumber: {{SeriesNumber}} +timeSpentReading: {{TimeSpentReading}} +--- -{{highlights}} -``` +# {{Title}} -### Examples +## Description -```markdown ---- -tags: -- books -bookTitle: {{title}} ---- +{{Description}} -# {{title}} +## Highlights {{highlights}} ``` ### Variables -| Tag | Description | Example | -|------------|--------------------------------------------------|------------------| -| highlights | Will get replaced with the extracted highlights. | `{{highlights}}` | -| title | The title of the book | `{{title}}` | +| Tag | Description | Example | +| ---------------- | ------------------------------------------------ | ---------------------- | +| highlights | Will get replaced with the extracted highlights. | `{{highlights}}` | +| title | The title of the book. | `{{title}}` | +| author | The author of the book. | `{{author}}` | +| pulbisher | The publisher of the book | `{{publihser}}` | +| dateLastRead | The date the book was last read in ISO format. | `{{dateLastRead}}` | +| readStatus | Can be: Unopened, Reading, Read. | `{{readStatus}}` | +| isbn | The ISBN of the book. | `{{isbn}}` | +| series | The series of which the book is a part of. | `{{series}}` | +| seriesNumber | The position of the book in the series. | `{{seriesNumber}}` | +| timeSpentReading | The time spent reading the book. | `{{timeSpentReading}}` | ## Highlight markers The plugin uses comments as highlight markers, to enable support for keeping existing highlights. All content between these markers will be transferred to the updated file. diff --git a/src/database/Highlight.ts b/src/database/Highlight.ts index 9730c8a..a9c8d3a 100644 --- a/src/database/Highlight.ts +++ b/src/database/Highlight.ts @@ -1,5 +1,5 @@ import moment from 'moment'; -import { Bookmark, Content, Highlight } from "./interfaces"; +import { BookDetails, Bookmark, Content, Highlight } from "./interfaces"; import { Repository } from "./repository"; type bookTitle = string @@ -18,11 +18,25 @@ export class HighlightService { repo: Repository unkonwnBookTitle = 'Unknown Title' + unknownAuthor = 'Unknown Author' constructor(repo: Repository) { this.repo = repo } + async getBookDetailsFromBookTitle(title: string): Promise { + const details = await this.repo.getBookDetailsByBookTitle(title) + + if (details == null) { + return { + title: this.unkonwnBookTitle, + author: this.unknownAuthor + } + } + + return details; + } + extractExistingHighlight(bookmark: bookmark, existingContent: string): string { // Define search terms const startSearch = `%%START-${bookmark.bookmarkId}%%` diff --git a/src/database/interfaces.ts b/src/database/interfaces.ts index 39c0ec1..10841ea 100644 --- a/src/database/interfaces.ts +++ b/src/database/interfaces.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-unused-vars */ export interface Bookmark { bookmarkId: string, text: string; @@ -17,3 +18,24 @@ export interface Highlight { bookmark: Bookmark; content: Content; } + +export interface BookDetails { + title: string; + author: string; + description?: string; + publisher?: string; + dateLastRead?: Date; + readStatus?: ReadStatus; + percentRead?: number; + isbn?: string; + series?: string; + seriesNumber?: number; + timeSpentReading?: number; +} + +export enum ReadStatus { + Unknown = -1, + Unopened = 0, + Reading = 1, + Read = 2 +} \ No newline at end of file diff --git a/src/database/repository.test.ts b/src/database/repository.test.ts index a0486c4..8b7768d 100644 --- a/src/database/repository.test.ts +++ b/src/database/repository.test.ts @@ -50,4 +50,42 @@ describe('Repository', async function () { }); chai.expect(await repo.getAllContentByBookTitle(titles.at(Math.floor(Math.random() * titles.length)) ?? "")).length.above(0) }); + it('getBookDetailsOnePunchMan', async function () { + const details = await repo.getBookDetailsByBookTitle("One-Punch Man, Vol. 2") + + chai.expect(details).not.null + chai.expect(details?.title).is.eq("One-Punch Man, Vol. 2") + chai.expect(details?.author).is.eq("ONE") + chai.expect(details?.description).not.null + chai.expect(details?.publisher).is.eq("VIZ Media LLC") + chai.expect(details?.dateLastRead).not.null + chai.expect(details?.readStatus).is.eq(2) + chai.expect(details?.percentRead).is.eq(100) + chai.expect(details?.isbn).is.eq("9781421585659") + chai.expect(details?.seriesNumber).is.eq(2) + chai.expect(details?.series).is.eq("One-Punch man") + chai.expect(details?.timeSpentReading).is.eq(780) + }); + it('getAllBookDetailsByBookTitle', async function () { + const bookmarks = await repo.getAllBookmark() + let titles: string[] = [] + + bookmarks.forEach(async b => { + let content = await this.repo.getContentByContentId(b.contentId) + + if (content == null) { + content = await this.repo.getContentLikeContentId(b.contentId) + } + + titles.push(content.title) + }) + + titles = titles.filter((v, i, a) => a.indexOf(v) === i) + + titles.forEach(async t => { + const details = await repo.getBookDetailsByBookTitle(t) + + chai.expect(details).not.null; + }) + }); }); diff --git a/src/database/repository.ts b/src/database/repository.ts index 18b3871..b8d7359 100644 --- a/src/database/repository.ts +++ b/src/database/repository.ts @@ -1,5 +1,5 @@ import { Database, Statement } from "sql.js"; -import { Bookmark, Content } from "./interfaces"; +import { BookDetails, Bookmark, Content } from "./interfaces"; export class Repository { db: Database @@ -165,6 +165,42 @@ export class Repository { return contents } + async getBookDetailsByBookTitle(bookTitle: string): Promise { + const statement = this.db.prepare( + `select Attribution, Description, Publisher, DateLastRead, ReadStatus, ___PercentRead, ISBN, Series, SeriesNumber, TimeSpentReading from content where Title = $title limit 1;`, + { + $title: bookTitle + } + ) + + if (!statement.step()) { + return null + } + + const row = statement.get() + + if (row.length == 0 || row[0] == null) { + console.debug("Used query: select Attribution, Description, Publisher, DateLastRead, ReadStatus, ___PercentRead, ISBN, Series, SeriesNumber, TimeSpentReading from content where Title = $title limit 2;", { $title: bookTitle, result: row }) + console.warn("Could not find book details in database") + + return null + } + + return { + title: bookTitle, + author: row[0].toString(), + description: row[1]?.toString(), + publisher: row[2]?.toString(), + dateLastRead: row[3] ? new Date(row[3].toString()) : undefined, + readStatus: row[4] ? +row[4].toString() : 0, + percentRead: row[5] ? +row[5].toString() : 0, + isbn: row[6]?.toString(), + series: row[7]?.toString(), + seriesNumber: row[8] ? +row[8].toString() : undefined, + timeSpentReading: row[9] ? +row[9].toString() : 0, + } + } + private parseContentStatement(statement: Statement): Content[] { const contents: Content[] = [] diff --git a/src/modal/ExtractHighlightsModal.ts b/src/modal/ExtractHighlightsModal.ts index ce078ad..f98cd56 100644 --- a/src/modal/ExtractHighlightsModal.ts +++ b/src/modal/ExtractHighlightsModal.ts @@ -66,13 +66,15 @@ export class ExtractHighlightsModal extends Modal { } catch (error) { console.warn("Attempted to read file, but it does not already exist.") } + const markdown = service.fromMapToMarkdown(chapters, existingFile) + const details = await service.getBookDetailsFromBookTitle(bookTitle) // Write file await this.app.vault.adapter.write( fileName, - applyTemplateTransformations(template, markdown, bookTitle) + applyTemplateTransformations(template, markdown, details) ) } } diff --git a/src/template/template.test.ts b/src/template/template.test.ts index 6829cfd..6d84f5c 100644 --- a/src/template/template.test.ts +++ b/src/template/template.test.ts @@ -1,30 +1,70 @@ -import * as chai from 'chai'; -import {applyTemplateTransformations, defaultTemplate} from './template'; - +import * as chai from 'chai' +import { applyTemplateTransformations, defaultTemplate } from './template' describe('template', async function () { it('applyTemplateTransformations default', async function () { - const content = applyTemplateTransformations(defaultTemplate, "test", "test") + const content = applyTemplateTransformations(defaultTemplate, 'test', { + title: 'test title', + author: 'test' + }) chai.expect(content).deep.eq( - `# test + `--- +title: test title +author: test +publisher: +dateLastRead: +readStatus: Unknown +percentRead: +isbn: +series: +seriesNumber: +timeSpentReading: +--- + +# test title + +## Description + + + +## Highlights test` ) - }); + }) const templates = new Map([ [ - "default", + 'default', [ defaultTemplate, - `# test title + `--- +title: test title +author: test +publisher: +dateLastRead: +readStatus: Unknown +percentRead: +isbn: +series: +seriesNumber: +timeSpentReading: +--- + +# test title + +## Description + + + +## Highlights test` ] ], [ - "with front matter", + 'with front matter', [ ` --- @@ -42,16 +82,17 @@ title: test title test` ] - ], + ] ]) for (const [title, t] of templates) { it(`applyTemplateTransformations ${title}`, async function () { - const content = applyTemplateTransformations(t[0], "test", "test title") + const content = applyTemplateTransformations(t[0], 'test', { + title: 'test title', + author: 'test' + }) chai.expect(content).deep.eq(t[1]) - }); + }) } - - -}); +}) diff --git a/src/template/template.ts b/src/template/template.ts index 77f42d6..6405931 100644 --- a/src/template/template.ts +++ b/src/template/template.ts @@ -1,20 +1,83 @@ +import { BookDetails, ReadStatus } from "../database/interfaces" + export const defaultTemplate = ` +--- +title: {{Title}} +author: {{Author}} +publisher: {{Publisher}} +dateLastRead: {{DateLastRead}} +readStatus: {{ReadStatus}} +percentRead: {{PercentRead}} +isbn: {{ISBN}} +series: {{Series}} +seriesNumber: {{SeriesNumber}} +timeSpentReading: {{TimeSpentReading}} +--- + # {{Title}} +## Description + +{{Description}} + +## Highlights + {{highlights}} ` export function applyTemplateTransformations( rawTemplate: string, highlights: string, - bookTitle: string, + bookDetails: BookDetails, ): string { return rawTemplate + .replace( + /{{\s*Title\s*}}/gi, + bookDetails.title, + ) + .replace( + /{{\s*Author\s*}}/gi, + bookDetails.author, + ) + .replace( + /{{\s*Publisher\s*}}/gi, + bookDetails.publisher ?? '', + ) + .replace( + /{{\s*DateLastRead\s*}}/gi, + bookDetails.dateLastRead?.toISOString() ?? '', + ) + .replace( + /{{\s*ReadStatus\s*}}/gi, + ReadStatus[bookDetails.readStatus ?? ReadStatus.Unknown], + ) + .replace( + /{{\s*PercentRead\s*}}/gi, + bookDetails.percentRead?.toString() ?? '', + ) + .replace( + /{{\s*ISBN\s*}}/gi, + bookDetails.isbn ?? '', + ) + .replace( + /{{\s*Series\s*}}/gi, + bookDetails.series ?? '', + ) + .replace( + /{{\s*SeriesNumber\s*}}/gi, + bookDetails.seriesNumber?.toString() ?? '', + ) + .replace( + /{{\s*TimeSpentReading\s*}}/gi, + bookDetails.timeSpentReading?.toString() ?? '', + ) + .replace( + /{{\s*Description\s*}}/gi, + bookDetails.description ?? '', + ) .replace( /{{\s*highlights\s*}}/gi, highlights, - ).replace( - /{{\s*Title\s*}}/gi, - bookTitle, - ).trim() + ) + .trim() }