Browse Source

add fallback metadata for music videos (#134)

pull/135/head
Jason Dove 5 years ago committed by GitHub
parent
commit
8fea24a3a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      ErsatzTV.Core/Interfaces/Metadata/IFallbackMetadataProvider.cs
  2. 3
      ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs
  3. 8
      ErsatzTV.Core/Interfaces/Repositories/IMusicVideoRepository.cs
  4. 37
      ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs
  5. 22
      ErsatzTV.Core/Metadata/LocalMetadataProvider.cs
  6. 57
      ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs
  7. 107
      ErsatzTV.Infrastructure/Data/Repositories/MusicVideoRepository.cs

1
ErsatzTV.Core/Interfaces/Metadata/IFallbackMetadataProvider.cs

@ -8,6 +8,7 @@ namespace ErsatzTV.Core.Interfaces.Metadata
ShowMetadata GetFallbackMetadataForShow(string showFolder); ShowMetadata GetFallbackMetadataForShow(string showFolder);
Tuple<EpisodeMetadata, int> GetFallbackMetadata(Episode episode); Tuple<EpisodeMetadata, int> GetFallbackMetadata(Episode episode);
MovieMetadata GetFallbackMetadata(Movie movie); MovieMetadata GetFallbackMetadata(Movie movie);
MusicVideoMetadata GetFallbackMetadata(MusicVideo musicVideo);
string GetSortTitle(string title); string GetSortTitle(string title);
} }
} }

3
ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs

@ -1,19 +1,18 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using LanguageExt;
namespace ErsatzTV.Core.Interfaces.Metadata namespace ErsatzTV.Core.Interfaces.Metadata
{ {
public interface ILocalMetadataProvider public interface ILocalMetadataProvider
{ {
Task<ShowMetadata> GetMetadataForShow(string showFolder); Task<ShowMetadata> GetMetadataForShow(string showFolder);
Task<Option<MusicVideoMetadata>> GetMetadataForMusicVideo(string filePath);
Task<bool> RefreshSidecarMetadata(Movie movie, string nfoFileName); Task<bool> RefreshSidecarMetadata(Movie movie, string nfoFileName);
Task<bool> RefreshSidecarMetadata(Show televisionShow, string nfoFileName); Task<bool> RefreshSidecarMetadata(Show televisionShow, string nfoFileName);
Task<bool> RefreshSidecarMetadata(Episode episode, string nfoFileName); Task<bool> RefreshSidecarMetadata(Episode episode, string nfoFileName);
Task<bool> RefreshSidecarMetadata(MusicVideo musicVideo, string nfoFileName); Task<bool> RefreshSidecarMetadata(MusicVideo musicVideo, string nfoFileName);
Task<bool> RefreshFallbackMetadata(Movie movie); Task<bool> RefreshFallbackMetadata(Movie movie);
Task<bool> RefreshFallbackMetadata(Episode episode); Task<bool> RefreshFallbackMetadata(Episode episode);
Task<bool> RefreshFallbackMetadata(MusicVideo musicVideo);
Task<bool> RefreshFallbackMetadata(Show televisionShow, string showFolder); Task<bool> RefreshFallbackMetadata(Show televisionShow, string showFolder);
} }
} }

8
ErsatzTV.Core/Interfaces/Repositories/IMusicVideoRepository.cs

