Compare commits

...

8 Commits

Author SHA1 Message Date
8034f79753 Birthday
hurl file over http file

AddControllers

Formatting
2025-09-20 14:18:46 -07:00
895dab9413 update-assets-set-local-date-time-for-three-and-seven 2025-08-09 07:32:55 -07:00
c4d42d79a0 DigiKam4 Archive
Change to only need the one json file and changed AppSettings to not need a binder file

Editorconfig for no new line before open braces

Nuget package bumps

Database lowest-version-history
2025-07-27 16:03:47 -07:00
ab90adee42 Insert into asset tag instead of updating is archived field
Convert to Podman
2025-06-15 18:10:52 -07:00
ac4e4c277d set-archive-immich 2024-11-24 15:36:57 -07:00
0e03b784a2 save-random-paths
sync-immich
2024-11-23 23:46:24 -07:00
590b1b87ca InitializeCorsAndHttps 2024-11-23 13:46:53 -07:00
c8325aafca Ready to test pulling random assets 2024-11-23 11:53:11 -07:00
41 changed files with 1292 additions and 466 deletions

View File

@ -21,14 +21,14 @@ csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true csharp_indent_case_contents_when_block = true
csharp_indent_labels = one_less_than_current csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true csharp_indent_switch_labels = true
csharp_new_line_before_catch = true csharp_new_line_before_catch = false
csharp_new_line_before_else = true csharp_new_line_before_else = false
csharp_new_line_before_finally = true csharp_new_line_before_finally = false
csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = all csharp_new_line_before_open_brace = none
csharp_new_line_between_query_expression_clauses = true csharp_new_line_between_query_expression_clauses = true
csharp_prefer_braces = false csharp_prefer_braces = true
csharp_prefer_qualified_reference = true:error csharp_prefer_qualified_reference = true:error
csharp_prefer_simple_default_expression = true:warning csharp_prefer_simple_default_expression = true:warning
csharp_prefer_simple_using_statement = true:warning csharp_prefer_simple_using_statement = true:warning
@ -106,101 +106,37 @@ dotnet_code_quality_unused_parameters = all
dotnet_code_quality_unused_parameters = non_public dotnet_code_quality_unused_parameters = non_public
dotnet_code_quality.CAXXXX.api_surface = private, internal dotnet_code_quality.CAXXXX.api_surface = private, internal
dotnet_diagnostic.CA1001.severity = error # CA1001: Types that own disposable fields should be disposable dotnet_diagnostic.CA1001.severity = error # CA1001: Types that own disposable fields should be disposable
dotnet_diagnostic.CA1001.severity = none # Question - CA1001: Types that own disposable fields should be disposable
dotnet_diagnostic.CA1051.severity = error # CA1051: Do not declare visible instance fields dotnet_diagnostic.CA1051.severity = error # CA1051: Do not declare visible instance fields
dotnet_diagnostic.CA1051.severity = none # Question - CA1051: Do not declare visible instance fields
dotnet_diagnostic.CA1416.severity = none # Question - CA1416: This call site is reachable on all platforms. 'EventLogEntryType.Error' is only supported on: 'windows'.
dotnet_diagnostic.CA1510.severity = none # Question - CA1510: Use 'ArgumentNullException.ThrowIfNull' instead of explicitly throwing a new exception instance
dotnet_diagnostic.CA1511.severity = warning # CA1511: Use 'ArgumentException.ThrowIfNullOrEmpty' instead of explicitly throwing a new exception instance dotnet_diagnostic.CA1511.severity = warning # CA1511: Use 'ArgumentException.ThrowIfNullOrEmpty' instead of explicitly throwing a new exception instance
dotnet_diagnostic.CA1513.severity = warning # Use 'ObjectDisposedException.ThrowIf' instead of explicitly throwing a new exception instance dotnet_diagnostic.CA1513.severity = warning # Use 'ObjectDisposedException.ThrowIf' instead of explicitly throwing a new exception instance
dotnet_diagnostic.CA1825.severity = warning # CA1825: Avoid zero-length array allocations dotnet_diagnostic.CA1825.severity = warning # CA1825: Avoid zero-length array allocations
dotnet_diagnostic.CA1829.severity = error # CA1829: Use Length/Count property instead of Count() when available dotnet_diagnostic.CA1829.severity = warning # CA1829: Use Length/Count property instead of Count() when available
dotnet_diagnostic.CA1829.severity = none # Question - CA1829: Use Length/Count property instead of Enumerable.Count method
dotnet_diagnostic.CA1834.severity = warning # CA1834: Consider using 'StringBuilder.Append(char)' when applicable dotnet_diagnostic.CA1834.severity = warning # CA1834: Consider using 'StringBuilder.Append(char)' when applicable
dotnet_diagnostic.CA1860.severity = error # CA1860: Prefer comparing 'Count' to 0 rather than using 'Any()', both for clarity and for performance dotnet_diagnostic.CA1860.severity = error # CA1860: Prefer comparing 'Count' to 0 rather than using 'Any()', both for clarity and for performance
dotnet_diagnostic.CA1860.severity = none # Question - CA1860: Avoid using 'Enumerable.Any()' extension method
dotnet_diagnostic.CA1862.severity = warning # CA1862: Prefer using 'string.Equals(string, StringComparison)' to perform a case-insensitive comparison, but keep in mind that this might cause subtle changes in behavior, so make sure to conduct thorough testing after applying the suggestion, or if culturally sensitive comparison is not required, consider using 'StringComparison.OrdinalIgnoreCase' dotnet_diagnostic.CA1862.severity = warning # CA1862: Prefer using 'string.Equals(string, StringComparison)' to perform a case-insensitive comparison, but keep in mind that this might cause subtle changes in behavior, so make sure to conduct thorough testing after applying the suggestion, or if culturally sensitive comparison is not required, consider using 'StringComparison.OrdinalIgnoreCase'
dotnet_diagnostic.CA1862.severity = none # Question - CA1862: Prefer using 'string.Equals(string, StringComparison)' to perform a case-insensitive comparison, but keep in mind that this might cause subtle changes in behavior, so make sure to conduct thorough testing after applying the suggestion, or if culturally sensitive comparison is not required, consider using 'StringComparison.OrdinalIgnoreCase'
dotnet_diagnostic.CA1869.severity = none # CA1869: Avoid creating a new 'JsonSerializerOptions' instance for every serialization operation. Cache and reuse instances instead. dotnet_diagnostic.CA1869.severity = none # CA1869: Avoid creating a new 'JsonSerializerOptions' instance for every serialization operation. Cache and reuse instances instead.
dotnet_diagnostic.CA2200.severity = none # Question - CA2200: Re-throwing caught exception changes stack information
dotnet_diagnostic.CA2201.severity = none # CA2201: Exception type System.NullReferenceException is reserved by the runtime dotnet_diagnostic.CA2201.severity = none # CA2201: Exception type System.NullReferenceException is reserved by the runtime
dotnet_diagnostic.CA2208.severity = none # Question - CA2208: Instantiate argument exceptions correctly
dotnet_diagnostic.CA2211.severity = none # Question - CA2211: Non-constant fields should not be visible
dotnet_diagnostic.CA2249.severity = none # Question - CA2249: Use 'string.Contains' instead of 'string.IndexOf' to improve readability
dotnet_diagnostic.CA2253.severity = none # Question - CA2253: Named placeholders should not be numeric values
dotnet_diagnostic.CA2254.severity = none # CA2254: The logging message template should not vary between calls to 'LoggerExtensions.LogInformation(ILogger, string?, params object?[])' dotnet_diagnostic.CA2254.severity = none # CA2254: The logging message template should not vary between calls to 'LoggerExtensions.LogInformation(ILogger, string?, params object?[])'
dotnet_diagnostic.CS0103.severity = none # Question - CS0103: The name 'Functions' does not exist in the current context
dotnet_diagnostic.CS0168.severity = none # Question - CS0168: The variable 'ex' is declared but never used
dotnet_diagnostic.CS0219.severity = none # Question - CS0219: The variable 'result' is assigned but its value is never used
dotnet_diagnostic.CS0618.severity = none # Question - CS0618: Compiler Warning (level 2)
dotnet_diagnostic.CS0659.severity = none # Question - CS0659: Compiler Warning (level 3)
dotnet_diagnostic.CS8600.severity = none # Question - CS8600: Converting null literal or possible null value to non-nullable type
dotnet_diagnostic.CS8602.severity = none # Question - CS8602: Dereference of a possibly null reference.
dotnet_diagnostic.CS8603.severity = none # Question - CS8603: Possible null reference return
dotnet_diagnostic.CS8604.severity = none # Question - CS8604: Possible null reference argument for parameter.
dotnet_diagnostic.CS0612.severity = none # Question - CS0612: is obsolete
dotnet_diagnostic.CS8618.severity = none # Question - CS8618: Non-nullable variable must contain a non-null value when exiting constructor
dotnet_diagnostic.CS8625.severity = none # Question - CS8625: Cannot convert null literal to non-nullable reference type.
dotnet_diagnostic.CS8629.severity = none # Question - CS8629: Nullable value type may be null
dotnet_diagnostic.CS8765.severity = none # Question - CS8765: Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes)
dotnet_diagnostic.IDE0001.severity = warning # IDE0001: Simplify name dotnet_diagnostic.IDE0001.severity = warning # IDE0001: Simplify name
dotnet_diagnostic.IDE0002.severity = warning # Simplify (member access) - System.Version.Equals("1", "2"); Version.Equals("1", "2"); dotnet_diagnostic.IDE0002.severity = warning # Simplify (member access) - System.Version.Equals("1", "2"); Version.Equals("1", "2");
dotnet_diagnostic.IDE0004.severity = warning # IDE0004: Cast is redundant. dotnet_diagnostic.IDE0004.severity = warning # IDE0004: Cast is redundant.
dotnet_diagnostic.IDE0004.severity = none # Question - IDE0004: Cast is redundant. dotnet_diagnostic.IDE0005.severity = warning # Using directive is unnecessary
dotnet_diagnostic.IDE0003.severity = none # Question - IDE0003: this and Me preferences
dotnet_diagnostic.IDE0005.severity = error # Using directive is unnecessary
dotnet_diagnostic.IDE0005.severity = none # Question - IDE0005: Remove unnecessary using directives
dotnet_diagnostic.IDE0008.severity = none # Question - IDE0008: 'var' preferences
dotnet_diagnostic.IDE0010.severity = none # Add missing cases to switch statement (IDE0010) dotnet_diagnostic.IDE0010.severity = none # Add missing cases to switch statement (IDE0010)
dotnet_diagnostic.IDE0017.severity = none # Question - IDE0017: Use object initializers
dotnet_diagnostic.IDE0019.severity = none # Question - IDE0019: Use pattern matching to avoid 'as' followed by a 'null' check
dotnet_diagnostic.IDE0021.severity = none # Question - IDE0021: Use expression body for constructors
dotnet_diagnostic.IDE0022.severity = none # Question - IDE0022: Use expression body for methods
dotnet_diagnostic.IDE0025.severity = none # Question - IDE0025: Use expression body for properties
dotnet_diagnostic.IDE0027.severity = none # Question - IDE0027: Use expression body for accessor
dotnet_diagnostic.IDE0028.severity = error # IDE0028: Collection initialization can be simplified dotnet_diagnostic.IDE0028.severity = error # IDE0028: Collection initialization can be simplified
dotnet_diagnostic.IDE0028.severity = none # Question - IDE0028: Use collection initializers or expressions
dotnet_diagnostic.IDE0031.severity = warning # Use null propagation (IDE0031) dotnet_diagnostic.IDE0031.severity = warning # Use null propagation (IDE0031)
dotnet_diagnostic.IDE0031.severity = none # Question - IDE0031: Use null propagation (IDE0031)
dotnet_diagnostic.IDE0032.severity = none # Question - IDE0032: Use auto-implemented property
dotnet_diagnostic.IDE0037.severity = none # Question - IDE0037: Member name can be simplified
dotnet_diagnostic.IDE0040.severity = none # Question - IDE0040: Add accessibility modifiers
dotnet_diagnostic.IDE0041.severity = none # Question - IDE0041: Use 'is null' check
dotnet_diagnostic.IDE0044.severity = none # Question - IDE0044: Add readonly modifier
dotnet_diagnostic.IDE0047.severity = warning # IDE0047: Parentheses can be removed dotnet_diagnostic.IDE0047.severity = warning # IDE0047: Parentheses can be removed
dotnet_diagnostic.IDE0047.severity = none # Question - IDE0047: Parentheses preferences
dotnet_diagnostic.IDE0048.severity = none # Parentheses preferences (IDE0047 and IDE0048) dotnet_diagnostic.IDE0048.severity = none # Parentheses preferences (IDE0047 and IDE0048)
dotnet_diagnostic.IDE0049.severity = warning # Use language keywords instead of framework type names for type references (IDE0049) dotnet_diagnostic.IDE0049.severity = warning # Use language keywords instead of framework type names for type references (IDE0049)
dotnet_diagnostic.IDE0051.severity = error # Private member '' is unused [, ] dotnet_diagnostic.IDE0051.severity = error # Private member '' is unused [, ]
dotnet_diagnostic.IDE0051.severity = none # Question - IDE0051: Remove unused private member dotnet_diagnostic.IDE0058.severity = warning # IDE0058: Expression value is never used
dotnet_diagnostic.IDE0053.severity = none # Question - IDE0053: Use expression body for lambdas
dotnet_diagnostic.IDE0054.severity = none # Question - IDE0054: Use compound assignment
dotnet_diagnostic.IDE0055.severity = none # Question - IDE0055: Formatting rule
dotnet_diagnostic.IDE0057.severity = none # Question - IDE0057: Substring can be simplified
dotnet_diagnostic.IDE0058.severity = error # IDE0058: Expression value is never used
dotnet_diagnostic.IDE0058.severity = none # Question - IDE0058: Remove unnecessary expression value
dotnet_diagnostic.IDE0059.severity = none # Question - IDE0059: Remove unnecessary value assignment
dotnet_diagnostic.IDE0060.severity = error # IDE0060: Remove unused parameter dotnet_diagnostic.IDE0060.severity = error # IDE0060: Remove unused parameter
dotnet_diagnostic.IDE0060.severity = none # Question - IDE0060: Remove unused parameter
dotnet_diagnostic.IDE0063.severity = none # Question - IDE0063: Use simple 'using' statement
dotnet_diagnostic.IDE0066.severity = none # Question - IDE0066: Use switch expression
dotnet_diagnostic.IDE0074.severity = warning # IDE0074: Use compound assignment dotnet_diagnostic.IDE0074.severity = warning # IDE0074: Use compound assignment
dotnet_diagnostic.IDE0078.severity = none # Question - IDE0078: Use pattern matching
dotnet_diagnostic.IDE0100.severity = none # Question - IDE0100: Remove redundant equality
dotnet_diagnostic.IDE0090.severity = none # Question - IDE0090: Simplify new expression
dotnet_diagnostic.IDE0130.severity = none # Namespace does not match folder structure (IDE0130) dotnet_diagnostic.IDE0130.severity = none # Namespace does not match folder structure (IDE0130)
dotnet_diagnostic.IDE0161.severity = none # Question - IDE0161: Namespace declaration preferences
dotnet_diagnostic.IDE0270.severity = none # Question - IDE0270: Null check can be simplified
dotnet_diagnostic.IDE0270.severity = warning # IDE0270: Null check can be simplified dotnet_diagnostic.IDE0270.severity = warning # IDE0270: Null check can be simplified
dotnet_diagnostic.IDE0290.severity = none # Use primary constructor [Distance]csharp(IDE0290) dotnet_diagnostic.IDE0290.severity = none # Use primary constructor [Distance]csharp(IDE0290)
dotnet_diagnostic.IDE0300.severity = error # IDE0300: Collection initialization can be simplified dotnet_diagnostic.IDE0300.severity = error # IDE0300: Collection initialization can be simplified
dotnet_diagnostic.IDE0065.severity = none # Question - IDE0065: 'using' directive placement
dotnet_diagnostic.IDE0300.severity = none # Question - IDE0300: Collection initialization can be simplified
dotnet_diagnostic.IDE0301.severity = error #IDE0301: Collection initialization can be simplified dotnet_diagnostic.IDE0301.severity = error #IDE0301: Collection initialization can be simplified
dotnet_diagnostic.IDE0305.severity = none # IDE0305: Collection initialization can be simplified dotnet_diagnostic.IDE0305.severity = none # IDE0305: Collection initialization can be simplified
dotnet_diagnostic.IDE1006.severity = none # Question - IDE1006: Use collection expression for builder dotnet_style_prefer_collection_expression
dotnet_diagnostic.IDE2000.severity = error # IDE2000: Allow multiple blank lines
dotnet_diagnostic.IDE2000.severity = none # Question - IDE2000: Allow multiple blank lines
dotnet_naming_rule.abstract_method_should_be_pascal_case.severity = warning dotnet_naming_rule.abstract_method_should_be_pascal_case.severity = warning
dotnet_naming_rule.abstract_method_should_be_pascal_case.style = pascal_case dotnet_naming_rule.abstract_method_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.abstract_method_should_be_pascal_case.symbols = abstract_method dotnet_naming_rule.abstract_method_should_be_pascal_case.symbols = abstract_method

