1
1
import { Notice , type App } from "obsidian" ;
2
- import axios from "axios" ;
2
+ import axios , { AxiosError } from "axios" ;
3
+ import axiosRetry from "axios-retry" ;
3
4
import type { RaindropBookmark , RaindropCollection , RaindropCollectionGroup , RaindropHighlight , RaindropUser } from "./types" ;
4
5
import TokenManager from "./tokenManager" ;
6
+ import { Md5 } from "ts-md5" ;
5
7
6
8
const BASEURL = "https://api.raindrop.io/rest/v1" ;
7
9
8
10
interface NestedRaindropCollection {
9
- title : string ,
10
- parentId : number ,
11
+ title : string ;
12
+ parentId : number ;
11
13
}
12
14
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
+
13
35
export class RaindropAPI {
14
36
app : App ;
15
37
tokenManager : TokenManager ;
@@ -28,7 +50,7 @@ export class RaindropAPI {
28
50
const result = await axios . get ( url , {
29
51
params : params ,
30
52
headers : {
31
- " Authorization" : `Bearer ${ token } ` ,
53
+ Authorization : `Bearer ${ token } ` ,
32
54
"Content-Type" : "application/json" ,
33
55
} ,
34
56
} ) ;
@@ -50,12 +72,12 @@ export class RaindropAPI {
50
72
const nestedCollectionPromise = this . get ( `${ BASEURL } /collections/childrens` , { } ) ;
51
73
52
74
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" } ,
56
78
] ;
57
79
58
- const collectionGroupMap : { [ id : number ] : string } = { } ;
80
+ const collectionGroupMap : { [ id : number ] : string } = { } ;
59
81
if ( enableCollectionGroup ) {
60
82
const res = await this . get ( `${ BASEURL } /user` , { } ) ;
61
83
const groups = this . parseGroups ( res . user . groups ) ;
@@ -66,11 +88,11 @@ export class RaindropAPI {
66
88
} ) ;
67
89
}
68
90
69
- const rootCollectionMap : { [ id : number ] : string } = { } ;
91
+ const rootCollectionMap : { [ id : number ] : string } = { } ;
70
92
const rootCollections = await rootCollectionPromise ;
71
93
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" ] ;
74
96
if ( enableCollectionGroup ) {
75
97
title = `${ collectionGroupMap [ id ] } /${ title } ` ;
76
98
}
@@ -81,25 +103,25 @@ export class RaindropAPI {
81
103
} ) ;
82
104
} ) ;
83
105
84
- const nestedCollectionMap : { [ id : number ] : NestedRaindropCollection } = { } ;
106
+ const nestedCollectionMap : { [ id : number ] : NestedRaindropCollection } = { } ;
85
107
const nestedCollections = await nestedCollectionPromise ;
86
108
nestedCollections . items . forEach ( ( collection : any ) => {
87
- const id = collection [ ' _id' ] ;
109
+ const id = collection [ " _id" ] ;
88
110
nestedCollectionMap [ id ] = {
89
- title : collection [ ' title' ] ,
90
- parentId : collection [ ' parent' ] [ ' $id' ] ,
111
+ title : collection [ " title" ] ,
112
+ parentId : collection [ " parent" ] [ " $id" ] ,
91
113
} ;
92
114
} ) ;
93
115
94
116
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 ) {
99
121
title = `${ nestedCollectionMap [ parentId ] . title } /${ title } ` ;
100
122
parentId = nestedCollectionMap [ parentId ] . parentId ;
101
123
}
102
- if ( parentId && ( parentId in rootCollectionMap ) ) {
124
+ if ( parentId && parentId in rootCollectionMap ) {
103
125
title = `${ rootCollectionMap [ parentId ] } /${ title } ` ;
104
126
}
105
127
collections . push ( {
@@ -111,54 +133,59 @@ export class RaindropAPI {
111
133
return collections ;
112
134
}
113
135
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 ;
116
143
const res = await this . get ( `${ BASEURL } /raindrops/${ collectionId } ` , {
117
- "page" : 0 ,
118
- "sort" : "-lastUpdate"
144
+ page : 0 ,
145
+ perpage : pageSize ,
146
+ sort : "-lastUpdate" ,
119
147
} ) ;
120
148
const raindropsCnt = res . count ;
121
149
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 ;
124
152
let page = 1 ;
125
153
126
- const addNewPages = async ( page : number ) => {
154
+ const getPage = async ( page : number ) => {
127
155
const res = await this . get ( `${ BASEURL } /raindrops/${ collectionId } ` , {
128
- "page" : page ,
129
- "sort" : "-lastUpdate"
156
+ page : page ,
157
+ perpage : pageSize ,
158
+ sort : "-lastUpdate" ,
130
159
} ) ;
131
- bookmarks = bookmarks . concat ( this . parseRaindrops ( res . items ) ) ;
132
- }
160
+ return this . parseRaindrops ( res . items ) ;
161
+ } ;
133
162
134
- if ( bookmarks . length > 0 ) {
135
- if ( lastSync === undefined ) { // sync all
163
+ if ( lastSync === undefined ) {
164
+ if ( bookmarks . length > 0 ) {
165
+ yield bookmarks ;
136
166
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 ++ ) ;
144
169
}
145
- bookmarks = bookmarks . filter ( bookmark => {
170
+ }
171
+ } else {
172
+ const filterLastUpdate = ( bookmarks : RaindropBookmark [ ] ) => {
173
+ return bookmarks . filter ( ( bookmark ) => {
146
174
return bookmark . lastUpdate . getTime ( ) >= lastSync . getTime ( ) ;
147
175
} ) ;
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
+ }
148
185
}
149
186
}
150
187
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 ( ) ;
162
189
}
163
190
164
191
async getUser ( ) : Promise < RaindropUser > {
@@ -173,14 +200,14 @@ export class RaindropAPI {
173
200
try {
174
201
result = await axios . get ( `${ BASEURL } /user` , {
175
202
headers : {
176
- " Authorization" : `Bearer ${ token } ` ,
203
+ Authorization : `Bearer ${ token } ` ,
177
204
"Content-Type" : "application/json" ,
178
205
} ,
179
206
} ) ;
180
207
if ( result . status !== 200 ) {
181
208
throw new Error ( "Invalid token" ) ;
182
209
}
183
- } catch ( e ) {
210
+ } catch ( e ) {
184
211
throw new Error ( "Invalid token" ) ;
185
212
}
186
213
@@ -204,22 +231,22 @@ export class RaindropAPI {
204
231
205
232
private parseRaindrop ( raindrop : any ) : RaindropBookmark {
206
233
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" ] ,
220
247
creator : {
221
- name : raindrop [ ' creatorRef' ] [ ' name' ] ,
222
- id : raindrop [ ' creatorRef' ] [ ' _id' ] ,
248
+ name : raindrop [ " creatorRef" ] [ " name" ] ,
249
+ id : raindrop [ " creatorRef" ] [ " _id" ] ,
223
250
} ,
224
251
} ;
225
252
return bookmark ;
@@ -228,12 +255,13 @@ export class RaindropAPI {
228
255
private parseHighlights ( highlights : any ) : RaindropHighlight [ ] {
229
256
return highlights . map ( ( hl : any ) => {
230
257
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" ] } ` ) ,
237
265
} ;
238
266
return highlight ;
239
267
} ) ;
@@ -242,8 +270,8 @@ export class RaindropAPI {
242
270
private parseGroups ( groups : any ) : RaindropCollectionGroup [ ] {
243
271
return groups . map ( ( g : any ) => {
244
272
const group : RaindropCollectionGroup = {
245
- title : g [ ' title' ] ,
246
- collections : g [ ' collections' ] ,
273
+ title : g [ " title" ] ,
274
+ collections : g [ " collections" ] ,
247
275
} ;
248
276
return group ;
249
277
} ) ;
0 commit comments