Browse Source

load embedded song metadata (#495)

* load embedded song metadata

* index song artist and song album

* reset all song metadata
pull/499/head
Jason Dove 5 years ago committed by GitHub
parent
commit
8e08ff059f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      ErsatzTV.Application/MediaCards/Mapper.cs
  2. 17
      ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs
  3. 2
      ErsatzTV.Core.Tests/Metadata/LocalStatisticsProviderTests.cs
  4. 3
      ErsatzTV.Core/Domain/Metadata/MetadataKind.cs
  5. 4
      ErsatzTV.Core/Domain/Metadata/SongMetadata.cs
  6. 1
      ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs
  7. 5
      ErsatzTV.Core/Interfaces/Metadata/ILocalStatisticsProvider.cs
  8. 1
      ErsatzTV.Core/Interfaces/Metadata/ISongFolderScanner.cs
  9. 1
      ErsatzTV.Core/Interfaces/Repositories/ISongRepository.cs
  10. 3
      ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs
  11. 113
      ErsatzTV.Core/Metadata/LocalMetadataProvider.cs
  12. 66
      ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs
  13. 12
      ErsatzTV.Core/Metadata/MetadataFormatTag.cs
  14. 44
      ErsatzTV.Core/Metadata/SongFolderScanner.cs
  15. 8
      ErsatzTV.Infrastructure/Data/Configurations/Metadata/SongMetadataConfiguration.cs
  16. 32
      ErsatzTV.Infrastructure/Data/Repositories/SongRepository.cs
  17. 3843
      ErsatzTV.Infrastructure/Migrations/20211124122445_Add_SongAlbum.Designer.cs
  18. 101
      ErsatzTV.Infrastructure/Migrations/20211124122445_Add_SongAlbum.cs
  19. 3843
      ErsatzTV.Infrastructure/Migrations/20211124124420_Reset_SongMetadata.Designer.cs
  20. 30
      ErsatzTV.Infrastructure/Migrations/20211124124420_Reset_SongMetadata.cs
  21. 18
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  22. 11
      ErsatzTV.Infrastructure/Search/SearchIndex.cs

9
ErsatzTV.Application/MediaCards/Mapper.cs

@ -110,12 +110,15 @@ namespace ErsatzTV.Application.MediaCards @@ -110,12 +110,15 @@ namespace ErsatzTV.Application.MediaCards
otherVideoMetadata.OriginalTitle,
otherVideoMetadata.SortTitle);
internal static SongCardViewModel ProjectToViewModel(SongMetadata songMetadata) =>
new(
internal static SongCardViewModel ProjectToViewModel(SongMetadata songMetadata)
{
string album = string.IsNullOrWhiteSpace(songMetadata.Album) ? "" : $" - {songMetadata.Album}";
return new SongCardViewModel(
songMetadata.SongId,
songMetadata.Title,
songMetadata.OriginalTitle,
songMetadata.Artist + album,
songMetadata.SortTitle);
}
internal static ArtistCardViewModel ProjectToViewModel(ArtistMetadata artistMetadata) =>
new(

17
ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs

@ -70,7 +70,8 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -70,7 +70,8 @@ namespace ErsatzTV.Application.MediaSources.Commands
private async Task<Unit> PerformScan(RequestParameters parameters)
{
(LocalLibrary localLibrary, string ffprobePath, bool forceScan, int libraryRefreshInterval) = parameters;
(LocalLibrary localLibrary, string ffprobePath, string ffmpegPath, bool forceScan,
int libraryRefreshInterval) = parameters;
var sw = new Stopwatch();
sw.Start();
@ -124,6 +125,7 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -124,6 +125,7 @@ namespace ErsatzTV.Application.MediaSources.Commands
await _songFolderScanner.ScanFolder(
libraryPath,
ffprobePath,
ffmpegPath,
progressMin,
progressMax);
break;
@ -159,11 +161,12 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -159,11 +161,12 @@ namespace ErsatzTV.Application.MediaSources.Commands
}
private async Task<Validation<BaseError, RequestParameters>> Validate(IScanLocalLibrary request) =>
(await LocalLibraryMustExist(request), await ValidateFFprobePath(), await ValidateLibraryRefreshInterval())
(await LocalLibraryMustExist(request), await ValidateFFprobePath(), await ValidateFFmpegPath(), await ValidateLibraryRefreshInterval())
.Apply(
(library, ffprobePath, libraryRefreshInterval) => new RequestParameters(
(library, ffprobePath, ffmpegPath, libraryRefreshInterval) => new RequestParameters(
library,
ffprobePath,
ffmpegPath,
request.ForceScan,
libraryRefreshInterval));
@ -180,6 +183,13 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -180,6 +183,13 @@ namespace ErsatzTV.Application.MediaSources.Commands
ffprobePath =>
ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system"));
private Task<Validation<BaseError, string>> ValidateFFmpegPath() =>
_configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPath)
.FilterT(File.Exists)
.Map(
ffmpegPath =>
ffmpegPath.ToValidation<BaseError>("FFmpeg path does not exist on the file system"));
private Task<Validation<BaseError, int>> ValidateLibraryRefreshInterval() =>
_configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
.FilterT(lri => lri > 0)
@ -188,6 +198,7 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -188,6 +198,7 @@ namespace ErsatzTV.Application.MediaSources.Commands
private record RequestParameters(
LocalLibrary LocalLibrary,
string FFprobePath,
string FFmpegPath,
bool ForceScan,
int LibraryRefreshInterval);
}

2
ErsatzTV.Core.Tests/Metadata/LocalStatisticsProviderTests.cs

@ -25,7 +25,7 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -25,7 +25,7 @@ namespace ErsatzTV.Core.Tests.Metadata
new Mock<ILogger<LocalStatisticsProvider>>().Object);
var input = new LocalStatisticsProvider.FFprobe(
new LocalStatisticsProvider.FFprobeFormat("123.45"),
new LocalStatisticsProvider.FFprobeFormat("123.45", null),
new List<LocalStatisticsProvider.FFprobeStream>(),
new List<LocalStatisticsProvider.FFprobeChapter>());

