diff --git a/ErsatzTV.Core.Tests/Metadata/FallbackMetadataProviderTests.cs b/ErsatzTV.Core.Tests/Metadata/FallbackMetadataProviderTests.cs index 2e2500abc..859575595 100644 --- a/ErsatzTV.Core.Tests/Metadata/FallbackMetadataProviderTests.cs +++ b/ErsatzTV.Core.Tests/Metadata/FallbackMetadataProviderTests.cs @@ -25,11 +25,20 @@ namespace ErsatzTV.Core.Tests.Metadata [TestCase("Awesome Show - S01E02 - Episode Title-720p.mkv", "Awesome Show", 1, 2)] [TestCase("Awesome Show - s1e2 - Episode Title-720p.mkv", "Awesome Show", 1, 2)] [TestCase("Awesome Show - S1E2 - Episode Title-720p.mkv", "Awesome Show", 1, 2)] - [TestCase("Awesome Show (2021) - S01E02 - Description; More Description (1080p QUALITY codec GROUP).mkv", "Awesome Show (2021)", 1, 2)] - [TestCase("Awesome.Show.S01E02.Description.more.Description.QUAlity.codec.CODEC-GROUP.mkv", "Awesome.Show", 1, 2)] + [TestCase( + "Awesome Show (2021) - S01E02 - Description; More Description (1080p QUALITY codec GROUP).mkv", + "Awesome Show (2021)", + 1, + 2)] + [TestCase( + "Awesome.Show.S01E02.Description.more.Description.QUAlity.codec.CODEC-GROUP.mkv", + "Awesome.Show", + 1, + 2)] public void GetFallbackMetadata_ShouldHandleVariousFormats(string path, string title, int season, int episode) { - var metadata = FallbackMetadataProvider.GetFallbackMetadata(path); + MediaMetadata metadata = FallbackMetadataProvider.GetFallbackMetadata( + new MediaItem { Path = path, Source = new LocalMediaSource { MediaType = MediaType.TvShow } }); metadata.MediaType.Should().Be(MediaType.TvShow); metadata.Title.Should().Be(title); diff --git a/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs b/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs index a0c10c659..0723ec7fd 100644 --- a/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs +++ b/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Text.RegularExpressions; using ErsatzTV.Core.Domain; @@ -6,12 +7,32 @@ namespace ErsatzTV.Core.Metadata { public static class FallbackMetadataProvider { - public static MediaMetadata GetFallbackMetadata(string path) + public static MediaMetadata GetFallbackMetadata(MediaItem mediaItem) { - string fileName = Path.GetFileName(path); - var metadata = new MediaMetadata { Title = fileName ?? path }; + string fileName = Path.GetFileName(mediaItem.Path); + var metadata = new MediaMetadata { Title = fileName ?? mediaItem.Path }; if (fileName != null) + { + if (!(mediaItem.Source is LocalMediaSource localMediaSource)) + { + return metadata; + } + + return localMediaSource.MediaType switch + { + MediaType.TvShow => GetTvShowMetadata(fileName, metadata), + MediaType.Movie => GetMovieMetadata(fileName, metadata), + _ => metadata + }; + } + + return metadata; + } + + private static MediaMetadata GetTvShowMetadata(string fileName, MediaMetadata metadata) + { + try { const string PATTERN = @"^(.*?)[.\s-]+[sS](\d+)[eE](\d+).*\.\w+$"; Match match = Regex.Match(fileName, PATTERN); @@ -23,6 +44,31 @@ namespace ErsatzTV.Core.Metadata metadata.EpisodeNumber = int.Parse(match.Groups[3].Value); } } + catch (Exception) + { + // ignored + } + + return metadata; + } + + private static MediaMetadata GetMovieMetadata(string fileName, MediaMetadata metadata) + { + try + { + const string PATTERN = @"^(.*?)[.\(](\d{4})[.\)].*\.\w+$"; + Match match = Regex.Match(fileName, PATTERN); + if (match.Success) + { + metadata.MediaType = MediaType.Movie; + metadata.Title = match.Groups[1].Value; + metadata.Aired = new DateTime(int.Parse(match.Groups[2].Value), 1, 1); + } + } + catch (Exception) + { + // ignored + } return metadata; } diff --git a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs index 62fca8884..cf364add7 100644 --- a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs +++ b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs @@ -1,28 +1,35 @@ using System; using System.IO; -using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Xml.Serialization; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using LanguageExt; +using Microsoft.Extensions.Logging; using static LanguageExt.Prelude; namespace ErsatzTV.Core.Metadata { public class LocalMetadataProvider : ILocalMetadataProvider { + private static readonly XmlSerializer MovieSerializer = new(typeof(MovieNfo)); + private static readonly XmlSerializer TvShowSerializer = new(typeof(TvShowEpisodeNfo)); + private readonly ILogger _logger; + private readonly IMediaItemRepository _mediaItemRepository; - public LocalMetadataProvider(IMediaItemRepository mediaItemRepository) => + public LocalMetadataProvider(IMediaItemRepository mediaItemRepository, ILogger logger) + { _mediaItemRepository = mediaItemRepository; + _logger = logger; + } public async Task RefreshMetadata(MediaItem mediaItem) { Option maybeMetadata = await LoadMetadata(mediaItem); MediaMetadata metadata = - maybeMetadata.IfNone(() => FallbackMetadataProvider.GetFallbackMetadata(mediaItem.Path)); + maybeMetadata.IfNone(() => FallbackMetadataProvider.GetFallbackMetadata(mediaItem)); await ApplyMetadataUpdate(mediaItem, metadata); } @@ -50,54 +57,72 @@ namespace ErsatzTV.Core.Metadata string nfoFileName = Path.ChangeExtension(mediaItem.Path, "nfo"); if (nfoFileName == null || !File.Exists(nfoFileName)) { + _logger.LogDebug("NFO file does not exist at {Path}", nfoFileName); + return None; + } + + if (!(mediaItem.Source is LocalMediaSource localMediaSource)) + { + _logger.LogDebug("Media source {Name} is not a local media source", mediaItem.Source.Name); return None; } - var tvShowSerializer = new XmlSerializer(typeof(TvShowEpisodeNfo)); - var movieSerializer = new XmlSerializer(typeof(MovieNfo)); - - TryAsync tvShowAttempt = TryAsync( - async () => - { - await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open); - return tvShowSerializer.Deserialize(fileStream); - }); - TryAsync movieAttempt = TryAsync( - async () => - { - await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open); - return movieSerializer.Deserialize(fileStream); - }); - return await choice(tvShowAttempt, movieAttempt).Match>( - result => - { - switch (result) + return localMediaSource.MediaType switch + { + MediaType.Movie => await LoadMovieMetadata(nfoFileName), + MediaType.TvShow => await LoadTvShowMetadata(nfoFileName), + _ => None + }; + } + + private async Task> LoadTvShowMetadata(string nfoFileName) + { + try + { + await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read); + Option maybeNfo = TvShowSerializer.Deserialize(fileStream) as TvShowEpisodeNfo; + return maybeNfo.Match>( + nfo => new MediaMetadata { - case TvShowEpisodeNfo nfo: - return new MediaMetadata - { - MediaType = MediaType.TvShow, - Title = nfo.ShowTitle, - Subtitle = nfo.Title, - Description = nfo.Outline, - EpisodeNumber = nfo.Episode, - SeasonNumber = nfo.Season, - Aired = GetAired(nfo.Aired) - }; - case MovieNfo nfo: - return new MediaMetadata - { - MediaType = MediaType.Movie, - Title = nfo.Title, - Description = nfo.Outline, - ContentRating = nfo.ContentRating, - Aired = GetAired(nfo.Premiered) - }; - default: - return None; - } - }, - None); + MediaType = MediaType.TvShow, + Title = nfo.ShowTitle, + Subtitle = nfo.Title, + Description = nfo.Outline, + EpisodeNumber = nfo.Episode, + SeasonNumber = nfo.Season, + Aired = GetAired(nfo.Aired) + }, + None); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to read TV nfo metadata from {Path}", nfoFileName); + return None; + } + } + + private async Task> LoadMovieMetadata(string nfoFileName) + { + try + { + await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read); + Option maybeNfo = MovieSerializer.Deserialize(fileStream) as MovieNfo; + return maybeNfo.Match>( + nfo => new MediaMetadata + { + MediaType = MediaType.Movie, + Title = nfo.Title, + Description = nfo.Outline, + ContentRating = nfo.ContentRating, + Aired = GetAired(nfo.Premiered) + }, + None); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to read Movie nfo metadata from {Path}", nfoFileName); + return None; + } } private static DateTime? GetAired(string aired)