Insert into asset tag instead of updating is archived field

Convert to Podman
This commit is contained in:
2025-06-15 11:44:56 -07:00
parent ac4e4c277d
commit abee18494f
8 changed files with 305 additions and 133 deletions

89
.vscode/tasks.json vendored
View File

@ -88,6 +88,84 @@
],
"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",
"a3de856b5731",
"gitea.phares.duckdns.org:443/phares3757/immich-to-slideshow_webapp:latest"
],
"problemMatcher": "$msCompile"
},
{
"label": "podman-push",
"command": "podman",
"type": "process",
"args": [
"push",
"gitea.phares.duckdns.org:443/phares3757/immich-to-slideshow_webapp:latest"
],
"problemMatcher": "$msCompile"
},
{
"label": "Publish AOT",
"command": "dotnet",
@ -104,17 +182,6 @@
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "docker compose up --build",
"command": "docker",
"type": "process",
"args": [
"compose",
"up",
"--build"
],
"problemMatcher": "$msCompile"
}
]
}

View File

@ -22,9 +22,4 @@ ENV ASPNETCORE_HTTP_PORTS=5001
EXPOSE 5001
WORKDIR /app
COPY --from=publish /app/publish .
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
ENTRYPOINT [ "dotnet", "ImmichToSlideshow.dll" ]

View File

@ -27,6 +27,10 @@ public class AssetsController(AssetService assetService) : ControllerBase
public IActionResult GetRandomPaths(Guid ownerId) =>
Ok(_AssetService.GetRandomPaths(ownerId));
[HttpGet("archived-tag")]
public IActionResult GetArchivedTag() =>
Content(_AssetService.GetArchivedTag(), _ContentType);
[HttpGet("{ownerId:guid}/save-random-paths")]
public IActionResult SaveRandomPaths(Guid ownerId) =>
Content(_AssetService.SaveRandomPaths(ownerId), _ContentType);

View File

