apple-books-export 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/database.d.ts +37 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +187 -0
- package/dist/db-adapter.d.ts +36 -0
- package/dist/db-adapter.d.ts.map +1 -0
- package/dist/db-adapter.js +72 -0
- package/dist/exporters/csv.d.ts +6 -0
- package/dist/exporters/csv.d.ts.map +1 -0
- package/dist/exporters/csv.js +70 -0
- package/dist/exporters/html.d.ts +3 -0
- package/dist/exporters/html.d.ts.map +1 -0
- package/dist/exporters/html.js +775 -0
- package/dist/exporters/json.d.ts +6 -0
- package/dist/exporters/json.d.ts.map +1 -0
- package/dist/exporters/json.js +103 -0
- package/dist/exporters/markdown.d.ts +14 -0
- package/dist/exporters/markdown.d.ts.map +1 -0
- package/dist/exporters/markdown.js +145 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +232 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Denis Moskalets (denya.msk@gmail.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Apple Books Export
|
|
2
|
+
|
|
3
|
+
Export all your highlights, bookmarks, and notes from Apple Books on macOS.
|
|
4
|
+
|
|
5
|
+
## What is this?
|
|
6
|
+
|
|
7
|
+
If you highlight books in Apple Books, this tool lets you export all your highlights to HTML, Markdown, JSON, or CSV. Perfect for:
|
|
8
|
+
|
|
9
|
+
- Backing up your highlights
|
|
10
|
+
- Searching through all your notes
|
|
11
|
+
- Sharing quotes from books you've read
|
|
12
|
+
- Importing highlights into note-taking apps
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
No installation needed! Just run:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bunx apple-books-export
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This creates an `apple-books-export.html` file with all your highlights in a searchable, interactive page.
|
|
23
|
+
|
|
24
|
+
**Don't have Bun?** Install it with: `curl -fsSL https://bun.sh/install | bash`
|
|
25
|
+
|
|
26
|
+
Or use Node.js instead: `npx apple-books-export`
|
|
27
|
+
|
|
28
|
+
## Common Uses
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Export to searchable HTML (default)
|
|
32
|
+
bunx apple-books-export
|
|
33
|
+
|
|
34
|
+
# Export to Markdown files (one per book)
|
|
35
|
+
bunx apple-books-export markdown
|
|
36
|
+
|
|
37
|
+
# Export to a single JSON file
|
|
38
|
+
bunx apple-books-export json --single-file
|
|
39
|
+
|
|
40
|
+
# Export only highlights (skip bookmarks)
|
|
41
|
+
bunx apple-books-export --highlights-only
|
|
42
|
+
|
|
43
|
+
# Export only yellow highlights
|
|
44
|
+
bunx apple-books-export --colors yellow
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Export Formats
|
|
48
|
+
|
|
49
|
+
### HTML (default)
|
|
50
|
+
Creates a single webpage with all your highlights. Includes search, color-coding, and works offline.
|
|
51
|
+
|
|
52
|
+
### Markdown
|
|
53
|
+
Creates readable text files with YAML metadata. Great for importing into Obsidian or Notion.
|
|
54
|
+
|
|
55
|
+
### JSON
|
|
56
|
+
Exports raw data with all metadata. Useful for custom processing or analysis.
|
|
57
|
+
|
|
58
|
+
### CSV
|
|
59
|
+
Spreadsheet format for Excel or data analysis.
|
|
60
|
+
|
|
61
|
+
## Options
|
|
62
|
+
|
|
63
|
+
Run `bunx apple-books-export --help` to see all options.
|
|
64
|
+
|
|
65
|
+
Common options:
|
|
66
|
+
- `--output <path>` - Custom output location
|
|
67
|
+
- `--single-file` - Combine all books into one file (for Markdown/JSON)
|
|
68
|
+
- `--highlights-only` - Skip bookmarks and notes
|
|
69
|
+
- `--colors yellow,green` - Only export specific highlight colors
|
|
70
|
+
|
|
71
|
+
## Troubleshooting
|
|
72
|
+
|
|
73
|
+
**"Database not found"** - Make sure you have Apple Books installed and have created at least one highlight or bookmark.
|
|
74
|
+
|
|
75
|
+
**No highlights exported** - Check that you're not using filters that exclude all your highlights (e.g., `--colors pink` when you only have yellow highlights).
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT - Free to use and modify.
|
|
80
|
+
|
|
81
|
+
## Credits
|
|
82
|
+
|
|
83
|
+
Built with [Bun](https://bun.sh). Inspired by [jladicos/apple-books-highlights](https://github.com/jladicos/apple-books-highlights) and [angela-zhao/apple-books-annotations-exporter](https://github.com/angela-zhao/apple-books-annotations-exporter).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Annotation, Book, RawAnnotation } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Convert Apple's 2001-01-01 epoch timestamp to JavaScript Date
|
|
4
|
+
*/
|
|
5
|
+
export declare function convertAppleDate(timestamp: number): Date;
|
|
6
|
+
/**
|
|
7
|
+
* Find Apple Books SQLite database files
|
|
8
|
+
*/
|
|
9
|
+
export declare function findDatabases(customPaths?: {
|
|
10
|
+
annotations?: string;
|
|
11
|
+
library?: string;
|
|
12
|
+
}): {
|
|
13
|
+
annotationsDb: string;
|
|
14
|
+
libraryDb: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Query all annotations with book metadata
|
|
18
|
+
*/
|
|
19
|
+
export declare function queryAnnotations(annotationsDbPath: string, libraryDbPath: string): Promise<RawAnnotation[]>;
|
|
20
|
+
/**
|
|
21
|
+
* Transform raw annotations into structured Annotation objects
|
|
22
|
+
*/
|
|
23
|
+
export declare function transformAnnotations(rawAnnotations: RawAnnotation[]): Annotation[];
|
|
24
|
+
/**
|
|
25
|
+
* Group annotations by book
|
|
26
|
+
*/
|
|
27
|
+
export declare function groupByBook(rawAnnotations: RawAnnotation[]): Book[];
|
|
28
|
+
/**
|
|
29
|
+
* Filter annotations based on export options
|
|
30
|
+
*/
|
|
31
|
+
export declare function filterAnnotations(annotations: Annotation[], options: {
|
|
32
|
+
includeHighlights: boolean;
|
|
33
|
+
includeBookmarks: boolean;
|
|
34
|
+
includeNotes: boolean;
|
|
35
|
+
colorFilters?: string[];
|
|
36
|
+
}): Annotation[];
|
|
37
|
+
//# sourceMappingURL=database.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../src/database.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,aAAa,EAAmC,MAAM,YAAY,CAAC;AAOnG;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAMxD;AA4CD;;GAEG;AACH,wBAAgB,aAAa,CAAC,WAAW,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IACvF,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB,CAyCA;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,iBAAiB,EAAE,MAAM,EACzB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,aAAa,EAAE,CAAC,CA+B1B;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,cAAc,EAAE,aAAa,EAAE,GAAG,UAAU,EAAE,CAiBlF;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,cAAc,EAAE,aAAa,EAAE,GAAG,IAAI,EAAE,CAkCnE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,UAAU,EAAE,EACzB,OAAO,EAAE;IACP,iBAAiB,EAAE,OAAO,CAAC;IAC3B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,YAAY,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB,GACA,UAAU,EAAE,CAcd"}
|
package/dist/database.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { Database } from "./db-adapter.js";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { existsSync, readdirSync } from "fs";
|
|
5
|
+
const APPLE_BOOKS_CONTAINER = join(homedir(), "Library/Containers/com.apple.iBooksX/Data/Documents");
|
|
6
|
+
/**
|
|
7
|
+
* Convert Apple's 2001-01-01 epoch timestamp to JavaScript Date
|
|
8
|
+
*/
|
|
9
|
+
export function convertAppleDate(timestamp) {
|
|
10
|
+
// Apple epoch starts at 2001-01-01 00:00:00 UTC
|
|
11
|
+
// Unix epoch starts at 1970-01-01 00:00:00 UTC
|
|
12
|
+
// Difference is 978307200 seconds
|
|
13
|
+
const APPLE_EPOCH_OFFSET = 978307200;
|
|
14
|
+
return new Date((timestamp + APPLE_EPOCH_OFFSET) * 1000);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Map Apple's annotation style code to type and color
|
|
18
|
+
*/
|
|
19
|
+
function mapAnnotationStyle(style) {
|
|
20
|
+
// Based on research:
|
|
21
|
+
// 0 = underline (highlight)
|
|
22
|
+
// 1 = green highlight
|
|
23
|
+
// 2 = blue highlight
|
|
24
|
+
// 3 = yellow highlight
|
|
25
|
+
// 4 = pink highlight
|
|
26
|
+
// 5 = purple highlight
|
|
27
|
+
const styleMap = {
|
|
28
|
+
0: { type: 'highlight', color: 'underline' },
|
|
29
|
+
1: { type: 'highlight', color: 'green' },
|
|
30
|
+
2: { type: 'highlight', color: 'blue' },
|
|
31
|
+
3: { type: 'highlight', color: 'yellow' },
|
|
32
|
+
4: { type: 'highlight', color: 'pink' },
|
|
33
|
+
5: { type: 'highlight', color: 'purple' },
|
|
34
|
+
};
|
|
35
|
+
return styleMap[style] || { type: 'highlight', color: 'yellow' };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Determine annotation type based on content
|
|
39
|
+
*/
|
|
40
|
+
function determineAnnotationType(raw) {
|
|
41
|
+
// If it has a note, it's a note annotation
|
|
42
|
+
if (raw.note && raw.note.trim().length > 0) {
|
|
43
|
+
return 'note';
|
|
44
|
+
}
|
|
45
|
+
// If it has no selected text, it's a bookmark
|
|
46
|
+
if (!raw.text || raw.text.trim().length === 0) {
|
|
47
|
+
return 'bookmark';
|
|
48
|
+
}
|
|
49
|
+
// Otherwise it's a highlight
|
|
50
|
+
return 'highlight';
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Find Apple Books SQLite database files
|
|
54
|
+
*/
|
|
55
|
+
export function findDatabases(customPaths) {
|
|
56
|
+
if (customPaths?.annotations && customPaths?.library) {
|
|
57
|
+
return {
|
|
58
|
+
annotationsDb: customPaths.annotations,
|
|
59
|
+
libraryDb: customPaths.library,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// Find AEAnnotation database
|
|
63
|
+
const annotationsDir = join(APPLE_BOOKS_CONTAINER, "AEAnnotation");
|
|
64
|
+
if (!existsSync(annotationsDir)) {
|
|
65
|
+
throw new Error(`Annotations directory not found: ${annotationsDir}`);
|
|
66
|
+
}
|
|
67
|
+
const annotationFiles = readdirSync(annotationsDir).filter(f => f.startsWith("AEAnnotation_") && f.endsWith(".sqlite"));
|
|
68
|
+
if (annotationFiles.length === 0) {
|
|
69
|
+
throw new Error(`No annotation database found in ${annotationsDir}`);
|
|
70
|
+
}
|
|
71
|
+
const annotationsDb = join(annotationsDir, annotationFiles[0]);
|
|
72
|
+
// Find BKLibrary database
|
|
73
|
+
const libraryDir = join(APPLE_BOOKS_CONTAINER, "BKLibrary");
|
|
74
|
+
if (!existsSync(libraryDir)) {
|
|
75
|
+
throw new Error(`Library directory not found: ${libraryDir}`);
|
|
76
|
+
}
|
|
77
|
+
const libraryFiles = readdirSync(libraryDir).filter(f => f.startsWith("BKLibrary") && f.endsWith(".sqlite"));
|
|
78
|
+
if (libraryFiles.length === 0) {
|
|
79
|
+
throw new Error(`No library database found in ${libraryDir}`);
|
|
80
|
+
}
|
|
81
|
+
const libraryDb = join(libraryDir, libraryFiles[0]);
|
|
82
|
+
return { annotationsDb, libraryDb };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Query all annotations with book metadata
|
|
86
|
+
*/
|
|
87
|
+
export async function queryAnnotations(annotationsDbPath, libraryDbPath) {
|
|
88
|
+
const annotationsDb = await Database.create(annotationsDbPath, { readonly: true });
|
|
89
|
+
// Attach the library database
|
|
90
|
+
annotationsDb.run(`ATTACH DATABASE '${libraryDbPath}' AS library`);
|
|
91
|
+
const query = `
|
|
92
|
+
SELECT
|
|
93
|
+
ZAEANNOTATION.Z_PK as id,
|
|
94
|
+
ZAEANNOTATION.ZANNOTATIONASSETID as assetId,
|
|
95
|
+
ZAEANNOTATION.ZANNOTATIONSELECTEDTEXT as text,
|
|
96
|
+
ZAEANNOTATION.ZANNOTATIONNOTE as note,
|
|
97
|
+
ZAEANNOTATION.ZANNOTATIONSTYLE as style,
|
|
98
|
+
ZAEANNOTATION.ZFUTUREPROOFING5 as location,
|
|
99
|
+
ZAEANNOTATION.ZANNOTATIONCREATIONDATE as createdAt,
|
|
100
|
+
ZAEANNOTATION.ZANNOTATIONMODIFICATIONDATE as modifiedAt,
|
|
101
|
+
ZAEANNOTATION.ZANNOTATIONDELETED as deleted,
|
|
102
|
+
library.ZBKLIBRARYASSET.ZTITLE as title,
|
|
103
|
+
library.ZBKLIBRARYASSET.ZAUTHOR as author,
|
|
104
|
+
library.ZBKLIBRARYASSET.ZGENRE as genre
|
|
105
|
+
FROM ZAEANNOTATION
|
|
106
|
+
LEFT JOIN library.ZBKLIBRARYASSET
|
|
107
|
+
ON ZAEANNOTATION.ZANNOTATIONASSETID = library.ZBKLIBRARYASSET.ZASSETID
|
|
108
|
+
WHERE ZAEANNOTATION.ZANNOTATIONDELETED = 0
|
|
109
|
+
ORDER BY title, createdAt
|
|
110
|
+
`;
|
|
111
|
+
const results = annotationsDb.query(query).all();
|
|
112
|
+
annotationsDb.close();
|
|
113
|
+
return results;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Transform raw annotations into structured Annotation objects
|
|
117
|
+
*/
|
|
118
|
+
export function transformAnnotations(rawAnnotations) {
|
|
119
|
+
return rawAnnotations.map(raw => {
|
|
120
|
+
const type = determineAnnotationType(raw);
|
|
121
|
+
const { color } = mapAnnotationStyle(raw.style);
|
|
122
|
+
return {
|
|
123
|
+
id: raw.id,
|
|
124
|
+
type,
|
|
125
|
+
color,
|
|
126
|
+
text: raw.text,
|
|
127
|
+
note: raw.note,
|
|
128
|
+
location: raw.location,
|
|
129
|
+
chapter: null, // Apple Books doesn't store chapter info in accessible way
|
|
130
|
+
createdAt: convertAppleDate(raw.createdAt),
|
|
131
|
+
modifiedAt: convertAppleDate(raw.modifiedAt),
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Group annotations by book
|
|
137
|
+
*/
|
|
138
|
+
export function groupByBook(rawAnnotations) {
|
|
139
|
+
const bookMap = new Map();
|
|
140
|
+
for (const raw of rawAnnotations) {
|
|
141
|
+
const assetId = raw.assetId;
|
|
142
|
+
if (!bookMap.has(assetId)) {
|
|
143
|
+
bookMap.set(assetId, {
|
|
144
|
+
assetId,
|
|
145
|
+
title: raw.title,
|
|
146
|
+
author: raw.author,
|
|
147
|
+
genre: raw.genre,
|
|
148
|
+
annotations: [],
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
const book = bookMap.get(assetId);
|
|
152
|
+
const type = determineAnnotationType(raw);
|
|
153
|
+
const { color } = mapAnnotationStyle(raw.style);
|
|
154
|
+
book.annotations.push({
|
|
155
|
+
id: raw.id,
|
|
156
|
+
type,
|
|
157
|
+
color,
|
|
158
|
+
text: raw.text,
|
|
159
|
+
note: raw.note,
|
|
160
|
+
location: raw.location,
|
|
161
|
+
chapter: null,
|
|
162
|
+
createdAt: convertAppleDate(raw.createdAt),
|
|
163
|
+
modifiedAt: convertAppleDate(raw.modifiedAt),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return Array.from(bookMap.values());
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Filter annotations based on export options
|
|
170
|
+
*/
|
|
171
|
+
export function filterAnnotations(annotations, options) {
|
|
172
|
+
return annotations.filter(ann => {
|
|
173
|
+
// Filter by type
|
|
174
|
+
if (ann.type === 'highlight' && !options.includeHighlights)
|
|
175
|
+
return false;
|
|
176
|
+
if (ann.type === 'bookmark' && !options.includeBookmarks)
|
|
177
|
+
return false;
|
|
178
|
+
if (ann.type === 'note' && !options.includeNotes)
|
|
179
|
+
return false;
|
|
180
|
+
// Filter by color
|
|
181
|
+
if (options.colorFilters && options.colorFilters.length > 0) {
|
|
182
|
+
if (!options.colorFilters.includes(ann.color))
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database adapter for dual runtime support (Bun + Node.js)
|
|
3
|
+
*
|
|
4
|
+
* Provides a unified SQLite interface that works with:
|
|
5
|
+
* - Bun's built-in bun:sqlite (zero dependencies)
|
|
6
|
+
* - Node.js's better-sqlite3 (auto-installed)
|
|
7
|
+
*/
|
|
8
|
+
type QueryResult = any[];
|
|
9
|
+
interface DatabaseInterface {
|
|
10
|
+
query(sql: string): {
|
|
11
|
+
all(): QueryResult;
|
|
12
|
+
};
|
|
13
|
+
run(sql: string): void;
|
|
14
|
+
close(): void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Unified Database class that works in both Bun and Node.js
|
|
18
|
+
*
|
|
19
|
+
* This uses a factory pattern since constructors can't be async.
|
|
20
|
+
*/
|
|
21
|
+
export declare class Database implements DatabaseInterface {
|
|
22
|
+
private db;
|
|
23
|
+
private isBun;
|
|
24
|
+
private constructor();
|
|
25
|
+
/**
|
|
26
|
+
* Create a new Database instance (async factory)
|
|
27
|
+
*/
|
|
28
|
+
static create(path: string, options?: {
|
|
29
|
+
readonly?: boolean;
|
|
30
|
+
}): Promise<Database>;
|
|
31
|
+
query(sql: string): any;
|
|
32
|
+
run(sql: string): void;
|
|
33
|
+
close(): void;
|
|
34
|
+
}
|
|
35
|
+
export {};
|
|
36
|
+
//# sourceMappingURL=db-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db-adapter.d.ts","sourceRoot":"","sources":["../src/db-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,KAAK,WAAW,GAAG,GAAG,EAAE,CAAC;AAEzB,UAAU,iBAAiB;IACzB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG;QAAE,GAAG,IAAI,WAAW,CAAA;KAAE,CAAC;IAC3C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,IAAI,IAAI,CAAC;CACf;AASD;;;;GAIG;AACH,qBAAa,QAAS,YAAW,iBAAiB;IAChD,OAAO,CAAC,EAAE,CAAM;IAChB,OAAO,CAAC,KAAK,CAAU;IAEvB,OAAO;IAKP;;OAEG;WACU,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,QAAQ,CAAC;IAmBtF,KAAK,CAAC,GAAG,EAAE,MAAM;IAYjB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAUtB,KAAK,IAAI,IAAI;CAGd"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database adapter for dual runtime support (Bun + Node.js)
|
|
3
|
+
*
|
|
4
|
+
* Provides a unified SQLite interface that works with:
|
|
5
|
+
* - Bun's built-in bun:sqlite (zero dependencies)
|
|
6
|
+
* - Node.js's better-sqlite3 (auto-installed)
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Detect runtime environment
|
|
10
|
+
*/
|
|
11
|
+
function isBunRuntime() {
|
|
12
|
+
return typeof globalThis.Bun !== "undefined";
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Unified Database class that works in both Bun and Node.js
|
|
16
|
+
*
|
|
17
|
+
* This uses a factory pattern since constructors can't be async.
|
|
18
|
+
*/
|
|
19
|
+
export class Database {
|
|
20
|
+
db;
|
|
21
|
+
isBun;
|
|
22
|
+
constructor(db, isBun) {
|
|
23
|
+
this.db = db;
|
|
24
|
+
this.isBun = isBun;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Create a new Database instance (async factory)
|
|
28
|
+
*/
|
|
29
|
+
static async create(path, options) {
|
|
30
|
+
const isBun = isBunRuntime();
|
|
31
|
+
if (isBun) {
|
|
32
|
+
// Bun runtime - use built-in bun:sqlite
|
|
33
|
+
// Dynamic import works in both runtimes
|
|
34
|
+
const { Database: BunDB } = await import('bun:sqlite');
|
|
35
|
+
const db = new BunDB(path, options);
|
|
36
|
+
return new Database(db, isBun);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
// Node.js runtime - use better-sqlite3
|
|
40
|
+
// Dynamic import works in ESM
|
|
41
|
+
const BetterSqlite3Module = await import('better-sqlite3');
|
|
42
|
+
const BetterSqlite3 = BetterSqlite3Module.default || BetterSqlite3Module;
|
|
43
|
+
const db = new BetterSqlite3(path, options);
|
|
44
|
+
return new Database(db, isBun);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
query(sql) {
|
|
48
|
+
if (this.isBun) {
|
|
49
|
+
// Bun: db.query(sql).all()
|
|
50
|
+
return this.db.query(sql);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// Node.js: db.prepare(sql).all()
|
|
54
|
+
return {
|
|
55
|
+
all: () => this.db.prepare(sql).all()
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
run(sql) {
|
|
60
|
+
if (this.isBun) {
|
|
61
|
+
// Bun: db.run(sql)
|
|
62
|
+
this.db.run(sql);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// Node.js: db.prepare(sql).run()
|
|
66
|
+
this.db.prepare(sql).run();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
close() {
|
|
70
|
+
this.db.close();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csv.d.ts","sourceRoot":"","sources":["../../src/exporters/csv.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AA4BxC;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAgDrE"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync } from "fs";
|
|
2
|
+
import { dirname } from "path";
|
|
3
|
+
/**
|
|
4
|
+
* Escape CSV field value
|
|
5
|
+
*/
|
|
6
|
+
function escapeCsvField(value) {
|
|
7
|
+
if (value === null || value === undefined) {
|
|
8
|
+
return '';
|
|
9
|
+
}
|
|
10
|
+
const stringValue = String(value);
|
|
11
|
+
// If the value contains quotes, commas, or newlines, it needs to be quoted
|
|
12
|
+
if (stringValue.includes('"') || stringValue.includes(',') || stringValue.includes('\n')) {
|
|
13
|
+
// Escape quotes by doubling them
|
|
14
|
+
return `"${stringValue.replace(/"/g, '""')}"`;
|
|
15
|
+
}
|
|
16
|
+
return stringValue;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Format date for CSV
|
|
20
|
+
*/
|
|
21
|
+
function formatDate(date) {
|
|
22
|
+
return date.toISOString().replace('T', ' ').substring(0, 19);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Export books to CSV file
|
|
26
|
+
*/
|
|
27
|
+
export function exportToCsv(books, outputPath) {
|
|
28
|
+
// Ensure parent directory exists
|
|
29
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
30
|
+
// CSV header
|
|
31
|
+
const headers = [
|
|
32
|
+
'assetId',
|
|
33
|
+
'title',
|
|
34
|
+
'author',
|
|
35
|
+
'genre',
|
|
36
|
+
'annotationId',
|
|
37
|
+
'type',
|
|
38
|
+
'color',
|
|
39
|
+
'text',
|
|
40
|
+
'note',
|
|
41
|
+
'location',
|
|
42
|
+
'chapter',
|
|
43
|
+
'createdAt',
|
|
44
|
+
'modifiedAt',
|
|
45
|
+
];
|
|
46
|
+
let csv = headers.join(',') + '\n';
|
|
47
|
+
// Add rows
|
|
48
|
+
for (const book of books) {
|
|
49
|
+
for (const annotation of book.annotations) {
|
|
50
|
+
const row = [
|
|
51
|
+
escapeCsvField(book.assetId),
|
|
52
|
+
escapeCsvField(book.title),
|
|
53
|
+
escapeCsvField(book.author),
|
|
54
|
+
escapeCsvField(book.genre),
|
|
55
|
+
escapeCsvField(String(annotation.id)),
|
|
56
|
+
escapeCsvField(annotation.type),
|
|
57
|
+
escapeCsvField(annotation.color),
|
|
58
|
+
escapeCsvField(annotation.text),
|
|
59
|
+
escapeCsvField(annotation.note),
|
|
60
|
+
escapeCsvField(annotation.location),
|
|
61
|
+
escapeCsvField(annotation.chapter),
|
|
62
|
+
escapeCsvField(formatDate(annotation.createdAt)),
|
|
63
|
+
escapeCsvField(formatDate(annotation.modifiedAt)),
|
|
64
|
+
];
|
|
65
|
+
csv += row.join(',') + '\n';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
writeFileSync(outputPath, csv, 'utf-8');
|
|
69
|
+
return outputPath;
|
|
70
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../src/exporters/html.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAc,MAAM,aAAa,CAAC;AAqHpD,wBAAgB,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CA+qBtE"}
|