8
.vscode/bash.md vendored
View File

@ -15,3 +15,11 @@ dotnet run --project src/ImmichToSlideshow
```bash 1731643960696 = 638672407606960000 = Thu Nov 14 2024 21:12:40 GMT-0700 (Mountain Standard Time) ```bash 1731643960696 = 638672407606960000 = Thu Nov 14 2024 21:12:40 GMT-0700 (Mountain Standard Time)
docker compose up --build docker compose up --build
``` ```
```bash 1732418534573 = 638680153345730000 = Sat Nov 23 2024 20:22:14 GMT-0700 (Mountain Standard Time)
# docker system prune
docker build .
docker login gitea.phares.duckdns.org:443
docker tag fa136e5bb221 gitea.phares.duckdns.org:443/phares3757/immich-to-slideshow:latest
docker push gitea.phares.duckdns.org:443/phares3757/immich-to-slideshow:latest
```

24
.vscode/launch.json vendored
View File

@ -8,8 +8,8 @@
"name": ".NET Core Launch (console)", "name": ".NET Core Launch (console)",
"type": "coreclr", "type": "coreclr",
"request": "launch", "request": "launch",
"preLaunchTask": "build", "preLaunchTask": "Build",
"program": "${workspaceFolder}/src/ImmichToSlideshow/bin/Debug/net8.0/ImmichToSlideshow.dll", "program": "${workspaceFolder}/src/ImmichToSlideshow/bin/Debug/net9.0/ImmichToSlideshow.dll",
"args": [ "args": [
"s", "s",
"test" "test"
@ -28,6 +28,24 @@
"request": "launch", "request": "launch",
"name": "node Launch Current Opened File", "name": "node Launch Current Opened File",
"program": "${file}" "program": "${file}"
} },
{
"name": "Docker",
"type": "coreclr",
"request": "attach",
"sourceFileMap": {
"/app": "${workspaceRoot}"
},
"processId": "${command:pickRemoteProcess}",
"pipeTransport": {
"debuggerPath": "/root/vsdbg/vsdbg",
"pipeProgram": "docker",
"pipeCwd": "${workspaceRoot}",
"quoteArgs": false,
"pipeArgs": [
"exec -i immich-to-slideshow-server"
]
}
},
] ]
} }

129
.vscode/settings.json vendored
View File

@ -1,34 +1,107 @@
{ {
"[markdown]": { "files.associations": {
"editor.wordWrap": "off" "*.ffs_gui": "xml",
"*.hurl": "http",
"*.org": "ini",
"*.net": "ini",
"default": "ini"
},
"[markdown]": {
"editor.wordWrap": "off"
},
"files.exclude": {
"**/.git": false,
"**/node_modules": true
},
"files.watcherExclude": {
"**/node_modules": true
},
"cSpell.words": [
"accessibilities",
"ackages",
"Acks",
"aspnet",
"ASPNETCORE",
"binlog",
"buildhelp",
"cachefile",
"CAXXXX",
"checkin",
"codegen",
"coveragexml",
"csdef",
"dbmdl",
"dbproj",
"DENITED",
"Digi",
"dlldata",
"docstates",
"ebug",
"elease",
"eleases",
"esult",
"Fractors",
"gitea",
"Immich",
"Infineon",
"Installshield",
"iobj",
"ipch",
"ipdb",
"jmconfig",
"mfractor",
"Npgsql",
"Nsight",
"ntvs",
"NUNIT",
"nupkg",
"nvuser",
"opendb",
"opensdf",
"paket",
"Paket",
"pidb",
"psess",
"PTVS",
"publishproj",
"publishsettings",
"pycache",
"rptproj",
"rsuser",
"schemaview",
"Silverlight",
"svclog",
"Telerik's",
"Thumbhash",
"ublish",
"uild",
"userosscache",
"userprefs",
"vspscc",
"vspx",
"vssscc",
"wwwroot",
"xpress"
],
"rest-client.environmentVariables": {
"$shared": {
"productId": "asdfasdf",
"reviewId": "asdfasdf"
}, },
"files.exclude": { "dev": {
"**/.git": false, "ownerId": "5f0b1052-466d-44de-a554-226d7256850d",
"**/node_modules": true "host": "https://localhost:5003",
"token": "ey..dev",
}, },
"files.watcherExclude": { "affirm": {
"**/node_modules": true "ownerId": "5f0b1052-466d-44de-a554-226d7256850d",
"host": "https://immich-to-slideshow.affirm.duckdns.org/",
"token": "ey..affirm"
}, },
"cSpell.words": [ "phares": {
"accessibilities", "ownerId": "5f0b1052-466d-44de-a554-226d7256850d",
"Acks", "host": "https://immich-to-slideshow.phares.duckdns.org/",
"aspnet", "token": "ey..phares"
"ASPNETCORE",
"CAXXXX",
"DENITED",
"Immich",
"Infineon",
"Npgsql",
"Thumbhash"
],
"rest-client.environmentVariables": {
"$shared": {
"productId": "asdfasdf",
"reviewId": "asdfasdf"
},
"dev": {
"host": "http://localhost:5003",
"token": "ey..dev"
}
} }
}
} }