3
ErsatzTV.Core/Domain/Metadata/MetadataKind.cs

@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
{
Fallback = 0,
Sidecar = 1,
External = 2
External = 2,
Embedded = 3
}
}

4
ErsatzTV.Core/Domain/Metadata/SongMetadata.cs

@ -2,6 +2,10 @@ @@ -2,6 +2,10 @@
{
public class SongMetadata : Metadata
{
public string Album { get; set; }
public string Artist { get; set; }
public string Date { get; set; }
public string Track { get; set; }
public int SongId { get; set; }
public Song Song { get; set; }
}

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

@ -12,6 +12,7 @@ namespace ErsatzTV.Core.Interfaces.Metadata @@ -12,6 +12,7 @@ namespace ErsatzTV.Core.Interfaces.Metadata
Task<bool> RefreshSidecarMetadata(Episode episode, string nfoFileName);
Task<bool> RefreshSidecarMetadata(Artist artist, string nfoFileName);
Task<bool> RefreshSidecarMetadata(MusicVideo musicVideo, string nfoFileName);
Task<bool> RefreshTagMetadata(Song song, string ffprobePath);
Task<bool> RefreshFallbackMetadata(Movie movie);
Task<bool> RefreshFallbackMetadata(Episode episode);
Task<bool> RefreshFallbackMetadata(Artist artist, string artistFolder);

5
ErsatzTV.Core/Interfaces/Metadata/ILocalStatisticsProvider.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using LanguageExt;
@ -8,5 +9,7 @@ namespace ErsatzTV.Core.Interfaces.Metadata @@ -8,5 +9,7 @@ namespace ErsatzTV.Core.Interfaces.Metadata
{
Task<Either<BaseError, bool>> RefreshStatistics(string ffprobePath, MediaItem mediaItem);
Task<Either<BaseError, bool>> RefreshStatistics(string ffprobePath, MediaItem mediaItem, string mediaItemPath);
Task<Either<BaseError, Dictionary<string, string>>> GetFormatTags(string ffprobePath, MediaItem mediaItem);
}
}

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

