diff options
| author | Daniel Wieser <me@daniel-wieser.com> | 2026-02-09 01:06:54 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-02-09 00:06:54 +0000 |
| commit | fbe7e3a901862e5766662735a623a5a935c87c0b (patch) | |
| tree | d5a9cb347b1368e2c25ec07924d4711a5d72388e | |
| parent | 485e9948b1d6d40df44a781c5133f6698b1f872b (diff) | |
| download | karakeep-fbe7e3a901862e5766662735a623a5a935c87c0b.tar.zst | |
feat: Added Import for Instapaper (#2434)
* Added Instapaper import
* Fixes #1444 Added Instapaper import support
| -rw-r--r-- | apps/web/components/settings/ImportExport.tsx | 19 | ||||
| -rw-r--r-- | apps/web/lib/i18n/locales/en/translation.json | 1 | ||||
| -rw-r--r-- | packages/shared/import-export/parsers.ts | 64 |
3 files changed, 83 insertions, 1 deletions
diff --git a/apps/web/components/settings/ImportExport.tsx b/apps/web/components/settings/ImportExport.tsx index 4aa84e44..e02297c9 100644 --- a/apps/web/components/settings/ImportExport.tsx +++ b/apps/web/components/settings/ImportExport.tsx @@ -271,6 +271,25 @@ export function ImportExportRow() { </FilePickerButton> </ImportCard> <ImportCard + text="Instapaper" + description={t( + "settings.import.import_bookmarks_from_instapaper_export", + )} + > + <FilePickerButton + size={"sm"} + loading={false} + accept=".csv" + multiple={false} + className="flex items-center gap-2" + onFileSelect={(file) => + runUploadBookmarkFile({ file, source: "instapaper" }) + } + > + <p>Import</p> + </FilePickerButton> + </ImportCard> + <ImportCard text="Karakeep" description={t( "settings.import.import_bookmarks_from_karakeep_export", diff --git a/apps/web/lib/i18n/locales/en/translation.json b/apps/web/lib/i18n/locales/en/translation.json index 567ffb49..41d5312e 100644 --- a/apps/web/lib/i18n/locales/en/translation.json +++ b/apps/web/lib/i18n/locales/en/translation.json @@ -305,6 +305,7 @@ "import_bookmarks_from_karakeep_export": "Import Bookmarks from Karakeep export", "import_bookmarks_from_tab_session_manager_export": "Import Bookmarks from Tab Session Manager", "import_bookmarks_from_mymind_export": "Import Bookmarks from mymind export", + "import_bookmarks_from_instapaper_export": "Import Bookmarks from Instapaper export", "export_links_and_notes": "Export Links and Notes", "imported_bookmarks": "Imported Bookmarks" }, diff --git a/packages/shared/import-export/parsers.ts b/packages/shared/import-export/parsers.ts index df3d2c45..24d85c80 100644 --- a/packages/shared/import-export/parsers.ts +++ b/packages/shared/import-export/parsers.ts @@ -16,7 +16,8 @@ export type ImportSource = | "karakeep" | "linkwarden" | "tab-session-manager" - | "mymind"; + | "mymind" + | "instapaper"; export interface ParsedBookmark { title: string; @@ -357,6 +358,64 @@ function parseMymindBookmarkFile(textContent: string): ParsedBookmark[] { }); } +function parseInstapaperBookmarkFile(textContent: string): ParsedBookmark[] { + const zInstapaperRecordScheme = z.object({ + URL: z.string(), + Title: z.string(), + Selection: z.string(), + Folder: z.string(), + Timestamp: z.string(), + Tags: z.string(), + }); + + const zInstapaperExportScheme = z.array(zInstapaperRecordScheme); + + const record = parse(textContent, { + columns: true, + skip_empty_lines: true, + }); + + const parsed = zInstapaperExportScheme.safeParse(record); + + if (!parsed.success) { + throw new Error( + `CSV file contains an invalid instapaper bookmark file: ${parsed.error.toString()}`, + ); + } + + return parsed.data.map((record) => { + let content: ParsedBookmark["content"]; + if (record.URL && record.URL.trim().length > 0) { + content = { type: BookmarkTypes.LINK as const, url: record.URL.trim() }; + } else if (record.Selection && record.Selection.trim().length > 0) { + content = { + type: BookmarkTypes.TEXT as const, + text: record.Selection.trim(), + }; + } + + const addDate = parseInt(record.Timestamp); + + let tags: string[] = []; + try { + const parsedTags = JSON.parse(record.Tags); + if (Array.isArray(parsedTags)) { + tags = parsedTags.map((tag) => tag.toString().trim()); + } + } catch { + tags = []; + } + + return { + title: record.Title || "", + content, + addDate, + tags, + paths: [], // TODO + }; + }); +} + function deduplicateBookmarks(bookmarks: ParsedBookmark[]): ParsedBookmark[] { const deduplicatedBookmarksMap = new Map<string, ParsedBookmark>(); const textBookmarks: ParsedBookmark[] = []; @@ -428,6 +487,9 @@ export function parseImportFile( case "mymind": result = parseMymindBookmarkFile(textContent); break; + case "instapaper": + result = parseInstapaperBookmarkFile(textContent); + break; } return deduplicateBookmarks(result); } |
