Compare commits
24 Commits
develop
...
4c85fee004
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c85fee004 | ||
|
|
3039fdfd39 | ||
|
|
f2db2e5204 | ||
|
|
021e186611 | ||
|
|
9d5e810cc6 | ||
|
|
be1e02bcf2 | ||
|
|
9821cb1c46 | ||
|
|
a0ec82f6b9 | ||
|
|
6f54e7a6ba | ||
|
|
c17fd84f0a | ||
|
|
02a938bba0 | ||
|
|
af9e3e25da | ||
|
|
9ff4f976d6 | ||
|
|
c771e1446c | ||
|
|
b0406ac4c8 | ||
|
|
a32e062d07 | ||
|
|
8ddd9c0dac | ||
|
|
67dd365ffc | ||
|
|
bf176e0ce5 | ||
|
|
565b888620 | ||
|
|
e371d73601 | ||
|
|
909d4d5e68 | ||
|
|
bd8af5b28f | ||
|
|
4986e401c4 |
6
.idea/.idea.MilkyShots/.idea/copilot.data.migration.agent.xml
generated
Normal file
6
.idea/.idea.MilkyShots/.idea/copilot.data.migration.agent.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AgentMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/.idea.MilkyShots/.idea/copilot.data.migration.edit.xml
generated
Normal file
6
.idea/.idea.MilkyShots/.idea/copilot.data.migration.edit.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="EditMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
2
.idea/.idea.MilkyShots/.idea/watcherTasks.xml
generated
2
.idea/.idea.MilkyShots/.idea/watcherTasks.xml
generated
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ProjectTasksOptions">
|
<component name="ProjectTasksOptions">
|
||||||
<TaskOptions isEnabled="true">
|
<TaskOptions isEnabled="false">
|
||||||
<option name="arguments" value="$FileName$:$FileNameWithoutExtension$.css" />
|
<option name="arguments" value="$FileName$:$FileNameWithoutExtension$.css" />
|
||||||
<option name="checkSyntaxErrors" value="true" />
|
<option name="checkSyntaxErrors" value="true" />
|
||||||
<option name="description" />
|
<option name="description" />
|
||||||
|
|||||||
29
Butter/Dtos/Jobs/JobStatusDto.cs
Normal file
29
Butter/Dtos/Jobs/JobStatusDto.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using Butter.Types;
|
||||||
|
|
||||||
|
namespace Butter.Dtos.Jobs;
|
||||||
|
|
||||||
|
public class JobStatusDto {
|
||||||
|
public Guid Id { get; init; }
|
||||||
|
public string Name { get; init; } = null!;
|
||||||
|
public EJobType JobType { get; init; }
|
||||||
|
public EJobStatus Status { get; init; }
|
||||||
|
public DateTime Created { get; init; }
|
||||||
|
public DateTime? Started { get; init; }
|
||||||
|
public DateTime? Finished { get; init; }
|
||||||
|
public string? Message { get; init; }
|
||||||
|
public float Progress { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum EJobType {
|
||||||
|
FileSystemScan,
|
||||||
|
ThumbnailGeneration,
|
||||||
|
PreviewGeneration,
|
||||||
|
MetadataExtraction,
|
||||||
|
IntegrityCheck,
|
||||||
|
PHashGeneration
|
||||||
|
}
|
||||||
|
|
||||||
|
public class JobRequestDto {
|
||||||
|
public EJobType JobType { get; init; }
|
||||||
|
public List<string> Parameters { get; init; } = new();
|
||||||
|
}
|
||||||
@@ -3,9 +3,12 @@ using Butter.Settings;
|
|||||||
namespace Butter.Dtos.Settings;
|
namespace Butter.Dtos.Settings;
|
||||||
|
|
||||||
public class SettingDto {
|
public class SettingDto {
|
||||||
public required string Name { get; set; } = string.Empty;
|
public required string Name { get; set; } = string.Empty;
|
||||||
public string? Value { get; set; } = string.Empty;
|
public string? Value { get; set; } = string.Empty;
|
||||||
public required string? Description { get; set; }
|
public required string? Description { get; set; }
|
||||||
public required EType Type { get; set; }
|
public required EType Type { get; set; }
|
||||||
public required DisplayType DisplayType { get; set; }
|
public required DisplayType DisplayType { get; set; }
|
||||||
|
// for range type settings min max and step are used.
|
||||||
|
//
|
||||||
|
public string[]? Options { get; set; }
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,8 @@ public enum DisplayType {
|
|||||||
Checkbox = 4,
|
Checkbox = 4,
|
||||||
Switch = 5,
|
Switch = 5,
|
||||||
DateTimePicker = 6,
|
DateTimePicker = 6,
|
||||||
TimePicker = 7
|
TimePicker = 7,
|
||||||
|
Po2W = 8, // Power of 2
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class DisplayTypeExtension {
|
public static class DisplayTypeExtension {
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ public enum Settings {
|
|||||||
FolderScanInterval, //Interval in minutes for folder scanning
|
FolderScanInterval, //Interval in minutes for folder scanning
|
||||||
FileUploadEnabled, //Enable or disable file upload
|
FileUploadEnabled, //Enable or disable file upload
|
||||||
FileUploadMaxSize, //Maximum file size for uploads in bytes
|
FileUploadMaxSize, //Maximum file size for uploads in bytes
|
||||||
|
ThumbnailPath, //Path to store thumbnails
|
||||||
|
PreviewPath, //Path to store previews
|
||||||
|
ThumbnailSize, //Longest side size for thumbnails
|
||||||
|
PreviewSize, //Longest side size for previews
|
||||||
|
MaxConcurrentJobs //Maximum number of concurrent jobs
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class SettingsExtensions {
|
public static class SettingsExtensions {
|
||||||
@@ -16,6 +21,11 @@ public static class SettingsExtensions {
|
|||||||
Settings.FolderScanInterval => "Folder Scan Interval",
|
Settings.FolderScanInterval => "Folder Scan Interval",
|
||||||
Settings.FileUploadEnabled => "File Upload Enabled",
|
Settings.FileUploadEnabled => "File Upload Enabled",
|
||||||
Settings.FileUploadMaxSize => "File Upload MaxSize",
|
Settings.FileUploadMaxSize => "File Upload MaxSize",
|
||||||
|
Settings.ThumbnailPath => "Thumbnail Path",
|
||||||
|
Settings.PreviewPath => "Preview Path",
|
||||||
|
Settings.ThumbnailSize => "Thumbnail Size",
|
||||||
|
Settings.PreviewSize => "Preview Size",
|
||||||
|
Settings.MaxConcurrentJobs => "Max Concurrent Jobs",
|
||||||
_ => setting.ToString() // Fallback to the enum name
|
_ => setting.ToString() // Fallback to the enum name
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Lactose.Jobs;
|
namespace Butter.Types;
|
||||||
|
|
||||||
public enum EJobStatus {
|
public enum EJobStatus {
|
||||||
Queued, // Not yet started
|
Queued, // Not yet started
|
||||||
@@ -15,6 +15,7 @@ public class LactoseDbContext : DbContext {
|
|||||||
public LactoseDbContext(DbContextOptions options) : base(options) {}
|
public LactoseDbContext(DbContextOptions options) : base(options) {}
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
||||||
|
modelBuilder.HasPostgresExtension("vector");
|
||||||
//Album Relationships
|
//Album Relationships
|
||||||
modelBuilder.Entity<Album>().HasOne(e => e.UserOwner).WithMany(e => e.OwnedAlbums);
|
modelBuilder.Entity<Album>().HasOne(e => e.UserOwner).WithMany(e => e.OwnedAlbums);
|
||||||
modelBuilder.Entity<Album>().HasOne(e => e.PersonOwner).WithMany(e => e.Albums);
|
modelBuilder.Entity<Album>().HasOne(e => e.PersonOwner).WithMany(e => e.Albums);
|
||||||
|
|||||||
97
Lactose/Controllers/JobsController.cs
Normal file
97
Lactose/Controllers/JobsController.cs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
using Butter.Dtos.Jobs;
|
||||||
|
using Butter.Types;
|
||||||
|
using Lactose.Jobs;
|
||||||
|
using Lactose.Mapper;
|
||||||
|
using Lactose.Services;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Lactose.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Authorize(Roles = "Admin")]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class JobsController(JobManager jobManager, JobScheduler jobScheduler, LactoseAuthService authService) : ControllerBase {
|
||||||
|
[HttpGet]
|
||||||
|
[Authorize(Roles = "Admin")]
|
||||||
|
public ActionResult<List<JobStatusDto>> GetAll() {
|
||||||
|
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||||
|
|
||||||
|
if (accessLevel != EAccessLevel.Admin) { return Unauthorized(); }
|
||||||
|
|
||||||
|
return Ok(jobManager.Jobs.Select(j => j.ToJobStatusDto()).ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{jobId}")]
|
||||||
|
[Authorize(Roles = "Admin")]
|
||||||
|
public ActionResult<JobStatusDto> Get(Guid jobId) {
|
||||||
|
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||||
|
|
||||||
|
if (accessLevel != EAccessLevel.Admin) { return Unauthorized(); }
|
||||||
|
|
||||||
|
var job = jobManager.GetJob(jobId);
|
||||||
|
|
||||||
|
if (job == null) { return NotFound(); }
|
||||||
|
return Ok(job.ToJobStatusDto());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{jobId}")]
|
||||||
|
[Authorize(Roles = "Admin")]
|
||||||
|
public ActionResult Cancel(Guid jobId) {
|
||||||
|
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||||
|
|
||||||
|
if (accessLevel != EAccessLevel.Admin) { return Unauthorized(); }
|
||||||
|
var job = jobManager.GetJob(jobId);
|
||||||
|
|
||||||
|
if (job == null) { return NotFound(); }
|
||||||
|
job.Cancel();
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut]
|
||||||
|
[Authorize(Roles = "Admin")]
|
||||||
|
public ActionResult Enqueue([FromBody] JobRequestDto jobRequest) {
|
||||||
|
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||||
|
|
||||||
|
if (accessLevel != EAccessLevel.Admin) { return Unauthorized(); }
|
||||||
|
|
||||||
|
switch (jobRequest.JobType) {
|
||||||
|
case EJobType.FileSystemScan:
|
||||||
|
if (jobRequest.Parameters.Count != 0)
|
||||||
|
return BadRequest("FullFileSystemScan job does not require any parameters.");
|
||||||
|
|
||||||
|
jobScheduler.QueueFileSystemCrawl(null);
|
||||||
|
return Ok();
|
||||||
|
case EJobType.ThumbnailGeneration:
|
||||||
|
if (jobRequest.Parameters.Count != 0)
|
||||||
|
return BadRequest("ThumbnailGeneration job does not require any parameters.");
|
||||||
|
|
||||||
|
jobScheduler.QueueThumbnailGeneration(null);
|
||||||
|
return Ok();
|
||||||
|
case EJobType.PreviewGeneration:
|
||||||
|
if (jobRequest.Parameters.Count != 0)
|
||||||
|
return BadRequest("PreviewGeneration job does not require any parameters.");
|
||||||
|
|
||||||
|
jobScheduler.QueuePreviewGeneration(null);
|
||||||
|
return Ok();
|
||||||
|
case EJobType.MetadataExtraction:
|
||||||
|
if (jobRequest.Parameters.Count != 0)
|
||||||
|
return BadRequest("MetadataExtraction job does not require any parameters.");
|
||||||
|
|
||||||
|
jobScheduler.QueueMetadataExtraction(null);
|
||||||
|
return Ok();
|
||||||
|
case EJobType.IntegrityCheck:
|
||||||
|
if (jobRequest.Parameters.Count != 0)
|
||||||
|
return BadRequest("IntegrityCheck job does not require any parameters.");
|
||||||
|
|
||||||
|
jobScheduler.QueueIntegrityCheck(null);
|
||||||
|
return Ok();
|
||||||
|
case EJobType.PHashGeneration:
|
||||||
|
if (jobRequest.Parameters.Count > 1)
|
||||||
|
return BadRequest("PHashGeneration job takes zero or one parameter (the asset ID).");
|
||||||
|
jobScheduler.QueuePHashGeneration(jobRequest.Parameters.FirstOrDefault());
|
||||||
|
return Ok();
|
||||||
|
default: return BadRequest("Invalid job type.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,37 +1,92 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"Name": "User Registration Enabled",
|
"Name": "User Registration Enabled",
|
||||||
"Value" : "false",
|
"Value": "false",
|
||||||
"Description": "Sets if user registration is enabled or not.",
|
"Description": "Sets if user registration is enabled or not.",
|
||||||
"Type" : 2,
|
"Type": 2,
|
||||||
"DisplayType" : 5
|
"DisplayType": 5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Name": "Folder Scan Enabled",
|
"Name": "Folder Scan Enabled",
|
||||||
"Value" : "true",
|
"Value": "true",
|
||||||
"Description": "Sets if the folder scan service should be running or not.",
|
"Description": "Sets if the folder scan service should be running or not.",
|
||||||
"Type" : 2,
|
"Type": 2,
|
||||||
"DisplayType" : 5
|
"DisplayType": 5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Name": "Folder Scan Interval",
|
"Name": "Folder Scan Interval",
|
||||||
"Value" : "",
|
"Value": "",
|
||||||
"Description": "Time interval after the previous scan has finished before a new folder scan is started.",
|
"Description": "Time interval after the previous scan has finished before a new folder scan is started.",
|
||||||
"Type" : 4,
|
"Type": 4,
|
||||||
"DisplayType" : 7
|
"DisplayType": 7,
|
||||||
|
"Options": [
|
||||||
|
"5",
|
||||||
|
"360",
|
||||||
|
"5"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Name": "File Upload Enabled",
|
"Name": "File Upload Enabled",
|
||||||
"Value" : "false",
|
"Value": "false",
|
||||||
"Description": "NOT IMPLEMENTED YET: Sets if the file upload service should be running or not.",
|
"Description": "NOT IMPLEMENTED YET: Sets if the file upload service should be running or not.",
|
||||||
"Type" : 2,
|
"Type": 2,
|
||||||
"DisplayType" : 5
|
"DisplayType": 5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Name": "File Upload MaxSize",
|
"Name": "File Upload MaxSize",
|
||||||
"Value" : "0",
|
"Value": "0",
|
||||||
"Description": "Max file size for uploads in bytes.",
|
"Description": "Max file size for uploads in bytes.",
|
||||||
"Type" : 1,
|
"Type": 1,
|
||||||
"DisplayType" :2
|
"DisplayType": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Thumbnail Path",
|
||||||
|
"Value": "",
|
||||||
|
"Description": "Path where thumbnails are stored.",
|
||||||
|
"Type": 0,
|
||||||
|
"DisplayType": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Preview Path",
|
||||||
|
"Value": "",
|
||||||
|
"Description": "Path where thumbnails are stored.",
|
||||||
|
"Type": 0,
|
||||||
|
"DisplayType": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Thumbnail Size",
|
||||||
|
"Value": "512",
|
||||||
|
"Description": "Size for longest side of the thumbnail in pixels.",
|
||||||
|
"Type": 1,
|
||||||
|
"DisplayType": 8,
|
||||||
|
"Options": [
|
||||||
|
"8",
|
||||||
|
"12",
|
||||||
|
"1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Preview Size",
|
||||||
|
"Value": "2048",
|
||||||
|
"Description": "Size for longest side of the preview in pixels.",
|
||||||
|
"Type": 1,
|
||||||
|
"DisplayType": 8,
|
||||||
|
"Options": [
|
||||||
|
"8",
|
||||||
|
"12",
|
||||||
|
"1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Max Concurrent Jobs",
|
||||||
|
"Value": "4",
|
||||||
|
"Description": "Max number of concurrent jobs",
|
||||||
|
"Type": 1,
|
||||||
|
"DisplayType": 2,
|
||||||
|
"Options": [
|
||||||
|
"1",
|
||||||
|
"32",
|
||||||
|
"1"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -2,14 +2,15 @@ using Butter;
|
|||||||
using Butter.Types;
|
using Butter.Types;
|
||||||
using Lactose.Models;
|
using Lactose.Models;
|
||||||
using Lactose.Repositories;
|
using Lactose.Repositories;
|
||||||
|
using System.Collections;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using static System.String;
|
using static System.String;
|
||||||
|
|
||||||
namespace Lactose.Jobs;
|
namespace Lactose.Jobs;
|
||||||
|
|
||||||
public class FileSystemCrawlJob : Job {
|
public sealed class FileSystemCrawlJob : Job {
|
||||||
public override string Name { get; } = "Filesystem Crawl Job";
|
public override string Name { get; } = "Directory Scan: ";
|
||||||
public override JobStatus Status { get; }
|
public override JobStatus JobStatus { get; }
|
||||||
IAssetRepository assetRepository;
|
IAssetRepository assetRepository;
|
||||||
string workingPath;
|
string workingPath;
|
||||||
ILogger<FileSystemCrawlJob> logger;
|
ILogger<FileSystemCrawlJob> logger;
|
||||||
@@ -22,15 +23,16 @@ public class FileSystemCrawlJob : Job {
|
|||||||
IAssetRepository assetRepository,
|
IAssetRepository assetRepository,
|
||||||
JobManager jobManager
|
JobManager jobManager
|
||||||
) {
|
) {
|
||||||
Status = new(this);
|
JobStatus = new(this);
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.assetRepository = assetRepository;
|
this.assetRepository = assetRepository;
|
||||||
this.workingPath = workingPath;
|
this.workingPath = workingPath;
|
||||||
this.jobManager = jobManager;
|
this.jobManager = jobManager;
|
||||||
|
Name += $"{workingPath}";
|
||||||
}
|
}
|
||||||
|
|
||||||
///<inheritdoc />
|
///<inheritdoc />
|
||||||
protected override void TaskJob(CancellationToken token) {
|
protected override async Task TaskJob(CancellationToken token) {
|
||||||
logger.LogInformation($"Started crawling directory {workingPath}");
|
logger.LogInformation($"Started crawling directory {workingPath}");
|
||||||
|
|
||||||
DirectoryInfo dir = new(workingPath);
|
DirectoryInfo dir = new(workingPath);
|
||||||
@@ -54,7 +56,10 @@ public class FileSystemCrawlJob : Job {
|
|||||||
|
|
||||||
foreach (var folder in folders) {
|
foreach (var folder in folders) {
|
||||||
if (token.IsCancellationRequested) {
|
if (token.IsCancellationRequested) {
|
||||||
Status.Cancel();
|
// Cancel all child jobs
|
||||||
|
childJobs.ForEach(j => j.Cancel());
|
||||||
|
// Report this job as canceled
|
||||||
|
JobStatus.Cancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,17 +70,20 @@ public class FileSystemCrawlJob : Job {
|
|||||||
childJobs.Add(job);
|
childJobs.Add(job);
|
||||||
jobManager.EnqueueJob(job);
|
jobManager.EnqueueJob(job);
|
||||||
logger.LogDebug($"Enqueued crawl job for folder {folder.FullName}");
|
logger.LogDebug($"Enqueued crawl job for folder {folder.FullName}");
|
||||||
Status.UpdateProgress(Math.Clamp(steps/(float)totalSteps, 0, 1), $"Processing folder {folder.FullName}");
|
JobStatus.UpdateProgress(Math.Clamp(steps/(float)totalSteps, 0, 1), $"Processing folder {folder.FullName}");
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var file in files) {
|
foreach (var file in files) {
|
||||||
if (token.IsCancellationRequested) {
|
if (token.IsCancellationRequested) {
|
||||||
Status.Cancel();
|
// Cancel all child jobs
|
||||||
|
childJobs.ForEach(j => j.Cancel());
|
||||||
|
// Report this job as canceled
|
||||||
|
JobStatus.Cancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
steps++;
|
steps++;
|
||||||
Status.UpdateProgress(Math.Clamp(steps/(float)totalSteps, 0, 1), $"Processing file {file.FullName}");
|
JobStatus.UpdateProgress(Math.Clamp(steps/(float)totalSteps, 0, 1), $"Processing file {file.FullName}");
|
||||||
|
|
||||||
if (assetRepository.FindByPath(file.FullName) != null) continue;
|
if (assetRepository.FindByPath(file.FullName) != null) continue;
|
||||||
|
|
||||||
@@ -94,19 +102,19 @@ public class FileSystemCrawlJob : Job {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wait for all child jobs to complete
|
// Wait for all child jobs to complete
|
||||||
Status.UpdateProgress(Math.Clamp(steps/(float)totalSteps, 0, 1), $"Waiting for {childJobs.Count} child jobs to complete.");
|
JobStatus.UpdateProgress(Math.Clamp(steps/(float)totalSteps, 0, 1), $"Waiting for {childJobs.Count} child jobs to complete.");
|
||||||
Status.Status = EJobStatus.Waiting;
|
JobStatus.Wait();
|
||||||
|
|
||||||
while (childJobs.Count > 0) {
|
while (childJobs.Count > 0) {
|
||||||
if (token.IsCancellationRequested) {
|
if (token.IsCancellationRequested) {
|
||||||
// Cancel all child jobs
|
// Cancel all child jobs
|
||||||
childJobs.ForEach(j => j.Cancel());
|
childJobs.ForEach(j => j.Cancel());
|
||||||
// Cancel this job as well
|
// Cancel this job as well
|
||||||
Status.Cancel();
|
JobStatus.Cancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Status.Complete("Crawl completed successfully.");
|
JobStatus.Complete("Crawl completed successfully.");
|
||||||
}
|
}
|
||||||
|
|
||||||
Asset? AssetFromPath(string filePath) {
|
Asset? AssetFromPath(string filePath) {
|
||||||
@@ -141,7 +149,7 @@ public class FileSystemCrawlJob : Job {
|
|||||||
_ => throw new ArgumentOutOfRangeException()
|
_ => throw new ArgumentOutOfRangeException()
|
||||||
},
|
},
|
||||||
FileSize = finfo.Length,
|
FileSize = finfo.Length,
|
||||||
Hash = []
|
Hash = new BitArray(64)
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.LogDebug(
|
logger.LogDebug(
|
||||||
|
|||||||
188
Lactose/Jobs/IntegrityCheckJob.cs
Normal file
188
Lactose/Jobs/IntegrityCheckJob.cs
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
using Butter.Settings;
|
||||||
|
using Lactose.Context;
|
||||||
|
using Lactose.Models;
|
||||||
|
using Lactose.Utils;
|
||||||
|
|
||||||
|
namespace Lactose.Jobs;
|
||||||
|
|
||||||
|
public class IntegrityCheckJob (ILogger<IntegrityCheckJob> logger, IServiceProvider serviceProvider): Job{
|
||||||
|
const float totalSteps = 8;
|
||||||
|
|
||||||
|
public override string Name => "Integrity Check Job";
|
||||||
|
|
||||||
|
void IncrementProgress(float f) => JobStatus.UpdateProgress(f/totalSteps, "Database connection successful.");
|
||||||
|
|
||||||
|
protected override async Task TaskJob(CancellationToken token) {
|
||||||
|
|
||||||
|
float progress = 0;
|
||||||
|
logger.LogInformation("Starting integrity check job...");
|
||||||
|
var scope = serviceProvider.CreateScope();
|
||||||
|
var dbContext = scope.ServiceProvider.GetRequiredService<LactoseDbContext>();
|
||||||
|
|
||||||
|
//Step 1
|
||||||
|
// Check database connection
|
||||||
|
if (!dbContext.Database.CanConnect()) {
|
||||||
|
logger.LogError("Cannot connect to the database.");
|
||||||
|
JobStatus.Fail("Cannot connect to the database.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IncrementProgress(++progress);
|
||||||
|
|
||||||
|
// Step 2
|
||||||
|
// Check for pending migrations
|
||||||
|
var pendingMigrations = dbContext.Database.GetPendingMigrations().ToList();
|
||||||
|
|
||||||
|
if (pendingMigrations.Any()) {
|
||||||
|
logger.LogWarning("There are pending migrations:");
|
||||||
|
|
||||||
|
foreach (var migration in pendingMigrations) logger.LogWarning($"- {migration}");
|
||||||
|
|
||||||
|
// Apply migrations
|
||||||
|
try {
|
||||||
|
dbContext.Database.Migrate();
|
||||||
|
logger.LogInformation("Applied pending migrations successfully.");
|
||||||
|
} catch (Exception ex) {
|
||||||
|
logger.LogError(ex, "Error applying migrations.");
|
||||||
|
JobStatus.Fail("Error applying migrations.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IncrementProgress(++progress);
|
||||||
|
|
||||||
|
// A bit of context: Load() will load all entities from the table into the DbContext.
|
||||||
|
// Essentially it will ask the DB for all rows in the table and create entities for them in the DbContext.
|
||||||
|
// This will basically warm up the cache of the DbContext so that when we later access all the rows in the table,
|
||||||
|
// it won't lazy load them one by one, one call at the time.
|
||||||
|
// This is important because lazy loading one by one would be a huge performance hit,
|
||||||
|
// especially if there are many rows in the table.
|
||||||
|
// While for folders it might not be a big deal, this is just for consistency with all the other steps,
|
||||||
|
// especially the asset one.
|
||||||
|
|
||||||
|
// Step 3
|
||||||
|
// Check for folders existence
|
||||||
|
dbContext.Folders.Load();
|
||||||
|
foreach (Folder folder in dbContext.Folders.ToList()) {
|
||||||
|
if (!Directory.Exists(folder.BasePath)) {
|
||||||
|
logger.LogWarning($"Folder {folder.Id} does not exist.");
|
||||||
|
folder.Active = false;
|
||||||
|
dbContext.Folders.Update(folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
var affectedAssets = dbContext.Assets.Where(a => a.FolderId == folder.Id);
|
||||||
|
dbContext.Assets.RemoveRange(affectedAssets);
|
||||||
|
dbContext.SaveChanges();
|
||||||
|
logger.LogWarning("Removed {Count} assets associated with folder {FolderId}.", affectedAssets.Count(), folder.Id);
|
||||||
|
}
|
||||||
|
IncrementProgress(++progress);
|
||||||
|
|
||||||
|
// Step 4
|
||||||
|
// Check for assets existence
|
||||||
|
dbContext.Assets.Load();
|
||||||
|
foreach (Asset asset in dbContext.Assets) {
|
||||||
|
if (File.Exists(asset.OriginalPath)) continue;
|
||||||
|
dbContext.Assets.Remove(asset);
|
||||||
|
logger.LogWarning($"Asset {asset.Id} does not exist. Removing from database.");
|
||||||
|
}
|
||||||
|
|
||||||
|
int changes = dbContext.SaveChanges();
|
||||||
|
logger.LogInformation("Removed {Changes} invalid entries from the database.", changes);
|
||||||
|
IncrementProgress(++progress);
|
||||||
|
|
||||||
|
// Step 5
|
||||||
|
// Check for empty albums
|
||||||
|
dbContext.Albums.Load();
|
||||||
|
dbContext.Albums.Include(a => a.Assets)
|
||||||
|
.Where(a => a.Assets!.Count == 0)
|
||||||
|
.ToList().ForEach(a => {
|
||||||
|
logger.LogWarning($"Album {a.Id} is empty. Removing from database.");
|
||||||
|
dbContext.Albums.Remove(a);
|
||||||
|
});
|
||||||
|
changes = dbContext.SaveChanges();
|
||||||
|
logger.LogInformation("Removed {Changes} empty albums from the database.", changes);
|
||||||
|
IncrementProgress(++progress);
|
||||||
|
|
||||||
|
// Step 6
|
||||||
|
// Check for Persons without faces
|
||||||
|
dbContext.People.Include(p => p.Faces)
|
||||||
|
.Where(p => p.Faces != null && p.Faces.Count == 0)
|
||||||
|
.AsEnumerable()
|
||||||
|
.ForEach(p => {
|
||||||
|
logger.LogWarning($"Person {p.Id} has no faces. Removing from database.");
|
||||||
|
dbContext.People.Remove(p);
|
||||||
|
});
|
||||||
|
|
||||||
|
changes = dbContext.SaveChanges();
|
||||||
|
logger.LogInformation("Removed {Changes} people without faces from the database.", changes);
|
||||||
|
IncrementProgress(++progress);
|
||||||
|
|
||||||
|
// Step 7
|
||||||
|
// Check for orphaned thumbnails
|
||||||
|
dbContext.Settings.Load();
|
||||||
|
dbContext.Assets.Load();
|
||||||
|
var thumbnailPath = dbContext.Settings.First(s => s.Name == Settings.ThumbnailPath.AsString()).Value;
|
||||||
|
|
||||||
|
if (!Directory.Exists(thumbnailPath)) {
|
||||||
|
logger.LogWarning("Thumbnail path does not exist. Creating...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (thumbnailPath != null)
|
||||||
|
Directory.CreateDirectory(thumbnailPath);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.LogError(e, "Could not create thumbnail directory.");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogWarning("Marking all assets as missing thumbnails.");
|
||||||
|
dbContext.Assets.ForEach(a => a.ThumbnailPath = "");
|
||||||
|
changes = dbContext.SaveChanges();
|
||||||
|
logger.LogInformation("Marked {Changes} assets as missing thumbnails.", changes);
|
||||||
|
} else {
|
||||||
|
dbContext.Assets.Where(a => !String.IsNullOrEmpty(a.ThumbnailPath)).AsEnumerable()
|
||||||
|
.Where(a => !File.Exists(a.ThumbnailPath)).AsEnumerable()
|
||||||
|
.ForEach(a => {
|
||||||
|
logger.LogWarning($"Asset {a.Id} has a missing thumbnail. Marking as missing.");
|
||||||
|
a.ThumbnailPath = "";
|
||||||
|
dbContext.Assets.Update(a);
|
||||||
|
});
|
||||||
|
changes = dbContext.SaveChanges();
|
||||||
|
logger.LogInformation("Marked {Changes} assets as missing thumbnails.", changes);
|
||||||
|
}
|
||||||
|
|
||||||
|
IncrementProgress(++progress);
|
||||||
|
|
||||||
|
// Step 8
|
||||||
|
// Check for orphaned previews
|
||||||
|
|
||||||
|
var previewPath = dbContext.Settings.First(s => s.Name == Settings.PreviewPath.AsString()).Value;
|
||||||
|
if (!Directory.Exists(previewPath)) {
|
||||||
|
logger.LogWarning("Preview path does not exist. Creating...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (previewPath != null)
|
||||||
|
Directory.CreateDirectory(previewPath);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.LogError(e, "Could not create preview directory.");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogWarning("Marking all assets as missing previews.");
|
||||||
|
dbContext.Assets.ForEach(a => a.PreviewPath = "");
|
||||||
|
changes = dbContext.SaveChanges();
|
||||||
|
logger.LogInformation("Marked {Changes} assets as missing previews.", changes);
|
||||||
|
} else {
|
||||||
|
dbContext.Assets.Where(a => !String.IsNullOrEmpty(a.PreviewPath)).AsEnumerable()
|
||||||
|
.Where(a => !File.Exists(a.PreviewPath)).AsEnumerable()
|
||||||
|
.ForEach(a => {
|
||||||
|
logger.LogWarning($"Asset {a.Id} has a missing preview. Marking as missing.");
|
||||||
|
a.PreviewPath = "";
|
||||||
|
dbContext.Assets.Update(a);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
changes = dbContext.SaveChanges();
|
||||||
|
logger.LogInformation("Marked {Changes} assets as missing previews.", changes);
|
||||||
|
|
||||||
|
IncrementProgress(++progress);
|
||||||
|
|
||||||
|
// Complete
|
||||||
|
logger.LogInformation("Integrity check completed successfully.");
|
||||||
|
JobStatus.Complete("Integrity check completed successfully.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Butter.Types;
|
||||||
|
|
||||||
namespace Lactose.Jobs;
|
namespace Lactose.Jobs;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -8,33 +10,40 @@ namespace Lactose.Jobs;
|
|||||||
public abstract class Job {
|
public abstract class Job {
|
||||||
public virtual Guid Id { get; } = Guid.NewGuid();
|
public virtual Guid Id { get; } = Guid.NewGuid();
|
||||||
public virtual string Name { get; } = "Unnamed Job";
|
public virtual string Name { get; } = "Unnamed Job";
|
||||||
public virtual JobStatus Status { get; }
|
public virtual JobStatus JobStatus { get; }
|
||||||
Task? task;
|
Task? task;
|
||||||
CancellationTokenSource? cts;
|
CancellationTokenSource? cts;
|
||||||
|
|
||||||
protected Job() => Status = new JobStatus(this);
|
protected Job() => JobStatus = new JobStatus(this);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets up the task and the cancellation token, then starts the task (triggering the Started event)
|
/// Sets up the task and the cancellation token, then starts the task (triggering the Started event)
|
||||||
/// Also sets up a continuation to handle completion, failure, or cancellation of the task.
|
/// Also sets up a continuation to handle completion, failure, or cancellation of the task.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void Start() {
|
public virtual void Start() {
|
||||||
Status.Start();
|
JobStatus.Start();
|
||||||
cts = new CancellationTokenSource();
|
cts = new CancellationTokenSource();
|
||||||
task = Task.Run(() => TaskJob(cts.Token));
|
task = Task.Run(() => TaskJob(cts.Token));
|
||||||
|
|
||||||
task.ContinueWith(t => {
|
task.ContinueWith(t => {
|
||||||
if (t.IsCanceled) {
|
switch (JobStatus.Status) {
|
||||||
Status.Cancel("Job was canceled");
|
case EJobStatus.Canceled:
|
||||||
Canceled?.Invoke(this, Status);
|
Canceled?.Invoke(this, JobStatus);
|
||||||
} else if (t.IsFaulted) {
|
break;
|
||||||
Status.Fail(t.Exception?.Message ?? "Job failed with an unknown error");
|
case EJobStatus.Failed:
|
||||||
Failed?.Invoke(this, Status);
|
Failed?.Invoke(this, JobStatus);
|
||||||
} else {
|
break;
|
||||||
Status.Complete("Job completed successfully");
|
case EJobStatus.Running: // If still running or waiting, mark as completed
|
||||||
Completed?.Invoke(this, Status);
|
case EJobStatus.Waiting: //somebody forgot to update the status and the method returned.
|
||||||
|
case EJobStatus.Completed:
|
||||||
|
Completed?.Invoke(this, JobStatus);
|
||||||
|
break;
|
||||||
|
case EJobStatus.Queued: // This should never happen
|
||||||
|
default:
|
||||||
|
Completed?.Invoke(this, JobStatus);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
Done?.Invoke(this, Status);
|
Done?.Invoke(this, JobStatus);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -46,7 +55,7 @@ public abstract class Job {
|
|||||||
/// Invoke ProgressChanged event to report progress updates.
|
/// Invoke ProgressChanged event to report progress updates.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="token">Cancellation toke to retrieve when a task cancellation has been requested</param>
|
/// <param name="token">Cancellation toke to retrieve when a task cancellation has been requested</param>
|
||||||
protected abstract void TaskJob(CancellationToken token);
|
protected abstract Task TaskJob(CancellationToken token);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cancels the job if it's running. Canceled event will be called by the callbacks on the task.
|
/// Cancels the job if it's running. Canceled event will be called by the callbacks on the task.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Butter.Types;
|
||||||
using Lactose.Utils;
|
using Lactose.Utils;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ public class JobManager(IServiceProvider serviceProvider, ILogger<JobManager> lo
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the maximum number of jobs that can run concurrently.
|
/// Gets or sets the maximum number of jobs that can run concurrently.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int MaxConcurrentJobs { get; set; } = 8;
|
public int MaxConcurrentJobs { get; set; } = 4;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a job to the manager.
|
/// Adds a job to the manager.
|
||||||
@@ -45,7 +46,7 @@ public class JobManager(IServiceProvider serviceProvider, ILogger<JobManager> lo
|
|||||||
public void RemoveJob(Guid id) {
|
public void RemoveJob(Guid id) {
|
||||||
if (!jobs.ContainsKey(id)) throw new InvalidOperationException($"Job with ID {id} doesn't exist");
|
if (!jobs.ContainsKey(id)) throw new InvalidOperationException($"Job with ID {id} doesn't exist");
|
||||||
|
|
||||||
switch (jobs[id].Status.Status) {
|
switch (jobs[id].JobStatus.Status) {
|
||||||
case EJobStatus.Running:
|
case EJobStatus.Running:
|
||||||
case EJobStatus.Waiting:
|
case EJobStatus.Waiting:
|
||||||
// Schedule job removal from the dictionary once it's canceled
|
// Schedule job removal from the dictionary once it's canceled
|
||||||
@@ -68,8 +69,9 @@ public class JobManager(IServiceProvider serviceProvider, ILogger<JobManager> lo
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Injects required dependencies and creates a new instance of the specified job type.
|
/// Injects required dependencies and creates a new instance of the specified job type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Job CreateJob<T>() where T : Job {
|
public T CreateJob<T>() where T : Job {
|
||||||
var job = ActivatorUtilities.CreateInstance<T>(serviceProvider);
|
var scope = serviceProvider.CreateScope();
|
||||||
|
var job = ActivatorUtilities.CreateInstance<T>(scope.ServiceProvider);
|
||||||
logger.LogInformation($"Job with ID {job.Id} has been created.");
|
logger.LogInformation($"Job with ID {job.Id} has been created.");
|
||||||
return job;
|
return job;
|
||||||
}
|
}
|
||||||
@@ -77,7 +79,7 @@ public class JobManager(IServiceProvider serviceProvider, ILogger<JobManager> lo
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Injects required dependencies and creates a new instance of the specified job type, passing the provided arguments to its constructor.
|
/// Injects required dependencies and creates a new instance of the specified job type, passing the provided arguments to its constructor.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Job CreateJob<T>(params object[] args) where T : Job {
|
public T CreateJob<T>(params object[] args) where T : Job {
|
||||||
var scope = serviceProvider.CreateScope();
|
var scope = serviceProvider.CreateScope();
|
||||||
var job = ActivatorUtilities.CreateInstance<T>(scope.ServiceProvider, args);
|
var job = ActivatorUtilities.CreateInstance<T>(scope.ServiceProvider, args);
|
||||||
logger.LogInformation($"Job with ID {job.Id} has been created.");
|
logger.LogInformation($"Job with ID {job.Id} has been created.");
|
||||||
@@ -94,15 +96,18 @@ public class JobManager(IServiceProvider serviceProvider, ILogger<JobManager> lo
|
|||||||
var service = new Task(() => {
|
var service = new Task(() => {
|
||||||
while (!stoppingToken.IsCancellationRequested) {
|
while (!stoppingToken.IsCancellationRequested) {
|
||||||
// Skip adding more jobs if we reached the max concurrent jobs limit
|
// Skip adding more jobs if we reached the max concurrent jobs limit
|
||||||
int activeRunningJobs = jobs.Count(job => job.Value.Status.Status == EJobStatus.Running);
|
int activeRunningJobs = jobs.Count(job => job.Value.JobStatus.Status == EJobStatus.Running);
|
||||||
if (activeRunningJobs >= MaxConcurrentJobs) continue;
|
if (activeRunningJobs >= MaxConcurrentJobs) continue;
|
||||||
|
|
||||||
//Take jobs from the jobs list that are queued and start them until we reach the max concurrent jobs limit
|
//Take jobs from the jobs list that are queued and start them until we reach the max concurrent jobs limit
|
||||||
jobs.Where(job => job.Value.Status.Status == EJobStatus.Queued)
|
jobs.Where(job => job.Value.JobStatus.Status == EJobStatus.Queued)
|
||||||
.Select(job => job.Value)
|
.Select(job => job.Value)
|
||||||
.Take(MaxConcurrentJobs - activeRunningJobs)
|
.Take(MaxConcurrentJobs - activeRunningJobs)
|
||||||
.ForEach(job => {
|
.ForEach(job => {
|
||||||
job.Done += (sender, status) => jobs.Remove(status.Id, out _);
|
job.Done += (sender, status) => {
|
||||||
|
Task.Delay(10000).Wait();
|
||||||
|
jobs.Remove(status.Id, out _);
|
||||||
|
};
|
||||||
job.Start();
|
job.Start();
|
||||||
logger.LogInformation($"Job {job.Id} : {job.Name} has been started.");
|
logger.LogInformation($"Job {job.Id} : {job.Name} has been started.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
|
using Butter.Types;
|
||||||
|
|
||||||
namespace Lactose.Jobs;
|
namespace Lactose.Jobs;
|
||||||
|
|
||||||
public class JobStatus(Job job) {
|
public class JobStatus(Job job) {
|
||||||
public Guid Id => job.Id;
|
public Guid Id => job.Id;
|
||||||
public string Name => job.Name;
|
public string Name => job.Name;
|
||||||
public EJobStatus Status { get; set; } = EJobStatus.Queued;
|
public EJobStatus Status { get; private set; } = EJobStatus.Queued;
|
||||||
public DateTime Created { get; private set; } = DateTime.UtcNow;
|
public DateTime Created { get; private set; } = DateTime.UtcNow;
|
||||||
public DateTime? Started { get; private set; }
|
public DateTime? Started { get; private set; }
|
||||||
public DateTime? Finished { get; private set; }
|
public DateTime? Finished { get; private set; }
|
||||||
public string? Message { get; private set; }
|
public string? Message { get; private set; }
|
||||||
public float Progress { get; private set; } = 0;
|
public float Progress { get; private set; } = 0;
|
||||||
|
|
||||||
public override string ToString() => $"{Name} - {Status} - {Progress * 100}%";
|
public override string ToString() => $"{Name} - {Status} - {Progress*100}%";
|
||||||
|
|
||||||
public void Start(string message = "Job started") {
|
public void Start(string message = "Job started") {
|
||||||
Status = EJobStatus.Running;
|
Status = EJobStatus.Running;
|
||||||
@@ -19,6 +21,11 @@ public class JobStatus(Job job) {
|
|||||||
Message = message;
|
Message = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Wait(string message = "Job waiting") {
|
||||||
|
Status = EJobStatus.Waiting;
|
||||||
|
Message = message;
|
||||||
|
}
|
||||||
|
|
||||||
public void UpdateProgress(float progress, string? message = null) {
|
public void UpdateProgress(float progress, string? message = null) {
|
||||||
if (progress < 0 || progress > 1)
|
if (progress < 0 || progress > 1)
|
||||||
throw new ArgumentOutOfRangeException(nameof(progress), "Progress must be between 0 and 1");
|
throw new ArgumentOutOfRangeException(nameof(progress), "Progress must be between 0 and 1");
|
||||||
|
|||||||
122
Lactose/Jobs/PHashJob.cs
Normal file
122
Lactose/Jobs/PHashJob.cs
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
using Butter.Types;
|
||||||
|
using Lactose.Models;
|
||||||
|
using Lactose.Repositories;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using CoenM.ImageHash.HashAlgorithms;
|
||||||
|
using Lactose.Utils;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using System.Collections;
|
||||||
|
|
||||||
|
namespace Lactose.Jobs;
|
||||||
|
|
||||||
|
public sealed class PHashJob : Job {
|
||||||
|
private readonly PHashJob? ParentJob;
|
||||||
|
private readonly Asset? asset;
|
||||||
|
private readonly IAssetRepository assetRepository;
|
||||||
|
private readonly JobManager? jobManager;
|
||||||
|
private int processedAssets = 0;
|
||||||
|
private int totalAssets = 1; // Avoid division by zero
|
||||||
|
private List<PHashJob> subJobs = [];
|
||||||
|
private ILogger<PHashJob> Logger { get; }
|
||||||
|
public override string Name { get; }
|
||||||
|
|
||||||
|
public PHashJob(ILogger<PHashJob> logger, IAssetRepository assetRepository, JobManager jobManager) : base() {
|
||||||
|
Name = "Image Hashing";
|
||||||
|
Logger = logger;
|
||||||
|
this.assetRepository = assetRepository;
|
||||||
|
this.jobManager = jobManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For the sub jobs
|
||||||
|
public PHashJob(PHashJob parentJob, Asset asset, ILogger<PHashJob> logger, IAssetRepository assetRepository) : base() {
|
||||||
|
ParentJob = parentJob;
|
||||||
|
Name = "Perceptual Hash Calculation";
|
||||||
|
Logger = logger;
|
||||||
|
this.asset = asset;
|
||||||
|
this.assetRepository = assetRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task TaskJob(CancellationToken token) {
|
||||||
|
if (ParentJob == null) {
|
||||||
|
JobStatus.Start();
|
||||||
|
Logger.LogInformation("Starting master PHash job for all assets missing pHash");
|
||||||
|
var MissingPHashAssets = assetRepository.GetAssetsMissingPHash(out totalAssets);
|
||||||
|
Logger.LogInformation($"Found {totalAssets} assets missing pHash.");
|
||||||
|
|
||||||
|
foreach (var missingPHashAsset in MissingPHashAssets) {
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
// Create a sub-job for each asset
|
||||||
|
if (jobManager == null) {
|
||||||
|
Logger.LogError("JobManager is not available. Cannot create sub-jobs.");
|
||||||
|
subJobs.ForEach(j => j.Cancel());
|
||||||
|
JobStatus.Fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var job = jobManager.CreateJob<PHashJob>(this, missingPHashAsset);
|
||||||
|
job.Done += (_, _) => IncrementProcessedAssets(); // Increment processed count when sub-job is done
|
||||||
|
subJobs.Add((PHashJob)job);
|
||||||
|
jobManager.EnqueueJob(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
JobStatus.Wait("Waiting for sub-jobs to complete");
|
||||||
|
|
||||||
|
// Wait for all sub-jobs to complete
|
||||||
|
while (processedAssets < totalAssets) {
|
||||||
|
if (!token.IsCancellationRequested) continue;
|
||||||
|
subJobs.ForEach(j => j.Cancel());
|
||||||
|
JobStatus.Cancel("Cancellation requested from user.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This is a sub-job
|
||||||
|
if (asset == null) {
|
||||||
|
Logger.LogError("Sub-job started without an asset. Job failed.");
|
||||||
|
JobStatus.Fail("Sub-job started without an asset.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JobStatus.Start();
|
||||||
|
Logger.LogDebug($"Calculating pHash for asset {asset.Id} | {asset.OriginalPath}");
|
||||||
|
|
||||||
|
try {
|
||||||
|
PerceptualHash pHash = new();
|
||||||
|
using var image = Image.Load<Rgba32>(asset.OriginalPath);
|
||||||
|
asset.Hash = pHash.Hash(image).ToBitArray();
|
||||||
|
} catch (ArgumentNullException ex) {
|
||||||
|
Logger.LogError(ex, $"Image at path {asset.OriginalPath} could not be found. Setting hash to 0.");
|
||||||
|
asset.Hash = new BitArray(64);
|
||||||
|
JobStatus.Fail(ex.Message);
|
||||||
|
} catch (UnknownImageFormatException ex) {
|
||||||
|
Logger.LogError(
|
||||||
|
ex,
|
||||||
|
$"Image at path {asset.OriginalPath} is in an unknown or unsupported format. Setting hash to 0."
|
||||||
|
);
|
||||||
|
|
||||||
|
asset.Hash = new BitArray(64);
|
||||||
|
JobStatus.Fail(ex.Message);
|
||||||
|
} catch (InvalidImageContentException ex) {
|
||||||
|
Logger.LogError(ex, $"Image at path {asset.OriginalPath} is corrupted or unreadable. Setting hash to 0.");
|
||||||
|
asset.Hash = new BitArray(64);
|
||||||
|
JobStatus.Fail(ex.Message);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.LogError(
|
||||||
|
ex,
|
||||||
|
$"Failed to calculate pHash for asset {asset.Id} | {asset.OriginalPath}. Setting hash to 0."
|
||||||
|
);
|
||||||
|
|
||||||
|
asset.Hash = new BitArray(64);
|
||||||
|
JobStatus.Fail(ex.Message);
|
||||||
|
}
|
||||||
|
assetRepository.Update(asset);
|
||||||
|
assetRepository.Save();
|
||||||
|
var msg = $"Completed pHash calculation for asset {asset.Id} | {asset.OriginalPath}";
|
||||||
|
Logger.LogInformation(msg);
|
||||||
|
JobStatus.Complete(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void IncrementProcessedAssets() {
|
||||||
|
processedAssets++;
|
||||||
|
JobStatus.UpdateProgress((float)processedAssets / totalAssets);
|
||||||
|
}
|
||||||
|
}
|
||||||
200
Lactose/Jobs/ThumbnailJob.cs
Normal file
200
Lactose/Jobs/ThumbnailJob.cs
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
using Butter.Settings;
|
||||||
|
using Lactose.Models;
|
||||||
|
using Lactose.Repositories;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
|
||||||
|
namespace Lactose.Jobs;
|
||||||
|
|
||||||
|
public class ThumbnailJob : Job {
|
||||||
|
ThumbnailJob? ParentJob;
|
||||||
|
List<ThumbnailJob>? subJobs;
|
||||||
|
Asset? Asset;
|
||||||
|
string? ThumbnailPath;
|
||||||
|
int processedAssets = 0;
|
||||||
|
int totalAssets = 1; // Avoid division by zero
|
||||||
|
int ThumbnailSize;
|
||||||
|
ILogger<ThumbnailJob> Logger;
|
||||||
|
ISettingsRepository SettingsRepository;
|
||||||
|
IAssetRepository AssetRepository;
|
||||||
|
JobManager? JobManager;
|
||||||
|
|
||||||
|
public override string Name { get; }
|
||||||
|
|
||||||
|
public ThumbnailJob(
|
||||||
|
ILogger<ThumbnailJob> logger,
|
||||||
|
ISettingsRepository settingsRepository,
|
||||||
|
IAssetRepository assetRepository,
|
||||||
|
JobManager jobManager
|
||||||
|
) {
|
||||||
|
Logger = logger;
|
||||||
|
SettingsRepository = settingsRepository;
|
||||||
|
AssetRepository = assetRepository;
|
||||||
|
JobManager = jobManager;
|
||||||
|
Name = "Thumbnail Generation Job";
|
||||||
|
}
|
||||||
|
|
||||||
|
public ThumbnailJob(
|
||||||
|
ThumbnailJob parentJob,
|
||||||
|
Asset asset,
|
||||||
|
int thumbnailSize,
|
||||||
|
string thumbnailPath,
|
||||||
|
ILogger<ThumbnailJob> logger,
|
||||||
|
ISettingsRepository settingsRepository,
|
||||||
|
IAssetRepository assetRepository
|
||||||
|
) {
|
||||||
|
Logger = logger;
|
||||||
|
SettingsRepository = settingsRepository;
|
||||||
|
AssetRepository = assetRepository;
|
||||||
|
Asset = asset;
|
||||||
|
Name = $"Thumbnail Gen for {Asset?.OriginalPath}";
|
||||||
|
ParentJob = parentJob;
|
||||||
|
ThumbnailSize = thumbnailSize;
|
||||||
|
ThumbnailPath = thumbnailPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task TaskJob(CancellationToken token) {
|
||||||
|
if (ParentJob == null) MasterJob(token);
|
||||||
|
else SlaveJob(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SlaveJob(CancellationToken token) {
|
||||||
|
if (token.IsCancellationRequested) {
|
||||||
|
JobStatus.Cancel("Cancellation requested from user.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Asset == null) {
|
||||||
|
Logger.LogError("Sub-job started without an asset. Canceling job.");
|
||||||
|
JobStatus.Fail("Sub-job started without an asset. Canceling job.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JobStatus.Start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (string.IsNullOrEmpty(Asset.OriginalPath) || !File.Exists(Asset.OriginalPath)) {
|
||||||
|
var msg = $"Original file for asset ID {Asset.Id} not found at path {Asset.OriginalPath}";
|
||||||
|
Logger.LogWarning(msg);
|
||||||
|
JobStatus.Fail(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var image = Image.Load(Asset.OriginalPath);
|
||||||
|
|
||||||
|
image.Mutate(x => x.Resize(
|
||||||
|
new ResizeOptions {
|
||||||
|
Size = new Size(ThumbnailSize),
|
||||||
|
Mode = ResizeMode.Max,
|
||||||
|
Sampler = KnownResamplers.CatmullRom
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ThumbnailPath == null) {
|
||||||
|
var msg = "Thumbnail path is not set. Cannot save thumbnail.";
|
||||||
|
Logger.LogError(msg);
|
||||||
|
JobStatus.Fail(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = PathFromGuid(Asset.Id, ThumbnailPath);
|
||||||
|
|
||||||
|
if (!Directory.Exists(path)) Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||||
|
|
||||||
|
image.SaveAsJpeg(
|
||||||
|
path,
|
||||||
|
new JpegEncoder() {
|
||||||
|
ColorType = JpegEncodingColor.Rgb,
|
||||||
|
Quality = 75,
|
||||||
|
SkipMetadata = true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Asset.ThumbnailPath = path;
|
||||||
|
AssetRepository.Update(Asset);
|
||||||
|
AssetRepository.Save();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.LogError(ex, $"Failed to generate thumbnail for asset ID {Asset.Id}");
|
||||||
|
JobStatus.Fail($"Error: {ex.Message}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogInformation($"Thumbnail generated for asset ID {Asset.Id} at {Asset.ThumbnailPath}");
|
||||||
|
JobStatus.Complete($"Thumbnail generated for asset ID {Asset.Id} at {Asset.ThumbnailPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
void MasterJob(CancellationToken token) {
|
||||||
|
if (JobManager == null) {
|
||||||
|
Logger.LogError("JobManager is not available. Cannot create sub-jobs.");
|
||||||
|
JobStatus.Fail("JobManager is not available. Cannot create sub-jobs.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JobStatus.Start();
|
||||||
|
Logger.LogInformation("Starting master Thumbnail job for all assets.");
|
||||||
|
var thumbPath = SettingsRepository.Get(Settings.ThumbnailPath.AsString());
|
||||||
|
//TODO: Add thumbnail size setting to the system
|
||||||
|
|
||||||
|
if (thumbPath == null) {
|
||||||
|
Logger.LogError("Thumbnail path not found. Cannot proceed with thumbnail generation.");
|
||||||
|
JobStatus.Fail("Thumbnail path not found. Cannot proceed with thumbnail generation.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var missingThumbnail = AssetRepository.GetAssetsMissingThumbnail(out totalAssets);
|
||||||
|
Logger.LogInformation($"Found {totalAssets} assets missing thumbnails.");
|
||||||
|
|
||||||
|
subJobs = new List<ThumbnailJob>();
|
||||||
|
|
||||||
|
foreach (var asset in missingThumbnail) {
|
||||||
|
if (token.IsCancellationRequested) {
|
||||||
|
lock (subJobs) subJobs.ForEach(j => j.Cancel());
|
||||||
|
JobStatus.Cancel("Cancellation requested from user.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a sub-job for each asset
|
||||||
|
var job = JobManager.CreateJob<ThumbnailJob>(this, asset, 256, thumbPath.Value!);
|
||||||
|
|
||||||
|
job.Done += (o, _) => {
|
||||||
|
IncrementProcessedAssets();
|
||||||
|
lock (subJobs) subJobs.Remove((o as ThumbnailJob)!);
|
||||||
|
};
|
||||||
|
|
||||||
|
lock (subJobs) subJobs.Add(job);
|
||||||
|
JobManager.EnqueueJob(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
JobStatus.Wait();
|
||||||
|
|
||||||
|
// Wait for all sub-jobs to complete
|
||||||
|
while (subJobs.Count > 0) {
|
||||||
|
if (token.IsCancellationRequested) {
|
||||||
|
lock (subJobs) subJobs.ForEach(j => j.Cancel());
|
||||||
|
JobStatus.Cancel("User requested cancellation.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JobStatus.Complete("All thumbnails generated successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
void IncrementProcessedAssets() {
|
||||||
|
processedAssets++;
|
||||||
|
JobStatus.UpdateProgress((float)processedAssets / totalAssets, $"Waiting for {subJobs.Count} sub-jobs to complete.");
|
||||||
|
}
|
||||||
|
|
||||||
|
static string PathFromGuid(Guid id, string root) {
|
||||||
|
var s = id.ToString("N"); // 32 chars, no dashes
|
||||||
|
|
||||||
|
return Path.Combine(
|
||||||
|
root,
|
||||||
|
s[..2], // 3F
|
||||||
|
s.Substring(2, 2), // 25
|
||||||
|
s.Substring(4, 2), // 04
|
||||||
|
$"{s}.jpg"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CoenM.ImageSharp.ImageHash" Version="1.3.6" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.15" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.15" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.15" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.15" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.15" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.15" />
|
||||||
@@ -22,6 +23,8 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" />
|
||||||
|
<PackageReference Include="Pgvector.EntityFrameworkCore" Version="0.2.2" />
|
||||||
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
25
Lactose/Mapper/JobMapper.cs
Normal file
25
Lactose/Mapper/JobMapper.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using Butter.Dtos.Jobs;
|
||||||
|
using Lactose.Jobs;
|
||||||
|
|
||||||
|
namespace Lactose.Mapper;
|
||||||
|
|
||||||
|
public static class JobMapper {
|
||||||
|
public static JobStatusDto ToJobStatusDto(this Job job) => new JobStatusDto() {
|
||||||
|
Id = job.Id,
|
||||||
|
Name = job.Name,
|
||||||
|
Status = job.JobStatus.Status,
|
||||||
|
Progress = job.JobStatus.Progress,
|
||||||
|
Message = job.JobStatus.Message,
|
||||||
|
Created = job.JobStatus.Created,
|
||||||
|
Started = job.JobStatus.Started,
|
||||||
|
Finished = job.JobStatus.Finished,
|
||||||
|
|
||||||
|
JobType = job switch {
|
||||||
|
FileSystemCrawlJob => EJobType.FileSystemScan,
|
||||||
|
ThumbnailJob => EJobType.ThumbnailGeneration,
|
||||||
|
IntegrityCheckJob => EJobType.IntegrityCheck,
|
||||||
|
PHashJob => EJobType.PHashGeneration,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(job))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -17,7 +17,8 @@ public static class SettingsMapper {
|
|||||||
Value = setting.Value,
|
Value = setting.Value,
|
||||||
Description = setting.Description,
|
Description = setting.Description,
|
||||||
Type = setting.Type,
|
Type = setting.Type,
|
||||||
DisplayType = setting.DisplayType
|
DisplayType = setting.DisplayType,
|
||||||
|
Options = setting.Options
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -29,6 +30,7 @@ public static class SettingsMapper {
|
|||||||
Value = settingDto.Value,
|
Value = settingDto.Value,
|
||||||
Description = settingDto.Description,
|
Description = settingDto.Description,
|
||||||
Type = settingDto.Type,
|
Type = settingDto.Type,
|
||||||
DisplayType = settingDto.DisplayType
|
DisplayType = settingDto.DisplayType,
|
||||||
|
Options = settingDto.Options
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
465
Lactose/Migrations/20250903205921_ULongHash.Designer.cs
generated
Normal file
465
Lactose/Migrations/20250903205921_ULongHash.Designer.cs
generated
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Lactose.Context;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Lactose.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(LactoseDbContext))]
|
||||||
|
[Migration("20250903205921_ULongHash")]
|
||||||
|
partial class ULongHash
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.15")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("AlbumAsset", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("AlbumsId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("AssetsId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("AlbumsId", "AssetsId");
|
||||||
|
|
||||||
|
b.HasIndex("AssetsId");
|
||||||
|
|
||||||
|
b.ToTable("AlbumAsset");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AssetTag", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("AssetsId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("TagsId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("AssetsId", "TagsId");
|
||||||
|
|
||||||
|
b.HasIndex("TagsId");
|
||||||
|
|
||||||
|
b.ToTable("AssetTag");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("CreatedAt")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("PersonOwnerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(255)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UserOwnerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("PersonOwnerId");
|
||||||
|
|
||||||
|
b.HasIndex("UserOwnerId");
|
||||||
|
|
||||||
|
b.ToTable("Albums");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<float?>("Duration")
|
||||||
|
.HasColumnType("real");
|
||||||
|
|
||||||
|
b.Property<long>("FileSize")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<Guid?>("FolderId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<float?>("FrameRate")
|
||||||
|
.HasColumnType("real");
|
||||||
|
|
||||||
|
b.Property<decimal>("Hash")
|
||||||
|
.HasColumnType("numeric(20,0)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsPubliclyShared")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("MimeType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(255)");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalFilename")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(255)");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalPath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(2048)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("OwnerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("PreviewPath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(2048)");
|
||||||
|
|
||||||
|
b.Property<int>("ResolutionHeight")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("ResolutionWidth")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("ThumbnailPath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(2048)");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("FolderId");
|
||||||
|
|
||||||
|
b.HasIndex("OriginalPath")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("OwnerId");
|
||||||
|
|
||||||
|
b.ToTable("Assets");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AssetId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("BoundingBoxX1")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("BoundingBoxX2")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("BoundingBoxY1")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("BoundingBoxY2")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("ImageHeight")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("ImageWidth")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<Guid?>("PersonId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AssetId");
|
||||||
|
|
||||||
|
b.HasIndex("PersonId");
|
||||||
|
|
||||||
|
b.ToTable("Faces");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("Active")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("BasePath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(2048)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Folders");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(255)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("People");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Setting", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<int>("DisplayType")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)");
|
||||||
|
|
||||||
|
b.HasKey("Name");
|
||||||
|
|
||||||
|
b.ToTable("Settings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(255)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("ParentId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ParentId");
|
||||||
|
|
||||||
|
b.ToTable("Tags");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("AccessLevel")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AssetId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("BannedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(128)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastLogin")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Password")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(255)");
|
||||||
|
|
||||||
|
b.Property<string>("RefreshToken")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(255)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("RefreshTokenExpires")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(64)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AssetId");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AlbumAsset", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Lactose.Models.Album", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AlbumsId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Lactose.Models.Asset", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AssetsId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AssetTag", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Lactose.Models.Asset", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AssetsId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Lactose.Models.Tag", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TagsId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Lactose.Models.Person", "PersonOwner")
|
||||||
|
.WithMany("Albums")
|
||||||
|
.HasForeignKey("PersonOwnerId");
|
||||||
|
|
||||||
|
b.HasOne("Lactose.Models.User", "UserOwner")
|
||||||
|
.WithMany("OwnedAlbums")
|
||||||
|
.HasForeignKey("UserOwnerId");
|
||||||
|
|
||||||
|
b.Navigation("PersonOwner");
|
||||||
|
|
||||||
|
b.Navigation("UserOwner");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Lactose.Models.Folder", "Folder")
|
||||||
|
.WithMany("Assets")
|
||||||
|
.HasForeignKey("FolderId");
|
||||||
|
|
||||||
|
b.HasOne("Lactose.Models.User", "Owner")
|
||||||
|
.WithMany("OwnedAssets")
|
||||||
|
.HasForeignKey("OwnerId");
|
||||||
|
|
||||||
|
b.Navigation("Folder");
|
||||||
|
|
||||||
|
b.Navigation("Owner");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Lactose.Models.Asset", "Asset")
|
||||||
|
.WithMany("Faces")
|
||||||
|
.HasForeignKey("AssetId");
|
||||||
|
|
||||||
|
b.HasOne("Lactose.Models.Person", "Person")
|
||||||
|
.WithMany("Faces")
|
||||||
|
.HasForeignKey("PersonId");
|
||||||
|
|
||||||
|
b.Navigation("Asset");
|
||||||
|
|
||||||
|
b.Navigation("Person");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Lactose.Models.Tag", "Parent")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ParentId");
|
||||||
|
|
||||||
|
b.Navigation("Parent");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Lactose.Models.Asset", null)
|
||||||
|
.WithMany("SharedWith")
|
||||||
|
.HasForeignKey("AssetId");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Faces");
|
||||||
|
|
||||||
|
b.Navigation("SharedWith");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Assets");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Albums");
|
||||||
|
|
||||||
|
b.Navigation("Faces");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("OwnedAlbums");
|
||||||
|
|
||||||
|
b.Navigation("OwnedAssets");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
Lactose/Migrations/20250903205921_ULongHash.cs
Normal file
36
Lactose/Migrations/20250903205921_ULongHash.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Lactose.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class ULongHash : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Hash",
|
||||||
|
table: "Assets");
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "Hash",
|
||||||
|
table: "Assets",
|
||||||
|
type: "numeric(20,0)",
|
||||||
|
nullable: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Hash",
|
||||||
|
table: "Assets");
|
||||||
|
migrationBuilder.AddColumn<byte[]>(
|
||||||
|
name: "Hash",
|
||||||
|
table: "Assets",
|
||||||
|
type: "BYTEA",
|
||||||
|
nullable: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
468
Lactose/Migrations/20250904003848_VectorExtensions.Designer.cs
generated
Normal file
468
Lactose/Migrations/20250904003848_VectorExtensions.Designer.cs
generated
Normal file
@@ -0,0 +1,468 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using Lactose.Context;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Lactose.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(LactoseDbContext))]
|
||||||
|
[Migration("20250904003848_VectorExtensions")]
|
||||||
|
partial class VectorExtensions
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.15")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector");
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("AlbumAsset", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("AlbumsId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("AssetsId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("AlbumsId", "AssetsId");
|
||||||
|
|
||||||
|
b.HasIndex("AssetsId");
|
||||||
|
|
||||||
|
b.ToTable("AlbumAsset");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AssetTag", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("AssetsId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("TagsId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("AssetsId", "TagsId");
|
||||||
|
|
||||||
|
b.HasIndex("TagsId");
|
||||||
|
|
||||||
|
b.ToTable("AssetTag");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("CreatedAt")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("PersonOwnerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(255)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UserOwnerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("PersonOwnerId");
|
||||||
|
|
||||||
|
b.HasIndex("UserOwnerId");
|
||||||
|
|
||||||
|
b.ToTable("Albums");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<float?>("Duration")
|
||||||
|
.HasColumnType("real");
|
||||||
|
|
||||||
|
b.Property<long>("FileSize")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<Guid?>("FolderId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<float?>("FrameRate")
|
||||||
|
.HasColumnType("real");
|
||||||
|
|
||||||
|
b.Property<BitArray>("Hash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("bit(64)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsPubliclyShared")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("MimeType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(255)");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalFilename")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(255)");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalPath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(2048)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("OwnerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("PreviewPath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(2048)");
|
||||||
|
|
||||||
|
b.Property<int>("ResolutionHeight")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("ResolutionWidth")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("ThumbnailPath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(2048)");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("FolderId");
|
||||||
|
|
||||||
|
b.HasIndex("OriginalPath")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("OwnerId");
|
||||||
|
|
||||||
|
b.ToTable("Assets");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AssetId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("BoundingBoxX1")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("BoundingBoxX2")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("BoundingBoxY1")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("BoundingBoxY2")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("ImageHeight")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("ImageWidth")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<Guid?>("PersonId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AssetId");
|
||||||
|
|
||||||
|
b.HasIndex("PersonId");
|
||||||
|
|
||||||
|
b.ToTable("Faces");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("Active")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("BasePath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(2048)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Folders");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(255)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("People");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Setting", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<int>("DisplayType")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)");
|
||||||
|
|
||||||
|
b.HasKey("Name");
|
||||||
|
|
||||||
|
b.ToTable("Settings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(255)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("ParentId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ParentId");
|
||||||
|
|
||||||
|
b.ToTable("Tags");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("AccessLevel")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AssetId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("BannedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(128)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastLogin")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Password")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(255)");
|
||||||
|
|
||||||
|
b.Property<string>("RefreshToken")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(255)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("RefreshTokenExpires")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(64)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AssetId");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AlbumAsset", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Lactose.Models.Album", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AlbumsId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Lactose.Models.Asset", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AssetsId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AssetTag", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Lactose.Models.Asset", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AssetsId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Lactose.Models.Tag", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TagsId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Lactose.Models.Person", "PersonOwner")
|
||||||
|
.WithMany("Albums")
|
||||||
|
.HasForeignKey("PersonOwnerId");
|
||||||
|
|
||||||
|
b.HasOne("Lactose.Models.User", "UserOwner")
|
||||||
|
.WithMany("OwnedAlbums")
|
||||||
|
.HasForeignKey("UserOwnerId");
|
||||||
|
|
||||||
|
b.Navigation("PersonOwner");
|
||||||
|
|
||||||
|
b.Navigation("UserOwner");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Lactose.Models.Folder", "Folder")
|
||||||
|
.WithMany("Assets")
|
||||||
|
.HasForeignKey("FolderId");
|
||||||
|
|
||||||
|
b.HasOne("Lactose.Models.User", "Owner")
|
||||||
|
.WithMany("OwnedAssets")
|
||||||
|
.HasForeignKey("OwnerId");
|
||||||
|
|
||||||
|
b.Navigation("Folder");
|
||||||
|
|
||||||
|
b.Navigation("Owner");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Lactose.Models.Asset", "Asset")
|
||||||
|
.WithMany("Faces")
|
||||||
|
.HasForeignKey("AssetId");
|
||||||
|
|
||||||
|
b.HasOne("Lactose.Models.Person", "Person")
|
||||||
|
.WithMany("Faces")
|
||||||
|
.HasForeignKey("PersonId");
|
||||||
|
|
||||||
|
b.Navigation("Asset");
|
||||||
|
|
||||||
|
b.Navigation("Person");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Lactose.Models.Tag", "Parent")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ParentId");
|
||||||
|
|
||||||
|
b.Navigation("Parent");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Lactose.Models.Asset", null)
|
||||||
|
.WithMany("SharedWith")
|
||||||
|
.HasForeignKey("AssetId");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Faces");
|
||||||
|
|
||||||
|
b.Navigation("SharedWith");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Assets");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Albums");
|
||||||
|
|
||||||
|
b.Navigation("Faces");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("OwnedAlbums");
|
||||||
|
|
||||||
|
b.Navigation("OwnedAssets");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
Lactose/Migrations/20250904003848_VectorExtensions.cs
Normal file
43
Lactose/Migrations/20250904003848_VectorExtensions.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Lactose.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class VectorExtensions : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterDatabase()
|
||||||
|
.Annotation("Npgsql:PostgresExtension:vector", ",,");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Hash",
|
||||||
|
table: "Assets");
|
||||||
|
migrationBuilder.AddColumn<BitArray>(
|
||||||
|
name: "Hash",
|
||||||
|
table: "Assets",
|
||||||
|
type: "bit(64)",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: new BitArray(64));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterDatabase()
|
||||||
|
.OldAnnotation("Npgsql:PostgresExtension:vector", ",,");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn("Hash", "Assets");
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "Hash",
|
||||||
|
table: "Assets",
|
||||||
|
type: "numeric(20,0)",
|
||||||
|
nullable: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
471
Lactose/Migrations/20250905024004_AddedSettingsOptions.Designer.cs
generated
Normal file
471
Lactose/Migrations/20250905024004_AddedSettingsOptions.Designer.cs
generated
Normal file
@@ -0,0 +1,471 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using Lactose.Context;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Lactose.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(LactoseDbContext))]
|
||||||
|
[Migration("20250905024004_AddedSettingsOptions")]
|
||||||
|
partial class AddedSettingsOptions
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.15")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vectors");
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("AlbumAsset", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("AlbumsId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("AssetsId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("AlbumsId", "AssetsId");
|
||||||
|
|
||||||
|
b.HasIndex("AssetsId");
|
||||||
|
|
||||||
|
b.ToTable("AlbumAsset");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AssetTag", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("AssetsId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("TagsId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("AssetsId", "TagsId");
|
||||||
|
|
||||||
|
b.HasIndex("TagsId");
|
||||||
|
|
||||||
|
b.ToTable("AssetTag");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("CreatedAt")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("PersonOwnerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(255)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UserOwnerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("PersonOwnerId");
|
||||||
|
|
||||||
|
b.HasIndex("UserOwnerId");
|
||||||
|
|
||||||
|
b.ToTable("Albums");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<float?>("Duration")
|
||||||
|
.HasColumnType("real");
|
||||||
|
|
||||||
|
b.Property<long>("FileSize")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<Guid?>("FolderId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<float?>("FrameRate")
|
||||||
|
.HasColumnType("real");
|
||||||
|
|
||||||
|
b.Property<BitArray>("Hash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("bit(64)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsPubliclyShared")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("MimeType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(255)");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalFilename")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(255)");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalPath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(2048)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("OwnerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("PreviewPath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(2048)");
|
||||||
|
|
||||||
|
b.Property<int>("ResolutionHeight")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("ResolutionWidth")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("ThumbnailPath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(2048)");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("FolderId");
|
||||||
|
|
||||||
|
b.HasIndex("OriginalPath")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("OwnerId");
|
||||||
|
|
||||||
|
b.ToTable("Assets");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AssetId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("BoundingBoxX1")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("BoundingBoxX2")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("BoundingBoxY1")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("BoundingBoxY2")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("ImageHeight")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("ImageWidth")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<Guid?>("PersonId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AssetId");
|
||||||
|
|
||||||
|
b.HasIndex("PersonId");
|
||||||
|
|
||||||
|
b.ToTable("Faces");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("Active")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("BasePath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(2048)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Folders");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(255)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("People");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Setting", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<int>("DisplayType")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string[]>("Options")
|
||||||
|
.HasColumnType("text[]");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)");
|
||||||
|
|
||||||
|
b.HasKey("Name");
|
||||||
|
|
||||||
|
b.ToTable("Settings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(255)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("ParentId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ParentId");
|
||||||
|
|
||||||
|
b.ToTable("Tags");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("AccessLevel")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AssetId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("BannedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(128)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastLogin")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Password")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(255)");
|
||||||
|
|
||||||
|
b.Property<string>("RefreshToken")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(255)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("RefreshTokenExpires")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("VARCHAR(64)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AssetId");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AlbumAsset", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Lactose.Models.Album", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AlbumsId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Lactose.Models.Asset", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AssetsId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("AssetTag", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Lactose.Models.Asset", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AssetsId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Lactose.Models.Tag", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TagsId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Lactose.Models.Person", "PersonOwner")
|
||||||
|
.WithMany("Albums")
|
||||||
|
.HasForeignKey("PersonOwnerId");
|
||||||
|
|
||||||
|
b.HasOne("Lactose.Models.User", "UserOwner")
|
||||||
|
.WithMany("OwnedAlbums")
|
||||||
|
.HasForeignKey("UserOwnerId");
|
||||||
|
|
||||||
|
b.Navigation("PersonOwner");
|
||||||
|
|
||||||
|
b.Navigation("UserOwner");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Lactose.Models.Folder", "Folder")
|
||||||
|
.WithMany("Assets")
|
||||||
|
.HasForeignKey("FolderId");
|
||||||
|
|
||||||
|
b.HasOne("Lactose.Models.User", "Owner")
|
||||||
|
.WithMany("OwnedAssets")
|
||||||
|
.HasForeignKey("OwnerId");
|
||||||
|
|
||||||
|
b.Navigation("Folder");
|
||||||
|
|
||||||
|
b.Navigation("Owner");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Lactose.Models.Asset", "Asset")
|
||||||
|
.WithMany("Faces")
|
||||||
|
.HasForeignKey("AssetId");
|
||||||
|
|
||||||
|
b.HasOne("Lactose.Models.Person", "Person")
|
||||||
|
.WithMany("Faces")
|
||||||
|
.HasForeignKey("PersonId");
|
||||||
|
|
||||||
|
b.Navigation("Asset");
|
||||||
|
|
||||||
|
b.Navigation("Person");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Lactose.Models.Tag", "Parent")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ParentId");
|
||||||
|
|
||||||
|
b.Navigation("Parent");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Lactose.Models.Asset", null)
|
||||||
|
.WithMany("SharedWith")
|
||||||
|
.HasForeignKey("AssetId");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Faces");
|
||||||
|
|
||||||
|
b.Navigation("SharedWith");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Assets");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Albums");
|
||||||
|
|
||||||
|
b.Navigation("Faces");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("OwnedAlbums");
|
||||||
|
|
||||||
|
b.Navigation("OwnedAssets");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
Lactose/Migrations/20250905024004_AddedSettingsOptions.cs
Normal file
29
Lactose/Migrations/20250905024004_AddedSettingsOptions.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Lactose.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddedSettingsOptions : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string[]>(
|
||||||
|
name: "Options",
|
||||||
|
table: "Settings",
|
||||||
|
type: "text[]",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Options",
|
||||||
|
table: "Settings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using Lactose.Context;
|
using Lactose.Context;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -20,6 +21,7 @@ namespace Lactose.Migrations
|
|||||||
.HasAnnotation("ProductVersion", "8.0.15")
|
.HasAnnotation("ProductVersion", "8.0.15")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vectors");
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
modelBuilder.Entity("AlbumAsset", b =>
|
modelBuilder.Entity("AlbumAsset", b =>
|
||||||
@@ -108,9 +110,9 @@ namespace Lactose.Migrations
|
|||||||
b.Property<float?>("FrameRate")
|
b.Property<float?>("FrameRate")
|
||||||
.HasColumnType("real");
|
.HasColumnType("real");
|
||||||
|
|
||||||
b.Property<byte[]>("Hash")
|
b.Property<BitArray>("Hash")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("BYTEA");
|
.HasColumnType("bit(64)");
|
||||||
|
|
||||||
b.Property<bool>("IsPubliclyShared")
|
b.Property<bool>("IsPubliclyShared")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
@@ -256,6 +258,9 @@ namespace Lactose.Migrations
|
|||||||
b.Property<int>("DisplayType")
|
b.Property<int>("DisplayType")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string[]>("Options")
|
||||||
|
.HasColumnType("text[]");
|
||||||
|
|
||||||
b.Property<int>("Type")
|
b.Property<int>("Type")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Butter.Types;
|
using Butter.Types;
|
||||||
|
using System.Collections;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
@@ -108,8 +109,8 @@ public class Asset {
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Computed hash of the asset.
|
/// Computed hash of the asset.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Required][Column(TypeName = "BYTEA")]
|
[Required][Column(TypeName = "bit(64)")]
|
||||||
public byte[] Hash { get; set; } = [];
|
public BitArray Hash { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or Sets which folder the asset is in.
|
/// Gets or Sets which folder the asset is in.
|
||||||
|
|||||||
@@ -16,4 +16,6 @@ public class Setting {
|
|||||||
public required EType Type { get; set; }
|
public required EType Type { get; set; }
|
||||||
|
|
||||||
public required DisplayType DisplayType { get; set; }
|
public required DisplayType DisplayType { get; set; }
|
||||||
|
|
||||||
|
public string[]? Options { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Identity;
|
|||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
|
using Pgvector.EntityFrameworkCore;
|
||||||
|
|
||||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -48,7 +49,7 @@ builder.Services.AddDbContext<LactoseDbContext>(
|
|||||||
#endif
|
#endif
|
||||||
;*/
|
;*/
|
||||||
|
|
||||||
options.UseNpgsql(strBuilder.ConnectionString)
|
options.UseNpgsql(strBuilder.ConnectionString, o => o.UseVector())
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
.LogTo(Console.WriteLine)
|
.LogTo(Console.WriteLine)
|
||||||
.EnableSensitiveDataLogging()
|
.EnableSensitiveDataLogging()
|
||||||
@@ -76,6 +77,7 @@ builder.Services.AddTransient<IMediaRepository, MediaRepository>();
|
|||||||
builder.Services.AddTransient<ITokenService, TokenService>();
|
builder.Services.AddTransient<ITokenService, TokenService>();
|
||||||
builder.Services.AddTransient<LactoseAuthService>();
|
builder.Services.AddTransient<LactoseAuthService>();
|
||||||
builder.Services.AddSingleton<JobManager>();
|
builder.Services.AddSingleton<JobManager>();
|
||||||
|
builder.Services.AddSingleton<JobScheduler>();
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
|
|
||||||
@@ -153,7 +155,7 @@ builder.Services
|
|||||||
|
|
||||||
//Add the job manager service
|
//Add the job manager service
|
||||||
builder.Services.AddHostedService<JobManager>(p => p.GetRequiredService<JobManager>());
|
builder.Services.AddHostedService<JobManager>(p => p.GetRequiredService<JobManager>());
|
||||||
builder.Services.AddHostedService<JobScheduler>();
|
builder.Services.AddHostedService<JobScheduler>(p => p.GetRequiredService<JobScheduler>());
|
||||||
WebApplication app = builder.Build();
|
WebApplication app = builder.Build();
|
||||||
|
|
||||||
using (var scope = app.Services.CreateScope()) {
|
using (var scope = app.Services.CreateScope()) {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using Lactose.Context;
|
using Lactose.Context;
|
||||||
using Lactose.Models;
|
using Lactose.Models;
|
||||||
|
using Pgvector.EntityFrameworkCore;
|
||||||
|
using System.Collections;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
|
|
||||||
namespace Lactose.Repositories;
|
namespace Lactose.Repositories;
|
||||||
@@ -43,5 +45,33 @@ public class AssetRepository(LactoseDbContext context) : IAssetRepository {
|
|||||||
|
|
||||||
public Asset? FindByPath(string path) => context.Assets.FirstOrDefault(a => a.OriginalPath == path);
|
public Asset? FindByPath(string path) => context.Assets.FirstOrDefault(a => a.OriginalPath == path);
|
||||||
|
|
||||||
|
public IEnumerable<Asset> GetAssetsMissingPHash(out int count) {
|
||||||
|
var empty = new BitArray(64);
|
||||||
|
count = context.Assets.Count(a => a.Hash == empty);
|
||||||
|
return context.Assets.Where(a => a.Hash == empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<List<Asset>> GetDuplicates() => context.Assets
|
||||||
|
.GroupBy(a => new { a.Hash })
|
||||||
|
.Where(g => g.Count() > 1)
|
||||||
|
.Select(g => g.ToList());
|
||||||
|
|
||||||
|
public IEnumerable<Asset> GetWithinHammingDistance(ulong phash, int distance) {
|
||||||
|
if(distance < 0 || distance > 63)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(distance), "Distance must be between 0 and 64.");
|
||||||
|
|
||||||
|
return context.Assets
|
||||||
|
.OrderBy(a => a.HammingDistance(phash))
|
||||||
|
.Where(a => a.Hash.HammingDistance(phash) <= distance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<Asset> GetWithinHammingDistance(ulong phash, float distance)
|
||||||
|
=> GetWithinHammingDistance(phash, (int)(distance * 64));
|
||||||
|
|
||||||
|
public IEnumerable<Asset> GetAssetsMissingThumbnail(out int count) {
|
||||||
|
count = context.Assets.Count(a => a.ThumbnailPath == null || a.ThumbnailPath == "");
|
||||||
|
return context.Assets.Where(a => a.ThumbnailPath == null || a.ThumbnailPath == "");
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose() => context.Dispose();
|
public void Dispose() => context.Dispose();
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Lactose.Models;
|
using Lactose.Models;
|
||||||
|
using System.Collections;
|
||||||
|
|
||||||
namespace Lactose.Repositories;
|
namespace Lactose.Repositories;
|
||||||
|
|
||||||
@@ -71,4 +72,13 @@ public interface IAssetRepository : IDisposable {
|
|||||||
/// <param name="filePath">The file path of the asset.</param>
|
/// <param name="filePath">The file path of the asset.</param>
|
||||||
/// <returns>Null if not found, otherwise the requested asset.</returns>
|
/// <returns>Null if not found, otherwise the requested asset.</returns>
|
||||||
Asset? FindByPath(string filePath);
|
Asset? FindByPath(string filePath);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds all assets that are missing a perceptual hash (pHash).
|
||||||
|
/// </summary>
|
||||||
|
IEnumerable<Asset> GetAssetsMissingPHash(out int count);
|
||||||
|
|
||||||
|
public IEnumerable<Asset> GetWithinHammingDistance(ulong phash, float distance);
|
||||||
|
|
||||||
|
IEnumerable<Asset> GetAssetsMissingThumbnail(out int totalAssets);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ interface IDbInitializer {
|
|||||||
void Initialize();
|
void Initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
class DbInitializer(LactoseDbContext dbContext, IPasswordHasher<User> passwordHasher) : IDbInitializer {
|
internal class DbInitializer(LactoseDbContext dbContext, IPasswordHasher<User> passwordHasher) : IDbInitializer {
|
||||||
public void Initialize() {
|
public void Initialize() {
|
||||||
ArgumentNullException.ThrowIfNull(dbContext, nameof(dbContext));
|
ArgumentNullException.ThrowIfNull(dbContext, nameof(dbContext));
|
||||||
|
|
||||||
@@ -52,9 +52,21 @@ class DbInitializer(LactoseDbContext dbContext, IPasswordHasher<User> passwordHa
|
|||||||
if (settings is not null) {
|
if (settings is not null) {
|
||||||
//remove all settings that are not in the default settings file
|
//remove all settings that are not in the default settings file
|
||||||
var existingSettings = dbContext.Settings.ToList();
|
var existingSettings = dbContext.Settings.ToList();
|
||||||
foreach (Setting existingSetting in existingSettings
|
|
||||||
.Where(existingSetting => settings.All(s => s.Name != existingSetting.Name)))
|
foreach (Setting existingSetting in existingSettings.Where(existingSetting
|
||||||
dbContext.Settings.Remove(existingSetting);
|
=> settings.All(s => s.Name != existingSetting.Name)
|
||||||
|
)) dbContext.Settings.Remove(existingSetting);
|
||||||
|
|
||||||
|
settings.First(s => s.Name == Settings.ThumbnailPath.AsString()).Value
|
||||||
|
= Path.Combine(Environment.CurrentDirectory, "thumbnails");
|
||||||
|
|
||||||
|
settings.First(s => s.Name == Settings.PreviewPath.AsString()).Value
|
||||||
|
= Path.Combine(Environment.CurrentDirectory, "previews");
|
||||||
|
|
||||||
|
// set max concurrent jobs to number of processors - 2, at least one. (who the fuck is using a single core cpu?)
|
||||||
|
settings.First(s => s.Name == Settings.MaxConcurrentJobs.AsString()).Value
|
||||||
|
= (Math.Max(Environment.ProcessorCount - 2, 1)).ToString();
|
||||||
|
|
||||||
// add or update settings from the default settings file leaving value unchanged
|
// add or update settings from the default settings file leaving value unchanged
|
||||||
foreach (var setting in settings) {
|
foreach (var setting in settings) {
|
||||||
if (!dbContext.Settings.Any(s => s.Name == setting.Name)) { dbContext.Settings.Add(setting); } else {
|
if (!dbContext.Settings.Any(s => s.Name == setting.Name)) { dbContext.Settings.Add(setting); } else {
|
||||||
@@ -62,9 +74,11 @@ class DbInitializer(LactoseDbContext dbContext, IPasswordHasher<User> passwordHa
|
|||||||
dbSetting.Description = setting.Description;
|
dbSetting.Description = setting.Description;
|
||||||
dbSetting.Type = setting.Type;
|
dbSetting.Type = setting.Type;
|
||||||
dbSetting.DisplayType = setting.DisplayType;
|
dbSetting.DisplayType = setting.DisplayType;
|
||||||
|
dbSetting.Options = setting.Options;
|
||||||
dbContext.Settings.Update(dbSetting);
|
dbContext.Settings.Update(dbSetting);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dbContext.SaveChanges();
|
dbContext.SaveChanges();
|
||||||
} //else, we are a bit fucked.
|
} //else, we are a bit fucked.
|
||||||
|
|
||||||
@@ -84,5 +98,6 @@ class DbInitializer(LactoseDbContext dbContext, IPasswordHasher<User> passwordHa
|
|||||||
dbContext.SaveChanges();
|
dbContext.SaveChanges();
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Butter.Settings;
|
using Butter.Settings;
|
||||||
|
using Butter.Types;
|
||||||
using Lactose.Jobs;
|
using Lactose.Jobs;
|
||||||
using Lactose.Models;
|
using Lactose.Models;
|
||||||
using Lactose.Repositories;
|
using Lactose.Repositories;
|
||||||
@@ -31,14 +32,6 @@ public class JobScheduler(IServiceProvider serviceProvider, ILogger<JobScheduler
|
|||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
|
|
||||||
void QueueFileSystemCrawl(object? state) {
|
|
||||||
// don't queue if there's already a crawl job in progress
|
|
||||||
if(jobManager.Jobs.Any(j => j is FileSystemCrawlJob)) return;
|
|
||||||
// queue a crawl job for each active folder
|
|
||||||
folders?.ForEach(f => jobManager.EnqueueJob(jobManager.CreateJob<FileSystemCrawlJob>(f.BasePath)));
|
|
||||||
logger?.LogInformation("Queued file system crawl jobs.");
|
|
||||||
}
|
|
||||||
|
|
||||||
void FetchSettings() {
|
void FetchSettings() {
|
||||||
logger?.LogInformation("Fetching settings...");
|
logger?.LogInformation("Fetching settings...");
|
||||||
var scope = serviceProvider.CreateScope();
|
var scope = serviceProvider.CreateScope();
|
||||||
@@ -89,4 +82,69 @@ public class JobScheduler(IServiceProvider serviceProvider, ILogger<JobScheduler
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void QueueFileSystemCrawl(object? state) {
|
||||||
|
if (jobManager == null) {
|
||||||
|
logger?.LogWarning("JobManager is not available, cannot queue file system crawl.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// don't queue if there's already a crawl job in progress
|
||||||
|
if (jobManager.Jobs.Any(j => j is FileSystemCrawlJob && j.JobStatus.Status is EJobStatus.Running or EJobStatus.Queued)) {
|
||||||
|
logger?.LogWarning("A file system crawl job is already in progress, cannot queue another at this moment.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// queue a crawl job for each active folder
|
||||||
|
folders?.ForEach(f => { jobManager.EnqueueJob(jobManager.CreateJob<FileSystemCrawlJob>(f.BasePath)); });
|
||||||
|
logger?.LogInformation("Queued file system crawl jobs.");
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void QueueThumbnailGeneration(object? o) {
|
||||||
|
if (jobManager == null) {
|
||||||
|
logger?.LogWarning("JobManager is not available, cannot queue thumbnail generation.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// don't queue if there's already a thumbnail job in progress
|
||||||
|
if (jobManager.Jobs.Any(j => j is ThumbnailJob && j.JobStatus.Status is EJobStatus.Running or EJobStatus.Queued)) {
|
||||||
|
logger?.LogInformation("Thumbnail job is already in progress, cannot queue another at this moment.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
jobManager.EnqueueJob(jobManager.CreateJob<ThumbnailJob>());
|
||||||
|
logger?.LogInformation("Queued thumbnail job.");
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void QueuePreviewGeneration(object? o) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void QueueMetadataExtraction(object? o) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void QueueIntegrityCheck(object? o) {
|
||||||
|
if (jobManager == null) {
|
||||||
|
logger?.LogWarning("JobManager is not available, cannot queue integrity check.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// don't queue if there's already an integrity check job in progress
|
||||||
|
if (jobManager.Jobs.Any(j => j is IntegrityCheckJob && j.JobStatus.Status is EJobStatus.Running or EJobStatus.Queued)) {
|
||||||
|
logger?.LogInformation("Integrity check job is already in progress, cannot queue another at this moment.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
jobManager.EnqueueJob(jobManager.CreateJob<IntegrityCheckJob>());
|
||||||
|
logger?.LogInformation("Queued integrity check job.");
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void QueuePHashGeneration(string? firstOrDefault) {
|
||||||
|
if (jobManager == null) {
|
||||||
|
logger?.LogWarning("JobManager is not available, cannot queue pHash generation.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// don't queue if there's already a pHash job in progress
|
||||||
|
if (jobManager.Jobs.Any(j => j is PHashJob && j.JobStatus.Status is EJobStatus.Running or EJobStatus.Queued)) {
|
||||||
|
logger?.LogInformation("pHash job is already in progress, cannot queue another at this moment.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
jobManager.EnqueueJob(jobManager.CreateJob<PHashJob>());
|
||||||
|
logger?.LogInformation("Queued pHash job.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,25 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Lactose.Utils;
|
namespace Lactose.Utils;
|
||||||
|
|
||||||
public static class EnumerableExtensions {
|
public static class EnumerableExtensions {
|
||||||
public static void ForEach<TSource>(this IEnumerable<TSource> source, Action<TSource> action) {
|
public static void ForEach<TSource>(this IEnumerable<TSource> source, Action<TSource> action) {
|
||||||
|
|
||||||
foreach (var item in source) {
|
foreach (var item in source) {
|
||||||
action(item);
|
action(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class HashExtensions {
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static BitArray ToBitArray(this ulong hash) => new BitArray(BitConverter.GetBytes(hash));
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static ulong ToUlong(this BitArray bits) {
|
||||||
|
if (bits.Length > 64) throw new ArgumentException("BitArray length must be at most 64 bits.");
|
||||||
|
var array = new byte[8];
|
||||||
|
bits.CopyTo(array, 0);
|
||||||
|
return BitConverter.ToUInt64(array, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Debug",
|
"Default": "Warning",
|
||||||
"Microsoft.AspNetCore": "Information",
|
"Microsoft.AspNetCore": "Warning",
|
||||||
"Microsoft.EntityFrameworkCore": "Warning",
|
"Microsoft.EntityFrameworkCore": "Warning",
|
||||||
"Lactose.Services" : "Trace"
|
"Lactose.Services" : "Information"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SignKey": {
|
"SignKey": {
|
||||||
@@ -12,6 +12,6 @@
|
|||||||
},
|
},
|
||||||
"DatabaseAddress": {
|
"DatabaseAddress": {
|
||||||
"Host": "database",
|
"Host": "database",
|
||||||
"Port": 5432
|
"Port": 3306
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
{
|
{
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Debug",
|
"Default": "Warning",
|
||||||
"System": "Warning",
|
"Microsoft.AspNetCore": "Warning",
|
||||||
"Microsoft.AspNetCore": "Warning",
|
"Microsoft.EntityFrameworkCore": "Warning",
|
||||||
"Microsoft.EntityFrameworkCore": "Warning",
|
"Lactose.Services" : "Trace"
|
||||||
"Lactose.Services" : "Trace"
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"DatabaseAddress": {
|
"DatabaseAddress": {
|
||||||
"Host": "database",
|
"Host": "database",
|
||||||
"Port": 3306
|
"Port": 5432
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
{
|
{
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Warning",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning",
|
||||||
|
"Microsoft.EntityFrameworkCore": "Warning",
|
||||||
|
"Lactose": "Information"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" />
|
<Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" />
|
||||||
<script src="lib/js/bootstrap.bundle.min.js"></script>
|
<script src="lib/js/bootstrap.bundle.min.js"></script>
|
||||||
|
@* ReSharper disable once Html.PathError *@
|
||||||
<script src="_framework/blazor.web.js"></script>
|
<script src="_framework/blazor.web.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-body align-items-center">
|
<div class="card-body align-items-center">
|
||||||
<div class="d-flex flex-row flex-grow-1 align-items-center">
|
<div class="d-flex flex-row flex-grow-1 align-items-center">
|
||||||
<i class="bi bi-folder2 ms-3 mx-1"></i><p class="mx-1 mb-0 @Display">@(Folder.BasePath ?? "Unknown Folder")</p>
|
<i class="bi bi-folder2 ms-3 mx-1"></i><p class="mx-1 mb-0 @Display">@Folder.BasePath</p>
|
||||||
<input class="form-control form-control-sm mx-1 @Edit" type="text" @bind="Folder.BasePath"/>
|
<input class="form-control form-control-sm mx-1 @Edit" type="text" @bind="Folder.BasePath"/>
|
||||||
<button class="btn btn-sm btn-success @Edit" @onclick="OnConfirmEdit"><i class="bi bi-check"></i></button>
|
<button class="btn btn-sm btn-success @Edit" @onclick="OnConfirmEdit"><i class="bi bi-check"></i></button>
|
||||||
<div class="ms-auto form-switch">
|
<div class="ms-auto form-switch">
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
bool editing = false;
|
bool editing;
|
||||||
string Display => editing ? "d-none" : "";
|
string Display => editing ? "d-none" : "";
|
||||||
string Edit => editing ? "" : "d-none";
|
string Edit => editing ? "" : "d-none";
|
||||||
|
|
||||||
|
|||||||
86
MilkStream/Components/JobCard.razor
Normal file
86
MilkStream/Components/JobCard.razor
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
@using Butter.Dtos.Jobs
|
||||||
|
@using Butter.Types
|
||||||
|
@using MilkStream.Services
|
||||||
|
@inject JobsService JobsService
|
||||||
|
|
||||||
|
<div class="card m-2 @BorderColor h-100">
|
||||||
|
<div class="card-header h-100">
|
||||||
|
@switch (Job.JobType) {
|
||||||
|
case EJobType.FileSystemScan: <i class="me-2 bi bi-folder2-open"></i> break;
|
||||||
|
case EJobType.ThumbnailGeneration: <i class="me-2 bi bi-pip"></i> break;
|
||||||
|
case EJobType.PreviewGeneration: <i class="me-2 bi bi-file-image"></i> break;
|
||||||
|
case EJobType.MetadataExtraction: <i class="me-2 bi bi-file-earmark-break"></i> break;
|
||||||
|
case EJobType.IntegrityCheck: <i class="me-2 bi bi-file-earmark-check"></i> break;
|
||||||
|
}
|
||||||
|
@Job.Name <span class="badge @TextColor">@Job.Status</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="progress mb-2" role="progressbar">
|
||||||
|
<div class="@BgColor" style="width: @(Job.Progress*100)%">@(Job.Progress*100)%</div>
|
||||||
|
</div>
|
||||||
|
<p class="card-text"><i class="me-2 bi bi-stopwatch"></i>
|
||||||
|
@switch (Job.Status) {
|
||||||
|
case EJobStatus.Queued:
|
||||||
|
@Job.Created.ToLocalTime().ToString("g")
|
||||||
|
<p class="card-text"><i class="me-2 bi bi-hourglass-split"></i>@(DateTime.Now - Job.Created.ToLocalTime())</p>
|
||||||
|
break;
|
||||||
|
case EJobStatus.Running:
|
||||||
|
case EJobStatus.Waiting:
|
||||||
|
@Job.Started?.ToLocalTime().ToString("g")
|
||||||
|
<p class="card-text"><i class="me-2 bi bi-hourglass-split"></i>@(DateTime.Now - Job.Started?.ToLocalTime())</p>
|
||||||
|
break;
|
||||||
|
case EJobStatus.Completed:
|
||||||
|
case EJobStatus.Canceled:
|
||||||
|
case EJobStatus.Failed:
|
||||||
|
@Job.Finished?.ToLocalTime().ToString("g")
|
||||||
|
<p class="card-text"><i class="me-2 bi bi-hourglass-split"></i>@(Job.Finished?.ToLocalTime() - Job.Started?.ToLocalTime())</p>
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
<button class="btn btn-sm btn-danger" @onclick="Cancel"><i class="bi bi-trash"></i></button>
|
||||||
|
|
||||||
|
<div class="accordion accordion-flush" id="@Job.Id">
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#flush-@Job.Id" aria-expanded="false" aria-controls="flush-collapseOne">
|
||||||
|
Message:
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="flush-@Job.Id" class="accordion-collapse collapse" data-bs-parent="#@Job.Id">
|
||||||
|
@Job.Message
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public required JobStatusDto Job { get; set; }
|
||||||
|
string BorderColor => Job.Status switch {
|
||||||
|
EJobStatus.Queued => "border-secondary",
|
||||||
|
EJobStatus.Running => "border-primary",
|
||||||
|
EJobStatus.Waiting => "border-info",
|
||||||
|
EJobStatus.Completed => "border-success",
|
||||||
|
EJobStatus.Failed => "border-danger",
|
||||||
|
_ => "border-dark",
|
||||||
|
};
|
||||||
|
string TextColor => Job.Status switch {
|
||||||
|
EJobStatus.Queued => "text-bg-secondary",
|
||||||
|
EJobStatus.Running => "text-bg-primary",
|
||||||
|
EJobStatus.Waiting => "text-bg-info",
|
||||||
|
EJobStatus.Completed => "text-bg-success",
|
||||||
|
EJobStatus.Failed => "text-bg-danger",
|
||||||
|
_ => "text-bg-dark",
|
||||||
|
};
|
||||||
|
string BgColor => Job.Status switch {
|
||||||
|
EJobStatus.Queued => "progress-bar text-bg-secondary",
|
||||||
|
EJobStatus.Running => "progress-bar text-bg-primary",
|
||||||
|
EJobStatus.Waiting => "progress-bar text-bg-info",
|
||||||
|
EJobStatus.Completed => "progress-bar text-bg-success",
|
||||||
|
EJobStatus.Failed => "progress-bar text-bg-danger",
|
||||||
|
_ => "progress-bar text-bg-dark",
|
||||||
|
};
|
||||||
|
|
||||||
|
public async Task Cancel() => await JobsService.CancelJob(Job);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<NavMenu/>
|
<NavMenu/>
|
||||||
<main class="container-xxl d-flex flex-grow-1">
|
<main class="container-xxl d-flex flex-grow-1">
|
||||||
<div class="d-flex flex-column border border-1 border-opacity-50 border-secondary-subtle rounded-3 m-lg-5 m-1 p-lg-5 p-1 flex-grow-1">
|
<div class="d-flex flex-column border border-1 border-opacity-50 border-secondary-subtle rounded-3 mt-4 m-lg-2 m-1 p-lg-4 p-2 flex-grow-1">
|
||||||
@Body
|
@Body
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -10,7 +10,6 @@
|
|||||||
<span class="text-primary m-auto">Footer (duh!)</span>
|
<span class="text-primary m-auto">Footer (duh!)</span>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|
||||||
<div id="blazor-error-ui">
|
<div id="blazor-error-ui">
|
||||||
An unhandled error has occurred.
|
An unhandled error has occurred.
|
||||||
<a href="" class="reload">Reload</a>
|
<a href="" class="reload">Reload</a>
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
@using Butter.Types
|
@using Butter.Types
|
||||||
|
|
||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
@inject NavigationManager navigation
|
@inject NavigationManager Navigation
|
||||||
@inject UserService userService
|
@inject UserService UserService
|
||||||
@inject LoginService loginService
|
@inject LoginService LoginService
|
||||||
@inject ProtectedLocalStorage localStorage
|
@inject ProtectedLocalStorage LocalStorage
|
||||||
|
|
||||||
<div class="navbar navbar-expand-sm bg-dark-subtle">
|
<div class="navbar navbar-expand-sm bg-dark-subtle">
|
||||||
<div class="container-xl">
|
<div class="container-xl">
|
||||||
@@ -47,18 +47,25 @@
|
|||||||
<i class="bi bi-search-heart"></i>
|
<i class="bi bi-search-heart"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mx-2">@loginService.LoggedUser?.Username</div>
|
<div class="mx-2">@LoginService.LoggedUser?.Username</div>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-lg btn-outline-light" type="button">
|
<button class="btn btn-outline-light" type="button">
|
||||||
<i class="bi bi-person-circle"></i>
|
<i class="bi bi-person-circle"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-lg btn-outline-light dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown"></button>
|
<button class="btn btn-outline-light dropdown-toggle dropdown-toggle-split"
|
||||||
|
data-bs-toggle="dropdown"></button>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
<li><a class="dropdown-item" href="#"><i class="bi bi-person-lines-fill"></i> Profile</a></li>
|
<li><a class="dropdown-item" @onclick="OnProfileClick"><i class="bi bi-person-lines-fill"></i> Profile</a></li>
|
||||||
@if(Admin) {
|
@if (Admin) {
|
||||||
<li><a class="dropdown-item" @onclick="OnSettingsClick"><i class="bi bi-gear-wide-connected"></i> Settings</a></li>
|
<li><a class="dropdown-item" @onclick="OnSettingsClick"><i
|
||||||
|
class="bi bi-gear-wide-connected"></i> Settings</a></li>
|
||||||
|
<li><a class="dropdown-item" @onclick="OnSettings1Click"><i
|
||||||
|
class="bi bi-gear-wide-connected"></i> Settings 1</a></li>
|
||||||
|
<li><a class="dropdown-item" @onclick="OnJobsClick"><i class="bi bi-kanban-fill"></i>
|
||||||
|
Jobs</a></li>
|
||||||
}
|
}
|
||||||
<li><a class="dropdown-item" @onclick="OnLogout"><i class="bi bi-door-closed"></i> Logout</a></li>
|
<li><a class="dropdown-item" @onclick="OnLogout"><i class="bi bi-door-closed"></i>
|
||||||
|
Logout</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -68,15 +75,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code{
|
@code{
|
||||||
bool LoggedIn => loginService.IsLoggedIn;
|
bool LoggedIn => LoginService.IsLoggedIn;
|
||||||
bool Admin => loginService.LoggedUser?.AccessLevel == EAccessLevel.Admin;
|
bool Admin => LoginService.LoggedUser?.AccessLevel == EAccessLevel.Admin;
|
||||||
bool isConnected;
|
bool isConnected;
|
||||||
bool needsUpdate;
|
bool needsUpdate;
|
||||||
|
|
||||||
protected override void OnInitialized() {
|
protected async override Task OnInitializedAsync() {
|
||||||
base.OnInitialized();
|
LoginService.LoggedUserChanged += (_, _) => needsUpdate = true;
|
||||||
loginService.LoggedUserChanged += (sender, dto) => needsUpdate = true;
|
await base.OnInitializedAsync();
|
||||||
LoadStateAsync().ConfigureAwait(false);
|
//await LoadStateAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async override Task OnAfterRenderAsync(bool firstRender) {
|
protected async override Task OnAfterRenderAsync(bool firstRender) {
|
||||||
@@ -89,27 +96,34 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadStateAsync() {
|
private async Task LoadStateAsync() {
|
||||||
var auth = await localStorage.GetAsync<AuthInfo>("auth");
|
var auth = await LocalStorage.GetAsync<AuthInfo>("auth");
|
||||||
if (auth.Success == false) return;
|
if (auth.Success == false) return;
|
||||||
loginService.AuthInfo = auth.Value;
|
|
||||||
loginService.LoggedUser = await userService.GetUserAsync();//.ConfigureAwait(false);
|
LoginService.AuthInfo = auth.Value;
|
||||||
|
LoginService.LoggedUser = await UserService.GetUserAsync(); //.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task OnLoginClick() {
|
async Task OnLoginClick() {
|
||||||
loginService.LoggedUser = await userService.GetUserAsync();
|
LoginService.LoggedUser = await UserService.GetUserAsync();
|
||||||
|
|
||||||
if (loginService.LoggedUser != null) needsUpdate = true;
|
if (LoginService.LoggedUser != null) needsUpdate = true;
|
||||||
else navigation.NavigateTo("/login");
|
else Navigation.NavigateTo("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnRegisterClick() => navigation.NavigateTo("/register");
|
void OnRegisterClick() => Navigation.NavigateTo("/register");
|
||||||
|
|
||||||
void OnSettingsClick() => navigation.NavigateTo("/settings");
|
void OnSettingsClick() => Navigation.NavigateTo("/settings");
|
||||||
|
|
||||||
|
void OnJobsClick() => Navigation.NavigateTo("/jobs");
|
||||||
|
|
||||||
async Task OnLogout() {
|
async Task OnLogout() {
|
||||||
_ = loginService.Logout();
|
_ = LoginService.Logout();
|
||||||
await localStorage.DeleteAsync("auth");
|
await LocalStorage.DeleteAsync("auth");
|
||||||
navigation.NavigateTo("/", true);
|
Navigation.NavigateTo("/", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OnSettings1Click() => Navigation.NavigateTo("/Settings1");
|
||||||
|
|
||||||
|
void OnProfileClick() => Navigation.NavigateTo($"/User/{LoginService.AuthInfo?.UserId}", true);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
@using Butter.Dtos;
|
@using Butter.Dtos;
|
||||||
@using MilkStream.Components.Layout
|
|
||||||
@using MilkStream.Services
|
@using MilkStream.Services
|
||||||
|
|
||||||
@inject NavigationManager navigationManager
|
@inject NavigationManager NavigationManager
|
||||||
@inject LoginService loginService
|
@inject LoginService LoginService
|
||||||
@inject ProtectedSessionStorage sessionStorage
|
@inject ProtectedSessionStorage SessionStorage
|
||||||
@inject ProtectedLocalStorage localStorage
|
@inject ProtectedLocalStorage LocalStorage
|
||||||
|
|
||||||
<PageTitle>Home</PageTitle>
|
<PageTitle>Home</PageTitle>
|
||||||
|
|
||||||
@@ -17,7 +16,7 @@
|
|||||||
@if (mediaList.Count == 0) {
|
@if (mediaList.Count == 0) {
|
||||||
<div class="border-warning border-5 border-opacity-100 rounded-3 p-3 m-5 bg-warning-subtle text-center">
|
<div class="border-warning border-5 border-opacity-100 rounded-3 p-3 m-5 bg-warning-subtle text-center">
|
||||||
<h2 class="text-warning">No media available</h2>
|
<h2 class="text-warning">No media available</h2>
|
||||||
@if (loginService.IsLoggedIn) {
|
@if (LoginService.IsLoggedIn) {
|
||||||
<p class="text-warning-emphasis">You are logged in, but there is no media available at the moment.</p>
|
<p class="text-warning-emphasis">You are logged in, but there is no media available at the moment.</p>
|
||||||
} else {
|
} else {
|
||||||
<p class="text-warning-emphasis">Please <a href="/login" class="link-warning">log-in</a> to be able to browse any media.</p>
|
<p class="text-warning-emphasis">Please <a href="/login" class="link-warning">log-in</a> to be able to browse any media.</p>
|
||||||
@@ -41,7 +40,7 @@
|
|||||||
|
|
||||||
protected async override Task OnAfterRenderAsync(bool firstRender) {
|
protected async override Task OnAfterRenderAsync(bool firstRender) {
|
||||||
if (firstRender) {
|
if (firstRender) {
|
||||||
loginService.LoggedUserChanged += (sender, dto) => StateHasChanged();
|
LoginService.LoggedUserChanged += (_, _) => StateHasChanged();
|
||||||
//do nothing for now
|
//do nothing for now
|
||||||
await Task.Delay(1);
|
await Task.Delay(1);
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
|||||||
54
MilkStream/Components/Pages/Jobs.razor
Normal file
54
MilkStream/Components/Pages/Jobs.razor
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
@page "/Jobs"
|
||||||
|
@using Butter.Dtos.Jobs
|
||||||
|
@using MilkStream.Services
|
||||||
|
@inject JobsService JobsService
|
||||||
|
@inject LoginService LoginService
|
||||||
|
|
||||||
|
<h3>Jobs</h3>
|
||||||
|
<div class="d-flex gap-2 mb-3">
|
||||||
|
<button class="btn btn-primary" @onclick="LaunchScan">Scan Files</button>
|
||||||
|
<button class="btn btn-primary" @onclick="LaunchThumb">Make Thumbs</button>
|
||||||
|
<button class="btn btn-primary" @onclick="LaunchMeta">Scan Metadata</button>
|
||||||
|
<button class="btn btn-warning" @onclick="LaunchIntegrityCheck">Integrity check</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row row-cols-1 row-cols-sm-1 row-cols-md-2 row-cols-lg-3 g-1 g-md-2">
|
||||||
|
@foreach (var job in jobs.OrderBy(j => j.Created)) {
|
||||||
|
<div class="col"><JobCard Job="job"/></div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@code {
|
||||||
|
readonly PeriodicTimer timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
|
||||||
|
List<JobStatusDto> jobs = [];
|
||||||
|
|
||||||
|
protected async override Task OnInitializedAsync() {
|
||||||
|
LoginService.LoggedUserChanged += async (_, _) => {
|
||||||
|
jobs = await JobsService.GetJobs();
|
||||||
|
await RefreshJobs();
|
||||||
|
StateHasChanged();
|
||||||
|
};
|
||||||
|
if (LoginService.IsLoggedIn) {
|
||||||
|
jobs = await JobsService.GetJobs();
|
||||||
|
await RefreshJobs();
|
||||||
|
}
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task RefreshJobs() {
|
||||||
|
while (await timer.WaitForNextTickAsync()) {
|
||||||
|
jobs = await JobsService.GetJobs();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Task LaunchScan() => JobsService.StartJob(EJobType.FileSystemScan);
|
||||||
|
|
||||||
|
Task LaunchThumb() => JobsService.StartJob(EJobType.ThumbnailGeneration);
|
||||||
|
|
||||||
|
Task LaunchIntegrityCheck() => JobsService.StartJob(EJobType.IntegrityCheck);
|
||||||
|
|
||||||
|
Task LaunchMeta() => JobsService.StartJob(EJobType.MetadataExtraction);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
switch (option.DisplayType) {
|
switch (option.DisplayType) {
|
||||||
// TODO: should implement text and password at some point
|
// TODO: should implement text and password at some point
|
||||||
case DisplayType.Text:
|
case DisplayType.Text:
|
||||||
<input type="text" id="@option.Name" class="form-control" @bind="@option.Value"/>
|
<SettingString Setting="option"/>
|
||||||
break;
|
break;
|
||||||
case DisplayType.Password:
|
case DisplayType.Password:
|
||||||
<input type="password" id="@option.Name" class="form-control" @bind="@option.Value"/>
|
<input type="password" id="@option.Name" class="form-control" @bind="@option.Value"/>
|
||||||
@@ -37,7 +37,10 @@
|
|||||||
<div class="">DATETIME NOT IMPLEMENTED Value: @option.Value</div>
|
<div class="">DATETIME NOT IMPLEMENTED Value: @option.Value</div>
|
||||||
break;
|
break;
|
||||||
case DisplayType.TimePicker:
|
case DisplayType.TimePicker:
|
||||||
<SettingRange Setting="option"/>
|
<SettingRange Setting="option" MinValue="Convert.ToInt32(option.Options?[0])" MaxValue="Convert.ToInt32(option.Options?[1])" Step="Convert.ToInt32(option.Options?[2])"/>
|
||||||
|
break;
|
||||||
|
case DisplayType.Po2W:
|
||||||
|
<SettingRange Setting="option" IsPowerOfTwo="true" MinValue="Convert.ToInt32(option.Options?[0])" MaxValue="Convert.ToInt32(option.Options?[1])" Step="Convert.ToInt32(option.Options?[2])"/>
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
94
MilkStream/Components/Pages/SettingsRework.razor
Normal file
94
MilkStream/Components/Pages/SettingsRework.razor
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
@page "/Settings1"
|
||||||
|
@using Butter.Dtos.Folder
|
||||||
|
@using Butter.Dtos.Settings
|
||||||
|
@using Butter.Settings
|
||||||
|
@using Butter.Types
|
||||||
|
@using MilkStream.Components.SettingBoxes
|
||||||
|
@using MilkStream.Services
|
||||||
|
@using System.Linq
|
||||||
|
|
||||||
|
@inject NavigationManager NavigationManager
|
||||||
|
@inject LoginService LoginService
|
||||||
|
@inject SettingsService SettingsService
|
||||||
|
@inject FoldersService FoldersService
|
||||||
|
|
||||||
|
<h3>Settings</h3>
|
||||||
|
@if (areSettingsLoading || settings is {Count: 0}) {
|
||||||
|
<LoadSpinner/>
|
||||||
|
} else {
|
||||||
|
<div class="row row-cols-2">
|
||||||
|
<div class="col">
|
||||||
|
<!--Folder Scan Enabled-->
|
||||||
|
<SettingSwitch Setting="settings!.FirstOrDefault(s => s.Name == Butter.Settings.Settings.FolderScanEnabled.AsString())"/>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<!--Folder Scan Interval-->
|
||||||
|
<SettingRange Setting="settings!.FirstOrDefault(s => s.Name == Butter.Settings.Settings.FolderScanInterval.AsString())" MinValue="5" MaxValue="360" Step="5"/>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<!--File Upload Enabled-->
|
||||||
|
<SettingSwitch Setting="settings!.FirstOrDefault(s => s.Name == Butter.Settings.Settings.FileUploadEnabled.AsString())"/>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<!--File Upload Max Size-->
|
||||||
|
<SettingRange Setting="settings!.FirstOrDefault(s => s.Name == Butter.Settings.Settings.FileUploadMaxSize.AsString())" MinValue="1024" MaxValue="1000000" Step="1000"/>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<!--Thumbnail Path-->
|
||||||
|
<SettingString Setting="settings!.FirstOrDefault(s => s.Name == Butter.Settings.Settings.ThumbnailPath.AsString())"/>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<!--Thumbnail Size-->
|
||||||
|
<SettingRange Setting="settings!.FirstOrDefault(s => s.Name == Butter.Settings.Settings.ThumbnailSize.AsString())" IsPowerOfTwo="true" MinValue="8" MaxValue="12" Step="1"/>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<!--Previews Path-->
|
||||||
|
<SettingString Setting="settings!.FirstOrDefault(s => s.Name == Butter.Settings.Settings.PreviewPath.AsString())"/>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<!--Previews Size-->
|
||||||
|
<SettingRange Setting="settings!.FirstOrDefault(s => s.Name == Butter.Settings.Settings.PreviewSize.AsString())" IsPowerOfTwo="true" MinValue="8" MaxValue="14" Step="1"/>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<!--Max concurrent jobs -->
|
||||||
|
<SettingRange Setting="settings!.FirstOrDefault(s => s.Name == Butter.Settings.Settings.MaxConcurrentJobs.AsString())" MinValue="1" MaxValue="32" Step="1"/>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<!-- User registration enabled -->
|
||||||
|
<SettingSwitch Setting="settings!.FirstOrDefault(s => s.Name == Butter.Settings.Settings.UserRegistrationEnabled.AsString())"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-outline-primary my-2" @onclick="SaveSettings"><i class="bi bi-floppy2 mx-1"></i>Save Changes</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<h3>Folders</h3>
|
||||||
|
|
||||||
|
|
||||||
|
@code {
|
||||||
|
bool areSettingsLoading = true;
|
||||||
|
List<SettingDto>? settings = new();
|
||||||
|
|
||||||
|
protected async override Task OnInitializedAsync() {
|
||||||
|
if (LoginService.IsLoggedIn) {
|
||||||
|
await LoadSettings();
|
||||||
|
} else {
|
||||||
|
LoginService.AuthInfoChanged += async (_, _) => {
|
||||||
|
await LoadSettings();
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task LoadSettings() {
|
||||||
|
settings = await SettingsService.GetAllSettings();
|
||||||
|
if(settings != null) areSettingsLoading = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SaveSettings() {
|
||||||
|
if (settings != null) {
|
||||||
|
SettingsService.UpdateSettings(settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
62
MilkStream/Components/Pages/User.razor
Normal file
62
MilkStream/Components/Pages/User.razor
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
@page "/User/{UserId:guid}"
|
||||||
|
@using Butter.Dtos.User
|
||||||
|
@using Butter.Types
|
||||||
|
@using MilkStream.Services
|
||||||
|
@inject LoginService LoginService
|
||||||
|
@inject UserService UserService
|
||||||
|
|
||||||
|
<h3>@(user?.Username ?? "Username")</h3>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
@if (isSelf || AdminAccess) {
|
||||||
|
<div class="col-2"><i class="bi bi-envelope-at"></i> EMail:</div>
|
||||||
|
<div class="col-2">@user?.Email</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-2"><i class="bi bi-person-fill-lock"></i> Role:</div>
|
||||||
|
<div class="col-2">@user?.AccessLevel.ToString()</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-2"><i class="bi bi-calendar-date"></i> Creation Date:</div>
|
||||||
|
<div class="col-2">@user?.CreatedAt</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-2"><i class="bi bi-door-open"></i> Last Login:</div>
|
||||||
|
<div class="col-2">@(user?.LastLogin.ToString() ?? "Never")</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-2"><i class="bi bi-ban"></i>Banned:</div>
|
||||||
|
<div class="col-2">@(user is {IsBanned: true} ? $"Yes ({user.BannedAt.ToString()})" : "No")</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
@if (AdminAccess) {
|
||||||
|
<div class="col-2"><i class="bi bi-trash3"></i>Deleted:</div>
|
||||||
|
<div class="col-2">@(user is not {DeletedAt: null} ? user!.DeletedAt.ToString() : "No")</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@code {
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
|
||||||
|
bool isSelf;
|
||||||
|
bool AdminAccess => LoginService.LoggedUser?.AccessLevel == EAccessLevel.Admin;
|
||||||
|
UserInfoDto? user;
|
||||||
|
|
||||||
|
protected async override Task OnInitializedAsync() {
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
if (LoginService.IsLoggedIn) await LoadUser();
|
||||||
|
LoginService.AuthInfoChanged += async (_, _) => await LoadUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task LoadUser() {
|
||||||
|
if (UserId == LoginService.AuthInfo?.UserId) isSelf = true;
|
||||||
|
user = await UserService.GetUserAsync();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -5,13 +5,13 @@
|
|||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public required SettingDto Setting { get; set; }
|
public required SettingDto? Setting { get; set; }
|
||||||
|
|
||||||
protected virtual SettingDto GetSetting() {
|
protected virtual SettingDto? GetSetting() {
|
||||||
return Setting;
|
return Setting;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnInitialized() {
|
protected override void OnInitialized() {
|
||||||
SettingsService.BeginSave += (sender, args) => SettingsService.UpdateSetting(GetSetting());
|
SettingsService.BeginSave += (_, _) => SettingsService.UpdateSetting(GetSetting());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,15 +19,16 @@
|
|||||||
private float currentValue;
|
private float currentValue;
|
||||||
|
|
||||||
protected override void OnInitialized() {
|
protected override void OnInitialized() {
|
||||||
SettingsService.BeginSave += (sender, args) => SettingsService.UpdateSetting(GetSetting());
|
SettingsService.BeginSave += (_, _) => SettingsService.UpdateSetting(GetSetting());
|
||||||
if (float.TryParse(Setting.Value, out float value)) {
|
if (float.TryParse(Setting?.Value, out float value)) {
|
||||||
currentValue = value;
|
currentValue = value;
|
||||||
} else {
|
} else {
|
||||||
currentValue = 0; // Default value if parsing fails
|
currentValue = 0; // Default value if parsing fails
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override SettingDto GetSetting() {
|
protected override SettingDto? GetSetting() {
|
||||||
|
if (Setting == null) return null;
|
||||||
Setting.Value = currentValue.ToString(CultureInfo.InvariantCulture);
|
Setting.Value = currentValue.ToString(CultureInfo.InvariantCulture);
|
||||||
return Setting;
|
return Setting;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,24 +8,37 @@
|
|||||||
<div class="card-body d-flex justify-content-between align-items-center">
|
<div class="card-body d-flex justify-content-between align-items-center">
|
||||||
<p class="card-text">@(Setting?.Description ?? "No description provided")</p>
|
<p class="card-text">@(Setting?.Description ?? "No description provided")</p>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<div class="mx-2">@currentValue</div>
|
<div class="mx-2">@(IsPowerOfTwo ? Math.Pow(2, currentValue) : currentValue)</div>
|
||||||
<input type="range" id="@Setting?.Name" class="form-range" @bind="@currentValue"/>
|
<input type="range" id="@Setting?.Name" class="form-range" @bind="@currentValue" min="@MinValue" max="@MaxValue"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public int MinValue {get; set;}
|
||||||
|
[Parameter]
|
||||||
|
public int MaxValue {get; set;} = 100;
|
||||||
|
[Parameter]
|
||||||
|
public bool IsPowerOfTwo {get; set;}
|
||||||
|
[Parameter]
|
||||||
|
public int Step {get; set;} = 1;
|
||||||
private int currentValue;
|
private int currentValue;
|
||||||
|
|
||||||
protected override void OnInitialized() {
|
protected override void OnInitialized() {
|
||||||
SettingsService.BeginSave += (sender, args) => SettingsService.UpdateSetting(GetSetting());
|
SettingsService.BeginSave += (_, _) => SettingsService.UpdateSetting(GetSetting());
|
||||||
if (int.TryParse(Setting.Value, out int value)) { currentValue = value; } else {
|
|
||||||
currentValue = 0; // Default value if parsing fails
|
if (IsPowerOfTwo) {
|
||||||
|
var converted = int.TryParse(Setting?.Value, out int value);
|
||||||
|
currentValue = converted ? (int)Math.Log2(value) : 0; // Default value if parsing fails
|
||||||
|
} else {
|
||||||
|
currentValue = int.TryParse(Setting?.Value, out int value) ? value : 0; // Default value if parsing fails
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override SettingDto GetSetting() {
|
protected override SettingDto? GetSetting() {
|
||||||
Setting.Value = currentValue.ToString();
|
if (Setting == null) return null;
|
||||||
|
Setting.Value = IsPowerOfTwo ? ((int)Math.Pow(2, currentValue)).ToString() : currentValue.ToString();
|
||||||
return Setting;
|
return Setting;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
30
MilkStream/Components/SettingBoxes/SettingString.razor
Normal file
30
MilkStream/Components/SettingBoxes/SettingString.razor
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
@using Butter.Dtos.Settings
|
||||||
|
@using MilkStream.Services
|
||||||
|
@inherits SettingBox
|
||||||
|
@inject SettingsService SettingsService
|
||||||
|
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">@(Setting?.Name ?? "NO NAME")</div>
|
||||||
|
<div class="card-body d-flex justify-content-between align-items-center">
|
||||||
|
<p class="card-text">@(Setting?.Description ?? "No description provided")</p>
|
||||||
|
<div class="d-flex">
|
||||||
|
<input type="text" id="@Setting?.Name" class="form-control" @bind="@currentValue" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private string currentValue = string.Empty;
|
||||||
|
|
||||||
|
protected override void OnInitialized() {
|
||||||
|
base.OnInitialized();
|
||||||
|
SettingsService.BeginSave += (_, _) => SettingsService.UpdateSetting(GetSetting());
|
||||||
|
currentValue = Setting?.Value ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override SettingDto? GetSetting() {
|
||||||
|
if (Setting == null) return null;
|
||||||
|
Setting.Value = currentValue;
|
||||||
|
return Setting;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
@using Butter.Dtos.Settings
|
@using Butter.Dtos.Settings
|
||||||
@using Butter.Settings
|
|
||||||
@using MilkStream.Services
|
@using MilkStream.Services
|
||||||
@inherits SettingBox
|
@inherits SettingBox
|
||||||
@inject SettingsService SettingsService
|
@inject SettingsService SettingsService
|
||||||
@@ -18,13 +17,14 @@
|
|||||||
private bool isChecked;
|
private bool isChecked;
|
||||||
|
|
||||||
protected override void OnInitialized() {
|
protected override void OnInitialized() {
|
||||||
SettingsService.BeginSave += (sender, args) => SettingsService.UpdateSetting(GetSetting());
|
SettingsService.BeginSave += (_, _) => SettingsService.UpdateSetting(GetSetting());
|
||||||
if (bool.TryParse(Setting.Value, out var parsedValue)) {
|
if (bool.TryParse(Setting?.Value, out var parsedValue)) {
|
||||||
isChecked = parsedValue;
|
isChecked = parsedValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override SettingDto GetSetting() {
|
protected override SettingDto? GetSetting() {
|
||||||
|
if (Setting == null) return null;
|
||||||
Setting.Value = isChecked.ToString();
|
Setting.Value = isChecked.ToString();
|
||||||
return Setting;
|
return Setting;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:String x:Key="/Default/CodeInspection/WebPathMapping/IgnoredPaths/=BLAZOR_002EWEB_002EJS/@EntryIndexedValue">blazor.web.js</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/WebPathMapping/IgnoredPaths/=WWWROOT_005CMILKSTREAM_002ESTYLES_002ECSS/@EntryIndexedValue">wwwroot\MilkStream.styles.css</s:String>
|
<s:String x:Key="/Default/CodeInspection/WebPathMapping/IgnoredPaths/=WWWROOT_005CMILKSTREAM_002ESTYLES_002ECSS/@EntryIndexedValue">wwwroot\MilkStream.styles.css</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/WebPathMapping/MappedPaths/=WWWROOT_005C_005FFRAMEWORK/@EntryIndexedValue"></s:String>
|
<s:String x:Key="/Default/CodeInspection/WebPathMapping/MappedPaths/=WWWROOT_005C_005FFRAMEWORK/@EntryIndexedValue"></s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/WebPathMapping/PathsInCorrectCasing/=BLAZOR_002EWEB_002EJS/@EntryIndexedValue">blazor.web.js</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/WebPathMapping/PathsInCorrectCasing/=WWWROOT_005CMILKSTREAM_002ESTYLES_002ECSS/@EntryIndexedValue">wwwroot\MilkStream.styles.css</s:String>
|
<s:String x:Key="/Default/CodeInspection/WebPathMapping/PathsInCorrectCasing/=WWWROOT_005CMILKSTREAM_002ESTYLES_002ECSS/@EntryIndexedValue">wwwroot\MilkStream.styles.css</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/WebPathMapping/PathsInCorrectCasing/=WWWROOT_005C_005FFRAMEWORK/@EntryIndexedValue">wwwroot\_framework</s:String></wpf:ResourceDictionary>
|
<s:String x:Key="/Default/CodeInspection/WebPathMapping/PathsInCorrectCasing/=WWWROOT_005C_005FFRAMEWORK/@EntryIndexedValue">wwwroot\_framework</s:String></wpf:ResourceDictionary>
|
||||||
@@ -15,13 +15,14 @@ builder.Services.AddSassCompiler();
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
//TODO: this probably needs a better system for the settings.
|
//TODO: this probably needs a better system for the settings.
|
||||||
builder.Services.AddHttpClient("MilkstreamClient").AddHttpMessageHandler<JwtTokenRefresher>();
|
builder.Services.AddHttpClient("MilkStreamClient").AddHttpMessageHandler<JwtTokenRefresher>();
|
||||||
builder.Services.Configure<ServiceOptions>(options => { options.BaseUrl = builder.Configuration["BaseUrl"]!; });
|
builder.Services.Configure<ServiceOptions>(options => { options.BaseUrl = builder.Configuration["BaseUrl"]!; });
|
||||||
builder.Services.AddScoped<LoginService>();
|
builder.Services.AddScoped<LoginService>();
|
||||||
builder.Services.AddScoped<MediaService>();
|
builder.Services.AddScoped<MediaService>();
|
||||||
builder.Services.AddScoped<UserService>();
|
builder.Services.AddScoped<UserService>();
|
||||||
builder.Services.AddScoped<SettingsService>();
|
builder.Services.AddScoped<SettingsService>();
|
||||||
builder.Services.AddScoped<FoldersService>();
|
builder.Services.AddScoped<FoldersService>();
|
||||||
|
builder.Services.AddScoped<JobsService>();
|
||||||
//http listener for all requests with automatic JWT token refresh on 401 Unauthorized responses.
|
//http listener for all requests with automatic JWT token refresh on 401 Unauthorized responses.
|
||||||
builder.Services.AddScoped<JwtTokenRefresher>();
|
builder.Services.AddScoped<JwtTokenRefresher>();
|
||||||
builder.Services.AddLogging();
|
builder.Services.AddLogging();
|
||||||
|
|||||||
36
MilkStream/Services/JobsService.cs
Normal file
36
MilkStream/Services/JobsService.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using Butter.Dtos.Jobs;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace MilkStream.Services;
|
||||||
|
|
||||||
|
public sealed class JobsService(
|
||||||
|
IOptions<ServiceOptions> options,
|
||||||
|
IHttpClientFactory httpClientFactory,
|
||||||
|
LoginService loginService,
|
||||||
|
ILogger<JobsService> logger
|
||||||
|
) : AuthServiceBase(options, httpClientFactory, loginService, logger) {
|
||||||
|
public async Task<List<JobStatusDto>> GetJobs() {
|
||||||
|
|
||||||
|
var response = await Client.GetAsync("/api/jobs");
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var jobs = await response.Content.ReadFromJsonAsync<List<JobStatusDto>>();
|
||||||
|
return jobs ?? new List<JobStatusDto>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StartJob(EJobType jobType) {
|
||||||
|
var response = await Client.PutAsJsonAsync("/api/jobs", new JobRequestDto() {
|
||||||
|
JobType = jobType
|
||||||
|
});
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CancelJob(JobStatusDto job) {
|
||||||
|
var response = await Client.DeleteAsync($"/api/jobs/{job.Id}");
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,13 +20,15 @@ public sealed class SettingsService : AuthServiceBase {
|
|||||||
|
|
||||||
public async Task<List<SettingDto>?> GetAllSettings() {
|
public async Task<List<SettingDto>?> GetAllSettings() {
|
||||||
var response = await Client.GetAsync("api/settings");
|
var response = await Client.GetAsync("api/settings");
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
if (response.IsSuccessStatusCode) return await response.Content.ReadFromJsonAsync<List<SettingDto>>();
|
return await response.Content.ReadFromJsonAsync<List<SettingDto>>();
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateSetting(SettingDto setting) {
|
public void UpdateSetting(SettingDto? setting) {
|
||||||
|
if (setting == null) {
|
||||||
|
Logger.Log(LogLevel.Warning, "Setting is null, cannot update.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
Logger.Log(LogLevel.Information, $"Updating setting {setting.Name} to {setting.Value}");
|
Logger.Log(LogLevel.Information, $"Updating setting {setting.Name} to {setting.Value}");
|
||||||
var response = Client.PostAsJsonAsync("api/settings", setting).Result;
|
var response = Client.PostAsJsonAsync("api/settings", setting).Result;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Debug",
|
"Default": "Information",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ EndProject
|
|||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lactose", "Lactose\Lactose.csproj", "{054E855F-A730-4FA4-895B-5147AF1877D9}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lactose", "Lactose\Lactose.csproj", "{054E855F-A730-4FA4-895B-5147AF1877D9}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MilkStream", "MilkStream\MilkStream.csproj", "{A3A0FAEB-B4D2-4984-986A-3DE2DA32ADEF}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MilkStream", "MilkStream\MilkStream.csproj", "{A3A0FAEB-B4D2-4984-986A-3DE2DA32ADEF}"
|
||||||
|
ProjectSection(ProjectDependencies) = postProject
|
||||||
|
{054E855F-A730-4FA4-895B-5147AF1877D9} = {054E855F-A730-4FA4-895B-5147AF1877D9}
|
||||||
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Butter", "Butter\Butter.csproj", "{CB1B8C0E-0F47-456E-ACD7-22F772EBF948}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Butter", "Butter\Butter.csproj", "{CB1B8C0E-0F47-456E-ACD7-22F772EBF948}"
|
||||||
EndProject
|
EndProject
|
||||||
@@ -21,13 +24,16 @@ Global
|
|||||||
{054E855F-A730-4FA4-895B-5147AF1877D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{054E855F-A730-4FA4-895B-5147AF1877D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{054E855F-A730-4FA4-895B-5147AF1877D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{054E855F-A730-4FA4-895B-5147AF1877D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{054E855F-A730-4FA4-895B-5147AF1877D9}.Release|Any CPU.Build.0 = Release|Any CPU
|
{054E855F-A730-4FA4-895B-5147AF1877D9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{054E855F-A730-4FA4-895B-5147AF1877D9}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
|
||||||
{A3A0FAEB-B4D2-4984-986A-3DE2DA32ADEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{A3A0FAEB-B4D2-4984-986A-3DE2DA32ADEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{A3A0FAEB-B4D2-4984-986A-3DE2DA32ADEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{A3A0FAEB-B4D2-4984-986A-3DE2DA32ADEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{A3A0FAEB-B4D2-4984-986A-3DE2DA32ADEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{A3A0FAEB-B4D2-4984-986A-3DE2DA32ADEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{A3A0FAEB-B4D2-4984-986A-3DE2DA32ADEF}.Release|Any CPU.Build.0 = Release|Any CPU
|
{A3A0FAEB-B4D2-4984-986A-3DE2DA32ADEF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A3A0FAEB-B4D2-4984-986A-3DE2DA32ADEF}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
|
||||||
{CB1B8C0E-0F47-456E-ACD7-22F772EBF948}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{CB1B8C0E-0F47-456E-ACD7-22F772EBF948}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{CB1B8C0E-0F47-456E-ACD7-22F772EBF948}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{CB1B8C0E-0F47-456E-ACD7-22F772EBF948}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{CB1B8C0E-0F47-456E-ACD7-22F772EBF948}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{CB1B8C0E-0F47-456E-ACD7-22F772EBF948}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{CB1B8C0E-0F47-456E-ACD7-22F772EBF948}.Release|Any CPU.Build.0 = Release|Any CPU
|
{CB1B8C0E-0F47-456E-ACD7-22F772EBF948}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{CB1B8C0E-0F47-456E-ACD7-22F772EBF948}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AConcurrentDictionary_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fc3a41f4911b2871fe46ff44169115d8ffb305313e0fe77907cfeca42acf5e9_003FConcurrentDictionary_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AConcurrentDictionary_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fc3a41f4911b2871fe46ff44169115d8ffb305313e0fe77907cfeca42acf5e9_003FConcurrentDictionary_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AConfigurationExtensions_002Ecs_002Fl_003AC_0021_003FUsers_003Fairon_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fb8c1c57742337e59591581af3eb2c647e9ed52d2951655fd74b1facf3ff520_003FConfigurationExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AConfigurationExtensions_002Ecs_002Fl_003AC_0021_003FUsers_003Fairon_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fb8c1c57742337e59591581af3eb2c647e9ed52d2951655fd74b1facf3ff520_003FConfigurationExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AConfigurationManager_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbbcfe7ab04c9c5e854e8084459eb29d1ba45cca1ecfec1ad876fafa1267_003FConfigurationManager_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AConfigurationManager_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbbcfe7ab04c9c5e854e8084459eb29d1ba45cca1ecfec1ad876fafa1267_003FConfigurationManager_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AControllerBase_002Ecs_002Fl_003AC_0021_003FUsers_003Fairon_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F9e99606fc18442e3a934d0cf2081796f1de928_003F8f_003F76fd927b_003FControllerBase_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AControllerBase_002Ecs_002Fl_003AC_0021_003FUsers_003Fnivek_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F96a4cc66aa48dda8a9d8be8febec4a769fc143f7a86dd53ecdebfb9c3177d_003FControllerBase_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AControllerBase_002Ecs_002Fl_003AC_0021_003FUsers_003Fnivek_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F96a4cc66aa48dda8a9d8be8febec4a769fc143f7a86dd53ecdebfb9c3177d_003FControllerBase_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADefaultInterpolatedStringHandler_002Ecs_002Fl_003AC_0021_003FUsers_003Fairon_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fc1c946eaa6d8ddaaea1b63e936ca8ed2791d9316c5b025a41d445891e8a59ecd_003FDefaultInterpolatedStringHandler_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADefaultInterpolatedStringHandler_002Ecs_002Fl_003AC_0021_003FUsers_003Fairon_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fc1c946eaa6d8ddaaea1b63e936ca8ed2791d9316c5b025a41d445891e8a59ecd_003FDefaultInterpolatedStringHandler_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADefaultJsonTypeInfoResolver_002EConverters_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F16171b2c80ba9499f621a6bdb243caf2a6f19af216cfead9653432cd5aae_003FDefaultJsonTypeInfoResolver_002EConverters_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADefaultJsonTypeInfoResolver_002EConverters_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F16171b2c80ba9499f621a6bdb243caf2a6f19af216cfead9653432cd5aae_003FDefaultJsonTypeInfoResolver_002EConverters_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExecutionContext_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F9eda537f15ea23cdfae523c19e87eb303a3ded88937ae7e55919387a43f70_003FExecutionContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExecutionContext_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F9eda537f15ea23cdfae523c19e87eb303a3ded88937ae7e55919387a43f70_003FExecutionContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFSharpCoreReflectionProxy_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F74e6dceae40f3123c8fc6d1fb41fb1f5bd4eb9214623f4ab4943efe5fc52ad9_003FFSharpCoreReflectionProxy_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFSharpCoreReflectionProxy_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F74e6dceae40f3123c8fc6d1fb41fb1f5bd4eb9214623f4ab4943efe5fc52ad9_003FFSharpCoreReflectionProxy_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFSharpTypeConverterFactory_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Ff5b0593df95acacf3652937716a4f2a2135151dd5a2ce4d133d4a43608c32af_003FFSharpTypeConverterFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFSharpTypeConverterFactory_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Ff5b0593df95acacf3652937716a4f2a2135151dd5a2ce4d133d4a43608c32af_003FFSharpTypeConverterFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFuture_002Ecs_002Fl_003AC_0021_003FUsers_003Fairon_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F6f90c6c13479b8c2a5be98f3d75dfc3bd885a055652d8a32904ca2448132949e_003FFuture_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpResponseMessage_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F21d9c87abd9d4814ab095cd6be92e1911a6928_003F86_003Fcd4a7567_003FHttpResponseMessage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpResponseMessage_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F21d9c87abd9d4814ab095cd6be92e1911a6928_003F86_003Fcd4a7567_003FHttpResponseMessage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIFileInfo_002Ecs_002Fl_003AC_0021_003FUsers_003Fnivek_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F781590a6c8e24e6b9f85d75a1ac30c819918_003F47_003Fda5e789a_003FIFileInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIFileInfo_002Ecs_002Fl_003AC_0021_003FUsers_003Fnivek_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F781590a6c8e24e6b9f85d75a1ac30c819918_003F47_003Fda5e789a_003FIFileInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFileInfo_002Ecs_002Fl_003AC_0021_003FUsers_003Fnivek_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F9fcad18e31a1983467a9fd4bfef344ee97d72fa4ab6ba20353b8f78db8437b1_003FFileInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFileInfo_002Ecs_002Fl_003AC_0021_003FUsers_003Fnivek_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F9fcad18e31a1983467a9fd4bfef344ee97d72fa4ab6ba20353b8f78db8437b1_003FFileInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
@@ -44,6 +46,7 @@
|
|||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMethodBaseInvoker_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F2832e8c2b81f4641b3863f406ce3a519c90938_003F1c_003F3b0d22ec_003FMethodBaseInvoker_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMethodBaseInvoker_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F2832e8c2b81f4641b3863f406ce3a519c90938_003F1c_003F3b0d22ec_003FMethodBaseInvoker_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMethodBaseInvoker_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fd882146b4f265f10bcbec2663fce248db9ffec5fa1aeaf76e32a11ba5eafcd6_003FMethodBaseInvoker_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMethodBaseInvoker_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fd882146b4f265f10bcbec2663fce248db9ffec5fa1aeaf76e32a11ba5eafcd6_003FMethodBaseInvoker_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMethodBaseInvoker_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F4363be3555734c96804bc429c863e56ab2e200_003F2f_003F2c688ff0_003FMethodBaseInvoker_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMethodBaseInvoker_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F4363be3555734c96804bc429c863e56ab2e200_003F2f_003F2c688ff0_003FMethodBaseInvoker_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMonitor_002ECoreCLR_002Ecs_002Fl_003AC_0021_003FUsers_003Fairon_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F36f346b6c0454bc8a6afa7aed38119fe5bbffcc983298d9bfa4dbd5f49461f_003FMonitor_002ECoreCLR_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMonitor_002ECoreCLR_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F36f346b6c0454bc8a6afa7aed38119fe5bbffcc983298d9bfa4dbd5f49461f_003FMonitor_002ECoreCLR_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMonitor_002ECoreCLR_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F36f346b6c0454bc8a6afa7aed38119fe5bbffcc983298d9bfa4dbd5f49461f_003FMonitor_002ECoreCLR_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMonitor_002Ecs_002Fl_003AC_0021_003FUsers_003Fairon_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F2b90705926ae4f10aa01bbe3f1025828c90908_003F1a_003F5b817523_003FMonitor_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMonitor_002Ecs_002Fl_003AC_0021_003FUsers_003Fairon_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F2b90705926ae4f10aa01bbe3f1025828c90908_003F1a_003F5b817523_003FMonitor_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANullable_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F5acc345db3c207bc9d886a36ff14867ef8d65557432172c2a42f19aeac04d1b_003FNullable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANullable_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F5acc345db3c207bc9d886a36ff14867ef8d65557432172c2a42f19aeac04d1b_003FNullable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
@@ -61,9 +64,11 @@
|
|||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARuntimeCustomAttributeData_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F167c292414d4cdbd966d4cb3bfda345c71ff4f9f149871eb4f4c65b6371ec63_003FRuntimeCustomAttributeData_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARuntimeCustomAttributeData_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F167c292414d4cdbd966d4cb3bfda345c71ff4f9f149871eb4f4c65b6371ec63_003FRuntimeCustomAttributeData_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARuntimeType_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F3791ade316feeb2be344648da3c7d31719ff492f7733b643bf8c24d7629883_003FRuntimeType_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARuntimeType_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F3791ade316feeb2be344648da3c7d31719ff492f7733b643bf8c24d7629883_003FRuntimeType_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASerializationExtensions_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F1fa9d27d36b95177aad139996d2e72727f1a5e60fcc839c2f1ac67d031ca5_003FSerializationExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASerializationExtensions_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F1fa9d27d36b95177aad139996d2e72727f1a5e60fcc839c2f1ac67d031ca5_003FSerializationExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceProvider_002Ecs_002Fl_003AC_0021_003FUsers_003Fairon_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F7d81b2d4f22bee75e5438c707251ae43cb0974c207db91ffc159118c84b4eb9_003FServiceProvider_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASessionMiddlewareExtensions_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F7c7ebbdc5d4270fb17dee7c3eb17d64c7845f617f3c679ce4e22ad4bd11192d_003FSessionMiddlewareExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASessionMiddlewareExtensions_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F7c7ebbdc5d4270fb17dee7c3eb17d64c7845f617f3c679ce4e22ad4bd11192d_003FSessionMiddlewareExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASessionStorageService_002Eg_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fc5abb9d0ed5bcffd706887e8cabcd2f1947e67c_003FSessionStorageService_002Eg_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASessionStorageService_002Eg_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fc5abb9d0ed5bcffd706887e8cabcd2f1947e67c_003FSessionStorageService_002Eg_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AString_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F2832e8c2b81f4641b3863f406ce3a519c90938_003F06_003F3b8d7821_003FString_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AString_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F2832e8c2b81f4641b3863f406ce3a519c90938_003F06_003F3b8d7821_003FString_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATask_002Ecs_002Fl_003AC_0021_003FUsers_003Fairon_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fa77b231cea33ff338c41dc7869d6c493d4dae137ff51e342173efa61d933_003FTask_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATask_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F2832e8c2b81f4641b3863f406ce3a519c90938_003F27_003F7cad2316_003FTask_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATask_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F2832e8c2b81f4641b3863f406ce3a519c90938_003F27_003F7cad2316_003FTask_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThreadPoolWorkQueue_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fac24a7c2e0f296e231c484b3fd930268f76c06dc75f4aad1df7aaf09f2c1227_003FThreadPoolWorkQueue_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThreadPoolWorkQueue_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fac24a7c2e0f296e231c484b3fd930268f76c06dc75f4aad1df7aaf09f2c1227_003FThreadPoolWorkQueue_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F2c8e7ca976f350cba9836d5565dac56b11e0b56656fa786460eb1395857a6fa_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F2c8e7ca976f350cba9836d5565dac56b11e0b56656fa786460eb1395857a6fa_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
@@ -72,5 +77,10 @@
|
|||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUtf8JsonWriterCache_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F96d87fca98b4167c66ffae61c9ee88dc182d6e7dbc7eb8dbf41297e660eb_003FUtf8JsonWriterCache_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUtf8JsonWriterCache_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F96d87fca98b4167c66ffae61c9ee88dc182d6e7dbc7eb8dbf41297e660eb_003FUtf8JsonWriterCache_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AValueTask_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F9ec7c6b688aa4b89a1477dc1b679f62bf856b50196f4b8d19cd77f86df0abc_003FValueTask_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AValueTask_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F9ec7c6b688aa4b89a1477dc1b679f62bf856b50196f4b8d19cd77f86df0abc_003FValueTask_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AWriteStackFrame_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fcfb06811acbbb25f4c1f3ae4eee74f65da5b2e9bd1bdc01ad979ac9b6745e9_003FWriteStackFrame_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AWriteStackFrame_002Ecs_002Fl_003AC_0021_003FUsers_003FREDCODE_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fcfb06811acbbb25f4c1f3ae4eee74f65da5b2e9bd1bdc01ad979ac9b6745e9_003FWriteStackFrame_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/SweaWarningsMode/@EntryValue">ShowAndRun</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/ACCESSOR_OWNER_BODY/@EntryValue">ExpressionBody</s:String>
|
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/ACCESSOR_OWNER_BODY/@EntryValue">ExpressionBody</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/ThisQualifier/INSTANCE_MEMBERS_QUALIFY_DECLARED_IN/@EntryValue">0</s:String></wpf:ResourceDictionary>
|
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/ThisQualifier/INSTANCE_MEMBERS_QUALIFY_DECLARED_IN/@EntryValue">0</s:String>
|
||||||
|
<s:Boolean x:Key="/Default/Dpa/IsEnabledInDebug/@EntryValue">True</s:Boolean>
|
||||||
|
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue"><AssemblyExplorer>
|
||||||
|
<Assembly Path="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.16\ref\net8.0\System.Collections.Concurrent.dll" />
|
||||||
|
</AssemblyExplorer></s:String></wpf:ResourceDictionary>
|
||||||
|
|||||||
@@ -7,17 +7,20 @@
|
|||||||
context: .
|
context: .
|
||||||
dockerfile: Lactose/Dockerfile
|
dockerfile: Lactose/Dockerfile
|
||||||
environment:
|
environment:
|
||||||
- ASPNETCORE_ENVIRONMENT=Development
|
- ASPNETCORE_ENVIRONMENT=Docker
|
||||||
ports:
|
ports:
|
||||||
- "5162:8080"
|
- "5162:8080"
|
||||||
depends_on:
|
depends_on:
|
||||||
- database
|
- database
|
||||||
volumes:
|
volumes:
|
||||||
- "/root/MilkyShots/storageImages:/diary:ro"
|
- "/root/MilkyShots/storageImages:/diary:ro"
|
||||||
|
- "/root/MilkyShots/thumbs:/app/thumbnails:rw"
|
||||||
|
- "/root/MilkyShots/previews:/app/previews:rw"
|
||||||
#- "./storageImages:/diary:ro"
|
#- "./storageImages:/diary:ro"
|
||||||
|
|
||||||
database:
|
database:
|
||||||
image: tensorchord/pgvecto-rs:pg17-v0.4.0
|
#image: tensorchord/pgvecto-rs:pg17-v0.4.0
|
||||||
|
image: pgvector/pgvector:pg17-trixie
|
||||||
container_name: database
|
container_name: database
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: root
|
POSTGRES_USER: root
|
||||||
|
|||||||
Reference in New Issue
Block a user