mirror of
https://github.com/spaytac/linkdingsync.git
synced 2026-01-21 16:54:45 +00:00
**update** initial commit
This commit is contained in:
commit
0a1e04d30d
27
.dockerignore
Normal file
27
.dockerignore
Normal file
@ -0,0 +1,27 @@
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/.idea
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
**/appsettings.json
|
||||
**/appsettings.*.json
|
||||
LICENSE
|
||||
README.md
|
||||
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
bin/
|
||||
obj/
|
||||
/packages/
|
||||
riderModule.iml
|
||||
/_ReSharper.Caches/
|
||||
**/.env
|
||||
.env
|
||||
13
.idea/.idea.Linkding.Sync/.idea/.gitignore
vendored
Normal file
13
.idea/.idea.Linkding.Sync/.idea/.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Rider ignored files
|
||||
/contentModel.xml
|
||||
/.idea.Linkding.Sync.iml
|
||||
/modules.xml
|
||||
/projectSettingsUpdater.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
13
.idea/.idea.Linkding.Sync/.idea/active-tab-highlighter.xml
Normal file
13
.idea/.idea.Linkding.Sync/.idea/active-tab-highlighter.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ActiveTabHighlighterConfiguration">
|
||||
<option name="background">
|
||||
<PersistentColor>
|
||||
<option name="enabled" value="true" />
|
||||
<option name="red" value="173" />
|
||||
<option name="green" value="46" />
|
||||
<option name="blue" value="156" />
|
||||
</PersistentColor>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
11
.idea/.idea.Linkding.Sync/.idea/aws.xml
Normal file
11
.idea/.idea.Linkding.Sync/.idea/aws.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="accountSettings">
|
||||
<option name="activeRegion" value="us-east-1" />
|
||||
<option name="recentlyUsedRegions">
|
||||
<list>
|
||||
<option value="us-east-1" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
414
.idea/.idea.Linkding.Sync/.idea/dbnavigator.xml
Normal file
414
.idea/.idea.Linkding.Sync/.idea/dbnavigator.xml
Normal file
@ -0,0 +1,414 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DBNavigator.Project.DataEditorManager">
|
||||
<record-view-column-sorting-type value="BY_INDEX" />
|
||||
<value-preview-text-wrapping value="true" />
|
||||
<value-preview-pinned value="false" />
|
||||
</component>
|
||||
<component name="DBNavigator.Project.DatabaseEditorStateManager">
|
||||
<last-used-providers />
|
||||
</component>
|
||||
<component name="DBNavigator.Project.DatabaseFileManager">
|
||||
<open-files />
|
||||
</component>
|
||||
<component name="DBNavigator.Project.Settings">
|
||||
<connections />
|
||||
<browser-settings>
|
||||
<general>
|
||||
<display-mode value="TABBED" />
|
||||
<navigation-history-size value="100" />
|
||||
<show-object-details value="false" />
|
||||
</general>
|
||||
<filters>
|
||||
<object-type-filter>
|
||||
<object-type name="SCHEMA" enabled="true" />
|
||||
<object-type name="USER" enabled="true" />
|
||||
<object-type name="ROLE" enabled="true" />
|
||||
<object-type name="PRIVILEGE" enabled="true" />
|
||||
<object-type name="CHARSET" enabled="true" />
|
||||
<object-type name="TABLE" enabled="true" />
|
||||
<object-type name="VIEW" enabled="true" />
|
||||
<object-type name="MATERIALIZED_VIEW" enabled="true" />
|
||||
<object-type name="NESTED_TABLE" enabled="true" />
|
||||
<object-type name="COLUMN" enabled="true" />
|
||||
<object-type name="INDEX" enabled="true" />
|
||||
<object-type name="CONSTRAINT" enabled="true" />
|
||||
<object-type name="DATASET_TRIGGER" enabled="true" />
|
||||
<object-type name="DATABASE_TRIGGER" enabled="true" />
|
||||
<object-type name="SYNONYM" enabled="true" />
|
||||
<object-type name="SEQUENCE" enabled="true" />
|
||||
<object-type name="PROCEDURE" enabled="true" />
|
||||
<object-type name="FUNCTION" enabled="true" />
|
||||
<object-type name="PACKAGE" enabled="true" />
|
||||
<object-type name="TYPE" enabled="true" />
|
||||
<object-type name="TYPE_ATTRIBUTE" enabled="true" />
|
||||
<object-type name="ARGUMENT" enabled="true" />
|
||||
<object-type name="DIMENSION" enabled="true" />
|
||||
<object-type name="CLUSTER" enabled="true" />
|
||||
<object-type name="DBLINK" enabled="true" />
|
||||
</object-type-filter>
|
||||
</filters>
|
||||
<sorting>
|
||||
<object-type name="COLUMN" sorting-type="NAME" />
|
||||
<object-type name="FUNCTION" sorting-type="NAME" />
|
||||
<object-type name="PROCEDURE" sorting-type="NAME" />
|
||||
<object-type name="ARGUMENT" sorting-type="POSITION" />
|
||||
<object-type name="TYPE ATTRIBUTE" sorting-type="POSITION" />
|
||||
</sorting>
|
||||
<default-editors>
|
||||
<object-type name="VIEW" editor-type="SELECTION" />
|
||||
<object-type name="PACKAGE" editor-type="SELECTION" />
|
||||
<object-type name="TYPE" editor-type="SELECTION" />
|
||||
</default-editors>
|
||||
</browser-settings>
|
||||
<navigation-settings>
|
||||
<lookup-filters>
|
||||
<lookup-objects>
|
||||
<object-type name="SCHEMA" enabled="true" />
|
||||
<object-type name="USER" enabled="false" />
|
||||
<object-type name="ROLE" enabled="false" />
|
||||
<object-type name="PRIVILEGE" enabled="false" />
|
||||
<object-type name="CHARSET" enabled="false" />
|
||||
<object-type name="TABLE" enabled="true" />
|
||||
<object-type name="VIEW" enabled="true" />
|
||||
<object-type name="MATERIALIZED VIEW" enabled="true" />
|
||||
<object-type name="INDEX" enabled="true" />
|
||||
<object-type name="CONSTRAINT" enabled="true" />
|
||||
<object-type name="DATASET TRIGGER" enabled="true" />
|
||||
<object-type name="DATABASE TRIGGER" enabled="true" />
|
||||
<object-type name="SYNONYM" enabled="false" />
|
||||
<object-type name="SEQUENCE" enabled="true" />
|
||||
<object-type name="PROCEDURE" enabled="true" />
|
||||
<object-type name="FUNCTION" enabled="true" />
|
||||
<object-type name="PACKAGE" enabled="true" />
|
||||
<object-type name="TYPE" enabled="true" />
|
||||
<object-type name="DIMENSION" enabled="false" />
|
||||
<object-type name="CLUSTER" enabled="false" />
|
||||
<object-type name="DBLINK" enabled="true" />
|
||||
</lookup-objects>
|
||||
<force-database-load value="false" />
|
||||
<prompt-connection-selection value="true" />
|
||||
<prompt-schema-selection value="true" />
|
||||
</lookup-filters>
|
||||
</navigation-settings>
|
||||
<dataset-grid-settings>
|
||||
<general>
|
||||
<enable-zooming value="true" />
|
||||
<enable-column-tooltip value="true" />
|
||||
</general>
|
||||
<sorting>
|
||||
<nulls-first value="true" />
|
||||
<max-sorting-columns value="4" />
|
||||
</sorting>
|
||||
<audit-columns>
|
||||
<column-names value="" />
|
||||
<visible value="true" />
|
||||
<editable value="false" />
|
||||
</audit-columns>
|
||||
</dataset-grid-settings>
|
||||
<dataset-editor-settings>
|
||||
<text-editor-popup>
|
||||
<active value="false" />
|
||||
<active-if-empty value="false" />
|
||||
<data-length-threshold value="100" />
|
||||
<popup-delay value="1000" />
|
||||
</text-editor-popup>
|
||||
<values-actions-popup>
|
||||
<show-popup-button value="true" />
|
||||
<element-count-threshold value="1000" />
|
||||
<data-length-threshold value="250" />
|
||||
</values-actions-popup>
|
||||
<general>
|
||||
<fetch-block-size value="100" />
|
||||
<fetch-timeout value="30" />
|
||||
<trim-whitespaces value="true" />
|
||||
<convert-empty-strings-to-null value="true" />
|
||||
<select-content-on-cell-edit value="true" />
|
||||
<large-value-preview-active value="true" />
|
||||
</general>
|
||||
<filters>
|
||||
<prompt-filter-dialog value="true" />
|
||||
<default-filter-type value="BASIC" />
|
||||
</filters>
|
||||
<qualified-text-editor text-length-threshold="300">
|
||||
<content-types>
|
||||
<content-type name="Text" enabled="true" />
|
||||
<content-type name="Properties" enabled="true" />
|
||||
<content-type name="XML" enabled="true" />
|
||||
<content-type name="DTD" enabled="true" />
|
||||
<content-type name="HTML" enabled="true" />
|
||||
<content-type name="XHTML" enabled="true" />
|
||||
<content-type name="CSS" enabled="true" />
|
||||
<content-type name="SQL" enabled="true" />
|
||||
<content-type name="PL/SQL" enabled="true" />
|
||||
<content-type name="JavaScript" enabled="true" />
|
||||
<content-type name="JSON" enabled="true" />
|
||||
<content-type name="JSON5" enabled="true" />
|
||||
<content-type name="YAML" enabled="true" />
|
||||
<content-type name="C#" enabled="true" />
|
||||
<content-type name="C++" enabled="true" />
|
||||
</content-types>
|
||||
</qualified-text-editor>
|
||||
<record-navigation>
|
||||
<navigation-target value="VIEWER" />
|
||||
</record-navigation>
|
||||
</dataset-editor-settings>
|
||||
<code-editor-settings>
|
||||
<general>
|
||||
<show-object-navigation-gutter value="false" />
|
||||
<show-spec-declaration-navigation-gutter value="true" />
|
||||
<enable-spellchecking value="true" />
|
||||
<enable-reference-spellchecking value="false" />
|
||||
</general>
|
||||
<confirmations>
|
||||
<save-changes value="false" />
|
||||
<revert-changes value="true" />
|
||||
</confirmations>
|
||||
</code-editor-settings>
|
||||
<code-completion-settings>
|
||||
<filters>
|
||||
<basic-filter>
|
||||
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
|
||||
<filter-element type="RESERVED_WORD" id="function" selected="true" />
|
||||
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
|
||||
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
|
||||
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
|
||||
<filter-element type="OBJECT" id="schema" selected="true" />
|
||||
<filter-element type="OBJECT" id="role" selected="true" />
|
||||
<filter-element type="OBJECT" id="user" selected="true" />
|
||||
<filter-element type="OBJECT" id="privilege" selected="true" />
|
||||
<user-schema>
|
||||
<filter-element type="OBJECT" id="table" selected="true" />
|
||||
<filter-element type="OBJECT" id="view" selected="true" />
|
||||
<filter-element type="OBJECT" id="materialized view" selected="true" />
|
||||
<filter-element type="OBJECT" id="index" selected="true" />
|
||||
<filter-element type="OBJECT" id="constraint" selected="true" />
|
||||
<filter-element type="OBJECT" id="trigger" selected="true" />
|
||||
<filter-element type="OBJECT" id="synonym" selected="false" />
|
||||
<filter-element type="OBJECT" id="sequence" selected="true" />
|
||||
<filter-element type="OBJECT" id="procedure" selected="true" />
|
||||
<filter-element type="OBJECT" id="function" selected="true" />
|
||||
<filter-element type="OBJECT" id="package" selected="true" />
|
||||
<filter-element type="OBJECT" id="type" selected="true" />
|
||||
<filter-element type="OBJECT" id="dimension" selected="true" />
|
||||
<filter-element type="OBJECT" id="cluster" selected="true" />
|
||||
<filter-element type="OBJECT" id="dblink" selected="true" />
|
||||
</user-schema>
|
||||
<public-schema>
|
||||
<filter-element type="OBJECT" id="table" selected="false" />
|
||||
<filter-element type="OBJECT" id="view" selected="false" />
|
||||
<filter-element type="OBJECT" id="materialized view" selected="false" />
|
||||
<filter-element type="OBJECT" id="index" selected="false" />
|
||||
<filter-element type="OBJECT" id="constraint" selected="false" />
|
||||
<filter-element type="OBJECT" id="trigger" selected="false" />
|
||||
<filter-element type="OBJECT" id="synonym" selected="false" />
|
||||
<filter-element type="OBJECT" id="sequence" selected="false" />
|
||||
<filter-element type="OBJECT" id="procedure" selected="false" />
|
||||
<filter-element type="OBJECT" id="function" selected="false" />
|
||||
<filter-element type="OBJECT" id="package" selected="false" />
|
||||
<filter-element type="OBJECT" id="type" selected="false" />
|
||||
<filter-element type="OBJECT" id="dimension" selected="false" />
|
||||
<filter-element type="OBJECT" id="cluster" selected="false" />
|
||||
<filter-element type="OBJECT" id="dblink" selected="false" />
|
||||
</public-schema>
|
||||
<any-schema>
|
||||
<filter-element type="OBJECT" id="table" selected="true" />
|
||||
<filter-element type="OBJECT" id="view" selected="true" />
|
||||
<filter-element type="OBJECT" id="materialized view" selected="true" />
|
||||
<filter-element type="OBJECT" id="index" selected="true" />
|
||||
<filter-element type="OBJECT" id="constraint" selected="true" />
|
||||
<filter-element type="OBJECT" id="trigger" selected="true" />
|
||||
<filter-element type="OBJECT" id="synonym" selected="true" />
|
||||
<filter-element type="OBJECT" id="sequence" selected="true" />
|
||||
<filter-element type="OBJECT" id="procedure" selected="true" />
|
||||
<filter-element type="OBJECT" id="function" selected="true" />
|
||||
<filter-element type="OBJECT" id="package" selected="true" />
|
||||
<filter-element type="OBJECT" id="type" selected="true" />
|
||||
<filter-element type="OBJECT" id="dimension" selected="true" />
|
||||
<filter-element type="OBJECT" id="cluster" selected="true" />
|
||||
<filter-element type="OBJECT" id="dblink" selected="true" />
|
||||
</any-schema>
|
||||
</basic-filter>
|
||||
<extended-filter>
|
||||
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
|
||||
<filter-element type="RESERVED_WORD" id="function" selected="true" />
|
||||
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
|
||||
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
|
||||
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
|
||||
<filter-element type="OBJECT" id="schema" selected="true" />
|
||||
<filter-element type="OBJECT" id="user" selected="true" />
|
||||
<filter-element type="OBJECT" id="role" selected="true" />
|
||||
<filter-element type="OBJECT" id="privilege" selected="true" />
|
||||
<user-schema>
|
||||
<filter-element type="OBJECT" id="table" selected="true" />
|
||||
<filter-element type="OBJECT" id="view" selected="true" />
|
||||
<filter-element type="OBJECT" id="materialized view" selected="true" />
|
||||
<filter-element type="OBJECT" id="index" selected="true" />
|
||||
<filter-element type="OBJECT" id="constraint" selected="true" />
|
||||
<filter-element type="OBJECT" id="trigger" selected="true" />
|
||||
<filter-element type="OBJECT" id="synonym" selected="true" />
|
||||
<filter-element type="OBJECT" id="sequence" selected="true" />
|
||||
<filter-element type="OBJECT" id="procedure" selected="true" />
|
||||
<filter-element type="OBJECT" id="function" selected="true" />
|
||||
<filter-element type="OBJECT" id="package" selected="true" />
|
||||
<filter-element type="OBJECT" id="type" selected="true" />
|
||||
<filter-element type="OBJECT" id="dimension" selected="true" />
|
||||
<filter-element type="OBJECT" id="cluster" selected="true" />
|
||||
<filter-element type="OBJECT" id="dblink" selected="true" />
|
||||
</user-schema>
|
||||
<public-schema>
|
||||
<filter-element type="OBJECT" id="table" selected="true" />
|
||||
<filter-element type="OBJECT" id="view" selected="true" />
|
||||
<filter-element type="OBJECT" id="materialized view" selected="true" />
|
||||
<filter-element type="OBJECT" id="index" selected="true" />
|
||||
<filter-element type="OBJECT" id="constraint" selected="true" />
|
||||
<filter-element type="OBJECT" id="trigger" selected="true" />
|
||||
<filter-element type="OBJECT" id="synonym" selected="true" />
|
||||
<filter-element type="OBJECT" id="sequence" selected="true" />
|
||||
<filter-element type="OBJECT" id="procedure" selected="true" />
|
||||
<filter-element type="OBJECT" id="function" selected="true" />
|
||||
<filter-element type="OBJECT" id="package" selected="true" />
|
||||
<filter-element type="OBJECT" id="type" selected="true" />
|
||||
<filter-element type="OBJECT" id="dimension" selected="true" />
|
||||
<filter-element type="OBJECT" id="cluster" selected="true" />
|
||||
<filter-element type="OBJECT" id="dblink" selected="true" />
|
||||
</public-schema>
|
||||
<any-schema>
|
||||
<filter-element type="OBJECT" id="table" selected="true" />
|
||||
<filter-element type="OBJECT" id="view" selected="true" />
|
||||
<filter-element type="OBJECT" id="materialized view" selected="true" />
|
||||
<filter-element type="OBJECT" id="index" selected="true" />
|
||||
<filter-element type="OBJECT" id="constraint" selected="true" />
|
||||
<filter-element type="OBJECT" id="trigger" selected="true" />
|
||||
<filter-element type="OBJECT" id="synonym" selected="true" />
|
||||
<filter-element type="OBJECT" id="sequence" selected="true" />
|
||||
<filter-element type="OBJECT" id="procedure" selected="true" />
|
||||
<filter-element type="OBJECT" id="function" selected="true" />
|
||||
<filter-element type="OBJECT" id="package" selected="true" />
|
||||
<filter-element type="OBJECT" id="type" selected="true" />
|
||||
<filter-element type="OBJECT" id="dimension" selected="true" />
|
||||
<filter-element type="OBJECT" id="cluster" selected="true" />
|
||||
<filter-element type="OBJECT" id="dblink" selected="true" />
|
||||
</any-schema>
|
||||
</extended-filter>
|
||||
</filters>
|
||||
<sorting enabled="true">
|
||||
<sorting-element type="RESERVED_WORD" id="keyword" />
|
||||
<sorting-element type="RESERVED_WORD" id="datatype" />
|
||||
<sorting-element type="OBJECT" id="column" />
|
||||
<sorting-element type="OBJECT" id="table" />
|
||||
<sorting-element type="OBJECT" id="view" />
|
||||
<sorting-element type="OBJECT" id="materialized view" />
|
||||
<sorting-element type="OBJECT" id="index" />
|
||||
<sorting-element type="OBJECT" id="constraint" />
|
||||
<sorting-element type="OBJECT" id="trigger" />
|
||||
<sorting-element type="OBJECT" id="synonym" />
|
||||
<sorting-element type="OBJECT" id="sequence" />
|
||||
<sorting-element type="OBJECT" id="procedure" />
|
||||
<sorting-element type="OBJECT" id="function" />
|
||||
<sorting-element type="OBJECT" id="package" />
|
||||
<sorting-element type="OBJECT" id="type" />
|
||||
<sorting-element type="OBJECT" id="dimension" />
|
||||
<sorting-element type="OBJECT" id="cluster" />
|
||||
<sorting-element type="OBJECT" id="dblink" />
|
||||
<sorting-element type="OBJECT" id="schema" />
|
||||
<sorting-element type="OBJECT" id="role" />
|
||||
<sorting-element type="OBJECT" id="user" />
|
||||
<sorting-element type="RESERVED_WORD" id="function" />
|
||||
<sorting-element type="RESERVED_WORD" id="parameter" />
|
||||
</sorting>
|
||||
<format>
|
||||
<enforce-code-style-case value="true" />
|
||||
</format>
|
||||
</code-completion-settings>
|
||||
<execution-engine-settings>
|
||||
<statement-execution>
|
||||
<fetch-block-size value="100" />
|
||||
<execution-timeout value="20" />
|
||||
<debug-execution-timeout value="600" />
|
||||
<focus-result value="false" />
|
||||
<prompt-execution value="false" />
|
||||
</statement-execution>
|
||||
<script-execution>
|
||||
<command-line-interfaces />
|
||||
<execution-timeout value="300" />
|
||||
</script-execution>
|
||||
<method-execution>
|
||||
<execution-timeout value="30" />
|
||||
<debug-execution-timeout value="600" />
|
||||
<parameter-history-size value="10" />
|
||||
</method-execution>
|
||||
</execution-engine-settings>
|
||||
<operation-settings>
|
||||
<transactions>
|
||||
<uncommitted-changes>
|
||||
<on-project-close value="ASK" />
|
||||
<on-disconnect value="ASK" />
|
||||
<on-autocommit-toggle value="ASK" />
|
||||
</uncommitted-changes>
|
||||
<multiple-uncommitted-changes>
|
||||
<on-commit value="ASK" />
|
||||
<on-rollback value="ASK" />
|
||||
</multiple-uncommitted-changes>
|
||||
</transactions>
|
||||
<session-browser>
|
||||
<disconnect-session value="ASK" />
|
||||
<kill-session value="ASK" />
|
||||
<reload-on-filter-change value="false" />
|
||||
</session-browser>
|
||||
<compiler>
|
||||
<compile-type value="KEEP" />
|
||||
<compile-dependencies value="ASK" />
|
||||
<always-show-controls value="false" />
|
||||
</compiler>
|
||||
<debugger>
|
||||
<debugger-type value="JDBC" />
|
||||
<use-generic-runners value="true" />
|
||||
</debugger>
|
||||
</operation-settings>
|
||||
<ddl-file-settings>
|
||||
<extensions>
|
||||
<mapping file-type-id="VIEW" extensions="vw" />
|
||||
<mapping file-type-id="TRIGGER" extensions="trg" />
|
||||
<mapping file-type-id="PROCEDURE" extensions="prc" />
|
||||
<mapping file-type-id="FUNCTION" extensions="fnc" />
|
||||
<mapping file-type-id="PACKAGE" extensions="pkg" />
|
||||
<mapping file-type-id="PACKAGE_SPEC" extensions="pks" />
|
||||
<mapping file-type-id="PACKAGE_BODY" extensions="pkb" />
|
||||
<mapping file-type-id="TYPE" extensions="tpe" />
|
||||
<mapping file-type-id="TYPE_SPEC" extensions="tps" />
|
||||
<mapping file-type-id="TYPE_BODY" extensions="tpb" />
|
||||
</extensions>
|
||||
<general>
|
||||
<lookup-ddl-files value="true" />
|
||||
<create-ddl-files value="false" />
|
||||
<synchronize-ddl-files value="true" />
|
||||
<use-qualified-names value="false" />
|
||||
<make-scripts-rerunnable value="true" />
|
||||
</general>
|
||||
</ddl-file-settings>
|
||||
<general-settings>
|
||||
<regional-settings>
|
||||
<date-format value="MEDIUM" />
|
||||
<number-format value="UNGROUPED" />
|
||||
<locale value="SYSTEM_DEFAULT" />
|
||||
<use-custom-formats value="false" />
|
||||
</regional-settings>
|
||||
<environment>
|
||||
<environment-types>
|
||||
<environment-type id="development" name="Development" description="Development environment" color="-2430209/-12296320" readonly-code="false" readonly-data="false" />
|
||||
<environment-type id="integration" name="Integration" description="Integration environment" color="-2621494/-12163514" readonly-code="true" readonly-data="false" />
|
||||
<environment-type id="production" name="Production" description="Productive environment" color="-11574/-10271420" readonly-code="true" readonly-data="true" />
|
||||
<environment-type id="other" name="Other" description="" color="-1576/-10724543" readonly-code="false" readonly-data="false" />
|
||||
</environment-types>
|
||||
<visibility-settings>
|
||||
<connection-tabs value="true" />
|
||||
<dialog-headers value="true" />
|
||||
<object-editor-tabs value="true" />
|
||||
<script-editor-tabs value="false" />
|
||||
<execution-result-tabs value="true" />
|
||||
</visibility-settings>
|
||||
</environment>
|
||||
</general-settings>
|
||||
</component>
|
||||
</project>
|
||||
4
.idea/.idea.Linkding.Sync/.idea/encodings.xml
Normal file
4
.idea/.idea.Linkding.Sync/.idea/encodings.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||
</project>
|
||||
15
.idea/.idea.Linkding.Sync/.idea/git_toolbox_prj.xml
Normal file
15
.idea/.idea.Linkding.Sync/.idea/git_toolbox_prj.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GitToolBoxProjectSettings">
|
||||
<option name="commitMessageIssueKeyValidationOverride">
|
||||
<BoolValueOverride>
|
||||
<option name="enabled" value="true" />
|
||||
</BoolValueOverride>
|
||||
</option>
|
||||
<option name="commitMessageValidationEnabledOverride">
|
||||
<BoolValueOverride>
|
||||
<option name="enabled" value="true" />
|
||||
</BoolValueOverride>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
10
.idea/.idea.Linkding.Sync/.idea/indexLayout.xml
Normal file
10
.idea/.idea.Linkding.Sync/.idea/indexLayout.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="UserContentModel">
|
||||
<attachedFolders>
|
||||
<Path>examples</Path>
|
||||
</attachedFolders>
|
||||
<explicitIncludes />
|
||||
<explicitExcludes />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/.idea.Linkding.Sync/.idea/vcs.xml
Normal file
6
.idea/.idea.Linkding.Sync/.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
19
Dockerfile_Linkding
Normal file
19
Dockerfile_Linkding
Normal file
@ -0,0 +1,19 @@
|
||||
FROM mcr.microsoft.com/dotnet/runtime:6.0 AS base
|
||||
WORKDIR /app
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ["src/Linkding/Linkding.csproj", "src/Linkding/"]
|
||||
RUN dotnet restore "src/Linkding/Linkding.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/src/Linkding"
|
||||
RUN dotnet build "Linkding.csproj" -c Release -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "Linkding.csproj" -c Release -o /app/publish
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
RUN mkdir ./data
|
||||
ENTRYPOINT ["dotnet", "Linkding.dll"]
|
||||
19
Dockerfile_Wallabag
Normal file
19
Dockerfile_Wallabag
Normal file
@ -0,0 +1,19 @@
|
||||
FROM mcr.microsoft.com/dotnet/runtime:6.0 AS base
|
||||
WORKDIR /app
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ["src/Wallabag/Wallabag.csproj", "src/Wallabag/"]
|
||||
RUN dotnet restore "src/Wallabag/Wallabag.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/src/Wallabag"
|
||||
RUN dotnet build "Wallabag.csproj" -c Release -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "Wallabag.csproj" -c Release -o /app/publish
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
RUN mkdir ./data
|
||||
ENTRYPOINT ["dotnet", "Wallabag.dll"]
|
||||
61
Linkding.Sync.sln
Normal file
61
Linkding.Sync.sln
Normal file
@ -0,0 +1,61 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3167CB7E-6412-49DB-BB42-E91E07B51C5E}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
README.md = README.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Services", "Services", "{2775FBD9-7954-4A8B-8831-31C7670209A3}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Linkding.Client", "src\Services\Linkding.Client\Linkding.Client.csproj", "{1880BB32-4013-45F5-9DB4-6864F9836525}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wallabag.Client", "src\Services\Wallabag.Client\Wallabag.Client.csproj", "{2776E05F-F35A-4421-A792-0F9B0CC48475}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Domain", "Domain", "{586A8F51-1A5D-42B9-8A37-8B2010905EB1}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "src\Domain\Core\Core.csproj", "{0EA1E116-6560-4C1D-8EC8-6ACE3F5E1C25}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{953FCC4E-C759-47DD-B20A-78FBA2E34F35}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wallabag", "src\Wallabag\Wallabag.csproj", "{5EFB26BC-CD34-4DE6-B0E3-9F1E387D4177}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Linkding", "src\Linkding\Linkding.csproj", "{3E78F171-D237-46DF-8A27-DAADE7CA1940}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{1880BB32-4013-45F5-9DB4-6864F9836525}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1880BB32-4013-45F5-9DB4-6864F9836525}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1880BB32-4013-45F5-9DB4-6864F9836525}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1880BB32-4013-45F5-9DB4-6864F9836525}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2776E05F-F35A-4421-A792-0F9B0CC48475}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2776E05F-F35A-4421-A792-0F9B0CC48475}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2776E05F-F35A-4421-A792-0F9B0CC48475}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2776E05F-F35A-4421-A792-0F9B0CC48475}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0EA1E116-6560-4C1D-8EC8-6ACE3F5E1C25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0EA1E116-6560-4C1D-8EC8-6ACE3F5E1C25}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0EA1E116-6560-4C1D-8EC8-6ACE3F5E1C25}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0EA1E116-6560-4C1D-8EC8-6ACE3F5E1C25}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5EFB26BC-CD34-4DE6-B0E3-9F1E387D4177}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5EFB26BC-CD34-4DE6-B0E3-9F1E387D4177}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5EFB26BC-CD34-4DE6-B0E3-9F1E387D4177}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5EFB26BC-CD34-4DE6-B0E3-9F1E387D4177}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3E78F171-D237-46DF-8A27-DAADE7CA1940}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3E78F171-D237-46DF-8A27-DAADE7CA1940}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3E78F171-D237-46DF-8A27-DAADE7CA1940}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3E78F171-D237-46DF-8A27-DAADE7CA1940}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{2775FBD9-7954-4A8B-8831-31C7670209A3} = {3167CB7E-6412-49DB-BB42-E91E07B51C5E}
|
||||
{1880BB32-4013-45F5-9DB4-6864F9836525} = {2775FBD9-7954-4A8B-8831-31C7670209A3}
|
||||
{2776E05F-F35A-4421-A792-0F9B0CC48475} = {2775FBD9-7954-4A8B-8831-31C7670209A3}
|
||||
{586A8F51-1A5D-42B9-8A37-8B2010905EB1} = {3167CB7E-6412-49DB-BB42-E91E07B51C5E}
|
||||
{0EA1E116-6560-4C1D-8EC8-6ACE3F5E1C25} = {586A8F51-1A5D-42B9-8A37-8B2010905EB1}
|
||||
{953FCC4E-C759-47DD-B20A-78FBA2E34F35} = {3167CB7E-6412-49DB-BB42-E91E07B51C5E}
|
||||
{5EFB26BC-CD34-4DE6-B0E3-9F1E387D4177} = {3167CB7E-6412-49DB-BB42-E91E07B51C5E}
|
||||
{3E78F171-D237-46DF-8A27-DAADE7CA1940} = {3167CB7E-6412-49DB-BB42-E91E07B51C5E}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
126
README.md
Normal file
126
README.md
Normal file
@ -0,0 +1,126 @@
|
||||
# Linkding Sync
|
||||
LinkdingSync is a collection of tools that make life with [Linkding](https://github.com/sissbruecker/linkding) easier.
|
||||
|
||||
One of the workers is for syncing to [Wallabag](https://wallabag.org/en).
|
||||
|
||||
## Getting Started
|
||||
It is recommended to use the Docker images. Otherwise, a .NET 6 environment is required to customize and build the code.
|
||||
|
||||
## Environment Variables
|
||||
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.
|
||||
|
||||
### WallabagSync
|
||||
Environment variables for the wallabag worker.
|
||||
|
||||
| Variable | Value | Description | Attention |
|
||||
|------------------------|-----------|--------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Worker__Intervall | 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) |
|
||||
| Worker__SyncTag | text | The linkding tag to create the bookmarks in Wallabag. (default value 'readlater') | |
|
||||
| Linkding__Url | text | URL to the linkding instance | |
|
||||
| Linkding__Key | text | The linkding application key | [Instructions](https://github.com/sissbruecker/linkding/blob/master/docs/API.md) |
|
||||
| Wallabag__Url | text | URL to the Wallabag instance | |
|
||||
| Wallabag__Username | text | Wallabag User Name | |
|
||||
| Wallabag__Password | text | Wallabag User Password | |
|
||||
| Wallabag__ClientId | text | Wallabag Client Id | |
|
||||
| Wallabag__ClientSecret | text | Wallabag Client Secret | |
|
||||
|
||||
### LinkdingUpdater
|
||||
Environment variables for the linkding worker.
|
||||
|
||||
| Variable | Value | Description | Attention |
|
||||
|------------------------|-----------|--------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Worker__Intervall | 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) |
|
||||
| Worker__SyncTag | text | The linkding tag to create the bookmarks in Wallabag. | |
|
||||
| Linkding__Url | text | URL to the linkding instance | |
|
||||
| Linkding__Key | text | The linkding application key | [Instructions](https://github.com/sissbruecker/linkding/blob/master/docs/API.md) |
|
||||
|
||||
## Configuration
|
||||
The following explains the configuration options.
|
||||
### WallabagSync
|
||||
The configuration is optional. In the configuration (**YAML File**) rules can be defined in regex to exclude certain domains from sync.
|
||||
|
||||
Exampel:
|
||||
````yaml
|
||||
excludedDomains:
|
||||
- name: youtube
|
||||
pattern: 'https://[[a-zA-Z0-9]+\.]?(youtube)\.com(?:/.*)?'
|
||||
- name: ebay
|
||||
pattern: 'https://[[a-zA-Z0-9]+\.]?(ebay)\.(com|de|fr)(?:/.*)?'
|
||||
- name: amazon
|
||||
pattern: 'https://[[a-zA-Z0-9]+\.]?(amazon)\.(com|de|fr)(?:/.*)?'
|
||||
````
|
||||
With this configuration every matching bookmark from linkding will be excluded from the sync.
|
||||
|
||||
### LinkdingUpdater
|
||||
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.
|
||||
|
||||
If operated without a configuration file, only the year of the tag is added (currently).
|
||||
|
||||
Example:
|
||||
````yaml
|
||||
urlTagMapping:
|
||||
- name: microsoft_azure
|
||||
url: https://github.com/azure
|
||||
- name: microsoft_azuread
|
||||
url: https://github.com/AzureAD
|
||||
- name: microsoft_dotnet
|
||||
url: https://github.com/dotnet-architecture
|
||||
|
||||
taggingRule:
|
||||
- name: reddit
|
||||
pattern: https://(?:www\.)?(reddit)\.com(?:/r/)?([ a-zA-Z0-9\-\+_ ]+)?(?:/.*)?
|
||||
replace: $1,$2
|
||||
- name: microsoft
|
||||
pattern: https://([ a-zA-Z0-9 ]+)?[ \. ]?(microsoft)\.com(?:/.*)?
|
||||
replace: $1,$2
|
||||
- name: microsoft_docs
|
||||
pattern: 'https://(?:docs)\.(?:microsoft)\.com[ / ]?(?: [ a-zA-Z0-9\-\+_ ]+)(?:/)?([ a-zA-Z0-9\-\+_ ]+)?(?:/)?([ a-zA-Z0-9\-\+_ ]+)?(?:/.*)?'
|
||||
replace: $1,$2
|
||||
- name: youtube
|
||||
pattern: https://[ [ a-zA-Z0-9 ]+\. ]?(youtube)\.com(?:/.*)?
|
||||
replace: $1
|
||||
- name: ebay
|
||||
pattern: https://[ [ a-zA-Z0-9 ]+\. ]?(ebay)\.(com|de|fr)(?:/.*)?
|
||||
replace: $1
|
||||
- name: amazon
|
||||
pattern: https://[ [ a-zA-Z0-9 ]+\. ]?(amazon)\.(com|de|fr)(?:/.*)?
|
||||
replace: $1
|
||||
- name: docker
|
||||
pattern: https://([ a-zA-Z0-9 ]+)?[ \. ]?(docker)\.com(?:/.*)?
|
||||
replace: $1,$2
|
||||
- name: xbox
|
||||
pattern: https://[ [ a-zA-Z0-9 ]+\. ]?(xbox)\.com(?:/.*)?
|
||||
replace: $1
|
||||
- name: github
|
||||
pattern: https://([ a-zA-Z0-9 ]+)?[ \. ]?(github)\.com[ / ]?([ a-zA-Z0-9\-\+_ ]+)(?:/)?([ a-zA-Z0-9\-\+_ ]+)?(?:/.*)?
|
||||
replace: $1,$2,$3,$4
|
||||
- name: github.io
|
||||
pattern: https://([ a-zA-Z0-9 ]+)\.(github)\.io[ / ]?([ a-zA-Z0-9\-\+_ ]+)(?:/)?([ a-zA-Z0-9\-\+_ ]+)?(?:/.*)?
|
||||
replace: $1,$2,$3
|
||||
````
|
||||
|
||||
## Docker Run
|
||||
```
|
||||
docker run --rm -it --env-file .env -v <path>/config.yml:/app/data/config.yml linkdingsync/wallabag:latest
|
||||
```
|
||||
|
||||
You can also
|
||||
|
||||
## Docker Compose
|
||||
You can find [examples](./examples/) in the examples folder..
|
||||
|
||||
- [Wallabag Example](./examples/wallabag/)
|
||||
- [LinkdingUpdater Example](./examples/linkding/)
|
||||
|
||||
|
||||
## Build Docker Image
|
||||
|
||||
### LinkdingUpdater
|
||||
```
|
||||
docker build -t linkdingsync/linkdingupdater:latest -f .\Dockerfile_Linkding .
|
||||
```
|
||||
|
||||
### WallabagSync
|
||||
```
|
||||
docker build -t linkdingsync/wallabag:latest -f .\Dockerfile_Wallabag .
|
||||
```
|
||||
39
examples/linkding/config.yml
Normal file
39
examples/linkding/config.yml
Normal file
@ -0,0 +1,39 @@
|
||||
urlTagMapping:
|
||||
- name: microsoft_azure
|
||||
url: https://github.com/azure
|
||||
- name: microsoft_azuread
|
||||
url: https://github.com/AzureAD
|
||||
- name: microsoft_dotnet
|
||||
url: https://github.com/dotnet-architecture
|
||||
|
||||
taggingRule:
|
||||
- name: reddit
|
||||
pattern: https://(?:www\.)?(reddit)\.com(?:/r/)?([ a-zA-Z0-9\-\+_ ]+)?(?:/.*)?
|
||||
replace: $1,$2
|
||||
- name: microsoft
|
||||
pattern: https://([ a-zA-Z0-9 ]+)?[ \. ]?(microsoft)\.com(?:/.*)?
|
||||
replace: $1,$2
|
||||
- name: microsoft_docs
|
||||
pattern: 'https://(?:docs)\.(?:microsoft)\.com[ / ]?(?: [ a-zA-Z0-9\-\+_ ]+)(?:/)?([ a-zA-Z0-9\-\+_ ]+)?(?:/)?([ a-zA-Z0-9\-\+_ ]+)?(?:/.*)?'
|
||||
replace: $1,$2
|
||||
- name: youtube
|
||||
pattern: https://[ [ a-zA-Z0-9 ]+\. ]?(youtube)\.com(?:/.*)?
|
||||
replace: $1
|
||||
- name: ebay
|
||||
pattern: https://[ [ a-zA-Z0-9 ]+\. ]?(ebay)\.(com|de|fr)(?:/.*)?
|
||||
replace: $1
|
||||
- name: amazon
|
||||
pattern: https://[ [ a-zA-Z0-9 ]+\. ]?(amazon)\.(com|de|fr)(?:/.*)?
|
||||
replace: $1
|
||||
- name: docker
|
||||
pattern: https://([ a-zA-Z0-9 ]+)?[ \. ]?(docker)\.com(?:/.*)?
|
||||
replace: $1,$2
|
||||
- name: xbox
|
||||
pattern: https://[ [ a-zA-Z0-9 ]+\. ]?(xbox)\.com(?:/.*)?
|
||||
replace: $1
|
||||
- name: github
|
||||
pattern: https://([ a-zA-Z0-9 ]+)?[ \. ]?(github)\.com[ / ]?([ a-zA-Z0-9\-\+_ ]+)(?:/)?([ a-zA-Z0-9\-\+_ ]+)?(?:/.*)?
|
||||
replace: $1,$2,$3,$4
|
||||
- name: github.io
|
||||
pattern: https://([ a-zA-Z0-9 ]+)\.(github)\.io[ / ]?([ a-zA-Z0-9\-\+_ ]+)(?:/)?([ a-zA-Z0-9\-\+_ ]+)?(?:/.*)?
|
||||
replace: $1,$2,$3
|
||||
13
examples/linkding/docker-compose.yml
Normal file
13
examples/linkding/docker-compose.yml
Normal file
@ -0,0 +1,13 @@
|
||||
version: '3.9'
|
||||
|
||||
services:
|
||||
linkdingupdater:
|
||||
image: linkdingsync/linkdingupdater:latest
|
||||
volumes:
|
||||
- ./config.yml:/data/config.yml
|
||||
# env_file:
|
||||
# - .env
|
||||
environment:
|
||||
- Worker__Intervall=0
|
||||
- Linkding__Url=https://<url>
|
||||
- Linkding__Key=<secret>
|
||||
7
examples/wallabag/config/config.yml
Normal file
7
examples/wallabag/config/config.yml
Normal file
@ -0,0 +1,7 @@
|
||||
excludedDomains:
|
||||
- name: youtube
|
||||
pattern: 'https://[[a-zA-Z0-9]+\.]?(youtube)\.com(?:/.*)?'
|
||||
- name: ebay
|
||||
pattern: 'https://[[a-zA-Z0-9]+\.]?(ebay)\.(com|de|fr)(?:/.*)?'
|
||||
- name: amazon
|
||||
pattern: 'https://[[a-zA-Z0-9]+\.]?(amazon)\.(com|de|fr)(?:/.*)?'
|
||||
19
examples/wallabag/docker-compose.yml
Normal file
19
examples/wallabag/docker-compose.yml
Normal file
@ -0,0 +1,19 @@
|
||||
version: '3.9'
|
||||
|
||||
services:
|
||||
wallabagsync:
|
||||
image: linkdingsync/wallabag:latest
|
||||
volumes:
|
||||
- ./config.yml:/data/config.yml
|
||||
# env_file:
|
||||
# - .env
|
||||
environment:
|
||||
- Worker__Intervall=0
|
||||
- Worker__SyncTag=<tagName>
|
||||
- Linkding__Url=https://<url>
|
||||
- Linkding__Key=<secret>
|
||||
- Wallabag__Url=https://<url>
|
||||
- Wallabag__Username=<username>
|
||||
- Wallabag__Password=<password>
|
||||
- Wallabag__ClientId=<clientId>
|
||||
- Wallabag__ClientSecret=<secret>
|
||||
7
global.json
Normal file
7
global.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "6.0.0",
|
||||
"rollForward": "latestMinor",
|
||||
"allowPrerelease": false
|
||||
}
|
||||
}
|
||||
17
src/Domain/Core/Abstraction/ILinkdingService.cs
Normal file
17
src/Domain/Core/Abstraction/ILinkdingService.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Core.Entities.Linkding;
|
||||
|
||||
namespace Linkding.Client
|
||||
{
|
||||
public interface ILinkdingService
|
||||
{
|
||||
Task<IEnumerable<Bookmark>> GetBookmarksAsync(int limit = 100, int offset = 0);
|
||||
Task<IEnumerable<Bookmark>> GetAllBookmarksAsync();
|
||||
Task UpdateBookmarkCollectionAsync(IEnumerable<Bookmark> bookmarks);
|
||||
Task UpdateBookmarkCollectionAsync(IEnumerable<BookmarkUpdatePayload> bookmarks);
|
||||
Task UpdateBookmarkAsync(BookmarkUpdatePayload bookmark);
|
||||
Task<BookmarksResult> GetBookmarkResultsAsync(int limit = 100, int offset = 0);
|
||||
Task<BookmarksResult> GetBookmarkResultsAsync(string url);
|
||||
}
|
||||
}
|
||||
38
src/Domain/Core/Abstraction/IWallabagService.cs
Normal file
38
src/Domain/Core/Abstraction/IWallabagService.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading.Tasks;
|
||||
using Core.Entities.Wallabag;
|
||||
|
||||
namespace Core.Abstraction
|
||||
{
|
||||
public interface IWallabagService
|
||||
{
|
||||
Task<AuthenticationHeaderValue> GetAuthenticationHeaderAsync(IEnumerable<string> scopes = null);
|
||||
|
||||
Task<HttpResponseMessage> GetAsync(string endpoint, IEnumerable<string> scopes = null,
|
||||
bool httpCompletionResponseContentRead = false);
|
||||
|
||||
Task<T> GetJsonAsync<T>(string endpoint, IEnumerable<string> scopes = null);
|
||||
|
||||
Task<HttpResponseMessage> PostWithFormDataAsnyc(string endpoint, FormUrlEncodedContent content,
|
||||
IEnumerable<string> scopes = null);
|
||||
|
||||
Task<HttpResponseMessage> PostAsync(string endpoint, HttpContent content,
|
||||
IEnumerable<string> scopes = null);
|
||||
|
||||
Task<HttpResponseMessage> PostAsync(string endpoint, HttpRequestMessage request,
|
||||
IEnumerable<string> scopes = null);
|
||||
|
||||
Task<HttpResponseMessage> PutAsync(string endpoint, HttpContent content,
|
||||
IEnumerable<string> scopes = null);
|
||||
|
||||
Task<HttpResponseMessage> DeleteAsync(string endpoint, IEnumerable<string> scopes = null,
|
||||
HttpContent content = null);
|
||||
|
||||
Task<IEnumerable<WallabagItem>> GetEntries(string format = "json", int limit = 50, bool full = false);
|
||||
Task<WallabagItem> GetEntryById(int id, string format = "json");
|
||||
Task<WallabagItem> AddEntryByUrl(string url, IEnumerable<string> tags = null, string format = "json");
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Wallabag.Client.Converters
|
||||
{
|
||||
public class DateTimeConverterForCustomStandard : JsonConverter<DateTime>
|
||||
{
|
||||
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var dateTimeString = reader.GetString();
|
||||
|
||||
if (string.IsNullOrEmpty(dateTimeString))
|
||||
{
|
||||
return DateTime.MinValue;
|
||||
}
|
||||
DateTime dt = DateTime.ParseExact(dateTimeString, "yyyy-MM-dd'T'HH:mm:ssK",
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AdjustToUniversal);
|
||||
|
||||
return dt;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Wallabag.Client.Converters
|
||||
{
|
||||
public class DateTimeOffsetConverterUsingDateTimeParse : JsonConverter<DateTimeOffset>
|
||||
{
|
||||
public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var dateTimeString = reader.GetString();
|
||||
return DateTimeOffset.Parse(dateTimeString);
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/Domain/Core/Core.csproj
Normal file
14
src/Domain/Core/Core.csproj
Normal file
@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
39
src/Domain/Core/Entities/Linkding/Bookmark.cs
Normal file
39
src/Domain/Core/Entities/Linkding/Bookmark.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Core.Entities.Linkding
|
||||
{
|
||||
public class Bookmark : BookmarkBase
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("website_title")]
|
||||
public string WebsiteTitle { get; set; }
|
||||
[JsonPropertyName("website_description")]
|
||||
public string WebsiteDescription { get; set; }
|
||||
[JsonPropertyName("is_archived")]
|
||||
public bool IsArchived { get; set; }
|
||||
[JsonPropertyName("unread")]
|
||||
public bool Unread { get; set; }
|
||||
[JsonPropertyName("date_added")]
|
||||
public DateTime DateAdded { get; set; }
|
||||
[JsonPropertyName("date_modified")]
|
||||
public DateTime DateModified { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
// "id": 1,
|
||||
// "url": "https://example.com",
|
||||
// "title": "Example title",
|
||||
// "description": "Example description",
|
||||
// "website_title": "Website title",
|
||||
// "website_description": "Website description",
|
||||
// "is_archived": false,
|
||||
// "unread": false,
|
||||
// "tag_names": [
|
||||
// "tag1",
|
||||
// "tag2"
|
||||
// ],
|
||||
// "date_added": "2020-09-26T09:46:23.006313Z",
|
||||
// "date_modified": "2020-09-26T16:01:14.275335Z"
|
||||
17
src/Domain/Core/Entities/Linkding/BookmarkBase.cs
Normal file
17
src/Domain/Core/Entities/Linkding/BookmarkBase.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Core.Entities.Linkding
|
||||
{
|
||||
public class BookmarkBase
|
||||
{
|
||||
[JsonPropertyName("url")]
|
||||
public string Url { get; set; }
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; }
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; set; }
|
||||
[JsonPropertyName("tag_names")]
|
||||
public IEnumerable<string> TagNames { get; set; } = new List<string>();
|
||||
}
|
||||
}
|
||||
12
src/Domain/Core/Entities/Linkding/BookmarkCreatePayload.cs
Normal file
12
src/Domain/Core/Entities/Linkding/BookmarkCreatePayload.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Core.Entities.Linkding
|
||||
{
|
||||
public class BookmarkCreatePayload : BookmarkBase
|
||||
{
|
||||
[JsonPropertyName("is_archived")]
|
||||
public bool IsArchived { get; set; } = false;
|
||||
|
||||
[JsonPropertyName("unread")] public bool Unread { get; set; } = false;
|
||||
}
|
||||
}
|
||||
10
src/Domain/Core/Entities/Linkding/BookmarkUpdatePayload.cs
Normal file
10
src/Domain/Core/Entities/Linkding/BookmarkUpdatePayload.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Core.Entities.Linkding
|
||||
{
|
||||
public class BookmarkUpdatePayload : BookmarkBase
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
namespace Core.Entities.Linkding
|
||||
{
|
||||
public class BookmarkUpdateResult : BookmarkUpdatePayload
|
||||
{
|
||||
public bool Success { get; set; } = true;
|
||||
}
|
||||
}
|
||||
12
src/Domain/Core/Entities/Linkding/BookmarksResult.cs
Normal file
12
src/Domain/Core/Entities/Linkding/BookmarksResult.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Core.Entities.Linkding
|
||||
{
|
||||
public class BookmarksResult
|
||||
{
|
||||
public long Count { get; set; }
|
||||
public string? Next { get; set; }
|
||||
public string? Previous { get; set; }
|
||||
public List<Bookmark?> Results { get; set; }
|
||||
}
|
||||
}
|
||||
20
src/Domain/Core/Entities/Linkding/HandlerResult.cs
Normal file
20
src/Domain/Core/Entities/Linkding/HandlerResult.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using Core.Entities.Linkding;
|
||||
|
||||
namespace LinkdingUpdater.Handler
|
||||
{
|
||||
public class HandlerResult
|
||||
{
|
||||
public bool PerformAction { get; set; } = false;
|
||||
|
||||
public LinkdingItemAction Action { get; set; }
|
||||
public bool HasError { get; set; } = false;
|
||||
public string ErrorMessage { get; set; } = string.Empty;
|
||||
public Bookmark Instance { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
public enum LinkdingItemAction
|
||||
{
|
||||
Update,
|
||||
Delete
|
||||
}
|
||||
15
src/Domain/Core/Entities/Linkding/Tag.cs
Normal file
15
src/Domain/Core/Entities/Linkding/Tag.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Core.Entities.Linkding
|
||||
{
|
||||
public class Tag
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
[JsonPropertyName("date_added")]
|
||||
public DateTime DateAdded { get; set; }
|
||||
}
|
||||
}
|
||||
10
src/Domain/Core/Entities/Linkding/TagCreatePayload.cs
Normal file
10
src/Domain/Core/Entities/Linkding/TagCreatePayload.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Core.Entities.Linkding
|
||||
{
|
||||
public class TagCreatePayload
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
30
src/Domain/Core/Entities/Wallabag/Annotation.cs
Normal file
30
src/Domain/Core/Entities/Wallabag/Annotation.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Wallabag.Client.Converters;
|
||||
|
||||
namespace Core.Entities.Wallabag
|
||||
{
|
||||
public class Annotation
|
||||
{
|
||||
[JsonPropertyName("user")] public string User { get; set; }
|
||||
|
||||
[JsonPropertyName("annotator_schema_version")]
|
||||
public string AnnotatorSchemaVersion { get; set; }
|
||||
|
||||
[JsonPropertyName("id")] public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("text")] public string Text { get; set; }
|
||||
|
||||
|
||||
[JsonConverter(typeof(DateTimeConverterForCustomStandard))]
|
||||
[JsonPropertyName("created_at")] public DateTime? CreatedAt { get; set; } = null;
|
||||
|
||||
[JsonConverter(typeof(DateTimeConverterForCustomStandard))]
|
||||
[JsonPropertyName("updated_at")] public DateTime? UpdatedAt { get; set; } = null;
|
||||
|
||||
[JsonPropertyName("quote")] public string Quote { get; set; }
|
||||
|
||||
[JsonPropertyName("ranges")] public List<Range> Ranges { get; set; }
|
||||
}
|
||||
}
|
||||
11
src/Domain/Core/Entities/Wallabag/Embedded.cs
Normal file
11
src/Domain/Core/Entities/Wallabag/Embedded.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Core.Entities.Wallabag
|
||||
{
|
||||
public class Embedded
|
||||
{
|
||||
[JsonPropertyName("items")]
|
||||
public List<WallabagItem> Items { get; set; }
|
||||
}
|
||||
}
|
||||
28
src/Domain/Core/Entities/Wallabag/Headers.cs
Normal file
28
src/Domain/Core/Entities/Wallabag/Headers.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Core.Entities.Wallabag
|
||||
{
|
||||
public class Headers
|
||||
{
|
||||
[JsonPropertyName("server")] public string Server { get; set; }
|
||||
|
||||
[JsonPropertyName("date")] public string Date { get; set; }
|
||||
|
||||
[JsonPropertyName("content-type")] public string ContentType { get; set; }
|
||||
|
||||
[JsonPropertyName("content-length")] public string ContentLength { get; set; }
|
||||
|
||||
[JsonPropertyName("connection")] public string Connection { get; set; }
|
||||
|
||||
[JsonPropertyName("x-powered-by")] public string XPoweredBy { get; set; }
|
||||
|
||||
[JsonPropertyName("cache-control")] public string CacheControl { get; set; }
|
||||
|
||||
[JsonPropertyName("etag")] public string Etag { get; set; }
|
||||
|
||||
[JsonPropertyName("vary")] public string Vary { get; set; }
|
||||
|
||||
[JsonPropertyName("strict-transport-security")]
|
||||
public string StrictTransportSecurity { get; set; }
|
||||
}
|
||||
}
|
||||
9
src/Domain/Core/Entities/Wallabag/HttpLink.cs
Normal file
9
src/Domain/Core/Entities/Wallabag/HttpLink.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Core.Entities.Wallabag
|
||||
{
|
||||
public class HttpLink
|
||||
{
|
||||
[JsonPropertyName("href")] public string Href { get; set; }
|
||||
}
|
||||
}
|
||||
9
src/Domain/Core/Entities/Wallabag/Links.cs
Normal file
9
src/Domain/Core/Entities/Wallabag/Links.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Core.Entities.Wallabag
|
||||
{
|
||||
public class Links
|
||||
{
|
||||
[JsonPropertyName("self")] public HttpLink? Self { get; set; } = null;
|
||||
}
|
||||
}
|
||||
13
src/Domain/Core/Entities/Wallabag/QueryLinks.cs
Normal file
13
src/Domain/Core/Entities/Wallabag/QueryLinks.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Core.Entities.Wallabag
|
||||
{
|
||||
public class QueryLinks : Links
|
||||
{
|
||||
[JsonPropertyName("first")] public HttpLink? First { get; set; } = null;
|
||||
|
||||
[JsonPropertyName("last")] public HttpLink? Last { get; set; } = null;
|
||||
|
||||
[JsonPropertyName("next")] public HttpLink? Next { get; set; } = null;
|
||||
}
|
||||
}
|
||||
15
src/Domain/Core/Entities/Wallabag/Range.cs
Normal file
15
src/Domain/Core/Entities/Wallabag/Range.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Core.Entities.Wallabag
|
||||
{
|
||||
public class Range
|
||||
{
|
||||
[JsonPropertyName("start")] public string Start { get; set; }
|
||||
|
||||
[JsonPropertyName("startOffset")] public string StartOffset { get; set; }
|
||||
|
||||
[JsonPropertyName("end")] public string End { get; set; }
|
||||
|
||||
[JsonPropertyName("endOffset")] public string EndOffset { get; set; }
|
||||
}
|
||||
}
|
||||
13
src/Domain/Core/Entities/Wallabag/Tag.cs
Normal file
13
src/Domain/Core/Entities/Wallabag/Tag.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Core.Entities.Wallabag
|
||||
{
|
||||
public class Tag
|
||||
{
|
||||
[JsonPropertyName("id")] public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("label")] public string Label { get; set; }
|
||||
|
||||
[JsonPropertyName("slug")] public string Slug { get; set; }
|
||||
}
|
||||
}
|
||||
77
src/Domain/Core/Entities/Wallabag/WallabagItem.cs
Normal file
77
src/Domain/Core/Entities/Wallabag/WallabagItem.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Wallabag.Client.Converters;
|
||||
|
||||
namespace Core.Entities.Wallabag
|
||||
{
|
||||
public class WallabagItem
|
||||
{
|
||||
[JsonPropertyName("is_archived")] public int IsArchived { get; set; }
|
||||
|
||||
[JsonPropertyName("is_starred")] public int IsStarred { get; set; }
|
||||
|
||||
[JsonPropertyName("user_name")] public string UserName { get; set; }
|
||||
|
||||
[JsonPropertyName("user_email")] public string UserEmail { get; set; }
|
||||
|
||||
[JsonPropertyName("user_id")] public int UserId { get; set; }
|
||||
|
||||
[JsonPropertyName("tags")] public List<Tag> Tags { get; set; }
|
||||
|
||||
[JsonPropertyName("is_public")] public bool IsPublic { get; set; }
|
||||
|
||||
[JsonPropertyName("id")] public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("uid")] public string? Uid { get; set; } = null;
|
||||
|
||||
[JsonPropertyName("title")] public string Title { get; set; }
|
||||
|
||||
[JsonPropertyName("url")] public string Url { get; set; }
|
||||
|
||||
[JsonPropertyName("hashed_url")] public string HashedUrl { get; set; }
|
||||
|
||||
[JsonPropertyName("origin_url")] public string? OriginUrl { get; set; } = null;
|
||||
|
||||
[JsonPropertyName("given_url")] public string GivenUrl { get; set; }
|
||||
|
||||
[JsonPropertyName("hashed_given_url")] public string HashedGivenUrl { get; set; }
|
||||
|
||||
[JsonConverter(typeof(DateTimeConverterForCustomStandard))]
|
||||
[JsonPropertyName("archived_at")] public DateTime? ArchivedAt { get; set; } = null;
|
||||
|
||||
[JsonPropertyName("content")] public string? Content { get; set; } = null;
|
||||
|
||||
[JsonConverter(typeof(DateTimeConverterForCustomStandard))]
|
||||
[JsonPropertyName("created_at")] public DateTime CreatedAt { get; set; }
|
||||
|
||||
[JsonConverter(typeof(DateTimeConverterForCustomStandard))]
|
||||
[JsonPropertyName("updated_at")] public DateTime UpdatedAt { get; set; }
|
||||
|
||||
[JsonConverter(typeof(DateTimeConverterForCustomStandard))]
|
||||
[JsonPropertyName("published_at")] public DateTime PublishedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("published_by")] public List<string> PublishedBy { get; set; }
|
||||
|
||||
[JsonConverter(typeof(DateTimeConverterForCustomStandard))]
|
||||
[JsonPropertyName("starred_at")] public DateTime? StarredAt { get; set; } = null;
|
||||
|
||||
[JsonPropertyName("annotations")] public List<Annotation> Annotations { get; set; }
|
||||
|
||||
[JsonPropertyName("mimetype")] public string Mimetype { get; set; }
|
||||
|
||||
[JsonPropertyName("language")] public string Language { get; set; }
|
||||
|
||||
[JsonPropertyName("reading_time")] public int ReadingTime { get; set; }
|
||||
|
||||
[JsonPropertyName("domain_name")] public string DomainName { get; set; }
|
||||
|
||||
[JsonPropertyName("preview_picture")] public string PreviewPicture { get; set; }
|
||||
|
||||
[JsonPropertyName("http_status")] public string HttpStatus { get; set; }
|
||||
|
||||
[JsonPropertyName("headers")] public Headers Headers { get; set; }
|
||||
|
||||
[JsonPropertyName("_links")] public Links Links { get; set; }
|
||||
}
|
||||
}
|
||||
25
src/Domain/Core/Entities/Wallabag/WallabagQuery.cs
Normal file
25
src/Domain/Core/Entities/Wallabag/WallabagQuery.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Core.Entities.Wallabag
|
||||
{
|
||||
public class WallabagQuery
|
||||
{
|
||||
[JsonPropertyName("page")]
|
||||
public int Page { get; set; }
|
||||
|
||||
[JsonPropertyName("limit")]
|
||||
public int Limit { get; set; }
|
||||
|
||||
[JsonPropertyName("pages")]
|
||||
public int Pages { get; set; }
|
||||
|
||||
[JsonPropertyName("total")]
|
||||
public int Total { get; set; }
|
||||
|
||||
[JsonPropertyName("_links")]
|
||||
public QueryLinks QueryLinks { get; set; }
|
||||
|
||||
[JsonPropertyName("_embedded")]
|
||||
public Embedded Embedded { get; set; }
|
||||
}
|
||||
}
|
||||
14
src/Domain/Core/Handler/ILinkdingTaskHandler.cs
Normal file
14
src/Domain/Core/Handler/ILinkdingTaskHandler.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System.Threading.Tasks;
|
||||
using Core.Entities.Linkding;
|
||||
using LinkdingUpdater.Handler;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Core.Handler
|
||||
{
|
||||
public interface ILinkdingTaskHandler
|
||||
{
|
||||
string Command { get; }
|
||||
Task<HandlerResult> ProcessAsync(Bookmark bookmark, ILogger logger, IConfiguration configuration);
|
||||
}
|
||||
}
|
||||
18
src/Domain/Core/Handler/ISyncTaskHandler.cs
Normal file
18
src/Domain/Core/Handler/ISyncTaskHandler.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Core.Entities.Wallabag;
|
||||
using Linkding.Client;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Core.Handler
|
||||
{
|
||||
public interface ISyncTaskHandler<T>
|
||||
{
|
||||
Type HandlerType { get; }
|
||||
string Command { get; }
|
||||
|
||||
Task ProcessAsync(IEnumerable<WallabagItem> items, T destinationService, ILinkdingService linkdingService, ILogger logger, IConfiguration configuration);
|
||||
}
|
||||
}
|
||||
19
src/Linkding/Dockerfile
Normal file
19
src/Linkding/Dockerfile
Normal file
@ -0,0 +1,19 @@
|
||||
FROM mcr.microsoft.com/dotnet/runtime:6.0 AS base
|
||||
WORKDIR /app
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ["src/LinkdingService/LinkdingService.csproj", "src/LinkdingService/"]
|
||||
RUN dotnet restore "src/LinkdingService/LinkdingService.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/src/LinkdingService"
|
||||
RUN dotnet build "LinkdingService.csproj" -c Release -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "LinkdingService.csproj" -c Release -o /app/publish
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
RUN mkdir ./data
|
||||
ENTRYPOINT ["dotnet", "LinkdingService.dll"]
|
||||
18
src/Linkding/Extensions/ServiceRegistrationExtensions.cs
Normal file
18
src/Linkding/Extensions/ServiceRegistrationExtensions.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using Linkding.Options;
|
||||
using Linkding.Settings;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static class ServiceRegistrationExtensions
|
||||
{
|
||||
public static IServiceCollection Add_Linkding_Worker(this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
var configSection = configuration.GetSection(WorkerSettings.Position);
|
||||
services.Configure<WorkerSettings>(configSection);
|
||||
services.AddSingleton<SettingsService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
71
src/Linkding/Handler/AddPopularSitesAsTagHandler.cs
Normal file
71
src/Linkding/Handler/AddPopularSitesAsTagHandler.cs
Normal file
@ -0,0 +1,71 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using Core.Abstraction;
|
||||
using Core.Entities.Linkding;
|
||||
using Core.Handler;
|
||||
using Linkding.Settings;
|
||||
using LinkdingUpdater.Handler;
|
||||
|
||||
namespace Linkding.Handler;
|
||||
|
||||
public class AddPopularSitesAsTagHandler : ILinkdingTaskHandler
|
||||
{
|
||||
private record RegexExpressionGroups(string Expression, string Replace);
|
||||
|
||||
public string Command { get; } = "AddPopularSitesAsTag";
|
||||
public async Task<HandlerResult> ProcessAsync(Bookmark bookmark, ILogger logger, IConfiguration configuration)
|
||||
{
|
||||
var settings = SettingsService.Settings;
|
||||
|
||||
var returnValue = new HandlerResult() {Instance = bookmark};
|
||||
Regex r = null;
|
||||
Match m = null;
|
||||
foreach (var regexEntry in settings.taggingRule)
|
||||
{
|
||||
try
|
||||
{
|
||||
r = new Regex(regexEntry.pattern, RegexOptions.IgnoreCase);
|
||||
m = r.Match(returnValue.Instance.Url);
|
||||
if (m.Success)
|
||||
{
|
||||
var tagsCommaSeparated = r.Replace(returnValue.Instance.Url, regexEntry.replace);
|
||||
if (!string.IsNullOrEmpty(tagsCommaSeparated))
|
||||
{
|
||||
var tags = tagsCommaSeparated.Split(',');
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(tag) && !returnValue.Instance.TagNames.Contains(tag) &&
|
||||
returnValue.Instance.TagNames.FirstOrDefault(x => x.ToLower() == tag.ToLower()) == null)
|
||||
{
|
||||
|
||||
returnValue.Instance.TagNames = returnValue.Instance.TagNames.Add(tag);
|
||||
returnValue.PerformAction = true;
|
||||
returnValue.Action = LinkdingItemAction.Update;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
r = null;
|
||||
m = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
foreach (var urlKeyValue in settings.urlTagMapping)
|
||||
{
|
||||
if (returnValue.Instance.Url.ToLower().StartsWith(urlKeyValue.url.ToLower()) && returnValue.Instance.TagNames.FirstOrDefault(x => x.ToLower() == urlKeyValue.name.ToLower()) == null)
|
||||
{
|
||||
returnValue.Instance.TagNames = returnValue.Instance.TagNames.Add(urlKeyValue.name);
|
||||
|
||||
returnValue.PerformAction = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
}
|
||||
51
src/Linkding/Handler/AddYearToBookmarkHandler.cs
Normal file
51
src/Linkding/Handler/AddYearToBookmarkHandler.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using Core.Abstraction;
|
||||
using Core.Entities.Linkding;
|
||||
using Core.Handler;
|
||||
using LinkdingUpdater.Handler;
|
||||
|
||||
namespace Linkding.Handler;
|
||||
|
||||
public class AddYearToBookmarkHandler : ILinkdingTaskHandler
|
||||
{
|
||||
public string Command { get; } = "AddYearToBookmark";
|
||||
|
||||
public async Task<HandlerResult> ProcessAsync(Bookmark bookmark, ILogger logger, IConfiguration configuration)
|
||||
{
|
||||
var returnValue = new HandlerResult() {Instance = bookmark};
|
||||
|
||||
var update = false;
|
||||
var createdYear = returnValue.Instance.DateAdded.GetYear();
|
||||
|
||||
if (createdYear != "1970")
|
||||
{
|
||||
var tagName = returnValue.Instance.TagNames.FirstOrDefault(x => x.Equals(createdYear));
|
||||
if (tagName == null)
|
||||
{
|
||||
logger.LogInformation(
|
||||
$"Detected bookmark ({returnValue.Instance.WebsiteTitle} - {returnValue.Instance.Id}) without year-tag ... Try to update");
|
||||
returnValue.Instance.TagNames = returnValue.Instance.TagNames.Add(createdYear);
|
||||
update = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var wrongTagName = returnValue.Instance.TagNames.FirstOrDefault(x => x.Equals("1970"));
|
||||
if (wrongTagName != null)
|
||||
{
|
||||
logger.LogInformation(
|
||||
$"Detected bookmark ({returnValue.Instance.WebsiteTitle} - {returnValue.Instance.Id}) with '1970' year-tag ... Try to update");
|
||||
returnValue.Instance.TagNames = returnValue.Instance.TagNames.Where(x => !x.Equals("1970")).Select(x => x);
|
||||
update = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (update)
|
||||
{
|
||||
logger.LogInformation($"Start updating bookmark {returnValue.Instance.WebsiteTitle} - {returnValue.Instance.Id}");
|
||||
returnValue.PerformAction = true;
|
||||
returnValue.Action = LinkdingItemAction.Update;
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
}
|
||||
25
src/Linkding/Linkding.csproj
Normal file
25
src/Linkding/Linkding.csproj
Normal file
@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>dotnet-LinkdingService-80165DE2-FA70-4803-B366-DF8F24CF86BE</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
|
||||
<PackageReference Include="YamlDotNet" Version="12.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\..\.dockerignore">
|
||||
<Link>.dockerignore</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Services\Linkding.Client\Linkding.Client.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
8
src/Linkding/Options/WorkerSettings.cs
Normal file
8
src/Linkding/Options/WorkerSettings.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Linkding.Options;
|
||||
|
||||
public class WorkerSettings
|
||||
{
|
||||
public const string Position = "Worker";
|
||||
|
||||
public int Intervall { get; set; } = 0;
|
||||
}
|
||||
11
src/Linkding/Program.cs
Normal file
11
src/Linkding/Program.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using Linkding;
|
||||
|
||||
IHost host = Host.CreateDefaultBuilder(args)
|
||||
.ConfigureServices((ctx, services) =>
|
||||
{
|
||||
services.Add_Linkding_HttpClient(ctx.Configuration);
|
||||
services.AddHostedService<Worker>();
|
||||
})
|
||||
.Build();
|
||||
|
||||
await host.RunAsync();
|
||||
11
src/Linkding/Properties/launchSettings.json
Normal file
11
src/Linkding/Properties/launchSettings.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"profiles": {
|
||||
"LinkdingService": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"environmentVariables": {
|
||||
"DOTNET_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
68
src/Linkding/Settings/SettingsService.cs
Normal file
68
src/Linkding/Settings/SettingsService.cs
Normal file
@ -0,0 +1,68 @@
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
|
||||
namespace Linkding.Settings;
|
||||
|
||||
public class SettingYaml
|
||||
{
|
||||
public List<UrlTagMapping> urlTagMapping { get; set; } = new ();
|
||||
public List<TaggingRule> taggingRule { get; set; } = new ();
|
||||
}
|
||||
|
||||
public class UrlTagMapping
|
||||
{
|
||||
public string name { get; set; }
|
||||
public string url { get; set; }
|
||||
}
|
||||
|
||||
public class TaggingRule
|
||||
{
|
||||
public string name { get; set; }
|
||||
public string pattern { get; set; }
|
||||
public string replace { get; set; }
|
||||
}
|
||||
|
||||
public class SettingsService
|
||||
{
|
||||
private const string fileName = "data/config.yml";
|
||||
|
||||
private static SettingYaml _settings = null;
|
||||
|
||||
public static SettingYaml Settings
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_settings == null)
|
||||
{
|
||||
Initialize();
|
||||
}
|
||||
|
||||
return _settings;
|
||||
}
|
||||
private set
|
||||
{
|
||||
_settings = value;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Initialize()
|
||||
{
|
||||
var filePath = Path.Combine(Environment.CurrentDirectory, fileName);
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
|
||||
if (fileInfo.Exists)
|
||||
{
|
||||
var deserializer = new DeserializerBuilder()
|
||||
.WithNamingConvention(CamelCaseNamingConvention.Instance) // see height_in_inches in sample yml
|
||||
.Build();
|
||||
|
||||
var yml = File.ReadAllText(fileInfo.FullName);
|
||||
|
||||
Settings = deserializer.Deserialize<SettingYaml>(yml);
|
||||
}
|
||||
else
|
||||
{
|
||||
Settings = new SettingYaml();
|
||||
}
|
||||
}
|
||||
}
|
||||
178
src/Linkding/Worker.cs
Normal file
178
src/Linkding/Worker.cs
Normal file
@ -0,0 +1,178 @@
|
||||
using Core.Abstraction;
|
||||
using Core.Entities.Linkding;
|
||||
using Core.Handler;
|
||||
using Linkding.Client;
|
||||
using Linkding.Client.Options;
|
||||
using Linkding.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Linkding;
|
||||
|
||||
public class Worker : BackgroundService
|
||||
{
|
||||
private readonly IHostApplicationLifetime _hostApplicationLifetime;
|
||||
private readonly ILogger<Worker> _logger;
|
||||
private readonly LinkdingService _linkdingService;
|
||||
private readonly LinkdingSettings _linkdingSettings;
|
||||
private readonly WorkerSettings _settings;
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public Worker(ILogger<Worker> logger, LinkdingService linkdingService,
|
||||
IOptions<LinkdingSettings> linkdingSettings, IOptions<WorkerSettings> settings, IConfiguration configuration, IHostApplicationLifetime hostApplicationLifetime)
|
||||
{
|
||||
_logger = logger;
|
||||
_linkdingService = linkdingService;
|
||||
_configuration = configuration;
|
||||
_hostApplicationLifetime = hostApplicationLifetime;
|
||||
_settings = settings.Value;
|
||||
_linkdingSettings = linkdingSettings.Value;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
|
||||
|
||||
await RunTaskHandler();
|
||||
int delay = _settings.Intervall * 60000;
|
||||
|
||||
if (delay > 0)
|
||||
{
|
||||
_logger.LogInformation($"Worker paused for: {_settings.Intervall} minutes");
|
||||
|
||||
await Task.Delay(delay, stoppingToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation($"Intervall value is '0' --> stopping worker");
|
||||
_hostApplicationLifetime.StopApplication();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RunTaskHandler()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_linkdingSettings.Url) && _linkdingSettings.UpdateBookmarks)
|
||||
{
|
||||
_logger.LogInformation($"Starting updating bookmarks for {_linkdingSettings.Url}");
|
||||
|
||||
_logger.LogInformation("Collecting Handler");
|
||||
var handlers = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(s => s.GetTypes())
|
||||
.Where(p => typeof(ILinkdingTaskHandler).IsAssignableFrom(p) && p.IsClass);
|
||||
|
||||
var updatedBookmarksCount = 0;
|
||||
var updateBookmarks = new List<Bookmark>();
|
||||
var deleteBookmarks = new List<Bookmark>();
|
||||
if (handlers != null && handlers.Count() > 0)
|
||||
{
|
||||
var linkdingBookmarks = await _linkdingService.GetAllBookmarksAsync();
|
||||
if (linkdingBookmarks.Count() > 0)
|
||||
{
|
||||
|
||||
_logger.LogInformation($"{linkdingBookmarks.Count()} bookmarks found in {_linkdingSettings.Url}");
|
||||
|
||||
foreach (var handler in handlers)
|
||||
{
|
||||
ILinkdingTaskHandler handlerInstance = null;
|
||||
try
|
||||
{
|
||||
handlerInstance = (ILinkdingTaskHandler) Activator.CreateInstance(handler);
|
||||
|
||||
foreach (var linkdingBookmark in linkdingBookmarks)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug($"Start executing {handlerInstance.Command}");
|
||||
// var updateBookmark = updateBookmarks.FirstOrDefault(x => x.Id == linkdingBookmark.Id);
|
||||
var existingBookmarkIndexInt =
|
||||
updateBookmarks.FindIndex(x => x.Id == linkdingBookmark.Id);
|
||||
|
||||
var bookmarkInstance = existingBookmarkIndexInt != -1
|
||||
? updateBookmarks[existingBookmarkIndexInt]
|
||||
: linkdingBookmark;
|
||||
|
||||
var result = await handlerInstance.ProcessAsync(bookmarkInstance, _logger, _configuration);
|
||||
|
||||
if (result.HasError)
|
||||
{
|
||||
_logger.LogWarning(result.ErrorMessage, handlerInstance.Command);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (result.PerformAction)
|
||||
{
|
||||
if (result.Action == LinkdingItemAction.Delete)
|
||||
{
|
||||
if (existingBookmarkIndexInt != -1)
|
||||
{
|
||||
updateBookmarks.RemoveAt(existingBookmarkIndexInt);
|
||||
}
|
||||
|
||||
var bookmarkToDelete = deleteBookmarks.FirstOrDefault(x =>
|
||||
x.Url.ToLower() == result.Instance.Url.ToLower());
|
||||
if (bookmarkToDelete == null)
|
||||
{
|
||||
deleteBookmarks.Add(result.Instance);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (existingBookmarkIndexInt != -1)
|
||||
{
|
||||
updateBookmarks[existingBookmarkIndexInt] = result.Instance;
|
||||
}
|
||||
else
|
||||
{
|
||||
updateBookmarks.Add(result.Instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogDebug($"Finished {handlerInstance.Command}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
var message = $"... {e.Message}";
|
||||
|
||||
if (handlerInstance != null && !string.IsNullOrEmpty(handlerInstance.Command))
|
||||
{
|
||||
message = $"Error while executing {handlerInstance.Command}! {message}";
|
||||
}
|
||||
else
|
||||
{
|
||||
message = $"Error while executing handler! {message}";
|
||||
}
|
||||
|
||||
_logger.LogError(message, "Calling Handler", e);
|
||||
// throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation($"no bookmarks found in {_linkdingSettings.Url}");
|
||||
}
|
||||
|
||||
if (updateBookmarks.Count() > 0)
|
||||
{
|
||||
_logger.LogDebug($"Start updating bookmarks");
|
||||
await _linkdingService.UpdateBookmarkCollectionAsync(updateBookmarks);
|
||||
_logger.LogDebug($"Successfully updated bookmarks");
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation($"Finished updating bookmarks for {_linkdingSettings.Url}");
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/Linkding/appsettings.Development.json
Normal file
8
src/Linkding/appsettings.Development.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/Linkding/appsettings.json
Normal file
8
src/Linkding/appsettings.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/Linkding/config.yml
Normal file
39
src/Linkding/config.yml
Normal file
@ -0,0 +1,39 @@
|
||||
urlTagMapping:
|
||||
- name: microsoft_azure
|
||||
url: https://github.com/azure
|
||||
- name: microsoft_azuread
|
||||
url: https://github.com/AzureAD
|
||||
- name: microsoft_dotnet
|
||||
url: https://github.com/dotnet-architecture
|
||||
|
||||
taggingRule:
|
||||
- name: reddit
|
||||
pattern: https://(?:www\.)?(reddit)\.com(?:/r/)?([ a-zA-Z0-9\-\+_ ]+)?(?:/.*)?
|
||||
replace: $1,$2
|
||||
- name: microsoft
|
||||
pattern: https://([ a-zA-Z0-9 ]+)?[ \. ]?(microsoft)\.com(?:/.*)?
|
||||
replace: $1,$2
|
||||
- name: microsoft_docs
|
||||
pattern: "https://(?:docs)\.(?:microsoft)\.com[ / ]?(?: [ a-zA-Z0-9\-\+_ ]+)(?:/)?([ a-zA-Z0-9\-\+_ ]+)?(?:/)?([ a-zA-Z0-9\-\+_ ]+)?(?:/.*)?"
|
||||
replace: $1,$2
|
||||
- name: youtube
|
||||
pattern: https://[ [ a-zA-Z0-9 ]+\. ]?(youtube)\.com(?:/.*)?
|
||||
replace: $1
|
||||
- name: ebay
|
||||
pattern: https://[ [ a-zA-Z0-9 ]+\. ]?(ebay)\.(com|de|fr)(?:/.*)?
|
||||
replace: $1
|
||||
- name: amazon
|
||||
pattern: https://[ [ a-zA-Z0-9 ]+\. ]?(amazon)\.(com|de|fr)(?:/.*)?
|
||||
replace: $1
|
||||
- name: docker
|
||||
pattern: https://([ a-zA-Z0-9 ]+)?[ \. ]?(docker)\.com(?:/.*)?
|
||||
replace: $1,$2
|
||||
- name: xbox
|
||||
pattern: https://[ [ a-zA-Z0-9 ]+\. ]?(xbox)\.com(?:/.*)?
|
||||
replace: $1
|
||||
- name: github
|
||||
pattern: https://([ a-zA-Z0-9 ]+)?[ \. ]?(github)\.com[ / ]?([ a-zA-Z0-9\-\+_ ]+)(?:/)?([ a-zA-Z0-9\-\+_ ]+)?(?:/.*)?
|
||||
replace: $1,$2,$3,$4
|
||||
- name: github.io
|
||||
pattern: https://([ a-zA-Z0-9 ]+)\.(github)\.io[ / ]?([ a-zA-Z0-9\-\+_ ]+)(?:/)?([ a-zA-Z0-9\-\+_ ]+)?(?:/.*)?
|
||||
replace: $1,$2,$3
|
||||
@ -0,0 +1,12 @@
|
||||
using AutoMapper;
|
||||
using Core.Entities.Linkding;
|
||||
|
||||
namespace Linkding.Client.Automapper;
|
||||
|
||||
public class LinkdingBookmarkProfile : Profile
|
||||
{
|
||||
public LinkdingBookmarkProfile()
|
||||
{
|
||||
CreateMap<Bookmark, BookmarkUpdatePayload>();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
using System.Net.Http.Headers;
|
||||
using Linkding.Client;
|
||||
using Linkding.Client.Options;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Polly;
|
||||
using Polly.Extensions.Http;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static class ServiceRegistrationExtensions
|
||||
{
|
||||
public static IServiceCollection Add_Linkding_HttpClient(this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
var configSection = configuration.GetSection(LinkdingSettings.Position);
|
||||
services.Configure<LinkdingSettings>(configSection);
|
||||
services.AddHttpClient<LinkdingService>()
|
||||
.SetHandlerLifetime(TimeSpan.FromMinutes(5)) //Set lifetime to five minutes
|
||||
.AddPolicyHandler(GetRetryPolicy());
|
||||
|
||||
services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
|
||||
{
|
||||
return HttpPolicyExtensions
|
||||
.HandleTransientHttpError()
|
||||
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
.WaitAndRetryAsync(6, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2,
|
||||
retryAttempt)));
|
||||
}
|
||||
}
|
||||
38
src/Services/Linkding.Client/Extensions/System.cs
Normal file
38
src/Services/Linkding.Client/Extensions/System.cs
Normal file
@ -0,0 +1,38 @@
|
||||
namespace System;
|
||||
|
||||
public static class System
|
||||
{
|
||||
public static DateTime CreateDateTime(this long ticks)
|
||||
{
|
||||
var dateTimeOffset = DateTimeOffset.FromUnixTimeSeconds(ticks);
|
||||
|
||||
return dateTimeOffset.UtcDateTime;
|
||||
}
|
||||
|
||||
public static DateTime CreateDateTime(this string ticksString)
|
||||
{
|
||||
if (long.TryParse(ticksString, out var dateAddedUnixEpoch))
|
||||
{
|
||||
return dateAddedUnixEpoch.CreateDateTime();
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public static string GetYear(this DateTime date)
|
||||
{
|
||||
return date.ToString("yyyy");
|
||||
}
|
||||
|
||||
public static string GetYear(this long ticks)
|
||||
{
|
||||
var dateTime = ticks.CreateDateTime();
|
||||
return dateTime.GetYear();
|
||||
}
|
||||
|
||||
public static string GetYear(this string ticksString)
|
||||
{
|
||||
var dateTime = ticksString.CreateDateTime();
|
||||
return dateTime.GetYear();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace System.Collections.Generic;
|
||||
|
||||
public static class SystemCollectionGenericExtesions
|
||||
{
|
||||
public static IEnumerable<T> Add<T>(this IEnumerable<T> e, T value) {
|
||||
foreach ( var cur in e) {
|
||||
yield return cur;
|
||||
}
|
||||
yield return value;
|
||||
}
|
||||
}
|
||||
34
src/Services/Linkding.Client/Linkding.Client.csproj
Normal file
34
src/Services/Linkding.Client/Linkding.Client.csproj
Normal file
@ -0,0 +1,34 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>AnyCPU</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="12.0.1" />
|
||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="6.0.13" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Models\Bookmark.cs" />
|
||||
<Compile Remove="Models\BookmarkBase.cs" />
|
||||
<Compile Remove="Models\BookmarkCreatePayload.cs" />
|
||||
<Compile Remove="Models\BookmarkUpdatePayload.cs" />
|
||||
<Compile Remove="Models\BookmarkUpdateResult.cs" />
|
||||
<Compile Remove="Models\Tag.cs" />
|
||||
<Compile Remove="Models\TagCreatePayload.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Domain\Core\Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
127
src/Services/Linkding.Client/LinkdingService.cs
Normal file
127
src/Services/Linkding.Client/LinkdingService.cs
Normal file
@ -0,0 +1,127 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using AutoMapper;
|
||||
using Core.Entities.Linkding;
|
||||
using Linkding.Client.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Linkding.Client;
|
||||
|
||||
public class LinkdingService : ILinkdingService
|
||||
{
|
||||
private readonly LinkdingSettings _settings;
|
||||
private readonly IMapper _mapper;
|
||||
public readonly HttpClient _client;
|
||||
|
||||
public LinkdingService(HttpClient client, IOptions<LinkdingSettings> settings, IMapper mapper)
|
||||
{
|
||||
_settings = settings.Value;
|
||||
_client = client;
|
||||
_mapper = mapper;
|
||||
_client.BaseAddress = new Uri(_settings.Url);
|
||||
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Token", _settings.Key);
|
||||
}
|
||||
|
||||
private LinkdingService(string url, string key)
|
||||
{
|
||||
_client = new HttpClient();
|
||||
_client.BaseAddress = new Uri(url);
|
||||
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Token", key);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Bookmark>> GetBookmarksAsync(int limit = 100, int offset = 0)
|
||||
{
|
||||
var bookmarks = new List<Bookmark>();
|
||||
|
||||
var result = await GetBookmarkResultsAsync(limit, offset);
|
||||
if (result != null && result.Results?.Count() > 0)
|
||||
{
|
||||
bookmarks = result.Results;
|
||||
}
|
||||
|
||||
return bookmarks;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Bookmark>> GetAllBookmarksAsync()
|
||||
{
|
||||
IEnumerable<Bookmark> bookmarks = new List<Bookmark>();
|
||||
|
||||
var result = await GetBookmarkResultsAsync();
|
||||
if (result != null && result.Results?.Count() > 0)
|
||||
{
|
||||
bookmarks = result.Results;
|
||||
if (result.Count > 100)
|
||||
{
|
||||
while (!string.IsNullOrEmpty(result.Next))
|
||||
{
|
||||
result = await GetBookmarkResultsAsync(result.Next);
|
||||
if (result.Results?.Count() > 0)
|
||||
{
|
||||
bookmarks = bookmarks.Concat(result.Results);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bookmarks;
|
||||
}
|
||||
|
||||
public async Task UpdateBookmarkCollectionAsync(IEnumerable<Bookmark> bookmarks)
|
||||
{
|
||||
foreach (var bookmark in bookmarks)
|
||||
{
|
||||
var payload = _mapper.Map<BookmarkUpdatePayload>(bookmark);
|
||||
await UpdateBookmarkAsync(payload);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateBookmarkCollectionAsync(IEnumerable<BookmarkUpdatePayload> bookmarks)
|
||||
{
|
||||
foreach (var bookmark in bookmarks)
|
||||
{
|
||||
await UpdateBookmarkAsync(bookmark);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateBookmarkAsync(BookmarkUpdatePayload bookmark)
|
||||
{
|
||||
var result = await _client.PutAsJsonAsync($"/api/bookmarks/{bookmark.Id}/", bookmark);
|
||||
if (result.IsSuccessStatusCode)
|
||||
{
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<BookmarksResult> GetBookmarkResultsAsync(int limit = 100, int offset = 0)
|
||||
{
|
||||
BookmarksResult bookmarkResult = null;
|
||||
|
||||
var url = $"/api/bookmarks/";
|
||||
|
||||
bookmarkResult = await GetBookmarkResultsAsync(url);
|
||||
|
||||
return bookmarkResult;
|
||||
}
|
||||
|
||||
public async Task<BookmarksResult> GetBookmarkResultsAsync(string url)
|
||||
{
|
||||
BookmarksResult bookmarkResult = null;
|
||||
|
||||
bookmarkResult = await _client.GetFromJsonAsync<BookmarksResult>(url);
|
||||
|
||||
return bookmarkResult;
|
||||
}
|
||||
|
||||
public static LinkdingService Create(string url, string key)
|
||||
{
|
||||
return new LinkdingService(url, key);
|
||||
}
|
||||
}
|
||||
10
src/Services/Linkding.Client/Options/LinkdingSettings.cs
Normal file
10
src/Services/Linkding.Client/Options/LinkdingSettings.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace Linkding.Client.Options;
|
||||
|
||||
public class LinkdingSettings
|
||||
{
|
||||
public const string Position = "Linkding";
|
||||
|
||||
public string Key { get; set; }
|
||||
public string Url { get; set; }
|
||||
public bool UpdateBookmarks { get; set; } = true;
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
namespace Wallabag.Client.Contracts;
|
||||
|
||||
public interface IAccessTokenProvider
|
||||
{
|
||||
Task<string> GetToken(IEnumerable<string> scopes);
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Wallabag.Client.Converters;
|
||||
|
||||
public class DateTimeConverterForCustomStandard : JsonConverter<DateTime>
|
||||
{
|
||||
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var dateTimeString = reader.GetString();
|
||||
|
||||
if (string.IsNullOrEmpty(dateTimeString))
|
||||
{
|
||||
return DateTime.MinValue;
|
||||
}
|
||||
DateTime dt = DateTime.ParseExact(dateTimeString, "yyyy-MM-dd'T'HH:mm:ssK",
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AdjustToUniversal);
|
||||
|
||||
return dt;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Wallabag.Client.Converters;
|
||||
|
||||
public class DateTimeOffsetConverterUsingDateTimeParse : JsonConverter<DateTimeOffset>
|
||||
{
|
||||
public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var dateTimeString = reader.GetString();
|
||||
return DateTimeOffset.Parse(dateTimeString);
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
using System.Net.Http.Headers;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Polly;
|
||||
using Polly.Extensions.Http;
|
||||
using Wallabag.Client;
|
||||
using Wallabag.Client.Contracts;
|
||||
using Wallabag.Client.OAuth;
|
||||
using Wallabag.Client.Options;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static class ServiceRegistrationExtensions
|
||||
{
|
||||
public static IServiceCollection Add_Wallabag_HttpClient(this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
var configSection = configuration.GetSection(WallabagSettings.Position);
|
||||
services.Configure<WallabagSettings>(configSection);
|
||||
services.AddScoped<IAccessTokenProvider, OAuthTokenProvider>();
|
||||
services.AddScoped<AuthenticationClient>();
|
||||
services.AddHttpClient<WallabagService>()
|
||||
.SetHandlerLifetime(TimeSpan.FromMinutes(5)) //Set lifetime to five minutes
|
||||
.AddPolicyHandler(GetRetryPolicy());
|
||||
|
||||
// services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
|
||||
{
|
||||
return HttpPolicyExtensions
|
||||
.HandleTransientHttpError()
|
||||
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
.WaitAndRetryAsync(6, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2,
|
||||
retryAttempt)));
|
||||
}
|
||||
}
|
||||
26
src/Services/Wallabag.Client/Extensions/URIExtensions.cs
Normal file
26
src/Services/Wallabag.Client/Extensions/URIExtensions.cs
Normal file
@ -0,0 +1,26 @@
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace System;
|
||||
|
||||
public static class URIExtensions
|
||||
{
|
||||
public static string AppendToURL(this string uri1, string uri2)
|
||||
{
|
||||
return AppendToUrlInternal(uri1, uri2);
|
||||
}
|
||||
|
||||
public static string AppendToURL(this string baseURL, params string[] segments)
|
||||
{
|
||||
return AppendToUrlInternal(baseURL, segments);
|
||||
}
|
||||
|
||||
private static string AppendToUrlInternal(this string baseURL, params string[] segments)
|
||||
{
|
||||
return string.Join("/", new[] { baseURL.TrimEnd('/') }
|
||||
.Concat(segments.Select(s => s.Trim('/'))));
|
||||
}
|
||||
|
||||
public static Uri Append(this Uri uri, params string[] paths)
|
||||
{
|
||||
return new Uri(paths.Aggregate(uri.AbsoluteUri, (current, path) => string.Format("{0}/{1}", current.TrimEnd('/'), path.TrimStart('/'))));
|
||||
}
|
||||
}
|
||||
6
src/Services/Wallabag.Client/Models/WallabagEntry.cs
Normal file
6
src/Services/Wallabag.Client/Models/WallabagEntry.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Wallabag.Client.Models;
|
||||
|
||||
public class WallabagEntry : WallabagPayload
|
||||
{
|
||||
public int Id { get; set; }
|
||||
}
|
||||
17
src/Services/Wallabag.Client/Models/WallabagPayload.cs
Normal file
17
src/Services/Wallabag.Client/Models/WallabagPayload.cs
Normal file
@ -0,0 +1,17 @@
|
||||
namespace Wallabag.Client.Models;
|
||||
|
||||
public class WallabagPayload
|
||||
{
|
||||
public string Url { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Tags { get; set; }
|
||||
public int Archive { get; set; }
|
||||
public int Starred { get; set; }
|
||||
public string Content { get; set; }
|
||||
public string Language { get; set; }
|
||||
public string PreviewPicture { get; set; }
|
||||
public DateTime PublishedAt { get; set; }
|
||||
public string Authors { get; set; }
|
||||
public int Publich { get; set; }
|
||||
public string OriginUrl { get; set; }
|
||||
}
|
||||
77
src/Services/Wallabag.Client/OAuth/AuthenticationClient.cs
Normal file
77
src/Services/Wallabag.Client/OAuth/AuthenticationClient.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Wallabag.Client.Options;
|
||||
|
||||
namespace Wallabag.Client.OAuth;
|
||||
|
||||
public record WallabagToken(string token_type, string access_token, string refresh_token, int expires_in, string scope);
|
||||
|
||||
public class AuthenticationClient
|
||||
{
|
||||
protected const string AuthPath = "/oauth/v2/token";
|
||||
|
||||
public readonly HttpClient _client;
|
||||
public readonly WallabagSettings _settings;
|
||||
|
||||
public AuthenticationClient(HttpClient client, IOptions<WallabagSettings> settings)
|
||||
{
|
||||
_client = client;
|
||||
_settings = settings.Value;
|
||||
// var baseAdress = new Uri(_settings.Url);
|
||||
// client.BaseAddress = baseAdress.Append(AuthPath);
|
||||
client.BaseAddress = new Uri(_settings.Url);
|
||||
}
|
||||
|
||||
public async Task<WallabagToken> Authenticate(IEnumerable<string> scopes = null)
|
||||
{
|
||||
var parameters = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new("client_id", _settings.ClientId),
|
||||
new("client_secret", _settings.ClientSecret),
|
||||
new("grant_type", _settings.GrandType),
|
||||
new("username", _settings.Username),
|
||||
new("password", _settings.Password)
|
||||
};
|
||||
|
||||
if (scopes != null && scopes.Count() > 0)
|
||||
{
|
||||
parameters.Add(new("scope", string.Join(" ", scopes)));
|
||||
}
|
||||
var response = await _client.PostAsync("oauth/v2/token", new FormUrlEncodedContent(parameters));
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
var authentication = await response.Content.ReadFromJsonAsync<WallabagToken>();
|
||||
if (authentication == null)
|
||||
{
|
||||
throw new HttpRequestException("Could not retrieve authentication data.");
|
||||
}
|
||||
return authentication;
|
||||
}
|
||||
public async Task<WallabagToken> RefreshToken(string refreshToken, IEnumerable<string> scopes = null)
|
||||
{
|
||||
var parameters = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new("client_id", _settings.ClientId),
|
||||
new("client_secret", _settings.ClientSecret),
|
||||
new("grant_type", "refresh_token"),
|
||||
new("refresh_token", refreshToken),
|
||||
new("username", _settings.Username),
|
||||
new("password", _settings.Password)
|
||||
};
|
||||
|
||||
if (scopes != null && scopes.Count() > 0)
|
||||
{
|
||||
parameters.Add(new("scope", string.Join(" ", scopes)));
|
||||
}
|
||||
|
||||
var response = await _client.PostAsync("oauth/v2/token", new FormUrlEncodedContent(parameters));
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
var authentication = await response.Content.ReadFromJsonAsync<WallabagToken>();
|
||||
if (authentication == null)
|
||||
{
|
||||
throw new HttpRequestException("Could not retrieve authentication data.");
|
||||
}
|
||||
return authentication;
|
||||
}
|
||||
}
|
||||
43
src/Services/Wallabag.Client/OAuth/OAuthTokenProvider.cs
Normal file
43
src/Services/Wallabag.Client/OAuth/OAuthTokenProvider.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using Wallabag.Client.Contracts;
|
||||
|
||||
namespace Wallabag.Client.OAuth;
|
||||
|
||||
public class OAuthTokenProvider : IAccessTokenProvider
|
||||
{
|
||||
protected record TokenCache(string access_token, DateTime expires);
|
||||
|
||||
private readonly AuthenticationClient _client;
|
||||
|
||||
protected Dictionary<string, TokenCache> cache = new ();
|
||||
|
||||
public OAuthTokenProvider(AuthenticationClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public async Task<string> GetToken(IEnumerable<string> scopes = null)
|
||||
{
|
||||
string cacheKey = "wallabag+token";
|
||||
|
||||
if (scopes != null && scopes.Count() > 0)
|
||||
{
|
||||
cacheKey = string.Join('+', scopes);
|
||||
}
|
||||
|
||||
if (cache.ContainsKey(cacheKey))
|
||||
{
|
||||
var tokenCache = cache[cacheKey];
|
||||
if (tokenCache.expires > DateTime.Now)
|
||||
{
|
||||
return tokenCache.access_token;
|
||||
}
|
||||
else
|
||||
{
|
||||
cache.Remove(cacheKey);
|
||||
}
|
||||
}
|
||||
var auth = await _client.Authenticate(scopes);
|
||||
cache.Add(cacheKey, new TokenCache(auth.access_token, DateTime.Now.AddSeconds(auth.expires_in)));
|
||||
return auth.access_token;
|
||||
}
|
||||
}
|
||||
13
src/Services/Wallabag.Client/Options/WallabagSettings.cs
Normal file
13
src/Services/Wallabag.Client/Options/WallabagSettings.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace Wallabag.Client.Options;
|
||||
|
||||
public class WallabagSettings
|
||||
{
|
||||
public const string Position = "Wallabag";
|
||||
|
||||
public string Url { get; set; } = "https://app.wallabag.it";
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string ClientId { get; set; }
|
||||
public string ClientSecret { get; set; }
|
||||
public string GrandType { get; set; } = "password";
|
||||
}
|
||||
34
src/Services/Wallabag.Client/Wallabag.Client.csproj
Normal file
34
src/Services/Wallabag.Client/Wallabag.Client.csproj
Normal file
@ -0,0 +1,34 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="6.0.13" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Models\Result\Annotation.cs" />
|
||||
<Compile Remove="Models\Result\Embedded.cs" />
|
||||
<Compile Remove="Models\Result\Headers.cs" />
|
||||
<Compile Remove="Models\Result\HttpLink.cs" />
|
||||
<Compile Remove="Models\Result\Links.cs" />
|
||||
<Compile Remove="Models\Result\QueryLinks.cs" />
|
||||
<Compile Remove="Models\Result\Range.cs" />
|
||||
<Compile Remove="Models\Result\Tag.cs" />
|
||||
<Compile Remove="Models\Result\WallabagItem.cs" />
|
||||
<Compile Remove="Models\Result\WallabagQuery.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Domain\Core\Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
108
src/Services/Wallabag.Client/WallabagServiceBase.cs
Normal file
108
src/Services/Wallabag.Client/WallabagServiceBase.cs
Normal file
@ -0,0 +1,108 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Core.Abstraction;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Wallabag.Client.Contracts;
|
||||
using Wallabag.Client.Converters;
|
||||
using Wallabag.Client.Options;
|
||||
|
||||
namespace Wallabag.Client;
|
||||
|
||||
public partial class WallabagService : IWallabagService
|
||||
{
|
||||
private readonly WallabagSettings _settings;
|
||||
private IAccessTokenProvider _accessTokenProvider;
|
||||
public readonly HttpClient _client;
|
||||
|
||||
public WallabagService(HttpClient client, IOptions<WallabagSettings> settings,
|
||||
IAccessTokenProvider accessTokenProvider)
|
||||
{
|
||||
_client = client;
|
||||
_accessTokenProvider = accessTokenProvider;
|
||||
_settings = settings.Value;
|
||||
_client.BaseAddress = new Uri(_settings.Url);
|
||||
}
|
||||
|
||||
public async Task<AuthenticationHeaderValue> GetAuthenticationHeaderAsync(IEnumerable<string> scopes = null)
|
||||
{
|
||||
return new AuthenticationHeaderValue("Bearer", await _accessTokenProvider.GetToken(scopes));
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> GetAsync(string endpoint, IEnumerable<string> scopes = null,
|
||||
bool httpCompletionResponseContentRead = false)
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, endpoint);
|
||||
request.Headers.Authorization = await GetAuthenticationHeaderAsync(scopes);
|
||||
|
||||
HttpResponseMessage response = null;
|
||||
|
||||
if (httpCompletionResponseContentRead)
|
||||
{
|
||||
response = await _client.SendAsync(request, HttpCompletionOption.ResponseContentRead);
|
||||
}
|
||||
else
|
||||
{
|
||||
response = await _client.SendAsync(request);
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
return response;
|
||||
}
|
||||
|
||||
public async Task<T> GetJsonAsync<T>(string endpoint, IEnumerable<string> scopes = null)
|
||||
{
|
||||
var response = await GetAsync(endpoint, scopes);
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<T>();
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> PostWithFormDataAsnyc(string endpoint, FormUrlEncodedContent content,
|
||||
IEnumerable<string> scopes = null)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
|
||||
request.Content = content;
|
||||
|
||||
return await PostAsync(endpoint, request, scopes);
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> PostAsync(string endpoint, HttpContent content,
|
||||
IEnumerable<string> scopes = null)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
|
||||
request.Content = content;
|
||||
|
||||
return await PostAsync(endpoint, request, scopes);
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> PostAsync(string endpoint, HttpRequestMessage request,
|
||||
IEnumerable<string> scopes = null)
|
||||
{
|
||||
request.Headers.Authorization = await GetAuthenticationHeaderAsync(scopes);
|
||||
var response = await _client.SendAsync(request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return response;
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> PutAsync(string endpoint, HttpContent content,
|
||||
IEnumerable<string> scopes = null)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Put, endpoint);
|
||||
request.Headers.Authorization = await GetAuthenticationHeaderAsync(scopes);
|
||||
request.Content = content;
|
||||
var response = await _client.SendAsync(request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return response;
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> DeleteAsync(string endpoint, IEnumerable<string> scopes = null,
|
||||
HttpContent content = null)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Delete, endpoint);
|
||||
request.Headers.Authorization = await GetAuthenticationHeaderAsync(scopes);
|
||||
request.Content = content;
|
||||
var response = await _client.SendAsync(request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return response;
|
||||
}
|
||||
}
|
||||
85
src/Services/Wallabag.Client/WallabagServiceEntries.cs
Normal file
85
src/Services/Wallabag.Client/WallabagServiceEntries.cs
Normal file
@ -0,0 +1,85 @@
|
||||
using System.Net.Http.Json;
|
||||
using Core.Entities.Wallabag;
|
||||
using Wallabag.Client.Models;
|
||||
|
||||
namespace Wallabag.Client;
|
||||
|
||||
public partial class WallabagService
|
||||
{
|
||||
public async Task<IEnumerable<WallabagItem>> GetEntries(string format = "json", int limit = 50, bool full = false)
|
||||
{
|
||||
var bookmarks = new List<WallabagItem>();
|
||||
var url = $"/api/entries.{format}?perPage={limit}";
|
||||
if (!full)
|
||||
{
|
||||
url = $"{url}&detail=metadata";
|
||||
}
|
||||
|
||||
var allQuery = await GetJsonAsync<WallabagQuery>(url);
|
||||
|
||||
if (allQuery != null && allQuery.Embedded != null && allQuery.Embedded.Items != null && allQuery.Embedded.Items.Count() > 0)
|
||||
{
|
||||
bookmarks = allQuery.Embedded.Items;
|
||||
|
||||
if (allQuery.Total > limit)
|
||||
{
|
||||
while (allQuery.QueryLinks.Next != null && !string.IsNullOrEmpty(allQuery.QueryLinks.Next.Href))
|
||||
{
|
||||
// url = allQuery.QueryLinks.Next.Href.Replace(_settings.Url, "");
|
||||
url = allQuery.QueryLinks.Next.Href.Replace("http://", "https://");
|
||||
allQuery = await GetJsonAsync<WallabagQuery>(url);
|
||||
bookmarks.AddRange(allQuery.Embedded.Items);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bookmarks;
|
||||
}
|
||||
|
||||
public async Task<WallabagItem> GetEntryById(int id, string format = "json")
|
||||
{
|
||||
var url = $"/api/entries/{id}.{format}";
|
||||
var item = await GetJsonAsync<WallabagItem>(url);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
public async Task<WallabagItem> AddEntryByUrl(string url, IEnumerable<string> tags = null, string format = "json")
|
||||
{
|
||||
var endpoint = $"/api/entries.{format}";
|
||||
|
||||
var keyVals = new Dictionary<string, string>();
|
||||
keyVals.Add("url", url);
|
||||
if (tags != null && tags.Count() > 0)
|
||||
{
|
||||
keyVals.Add("tags", string.Join(",", tags));
|
||||
}
|
||||
|
||||
var content = new FormUrlEncodedContent(keyVals);
|
||||
var response = await PostWithFormDataAsnyc(endpoint, content);
|
||||
|
||||
var item = await response.Content.ReadFromJsonAsync<WallabagItem>();
|
||||
return item;
|
||||
}
|
||||
|
||||
// private async Task<WallabagEntry> GetBookmarkResultsAsync(int limit = 100, int offset = 0)
|
||||
// {
|
||||
// WallabagEntry bookmarkResult = null;
|
||||
//
|
||||
// var url = $"/api/bookmarks/";
|
||||
//
|
||||
// bookmarkResult = await GetBookmarkResultsAsync(url);
|
||||
//
|
||||
// return bookmarkResult;
|
||||
// }
|
||||
//
|
||||
// private async Task<WallabagEntry> GetBookmarkResultsAsync(string url)
|
||||
// {
|
||||
// WallabagEntry bookmarkResult = null;
|
||||
//
|
||||
// bookmarkResult = await _client.GetFromJsonAsync<WallabagEntry>(url);
|
||||
//
|
||||
// return bookmarkResult;
|
||||
// }
|
||||
|
||||
}
|
||||
18
src/Wallabag/Extensions/ServiceRegistrationExtensions.cs
Normal file
18
src/Wallabag/Extensions/ServiceRegistrationExtensions.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using Wallabag.Options;
|
||||
using Wallabag.Settings;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static class ServiceRegistrationExtensions
|
||||
{
|
||||
public static IServiceCollection Add_Wallabag_Worker(this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
var configSection = configuration.GetSection(WorkerSettings.Position);
|
||||
services.Configure<WorkerSettings>(configSection);
|
||||
services.AddSingleton<SettingsService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
91
src/Wallabag/Handler/LinkdingBookmarkToWallabagHandler.cs
Normal file
91
src/Wallabag/Handler/LinkdingBookmarkToWallabagHandler.cs
Normal file
@ -0,0 +1,91 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using Core.Entities.Wallabag;
|
||||
using Core.Handler;
|
||||
using Linkding.Client;
|
||||
using Wallabag.Client;
|
||||
using Wallabag.Settings;
|
||||
|
||||
namespace Wallabag.Handler
|
||||
{
|
||||
public class LinkdingBookmarkToWallabagHandler : ISyncTaskHandler<WallabagService>
|
||||
{
|
||||
public Type HandlerType { get; } = typeof(WallabagService);
|
||||
public string Command { get; } = "LinkdingBookmarkToWallabag";
|
||||
|
||||
public async Task ProcessAsync(IEnumerable<WallabagItem> items, WallabagService destinationService,
|
||||
ILinkdingService linkdingService,
|
||||
ILogger logger, IConfiguration configuration)
|
||||
{
|
||||
var wallabagsNormalized = new Dictionary<int, string>();
|
||||
var updatedWallabags = new Dictionary<string, IEnumerable<string>>();
|
||||
var wallabagToRemove = new List<int>();
|
||||
var linkdingBookmarks = await linkdingService.GetAllBookmarksAsync();
|
||||
|
||||
if (linkdingBookmarks != null && linkdingBookmarks.Count() > 0)
|
||||
{
|
||||
var settings = SettingsService.Settings;
|
||||
|
||||
var tagName = configuration.GetValue<string>("Worker:SyncTag");
|
||||
|
||||
linkdingBookmarks =
|
||||
linkdingBookmarks.Where(x => x.TagNames.Contains(tagName)).OrderBy(x => x.DateAdded);
|
||||
|
||||
Regex r = null;
|
||||
Match m = null;
|
||||
foreach (var bookmark in linkdingBookmarks)
|
||||
{
|
||||
var cleanUrl =
|
||||
bookmark.Url.Replace(
|
||||
"?utm_source=share&utm_medium=android_app&utm_name=androidcss&utm_term=2&utm_content=share_button",
|
||||
"");
|
||||
// var existingElement = items.FirstOrDefault(x => x.Url.ToLower() == cleanUrl.ToLower());
|
||||
var existingElement = items.FirstOrDefault(x =>
|
||||
x.Url.ToLower() == cleanUrl.ToLower() || x.OriginUrl?.ToLower() == cleanUrl.ToLower());
|
||||
if (existingElement == null)
|
||||
{
|
||||
var addToWallabag = true;
|
||||
foreach (var p in settings.excludedDomains)
|
||||
{
|
||||
r = new Regex(p.pattern, RegexOptions.IgnoreCase);
|
||||
m = r.Match(cleanUrl);
|
||||
|
||||
if (m.Success)
|
||||
{
|
||||
addToWallabag = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (addToWallabag && !updatedWallabags.ContainsKey(bookmark.Url))
|
||||
{
|
||||
updatedWallabags.Add(cleanUrl,
|
||||
bookmark.TagNames.Where(x => !x.Equals(tagName, StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation($"no bookmarks found");
|
||||
}
|
||||
|
||||
if (updatedWallabags.Count() > 0)
|
||||
{
|
||||
logger.LogInformation($"Detected {updatedWallabags.Count()} bookmarks... Start syncing");
|
||||
|
||||
foreach (var (url, tags) in updatedWallabags)
|
||||
{
|
||||
var result = await destinationService.AddEntryByUrl(url, tags);
|
||||
|
||||
if (result.ReadingTime == 0)
|
||||
{
|
||||
wallabagToRemove.Add(result.Id);
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogInformation($"{updatedWallabags.Count()} bookmarks synced");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/Wallabag/Options/WorkerSettings.cs
Normal file
10
src/Wallabag/Options/WorkerSettings.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace Wallabag.Options;
|
||||
|
||||
public class WorkerSettings
|
||||
{
|
||||
public const string Position = "Worker";
|
||||
|
||||
public int Intervall { get; set; } = 0;
|
||||
|
||||
public string SyncTag { get; set; } = "readlater";
|
||||
}
|
||||
13
src/Wallabag/Program.cs
Normal file
13
src/Wallabag/Program.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using Wallabag;
|
||||
|
||||
IHost host = Host.CreateDefaultBuilder(args)
|
||||
.ConfigureServices((ctx, services) =>
|
||||
{
|
||||
services.Add_Wallabag_HttpClient(ctx.Configuration);
|
||||
services.Add_Linkding_HttpClient(ctx.Configuration);
|
||||
services.Add_Wallabag_Worker(ctx.Configuration);
|
||||
services.AddHostedService<Worker>();
|
||||
})
|
||||
.Build();
|
||||
|
||||
await host.RunAsync();
|
||||
11
src/Wallabag/Properties/launchSettings.json
Normal file
11
src/Wallabag/Properties/launchSettings.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"profiles": {
|
||||
"WallabagWorker": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"environmentVariables": {
|
||||
"DOTNET_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/Wallabag/Settings/SettingsService.cs
Normal file
60
src/Wallabag/Settings/SettingsService.cs
Normal file
@ -0,0 +1,60 @@
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
|
||||
namespace Wallabag.Settings;
|
||||
|
||||
public class SettingYaml
|
||||
{
|
||||
public List<ExcludedDomainPattern> excludedDomains { get; set; } = new ();
|
||||
}
|
||||
|
||||
public class ExcludedDomainPattern
|
||||
{
|
||||
public string name { get; set; }
|
||||
public string pattern { get; set; }
|
||||
}
|
||||
|
||||
public class SettingsService
|
||||
{
|
||||
private const string fileName = "data/config.yml";
|
||||
|
||||
private static SettingYaml _settings = null;
|
||||
|
||||
public static SettingYaml Settings
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_settings == null)
|
||||
{
|
||||
Initialize();
|
||||
}
|
||||
|
||||
return _settings;
|
||||
}
|
||||
private set
|
||||
{
|
||||
_settings = value;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Initialize()
|
||||
{
|
||||
var filePath = Path.Combine(Environment.CurrentDirectory, fileName);
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
|
||||
if (fileInfo.Exists)
|
||||
{
|
||||
var deserializer = new DeserializerBuilder()
|
||||
.WithNamingConvention(CamelCaseNamingConvention.Instance) // see height_in_inches in sample yml
|
||||
.Build();
|
||||
|
||||
var yml = File.ReadAllText(fileInfo.FullName);
|
||||
|
||||
Settings = deserializer.Deserialize<SettingYaml>(yml);
|
||||
}
|
||||
else
|
||||
{
|
||||
Settings = new SettingYaml();
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/Wallabag/Wallabag.csproj
Normal file
20
src/Wallabag/Wallabag.csproj
Normal file
@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>dotnet-WallabagWorker-D5E52F5F-C642-4BF8-9FD7-8C6C417B0D3A</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
|
||||
<PackageReference Include="YamlDotNet" Version="12.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Services\Linkding.Client\Linkding.Client.csproj" />
|
||||
<ProjectReference Include="..\Services\Wallabag.Client\Wallabag.Client.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
96
src/Wallabag/Worker.cs
Normal file
96
src/Wallabag/Worker.cs
Normal file
@ -0,0 +1,96 @@
|
||||
using Core.Abstraction;
|
||||
using Core.Entities.Linkding;
|
||||
using Core.Handler;
|
||||
using Linkding.Client;
|
||||
using Linkding.Client.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Wallabag.Client;
|
||||
using Wallabag.Client.Options;
|
||||
using Wallabag.Options;
|
||||
|
||||
namespace Wallabag;
|
||||
|
||||
public class Worker : BackgroundService
|
||||
{
|
||||
private readonly IHostApplicationLifetime _hostApplicationLifetime;
|
||||
private readonly ILogger<Worker> _logger;
|
||||
private readonly LinkdingService _linkdingService;
|
||||
private readonly WallabagService _wallabagService;
|
||||
private readonly LinkdingSettings _linkdingSettings;
|
||||
private readonly WallabagSettings _wallabagSettings;
|
||||
private readonly WorkerSettings _settings;
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
|
||||
public Worker(ILogger<Worker> logger, LinkdingService linkdingService, WallabagService wallabagService,
|
||||
IOptions<LinkdingSettings> linkdingSettings, IOptions<WorkerSettings> settings, IOptions<WallabagSettings> wallabagSettings, IConfiguration configuration, IHostApplicationLifetime hostApplicationLifetime)
|
||||
{
|
||||
_logger = logger;
|
||||
_linkdingService = linkdingService;
|
||||
_wallabagService = wallabagService;
|
||||
_configuration = configuration;
|
||||
_hostApplicationLifetime = hostApplicationLifetime;
|
||||
_wallabagSettings = wallabagSettings.Value;
|
||||
_settings = settings.Value;
|
||||
_linkdingSettings = linkdingSettings.Value;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
|
||||
|
||||
await RunSyncWallabag();
|
||||
int delay = _settings.Intervall * 60000;
|
||||
|
||||
if (delay > 0)
|
||||
{
|
||||
_logger.LogInformation($"Worker paused for: {_settings.Intervall} minutes");
|
||||
|
||||
await Task.Delay(delay, stoppingToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation($"Delay was set to '0' --> stopping worker");
|
||||
_hostApplicationLifetime.StopApplication();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RunSyncWallabag()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_wallabagSettings.Url))
|
||||
{
|
||||
|
||||
_logger.LogInformation($"Starting updating bookmarks for {_linkdingSettings.Url}");
|
||||
_logger.LogInformation("Collectin LinkdingService Handler");
|
||||
var wallabagHandlers = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(s => s.GetTypes())
|
||||
.Where(p => typeof(ISyncTaskHandler<WallabagService>).IsAssignableFrom(p) && p.IsClass);
|
||||
|
||||
if (wallabagHandlers != null && wallabagHandlers.Count() > 0)
|
||||
{
|
||||
var wallabags = await _wallabagService.GetEntries();
|
||||
|
||||
foreach (var handler in wallabagHandlers)
|
||||
{
|
||||
ISyncTaskHandler<WallabagService> handlerInstance = null;
|
||||
try
|
||||
{
|
||||
handlerInstance = (ISyncTaskHandler<WallabagService>) Activator.CreateInstance(handler);
|
||||
|
||||
await handlerInstance.ProcessAsync(wallabags, _wallabagService, _linkdingService, _logger, _configuration);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation($"no bookmarks found in {_linkdingSettings.Url}");
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/Wallabag/appsettings.Development.json
Normal file
8
src/Wallabag/appsettings.Development.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/Wallabag/appsettings.json
Normal file
8
src/Wallabag/appsettings.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/Wallabag/config.yml
Normal file
7
src/Wallabag/config.yml
Normal file
@ -0,0 +1,7 @@
|
||||
excludedDomains:
|
||||
- name: youtube
|
||||
pattern: https://[[a-zA-Z0-9]+\.]?(youtube)\.com(?:/.*)?
|
||||
- name: ebay
|
||||
pattern: https://[[a-zA-Z0-9]+\.]?(ebay)\.(com|de|fr)(?:/.*)?
|
||||
- name: amazon
|
||||
pattern: https://[[a-zA-Z0-9]+\.]?(amazon)\.(com|de|fr)(?:/.*)?
|
||||
Loading…
Reference in New Issue
Block a user