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" }