@ -9,6 +9,7 @@ namespace ErsatzTV.Core.Interfaces.Metadata @@ -9,6 +9,7 @@ namespace ErsatzTV.Core.Interfaces.Metadata
Task<Either<BaseError, Unit>> ScanFolder(
LibraryPath libraryPath,
string ffprobePath,
string ffmpegPath,
decimal progressMin,
decimal progressMax);
}

1
ErsatzTV.Core/Interfaces/Repositories/ISongRepository.cs

@ -11,6 +11,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -11,6 +11,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
Task<Either<BaseError, MediaItemScanResult<Song>>> GetOrAdd(LibraryPath libraryPath, string path);
Task<IEnumerable<string>> FindSongPaths(LibraryPath libraryPath);
Task<List<int>> DeleteByPath(LibraryPath libraryPath, string path);
Task<bool> AddGenre(SongMetadata metadata, Genre genre);
Task<bool> AddTag(SongMetadata metadata, Tag tag);
Task<List<SongMetadata>> GetSongsForCards(List<int> ids);
}

3
ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs

@ -115,7 +115,8 @@ namespace ErsatzTV.Core.Metadata @@ -115,7 +115,8 @@ namespace ErsatzTV.Core.Metadata
Song = song
};
return GetSongMetadata(path, metadata); }
return GetSongMetadata(path, metadata);
}
public string GetSortTitle(string title)
{

113
ErsatzTV.Core/Metadata/LocalMetadataProvider.cs

@ -5,6 +5,7 @@ using System.Linq; @@ -5,6 +5,7 @@ using System.Linq;
using System.Threading.Tasks;
using System.Xml.Serialization;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Metadata.Nfo;
using ErsatzTV.Core.Interfaces.Repositories;
@ -23,6 +24,7 @@ namespace ErsatzTV.Core.Metadata @@ -23,6 +24,7 @@ namespace ErsatzTV.Core.Metadata
private static readonly XmlSerializer MusicVideoSerializer = new(typeof(MusicVideoNfo));
private readonly IArtistRepository _artistRepository;
private readonly IEpisodeNfoReader _episodeNfoReader;
private readonly ILocalStatisticsProvider _localStatisticsProvider;
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<LocalMetadataProvider> _logger;
@ -45,6 +47,7 @@ namespace ErsatzTV.Core.Metadata @@ -45,6 +47,7 @@ namespace ErsatzTV.Core.Metadata
IFallbackMetadataProvider fallbackMetadataProvider,
ILocalFileSystem localFileSystem,
IEpisodeNfoReader episodeNfoReader,
ILocalStatisticsProvider localStatisticsProvider,
ILogger<LocalMetadataProvider> logger)
{
_metadataRepository = metadataRepository;
@ -57,6 +60,7 @@ namespace ErsatzTV.Core.Metadata @@ -57,6 +60,7 @@ namespace ErsatzTV.Core.Metadata
_fallbackMetadataProvider = fallbackMetadataProvider;
_localFileSystem = localFileSystem;
_episodeNfoReader = episodeNfoReader;
_localStatisticsProvider = localStatisticsProvider;
_logger = logger;
}
@ -133,6 +137,12 @@ namespace ErsatzTV.Core.Metadata @@ -133,6 +137,12 @@ namespace ErsatzTV.Core.Metadata
metadata => ApplyMetadataUpdate(musicVideo, metadata),
() => RefreshFallbackMetadata(musicVideo)));
public Task<bool> RefreshTagMetadata(Song song, string ffprobePath) =>
LoadSongMetadata(song, ffprobePath).Bind(
maybeMetadata => maybeMetadata.Match(
metadata => ApplyMetadataUpdate(song, metadata),
() => RefreshFallbackMetadata(song)));
public Task<bool> RefreshFallbackMetadata(Movie movie) =>
ApplyMetadataUpdate(movie, _fallbackMetadataProvider.GetFallbackMetadata(movie));
@ -148,9 +158,9 @@ namespace ErsatzTV.Core.Metadata @@ -148,9 +158,9 @@ namespace ErsatzTV.Core.Metadata
() => Task.FromResult(false));
public Task<bool> RefreshFallbackMetadata(Song song) =>
_fallbackMetadataProvider.GetFallbackMetadata(song).Match(
metadata => ApplyMetadataUpdate(song, metadata),
() => Task.FromResult(false));
_fallbackMetadataProvider.GetFallbackMetadata(song).Match(
metadata => ApplyMetadataUpdate(song, metadata),
() => Task.FromResult(false));
public Task<bool> RefreshFallbackMetadata(MusicVideo musicVideo) =>
_fallbackMetadataProvider.GetFallbackMetadata(musicVideo).Match(
@ -190,6 +200,95 @@ namespace ErsatzTV.Core.Metadata @@ -190,6 +200,95 @@ namespace ErsatzTV.Core.Metadata
}
}
private async Task<Option<SongMetadata>> LoadSongMetadata(Song song, string ffprobePath)
{
string path = song.GetHeadVersion().MediaFiles.Head().Path;
try
{
Either<BaseError, Dictionary<string, string>> maybeTags =
await _localStatisticsProvider.GetFormatTags(ffprobePath, song);
return maybeTags.Match(
tags =>
{
Option<SongMetadata> maybeFallbackMetadata =
_fallbackMetadataProvider.GetFallbackMetadata(song);
var result = new SongMetadata
{
MetadataKind = MetadataKind.Embedded,
DateAdded = DateTime.UtcNow,
DateUpdated = File.GetLastWriteTimeUtc(path),
Artwork = new List<Artwork>(),
Actors = new List<Actor>(),
Genres = new List<Genre>(),
Studios = new List<Studio>(),
Tags = new List<Tag>()
};
// TODO: check for cover artwork, and use for watermark if not embedded
// maybe add album as entity rather than string?
if (tags.TryGetValue(MetadataFormatTag.Album, out string album))
{
result.Album = album;
}
if (tags.TryGetValue(MetadataFormatTag.Artist, out string artist))
{
result.Artist = artist;
}
if (tags.TryGetValue(MetadataFormatTag.Date, out string date))
{
result.Date = date;
}
if (tags.TryGetValue(MetadataFormatTag.Genre, out string genre))
{
// TODO: split genres? or is this only ever one?
result.Genres.Add(new Genre { Name = genre });
}
if (tags.TryGetValue(MetadataFormatTag.Title, out string title))
{
result.Title = title;
result.OriginalTitle = title;
}
if (tags.TryGetValue(MetadataFormatTag.Track, out string track))
{
result.Track = track;
}
foreach (SongMetadata fallbackMetadata in maybeFallbackMetadata)
{
if (string.IsNullOrWhiteSpace(result.Title))
{
result.Title = fallbackMetadata.Title;
result.OriginalTitle = fallbackMetadata.OriginalTitle;
}
// preserve folder tagging - maybe someone uses this
foreach (Tag tag in fallbackMetadata.Tags)
{
result.Tags.Add(tag);
}
}
return result;
},
_ => Option<SongMetadata>.None);
}
catch (Exception ex)
{
_logger.LogInformation(ex, "Failed to read embedded song metadata from {Path}", path);
return None;
}
}
private async Task<bool> ApplyMetadataUpdate(Episode episode, List<EpisodeMetadata> episodeMetadata)
{
var updated = false;
@ -675,6 +774,10 @@ namespace ErsatzTV.Core.Metadata @@ -675,6 +774,10 @@ namespace ErsatzTV.Core.Metadata
async existing =>
{
existing.Title = metadata.Title;
existing.Artist = metadata.Artist;
existing.Album = metadata.Album;
existing.Date = metadata.Date;
existing.Track = metadata.Track;
if (existing.DateAdded == SystemTime.MinValueUtc)
{
@ -691,7 +794,7 @@ namespace ErsatzTV.Core.Metadata @@ -691,7 +794,7 @@ namespace ErsatzTV.Core.Metadata
bool updated = await UpdateMetadataCollections(
existing,
metadata,
(_, _) => Task.FromResult(false),
_songRepository.AddGenre,
_songRepository.AddTag,
(_, _) => Task.FromResult(false),
(_, _) => Task.FromResult(false));
@ -978,7 +1081,7 @@ namespace ErsatzTV.Core.Metadata @@ -978,7 +1081,7 @@ namespace ErsatzTV.Core.Metadata
}
}
if (existing is not MusicVideoMetadata)
if (existing is not MusicVideoMetadata and not SongMetadata)
{
foreach (Actor actor in existing.Actors
.Filter(a => incoming.Actors.All(a2 => a2.Name != a.Name))

66
ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs

@ -69,6 +69,60 @@ namespace ErsatzTV.Core.Metadata @@ -69,6 +69,60 @@ namespace ErsatzTV.Core.Metadata
}
}
public async Task<Either<BaseError, Dictionary<string, string>>> GetFormatTags(
string ffprobePath,
MediaItem mediaItem)
{
try
{
string mediaItemPath = mediaItem.GetHeadVersion().MediaFiles.Head().Path;
Either<BaseError, FFprobe> maybeProbe = await GetProbeOutput(ffprobePath, mediaItemPath);
return maybeProbe.Match(
ffprobe =>
{
var result = new Dictionary<string, string>();
if (!string.IsNullOrWhiteSpace(ffprobe?.format?.tags?.album))
{
result.Add(MetadataFormatTag.Album, ffprobe.format.tags.album);
}
if (!string.IsNullOrWhiteSpace(ffprobe?.format?.tags?.artist))
{
result.Add(MetadataFormatTag.Artist, ffprobe.format.tags.artist);
}
if (!string.IsNullOrWhiteSpace(ffprobe?.format?.tags?.date))
{
result.Add(MetadataFormatTag.Date, ffprobe.format.tags.date);
}
if (!string.IsNullOrWhiteSpace(ffprobe?.format?.tags?.genre))
{
result.Add(MetadataFormatTag.Genre, ffprobe.format.tags.genre);
}
if (!string.IsNullOrWhiteSpace(ffprobe?.format?.tags?.title))
{
result.Add(MetadataFormatTag.Title, ffprobe.format.tags.title);
}
if (!string.IsNullOrWhiteSpace(ffprobe?.format?.tags?.track))
{
result.Add(MetadataFormatTag.Track, ffprobe.format.tags.track);
}
return Right<BaseError, Dictionary<string, string>>(result);
},
Left<BaseError, Dictionary<string, string>>);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to get format tags for media item {Id}", mediaItem.Id);
return BaseError.New(ex.Message);
}
}
private async Task<bool> ApplyVersionUpdate(MediaItem mediaItem, MediaVersion version, string filePath)
{
MediaVersion mediaItemVersion = mediaItem.GetHeadVersion();
@ -277,7 +331,7 @@ namespace ErsatzTV.Core.Metadata @@ -277,7 +331,7 @@ namespace ErsatzTV.Core.Metadata
last.EndTime = version.Duration;
}
}
return version;
},
_ => new MediaVersion
@ -299,12 +353,20 @@ namespace ErsatzTV.Core.Metadata @@ -299,12 +353,20 @@ namespace ErsatzTV.Core.Metadata
// ReSharper disable InconsistentNaming
public record FFprobe(FFprobeFormat format, List<FFprobeStream> streams, List<FFprobeChapter> chapters);
public record FFprobeFormat(string duration);
public record FFprobeFormat(string duration, FFprobeFormatTags tags);
public record FFprobeDisposition(int @default, int forced, int attached_pic);
public record FFprobeTags(string language, string title);
public record FFprobeFormatTags(
string title,
string artist,
string album,
string track,
string genre,
string date);
public record FFprobeStream(
int index,
string codec_name,

12
ErsatzTV.Core/Metadata/MetadataFormatTag.cs

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
namespace ErsatzTV.Core.Metadata
{
public static class MetadataFormatTag
{
public static readonly string Album = "album";
public static readonly string Artist = "artist";
public static readonly string Date = "date";
public static readonly string Genre = "genre";
public static readonly string Title = "title";
public static readonly string Track = "track";
}
}

44
ErsatzTV.Core/Metadata/SongFolderScanner.cs

@ -5,6 +5,7 @@ using System.Linq; @@ -5,6 +5,7 @@ using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
@ -59,6 +60,7 @@ namespace ErsatzTV.Core.Metadata @@ -59,6 +60,7 @@ namespace ErsatzTV.Core.Metadata
public async Task<Either<BaseError, Unit>> ScanFolder(
LibraryPath libraryPath,
string ffprobePath,
string ffmpegPath,
decimal progressMin,
decimal progressMax)
{
@ -122,7 +124,8 @@ namespace ErsatzTV.Core.Metadata @@ -122,7 +124,8 @@ namespace ErsatzTV.Core.Metadata
Either<BaseError, MediaItemScanResult<Song>> maybeSong = await _songRepository
.GetOrAdd(libraryPath, file)
.BindT(video => UpdateStatistics(video, ffprobePath))
.BindT(UpdateMetadata);
.BindT(video => UpdateMetadata(video, ffprobePath));
// .BindT(video => UpdateThumbnail(video, ffprobePath, ffmpegPath));
await maybeSong.Match(
async result =>
@ -167,18 +170,24 @@ namespace ErsatzTV.Core.Metadata @@ -167,18 +170,24 @@ namespace ErsatzTV.Core.Metadata
}
private async Task<Either<BaseError, MediaItemScanResult<Song>>> UpdateMetadata(
MediaItemScanResult<Song> result)
MediaItemScanResult<Song> result, string ffprobePath)
{
try
{
Song song = result.Item;
if (!Optional(song.SongMetadata).Flatten().Any())
string path = song.GetHeadVersion().MediaFiles.Head().Path;
bool shouldUpdate = Optional(song.SongMetadata).Flatten().HeadOrNone().Match(
m => m.MetadataKind == MetadataKind.Fallback ||
m.DateUpdated != _localFileSystem.GetLastWriteTime(path),
true);
if (shouldUpdate)
{
song.SongMetadata ??= new List<SongMetadata>();
string path = song.MediaVersions.Head().MediaFiles.Head().Path;
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", path);
if (await _localMetadataProvider.RefreshFallbackMetadata(song))
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Metadata", path);
if (await _localMetadataProvider.RefreshTagMetadata(song, ffprobePath))
{
result.IsUpdated = true;
}
@ -191,5 +200,28 @@ namespace ErsatzTV.Core.Metadata @@ -191,5 +200,28 @@ namespace ErsatzTV.Core.Metadata
return BaseError.New(ex.ToString());
}
}
// private async Task<Either<BaseError, MediaItemScanResult<Song>>> UpdateThumbnail(
// MediaItemScanResult<Song> result,
// string ffprobePath,
// string ffmpegPath)
// {
// try
// {
// Song song = result.Item;
// await LocateThumbnail(song).IfSomeAsync(
// async thumbnailFile =>
// {
// SongMetadata metadata = song.SongMetadata.Head();
// await RefreshArtwork(thumbnailFile, metadata, ArtworkKind.Thumbnail);
// });
//
// return result;
// }
// catch (Exception ex)
// {
// return BaseError.New(ex.ToString());
// }
// }
}
}

