├── .dockerignore
├── .github
└── workflows
│ └── publish.yml
├── .gitignore
├── .idea
└── .idea.Linkding.Sync
│ └── .idea
│ ├── .gitignore
│ ├── GitLink.xml
│ ├── active-tab-highlighter.xml
│ ├── aws.xml
│ ├── dbnavigator.xml
│ ├── encodings.xml
│ ├── git_toolbox_prj.xml
│ ├── indexLayout.xml
│ └── vcs.xml
├── Dockerfile_Linkding
├── Dockerfile_Wallabag
├── Linkding.Sync.sln
├── README.md
├── examples
├── linkding
│ ├── config.yml
│ └── docker-compose.yml
└── wallabag
│ ├── config.yml
│ └── docker-compose.yml
├── global.json
└── src
├── Domain
└── Core
│ ├── Abstraction
│ ├── ILinkdingService.cs
│ └── IWallabagService.cs
│ ├── Converters
│ ├── DateTimeConverterForCustomStandard.cs
│ └── DateTimeOffsetConverterUsingDateTimeParse.cs
│ ├── Core.csproj
│ ├── Entities
│ ├── Linkding
│ │ ├── Bookmark.cs
│ │ ├── BookmarkBase.cs
│ │ ├── BookmarkCreatePayload.cs
│ │ ├── BookmarkUpdatePayload.cs
│ │ ├── BookmarkUpdateResult.cs
│ │ ├── BookmarksResult.cs
│ │ ├── HandlerResult.cs
│ │ ├── Tag.cs
│ │ └── TagCreatePayload.cs
│ └── Wallabag
│ │ ├── Annotation.cs
│ │ ├── Embedded.cs
│ │ ├── Headers.cs
│ │ ├── HttpLink.cs
│ │ ├── Links.cs
│ │ ├── QueryLinks.cs
│ │ ├── Range.cs
│ │ ├── Tag.cs
│ │ ├── WallabagItem.cs
│ │ └── WallabagQuery.cs
│ └── Handler
│ ├── ILinkdingSyncTaskHandler.cs
│ ├── ILinkdingTagTaskHandler.cs
│ ├── ILinkdingTaskHandler.cs
│ └── ISyncTaskHandler.cs
├── Linkding
├── Dockerfile
├── Extensions
│ ├── ServiceRegistrationExtensions.cs
│ └── StringExtensions.cs
├── Handler
│ ├── AddPopularSitesAsTagHandler.cs
│ ├── AddYearToBookmarkHandler.cs
│ ├── NormalizeTagHandler.cs
│ └── UpdateTargetLinkdingHandler.cs
├── Linkding.csproj
├── Options
│ └── WorkerSettings.cs
├── Program.cs
├── Properties
│ └── launchSettings.json
├── Settings
│ └── SettingsService.cs
├── Worker.cs
├── appsettings.Development.json
├── appsettings.json
└── data
│ └── config.yml
├── Services
├── Linkding.Client
│ ├── Automapper
│ │ └── LinkdingBookmarkProfile.cs
│ ├── Extensions
│ │ ├── BookmarkExtensions.cs
│ │ ├── ServiceRegistrationExtensions.cs
│ │ ├── System.cs
│ │ └── SystemCollectionGenericExtesions.cs
│ ├── Linkding.Client.csproj
│ ├── LinkdingService.cs
│ └── Options
│ │ └── LinkdingSettings.cs
└── Wallabag.Client
│ ├── Contracts
│ └── IAccessTokenProvider.cs
│ ├── Converters
│ ├── DateTimeConverterForCustomStandard.cs
│ └── DateTimeOffsetConverterUsingDateTimeParse.cs
│ ├── Extensions
│ ├── ServiceRegistrationExtensions.cs
│ └── URIExtensions.cs
│ ├── Models
│ ├── WallabagEntry.cs
│ └── WallabagPayload.cs
│ ├── OAuth
│ ├── AuthenticationClient.cs
│ └── OAuthTokenProvider.cs
│ ├── Options
│ └── WallabagSettings.cs
│ ├── Wallabag.Client.csproj
│ ├── WallabagServiceBase.cs
│ └── WallabagServiceEntries.cs
└── Wallabag
├── Extensions
└── ServiceRegistrationExtensions.cs
├── Handler
└── LinkdingBookmarkToWallabagHandler.cs
├── Options
└── WorkerSettings.cs
├── Program.cs
├── Properties
└── launchSettings.json
├── Settings
└── SettingsService.cs
├── Wallabag.csproj
├── Worker.cs
├── appsettings.Development.json
├── appsettings.json
└── data
└── config.yml
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/.dockerignore
2 | **/.env
3 | **/.git
4 | **/.gitignore
5 | **/.project
6 | **/.settings
7 | **/.toolstarget
8 | **/.vs
9 | **/.vscode
10 | **/.idea
11 | **/*.*proj.user
12 | **/*.dbmdl
13 | **/*.jfm
14 | **/azds.yaml
15 | **/bin
16 | **/charts
17 | **/docker-compose*
18 | **/Dockerfile*
19 | **/node_modules
20 | **/npm-debug.log
21 | **/obj
22 | **/secrets.dev.yaml
23 | **/values.dev.yaml
24 | **/appsettings.json
25 | **/appsettings.*.json
26 | LICENSE
27 | README.md
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Images to GHCR
2 |
3 | env:
4 | DOTNET_VERSION: '8.0.x'
5 |
6 | on:
7 | push:
8 | branches:
9 | - main
10 | workflow_dispatch:
11 |
12 | jobs:
13 | push-store-image:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: 'Checkout GitHub Action'
17 | uses: actions/checkout@main
18 |
19 | - name: 'Login to GitHub Container Registry'
20 | uses: docker/login-action@v1
21 | with:
22 | registry: ghcr.io
23 | username: ${{github.actor}}
24 | password: ${{secrets.GITHUB_TOKEN}}
25 |
26 | - name: 'Build WallabagSync Image'
27 | run: |
28 | docker build -f ./Dockerfile_Wallabag --tag ghcr.io/spaytac/wallabag-sync:latest .
29 | docker push ghcr.io/spaytac/wallabag-sync:latest
30 |
31 | - name: 'Build LinkdingUpdater Image'
32 | run: |
33 | docker build -f ./Dockerfile_Linkding --tag ghcr.io/spaytac/linkding-updater:latest .
34 | docker push ghcr.io/spaytac/linkding-updater:latest
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bin/
2 | obj/
3 | /packages/
4 | riderModule.iml
5 | /_ReSharper.Caches/
6 | **/.env
7 | .env
8 | compose/
9 | compose-test/
10 | .vscode/
11 | .idea/
--------------------------------------------------------------------------------
/.idea/.idea.Linkding.Sync/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Rider ignored files
5 | /contentModel.xml
6 | /.idea.Linkding.Sync.iml
7 | /modules.xml
8 | /projectSettingsUpdater.xml
9 | # Editor-based HTTP Client requests
10 | /httpRequests/
11 | # Datasource local storage ignored files
12 | /dataSources/
13 | /dataSources.local.xml
14 |
--------------------------------------------------------------------------------
/.idea/.idea.Linkding.Sync/.idea/GitLink.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/.idea.Linkding.Sync/.idea/active-tab-highlighter.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
--------------------------------------------------------------------------------
/.idea/.idea.Linkding.Sync/.idea/aws.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.idea/.idea.Linkding.Sync/.idea/dbnavigator.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
--------------------------------------------------------------------------------
/.idea/.idea.Linkding.Sync/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/.idea.Linkding.Sync/.idea/git_toolbox_prj.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.idea/.idea.Linkding.Sync/.idea/indexLayout.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | examples
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/.idea.Linkding.Sync/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Dockerfile_Linkding:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base
2 | WORKDIR /app
3 |
4 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
5 | WORKDIR /src
6 | COPY ["src/Linkding/Linkding.csproj", "src/Linkding/"]
7 | RUN dotnet restore "src/Linkding/Linkding.csproj"
8 | COPY . .
9 | WORKDIR "/src/src/Linkding"
10 | RUN dotnet build "Linkding.csproj" -c Release -o /app/build
11 |
12 | FROM build AS publish
13 | RUN dotnet publish "Linkding.csproj" -c Release -o /app/publish
14 |
15 | FROM base AS final
16 | WORKDIR /app
17 | COPY --from=publish /app/publish .
18 | RUN mkdir ./data
19 | ENTRYPOINT ["dotnet", "Linkding.dll"]
20 |
--------------------------------------------------------------------------------
/Dockerfile_Wallabag:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base
2 | WORKDIR /app
3 |
4 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
5 | WORKDIR /src
6 | COPY ["src/Wallabag/Wallabag.csproj", "src/Wallabag/"]
7 | RUN dotnet restore "src/Wallabag/Wallabag.csproj"
8 | COPY . .
9 | WORKDIR "/src/src/Wallabag"
10 | RUN dotnet build "Wallabag.csproj" -c Release -o /app/build
11 |
12 | FROM build AS publish
13 | RUN dotnet publish "Wallabag.csproj" -c Release -o /app/publish
14 |
15 | FROM base AS final
16 | WORKDIR /app
17 | COPY --from=publish /app/publish .
18 | RUN mkdir ./data
19 | ENTRYPOINT ["dotnet", "Wallabag.dll"]
20 |
--------------------------------------------------------------------------------
/Linkding.Sync.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3167CB7E-6412-49DB-BB42-E91E07B51C5E}"
4 | ProjectSection(SolutionItems) = preProject
5 | README.md = README.md
6 | EndProjectSection
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Services", "Services", "{2775FBD9-7954-4A8B-8831-31C7670209A3}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Linkding.Client", "src\Services\Linkding.Client\Linkding.Client.csproj", "{1880BB32-4013-45F5-9DB4-6864F9836525}"
11 | EndProject
12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wallabag.Client", "src\Services\Wallabag.Client\Wallabag.Client.csproj", "{2776E05F-F35A-4421-A792-0F9B0CC48475}"
13 | EndProject
14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Domain", "Domain", "{586A8F51-1A5D-42B9-8A37-8B2010905EB1}"
15 | EndProject
16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "src\Domain\Core\Core.csproj", "{0EA1E116-6560-4C1D-8EC8-6ACE3F5E1C25}"
17 | EndProject
18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{953FCC4E-C759-47DD-B20A-78FBA2E34F35}"
19 | EndProject
20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wallabag", "src\Wallabag\Wallabag.csproj", "{5EFB26BC-CD34-4DE6-B0E3-9F1E387D4177}"
21 | EndProject
22 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Linkding", "src\Linkding\Linkding.csproj", "{3E78F171-D237-46DF-8A27-DAADE7CA1940}"
23 | EndProject
24 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Items", "Items", "{328C408E-A1CD-4CDC-B13D-A4EC2F8C8494}"
25 | ProjectSection(SolutionItems) = preProject
26 | .dockerignore = .dockerignore
27 | .gitignore = .gitignore
28 | Dockerfile_Linkding = Dockerfile_Linkding
29 | Dockerfile_Wallabag = Dockerfile_Wallabag
30 | EndProjectSection
31 | EndProject
32 | Global
33 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
34 | Debug|Any CPU = Debug|Any CPU
35 | Release|Any CPU = Release|Any CPU
36 | EndGlobalSection
37 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
38 | {1880BB32-4013-45F5-9DB4-6864F9836525}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
39 | {1880BB32-4013-45F5-9DB4-6864F9836525}.Debug|Any CPU.Build.0 = Debug|Any CPU
40 | {1880BB32-4013-45F5-9DB4-6864F9836525}.Release|Any CPU.ActiveCfg = Release|Any CPU
41 | {1880BB32-4013-45F5-9DB4-6864F9836525}.Release|Any CPU.Build.0 = Release|Any CPU
42 | {2776E05F-F35A-4421-A792-0F9B0CC48475}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
43 | {2776E05F-F35A-4421-A792-0F9B0CC48475}.Debug|Any CPU.Build.0 = Debug|Any CPU
44 | {2776E05F-F35A-4421-A792-0F9B0CC48475}.Release|Any CPU.ActiveCfg = Release|Any CPU
45 | {2776E05F-F35A-4421-A792-0F9B0CC48475}.Release|Any CPU.Build.0 = Release|Any CPU
46 | {0EA1E116-6560-4C1D-8EC8-6ACE3F5E1C25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
47 | {0EA1E116-6560-4C1D-8EC8-6ACE3F5E1C25}.Debug|Any CPU.Build.0 = Debug|Any CPU
48 | {0EA1E116-6560-4C1D-8EC8-6ACE3F5E1C25}.Release|Any CPU.ActiveCfg = Release|Any CPU
49 | {0EA1E116-6560-4C1D-8EC8-6ACE3F5E1C25}.Release|Any CPU.Build.0 = Release|Any CPU
50 | {5EFB26BC-CD34-4DE6-B0E3-9F1E387D4177}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
51 | {5EFB26BC-CD34-4DE6-B0E3-9F1E387D4177}.Debug|Any CPU.Build.0 = Debug|Any CPU
52 | {5EFB26BC-CD34-4DE6-B0E3-9F1E387D4177}.Release|Any CPU.ActiveCfg = Release|Any CPU
53 | {5EFB26BC-CD34-4DE6-B0E3-9F1E387D4177}.Release|Any CPU.Build.0 = Release|Any CPU
54 | {3E78F171-D237-46DF-8A27-DAADE7CA1940}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
55 | {3E78F171-D237-46DF-8A27-DAADE7CA1940}.Debug|Any CPU.Build.0 = Debug|Any CPU
56 | {3E78F171-D237-46DF-8A27-DAADE7CA1940}.Release|Any CPU.ActiveCfg = Release|Any CPU
57 | {3E78F171-D237-46DF-8A27-DAADE7CA1940}.Release|Any CPU.Build.0 = Release|Any CPU
58 | EndGlobalSection
59 | GlobalSection(NestedProjects) = preSolution
60 | {2775FBD9-7954-4A8B-8831-31C7670209A3} = {3167CB7E-6412-49DB-BB42-E91E07B51C5E}
61 | {1880BB32-4013-45F5-9DB4-6864F9836525} = {2775FBD9-7954-4A8B-8831-31C7670209A3}
62 | {2776E05F-F35A-4421-A792-0F9B0CC48475} = {2775FBD9-7954-4A8B-8831-31C7670209A3}
63 | {586A8F51-1A5D-42B9-8A37-8B2010905EB1} = {3167CB7E-6412-49DB-BB42-E91E07B51C5E}
64 | {0EA1E116-6560-4C1D-8EC8-6ACE3F5E1C25} = {586A8F51-1A5D-42B9-8A37-8B2010905EB1}
65 | {953FCC4E-C759-47DD-B20A-78FBA2E34F35} = {3167CB7E-6412-49DB-BB42-E91E07B51C5E}
66 | {5EFB26BC-CD34-4DE6-B0E3-9F1E387D4177} = {3167CB7E-6412-49DB-BB42-E91E07B51C5E}
67 | {3E78F171-D237-46DF-8A27-DAADE7CA1940} = {3167CB7E-6412-49DB-BB42-E91E07B51C5E}
68 | EndGlobalSection
69 | EndGlobal
70 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Linkding Sync
2 | LinkdingSync is a collection of tools that make life with [Linkding](https://github.com/sissbruecker/linkding) easier.
3 |
4 | One of the workers is for syncing to [Wallabag](https://wallabag.org/en).
5 |
6 | ## Getting Started
7 | It is recommended to use the Docker images. Otherwise, a .NET 8 environment is required to customize and build the code.
8 |
9 | ## Environment Variables
10 | For the containers to work, the environment variables must be passed. This can be done either directly via the Docker run **-e** switch, via the **environment** settings in a Docker compose definition, or via an environment variable file.
11 |
12 | ### WallabagSync
13 | Environment variables for the wallabag worker.
14 |
15 | | Variable | Value | Description | Attention |
16 | |------------------------|-----------|--------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
17 | | Worker__Interval | int (>=0) | This value sets the execution schedule in minutes. 1 = every minute, 10 = every 10 minutes (default value 0) | 0 = runs only one time. The container will be stopped after the execution. This method is the preferred way to run the container with a scheduler (e.g. cron) |
18 | | Worker__SyncTag | text | The linkding tag to create the bookmarks in Wallabag. (default value 'readlater') | |
19 | | Linkding__Url | text | URL to the linkding instance | |
20 | | Linkding__Key | text | The linkding application key | [Instructions](https://github.com/sissbruecker/linkding/blob/master/docs/API.md) |
21 | | Wallabag__Url | text | URL to the Wallabag instance | |
22 | | Wallabag__Username | text | Wallabag User Name | |
23 | | Wallabag__Password | text | Wallabag User Password | |
24 | | Wallabag__ClientId | text | Wallabag Client Id | |
25 | | Wallabag__ClientSecret | text | Wallabag Client Secret | |
26 |
27 | ### LinkdingUpdater
28 | Environment variables for the linkding worker.
29 |
30 | | Variable | Value | Description | Attention |
31 | |-------------------------------|-----------|--------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
32 | | Worker__Interval | int (>=0) | This value sets the execution schedule in minutes. 1 = every minute, 10 = every 10 minutes | 0 = runs only one time. The container will be stopped after the execution. This method is the preferred way to run the container with a scheduler (e.g. cron) |
33 | | Worker__Worker__TagNameLength | int | The max tag name length. Default is 64 characters | |
34 | | Worker__Tasks__X | text | The tasks which should be executed. X is the array index for the tasks entry | currently supported tasks: AddPopularSitesAsTag,AddYearToBookmark. |
35 | | Linkding__Url | text | URL to the linkding instance | |
36 | | Linkding__Key | text | The linkding application key | [Instructions](https://github.com/sissbruecker/linkding/blob/master/docs/API.md) |
37 |
38 | #### Tasks
39 | Tasks define the logic that directly make changes to the bookmarks.
40 | Currently available tasks:
41 |
42 | | Name | Description |
43 | |-----------------------|------------------------------------------------------------------------------------------------------------------------------------|
44 | | AddYearToBookmark | If the bookmark does not have the year of the creation date as a tag, it will be added. |
45 | | AddPopularSitesAsTag | This is the task for dynamic creation of tags. For this task to work, the rules must be passed to the container as configuration. |
46 |
47 | The tasks are defined as environment variables, if no tasks are defined, the container will be executed but no changes will be made.
48 | The tasks are passed as follows:
49 | ```yaml
50 | version: '3.9'
51 |
52 | services:
53 | linkdingupdater:
54 | image: ghcr.io/spaytac/linkding-updater:latest
55 | volumes:
56 | - ./config.yml:/app/data/config.yml
57 | # env_file:
58 | # - .env
59 | environment:
60 | - Worker__Interval=0
61 | - Worker__TagNameLength=64
62 | - Worker__Tasks__0=AddPopularSitesAsTag
63 | - Worker__Tasks__1=AddYearToBookmark
64 | - Linkding__Url=https://
65 | - Linkding__Key=
66 |
67 |
68 | ```
69 |
70 | ## Configuration
71 | The following explains the configuration options. The Configuration file must be mapped to **/app/data/config.yml**
72 | ### WallabagSync
73 | The configuration is optional. In the configuration (**YAML File**) rules can be defined in regex to exclude certain domains from sync.
74 |
75 | Exampel:
76 | ```yaml
77 | excludedDomains:
78 | - name: youtube
79 | pattern: 'https://[[a-zA-Z0-9]+\.]?(youtube)\.com(?:/.*)?'
80 | - name: ebay
81 | pattern: 'https://[[a-zA-Z0-9]+\.]?(ebay)\.(com|de|fr)(?:/.*)?'
82 | - name: amazon
83 | pattern: 'https://[[a-zA-Z0-9]+\.]?(amazon)\.(com|de|fr)(?:/.*)?'
84 | ```
85 | With this configuration every matching bookmark from linkding will be excluded from the sync.
86 |
87 | ### LinkdingUpdater
88 | The configuration is optional. In the configuration (**YAML File**) rules can be defined in regex to assign tags dynamically. Additionally tags can be defined to domains.
89 |
90 | If operated without a configuration file, only the year of the tag is added (currently).
91 |
92 | Example:
93 | ```yaml
94 | urlTagMapping:
95 | - name: microsoft_azure
96 | url: https://github.com/azure
97 | - name: microsoft_azuread
98 | url: https://github.com/AzureAD
99 | - name: microsoft_dotnet
100 | url: https://github.com/dotnet-architecture
101 |
102 | taggingRule:
103 | - name: reddit
104 | pattern: https://(?:www\.)?(reddit)\.com(?:/r/)?([a-zA-Z0-9\-\+_]+)?(?:/.*)?
105 | replace: $1,$2
106 | - name: microsoft
107 | pattern: https://([a-zA-Z0-9]+)?[\.]?(microsoft)\.com(?:/.*)?
108 | replace: $1,$2
109 | - name: microsoft_docs
110 | pattern: 'https://(?:docs)\.(?:microsoft)\.com[/]?(?: [a-zA-Z0-9\-\+_]+)(?:/)?([a-zA-Z0-9\-\+_]+)?(?:/)?([a-zA-Z0-9\-\+_]+)?(?:/.*)?'
111 | replace: $1,$2
112 | - name: youtube
113 | pattern: https://[[a-zA-Z0-9]+\.]?(youtube)\.com(?:/.*)?
114 | replace: $1
115 | - name: ebay
116 | pattern: https://[[a-zA-Z0-9]+\.]?(ebay)\.(com|de|fr)(?:/.*)?
117 | replace: $1
118 | - name: amazon
119 | pattern: https://[[a-zA-Z0-9]+\.]?(amazon)\.(com|de|fr)(?:/.*)?
120 | replace: $1
121 | - name: docker
122 | pattern: https://([a-zA-Z0-9]+)?[\.]?(docker)\.com(?:/.*)?
123 | replace: $1,$2
124 | - name: xbox
125 | pattern: https://[[a-zA-Z0-9]+\.]?(xbox)\.com(?:/.*)?
126 | replace: $1
127 | - name: github
128 | pattern: https://([a-zA-Z0-9]+)?[\.]?(github)\.com[/]?([a-zA-Z0-9\-\+_]+)(?:/)?([a-zA-Z0-9\-\+_]+)?(?:/.*)?
129 | replace: $1,$2,$3,$4
130 | - name: github.io
131 | pattern: https://([a-zA-Z0-9]+)\.(github)\.io[/]?([a-zA-Z0-9\-\+_]+)(?:/)?([a-zA-Z0-9\-\+_]+)?(?:/.*)?
132 | replace: $1,$2,$3
133 | ```
134 |
135 | #### urlTagMapping
136 | If the bookmark should match one - or more - of the urlTagMappings, then value of name is added as tag to this bookmark.
137 |
138 | Example:
139 | https://github.com/azure/something will be tagged with microsoft_azure
140 |
141 | #### taggingRule
142 | Dynamic tags can be assigned to the bookmarks on the basis of the URL using regular expression. If one of the patterns matches, the values of the groups are added to the bookmark as a tag.
143 |
144 | Example:
145 | Here is an example using a reddit bookmark.
146 | Url:
147 | ```
148 | https://www.reddit.com/r/selfhosted/comments/yzq6qp/running_a_mostly_sbcbased_nomad_cluster_in_my/?utm_source=share&utm_medium=android_app&utm_name=androidcss&utm_term=2&utm_content=share_button
149 | ```
150 | Pattern:
151 | ```regex
152 | https://(?:www\.)?(reddit)\.com(?:/r/)?([a-zA-Z0-9\-\+_]+)?(?:/.*)?
153 | ```
154 | Matches:
155 | - https://www.$${\color{red}reddit}$$.com/r/$${\color{red}selfhosted}$$/comments/yzq6qp/running_a_mostly_sbcbased_nomad_cluster_in_my/?utm_source=share&utm_medium=android_app&utm_name=androidcss&utm_term=2&utm_content=share_button
156 |
157 | if you would change the pattern to the following.
158 | Pattern:
159 | ```regex
160 | https://(?:www\.)?(reddit)\.com(?:/)?(r/[a-zA-Z0-9\-\+_]+)?(?:/.*)?
161 | ```
162 |
163 | then the following would match.
164 | - https://www.$${\color{red}reddit}$$.com/$${\color{red}r/selfhosted}$$/comments/yzq6qp/running_a_mostly_sbcbased_nomad_cluster_in_my/?utm_source=share&utm_medium=android_app&utm_name=androidcss&utm_term=2&utm_content=share_button
165 |
166 | ## Docker Run
167 | ```bash
168 | docker run --rm -it --env-file .env -v /config.yml:/app/data/config.yml ghcr.io/spaytac/linkding-updater:latest
169 | ```
170 |
171 | ```bash
172 | docker run --rm -it --env-file .env -v /config.yml:/app/data/config.yml ghcr.io/spaytac/wallabag-sync:latest
173 | ```
174 |
175 | ## Docker Compose
176 | You can find [examples](./examples/) in the examples folder..
177 |
178 | - [WallabagSync Example](./examples/wallabag/)
179 | - [LinkdingUpdater Example](./examples/linkding/)
180 |
181 |
182 | ## Build Docker Images
183 |
184 | ### LinkdingUpdater
185 | ```
186 | docker build -t linkdingsync/linkding-updater:latest -f .\Dockerfile_Linkding .
187 | ```
188 |
189 | ### WallabagSync
190 | ```
191 | docker build -t linkdingsync/wallabag-sync:latest -f .\Dockerfile_Wallabag .
192 | ```
--------------------------------------------------------------------------------
/examples/linkding/config.yml:
--------------------------------------------------------------------------------
1 | urlTagMapping:
2 | - name: microsoft
3 | url: https://github.com/azure
4 | - name: microsoft
5 | url: https://github.com/AzureAD
6 | - name: microsoft
7 | url: https://github.com/dotnet-architecture
8 |
9 | taggingRule:
10 | - name: reddit
11 | pattern: https://(?:www\.)?(reddit)\.com(?:/r/)?([a-zA-Z0-9\-\+_]+)?(?:/.*)?
12 | replace: $1,$2
13 | - name: microsoft
14 | pattern: https://([a-zA-Z0-9]+)?[\.]?(microsoft)\.com(?:/.*)?
15 | replace: $1,$2
16 | - name: microsoft_docs
17 | pattern: 'https://(?:docs)\.(?:microsoft)\.com[/]?(?: [a-zA-Z0-9\-\+_]+)(?:/)?([a-zA-Z0-9\-\+_]+)?(?:/)?([a-zA-Z0-9\-\+_]+)?(?:/.*)?'
18 | replace: $1,$2
19 | - name: youtube
20 | pattern: https://[[a-zA-Z0-9]+\.]?(youtube)\.com(?:/.*)?
21 | replace: $1
22 | - name: ebay
23 | pattern: https://[[a-zA-Z0-9]+\.]?(ebay)\.(com|de|fr)(?:/.*)?
24 | replace: $1
25 | - name: amazon
26 | pattern: https://[[a-zA-Z0-9]+\.]?(amazon)\.(com|de|fr)(?:/.*)?
27 | replace: $1
28 | - name: docker
29 | pattern: https://([a-zA-Z0-9]+)?[\.]?(docker)\.com(?:/.*)?
30 | replace: $1,$2
31 | - name: xbox
32 | pattern: https://[[a-zA-Z0-9]+\.]?(xbox)\.com(?:/.*)?
33 | replace: $1
34 | - name: github
35 | pattern: https://([ a-zA-Z0-9 ]+)?[ \. ]?(github)\.com[ / ]?([ a-zA-Z0-9\-\+_ ]+)(?:/)?([ a-zA-Z0-9\-\+_ ]+)?(?:/.*)?
36 | replace: $2,$3,$4
37 | - name: github.io
38 | pattern: https://([ a-zA-Z0-9 ]+)\.(github)\.io[ / ]?([ a-zA-Z0-9\-\+_ ]+)(?:/)?([ a-zA-Z0-9\-\+_ ]+)?(?:/.*)?
39 | replace: $1,$2,$3
--------------------------------------------------------------------------------
/examples/linkding/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.9'
2 |
3 | services:
4 | linkdingupdater:
5 | image: ghcr.io/spaytac/linkding-updater:latest
6 | volumes:
7 | - ./config.yml:/app/data/config.yml
8 | # env_file:
9 | # - .env
10 | environment:
11 | - Worker__Interval=0
12 | - Worker__TagNameLength=64
13 | - Worker__Tasks__0=AddPopularSitesAsTag
14 | - Worker__Tasks__1=AddYearToBookmark
15 | - Linkding__Url=https://
16 | - Linkding__Key=
17 |
--------------------------------------------------------------------------------
/examples/wallabag/config.yml:
--------------------------------------------------------------------------------
1 | excludedDomains:
2 | - name: youtube
3 | pattern: 'https://[[a-zA-Z0-9]+\.]?(youtube)\.com(?:/.*)?'
4 | - name: ebay
5 | pattern: 'https://[[a-zA-Z0-9]+\.]?(ebay)\.(com|de|fr)(?:/.*)?'
6 | - name: amazon
7 | pattern: 'https://[[a-zA-Z0-9]+\.]?(amazon)\.(com|de|fr)(?:/.*)?'
--------------------------------------------------------------------------------
/examples/wallabag/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.9'
2 |
3 | services:
4 | wallabagsync:
5 | image: ghcr.io/spaytac/wallabag-sync:latest
6 | volumes:
7 | - ./config.yml:/app/data/config.yml
8 | # env_file:
9 | # - .env
10 | environment:
11 | - Worker__Interval=0
12 | - Worker__SyncTag=
13 | - Linkding__Url=https://
14 | - Linkding__Key=
15 | - Wallabag__Url=https://
16 | - Wallabag__Username=
17 | - Wallabag__Password=
18 | - Wallabag__ClientId=
19 | - Wallabag__ClientSecret=
20 |
--------------------------------------------------------------------------------
/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdk": {
3 | "version": "8.0.0",
4 | "rollForward": "latestMinor",
5 | "allowPrerelease": false
6 | }
7 | }
--------------------------------------------------------------------------------
/src/Domain/Core/Abstraction/ILinkdingService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Threading.Tasks;
3 | using Core.Entities.Linkding;
4 |
5 | namespace Linkding.Client
6 | {
7 | public interface ILinkdingService
8 | {
9 | Task> GetBookmarksAsync(int limit = 100, int offset = 0);
10 | Task> GetAllBookmarksAsync();
11 | Task AddBookmarkCollectionAsync(IEnumerable bookmarks);
12 | Task AddBookmarkCollectionAsync(IEnumerable bookmarks);
13 | Task AddBookmarkAsync(BookmarkCreatePayload bookmark);
14 |
15 | Task UpdateBookmarkCollectionAsync(IEnumerable bookmarks);
16 | Task UpdateBookmarkAsync(int id, BookmarkUpdatePayload bookmark);
17 | Task GetBookmarkResultsAsync(int limit = 100, int offset = 0);
18 | Task GetBookmarkResultsAsync(string url);
19 | }
20 | }
--------------------------------------------------------------------------------
/src/Domain/Core/Abstraction/IWallabagService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Net.Http;
3 | using System.Net.Http.Headers;
4 | using System.Threading.Tasks;
5 | using Core.Entities.Wallabag;
6 |
7 | namespace Core.Abstraction
8 | {
9 | public interface IWallabagService
10 | {
11 | Task GetAuthenticationHeaderAsync(IEnumerable scopes = null);
12 |
13 | Task GetAsync(string endpoint, IEnumerable scopes = null,
14 | bool httpCompletionResponseContentRead = false);
15 |
16 | Task GetJsonAsync(string endpoint, IEnumerable scopes = null);
17 |
18 | Task PostWithFormDataAsnyc(string endpoint, FormUrlEncodedContent content,
19 | IEnumerable scopes = null);
20 |
21 | Task PostAsync(string endpoint, HttpContent content,
22 | IEnumerable scopes = null);
23 |
24 | Task PostAsync(string endpoint, HttpRequestMessage request,
25 | IEnumerable scopes = null);
26 |
27 | Task PutAsync(string endpoint, HttpContent content,
28 | IEnumerable scopes = null);
29 |
30 | Task DeleteAsync(string endpoint, IEnumerable scopes = null,
31 | HttpContent content = null);
32 |
33 | Task> GetEntries(string format = "json", int limit = 50, bool full = false);
34 | Task GetEntryById(int id, string format = "json");
35 | Task AddEntryByUrl(string url, IEnumerable tags = null, string format = "json");
36 | }
37 | }
38 |
39 |
--------------------------------------------------------------------------------
/src/Domain/Core/Converters/DateTimeConverterForCustomStandard.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.Text.Json;
4 | using System.Text.Json.Serialization;
5 |
6 | namespace Wallabag.Client.Converters
7 | {
8 | public class DateTimeConverterForCustomStandard : JsonConverter
9 | {
10 | public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
11 | {
12 | var dateTimeString = reader.GetString();
13 |
14 | if (string.IsNullOrEmpty(dateTimeString))
15 | {
16 | return DateTime.MinValue;
17 | }
18 | DateTime dt = DateTime.ParseExact(dateTimeString, "yyyy-MM-dd'T'HH:mm:ssK",
19 | CultureInfo.InvariantCulture,
20 | DateTimeStyles.AdjustToUniversal);
21 |
22 | return dt;
23 | }
24 |
25 | public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
26 | {
27 | writer.WriteStringValue(value.ToString());
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/src/Domain/Core/Converters/DateTimeOffsetConverterUsingDateTimeParse.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text.Json;
3 | using System.Text.Json.Serialization;
4 |
5 | namespace Wallabag.Client.Converters
6 | {
7 | public class DateTimeOffsetConverterUsingDateTimeParse : JsonConverter
8 | {
9 | public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
10 | {
11 | var dateTimeString = reader.GetString();
12 | return DateTimeOffset.Parse(dateTimeString);
13 | }
14 |
15 | public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
16 | {
17 | writer.WriteStringValue(value.ToString());
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/src/Domain/Core/Core.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.1
5 | enable
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/Domain/Core/Entities/Linkding/Bookmark.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text.Json.Serialization;
3 |
4 | namespace Core.Entities.Linkding
5 | {
6 | public class Bookmark : BookmarkBase
7 | {
8 | [JsonPropertyName("id")]
9 | public int Id { get; set; }
10 |
11 | [JsonPropertyName("website_title")]
12 | public string WebsiteTitle { get; set; }
13 | [JsonPropertyName("website_description")]
14 | public string WebsiteDescription { get; set; }
15 | [JsonPropertyName("is_archived")]
16 | public bool IsArchived { get; set; }
17 | [JsonPropertyName("unread")]
18 | public bool Unread { get; set; }
19 | [JsonPropertyName("date_added")]
20 | public DateTime DateAdded { get; set; }
21 | [JsonPropertyName("date_modified")]
22 | public DateTime DateModified { get; set; }
23 | }
24 | }
25 |
26 | // "id": 1,
27 | // "url": "https://example.com",
28 | // "title": "Example title",
29 | // "description": "Example description",
30 | // "website_title": "Website title",
31 | // "website_description": "Website description",
32 | // "is_archived": false,
33 | // "unread": false,
34 | // "tag_names": [
35 | // "tag1",
36 | // "tag2"
37 | // ],
38 | // "date_added": "2020-09-26T09:46:23.006313Z",
39 | // "date_modified": "2020-09-26T16:01:14.275335Z"
--------------------------------------------------------------------------------
/src/Domain/Core/Entities/Linkding/BookmarkBase.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Text.Json.Serialization;
3 |
4 | namespace Core.Entities.Linkding
5 | {
6 | public class BookmarkBase
7 | {
8 | [JsonPropertyName("url")]
9 | public string Url { get; set; }
10 | [JsonPropertyName("title")]
11 | public string Title { get; set; }
12 | [JsonPropertyName("description")]
13 | public string Description { get; set; }
14 | [JsonPropertyName("tag_names")]
15 | public IEnumerable TagNames { get; set; } = new List();
16 | }
17 | }
--------------------------------------------------------------------------------
/src/Domain/Core/Entities/Linkding/BookmarkCreatePayload.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace Core.Entities.Linkding
4 | {
5 | public class BookmarkCreatePayload : BookmarkBase
6 | {
7 | [JsonPropertyName("is_archived")]
8 | public bool IsArchived { get; set; } = false;
9 |
10 | [JsonPropertyName("unread")] public bool Unread { get; set; } = false;
11 | }
12 | }
--------------------------------------------------------------------------------
/src/Domain/Core/Entities/Linkding/BookmarkUpdatePayload.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace Core.Entities.Linkding
4 | {
5 | public class BookmarkUpdatePayload : BookmarkBase
6 | {
7 | // [JsonPropertyName("id")]
8 | // public int Id { get; set; }
9 | }
10 | }
--------------------------------------------------------------------------------
/src/Domain/Core/Entities/Linkding/BookmarkUpdateResult.cs:
--------------------------------------------------------------------------------
1 | namespace Core.Entities.Linkding
2 | {
3 | public class BookmarkUpdateResult : BookmarkUpdatePayload
4 | {
5 | public bool Success { get; set; } = true;
6 | }
7 | }
--------------------------------------------------------------------------------
/src/Domain/Core/Entities/Linkding/BookmarksResult.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace Core.Entities.Linkding
4 | {
5 | public class BookmarksResult
6 | {
7 | public long Count { get; set; }
8 | public string? Next { get; set; }
9 | public string? Previous { get; set; }
10 | public List Results { get; set; }
11 | }
12 | }
--------------------------------------------------------------------------------
/src/Domain/Core/Entities/Linkding/HandlerResult.cs:
--------------------------------------------------------------------------------
1 | using Core.Entities.Linkding;
2 |
3 | namespace LinkdingUpdater.Handler
4 | {
5 | public class HandlerResult
6 | {
7 | public bool PerformAction { get; set; } = false;
8 |
9 | public LinkdingItemAction Action { get; set; }
10 | public bool HasError { get; set; } = false;
11 | public string ErrorMessage { get; set; } = string.Empty;
12 | public Bookmark Instance { get; set; }
13 | }
14 | }
15 |
16 | public enum LinkdingItemAction
17 | {
18 | Update,
19 | Delete
20 | }
--------------------------------------------------------------------------------
/src/Domain/Core/Entities/Linkding/Tag.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text.Json.Serialization;
3 |
4 | namespace Core.Entities.Linkding
5 | {
6 | public class Tag
7 | {
8 | [JsonPropertyName("id")]
9 | public int Id { get; set; }
10 | [JsonPropertyName("name")]
11 | public string Name { get; set; }
12 | [JsonPropertyName("date_added")]
13 | public DateTime DateAdded { get; set; }
14 | }
15 | }
--------------------------------------------------------------------------------
/src/Domain/Core/Entities/Linkding/TagCreatePayload.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace Core.Entities.Linkding
4 | {
5 | public class TagCreatePayload
6 | {
7 | [JsonPropertyName("name")]
8 | public string Name { get; set; }
9 | }
10 | }
--------------------------------------------------------------------------------
/src/Domain/Core/Entities/Wallabag/Annotation.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text.Json.Serialization;
4 | using Wallabag.Client.Converters;
5 |
6 | namespace Core.Entities.Wallabag
7 | {
8 | public class Annotation
9 | {
10 | [JsonPropertyName("user")] public string User { get; set; }
11 |
12 | [JsonPropertyName("annotator_schema_version")]
13 | public string AnnotatorSchemaVersion { get; set; }
14 |
15 | [JsonPropertyName("id")] public int Id { get; set; }
16 |
17 | [JsonPropertyName("text")] public string Text { get; set; }
18 |
19 |
20 | [JsonConverter(typeof(DateTimeConverterForCustomStandard))]
21 | [JsonPropertyName("created_at")] public DateTime? CreatedAt { get; set; } = null;
22 |
23 | [JsonConverter(typeof(DateTimeConverterForCustomStandard))]
24 | [JsonPropertyName("updated_at")] public DateTime? UpdatedAt { get; set; } = null;
25 |
26 | [JsonPropertyName("quote")] public string Quote { get; set; }
27 |
28 | [JsonPropertyName("ranges")] public List Ranges { get; set; }
29 | }
30 | }
--------------------------------------------------------------------------------
/src/Domain/Core/Entities/Wallabag/Embedded.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Text.Json.Serialization;
3 |
4 | namespace Core.Entities.Wallabag
5 | {
6 | public class Embedded
7 | {
8 | [JsonPropertyName("items")]
9 | public List Items { get; set; }
10 | }
11 | }
--------------------------------------------------------------------------------
/src/Domain/Core/Entities/Wallabag/Headers.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace Core.Entities.Wallabag
4 | {
5 | public class Headers
6 | {
7 | [JsonPropertyName("server")] public string Server { get; set; }
8 |
9 | [JsonPropertyName("date")] public string Date { get; set; }
10 |
11 | [JsonPropertyName("content-type")] public string ContentType { get; set; }
12 |
13 | [JsonPropertyName("content-length")] public string ContentLength { get; set; }
14 |
15 | [JsonPropertyName("connection")] public string Connection { get; set; }
16 |
17 | [JsonPropertyName("x-powered-by")] public string XPoweredBy { get; set; }
18 |
19 | [JsonPropertyName("cache-control")] public string CacheControl { get; set; }
20 |
21 | [JsonPropertyName("etag")] public string Etag { get; set; }
22 |
23 | [JsonPropertyName("vary")] public string Vary { get; set; }
24 |
25 | [JsonPropertyName("strict-transport-security")]
26 | public string StrictTransportSecurity { get; set; }
27 | }
28 | }
--------------------------------------------------------------------------------
/src/Domain/Core/Entities/Wallabag/HttpLink.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace Core.Entities.Wallabag
4 | {
5 | public class HttpLink
6 | {
7 | [JsonPropertyName("href")] public string Href { get; set; }
8 | }
9 | }
--------------------------------------------------------------------------------
/src/Domain/Core/Entities/Wallabag/Links.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace Core.Entities.Wallabag
4 | {
5 | public class Links
6 | {
7 | [JsonPropertyName("self")] public HttpLink? Self { get; set; } = null;
8 | }
9 | }
--------------------------------------------------------------------------------
/src/Domain/Core/Entities/Wallabag/QueryLinks.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace Core.Entities.Wallabag
4 | {
5 | public class QueryLinks : Links
6 | {
7 | [JsonPropertyName("first")] public HttpLink? First { get; set; } = null;
8 |
9 | [JsonPropertyName("last")] public HttpLink? Last { get; set; } = null;
10 |
11 | [JsonPropertyName("next")] public HttpLink? Next { get; set; } = null;
12 | }
13 | }
--------------------------------------------------------------------------------
/src/Domain/Core/Entities/Wallabag/Range.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace Core.Entities.Wallabag
4 | {
5 | public class Range
6 | {
7 | [JsonPropertyName("start")] public string Start { get; set; }
8 |
9 | [JsonPropertyName("startOffset")] public string StartOffset { get; set; }
10 |
11 | [JsonPropertyName("end")] public string End { get; set; }
12 |
13 | [JsonPropertyName("endOffset")] public string EndOffset { get; set; }
14 | }
15 | }
--------------------------------------------------------------------------------
/src/Domain/Core/Entities/Wallabag/Tag.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace Core.Entities.Wallabag
4 | {
5 | public class Tag
6 | {
7 | [JsonPropertyName("id")] public int Id { get; set; }
8 |
9 | [JsonPropertyName("label")] public string Label { get; set; }
10 |
11 | [JsonPropertyName("slug")] public string Slug { get; set; }
12 | }
13 | }
--------------------------------------------------------------------------------
/src/Domain/Core/Entities/Wallabag/WallabagItem.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text.Json.Serialization;
4 | using Wallabag.Client.Converters;
5 |
6 | namespace Core.Entities.Wallabag
7 | {
8 | public class WallabagItem
9 | {
10 | [JsonPropertyName("is_archived")] public int IsArchived { get; set; }
11 |
12 | [JsonPropertyName("is_starred")] public int IsStarred { get; set; }
13 |
14 | [JsonPropertyName("user_name")] public string UserName { get; set; }
15 |
16 | [JsonPropertyName("user_email")] public string UserEmail { get; set; }
17 |
18 | [JsonPropertyName("user_id")] public int UserId { get; set; }
19 |
20 | [JsonPropertyName("tags")] public List Tags { get; set; }
21 |
22 | [JsonPropertyName("is_public")] public bool IsPublic { get; set; }
23 |
24 | [JsonPropertyName("id")] public int Id { get; set; }
25 |
26 | [JsonPropertyName("uid")] public string? Uid { get; set; } = null;
27 |
28 | [JsonPropertyName("title")] public string Title { get; set; }
29 |
30 | [JsonPropertyName("url")] public string Url { get; set; }
31 |
32 | [JsonPropertyName("hashed_url")] public string HashedUrl { get; set; }
33 |
34 | [JsonPropertyName("origin_url")] public string? OriginUrl { get; set; } = null;
35 |
36 | [JsonPropertyName("given_url")] public string GivenUrl { get; set; }
37 |
38 | [JsonPropertyName("hashed_given_url")] public string HashedGivenUrl { get; set; }
39 |
40 | [JsonConverter(typeof(DateTimeConverterForCustomStandard))]
41 | [JsonPropertyName("archived_at")] public DateTime? ArchivedAt { get; set; } = null;
42 |
43 | [JsonPropertyName("content")] public string? Content { get; set; } = null;
44 |
45 | [JsonConverter(typeof(DateTimeConverterForCustomStandard))]
46 | [JsonPropertyName("created_at")] public DateTime CreatedAt { get; set; }
47 |
48 | [JsonConverter(typeof(DateTimeConverterForCustomStandard))]
49 | [JsonPropertyName("updated_at")] public DateTime UpdatedAt { get; set; }
50 |
51 | [JsonConverter(typeof(DateTimeConverterForCustomStandard))]
52 | [JsonPropertyName("published_at")] public DateTime PublishedAt { get; set; }
53 |
54 | [JsonPropertyName("published_by")] public List PublishedBy { get; set; }
55 |
56 | [JsonConverter(typeof(DateTimeConverterForCustomStandard))]
57 | [JsonPropertyName("starred_at")] public DateTime? StarredAt { get; set; } = null;
58 |
59 | [JsonPropertyName("annotations")] public List Annotations { get; set; }
60 |
61 | [JsonPropertyName("mimetype")] public string Mimetype { get; set; }
62 |
63 | [JsonPropertyName("language")] public string Language { get; set; }
64 |
65 | [JsonPropertyName("reading_time")] public int ReadingTime { get; set; }
66 |
67 | [JsonPropertyName("domain_name")] public string DomainName { get; set; }
68 |
69 | [JsonPropertyName("preview_picture")] public string PreviewPicture { get; set; }
70 |
71 | [JsonPropertyName("http_status")] public string HttpStatus { get; set; }
72 |
73 | [JsonPropertyName("headers")] public Headers Headers { get; set; }
74 |
75 | [JsonPropertyName("_links")] public Links Links { get; set; }
76 | }
77 | }
--------------------------------------------------------------------------------
/src/Domain/Core/Entities/Wallabag/WallabagQuery.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace Core.Entities.Wallabag
4 | {
5 | public class WallabagQuery
6 | {
7 | [JsonPropertyName("page")]
8 | public int Page { get; set; }
9 |
10 | [JsonPropertyName("limit")]
11 | public int Limit { get; set; }
12 |
13 | [JsonPropertyName("pages")]
14 | public int Pages { get; set; }
15 |
16 | [JsonPropertyName("total")]
17 | public int Total { get; set; }
18 |
19 | [JsonPropertyName("_links")]
20 | public QueryLinks QueryLinks { get; set; }
21 |
22 | [JsonPropertyName("_embedded")]
23 | public Embedded Embedded { get; set; }
24 | }
25 | }
--------------------------------------------------------------------------------
/src/Domain/Core/Handler/ILinkdingSyncTaskHandler.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Threading.Tasks;
3 | using Core.Entities.Linkding;
4 | using Linkding.Client;
5 | using Microsoft.Extensions.Configuration;
6 | using Microsoft.Extensions.Logging;
7 |
8 | namespace Core.Handler
9 | {
10 | public interface ILinkdingSyncTaskHandler
11 | {
12 | string Command { get; }
13 | Task ProcessAsync(IEnumerable bookmarks, ILinkdingService linkdingService, ILogger logger, IConfiguration configuration);
14 | }
15 | }
--------------------------------------------------------------------------------
/src/Domain/Core/Handler/ILinkdingTagTaskHandler.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Core.Entities.Linkding;
3 | using LinkdingUpdater.Handler;
4 | using Microsoft.Extensions.Configuration;
5 | using Microsoft.Extensions.Logging;
6 |
7 | namespace Core.Handler
8 | {
9 | public interface ILinkdingTagTaskHandler
10 | {
11 | string Command { get; }
12 | Task ProcessAsync(Tag tag, ILogger logger, IConfiguration configuration);
13 | }
14 | }
--------------------------------------------------------------------------------
/src/Domain/Core/Handler/ILinkdingTaskHandler.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Core.Entities.Linkding;
3 | using LinkdingUpdater.Handler;
4 | using Microsoft.Extensions.Configuration;
5 | using Microsoft.Extensions.Logging;
6 |
7 | namespace Core.Handler
8 | {
9 | public interface ILinkdingTaskHandler
10 | {
11 | string Command { get; }
12 | Task ProcessAsync(Bookmark bookmark, ILogger logger, IConfiguration configuration);
13 | }
14 | }
--------------------------------------------------------------------------------
/src/Domain/Core/Handler/ISyncTaskHandler.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using Core.Entities.Wallabag;
5 | using Linkding.Client;
6 | using Microsoft.Extensions.Configuration;
7 | using Microsoft.Extensions.Logging;
8 |
9 | namespace Core.Handler
10 | {
11 | public interface ISyncTaskHandler
12 | {
13 | Type HandlerType { get; }
14 | string Command { get; }
15 |
16 | Task ProcessAsync(IEnumerable items, T destinationService, ILinkdingService linkdingService, ILogger logger, IConfiguration configuration);
17 | }
18 | }
--------------------------------------------------------------------------------
/src/Linkding/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/runtime:6.0 AS base
2 | WORKDIR /app
3 |
4 | FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
5 | WORKDIR /src
6 | COPY ["src/LinkdingService/LinkdingService.csproj", "src/LinkdingService/"]
7 | RUN dotnet restore "src/LinkdingService/LinkdingService.csproj"
8 | COPY . .
9 | WORKDIR "/src/src/LinkdingService"
10 | RUN dotnet build "LinkdingService.csproj" -c Release -o /app/build
11 |
12 | FROM build AS publish
13 | RUN dotnet publish "LinkdingService.csproj" -c Release -o /app/publish
14 |
15 | FROM base AS final
16 | WORKDIR /app
17 | COPY --from=publish /app/publish .
18 | RUN mkdir ./data
19 | ENTRYPOINT ["dotnet", "LinkdingService.dll"]
20 |
--------------------------------------------------------------------------------
/src/Linkding/Extensions/ServiceRegistrationExtensions.cs:
--------------------------------------------------------------------------------
1 | using Linkding.Options;
2 | using Linkding.Settings;
3 |
4 | // ReSharper disable once CheckNamespace
5 | namespace Microsoft.Extensions.DependencyInjection;
6 |
7 | public static class ServiceRegistrationExtensions
8 | {
9 | public static IServiceCollection Add_Linkding_Worker(this IServiceCollection services,
10 | IConfiguration configuration)
11 | {
12 | var configSection = configuration.GetSection(WorkerSettings.Position);
13 | services.Configure(configSection);
14 | services.AddSingleton();
15 |
16 | return services;
17 | }
18 | }
--------------------------------------------------------------------------------
/src/Linkding/Extensions/StringExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace Microsoft.Extensions.DependencyInjection;
2 |
3 | public static class StringExtensions
4 | {
5 | public static string NormalizeTag(this string tag, int maxTagLength = 64)
6 | {
7 | //in the database only 64 characters are allowed for tags
8 | if (tag.Length >= maxTagLength)
9 | {
10 | tag = tag.Substring(0, (maxTagLength));
11 | }
12 |
13 | return tag;
14 | }
15 | }
--------------------------------------------------------------------------------
/src/Linkding/Handler/AddPopularSitesAsTagHandler.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using System.Text.RegularExpressions;
3 | using Core.Abstraction;
4 | using Core.Entities.Linkding;
5 | using Core.Handler;
6 | using Linkding.Settings;
7 | using LinkdingUpdater.Handler;
8 |
9 | namespace Linkding.Handler;
10 |
11 | public class AddPopularSitesAsTagHandler : ILinkdingTaskHandler
12 | {
13 | public string Command { get; } = "AddPopularSitesAsTag";
14 | public async Task ProcessAsync(Bookmark bookmark, ILogger logger, IConfiguration configuration)
15 | {
16 | var settings = SettingsService.Settings;
17 |
18 | var returnValue = new HandlerResult() {Instance = bookmark};
19 | Regex r = null;
20 | Match m = null;
21 | foreach (var regexEntry in settings.taggingRule)
22 | {
23 | try
24 | {
25 | r = new Regex(regexEntry.pattern, RegexOptions.IgnoreCase);
26 | m = r.Match(returnValue.Instance.Url);
27 | if (m.Success)
28 | {
29 | var tagsCommaSeparated = r.Replace(returnValue.Instance.Url, regexEntry.replace);
30 | if (!string.IsNullOrEmpty(tagsCommaSeparated))
31 | {
32 | var tags = tagsCommaSeparated.Split(',');
33 | foreach (var tag in tags)
34 | {
35 | var normalizeTag = tag.NormalizeTag();
36 | if (!string.IsNullOrEmpty(normalizeTag) && !returnValue.Instance.TagNames.Contains(normalizeTag) &&
37 | returnValue.Instance.TagNames.FirstOrDefault(x => x.ToLower() == normalizeTag.ToLower()) == null)
38 | {
39 |
40 | returnValue.Instance.TagNames = returnValue.Instance.TagNames.Add(normalizeTag);
41 | returnValue.PerformAction = true;
42 | returnValue.Action = LinkdingItemAction.Update;
43 | }
44 | }
45 | }
46 | }
47 | }
48 | finally
49 | {
50 | r = null;
51 | m = null;
52 | }
53 |
54 | }
55 |
56 |
57 | foreach (var urlKeyValue in settings.urlTagMapping)
58 | {
59 | if (returnValue.Instance.Url.ToLower().StartsWith(urlKeyValue.url.ToLower()) && returnValue.Instance.TagNames.FirstOrDefault(x => x.ToLower() == urlKeyValue.name.ToLower()) == null)
60 | {
61 | returnValue.Instance.TagNames = returnValue.Instance.TagNames.Add(urlKeyValue.name);
62 |
63 | returnValue.PerformAction = true;
64 | }
65 | }
66 |
67 |
68 | return returnValue;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/Linkding/Handler/AddYearToBookmarkHandler.cs:
--------------------------------------------------------------------------------
1 | using Core.Abstraction;
2 | using Core.Entities.Linkding;
3 | using Core.Handler;
4 | using LinkdingUpdater.Handler;
5 |
6 | namespace Linkding.Handler;
7 |
8 | public class AddYearToBookmarkHandler : ILinkdingTaskHandler
9 | {
10 | public string Command { get; } = "AddYearToBookmark";
11 |
12 | public async Task ProcessAsync(Bookmark bookmark, ILogger logger, IConfiguration configuration)
13 | {
14 | var returnValue = new HandlerResult() {Instance = bookmark};
15 |
16 | var update = false;
17 | var createdYear = returnValue.Instance.DateAdded.GetYear();
18 |
19 | if (createdYear != "1970")
20 | {
21 | var tagName = returnValue.Instance.TagNames.FirstOrDefault(x => x.Equals(createdYear));
22 | if (tagName == null)
23 | {
24 | logger.LogInformation(
25 | $"Detected bookmark ({returnValue.Instance.WebsiteTitle} - {returnValue.Instance.Id}) without year-tag ... Try to update");
26 | returnValue.Instance.TagNames = returnValue.Instance.TagNames.Add(createdYear);
27 | update = true;
28 | }
29 | }
30 | else
31 | {
32 | var wrongTagName = returnValue.Instance.TagNames.FirstOrDefault(x => x.Equals("1970"));
33 | if (wrongTagName != null)
34 | {
35 | logger.LogInformation(
36 | $"Detected bookmark ({returnValue.Instance.WebsiteTitle} - {returnValue.Instance.Id}) with '1970' year-tag ... Try to update");
37 | returnValue.Instance.TagNames = returnValue.Instance.TagNames.Where(x => !x.Equals("1970")).Select(x => x);
38 | update = true;
39 | }
40 | }
41 |
42 | if (update)
43 | {
44 | logger.LogInformation($"Start updating bookmark {returnValue.Instance.WebsiteTitle} - {returnValue.Instance.Id}");
45 | returnValue.PerformAction = true;
46 | returnValue.Action = LinkdingItemAction.Update;
47 | }
48 |
49 | return returnValue;
50 | }
51 | }
--------------------------------------------------------------------------------
/src/Linkding/Handler/NormalizeTagHandler.cs:
--------------------------------------------------------------------------------
1 | using Core.Entities.Linkding;
2 | using Core.Handler;
3 | using LinkdingUpdater.Handler;
4 |
5 | namespace Linkding.Handler;
6 |
7 | public class NormalizeTagHandler : ILinkdingTaskHandler
8 | {
9 | public string Command { get; } = "NormalizeTag";
10 |
11 | public async Task ProcessAsync(Bookmark bookmark, ILogger logger, IConfiguration configuration)
12 | {
13 | var returnValue = new HandlerResult() {Instance = bookmark};
14 | var update = false;
15 |
16 | var maxTagLength = configuration.GetValue("Worker:TagNameLength");
17 |
18 | var normalizedTagnames = new List();
19 | foreach (var tagName in returnValue.Instance.TagNames)
20 | {
21 | if (tagName.Length > maxTagLength)
22 | {
23 | var normalizeTag = tagName.NormalizeTag();
24 | normalizedTagnames.Add(normalizeTag);
25 | update = true;
26 | }
27 | else
28 | {
29 | normalizedTagnames.Add(tagName);
30 | }
31 | }
32 |
33 | if (update)
34 | {
35 | logger.LogInformation($"Start updating bookmark {returnValue.Instance.WebsiteTitle} - {returnValue.Instance.Id}");
36 | returnValue.Instance.TagNames = normalizedTagnames;
37 | returnValue.PerformAction = true;
38 | returnValue.Action = LinkdingItemAction.Update;
39 | }
40 |
41 | return returnValue;
42 | }
43 | }
--------------------------------------------------------------------------------
/src/Linkding/Handler/UpdateTargetLinkdingHandler.cs:
--------------------------------------------------------------------------------
1 | using Core.Entities.Linkding;
2 | using Core.Handler;
3 | using Linkding.Client;
4 | using Linkding.Client.Extensions;
5 |
6 | namespace Linkding.Handler;
7 |
8 | public class UpdateTargetLinkdingHandler : ILinkdingSyncTaskHandler
9 | {
10 | public string Command { get; } = "UpdateTargetLinkding";
11 |
12 | public async Task ProcessAsync(IEnumerable bookmarks, ILinkdingService linkdingService, ILogger logger,
13 | IConfiguration configuration)
14 | {
15 | var linkdingBookmarks = await linkdingService.GetAllBookmarksAsync();
16 | var addedBookmarks = new List();
17 | var updatedBookmarks = new List();
18 |
19 | if (linkdingBookmarks.Count() > 0)
20 | {
21 |
22 | foreach (var bookmark in bookmarks)
23 | {
24 | var linkdingBookmark = linkdingBookmarks.FirstOrDefault(x => x.Url == bookmark.Url);
25 | if (linkdingBookmark == null)
26 | {
27 | addedBookmarks.Add(bookmark.MapToCreatePayload());
28 | }
29 | else
30 | {
31 | bookmark.Id = linkdingBookmark.Id;
32 | bookmark.Title = !string.IsNullOrEmpty(bookmark.Title) ? bookmark.Title.Trim() : bookmark.WebsiteTitle;
33 | bookmark.Description = !string.IsNullOrEmpty(bookmark.Description) ? bookmark.Description.Trim() : bookmark.WebsiteDescription;
34 |
35 | if (!string.IsNullOrEmpty(bookmark.Title) && !string.IsNullOrEmpty(bookmark.Description) &&
36 | (!linkdingBookmark.Title.Equals(bookmark.Title.Trim(), StringComparison.OrdinalIgnoreCase) ||
37 | !linkdingBookmark.Description.Equals(bookmark.Description.Trim(), StringComparison.OrdinalIgnoreCase) ||
38 | linkdingBookmark.TagNames.Count() != bookmark.TagNames.Count()))
39 | {
40 | updatedBookmarks.Add(bookmark);
41 | }
42 | else
43 | {
44 | var difference = linkdingBookmark.TagNames.Where(t =>
45 | !bookmark.TagNames.Any(b => b.Equals(t, StringComparison.OrdinalIgnoreCase)));
46 |
47 | if (difference.Count() > 0)
48 | {
49 | updatedBookmarks.Add(bookmark);
50 | }
51 | }
52 | }
53 | }
54 | }
55 | else
56 | {
57 | foreach (var bookmark in linkdingBookmarks)
58 | {
59 | addedBookmarks.Add(bookmark.MapToCreatePayload());
60 | }
61 | }
62 |
63 | if (updatedBookmarks.Count > 0)
64 | {
65 | await linkdingService.UpdateBookmarkCollectionAsync(updatedBookmarks);
66 | }
67 |
68 | if (addedBookmarks.Count > 0)
69 | {
70 | await linkdingService.AddBookmarkCollectionAsync(addedBookmarks);
71 | }
72 | }
73 | }
--------------------------------------------------------------------------------
/src/Linkding/Linkding.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 | dotnet-LinkdingService-80165DE2-FA70-4803-B366-DF8F24CF86BE
8 | Linux
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | .dockerignore
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | Never
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/Linkding/Options/WorkerSettings.cs:
--------------------------------------------------------------------------------
1 | namespace Linkding.Options;
2 |
3 | public class WorkerSettings
4 | {
5 | public const string Position = "Worker";
6 |
7 | public int Interval { get; set; } = 0;
8 | public int TagNameLength { get; set; } = 64;
9 | public List Tasks { get; set; } = new List();
10 | public List TargetLinkdingUrl { get; set; } = new List();
11 | public List TargetLinkdingKey { get; set; } = new List();
12 | }
--------------------------------------------------------------------------------
/src/Linkding/Program.cs:
--------------------------------------------------------------------------------
1 | using Linkding;
2 |
3 | IHost host = Host.CreateDefaultBuilder(args)
4 | .ConfigureServices((ctx, services) =>
5 | {
6 | services.Add_Linkding_HttpClient(ctx.Configuration);
7 | services.Add_Linkding_Worker(ctx.Configuration);
8 | services.AddHostedService();
9 | }).ConfigureHostConfiguration((builder) =>
10 | {
11 | builder
12 | .AddEnvironmentVariables()
13 | #if DEBUG
14 | .AddJsonFile("appsettings.json")
15 | .AddJsonFile("appsettings.Development.json")
16 | #endif
17 | .AddUserSecrets(true)
18 | .AddCommandLine(args);
19 | })
20 | .Build();
21 |
22 | await host.RunAsync();
--------------------------------------------------------------------------------
/src/Linkding/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "LinkdingService": {
4 | "commandName": "Project",
5 | "dotnetRunMessages": true,
6 | "environmentVariables": {
7 | "DOTNET_ENVIRONMENT": "Development"
8 | }
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/Linkding/Settings/SettingsService.cs:
--------------------------------------------------------------------------------
1 | using YamlDotNet.Serialization;
2 | using YamlDotNet.Serialization.NamingConventions;
3 |
4 | namespace Linkding.Settings;
5 |
6 | public class SettingYaml
7 | {
8 | public List urlTagMapping { get; set; } = new ();
9 | public List taggingRule { get; set; } = new ();
10 | }
11 |
12 | public class UrlTagMapping
13 | {
14 | public string name { get; set; }
15 | public string url { get; set; }
16 | }
17 |
18 | public class TaggingRule
19 | {
20 | public string name { get; set; }
21 | public string pattern { get; set; }
22 | public string replace { get; set; }
23 | }
24 |
25 | public class SettingsService
26 | {
27 | private const string fileName = "data/config.yml";
28 |
29 | private static SettingYaml _settings = null;
30 |
31 | public static SettingYaml Settings
32 | {
33 | get
34 | {
35 | if (_settings == null)
36 | {
37 | Initialize();
38 | }
39 |
40 | return _settings;
41 | }
42 | private set
43 | {
44 | _settings = value;
45 | }
46 | }
47 |
48 | private static void Initialize()
49 | {
50 | var filePath = Path.Combine(Environment.CurrentDirectory, fileName);
51 | var fileInfo = new FileInfo(filePath);
52 |
53 | if (fileInfo.Exists)
54 | {
55 | var deserializer = new DeserializerBuilder()
56 | .WithNamingConvention(CamelCaseNamingConvention.Instance) // see height_in_inches in sample yml
57 | .Build();
58 |
59 | var yml = File.ReadAllText(fileInfo.FullName);
60 |
61 | Settings = deserializer.Deserialize(yml);
62 | }
63 | else
64 | {
65 | Settings = new SettingYaml();
66 | }
67 | }
68 | }
--------------------------------------------------------------------------------
/src/Linkding/Worker.cs:
--------------------------------------------------------------------------------
1 | using Core.Abstraction;
2 | using Core.Entities.Linkding;
3 | using Core.Handler;
4 | using Linkding.Client;
5 | using Linkding.Client.Options;
6 | using Linkding.Options;
7 | using Microsoft.Extensions.Options;
8 |
9 | namespace Linkding;
10 |
11 | public class Worker : BackgroundService
12 | {
13 | private readonly IHostApplicationLifetime _hostApplicationLifetime;
14 | private readonly ILogger _logger;
15 | private readonly LinkdingService _linkdingService;
16 | private readonly LinkdingSettings _linkdingSettings;
17 | private readonly WorkerSettings _settings;
18 | private readonly IConfiguration _configuration;
19 |
20 | public Worker(ILogger logger, LinkdingService linkdingService,
21 | IOptions linkdingSettings, IOptions settings, IConfiguration configuration, IHostApplicationLifetime hostApplicationLifetime)
22 | {
23 | _logger = logger;
24 | _linkdingService = linkdingService;
25 | _configuration = configuration;
26 | _hostApplicationLifetime = hostApplicationLifetime;
27 | _settings = settings.Value;
28 | _linkdingSettings = linkdingSettings.Value;
29 | }
30 |
31 | protected override async Task ExecuteAsync(CancellationToken stoppingToken)
32 | {
33 | while (!stoppingToken.IsCancellationRequested)
34 | {
35 | _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
36 |
37 | await RunBookmarksTaskHandler();
38 | int delay = _settings.Interval * 60000;
39 |
40 | if (delay > 0)
41 | {
42 | _logger.LogInformation($"Worker paused for: {_settings.Interval} minutes");
43 |
44 | await Task.Delay(delay, stoppingToken);
45 | }
46 | else
47 | {
48 | _logger.LogInformation($"Interval value is '0' --> stopping worker");
49 | _hostApplicationLifetime.StopApplication();
50 | }
51 | }
52 | }
53 |
54 | public async Task RunBookmarksTaskHandler()
55 | {
56 | IEnumerable linkdingBookmarks = null;
57 | if (!string.IsNullOrEmpty(_linkdingSettings.Url) && _linkdingSettings.UpdateBookmarks)
58 | {
59 | _logger.LogInformation($"Starting updating bookmarks for {_linkdingSettings.Url}");
60 |
61 | _logger.LogInformation("Collecting Tag Handler");
62 | var tagHandlers = AppDomain.CurrentDomain.GetAssemblies()
63 | .SelectMany(s => s.GetTypes())
64 | .Where(p => typeof(ILinkdingTaskHandler).IsAssignableFrom(p) && p.IsClass);
65 |
66 | var updatedBookmarksCount = 0;
67 | var updateBookmarks = new List();
68 | var deleteBookmarks = new List();
69 | if (tagHandlers != null && tagHandlers.Count() > 0)
70 | {
71 | linkdingBookmarks = await _linkdingService.GetAllBookmarksAsync();
72 | if (linkdingBookmarks.Count() > 0)
73 | {
74 | var tasksLowerCase = _settings.Tasks.Select(x => x.ToLower());
75 |
76 | _logger.LogInformation($"{linkdingBookmarks.Count()} bookmarks found in {_linkdingSettings.Url}");
77 |
78 | foreach (var handler in tagHandlers)
79 | {
80 | ILinkdingTaskHandler handlerInstance = null;
81 | try
82 | {
83 | handlerInstance = (ILinkdingTaskHandler) Activator.CreateInstance(handler);
84 |
85 | var task = tasksLowerCase.FirstOrDefault(x =>
86 | x.Equals(handlerInstance.Command, StringComparison.InvariantCultureIgnoreCase));
87 |
88 | if (string.IsNullOrEmpty(task))
89 | {
90 | continue;
91 | }
92 |
93 | foreach (var linkdingBookmark in linkdingBookmarks)
94 | {
95 | try
96 | {
97 | _logger.LogDebug($"Start executing {task}");
98 | // var updateBookmark = updateBookmarks.FirstOrDefault(x => x.Id == linkdingBookmark.Id);
99 | var existingBookmarkIndexInt =
100 | updateBookmarks.FindIndex(x => x.Id == linkdingBookmark.Id);
101 |
102 | var bookmarkInstance = existingBookmarkIndexInt != -1
103 | ? updateBookmarks[existingBookmarkIndexInt]
104 | : linkdingBookmark;
105 |
106 | var result = await handlerInstance.ProcessAsync(bookmarkInstance, _logger, _configuration);
107 |
108 | if (result.HasError)
109 | {
110 | _logger.LogWarning(result.ErrorMessage, handlerInstance.Command);
111 | }
112 | else
113 | {
114 | if (result.PerformAction)
115 | {
116 | if (result.Action == LinkdingItemAction.Delete)
117 | {
118 | if (existingBookmarkIndexInt != -1)
119 | {
120 | updateBookmarks.RemoveAt(existingBookmarkIndexInt);
121 | }
122 |
123 | var bookmarkToDelete = deleteBookmarks.FirstOrDefault(x =>
124 | x.Url.ToLower() == result.Instance.Url.ToLower());
125 | if (bookmarkToDelete == null)
126 | {
127 | deleteBookmarks.Add(result.Instance);
128 | }
129 | }
130 | else
131 | {
132 | if (existingBookmarkIndexInt != -1)
133 | {
134 | updateBookmarks[existingBookmarkIndexInt] = result.Instance;
135 | }
136 | else
137 | {
138 | updateBookmarks.Add(result.Instance);
139 | }
140 |
141 | linkdingBookmark.TagNames = result.Instance.TagNames;
142 | }
143 | }
144 | }
145 |
146 | _logger.LogDebug($"Finished {handlerInstance.Command}");
147 | }
148 | catch (Exception e)
149 | {
150 | Console.WriteLine(e);
151 | var message = $"... {e.Message}";
152 |
153 | if (handlerInstance != null && !string.IsNullOrEmpty(handlerInstance.Command))
154 | {
155 | message = $"Error while executing {handlerInstance.Command}! {message}";
156 | }
157 | else
158 | {
159 | message = $"Error while executing handler! {message}";
160 | }
161 |
162 | _logger.LogError(message, "Calling Handler", e);
163 | // throw;
164 | }
165 | }
166 | }
167 | catch (Exception e)
168 | {
169 | Console.WriteLine(e);
170 | // throw;
171 | }
172 | }
173 | }
174 | else
175 | {
176 | _logger.LogInformation($"no bookmarks found in {_linkdingSettings.Url}");
177 | }
178 |
179 | if (updateBookmarks.Count() > 0)
180 | {
181 | _logger.LogDebug($"Start updating bookmarks");
182 | await _linkdingService.UpdateBookmarkCollectionAsync(updateBookmarks);
183 | _logger.LogDebug($"Successfully updated bookmarks");
184 | }
185 | }
186 |
187 | _logger.LogInformation($"Finished updating bookmarks for {_linkdingSettings.Url}");
188 |
189 | _logger.LogInformation("Collecting Sync Handler");
190 | var syncHandlers = AppDomain.CurrentDomain.GetAssemblies()
191 | .SelectMany(s => s.GetTypes())
192 | .Where(p => typeof(ILinkdingSyncTaskHandler).IsAssignableFrom(p) && p.IsClass);
193 |
194 | if (syncHandlers != null && syncHandlers.Count() > 0)
195 | {
196 | if (linkdingBookmarks == null)
197 | {
198 | linkdingBookmarks = await _linkdingService.GetAllBookmarksAsync();
199 | }
200 |
201 | if (linkdingBookmarks.Count() > 0)
202 | {
203 | var tasksLowerCase = _settings.Tasks.Select(x => x.ToLower());
204 |
205 | if (_settings.TargetLinkdingKey.Count == _settings.TargetLinkdingUrl.Count)
206 | {
207 | var targetInstances = new Dictionary();
208 | for (var i = 0; i < _settings.TargetLinkdingKey.Count(); i++)
209 | {
210 | LinkdingService targetService = null;
211 | try
212 | {
213 | var url = _settings.TargetLinkdingUrl[i];
214 | var key = _settings.TargetLinkdingKey[i];
215 | targetService = LinkdingService.Create(url, key);
216 |
217 | foreach (var syncHandler in syncHandlers)
218 | {
219 | ILinkdingSyncTaskHandler handlerInstance = null;
220 | try
221 | {
222 | handlerInstance = (ILinkdingSyncTaskHandler) Activator.CreateInstance(syncHandler);
223 |
224 | var task = tasksLowerCase.FirstOrDefault(x =>
225 | x.Equals(handlerInstance.Command, StringComparison.InvariantCultureIgnoreCase));
226 |
227 | if (string.IsNullOrEmpty(task))
228 | {
229 | continue;
230 | }
231 |
232 | _logger.LogDebug($"Start executing {task}");
233 | await handlerInstance.ProcessAsync(linkdingBookmarks, targetService, _logger, _configuration);
234 | _logger.LogDebug($"{task} executed successfully!");
235 | }
236 | catch (Exception ex)
237 | {
238 | Console.WriteLine(ex);
239 | var message = $"... {ex.Message}";
240 |
241 | if (handlerInstance != null && !string.IsNullOrEmpty(handlerInstance.Command))
242 | {
243 | message = $"Error while executing {handlerInstance.Command}! {message}";
244 | }
245 | else
246 | {
247 | message = $"Error while executing handler! {message}";
248 | }
249 |
250 | _logger.LogError(message, "Calling Handler", ex);
251 | }
252 | }
253 | }
254 | catch (Exception e)
255 | {
256 | Console.WriteLine(e);
257 | {throw;}
258 | }
259 | finally
260 | {
261 | targetService = null;
262 | }
263 | }
264 | }
265 | }
266 | }
267 | }
268 | }
269 | }
--------------------------------------------------------------------------------
/src/Linkding/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.Hosting.Lifetime": "Information"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/Linkding/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.Hosting.Lifetime": "Information"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/Linkding/data/config.yml:
--------------------------------------------------------------------------------
1 | urlTagMapping:
2 | - name: microsoft
3 | url: https://github.com/azure
4 | - name: microsoft
5 | url: https://github.com/AzureAD
6 | - name: microsoft
7 | url: https://github.com/dotnet-architecture
8 |
9 | taggingRule:
10 | - name: reddit
11 | pattern: https://(?:www\.)?(reddit)\.com(?:/r/)?([a-zA-Z0-9\-\+_]+)?(?:/.*)?
12 | replace: $1,$2
13 | - name: microsoft
14 | pattern: https://([a-zA-Z0-9]+)?[\.]?(microsoft)\.com(?:/.*)?
15 | replace: $1,$2
16 | - name: microsoft_docs
17 | pattern: 'https://(?:docs)\.(?:microsoft)\.com[/]?(?: [a-zA-Z0-9\-\+_]+)(?:/)?([a-zA-Z0-9\-\+_]+)?(?:/)?([a-zA-Z0-9\-\+_]+)?(?:/.*)?'
18 | replace: $1,$2
19 | - name: youtube
20 | pattern: https://[[a-zA-Z0-9]+\.]?(youtube)\.com(?:/.*)?
21 | replace: $1
22 | - name: ebay
23 | pattern: https://[[a-zA-Z0-9]+\.]?(ebay)\.(com|de|fr)(?:/.*)?
24 | replace: $1
25 | - name: amazon
26 | pattern: https://[[a-zA-Z0-9]+\.]?(amazon)\.(com|de|fr)(?:/.*)?
27 | replace: $1
28 | - name: docker
29 | pattern: https://([a-zA-Z0-9]+)?[\.]?(docker)\.com(?:/.*)?
30 | replace: $1,$2
31 | - name: xbox
32 | pattern: https://[[a-zA-Z0-9]+\.]?(xbox)\.com(?:/.*)?
33 | replace: $1
34 | - name: github
35 | pattern: https://([ a-zA-Z0-9 ]+)?[ \. ]?(github)\.com[ / ]?([ a-zA-Z0-9\-\+_ ]+)(?:/)?([ a-zA-Z0-9\-\+_ ]+)?(?:/.*)?
36 | replace: $2,$3,$4
37 | - name: github.io
38 | pattern: https://([ a-zA-Z0-9 ]+)\.(github)\.io[ / ]?([ a-zA-Z0-9\-\+_ ]+)(?:/)?([ a-zA-Z0-9\-\+_ ]+)?(?:/.*)?
39 | replace: $1,$2,$3
--------------------------------------------------------------------------------
/src/Services/Linkding.Client/Automapper/LinkdingBookmarkProfile.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using Core.Entities.Linkding;
3 |
4 | namespace Linkding.Client.Automapper;
5 |
6 | public class LinkdingBookmarkProfile : Profile
7 | {
8 | public LinkdingBookmarkProfile()
9 | {
10 | CreateMap();
11 | CreateMap();
12 | }
13 | }
--------------------------------------------------------------------------------
/src/Services/Linkding.Client/Extensions/BookmarkExtensions.cs:
--------------------------------------------------------------------------------
1 | using Core.Entities.Linkding;
2 |
3 | namespace Linkding.Client.Extensions;
4 |
5 | public static class BookmarkExtensions
6 | {
7 | public static BookmarkCreatePayload MapToCreatePayload(this Bookmark bookmark)
8 | {
9 | var payload = new BookmarkCreatePayload();
10 | payload.Title = bookmark.Title;
11 | payload.Description = bookmark.Description;
12 | payload.Url = bookmark.Url;
13 | payload.Unread = bookmark.Unread;
14 | payload.IsArchived = payload.IsArchived;
15 |
16 | foreach (var tagName in bookmark.TagNames)
17 | {
18 | payload.TagNames = payload.TagNames.Add(tagName);
19 | }
20 |
21 | return payload;
22 | }
23 |
24 | public static BookmarkUpdatePayload MapToUpdatePayload(this Bookmark bookmark)
25 | {
26 | var payload = new BookmarkUpdatePayload();
27 | payload.Title = bookmark.Title;
28 | payload.Description = bookmark.Description;
29 | payload.Url = bookmark.Url;
30 |
31 | foreach (var tagName in bookmark.TagNames)
32 | {
33 | payload.TagNames = payload.TagNames.Add(tagName);
34 | }
35 |
36 | return payload;
37 | }
38 | }
--------------------------------------------------------------------------------
/src/Services/Linkding.Client/Extensions/ServiceRegistrationExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using Linkding.Client;
3 | using Linkding.Client.Options;
4 | using Microsoft.Extensions.Configuration;
5 | using Polly;
6 | using Polly.Extensions.Http;
7 |
8 | // ReSharper disable once CheckNamespace
9 | namespace Microsoft.Extensions.DependencyInjection;
10 |
11 | public static class ServiceRegistrationExtensions
12 | {
13 | public static IServiceCollection Add_Linkding_HttpClient(this IServiceCollection services,
14 | IConfiguration configuration)
15 | {
16 | var configSection = configuration.GetSection(LinkdingSettings.Position);
17 | services.Configure(configSection);
18 | services.AddHttpClient()
19 | .SetHandlerLifetime(TimeSpan.FromMinutes(5)) //Set lifetime to five minutes
20 | .AddPolicyHandler(GetRetryPolicy());
21 |
22 | var assemblies = AppDomain.CurrentDomain.GetAssemblies();
23 | services.AddAutoMapper(assemblies);
24 |
25 | return services;
26 | }
27 |
28 | static IAsyncPolicy GetRetryPolicy()
29 | {
30 | return HttpPolicyExtensions
31 | .HandleTransientHttpError()
32 | .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
33 | .WaitAndRetryAsync(6, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2,
34 | retryAttempt)));
35 | }
36 | }
--------------------------------------------------------------------------------
/src/Services/Linkding.Client/Extensions/System.cs:
--------------------------------------------------------------------------------
1 | namespace System;
2 |
3 | public static class System
4 | {
5 | public static DateTime CreateDateTime(this long ticks)
6 | {
7 | var dateTimeOffset = DateTimeOffset.FromUnixTimeSeconds(ticks);
8 |
9 | return dateTimeOffset.UtcDateTime;
10 | }
11 |
12 | public static DateTime CreateDateTime(this string ticksString)
13 | {
14 | if (long.TryParse(ticksString, out var dateAddedUnixEpoch))
15 | {
16 | return dateAddedUnixEpoch.CreateDateTime();
17 | }
18 |
19 | return default;
20 | }
21 |
22 | public static string GetYear(this DateTime date)
23 | {
24 | return date.ToString("yyyy");
25 | }
26 |
27 | public static string GetYear(this long ticks)
28 | {
29 | var dateTime = ticks.CreateDateTime();
30 | return dateTime.GetYear();
31 | }
32 |
33 | public static string GetYear(this string ticksString)
34 | {
35 | var dateTime = ticksString.CreateDateTime();
36 | return dateTime.GetYear();
37 | }
38 | }
--------------------------------------------------------------------------------
/src/Services/Linkding.Client/Extensions/SystemCollectionGenericExtesions.cs:
--------------------------------------------------------------------------------
1 | // ReSharper disable once CheckNamespace
2 | namespace System.Collections.Generic;
3 |
4 | public static class SystemCollectionGenericExtesions
5 | {
6 | public static IEnumerable Add(this IEnumerable e, T value) {
7 | foreach ( var cur in e) {
8 | yield return cur;
9 | }
10 | yield return value;
11 | }
12 | }
--------------------------------------------------------------------------------
/src/Services/Linkding.Client/Linkding.Client.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 | AnyCPU
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/Services/Linkding.Client/LinkdingService.cs:
--------------------------------------------------------------------------------
1 | using System.Net.Http.Headers;
2 | using System.Net.Http.Json;
3 | using System.Text;
4 | using System.Text.Json;
5 | using AutoMapper;
6 | using Core.Entities.Linkding;
7 | using Linkding.Client.Options;
8 | using Microsoft.Extensions.Options;
9 |
10 | namespace Linkding.Client;
11 |
12 | public class LinkdingService : ILinkdingService
13 | {
14 | private readonly LinkdingSettings _settings;
15 | private readonly IMapper _mapper;
16 | public readonly HttpClient _client;
17 |
18 | public LinkdingService(HttpClient client, IOptions settings, IMapper mapper)
19 | {
20 | _settings = settings.Value;
21 | _client = client;
22 | _mapper = mapper;
23 | _client.BaseAddress = new Uri(_settings.Url);
24 | _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Token", _settings.Key);
25 | }
26 |
27 | private LinkdingService(string url, string key)
28 | {
29 | _client = new HttpClient();
30 | _client.BaseAddress = new Uri(url);
31 | _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Token", key);
32 | }
33 |
34 | public async Task> GetBookmarksAsync(int limit = 100, int offset = 0)
35 | {
36 | var bookmarks = new List();
37 |
38 | var result = await GetBookmarkResultsAsync(limit, offset);
39 | if (result != null && result.Results?.Count() > 0)
40 | {
41 | bookmarks = result.Results;
42 | }
43 |
44 | return bookmarks;
45 | }
46 |
47 | public async Task> GetAllBookmarksAsync()
48 | {
49 | IEnumerable bookmarks = new List();
50 |
51 | var result = await GetBookmarkResultsAsync();
52 | if (result != null && result.Results?.Count() > 0)
53 | {
54 | bookmarks = result.Results;
55 | if (result.Count > 100)
56 | {
57 | while (!string.IsNullOrEmpty(result.Next))
58 | {
59 | result = await GetBookmarkResultsAsync(result.Next);
60 | if (result.Results?.Count() > 0)
61 | {
62 | bookmarks = bookmarks.Concat(result.Results);
63 | }
64 | else
65 | {
66 | break;
67 | }
68 | }
69 | }
70 | }
71 |
72 | return bookmarks;
73 | }
74 |
75 | public async Task AddBookmarkCollectionAsync(IEnumerable bookmarks)
76 | {
77 | foreach (var bookmark in bookmarks)
78 | {
79 | var payload = _mapper.Map(bookmark);
80 | await AddBookmarkAsync(payload);
81 | }
82 | }
83 |
84 | public async Task AddBookmarkCollectionAsync(IEnumerable bookmarks)
85 | {
86 | foreach (var bookmark in bookmarks)
87 | {
88 | await AddBookmarkAsync(bookmark);
89 | }
90 | }
91 |
92 | public async Task AddBookmarkAsync(BookmarkCreatePayload bookmark)
93 | {
94 | var content = JsonSerializer.Serialize(bookmark);
95 | var requestContent = new StringContent(content, Encoding.UTF8, "application/json");
96 | var uri = $"/api/bookmarks/";
97 |
98 | try
99 | {
100 | var response = await _client.PostAsync(uri, requestContent);
101 | response.EnsureSuccessStatusCode();
102 | }
103 | catch (Exception e)
104 | {
105 | Console.WriteLine(e);
106 | // throw;
107 | }
108 |
109 | var result = await _client.PostAsJsonAsync($"/api/bookmarks/", bookmark);
110 | if (result.IsSuccessStatusCode)
111 | {
112 |
113 | }
114 | else
115 | {
116 |
117 | }
118 | }
119 |
120 | public async Task UpdateBookmarkCollectionAsync(IEnumerable bookmarks)
121 | {
122 | foreach (var bookmark in bookmarks)
123 | {
124 | try
125 | {
126 | var payload = _mapper.Map(bookmark);
127 | await UpdateBookmarkAsync(bookmark.Id, payload);
128 | }
129 | catch (Exception e)
130 | {
131 | Console.WriteLine(e);
132 | // throw;
133 | }
134 | }
135 | }
136 |
137 | public async Task UpdateBookmarkAsync(int id, BookmarkUpdatePayload bookmark)
138 | {
139 | var content = JsonSerializer.Serialize(bookmark);
140 | var requestContent = new StringContent(content, Encoding.UTF8, "application/json");
141 | var uri = $"/api/bookmarks/{id}/";// Path.Combine("api/bookmarks", $"{id}");
142 |
143 | try
144 | {
145 | var response = await _client.PutAsync(uri, requestContent);
146 | response.EnsureSuccessStatusCode();
147 | }
148 | catch (Exception e)
149 | {
150 | Console.WriteLine(e);
151 | // throw;
152 | }
153 | }
154 |
155 | public async Task GetBookmarkResultsAsync(int limit = 100, int offset = 0)
156 | {
157 | BookmarksResult bookmarkResult = null;
158 |
159 | var url = $"/api/bookmarks/";
160 |
161 | bookmarkResult = await GetBookmarkResultsAsync(url);
162 |
163 | return bookmarkResult;
164 | }
165 |
166 | public async Task GetBookmarkResultsAsync(string url)
167 | {
168 | BookmarksResult bookmarkResult = null;
169 |
170 | bookmarkResult = await _client.GetFromJsonAsync(url);
171 |
172 | return bookmarkResult;
173 | }
174 |
175 | public static LinkdingService Create(string url, string key)
176 | {
177 | return new LinkdingService(url, key);
178 | }
179 | }
--------------------------------------------------------------------------------
/src/Services/Linkding.Client/Options/LinkdingSettings.cs:
--------------------------------------------------------------------------------
1 | namespace Linkding.Client.Options;
2 |
3 | public class LinkdingSettings
4 | {
5 | public const string Position = "Linkding";
6 |
7 | public string Key { get; set; }
8 | public string Url { get; set; }
9 | public bool UpdateBookmarks { get; set; } = true;
10 | }
--------------------------------------------------------------------------------
/src/Services/Wallabag.Client/Contracts/IAccessTokenProvider.cs:
--------------------------------------------------------------------------------
1 | namespace Wallabag.Client.Contracts;
2 |
3 | public interface IAccessTokenProvider
4 | {
5 | Task GetToken(IEnumerable scopes);
6 | }
--------------------------------------------------------------------------------
/src/Services/Wallabag.Client/Converters/DateTimeConverterForCustomStandard.cs:
--------------------------------------------------------------------------------
1 | using System.Globalization;
2 | using System.Text.Json;
3 | using System.Text.Json.Serialization;
4 |
5 | namespace Wallabag.Client.Converters;
6 |
7 | public class DateTimeConverterForCustomStandard : JsonConverter
8 | {
9 | public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
10 | {
11 | var dateTimeString = reader.GetString();
12 |
13 | if (string.IsNullOrEmpty(dateTimeString))
14 | {
15 | return DateTime.MinValue;
16 | }
17 | DateTime dt = DateTime.ParseExact(dateTimeString, "yyyy-MM-dd'T'HH:mm:ssK",
18 | CultureInfo.InvariantCulture,
19 | DateTimeStyles.AdjustToUniversal);
20 |
21 | return dt;
22 | }
23 |
24 | public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
25 | {
26 | writer.WriteStringValue(value.ToString());
27 | }
28 | }
--------------------------------------------------------------------------------
/src/Services/Wallabag.Client/Converters/DateTimeOffsetConverterUsingDateTimeParse.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using System.Text.Json.Serialization;
3 |
4 | namespace Wallabag.Client.Converters;
5 |
6 | public class DateTimeOffsetConverterUsingDateTimeParse : JsonConverter
7 | {
8 | public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
9 | {
10 | var dateTimeString = reader.GetString();
11 | return DateTimeOffset.Parse(dateTimeString);
12 | }
13 |
14 | public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
15 | {
16 | writer.WriteStringValue(value.ToString());
17 | }
18 | }
--------------------------------------------------------------------------------
/src/Services/Wallabag.Client/Extensions/ServiceRegistrationExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Net.Http.Headers;
2 | using Microsoft.Extensions.Configuration;
3 | using Polly;
4 | using Polly.Extensions.Http;
5 | using Wallabag.Client;
6 | using Wallabag.Client.Contracts;
7 | using Wallabag.Client.OAuth;
8 | using Wallabag.Client.Options;
9 |
10 | // ReSharper disable once CheckNamespace
11 | namespace Microsoft.Extensions.DependencyInjection;
12 |
13 | public static class ServiceRegistrationExtensions
14 | {
15 | public static IServiceCollection Add_Wallabag_HttpClient(this IServiceCollection services,
16 | IConfiguration configuration)
17 | {
18 | var configSection = configuration.GetSection(WallabagSettings.Position);
19 | services.Configure(configSection);
20 | // services.AddScoped();
21 | // services.AddScoped();
22 | services.AddSingleton();
23 | services.AddSingleton();
24 | services.AddHttpClient()
25 | .SetHandlerLifetime(TimeSpan.FromMinutes(5)) //Set lifetime to five minutes
26 | .AddPolicyHandler(GetRetryPolicy());
27 |
28 | // services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
29 |
30 | return services;
31 | }
32 |
33 | static IAsyncPolicy GetRetryPolicy()
34 | {
35 | return HttpPolicyExtensions
36 | .HandleTransientHttpError()
37 | .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
38 | .WaitAndRetryAsync(6, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2,
39 | retryAttempt)));
40 | }
41 | }
--------------------------------------------------------------------------------
/src/Services/Wallabag.Client/Extensions/URIExtensions.cs:
--------------------------------------------------------------------------------
1 | // ReSharper disable once CheckNamespace
2 | namespace System;
3 |
4 | public static class URIExtensions
5 | {
6 | public static string AppendToURL(this string uri1, string uri2)
7 | {
8 | return AppendToUrlInternal(uri1, uri2);
9 | }
10 |
11 | public static string AppendToURL(this string baseURL, params string[] segments)
12 | {
13 | return AppendToUrlInternal(baseURL, segments);
14 | }
15 |
16 | private static string AppendToUrlInternal(this string baseURL, params string[] segments)
17 | {
18 | return string.Join("/", new[] { baseURL.TrimEnd('/') }
19 | .Concat(segments.Select(s => s.Trim('/'))));
20 | }
21 |
22 | public static Uri Append(this Uri uri, params string[] paths)
23 | {
24 | return new Uri(paths.Aggregate(uri.AbsoluteUri, (current, path) => string.Format("{0}/{1}", current.TrimEnd('/'), path.TrimStart('/'))));
25 | }
26 | }
--------------------------------------------------------------------------------
/src/Services/Wallabag.Client/Models/WallabagEntry.cs:
--------------------------------------------------------------------------------
1 | namespace Wallabag.Client.Models;
2 |
3 | public class WallabagEntry : WallabagPayload
4 | {
5 | public int Id { get; set; }
6 | }
--------------------------------------------------------------------------------
/src/Services/Wallabag.Client/Models/WallabagPayload.cs:
--------------------------------------------------------------------------------
1 | namespace Wallabag.Client.Models;
2 |
3 | public class WallabagPayload
4 | {
5 | public string Url { get; set; }
6 | public string Title { get; set; }
7 | public string Tags { get; set; }
8 | public int Archive { get; set; }
9 | public int Starred { get; set; }
10 | public string Content { get; set; }
11 | public string Language { get; set; }
12 | public string PreviewPicture { get; set; }
13 | public DateTime PublishedAt { get; set; }
14 | public string Authors { get; set; }
15 | public int Publich { get; set; }
16 | public string OriginUrl { get; set; }
17 | }
--------------------------------------------------------------------------------
/src/Services/Wallabag.Client/OAuth/AuthenticationClient.cs:
--------------------------------------------------------------------------------
1 | using System.Net.Http.Json;
2 | using Microsoft.Extensions.Options;
3 | using Wallabag.Client.Options;
4 |
5 | namespace Wallabag.Client.OAuth;
6 |
7 | public record WallabagToken(string token_type, string access_token, string refresh_token, int expires_in, string scope);
8 |
9 | public class AuthenticationClient
10 | {
11 | protected const string AuthPath = "/oauth/v2/token";
12 |
13 | public readonly HttpClient _client;
14 | public readonly WallabagSettings _settings;
15 |
16 | public AuthenticationClient(HttpClient client, IOptions settings)
17 | {
18 | _client = client;
19 | _settings = settings.Value;
20 | // var baseAdress = new Uri(_settings.Url);
21 | // client.BaseAddress = baseAdress.Append(AuthPath);
22 | client.BaseAddress = new Uri(_settings.Url);
23 | }
24 |
25 | public async Task Authenticate(IEnumerable scopes = null)
26 | {
27 | var parameters = new List>
28 | {
29 | new("client_id", _settings.ClientId),
30 | new("client_secret", _settings.ClientSecret),
31 | new("grant_type", _settings.GrandType),
32 | new("username", _settings.Username),
33 | new("password", _settings.Password)
34 | };
35 |
36 | if (scopes != null && scopes.Count() > 0)
37 | {
38 | parameters.Add(new("scope", string.Join(" ", scopes)));
39 | }
40 | var response = await _client.PostAsync("oauth/v2/token", new FormUrlEncodedContent(parameters));
41 |
42 | response.EnsureSuccessStatusCode();
43 | var authentication = await response.Content.ReadFromJsonAsync();
44 | if (authentication == null)
45 | {
46 | throw new HttpRequestException("Could not retrieve authentication data.");
47 | }
48 | return authentication;
49 | }
50 | public async Task RefreshToken(string refreshToken, IEnumerable scopes = null)
51 | {
52 | var parameters = new List>
53 | {
54 | new("client_id", _settings.ClientId),
55 | new("client_secret", _settings.ClientSecret),
56 | new("grant_type", "refresh_token"),
57 | new("refresh_token", refreshToken),
58 | new("username", _settings.Username),
59 | new("password", _settings.Password)
60 | };
61 |
62 | if (scopes != null && scopes.Count() > 0)
63 | {
64 | parameters.Add(new("scope", string.Join(" ", scopes)));
65 | }
66 |
67 | var response = await _client.PostAsync("oauth/v2/token", new FormUrlEncodedContent(parameters));
68 |
69 | response.EnsureSuccessStatusCode();
70 | var authentication = await response.Content.ReadFromJsonAsync();
71 | if (authentication == null)
72 | {
73 | throw new HttpRequestException("Could not retrieve authentication data.");
74 | }
75 | return authentication;
76 | }
77 | }
--------------------------------------------------------------------------------
/src/Services/Wallabag.Client/OAuth/OAuthTokenProvider.cs:
--------------------------------------------------------------------------------
1 | using Wallabag.Client.Contracts;
2 |
3 | namespace Wallabag.Client.OAuth;
4 |
5 | public class OAuthTokenProvider : IAccessTokenProvider
6 | {
7 | protected record TokenCache(string access_token, DateTime expires);
8 |
9 | private readonly AuthenticationClient _client;
10 |
11 | protected Dictionary cache = new ();
12 |
13 | public OAuthTokenProvider(AuthenticationClient client)
14 | {
15 | _client = client;
16 | }
17 |
18 | public async Task GetToken(IEnumerable scopes = null)
19 | {
20 | string cacheKey = "wallabag+token";
21 |
22 | if (scopes != null && scopes.Count() > 0)
23 | {
24 | cacheKey = string.Join('+', scopes);
25 | }
26 |
27 | if (cache.ContainsKey(cacheKey))
28 | {
29 | var tokenCache = cache[cacheKey];
30 | if (tokenCache.expires > DateTime.Now)
31 | {
32 | return tokenCache.access_token;
33 | }
34 | else
35 | {
36 | cache.Remove(cacheKey);
37 | }
38 | }
39 | var auth = await _client.Authenticate(scopes);
40 | cache.Add(cacheKey, new TokenCache(auth.access_token, DateTime.Now.AddSeconds(auth.expires_in)));
41 | return auth.access_token;
42 | }
43 | }
--------------------------------------------------------------------------------
/src/Services/Wallabag.Client/Options/WallabagSettings.cs:
--------------------------------------------------------------------------------
1 | namespace Wallabag.Client.Options;
2 |
3 | public class WallabagSettings
4 | {
5 | public const string Position = "Wallabag";
6 |
7 | public string Url { get; set; } = "https://app.wallabag.it";
8 | public string Username { get; set; }
9 | public string Password { get; set; }
10 | public string ClientId { get; set; }
11 | public string ClientSecret { get; set; }
12 | public string GrandType { get; set; } = "password";
13 | }
--------------------------------------------------------------------------------
/src/Services/Wallabag.Client/Wallabag.Client.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 | AnyCPU
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/Services/Wallabag.Client/WallabagServiceBase.cs:
--------------------------------------------------------------------------------
1 | using System.Net.Http.Headers;
2 | using System.Net.Http.Json;
3 | using System.Text.Json;
4 | using Core.Abstraction;
5 | using Microsoft.Extensions.Options;
6 | using Wallabag.Client.Contracts;
7 | using Wallabag.Client.Converters;
8 | using Wallabag.Client.Options;
9 |
10 | namespace Wallabag.Client;
11 |
12 | public partial class WallabagService : IWallabagService
13 | {
14 | private readonly WallabagSettings _settings;
15 | private IAccessTokenProvider _accessTokenProvider;
16 | public readonly HttpClient _client;
17 |
18 | public WallabagService(HttpClient client, IOptions settings,
19 | IAccessTokenProvider accessTokenProvider)
20 | {
21 | _client = client;
22 | _accessTokenProvider = accessTokenProvider;
23 | _settings = settings.Value;
24 | _client.BaseAddress = new Uri(_settings.Url);
25 | }
26 |
27 | public async Task GetAuthenticationHeaderAsync(IEnumerable scopes = null)
28 | {
29 | return new AuthenticationHeaderValue("Bearer", await _accessTokenProvider.GetToken(scopes));
30 | }
31 |
32 | public async Task GetAsync(string endpoint, IEnumerable scopes = null,
33 | bool httpCompletionResponseContentRead = false)
34 | {
35 | var request = new HttpRequestMessage(HttpMethod.Get, endpoint);
36 | request.Headers.Authorization = await GetAuthenticationHeaderAsync(scopes);
37 |
38 | HttpResponseMessage response = null;
39 |
40 | if (httpCompletionResponseContentRead)
41 | {
42 | response = await _client.SendAsync(request, HttpCompletionOption.ResponseContentRead);
43 | }
44 | else
45 | {
46 | response = await _client.SendAsync(request);
47 | }
48 |
49 | response.EnsureSuccessStatusCode();
50 | return response;
51 | }
52 |
53 | public async Task GetJsonAsync(string endpoint, IEnumerable scopes = null)
54 | {
55 | var response = await GetAsync(endpoint, scopes);
56 |
57 | return await response.Content.ReadFromJsonAsync();
58 | }
59 |
60 | public async Task PostWithFormDataAsnyc(string endpoint, FormUrlEncodedContent content,
61 | IEnumerable scopes = null)
62 | {
63 | using var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
64 | request.Content = content;
65 |
66 | return await PostAsync(endpoint, request, scopes);
67 | }
68 |
69 | public async Task PostAsync(string endpoint, HttpContent content,
70 | IEnumerable scopes = null)
71 | {
72 | using var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
73 | request.Content = content;
74 |
75 | return await PostAsync(endpoint, request, scopes);
76 | }
77 |
78 | public async Task PostAsync(string endpoint, HttpRequestMessage request,
79 | IEnumerable scopes = null)
80 | {
81 | request.Headers.Authorization = await GetAuthenticationHeaderAsync(scopes);
82 | var response = await _client.SendAsync(request);
83 | response.EnsureSuccessStatusCode();
84 | return response;
85 | }
86 |
87 | public async Task PutAsync(string endpoint, HttpContent content,
88 | IEnumerable scopes = null)
89 | {
90 | using var request = new HttpRequestMessage(HttpMethod.Put, endpoint);
91 | request.Headers.Authorization = await GetAuthenticationHeaderAsync(scopes);
92 | request.Content = content;
93 | var response = await _client.SendAsync(request);
94 | response.EnsureSuccessStatusCode();
95 | return response;
96 | }
97 |
98 | public async Task DeleteAsync(string endpoint, IEnumerable scopes = null,
99 | HttpContent content = null)
100 | {
101 | using var request = new HttpRequestMessage(HttpMethod.Delete, endpoint);
102 | request.Headers.Authorization = await GetAuthenticationHeaderAsync(scopes);
103 | request.Content = content;
104 | var response = await _client.SendAsync(request);
105 | response.EnsureSuccessStatusCode();
106 | return response;
107 | }
108 | }
--------------------------------------------------------------------------------
/src/Services/Wallabag.Client/WallabagServiceEntries.cs:
--------------------------------------------------------------------------------
1 | using System.Net.Http.Json;
2 | using Core.Entities.Wallabag;
3 | using Wallabag.Client.Models;
4 |
5 | namespace Wallabag.Client;
6 |
7 | public partial class WallabagService
8 | {
9 | public async Task> GetEntries(string format = "json", int limit = 50, bool full = false)
10 | {
11 | var bookmarks = new List();
12 | var url = $"/api/entries.{format}?perPage={limit}";
13 | if (!full)
14 | {
15 | url = $"{url}&detail=metadata";
16 | }
17 |
18 | var allQuery = await GetJsonAsync(url);
19 |
20 | if (allQuery != null && allQuery.Embedded != null && allQuery.Embedded.Items != null && allQuery.Embedded.Items.Count() > 0)
21 | {
22 | bookmarks = allQuery.Embedded.Items;
23 |
24 | if (allQuery.Total > limit)
25 | {
26 | while (allQuery.QueryLinks.Next != null && !string.IsNullOrEmpty(allQuery.QueryLinks.Next.Href))
27 | {
28 | // url = allQuery.QueryLinks.Next.Href.Replace(_settings.Url, "");
29 | url = allQuery.QueryLinks.Next.Href;
30 | if (_client.BaseAddress.Scheme == "https")
31 | {
32 | url = allQuery.QueryLinks.Next.Href.Replace("http://", "https://");
33 | }
34 |
35 | allQuery = await GetJsonAsync(url);
36 | bookmarks.AddRange(allQuery.Embedded.Items);
37 | }
38 | }
39 | }
40 |
41 | return bookmarks;
42 | }
43 |
44 | public async Task GetEntryById(int id, string format = "json")
45 | {
46 | var url = $"/api/entries/{id}.{format}";
47 | var item = await GetJsonAsync(url);
48 |
49 | return item;
50 | }
51 |
52 | public async Task AddEntryByUrl(string url, IEnumerable tags = null, string format = "json")
53 | {
54 | var endpoint = $"/api/entries.{format}";
55 |
56 | var keyVals = new Dictionary();
57 | keyVals.Add("url", url);
58 | if (tags != null && tags.Count() > 0)
59 | {
60 | keyVals.Add("tags", string.Join(",", tags));
61 | }
62 |
63 | var content = new FormUrlEncodedContent(keyVals);
64 | var response = await PostWithFormDataAsnyc(endpoint, content);
65 |
66 | var item = await response.Content.ReadFromJsonAsync();
67 | return item;
68 | }
69 |
70 | // private async Task GetBookmarkResultsAsync(int limit = 100, int offset = 0)
71 | // {
72 | // WallabagEntry bookmarkResult = null;
73 | //
74 | // var url = $"/api/bookmarks/";
75 | //
76 | // bookmarkResult = await GetBookmarkResultsAsync(url);
77 | //
78 | // return bookmarkResult;
79 | // }
80 | //
81 | // private async Task GetBookmarkResultsAsync(string url)
82 | // {
83 | // WallabagEntry bookmarkResult = null;
84 | //
85 | // bookmarkResult = await _client.GetFromJsonAsync(url);
86 | //
87 | // return bookmarkResult;
88 | // }
89 |
90 | }
--------------------------------------------------------------------------------
/src/Wallabag/Extensions/ServiceRegistrationExtensions.cs:
--------------------------------------------------------------------------------
1 | using Wallabag.Options;
2 | using Wallabag.Settings;
3 |
4 | // ReSharper disable once CheckNamespace
5 | namespace Microsoft.Extensions.DependencyInjection;
6 |
7 | public static class ServiceRegistrationExtensions
8 | {
9 | public static IServiceCollection Add_Wallabag_Worker(this IServiceCollection services,
10 | IConfiguration configuration)
11 | {
12 | var configSection = configuration.GetSection(WorkerSettings.Position);
13 | services.Configure(configSection);
14 | services.AddSingleton();
15 |
16 | return services;
17 | }
18 | }
--------------------------------------------------------------------------------
/src/Wallabag/Handler/LinkdingBookmarkToWallabagHandler.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using System.Text.RegularExpressions;
3 | using Core.Entities.Wallabag;
4 | using Core.Handler;
5 | using Linkding.Client;
6 | using Wallabag.Client;
7 | using Wallabag.Settings;
8 |
9 | namespace Wallabag.Handler
10 | {
11 | public class LinkdingBookmarkToWallabagHandler : ISyncTaskHandler
12 | {
13 | public Type HandlerType { get; } = typeof(WallabagService);
14 | public string Command { get; } = "LinkdingBookmarkToWallabag";
15 |
16 | public async Task ProcessAsync(IEnumerable items, WallabagService destinationService,
17 | ILinkdingService linkdingService,
18 | ILogger logger, IConfiguration configuration)
19 | {
20 | var wallabagsNormalized = new Dictionary();
21 | var updatedWallabags = new Dictionary>();
22 | var wallabagToRemove = new List();
23 | var linkdingBookmarks = await linkdingService.GetAllBookmarksAsync();
24 |
25 | if (linkdingBookmarks != null && linkdingBookmarks.Count() > 0)
26 | {
27 | var settings = SettingsService.Settings;
28 |
29 | // var settingsString = JsonSerializer.Serialize(settings);
30 | // logger.LogInformation($"settings: {settingsString}");
31 |
32 | var tagName = configuration.GetValue("Worker:SyncTag");
33 |
34 | linkdingBookmarks =
35 | linkdingBookmarks.Where(x => x.TagNames.Contains(tagName)).OrderBy(x => x.DateAdded);
36 |
37 | Regex r = null;
38 | Match m = null;
39 | foreach (var bookmark in linkdingBookmarks)
40 | {
41 | var cleanUrl =
42 | bookmark.Url.Replace(
43 | "?utm_source=share&utm_medium=android_app&utm_name=androidcss&utm_term=2&utm_content=share_button",
44 | "");
45 | // var existingElement = items.FirstOrDefault(x => x.Url.ToLower() == cleanUrl.ToLower());
46 | var existingElement = items.FirstOrDefault(x =>
47 | x.Url.ToLower() == cleanUrl.ToLower() || x.OriginUrl?.ToLower() == cleanUrl.ToLower());
48 | if (existingElement == null)
49 | {
50 | var addToWallabag = true;
51 | foreach (var p in settings.excludedDomains)
52 | {
53 | r = new Regex(p.pattern, RegexOptions.IgnoreCase);
54 | m = r.Match(cleanUrl);
55 |
56 | if (m.Success)
57 | {
58 | addToWallabag = false;
59 | break;
60 | }
61 | }
62 |
63 | if (addToWallabag && !updatedWallabags.ContainsKey(cleanUrl))
64 | {
65 | updatedWallabags.Add(cleanUrl,
66 | bookmark.TagNames.Where(x => !x.Equals(tagName, StringComparison.OrdinalIgnoreCase)));
67 | }
68 | }
69 | }
70 | }
71 | else
72 | {
73 | logger.LogInformation($"no bookmarks found");
74 | }
75 |
76 | if (updatedWallabags.Count() > 0)
77 | {
78 | logger.LogInformation($"Detected {updatedWallabags.Count()} bookmarks... Start syncing");
79 |
80 | foreach (var (url, tags) in updatedWallabags)
81 | {
82 | var result = await destinationService.AddEntryByUrl(url, tags);
83 |
84 | if (result.ReadingTime == 0)
85 | {
86 | wallabagToRemove.Add(result.Id);
87 | }
88 | }
89 |
90 | logger.LogInformation($"{updatedWallabags.Count()} bookmarks synced");
91 | }
92 | }
93 | }
94 | }
--------------------------------------------------------------------------------
/src/Wallabag/Options/WorkerSettings.cs:
--------------------------------------------------------------------------------
1 | namespace Wallabag.Options;
2 |
3 | public class WorkerSettings
4 | {
5 | public const string Position = "Worker";
6 |
7 | public int Interval { get; set; } = 0;
8 |
9 | public string SyncTag { get; set; } = "readlater";
10 | }
--------------------------------------------------------------------------------
/src/Wallabag/Program.cs:
--------------------------------------------------------------------------------
1 | using Wallabag;
2 |
3 | IHost host = Host.CreateDefaultBuilder(args)
4 | .ConfigureServices((ctx, services) =>
5 | {
6 | services.Add_Wallabag_HttpClient(ctx.Configuration);
7 | services.Add_Linkding_HttpClient(ctx.Configuration);
8 | services.Add_Wallabag_Worker(ctx.Configuration);
9 | services.AddHostedService();
10 | }).ConfigureHostConfiguration((builder) =>
11 | {
12 | builder
13 | .AddEnvironmentVariables()
14 | #if DEBUG
15 | .AddJsonFile("appsettings.json")
16 | .AddJsonFile("appsettings.Development.json")
17 | #endif
18 | .AddUserSecrets(true)
19 | .AddCommandLine(args);
20 | })
21 | .Build();
22 |
23 | await host.RunAsync();
--------------------------------------------------------------------------------
/src/Wallabag/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "WallabagWorker": {
4 | "commandName": "Project",
5 | "dotnetRunMessages": true,
6 | "environmentVariables": {
7 | "DOTNET_ENVIRONMENT": "Development"
8 | }
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/Wallabag/Settings/SettingsService.cs:
--------------------------------------------------------------------------------
1 | using YamlDotNet.Serialization;
2 | using YamlDotNet.Serialization.NamingConventions;
3 |
4 | namespace Wallabag.Settings;
5 |
6 | public class SettingYaml
7 | {
8 | public List excludedDomains { get; set; } = new ();
9 | }
10 |
11 | public class ExcludedDomainPattern
12 | {
13 | public string name { get; set; }
14 | public string pattern { get; set; }
15 | }
16 |
17 | public class SettingsService
18 | {
19 | private const string fileName = "data/config.yml";
20 |
21 | private static SettingYaml _settings = null;
22 |
23 | public static SettingYaml Settings
24 | {
25 | get
26 | {
27 | if (_settings == null)
28 | {
29 | Initialize();
30 | }
31 |
32 | return _settings;
33 | }
34 | private set
35 | {
36 | _settings = value;
37 | }
38 | }
39 |
40 | private static void Initialize()
41 | {
42 | var filePath = Path.Combine(Environment.CurrentDirectory, fileName);
43 | var fileInfo = new FileInfo(filePath);
44 |
45 | if (fileInfo.Exists)
46 | {
47 | var deserializer = new DeserializerBuilder()
48 | .WithNamingConvention(CamelCaseNamingConvention.Instance) // see height_in_inches in sample yml
49 | .Build();
50 |
51 | var yml = File.ReadAllText(fileInfo.FullName);
52 |
53 | Settings = deserializer.Deserialize(yml);
54 | }
55 | else
56 | {
57 | Settings = new SettingYaml();
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/src/Wallabag/Wallabag.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 | dotnet-WallabagWorker-D5E52F5F-C642-4BF8-9FD7-8C6C417B0D3A
8 | Linux
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | PreserveNewest
25 |
26 |
27 | PreserveNewest
28 |
29 |
30 |
31 |
32 |
33 | Never
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/Wallabag/Worker.cs:
--------------------------------------------------------------------------------
1 | using Core.Abstraction;
2 | using Core.Entities.Linkding;
3 | using Core.Handler;
4 | using Linkding.Client;
5 | using Linkding.Client.Options;
6 | using Microsoft.Extensions.Options;
7 | using Wallabag.Client;
8 | using Wallabag.Client.Options;
9 | using Wallabag.Options;
10 |
11 | namespace Wallabag;
12 |
13 | public class Worker : BackgroundService
14 | {
15 | private readonly IHostApplicationLifetime _hostApplicationLifetime;
16 | private readonly ILogger _logger;
17 | private readonly LinkdingService _linkdingService;
18 | private readonly WallabagService _wallabagService;
19 | private readonly LinkdingSettings _linkdingSettings;
20 | private readonly WallabagSettings _wallabagSettings;
21 | private readonly WorkerSettings _settings;
22 | private readonly IConfiguration _configuration;
23 |
24 |
25 | public Worker(ILogger logger, LinkdingService linkdingService, WallabagService wallabagService,
26 | IOptions linkdingSettings, IOptions settings, IOptions wallabagSettings, IConfiguration configuration, IHostApplicationLifetime hostApplicationLifetime)
27 | {
28 | _logger = logger;
29 | _linkdingService = linkdingService;
30 | _wallabagService = wallabagService;
31 | _configuration = configuration;
32 | _hostApplicationLifetime = hostApplicationLifetime;
33 | _wallabagSettings = wallabagSettings.Value;
34 | _settings = settings.Value;
35 | _linkdingSettings = linkdingSettings.Value;
36 | }
37 |
38 | protected override async Task ExecuteAsync(CancellationToken stoppingToken)
39 | {
40 | while (!stoppingToken.IsCancellationRequested)
41 | {
42 | _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
43 |
44 | await RunSyncWallabag();
45 | int delay = _settings.Interval * 60000;
46 |
47 | if (delay > 0)
48 | {
49 | _logger.LogInformation($"Worker paused for: {_settings.Interval} minutes");
50 |
51 | await Task.Delay(delay, stoppingToken);
52 | }
53 | else
54 | {
55 | _logger.LogInformation($"Delay was set to '0' --> stopping worker");
56 | _hostApplicationLifetime.StopApplication();
57 | }
58 | }
59 | }
60 |
61 | public async Task RunSyncWallabag()
62 | {
63 | if (!string.IsNullOrEmpty(_wallabagSettings.Url))
64 | {
65 |
66 | _logger.LogInformation($"Starting updating bookmarks for {_linkdingSettings.Url}");
67 | _logger.LogInformation("Collectin LinkdingService Handler");
68 | var wallabagHandlers = AppDomain.CurrentDomain.GetAssemblies()
69 | .SelectMany(s => s.GetTypes())
70 | .Where(p => typeof(ISyncTaskHandler).IsAssignableFrom(p) && p.IsClass);
71 |
72 | if (wallabagHandlers != null && wallabagHandlers.Count() > 0)
73 | {
74 | var wallabags = await _wallabagService.GetEntries();
75 |
76 | foreach (var handler in wallabagHandlers)
77 | {
78 | ISyncTaskHandler handlerInstance = null;
79 | try
80 | {
81 | handlerInstance = (ISyncTaskHandler) Activator.CreateInstance(handler);
82 |
83 | await handlerInstance.ProcessAsync(wallabags, _wallabagService, _linkdingService, _logger, _configuration);
84 | }
85 | catch (Exception e)
86 | {
87 | Console.WriteLine(e);
88 | throw;
89 | }
90 | }
91 | }
92 |
93 | _logger.LogInformation($"no bookmarks found in {_linkdingSettings.Url}");
94 | }
95 | }
96 | }
--------------------------------------------------------------------------------
/src/Wallabag/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.Hosting.Lifetime": "Information"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/Wallabag/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.Hosting.Lifetime": "Information"
6 | }
7 | }
8 | }
--------------------------------------------------------------------------------
/src/Wallabag/data/config.yml:
--------------------------------------------------------------------------------
1 | excludedDomains:
2 | - name: youtube
3 | pattern: https://[[a-zA-Z0-9]+\.]?(youtube)\.com(?:/.*)?
4 | - name: ebay
5 | pattern: https://[[a-zA-Z0-9]+\.]?(ebay)\.(com|de|fr)(?:/.*)?
6 | - name: amazon
7 | pattern: https://[[a-zA-Z0-9]+\.]?(amazon)\.(com|de|fr)(?:/.*)?
--------------------------------------------------------------------------------