aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Wieser <me@daniel-wieser.com>2026-02-09 01:06:54 +0100
committerGitHub <noreply@github.com>2026-02-09 00:06:54 +0000
commitfbe7e3a901862e5766662735a623a5a935c87c0b (patch)
treed5a9cb347b1368e2c25ec07924d4711a5d72388e
parent485e9948b1d6d40df44a781c5133f6698b1f872b (diff)
downloadkarakeep-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.tsx19
-rw-r--r--apps/web/lib/i18n/locales/en/translation.json1
-rw-r--r--packages/shared/import-export/parsers.ts64
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);
}