Skip to content

Commit 2dcb9ac

Browse files
committed
v0.0.19
2 parents 370b7ee + 4948482 commit 2dcb9ac

18 files changed

+598
-474
lines changed

.prettierrc

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"printWidth": 160
3+
}

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Use `Raindrop Highlights: Open link in Raindrop` command to open the correspondi
2929

3030
Use `Raindrop Highlights: Manage collections to be synced` command to quickly open the collection management modal.
3131

32-
**NOTE**: Do not touch the front matter properties: `raindrop_id` and `raindrop_last_update`. These properties are used to identify the existing article to prevent file and highlights duplication.
32+
**NOTE**: Do not touch the front matter properties: `raindrop_id` and `raindrop_highlights`. These properties are used to identify the existing article to prevent file and highlights duplication.
3333

3434
### API Token
3535

esbuild.config.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ esbuild.build({
5959
...builtins],
6060
format: 'cjs',
6161
watch: !prod,
62-
target: 'es2016',
62+
target: 'es2018',
6363
logLevel: "info",
6464
sourcemap: prod ? false : 'inline',
6565
treeShaking: true,

manifest.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"id": "obsidian-raindrop-highlights",
33
"name": "Raindrop Highlights",
4-
"version": "0.0.18",
4+
"version": "0.0.19",
55
"minAppVersion": "0.14.0",
66
"description": "Sync your Raindrop.io highlights.",
77
"author": "kaiiiz",

package.json

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "obsidian-raindrop-highlights",
3-
"version": "0.0.18",
3+
"version": "0.0.19",
44
"description": "Sync your Raindrop.io highlights.",
55
"main": "main.js",
66
"scripts": {
@@ -14,24 +14,26 @@
1414
"devDependencies": {
1515
"@tsconfig/svelte": "3.0.0",
1616
"@types/node": "16.11.6",
17-
"@types/nunjucks": "3.2.1",
17+
"@types/nunjucks": "3.2.4",
1818
"@types/semver": "^7.5.0",
1919
"@types/truncate-utf8-bytes": "^1.0.0",
2020
"@typescript-eslint/eslint-plugin": "5.29.0",
2121
"@typescript-eslint/parser": "5.29.0",
2222
"builtin-modules": "3.3.0",
2323
"esbuild": "0.14.47",
2424
"esbuild-svelte": "0.7.1",
25-
"obsidian": "^1.2.8",
25+
"obsidian": "^1.4.11",
2626
"svelte": "3.49.0",
2727
"svelte-preprocess": "4.10.7",
2828
"tslib": "2.4.0",
2929
"typescript": "4.7.4"
3030
},
3131
"dependencies": {
3232
"axios": "0.27.2",
33-
"nunjucks": "3.2.3",
33+
"axios-retry": "^3.8.0",
34+
"nunjucks": "3.2.4",
3435
"sanitize-filename": "1.6.3",
35-
"semver": "^7.5.2"
36+
"semver": "^7.5.2",
37+
"ts-md5": "^1.3.1"
3638
}
3739
}

src/api.ts

+105-77
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,37 @@
11
import { Notice, type App } from "obsidian";
2-
import axios from "axios";
2+
import axios, { AxiosError } from "axios";
3+
import axiosRetry from "axios-retry";
34
import type { RaindropBookmark, RaindropCollection, RaindropCollectionGroup, RaindropHighlight, RaindropUser } from "./types";
45
import TokenManager from "./tokenManager";
6+
import { Md5 } from "ts-md5";
57

68
const BASEURL = "https://api.raindrop.io/rest/v1";
79

810
interface NestedRaindropCollection {
9-
title: string,
10-
parentId: number,
11+
title: string;
12+
parentId: number;
1113
}
1214

15+
axiosRetry(axios, {
16+
retries: 3,
17+
retryCondition: (error: AxiosError) => {
18+
if (error.response && error.response.status === 429) {
19+
new Notice("Too many requests, will retry sync after 1 minute", 5);
20+
console.warn(`Too many requests, will retry sync after 1 minute`);
21+
return true;
22+
} else {
23+
console.error(`request error: ${error}`);
24+
}
25+
return false;
26+
},
27+
retryDelay: () => {
28+
return 60000;
29+
},
30+
onRetry: (retryCount) => {
31+
new Notice(`Retry sync ${retryCount}/3`);
32+
},
33+
});
34+
1335
export class RaindropAPI {
1436
app: App;
1537
tokenManager: TokenManager;
@@ -28,7 +50,7 @@ export class RaindropAPI {
2850
const result = await axios.get(url, {
2951
params: params,
3052
headers: {
31-
"Authorization": `Bearer ${token}`,
53+
Authorization: `Bearer ${token}`,
3254
"Content-Type": "application/json",
3355
},
3456
});
@@ -50,12 +72,12 @@ export class RaindropAPI {
5072
const nestedCollectionPromise = this.get(`${BASEURL}/collections/childrens`, {});
5173

5274
const collections: RaindropCollection[] = [
53-
{ id: -1, title: 'Unsorted' },
54-
{ id: 0, title: 'All bookmarks' },
55-
{ id: -99, title: 'Trash' },
75+
{ id: -1, title: "Unsorted" },
76+
{ id: 0, title: "All bookmarks" },
77+
{ id: -99, title: "Trash" },
5678
];
5779

58-
const collectionGroupMap: {[id: number]: string} = {};
80+
const collectionGroupMap: { [id: number]: string } = {};
5981
if (enableCollectionGroup) {
6082
const res = await this.get(`${BASEURL}/user`, {});
6183
const groups = this.parseGroups(res.user.groups);
@@ -66,11 +88,11 @@ export class RaindropAPI {
6688
});
6789
}
6890

69-
const rootCollectionMap: {[id: number]: string} = {};
91+
const rootCollectionMap: { [id: number]: string } = {};
7092
const rootCollections = await rootCollectionPromise;
7193
rootCollections.items.forEach((collection: any) => {
72-
const id = collection['_id'];
73-
let title = collection['title'];
94+
const id = collection["_id"];
95+
let title = collection["title"];
7496
if (enableCollectionGroup) {
7597
title = `${collectionGroupMap[id]}/${title}`;
7698
}
@@ -81,25 +103,25 @@ export class RaindropAPI {
81103
});
82104
});
83105

84-
const nestedCollectionMap: {[id: number]: NestedRaindropCollection} = {};
106+
const nestedCollectionMap: { [id: number]: NestedRaindropCollection } = {};
85107
const nestedCollections = await nestedCollectionPromise;
86108
nestedCollections.items.forEach((collection: any) => {
87-
const id = collection['_id'];
109+
const id = collection["_id"];
88110
nestedCollectionMap[id] = {
89-
title: collection['title'],
90-
parentId: collection['parent']['$id'],
111+
title: collection["title"],
112+
parentId: collection["parent"]["$id"],
91113
};
92114
});
93115

94116
nestedCollections.items.forEach((collection: any) => {
95-
const id = collection['_id'];
96-
let parentId = collection['parent']['$id'];
97-
let title = collection['title'];
98-
while (parentId && (parentId in nestedCollectionMap)) {
117+
const id = collection["_id"];
118+
let parentId = collection["parent"]["$id"];
119+
let title = collection["title"];
120+
while (parentId && parentId in nestedCollectionMap) {
99121
title = `${nestedCollectionMap[parentId].title}/${title}`;
100122
parentId = nestedCollectionMap[parentId].parentId;
101123
}
102-
if (parentId && (parentId in rootCollectionMap)) {
124+
if (parentId && parentId in rootCollectionMap) {
103125
title = `${rootCollectionMap[parentId]}/${title}`;
104126
}
105127
collections.push({
@@ -111,54 +133,59 @@ export class RaindropAPI {
111133
return collections;
112134
}
113135

114-
async getRaindropsAfter(collectionId: number, lastSync?: Date): Promise<RaindropBookmark[]> {
115-
const notice = new Notice("Fetch Raindrops highlights", 0);
136+
async *getRaindropsAfter(collectionId: number, showNotice: boolean, lastSync?: Date): AsyncGenerator<RaindropBookmark[]> {
137+
let notice;
138+
if (showNotice) {
139+
notice = new Notice("Fetch Raindrops highlights", 0);
140+
}
141+
142+
const pageSize = 50;
116143
const res = await this.get(`${BASEURL}/raindrops/${collectionId}`, {
117-
"page": 0,
118-
"sort": "-lastUpdate"
144+
page: 0,
145+
perpage: pageSize,
146+
sort: "-lastUpdate",
119147
});
120148
const raindropsCnt = res.count;
121149
let bookmarks = this.parseRaindrops(res.items);
122-
let remainPages = Math.ceil(raindropsCnt / 25) - 1;
123-
const totalPages = Math.ceil(raindropsCnt / 25) - 1;
150+
const totalPages = Math.ceil(raindropsCnt / pageSize);
151+
let remainPages = totalPages - 1;
124152
let page = 1;
125153

126-
const addNewPages = async (page: number) => {
154+
const getPage = async (page: number) => {
127155
const res = await this.get(`${BASEURL}/raindrops/${collectionId}`, {
128-
"page": page,
129-
"sort": "-lastUpdate"
156+
page: page,
157+
perpage: pageSize,
158+
sort: "-lastUpdate",
130159
});
131-
bookmarks = bookmarks.concat(this.parseRaindrops(res.items));
132-
}
160+
return this.parseRaindrops(res.items);
161+
};
133162

134-
if (bookmarks.length > 0) {
135-
if (lastSync === undefined) { // sync all
163+
if (lastSync === undefined) {
164+
if (bookmarks.length > 0) {
165+
yield bookmarks;
136166
while (remainPages--) {
137-
notice.setMessage(`Sync Raindrop pages: ${totalPages - remainPages}/${totalPages}`)
138-
await addNewPages(page++);
139-
}
140-
} else { // sync article after lastSync
141-
while (bookmarks[bookmarks.length - 1].lastUpdate.getTime() >= lastSync.getTime() && remainPages--) {
142-
notice.setMessage(`Sync Raindrop pages: ${totalPages - remainPages}/${totalPages}`)
143-
await addNewPages(page++);
167+
notice?.setMessage(`Sync Raindrop pages: ${page + 1}/${totalPages}`);
168+
yield await getPage(page++);
144169
}
145-
bookmarks = bookmarks.filter(bookmark => {
170+
}
171+
} else {
172+
const filterLastUpdate = (bookmarks: RaindropBookmark[]) => {
173+
return bookmarks.filter((bookmark) => {
146174
return bookmark.lastUpdate.getTime() >= lastSync.getTime();
147175
});
176+
};
177+
const filteredBookmark = filterLastUpdate(bookmarks);
178+
if (filteredBookmark.length > 0) {
179+
yield filteredBookmark;
180+
while (bookmarks[bookmarks.length - 1].lastUpdate.getTime() >= lastSync.getTime() && remainPages--) {
181+
notice?.setMessage(`Sync Raindrop pages: ${page + 1}/${totalPages}`);
182+
let bookmarks = await getPage(page++);
183+
yield filterLastUpdate(bookmarks);
184+
}
148185
}
149186
}
150187

151-
// get real highlights (raindrop returns only 3 highlights in /raindrops/${collectionId} endpoint)
152-
for (const [idx, bookmark] of bookmarks.entries()) {
153-
notice.setMessage(`Sync Raindrop bookmarks: ${idx + 1}/${bookmarks.length}`)
154-
if (bookmark.highlights.length == 3) {
155-
const res = await this.get(`${BASEURL}/raindrop/${bookmark.id}`, {});
156-
bookmark['highlights'] = this.parseHighlights(res.item.highlights);
157-
}
158-
}
159-
160-
notice.hide();
161-
return bookmarks;
188+
notice?.hide();
162189
}
163190

164191
async getUser(): Promise<RaindropUser> {
@@ -173,14 +200,14 @@ export class RaindropAPI {
173200
try {
174201
result = await axios.get(`${BASEURL}/user`, {
175202
headers: {
176-
"Authorization": `Bearer ${token}`,
203+
Authorization: `Bearer ${token}`,
177204
"Content-Type": "application/json",
178205
},
179206
});
180207
if (result.status !== 200) {
181208
throw new Error("Invalid token");
182209
}
183-
} catch(e) {
210+
} catch (e) {
184211
throw new Error("Invalid token");
185212
}
186213

@@ -204,22 +231,22 @@ export class RaindropAPI {
204231

205232
private parseRaindrop(raindrop: any): RaindropBookmark {
206233
const bookmark: RaindropBookmark = {
207-
id: raindrop['_id'],
208-
collectionId: raindrop['collectionId'],
209-
title: raindrop['title'],
210-
highlights: this.parseHighlights(raindrop['highlights']),
211-
excerpt: raindrop['excerpt'],
212-
note: raindrop['note'],
213-
link: raindrop['link'],
214-
lastUpdate: new Date(raindrop['lastUpdate']),
215-
tags: raindrop['tags'],
216-
cover: raindrop['cover'],
217-
created: new Date(raindrop['created']),
218-
type: raindrop['type'],
219-
important: raindrop['important'],
234+
id: raindrop["_id"],
235+
collectionId: raindrop["collectionId"],
236+
title: raindrop["title"],
237+
highlights: this.parseHighlights(raindrop["highlights"]),
238+
excerpt: raindrop["excerpt"],
239+
note: raindrop["note"],
240+
link: raindrop["link"],
241+
lastUpdate: new Date(raindrop["lastUpdate"]),
242+
tags: raindrop["tags"],
243+
cover: raindrop["cover"],
244+
created: new Date(raindrop["created"]),
245+
type: raindrop["type"],
246+
important: raindrop["important"],
220247
creator: {
221-
name: raindrop['creatorRef']['name'],
222-
id: raindrop['creatorRef']['_id'],
248+
name: raindrop["creatorRef"]["name"],
249+
id: raindrop["creatorRef"]["_id"],
223250
},
224251
};
225252
return bookmark;
@@ -228,12 +255,13 @@ export class RaindropAPI {
228255
private parseHighlights(highlights: any): RaindropHighlight[] {
229256
return highlights.map((hl: any) => {
230257
const highlight: RaindropHighlight = {
231-
id: hl['_id'],
232-
color: hl['color'],
233-
text: hl['text'],
234-
lastUpdate: new Date(hl['lastUpdate']),
235-
created: new Date(hl['created']),
236-
note: hl['note'],
258+
id: hl["_id"],
259+
color: hl["color"],
260+
text: hl["text"],
261+
lastUpdate: new Date(hl["lastUpdate"]),
262+
created: new Date(hl["created"]),
263+
note: hl["note"],
264+
signature: Md5.hashStr(`${hl["color"]},${hl["text"]},${hl["note"]}`),
237265
};
238266
return highlight;
239267
});
@@ -242,8 +270,8 @@ export class RaindropAPI {
242270
private parseGroups(groups: any): RaindropCollectionGroup[] {
243271
return groups.map((g: any) => {
244272
const group: RaindropCollectionGroup = {
245-
title: g['title'],
246-
collections: g['collections'],
273+
title: g["title"],
274+
collections: g["collections"],
247275
};
248276
return group;
249277
});

src/constants.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import DEFAULT_TEMPLATE from './assets/defaultTemplate.njk';
1+
import DEFAULT_TEMPLATE from "./assets/defaultTemplate.njk";
22
import type { RaindropPluginSettings } from "./types";
33

4-
export const VERSION = '0.0.18';
4+
export const VERSION = "0.0.19";
55

66
export const DEFAULT_SETTINGS: RaindropPluginSettings = {
77
version: VERSION,
@@ -11,11 +11,13 @@ export const DEFAULT_SETTINGS: RaindropPluginSettings = {
1111
appendMode: true,
1212
collectionsFolders: true,
1313
onlyBookmarksWithHl: false,
14-
highlightsFolder: '/',
14+
highlightsFolder: "/",
1515
collectionGroups: false,
16+
autoSyncSuccessNotice: true,
1617
syncCollections: {},
1718
template: DEFAULT_TEMPLATE,
1819
metadataTemplate: "",
1920
filenameTemplate: "{{title}}",
2021
autoSyncInterval: 0,
22+
autoescape: true,
2123
};

0 commit comments

Comments
 (0)