8
ErsatzTV.Infrastructure/Data/Configurations/Metadata/SongMetadataConfiguration.cs

@ -14,9 +14,17 @@ namespace ErsatzTV.Infrastructure.Data.Configurations @@ -14,9 +14,17 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(mm => mm.Genres)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(mm => mm.Tags)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(mm => mm.Studios)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
}
}
}

32
ErsatzTV.Infrastructure/Data/Repositories/SongRepository.cs

@ -32,19 +32,24 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -32,19 +32,24 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
Option<Song> maybeExisting = await dbContext.Songs
.AsNoTracking()
.Include(ov => ov.SongMetadata)
.ThenInclude(ovm => ovm.Artwork)
.Include(ov => ov.SongMetadata)
.ThenInclude(ovm => ovm.Tags)
.Include(ov => ov.LibraryPath)
.Include(s => s.SongMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(s => s.SongMetadata)
.ThenInclude(sm => sm.Genres)
.Include(s => s.SongMetadata)
.ThenInclude(sm => sm.Studios)
.Include(s => s.SongMetadata)
.ThenInclude(sm => sm.Tags)
.Include(s => s.SongMetadata)
.Include(s => s.LibraryPath)
.ThenInclude(lp => lp.Library)
.Include(ov => ov.MediaVersions)
.ThenInclude(ov => ov.MediaFiles)
.Include(ov => ov.MediaVersions)
.ThenInclude(ov => ov.Streams)
.Include(ov => ov.TraktListItems)
.Include(s => s.MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(s => s.MediaVersions)
.ThenInclude(mv => mv.Streams)
.Include(s => s.TraktListItems)
.ThenInclude(tli => tli.TraktList)
.OrderBy(i => i.MediaVersions.First().MediaFiles.First().Path)
.OrderBy(s => s.MediaVersions.First().MediaFiles.First().Path)
.SingleOrDefaultAsync(i => i.MediaVersions.First().MediaFiles.First().Path == path);
return await maybeExisting.Match(
@ -90,6 +95,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -90,6 +95,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
return ids;
}
public Task<bool> AddGenre(SongMetadata metadata, Genre genre) =>
_dbConnection.ExecuteAsync(
"INSERT INTO Genre (Name, SongMetadataId) VALUES (@Name, @MetadataId)",
new { genre.Name, MetadataId = metadata.Id }).Map(result => result > 0);
public Task<bool> AddTag(SongMetadata metadata, Tag tag) =>
_dbConnection.ExecuteAsync(
"INSERT INTO Tag (Name, SongMetadataId) VALUES (@Name, @MetadataId)",

3843
ErsatzTV.Infrastructure/Migrations/20211124122445_Add_SongAlbum.Designer.cs generated

File diff suppressed because it is too large Load Diff

101
ErsatzTV.Infrastructure/Migrations/20211124122445_Add_SongAlbum.cs

@ -0,0 +1,101 @@ @@ -0,0 +1,101 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_SongAlbum : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Genre_SongMetadata_SongMetadataId",
table: "Genre");
migrationBuilder.DropForeignKey(
name: "FK_Studio_SongMetadata_SongMetadataId",
table: "Studio");
migrationBuilder.AddColumn<string>(
name: "Album",
table: "SongMetadata",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Artist",
table: "SongMetadata",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Date",
table: "SongMetadata",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Track",
table: "SongMetadata",
type: "TEXT",
nullable: true);
migrationBuilder.AddForeignKey(
name: "FK_Genre_SongMetadata_SongMetadataId",
table: "Genre",
column: "SongMetadataId",
principalTable: "SongMetadata",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Studio_SongMetadata_SongMetadataId",
table: "Studio",
column: "SongMetadataId",
principalTable: "SongMetadata",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Genre_SongMetadata_SongMetadataId",
table: "Genre");
migrationBuilder.DropForeignKey(
name: "FK_Studio_SongMetadata_SongMetadataId",
table: "Studio");
migrationBuilder.DropColumn(
name: "Album",
table: "SongMetadata");
migrationBuilder.DropColumn(
name: "Artist",
table: "SongMetadata");
migrationBuilder.DropColumn(
name: "Date",
table: "SongMetadata");
migrationBuilder.DropColumn(
name: "Track",
table: "SongMetadata");
migrationBuilder.AddForeignKey(
name: "FK_Genre_SongMetadata_SongMetadataId",
table: "Genre",
column: "SongMetadataId",
principalTable: "SongMetadata",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Studio_SongMetadata_SongMetadataId",
table: "Studio",
column: "SongMetadataId",
principalTable: "SongMetadata",
principalColumn: "Id");
}
}
}

