mirror of https://github.com/ErsatzTV/ErsatzTV.git
7 changed files with 216 additions and 1 deletions
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Playouts; |
||||
|
||||
public record SyncNextPlayout(string ChannelNumber) : IRequest, IBackgroundServiceRequest; |
||||
@ -0,0 +1,205 @@
@@ -0,0 +1,205 @@
|
||||
using System.Globalization; |
||||
using System.IO.Abstractions; |
||||
using System.Runtime.InteropServices; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Extensions; |
||||
using ErsatzTV.Core.Interfaces.Metadata; |
||||
using ErsatzTV.Core.Next; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using Microsoft.Extensions.Logging; |
||||
|
||||
namespace ErsatzTV.Application.Playouts; |
||||
|
||||
public partial class SyncNextPlayoutHandler( |
||||
IFileSystem fileSystem, |
||||
ILocalFileSystem localFileSystem, |
||||
IDbContextFactory<TvContext> dbContextFactory, |
||||
ILogger<SyncNextPlayoutHandler> logger) |
||||
: IRequestHandler<SyncNextPlayout> |
||||
{ |
||||
[LibraryImport("libc", EntryPoint = "rename", SetLastError = true)] |
||||
private static partial int Rename( |
||||
[MarshalAs(UnmanagedType.LPUTF8Str)] |
||||
string oldpath, |
||||
[MarshalAs(UnmanagedType.LPUTF8Str)] |
||||
string newpath |
||||
); |
||||
|
||||
public async Task Handle(SyncNextPlayout request, CancellationToken cancellationToken) |
||||
{ |
||||
// TODO: NEXT: support junctions on Windows
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
// gen new folder name
|
||||
string versionFolderName = DateTimeOffset.Now.ToUnixTimeMilliseconds().ToString(CultureInfo.InvariantCulture); |
||||
|
||||
string versionFolder = fileSystem.Path.Combine( |
||||
FileSystemLayout.NextPlayoutsFolder, |
||||
request.ChannelNumber, |
||||
versionFolderName); |
||||
|
||||
logger.LogDebug("versioned playout folder is {Folder}", versionFolder); |
||||
|
||||
localFileSystem.EnsureFolderExists(versionFolder); |
||||
|
||||
await WriteAllJsonTo(request.ChannelNumber, versionFolder, cancellationToken); |
||||
|
||||
string currentFolder = fileSystem.Path.Combine( |
||||
FileSystemLayout.NextPlayoutsFolder, |
||||
request.ChannelNumber, |
||||
"current"); |
||||
|
||||
// re-point symlink/junction to new folder
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) |
||||
{ |
||||
} |
||||
else |
||||
{ |
||||
string tempLink = fileSystem.Path.Combine( |
||||
FileSystemLayout.NextPlayoutsFolder, |
||||
request.ChannelNumber, |
||||
fileSystem.Path.GetRandomFileName()); |
||||
|
||||
fileSystem.File.CreateSymbolicLink(tempLink, versionFolderName); |
||||
_ = Rename(tempLink, currentFolder); |
||||
} |
||||
|
||||
CleanOldVersions( |
||||
fileSystem.Path.Combine(FileSystemLayout.NextPlayoutsFolder, request.ChannelNumber), |
||||
currentFolder); |
||||
} |
||||
|
||||
private async Task WriteAllJsonTo(string channelNumber, string targetFolder, CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
|
||||
List<int> localLibraryIds = await dbContext.LocalLibraries |
||||
.AsNoTracking() |
||||
.Map(l => l.Id) |
||||
.ToListAsync(cancellationToken); |
||||
|
||||
List<PlayoutItem> playoutItems = await dbContext.PlayoutItems |
||||
.AsNoTracking() |
||||
.Where(i => i.Playout.Channel.Number == channelNumber) |
||||
.Where(i => localLibraryIds.Contains(i.MediaItem.LibraryPath.LibraryId)) |
||||
.Include(i => i.MediaItem) |
||||
.ThenInclude(i => (i as Episode).MediaVersions) |
||||
.ThenInclude(mv => mv.MediaFiles) |
||||
.Include(i => i.MediaItem) |
||||
.ThenInclude(i => (i as Movie).MediaVersions) |
||||
.ThenInclude(mv => mv.MediaFiles) |
||||
.Include(i => i.MediaItem) |
||||
.ThenInclude(i => (i as OtherVideo).MediaVersions) |
||||
.ThenInclude(mv => mv.MediaFiles) |
||||
.Include(i => i.MediaItem) |
||||
.ThenInclude(i => (i as MusicVideo).MediaVersions) |
||||
.ThenInclude(mv => mv.MediaFiles) |
||||
.ToListAsync(cancellationToken); |
||||
|
||||
logger.LogDebug("Located {Count} local playout items", playoutItems.Count); |
||||
|
||||
foreach (IGrouping<DateTime, PlayoutItem> group in playoutItems.GroupBy(pi => pi.StartOffset.Date) |
||||
.Where(g => g.Any())) |
||||
{ |
||||
var first = group.First(); |
||||
var last = group.Last(); |
||||
|
||||
string fileName = fileSystem.Path.Combine( |
||||
targetFolder, |
||||
$"{first.StartOffset.ToUnixTimeMilliseconds()}_{last.FinishOffset.ToUnixTimeMilliseconds()}.json"); |
||||
|
||||
var playout = new Core.Next.Playout { Version = "https://ersatztv.org/playout/version/0.0.1", Items = [] }; |
||||
foreach (PlayoutItem playoutItem in group) |
||||
{ |
||||
if (playoutItem.MediaItem is not Episode && playoutItem.MediaItem is not Movie && |
||||
playoutItem.MediaItem is not OtherVideo && playoutItem.MediaItem is not MusicVideo) |
||||
{ |
||||
continue; |
||||
} |
||||
|
||||
string path = playoutItem.MediaItem.GetHeadVersion().MediaFiles.Head().Path; |
||||
|
||||
var nextPlayoutItem = new ItemElement |
||||
{ |
||||
Id = playoutItem.Id.ToString(CultureInfo.InvariantCulture), |
||||
Start = playoutItem.StartOffset.ToString("O"), |
||||
Finish = playoutItem.FinishOffset.ToString("O"), |
||||
Source = new ItemSource |
||||
{ |
||||
SourceType = SourceType.Local, |
||||
Path = path, |
||||
} |
||||
}; |
||||
|
||||
playout.Items.Add(nextPlayoutItem); |
||||
} |
||||
|
||||
await fileSystem.File.WriteAllTextAsync(fileName, playout.ToJson(), cancellationToken); |
||||
} |
||||
} |
||||
|
||||
public void CleanOldVersions( |
||||
string playoutRoot, |
||||
string currentLinkPath, |
||||
int keepVersions = 2, |
||||
TimeSpan? gracePeriod = null) |
||||
{ |
||||
gracePeriod ??= TimeSpan.FromMinutes(5); |
||||
|
||||
string currentResolvedPath = null; |
||||
if (Directory.Exists(currentLinkPath)) |
||||
{ |
||||
currentResolvedPath = Path.GetFullPath( |
||||
Path.Combine( |
||||
Path.GetDirectoryName(currentLinkPath) ?? "", |
||||
Directory.ResolveLinkTarget(currentLinkPath, true)?.FullName ?? "" |
||||
)); |
||||
} |
||||
|
||||
var directories = Directory.GetDirectories(playoutRoot) |
||||
.Select(d => new DirectoryInfo(d)) |
||||
.Where(d => long.TryParse(d.Name, out _)) |
||||
.OrderByDescending(d => d.Name) |
||||
.ToList(); |
||||
|
||||
int keptCount = 0; |
||||
|
||||
foreach (var dir in directories) |
||||
{ |
||||
string fullDir = dir.FullName; |
||||
|
||||
if (fullDir.Equals(currentResolvedPath, StringComparison.OrdinalIgnoreCase)) |
||||
{ |
||||
keptCount++; |
||||
continue; |
||||
} |
||||
|
||||
if (keptCount < keepVersions) |
||||
{ |
||||
keptCount++; |
||||
continue; |
||||
} |
||||
|
||||
if (DateTime.Now - dir.LastWriteTime < gracePeriod) |
||||
{ |
||||
continue; |
||||
} |
||||
|
||||
try |
||||
{ |
||||
dir.Delete(recursive: true); |
||||
logger.LogDebug("Cleaned up old playout version: {Folder}", dir.Name); |
||||
} |
||||
catch (IOException) |
||||
{ |
||||
// ignore errors; will be cleaned up next time through
|
||||
logger.LogDebug("Skipping busy folder: {Folder}", dir.Name); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue