Browse Source

async fixes (#128)

* refactor local metadata provider

* resolve async warnings

* more async fixes
pull/130/head
Jason Dove 4 years ago committed by GitHub
parent
commit
d4a2197dfa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs
  2. 9
      ErsatzTV.Application/ErsatzTV.Application.csproj
  3. 85
      ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs
  4. 10
      ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettingsHandler.cs
  5. 2
      ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionCustomOrderHandler.cs
  6. 2
      ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionHandler.cs
  7. 14
      ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSourcesHandler.cs
  8. 9
      ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj
  9. 2
      ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs
  10. 9
      ErsatzTV.Core/ErsatzTV.Core.csproj
  11. 9
      ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs
  12. 4
      ErsatzTV.Core/LanguageExtensions.cs
  13. 487
      ErsatzTV.Core/Metadata/LocalMetadataProvider.cs
  14. 40
      ErsatzTV.Core/Metadata/Nfo/MovieNfo.cs
  15. 36
      ErsatzTV.Core/Metadata/Nfo/MusicVideoNfo.cs
  16. 29
      ErsatzTV.Core/Metadata/Nfo/TvShowEpisodeNfo.cs
  17. 36
      ErsatzTV.Core/Metadata/Nfo/TvShowNfo.cs
  18. 4
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  19. 24
      ErsatzTV.Infrastructure/Data/DbInitializer.cs
  20. 4
      ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs
  21. 8
      ErsatzTV.Infrastructure/Data/Repositories/ConfigElementRepository.cs
  22. 4
      ErsatzTV.Infrastructure/Data/Repositories/FFmpegProfileRepository.cs
  23. 4
      ErsatzTV.Infrastructure/Data/Repositories/PlayoutRepository.cs
  24. 9
      ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj
  25. 8
      ErsatzTV.Infrastructure/Images/ImageCache.cs
  26. 1
      ErsatzTV.sln.DotSettings
  27. 9
      ErsatzTV/ErsatzTV.csproj
  28. 10
      ErsatzTV/Extensions/EitherToActionResult.cs
  29. 106
      ErsatzTV/Extensions/HostExtensions.cs
  30. 2
      ErsatzTV/Extensions/ListToActionResult.cs
  31. 4
      ErsatzTV/Extensions/OptionToActionResult.cs
  32. 4
      ErsatzTV/Extensions/ValidationToActionResult.cs
  33. 15
      ErsatzTV/Pages/FragmentNavigationBase.cs
  34. 11
      ErsatzTV/Program.cs
  35. 64
      ErsatzTV/Services/RunOnce/CacheCleanerService.cs
  36. 38
      ErsatzTV/Services/RunOnce/DatabaseMigratorService.cs
  37. 51
      ErsatzTV/Services/SchedulerService.cs
  38. 5
      ErsatzTV/Startup.cs

2
ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs

@ -82,7 +82,7 @@ namespace ErsatzTV.Application.Channels.Commands @@ -82,7 +82,7 @@ namespace ErsatzTV.Application.Channels.Commands
private async Task<Validation<BaseError, string>> ValidateNumber(UpdateChannel updateChannel)
{
Option<Channel> match = await _channelRepository.GetByNumber(updateChannel.Number);
int matchId = match.Map(c => c.Id).IfNone(updateChannel.ChannelId);
int matchId = await match.Map(c => c.Id).IfNoneAsync(updateChannel.ChannelId);
if (matchId == updateChannel.ChannelId)
{
if (Regex.IsMatch(updateChannel.Number, Channel.NumberValidator))

9
ErsatzTV.Application/ErsatzTV.Application.csproj

@ -2,11 +2,20 @@ @@ -2,11 +2,20 @@
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<NoWarn>VSTHRD200</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AsyncFixer" Version="1.5.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MediatR" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="16.9.60">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Winista.MimeDetect" Version="1.0.1" />
</ItemGroup>

85
ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs

@ -71,87 +71,32 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands @@ -71,87 +71,32 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
private async Task<Unit> ApplyUpdate(UpdateFFmpegSettings request)
{
await _configElementRepository.Get(ConfigElementKey.FFmpegPath).Match(
ce =>
{
ce.Value = request.Settings.FFmpegPath;
_configElementRepository.Update(ce);
},
() =>
{
var ce = new ConfigElement
{ Key = ConfigElementKey.FFmpegPath.Key, Value = request.Settings.FFmpegPath };
_configElementRepository.Add(ce);
});
await _configElementRepository.Get(ConfigElementKey.FFprobePath).Match(
ce =>
{
ce.Value = request.Settings.FFprobePath;
_configElementRepository.Update(ce);
},
() =>
{
var ce = new ConfigElement
{ Key = ConfigElementKey.FFprobePath.Key, Value = request.Settings.FFprobePath };
_configElementRepository.Add(ce);
});
await _configElementRepository.Get(ConfigElementKey.FFmpegDefaultProfileId).Match(
ce =>
{
ce.Value = request.Settings.DefaultFFmpegProfileId.ToString();
_configElementRepository.Update(ce);
},
() =>
{
var ce = new ConfigElement
{
Key = ConfigElementKey.FFmpegDefaultProfileId.Key,
Value = request.Settings.DefaultFFmpegProfileId.ToString()
};
_configElementRepository.Add(ce);
});
await _configElementRepository.Get(ConfigElementKey.FFmpegSaveReports).Match(
ce =>
{
ce.Value = request.Settings.SaveReports.ToString();
_configElementRepository.Update(ce);
},
() =>
{
var ce = new ConfigElement
{
Key = ConfigElementKey.FFmpegSaveReports.Key,
Value = request.Settings.SaveReports.ToString()
};
_configElementRepository.Add(ce);
});
await Upsert(ConfigElementKey.FFmpegPath, request.Settings.FFmpegPath);
await Upsert(ConfigElementKey.FFprobePath, request.Settings.FFprobePath);
await Upsert(ConfigElementKey.FFmpegDefaultProfileId, request.Settings.DefaultFFmpegProfileId.ToString());
await Upsert(ConfigElementKey.FFmpegSaveReports, request.Settings.SaveReports.ToString());
if (request.Settings.SaveReports && !Directory.Exists(FileSystemLayout.FFmpegReportsFolder))
{
Directory.CreateDirectory(FileSystemLayout.FFmpegReportsFolder);
}
await _configElementRepository.Get(ConfigElementKey.FFmpegPreferredLanguageCode).Match(
await Upsert(ConfigElementKey.FFmpegPreferredLanguageCode, request.Settings.PreferredLanguageCode);
return Unit.Default;
}
private Task Upsert(ConfigElementKey key, string value) =>
_configElementRepository.Get(key).Match(
ce =>
{
ce.Value = request.Settings.PreferredLanguageCode;
_configElementRepository.Update(ce);
ce.Value = value;
return _configElementRepository.Update(ce);
},
() =>
{
var ce = new ConfigElement
{
Key = ConfigElementKey.FFmpegPreferredLanguageCode.Key,
Value = request.Settings.PreferredLanguageCode
};
_configElementRepository.Add(ce);
var ce = new ConfigElement { Key = key.Key, Value = value };
return _configElementRepository.Add(ce);
});
return Unit.Default;
}
}
}

10
ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettingsHandler.cs

@ -29,11 +29,11 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries @@ -29,11 +29,11 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
return new FFmpegSettingsViewModel
{
FFmpegPath = ffmpegPath.IfNone(string.Empty),
FFprobePath = ffprobePath.IfNone(string.Empty),
DefaultFFmpegProfileId = defaultFFmpegProfileId.IfNone(0),
SaveReports = saveReports.IfNone(false),
PreferredLanguageCode = preferredLanguageCode.IfNone("eng")
FFmpegPath = await ffmpegPath.IfNoneAsync(string.Empty),
FFprobePath = await ffprobePath.IfNoneAsync(string.Empty),
DefaultFFmpegProfileId = await defaultFFmpegProfileId.IfNoneAsync(0),
SaveReports = await saveReports.IfNoneAsync(false),
PreferredLanguageCode = await preferredLanguageCode.IfNoneAsync("eng")
};
}
}

