diff --git a/ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs b/ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs index 2a3f71bca..35f96abb0 100644 --- a/ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs +++ b/ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs @@ -82,7 +82,7 @@ namespace ErsatzTV.Application.Channels.Commands private async Task> ValidateNumber(UpdateChannel updateChannel) { Option 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)) diff --git a/ErsatzTV.Application/ErsatzTV.Application.csproj b/ErsatzTV.Application/ErsatzTV.Application.csproj index e8685b7b3..a820b4061 100644 --- a/ErsatzTV.Application/ErsatzTV.Application.csproj +++ b/ErsatzTV.Application/ErsatzTV.Application.csproj @@ -2,11 +2,20 @@ net5.0 + VSTHRD200 + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs index 9c5d421af..18a1c83ce 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs @@ -71,87 +71,32 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands private async Task 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( - ce => - { - ce.Value = request.Settings.PreferredLanguageCode; - _configElementRepository.Update(ce); - }, - () => - { - var ce = new ConfigElement - { - Key = ConfigElementKey.FFmpegPreferredLanguageCode.Key, - Value = request.Settings.PreferredLanguageCode - }; - _configElementRepository.Add(ce); - }); - + await Upsert(ConfigElementKey.FFmpegPreferredLanguageCode, request.Settings.PreferredLanguageCode); return Unit.Default; } + + private Task Upsert(ConfigElementKey key, string value) => + _configElementRepository.Get(key).Match( + ce => + { + ce.Value = value; + return _configElementRepository.Update(ce); + }, + () => + { + var ce = new ConfigElement { Key = key.Key, Value = value }; + return _configElementRepository.Add(ce); + }); } } diff --git a/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettingsHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettingsHandler.cs index b6f521acb..b3a502b29 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettingsHandler.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettingsHandler.cs @@ -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") }; } } diff --git a/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionCustomOrderHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionCustomOrderHandler.cs index fe79cfb2b..96937c47b 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionCustomOrderHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionCustomOrderHandler.cs @@ -39,7 +39,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands Option 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)) diff --git a/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionHandler.cs index faa7307b8..d006ee94a 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionHandler.cs @@ -32,7 +32,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands private async Task 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) { diff --git a/ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSourcesHandler.cs b/ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSourcesHandler.cs index 4d7e82b8a..a0541ad3e 100644 --- a/ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSourcesHandler.cs +++ b/ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSourcesHandler.cs @@ -57,11 +57,11 @@ namespace ErsatzTV.Application.Plex.Commands return allExisting; } - private async Task SynchronizeServer(List allExisting, PlexMediaSource server) + private Task SynchronizeServer(List allExisting, PlexMediaSource server) { Option 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 await _mediaSourceRepository.Add(server); }); } - - private void MergeConnections( - List existing, - List 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)); - } } } diff --git a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj index ef5ce747e..07cb429f5 100644 --- a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj +++ b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj @@ -2,13 +2,22 @@ net5.0 + VSTHRD200 + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs b/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs index 68fda2ffd..85f727d0f 100644 --- a/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs +++ b/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs @@ -57,7 +57,7 @@ namespace ErsatzTV.Core.Tests.Metadata .Returns((_, _) => Right(true).AsTask()); // fallback metadata adds metadata to a movie, so we need to replicate that here - _localMetadataProvider.Setup(x => x.RefreshFallbackMetadata(It.IsAny())) + _localMetadataProvider.Setup(x => x.RefreshFallbackMetadata(It.IsAny())) .Returns( (MediaItem mediaItem) => { diff --git a/ErsatzTV.Core/ErsatzTV.Core.csproj b/ErsatzTV.Core/ErsatzTV.Core.csproj index b9863adf9..cf455e810 100644 --- a/ErsatzTV.Core/ErsatzTV.Core.csproj +++ b/ErsatzTV.Core/ErsatzTV.Core.csproj @@ -2,12 +2,21 @@ net5.0 + VSTHRD200 + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs b/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs index 6299ac903..c4807d643 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs +++ b/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs @@ -8,9 +8,12 @@ namespace ErsatzTV.Core.Interfaces.Metadata { Task GetMetadataForShow(string showFolder); Task> GetMetadataForMusicVideo(string filePath); - Task RefreshSidecarMetadata(MediaItem mediaItem, string path); - Task RefreshSidecarMetadata(Show televisionShow, string showFolder); - Task RefreshFallbackMetadata(MediaItem mediaItem); + Task RefreshSidecarMetadata(Movie movie, string nfoFileName); + Task RefreshSidecarMetadata(Show televisionShow, string nfoFileName); + Task RefreshSidecarMetadata(Episode episode, string nfoFileName); + Task RefreshSidecarMetadata(MusicVideo musicVideo, string nfoFileName); + Task RefreshFallbackMetadata(Movie movie); + Task RefreshFallbackMetadata(Episode episode); Task RefreshFallbackMetadata(Show televisionShow, string showFolder); } } diff --git a/ErsatzTV.Core/LanguageExtensions.cs b/ErsatzTV.Core/LanguageExtensions.cs index b181bc05e..dfd812b6d 100644 --- a/ErsatzTV.Core/LanguageExtensions.cs +++ b/ErsatzTV.Core/LanguageExtensions.cs @@ -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 ToEither(this Validation validation) => diff --git a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs index 39a2065f1..b465f10d4 100644 --- a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs +++ b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs @@ -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 }); } - public Task 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 RefreshSidecarMetadata(Movie movie, string nfoFileName) => + LoadMovieMetadata(movie, nfoFileName).Bind( + maybeMetadata => maybeMetadata.Match( + metadata => ApplyMetadataUpdate(movie, metadata), + () => Task.FromResult(false))); - public Task RefreshSidecarMetadata(Show televisionShow, string showFolder) => - LoadMetadata(televisionShow, showFolder).Bind( + public Task RefreshSidecarMetadata(Show televisionShow, string nfoFileName) => + LoadTelevisionShowMetadata(nfoFileName).Bind( maybeMetadata => maybeMetadata.Match( metadata => ApplyMetadataUpdate(televisionShow, metadata), () => Task.FromResult(false))); - public Task RefreshFallbackMetadata(MediaItem mediaItem) => - mediaItem switch - { - Episode e => ApplyMetadataUpdate(e, _fallbackMetadataProvider.GetFallbackMetadata(e)), - Movie m => ApplyMetadataUpdate(m, _fallbackMetadataProvider.GetFallbackMetadata(m)), - _ => Task.FromResult(false) - }; + public Task RefreshSidecarMetadata(Episode episode, string nfoFileName) => + LoadEpisodeMetadata(episode, nfoFileName).Bind( + maybeMetadata => maybeMetadata.Match( + metadata => ApplyMetadataUpdate(episode, metadata), + () => Task.FromResult(false))); + + public Task RefreshSidecarMetadata(MusicVideo musicVideo, string nfoFileName) => + LoadMusicVideoMetadata(nfoFileName).Bind( + maybeMetadata => maybeMetadata.Match( + metadata => ApplyMetadataUpdate(musicVideo, metadata), + () => Task.FromResult(false))); + + public Task RefreshFallbackMetadata(Movie movie) => + ApplyMetadataUpdate(movie, _fallbackMetadataProvider.GetFallbackMetadata(movie)); + + public Task RefreshFallbackMetadata(Episode episode) => + ApplyMetadataUpdate(episode, _fallbackMetadataProvider.GetFallbackMetadata(episode)); public Task RefreshFallbackMetadata(Show televisionShow, string showFolder) => ApplyMetadataUpdate(televisionShow, _fallbackMetadataProvider.GetFallbackMetadataForShow(showFolder)); @@ -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 ? _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 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 ? _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 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 ? _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 return await _metadataRepository.Add(metadata); }); - private async Task> 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>> 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> 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> 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> LoadTelevisionShowMetadata(string nfoFileName) { try @@ -605,7 +386,7 @@ namespace ErsatzTV.Core.Metadata } } - private async Task> LoadMovieMetadata(Movie mediaItem, string nfoFileName) + private async Task> LoadMovieMetadata(Movie movie, string nfoFileName) { try { @@ -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 return DateTime.TryParse(aired, out DateTime parsed) ? parsed : fallback; } - [XmlRoot("movie")] - public class MovieNfo + private async Task UpdateMetadataCollections( + T existing, + T incoming, + Func> addGenre, + Func> addTag, + Func> addStudio) + where T : Domain.Metadata { - [XmlElement("title")] - public string Title { get; set; } + var updated = false; - [XmlElement("outline")] - public string Outline { 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("year")] - public int Year { 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("mpaa")] - public string ContentRating { 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("premiered")] - public DateTime Premiered { 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("plot")] - public string Plot { 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("tagline")] - public string Tagline { 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("genre")] - public List Genres { get; set; } - - [XmlElement("tag")] - public List Tags { get; set; } - - [XmlElement("studio")] - public List 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 Genres { get; set; } - - [XmlElement("tag")] - public List Tags { get; set; } - - [XmlElement("studio")] - public List 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; } - - [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 Genres { get; set; } - - [XmlElement("tag")] - public List Tags { get; set; } - - [XmlElement("studio")] - public List Studios { get; set; } + return updated; } } } diff --git a/ErsatzTV.Core/Metadata/Nfo/MovieNfo.cs b/ErsatzTV.Core/Metadata/Nfo/MovieNfo.cs new file mode 100644 index 000000000..f77466d8e --- /dev/null +++ b/ErsatzTV.Core/Metadata/Nfo/MovieNfo.cs @@ -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 Genres { get; set; } + + [XmlElement("tag")] + public List Tags { get; set; } + + [XmlElement("studio")] + public List Studios { get; set; } + } +} diff --git a/ErsatzTV.Core/Metadata/Nfo/MusicVideoNfo.cs b/ErsatzTV.Core/Metadata/Nfo/MusicVideoNfo.cs new file mode 100644 index 000000000..f1786e187 --- /dev/null +++ b/ErsatzTV.Core/Metadata/Nfo/MusicVideoNfo.cs @@ -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 Genres { get; set; } + + [XmlElement("tag")] + public List Tags { get; set; } + + [XmlElement("studio")] + public List Studios { get; set; } + } +} diff --git a/ErsatzTV.Core/Metadata/Nfo/TvShowEpisodeNfo.cs b/ErsatzTV.Core/Metadata/Nfo/TvShowEpisodeNfo.cs new file mode 100644 index 000000000..3d5f1babd --- /dev/null +++ b/ErsatzTV.Core/Metadata/Nfo/TvShowEpisodeNfo.cs @@ -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; } + } +} diff --git a/ErsatzTV.Core/Metadata/Nfo/TvShowNfo.cs b/ErsatzTV.Core/Metadata/Nfo/TvShowNfo.cs new file mode 100644 index 000000000..cc577e6a1 --- /dev/null +++ b/ErsatzTV.Core/Metadata/Nfo/TvShowNfo.cs @@ -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 Genres { get; set; } + + [XmlElement("tag")] + public List Tags { get; set; } + + [XmlElement("studio")] + public List Studios { get; set; } + } +} diff --git a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs index 3066e4b88..81cef713a 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs @@ -57,7 +57,7 @@ namespace ErsatzTV.Core.Scheduling case ProgramScheduleItemCollectionType.Collection: Option> maybeItems = await _mediaCollectionRepository.GetItems(collectionKey.CollectionId ?? 0); - return Tuple(collectionKey, maybeItems.IfNone(new List())); + return Tuple(collectionKey, await maybeItems.IfNoneAsync(new List())); case ProgramScheduleItemCollectionType.TelevisionShow: List showItems = await _televisionRepository.GetShowItems(collectionKey.MediaItemId ?? 0); @@ -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( diff --git a/ErsatzTV.Infrastructure/Data/DbInitializer.cs b/ErsatzTV.Infrastructure/Data/DbInitializer.cs index bffc86373..41e0a4f86 100644 --- a/ErsatzTV.Infrastructure/Data/DbInitializer.cs +++ b/ErsatzTV.Infrastructure/Data/DbInitializer.cs @@ -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 { public static class DbInitializer { - public static Unit Initialize(TvContext context) + public static async Task Initialize(TvContext context, CancellationToken cancellationToken) { if (context.Resolutions.Any()) { @@ -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 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; diff --git a/ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs index 993118763..d0b5c1ece 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs @@ -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) diff --git a/ErsatzTV.Infrastructure/Data/Repositories/ConfigElementRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/ConfigElementRepository.cs index 8f7c2c166..c57693b39 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/ConfigElementRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/ConfigElementRepository.cs @@ -31,16 +31,16 @@ namespace ErsatzTV.Infrastructure.Data.Repositories public Task> GetValue(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(); } } } diff --git a/ErsatzTV.Infrastructure/Data/Repositories/FFmpegProfileRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/FFmpegProfileRepository.cs index e4f29bda1..e71c7c684 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/FFmpegProfileRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/FFmpegProfileRepository.cs @@ -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) diff --git a/ErsatzTV.Infrastructure/Data/Repositories/PlayoutRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/PlayoutRepository.cs index 082a26e0f..6ca926026 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/PlayoutRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/PlayoutRepository.cs @@ -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) diff --git a/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj b/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj index 14a0270e3..fbb727081 100644 --- a/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj +++ b/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj @@ -3,9 +3,14 @@ net5.0 true + VSTHRD200 + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -16,6 +21,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/ErsatzTV.Infrastructure/Images/ImageCache.cs b/ErsatzTV.Infrastructure/Images/ImageCache.cs index 48ab613d7..a5385717b 100644 --- a/ErsatzTV.Infrastructure/Images/ImageCache.cs +++ b/ErsatzTV.Infrastructure/Images/ImageCache.cs @@ -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 { private static readonly SHA1CryptoServiceProvider Crypto; private readonly ILocalFileSystem _localFileSystem; - private readonly ILogger _logger; static ImageCache() => Crypto = new SHA1CryptoServiceProvider(); - public ImageCache(ILocalFileSystem localFileSystem, ILogger logger) - { - _localFileSystem = localFileSystem; - _logger = logger; - } + public ImageCache(ILocalFileSystem localFileSystem) => _localFileSystem = localFileSystem; public async Task> ResizeImage(byte[] imageBuffer, int height) { diff --git a/ErsatzTV.sln.DotSettings b/ErsatzTV.sln.DotSettings index 02108aa4c..c720f7899 100644 --- a/ErsatzTV.sln.DotSettings +++ b/ErsatzTV.sln.DotSettings @@ -41,4 +41,5 @@ True True True + True True \ No newline at end of file diff --git a/ErsatzTV/ErsatzTV.csproj b/ErsatzTV/ErsatzTV.csproj index 410a98770..a75f60a2d 100644 --- a/ErsatzTV/ErsatzTV.csproj +++ b/ErsatzTV/ErsatzTV.csproj @@ -2,6 +2,7 @@ net5.0 + VSTHRD200 @@ -11,6 +12,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -20,6 +25,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/ErsatzTV/Extensions/EitherToActionResult.cs b/ErsatzTV/Extensions/EitherToActionResult.cs index dc77a3d87..df71dc988 100644 --- a/ErsatzTV/Extensions/EitherToActionResult.cs +++ b/ErsatzTV/Extensions/EitherToActionResult.cs @@ -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 ToActionResult(this Task> either) => either.Map(Match); @@ -16,13 +18,13 @@ namespace ErsatzTV.Extensions Left: l => new BadRequestObjectResult(l), Right: r => new OkObjectResult(r)); - private static async Task Match(Either either) => - await either.MatchAsync( + private static Task Match(Either either) => + either.Match>( async t => { await t; return new OkResult(); }, - e => new BadRequestObjectResult(e)); + e => Task.FromResult((IActionResult) new BadRequestObjectResult(e))); } } diff --git a/ErsatzTV/Extensions/HostExtensions.cs b/ErsatzTV/Extensions/HostExtensions.cs deleted file mode 100644 index 1bc1a6763..000000000 --- a/ErsatzTV/Extensions/HostExtensions.cs +++ /dev/null @@ -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(); - - private static TvContext Migrate(TvContext context, IServiceProvider provider) - { - ILogger logger = provider.GetRequiredService>(); - 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 logger = provider.GetRequiredService>(); - 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(); - 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>().LogError(ex, message); - return unit; - } - } -} diff --git a/ErsatzTV/Extensions/ListToActionResult.cs b/ErsatzTV/Extensions/ListToActionResult.cs index 47bb32f16..edd416125 100644 --- a/ErsatzTV/Extensions/ListToActionResult.cs +++ b/ErsatzTV/Extensions/ListToActionResult.cs @@ -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 ToActionResult(this Task> list) => diff --git a/ErsatzTV/Extensions/OptionToActionResult.cs b/ErsatzTV/Extensions/OptionToActionResult.cs index 8983a907a..021dbc6eb 100644 --- a/ErsatzTV/Extensions/OptionToActionResult.cs +++ b/ErsatzTV/Extensions/OptionToActionResult.cs @@ -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(this Option option) => diff --git a/ErsatzTV/Extensions/ValidationToActionResult.cs b/ErsatzTV/Extensions/ValidationToActionResult.cs index 64462e6c4..afd6c50cc 100644 --- a/ErsatzTV/Extensions/ValidationToActionResult.cs +++ b/ErsatzTV/Extensions/ValidationToActionResult.cs @@ -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(this Validation validation) => diff --git a/ErsatzTV/Pages/FragmentNavigationBase.cs b/ErsatzTV/Pages/FragmentNavigationBase.cs index 89513121e..b65824371 100644 --- a/ErsatzTV/Pages/FragmentNavigationBase.cs +++ b/ErsatzTV/Pages/FragmentNavigationBase.cs @@ -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 } } - 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 + } + } } } diff --git a/ErsatzTV/Program.cs b/ErsatzTV/Program.cs index 4505b1a35..e96284ff6 100644 --- a/ErsatzTV/Program.cs +++ b/ErsatzTV/Program.cs @@ -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 { 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 try { - await CreateHostBuilder(args) - .Build() - .SeedDatabase() - .CleanCacheFolder() - .RunAsync(); + await CreateHostBuilder(args).Build().RunAsync(); return 0; } catch (Exception ex) @@ -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() diff --git a/ErsatzTV/Services/RunOnce/CacheCleanerService.cs b/ErsatzTV/Services/RunOnce/CacheCleanerService.cs new file mode 100644 index 000000000..c5cbec843 --- /dev/null +++ b/ErsatzTV/Services/RunOnce/CacheCleanerService.cs @@ -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 _logger; + private readonly IServiceScopeFactory _serviceScopeFactory; + + public CacheCleanerService( + IServiceScopeFactory serviceScopeFactory, + ILogger logger) + { + _serviceScopeFactory = serviceScopeFactory; + _logger = logger; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + using IServiceScope scope = _serviceScopeFactory.CreateScope(); + await using TvContext dbContext = scope.ServiceProvider.GetRequiredService(); + + if (Directory.Exists(FileSystemLayout.LegacyImageCacheFolder)) + { + _logger.LogInformation("Migrating channel logos from legacy image cache folder"); + + List 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(); + 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; + } +} diff --git a/ErsatzTV/Services/RunOnce/DatabaseMigratorService.cs b/ErsatzTV/Services/RunOnce/DatabaseMigratorService.cs new file mode 100644 index 000000000..2d3c2ffc6 --- /dev/null +++ b/ErsatzTV/Services/RunOnce/DatabaseMigratorService.cs @@ -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 _logger; + private readonly IServiceScopeFactory _serviceScopeFactory; + + public DatabaseMigratorService( + IServiceScopeFactory serviceScopeFactory, + ILogger 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(); + await dbContext.Database.MigrateAsync(cancellationToken); + await DbInitializer.Initialize(dbContext, cancellationToken); + + _logger.LogInformation("Done applying database migrations"); + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } +} diff --git a/ErsatzTV/Services/SchedulerService.cs b/ErsatzTV/Services/SchedulerService.cs index 7a933390a..0ffa90f97 100644 --- a/ErsatzTV/Services/SchedulerService.cs +++ b/ErsatzTV/Services/SchedulerService.cs @@ -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 _channel; private readonly IEntityLocker _entityLocker; + private readonly ILogger _logger; private readonly IServiceScopeFactory _serviceScopeFactory; - private Timer _timer; public SchedulerService( IServiceScopeFactory serviceScopeFactory, ChannelWriter channel, - IEntityLocker entityLocker) + IEntityLocker entityLocker, + ILogger 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 + while (!cancellationToken.IsCancellationRequested) + { + if (!cancellationToken.IsCancellationRequested) + { + await DoWork(cancellationToken); + } - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken cancellationToken) - { - _timer?.Change(Timeout.Infinite, 0); - - 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 } } - private async Task RebuildSearchIndex(CancellationToken cancellationToken) => - await _channel.WriteAsync(new RebuildSearchIndex(), cancellationToken); + private ValueTask RebuildSearchIndex(CancellationToken cancellationToken) => + _channel.WriteAsync(new RebuildSearchIndex(), cancellationToken); } } diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index 68341dcee..1f95d3633 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -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 Log.Logger.Information( "ErsatzTV version {Version}", - Assembly.GetEntryAssembly().GetCustomAttribute() + Assembly.GetEntryAssembly()?.GetCustomAttribute() ?.InformationalVersion ?? "unknown"); Log.Logger.Warning("This is pre-alpha software and is likely to be unstable"); @@ -220,6 +221,8 @@ namespace ErsatzTV services.AddScoped(); services.AddScoped(); + services.AddHostedService(); + services.AddHostedService(); services.AddHostedService(); services.AddHostedService(); services.AddHostedService();