diff --git a/AZKi Server/AZKi Server.csproj b/AZKi Server/AZKi Server.csproj
index cfff6b1..cdc8627 100644
--- a/AZKi Server/AZKi Server.csproj
+++ b/AZKi Server/AZKi Server.csproj
@@ -4,6 +4,7 @@
net9.0
enable
enable
+ AZKiServer
@@ -12,6 +13,7 @@
+
diff --git a/AZKi Server/Models/MediaEntry.cs b/AZKi Server/Models/MediaEntry.cs
new file mode 100644
index 0000000..c89ced1
--- /dev/null
+++ b/AZKi Server/Models/MediaEntry.cs
@@ -0,0 +1,74 @@
+using MaybeError;
+using MaybeError.Errors;
+
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization.Attributes;
+
+using System.Text.RegularExpressions;
+
+namespace AZKiServer.Models;
+
+public partial class MediaEntry
+{
+ [BsonId]
+ public ObjectId Id { get; set; }
+ public MediaType Type { get; set; }
+ public required string Filepath { get; set; }
+ public DateTime Date { get; set; }
+ public byte CameraId { get; set; }
+
+ public static Maybe Parse(string relativePath)
+ {
+ var filename = Path.GetFileName(relativePath);
+
+ var match = FileParser().Match(filename);
+ if (!match.Success)
+ return new Error("Failed to parse file name");
+
+ try
+ {
+ var src = match.Groups["src"];
+ var cam = match.Groups["cam"];
+ var date = match.Groups["date"];
+ var ext = match.Groups["ext"];
+
+ return new MediaEntry
+ {
+ CameraId = byte.Parse(cam.Value),
+ Filepath = relativePath,
+ Date = ParseDate(date.Value),
+ Type = ext.Value switch
+ {
+ "mp4" => MediaType.Video,
+ _ => MediaType.Image
+ }
+
+ };
+
+ }catch(Exception ex)
+ {
+ return ex;
+ }
+ }
+
+ private static DateTime ParseDate(string dateString)
+ {
+ var year = dateString[0..4];
+ var month = dateString[4..6];
+ var day = dateString[6..8];
+ var hour = dateString[8..10];
+ var minute = dateString[10..12];
+ var sec = dateString[12..];
+ return new DateTime(int.Parse(year), int.Parse(month), int.Parse(day), int.Parse(hour), int.Parse(minute), int.Parse(sec), DateTimeKind.Local);
+ }
+
+ [GeneratedRegex("(?'src'.+)_(?'cam'\\d+)_(?'date'\\d+).(?'ext'\\w+)")]
+ private static partial Regex FileParser();
+}
+
+
+public enum MediaType
+{
+ Video,
+ Image
+}
\ No newline at end of file
diff --git a/AZKi Server/Services/FileScannerService.cs b/AZKi Server/Services/FileScannerService.cs
index d278d2f..c5e6424 100644
--- a/AZKi Server/Services/FileScannerService.cs
+++ b/AZKi Server/Services/FileScannerService.cs
@@ -1,16 +1,55 @@
+using AZKiServer.Models;
+
namespace AZKiServer.Services;
-public class FileScannerService : IHostedService
+public class FileScannerService(MediaService mediaService, IConfiguration config, ILogger logger) : IHostedService, IDisposable
{
+ private Timer? _timer;
+
+ public void Dispose()
+ {
+ _timer?.Dispose();
+ }
+
public Task StartAsync(CancellationToken cancellationToken)
{
- throw new NotImplementedException();
+ var path = config["SCAN_PATH"];
+ if (string.IsNullOrWhiteSpace(path))
+ return Task.CompletedTask;
+ _timer = new Timer((_) =>
+ {
+ ScanFilesAsync(path).Wait();
+ }, null, TimeSpan.FromMinutes(1), TimeSpan.FromHours(1));
+ return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
- throw new NotImplementedException();
+ _timer?.Dispose();
+ return Task.CompletedTask;
}
+
+ private async Task ScanFilesAsync(string path)
+ {
+ try
+ {
+ var files = Directory.GetFiles(path, "*", SearchOption.AllDirectories);
+ var existingFiles = await mediaService.GetExistingFilePathsAsync();
+ var entries = new List();
+ foreach (var filePath in files)
+ {
+ var relativePath = Path.GetRelativePath(path, filePath);
+ if (existingFiles.Contains(relativePath))
+ continue;
+ entries.Add(MediaEntry.Parse(relativePath));
+ }
+ await mediaService.AddMediaBulkAsync(entries);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Failed to read directory contents");
+ }
+ }
}
diff --git a/AZKi Server/Services/MediaService.cs b/AZKi Server/Services/MediaService.cs
index a9c7ff8..3558e24 100644
--- a/AZKi Server/Services/MediaService.cs
+++ b/AZKi Server/Services/MediaService.cs
@@ -1,6 +1,30 @@
-namespace AZKiServer.Services;
+using AZKiServer.Models;
-public class MediaService
+using MongoDB.Driver;
+
+using System.Collections.Frozen;
+
+namespace AZKiServer.Services;
+
+public class MediaService(IMongoDatabase db)
{
+ public readonly IMongoCollection _entries = db.GetCollection("media");
+ public async Task> GetExistingFilePathsAsync(CancellationToken cancellationToken = default)
+ {
+ var files = await _entries.Find("{}").Project(m => m.Filepath).ToListAsync(cancellationToken);
+ return files.ToFrozenSet();
+ }
+ public async Task AddMediaBulkAsync(List entries, CancellationToken cancellationToken = default)
+ {
+ await _entries.InsertManyAsync(entries, cancellationToken: cancellationToken);
+ }
+
+ public class IndexCreation : BackgroundService
+ {
+ protected override Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ throw new NotImplementedException();
+ }
+ }
}
diff --git a/AZKi Server/Utilz/DateUtils.cs b/AZKi Server/Utilz/DateUtils.cs
new file mode 100644
index 0000000..ef36958
--- /dev/null
+++ b/AZKi Server/Utilz/DateUtils.cs
@@ -0,0 +1,102 @@
+namespace AZKiServer.Utilz;
+
+public enum DateInterval
+{
+ Daily,
+ Weekly,
+ Monthly,
+ Yearly,
+ All,
+}
+
+public static class DateUtils
+{
+ public static TimeSpan ToUnixTime(this DateTime date)
+ {
+ return date - DateTime.UnixEpoch;
+ }
+
+ public static DateTime SnapToYear(this DateTime date)
+ {
+ return new DateTime(date.Year, 1, 1, 0, 0, 0, date.Kind);
+ }
+
+ public static DateTimeOffset SnapToYear(this DateTimeOffset date)
+ {
+ return new DateTimeOffset(date.Year, 1, 1, 0, 0, 0, date.Offset);
+ }
+
+ public static DateTime SnapToMonth(this DateTime date)
+ {
+ return new DateTime(date.Year, date.Month, 1, 0, 0, 0, date.Kind);
+ }
+
+ public static DateTimeOffset SnapToMonth(this DateTimeOffset date)
+ {
+ return new DateTimeOffset(date.Year, date.Month, 1, 0, 0, 0, date.Offset);
+ }
+
+ public static DateTime SnapToWeek(this DateTime date)
+ {
+ var d = new DateTime(date.Year, date.Month, date.Day, 0, 0, 0, date.Kind);
+ if (date.DayOfWeek == DayOfWeek.Sunday)
+ return d;
+ else
+ return d.AddDays(-(int)date.DayOfWeek);
+ }
+
+ public static DateTimeOffset SnapToWeek(this DateTimeOffset date)
+ {
+ var d = new DateTimeOffset(date.Year, date.Month, date.Day, 0, 0, 0, date.Offset);
+ if (date.DayOfWeek == DayOfWeek.Sunday)
+ return d;
+ else
+ return d.AddDays(-(int)date.DayOfWeek);
+ }
+
+ public static (DateTime from, DateTime to) ToInterval(this DateTime date, DateInterval internval)
+ {
+ return internval switch
+ {
+ DateInterval.Daily => (date, date.AddDays(1)),
+ DateInterval.Weekly => (date, date.AddDays(7)),
+ DateInterval.Monthly => (date, date.AddMonths(1)),
+ DateInterval.Yearly => (date, date.AddYears(1)),
+ DateInterval.All => (DateTime.MinValue, DateTime.MaxValue),
+ _ => throw new InvalidOperationException(),
+ };
+ }
+
+ public static (DateTimeOffset from, DateTimeOffset to) ToInterval(this DateTimeOffset date, DateInterval internval)
+ {
+ return internval switch
+ {
+ DateInterval.Daily => (date, date.AddDays(1)),
+ DateInterval.Weekly => (date, date.AddDays(7)),
+ DateInterval.Monthly => (date, date.AddMonths(1)),
+ DateInterval.Yearly => (date, date.AddYears(1)),
+ DateInterval.All => (DateTime.MinValue, DateTime.MaxValue),
+ _ => throw new InvalidOperationException(),
+ };
+ }
+
+ public static TimeSpan Days(this int days) => TimeSpan.FromDays(days);
+
+ public static TimeSpan Days(this float days) => TimeSpan.FromDays(days);
+
+ public static TimeSpan Hours(this int hours) => TimeSpan.FromHours(hours);
+
+ public static TimeSpan Hours(this float hours) => TimeSpan.FromHours(hours);
+
+ public static TimeSpan Minutes(this int minutes) => TimeSpan.FromMinutes(minutes);
+
+ public static TimeSpan Minutes(this float minutes) => TimeSpan.FromMinutes(minutes);
+
+ public static TimeSpan Seconds(this int seconds) => TimeSpan.FromSeconds(seconds);
+
+ public static TimeSpan Seconds(this float seconds) => TimeSpan.FromSeconds(seconds);
+
+ public static TimeSpan Miliseconds(this int miliseconds) => TimeSpan.FromMilliseconds(miliseconds);
+
+ public static TimeSpan Miliseconds(this float miliseconds) => TimeSpan.FromMilliseconds(miliseconds);
+}
\ No newline at end of file
diff --git a/AZKi Server/appsettings.Development.json b/AZKi Server/appsettings.Development.json
index ff66ba6..a0ddd56 100644
--- a/AZKi Server/appsettings.Development.json
+++ b/AZKi Server/appsettings.Development.json
@@ -1,8 +1,10 @@
{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning"
- }
- }
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "SCAN_LOCATION": "O:/cctv",
+ "DB_STRING": "mongodb://NinoIna:27017"
}