86
.vscode/tasks.json vendored
View File

@ -43,7 +43,7 @@
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
}, },
{ {
"label": "Format-Whitespaces", "label": "Format Whitespaces",
"command": "dotnet", "command": "dotnet",
"type": "process", "type": "process",
"args": [ "args": [
@ -53,7 +53,7 @@
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
}, },
{ {
"label": "build", "label": "Build",
"command": "dotnet", "command": "dotnet",
"type": "process", "type": "process",
"args": [ "args": [
@ -65,7 +65,7 @@
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
}, },
{ {
"label": "publish", "label": "Publish",
"command": "dotnet", "command": "dotnet",
"type": "process", "type": "process",
"args": [ "args": [
@ -77,7 +77,7 @@
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
}, },
{ {
"label": "watch", "label": "Watch",
"command": "dotnet", "command": "dotnet",
"type": "process", "type": "process",
"args": [ "args": [
@ -88,6 +88,84 @@
], ],
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
}, },
{
"label": "Podman Login",
"command": "podman",
"type": "process",
"args": [
"login",
"gitea.phares.duckdns.org:443"
],
"problemMatcher": "$msCompile"
},
{
"label": "Podman Compose Up Build",
"command": "podman",
"type": "process",
"args": [
"compose",
"up",
"--build"
],
"problemMatcher": "$msCompile"
},
{
"label": "Podman Build",
"command": "podman",
"type": "process",
"args": [
"build",
"-t",
"immich-to-slideshow",
"."
],
"problemMatcher": "$msCompile"
},
{
"label": "Podman Image List",
"command": "podman",
"type": "process",
"args": [
"image",
"ls"
],
"problemMatcher": "$msCompile"
},
{
"label": "Podman Run",
"command": "podman",
"type": "process",
"args": [
"run",
"-p",
"5001:5001",
"--name",
"immich-to-slideshow_webapp",
"a3de856b5731"
],
"problemMatcher": "$msCompile"
},
{
"label": "Podman Tag",
"command": "podman",
"type": "process",
"args": [
"tag",
"3f9eac99b7cf0710c5d762322ce4ded3f57fc008ee8310757760bb78ad8e029f",
"gitea.phares.duckdns.org:443/phares3757/immich-to-slideshow:latest"
],
"problemMatcher": "$msCompile"
},
{
"label": "Podman Push",
"command": "podman",
"type": "process",
"args": [
"push",
"gitea.phares.duckdns.org:443/phares3757/immich-to-slideshow:latest"
],
"problemMatcher": "$msCompile"
},
{ {
"label": "Publish AOT", "label": "Publish AOT",
"command": "dotnet", "command": "dotnet",

View File

@ -1,5 +1,5 @@
# Stage 1: Build Stage # Stage 1: Build Stage
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src WORKDIR /src
@ -17,14 +17,9 @@ FROM build AS publish
RUN dotnet publish 'ImmichToSlideshow.csproj' -c Release -o /app/publish RUN dotnet publish 'ImmichToSlideshow.csproj' -c Release -o /app/publish
# Stage 3: Run Stage # Stage 3: Run Stage
FROM mcr.microsoft.com/dotnet/aspnet:8.0 FROM mcr.microsoft.com/dotnet/aspnet:9.0
ENV ASPNETCORE_HTTP_PORTS=5001 ENV ASPNETCORE_HTTP_PORTS=5001
EXPOSE 5001 EXPOSE 5001
WORKDIR /app WORKDIR /app
COPY --from=publish /app/publish . COPY --from=publish /app/publish .
ENTRYPOINT [ "dotnet", "ImmichToSlideshow.dll" ] ENTRYPOINT [ "dotnet", "ImmichToSlideshow.dll" ]
# docker build -t sprint-console-001 .
# docker images ls | grep -i 001
# docker run -p 5001:5001 --name sprint-console-api-001 sprint-console-001
# docker run -p 5001:5001 --name sprint-console-api-001 sprint-console-001

View File

@ -1,6 +1,6 @@
services: services:
webapp: webapp:
container_name: immich-to-slideshow-api container_name: immich-to-slideshow
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile

View File

@ -1,6 +1,6 @@
{ {
"sdk": { "sdk": {
"rollForward": "latestMinor", "rollForward": "latestMinor",
"version": "8.0.100" "version": "9.0.100"
} }
} }

View File

@ -1,6 +0,0 @@
@immich_to_slideshow_HostAddress = http://localhost:5243
GET {{immich_to_slideshow_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@ -0,0 +1,80 @@
GET http://127.0.0.1:5001/api/v1/assets/columns/
Accept: application/json
HTTP 200
[Asserts]
header "Content-Type" == "application/json"
###
GET http://127.0.0.1:5001/api/v1/assets/owner-ids/
Accept: application/json
HTTP 200
[Asserts]
header "Content-Type" == "application/json"
###
GET http://127.0.0.1:5001/api/v1/assets/5f0b1052-466d-44de-a554-226d7256850d
Accept: application/json
HTTP 200
[Asserts]
header "Content-Type" == "application/json"
###
GET http://127.0.0.1:5001/api/v1/assets/5f0b1052-466d-44de-a554-226d7256850d/random-paths/
Accept: application/json
HTTP 200
[Asserts]
header "Content-Type" == "application/json"
###
GET http://127.0.0.1:5001/api/v1/assets/5f0b1052-466d-44de-a554-226d7256850d/save-random-paths/
Accept: application/json
HTTP 200
[Asserts]
header "Content-Type" == "application/json"
###
GET http://127.0.0.1:5001/api/v1/assets/5f0b1052-466d-44de-a554-226d7256850d/sync-immich/
Accept: application/json
HTTP 200
[Asserts]
header "Content-Type" == "application/json"
###
GET http://127.0.0.1:5001/api/v1/assets/5f0b1052-466d-44de-a554-226d7256850d/set-archive-immich/
Accept: application/json
HTTP 200
[Asserts]
header "Content-Type" == "application/json"
###
GET http://127.0.0.1:5001/api/v1/assets/5f0b1052-466d-44de-a554-226d7256850d/set-digi-kam-4-archive-immich/
Accept: application/json
HTTP 200
[Asserts]
header "Content-Type" == "application/json"
###
GET http://127.0.0.1:5001/api/v1/assets/5f0b1052-466d-44de-a554-226d7256850d/archived-tag/
Accept: application/json
HTTP 200
[Asserts]
header "Content-Type" == "application/json"
###

View File

@ -1 +0,0 @@
GET www.google.com

View File

@ -1,93 +1,54 @@
using ImmichToSlideshow.Models.Immich;
using ImmichToSlideshow.Services; using ImmichToSlideshow.Services;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Collections.ObjectModel;
namespace ImmichToSlideshow.Controllers; namespace ImmichToSlideshow.Controllers;
[ApiController] [ApiController]
[Route("[controller]")] [Route("api/v1/[controller]")]
public class AssetsController(AssetService assetService) : ControllerBase public class AssetsController(AssetService assetService) : ControllerBase {
{
private readonly string _ContentType = "application/json";
private readonly AssetService _AssetService = assetService; private readonly AssetService _AssetService = assetService;
[HttpGet()] [HttpGet("columns")]
public IActionResult Get() public IActionResult GetColumns() =>
{ Content(_AssetService.GetColumns() ?? string.Empty, _ContentType);
ReadOnlyCollection<Asset> assets = _AssetService.Get();
AssetResponse?[] assetResponses = AssetResponse.FromDomain(assets);
return Ok(assetResponses);
}
[HttpPost] [HttpGet("owner-ids")]
public IActionResult Create(CreateAssetRequest request) public IActionResult GetOwnerIds() =>
{ Content(_AssetService.GetOwnerIds() ?? string.Empty, _ContentType);
// mapping to internal representation
Asset asset = request.ToDomain();
// invoke the use case [HttpGet("{ownerId:guid}")]
_AssetService.Create(asset); public IActionResult Get(Guid ownerId) =>
Content(_AssetService.GetAssets(ownerId) ?? string.Empty, _ContentType);
// mapping to external representation [HttpGet("{ownerId:guid}/random-paths")]
AssetResponse assetResponse = AssetResponse.FromDomain(asset); public IActionResult GetRandomPaths(Guid ownerId) =>
Ok(_AssetService.GetRandomPaths(ownerId, monthDay: null));
// return 201 created response [HttpGet("{ownerId:guid}/archived-tag")]
return CreatedAtAction( public IActionResult GetArchivedTag(Guid ownerId) =>
actionName: nameof(Get), Content(_AssetService.GetArchivedTag(ownerId) ?? string.Empty, _ContentType);
routeValues: new { AssetId = asset.Id },
value: assetResponse);
}
[HttpGet("{assetId:guid}")] [HttpGet("{ownerId:guid}/save-random-paths")]
public IActionResult Get(Guid assetId) public IActionResult SaveRandomPaths(Guid ownerId, string? month_day) =>
{ Content(_AssetService.SaveRandomPaths(ownerId, monthDay: month_day) ?? string.Empty, _ContentType);
//get the asset
Asset? asset = _AssetService.Get(assetId);
// mapping to external representation [HttpGet("{ownerId:guid}/sync-immich")]
AssetResponse? assetResponse = AssetResponse.FromDomain(asset); public IActionResult SyncImmich(Guid ownerId) =>
Ok(_AssetService.SyncImmich(ownerId));
// return 200 ok response [HttpGet("{ownerId:guid}/set-archive-immich")]
return assetResponse is null public IActionResult SetArchiveImmich(Guid ownerId) =>
? Problem(statusCode: StatusCodes.Status404NotFound, detail: $"Asset not found {assetId}") Ok(_AssetService.SetArchiveImmich(ownerId));
: Ok(assetResponse);
}
public record CreateAssetRequest(string Id, [HttpGet("{ownerId:guid}/set-digi-kam-4-archive-immich")]
string DeviceAssetId, public IActionResult SetDigiKam4ArchiveImmich(Guid ownerId) =>
string OwnerId, Ok(_AssetService.SetDigiKam4ArchiveImmich(ownerId));
string OriginalFileName,
string Path)
{
public Asset ToDomain() => [HttpGet("update-assets-set-local-date-time-for-three-and-seven")]
Asset.Get(id: Id, public IActionResult UpdateAssetsSetLocalDateTimeForThreeAndSeven() =>
deviceAssetId: DeviceAssetId, Ok(_AssetService.UpdateAssetsSetLocalDateTimeForThreeAndSeven());
ownerId: OwnerId,
originalFileName: OriginalFileName,
path: Path);
}
public record AssetResponse(string Id,
string DeviceAssetId,
string OwnerId,
string OriginalFileName,
string Path)
{
public static AssetResponse? FromDomain(Asset? asset) =>
asset is null ? null : new AssetResponse(
Id: asset.Id,
DeviceAssetId: asset.DeviceAssetId,
OwnerId: asset.OwnerId,
OriginalFileName: asset.OriginalFileName,
Path: asset.Path);
public static AssetResponse?[] FromDomain(IEnumerable<Asset> assets) =>
assets.Select(FromDomain).ToArray();
}
} }

View File

@ -1,13 +1,14 @@
using ImmichToSlideshow.Models;
using ImmichToSlideshow.Services; using ImmichToSlideshow.Services;
namespace ImmichToSlideshow.DependencyInjection; namespace ImmichToSlideshow.DependencyInjection;
public static class ServiceCollectionExtensions public static class ServiceCollectionExtensions {
{
public static IServiceCollection AddServices(this IServiceCollection services) public static IServiceCollection AddServices(this IServiceCollection services, AppSettings appSettings) {
{ _ = services.AddControllers();
_ = services.AddScoped<AssetService>(); _ = services.AddScoped<AssetService>();
_ = services.AddSingleton(_ => appSettings);
return services; return services;
} }

View File

@ -0,0 +1,74 @@
using ImmichToSlideshow.Models.DigiKam;
using System.Collections.ObjectModel;
namespace ImmichToSlideshow.Domain;
// Business concerns
public record ImageTag(long Id,
string Name,
string Tag) {
internal static ReadOnlyCollection<ImageTag>? Get(string tag, string? tagsPath, string? imageTagsPath, string? imagesPath) {
List<ImageTag>? results;
ReadOnlyCollection<Tags>? tags = Tags.GetTags(tagsPath);
if (tags is null || tags.Count == 0) {
results = null;
} else {
ReadOnlyCollection<int> tagIds = GetTagIds(tag, tags);
if (tagIds.Count == 0) {
results = null;
} else {
ReadOnlyCollection<ImageTags>? imageTags = ImageTags.GetImageTags(imageTagsPath);
if (imageTags is null || imageTags.Count == 0) {
results = null;
} else {
ReadOnlyCollection<int> imageIds = GetImageIds(imageTags, tagIds);
if (imageIds.Count == 0) {
results = null;
} else {
results = [];
long id;
ImageTag imageTag;
ReadOnlyCollection<Images>? images = Images.GetImages(imagesPath);
if (images is null || images.Count == 0) {
results = null;
} else {
foreach (Images i in images) {
if (!imageIds.Contains(i.Id) || !long.TryParse(i.Name.Split('.')[0], out id)) {
continue;
}
imageTag = new(Id: id, Name: i.Name, Tag: tag);
results.Add(imageTag);
}
}
}
}
}
}
return results?.AsReadOnly();
}
private static ReadOnlyCollection<int> GetTagIds(string tag, ReadOnlyCollection<Tags> tags) {
List<int> results = [];
foreach (Tags t in tags) {
if (t.Name != tag) {
continue;
}
results.Add(t.Id);
}
return results.AsReadOnly();
}
private static ReadOnlyCollection<int> GetImageIds(ReadOnlyCollection<ImageTags> imageTags, ReadOnlyCollection<int> tagIds) {
List<int> results = [];
foreach (ImageTags i in imageTags) {
if (!tagIds.Contains(i.TagId)) {
continue;
}
results.Add(i.ImageId);
}
return results.AsReadOnly();
}
}

View File

@ -1,13 +1,8 @@
namespace ImmichToSlideshow.Domain; namespace ImmichToSlideshow.Domain;
public class Product // Business concerns
{
public Guid Id { get; init; } = Guid.NewGuid(); public record Product(Guid Id,
public required string Name { get; init; } string Name,
public required string Category { get; init; } string Category,
public required string SubCategory { get; init; } string SubCategory);
// Business concerns
}

View File

@ -1,14 +1,11 @@
namespace ImmichToSlideshow.Domain; namespace ImmichToSlideshow.Domain;
public class User // Business concerns
{
public Guid Id { get; init; } = Guid.NewGuid(); public record User(Guid Id,
public List<Product> Products { get; init; } = []; List<Product> Products) {
internal void AddProduct(Product product) => internal void AddProduct(Product product) =>
Products.Add(product); Products.Add(product);
// Business concerns
} }

View File

@ -1,12 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers> <RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers>
<UserSecretsId>cc24ad7a-1d95-4c47-a3ea-0d8475ab06da</UserSecretsId> <UserSecretsId>cc24ad7a-1d95-4c47-a3ea-0d8475ab06da</UserSecretsId>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Npgsql" Version="9.0.0" /> <PackageReference Include="Npgsql" Version="9.0.3" />
<PackageReference Include="System.Text.Json" Version="9.0.7" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -3,21 +3,87 @@ using System.Text.Json.Serialization;
namespace ImmichToSlideshow.Models; namespace ImmichToSlideshow.Models;
public record AppSettings(string Company, public record AppSettings(string Company, Settings Settings, string URLs, string[] WithOrigins) {
string ConnectionString,
string WorkingDirectoryName)
{
public override string ToString() public override string ToString() {
{
string result = JsonSerializer.Serialize(this, AppSettingsSourceGenerationContext.Default.AppSettings); string result = JsonSerializer.Serialize(this, AppSettingsSourceGenerationContext.Default.AppSettings);
return result; return result;
} }
private static void Verify(AppSettings appSettings) {
if (appSettings?.Company is null) {
throw new NullReferenceException(nameof(Company));
}
if (appSettings?.URLs is null) {
throw new NullReferenceException(nameof(URLs));
}
if (appSettings?.WithOrigins is null) {
throw new NullReferenceException(nameof(WithOrigins));
}
if (appSettings?.Settings?.AddDays is null) {
throw new NullReferenceException(nameof(Settings.AddDays));
}
if (appSettings?.Settings?.BirthdayFormat is null) {
throw new NullReferenceException(nameof(Settings.BirthdayFormat));
}
if (appSettings?.Settings?.ArchivedTag is null) {
throw new NullReferenceException(nameof(Settings.ArchivedTag));
}
if (appSettings?.Settings?.ConnectionString is null) {
throw new NullReferenceException(nameof(Settings.ConnectionString));
}
if (appSettings?.Settings?.DigiKam4 is null) {
throw new NullReferenceException(nameof(Settings.DigiKam4));
}
if (appSettings?.Settings?.FilterTags is null) {
throw new NullReferenceException(nameof(Settings.FilterTags));
}
if (appSettings?.Settings?.ImmichUploadDirectory is null) {
throw new NullReferenceException(nameof(Settings.ImmichUploadDirectory));
}
if (appSettings?.Settings?.People is null) {
throw new NullReferenceException(nameof(Settings.People));
}
if (appSettings?.Settings?.RandomResultsDirectory is null) {
throw new NullReferenceException(nameof(Settings.RandomResultsDirectory));
}
if (appSettings?.Settings?.SyncDirectory is null) {
throw new NullReferenceException(nameof(Settings.SyncDirectory));
}
}
public static AppSettings Get(IConfigurationRoot configurationRoot) {
AppSettings result;
#pragma warning disable IL3050, IL2026
string? urls = configurationRoot.GetSection(nameof(URLs)).Get<string>();
string? company = configurationRoot.GetSection(nameof(Company)).Get<string>();
Settings? settings = configurationRoot.GetSection(nameof(Settings)).Get<Settings>();
string[]? withOrigins = configurationRoot.GetSection(nameof(WithOrigins)).Get<string[]>();
#pragma warning restore IL3050, IL2026
if (company is null || settings is null || urls is null || withOrigins is null) {
List<string> paths = [];
foreach (IConfigurationProvider configurationProvider in configurationRoot.Providers) {
if (configurationProvider is not Microsoft.Extensions.Configuration.Json.JsonConfigurationProvider jsonConfigurationProvider) {
continue;
}
if (jsonConfigurationProvider.Source.FileProvider is not Microsoft.Extensions.FileProviders.PhysicalFileProvider physicalFileProvider) {
continue;
}
paths.Add(physicalFileProvider.Root);
}
throw new NotSupportedException($"Not found!{Environment.NewLine}{string.Join(Environment.NewLine, paths.Distinct())}");
}
result = new(Company: company,
Settings: settings,
URLs: urls,
WithOrigins: withOrigins);
Verify(result);
return result;
}
} }
[JsonSourceGenerationOptions(WriteIndented = true)] [JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(AppSettings))] [JsonSerializable(typeof(AppSettings))]
internal partial class AppSettingsSourceGenerationContext : JsonSerializerContext internal partial class AppSettingsSourceGenerationContext : JsonSerializerContext {
{
} }

View File

@ -1,2 +0,0 @@
[*.cs]
csharp_preserve_single_line_statements = true

View File

@ -1,65 +0,0 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace ImmichToSlideshow.Models.Binder;
public class AppSettings
{
public string? Company { get; set; }
public string? ConnectionString { get; set; }
public string? WorkingDirectoryName { get; set; }
public override string ToString()
{
string result = JsonSerializer.Serialize(this, BinderAppSettingsSourceGenerationContext.Default.AppSettings);
return result;
}
private static void PreVerify(IConfigurationRoot configurationRoot, AppSettings? appSettings)
{
if (appSettings?.Company is null)
{
List<string> paths = [];
foreach (IConfigurationProvider configurationProvider in configurationRoot.Providers)
{
if (configurationProvider is not Microsoft.Extensions.Configuration.Json.JsonConfigurationProvider jsonConfigurationProvider)
continue;
if (jsonConfigurationProvider.Source.FileProvider is not Microsoft.Extensions.FileProviders.PhysicalFileProvider physicalFileProvider)
continue;
paths.Add(physicalFileProvider.Root);
}
throw new NotSupportedException($"Not found!{Environment.NewLine}{string.Join(Environment.NewLine, paths.Distinct())}");
}
}
private static Models.AppSettings Get(AppSettings? appSettings)
{
Models.AppSettings result;
if (appSettings?.Company is null) throw new NullReferenceException(nameof(appSettings.Company));
if (appSettings?.ConnectionString is null) throw new NullReferenceException(nameof(appSettings.ConnectionString));
if (appSettings?.WorkingDirectoryName is null) throw new NullReferenceException(nameof(appSettings.WorkingDirectoryName));
result = new(appSettings.Company,
appSettings.ConnectionString,
appSettings.WorkingDirectoryName);
return result;
}
public static Models.AppSettings Get(IConfigurationRoot configurationRoot)
{
Models.AppSettings result;
#pragma warning disable IL3050, IL2026
AppSettings? appSettings = configurationRoot.Get<AppSettings>();
#pragma warning restore IL3050, IL2026
PreVerify(configurationRoot, appSettings);
result = Get(appSettings);
return result;
}
}
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(AppSettings))]
internal partial class BinderAppSettingsSourceGenerationContext : JsonSerializerContext
{
}

View File

@ -0,0 +1,11 @@
using System.Text.Json.Serialization;
namespace ImmichToSlideshow.Models.DigiKam;
public record AlbumRoots([property: JsonPropertyName("caseSensitivity")] int CaseSensitivity,
[property: JsonPropertyName("id")] int Id,
[property: JsonPropertyName("identifier")] string Identifier,
[property: JsonPropertyName("label")] string Label,
[property: JsonPropertyName("specificPath")] string SpecificPath,
[property: JsonPropertyName("status")] int Status,
[property: JsonPropertyName("type")] int Type);

View File

@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace ImmichToSlideshow.Models.DigiKam;
public record Albums([property: JsonPropertyName("albumRoot")] int AlbumRoot,
[property: JsonPropertyName("caption")] object Caption,
[property: JsonPropertyName("collection")] object Collection,
[property: JsonPropertyName("date")] string Date,
[property: JsonPropertyName("icon")] object Icon,
[property: JsonPropertyName("id")] int Id,
[property: JsonPropertyName("modificationDate")] DateTime ModificationDate,
[property: JsonPropertyName("relativePath")] string RelativePath);

View File

@ -0,0 +1,11 @@
using System.Text.Json.Serialization;
namespace ImmichToSlideshow.Models.DigiKam;
public record ImageComments([property: JsonPropertyName("author")] object Author,
[property: JsonPropertyName("comment")] string Comment,
[property: JsonPropertyName("date")] object Date,
[property: JsonPropertyName("id")] int Id,
[property: JsonPropertyName("imageid")] int ImageId,
[property: JsonPropertyName("language")] string Language,
[property: JsonPropertyName("type")] int Type);

View File

@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace ImmichToSlideshow.Models.DigiKam;
public record ImagePositions([property: JsonPropertyName("accuracy")] object Accuracy,
[property: JsonPropertyName("altitude")] double? Altitude,
[property: JsonPropertyName("description")] object Description,
[property: JsonPropertyName("imageid")] int ImageId,
[property: JsonPropertyName("latitude")] string Latitude,
[property: JsonPropertyName("latitudeNumber")] double? LatitudeNumber,
[property: JsonPropertyName("longitude")] string Longitude,
[property: JsonPropertyName("longitudeNumber")] double? LongitudeNumber,
[property: JsonPropertyName("orientation")] object Orientation,
[property: JsonPropertyName("roll")] object Roll,
[property: JsonPropertyName("tilt")] object Tilt);

View File

@ -0,0 +1,8 @@
using System.Text.Json.Serialization;
namespace ImmichToSlideshow.Models.DigiKam;
public record ImageTagProperties([property: JsonPropertyName("imageid")] int ImageId,
[property: JsonPropertyName("property")] string Property,
[property: JsonPropertyName("tagid")] int TagId,
[property: JsonPropertyName("value")] string Value);

View File

@ -0,0 +1,21 @@
using System.Collections.ObjectModel;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace ImmichToSlideshow.Models.DigiKam;
public record ImageTags([property: JsonPropertyName("imageid")] int ImageId,
[property: JsonPropertyName("tagid")] int TagId) {
internal static ReadOnlyCollection<ImageTags>? GetImageTags(string? path) {
ImageTags[]? results;
if (string.IsNullOrEmpty(path) || !File.Exists(path)) {
results = null;
} else {
string json = File.ReadAllText(path);
results = JsonSerializer.Deserialize<ImageTags[]>(json);
}
return results?.AsReadOnly();
}
}

View File

@ -0,0 +1,28 @@
using System.Collections.ObjectModel;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace ImmichToSlideshow.Models.DigiKam;
public record Images([property: JsonPropertyName("album")] int? Album,
[property: JsonPropertyName("category")] int Category,
[property: JsonPropertyName("fileSize")] object FileSize,
[property: JsonPropertyName("id")] int Id,
[property: JsonPropertyName("manualOrder")] object ManualOrder,
[property: JsonPropertyName("modificationDate")] DateTime ModificationDate,
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("status")] int Status,
[property: JsonPropertyName("uniqueHash")] string UniqueHash) {
internal static ReadOnlyCollection<Images>? GetImages(string? path) {
Images[]? results;
if (string.IsNullOrEmpty(path) || !File.Exists(path)) {
results = null;
} else {
string json = File.ReadAllText(path);
results = JsonSerializer.Deserialize<Images[]>(json);
}
return results?.AsReadOnly();
}
}

View File

@ -0,0 +1,7 @@
using System.Text.Json.Serialization;
namespace ImmichToSlideshow.Models.DigiKam;
public record TagProperties([property: JsonPropertyName("property")] string Property,
[property: JsonPropertyName("tagid")] int TagId,
[property: JsonPropertyName("value")] object Value);

View File

@ -0,0 +1,24 @@
using System.Collections.ObjectModel;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace ImmichToSlideshow.Models.DigiKam;
public record Tags([property: JsonPropertyName("icon")] object Icon,
[property: JsonPropertyName("iconkde")] object IconKDE,
[property: JsonPropertyName("id")] int Id,
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("pid")] int PId) {
internal static ReadOnlyCollection<Tags>? GetTags(string? path) {
Tags[]? results;
if (string.IsNullOrEmpty(path) || !File.Exists(path)) {
results = null;
} else {
string json = File.ReadAllText(path);
results = JsonSerializer.Deserialize<Tags[]>(json);
}
return results?.AsReadOnly();
}
}

View File

@ -0,0 +1,22 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace ImmichToSlideshow.Models;
public record DigiKam4(string Images,
string ImageTagProperties,
string ImageTags,
string TagProperties,
string Tags) {
public override string ToString() {
string result = JsonSerializer.Serialize(this, DigiKam4SourceGenerationContext.Default.DigiKam4);
return result;
}
}
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(DigiKam4))]
public partial class DigiKam4SourceGenerationContext : JsonSerializerContext {
}

View File

@ -0,0 +1,40 @@
using ImmichToSlideshow.Domain;
using System.Collections.ObjectModel;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace ImmichToSlideshow.Models;
public sealed record Identifier(string[] DirectoryNames,
string Extension,
bool? HasDateTimeOriginal,
int Id,
long Length,
string PaddedId,
long Ticks) {
public override string ToString() {
string result = JsonSerializer.Serialize(this, IdentifierSourceGenerationContext.Default.Identifier);
return result;
}
internal static string GetDeviceAssetIds(ReadOnlyCollection<Identifier> identifiers) =>
$"'{string.Join($"',{Environment.NewLine}'", (from l in identifiers select GetDeviceAssetId(l)).ToArray())}'";
internal static string GetDeviceAssetIds(ReadOnlyCollection<ImageTag> imageTags) =>
$"'{string.Join($"',{Environment.NewLine}'", (from l in imageTags select l.Name).ToArray())}'";
internal static string GetDeviceAssetId(Identifier identifier) =>
$"{identifier.PaddedId}{identifier.Extension}";
}
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(Identifier))]
public partial class IdentifierSourceGenerationContext : JsonSerializerContext {
}
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(Identifier[]))]
public partial class IdentifierCollectionSourceGenerationContext : JsonSerializerContext {
}

View File

@ -5,58 +5,47 @@ namespace ImmichToSlideshow.Models.Immich;
public record Asset([property: JsonPropertyName("id")] string Id, public record Asset([property: JsonPropertyName("id")] string Id,
[property: JsonPropertyName("deviceAssetId")] string DeviceAssetId, [property: JsonPropertyName("deviceAssetId")] string DeviceAssetId,
[property: JsonPropertyName("ownerId")] string OwnerId, // [property: JsonPropertyName("ownerId")] string OwnerId,
[property: JsonPropertyName("deviceId")] string DeviceId, // [property: JsonPropertyName("deviceId")] string DeviceId,
[property: JsonPropertyName("type")] string Type, // [property: JsonPropertyName("type")] string Type,
[property: JsonPropertyName("originalPath")] string OriginalPath, [property: JsonPropertyName("originalPath")] string OriginalPath,
[property: JsonPropertyName("fileCreatedAt")] DateTime FileCreatedAt, // [property: JsonPropertyName("fileCreatedAt")] DateTime FileCreatedAt,
[property: JsonPropertyName("fileModifiedAt")] DateTime FileModifiedAt, // [property: JsonPropertyName("fileModifiedAt")] DateTime FileModifiedAt,
[property: JsonPropertyName("isFavorite")] bool IsFavorite, // [property: JsonPropertyName("isFavorite")] bool IsFavorite,
[property: JsonPropertyName("duration")] string Duration, // [property: JsonPropertyName("duration")] string? Duration,
[property: JsonPropertyName("encodedVideoPath")] string EncodedVideoPath, // [property: JsonPropertyName("encodedVideoPath")] string EncodedVideoPath,
[property: JsonPropertyName("checksum")] string Checksum, // [property: JsonPropertyName("checksum")] string Checksum,
[property: JsonPropertyName("isVisible")] bool IsVisible, // [property: JsonPropertyName("isVisible")] bool IsVisible,
[property: JsonPropertyName("livePhotoVideoId")] object LivePhotoVideoId, // [property: JsonPropertyName("livePhotoVideoId")] object? LivePhotoVideoId,
[property: JsonPropertyName("updatedAt")] DateTime UpdatedAt, // [property: JsonPropertyName("updatedAt")] DateTime UpdatedAt,
[property: JsonPropertyName("createdAt")] DateTime CreatedAt, // [property: JsonPropertyName("createdAt")] DateTime CreatedAt,
[property: JsonPropertyName("isArchived")] bool IsArchived, // [property: JsonPropertyName("isArchived")] bool IsArchived,
[property: JsonPropertyName("originalFileName")] string OriginalFileName, [property: JsonPropertyName("originalFileName")] string OriginalFileName,
[property: JsonPropertyName("sidecarPath")] object SidecarPath, // [property: JsonPropertyName("sidecarPath")] object? SidecarPath,
[property: JsonPropertyName("thumbhash")] string Thumbhash, // [property: JsonPropertyName("thumbhash")] string Thumbhash,
[property: JsonPropertyName("isOffline")] bool IsOffline, // [property: JsonPropertyName("isOffline")] bool IsOffline,
[property: JsonPropertyName("libraryId")] string LibraryId, // [property: JsonPropertyName("libraryId")] string LibraryId,
[property: JsonPropertyName("isExternal")] bool IsExternal, // [property: JsonPropertyName("isExternal")] bool IsExternal,
[property: JsonPropertyName("deletedAt")] object DeletedAt, // [property: JsonPropertyName("deletedAt")] object? DeletedAt,
[property: JsonPropertyName("localDateTime")] DateTime LocalDateTime, // [property: JsonPropertyName("localDateTime")] DateTime LocalDateTime,
[property: JsonPropertyName("stackId")] object StackId, // [property: JsonPropertyName("stackId")]? object? StackId,
[property: JsonPropertyName("duplicateId")] string DuplicateId, [property: JsonPropertyName("duplicateId")] string? DuplicateId,
[property: JsonPropertyName("status")] string Status, // [property: JsonPropertyName("status")] string Status,
[property: JsonPropertyName("path")] string Path) [property: JsonPropertyName("path")] string Path) {
{
public override string ToString() public override string ToString() {
{
string result = JsonSerializer.Serialize(this, AssetSourceGenerationContext.Default.Asset); string result = JsonSerializer.Serialize(this, AssetSourceGenerationContext.Default.Asset);
return result; return result;
} }
public static Asset Get(string id,
string deviceAssetId,
string ownerId,
string originalFileName,
string path) =>
throw new Exception();
} }
[JsonSourceGenerationOptions(WriteIndented = true)] [JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(Asset))] [JsonSerializable(typeof(Asset))]
internal partial class AssetSourceGenerationContext : JsonSerializerContext internal partial class AssetSourceGenerationContext : JsonSerializerContext {
{
} }
[JsonSourceGenerationOptions(WriteIndented = true)] [JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(Asset[]))] [JsonSerializable(typeof(Asset[]))]
internal partial class AssetCollectionSourceGenerationContext : JsonSerializerContext internal partial class AssetCollectionSourceGenerationContext : JsonSerializerContext {
{
} }

