[CHANGE] Reworked db with interceptors
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>Application</title>
|
<title>YouTube Manager server</title>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<base href="/"/>
|
<base href="/"/>
|
||||||
@@ -11,7 +11,6 @@
|
|||||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet"/>
|
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet"/>
|
||||||
<link href="_content/MudBlazor/MudBlazor.min.css?v=@Metadata.Version" rel="stylesheet"/>
|
<link href="_content/MudBlazor/MudBlazor.min.css?v=@Metadata.Version" rel="stylesheet"/>
|
||||||
<HeadOutlet/>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@@ -107,7 +107,11 @@ public partial class EventConsole : ComponentBase
|
|||||||
_batchLock.Release();
|
_batchLock.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
_serviceEvents.AddRange(batch);
|
foreach (var serviceEvent in _batchBuffer.Where(serviceEvent => !_serviceEvents.Contains(serviceEvent)))
|
||||||
|
{
|
||||||
|
_serviceEvents.Add(serviceEvent);
|
||||||
|
}
|
||||||
|
|
||||||
_lastBatchUpdate = DateTime.UtcNow;
|
_lastBatchUpdate = DateTime.UtcNow;
|
||||||
|
|
||||||
if (_virtualize != null)
|
if (_virtualize != null)
|
||||||
|
@@ -6,6 +6,8 @@
|
|||||||
@inject ILibraryService LibraryService
|
@inject ILibraryService LibraryService
|
||||||
@inject IDialogService DialogService
|
@inject IDialogService DialogService
|
||||||
@inject IOptions<LibrarySettings> LibraryOptions
|
@inject IOptions<LibrarySettings> LibraryOptions
|
||||||
|
@inject ClientService ClientService
|
||||||
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
<PageTitle>Channels</PageTitle>
|
<PageTitle>Channels</PageTitle>
|
||||||
|
|
||||||
|
@@ -28,21 +28,21 @@ public partial class Channels : ComponentBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var client = (ClientPrep)result.Data;
|
var clientPrep = (ClientPrep)result.Data;
|
||||||
if (client == null)
|
if (clientPrep?.YouTubeClient == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*var savedResult = await ClientManager.SaveClientAsync(client);
|
var savedResult = await ClientService.SaveClientAsync(clientPrep.YouTubeClient, clientPrep.Channel);
|
||||||
if (!savedResult.IsSuccess)
|
if (!savedResult.IsSuccess)
|
||||||
{
|
{
|
||||||
Snackbar.Add($"Failed to store client: {savedResult.Error?.Description ?? "Unknown!"}", Severity.Error);
|
Snackbar.Add($"Failed to store client: {savedResult.Error?.Description ?? "Unknown!"}", Severity.Error);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Snackbar.Add($"Client {client.External.Channel?.Handle ?? client.Id} saved!", Severity.Success);
|
Snackbar.Add($"Client {clientPrep.Channel?.Handle ?? clientPrep.YouTubeClient.Id} saved!", Severity.Success);
|
||||||
}*/
|
}
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
@page "/Development"
|
@page "/Development"
|
||||||
@using Manager.App.Components.Application.Dev
|
@using Manager.App.Components.Application.Dev
|
||||||
<title>Development page</title>
|
<PageTitle>Development page</PageTitle>
|
||||||
|
|
||||||
<MudTabs Outlined Position="Position.Left" PanelClass="pa-4" ApplyEffectsToContainer Style="height: 100%">
|
<MudTabs Outlined Position="Position.Left" PanelClass="pa-4" ApplyEffectsToContainer Style="height: 100%">
|
||||||
<MudTabPanel Text="Authentication">
|
<MudTabPanel Text="Authentication">
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
@inject BackgroundServiceRegistry ServiceRegistry
|
@inject BackgroundServiceRegistry ServiceRegistry
|
||||||
|
|
||||||
<title>Services</title>
|
<PageTitle>Services</PageTitle>
|
||||||
|
|
||||||
<MudDataGrid T="ExtendedBackgroundService" Items="@_backgroundServices" Filterable QuickFilter="@QuickFilter">
|
<MudDataGrid T="ExtendedBackgroundService" Items="@_backgroundServices" Filterable QuickFilter="@QuickFilter">
|
||||||
<ToolBarContent>
|
<ToolBarContent>
|
||||||
@@ -39,4 +39,4 @@
|
|||||||
</MudDataGrid>
|
</MudDataGrid>
|
||||||
|
|
||||||
<EventConsole AsyncEnumerable="@GetEventAsyncEnumerable()" InitialEvents="@GetInitialEvents()"
|
<EventConsole AsyncEnumerable="@GetEventAsyncEnumerable()" InitialEvents="@GetInitialEvents()"
|
||||||
Elevation="0" Class="mt-3" Style="flex: 1; display: flex; flex-direction: column; min-height: 0;"/>
|
Elevation="0" Class="mt-3" Style="flex: 1; display: flex; flex-direction: column; min-height: 350px;"/>
|
||||||
|
@@ -3,4 +3,5 @@
|
|||||||
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)"/>
|
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)"/>
|
||||||
<FocusOnNavigate RouteData="routeData" Selector="h1"/>
|
<FocusOnNavigate RouteData="routeData" Selector="h1"/>
|
||||||
</Found>
|
</Found>
|
||||||
</Router>
|
</Router>
|
||||||
|
<HeadOutlet />
|
@@ -1,34 +1,32 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using DotBased.Logging;
|
using DotBased.Logging;
|
||||||
using DotBased.Monads;
|
using DotBased.Monads;
|
||||||
using Manager.App.Models.Library;
|
|
||||||
using Manager.Data.Entities.LibraryContext;
|
using Manager.Data.Entities.LibraryContext;
|
||||||
using Manager.YouTube;
|
using Manager.YouTube;
|
||||||
|
using Manager.YouTube.Models.Innertube;
|
||||||
|
|
||||||
namespace Manager.App.Services.System;
|
namespace Manager.App.Services.System;
|
||||||
|
|
||||||
public class ClientService(IServiceScopeFactory scopeFactory, ILogger<ClientService> logger)
|
public class ClientService(IServiceScopeFactory scopeFactory, ILogger<ClientService> logger)
|
||||||
: ExtendedBackgroundService("ClientService", "Managing YouTube clients", logger, TimeSpan.FromMilliseconds(100))
|
: ExtendedBackgroundService(nameof(ClientService), "Managing YouTube clients", logger, TimeSpan.FromMinutes(10))
|
||||||
{
|
{
|
||||||
private readonly List<YouTubeClient> _clients = [];
|
private readonly List<YouTubeClient> _clients = [];
|
||||||
private CancellationToken _cancellationToken;
|
private CancellationToken _cancellationToken;
|
||||||
private ILibraryService? _libraryService;
|
private ILibraryService? _libraryService;
|
||||||
|
|
||||||
protected override async Task InitializeAsync(CancellationToken stoppingToken)
|
protected override Task InitializeAsync(CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
_cancellationToken = stoppingToken;
|
_cancellationToken = stoppingToken;
|
||||||
stoppingToken.Register(CancellationRequested);
|
stoppingToken.Register(CancellationRequested);
|
||||||
using var scope = scopeFactory.CreateScope();
|
using var scope = scopeFactory.CreateScope();
|
||||||
_libraryService = scope.ServiceProvider.GetRequiredService<ILibraryService>();
|
_libraryService = scope.ServiceProvider.GetRequiredService<ILibraryService>();
|
||||||
LogEvent("Initializing service...");
|
LogEvent("Initializing service...");
|
||||||
//Pause();
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task ExecuteServiceAsync(CancellationToken stoppingToken)
|
protected override Task ExecuteServiceAsync(CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
LogEvent("Sending event...");
|
return Task.CompletedTask;
|
||||||
LogEvent("Sending warning event...", LogSeverity.Warning);
|
|
||||||
LogEvent("Sending error event...", LogSeverity.Error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CancellationRequested()
|
private void CancellationRequested()
|
||||||
@@ -36,42 +34,54 @@ public class ClientService(IServiceScopeFactory scopeFactory, ILogger<ClientServ
|
|||||||
// Clear up
|
// Clear up
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<ClientPrep>> PrepareClient()
|
public async Task<Result> SaveClientAsync(YouTubeClient client, Channel? channelInfo = null, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (_libraryService == null)
|
||||||
return ResultError.Fail("Not implemented!");
|
{
|
||||||
}
|
return ResultError.Fail("Library service is not initialized!.");
|
||||||
|
}
|
||||||
|
|
||||||
/*public async Task<Result> SaveClientAsync(YouTubeClient client, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(client.Id))
|
if (string.IsNullOrWhiteSpace(client.Id))
|
||||||
{
|
{
|
||||||
|
LogEvent("Failed to store client no ID!", LogSeverity.Warning);
|
||||||
return ResultError.Fail("Client does not have an ID, cannot save to library database!");
|
return ResultError.Fail("Client does not have an ID, cannot save to library database!");
|
||||||
}
|
}
|
||||||
|
|
||||||
var channelResult = await libraryService.GetChannelByIdAsync(client.Id, cancellationToken);
|
var channelResult = await _libraryService.GetChannelByIdAsync(client.Id, cancellationToken);
|
||||||
|
|
||||||
ChannelEntity? channel;
|
ChannelEntity? channel;
|
||||||
if (channelResult.IsSuccess)
|
try
|
||||||
{
|
{
|
||||||
channel = channelResult.Value;
|
if (channelResult.IsSuccess)
|
||||||
UpdateChannelEntity(channel, client);
|
{
|
||||||
|
channel = channelResult.Value;
|
||||||
|
UpdateChannelEntity(client, channel, channelInfo);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
channel = CreateNewChannelFromClient(client, channelInfo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
channel = CreateNewChannelFromClient(client);
|
LogEvent("Failed to save client: " + e.Message, LogSeverity.Warning);
|
||||||
|
return ResultError.Error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
var saveResult = await libraryService.SaveChannelAsync(channel, cancellationToken);
|
var saveResult = await _libraryService.SaveChannelAsync(channel, cancellationToken);
|
||||||
return saveResult;
|
return saveResult;
|
||||||
}*/
|
}
|
||||||
|
|
||||||
/*private void UpdateChannelEntity(ChannelEntity channel, YouTubeClient client)
|
private void UpdateChannelEntity(YouTubeClient client, ChannelEntity entity, Channel? channelInfo)
|
||||||
{
|
{
|
||||||
channel.Name = client.External.Channel?.ChannelName;
|
if (channelInfo != null)
|
||||||
channel.Handle = client.External.Channel?.Handle;
|
{
|
||||||
channel.Description = client.External.Channel?.Description;
|
entity.Name = channelInfo.ChannelName;
|
||||||
var clientAcc = channel.ClientAccount;
|
entity.Handle = channelInfo.Handle;
|
||||||
|
entity.Description = channelInfo.Description;
|
||||||
|
}
|
||||||
|
|
||||||
|
var clientAcc = entity.ClientAccount;
|
||||||
if (clientAcc != null)
|
if (clientAcc != null)
|
||||||
{
|
{
|
||||||
clientAcc.UserAgent = clientAcc.UserAgent;
|
clientAcc.UserAgent = clientAcc.UserAgent;
|
||||||
@@ -99,8 +109,13 @@ public class ClientService(IServiceScopeFactory scopeFactory, ILogger<ClientServ
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChannelEntity CreateNewChannelFromClient(YouTubeClient client)
|
private ChannelEntity CreateNewChannelFromClient(YouTubeClient client, Channel? channelInfo)
|
||||||
{
|
{
|
||||||
|
if (channelInfo == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(channelInfo), "Channel information required to store new client/account.");
|
||||||
|
}
|
||||||
|
|
||||||
var cookies = new List<HttpCookieEntity>();
|
var cookies = new List<HttpCookieEntity>();
|
||||||
foreach (var cookieObj in client.CookieContainer.GetAllCookies())
|
foreach (var cookieObj in client.CookieContainer.GetAllCookies())
|
||||||
{
|
{
|
||||||
@@ -133,22 +148,21 @@ public class ClientService(IServiceScopeFactory scopeFactory, ILogger<ClientServ
|
|||||||
var channel = new ChannelEntity
|
var channel = new ChannelEntity
|
||||||
{
|
{
|
||||||
Id = client.Id,
|
Id = client.Id,
|
||||||
Name = client.External.Channel?.ChannelName,
|
Name = channelInfo.ChannelName,
|
||||||
Handle = client.External.Channel?.Handle,
|
Handle = channelInfo.Handle,
|
||||||
Description = client.External.Channel?.Description,
|
Description = channelInfo.Description,
|
||||||
ClientAccount = clientAcc
|
ClientAccount = clientAcc
|
||||||
};
|
};
|
||||||
return channel;
|
return channel;
|
||||||
}*/
|
}
|
||||||
|
|
||||||
public async Task<Result<YouTubeClient>> LoadClientByIdAsync(string id)
|
/*public async Task<Result<YouTubeClient>> LoadClientByIdAsync(string id)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(id))
|
if (string.IsNullOrWhiteSpace(id))
|
||||||
{
|
{
|
||||||
return ResultError.Fail("Client ID is empty!");
|
return ResultError.Fail("Client ID is empty!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return ResultError.Fail("Not implemented");
|
return ResultError.Fail("Not implemented");
|
||||||
}
|
}*/
|
||||||
}
|
}
|
111
Manager.Data/Contexts/AuditInterceptor.cs
Normal file
111
Manager.Data/Contexts/AuditInterceptor.cs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Manager.Data.Entities.Audit;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||||
|
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||||
|
|
||||||
|
namespace Manager.Data.Contexts;
|
||||||
|
|
||||||
|
public class AuditInterceptor : SaveChangesInterceptor
|
||||||
|
{
|
||||||
|
public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
|
||||||
|
{
|
||||||
|
AddHistory(eventData.Context);
|
||||||
|
return base.SavingChanges(eventData, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
|
||||||
|
DbContextEventData eventData,
|
||||||
|
InterceptionResult<int> result,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
AddHistory(eventData.Context);
|
||||||
|
return base.SavingChangesAsync(eventData, result, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddHistory(DbContext? context)
|
||||||
|
{
|
||||||
|
if (context == null) return;
|
||||||
|
|
||||||
|
var entries = context.ChangeTracker.Entries()
|
||||||
|
.Where(e => e.State is EntityState.Modified or EntityState.Deleted or EntityState.Added && Attribute.IsDefined(e.Entity.GetType(),
|
||||||
|
typeof(AuditableAttribute)));
|
||||||
|
|
||||||
|
var histories = new List<EntityHistory>();
|
||||||
|
|
||||||
|
foreach (var entry in entries)
|
||||||
|
{
|
||||||
|
var primaryKey = entry.Properties.First(p => p.Metadata.IsPrimaryKey()).CurrentValue?.ToString() ?? "Unknown";
|
||||||
|
|
||||||
|
var declaredProperties = entry.Entity.GetType()
|
||||||
|
.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
|
||||||
|
.Where(p => !Attribute.IsDefined(p.DeclaringType!, typeof(NoAuditAttribute)))
|
||||||
|
.Select(p => p.Name)
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
var allowedProperties = entry.Properties.Where(p => declaredProperties.Contains(p.Metadata.Name));
|
||||||
|
|
||||||
|
switch (entry.State)
|
||||||
|
{
|
||||||
|
case EntityState.Added:
|
||||||
|
histories.AddRange(allowedProperties
|
||||||
|
.Where(p => p.CurrentValue != null)
|
||||||
|
.Select(p => CreateHistory(entry, p, entry.State, primaryKey))
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case EntityState.Modified:
|
||||||
|
histories.AddRange(allowedProperties
|
||||||
|
.Where(p => p.IsModified)
|
||||||
|
.Select(p => CreateHistory(entry, p, entry.State, primaryKey))
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case EntityState.Deleted:
|
||||||
|
histories.AddRange(allowedProperties
|
||||||
|
.Select(p => CreateHistory(entry, p, entry.State, primaryKey))
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (histories.Count != 0)
|
||||||
|
{
|
||||||
|
context.Set<EntityHistory>().AddRange(histories);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private EntityHistory CreateHistory(EntityEntry entry, PropertyEntry prop, EntityState changeType, string? primaryKey)
|
||||||
|
{
|
||||||
|
return new EntityHistory
|
||||||
|
{
|
||||||
|
EntityName = entry.Entity.GetType().Name,
|
||||||
|
EntityId = primaryKey ?? "Unknown",
|
||||||
|
PropertyName = prop.Metadata.Name,
|
||||||
|
OldValue = SerializeValue(prop.OriginalValue),
|
||||||
|
NewValue = SerializeValue(prop.CurrentValue),
|
||||||
|
ModifiedUtc = DateTime.UtcNow,
|
||||||
|
ChangedBy = "SYSTEM",
|
||||||
|
ChangeType = changeType
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly JsonSerializerOptions _jsonSerializerOptions = new()
|
||||||
|
{
|
||||||
|
WriteIndented = false,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||||
|
};
|
||||||
|
|
||||||
|
private string? SerializeValue(object? value)
|
||||||
|
{
|
||||||
|
if (value == null) return null;
|
||||||
|
|
||||||
|
var type = value.GetType();
|
||||||
|
|
||||||
|
if (type.IsPrimitive || type == typeof(string) || type == typeof(DateTime) || type == typeof(decimal))
|
||||||
|
{
|
||||||
|
return value.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonSerializer.Serialize(value, _jsonSerializerOptions);
|
||||||
|
}
|
||||||
|
}
|
38
Manager.Data/Contexts/DateInterceptor.cs
Normal file
38
Manager.Data/Contexts/DateInterceptor.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using Manager.Data.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||||
|
|
||||||
|
namespace Manager.Data.Contexts;
|
||||||
|
|
||||||
|
public class DateInterceptor : SaveChangesInterceptor
|
||||||
|
{
|
||||||
|
public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
|
||||||
|
{
|
||||||
|
UpdateEntryDates(eventData.Context);
|
||||||
|
return base.SavingChanges(eventData, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result,
|
||||||
|
CancellationToken cancellationToken = new())
|
||||||
|
{
|
||||||
|
UpdateEntryDates(eventData.Context);
|
||||||
|
return base.SavingChangesAsync(eventData, result, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateEntryDates(DbContext? context)
|
||||||
|
{
|
||||||
|
if (context == null) return;
|
||||||
|
|
||||||
|
var entries = context.ChangeTracker.Entries().Where(x => x is { Entity: DateTimeBase, State: EntityState.Added or EntityState.Modified });
|
||||||
|
|
||||||
|
foreach (var entity in entries)
|
||||||
|
{
|
||||||
|
((DateTimeBase)entity.Entity).LastModifiedUtc = DateTime.UtcNow;
|
||||||
|
|
||||||
|
if (entity.State == EntityState.Added)
|
||||||
|
{
|
||||||
|
((DateTimeBase)entity.Entity).CreatedAtUtc = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
using Manager.Data.Entities;
|
using Manager.Data.Entities.Audit;
|
||||||
using Manager.Data.Entities.LibraryContext;
|
using Manager.Data.Entities.LibraryContext;
|
||||||
using Manager.Data.Entities.LibraryContext.Join;
|
using Manager.Data.Entities.LibraryContext.Join;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -13,6 +13,8 @@ public sealed class LibraryDbContext : DbContext
|
|||||||
ChangeTracker.LazyLoadingEnabled = false;
|
ChangeTracker.LazyLoadingEnabled = false;
|
||||||
Database.EnsureCreated();
|
Database.EnsureCreated();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DbSet<EntityHistory> Histories { get; set; }
|
||||||
|
|
||||||
public DbSet<CaptionEntity> Captions { get; set; }
|
public DbSet<CaptionEntity> Captions { get; set; }
|
||||||
public DbSet<ChannelEntity> Channels { get; set; }
|
public DbSet<ChannelEntity> Channels { get; set; }
|
||||||
@@ -21,9 +23,20 @@ public sealed class LibraryDbContext : DbContext
|
|||||||
public DbSet<MediaEntity> Media { get; set; }
|
public DbSet<MediaEntity> Media { get; set; }
|
||||||
public DbSet<MediaFormatEntity> MediaFormats { get; set; }
|
public DbSet<MediaFormatEntity> MediaFormats { get; set; }
|
||||||
public DbSet<PlaylistEntity> Playlists { get; set; }
|
public DbSet<PlaylistEntity> Playlists { get; set; }
|
||||||
|
// Other media (images)?
|
||||||
|
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
|
{
|
||||||
|
optionsBuilder.AddInterceptors(new DateInterceptor(), new AuditInterceptor());
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
|
modelBuilder.Entity<EntityHistory>(eh =>
|
||||||
|
{
|
||||||
|
eh.ToTable("entity_history");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<CaptionEntity>(ce =>
|
modelBuilder.Entity<CaptionEntity>(ce =>
|
||||||
{
|
{
|
||||||
ce.ToTable("captions");
|
ce.ToTable("captions");
|
||||||
@@ -101,31 +114,4 @@ public sealed class LibraryDbContext : DbContext
|
|||||||
|
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override int SaveChanges()
|
|
||||||
{
|
|
||||||
UpdateEntryDates();
|
|
||||||
return base.SaveChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new())
|
|
||||||
{
|
|
||||||
UpdateEntryDates();
|
|
||||||
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateEntryDates()
|
|
||||||
{
|
|
||||||
var entries = ChangeTracker.Entries().Where(x => x is { Entity: DateTimeBase, State: EntityState.Added or EntityState.Modified });
|
|
||||||
|
|
||||||
foreach (var entity in entries)
|
|
||||||
{
|
|
||||||
((DateTimeBase)entity.Entity).LastModifiedUtc = DateTime.UtcNow;
|
|
||||||
|
|
||||||
if (entity.State == EntityState.Added)
|
|
||||||
{
|
|
||||||
((DateTimeBase)entity.Entity).CreatedAtUtc = DateTime.UtcNow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
9
Manager.Data/Entities/Audit/AuditableAttribute.cs
Normal file
9
Manager.Data/Entities/Audit/AuditableAttribute.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Manager.Data.Entities.Audit;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Make all properties in the entity audible, if they are changed this will be stored as a history in the db.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class)]
|
||||||
|
public class AuditableAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
22
Manager.Data/Entities/Audit/EntityHistory.cs
Normal file
22
Manager.Data/Entities/Audit/EntityHistory.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Manager.Data.Entities.Audit;
|
||||||
|
|
||||||
|
public class EntityHistory
|
||||||
|
{
|
||||||
|
[MaxLength(200)]
|
||||||
|
public required string EntityName { get; set; }
|
||||||
|
[MaxLength(200)]
|
||||||
|
public required string EntityId { get; set; }
|
||||||
|
[MaxLength(200)]
|
||||||
|
public required string PropertyName { get; set; }
|
||||||
|
[MaxLength(1000)]
|
||||||
|
public string? OldValue { get; set; }
|
||||||
|
[MaxLength(1000)]
|
||||||
|
public string? NewValue { get; set; }
|
||||||
|
public DateTime ModifiedUtc { get; set; } = DateTime.UtcNow;
|
||||||
|
[MaxLength(200)]
|
||||||
|
public string? ChangedBy { get; set; }
|
||||||
|
public EntityState ChangeType { get; set; }
|
||||||
|
}
|
9
Manager.Data/Entities/Audit/NoAuditAttribute.cs
Normal file
9
Manager.Data/Entities/Audit/NoAuditAttribute.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Manager.Data.Entities.Audit;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies to ignore the properties in the entity to not audit.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class)]
|
||||||
|
public class NoAuditAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
@@ -1,5 +1,8 @@
|
|||||||
|
using Manager.Data.Entities.Audit;
|
||||||
|
|
||||||
namespace Manager.Data.Entities;
|
namespace Manager.Data.Entities;
|
||||||
|
|
||||||
|
[NoAudit]
|
||||||
public abstract class DateTimeBase
|
public abstract class DateTimeBase
|
||||||
{
|
{
|
||||||
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Manager.Data.Entities.Audit;
|
||||||
|
|
||||||
namespace Manager.Data.Entities.LibraryContext;
|
namespace Manager.Data.Entities.LibraryContext;
|
||||||
|
|
||||||
|
[Auditable]
|
||||||
public class CaptionEntity : DateTimeBase
|
public class CaptionEntity : DateTimeBase
|
||||||
{
|
{
|
||||||
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)]
|
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)]
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Manager.Data.Entities.Audit;
|
||||||
|
|
||||||
namespace Manager.Data.Entities.LibraryContext;
|
namespace Manager.Data.Entities.LibraryContext;
|
||||||
|
|
||||||
|
[Auditable]
|
||||||
public class ChannelEntity : DateTimeBase
|
public class ChannelEntity : DateTimeBase
|
||||||
{
|
{
|
||||||
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)]
|
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)]
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Manager.Data.Entities.Audit;
|
||||||
|
|
||||||
namespace Manager.Data.Entities.LibraryContext;
|
namespace Manager.Data.Entities.LibraryContext;
|
||||||
|
|
||||||
|
[Auditable]
|
||||||
public class ClientAccountEntity : DateTimeBase
|
public class ClientAccountEntity : DateTimeBase
|
||||||
{
|
{
|
||||||
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)]
|
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)]
|
||||||
|
@@ -1,9 +1,13 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Manager.Data.Entities.Audit;
|
||||||
|
|
||||||
namespace Manager.Data.Entities.LibraryContext;
|
namespace Manager.Data.Entities.LibraryContext;
|
||||||
|
|
||||||
|
[Auditable]
|
||||||
public class HttpCookieEntity : DateTimeBase
|
public class HttpCookieEntity : DateTimeBase
|
||||||
{
|
{
|
||||||
|
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)]
|
||||||
|
public required string ClientId { get; set; }
|
||||||
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)]
|
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)]
|
||||||
public required string Name { get; set; }
|
public required string Name { get; set; }
|
||||||
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)]
|
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)]
|
||||||
@@ -15,6 +19,4 @@ public class HttpCookieEntity : DateTimeBase
|
|||||||
public DateTimeOffset? ExpiresUtc { get; set; }
|
public DateTimeOffset? ExpiresUtc { get; set; }
|
||||||
public bool Secure { get; set; }
|
public bool Secure { get; set; }
|
||||||
public bool HttpOnly { get; set; }
|
public bool HttpOnly { get; set; }
|
||||||
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)]
|
|
||||||
public required string ClientId { get; set; }
|
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Manager.Data.Entities.Audit;
|
||||||
using Manager.Data.Entities.LibraryContext.Join;
|
using Manager.Data.Entities.LibraryContext.Join;
|
||||||
|
|
||||||
namespace Manager.Data.Entities.LibraryContext;
|
namespace Manager.Data.Entities.LibraryContext;
|
||||||
|
|
||||||
|
[Auditable]
|
||||||
public class MediaEntity : DateTimeBase
|
public class MediaEntity : DateTimeBase
|
||||||
{
|
{
|
||||||
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)]
|
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)]
|
||||||
@@ -17,7 +19,6 @@ public class MediaEntity : DateTimeBase
|
|||||||
public List<MediaFormatEntity> Formats { get; set; } = [];
|
public List<MediaFormatEntity> Formats { get; set; } = [];
|
||||||
public List<CaptionEntity> Captions { get; set; } = [];
|
public List<CaptionEntity> Captions { get; set; } = [];
|
||||||
public List<PlaylistMedia> PlaylistMedias { get; set; } = [];
|
public List<PlaylistMedia> PlaylistMedias { get; set; } = [];
|
||||||
|
|
||||||
public MediaExternalState ExternalState { get; set; } = MediaExternalState.Online;
|
public MediaExternalState ExternalState { get; set; } = MediaExternalState.Online;
|
||||||
public bool IsDownloaded { get; set; }
|
public bool IsDownloaded { get; set; }
|
||||||
public MediaState State { get; set; } = MediaState.Indexed;
|
public MediaState State { get; set; } = MediaState.Indexed;
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Manager.Data.Entities.Audit;
|
||||||
using Manager.Data.Entities.LibraryContext.Join;
|
using Manager.Data.Entities.LibraryContext.Join;
|
||||||
|
|
||||||
namespace Manager.Data.Entities.LibraryContext;
|
namespace Manager.Data.Entities.LibraryContext;
|
||||||
|
|
||||||
|
[Auditable]
|
||||||
public class PlaylistEntity : DateTimeBase
|
public class PlaylistEntity : DateTimeBase
|
||||||
{
|
{
|
||||||
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)]
|
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)]
|
||||||
|
@@ -31,4 +31,9 @@ public static class NetworkService
|
|||||||
return ResultError.Error(e);
|
return ResultError.Error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<Result<byte[]>> DownloadBytesAsync(HttpRequestMessage request, YouTubeClient client)
|
||||||
|
{
|
||||||
|
return ResultError.Fail("Not implemented");
|
||||||
|
}
|
||||||
}
|
}
|
@@ -204,7 +204,7 @@ public sealed class YouTubeClient : IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
HttpClient?.Dispose();
|
HttpClient.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Result<string>> GetCurrentAccountIdAsync()
|
private async Task<Result<string>> GetCurrentAccountIdAsync()
|
||||||
|
Reference in New Issue
Block a user