3843
ErsatzTV.Infrastructure/Migrations/20211124124420_Reset_SongMetadata.Designer.cs generated

File diff suppressed because it is too large Load Diff

30
ErsatzTV.Infrastructure/Migrations/20211124124420_Reset_SongMetadata.cs

@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Reset_SongMetadata : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
@"UPDATE LibraryPath SET LastScan = '0001-01-01 00:00:00' WHERE Id IN
(SELECT LP.Id FROM LibraryPath LP INNER JOIN Library L on L.Id = LP.LibraryId WHERE MediaKind = 5)");
migrationBuilder.Sql(
@"UPDATE Library SET LastScan = '0001-01-01 00:00:00' WHERE MediaKind = 5");
migrationBuilder.Sql(
@"UPDATE SongMetadata SET DateUpdated = '0001-01-01 00:00:00'");
migrationBuilder.Sql(
@"UPDATE LibraryFolder SET Etag = NULL WHERE Id IN
(SELECT LF.Id FROM LibraryFolder LF INNER JOIN LibraryPath LP on LF.LibraryPathId = LP.Id INNER JOIN Library L on LP.LibraryId = L.Id WHERE MediaKind = 5)");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

18
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -1637,6 +1637,15 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1637,6 +1637,15 @@ namespace ErsatzTV.Infrastructure.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Album")
.HasColumnType("TEXT");
b.Property<string>("Artist")
.HasColumnType("TEXT");
b.Property<string>("Date")
.HasColumnType("TEXT");
b.Property<DateTime>("DateAdded")
.HasColumnType("TEXT");
@ -1661,6 +1670,9 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1661,6 +1670,9 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<string>("Track")
.HasColumnType("TEXT");
b.Property<int?>("Year")
.HasColumnType("INTEGER");
@ -2553,7 +2565,8 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -2553,7 +2565,8 @@ namespace ErsatzTV.Infrastructure.Migrations
b.HasOne("ErsatzTV.Core.Domain.SongMetadata", null)
.WithMany("Genres")
.HasForeignKey("SongMetadataId");
.HasForeignKey("SongMetadataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinConnection", b =>
@ -3116,7 +3129,8 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -3116,7 +3129,8 @@ namespace ErsatzTV.Infrastructure.Migrations
b.HasOne("ErsatzTV.Core.Domain.SongMetadata", null)
.WithMany("Studios")
.HasForeignKey("SongMetadataId");
.HasForeignKey("SongMetadataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Style", b =>

11
ErsatzTV.Infrastructure/Search/SearchIndex.cs

@ -54,6 +54,7 @@ namespace ErsatzTV.Infrastructure.Search @@ -54,6 +54,7 @@ namespace ErsatzTV.Infrastructure.Search
private const string TraktListField = "trakt_list";
private const string AlbumField = "album";
private const string MinutesField = "minutes";
private const string ArtistField = "artist";
public const string MovieType = "movie";
public const string ShowType = "show";
@ -851,6 +852,16 @@ namespace ErsatzTV.Infrastructure.Search @@ -851,6 +852,16 @@ namespace ErsatzTV.Infrastructure.Search
doc.Add(new Int32Field(MinutesField, (int)Math.Ceiling(version.Duration.TotalMinutes), Field.Store.NO));
}
if (!string.IsNullOrWhiteSpace(metadata.Album))
{
doc.Add(new TextField(AlbumField, metadata.Album, Field.Store.NO));
}
if (!string.IsNullOrWhiteSpace(metadata.Artist))
{
doc.Add(new TextField(ArtistField, metadata.Artist, Field.Store.NO));
}
foreach (Tag tag in metadata.Tags)
{
doc.Add(new TextField(TagField, tag.Name, Field.Store.NO));

Loading…
Cancel
Save