diff --git a/src/TeslaCamPlayer.BlazorHosted/Client/Pages/Index.razor.cs b/src/TeslaCamPlayer.BlazorHosted/Client/Pages/Index.razor.cs
index 9af40d6..7b50cdc 100644
--- a/src/TeslaCamPlayer.BlazorHosted/Client/Pages/Index.razor.cs
+++ b/src/TeslaCamPlayer.BlazorHosted/Client/Pages/Index.razor.cs
@@ -33,6 +33,7 @@ public partial class Index : ComponentBase
private bool _showFilter;
private bool _filterChanged;
private EventFilterValues _eventFilter = new();
+ private State _state = new();
protected override async Task OnInitializedAsync()
{
@@ -44,7 +45,7 @@ protected override async Task OnInitializedAsync()
protected override async Task OnAfterRenderAsync(bool firstRender)
{
- if (!_setDatePickerInitialDate && _filteredclips?.Any() == true && _datePicker != null)
+ if (!_setDatePickerInitialDate && _filteredclips?.Length > 0 && _datePicker != null)
{
_setDatePickerInitialDate = true;
var latestClip = _filteredclips.MaxBy(c => c.EndDate)!;
@@ -57,15 +58,43 @@ private async Task RefreshEventsAsync(bool refreshCache)
{
_filteredclips = null;
_clips = null;
+ _setDatePickerInitialDate = false;
await Task.Delay(10);
await InvokeAsync(StateHasChanged);
- _setDatePickerInitialDate = false;
- _clips = await HttpClient.GetFromNewtonsoftJsonAsync
("Api/GetClips?refreshCache=" + refreshCache);
+ if (!refreshCache)
+ {
+ _clips = await HttpClient.GetFromNewtonsoftJsonAsync("Api/GetClips.json");
+ if (_clips != null)
+ return;
+ }
+
+ if (refreshCache)
+ {
+ _state = new("Refreshing...", 1, 0);
+ await HttpClient.PostAsync("Api/RefreshClips", null);
+ }
+
+ await WaitForProcessing();
+ _clips = await HttpClient.GetFromNewtonsoftJsonAsync("Api/GetClips.json");
FilterClips();
}
+ private async Task WaitForProcessing()
+ {
+ while (true)
+ {
+ _state = await HttpClient.GetFromNewtonsoftJsonAsync("Api/GetState.json");
+ if (!_state.IsProcessing)
+ break;
+
+ _ = InvokeAsync(StateHasChanged);
+
+ await Task.Delay(1000);
+ }
+ }
+
private void FilterClips()
{
_filteredclips = (_clips ??= Array.Empty())
@@ -114,7 +143,7 @@ private static string[] GetClipIcons(Clip clip)
};
if (clip.Type == ClipType.Recent || clip.Type == ClipType.Unknown || clip.Event == null)
- return new[] { baseIcon };
+ return [baseIcon];
var secondIcon = clip.Event.Reason switch
{
@@ -128,7 +157,7 @@ private static string[] GetClipIcons(Clip clip)
if (clip.Event.Reason.StartsWith(CamEvents.SentryAwareAccelerationPrefix))
secondIcon = Icons.Material.Filled.OpenWith;
- return secondIcon == null ? new [] { baseIcon } : new[] { baseIcon, secondIcon };
+ return secondIcon == null ? [baseIcon] : [baseIcon, secondIcon];
}
private class ScrollToOptions
diff --git a/src/TeslaCamPlayer.BlazorHosted/Client/TeslaCamPlayer.BlazorHosted.Client.csproj b/src/TeslaCamPlayer.BlazorHosted/Client/TeslaCamPlayer.BlazorHosted.Client.csproj
index b19a2d9..3dd6c35 100644
--- a/src/TeslaCamPlayer.BlazorHosted/Client/TeslaCamPlayer.BlazorHosted.Client.csproj
+++ b/src/TeslaCamPlayer.BlazorHosted/Client/TeslaCamPlayer.BlazorHosted.Client.csproj
@@ -7,9 +7,9 @@
-
-
-
+
+
+
diff --git a/src/TeslaCamPlayer.BlazorHosted/Server/Controllers/ApiController.cs b/src/TeslaCamPlayer.BlazorHosted/Server/Controllers/ApiController.cs
index e9106c1..31a2bbe 100644
--- a/src/TeslaCamPlayer.BlazorHosted/Server/Controllers/ApiController.cs
+++ b/src/TeslaCamPlayer.BlazorHosted/Server/Controllers/ApiController.cs
@@ -7,7 +7,7 @@
namespace TeslaCamPlayer.BlazorHosted.Server.Controllers;
[ApiController]
-[Route("Api/[action]")]
+[Route("Api")]
public class ApiController : ControllerBase
{
private readonly IClipsService _clipsService;
@@ -19,18 +19,29 @@ public ApiController(ISettingsProvider settingsProvider, IClipsService clipsServ
_clipsService = clipsService;
}
- [HttpGet]
- public async Task GetClips(bool refreshCache = false)
- => await _clipsService.GetClipsAsync(refreshCache);
+ [HttpGet("GetClips.json")]
+ public async Task GetClips()
+ => await _clipsService.GetClipsAsync();
+
+ [HttpPost("RefreshClips")]
+ public IActionResult RefreshClips()
+ {
+ _clipsService.RefreshClips();
+ return Ok();
+ }
+
+ [HttpGet("GetState.json")]
+ public State GetState()
+ => _clipsService.State;
private bool IsUnderRootPath(string path)
=> path.StartsWith(_rootFullPath);
- [HttpGet("{path}.mp4")]
+ [HttpGet("Video/{path}.mp4")]
public IActionResult Video(string path)
=> ServeFile(path, ".mp4", "video/mp4", true);
- [HttpGet("{path}.png")]
+ [HttpGet("Thumbnail/{path}.png")]
public IActionResult Thumbnail(string path)
=> ServeFile(path, ".png", "image/png");
diff --git a/src/TeslaCamPlayer.BlazorHosted/Server/Program.cs b/src/TeslaCamPlayer.BlazorHosted/Server/Program.cs
index eed8bfc..b861f98 100644
--- a/src/TeslaCamPlayer.BlazorHosted/Server/Program.cs
+++ b/src/TeslaCamPlayer.BlazorHosted/Server/Program.cs
@@ -12,10 +12,12 @@
var builder = WebApplication.CreateBuilder(args);
-builder.Services.AddControllersWithViews().AddNewtonsoftJson();
+builder.Services
+ .AddControllers()
+ .AddNewtonsoftJson();
builder.Services.AddRazorPages();
builder.Services.AddSingleton();
-builder.Services.AddTransient();
+builder.Services.AddSingleton();
#if WINDOWS
builder.Services.AddTransient();
#elif DOCKER
@@ -55,7 +57,6 @@
app.UseRouting();
-
app.MapRazorPages();
app.MapControllers();
app.MapFallbackToFile("index.html");
diff --git a/src/TeslaCamPlayer.BlazorHosted/Server/Properties/launchSettings.json b/src/TeslaCamPlayer.BlazorHosted/Server/Properties/launchSettings.json
index 1711492..76f9334 100644
--- a/src/TeslaCamPlayer.BlazorHosted/Server/Properties/launchSettings.json
+++ b/src/TeslaCamPlayer.BlazorHosted/Server/Properties/launchSettings.json
@@ -12,7 +12,7 @@
},
"https": {
"commandName": "Project",
- "launchBrowser": false,
+ "launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
diff --git a/src/TeslaCamPlayer.BlazorHosted/Server/Services/ClipsService.cs b/src/TeslaCamPlayer.BlazorHosted/Server/Services/ClipsService.cs
index 347871c..a8f3174 100644
--- a/src/TeslaCamPlayer.BlazorHosted/Server/Services/ClipsService.cs
+++ b/src/TeslaCamPlayer.BlazorHosted/Server/Services/ClipsService.cs
@@ -1,9 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Text.RegularExpressions;
-using System.Threading.Tasks;
+using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Serilog;
using TeslaCamPlayer.BlazorHosted.Server.Providers.Interfaces;
@@ -18,10 +13,13 @@ public partial class ClipsService : IClipsService
private static readonly string CacheFilePath = Path.Combine(AppContext.BaseDirectory, "clips.json");
private static readonly Regex FileNameRegex = FileNameRegexGenerated();
- private static Clip[] _cache;
+ private static readonly SemaphoreSlim RefreshSemaphore = new(1, 1);
+ private static Clip[] _clips;
private readonly ISettingsProvider _settingsProvider;
private readonly IFfProbeService _ffProbeService;
+
+ public State State { get; private set; } = new();
public ClipsService(ISettingsProvider settingsProvider, IFfProbeService ffProbeService)
{
@@ -29,52 +27,106 @@ public ClipsService(ISettingsProvider settingsProvider, IFfProbeService ffProbeS
_ffProbeService = ffProbeService;
}
- private async Task GetCachedAsync()
- => File.Exists(CacheFilePath)
- ? JsonConvert.DeserializeObject(await File.ReadAllTextAsync(CacheFilePath))
- : null;
+ private static async Task GetCachedAsync()
+ {
+ try
+ {
+ return File.Exists(CacheFilePath)
+ ? JsonConvert.DeserializeObject(await File.ReadAllTextAsync(CacheFilePath))
+ : null;
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Failed to read cached clips data from {Path}", CacheFilePath);
+ return null;
+ }
+ }
- public async Task GetClipsAsync(bool refreshCache = false)
+ public async Task GetClipsAsync()
{
- _cache ??= await GetCachedAsync();
+ _clips ??= await GetCachedAsync();
- if (!refreshCache && _cache != null)
- return _cache;
+ if (_clips != null)
+ return _clips;
+
+ if (!State.IsProcessing)
+ State = new("", 1, 1);
+
+ _ = Task.Run(RefreshClips);
+ return null;
+ }
+
+ public async void RefreshClips()
+ {
+ var isRefreshing = RefreshSemaphore.CurrentCount == 0;
+ await RefreshSemaphore.WaitAsync();
+ if (isRefreshing)
+ return;
+
+ try
+ {
+ await RefreshClipsInternal();
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Failed to refresh clips");
+ }
+ finally
+ {
+ RefreshSemaphore.Release();
+ }
+ }
- _cache ??= [];
+ private async Task RefreshClipsInternal()
+ {
+ var knownClips = _clips ?? [];
- var knownVideoFiles = _cache
+ var knownVideoFiles = knownClips
.SelectMany(c => c.Segments.SelectMany(s => s.VideoFiles))
.Where(f => f != null)
.ToDictionary(v => v.FilePath, v => v);
- var videoFiles = (await Task.WhenAll(Directory
+ var files = Directory
.GetFiles(_settingsProvider.Settings.ClipsRootPath, "*.mp4", SearchOption.AllDirectories)
- .AsParallel()
.Select(path => new { Path = path, RegexMatch = FileNameRegex.Match(path) })
.Where(f => f.RegexMatch.Success)
- .ToList()
- .Select(async f => knownVideoFiles.TryGetValue(f.Path, out var knownVideo) ? knownVideo : await TryParseVideoFileAsync(f.Path, f.RegexMatch))))
- .AsParallel()
- .Where(vfi => vfi != null)
- .ToList();
+ .ToArray();
- var recentClips = GetRecentClips(videoFiles
- .Where(vfi => vfi.ClipType == ClipType.Recent).ToList());
-
- var clips = videoFiles
+ var videoFiles = new List(files.Length);
+ for (var i = 0; i < files.Length; i++)
+ {
+ State = new("Analyzing video files...", files.Length, i + 1);
+
+ var file = files[i];
+
+ if (knownVideoFiles.TryGetValue(file.Path, out var knownVideo))
+ videoFiles.Add(knownVideo);
+
+ var parsedFile = await TryParseVideoFileAsync(file.Path, file.RegexMatch);
+ if (parsedFile != null)
+ videoFiles.Add(parsedFile);
+ }
+
+ var recentClips = GetRecentClips(videoFiles.Where(vfi => vfi.ClipType == ClipType.Recent).ToList());
+
+ var distinctClips = videoFiles
.Select(vfi => vfi.EventFolderName)
+ .Where(f => !string.IsNullOrWhiteSpace(f))
.Distinct()
- .AsParallel()
- .Where(e => !string.IsNullOrWhiteSpace(e))
- .Select(e => ParseClip(e, videoFiles))
- .Concat(recentClips.AsParallel())
- .OrderByDescending(c => c.StartDate)
.ToArray();
+ var clips = new List();
+ for (var i = 0; i < distinctClips.Length; i++)
+ {
+ clips.Add(await ParseClip(distinctClips[i], videoFiles));
+ State = new("Reading event data...", distinctClips.Length, i + 1);
+ }
- _cache = clips;
- await File.WriteAllTextAsync(CacheFilePath, JsonConvert.SerializeObject(clips));
- return _cache;
+ _clips = clips
+ .Concat(recentClips)
+ .OrderByDescending(c => c.StartDate)
+ .ToArray();
+
+ await File.WriteAllTextAsync(CacheFilePath, JsonConvert.SerializeObject(_clips));
}
private static IEnumerable GetRecentClips(List recentVideoFiles)
@@ -190,7 +242,7 @@ private async Task ParseVideoFileAsync(string path, Match regexMatch)
};
}
- private static Clip ParseClip(string eventFolderName, IEnumerable videoFiles)
+ private static async Task ParseClip(string eventFolderName, IEnumerable videoFiles)
{
var eventVideoFiles = videoFiles
.AsParallel()
@@ -213,7 +265,7 @@ private static Clip ParseClip(string eventFolderName, IEnumerable vid
var eventFolderPath = Path.GetDirectoryName(eventVideoFiles.First().FilePath)!;
var expectedEventJsonPath = Path.Combine(eventFolderPath, "event.json");
- var eventInfo = TryReadEvent(expectedEventJsonPath);
+ var eventInfo = await TryReadEvent(expectedEventJsonPath);
var expectedEventThumbnailPath = Path.Combine(eventFolderPath, "thumb.png");
var thumbnailUrl = File.Exists(expectedEventThumbnailPath)
@@ -227,14 +279,14 @@ private static Clip ParseClip(string eventFolderName, IEnumerable vid
};
}
- private static Event TryReadEvent(string path)
+ private static async Task TryReadEvent(string path)
{
try
{
if (!File.Exists(path))
return null;
- var json = File.ReadAllText(path);
+ var json = await File.ReadAllTextAsync(path);
return JsonConvert.DeserializeObject(json);
}
catch (Exception e)
diff --git a/src/TeslaCamPlayer.BlazorHosted/Server/Services/Interfaces/IClipsService.cs b/src/TeslaCamPlayer.BlazorHosted/Server/Services/Interfaces/IClipsService.cs
index 47f210a..403fe1e 100644
--- a/src/TeslaCamPlayer.BlazorHosted/Server/Services/Interfaces/IClipsService.cs
+++ b/src/TeslaCamPlayer.BlazorHosted/Server/Services/Interfaces/IClipsService.cs
@@ -4,5 +4,7 @@ namespace TeslaCamPlayer.BlazorHosted.Server.Services.Interfaces;
public interface IClipsService
{
- Task GetClipsAsync(bool refreshCache = false);
+ Task GetClipsAsync();
+ State State { get; }
+ void RefreshClips();
}
\ No newline at end of file
diff --git a/src/TeslaCamPlayer.BlazorHosted/Server/TeslaCamPlayer.BlazorHosted.Server.csproj b/src/TeslaCamPlayer.BlazorHosted/Server/TeslaCamPlayer.BlazorHosted.Server.csproj
index e0124c5..8756f62 100644
--- a/src/TeslaCamPlayer.BlazorHosted/Server/TeslaCamPlayer.BlazorHosted.Server.csproj
+++ b/src/TeslaCamPlayer.BlazorHosted/Server/TeslaCamPlayer.BlazorHosted.Server.csproj
@@ -11,8 +11,8 @@
-
-
+
+
diff --git a/src/TeslaCamPlayer.BlazorHosted/Shared/Models/Cameras.cs b/src/TeslaCamPlayer.BlazorHosted/Shared/Models/Cameras.cs
index 8426794..1983be9 100644
--- a/src/TeslaCamPlayer.BlazorHosted/Shared/Models/Cameras.cs
+++ b/src/TeslaCamPlayer.BlazorHosted/Shared/Models/Cameras.cs
@@ -1,5 +1,17 @@
namespace TeslaCamPlayer.BlazorHosted.Shared.Models;
+///
+/// According to a random post on the internet
+/// 0 = front camera
+/// 1 = fisheye
+/// 2 = narrow
+/// 3 = left repeater
+/// 4 = right repeater
+/// 5 = left B pillar
+/// 6 = right B pillar
+/// 7 = rear
+/// 8 = cabin
+///
public enum Cameras {
Unknown = -1,
Front = 0,
diff --git a/src/TeslaCamPlayer.BlazorHosted/Shared/Models/Event.cs b/src/TeslaCamPlayer.BlazorHosted/Shared/Models/Event.cs
index cce2f1f..610d6c5 100644
--- a/src/TeslaCamPlayer.BlazorHosted/Shared/Models/Event.cs
+++ b/src/TeslaCamPlayer.BlazorHosted/Shared/Models/Event.cs
@@ -2,18 +2,6 @@
namespace TeslaCamPlayer.BlazorHosted.Shared.Models;
-///
-/// According to a random post on the internet
-/// 0 = front camera
-/// 1 = fisheye
-/// 2 = narrow
-/// 3 = left repeater
-/// 4 = right repeater
-/// 5 = left B pillar
-/// 6 = right B pillar
-/// 7 = rear
-/// 8 = cabin
-///
public class Event
{
[JsonProperty("timestamp")]
diff --git a/src/TeslaCamPlayer.BlazorHosted/Shared/Models/State.cs b/src/TeslaCamPlayer.BlazorHosted/Shared/Models/State.cs
new file mode 100644
index 0000000..c2ec2dd
--- /dev/null
+++ b/src/TeslaCamPlayer.BlazorHosted/Shared/Models/State.cs
@@ -0,0 +1,29 @@
+using System.Text.Json.Serialization;
+
+namespace TeslaCamPlayer.BlazorHosted.Shared.Models;
+
+public class State
+{
+ [JsonIgnore]
+ public bool IsProcessing => ProcessedFiles < ProcessingFiles;
+
+ [JsonIgnore]
+ public int Percentage => (int)((double)ProcessedFiles / ProcessingFiles * 100);
+
+ public string Description { get; set; }
+
+ public int ProcessingFiles { get; set; }
+
+ public int ProcessedFiles { get; set; }
+
+ public State(string description, int processingFiles, int processedFiles)
+ {
+ Description = description;
+ ProcessingFiles = processingFiles;
+ ProcessedFiles = processedFiles;
+ }
+
+ public State()
+ {
+ }
+}
\ No newline at end of file