2
ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionCustomOrderHandler.cs

@ -39,7 +39,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands @@ -39,7 +39,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
Option<CollectionItem> maybeCollectionItem =
c.CollectionItems.FirstOrDefault(ci => ci.MediaItemId == updateItem.MediaItemId);
maybeCollectionItem.IfSome(ci => ci.CustomIndex = updateItem.CustomIndex);
await maybeCollectionItem.IfSomeAsync(ci => ci.CustomIndex = updateItem.CustomIndex);
}
if (await _mediaCollectionRepository.Update(c))

2
ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionHandler.cs

@ -32,7 +32,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands @@ -32,7 +32,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
private async Task<Unit> ApplyUpdateRequest(Collection c, UpdateCollection request)
{
c.Name = request.Name;
request.UseCustomPlaybackOrder.IfSome(
await request.UseCustomPlaybackOrder.IfSomeAsync(
useCustomPlaybackOrder => c.UseCustomPlaybackOrder = useCustomPlaybackOrder);
if (await _mediaCollectionRepository.Update(c) && request.UseCustomPlaybackOrder.IsSome)
{

14
ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSourcesHandler.cs

@ -57,11 +57,11 @@ namespace ErsatzTV.Application.Plex.Commands @@ -57,11 +57,11 @@ namespace ErsatzTV.Application.Plex.Commands
return allExisting;
}
private async Task SynchronizeServer(List<PlexMediaSource> allExisting, PlexMediaSource server)
private Task SynchronizeServer(List<PlexMediaSource> allExisting, PlexMediaSource server)
{
Option<PlexMediaSource> maybeExisting =
allExisting.Find(s => s.ClientIdentifier == server.ClientIdentifier);
await maybeExisting.Match(
return maybeExisting.Match(
existing =>
{
existing.Platform = server.Platform;
@ -84,15 +84,5 @@ namespace ErsatzTV.Application.Plex.Commands @@ -84,15 +84,5 @@ namespace ErsatzTV.Application.Plex.Commands
await _mediaSourceRepository.Add(server);
});
}
private void MergeConnections(
List<PlexConnection> existing,
List<PlexConnection> incoming)
{
var toAdd = incoming.Filter(connection => existing.All(c => c.Uri != connection.Uri)).ToList();
var toRemove = existing.Filter(connection => incoming.All(c => c.Uri != connection.Uri)).ToList();
existing.AddRange(toAdd);
toRemove.ForEach(c => existing.Remove(c));
}
}
}

9
ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj

@ -2,13 +2,22 @@ @@ -2,13 +2,22 @@
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<NoWarn>VSTHRD200</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AsyncFixer" Version="1.5.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="LanguageExt.Core" Version="3.4.15" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="16.9.60">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="NUnit" Version="3.13.1" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />

2
ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs

@ -57,7 +57,7 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -57,7 +57,7 @@ namespace ErsatzTV.Core.Tests.Metadata
.Returns<string, MediaItem>((_, _) => Right<BaseError, bool>(true).AsTask());
// fallback metadata adds metadata to a movie, so we need to replicate that here
_localMetadataProvider.Setup(x => x.RefreshFallbackMetadata(It.IsAny<MediaItem>()))
_localMetadataProvider.Setup(x => x.RefreshFallbackMetadata(It.IsAny<Movie>()))
.Returns(
(MediaItem mediaItem) =>
{

9
ErsatzTV.Core/ErsatzTV.Core.csproj

@ -2,12 +2,21 @@ @@ -2,12 +2,21 @@
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<NoWarn>VSTHRD200</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AsyncFixer" Version="1.5.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="LanguageExt.Core" Version="3.4.15" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="16.9.60">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />

9
ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs

@ -8,9 +8,12 @@ namespace ErsatzTV.Core.Interfaces.Metadata @@ -8,9 +8,12 @@ namespace ErsatzTV.Core.Interfaces.Metadata
{
Task<ShowMetadata> GetMetadataForShow(string showFolder);
Task<Option<MusicVideoMetadata>> GetMetadataForMusicVideo(string filePath);
Task<bool> RefreshSidecarMetadata(MediaItem mediaItem, string path);
Task<bool> RefreshSidecarMetadata(Show televisionShow, string showFolder);
Task<bool> RefreshFallbackMetadata(MediaItem mediaItem);
Task<bool> RefreshSidecarMetadata(Movie movie, string nfoFileName);
Task<bool> RefreshSidecarMetadata(Show televisionShow, string nfoFileName);
Task<bool> RefreshSidecarMetadata(Episode episode, string nfoFileName);
Task<bool> RefreshSidecarMetadata(MusicVideo musicVideo, string nfoFileName);
Task<bool> RefreshFallbackMetadata(Movie movie);
Task<bool> RefreshFallbackMetadata(Episode episode);
Task<bool> RefreshFallbackMetadata(Show televisionShow, string showFolder);
}
}

4
ErsatzTV.Core/LanguageExtensions.cs

@ -1,9 +1,11 @@ @@ -1,9 +1,11 @@
using System.Threading.Tasks;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using LanguageExt;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core
{
[SuppressMessage("ReSharper", "VSTHRD003")]
public static class LanguageExtensions
{
public static Either<BaseError, TR> ToEither<TR>(this Validation<BaseError, TR> validation) =>

487
ErsatzTV.Core/Metadata/LocalMetadataProvider.cs

@ -7,6 +7,7 @@ using System.Xml.Serialization; @@ -7,6 +7,7 @@ using System.Xml.Serialization;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata.Nfo;
using LanguageExt;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
@ -86,40 +87,35 @@ namespace ErsatzTV.Core.Metadata @@ -86,40 +87,35 @@ namespace ErsatzTV.Core.Metadata
});
}
public Task<bool> RefreshSidecarMetadata(MediaItem mediaItem, string path) =>
mediaItem switch
{
Episode e => LoadMetadata(e, path)
.Bind(
maybeMetadata => maybeMetadata.Match(
metadata => ApplyMetadataUpdate(e, metadata),
() => Task.FromResult(false))),
Movie m => LoadMetadata(m, path)
.Bind(
maybeMetadata => maybeMetadata.Match(
metadata => ApplyMetadataUpdate(m, metadata),
() => Task.FromResult(false))),
MusicVideo mv => LoadMetadata(mv, path)
.Bind(
maybeMetadata => maybeMetadata.Match(
metadata => ApplyMetadataUpdate(mv, metadata),
() => Task.FromResult(false))),
_ => Task.FromResult(false)
};
public Task<bool> RefreshSidecarMetadata(Show televisionShow, string showFolder) =>
LoadMetadata(televisionShow, showFolder).Bind(
public Task<bool> RefreshSidecarMetadata(Movie movie, string nfoFileName) =>
LoadMovieMetadata(movie, nfoFileName).Bind(
maybeMetadata => maybeMetadata.Match(
metadata => ApplyMetadataUpdate(movie, metadata),
() => Task.FromResult(false)));
public Task<bool> RefreshSidecarMetadata(Show televisionShow, string nfoFileName) =>
LoadTelevisionShowMetadata(nfoFileName).Bind(
maybeMetadata => maybeMetadata.Match(
metadata => ApplyMetadataUpdate(televisionShow, metadata),
() => Task.FromResult(false)));
public Task<bool> RefreshFallbackMetadata(MediaItem mediaItem) =>
mediaItem switch
{
Episode e => ApplyMetadataUpdate(e, _fallbackMetadataProvider.GetFallbackMetadata(e)),
Movie m => ApplyMetadataUpdate(m, _fallbackMetadataProvider.GetFallbackMetadata(m)),
_ => Task.FromResult(false)
};
public Task<bool> RefreshSidecarMetadata(Episode episode, string nfoFileName) =>
LoadEpisodeMetadata(episode, nfoFileName).Bind(
maybeMetadata => maybeMetadata.Match(
metadata => ApplyMetadataUpdate(episode, metadata),
() => Task.FromResult(false)));
public Task<bool> RefreshSidecarMetadata(MusicVideo musicVideo, string nfoFileName) =>
LoadMusicVideoMetadata(nfoFileName).Bind(
maybeMetadata => maybeMetadata.Match(
metadata => ApplyMetadataUpdate(musicVideo, metadata),
() => Task.FromResult(false)));
public Task<bool> RefreshFallbackMetadata(Movie movie) =>
ApplyMetadataUpdate(movie, _fallbackMetadataProvider.GetFallbackMetadata(movie));
public Task<bool> RefreshFallbackMetadata(Episode episode) =>
ApplyMetadataUpdate(episode, _fallbackMetadataProvider.GetFallbackMetadata(episode));
public Task<bool> RefreshFallbackMetadata(Show televisionShow, string showFolder) =>
ApplyMetadataUpdate(televisionShow, _fallbackMetadataProvider.GetFallbackMetadataForShow(showFolder));
@ -205,8 +201,6 @@ namespace ErsatzTV.Core.Metadata @@ -205,8 +201,6 @@ namespace ErsatzTV.Core.Metadata
Optional(movie.MovieMetadata).Flatten().HeadOrNone().Match(
async existing =>
{
var updated = false;
existing.Outline = metadata.Outline;
existing.Plot = metadata.Plot;
existing.Tagline = metadata.Tagline;
@ -226,67 +220,12 @@ namespace ErsatzTV.Core.Metadata @@ -226,67 +220,12 @@ namespace ErsatzTV.Core.Metadata
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
: metadata.SortTitle;
foreach (Genre genre in existing.Genres.Filter(g => metadata.Genres.All(g2 => g2.Name != g.Name))
.ToList())
{
existing.Genres.Remove(genre);
if (await _metadataRepository.RemoveGenre(genre))
{
updated = true;
}
}
foreach (Genre genre in metadata.Genres.Filter(g => existing.Genres.All(g2 => g2.Name != g.Name))
.ToList())
{
existing.Genres.Add(genre);
if (await _movieRepository.AddGenre(existing, genre))
{
updated = true;
}
}
foreach (Tag tag in existing.Tags.Filter(t => metadata.Tags.All(t2 => t2.Name != t.Name))
.ToList())
{
existing.Tags.Remove(tag);
if (await _metadataRepository.RemoveTag(tag))
{
updated = true;
}
}
foreach (Tag tag in metadata.Tags.Filter(t => existing.Tags.All(t2 => t2.Name != t.Name))
.ToList())
{
existing.Tags.Add(tag);
if (await _movieRepository.AddTag(existing, tag))
{
updated = true;
}
}
foreach (Studio studio in existing.Studios
.Filter(s => metadata.Studios.All(s2 => s2.Name != s.Name))
.ToList())
{
existing.Studios.Remove(studio);
if (await _metadataRepository.RemoveStudio(studio))
{
updated = true;
}
}
foreach (Studio studio in metadata.Studios
.Filter(s => existing.Studios.All(s2 => s2.Name != s.Name))
.ToList())
{
existing.Studios.Add(studio);
if (await _movieRepository.AddStudio(existing, studio))
{
updated = true;
}
}
bool updated = await UpdateMetadataCollections(
existing,
metadata,
_movieRepository.AddGenre,
_movieRepository.AddTag,
_movieRepository.AddStudio);
return await _metadataRepository.Update(existing) || updated;
},
@ -305,8 +244,6 @@ namespace ErsatzTV.Core.Metadata @@ -305,8 +244,6 @@ namespace ErsatzTV.Core.Metadata
Optional(show.ShowMetadata).Flatten().HeadOrNone().Match(
async existing =>
{
var updated = false;
existing.Outline = metadata.Outline;
existing.Plot = metadata.Plot;
existing.Tagline = metadata.Tagline;
@ -326,67 +263,12 @@ namespace ErsatzTV.Core.Metadata @@ -326,67 +263,12 @@ namespace ErsatzTV.Core.Metadata
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
: metadata.SortTitle;
foreach (Genre genre in existing.Genres.Filter(g => metadata.Genres.All(g2 => g2.Name != g.Name))
.ToList())
{
existing.Genres.Remove(genre);
if (await _metadataRepository.RemoveGenre(genre))
{
updated = true;
}
}
foreach (Genre genre in metadata.Genres.Filter(g => existing.Genres.All(g2 => g2.Name != g.Name))
.ToList())
{
existing.Genres.Add(genre);
if (await _televisionRepository.AddGenre(existing, genre))
{
updated = true;
}
}
foreach (Tag tag in existing.Tags.Filter(t => metadata.Tags.All(t2 => t2.Name != t.Name))
.ToList())
{
existing.Tags.Remove(tag);
if (await _metadataRepository.RemoveTag(tag))
{
updated = true;
}
}
foreach (Tag tag in metadata.Tags.Filter(t => existing.Tags.All(t2 => t2.Name != t.Name))
.ToList())
{
existing.Tags.Add(tag);
if (await _televisionRepository.AddTag(existing, tag))
{
updated = true;
}
}
foreach (Studio studio in existing.Studios
.Filter(s => metadata.Studios.All(s2 => s2.Name != s.Name))
.ToList())
{
existing.Studios.Remove(studio);
if (await _metadataRepository.RemoveStudio(studio))
{
updated = true;
}
}
foreach (Studio studio in metadata.Studios
.Filter(s => existing.Studios.All(s2 => s2.Name != s.Name))
.ToList())
{
existing.Studios.Add(studio);
if (await _televisionRepository.AddStudio(existing, studio))
{
updated = true;
}
}
bool updated = await UpdateMetadataCollections(
existing,
metadata,
_televisionRepository.AddGenre,
_televisionRepository.AddTag,
_televisionRepository.AddStudio);
return await _metadataRepository.Update(existing) || updated;
},
@ -405,8 +287,6 @@ namespace ErsatzTV.Core.Metadata @@ -405,8 +287,6 @@ namespace ErsatzTV.Core.Metadata
Optional(musicVideo.MusicVideoMetadata).Flatten().HeadOrNone().Match(
async existing =>
{
var updated = false;
existing.Artist = metadata.Artist;
existing.Title = metadata.Title;
existing.Year = metadata.Year;
@ -426,67 +306,12 @@ namespace ErsatzTV.Core.Metadata @@ -426,67 +306,12 @@ namespace ErsatzTV.Core.Metadata
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
: metadata.SortTitle;
foreach (Genre genre in existing.Genres.Filter(g => metadata.Genres.All(g2 => g2.Name != g.Name))
.ToList())
{
existing.Genres.Remove(genre);
if (await _metadataRepository.RemoveGenre(genre))
{
updated = true;
}
}
foreach (Genre genre in metadata.Genres.Filter(g => existing.Genres.All(g2 => g2.Name != g.Name))
.ToList())
{
existing.Genres.Add(genre);
if (await _musicVideoRepository.AddGenre(existing, genre))
{
updated = true;
}
}
foreach (Tag tag in existing.Tags.Filter(t => metadata.Tags.All(t2 => t2.Name != t.Name))
.ToList())
{
existing.Tags.Remove(tag);
if (await _metadataRepository.RemoveTag(tag))
{
updated = true;
}
}
foreach (Tag tag in metadata.Tags.Filter(t => existing.Tags.All(t2 => t2.Name != t.Name))
.ToList())
{
existing.Tags.Add(tag);
if (await _musicVideoRepository.AddTag(existing, tag))
{
updated = true;
}
}
foreach (Studio studio in existing.Studios
.Filter(s => metadata.Studios.All(s2 => s2.Name != s.Name))
.ToList())
{
existing.Studios.Remove(studio);
if (await _metadataRepository.RemoveStudio(studio))
{
updated = true;
}
}
foreach (Studio studio in metadata.Studios
.Filter(s => existing.Studios.All(s2 => s2.Name != s.Name))
.ToList())
{
existing.Studios.Add(studio);
if (await _musicVideoRepository.AddStudio(existing, studio))
{
updated = true;
}
}
bool updated = await UpdateMetadataCollections(
existing,
metadata,
_musicVideoRepository.AddGenre,
_musicVideoRepository.AddTag,
_musicVideoRepository.AddStudio);
return await _metadataRepository.Update(existing) || updated;
},
@ -501,50 +326,6 @@ namespace ErsatzTV.Core.Metadata @@ -501,50 +326,6 @@ namespace ErsatzTV.Core.Metadata
return await _metadataRepository.Add(metadata);
});
private async Task<Option<MovieMetadata>> LoadMetadata(Movie mediaItem, string nfoFileName)
{
if (nfoFileName == null || !File.Exists(nfoFileName))
{
_logger.LogDebug("NFO file does not exist at {Path}", nfoFileName);
return None;
}
return await LoadMovieMetadata(mediaItem, nfoFileName);
}
private async Task<Option<Tuple<EpisodeMetadata, int>>> LoadMetadata(Episode mediaItem, string nfoFileName)
{
if (nfoFileName == null || !File.Exists(nfoFileName))
{
_logger.LogDebug("NFO file does not exist at {Path}", nfoFileName);
return None;
}
return await LoadEpisodeMetadata(mediaItem, nfoFileName);
}
private async Task<Option<ShowMetadata>> LoadMetadata(Show televisionShow, string nfoFileName)
{
if (nfoFileName == null || !File.Exists(nfoFileName))
{
_logger.LogDebug("NFO file does not exist at {Path}", nfoFileName);
return None;
}
return await LoadTelevisionShowMetadata(nfoFileName);
}
private async Task<Option<MusicVideoMetadata>> LoadMetadata(MusicVideo musicVideo, string nfoFileName)
{
if (nfoFileName == null || !File.Exists(nfoFileName))
{
_logger.LogDebug("NFO file does not exist at {Path}", nfoFileName);
return None;
}
return await LoadMusicVideoMetadata(nfoFileName);
}
private async Task<Option<ShowMetadata>> LoadTelevisionShowMetadata(string nfoFileName)
{
try
@ -605,7 +386,7 @@ namespace ErsatzTV.Core.Metadata @@ -605,7 +386,7 @@ namespace ErsatzTV.Core.Metadata
}
}
private async Task<Option<MovieMetadata>> LoadMovieMetadata(Movie mediaItem, string nfoFileName)
private async Task<Option<MovieMetadata>> LoadMovieMetadata(Movie movie, string nfoFileName)
{
try
{
@ -632,7 +413,7 @@ namespace ErsatzTV.Core.Metadata @@ -632,7 +413,7 @@ namespace ErsatzTV.Core.Metadata
catch (Exception ex)
{
_logger.LogInformation(ex, "Failed to read Movie nfo metadata from {Path}", nfoFileName);
return _fallbackMetadataProvider.GetFallbackMetadata(mediaItem);
return _fallbackMetadataProvider.GetFallbackMetadata(movie);
}
}
@ -668,125 +449,79 @@ namespace ErsatzTV.Core.Metadata @@ -668,125 +449,79 @@ namespace ErsatzTV.Core.Metadata
return DateTime.TryParse(aired, out DateTime parsed) ? parsed : fallback;
}
[XmlRoot("movie")]
public class MovieNfo
private async Task<bool> UpdateMetadataCollections<T>(
T existing,
T incoming,
Func<T, Genre, Task<bool>> addGenre,
Func<T, Tag, Task<bool>> addTag,
Func<T, Studio, Task<bool>> addStudio)
where T : Domain.Metadata
{
[XmlElement("title")]
public string Title { get; set; }
[XmlElement("outline")]
public string Outline { get; set; }
var updated = false;
[XmlElement("year")]
public int Year { get; set; }
[XmlElement("mpaa")]
public string ContentRating { get; set; }
[XmlElement("premiered")]
public DateTime Premiered { get; set; }
[XmlElement("plot")]
public string Plot { get; set; }
[XmlElement("tagline")]
public string Tagline { get; set; }
[XmlElement("genre")]
public List<string> Genres { get; set; }
[XmlElement("tag")]
public List<string> Tags { get; set; }
[XmlElement("studio")]
public List<string> Studios { get; set; }
}
[XmlRoot("tvshow")]
public class TvShowNfo
{
[XmlElement("title")]
public string Title { get; set; }
[XmlElement("year")]
public int Year { get; set; }
[XmlElement("plot")]
public string Plot { get; set; }
[XmlElement("outline")]
public string Outline { get; set; }
[XmlElement("tagline")]
public string Tagline { get; set; }
[XmlElement("premiered")]
public string Premiered { get; set; }
[XmlElement("genre")]
public List<string> Genres { get; set; }
[XmlElement("tag")]
public List<string> Tags { get; set; }
[XmlElement("studio")]
public List<string> Studios { get; set; }
}
[XmlRoot("episodedetails")]
public class TvShowEpisodeNfo
{
[XmlElement("showtitle")]
public string ShowTitle { get; set; }
[XmlElement("title")]
public string Title { get; set; }
[XmlElement("episode")]
public int Episode { get; set; }
[XmlElement("season")]
public int Season { get; set; }
[XmlElement("mpaa")]
public string ContentRating { get; set; }
[XmlElement("aired")]
public string Aired { get; set; }
[XmlElement("plot")]
public string Plot { get; set; }
}
[XmlRoot("musicvideo")]
public class MusicVideoNfo
{
[XmlElement("artist")]
public string Artist { get; set; }
[XmlElement("title")]
public string Title { get; set; }
[XmlElement("album")]
public string Album { get; set; }
foreach (Genre genre in existing.Genres.Filter(g => incoming.Genres.All(g2 => g2.Name != g.Name))
.ToList())
{
existing.Genres.Remove(genre);
if (await _metadataRepository.RemoveGenre(genre))
{
updated = true;
}
}
[XmlElement("plot")]
public string Plot { get; set; }
foreach (Genre genre in incoming.Genres.Filter(g => existing.Genres.All(g2 => g2.Name != g.Name))
.ToList())
{
existing.Genres.Add(genre);
if (await addGenre(existing, genre))
{
updated = true;
}
}
[XmlElement("premiered")]
public string Premiered { get; set; }
foreach (Tag tag in existing.Tags.Filter(t => incoming.Tags.All(t2 => t2.Name != t.Name))
.ToList())
{
existing.Tags.Remove(tag);
if (await _metadataRepository.RemoveTag(tag))
{
updated = true;
}
}
[XmlElement("year")]
public int Year { get; set; }
foreach (Tag tag in incoming.Tags.Filter(t => existing.Tags.All(t2 => t2.Name != t.Name))
.ToList())
{
existing.Tags.Add(tag);
if (await addTag(existing, tag))
{
updated = true;
}
}
[XmlElement("genre")]
public List<string> Genres { get; set; }
foreach (Studio studio in existing.Studios
.Filter(s => incoming.Studios.All(s2 => s2.Name != s.Name))
.ToList())
{
existing.Studios.Remove(studio);
if (await _metadataRepository.RemoveStudio(studio))
{
updated = true;
}
}
[XmlElement("tag")]
public List<string> Tags { get; set; }
foreach (Studio studio in incoming.Studios
.Filter(s => existing.Studios.All(s2 => s2.Name != s.Name))
.ToList())
{
existing.Studios.Add(studio);
if (await addStudio(existing, studio))
{
updated = true;
}
}
[XmlElement("studio")]
public List<string> Studios { get; set; }
return updated;
}
}
}

