Database redesign (#31)

* starting database redesign

* set season and episode numbers

* use datetimes in db (utc); update movie metadata

* get movie cards from new table

* copy show/episode metadata

* remove old movie metadata type

* rename new movie metadata type

* code cleanup

* start to remove old television classes

* remove old television tables from database

* fix playout building

* fix collection views

* fix show/season views

* clean up movie metadata table

* fix scanner tests

* add libraries ui

* code cleanup

* fix movie scanning/metadata

* add library scan button to ui

* delete library path from ui

* temp disable movie scanning

* remove orphan media items and prevent duplicate paths

* attach artwork to metadata

* fix split show/season display

* fix television artwork

* store year distinct from release date

* fix collections ui

* code cleanup

* add library paths from ui

* fix adding to collections from ui

* fix schedule items loading

* schedule editing works again

* remove some todos

* more cleanup

* fix unit tests

* fix episode sorting

* fix deleting show library paths

* remove unused class

* fix playout list in ui

* fix log viewer

* start to use version/file instead of statistics

* clean up old columns

* fix playout display (time zone)

* fix playback

* fix channel guide time zone

* cascade more deletes

* fix compiler warnings

* fix adding new seasons

* use artwork for channel logo

* clean cache folder on startup (move channel logos, delete everything else)

* log database migration

* update homepage docs for libraries

* fix adding new channel with logo

* fix episode numbers in epg
This commit is contained in:
Jason Dove
2021-02-28 17:48:01 +00:00
committed by GitHub
parent e25b9edd01
commit f392bab118
392 changed files with 52108 additions and 4079 deletions
@@ -3,57 +3,39 @@ using System.IO;
using System.Text.RegularExpressions;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Metadata
{
public class FallbackMetadataProvider : IFallbackMetadataProvider
{
public TelevisionShowMetadata GetFallbackMetadataForShow(string showFolder)
public ShowMetadata GetFallbackMetadataForShow(string showFolder)
{
string fileName = Path.GetFileName(showFolder);
var metadata = new TelevisionShowMetadata
{ Source = MetadataSource.Fallback, Title = fileName ?? showFolder };
var metadata = new ShowMetadata
{ MetadataKind = MetadataKind.Fallback, Title = fileName ?? showFolder };
return GetTelevisionShowMetadata(fileName, metadata);
}
public static TelevisionEpisodeMetadata GetFallbackMetadata(TelevisionEpisodeMediaItem mediaItem)
public static Tuple<EpisodeMetadata, int> GetFallbackMetadata(Episode episode)
{
string fileName = Path.GetFileName(mediaItem.Path);
var metadata = new TelevisionEpisodeMetadata
{ Source = MetadataSource.Fallback, Title = fileName ?? mediaItem.Path };
if (fileName != null)
{
if (!(mediaItem.Source is LocalMediaSource))
{
return metadata;
}
return GetEpisodeMetadata(fileName, metadata);
}
return metadata;
string path = episode.MediaVersions.Head().MediaFiles.Head().Path;
string fileName = Path.GetFileName(path);
var metadata = new EpisodeMetadata
{ MetadataKind = MetadataKind.Fallback, Title = fileName ?? path };
return fileName != null ? GetEpisodeMetadata(fileName, metadata) : Tuple(metadata, 0);
}
public static MovieMetadata GetFallbackMetadata(MovieMediaItem mediaItem)
public static MovieMetadata GetFallbackMetadata(Movie movie)
{
string fileName = Path.GetFileName(mediaItem.Path);
var metadata = new MovieMetadata { Source = MetadataSource.Fallback, Title = fileName ?? mediaItem.Path };
string path = movie.MediaVersions.Head().MediaFiles.Head().Path;
string fileName = Path.GetFileName(path);
var metadata = new MovieMetadata { MetadataKind = MetadataKind.Fallback, Title = fileName ?? path };
if (fileName != null)
{
if (!(mediaItem.Source is LocalMediaSource))
{
return metadata;
}
return GetMovieMetadata(fileName, metadata);
}
return metadata;
return fileName != null ? GetMovieMetadata(fileName, metadata) : metadata;
}
private static TelevisionEpisodeMetadata GetEpisodeMetadata(string fileName, TelevisionEpisodeMetadata metadata)
private static Tuple<EpisodeMetadata, int> GetEpisodeMetadata(string fileName, EpisodeMetadata metadata)
{
try
{
@@ -62,8 +44,7 @@ namespace ErsatzTV.Core.Metadata
if (match.Success)
{
metadata.Title = match.Groups[1].Value;
metadata.Season = int.Parse(match.Groups[2].Value);
metadata.Episode = int.Parse(match.Groups[3].Value);
return Tuple(metadata, int.Parse(match.Groups[3].Value));
}
}
catch (Exception)
@@ -71,7 +52,7 @@ namespace ErsatzTV.Core.Metadata
// ignored
}
return metadata;
return Tuple(metadata, 0);
}
private static MovieMetadata GetMovieMetadata(string fileName, MovieMetadata metadata)
@@ -84,6 +65,7 @@ namespace ErsatzTV.Core.Metadata
{
metadata.Title = match.Groups[1].Value;
metadata.Year = int.Parse(match.Groups[2].Value);
metadata.ReleaseDate = new DateTime(int.Parse(match.Groups[2].Value), 1, 1);
}
}
catch (Exception)
@@ -94,9 +76,9 @@ namespace ErsatzTV.Core.Metadata
return metadata;
}
private static TelevisionShowMetadata GetTelevisionShowMetadata(
private static ShowMetadata GetTelevisionShowMetadata(
string fileName,
TelevisionShowMetadata metadata)
ShowMetadata metadata)
{
try
{
@@ -106,6 +88,7 @@ namespace ErsatzTV.Core.Metadata
{
metadata.Title = match.Groups[1].Value;
metadata.Year = int.Parse(match.Groups[2].Value);
metadata.ReleaseDate = new DateTime(int.Parse(match.Groups[2].Value), 1, 1);
}
}
catch (Exception)
+16 -2
View File
@@ -4,6 +4,7 @@ using System.IO;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using LanguageExt;
using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Metadata
@@ -13,8 +14,8 @@ namespace ErsatzTV.Core.Metadata
public DateTime GetLastWriteTime(string path) =>
Try(File.GetLastWriteTimeUtc(path)).IfFail(() => DateTime.MinValue);
public bool IsMediaSourceAccessible(LocalMediaSource localMediaSource) =>
Directory.Exists(localMediaSource.Folder);
public bool IsLibraryPathAccessible(LibraryPath libraryPath) =>
Directory.Exists(libraryPath.Path);
public IEnumerable<string> ListSubdirectories(string folder) =>
Try(Directory.EnumerateDirectories(folder)).IfFail(new List<string>());
@@ -24,5 +25,18 @@ namespace ErsatzTV.Core.Metadata
public bool FileExists(string path) => File.Exists(path);
public Task<byte[]> ReadAllBytes(string path) => File.ReadAllBytesAsync(path);
public Unit CopyFile(string source, string destination)
{
string directory = Path.GetDirectoryName(destination) ?? string.Empty;
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
File.Copy(source, destination, true);
return Unit.Default;
}
}
}
+57 -28
View File
@@ -2,18 +2,21 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Domain;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
using LanguageExt;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Metadata
{
public abstract class LocalFolderScanner
{
private static readonly SHA1CryptoServiceProvider Crypto;
public static readonly List<string> VideoFileExtensions = new()
{
".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".ogg", ".mp4",
@@ -46,6 +49,8 @@ namespace ErsatzTV.Core.Metadata
private readonly ILocalStatisticsProvider _localStatisticsProvider;
private readonly ILogger _logger;
static LocalFolderScanner() => Crypto = new SHA1CryptoServiceProvider();
protected LocalFolderScanner(
ILocalFileSystem localFileSystem,
ILocalStatisticsProvider localStatisticsProvider,
@@ -63,11 +68,18 @@ namespace ErsatzTV.Core.Metadata
{
try
{
if (mediaItem.Statistics is null ||
(mediaItem.Statistics.LastWriteTime ?? DateTime.MinValue) <
_localFileSystem.GetLastWriteTime(mediaItem.Path))
MediaVersion version = mediaItem switch
{
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", mediaItem.Path);
Movie m => m.MediaVersions.Head(),
Episode e => e.MediaVersions.Head(),
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
};
string path = version.MediaFiles.Head().Path;
if (version.DateUpdated < _localFileSystem.GetLastWriteTime(path))
{
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", path);
await _localStatisticsProvider.RefreshStatistics(ffprobePath, mediaItem);
}
@@ -79,30 +91,47 @@ namespace ErsatzTV.Core.Metadata
}
}
protected async Task SavePosterToDisk<T>(
T show,
string posterPath,
Func<T, Task<bool>> update,
int height = 220) where T : IHasAPoster
protected bool RefreshArtwork(string artworkFile, Domain.Metadata metadata, ArtworkKind artworkKind)
{
byte[] originalBytes = await _localFileSystem.ReadAllBytes(posterPath);
Either<BaseError, string> maybeHash = await _imageCache.ResizeAndSaveImage(originalBytes, height, null);
await maybeHash.Match(
hash =>
{
show.Poster = hash;
show.PosterLastWriteTime = _localFileSystem.GetLastWriteTime(posterPath);
return update(show);
},
error =>
{
_logger.LogWarning("Unable to save poster to disk from {Path}: {Error}", posterPath, error.Value);
return Task.CompletedTask;
});
}
DateTime lastWriteTime = _localFileSystem.GetLastWriteTime(artworkFile);
protected Task<Either<BaseError, string>> SavePosterToDisk(string posterPath, int height = 220) =>
_localFileSystem.ReadAllBytes(posterPath)
.Bind(bytes => _imageCache.ResizeAndSaveImage(bytes, height, null));
metadata.Artwork ??= new List<Artwork>();
Option<Artwork> maybePoster =
Optional(metadata.Artwork).Flatten().FirstOrDefault(a => a.ArtworkKind == artworkKind);
bool shouldRefresh = maybePoster.Match(
artwork => artwork.DateUpdated < lastWriteTime,
true);
if (shouldRefresh)
{
_logger.LogDebug("Refreshing {Attribute} from {Path}", artworkKind, artworkFile);
string cacheName = _imageCache.CopyArtworkToCache(artworkFile, artworkKind);
maybePoster.Match(
artwork =>
{
artwork.Path = cacheName;
artwork.DateUpdated = lastWriteTime;
},
() =>
{
var artwork = new Artwork
{
Path = cacheName,
DateAdded = DateTime.UtcNow,
DateUpdated = lastWriteTime,
ArtworkKind = artworkKind
};
metadata.Artwork.Add(artwork);
});
return true;
}
return false;
}
}
}
+128 -94
View File
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Xml.Serialization;
@@ -37,94 +38,132 @@ namespace ErsatzTV.Core.Metadata
_logger = logger;
}
public Task<TelevisionShowMetadata> GetMetadataForShow(string showFolder)
public async Task<ShowMetadata> GetMetadataForShow(string showFolder)
{
string nfoFileName = Path.Combine(showFolder, "tvshow.nfo");
return Optional(_localFileSystem.FileExists(nfoFileName))
.Filter(identity).AsTask()
.Bind(_ => LoadTelevisionShowMetadata(nfoFileName))
.IfNoneAsync(() => _fallbackMetadataProvider.GetFallbackMetadataForShow(showFolder).AsTask())
.Map(
m =>
{
m.SortTitle = GetSortTitle(m.Title);
return m;
});
Option<ShowMetadata> maybeMetadata = None;
if (_localFileSystem.FileExists(nfoFileName))
{
maybeMetadata = await LoadTelevisionShowMetadata(nfoFileName);
}
return maybeMetadata.IfNone(
() =>
{
ShowMetadata metadata = _fallbackMetadataProvider.GetFallbackMetadataForShow(showFolder);
metadata.SortTitle = GetSortTitle(metadata.Title);
return metadata;
});
}
public Task<Unit> RefreshSidecarMetadata(MediaItem mediaItem, string path) =>
mediaItem switch
{
TelevisionEpisodeMediaItem e => LoadMetadata(e, path)
Episode e => LoadMetadata(e, path)
.Bind(maybeMetadata => maybeMetadata.IfSomeAsync(metadata => ApplyMetadataUpdate(e, metadata))),
MovieMediaItem m => LoadMetadata(m, path)
Movie m => LoadMetadata(m, path)
.Bind(maybeMetadata => maybeMetadata.IfSomeAsync(metadata => ApplyMetadataUpdate(m, metadata))),
_ => Task.FromResult(Unit.Default)
};
public Task<Unit> RefreshSidecarMetadata(TelevisionShow televisionShow, string showFolder) =>
public Task<Unit> RefreshSidecarMetadata(Show televisionShow, string showFolder) =>
LoadMetadata(televisionShow, showFolder).Bind(
maybeMetadata => maybeMetadata.IfSomeAsync(metadata => ApplyMetadataUpdate(televisionShow, metadata)));
public Task<Unit> RefreshFallbackMetadata(MediaItem mediaItem) =>
mediaItem switch
{
TelevisionEpisodeMediaItem e => ApplyMetadataUpdate(e, FallbackMetadataProvider.GetFallbackMetadata(e))
Episode e => ApplyMetadataUpdate(e, FallbackMetadataProvider.GetFallbackMetadata(e))
.ToUnit(),
MovieMediaItem m => ApplyMetadataUpdate(m, FallbackMetadataProvider.GetFallbackMetadata(m)).ToUnit(),
Movie m => ApplyMetadataUpdate(m, FallbackMetadataProvider.GetFallbackMetadata(m)).ToUnit(),
_ => Task.FromResult(Unit.Default)
};
public Task<Unit> RefreshFallbackMetadata(TelevisionShow televisionShow, string showFolder) =>
public Task<Unit> RefreshFallbackMetadata(Show televisionShow, string showFolder) =>
ApplyMetadataUpdate(televisionShow, _fallbackMetadataProvider.GetFallbackMetadataForShow(showFolder))
.ToUnit();
private async Task ApplyMetadataUpdate(TelevisionEpisodeMediaItem mediaItem, TelevisionEpisodeMetadata metadata)
private async Task ApplyMetadataUpdate(Episode episode, Tuple<EpisodeMetadata, int> metadataEpisodeNumber)
{
mediaItem.Metadata ??= new TelevisionEpisodeMetadata { TelevisionEpisodeId = mediaItem.Id };
mediaItem.Metadata.Source = metadata.Source;
mediaItem.Metadata.LastWriteTime = metadata.LastWriteTime;
mediaItem.Metadata.Title = metadata.Title;
mediaItem.Metadata.SortTitle = GetSortTitle(metadata.Title);
mediaItem.Metadata.Season = metadata.Season;
mediaItem.Metadata.Episode = metadata.Episode;
mediaItem.Metadata.Plot = metadata.Plot;
mediaItem.Metadata.Aired = metadata.Aired;
(EpisodeMetadata metadata, int episodeNumber) = metadataEpisodeNumber;
episode.EpisodeNumber = episodeNumber;
Optional(episode.EpisodeMetadata).Flatten().HeadOrNone().Match(
existing =>
{
existing.Outline = metadata.Outline;
existing.Plot = metadata.Plot;
existing.Tagline = metadata.Tagline;
existing.Title = metadata.Title;
existing.DateAdded = metadata.DateAdded;
existing.DateUpdated = metadata.DateUpdated;
existing.MetadataKind = metadata.MetadataKind;
existing.OriginalTitle = metadata.OriginalTitle;
existing.ReleaseDate = metadata.ReleaseDate;
existing.Year = metadata.Year;
existing.SortTitle = metadata.SortTitle ?? GetSortTitle(metadata.Title);
},
() =>
{
metadata.SortTitle ??= GetSortTitle(metadata.Title);
episode.EpisodeMetadata = new List<EpisodeMetadata> { metadata };
});
await _televisionRepository.Update(mediaItem);
await _televisionRepository.Update(episode);
}
private async Task ApplyMetadataUpdate(MovieMediaItem mediaItem, MovieMetadata metadata)
private async Task ApplyMetadataUpdate(Movie movie, MovieMetadata metadata)
{
mediaItem.Metadata ??= new MovieMetadata();
mediaItem.Metadata.Source = metadata.Source;
mediaItem.Metadata.LastWriteTime = metadata.LastWriteTime;
mediaItem.Metadata.Title = metadata.Title;
mediaItem.Metadata.SortTitle = GetSortTitle(metadata.Title);
mediaItem.Metadata.Year = metadata.Year;
mediaItem.Metadata.Premiered = metadata.Premiered;
mediaItem.Metadata.Plot = metadata.Plot;
mediaItem.Metadata.Outline = metadata.Outline;
mediaItem.Metadata.Tagline = metadata.Tagline;
mediaItem.Metadata.ContentRating = metadata.ContentRating;
Optional(movie.MovieMetadata).Flatten().HeadOrNone().Match(
existing =>
{
existing.Outline = metadata.Outline;
existing.Plot = metadata.Plot;
existing.Tagline = metadata.Tagline;
existing.Title = metadata.Title;
existing.DateAdded = metadata.DateAdded;
existing.DateUpdated = metadata.DateUpdated;
existing.MetadataKind = metadata.MetadataKind;
existing.OriginalTitle = metadata.OriginalTitle;
existing.ReleaseDate = metadata.ReleaseDate;
existing.Year = metadata.Year;
existing.SortTitle = metadata.SortTitle ?? GetSortTitle(metadata.Title);
},
() =>
{
metadata.SortTitle ??= GetSortTitle(metadata.Title);
movie.MovieMetadata = new List<MovieMetadata> { metadata };
});
await _mediaItemRepository.Update(mediaItem);
await _mediaItemRepository.Update(movie);
}
private async Task ApplyMetadataUpdate(TelevisionShow televisionShow, TelevisionShowMetadata metadata)
private async Task ApplyMetadataUpdate(Show show, ShowMetadata metadata)
{
televisionShow.Metadata ??= new TelevisionShowMetadata();
televisionShow.Metadata.Source = metadata.Source;
televisionShow.Metadata.LastWriteTime = metadata.LastWriteTime;
televisionShow.Metadata.Title = metadata.Title;
televisionShow.Metadata.Plot = metadata.Plot;
televisionShow.Metadata.Year = metadata.Year;
televisionShow.Metadata.SortTitle = GetSortTitle(metadata.Title);
Optional(show.ShowMetadata).Flatten().HeadOrNone().Match(
existing =>
{
existing.Outline = metadata.Outline;
existing.Plot = metadata.Plot;
existing.Tagline = metadata.Tagline;
existing.Title = metadata.Title;
existing.DateAdded = metadata.DateAdded;
existing.DateUpdated = metadata.DateUpdated;
existing.MetadataKind = metadata.MetadataKind;
existing.OriginalTitle = metadata.OriginalTitle;
existing.ReleaseDate = metadata.ReleaseDate;
existing.Year = metadata.Year;
existing.SortTitle = metadata.SortTitle ?? GetSortTitle(metadata.Title);
},
() =>
{
metadata.SortTitle ??= GetSortTitle(metadata.Title);
show.ShowMetadata = new List<ShowMetadata> { metadata };
});
await _televisionRepository.Update(televisionShow);
await _televisionRepository.Update(show);
}
private async Task<Option<MovieMetadata>> LoadMetadata(MovieMediaItem mediaItem, string nfoFileName)
private async Task<Option<MovieMetadata>> LoadMetadata(Movie mediaItem, string nfoFileName)
{
if (nfoFileName == null || !File.Exists(nfoFileName))
{
@@ -132,18 +171,10 @@ namespace ErsatzTV.Core.Metadata
return None;
}
if (!(mediaItem.Source is LocalMediaSource))
{
_logger.LogDebug("Media source {Name} is not a local media source", mediaItem.Source.Name);
return None;
}
return await LoadMovieMetadata(mediaItem, nfoFileName);
}
private async Task<Option<TelevisionEpisodeMetadata>> LoadMetadata(
TelevisionEpisodeMediaItem mediaItem,
string nfoFileName)
private async Task<Option<Tuple<EpisodeMetadata, int>>> LoadMetadata(Episode mediaItem, string nfoFileName)
{
if (nfoFileName == null || !File.Exists(nfoFileName))
{
@@ -151,18 +182,10 @@ namespace ErsatzTV.Core.Metadata
return None;
}
if (!(mediaItem.Source is LocalMediaSource))
{
_logger.LogDebug("Media source {Name} is not a local media source", mediaItem.Source.Name);
return None;
}
return await LoadEpisodeMetadata(mediaItem, nfoFileName);
}
private async Task<Option<TelevisionShowMetadata>> LoadMetadata(
TelevisionShow televisionShow,
string nfoFileName)
private async Task<Option<ShowMetadata>> LoadMetadata(Show televisionShow, string nfoFileName)
{
if (nfoFileName == null || !File.Exists(nfoFileName))
{
@@ -173,20 +196,23 @@ namespace ErsatzTV.Core.Metadata
return await LoadTelevisionShowMetadata(nfoFileName);
}
private async Task<Option<TelevisionShowMetadata>> LoadTelevisionShowMetadata(string nfoFileName)
private async Task<Option<ShowMetadata>> LoadTelevisionShowMetadata(string nfoFileName)
{
try
{
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read);
Option<TvShowNfo> maybeNfo = TvShowSerializer.Deserialize(fileStream) as TvShowNfo;
return maybeNfo.Match<Option<TelevisionShowMetadata>>(
nfo => new TelevisionShowMetadata
return maybeNfo.Match<Option<ShowMetadata>>(
nfo => new ShowMetadata
{
Source = MetadataSource.Sidecar,
LastWriteTime = File.GetLastWriteTimeUtc(nfoFileName),
MetadataKind = MetadataKind.Sidecar,
DateUpdated = File.GetLastWriteTimeUtc(nfoFileName),
Title = nfo.Title,
Plot = nfo.Plot,
Year = nfo.Year
Outline = nfo.Outline,
Tagline = nfo.Tagline,
Year = nfo.Year,
ReleaseDate = GetAired(nfo.Premiered) ?? new DateTime(nfo.Year, 1, 1)
},
None);
}
@@ -197,35 +223,35 @@ namespace ErsatzTV.Core.Metadata
}
}
private async Task<Option<TelevisionEpisodeMetadata>> LoadEpisodeMetadata(
TelevisionEpisodeMediaItem mediaItem,
string nfoFileName)
private async Task<Option<Tuple<EpisodeMetadata, int>>> LoadEpisodeMetadata(Episode episode, string nfoFileName)
{
try
{
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read);
Option<TvShowEpisodeNfo> maybeNfo = EpisodeSerializer.Deserialize(fileStream) as TvShowEpisodeNfo;
return maybeNfo.Match<Option<TelevisionEpisodeMetadata>>(
nfo => new TelevisionEpisodeMetadata
return maybeNfo.Match<Option<Tuple<EpisodeMetadata, int>>>(
nfo =>
{
Source = MetadataSource.Sidecar,
LastWriteTime = File.GetLastWriteTimeUtc(nfoFileName),
Title = nfo.Title,
Aired = GetAired(nfo.Aired),
Episode = nfo.Episode,
Season = nfo.Season,
Plot = nfo.Plot
var metadata = new EpisodeMetadata
{
MetadataKind = MetadataKind.Sidecar,
DateUpdated = File.GetLastWriteTimeUtc(nfoFileName),
Title = nfo.Title,
ReleaseDate = GetAired(nfo.Aired),
Plot = nfo.Plot
};
return Tuple(metadata, nfo.Episode);
},
None);
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Failed to read TV episode nfo metadata from {Path}", nfoFileName);
return FallbackMetadataProvider.GetFallbackMetadata(mediaItem);
return FallbackMetadataProvider.GetFallbackMetadata(episode);
}
}
private async Task<Option<MovieMetadata>> LoadMovieMetadata(MovieMediaItem mediaItem, string nfoFileName)
private async Task<Option<MovieMetadata>> LoadMovieMetadata(Movie mediaItem, string nfoFileName)
{
try
{
@@ -234,15 +260,14 @@ namespace ErsatzTV.Core.Metadata
return maybeNfo.Match<Option<MovieMetadata>>(
nfo => new MovieMetadata
{
Source = MetadataSource.Sidecar,
LastWriteTime = File.GetLastWriteTimeUtc(nfoFileName),
MetadataKind = MetadataKind.Sidecar,
DateUpdated = File.GetLastWriteTimeUtc(nfoFileName),
Title = nfo.Title,
Year = nfo.Year,
Premiered = nfo.Premiered,
ReleaseDate = nfo.Premiered,
Plot = nfo.Plot,
Outline = nfo.Outline,
Tagline = nfo.Tagline,
ContentRating = nfo.ContentRating
Tagline = nfo.Tagline
},
None);
}
@@ -324,6 +349,15 @@ namespace ErsatzTV.Core.Metadata
[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; }
}
[XmlRoot("episodedetails")]
@@ -33,42 +33,49 @@ namespace ErsatzTV.Core.Metadata
{
try
{
FFprobe ffprobe = await GetProbeOutput(ffprobePath, mediaItem);
MediaItemStatistics statistics = ProjectToMediaItemStatistics(ffprobe);
return await ApplyStatisticsUpdate(mediaItem, statistics);
string filePath = mediaItem switch
{
Movie m => m.MediaVersions.Head().MediaFiles.Head().Path,
Episode e => e.MediaVersions.Head().MediaFiles.Head().Path,
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
};
FFprobe ffprobe = await GetProbeOutput(ffprobePath, filePath);
MediaVersion version = ProjectToMediaVersion(ffprobe);
return await ApplyVersionUpdate(mediaItem, version, filePath);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to refresh statistics for media item at {Path}", mediaItem.Path);
_logger.LogWarning(ex, "Failed to refresh statistics for media item {Id}", mediaItem.Id);
return false;
}
}
private async Task<bool> ApplyStatisticsUpdate(
MediaItem mediaItem,
MediaItemStatistics statistics)
private async Task<bool> ApplyVersionUpdate(MediaItem mediaItem, MediaVersion version, string filePath)
{
if (mediaItem.Statistics == null)
MediaVersion mediaItemVersion = mediaItem switch
{
mediaItem.Statistics = new MediaItemStatistics();
}
Movie m => m.MediaVersions.Head(),
Episode e => e.MediaVersions.Head(),
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
};
bool durationChange = mediaItem.Statistics.Duration != statistics.Duration;
bool durationChange = mediaItemVersion.Duration != version.Duration;
mediaItem.Statistics.LastWriteTime = _localFileSystem.GetLastWriteTime(mediaItem.Path);
mediaItem.Statistics.Duration = statistics.Duration;
mediaItem.Statistics.AudioCodec = statistics.AudioCodec;
mediaItem.Statistics.SampleAspectRatio = statistics.SampleAspectRatio;
mediaItem.Statistics.DisplayAspectRatio = statistics.DisplayAspectRatio;
mediaItem.Statistics.Width = statistics.Width;
mediaItem.Statistics.Height = statistics.Height;
mediaItem.Statistics.VideoCodec = statistics.VideoCodec;
mediaItem.Statistics.VideoScanType = statistics.VideoScanType;
mediaItemVersion.DateUpdated = _localFileSystem.GetLastWriteTime(filePath);
mediaItemVersion.Duration = version.Duration;
mediaItemVersion.AudioCodec = version.AudioCodec;
mediaItemVersion.SampleAspectRatio = version.SampleAspectRatio;
mediaItemVersion.DisplayAspectRatio = version.DisplayAspectRatio;
mediaItemVersion.Width = version.Width;
mediaItemVersion.Height = version.Height;
mediaItemVersion.VideoCodec = version.VideoCodec;
mediaItemVersion.VideoScanKind = version.VideoScanKind;
return await _mediaItemRepository.Update(mediaItem) && durationChange;
}
private Task<FFprobe> GetProbeOutput(string ffprobePath, MediaItem mediaItem)
private Task<FFprobe> GetProbeOutput(string ffprobePath, string filePath)
{
var startInfo = new ProcessStartInfo
{
@@ -85,7 +92,7 @@ namespace ErsatzTV.Core.Metadata
startInfo.ArgumentList.Add("-show_format");
startInfo.ArgumentList.Add("-show_streams");
startInfo.ArgumentList.Add("-i");
startInfo.ArgumentList.Add(mediaItem.Path);
startInfo.ArgumentList.Add(filePath);
var probe = new Process
{
@@ -101,7 +108,7 @@ namespace ErsatzTV.Core.Metadata
});
}
private MediaItemStatistics ProjectToMediaItemStatistics(FFprobe probeOutput) =>
private MediaVersion ProjectToMediaVersion(FFprobe probeOutput) =>
Optional(probeOutput)
.Filter(json => json?.format != null && json.streams != null)
.ToValidation<BaseError>("Unable to parse ffprobe output")
@@ -111,41 +118,35 @@ namespace ErsatzTV.Core.Metadata
{
var duration = TimeSpan.FromSeconds(double.Parse(json.format.duration));
var statistics = new MediaItemStatistics { Duration = duration };
var version = new MediaVersion { Name = "Main", Duration = duration };
FFprobeStream audioStream = json.streams.FirstOrDefault(s => s.codec_type == "audio");
if (audioStream != null)
{
statistics = statistics with
{
AudioCodec = audioStream.codec_name
};
version.AudioCodec = audioStream.codec_name;
}
FFprobeStream videoStream = json.streams.FirstOrDefault(s => s.codec_type == "video");
if (videoStream != null)
{
statistics = statistics with
{
SampleAspectRatio = videoStream.sample_aspect_ratio,
DisplayAspectRatio = videoStream.display_aspect_ratio,
Width = videoStream.width,
Height = videoStream.height,
VideoCodec = videoStream.codec_name,
VideoScanType = ScanTypeFromFieldOrder(videoStream.field_order)
};
version.SampleAspectRatio = videoStream.sample_aspect_ratio;
version.DisplayAspectRatio = videoStream.display_aspect_ratio;
version.Width = videoStream.width;
version.Height = videoStream.height;
version.VideoCodec = videoStream.codec_name;
version.VideoScanKind = ScanKindFromFieldOrder(videoStream.field_order);
}
return statistics;
return version;
},
_ => new MediaItemStatistics());
_ => new MediaVersion { Name = "Main" });
private VideoScanType ScanTypeFromFieldOrder(string fieldOrder) =>
private VideoScanKind ScanKindFromFieldOrder(string fieldOrder) =>
fieldOrder?.ToLowerInvariant() switch
{
var x when x == "tt" || x == "bb" || x == "tb" || x == "bt" => VideoScanType.Interlaced,
"progressive" => VideoScanType.Progressive,
_ => VideoScanType.Unknown
var x when x == "tt" || x == "bb" || x == "tb" || x == "bt" => VideoScanKind.Interlaced,
"progressive" => VideoScanKind.Progressive,
_ => VideoScanKind.Unknown
};
// ReSharper disable InconsistentNaming
+28 -24
View File
@@ -37,15 +37,15 @@ namespace ErsatzTV.Core.Metadata
_logger = logger;
}
public async Task<Either<BaseError, Unit>> ScanFolder(LocalMediaSource localMediaSource, string ffprobePath)
public async Task<Either<BaseError, Unit>> ScanFolder(LibraryPath libraryPath, string ffprobePath)
{
if (!_localFileSystem.IsMediaSourceAccessible(localMediaSource))
if (!_localFileSystem.IsLibraryPathAccessible(libraryPath))
{
return new MediaSourceInaccessible();
}
var folderQueue = new Queue<string>();
foreach (string folder in _localFileSystem.ListSubdirectories(localMediaSource.Folder).OrderBy(identity))
foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path).OrderBy(identity))
{
folderQueue.Enqueue(folder);
}
@@ -73,10 +73,10 @@ namespace ErsatzTV.Core.Metadata
foreach (string file in allFiles.OrderBy(identity))
{
// TODO: optimize dbcontext use here, do we need tracking? can we make partial updates with dapper?
// TODO: figure out how to rebuild playlists
Either<BaseError, MovieMediaItem> x = await _movieRepository.GetOrAdd(localMediaSource.Id, file);
Either<BaseError, MovieMediaItem> maybeMovie = await x.AsTask()
Either<BaseError, Movie> maybeMovie = await _movieRepository
.GetOrAdd(libraryPath, file)
.BindT(movie => UpdateStatistics(movie, ffprobePath).MapT(_ => movie))
.BindT(UpdateMetadata)
.BindT(UpdatePoster);
@@ -89,16 +89,19 @@ namespace ErsatzTV.Core.Metadata
return Unit.Default;
}
private async Task<Either<BaseError, MovieMediaItem>> UpdateMetadata(MovieMediaItem movie)
private async Task<Either<BaseError, Movie>> UpdateMetadata(Movie movie)
{
try
{
await LocateNfoFile(movie).Match(
async nfoFile =>
{
if (movie.Metadata == null || movie.Metadata.Source == MetadataSource.Fallback ||
(movie.Metadata.LastWriteTime ?? DateTime.MinValue) <
_localFileSystem.GetLastWriteTime(nfoFile))
bool shouldUpdate = Optional(movie.MovieMetadata).Flatten().HeadOrNone().Match(
m => m.MetadataKind == MetadataKind.Fallback ||
m.DateUpdated < _localFileSystem.GetLastWriteTime(nfoFile),
true);
if (shouldUpdate)
{
_logger.LogDebug("Refreshing {Attribute} from {Path}", "Sidecar Metadata", nfoFile);
await _localMetadataProvider.RefreshSidecarMetadata(movie, nfoFile);
@@ -106,9 +109,10 @@ namespace ErsatzTV.Core.Metadata
},
async () =>
{
if (movie.Metadata == null)
if (!Optional(movie.MovieMetadata).Flatten().Any())
{
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", movie.Path);
string path = movie.MediaVersions.Head().MediaFiles.Head().Path;
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", path);
await _localMetadataProvider.RefreshFallbackMetadata(movie);
}
});
@@ -121,19 +125,17 @@ namespace ErsatzTV.Core.Metadata
}
}
private async Task<Either<BaseError, MovieMediaItem>> UpdatePoster(MovieMediaItem movie)
private async Task<Either<BaseError, Movie>> UpdatePoster(Movie movie)
{
try
{
await LocatePoster(movie).IfSomeAsync(
async posterFile =>
{
if (string.IsNullOrWhiteSpace(movie.Poster) ||
(movie.PosterLastWriteTime ?? DateTime.MinValue) <
_localFileSystem.GetLastWriteTime(posterFile))
MovieMetadata metadata = movie.MovieMetadata.Head();
if (RefreshArtwork(posterFile, metadata, ArtworkKind.Poster))
{
_logger.LogDebug("Refreshing {Attribute} from {Path}", "Poster", posterFile);
await SavePosterToDisk(movie, posterFile, _movieRepository.Update, 440);
await _movieRepository.Update(movie);
}
});
@@ -145,20 +147,22 @@ namespace ErsatzTV.Core.Metadata
}
}
private Option<string> LocateNfoFile(MovieMediaItem movie)
private Option<string> LocateNfoFile(Movie movie)
{
string movieAsNfo = Path.ChangeExtension(movie.Path, "nfo");
string movieNfo = Path.Combine(Path.GetDirectoryName(movie.Path) ?? string.Empty, "movie.nfo");
string path = movie.MediaVersions.Head().MediaFiles.Head().Path;
string movieAsNfo = Path.ChangeExtension(path, "nfo");
string movieNfo = Path.Combine(Path.GetDirectoryName(path) ?? string.Empty, "movie.nfo");
return Seq.create(movieAsNfo, movieNfo)
.Filter(s => _localFileSystem.FileExists(s))
.HeadOrNone();
}
private Option<string> LocatePoster(MovieMediaItem movie)
private Option<string> LocatePoster(Movie movie)
{
string folder = Path.GetDirectoryName(movie.Path) ?? string.Empty;
string path = movie.MediaVersions.Head().MediaFiles.Head().Path;
string folder = Path.GetDirectoryName(path) ?? string.Empty;
IEnumerable<string> possibleMoviePosters = ImageFileExtensions.Collect(
ext => new[] { $"poster.{ext}", Path.GetFileNameWithoutExtension(movie.Path) + $"-poster.{ext}" })
ext => new[] { $"poster.{ext}", Path.GetFileNameWithoutExtension(path) + $"-poster.{ext}" })
.Map(f => Path.Combine(folder, f));
Option<string> result = possibleMoviePosters.Filter(p => _localFileSystem.FileExists(p)).HeadOrNone();
return result;
+83 -107
View File
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
@@ -37,88 +39,65 @@ namespace ErsatzTV.Core.Metadata
_logger = logger;
}
public async Task<Unit> ScanFolder(LocalMediaSource localMediaSource, string ffprobePath)
public async Task<Either<BaseError, Unit>> ScanFolder(LibraryPath libraryPath, string ffprobePath)
{
if (!_localFileSystem.IsMediaSourceAccessible(localMediaSource))
if (!_localFileSystem.IsLibraryPathAccessible(libraryPath))
{
_logger.LogWarning(
"Media source is not accessible or missing; skipping scan of {Folder}",
localMediaSource.Folder);
return Unit.Default;
return new MediaSourceInaccessible();
}
var allShowFolders = _localFileSystem.ListSubdirectories(localMediaSource.Folder)
var allShowFolders = _localFileSystem.ListSubdirectories(libraryPath.Path)
.Filter(ShouldIncludeFolder)
.OrderBy(identity)
.ToList();
foreach (string showFolder in allShowFolders)
{
// TODO: check all sources for latest metadata?
Either<BaseError, TelevisionShow> maybeShow =
await FindOrCreateShow(localMediaSource.Id, showFolder)
Either<BaseError, Show> maybeShow =
await FindOrCreateShow(libraryPath.Id, showFolder)
.BindT(show => UpdateMetadataForShow(show, showFolder))
.BindT(show => UpdatePosterForShow(show, showFolder));
await maybeShow.Match(
show => ScanSeasons(localMediaSource, ffprobePath, show, showFolder),
show => ScanSeasons(libraryPath, ffprobePath, show, showFolder),
_ => Task.FromResult(Unit.Default));
}
await _televisionRepository.DeleteMissingSources(localMediaSource.Id, allShowFolders);
await _televisionRepository.DeleteEmptyShows();
return Unit.Default;
}
private async Task<Either<BaseError, TelevisionShow>> FindOrCreateShow(
int localMediaSourceId,
private async Task<Either<BaseError, Show>> FindOrCreateShow(
int libraryPathId,
string showFolder)
{
Option<TelevisionShow> maybeShowByPath =
await _televisionRepository.GetShowByPath(localMediaSourceId, showFolder);
return await maybeShowByPath.Match(
show => Right<BaseError, TelevisionShow>(show).AsTask(),
async () =>
{
TelevisionShowMetadata metadata = await _localMetadataProvider.GetMetadataForShow(showFolder);
Option<TelevisionShow> maybeShow = await _televisionRepository.GetShowByMetadata(metadata);
return await maybeShow.Match(
async show =>
{
show.Sources.Add(
new LocalTelevisionShowSource
{
MediaSourceId = localMediaSourceId,
Path = showFolder,
TelevisionShow = show
});
await _televisionRepository.Update(show);
return Right<BaseError, TelevisionShow>(show);
},
async () => await _televisionRepository.AddShow(localMediaSourceId, showFolder, metadata));
});
ShowMetadata metadata = await _localMetadataProvider.GetMetadataForShow(showFolder);
Option<Show> maybeShow = await _televisionRepository.GetShowByMetadata(libraryPathId, metadata);
return await maybeShow.Match(
show => Right<BaseError, Show>(show).AsTask(),
async () => await _televisionRepository.AddShow(libraryPathId, showFolder, metadata));
}
private async Task<Unit> ScanSeasons(
LocalMediaSource localMediaSource,
LibraryPath libraryPath,
string ffprobePath,
TelevisionShow show,
string showPath)
Show show,
string showFolder)
{
foreach (string seasonFolder in _localFileSystem.ListSubdirectories(showPath).Filter(ShouldIncludeFolder)
foreach (string seasonFolder in _localFileSystem.ListSubdirectories(showFolder).Filter(ShouldIncludeFolder)
.OrderBy(identity))
{
Option<int> maybeSeasonNumber = SeasonNumberForFolder(seasonFolder);
await maybeSeasonNumber.IfSomeAsync(
async seasonNumber =>
{
Either<BaseError, TelevisionSeason> maybeSeason = await _televisionRepository
.GetOrAddSeason(show, seasonFolder, seasonNumber)
.BindT(UpdatePoster);
Either<BaseError, Season> maybeSeason = await _televisionRepository
.GetOrAddSeason(show, libraryPath.Id, seasonNumber)
.BindT(season => UpdatePoster(season, seasonFolder));
await maybeSeason.Match(
season => ScanEpisodes(localMediaSource, ffprobePath, season),
season => ScanEpisodes(libraryPath, ffprobePath, season, seasonFolder),
_ => Task.FromResult(Unit.Default));
});
}
@@ -127,16 +106,17 @@ namespace ErsatzTV.Core.Metadata
}
private async Task<Unit> ScanEpisodes(
LocalMediaSource localMediaSource,
LibraryPath libraryPath,
string ffprobePath,
TelevisionSeason season)
Season season,
string seasonPath)
{
foreach (string file in _localFileSystem.ListFiles(season.Path)
foreach (string file in _localFileSystem.ListFiles(seasonPath)
.Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f))).OrderBy(identity))
{
// TODO: figure out how to rebuild playlists
Either<BaseError, TelevisionEpisodeMediaItem> maybeEpisode = await _televisionRepository
.GetOrAddEpisode(season, localMediaSource.Id, file)
Either<BaseError, Episode> maybeEpisode = await _televisionRepository
.GetOrAddEpisode(season, libraryPath, file)
.BindT(episode => UpdateStatistics(episode, ffprobePath).MapT(_ => episode))
.BindT(UpdateMetadata)
.BindT(UpdateThumbnail);
@@ -148,8 +128,8 @@ namespace ErsatzTV.Core.Metadata
return Unit.Default;
}
private async Task<Either<BaseError, TelevisionShow>> UpdateMetadataForShow(
TelevisionShow show,
private async Task<Either<BaseError, Show>> UpdateMetadataForShow(
Show show,
string showFolder)
{
try
@@ -157,9 +137,12 @@ namespace ErsatzTV.Core.Metadata
await LocateNfoFileForShow(showFolder).Match(
async nfoFile =>
{
if (show.Metadata == null || show.Metadata.Source == MetadataSource.Fallback ||
(show.Metadata.LastWriteTime ?? DateTime.MinValue) <
_localFileSystem.GetLastWriteTime(nfoFile))
bool shouldUpdate = Optional(show.ShowMetadata).Flatten().HeadOrNone().Match(
m => m.MetadataKind == MetadataKind.Fallback ||
m.DateUpdated < _localFileSystem.GetLastWriteTime(nfoFile),
true);
if (shouldUpdate)
{
_logger.LogDebug("Refreshing {Attribute} from {Path}", "Sidecar Metadata", nfoFile);
await _localMetadataProvider.RefreshSidecarMetadata(show, nfoFile);
@@ -167,7 +150,7 @@ namespace ErsatzTV.Core.Metadata
},
async () =>
{
if (show.Metadata == null)
if (!Optional(show.ShowMetadata).Flatten().Any())
{
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", showFolder);
await _localMetadataProvider.RefreshFallbackMetadata(show, showFolder);
@@ -182,17 +165,20 @@ namespace ErsatzTV.Core.Metadata
}
}
private async Task<Either<BaseError, TelevisionEpisodeMediaItem>> UpdateMetadata(
TelevisionEpisodeMediaItem episode)
private async Task<Either<BaseError, Episode>> UpdateMetadata(
Episode episode)
{
try
{
await LocateNfoFile(episode).Match(
async nfoFile =>
{
if (episode.Metadata == null || episode.Metadata.Source == MetadataSource.Fallback ||
(episode.Metadata.LastWriteTime ?? DateTime.MinValue) <
_localFileSystem.GetLastWriteTime(nfoFile))
bool shouldUpdate = Optional(episode.EpisodeMetadata).Flatten().HeadOrNone().Match(
m => m.MetadataKind == MetadataKind.Fallback ||
m.DateUpdated < _localFileSystem.GetLastWriteTime(nfoFile),
true);
if (shouldUpdate)
{
_logger.LogDebug("Refreshing {Attribute} from {Path}", "Sidecar Metadata", nfoFile);
await _localMetadataProvider.RefreshSidecarMetadata(episode, nfoFile);
@@ -200,9 +186,10 @@ namespace ErsatzTV.Core.Metadata
},
async () =>
{
if (episode.Metadata == null)
if (!Optional(episode.EpisodeMetadata).Flatten().Any())
{
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", episode.Path);
string path = episode.MediaVersions.Head().MediaFiles.Head().Path;
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", path);
await _localMetadataProvider.RefreshFallbackMetadata(episode);
}
});
@@ -215,8 +202,8 @@ namespace ErsatzTV.Core.Metadata
}
}
private async Task<Either<BaseError, TelevisionShow>> UpdatePosterForShow(
TelevisionShow show,
private async Task<Either<BaseError, Show>> UpdatePosterForShow(
Show show,
string showFolder)
{
try
@@ -224,27 +211,10 @@ namespace ErsatzTV.Core.Metadata
await LocatePosterForShow(showFolder).IfSomeAsync(
async posterFile =>
{
if (string.IsNullOrWhiteSpace(show.Poster) ||
(show.PosterLastWriteTime ?? DateTime.MinValue) <
_localFileSystem.GetLastWriteTime(posterFile))
ShowMetadata metadata = show.ShowMetadata.Head();
if (RefreshArtwork(posterFile, metadata, ArtworkKind.Poster))
{
_logger.LogDebug("Refreshing {Attribute} from {Path}", "Poster", posterFile);
Either<BaseError, string> maybePoster = await SavePosterToDisk(posterFile, 440);
await maybePoster.Match(
poster =>
{
show.Poster = poster;
show.PosterLastWriteTime = _localFileSystem.GetLastWriteTime(posterFile);
return _televisionRepository.Update(show);
},
error =>
{
_logger.LogWarning(
"Unable to save poster to disk from {Path}: {Error}",
posterFile,
error.Value);
return Task.CompletedTask;
});
await _televisionRepository.Update(show);
}
});
@@ -256,19 +226,24 @@ namespace ErsatzTV.Core.Metadata
}
}
private async Task<Either<BaseError, TelevisionSeason>> UpdatePoster(TelevisionSeason season)
private async Task<Either<BaseError, Season>> UpdatePoster(Season season, string seasonFolder)
{
try
{
await LocatePoster(season).IfSomeAsync(
await LocatePoster(season, seasonFolder).IfSomeAsync(
async posterFile =>
{
if (string.IsNullOrWhiteSpace(season.Poster) ||
(season.PosterLastWriteTime ?? DateTime.MinValue) <
_localFileSystem.GetLastWriteTime(posterFile))
season.SeasonMetadata ??= new List<SeasonMetadata>();
if (!season.SeasonMetadata.Any())
{
_logger.LogDebug("Refreshing {Attribute} from {Path}", "Poster", posterFile);
await SavePosterToDisk(season, posterFile, _televisionRepository.Update, 440);
season.SeasonMetadata.Add(new SeasonMetadata { SeasonId = season.Id });
}
SeasonMetadata metadata = season.SeasonMetadata.Head();
if (RefreshArtwork(posterFile, metadata, ArtworkKind.Poster))
{
await _televisionRepository.Update(season);
}
});
@@ -280,20 +255,17 @@ namespace ErsatzTV.Core.Metadata
}
}
private async Task<Either<BaseError, TelevisionEpisodeMediaItem>> UpdateThumbnail(
TelevisionEpisodeMediaItem episode)
private async Task<Either<BaseError, Episode>> UpdateThumbnail(Episode episode)
{
try
{
await LocateThumbnail(episode).IfSomeAsync(
async posterFile =>
{
if (string.IsNullOrWhiteSpace(episode.Poster) ||
(episode.PosterLastWriteTime ?? DateTime.MinValue) <
_localFileSystem.GetLastWriteTime(posterFile))
EpisodeMetadata metadata = episode.EpisodeMetadata.Head();
if (RefreshArtwork(posterFile, metadata, ArtworkKind.Thumbnail))
{
_logger.LogDebug("Refreshing {Attribute} from {Path}", "Thumbnail", posterFile);
await SavePosterToDisk(episode, posterFile, _televisionRepository.Update);
await _televisionRepository.Update(episode);
}
});
@@ -309,9 +281,12 @@ namespace ErsatzTV.Core.Metadata
Optional(Path.Combine(showFolder, "tvshow.nfo"))
.Filter(s => _localFileSystem.FileExists(s));
private Option<string> LocateNfoFile(TelevisionEpisodeMediaItem episode) =>
Optional(Path.ChangeExtension(episode.Path, "nfo"))
private Option<string> LocateNfoFile(Episode episode)
{
string path = episode.MediaVersions.Head().MediaFiles.Head().Path;
return Optional(Path.ChangeExtension(path, "nfo"))
.Filter(s => _localFileSystem.FileExists(s));
}
private Option<string> LocatePosterForShow(string showFolder) =>
ImageFileExtensions
@@ -320,20 +295,21 @@ namespace ErsatzTV.Core.Metadata
.Filter(s => _localFileSystem.FileExists(s))
.HeadOrNone();
private Option<string> LocatePoster(TelevisionSeason season)
private Option<string> LocatePoster(Season season, string seasonFolder)
{
string folder = Path.GetDirectoryName(season.Path) ?? string.Empty;
string folder = Path.GetDirectoryName(seasonFolder) ?? string.Empty;
return ImageFileExtensions
.Map(ext => Path.Combine(folder, $"season{season.Number:00}-poster.{ext}"))
.Map(ext => Path.Combine(folder, $"season{season.SeasonNumber:00}-poster.{ext}"))
.Filter(s => _localFileSystem.FileExists(s))
.HeadOrNone();
}
private Option<string> LocateThumbnail(TelevisionEpisodeMediaItem episode)
private Option<string> LocateThumbnail(Episode episode)
{
string folder = Path.GetDirectoryName(episode.Path) ?? string.Empty;
string path = episode.MediaVersions.Head().MediaFiles.Head().Path;
string folder = Path.GetDirectoryName(path) ?? string.Empty;
return ImageFileExtensions
.Map(ext => Path.GetFileNameWithoutExtension(episode.Path) + $"-thumb.{ext}")
.Map(ext => Path.GetFileNameWithoutExtension(path) + $"-thumb.{ext}")
.Map(f => Path.Combine(folder, f))
.Filter(f => _localFileSystem.FileExists(f))
.HeadOrNone();