@ -4,10 +4,12 @@ using System.Text.Json.Serialization;
namespace ImmichToSlideshow.Models;
public record AppSettings(int AddDays,
string ArchivedTag,
string Company,
string ConnectionString,
string CurrentCommit,
string CurrentResultsDirectory,
string[] FilterTags,
string ImmichUploadDirectory,
string RandomResultsDirectory,
string SyncDirectory,

View File

@ -7,10 +7,12 @@ public class AppSettings
{
public int? AddDays { get; set; }
public string? ArchivedTag { get; set; }
public string? Company { get; set; }
public string? ConnectionString { get; set; }
public string? CurrentCommit { get; set; }
public string? CurrentResultsDirectory { get; set; }
public string[]? FilterTags { get; set; }
public string? ImmichUploadDirectory { get; set; }
public string? RandomResultsDirectory { get; set; }
public string? SyncDirectory { get; set; }
@ -45,10 +47,12 @@ public class AppSettings
{
Models.AppSettings result;
if (appSettings?.AddDays is null) throw new NullReferenceException(nameof(appSettings.AddDays));
if (appSettings?.ArchivedTag is null) throw new NullReferenceException(nameof(appSettings.ArchivedTag));
if (appSettings?.Company is null) throw new NullReferenceException(nameof(appSettings.Company));
if (appSettings?.ConnectionString is null) throw new NullReferenceException(nameof(appSettings.ConnectionString));
if (appSettings?.CurrentCommit is null) throw new NullReferenceException(nameof(appSettings.CurrentCommit));
if (appSettings?.CurrentResultsDirectory is null) throw new NullReferenceException(nameof(appSettings.CurrentResultsDirectory));
if (appSettings?.FilterTags is null) throw new NullReferenceException(nameof(appSettings.FilterTags));
if (appSettings?.ImmichUploadDirectory is null) throw new NullReferenceException(nameof(appSettings.ImmichUploadDirectory));
if (appSettings?.RandomResultsDirectory is null) throw new NullReferenceException(nameof(appSettings.RandomResultsDirectory));
if (appSettings?.SyncDirectory is null) throw new NullReferenceException(nameof(appSettings.SyncDirectory));
@ -56,10 +60,12 @@ public class AppSettings
if (appSettings?.WithOrigins is null) throw new NullReferenceException(nameof(appSettings.WithOrigins));
if (appSettings?.WorkingDirectoryName is null) throw new NullReferenceException(nameof(appSettings.WorkingDirectoryName));
result = new(appSettings.AddDays.Value,
appSettings.ArchivedTag,
appSettings.Company,
appSettings.ConnectionString,
appSettings.CurrentCommit,
appSettings.CurrentResultsDirectory,
appSettings.FilterTags,
appSettings.ImmichUploadDirectory,
appSettings.RandomResultsDirectory,
appSettings.SyncDirectory,

View File

@ -0,0 +1,34 @@
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

@ -15,104 +15,7 @@ public class AssetService(ILogger<Program> logger, AppSettings appSettings)
private readonly ILogger<Program> _Logger = logger;
private readonly AppSettings _AppSettings = appSettings;
private static string GetColumnsCommandText()
{ // 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' ");
return string.Join(Environment.NewLine, results);
} // cSpell:enable
private static string GetOwnerIdActiveImageCommandText()
{ // cSpell:disable
List<string> results = new();
results.Add(" SELECT json_agg(j) ");
results.Add(" FROM ( ");
results.Add(" SELECT a.\"ownerId\" ");
results.Add(" FROM assets a ");
results.Add(" WHERE a.\"status\" = 'active' ");
results.Add(" AND a.\"type\" = 'IMAGE' ");
results.Add(" GROUP");
results.Add(" BY a.\"ownerId\" ");
results.Add(" ) j ");
return string.Join(Environment.NewLine, results);
} // cSpell:enable
private static string GetAssetActiveImagePreviewNotDuplicateCommandText()
{ // cSpell:disable
List<string> results = new();
results.Add(" SELECT json_agg(j) ");
results.Add(" FROM ( ");
results.Add(" SELECT a.\"id\" ");
results.Add(" , a.\"deviceAssetId\" ");
// results.Add(" , a.\"ownerId\" ");
// results.Add(" , a.\"deviceId\" ");
// results.Add(" , a.\"type\" ");
results.Add(" , a.\"originalPath\" ");
// results.Add(" , a.\"fileCreatedAt\" ");
// results.Add(" , a.\"fileModifiedAt\" ");
// results.Add(" , a.\"isFavorite\" ");
// results.Add(" , a.\"duration\" ");
// results.Add(" , a.\"encodedVideoPath\" ");
// results.Add(" , a.\"checksum\" ");
// results.Add(" , a.\"isVisible\" ");
// results.Add(" , a.\"livePhotoVideoId\" ");
// results.Add(" , a.\"updatedAt\" ");
// results.Add(" , a.\"createdAt\" ");
// results.Add(" , a.\"isArchived\" ");
results.Add(" , a.\"originalFileName\" ");
// results.Add(" , a.\"sidecarPath\" ");
// results.Add(" , a.\"thumbhash\" ");
// results.Add(" , a.\"isOffline\" ");
// results.Add(" , a.\"libraryId\" ");
// results.Add(" , a.\"isExternal\" ");
// results.Add(" , a.\"deletedAt\" ");
// results.Add(" , a.\"localDateTime\" ");
// results.Add(" , a.\"stackId\" ");
results.Add(" , a.\"duplicateId\" ");
// results.Add(" , a.\"status\" ");
results.Add(" , f.\"path\" ");
results.Add(" FROM assets a ");
results.Add(" INNER ");
results.Add(" JOIN asset_files f ");
results.Add(" ON a.\"id\" = f.\"assetId\" ");
results.Add(" WHERE a.\"status\" = 'active' ");
results.Add(" AND a.\"type\" = 'IMAGE' ");
results.Add(" AND f.\"type\" = 'preview' ");
results.Add(" AND a.\"duplicateId\" is null ");
results.Add(" AND a.\"isArchived\" = false ");
results.Add(" AND a.\"isExternal\" = true ");
results.Add(" AND a.\"isOffline\" = false ");
results.Add(" AND a.\"isVisible\" = true ");
results.Add(" AND a.\"ownerId\" = @ownerId ");
results.Add(" ) j ");
return string.Join(Environment.NewLine, results);
} // cSpell:enable
private static string SetAssetArchivedCommandText(string deviceAssetIds)
{ // cSpell:disable
List<string> results = new();
results.Add(" UPDATE assets ");
results.Add(" SET \"isArchived\" = true ");
results.Add(" WHERE \"isArchived\" = false ");
results.Add(" AND \"type\" = 'IMAGE' ");
results.Add(" AND \"ownerId\" = @ownerId ");
results.Add(" AND \"deviceAssetId\" in ( ");
results.Add(deviceAssetIds);
results.Add(" ) ");
return string.Join(Environment.NewLine, results);
} // cSpell:enable
private static int? ExecuteNonQuery(string connectionString, string commandText, NpgsqlParameter[] npgsqlParameters)
private static int? ExecuteNonQuery(string connectionString, string commandText, ReadOnlyCollection<NpgsqlParameter> npgsqlParameters)
{
int? result;
if (string.IsNullOrEmpty(connectionString))
@ -122,19 +25,19 @@ public class AssetService(ILogger<Program> logger, AppSettings appSettings)
using NpgsqlConnection npgsqlConnection = new(connectionString);
npgsqlConnection.Open();
using NpgsqlCommand npgsqlCommand = new(commandText, npgsqlConnection);
npgsqlCommand.Parameters.AddRange(npgsqlParameters);
npgsqlCommand.Parameters.AddRange(npgsqlParameters.ToArray());
result = npgsqlCommand.ExecuteNonQuery();
}
return result;
}
private static StringBuilder GetForJsonPath(string connectionString, string commandText, NpgsqlParameter[] npgsqlParameters)
private static StringBuilder GetForJsonPath(string connectionString, string commandText, ReadOnlyCollection<NpgsqlParameter> npgsqlParameters)
{
StringBuilder result = new();
using NpgsqlConnection npgsqlConnection = new(connectionString);
npgsqlConnection.Open();
using NpgsqlCommand npgsqlCommand = new(commandText, npgsqlConnection);
npgsqlCommand.Parameters.AddRange(npgsqlParameters);
npgsqlCommand.Parameters.AddRange(npgsqlParameters.ToArray());
NpgsqlDataReader npgsqlDataReader = npgsqlCommand.ExecuteReader(CommandBehavior.SequentialAccess);
while (npgsqlDataReader.Read())
_ = result.Append(npgsqlDataReader.GetString(0));
@ -144,9 +47,9 @@ public class AssetService(ILogger<Program> logger, AppSettings appSettings)
public string? GetColumns()
{
string result;
string commandText = GetColumnsCommandText();
NpgsqlParameter[] npgsqlParameters = [];
StringBuilder stringBuilder = GetForJsonPath(_AppSettings.ConnectionString, commandText, npgsqlParameters);
string commandText = CommandText.GetColumns();
StringBuilder stringBuilder = GetForJsonPath(_AppSettings.ConnectionString, commandText, npgsqlParameters.AsReadOnly());
result = stringBuilder.ToString();
if (result.Length == 1)
File.WriteAllText(".vscode/jsonl/.jsonl", result);
@ -156,9 +59,9 @@ public class AssetService(ILogger<Program> logger, AppSettings appSettings)
public string? GetOwnerIds()
{
string result;
string commandText = GetOwnerIdActiveImageCommandText();
NpgsqlParameter[] npgsqlParameters = [];
StringBuilder stringBuilder = GetForJsonPath(_AppSettings.ConnectionString, commandText, npgsqlParameters);
string commandText = CommandText.GetOwnerIdActiveImage();
StringBuilder stringBuilder = GetForJsonPath(_AppSettings.ConnectionString, commandText, npgsqlParameters.AsReadOnly());
result = stringBuilder.ToString();
if (result.Length == 1)
File.WriteAllText(".vscode/jsonl/.jsonl", result);
@ -168,9 +71,9 @@ public class AssetService(ILogger<Program> logger, AppSettings appSettings)
public string? GetAssets(Guid ownerId)
{
string result;
string commandText = GetAssetActiveImagePreviewNotDuplicateCommandText();
NpgsqlParameter[] npgsqlParameters = [new NpgsqlParameter(nameof(ownerId), ownerId)];
StringBuilder stringBuilder = GetForJsonPath(_AppSettings.ConnectionString, commandText, npgsqlParameters);
string commandText = CommandText.GetAssetActiveImagePreviewNotDuplicate(_AppSettings.FilterTags);
StringBuilder stringBuilder = GetForJsonPath(_AppSettings.ConnectionString, commandText, npgsqlParameters.AsReadOnly());
result = stringBuilder.ToString();
if (result.Length == 1)
File.WriteAllText(".vscode/jsonl/assets.jsonl", result);
@ -180,9 +83,9 @@ public class AssetService(ILogger<Program> logger, AppSettings appSettings)
public ReadOnlyCollection<string>? GetRandomPaths(Guid ownerId)
{
string[]? results;
string commandText = GetAssetActiveImagePreviewNotDuplicateCommandText();
NpgsqlParameter[] npgsqlParameters = [new NpgsqlParameter(nameof(ownerId), ownerId)];
StringBuilder stringBuilder = GetForJsonPath(_AppSettings.ConnectionString, commandText, npgsqlParameters);
string commandText = CommandText.GetAssetActiveImagePreviewNotDuplicate(_AppSettings.FilterTags);
StringBuilder stringBuilder = GetForJsonPath(_AppSettings.ConnectionString, commandText, npgsqlParameters.AsReadOnly());
string json = stringBuilder.ToString();
Random random = new();
string ownerIdValue = ownerId.ToString();
@ -191,6 +94,25 @@ public class AssetService(ILogger<Program> logger, AppSettings appSettings)
return results?.AsReadOnly();
}
public Tag[]? GetArchivedTag(AppSettings appSettings)
{
Tag[] results;
NpgsqlParameter[] npgsqlParameters = [];
string commandText = CommandText.GetArchivedTag(appSettings.ArchivedTag);
StringBuilder stringBuilder = GetForJsonPath(appSettings.ConnectionString, commandText, npgsqlParameters.AsReadOnly());
string json = stringBuilder.ToString();
results = JsonSerializer.Deserialize(json, TagCollectionSourceGenerationContext.Default.TagArray);
return results;
}
public string? GetArchivedTag()
{
string result;
Tag[]? tags = GetArchivedTag(_AppSettings);
result = tags is null || tags.Length != 1 ? null : tags[0].Id;
return result;
}
public string? SaveRandomPaths(Guid ownerId)
{
string? results;
@ -252,6 +174,9 @@ public class AssetService(ILogger<Program> logger, AppSettings appSettings)
return results.AsReadOnly();
}
private static string GetDeviceAssetIds(ReadOnlyCollection<Identifier> identifiers, ReadOnlyCollection<string> assets) =>
$"'{string.Join($"',{Environment.NewLine}'", (from l in identifiers where !assets.Contains($"{l.PaddedId}{l.Extension}") select $"{l.PaddedId}{l.Extension}").ToArray())}'";
public ReadOnlyCollection<int> SetArchiveImmich(Guid ownerId)
{
List<int> results;
@ -265,20 +190,41 @@ public class AssetService(ILogger<Program> logger, AppSettings appSettings)
results = null;
else
{
string json = File.ReadAllText(checkFile);
string json;
string commandText;
json = File.ReadAllText(checkFile);
Identifier[]? identifiers = JsonSerializer.Deserialize<Identifier[]>(json);
if (identifiers is null || identifiers.Length == 0)
results = null;
else
{
string deviceAssetIds = $"'{string.Join($"',{Environment.NewLine}'", identifiers.Select(l => $"{l.PaddedId}{l.Extension}").ToArray())}'";
string commandText = SetAssetArchivedCommandText(deviceAssetIds);
NpgsqlParameter[] npgsqlParameters = [new NpgsqlParameter(nameof(ownerId), ownerId)];
int? result = ExecuteNonQuery(_AppSettings.ConnectionString, commandText, npgsqlParameters);
if (result is null)
Tag[]? tags = GetArchivedTag(_AppSettings);
if (tags is null || tags.Length != 1)
results = null;
else
results = [result.Value];
{
List<NpgsqlParameter> npgsqlParameters = [];
npgsqlParameters.Add(new NpgsqlParameter(nameof(ownerId), ownerId));
commandText = CommandText.GetAssetActiveImagePreviewNotDuplicate(_AppSettings.FilterTags);
StringBuilder stringBuilder = GetForJsonPath(_AppSettings.ConnectionString, commandText, npgsqlParameters.AsReadOnly());
json = stringBuilder.ToString();
Asset[]? assets = JsonSerializer.Deserialize(json, AssetCollectionSourceGenerationContext.Default.AssetArray);
if (assets is null || assets.Length == 0)
results = null;
else
{
string tagsId = tags[0].Id;
npgsqlParameters.Add(new NpgsqlParameter(nameof(tagsId), tagsId));
string[] assetDeviceAssetIds = (from l in assets select l.DeviceAssetId).ToArray();
string deviceAssetIds = GetDeviceAssetIds(identifiers.AsReadOnly(), assetDeviceAssetIds.AsReadOnly());
commandText = CommandText.SetAssetArchived(deviceAssetIds);
int? result = ExecuteNonQuery(_AppSettings.ConnectionString, commandText, npgsqlParameters.AsReadOnly());
if (result is null)
results = null;
else
results = [result.Value];
}
}
}
}
}

View File

@ -0,0 +1,118 @@
namespace ImmichToSlideshow.Services;
internal static class CommandText
{
internal static string GetColumns()
{ // 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' ");
return string.Join(Environment.NewLine, results);
} // cSpell:enable
internal static string GetOwnerIdActiveImage()
{ // cSpell:disable
List<string> results = new();
results.Add(" SELECT json_agg(j) ");
results.Add(" FROM ( ");
results.Add(" SELECT a.\"ownerId\" ");
results.Add(" FROM assets a ");
results.Add(" WHERE a.\"status\" = 'active' ");
results.Add(" AND a.\"type\" = 'IMAGE' ");
results.Add(" GROUP");
results.Add(" BY a.\"ownerId\" ");
results.Add(" ) j ");
return string.Join(Environment.NewLine, results);
} // cSpell:enable
internal static string GetAssetActiveImagePreviewNotDuplicate(string[] filterTags)
{ // cSpell:disable
List<string> results = new();
results.Add(" SELECT json_agg(j) ");
results.Add(" FROM ( ");
results.Add(" SELECT a.\"id\" ");
results.Add(" , a.\"deviceAssetId\" ");
// results.Add(" , a.\"ownerId\" ");
// results.Add(" , a.\"deviceId\" ");
// results.Add(" , a.\"type\" ");
results.Add(" , a.\"originalPath\" ");
// results.Add(" , a.\"fileCreatedAt\" ");
// results.Add(" , a.\"fileModifiedAt\" ");
// results.Add(" , a.\"isFavorite\" ");
// results.Add(" , a.\"duration\" ");
// results.Add(" , a.\"encodedVideoPath\" ");
// results.Add(" , a.\"checksum\" ");
// results.Add(" , a.\"isVisible\" ");
// results.Add(" , a.\"livePhotoVideoId\" ");
// results.Add(" , a.\"updatedAt\" ");
// results.Add(" , a.\"createdAt\" ");
// results.Add(" , a.\"isArchived\" ");
results.Add(" , a.\"originalFileName\" ");
// results.Add(" , a.\"sidecarPath\" ");
// results.Add(" , a.\"thumbhash\" ");
// results.Add(" , a.\"isOffline\" ");
// results.Add(" , a.\"libraryId\" ");
// results.Add(" , a.\"isExternal\" ");
// results.Add(" , a.\"deletedAt\" ");
// results.Add(" , a.\"localDateTime\" ");
// results.Add(" , a.\"stackId\" ");
results.Add(" , a.\"duplicateId\" ");
// results.Add(" , a.\"status\" ");
results.Add(" , f.\"path\" ");
results.Add(" FROM assets a ");
results.Add(" INNER ");
results.Add(" JOIN asset_files f ");
results.Add(" ON a.\"id\" = f.\"assetId\" ");
results.Add(" WHERE a.\"status\" = 'active' ");
results.Add(" AND a.\"type\" = 'IMAGE' ");
results.Add(" AND f.\"type\" = 'preview' ");
results.Add(" AND a.\"duplicateId\" is null ");
results.Add(" AND a.\"id\" not in ( ");
results.Add(" SELECT \"assetsId\" ");
results.Add(" FROM tag_asset g ");
results.Add(" JOIN tags t ");
results.Add(" ON g.\"tagsId\" = t.\"id\" ");
results.Add($" WHERE t.\"value\" in ('{string.Join("','", filterTags)}') ");
results.Add(" ) ");
results.Add(" AND a.\"isExternal\" = true ");
results.Add(" AND a.\"isOffline\" = false ");
results.Add(" AND a.\"ownerId\" = @ownerId ");
results.Add(" ) j ");
return string.Join(Environment.NewLine, results);
} // cSpell:enable
internal static string SetAssetArchived(string deviceAssetIds)
{ // cSpell:disable
List<string> results = new();
results.Add(" INSERT INTO tag_asset ");
results.Add(" (\"assetsId\", @tagsId ");
results.Add(" SELECT a.\"id\" ");
results.Add(" FROM assets a ");
results.Add(" WHERE a.\"type\" = 'IMAGE' ");
results.Add(" AND a.\"ownerId\" = @ownerId ");
results.Add(" AND a.\"deviceAssetId\" in ( ");
results.Add(deviceAssetIds);
results.Add(" ) ");
return string.Join(Environment.NewLine, results);
} // cSpell:enable
internal static string GetArchivedTag(string archivedTag)
{ // cSpell:disable
List<string> results = new();
results.Add(" SELECT json_agg(t) ");
results.Add(" FROM tags t ");
results.Add($" WHERE value = '{archivedTag}' ");
return string.Join(Environment.NewLine, results);
} // cSpell:enable
}