View File

@ -0,0 +1,30 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace ImmichToSlideshow.Models.Immich;
public record Tag([property: JsonPropertyName("id")] string Id,
[property: JsonPropertyName("userId")] string UserId,
[property: JsonPropertyName("value")] string OwnerId,
// [property: JsonPropertyName("createdAt")] DateTime CreatedAt,
// [property: JsonPropertyName("updatedAt")] DateTime UpdatedAt,
// [property: JsonPropertyName("color")] char Color,
// [property: JsonPropertyName("parentId")] string ParentId,
[property: JsonPropertyName("updateId")] string UpdateId) {
public override string ToString() {
string result = JsonSerializer.Serialize(this, TagSourceGenerationContext.Default.Tag);
return result;
}
}
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(Tag))]
internal partial class TagSourceGenerationContext : JsonSerializerContext {
}
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(Tag[]))]
internal partial class TagCollectionSourceGenerationContext : JsonSerializerContext {
}

View File

@ -0,0 +1,29 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace ImmichToSlideshow.Models;
public record Settings(int AddDays,
string ArchivedTag,
string BirthdayFormat,
string ConnectionString,
DigiKam4? DigiKam4,
string[] FilterTags,
string ImmichUploadDirectory,
float LowestVersionHistory,
string NotNinePath,
Dictionary<string, string> People,
string RandomResultsDirectory,
string SyncDirectory) {
public override string ToString() {
string result = JsonSerializer.Serialize(this, SettingsSourceGenerationContext.Default.Settings);
return result;
}
}
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(Settings))]
public partial class SettingsSourceGenerationContext : JsonSerializerContext {
}