@ -8,13 +8,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
{ {
public interface IMusicVideoRepository public interface IMusicVideoRepository
{ {
Task<Option<MusicVideo>> GetByMetadata(LibraryPath libraryPath, MusicVideoMetadata metadata); Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> GetOrAdd(LibraryPath libraryPath, string path);
Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> Add(
LibraryPath libraryPath,
string filePath,
MusicVideoMetadata metadata);
Task<IEnumerable<string>> FindMusicVideoPaths(LibraryPath libraryPath); Task<IEnumerable<string>> FindMusicVideoPaths(LibraryPath libraryPath);
Task<List<int>> DeleteByPath(LibraryPath libraryPath, string path); Task<List<int>> DeleteByPath(LibraryPath libraryPath, string path);
Task<bool> AddGenre(MusicVideoMetadata metadata, Genre genre); Task<bool> AddGenre(MusicVideoMetadata metadata, Genre genre);

37
ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs

@ -36,6 +36,19 @@ namespace ErsatzTV.Core.Metadata
return fileName != null ? GetMovieMetadata(fileName, metadata) : metadata; return fileName != null ? GetMovieMetadata(fileName, metadata) : metadata;
} }
public MusicVideoMetadata GetFallbackMetadata(MusicVideo musicVideo)
{
string path = musicVideo.MediaVersions.Head().MediaFiles.Head().Path;
string fileName = Path.GetFileName(path);
var metadata = new MusicVideoMetadata
{
MetadataKind = MetadataKind.Fallback,
Title = fileName ?? path
};
return fileName != null ? GetMusicVideoMetadata(fileName, metadata) : metadata;
}
public string GetSortTitle(string title) public string GetSortTitle(string title)
{ {
if (string.IsNullOrWhiteSpace(title)) if (string.IsNullOrWhiteSpace(title))
@ -112,6 +125,30 @@ namespace ErsatzTV.Core.Metadata
return metadata; return metadata;
} }
private MusicVideoMetadata GetMusicVideoMetadata(string fileName, MusicVideoMetadata metadata)
{
try
{
const string PATTERN = @"^(.*?) - (.*?).\w+$";
Match match = Regex.Match(fileName, PATTERN);
if (match.Success)
{
metadata.Artist = match.Groups[1].Value;
metadata.Title = match.Groups[2].Value;
metadata.Genres = new List<Genre>();
metadata.Tags = new List<Tag>();
metadata.Studios = new List<Studio>();
metadata.DateUpdated = DateTime.UtcNow;
}
}
catch (Exception)
{
// ignored
}
return metadata;
}
private ShowMetadata GetTelevisionShowMetadata(string fileName, ShowMetadata metadata) private ShowMetadata GetTelevisionShowMetadata(string fileName, ShowMetadata metadata)
{ {
try try

22
ErsatzTV.Core/Metadata/LocalMetadataProvider.cs

@ -70,23 +70,6 @@ namespace ErsatzTV.Core.Metadata
}); });
} }
public async Task<Option<MusicVideoMetadata>> GetMetadataForMusicVideo(string filePath)
{
string nfoFileName = Path.ChangeExtension(filePath, "nfo");
Option<MusicVideoMetadata> maybeMetadata = None;
if (_localFileSystem.FileExists(nfoFileName))
{
maybeMetadata = await LoadMusicVideoMetadata(nfoFileName);
}
return maybeMetadata.Map(
metadata =>
{
metadata.SortTitle = _fallbackMetadataProvider.GetSortTitle(metadata.Title);
return metadata;
});
}
public Task<bool> RefreshSidecarMetadata(Movie movie, string nfoFileName) => public Task<bool> RefreshSidecarMetadata(Movie movie, string nfoFileName) =>
LoadMovieMetadata(movie, nfoFileName).Bind( LoadMovieMetadata(movie, nfoFileName).Bind(
maybeMetadata => maybeMetadata.Match( maybeMetadata => maybeMetadata.Match(
@ -109,7 +92,7 @@ namespace ErsatzTV.Core.Metadata
LoadMusicVideoMetadata(nfoFileName).Bind( LoadMusicVideoMetadata(nfoFileName).Bind(
maybeMetadata => maybeMetadata.Match( maybeMetadata => maybeMetadata.Match(
metadata => ApplyMetadataUpdate(musicVideo, metadata), metadata => ApplyMetadataUpdate(musicVideo, metadata),
() => Task.FromResult(false))); () => RefreshFallbackMetadata(musicVideo)));
public Task<bool> RefreshFallbackMetadata(Movie movie) => public Task<bool> RefreshFallbackMetadata(Movie movie) =>
ApplyMetadataUpdate(movie, _fallbackMetadataProvider.GetFallbackMetadata(movie)); ApplyMetadataUpdate(movie, _fallbackMetadataProvider.GetFallbackMetadata(movie));
@ -117,6 +100,9 @@ namespace ErsatzTV.Core.Metadata
public Task<bool> RefreshFallbackMetadata(Episode episode) => public Task<bool> RefreshFallbackMetadata(Episode episode) =>
ApplyMetadataUpdate(episode, _fallbackMetadataProvider.GetFallbackMetadata(episode)); ApplyMetadataUpdate(episode, _fallbackMetadataProvider.GetFallbackMetadata(episode));
public Task<bool> RefreshFallbackMetadata(MusicVideo musicVideo) =>
ApplyMetadataUpdate(musicVideo, _fallbackMetadataProvider.GetFallbackMetadata(musicVideo));
public Task<bool> RefreshFallbackMetadata(Show televisionShow, string showFolder) => public Task<bool> RefreshFallbackMetadata(Show televisionShow, string showFolder) =>
ApplyMetadataUpdate(televisionShow, _fallbackMetadataProvider.GetFallbackMetadataForShow(showFolder)); ApplyMetadataUpdate(televisionShow, _fallbackMetadataProvider.GetFallbackMetadataForShow(showFolder));

57
ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs

@ -67,10 +67,7 @@ namespace ErsatzTV.Core.Metadata
var foldersCompleted = 0; var foldersCompleted = 0;
var folderQueue = new Queue<string>(); var folderQueue = new Queue<string>();
foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path).OrderBy(identity)) folderQueue.Enqueue(libraryPath.Path);
{
folderQueue.Enqueue(folder);
}
while (folderQueue.Count > 0) while (folderQueue.Count > 0)
{ {
@ -83,22 +80,15 @@ namespace ErsatzTV.Core.Metadata
var allFiles = _localFileSystem.ListFiles(musicVideoFolder) var allFiles = _localFileSystem.ListFiles(musicVideoFolder)
.Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f))) .Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f)))
.Filter( .Filter(f => f.Contains(" - "))
f => !ExtraFiles.Any(
e => Path.GetFileNameWithoutExtension(f).EndsWith(e, StringComparison.OrdinalIgnoreCase)))
.ToList(); .ToList();
if (allFiles.Count == 0)
{
foreach (string subdirectory in _localFileSystem.ListSubdirectories(musicVideoFolder) foreach (string subdirectory in _localFileSystem.ListSubdirectories(musicVideoFolder)
.OrderBy(identity)) .OrderBy(identity))
{ {
folderQueue.Enqueue(subdirectory); folderQueue.Enqueue(subdirectory);
} }
continue;
}
if (_localFileSystem.GetLastWriteTime(musicVideoFolder) < lastScan) if (_localFileSystem.GetLastWriteTime(musicVideoFolder) < lastScan)
{ {
continue; continue;
@ -107,8 +97,8 @@ namespace ErsatzTV.Core.Metadata
foreach (string file in allFiles.OrderBy(identity)) foreach (string file in allFiles.OrderBy(identity))
{ {
// TODO: figure out how to rebuild playouts // TODO: figure out how to rebuild playouts
Either<BaseError, MediaItemScanResult<MusicVideo>> maybeMusicVideo = Either<BaseError, MediaItemScanResult<MusicVideo>> maybeMusicVideo = await _musicVideoRepository
await FindOrCreateMusicVideo(libraryPath, file) .GetOrAdd(libraryPath, file)
.BindT(musicVideo => UpdateStatistics(musicVideo, ffprobePath)) .BindT(musicVideo => UpdateStatistics(musicVideo, ffprobePath))
.BindT(UpdateMetadata) .BindT(UpdateMetadata)
.BindT(UpdateThumbnail); .BindT(UpdateThumbnail);
@ -146,34 +136,13 @@ namespace ErsatzTV.Core.Metadata
return Unit.Default; return Unit.Default;
} }
private async Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> FindOrCreateMusicVideo(
LibraryPath libraryPath,
string filePath)
{
Option<MusicVideoMetadata> maybeMetadata = await _localMetadataProvider.GetMetadataForMusicVideo(filePath);
return await maybeMetadata.Match(
async metadata =>
{
Option<MusicVideo> maybeMusicVideo =
await _musicVideoRepository.GetByMetadata(libraryPath, metadata);
return await maybeMusicVideo.Match(
musicVideo =>
Right<BaseError, MediaItemScanResult<MusicVideo>>(
new MediaItemScanResult<MusicVideo>(musicVideo))
.AsTask(),
async () => await _musicVideoRepository.Add(libraryPath, filePath, metadata));
},
() => Left<BaseError, MediaItemScanResult<MusicVideo>>(
BaseError.New("Unable to locate metadata for music video")).AsTask());
}
private async Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> UpdateMetadata( private async Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> UpdateMetadata(
MediaItemScanResult<MusicVideo> result) MediaItemScanResult<MusicVideo> result)
{ {
try try
{ {
MusicVideo musicVideo = result.Item; MusicVideo musicVideo = result.Item;
return await LocateNfoFile(musicVideo).Match<Task<Either<BaseError, MediaItemScanResult<MusicVideo>>>>( await LocateNfoFile(musicVideo).Match(
async nfoFile => async nfoFile =>
{ {
bool shouldUpdate = Optional(musicVideo.MusicVideoMetadata).Flatten().HeadOrNone().Match( bool shouldUpdate = Optional(musicVideo.MusicVideoMetadata).Flatten().HeadOrNone().Match(
@ -189,11 +158,21 @@ namespace ErsatzTV.Core.Metadata
result.IsUpdated = true; result.IsUpdated = true;
} }
} }
},
async () =>
{
if (!Optional(musicVideo.MusicVideoMetadata).Flatten().Any())
{
string path = musicVideo.MediaVersions.Head().MediaFiles.Head().Path;
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", path);
if (await _localMetadataProvider.RefreshFallbackMetadata(musicVideo))
{
result.IsUpdated = true;
}
}
});
return result; return result;
},
() => Left<BaseError, MediaItemScanResult<MusicVideo>>(
BaseError.New("Unable to locate metadata for music video")).AsTask());
} }
catch (Exception ex) catch (Exception ex)
{ {

107
ErsatzTV.Infrastructure/Data/Repositories/MusicVideoRepository.cs

@ -25,20 +25,12 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
_dbConnection = dbConnection; _dbConnection = dbConnection;
} }
public async Task<Option<MusicVideo>> GetByMetadata(LibraryPath libraryPath, MusicVideoMetadata metadata) public async Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> GetOrAdd(
LibraryPath libraryPath,
string path)
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Option<int> maybeId = await dbContext.MusicVideoMetadata Option<MusicVideo> maybeExisting = await dbContext.MusicVideos
.Where(s => s.Artist == metadata.Artist && s.Title == metadata.Title && s.Year == metadata.Year)
.Where(s => s.MusicVideo.LibraryPathId == libraryPath.Id)
.SingleOrDefaultAsync()
.Map(Optional)
.MapT(sm => sm.MusicVideoId);
return await maybeId.Match(
id =>
{
return dbContext.MusicVideos
.AsNoTracking() .AsNoTracking()
.Include(mv => mv.MusicVideoMetadata) .Include(mv => mv.MusicVideoMetadata)
.ThenInclude(mvm => mvm.Artwork) .ThenInclude(mvm => mvm.Artwork)
@ -54,54 +46,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ThenInclude(mv => mv.MediaFiles) .ThenInclude(mv => mv.MediaFiles)
.Include(mv => mv.MediaVersions) .Include(mv => mv.MediaVersions)
.ThenInclude(mv => mv.Streams) .ThenInclude(mv => mv.Streams)
.OrderBy(mv => mv.Id) .OrderBy(i => i.MediaVersions.First().MediaFiles.First().Path)
.SingleOrDefaultAsync(mv => mv.Id == id) .SingleOrDefaultAsync(i => i.MediaVersions.First().MediaFiles.First().Path == path);
.Map(Optional);
}, return await maybeExisting.Match(
() => Option<MusicVideo>.None.AsTask()); mediaItem =>
} Right<BaseError, MediaItemScanResult<MusicVideo>>(
new MediaItemScanResult<MusicVideo>(mediaItem) { IsAdded = false }).AsTask(),
public async Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> Add( async () => await AddMusicVideo(dbContext, libraryPath.Id, path));
LibraryPath libraryPath,
string filePath,
MusicVideoMetadata metadata)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
try
{
metadata.DateAdded = DateTime.UtcNow;
metadata.Genres ??= new List<Genre>();
metadata.Tags ??= new List<Tag>();
metadata.Studios ??= new List<Studio>();
var musicVideo = new MusicVideo
{
LibraryPathId = libraryPath.Id,
MusicVideoMetadata = new List<MusicVideoMetadata> { metadata },
MediaVersions = new List<MediaVersion>
{
new()
{
MediaFiles = new List<MediaFile>
{
new() { Path = filePath }
},
Streams = new List<MediaStream>()
}
}
};
await dbContext.MusicVideos.AddAsync(musicVideo);
await dbContext.SaveChangesAsync();
await dbContext.Entry(musicVideo).Reference(s => s.LibraryPath).LoadAsync();
await dbContext.Entry(musicVideo.LibraryPath).Reference(lp => lp.Library).LoadAsync();
return new MediaItemScanResult<MusicVideo>(musicVideo) { IsAdded = true };
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
}
} }
public Task<IEnumerable<string>> FindMusicVideoPaths(LibraryPath libraryPath) => public Task<IEnumerable<string>> FindMusicVideoPaths(LibraryPath libraryPath) =>
@ -179,5 +131,40 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.SingleOrDefaultAsync(m => m.Id == musicVideoId) .SingleOrDefaultAsync(m => m.Id == musicVideoId)
.Map(Optional); .Map(Optional);
} }
private static async Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> AddMusicVideo(
TvContext dbContext,
int libraryPathId,
string path)
{
try
{
var musicVideo = new MusicVideo
{
LibraryPathId = libraryPathId,
MediaVersions = new List<MediaVersion>
{
new()
{
MediaFiles = new List<MediaFile>
{
new() { Path = path }
},
Streams = new List<MediaStream>()
}
}
};
await dbContext.MusicVideos.AddAsync(musicVideo);
await dbContext.SaveChangesAsync();
await dbContext.Entry(musicVideo).Reference(m => m.LibraryPath).LoadAsync();
await dbContext.Entry(musicVideo.LibraryPath).Reference(lp => lp.Library).LoadAsync();
return new MediaItemScanResult<MusicVideo>(musicVideo) { IsAdded = true };
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
}
}
} }
} }

Loading…
Cancel
Save