40
ErsatzTV.Core/Metadata/Nfo/MovieNfo.cs

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Xml.Serialization;
namespace ErsatzTV.Core.Metadata.Nfo
{
[XmlRoot("movie")]
public class MovieNfo
{
[XmlElement("title")]
public string Title { get; set; }
[XmlElement("outline")]
public string Outline { get; set; }
[XmlElement("year")]
public int Year { get; set; }
[XmlElement("mpaa")]
public string ContentRating { get; set; }
[XmlElement("premiered")]
public DateTime Premiered { get; set; }
[XmlElement("plot")]
public string Plot { get; set; }
[XmlElement("tagline")]
public string Tagline { get; set; }
[XmlElement("genre")]
public List<string> Genres { get; set; }
[XmlElement("tag")]
public List<string> Tags { get; set; }
[XmlElement("studio")]
public List<string> Studios { get; set; }
}
}

36
ErsatzTV.Core/Metadata/Nfo/MusicVideoNfo.cs

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.Xml.Serialization;
namespace ErsatzTV.Core.Metadata.Nfo
{
[XmlRoot("musicvideo")]
public class MusicVideoNfo
{
[XmlElement("artist")]
public string Artist { get; set; }
[XmlElement("title")]
public string Title { get; set; }
[XmlElement("album")]
public string Album { get; set; }
[XmlElement("plot")]
public string Plot { get; set; }
[XmlElement("premiered")]
public string Premiered { get; set; }
[XmlElement("year")]
public int Year { get; set; }
[XmlElement("genre")]
public List<string> Genres { get; set; }
[XmlElement("tag")]
public List<string> Tags { get; set; }
[XmlElement("studio")]
public List<string> Studios { get; set; }
}
}