View File

@ -1,7 +1,6 @@
namespace ImmichToSlideshow.Persistence.Database; namespace ImmichToSlideshow.Persistence.Database;
public static class DbConstants public static class DbConstants {
{
public const string DefaultConnectionStringPath = "Database:ConnectionStrings:DefaultConnection"; public const string DefaultConnectionStringPath = "Database:ConnectionStrings:DefaultConnection";

View File

@ -2,21 +2,22 @@ using ImmichToSlideshow.DependencyInjection;
using ImmichToSlideshow.Models; using ImmichToSlideshow.Models;
using ImmichToSlideshow.RequestPipeline; using ImmichToSlideshow.RequestPipeline;
WebApplicationBuilder webApplicationBuilder = WebApplication.CreateBuilder(args); namespace ImmichToSlideshow;
{
// configure services (DI) public class Program {
_ = webApplicationBuilder.Services.AddServices();
_ = webApplicationBuilder.Services.AddControllers(); public static void Main(string[] args) {
_ = webApplicationBuilder.Configuration.AddUserSecrets<Program>(); WebApplicationBuilder webApplicationBuilder = WebApplication.CreateBuilder(args);
AppSettings appSettings = ImmichToSlideshow.Models.Binder.AppSettings.Get(webApplicationBuilder.Configuration); _ = webApplicationBuilder.Configuration.AddUserSecrets<Program>();
_ = webApplicationBuilder.Services.AddSingleton(_ => appSettings); AppSettings appSettings = AppSettings.Get(webApplicationBuilder.Configuration);
} _ = webApplicationBuilder.Services.AddServices(appSettings);
WebApplication webApplication = webApplicationBuilder.Build(); WebApplication webApplication = webApplicationBuilder.Build();
{ ILogger<Program>? logger = webApplication.Services.GetRequiredService<ILogger<Program>>();
// configure request pipeline logger.LogInformation("Starting Web Application");
_ = webApplication.MapControllers(); _ = webApplication.InitializeCorsAndHttps(appSettings);
_ = webApplication.InitializeDatabase(); _ = webApplication.MapControllers();
} _ = webApplication.InitializeDatabase();
ILogger<Program>? logger = webApplication.Services.GetRequiredService<ILogger<Program>>(); webApplication.Run();
logger.LogInformation("Starting Web Application"); }
webApplication.Run();
}

View File

@ -1,12 +1,21 @@
using ImmichToSlideshow.Models;
namespace ImmichToSlideshow.RequestPipeline; namespace ImmichToSlideshow.RequestPipeline;
public static class WebApplicationExtensions public static class WebApplicationExtensions {
{
public static WebApplication InitializeDatabase(this WebApplication application) public static WebApplication InitializeDatabase(this WebApplication webApplication) =>
{
// DBInitializer.Initialize(application.Configuration[DbConstants.DefaultConnectionStringPath]!); // DBInitializer.Initialize(application.Configuration[DbConstants.DefaultConnectionStringPath]!);
return application; webApplication;
public static WebApplication InitializeCorsAndHttps(this WebApplication webApplication, AppSettings appSettings) {
_ = webApplication.UseCors(corsPolicyBuilder =>
corsPolicyBuilder.WithOrigins(appSettings.WithOrigins).AllowAnyHeader().AllowAnyMethod());
if (appSettings.URLs.Contains("https", StringComparison.InvariantCultureIgnoreCase)) {
_ = webApplication.UseHttpsRedirection();
_ = webApplication.UseHsts();
}
return webApplication;
} }
} }

View File

@ -1,5 +1,6 @@
using ImmichToSlideshow.Models.Immich; using ImmichToSlideshow.Domain;
using ImmichToSlideshow.Models; using ImmichToSlideshow.Models;
using ImmichToSlideshow.Models.Immich;
using Npgsql; using Npgsql;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Data; using System.Data;
@ -8,116 +9,275 @@ using System.Text.Json;
namespace ImmichToSlideshow.Services; namespace ImmichToSlideshow.Services;
public class AssetService public class AssetService(ILogger<Program> logger, AppSettings appSettings) {
{
private readonly AppSettings _AppSettings; #pragma warning disable CS9124
private readonly ILogger<Program> _Logger = logger;
private readonly Settings _Settings = appSettings.Settings;
#pragma warning restore CS9124
public AssetService(AppSettings appSettings) => private static int? ExecuteNonQuery(string connectionString, string commandText, ReadOnlyCollection<NpgsqlParameter> npgsqlParameters) {
_AppSettings = appSettings;
private static readonly List<Asset> _AssetsRepository = [];
private static string GetCommandText()
{ // cSpell:disable
List<string> results = new();
// results.Add(" SELECT COALESCE(SUM(checksum_failures), 0) ");
// results.Add(" FROM pg_stat_database ");
// results.Add(" SELECT json_agg(t) ");
// results.Add(" FROM information_schema.tables t ");
// results.Add(" WHERE table_schema='public' ");
// results.Add(" AND table_type='BASE TABLE' ");
// results.Add(" SELECT json_agg(c) ");
// results.Add(" FROM information_schema.columns c ");
// results.Add(" WHERE table_name ='assets' ");
// results.Add(" WHERE table_name ='libraries' ");
// results.Add(" WHERE table_name ='asset_files' ");
results.Add(" SELECT json_agg(j) ");
results.Add(" FROM ( ");
results.Add(" SELECT a.* ");
results.Add(" , f.\"path\" ");
results.Add(" FROM assets a ");
// results.Add(" FROM asset_files f ");
results.Add(" INNER ");
results.Add(" JOIN asset_files f ");
results.Add(" ON a.\"id\" = f.\"assetId\" ");
results.Add(" AND f.\"type\" = 'preview' ");
results.Add(" WHERE a.\"status\" = 'active' ");
// results.Add(" WHERE f.\"assetId\" = '4c1933ce-f5b3-4348-bcc3-978f99823d70' ");
results.Add(" AND a.\"isExternal\" = true ");
results.Add(" AND a.\"isOffline\" = false ");
results.Add(" AND a.\"isVisible\" = true ");
// results.Add(" AND a.\"id\" = '4c1933ce-f5b3-4348-bcc3-978f99823d70' ");
// results.Add(" AND a.\"originalFileName\"");
// results.Add(" LIKE '%still%' ");
// results.Add(" AND a.\"originalFileName\" = '979270910999.jpg' ");
results.Add(" ) j ");
return string.Join(Environment.NewLine, results);
} // cSpell:enable
private static int? ExecuteNonQuery(string connectionString, string commandText)
{
int? result; int? result;
if (string.IsNullOrEmpty(connectionString)) if (string.IsNullOrEmpty(connectionString)) {
result = null; result = null;
else } else {
{
using NpgsqlConnection npgsqlConnection = new(connectionString); using NpgsqlConnection npgsqlConnection = new(connectionString);
npgsqlConnection.Open(); npgsqlConnection.Open();
using NpgsqlCommand npgsqlCommand = new(commandText, npgsqlConnection); using NpgsqlCommand npgsqlCommand = new(commandText, npgsqlConnection);
npgsqlCommand.Parameters.AddRange(npgsqlParameters.ToArray());
result = npgsqlCommand.ExecuteNonQuery(); result = npgsqlCommand.ExecuteNonQuery();
} }
return result; return result;
} }
private static StringBuilder GetForJsonPath(string connectionString, string commandText) private static StringBuilder GetForJsonPath(string connectionString, string commandText, ReadOnlyCollection<NpgsqlParameter> npgsqlParameters) {
{ StringBuilder result = new();
StringBuilder stringBuilder = new();
using NpgsqlConnection npgsqlConnection = new(connectionString); using NpgsqlConnection npgsqlConnection = new(connectionString);
npgsqlConnection.Open(); npgsqlConnection.Open();
using NpgsqlCommand npgsqlCommand = new(commandText, npgsqlConnection); using NpgsqlCommand npgsqlCommand = new(commandText, npgsqlConnection);
npgsqlCommand.Parameters.AddRange(npgsqlParameters.ToArray());
NpgsqlDataReader npgsqlDataReader = npgsqlCommand.ExecuteReader(CommandBehavior.SequentialAccess); NpgsqlDataReader npgsqlDataReader = npgsqlCommand.ExecuteReader(CommandBehavior.SequentialAccess);
while (npgsqlDataReader.Read()) while (npgsqlDataReader.Read()) {
_ = stringBuilder.Append(npgsqlDataReader.GetString(0)); _ = result.Append(npgsqlDataReader.GetString(0));
return stringBuilder;
}
public ReadOnlyCollection<Asset>? Get()
{
string commandText = GetCommandText();
if (commandText.Length == 1)
{
int? result = ExecuteNonQuery(_AppSettings.ConnectionString, commandText);
if (result is null)
{ }
} }
StringBuilder stringBuilder = GetForJsonPath(_AppSettings.ConnectionString, commandText); return result;
if (commandText.Length == 1) }
File.WriteAllText(".vscode/jsonl/.jsonl", stringBuilder.ToString());
string json = stringBuilder.ToString(); public string? GetColumns() {
string result;
NpgsqlParameter[] npgsqlParameters = [];
string commandText = CommandText.GetColumns();
StringBuilder stringBuilder = GetForJsonPath(_Settings.ConnectionString, commandText, npgsqlParameters.AsReadOnly());
result = stringBuilder.ToString();
if (result.Length == 1) {
File.WriteAllText(".vscode/jsonl/.jsonl", result);
}
return result;
}
public string? GetOwnerIds() {
string result;
NpgsqlParameter[] npgsqlParameters = [];
string commandText = CommandText.GetOwnerIdActiveImage(_Settings.LowestVersionHistory);
StringBuilder stringBuilder = GetForJsonPath(_Settings.ConnectionString, commandText, npgsqlParameters.AsReadOnly());
result = stringBuilder.ToString();
if (result.Length == 1) {
File.WriteAllText(".vscode/jsonl/.jsonl", result);
}
return result;
}
public string? GetAssets(Guid ownerId) {
string result;
ReadOnlyCollection<string> people = Array.Empty<string>().AsReadOnly();
NpgsqlParameter[] npgsqlParameters = [new NpgsqlParameter(nameof(ownerId), ownerId)];
string commandText = CommandText.GetAssetActiveImagePreviewNotDuplicate(_Settings.LowestVersionHistory, _Settings.FilterTags.AsReadOnly(), people);
StringBuilder stringBuilder = GetForJsonPath(_Settings.ConnectionString, commandText, npgsqlParameters.AsReadOnly());
result = stringBuilder.ToString();
if (result.Length == 1) {
File.WriteAllText(".vscode/jsonl/assets.jsonl", result);
}
return result;
}
public ReadOnlyCollection<string>? GetRandomPaths(Guid ownerId, string? monthDay) {
string[]? results;
string commandText;
Random random = new();
List<string> people = [];
StringBuilder stringBuilder;
if (!string.IsNullOrEmpty(monthDay)) {
foreach (KeyValuePair<string, string> keyValuePair in _Settings.People) {
if (!keyValuePair.Key.Contains(monthDay)) {
continue;
}
people.Add($"People/{keyValuePair.Value.Trim('/')}");
}
}
try {
NpgsqlParameter[] npgsqlParameters = [new NpgsqlParameter(nameof(ownerId), ownerId)];
commandText = CommandText.GetAssetActiveImagePreviewNotDuplicate(_Settings.LowestVersionHistory, _Settings.FilterTags.AsReadOnly(), people.AsReadOnly());
stringBuilder = GetForJsonPath(_Settings.ConnectionString, commandText, npgsqlParameters.AsReadOnly());
} catch (Exception) {
people.Clear();
NpgsqlParameter[] npgsqlParameters = [new NpgsqlParameter(nameof(ownerId), ownerId)];
commandText = CommandText.GetAssetActiveImagePreviewNotDuplicate(_Settings.LowestVersionHistory, _Settings.FilterTags.AsReadOnly(), people.AsReadOnly());
stringBuilder = GetForJsonPath(_Settings.ConnectionString, commandText, npgsqlParameters.AsReadOnly());
}
string json = stringBuilder.ToString();
string ownerIdValue = ownerId.ToString();
Asset[]? assets = JsonSerializer.Deserialize(json, AssetCollectionSourceGenerationContext.Default.AssetArray); Asset[]? assets = JsonSerializer.Deserialize(json, AssetCollectionSourceGenerationContext.Default.AssetArray);
return assets?.AsReadOnly(); results = assets is null ? null : (from l in assets orderby random.NextSingle() select l.Path.Split(ownerIdValue)[1]).ToArray();
return results?.AsReadOnly();
} }
// 1. fetch user public Tag[]? GetArchivedTag(Settings settings, Guid ownerId) {
// 1. fetch asset Tag[]? results;
// 1. check wether the user reached the Guid userId = ownerId;
// 1. update the user string value = settings.ArchivedTag;
// 1. save the asset NpgsqlParameter[] npgsqlParameters = [
public void Create(Asset asset) new NpgsqlParameter(nameof(value), value),
{ new NpgsqlParameter(nameof(userId), userId),
// Guid userId, ];
if (asset is null) string commandText = CommandText.GetArchivedTag(_Settings.LowestVersionHistory);
throw new ArgumentNullException(nameof(asset)); StringBuilder stringBuilder = GetForJsonPath(settings.ConnectionString, commandText, npgsqlParameters.AsReadOnly());
string json = stringBuilder.ToString();
// User user = _UsersRepository.Find(x => x.Id == userId) results = JsonSerializer.Deserialize(json, TagCollectionSourceGenerationContext.Default.TagArray);
// ?? throw new InvalidOperationException(); return results;
// user.AddAsset(asset);
_AssetsRepository.Add(asset);
} }
public Asset? Get(Guid assetId) => public string? GetArchivedTag(Guid ownerId) {
_AssetsRepository.Find(l => l.Id == assetId.ToString()); string? result;
Tag[]? tags = GetArchivedTag(_Settings, ownerId);
result = tags is null || tags.Length != 1 ? null : tags[0].Id;
return result;
}
public string? SaveRandomPaths(Guid ownerId, string? monthDay) {
string? results = null;
FileInfo fileInfo;
DateTime dateTime = DateTime.Now;
ReadOnlyCollection<string>? paths;
bool? check = monthDay is null ? null : monthDay == "00-00";
if (Directory.Exists(_Settings.RandomResultsDirectory)) {
_ = Directory.CreateDirectory(_Settings.RandomResultsDirectory);
}
for (int i = 0; i < 366; i++) {
if (check is null || check.Value) {
monthDay = i == 0 && check is not null && check.Value ? "02-29" : dateTime.AddDays(i).ToString("MM-dd");
}
fileInfo = new(Path.Combine(_Settings.RandomResultsDirectory, $"{monthDay}.json"));
if (fileInfo.Exists && fileInfo.CreationTime > dateTime.AddDays(_Settings.AddDays)) {
results = null;
} else {
_Logger.LogDebug("Writing <{FullName}>", fileInfo.FullName);
paths = GetRandomPaths(ownerId, monthDay);
if (paths is null) {
results = null;
} else {
_Logger.LogInformation("{count} path(s)", paths.Count.ToString());
results = JsonSerializer.Serialize(paths);
File.WriteAllText(fileInfo.FullName, results);
}
}
if (check is null || !check.Value) {
break;
}
}
return results;
}
private record Record(string Source, string Destination);
public ReadOnlyCollection<string> SyncImmich(Guid ownerId) {
List<string> results = [];
Record record;
List<Record> records = [];
if (Directory.Exists(_Settings.SyncDirectory)) {
_ = Directory.CreateDirectory(_Settings.SyncDirectory);
}
int syncLength = _Settings.SyncDirectory.Length;
string[] syncFiles = Directory.GetFiles(_Settings.SyncDirectory, "*", SearchOption.AllDirectories);
string[] syncCheck = syncFiles.Select(l => l[syncLength..]).ToArray();
if (Directory.Exists(_Settings.ImmichUploadDirectory)) {
_ = Directory.CreateDirectory(_Settings.ImmichUploadDirectory);
}
int immichUploadLength = _Settings.ImmichUploadDirectory.Length;
string[] immichUploadFiles = Directory.GetFiles(_Settings.ImmichUploadDirectory, "*", SearchOption.AllDirectories);
string[] immichUploadCheck = immichUploadFiles.Select(l => l[immichUploadLength..]).ToArray();
for (int i = 0; i < immichUploadFiles.Length; i++) {
if (syncCheck.Contains(immichUploadCheck[i])) {
continue;
}
results.Add(immichUploadCheck[i]);
record = new(immichUploadFiles[i], string.Concat(_Settings.SyncDirectory, immichUploadCheck[i]));
records.Add(record);
}
_Logger.LogInformation("{count} file(s)", results.Count.ToString());
for (int i = 0; i < records.Count; i++) {
record = records[i];
_Logger.LogDebug("Copying <{source}>", record.Source);
File.Copy(record.Source, record.Destination);
}
return results.AsReadOnly();
}
public ReadOnlyCollection<int>? SetArchiveImmich(Guid ownerId) {
ReadOnlyCollection<int>? results;
if (!File.Exists(_Settings.NotNinePath)) {
results = null;
} else {
string json = File.ReadAllText(_Settings.NotNinePath);
Identifier[]? identifiers = JsonSerializer.Deserialize<Identifier[]>(json);
if (identifiers is null || identifiers.Length == 0) {
results = null;
} else {
Tag[]? tags = GetArchivedTag(_Settings, ownerId);
if (tags is null || tags.Length != 1) {
results = null;
} else {
results = SetArchiveImmich(logger, _Settings, ownerId, identifiers.AsReadOnly(), tags.AsReadOnly());
}
}
}
return results?.AsReadOnly();
}
private static ReadOnlyCollection<int>? SetArchiveImmich(ILogger<Program> logger, Settings settings, Guid ownerId, string tagsId, string deviceAssetIds) {
ReadOnlyCollection<int>? results;
NpgsqlParameter[] npgsqlParameters = [
new NpgsqlParameter(nameof(tagsId), tagsId),
new NpgsqlParameter(nameof(ownerId), ownerId),
];
string commandText = CommandText.SetAssetArchived(settings.LowestVersionHistory, deviceAssetIds);
logger.LogDebug(commandText.Replace($"@{nameof(tagsId)}", $"'{tagsId}'").Replace($"@{nameof(ownerId)}", $"'{ownerId}'".ToString()));
int? result = ExecuteNonQuery(settings.ConnectionString, commandText, npgsqlParameters.AsReadOnly());
results = result is null ? null : new([result.Value]);
return results?.AsReadOnly();
}
private static ReadOnlyCollection<int>? SetArchiveImmich(ILogger<Program> logger, Settings settings, Guid ownerId, ReadOnlyCollection<Identifier> identifiers, ReadOnlyCollection<Tag> tags) {
ReadOnlyCollection<int>? results;
string tagsId = tags[0].Id;
string deviceAssetIds = Identifier.GetDeviceAssetIds(identifiers);
results = SetArchiveImmich(logger, settings, ownerId, tagsId, deviceAssetIds);
return results?.AsReadOnly();
}
public ReadOnlyCollection<int>? SetDigiKam4ArchiveImmich(Guid ownerId) {
ReadOnlyCollection<int>? results;
ReadOnlyCollection<ImageTag>? imageTags = ImageTag.Get(tag: _Settings.ArchivedTag,
tagsPath: _Settings.DigiKam4?.Tags,
imageTagsPath: _Settings.DigiKam4?.ImageTags,
imagesPath: _Settings.DigiKam4?.Images);
if (imageTags is null || imageTags.Count == 0) {
results = null;
} else {
Tag[]? tags = GetArchivedTag(_Settings, ownerId);
if (tags is null || tags.Length != 1) {
results = null;
} else {
results = SetArchiveImmich(logger, _Settings, ownerId, imageTags, tags.AsReadOnly());
}
}
return results?.AsReadOnly();
}
private static ReadOnlyCollection<int>? SetArchiveImmich(ILogger<Program> logger, Settings settings, Guid ownerId, ReadOnlyCollection<ImageTag> imageTags, ReadOnlyCollection<Tag> tags) {
ReadOnlyCollection<int>? results;
string tagsId = tags[0].Id;
string deviceAssetIds = Identifier.GetDeviceAssetIds(imageTags);
results = SetArchiveImmich(logger, settings, ownerId, tagsId, deviceAssetIds);
return results?.AsReadOnly();
}
public ReadOnlyCollection<int>? UpdateAssetsSetLocalDateTimeForThreeAndSeven() {
ReadOnlyCollection<int>? results;
NpgsqlParameter[] npgsqlParameters = [];
string commandText = CommandText.UpdateAssetsSetLocalDateTimeForThreeAndSeven(_Settings.LowestVersionHistory);
int? result = ExecuteNonQuery(_Settings.ConnectionString, commandText, npgsqlParameters.AsReadOnly());
results = result is null ? null : new([result.Value]);
return results?.AsReadOnly();
}
} }

