Browse Source

cleanup artwork cache folder

pull/2779/head
Jason Dove 7 months ago
parent
commit
b0c5613256
No known key found for this signature in database
  1. 7
      CHANGELOG.md
  2. 150
      ErsatzTV.Application/Maintenance/Commands/DeleteOrphanedArtworkHandler.cs
  3. 13
      ErsatzTV/Controllers/Api/MaintenanceController.cs
  4. 4
      ErsatzTV/Services/SchedulerService.cs

7
CHANGELOG.md

@ -15,6 +15,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -15,6 +15,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `Software` - force software padding
- This can be used to work around buggy GPU driver behavior where padding is green instead of black
- This is most often seen with VAAPI acceleration (radeonsi or i965 drivers)
- Add API endpoint to clean artwork cache folder (on demand)
- POST `/api/maintenance/clean_artwork`
### Changed
- Disable automatic artwork database cleanup
- This will be re-enabled at some point in the future (after more testing)
- For now, the API should be used to clean as needed
### Fixed
- Use code signing on all Windows executables (`ErsatzTV-Windows.exe`, `ErsatzTV.exe`, `ErsatzTV.Scanner.exe`)

150
ErsatzTV.Application/Maintenance/Commands/DeleteOrphanedArtworkHandler.cs

@ -1,14 +1,150 @@ @@ -1,14 +1,150 @@
using ErsatzTV.Core;
using System.IO.Abstractions;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Application.Maintenance;
public class DeleteOrphanedArtworkHandler(IArtworkRepository artworkRepository)
public class DeleteOrphanedArtworkHandler(
IDbContextFactory<TvContext> dbContextFactory,
IArtworkRepository artworkRepository,
IFileSystem fileSystem,
ILogger<DeleteOrphanedArtworkHandler> logger)
: IRequestHandler<DeleteOrphanedArtwork, Either<BaseError, Unit>>
{
public Task<Either<BaseError, Unit>>
Handle(DeleteOrphanedArtwork request, CancellationToken cancellationToken) =>
artworkRepository.GetOrphanedArtworkIds()
.Bind(artworkRepository.Delete)
.Map(_ => Right<BaseError, Unit>(Unit.Default));
public async Task<Either<BaseError, Unit>> Handle(
DeleteOrphanedArtwork request,
CancellationToken cancellationToken)
{
try
{
await CleanUpDatabase();
await CleanUpFileSystem(cancellationToken);
return Unit.Default;
}
catch (Exception e)
{
return BaseError.New(e.Message);
}
}
private async Task CleanUpDatabase()
{
List<int> ids = await artworkRepository.GetOrphanedArtworkIds();
if (ids.Count > 0)
{
await artworkRepository.Delete(ids);
}
}
private async Task CleanUpFileSystem(CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
System.Collections.Generic.HashSet<string> validFiles = [];
var lastId = 0;
var hasMoreRows = true;
while (hasMoreRows)
{
List<MinimalArtwork> result = await dbContext.Artwork
.TagWithCallSite()
.AsNoTracking()
.Where(a => a.Id > lastId)
.OrderBy(a => a.Id)
.Take(1000)
.Select(a => new MinimalArtwork(a.Id, a.Path, a.BlurHash43, a.BlurHash54, a.BlurHash64))
.ToListAsync(cancellationToken);
int newLastId = lastId;
foreach (MinimalArtwork artwork in result)
{
newLastId = artwork.Id;
if (!artwork.Path.Contains('/'))
{
validFiles.Add(artwork.Path);
}
if (!string.IsNullOrWhiteSpace(artwork.BlurHash43))
{
validFiles.Add(artwork.BlurHash43);
}
if (!string.IsNullOrWhiteSpace(artwork.BlurHash54))
{
validFiles.Add(artwork.BlurHash54);
}
if (!string.IsNullOrWhiteSpace(artwork.BlurHash64))
{
validFiles.Add(artwork.BlurHash64);
}
}
if (lastId == newLastId)
{
hasMoreRows = false;
}
lastId = newLastId;
}
logger.LogDebug("Loaded {Count} artwork hashes (valid file names)", validFiles.Count);
var deleted = 0;
foreach (string file in fileSystem.Directory.EnumerateFiles(
FileSystemLayout.ArtworkCacheFolder,
"*.*",
SearchOption.AllDirectories))
{
string fileName = fileSystem.Path.GetFileName(file);
if (!validFiles.Contains(fileName))
{
try
{
fileSystem.File.Delete(file);
deleted++;
}
catch (Exception ex)
{
logger.LogWarning(ex, "Could not delete artwork file {File}", file);
}
}
}
logger.LogDebug("Deleted {Count} unused artwork cache files", deleted);
DeleteEmptySubfolders(FileSystemLayout.ArtworkCacheFolder);
}
private void DeleteEmptySubfolders(string path)
{
if (!fileSystem.Directory.Exists(path))
{
return;
}
foreach (string sub in fileSystem.Directory.GetDirectories(path))
{
DeleteEmptySubfolders(sub);
}
if (!fileSystem.Directory.EnumerateFileSystemEntries(path).Any())
{
try
{
fileSystem.Directory.Delete(path);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Could not delete empty cache folder {Folder}", path);
}
}
}
private sealed record MinimalArtwork(int Id, string Path, string BlurHash43, string BlurHash54, string BlurHash64);
}