29
ErsatzTV.Core/Metadata/Nfo/TvShowEpisodeNfo.cs

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
using System.Xml.Serialization;
namespace ErsatzTV.Core.Metadata.Nfo
{
[XmlRoot("episodedetails")]
public class TvShowEpisodeNfo
{
[XmlElement("showtitle")]
public string ShowTitle { get; set; }
[XmlElement("title")]
public string Title { get; set; }
[XmlElement("episode")]
public int Episode { get; set; }
[XmlElement("season")]
public int Season { get; set; }
[XmlElement("mpaa")]
public string ContentRating { get; set; }
[XmlElement("aired")]
public string Aired { get; set; }
[XmlElement("plot")]
public string Plot { get; set; }
}
}

36
ErsatzTV.Core/Metadata/Nfo/TvShowNfo.cs

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.Xml.Serialization;
namespace ErsatzTV.Core.Metadata.Nfo
{
[XmlRoot("tvshow")]
public class TvShowNfo
{
[XmlElement("title")]
public string Title { get; set; }
[XmlElement("year")]
public int Year { get; set; }
[XmlElement("plot")]
public string Plot { get; set; }
[XmlElement("outline")]
public string Outline { get; set; }
[XmlElement("tagline")]
public string Tagline { get; set; }
[XmlElement("premiered")]
public string Premiered { get; set; }
[XmlElement("genre")]
public List<string> Genres { get; set; }
[XmlElement("tag")]
public List<string> Tags { get; set; }
[XmlElement("studio")]
public List<string> Studios { get; set; }
}
}

