Fix fallback metadata parsing for TV (#3)

* fix yml formatting

* test build action

* move ci workflow

* fix unit tests from dotnet test command

* add release workflow

* try another matrix syntax

* fix path to csproj

* fix target framework

* more release fixes

* port fixes for running outside of docker

* more search fixes

* improve fallback metadata parsing, add tests

* fix odd behavior when searching collections

* more fallback metadata fixes

* refresh "other" metadata on startup
This commit is contained in:
Jason Dove
2021-02-11 11:57:32 +00:00
committed by GitHub
parent a2e85655d2
commit d04f3d574b
13 changed files with 90 additions and 29 deletions
@@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -25,7 +26,7 @@ namespace ErsatzTV.Application.MediaItems.Queries
if (!string.IsNullOrEmpty(request.SearchString)) if (!string.IsNullOrEmpty(request.SearchString))
{ {
allItems = allItems.Filter( allItems = allItems.Filter(
i => i.Metadata?.Title.ToLowerInvariant().Contains(request.SearchString.ToLowerInvariant()) == i => i.Metadata?.Title.Contains(request.SearchString, StringComparison.OrdinalIgnoreCase) ==
true); true);
} }
@@ -6,6 +6,7 @@ using NUnit.Framework;
namespace ErsatzTV.Core.Tests.FFmpeg namespace ErsatzTV.Core.Tests.FFmpeg
{ {
[TestFixture]
public class FFmpegPlaybackSettingsCalculatorTests public class FFmpegPlaybackSettingsCalculatorTests
{ {
public class CalculateSettings public class CalculateSettings
@@ -0,0 +1,40 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Metadata;
using FluentAssertions;
using NUnit.Framework;
namespace ErsatzTV.Core.Tests.Metadata
{
[TestFixture]
public class FallbackMetadataProviderTests
{
[Test]
[TestCase("Awesome Show - s01e02.mkv", "Awesome Show", 1, 2)]
[TestCase("Awesome Show - S01E02.mkv", "Awesome Show", 1, 2)]
[TestCase("Awesome Show - s1e2.mkv", "Awesome Show", 1, 2)]
[TestCase("Awesome Show - S1E2.mkv", "Awesome Show", 1, 2)]
[TestCase("Awesome Show - s01e02 - Episode Title.mkv", "Awesome Show", 1, 2)]
[TestCase("Awesome Show - S01E02 - Episode Title.mkv", "Awesome Show", 1, 2)]
[TestCase("Awesome Show - s1e2 - Episode Title.mkv", "Awesome Show", 1, 2)]
[TestCase("Awesome Show - S1E2 - Episode Title.mkv", "Awesome Show", 1, 2)]
[TestCase("Awesome Show (2021) - s01e02 - Episode Title.mkv", "Awesome Show (2021)", 1, 2)]
[TestCase("Awesome Show (2021) - S01E02 - Episode Title.mkv", "Awesome Show (2021)", 1, 2)]
[TestCase("Awesome Show (2021) - s1e2 - Episode Title.mkv", "Awesome Show (2021)", 1, 2)]
[TestCase("Awesome Show (2021) - S1E2 - Episode Title.mkv", "Awesome Show (2021)", 1, 2)]
[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 (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);
metadata.MediaType.Should().Be(MediaType.TvShow);
metadata.Title.Should().Be(title);
metadata.SeasonNumber.Should().Be(season);
metadata.EpisodeNumber.Should().Be(episode);
}
}
}
@@ -9,6 +9,7 @@ using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Tests.Scheduling namespace ErsatzTV.Core.Tests.Scheduling
{ {
[TestFixture]
public class ChronologicalContentTests public class ChronologicalContentTests
{ {
[Test] [Test]
@@ -14,6 +14,7 @@ using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Tests.Scheduling namespace ErsatzTV.Core.Tests.Scheduling
{ {
[TestFixture]
public class PlayoutBuilderTests public class PlayoutBuilderTests
{ {
private readonly ILogger<PlayoutBuilder> _logger; private readonly ILogger<PlayoutBuilder> _logger;
@@ -9,6 +9,7 @@ using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Tests.Scheduling namespace ErsatzTV.Core.Tests.Scheduling
{ {
[TestFixture]
public class RandomizedContentTests public class RandomizedContentTests
{ {
private const int KnownSeed = 22295; private const int KnownSeed = 22295;
@@ -9,6 +9,7 @@ using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Tests.Scheduling namespace ErsatzTV.Core.Tests.Scheduling
{ {
[TestFixture]
public class ShuffledContentTests public class ShuffledContentTests
{ {
// this seed will produce (shuffle) 1-10 in order // this seed will produce (shuffle) 1-10 in order
@@ -0,0 +1,30 @@
using System.IO;
using System.Text.RegularExpressions;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Metadata
{
public static class FallbackMetadataProvider
{
public static MediaMetadata GetFallbackMetadata(string path)
{
string fileName = Path.GetFileName(path);
var metadata = new MediaMetadata { Title = fileName ?? path };
if (fileName != null)
{
const string PATTERN = @"^(.*?)[.\s-]+[sS](\d+)[eE](\d+).*\.\w+$";
Match match = Regex.Match(fileName, PATTERN);
if (match.Success)
{
metadata.MediaType = MediaType.TvShow;
metadata.Title = match.Groups[1].Value;
metadata.SeasonNumber = int.Parse(match.Groups[2].Value);
metadata.EpisodeNumber = int.Parse(match.Groups[3].Value);
}
}
return metadata;
}
}
}
+2 -1
View File
@@ -82,12 +82,13 @@ namespace ErsatzTV.Core.Metadata
} }
// if exists, check if the file was modified // if exists, check if the file was modified
// also, try to re-categorize "other" by refreshing metadata
Seq<MediaItem> modifiedMediaItems = existingMediaItems.Filter( Seq<MediaItem> modifiedMediaItems = existingMediaItems.Filter(
mediaItem => mediaItem =>
{ {
DateTime lastWrite = File.GetLastWriteTimeUtc(mediaItem.Path); DateTime lastWrite = File.GetLastWriteTimeUtc(mediaItem.Path);
bool modified = lastWrite > mediaItem.LastWriteTime.IfNone(DateTime.MinValue); bool modified = lastWrite > mediaItem.LastWriteTime.IfNone(DateTime.MinValue);
return modified || mediaItem.Metadata == null; return modified || mediaItem.Metadata == null || mediaItem.Metadata.MediaType == MediaType.Other;
}); });
modifiedPlayoutIds.AddRange(await _playoutRepository.GetPlayoutIdsForMediaItems(modifiedMediaItems)); modifiedPlayoutIds.AddRange(await _playoutRepository.GetPlayoutIdsForMediaItems(modifiedMediaItems));
foreach (MediaItem mediaItem in modifiedMediaItems) foreach (MediaItem mediaItem in modifiedMediaItems)
@@ -21,7 +21,8 @@ namespace ErsatzTV.Core.Metadata
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 = maybeMetadata.IfNone(() => GetFallbackMetadata(mediaItem)); MediaMetadata metadata =
maybeMetadata.IfNone(() => FallbackMetadataProvider.GetFallbackMetadata(mediaItem.Path));
await ApplyMetadataUpdate(mediaItem, metadata); await ApplyMetadataUpdate(mediaItem, metadata);
} }
@@ -99,27 +100,6 @@ namespace ErsatzTV.Core.Metadata
None); None);
} }
private MediaMetadata GetFallbackMetadata(MediaItem mediaItem)
{
string fileName = Path.GetFileName(mediaItem.Path);
var metadata = new MediaMetadata { Title = fileName ?? mediaItem.Path };
if (fileName != null)
{
const string PATTERN = @"^(.*?)[\s-]+[sS](\d+)[eE](\d+)\.\w+$";
Match match = Regex.Match(fileName, PATTERN);
if (match.Success)
{
metadata.MediaType = MediaType.TvShow;
metadata.Title = match.Groups[1].Value;
metadata.SeasonNumber = int.Parse(match.Groups[2].Value);
metadata.EpisodeNumber = int.Parse(match.Groups[3].Value);
}
}
return metadata;
}
private static DateTime? GetAired(string aired) private static DateTime? GetAired(string aired)
{ {
if (string.IsNullOrWhiteSpace(aired)) if (string.IsNullOrWhiteSpace(aired))
+5 -1
View File
@@ -91,7 +91,11 @@
_totalItems = aggregateData.Count; _totalItems = aggregateData.Count;
_pagedData = aggregateData.Skip(state.Page * state.PageSize).Take(state.PageSize); _pagedData = aggregateData
.Skip(_totalItems <= state.PageSize ? 0 : state.Page * state.PageSize)
.Take(state.PageSize)
.OrderBy(c => c.Name);
return new TableData<MediaCollectionSummaryViewModel> { TotalItems = _totalItems, Items = _pagedData }; return new TableData<MediaCollectionSummaryViewModel> { TotalItems = _totalItems, Items = _pagedData };
} }
+2 -2
View File
@@ -49,10 +49,10 @@
} }
private Task<IEnumerable<ChannelViewModel>> SearchChannels(string value) => private Task<IEnumerable<ChannelViewModel>> SearchChannels(string value) =>
_channels.Filter(c => $"{c.Number} - {c.Name}".ToLowerInvariant().Contains(value ?? string.Empty)).AsTask(); _channels.Filter(c => $"{c.Number} - {c.Name}".Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
private Task<IEnumerable<ProgramScheduleViewModel>> SearchProgramSchedules(string value) => private Task<IEnumerable<ProgramScheduleViewModel>> SearchProgramSchedules(string value) =>
_programSchedules.Filter(c => c.Name.ToLowerInvariant().StartsWith(value ?? string.Empty)).AsTask(); _programSchedules.Filter(c => c.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
private async Task HandleSubmitAsync() private async Task HandleSubmitAsync()
+1 -1
View File
@@ -170,7 +170,7 @@
} }
private Task<IEnumerable<MediaCollectionViewModel>> SearchMediaCollections(string value) => private Task<IEnumerable<MediaCollectionViewModel>> SearchMediaCollections(string value) =>
_mediaCollections.Filter(c => c.Name.ToLowerInvariant().Contains((value ?? string.Empty).ToLowerInvariant())).AsTask(); _mediaCollections.Filter(c => c.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
private async Task SaveChanges() private async Task SaveChanges()
{ {