13
ErsatzTV/Controllers/Api/MaintenanceController.cs

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
using System.Threading.Channels;
using ErsatzTV.Application;
using ErsatzTV.Application.Maintenance;
using ErsatzTV.Core;
using MediatR;
@ -7,7 +9,7 @@ namespace ErsatzTV.Controllers.Api; @@ -7,7 +9,7 @@ namespace ErsatzTV.Controllers.Api;
[ApiController]
[EndpointGroupName("general")]
public class MaintenanceController(IMediator mediator)
public class MaintenanceController(IMediator mediator, ChannelWriter<IBackgroundServiceRequest> workerChannel)
{
[HttpGet("/api/maintenance/gc")]
[Tags("Maintenance")]
@ -36,4 +38,13 @@ public class MaintenanceController(IMediator mediator) @@ -36,4 +38,13 @@ public class MaintenanceController(IMediator mediator)
return new OkResult();
}
[HttpPost("/api/maintenance/clean_artwork")]
[Tags("Maintenance")]
[EndpointSummary("Clean artwork cache")]
public async Task<IActionResult> CleanArtwork(CancellationToken cancellationToken)
{
await workerChannel.WriteAsync(new DeleteOrphanedArtwork(), cancellationToken);
return new OkResult();
}
}

4
ErsatzTV/Services/SchedulerService.cs

@ -119,7 +119,6 @@ public class SchedulerService : BackgroundService @@ -119,7 +119,6 @@ public class SchedulerService : BackgroundService
{
try
{
await DeleteOrphanedArtwork(cancellationToken);
await DeleteOrphanedSubtitles(cancellationToken);
await RefreshMpegTsScripts(cancellationToken);
await RefreshChannelGuideChannelList(cancellationToken);
@ -390,9 +389,6 @@ public class SchedulerService : BackgroundService @@ -390,9 +389,6 @@ public class SchedulerService : BackgroundService
private ValueTask RefreshGraphicsElements(CancellationToken cancellationToken) =>
_workerChannel.WriteAsync(new RefreshGraphicsElements(), cancellationToken);
private ValueTask DeleteOrphanedArtwork(CancellationToken cancellationToken) =>
_workerChannel.WriteAsync(new DeleteOrphanedArtwork(), cancellationToken);
private ValueTask DeleteOrphanedSubtitles(CancellationToken cancellationToken) =>
_workerChannel.WriteAsync(new DeleteOrphanedSubtitles(), cancellationToken);

Loading…
Cancel
Save