4
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -57,7 +57,7 @@ namespace ErsatzTV.Core.Scheduling @@ -57,7 +57,7 @@ namespace ErsatzTV.Core.Scheduling
case ProgramScheduleItemCollectionType.Collection:
Option<List<MediaItem>> maybeItems =
await _mediaCollectionRepository.GetItems(collectionKey.CollectionId ?? 0);
return Tuple(collectionKey, maybeItems.IfNone(new List<MediaItem>()));
return Tuple(collectionKey, await maybeItems.IfNoneAsync(new List<MediaItem>()));
case ProgramScheduleItemCollectionType.TelevisionShow:
List<Episode> showItems =
await _televisionRepository.GetShowItems(collectionKey.MediaItemId ?? 0);
@ -167,7 +167,7 @@ namespace ErsatzTV.Core.Scheduling @@ -167,7 +167,7 @@ namespace ErsatzTV.Core.Scheduling
durationFinish.IsSome);
IMediaCollectionEnumerator enumerator = collectionEnumerators[CollectionKeyForItem(scheduleItem)];
enumerator.Current.IfSome(
await enumerator.Current.IfSomeAsync(
mediaItem =>
{
_logger.LogDebug(

24
ErsatzTV.Infrastructure/Data/DbInitializer.cs

@ -1,6 +1,8 @@ @@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using LanguageExt;
@ -8,7 +10,7 @@ namespace ErsatzTV.Infrastructure.Data @@ -8,7 +10,7 @@ namespace ErsatzTV.Infrastructure.Data
{
public static class DbInitializer
{
public static Unit Initialize(TvContext context)
public static async Task<Unit> Initialize(TvContext context, CancellationToken cancellationToken)
{
if (context.Resolutions.Any())
{
@ -22,28 +24,28 @@ namespace ErsatzTV.Infrastructure.Data @@ -22,28 +24,28 @@ namespace ErsatzTV.Infrastructure.Data
new() { Id = 3, Name = "1920x1080", Width = 1920, Height = 1080 },
new() { Id = 4, Name = "3840x2160", Width = 3840, Height = 2160 }
};
context.Resolutions.AddRange(resolutions);
context.SaveChanges();
await context.Resolutions.AddRangeAsync(resolutions, cancellationToken);
await context.SaveChangesAsync(cancellationToken);
var resolutionConfig = new ConfigElement
{
Key = ConfigElementKey.FFmpegDefaultResolutionId.Key,
Value = "3" // 1920x1080
};
context.ConfigElements.Add(resolutionConfig);
context.SaveChanges();
await context.ConfigElements.AddAsync(resolutionConfig, cancellationToken);
await context.SaveChangesAsync(cancellationToken);
var defaultProfile = FFmpegProfile.New("1920x1080 x264 ac3", resolutions[2]);
context.FFmpegProfiles.Add(defaultProfile);
context.SaveChanges();
await context.FFmpegProfiles.AddAsync(defaultProfile, cancellationToken);
await context.SaveChangesAsync(cancellationToken);
var profileConfig = new ConfigElement
{
Key = ConfigElementKey.FFmpegDefaultProfileId.Key,
Value = defaultProfile.Id.ToString()
};
context.ConfigElements.Add(profileConfig);
context.SaveChanges();
await context.ConfigElements.AddAsync(profileConfig, cancellationToken);
await context.SaveChangesAsync(cancellationToken);
var defaultChannel = new Channel(Guid.NewGuid())
{
@ -52,8 +54,8 @@ namespace ErsatzTV.Infrastructure.Data @@ -52,8 +54,8 @@ namespace ErsatzTV.Infrastructure.Data
FFmpegProfile = defaultProfile,
StreamingMode = StreamingMode.TransportStream
};
context.Channels.Add(defaultChannel);
context.SaveChanges();
await context.Channels.AddAsync(defaultChannel, cancellationToken);
await context.SaveChangesAsync(cancellationToken);
// TODO: create looping static image that mentions configuring via web
return Unit.Default;

4
ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs

@ -64,10 +64,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -64,10 +64,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ThenInclude(mm => mm.Artwork)
.ToListAsync();
public async Task Update(Channel channel)
public Task Update(Channel channel)
{
_dbContext.Channels.Update(channel);
await _dbContext.SaveChangesAsync();
return _dbContext.SaveChangesAsync();
}
public async Task Delete(int channelId)

8
ErsatzTV.Infrastructure/Data/Repositories/ConfigElementRepository.cs

@ -31,16 +31,16 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -31,16 +31,16 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public Task<Option<T>> GetValue<T>(ConfigElementKey key) =>
Get(key).MapT(ce => (T) Convert.ChangeType(ce.Value, typeof(T)));
public async Task Update(ConfigElement configElement)
public Task Update(ConfigElement configElement)
{
_dbContext.ConfigElements.Update(configElement);
await _dbContext.SaveChangesAsync();
return _dbContext.SaveChangesAsync();
}
public async Task Delete(ConfigElement configElement)
public Task Delete(ConfigElement configElement)
{
_dbContext.ConfigElements.Remove(configElement);
await _dbContext.SaveChangesAsync();
return _dbContext.SaveChangesAsync();
}
}
}

4
ErsatzTV.Infrastructure/Data/Repositories/FFmpegProfileRepository.cs

@ -32,10 +32,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -32,10 +32,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.Include(p => p.Resolution)
.ToListAsync();
public async Task Update(FFmpegProfile ffmpegProfile)
public Task Update(FFmpegProfile ffmpegProfile)
{
_dbContext.FFmpegProfiles.Update(ffmpegProfile);
await _dbContext.SaveChangesAsync();
return _dbContext.SaveChangesAsync();
}
public async Task Delete(int ffmpegProfileId)

4
ErsatzTV.Infrastructure/Data/Repositories/PlayoutRepository.cs

@ -124,10 +124,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -124,10 +124,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ToListAsync();
}
public async Task Update(Playout playout)
public Task Update(Playout playout)
{
_dbContext.Playouts.Update(playout);
await _dbContext.SaveChangesAsync();
return _dbContext.SaveChangesAsync();
}
public async Task Delete(int playoutId)

9
ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj

@ -3,9 +3,14 @@ @@ -3,9 +3,14 @@
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<NoWarn>VSTHRD200</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AsyncFixer" Version="1.5.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Dapper" Version="2.0.78" />
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00013" />
<PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00013" />
@ -16,6 +21,10 @@ @@ -16,6 +21,10 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.4" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="16.9.60">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Refit" Version="6.0.38" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.3" />
</ItemGroup>

8
ErsatzTV.Infrastructure/Images/ImageCache.cs

@ -8,7 +8,6 @@ using ErsatzTV.Core.Domain; @@ -8,7 +8,6 @@ using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
using LanguageExt;
using Microsoft.Extensions.Logging;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
@ -19,15 +18,10 @@ namespace ErsatzTV.Infrastructure.Images @@ -19,15 +18,10 @@ namespace ErsatzTV.Infrastructure.Images
{
private static readonly SHA1CryptoServiceProvider Crypto;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<ImageCache> _logger;
static ImageCache() => Crypto = new SHA1CryptoServiceProvider();
public ImageCache(ILocalFileSystem localFileSystem, ILogger<ImageCache> logger)
{
_localFileSystem = localFileSystem;
_logger = logger;
}
public ImageCache(ILocalFileSystem localFileSystem) => _localFileSystem = localFileSystem;
public async Task<Either<BaseError, byte[]>> ResizeImage(byte[] imageBuffer, int height)
{

1
ErsatzTV.sln.DotSettings

@ -41,4 +41,5 @@ @@ -41,4 +41,5 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=setsar/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=tvshow/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Vaapi/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=xmltv/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=yadif/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

9
ErsatzTV/ErsatzTV.csproj

@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<NoWarn>VSTHRD200</NoWarn>
</PropertyGroup>
<ItemGroup>
@ -11,6 +12,10 @@ @@ -11,6 +12,10 @@
<ItemGroup>
<PackageReference Include="Accelist.FluentValidation.Blazor" Version="4.0.0" />
<PackageReference Include="AsyncFixer" Version="1.5.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentValidation" Version="9.5.3" />
<PackageReference Include="FluentValidation.AspNetCore" Version="9.5.3" />
<PackageReference Include="LanguageExt.Core" Version="3.4.15" />
@ -20,6 +25,10 @@ @@ -20,6 +25,10 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="16.9.60">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MudBlazor" Version="5.0.6" />
<PackageReference Include="Refit.HttpClientFactory" Version="6.0.24" />
<PackageReference Include="Serilog" Version="2.10.0" />

10
ErsatzTV/Extensions/EitherToActionResult.cs

@ -1,10 +1,12 @@ @@ -1,10 +1,12 @@
using System.Threading.Tasks;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using LanguageExt;
using LanguageExt.Common;
using Microsoft.AspNetCore.Mvc;
namespace ErsatzTV.Extensions
{
[SuppressMessage("ReSharper", "VSTHRD003")]
public static class EitherToActionResult
{
public static Task<IActionResult> ToActionResult<TL, TR>(this Task<Either<TL, TR>> either) => either.Map(Match);
@ -16,13 +18,13 @@ namespace ErsatzTV.Extensions @@ -16,13 +18,13 @@ namespace ErsatzTV.Extensions
Left: l => new BadRequestObjectResult(l),
Right: r => new OkObjectResult(r));
private static async Task<IActionResult> Match(Either<Error, Task> either) =>
await either.MatchAsync<IActionResult>(
private static Task<IActionResult> Match(Either<Error, Task> either) =>
either.Match<Task<IActionResult>>(
async t =>
{
await t;
return new OkResult();
},
e => new BadRequestObjectResult(e));
e => Task.FromResult((IActionResult) new BadRequestObjectResult(e)));
}
}

106
ErsatzTV/Extensions/HostExtensions.cs

@ -1,106 +0,0 @@ @@ -1,106 +0,0 @@
using System;
using System.IO;
using System.Linq;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Infrastructure.Data;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
namespace ErsatzTV.Extensions
{
public static class HostExtensions
{
public static IHost SeedDatabase(this IHost host)
{
Unit _ = use(() => host.Services.CreateScope(), Seed);
return host;
}
public static IHost CleanCacheFolder(this IHost host)
{
Unit _ = use(() => host.Services.CreateScope(), CleanCache);
return host;
}
private static Unit Seed(IServiceScope scope) =>
Try(() => scope.ServiceProvider)
.Bind(services => Try(GetDbContext(services)))
.Bind(ctx => Try(Migrate(ctx, scope.ServiceProvider)))
.Bind(ctx => Try(InitializeDb(ctx)))
.IfFail(
ex =>
{
LogException(
ex,
"Error occured while migrating database; shutting down.",
scope.ServiceProvider);
Environment.Exit(13);
return unit;
});
private static Unit CleanCache(IServiceScope scope) =>
Try(() => scope.ServiceProvider)
.Bind(services => Try(GetDbContext(services)))
.Bind(ctx => Try(CleanCache(ctx, scope.ServiceProvider)))
.IfFail(ex => LogException(ex, "Error occured while cleaning cache", scope.ServiceProvider));
private static TvContext GetDbContext(IServiceProvider provider) =>
provider.GetRequiredService<TvContext>();
private static TvContext Migrate(TvContext context, IServiceProvider provider)
{
ILogger<Program> logger = provider.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Applying database migrations");
context.Database.Migrate();
logger.LogInformation("Done applying database migrations");
return context;
}
private static Unit InitializeDb(TvContext context) =>
DbInitializer.Initialize(context);
private static Unit CleanCache(TvContext context, IServiceProvider provider)
{
if (Directory.Exists(FileSystemLayout.LegacyImageCacheFolder))
{
ILogger<Program> logger = provider.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Migrating channel logos from legacy image cache folder");
var logos = context.Channels
.SelectMany(c => c.Artwork)
.Where(a => a.ArtworkKind == ArtworkKind.Logo)
.Map(a => a.Path)
.ToList();
ILocalFileSystem localFileSystem = provider.GetRequiredService<ILocalFileSystem>();
foreach (string logo in logos)
{
string legacyPath = Path.Combine(FileSystemLayout.LegacyImageCacheFolder, logo);
if (File.Exists(legacyPath))
{
string subfolder = logo.Substring(0, 2);
string newPath = Path.Combine(FileSystemLayout.LogoCacheFolder, subfolder, logo);
localFileSystem.CopyFile(legacyPath, newPath);
}
}
logger.LogInformation("Deleting legacy image cache folder");
Directory.Delete(FileSystemLayout.LegacyImageCacheFolder, true);
}
return Unit.Default;
}
private static Unit LogException(Exception ex, string message, IServiceProvider provider)
{
provider.GetRequiredService<ILogger<Program>>().LogError(ex, message);
return unit;
}
}
}

2
ErsatzTV/Extensions/ListToActionResult.cs

@ -1,10 +1,12 @@ @@ -1,10 +1,12 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using LanguageExt;
using Microsoft.AspNetCore.Mvc;
namespace ErsatzTV.Extensions
{
[SuppressMessage("ReSharper", "VSTHRD003")]
public static class ListToActionResult
{
public static Task<IActionResult> ToActionResult<T>(this Task<List<T>> list) =>

4
ErsatzTV/Extensions/OptionToActionResult.cs

@ -1,9 +1,11 @@ @@ -1,9 +1,11 @@
using System.Threading.Tasks;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using LanguageExt;
using Microsoft.AspNetCore.Mvc;
namespace ErsatzTV.Extensions
{
[SuppressMessage("ReSharper", "VSTHRD003")]
public static class OptionToActionResult
{
public static IActionResult ToActionResult<T>(this Option<T> option) =>

4
ErsatzTV/Extensions/ValidationToActionResult.cs

@ -1,10 +1,12 @@ @@ -1,10 +1,12 @@
using System.Threading.Tasks;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using ErsatzTV.Core;
using LanguageExt;
using Microsoft.AspNetCore.Mvc;
namespace ErsatzTV.Extensions
{
[SuppressMessage("ReSharper", "VSTHRD003")]
public static class ValidationToActionResult
{
public static IActionResult ToActionResult<T>(this Validation<BaseError, T> validation) =>

15
ErsatzTV/Pages/FragmentNavigationBase.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using ErsatzTV.Extensions;
using Microsoft.AspNetCore.Components;
@ -27,7 +28,17 @@ namespace ErsatzTV.Pages @@ -27,7 +28,17 @@ namespace ErsatzTV.Pages
}
}
private async void TryFragmentNavigation(object sender, LocationChangedEventArgs args) =>
await NavManager.NavigateToFragmentAsync(JsRuntime);
[SuppressMessage("ReSharper", "VSTHRD100")]
private async void TryFragmentNavigation(object sender, LocationChangedEventArgs args)
{
try
{
await NavManager.NavigateToFragmentAsync(JsRuntime);
}
catch (Exception)
{
// ignored
}
}
}
}

11
ErsatzTV/Program.cs

@ -2,7 +2,6 @@ using System; @@ -2,7 +2,6 @@ using System;
using System.IO;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Extensions;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
@ -12,7 +11,7 @@ namespace ErsatzTV @@ -12,7 +11,7 @@ namespace ErsatzTV
{
public class Program
{
public static IConfiguration Configuration { get; } = new ConfigurationBuilder()
private static IConfiguration Configuration { get; } = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", false, true)
.AddJsonFile(
@ -31,11 +30,7 @@ namespace ErsatzTV @@ -31,11 +30,7 @@ namespace ErsatzTV
try
{
await CreateHostBuilder(args)
.Build()
.SeedDatabase()
.CleanCacheFolder()
.RunAsync();
await CreateHostBuilder(args).Build().RunAsync();
return 0;
}
catch (Exception ex)
@ -49,7 +44,7 @@ namespace ErsatzTV @@ -49,7 +44,7 @@ namespace ErsatzTV
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
private static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(
webBuilder => webBuilder.UseStartup<Startup>()

64
ErsatzTV/Services/RunOnce/CacheCleanerService.cs

@ -0,0 +1,64 @@ @@ -0,0 +1,64 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Services.RunOnce
{
public class CacheCleanerService : IHostedService
{
private readonly ILogger<CacheCleanerService> _logger;
private readonly IServiceScopeFactory _serviceScopeFactory;
public CacheCleanerService(
IServiceScopeFactory serviceScopeFactory,
ILogger<CacheCleanerService> logger)
{
_serviceScopeFactory = serviceScopeFactory;
_logger = logger;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
using IServiceScope scope = _serviceScopeFactory.CreateScope();
await using TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
if (Directory.Exists(FileSystemLayout.LegacyImageCacheFolder))
{
_logger.LogInformation("Migrating channel logos from legacy image cache folder");
List<string> logos = await dbContext.Channels
.SelectMany(c => c.Artwork)
.Where(a => a.ArtworkKind == ArtworkKind.Logo)
.Map(a => a.Path)
.ToListAsync(cancellationToken);
ILocalFileSystem localFileSystem = scope.ServiceProvider.GetRequiredService<ILocalFileSystem>();
foreach (string logo in logos)
{
string legacyPath = Path.Combine(FileSystemLayout.LegacyImageCacheFolder, logo);
if (File.Exists(legacyPath))
{
string subfolder = logo.Substring(0, 2);
string newPath = Path.Combine(FileSystemLayout.LogoCacheFolder, subfolder, logo);
await localFileSystem.CopyFile(legacyPath, newPath);
}
}
_logger.LogInformation("Deleting legacy image cache folder");
Directory.Delete(FileSystemLayout.LegacyImageCacheFolder, true);
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
}

38
ErsatzTV/Services/RunOnce/DatabaseMigratorService.cs

@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Services.RunOnce
{
public class DatabaseMigratorService : IHostedService
{
private readonly ILogger<DatabaseMigratorService> _logger;
private readonly IServiceScopeFactory _serviceScopeFactory;
public DatabaseMigratorService(
IServiceScopeFactory serviceScopeFactory,
ILogger<DatabaseMigratorService> logger)
{
_serviceScopeFactory = serviceScopeFactory;
_logger = logger;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Applying database migrations");
using IServiceScope scope = _serviceScopeFactory.CreateScope();
await using TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
await dbContext.Database.MigrateAsync(cancellationToken);
await DbInitializer.Initialize(dbContext, cancellationToken);
_logger.LogInformation("Done applying database migrations");
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
}

51
ErsatzTV/Services/SchedulerService.cs

@ -15,50 +15,55 @@ using ErsatzTV.Infrastructure.Data; @@ -15,50 +15,55 @@ using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Services
{
public class SchedulerService : IHostedService
public class SchedulerService : BackgroundService
{
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IEntityLocker _entityLocker;
private readonly ILogger<SchedulerService> _logger;
private readonly IServiceScopeFactory _serviceScopeFactory;
private Timer _timer;
public SchedulerService(
IServiceScopeFactory serviceScopeFactory,
ChannelWriter<IBackgroundServiceRequest> channel,
IEntityLocker entityLocker)
IEntityLocker entityLocker,
ILogger<SchedulerService> logger)
{
_serviceScopeFactory = serviceScopeFactory;
_channel = channel;
_entityLocker = entityLocker;
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
_timer = new Timer(
async _ => await DoWork(cancellationToken),
null,
TimeSpan.FromSeconds(0), // fire immediately
TimeSpan.FromHours(1)); // repeat every hour
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_timer?.Change(Timeout.Infinite, 0);
while (!cancellationToken.IsCancellationRequested)
{
if (!cancellationToken.IsCancellationRequested)
{
await DoWork(cancellationToken);
}
return Task.CompletedTask;
await Task.Delay(TimeSpan.FromHours(1), cancellationToken);
}
}
private async Task DoWork(CancellationToken cancellationToken)
{
await RebuildSearchIndex(cancellationToken);
await BuildPlayouts(cancellationToken);
await ScanLocalMediaSources(cancellationToken);
await ScanPlexMediaSources(cancellationToken);
try
{
await RebuildSearchIndex(cancellationToken);
await BuildPlayouts(cancellationToken);
await ScanLocalMediaSources(cancellationToken);
await ScanPlexMediaSources(cancellationToken);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error during scheduler run");
}
}
private async Task BuildPlayouts(CancellationToken cancellationToken)
@ -114,7 +119,7 @@ namespace ErsatzTV.Services @@ -114,7 +119,7 @@ namespace ErsatzTV.Services
}
}
private async Task RebuildSearchIndex(CancellationToken cancellationToken) =>
await _channel.WriteAsync(new RebuildSearchIndex(), cancellationToken);
private ValueTask RebuildSearchIndex(CancellationToken cancellationToken) =>
_channel.WriteAsync(new RebuildSearchIndex(), cancellationToken);
}
}

5
ErsatzTV/Startup.cs

@ -30,6 +30,7 @@ using ErsatzTV.Infrastructure.Runtime; @@ -30,6 +30,7 @@ using ErsatzTV.Infrastructure.Runtime;
using ErsatzTV.Infrastructure.Search;
using ErsatzTV.Serialization;
using ErsatzTV.Services;
using ErsatzTV.Services.RunOnce;
using FluentValidation.AspNetCore;
using MediatR;
using Microsoft.AspNetCore.Builder;
@ -90,7 +91,7 @@ namespace ErsatzTV @@ -90,7 +91,7 @@ namespace ErsatzTV
Log.Logger.Information(
"ErsatzTV version {Version}",
Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()
Assembly.GetEntryAssembly()?.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
?.InformationalVersion ?? "unknown");
Log.Logger.Warning("This is pre-alpha software and is likely to be unstable");
@ -220,6 +221,8 @@ namespace ErsatzTV @@ -220,6 +221,8 @@ namespace ErsatzTV
services.AddScoped<IFFmpegStreamSelector, FFmpegStreamSelector>();
services.AddScoped<FFmpegProcessService>();
services.AddHostedService<DatabaseMigratorService>();
services.AddHostedService<CacheCleanerService>();
services.AddHostedService<PlexService>();
services.AddHostedService<FFmpegLocatorService>();
services.AddHostedService<WorkerService>();

Loading…
Cancel
Save