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. 119
      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;
} }

119
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,54 +57,72 @@ 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;
}
if (!(mediaItem.Source is LocalMediaSource localMediaSource))
{
_logger.LogDebug("Media source {Name} is not a local media source", mediaItem.Source.Name);
return None; return None;
} }
var tvShowSerializer = new XmlSerializer(typeof(TvShowEpisodeNfo)); return localMediaSource.MediaType switch
var movieSerializer = new XmlSerializer(typeof(MovieNfo)); {
MediaType.Movie => await LoadMovieMetadata(nfoFileName),
TryAsync<object> tvShowAttempt = TryAsync( MediaType.TvShow => await LoadTvShowMetadata(nfoFileName),
async () => _ => None
{ };
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open); }
return tvShowSerializer.Deserialize(fileStream);
}); private async Task<Option<MediaMetadata>> LoadTvShowMetadata(string nfoFileName)
TryAsync<object> movieAttempt = TryAsync( {
async () => try
{ {
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open); await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read);
return movieSerializer.Deserialize(fileStream); Option<TvShowEpisodeNfo> maybeNfo = TvShowSerializer.Deserialize(fileStream) as TvShowEpisodeNfo;
}); return maybeNfo.Match<Option<MediaMetadata>>(
return await choice(tvShowAttempt, movieAttempt).Match<object, Option<MediaMetadata>>( nfo => new MediaMetadata
result =>
{
switch (result)
{ {
case TvShowEpisodeNfo nfo: MediaType = MediaType.TvShow,
return new MediaMetadata Title = nfo.ShowTitle,
{ Subtitle = nfo.Title,
MediaType = MediaType.TvShow, Description = nfo.Outline,
Title = nfo.ShowTitle, EpisodeNumber = nfo.Episode,
Subtitle = nfo.Title, SeasonNumber = nfo.Season,
Description = nfo.Outline, Aired = GetAired(nfo.Aired)
EpisodeNumber = nfo.Episode, },
SeasonNumber = nfo.Season, None);
Aired = GetAired(nfo.Aired) }
}; catch (Exception ex)
case MovieNfo nfo: {
return new MediaMetadata _logger.LogDebug(ex, "Failed to read TV nfo metadata from {Path}", nfoFileName);
{ return None;
MediaType = MediaType.Movie, }
Title = nfo.Title, }
Description = nfo.Outline,
ContentRating = nfo.ContentRating, private async Task<Option<MediaMetadata>> LoadMovieMetadata(string nfoFileName)
Aired = GetAired(nfo.Premiered) {
}; try
default: {
return None; await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read);
} Option<MovieNfo> maybeNfo = MovieSerializer.Deserialize(fileStream) as MovieNfo;
}, return maybeNfo.Match<Option<MediaMetadata>>(
None); 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) private static DateTime? GetAired(string aired)

Loading…
Cancel
Save