Browse Source

Movie metadata fixes (#9)

* reorganize metadata parsing; only attempt to parse appropriate media type based on media source configuration

* add fallback metadata for movie sources

* only request read access for nfo metadata

* fix tests
pull/10/head
Jason Dove 5 years ago committed by GitHub
parent
commit
a3e20826a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      ErsatzTV.Core.Tests/Metadata/FallbackMetadataProviderTests.cs
  2. 54
      ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs
  3. 81
      ErsatzTV.Core/Metadata/LocalMetadataProvider.cs

15
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 - 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 - 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(
[TestCase("Awesome.Show.S01E02.Description.more.Description.QUAlity.codec.CODEC-GROUP.mkv", "Awesome.Show", 1, 2)] "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) 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.MediaType.Should().Be(MediaType.TvShow);
metadata.Title.Should().Be(title); metadata.Title.Should().Be(title);

54
ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs

@ -1,4 +1,5 @@
using System.IO; using System;
using System.IO;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
@ -6,12 +7,32 @@ namespace ErsatzTV.Core.Metadata
{ {
public static class FallbackMetadataProvider public static class FallbackMetadataProvider
{ {
public static MediaMetadata GetFallbackMetadata(string path) public static MediaMetadata GetFallbackMetadata(MediaItem mediaItem)
{ {
string fileName = Path.GetFileName(path); string fileName = Path.GetFileName(mediaItem.Path);
var metadata = new MediaMetadata { Title = fileName ?? path }; var metadata = new MediaMetadata { Title = fileName ?? mediaItem.Path };
if (fileName != null) 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+$"; const string PATTERN = @"^(.*?)[.\s-]+[sS](\d+)[eE](\d+).*\.\w+$";
Match match = Regex.Match(fileName, PATTERN); Match match = Regex.Match(fileName, PATTERN);
@ -23,6 +44,31 @@ namespace ErsatzTV.Core.Metadata
metadata.EpisodeNumber = int.Parse(match.Groups[3].Value); 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; return metadata;
} }

81
ErsatzTV.Core/Metadata/LocalMetadataProvider.cs

@ -1,28 +1,35 @@
using System; using System;
using System.IO; using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml.Serialization; using System.Xml.Serialization;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt; using LanguageExt;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude; using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Metadata namespace ErsatzTV.Core.Metadata
{ {
public class LocalMetadataProvider : ILocalMetadataProvider public class LocalMetadataProvider : ILocalMetadataProvider
{ {
private static readonly XmlSerializer MovieSerializer = new(typeof(MovieNfo));
private static readonly XmlSerializer TvShowSerializer = new(typeof(TvShowEpisodeNfo));
private readonly ILogger<LocalMetadataProvider> _logger;
private readonly IMediaItemRepository _mediaItemRepository; private readonly IMediaItemRepository _mediaItemRepository;
public LocalMetadataProvider(IMediaItemRepository mediaItemRepository) => public LocalMetadataProvider(IMediaItemRepository mediaItemRepository, ILogger<LocalMetadataProvider> logger)
{
_mediaItemRepository = mediaItemRepository; _mediaItemRepository = mediaItemRepository;
_logger = logger;
}
public async Task RefreshMetadata(MediaItem mediaItem) public async Task RefreshMetadata(MediaItem mediaItem)
{ {
Option<MediaMetadata> maybeMetadata = await LoadMetadata(mediaItem); Option<MediaMetadata> maybeMetadata = await LoadMetadata(mediaItem);
MediaMetadata metadata = MediaMetadata metadata =
maybeMetadata.IfNone(() => FallbackMetadataProvider.GetFallbackMetadata(mediaItem.Path)); maybeMetadata.IfNone(() => FallbackMetadataProvider.GetFallbackMetadata(mediaItem));
await ApplyMetadataUpdate(mediaItem, metadata); await ApplyMetadataUpdate(mediaItem, metadata);
} }
@ -50,31 +57,32 @@ namespace ErsatzTV.Core.Metadata
string nfoFileName = Path.ChangeExtension(mediaItem.Path, "nfo"); string nfoFileName = Path.ChangeExtension(mediaItem.Path, "nfo");
if (nfoFileName == null || !File.Exists(nfoFileName)) if (nfoFileName == null || !File.Exists(nfoFileName))
{ {
_logger.LogDebug("NFO file does not exist at {Path}", nfoFileName);
return None; return None;
} }
var tvShowSerializer = new XmlSerializer(typeof(TvShowEpisodeNfo)); if (!(mediaItem.Source is LocalMediaSource localMediaSource))
var movieSerializer = new XmlSerializer(typeof(MovieNfo));
TryAsync<object> tvShowAttempt = TryAsync(
async () =>
{ {
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open); _logger.LogDebug("Media source {Name} is not a local media source", mediaItem.Source.Name);
return tvShowSerializer.Deserialize(fileStream); return None;
}); }
TryAsync<object> movieAttempt = TryAsync(
async () => return localMediaSource.MediaType switch
{ {
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open); MediaType.Movie => await LoadMovieMetadata(nfoFileName),
return movieSerializer.Deserialize(fileStream); MediaType.TvShow => await LoadTvShowMetadata(nfoFileName),
}); _ => None
return await choice(tvShowAttempt, movieAttempt).Match<object, Option<MediaMetadata>>( };
result => }
private async Task<Option<MediaMetadata>> LoadTvShowMetadata(string nfoFileName)
{ {
switch (result) try
{ {
case TvShowEpisodeNfo nfo: await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read);
return new MediaMetadata Option<TvShowEpisodeNfo> maybeNfo = TvShowSerializer.Deserialize(fileStream) as TvShowEpisodeNfo;
return maybeNfo.Match<Option<MediaMetadata>>(
nfo => new MediaMetadata
{ {
MediaType = MediaType.TvShow, MediaType = MediaType.TvShow,
Title = nfo.ShowTitle, Title = nfo.ShowTitle,
@ -83,22 +91,39 @@ namespace ErsatzTV.Core.Metadata
EpisodeNumber = nfo.Episode, EpisodeNumber = nfo.Episode,
SeasonNumber = nfo.Season, SeasonNumber = nfo.Season,
Aired = GetAired(nfo.Aired) Aired = GetAired(nfo.Aired)
}; },
case MovieNfo nfo: None);
return new MediaMetadata }
catch (Exception ex)
{
_logger.LogDebug(ex, "Failed to read TV nfo metadata from {Path}", nfoFileName);
return None;
}
}
private async Task<Option<MediaMetadata>> LoadMovieMetadata(string nfoFileName)
{
try
{
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read);
Option<MovieNfo> maybeNfo = MovieSerializer.Deserialize(fileStream) as MovieNfo;
return maybeNfo.Match<Option<MediaMetadata>>(
nfo => new MediaMetadata
{ {
MediaType = MediaType.Movie, MediaType = MediaType.Movie,
Title = nfo.Title, Title = nfo.Title,
Description = nfo.Outline, Description = nfo.Outline,
ContentRating = nfo.ContentRating, ContentRating = nfo.ContentRating,
Aired = GetAired(nfo.Premiered) Aired = GetAired(nfo.Premiered)
};
default:
return None;
}
}, },
None); None);
} }
catch (Exception ex)
{
_logger.LogDebug(ex, "Failed to read Movie nfo metadata from {Path}", nfoFileName);
return None;
}
}
private static DateTime? GetAired(string aired) private static DateTime? GetAired(string aired)
{ {

Loading…
Cancel
Save