View File

@ -0,0 +1,218 @@
using System.Collections.ObjectModel;
namespace ImmichToSlideshow.Services;
internal static class CommandText {
internal static string GetColumns() { // cSpell:disable
List<string> results = [
// " SELECT COALESCE(SUM(checksum_failures), 0) ",
// " FROM pg_stat_database ",
// " SELECT json_agg(t) ",
// " FROM information_schema.tables t ",
// " WHERE table_schema= 'public' ",
// " AND table_type= 'BASE TABLE' ",
" SELECT json_agg(c) ",
" FROM information_schema.columns c ",
// " WHERE table_name = 'assets' ",
// " WHERE table_name = 'libraries' ",
" WHERE table_name = 'asset_files'; "
];
return string.Join(Environment.NewLine, results);
} // cSpell:enable
internal static string GetOwnerIdActiveImage(float lowestVersionHistory) { // cSpell:disable
List<string> results = [
" SELECT json_agg(j) ",
" FROM ( ",
" SELECT a.\"ownerId\" "
];
if (lowestVersionHistory <= 1.129) {
results.Add(" FROM assets a ");
} else {
results.Add(" FROM public.asset a ");
}
results.AddRange([
" WHERE a.\"status\" = 'active' ",
" AND a.\"type\" = 'IMAGE' ",
" GROUP",
" BY a.\"ownerId\" ",
" ) j; "
]);
return string.Join(Environment.NewLine, results);
} // cSpell:enable
internal static string GetAssetActiveImagePreviewNotDuplicate(float lowestVersionHistory, ReadOnlyCollection<string> filterTags, ReadOnlyCollection<string> people) { // cSpell:disable
List<string> results = [
" SELECT json_agg(j) ",
" FROM ( ",
" SELECT a.\"id\" ",
" , a.\"deviceAssetId\" ",
// " , a.\"ownerId\" ",
// " , a.\"deviceId\" ",
// " , a.\"type\" ",
" , a.\"originalPath\" ",
// " , a.\"fileCreatedAt\" ",
// " , a.\"fileModifiedAt\" ",
// " , a.\"isFavorite\" ",
// " , a.\"duration\" ",
// " , a.\"encodedVideoPath\" ",
// " , a.\"checksum\" ",
// " , a.\"isVisible\" ",
// " , a.\"livePhotoVideoId\" ",
// " , a.\"updatedAt\" ",
// " , a.\"createdAt\" ",
// " , a.\"isArchived\" ",
" , a.\"originalFileName\" ",
// " , a.\"sidecarPath\" ",
// " , a.\"thumbhash\" ",
// " , a.\"isOffline\" ",
// " , a.\"libraryId\" ",
// " , a.\"isExternal\" ",
// " , a.\"deletedAt\" ",
// " , a.\"localDateTime\" ",
// " , a.\"stackId\" ",
" , a.\"duplicateId\" ",
// " , a.\"status\" ",
" , f.\"path\" "
];
if (lowestVersionHistory <= 1.129) {
results.Add(" FROM assets a ");
} else {
results.Add(" FROM public.asset a ");
}
results.Add(" INNER ");
if (lowestVersionHistory <= 1.129) {
results.Add(" JOIN asset_files f ");
} else {
results.Add(" JOIN public.asset_file f ");
}
results.AddRange([
" ON a.\"id\" = f.\"assetId\" ",
" WHERE a.\"status\" = 'active' ",
" AND a.\"type\" = 'IMAGE' ",
" AND f.\"type\" = 'preview' ",
" AND a.\"duplicateId\" is null ",
" AND a.\"id\" not in ( ",
" SELECT \"assetsId\" "
]);
if (lowestVersionHistory <= 1.129) {
results.Add(" FROM tag_asset g ");
} else {
results.Add(" FROM public.tag_asset g ");
}
if (lowestVersionHistory <= 1.129) {
results.Add(" JOIN tags t ");
} else {
results.Add(" JOIN public.tag t ");
}
results.AddRange([
" ON g.\"tagsId\" = t.\"id\" ",
$" WHERE t.\"value\" in ('{string.Join("','", filterTags)}') ",
" ) "]);
if (people.Count > 0) {
results.AddRange([
" AND a.\"id\" in ( ",
" SELECT \"assetsId\" "
]);
if (lowestVersionHistory <= 1.129) {
results.Add(" FROM tag_asset g ");
} else {
results.Add(" FROM public.tag_asset g ");
}
if (lowestVersionHistory <= 1.129) {
results.Add(" JOIN tags t ");
} else {
results.Add(" JOIN public.tag t ");
}
results.AddRange([
" ON g.\"tagsId\" = t.\"id\" ",
$" WHERE t.\"value\" in ('{string.Join("','", people)}') ",
" ) "]);
}
results.AddRange([
" AND a.\"isExternal\" = true ",
" AND a.\"isOffline\" = false ",
" AND a.\"ownerId\" = @ownerId ",
" ) j; "
]);
return string.Join(Environment.NewLine, results);
} // cSpell:enable
internal static string SetAssetArchived(float lowestVersionHistory, string deviceAssetIds) { // cSpell:disable
List<string> results = [
" INSERT INTO tag_asset ",
" (\"assetsId\", \"tagsId\") ",
" SELECT a.\"id\", @tagsId::uuid "
];
if (lowestVersionHistory <= 1.129) {
results.Add(" FROM assets a ");
} else {
results.Add(" FROM public.asset a ");
}
results.AddRange([
" WHERE a.\"type\" = 'IMAGE' ",
" AND a.\"ownerId\" = @ownerId ",
" AND a.\"deviceAssetId\" in ( ",
deviceAssetIds,
" ) ",
" AND a.\"id\" not in ( ",
" SELECT \"id\" "
]);
if (lowestVersionHistory <= 1.129) {
results.Add(" FROM assets b ");
} else {
results.Add(" FROM public.asset b ");
}
results.Add(" INNER ");
if (lowestVersionHistory <= 1.129) {
results.Add(" JOIN tag_asset t ");
} else {
results.Add(" JOIN public.tag_asset t ");
}
results.AddRange([
" ON b.\"id\" = t.\"assetsId\" ",
" WHERE t.\"tagsId\" = @tagsId::uuid ",
" AND b.\"deviceAssetId\" in ( ",
deviceAssetIds,
" ) ",
" ); "
]);
return string.Join(Environment.NewLine, results);
} // cSpell:enable
internal static string GetArchivedTag(float lowestVersionHistory) { // cSpell:disable
List<string> results = [
" SELECT json_agg(t) "
];
if (lowestVersionHistory <= 1.129) {
results.Add(" FROM tags t ");
} else {
results.Add(" FROM public.tag t ");
}
results.AddRange([
" WHERE t.\"value\" = @value ",
" AND t.\"userId\" = @userId; "
]);
return string.Join(Environment.NewLine, results);
} // cSpell:enable
internal static string UpdateAssetsSetLocalDateTimeForThreeAndSeven(float lowestVersionHistory) { // cSpell:disable
List<string> results = [];
if (lowestVersionHistory <= 1.129) {
results.Add(" UPDATE assets ");
} else {
results.Add(" UPDATE public.asset ");
}
results.AddRange([
" SET \"localDateTime\" = TIMESTAMPTZ(\"localDateTime\") - INTERVAL '100 years' ",
" WHERE \"localDateTime\" > TIMESTAMPTZ '1990-01-01 00:00:00-00' ",
" AND ( ",
" \"deviceAssetId\" like '%3.%' ",
" OR \"deviceAssetId\" like '%7.%' ",
" ); ",
]);
return string.Join(Environment.NewLine, results);
} // cSpell:enable
}

View File

@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -1,9 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}