merge latest develop (#230)

* sync guids/provider ids (#227)

* sync guids from plex

* cleanup

* sync local guids

* sync jellyfin and emby guids

* add episodes to search index (#228)

* sync episode directors and writers

* display episode writers and directors

* remove missing episodes from search index

* show episodes in search results

* fix emby and jellyfin episode updates

* fix updating plex episodes

* don't delete channel logos on startup

* add episodes page; fix adding episodes to collection

* cleanup

* multi-part episode grouping fixes (#229)
This commit is contained in:
Jason Dove
2021-05-30 06:08:33 -05:00
committed by GitHub
parent bbd8bc6c7e
commit e506dd38a8
78 changed files with 8328 additions and 484 deletions
@@ -17,6 +17,18 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
builder.HasMany(em => em.Actors)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(mm => mm.Directors)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(mm => mm.Writers)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(mm => mm.Guids)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
}
}
}
@@ -0,0 +1,11 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class MetadataGuidConfiguration : IEntityTypeConfiguration<MetadataGuid>
{
public void Configure(EntityTypeBuilder<MetadataGuid> builder) => builder.ToTable("MetadataGuid");
}
}
@@ -37,6 +37,10 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
builder.HasMany(mm => mm.Writers)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(mm => mm.Guids)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
}
}
}
@@ -13,6 +13,10 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
builder.HasMany(em => em.Artwork)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(mm => mm.Guids)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
}
}
}
@@ -29,6 +29,10 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
builder.HasMany(sm => sm.Actors)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(mm => mm.Guids)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
}
}
}
@@ -21,6 +21,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
WHERE A.ArtistMetadataId IS NULL AND A.EpisodeMetadataId IS NULL
AND A.SeasonMetadataId IS NULL AND A.ShowMetadataId IS NULL
AND A.MovieMetadataId IS NULL AND A.MusicVideoMetadataId IS NULL
AND A.ChannelId IS NULL
AND NOT EXISTS (SELECT * FROM Actor WHERE Actor.ArtworkId = A.Id)")
.Map(result => result.ToList());
@@ -88,6 +88,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ThenInclude(mm => mm.Actors)
.Include(m => m.ShowMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(m => m.ShowMetadata)
.ThenInclude(mm => mm.Guids)
.Filter(m => m.ItemId == show.ItemId)
.OrderBy(m => m.ItemId)
.SingleOrDefaultAsync();
@@ -177,6 +179,21 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
metadata.Actors.Add(actor);
}
// guids
foreach (MetadataGuid guid in metadata.Guids
.Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
.ToList())
{
metadata.Guids.Remove(guid);
}
foreach (MetadataGuid guid in incomingMetadata.Guids
.Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid))
.ToList())
{
metadata.Guids.Add(guid);
}
metadata.ReleaseDate = incomingMetadata.ReleaseDate;
// poster
@@ -246,6 +263,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.Include(m => m.LibraryPath)
.Include(m => m.SeasonMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(m => m.SeasonMetadata)
.ThenInclude(mm => mm.Guids)
.Filter(m => m.ItemId == season.ItemId)
.OrderBy(m => m.ItemId)
.SingleOrDefaultAsync();
@@ -322,6 +341,21 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
fanArt.DateUpdated = incomingFanArt.DateUpdated;
}
// guids
foreach (MetadataGuid guid in metadata.Guids
.Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
.ToList())
{
metadata.Guids.Remove(guid);
}
foreach (MetadataGuid guid in incomingMetadata.Guids
.Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid))
.ToList())
{
metadata.Guids.Add(guid);
}
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
foreach (Artwork artworkToRemove in metadata.Artwork.Filter(a => !paths.Contains(a.Path)))
{
@@ -345,20 +379,37 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
await dbContext.Entry(episode).Reference(m => m.LibraryPath).LoadAsync();
await dbContext.Entry(episode.LibraryPath).Reference(lp => lp.Library).LoadAsync();
await dbContext.Entry(episode).Reference(e => e.Season).LoadAsync();
return true;
}
public async Task<Unit> Update(EmbyEpisode episode)
public async Task<Option<EmbyEpisode>> Update(EmbyEpisode episode)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Option<EmbyEpisode> maybeExisting = await dbContext.EmbyEpisodes
.Include(m => m.LibraryPath)
.ThenInclude(lp => lp.Library)
.Include(m => m.MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(m => m.MediaVersions)
.ThenInclude(mv => mv.Streams)
.Include(m => m.EpisodeMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(m => m.EpisodeMetadata)
.ThenInclude(mm => mm.Guids)
.Include(m => m.EpisodeMetadata)
.ThenInclude(mm => mm.Genres)
.Include(m => m.EpisodeMetadata)
.ThenInclude(mm => mm.Tags)
.Include(m => m.EpisodeMetadata)
.ThenInclude(mm => mm.Studios)
.Include(m => m.EpisodeMetadata)
.ThenInclude(mm => mm.Actors)
.Include(m => m.EpisodeMetadata)
.ThenInclude(mm => mm.Directors)
.Include(m => m.EpisodeMetadata)
.ThenInclude(mm => mm.Writers)
.Include(m => m.Season)
.Filter(m => m.ItemId == episode.ItemId)
.OrderBy(m => m.ItemId)
.SingleOrDefaultAsync();
@@ -402,6 +453,51 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
thumbnail.DateUpdated = incomingThumbnail.DateUpdated;
}
// directors
foreach (Director director in metadata.Directors
.Filter(d => incomingMetadata.Directors.All(d2 => d2.Name != d.Name))
.ToList())
{
metadata.Directors.Remove(director);
}
foreach (Director director in incomingMetadata.Directors
.Filter(d => metadata.Directors.All(d2 => d2.Name != d.Name))
.ToList())
{
metadata.Directors.Add(director);
}
// writers
foreach (Writer writer in metadata.Writers
.Filter(w => incomingMetadata.Writers.All(w2 => w2.Name != w.Name))
.ToList())
{
metadata.Writers.Remove(writer);
}
foreach (Writer writer in incomingMetadata.Writers
.Filter(w => metadata.Writers.All(w2 => w2.Name != w.Name))
.ToList())
{
metadata.Writers.Add(writer);
}
// guids
foreach (MetadataGuid guid in metadata.Guids
.Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
.ToList())
{
metadata.Guids.Remove(guid);
}
foreach (MetadataGuid guid in incomingMetadata.Guids
.Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid))
.ToList())
{
metadata.Guids.Add(guid);
}
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
foreach (Artwork artworkToRemove in metadata.Artwork.Filter(a => !paths.Contains(a.Path)))
{
@@ -422,7 +518,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
await dbContext.SaveChangesAsync();
return Unit.Default;
return maybeExisting;
}
public async Task<List<int>> RemoveMissingShows(EmbyLibrary library, List<string> showIds)
@@ -454,14 +550,25 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
WHERE LP.LibraryId = @LibraryId AND js.ItemId IN @SeasonIds)",
new { LibraryId = library.Id, SeasonIds = seasonIds }).ToUnit();
public Task<Unit> RemoveMissingEpisodes(EmbyLibrary library, List<string> episodeIds) =>
_dbConnection.ExecuteAsync(
public async Task<List<int>> RemoveMissingEpisodes(EmbyLibrary library, List<string> episodeIds)
{
List<int> ids = await _dbConnection.QueryAsync<int>(
@"SELECT m.Id FROM MediaItem m
INNER JOIN EmbyEpisode ee ON ee.Id = m.Id
INNER JOIN LibraryPath lp ON lp.Id = m.LibraryPathId
WHERE lp.LibraryId = @LibraryId AND ee.ItemId IN @EpisodeIds",
new { LibraryId = library.Id, EpisodeIds = episodeIds }).Map(result => result.ToList());
await _dbConnection.ExecuteAsync(
@"DELETE FROM MediaItem WHERE Id IN
(SELECT m.Id FROM MediaItem m
INNER JOIN EmbyEpisode je ON je.Id = m.Id
INNER JOIN EmbyEpisode ee ON ee.Id = m.Id
INNER JOIN LibraryPath LP on m.LibraryPathId = LP.Id
WHERE LP.LibraryId = @LibraryId AND je.ItemId IN @EpisodeIds)",
new { LibraryId = library.Id, EpisodeIds = episodeIds }).ToUnit();
WHERE LP.LibraryId = @LibraryId AND ee.ItemId IN @EpisodeIds)",
new { LibraryId = library.Id, EpisodeIds = episodeIds });
return ids;
}
public async Task<Unit> DeleteEmptySeasons(EmbyLibrary library)
{
@@ -88,6 +88,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ThenInclude(mm => mm.Actors)
.Include(m => m.ShowMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(m => m.ShowMetadata)
.ThenInclude(mm => mm.Guids)
.Filter(m => m.ItemId == show.ItemId)
.OrderBy(m => m.ItemId)
.SingleOrDefaultAsync();
@@ -177,6 +179,21 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
metadata.Actors.Add(actor);
}
// guids
foreach (MetadataGuid guid in metadata.Guids
.Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
.ToList())
{
metadata.Guids.Remove(guid);
}
foreach (MetadataGuid guid in incomingMetadata.Guids
.Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid))
.ToList())
{
metadata.Guids.Add(guid);
}
metadata.ReleaseDate = incomingMetadata.ReleaseDate;
// poster
@@ -263,6 +280,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.Include(m => m.LibraryPath)
.Include(m => m.SeasonMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(m => m.SeasonMetadata)
.ThenInclude(mm => mm.Guids)
.Filter(m => m.ItemId == season.ItemId)
.OrderBy(m => m.ItemId)
.SingleOrDefaultAsync();
@@ -322,6 +341,21 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
fanArt.DateUpdated = incomingFanArt.DateUpdated;
}
// guids
foreach (MetadataGuid guid in metadata.Guids
.Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
.ToList())
{
metadata.Guids.Remove(guid);
}
foreach (MetadataGuid guid in incomingMetadata.Guids
.Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid))
.ToList())
{
metadata.Guids.Add(guid);
}
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
foreach (Artwork artworkToRemove in metadata.Artwork.Filter(a => !paths.Contains(a.Path)))
{
@@ -345,20 +379,37 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
await dbContext.Entry(episode).Reference(m => m.LibraryPath).LoadAsync();
await dbContext.Entry(episode.LibraryPath).Reference(lp => lp.Library).LoadAsync();
await dbContext.Entry(episode).Reference(e => e.Season).LoadAsync();
return true;
}
public async Task<Unit> Update(JellyfinEpisode episode)
public async Task<Option<JellyfinEpisode>> Update(JellyfinEpisode episode)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Option<JellyfinEpisode> maybeExisting = await dbContext.JellyfinEpisodes
.Include(m => m.LibraryPath)
.ThenInclude(lp => lp.Library)
.Include(m => m.MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(m => m.MediaVersions)
.ThenInclude(mv => mv.Streams)
.Include(m => m.EpisodeMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(m => m.EpisodeMetadata)
.ThenInclude(mm => mm.Guids)
.Include(m => m.EpisodeMetadata)
.ThenInclude(mm => mm.Genres)
.Include(m => m.EpisodeMetadata)
.ThenInclude(mm => mm.Tags)
.Include(m => m.EpisodeMetadata)
.ThenInclude(mm => mm.Studios)
.Include(m => m.EpisodeMetadata)
.ThenInclude(mm => mm.Actors)
.Include(m => m.EpisodeMetadata)
.ThenInclude(mm => mm.Directors)
.Include(m => m.EpisodeMetadata)
.ThenInclude(mm => mm.Writers)
.Include(m => m.Season)
.Filter(m => m.ItemId == episode.ItemId)
.OrderBy(m => m.ItemId)
.SingleOrDefaultAsync();
@@ -402,6 +453,51 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
thumbnail.DateUpdated = incomingThumbnail.DateUpdated;
}
// directors
foreach (Director director in metadata.Directors
.Filter(d => incomingMetadata.Directors.All(d2 => d2.Name != d.Name))
.ToList())
{
metadata.Directors.Remove(director);
}
foreach (Director director in incomingMetadata.Directors
.Filter(d => metadata.Directors.All(d2 => d2.Name != d.Name))
.ToList())
{
metadata.Directors.Add(director);
}
// writers
foreach (Writer writer in metadata.Writers
.Filter(w => incomingMetadata.Writers.All(w2 => w2.Name != w.Name))
.ToList())
{
metadata.Writers.Remove(writer);
}
foreach (Writer writer in incomingMetadata.Writers
.Filter(w => metadata.Writers.All(w2 => w2.Name != w.Name))
.ToList())
{
metadata.Writers.Add(writer);
}
// guids
foreach (MetadataGuid guid in metadata.Guids
.Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
.ToList())
{
metadata.Guids.Remove(guid);
}
foreach (MetadataGuid guid in incomingMetadata.Guids
.Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid))
.ToList())
{
metadata.Guids.Add(guid);
}
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
foreach (Artwork artworkToRemove in metadata.Artwork.Filter(a => !paths.Contains(a.Path)))
{
@@ -422,7 +518,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
await dbContext.SaveChangesAsync();
return Unit.Default;
return maybeExisting;
}
public async Task<List<int>> RemoveMissingShows(JellyfinLibrary library, List<string> showIds)
@@ -454,14 +550,25 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
WHERE LP.LibraryId = @LibraryId AND js.ItemId IN @SeasonIds)",
new { LibraryId = library.Id, SeasonIds = seasonIds }).ToUnit();
public Task<Unit> RemoveMissingEpisodes(JellyfinLibrary library, List<string> episodeIds) =>
_dbConnection.ExecuteAsync(
public async Task<List<int>> RemoveMissingEpisodes(JellyfinLibrary library, List<string> episodeIds)
{
List<int> ids = await _dbConnection.QueryAsync<int>(
@"SELECT m.Id FROM MediaItem m
INNER JOIN JellyfinEpisode je ON je.Id = m.Id
INNER JOIN LibraryPath lp ON lp.Id = m.LibraryPathId
WHERE lp.LibraryId = @LibraryId AND je.ItemId IN @EpisodeIds",
new { LibraryId = library.Id, EpisodeIds = episodeIds }).Map(result => result.ToList());
await _dbConnection.ExecuteAsync(
@"DELETE FROM MediaItem WHERE Id IN
(SELECT m.Id FROM MediaItem m
INNER JOIN JellyfinEpisode je ON je.Id = m.Id
INNER JOIN LibraryPath LP on m.LibraryPathId = LP.Id
WHERE LP.LibraryId = @LibraryId AND je.ItemId IN @EpisodeIds)",
new { LibraryId = library.Id, EpisodeIds = episodeIds }).ToUnit();
new { LibraryId = library.Id, EpisodeIds = episodeIds });
return ids;
}
public async Task<Unit> DeleteEmptySeasons(JellyfinLibrary library)
{
@@ -155,9 +155,18 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ThenInclude(i => (i as Episode).EpisodeMetadata)
.ThenInclude(em => em.Artwork)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Episode).EpisodeMetadata)
.ThenInclude(em => em.Directors)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Episode).EpisodeMetadata)
.ThenInclude(em => em.Writers)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Episode).Season)
.ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Episode).Season)
.ThenInclude(s => s.SeasonMetadata)
.OrderBy(c => c.Id)
.SingleOrDefaultAsync(c => c.Id == id)
.Map(Optional);
@@ -354,10 +354,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
List<int> movieIds = await context.PlexMovies.Map(pm => pm.Id).ToListAsync();
List<int> showIds = await context.PlexShows.Map(ps => ps.Id).ToListAsync();
List<int> episodeIds = await context.PlexEpisodes.Map(pe => pe.Id).ToListAsync();
await context.SaveChangesAsync();
return movieIds.Append(showIds).ToList();
return movieIds.Append(showIds).Append(episodeIds).ToList();
}
public async Task<List<int>> DeletePlex(PlexMediaSource plexMediaSource)
@@ -404,6 +405,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
WHERE l.Id IN @ids)",
new { ids = libraryIds });
List<int> episodeIds = await _dbConnection.QueryAsync<int>(
@"SELECT m.Id FROM MediaItem m
INNER JOIN PlexEpisode pe ON pe.Id = m.Id
INNER JOIN LibraryPath lp ON lp.Id = m.LibraryPathId
INNER JOIN Library l ON l.Id = lp.LibraryId
WHERE l.Id IN @ids",
new { ids = libraryIds }).Map(result => result.ToList());
await _dbConnection.ExecuteAsync(
@"DELETE FROM MediaItem WHERE Id IN
(SELECT m.Id FROM MediaItem m
@@ -439,7 +448,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
WHERE l.Id IN @ids)",
new { ids = libraryIds });
return movieIds.Append(showIds).ToList();
return movieIds.Append(showIds).Append(episodeIds).ToList();
}
public Task EnablePlexLibrarySync(IEnumerable<int> libraryIds) =>
@@ -561,6 +570,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
WHERE l.Id IN @ids)",
new { ids = libraryIds });
List<int> episodeIds = await _dbConnection.QueryAsync<int>(
@"SELECT m.Id FROM MediaItem m
INNER JOIN JellyfinEpisode pe ON pe.Id = m.Id
INNER JOIN LibraryPath lp ON lp.Id = m.LibraryPathId
INNER JOIN Library l ON l.Id = lp.LibraryId
WHERE l.Id IN @ids",
new { ids = libraryIds }).Map(result => result.ToList());
await _dbConnection.ExecuteAsync(
@"DELETE FROM MediaItem WHERE Id IN
(SELECT m.Id FROM MediaItem m
@@ -596,7 +613,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
WHERE l.Id IN @ids)",
new { ids = libraryIds });
return movieIds.Append(showIds).ToList();
return movieIds.Append(showIds).Append(episodeIds).ToList();
}
public Task<Option<JellyfinLibrary>> GetJellyfinLibrary(int jellyfinLibraryId)
@@ -708,9 +725,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.Map(ps => ps.Id)
.ToListAsync();
List<int> episodeIds = await context.JellyfinEpisodes
.Where(m => libraryIds.Contains(m.LibraryPath.LibraryId))
.Map(ps => ps.Id)
.ToListAsync();
await context.SaveChangesAsync();
return movieIds.Append(showIds).ToList();
return movieIds.Append(showIds).Append(episodeIds).ToList();
}
public async Task<Unit> UpsertEmby(string address, string serverName, string operatingSystem)
@@ -904,9 +926,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.Map(ps => ps.Id)
.ToListAsync();
List<int> episodeIds = await context.EmbyEpisodes
.Where(m => libraryIds.Contains(m.LibraryPath.LibraryId))
.Map(ps => ps.Id)
.ToListAsync();
await context.SaveChangesAsync();
return movieIds.Append(showIds).ToList();
return movieIds.Append(showIds).Append(episodeIds).ToList();
}
public Task<Unit> EnableEmbyLibrarySync(IEnumerable<int> libraryIds) =>
@@ -941,6 +968,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
WHERE l.Id IN @ids)",
new { ids = libraryIds });
List<int> episodeIds = await _dbConnection.QueryAsync<int>(
@"SELECT m.Id FROM MediaItem m
INNER JOIN EmbyEpisode pe ON pe.Id = m.Id
INNER JOIN LibraryPath lp ON lp.Id = m.LibraryPathId
INNER JOIN Library l ON l.Id = lp.LibraryId
WHERE l.Id IN @ids",
new { ids = libraryIds }).Map(result => result.ToList());
await _dbConnection.ExecuteAsync(
@"DELETE FROM MediaItem WHERE Id IN
(SELECT m.Id FROM MediaItem m
@@ -976,7 +1011,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
WHERE l.Id IN @ids)",
new { ids = libraryIds });
return movieIds.Append(showIds).ToList();
return movieIds.Append(showIds).Append(episodeIds).ToList();
}
}
}
@@ -75,6 +75,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
}
}
foreach (MetadataGuid guid in Optional(metadata.Guids).Flatten())
{
dbContext.Entry(guid).State = EntityState.Added;
}
if (metadata is MovieMetadata movieMetadata)
{
foreach (Director director in Optional(movieMetadata.Directors).Flatten())
@@ -88,6 +93,19 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
}
}
if (metadata is EpisodeMetadata episodeMetadata)
{
foreach (Director director in Optional(episodeMetadata.Directors).Flatten())
{
dbContext.Entry(director).State = EntityState.Added;
}
foreach (Writer writer in Optional(episodeMetadata.Writers).Flatten())
{
dbContext.Entry(writer).State = EntityState.Added;
}
}
return await dbContext.SaveChangesAsync() > 0;
}
@@ -261,6 +279,44 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
@"UPDATE MovieMetadata SET ContentRating = @ContentRating WHERE Id = @Id",
new { metadata.Id, ContentRating = contentRating }).ToUnit();
public Task<bool> RemoveGuid(MetadataGuid guid) =>
_dbConnection.ExecuteAsync("DELETE FROM MetadataGuid WHERE Id = @GuidId", new { GuidId = guid.Id })
.Map(result => result > 0);
public Task<bool> AddGuid(Metadata metadata, MetadataGuid guid) =>
metadata switch
{
MovieMetadata =>
_dbConnection.ExecuteAsync(
"INSERT INTO MetadataGuid (Guid, MovieMetadataId) VALUES (@Guid, @MetadataId)",
new { guid.Guid, MetadataId = metadata.Id }).Map(result => result > 0),
ShowMetadata =>
_dbConnection.ExecuteAsync(
"INSERT INTO MetadataGuid (Guid, ShowMetadataId) VALUES (@Guid, @MetadataId)",
new { guid.Guid, MetadataId = metadata.Id }).Map(result => result > 0),
SeasonMetadata =>
_dbConnection.ExecuteAsync(
"INSERT INTO MetadataGuid (Guid, SeasonMetadataId) VALUES (@Guid, @MetadataId)",
new { guid.Guid, MetadataId = metadata.Id }).Map(result => result > 0),
EpisodeMetadata =>
_dbConnection.ExecuteAsync(
"INSERT INTO MetadataGuid (Guid, EpisodeMetadataId) VALUES (@Guid, @MetadataId)",
new { guid.Guid, MetadataId = metadata.Id }).Map(result => result > 0),
ArtistMetadata =>
_dbConnection.ExecuteAsync(
"INSERT INTO MetadataGuid (Guid, ArtistMetadataId) VALUES (@Guid, @MetadataId)",
new { guid.Guid, MetadataId = metadata.Id }).Map(result => result > 0),
_ => throw new NotSupportedException()
};
public Task<bool> RemoveDirector(Director director) =>
_dbConnection.ExecuteAsync("DELETE FROM Director WHERE Id = @DirectorId", new { DirectorId = director.Id })
.Map(result => result > 0);
public Task<bool> RemoveWriter(Writer writer) =>
_dbConnection.ExecuteAsync("DELETE FROM Writer WHERE Id = @WriterId", new { WriterId = writer.Id })
.Map(result => result > 0);
public Task<bool> RemoveGenre(Genre genre) =>
_dbConnection.ExecuteAsync("DELETE FROM Genre WHERE Id = @GenreId", new { GenreId = genre.Id })
.Map(result => result > 0);
@@ -119,6 +119,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ThenInclude(mm => mm.Directors)
.Include(i => i.MovieMetadata)
.ThenInclude(mm => mm.Writers)
.Include(i => i.MovieMetadata)
.ThenInclude(mm => mm.Guids)
.Include(i => i.MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(i => i.MediaVersions)
@@ -328,6 +330,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ThenInclude(mm => mm.Directors)
.Include(m => m.MovieMetadata)
.ThenInclude(mm => mm.Writers)
.Include(m => m.MovieMetadata)
.ThenInclude(mm => mm.Guids)
.Filter(m => m.ItemId == movie.ItemId)
.OrderBy(m => m.ItemId)
.SingleOrDefaultAsync();
@@ -447,6 +451,21 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
metadata.Writers.Add(writer);
}
// guids
foreach (MetadataGuid guid in metadata.Guids
.Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
.ToList())
{
metadata.Guids.Remove(guid);
}
foreach (MetadataGuid guid in incomingMetadata.Guids
.Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid))
.ToList())
{
metadata.Guids.Add(guid);
}
metadata.ReleaseDate = incomingMetadata.ReleaseDate;
// poster
@@ -565,6 +584,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ThenInclude(mm => mm.Writers)
.Include(m => m.MovieMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(m => m.MovieMetadata)
.ThenInclude(mm => mm.Guids)
.Filter(m => m.ItemId == movie.ItemId)
.OrderBy(m => m.ItemId)
.SingleOrDefaultAsync();
@@ -684,6 +705,21 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
metadata.Writers.Add(writer);
}
// guids
foreach (MetadataGuid guid in metadata.Guids
.Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
.ToList())
{
metadata.Guids.Remove(guid);
}
foreach (MetadataGuid guid in incomingMetadata.Guids
.Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid))
.ToList())
{
metadata.Guids.Add(guid);
}
metadata.ReleaseDate = incomingMetadata.ReleaseDate;
// poster
@@ -737,19 +773,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
return maybeExisting;
}
public Task<bool> RemoveDirector(Director director) =>
_dbConnection.ExecuteAsync("DELETE FROM Director WHERE Id = @DirectorId", new { DirectorId = director.Id })
.Map(result => result > 0);
public Task<bool> AddDirector(MovieMetadata metadata, Director director) =>
_dbConnection.ExecuteAsync(
"INSERT INTO Director (Name, MovieMetadataId) VALUES (@Name, @MetadataId)",
new { director.Name, MetadataId = metadata.Id }).Map(result => result > 0);
public Task<bool> RemoveWriter(Writer writer) =>
_dbConnection.ExecuteAsync("DELETE FROM Writer WHERE Id = @WriterId", new { WriterId = writer.Id })
.Map(result => result > 0);
public Task<bool> AddWriter(MovieMetadata metadata, Writer writer) =>
_dbConnection.ExecuteAsync(
"INSERT INTO Writer (Name, MovieMetadataId) VALUES (@Name, @MetadataId)",
@@ -47,6 +47,21 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ThenInclude(mm => mm.Writers)
.Include(mi => (mi as Movie).MediaVersions)
.ThenInclude(mm => mm.Streams)
.Include(mi => (mi as Episode).EpisodeMetadata)
.ThenInclude(em => em.Genres)
.Include(mi => (mi as Episode).EpisodeMetadata)
.ThenInclude(em => em.Tags)
.Include(mi => (mi as Episode).EpisodeMetadata)
.ThenInclude(em => em.Studios)
.Include(mi => (mi as Episode).EpisodeMetadata)
.ThenInclude(em => em.Actors)
.Include(mi => (mi as Episode).EpisodeMetadata)
.ThenInclude(em => em.Directors)
.Include(mi => (mi as Episode).EpisodeMetadata)
.ThenInclude(em => em.Writers)
.Include(mi => (mi as Episode).MediaVersions)
.ThenInclude(em => em.Streams)
.Include(mi => (mi as Episode).Season)
.Include(mi => (mi as Show).ShowMetadata)
.ThenInclude(mm => mm.Genres)
.Include(mi => (mi as Show).ShowMetadata)
@@ -31,6 +31,12 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
new { ShowIds = showIds })
.Map(c => c == showIds.Count);
public Task<bool> AllEpisodesExist(List<int> episodeIds) =>
_dbConnection.QuerySingleAsync<int>(
"SELECT COUNT(*) FROM Episode WHERE Id in @EpisodeIds",
new { EpisodeIds = episodeIds })
.Map(c => c == episodeIds.Count);
public async Task<List<Show>> GetAllShows()
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
@@ -63,31 +69,6 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.Map(Optional);
}
public async Task<int> GetShowCount()
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.ShowMetadata
.AsNoTracking()
.GroupBy(sm => new { sm.Title, sm.Year })
.CountAsync();
}
public async Task<List<ShowMetadata>> GetPagedShows(int pageNumber, int pageSize)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.ShowMetadata.FromSqlRaw(
@"SELECT * FROM ShowMetadata WHERE Id IN
(SELECT MIN(Id) FROM ShowMetadata GROUP BY Title, Year, MetadataKind HAVING MetadataKind = MAX(MetadataKind))
ORDER BY SortTitle
LIMIT {0} OFFSET {1}",
pageSize,
(pageNumber - 1) * pageSize)
.AsNoTracking()
.Include(mm => mm.Artwork)
.OrderBy(mm => mm.SortTitle)
.ToListAsync();
}
public async Task<List<ShowMetadata>> GetShowsForCards(List<int> ids)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
@@ -99,6 +80,27 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ToListAsync();
}
public async Task<List<EpisodeMetadata>> GetEpisodesForCards(List<int> ids)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.EpisodeMetadata
.AsNoTracking()
.Filter(em => ids.Contains(em.EpisodeId))
.Include(em => em.Artwork)
.Include(em => em.Directors)
.Include(em => em.Writers)
.Include(em => em.Episode)
.ThenInclude(e => e.Season)
.ThenInclude(s => s.SeasonMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(em => em.Episode)
.ThenInclude(e => e.Season)
.ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata)
.OrderBy(em => em.SortTitle)
.ToListAsync();
}
public async Task<List<Season>> GetAllSeasons()
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
@@ -187,6 +189,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.AsNoTracking()
.Filter(em => em.Episode.SeasonId == seasonId)
.Include(em => em.Artwork)
.Include(em => em.Directors)
.Include(em => em.Writers)
.Include(em => em.Episode)
.ThenInclude(e => e.Season)
.ThenInclude(s => s.Show)
@@ -225,6 +229,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.Include(s => s.ShowMetadata)
.ThenInclude(sm => sm.Actors)
.ThenInclude(a => a.Artwork)
.Include(s => s.ShowMetadata)
.ThenInclude(sm => sm.Guids)
.Include(s => s.LibraryPath)
.ThenInclude(lp => lp.Library)
.OrderBy(s => s.Id)
@@ -248,6 +254,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
metadata.Tags ??= new List<Tag>();
metadata.Studios ??= new List<Studio>();
metadata.Actors ??= new List<Actor>();
metadata.Guids ??= new List<MetadataGuid>();
var show = new Show
{
LibraryPathId = libraryPathId,
@@ -274,6 +281,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
Option<Season> maybeExisting = await dbContext.Seasons
.Include(s => s.SeasonMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(s => s.SeasonMetadata)
.ThenInclude(sm => sm.Guids)
.OrderBy(s => s.ShowId)
.ThenBy(s => s.SeasonNumber)
.SingleOrDefaultAsync(s => s.ShowId == show.Id && s.SeasonNumber == seasonNumber);
@@ -293,12 +302,27 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.Include(i => i.EpisodeMetadata)
.ThenInclude(em => em.Artwork)
.Include(i => i.EpisodeMetadata)
.ThenInclude(em => em.Genres)
.Include(i => i.EpisodeMetadata)
.ThenInclude(em => em.Tags)
.Include(i => i.EpisodeMetadata)
.ThenInclude(em => em.Studios)
.Include(i => i.EpisodeMetadata)
.ThenInclude(em => em.Actors)
.ThenInclude(a => a.Artwork)
.Include(i => i.EpisodeMetadata)
.ThenInclude(em => em.Guids)
.Include(i => i.EpisodeMetadata)
.ThenInclude(mm => mm.Directors)
.Include(i => i.EpisodeMetadata)
.ThenInclude(mm => mm.Writers)
.Include(i => i.MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(i => i.MediaVersions)
.ThenInclude(mv => mv.Streams)
.Include(i => i.LibraryPath)
.ThenInclude(lp => lp.Library)
.Include(i => i.Season)
.OrderBy(i => i.MediaVersions.First().MediaFiles.First().Path)
.SingleOrDefaultAsync(i => i.MediaVersions.First().MediaFiles.First().Path == path);
@@ -398,6 +422,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ThenInclude(a => a.Artwork)
.Include(i => i.ShowMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(i => i.ShowMetadata)
.ThenInclude(sm => sm.Guids)
.Include(i => i.LibraryPath)
.ThenInclude(lp => lp.Library)
.OrderBy(i => i.Key)
@@ -415,7 +441,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
Option<PlexSeason> maybeExisting = await dbContext.PlexSeasons
.AsNoTracking()
.Include(i => i.SeasonMetadata)
.ThenInclude(mm => mm.Artwork)
.ThenInclude(sm => sm.Artwork)
.Include(i => i.SeasonMetadata)
.ThenInclude(sm => sm.Guids)
.OrderBy(i => i.Key)
.SingleOrDefaultAsync(i => i.Key == item.Key);
@@ -431,6 +459,16 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.AsNoTracking()
.Include(i => i.EpisodeMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(i => i.EpisodeMetadata)
.ThenInclude(mm => mm.Genres)
.Include(i => i.EpisodeMetadata)
.ThenInclude(mm => mm.Tags)
.Include(i => i.EpisodeMetadata)
.ThenInclude(mm => mm.Studios)
.Include(i => i.EpisodeMetadata)
.ThenInclude(mm => mm.Directors)
.Include(i => i.EpisodeMetadata)
.ThenInclude(mm => mm.Writers)
.Include(i => i.MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(i => i.MediaVersions)
@@ -438,6 +476,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.Include(e => e.EpisodeMetadata)
.ThenInclude(em => em.Actors)
.ThenInclude(a => a.Artwork)
.Include(e => e.EpisodeMetadata)
.ThenInclude(em => em.Guids)
.Include(i => i.LibraryPath)
.ThenInclude(lp => lp.Library)
.Include(e => e.Season)
.OrderBy(i => i.Key)
.SingleOrDefaultAsync(i => i.Key == item.Key);
@@ -456,15 +499,27 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
WHERE P.Key = @ShowKey AND ps.Key not in @Keys)",
new { ShowKey = showKey, Keys = seasonKeys }).ToUnit();
public Task<Unit> RemoveMissingPlexEpisodes(string seasonKey, List<string> episodeKeys) =>
_dbConnection.ExecuteAsync(
public async Task<List<int>> RemoveMissingPlexEpisodes(string seasonKey, List<string> episodeKeys)
{
List<int> ids = await _dbConnection.QueryAsync<int>(
@"SELECT m.Id FROM MediaItem m
INNER JOIN Episode e ON m.Id = e.Id
INNER JOIN PlexEpisode pe ON pe.Id = m.Id
INNER JOIN PlexSeason P on P.Id = e.SeasonId
WHERE P.Key = @SeasonKey AND pe.Key not in @Keys",
new { SeasonKey = seasonKey, Keys = episodeKeys }).Map(result => result.ToList());
await _dbConnection.ExecuteAsync(
@"DELETE FROM MediaItem WHERE Id IN
(SELECT m.Id FROM MediaItem m
INNER JOIN Episode e ON m.Id = e.Id
INNER JOIN PlexEpisode pe ON pe.Id = m.Id
INNER JOIN PlexSeason P on P.Id = e.SeasonId
WHERE P.Key = @SeasonKey AND pe.Key not in @Keys)",
new { SeasonKey = seasonKey, Keys = episodeKeys }).ToUnit();
new { SeasonKey = seasonKey, Keys = episodeKeys });
return ids;
}
public async Task<Unit> SetEpisodeNumber(Episode episode, int episodeNumber)
{
@@ -475,6 +530,16 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
return Unit.Default;
}
public Task<bool> AddDirector(EpisodeMetadata metadata, Director director) =>
_dbConnection.ExecuteAsync(
"INSERT INTO Director (Name, EpisodeMetadataId) VALUES (@Name, @MetadataId)",
new { director.Name, MetadataId = metadata.Id }).Map(result => result > 0);
public Task<bool> AddWriter(EpisodeMetadata metadata, Writer writer) =>
_dbConnection.ExecuteAsync(
"INSERT INTO Writer (Name, EpisodeMetadataId) VALUES (@Name, @MetadataId)",
new { writer.Name, MetadataId = metadata.Id }).Map(result => result > 0);
public async Task<List<Episode>> GetShowItems(int showId)
{
IEnumerable<int> ids = await _dbConnection.QueryAsync<int>(
@@ -613,7 +678,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
{
new()
{
DateAdded = DateTime.UtcNow
DateAdded = DateTime.UtcNow,
Guids = new List<MetadataGuid>()
}
}
};
@@ -651,7 +717,13 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.MinValue,
MetadataKind = MetadataKind.Fallback,
Actors = new List<Actor>()
Actors = new List<Actor>(),
Guids = new List<MetadataGuid>(),
Writers = new List<Writer>(),
Directors = new List<Director>(),
Genres = new List<Genre>(),
Tags = new List<Tag>(),
Studios = new List<Studio>()
}
},
MediaVersions = new List<MediaVersion>
@@ -668,6 +740,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
};
await dbContext.Episodes.AddAsync(episode);
await dbContext.SaveChangesAsync();
await dbContext.Entry(episode).Reference(i => i.LibraryPath).LoadAsync();
await dbContext.Entry(episode.LibraryPath).Reference(lp => lp.Library).LoadAsync();
await dbContext.Entry(episode).Reference(e => e.Season).LoadAsync();
return episode;
}
catch (Exception ex)
@@ -730,10 +805,19 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
}
item.LibraryPathId = library.Paths.Head().Id;
EpisodeMetadata metadata = item.EpisodeMetadata.Head();
metadata.Genres ??= new List<Genre>();
metadata.Tags ??= new List<Tag>();
metadata.Studios ??= new List<Studio>();
metadata.Actors ??= new List<Actor>();
metadata.Directors ??= new List<Director>();
metadata.Writers ??= new List<Writer>();
await dbContext.PlexEpisodes.AddAsync(item);
await dbContext.SaveChangesAsync();
await dbContext.Entry(item).Reference(i => i.LibraryPath).LoadAsync();
await dbContext.Entry(item.LibraryPath).Reference(lp => lp.Library).LoadAsync();
await dbContext.Entry(item).Reference(e => e.Season).LoadAsync();
return item;
}
catch (Exception ex)
+35 -4
View File
@@ -244,7 +244,8 @@ namespace ErsatzTV.Infrastructure.Emby
Actors = Optional(item.People).Flatten().Collect(r => ProjectToActor(r, dateAdded)).ToList(),
Directors = Optional(item.People).Flatten().Collect(r => ProjectToDirector(r)).ToList(),
Writers = Optional(item.People).Flatten().Collect(r => ProjectToWriter(r)).ToList(),
Artwork = new List<Artwork>()
Artwork = new List<Artwork>(),
Guids = GuidsFromProviderIds(item.ProviderIds)
};
// set order on actors
@@ -365,7 +366,8 @@ namespace ErsatzTV.Infrastructure.Emby
Tags = Optional(item.Tags).Flatten().Map(t => new Tag { Name = t }).ToList(),
Studios = Optional(item.Studios).Flatten().Map(s => new Studio { Name = s.Name }).ToList(),
Actors = Optional(item.People).Flatten().Collect(r => ProjectToActor(r, dateAdded)).ToList(),
Artwork = new List<Artwork>()
Artwork = new List<Artwork>(),
Guids = GuidsFromProviderIds(item.ProviderIds)
};
// set order on actors
@@ -429,7 +431,8 @@ namespace ErsatzTV.Infrastructure.Emby
SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name),
Year = item.ProductionYear,
DateAdded = dateAdded,
Artwork = new List<Artwork>()
Artwork = new List<Artwork>(),
Guids = GuidsFromProviderIds(item.ProviderIds)
};
if (!string.IsNullOrWhiteSpace(item.ImageTags.Primary))
@@ -540,7 +543,10 @@ namespace ErsatzTV.Infrastructure.Emby
Tags = new List<Tag>(),
Studios = new List<Studio>(),
Actors = new List<Actor>(),
Artwork = new List<Artwork>()
Artwork = new List<Artwork>(),
Guids = GuidsFromProviderIds(item.ProviderIds),
Directors = Optional(item.People).Flatten().Collect(r => ProjectToDirector(r)).ToList(),
Writers = Optional(item.People).Flatten().Collect(r => ProjectToWriter(r)).ToList()
};
if (DateTime.TryParse(item.PremiereDate, out DateTime releaseDate))
@@ -561,5 +567,30 @@ namespace ErsatzTV.Infrastructure.Emby
return metadata;
}
private List<MetadataGuid> GuidsFromProviderIds(EmbyProviderIdsResponse providerIds)
{
var result = new List<MetadataGuid>();
if (providerIds != null)
{
if (!string.IsNullOrWhiteSpace(providerIds.Imdb))
{
result.Add(new MetadataGuid { Guid = $"imdb://{providerIds.Imdb}" });
}
if (!string.IsNullOrWhiteSpace(providerIds.Tmdb))
{
result.Add(new MetadataGuid { Guid = $"tmdb://{providerIds.Tmdb}" });
}
if (!string.IsNullOrWhiteSpace(providerIds.Tvdb))
{
result.Add(new MetadataGuid { Guid = $"tvdb://{providerIds.Tvdb}" });
}
}
return result;
}
}
}
+9 -4
View File
@@ -28,7 +28,7 @@ namespace ErsatzTV.Infrastructure.Emby
string parentId,
[Query]
string fields =
"Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,ProductionYear,PremiereDate,MediaSources,OfficialRating",
"Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,ProductionYear,PremiereDate,MediaSources,OfficialRating,ProviderIds",
[Query]
string includeItemTypes = "Movie",
[Query]
@@ -42,7 +42,7 @@ namespace ErsatzTV.Infrastructure.Emby
string parentId,
[Query]
string fields =
"Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,ProductionYear,PremiereDate,MediaSources,OfficialRating",
"Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,ProductionYear,PremiereDate,MediaSources,OfficialRating,ProviderIds",
[Query]
string includeItemTypes = "Series",
[Query]
@@ -55,10 +55,12 @@ namespace ErsatzTV.Infrastructure.Emby
[Query]
string parentId,
[Query]
string fields = "Path,DateCreated,Etag,Taglines",
string fields = "Path,DateCreated,Etag,Taglines,ProviderIds",
[Query]
string includeItemTypes = "Season",
[Query]
string excludeLocationTypes = "Virtual",
[Query]
bool recursive = true);
[Get("/Items")]
@@ -68,10 +70,13 @@ namespace ErsatzTV.Infrastructure.Emby
[Query]
string parentId,
[Query]
string fields = "Path,DateCreated,Etag,Overview,ProductionYear,PremiereDate,MediaSources,LocationType",
string fields =
"Path,DateCreated,Etag,Overview,ProductionYear,PremiereDate,MediaSources,LocationType,ProviderIds,People",
[Query]
string includeItemTypes = "Episode",
[Query]
string excludeLocationTypes = "Virtual",
[Query]
bool recursive = true);
}
}
@@ -15,6 +15,7 @@ namespace ErsatzTV.Infrastructure.Emby.Models
public List<string> Genres { get; set; }
public List<string> Tags { get; set; }
public int ProductionYear { get; set; }
public EmbyProviderIdsResponse ProviderIds { get; set; }
public string PremiereDate { get; set; }
public List<EmbyMediaStreamResponse> MediaStreams { get; set; }
public List<EmbyMediaSourceResponse> MediaSources { get; set; }
@@ -0,0 +1,9 @@
namespace ErsatzTV.Infrastructure.Emby.Models
{
public class EmbyProviderIdsResponse
{
public string Imdb { get; set; }
public string Tmdb { get; set; }
public string Tvdb { get; set; }
}
}
@@ -34,7 +34,8 @@ namespace ErsatzTV.Infrastructure.Jellyfin
[Query]
string parentId,
[Query]
string fields = "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,OfficialRating",
string fields =
"Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,OfficialRating,ProviderIds",
[Query]
string includeItemTypes = "Movie",
[Query]
@@ -49,7 +50,8 @@ namespace ErsatzTV.Infrastructure.Jellyfin
[Query]
string parentId,
[Query]
string fields = "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,OfficialRating",
string fields =
"Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,OfficialRating,ProviderIds",
[Query]
string includeItemTypes = "Series",
[Query]
@@ -64,7 +66,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin
[Query]
string parentId,
[Query]
string fields = "Path,DateCreated,Etag,Taglines",
string fields = "Path,DateCreated,Etag,Taglines,ProviderIds",
[Query]
string includeItemTypes = "Season",
[Query]
@@ -79,7 +81,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin
[Query]
string parentId,
[Query]
string fields = "Path,DateCreated,Etag,Overview",
string fields = "Path,DateCreated,Etag,Overview,ProviderIds,People",
[Query]
string includeItemTypes = "Episode",
[Query]
@@ -290,7 +290,8 @@ namespace ErsatzTV.Infrastructure.Jellyfin
Actors = Optional(item.People).Flatten().Collect(r => ProjectToActor(r, dateAdded)).ToList(),
Directors = Optional(item.People).Flatten().Collect(r => ProjectToDirector(r)).ToList(),
Writers = Optional(item.People).Flatten().Collect(r => ProjectToWriter(r)).ToList(),
Artwork = new List<Artwork>()
Artwork = new List<Artwork>(),
Guids = GuidsFromProviderIds(item.ProviderIds)
};
// set order on actors
@@ -416,7 +417,8 @@ namespace ErsatzTV.Infrastructure.Jellyfin
Tags = Optional(item.Tags).Flatten().Map(t => new Tag { Name = t }).ToList(),
Studios = Optional(item.Studios).Flatten().Map(s => new Studio { Name = s.Name }).ToList(),
Actors = Optional(item.People).Flatten().Collect(r => ProjectToActor(r, dateAdded)).ToList(),
Artwork = new List<Artwork>()
Artwork = new List<Artwork>(),
Guids = GuidsFromProviderIds(item.ProviderIds)
};
// set order on actors
@@ -485,7 +487,8 @@ namespace ErsatzTV.Infrastructure.Jellyfin
SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name),
Year = item.ProductionYear,
DateAdded = dateAdded,
Artwork = new List<Artwork>()
Artwork = new List<Artwork>(),
Guids = GuidsFromProviderIds(item.ProviderIds)
};
if (!string.IsNullOrWhiteSpace(item.ImageTags.Primary))
@@ -596,7 +599,10 @@ namespace ErsatzTV.Infrastructure.Jellyfin
Tags = new List<Tag>(),
Studios = new List<Studio>(),
Actors = new List<Actor>(),
Artwork = new List<Artwork>()
Artwork = new List<Artwork>(),
Guids = GuidsFromProviderIds(item.ProviderIds),
Directors = Optional(item.People).Flatten().Collect(r => ProjectToDirector(r)).ToList(),
Writers = Optional(item.People).Flatten().Collect(r => ProjectToWriter(r)).ToList()
};
if (DateTime.TryParse(item.PremiereDate, out DateTime releaseDate))
@@ -617,5 +623,30 @@ namespace ErsatzTV.Infrastructure.Jellyfin
return metadata;
}
private List<MetadataGuid> GuidsFromProviderIds(JellyfinProviderIdsResponse providerIds)
{
var result = new List<MetadataGuid>();
if (providerIds != null)
{
if (!string.IsNullOrWhiteSpace(providerIds.Imdb))
{
result.Add(new MetadataGuid { Guid = $"imdb://{providerIds.Imdb}" });
}
if (!string.IsNullOrWhiteSpace(providerIds.Tmdb))
{
result.Add(new MetadataGuid { Guid = $"tmdb://{providerIds.Tmdb}" });
}
if (!string.IsNullOrWhiteSpace(providerIds.Tvdb))
{
result.Add(new MetadataGuid { Guid = $"tvdb://{providerIds.Tvdb}" });
}
}
return result;
}
}
}
@@ -15,6 +15,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin.Models
public List<string> Genres { get; set; }
public List<string> Tags { get; set; }
public int ProductionYear { get; set; }
public JellyfinProviderIdsResponse ProviderIds { get; set; }
public string PremiereDate { get; set; }
public List<JellyfinMediaStreamResponse> MediaStreams { get; set; }
public string LocationType { get; set; }
@@ -0,0 +1,9 @@
namespace ErsatzTV.Infrastructure.Jellyfin.Models
{
public class JellyfinProviderIdsResponse
{
public string Imdb { get; set; }
public string Tmdb { get; set; }
public string Tvdb { get; set; }
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,124 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_MetadataGuid : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
// local and plex
migrationBuilder.Sql("UPDATE MovieMetadata SET DateUpdated = '0001-01-01 00:00:00'");
migrationBuilder.Sql("UPDATE ShowMetadata SET DateUpdated = '0001-01-01 00:00:00'");
migrationBuilder.Sql("UPDATE SeasonMetadata SET DateUpdated = '0001-01-01 00:00:00'");
migrationBuilder.Sql("UPDATE EpisodeMetadata SET DateUpdated = '0001-01-01 00:00:00'");
migrationBuilder.Sql(
@"UPDATE LibraryFolder SET Etag = NULL WHERE LibraryPathId IN
(SELECT LibraryPathId FROM LibraryPath LP
INNER JOIN Library L on LP.LibraryId = L.Id
WHERE L.MediaKind = 1)");
// emby
migrationBuilder.Sql("UPDATE EmbyMovie SET Etag = NULL");
migrationBuilder.Sql("UPDATE EmbyShow SET Etag = NULL");
migrationBuilder.Sql("UPDATE EmbySeason SET Etag = NULL");
migrationBuilder.Sql("UPDATE EmbyEpisode SET Etag = NULL");
// jellyfin
migrationBuilder.Sql("UPDATE JellyfinMovie SET Etag = NULL");
migrationBuilder.Sql("UPDATE JellyfinShow SET Etag = NULL");
migrationBuilder.Sql("UPDATE JellyfinSeason SET Etag = NULL");
migrationBuilder.Sql("UPDATE JellyfinEpisode SET Etag = NULL");
migrationBuilder.CreateTable(
name: "MetadataGuid",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Guid = table.Column<string>(type: "TEXT", nullable: true),
ArtistMetadataId = table.Column<int>(type: "INTEGER", nullable: true),
EpisodeMetadataId = table.Column<int>(type: "INTEGER", nullable: true),
MovieMetadataId = table.Column<int>(type: "INTEGER", nullable: true),
MusicVideoMetadataId = table.Column<int>(type: "INTEGER", nullable: true),
SeasonMetadataId = table.Column<int>(type: "INTEGER", nullable: true),
ShowMetadataId = table.Column<int>(type: "INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_MetadataGuid", x => x.Id);
table.ForeignKey(
name: "FK_MetadataGuid_ArtistMetadata_ArtistMetadataId",
column: x => x.ArtistMetadataId,
principalTable: "ArtistMetadata",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_MetadataGuid_EpisodeMetadata_EpisodeMetadataId",
column: x => x.EpisodeMetadataId,
principalTable: "EpisodeMetadata",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_MetadataGuid_MovieMetadata_MovieMetadataId",
column: x => x.MovieMetadataId,
principalTable: "MovieMetadata",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_MetadataGuid_MusicVideoMetadata_MusicVideoMetadataId",
column: x => x.MusicVideoMetadataId,
principalTable: "MusicVideoMetadata",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_MetadataGuid_SeasonMetadata_SeasonMetadataId",
column: x => x.SeasonMetadataId,
principalTable: "SeasonMetadata",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_MetadataGuid_ShowMetadata_ShowMetadataId",
column: x => x.ShowMetadataId,
principalTable: "ShowMetadata",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_MetadataGuid_ArtistMetadataId",
table: "MetadataGuid",
column: "ArtistMetadataId");
migrationBuilder.CreateIndex(
name: "IX_MetadataGuid_EpisodeMetadataId",
table: "MetadataGuid",
column: "EpisodeMetadataId");
migrationBuilder.CreateIndex(
name: "IX_MetadataGuid_MovieMetadataId",
table: "MetadataGuid",
column: "MovieMetadataId");
migrationBuilder.CreateIndex(
name: "IX_MetadataGuid_MusicVideoMetadataId",
table: "MetadataGuid",
column: "MusicVideoMetadataId");
migrationBuilder.CreateIndex(
name: "IX_MetadataGuid_SeasonMetadataId",
table: "MetadataGuid",
column: "SeasonMetadataId");
migrationBuilder.CreateIndex(
name: "IX_MetadataGuid_ShowMetadataId",
table: "MetadataGuid",
column: "ShowMetadataId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "MetadataGuid");
}
}
}
@@ -0,0 +1,75 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_EpisodeMetadataDirectorsWriters : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "EpisodeMetadataId",
table: "Writer",
type: "INTEGER",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "EpisodeMetadataId",
table: "Director",
type: "INTEGER",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_Writer_EpisodeMetadataId",
table: "Writer",
column: "EpisodeMetadataId");
migrationBuilder.CreateIndex(
name: "IX_Director_EpisodeMetadataId",
table: "Director",
column: "EpisodeMetadataId");
migrationBuilder.AddForeignKey(
name: "FK_Director_EpisodeMetadata_EpisodeMetadataId",
table: "Director",
column: "EpisodeMetadataId",
principalTable: "EpisodeMetadata",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Writer_EpisodeMetadata_EpisodeMetadataId",
table: "Writer",
column: "EpisodeMetadataId",
principalTable: "EpisodeMetadata",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Director_EpisodeMetadata_EpisodeMetadataId",
table: "Director");
migrationBuilder.DropForeignKey(
name: "FK_Writer_EpisodeMetadata_EpisodeMetadataId",
table: "Writer");
migrationBuilder.DropIndex(
name: "IX_Writer_EpisodeMetadataId",
table: "Writer");
migrationBuilder.DropIndex(
name: "IX_Director_EpisodeMetadataId",
table: "Director");
migrationBuilder.DropColumn(
name: "EpisodeMetadataId",
table: "Writer");
migrationBuilder.DropColumn(
name: "EpisodeMetadataId",
table: "Director");
}
}
}
@@ -274,6 +274,9 @@ namespace ErsatzTV.Infrastructure.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int?>("EpisodeMetadataId")
.HasColumnType("INTEGER");
b.Property<int?>("MovieMetadataId")
.HasColumnType("INTEGER");
@@ -282,6 +285,8 @@ namespace ErsatzTV.Infrastructure.Migrations
b.HasKey("Id");
b.HasIndex("EpisodeMetadataId");
b.HasIndex("MovieMetadataId");
b.ToTable("Director");
@@ -740,6 +745,50 @@ namespace ErsatzTV.Infrastructure.Migrations
b.ToTable("MediaVersion");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.MetadataGuid", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int?>("ArtistMetadataId")
.HasColumnType("INTEGER");
b.Property<int?>("EpisodeMetadataId")
.HasColumnType("INTEGER");
b.Property<string>("Guid")
.HasColumnType("TEXT");
b.Property<int?>("MovieMetadataId")
.HasColumnType("INTEGER");
b.Property<int?>("MusicVideoMetadataId")
.HasColumnType("INTEGER");
b.Property<int?>("SeasonMetadataId")
.HasColumnType("INTEGER");
b.Property<int?>("ShowMetadataId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ArtistMetadataId");
b.HasIndex("EpisodeMetadataId");
b.HasIndex("MovieMetadataId");
b.HasIndex("MusicVideoMetadataId");
b.HasIndex("SeasonMetadataId");
b.HasIndex("ShowMetadataId");
b.ToTable("MetadataGuid");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Mood", b =>
{
b.Property<int>("Id")
@@ -1281,6 +1330,9 @@ namespace ErsatzTV.Infrastructure.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int?>("EpisodeMetadataId")
.HasColumnType("INTEGER");
b.Property<int?>("MovieMetadataId")
.HasColumnType("INTEGER");
@@ -1289,6 +1341,8 @@ namespace ErsatzTV.Infrastructure.Migrations
b.HasKey("Id");
b.HasIndex("EpisodeMetadataId");
b.HasIndex("MovieMetadataId");
b.ToTable("Writer");
@@ -1770,6 +1824,11 @@ namespace ErsatzTV.Infrastructure.Migrations
modelBuilder.Entity("ErsatzTV.Core.Domain.Director", b =>
{
b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null)
.WithMany("Directors")
.HasForeignKey("EpisodeMetadataId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null)
.WithMany("Directors")
.HasForeignKey("MovieMetadataId")
@@ -1957,6 +2016,37 @@ namespace ErsatzTV.Infrastructure.Migrations
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.MetadataGuid", b =>
{
b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null)
.WithMany("Guids")
.HasForeignKey("ArtistMetadataId");
b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null)
.WithMany("Guids")
.HasForeignKey("EpisodeMetadataId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null)
.WithMany("Guids")
.HasForeignKey("MovieMetadataId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null)
.WithMany("Guids")
.HasForeignKey("MusicVideoMetadataId");
b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null)
.WithMany("Guids")
.HasForeignKey("SeasonMetadataId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null)
.WithMany("Guids")
.HasForeignKey("ShowMetadataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Mood", b =>
{
b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null)
@@ -2255,6 +2345,11 @@ namespace ErsatzTV.Infrastructure.Migrations
modelBuilder.Entity("ErsatzTV.Core.Domain.Writer", b =>
{
b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null)
.WithMany("Writers")
.HasForeignKey("EpisodeMetadataId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null)
.WithMany("Writers")
.HasForeignKey("MovieMetadataId")
@@ -2572,6 +2667,8 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Navigation("Genres");
b.Navigation("Guids");
b.Navigation("Moods");
b.Navigation("Studios");
@@ -2599,11 +2696,17 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Navigation("Artwork");
b.Navigation("Directors");
b.Navigation("Genres");
b.Navigation("Guids");
b.Navigation("Studios");
b.Navigation("Tags");
b.Navigation("Writers");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b =>
@@ -2645,6 +2748,8 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Navigation("Genres");
b.Navigation("Guids");
b.Navigation("Studios");
b.Navigation("Tags");
@@ -2660,6 +2765,8 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Navigation("Genres");
b.Navigation("Guids");
b.Navigation("Studios");
b.Navigation("Tags");
@@ -2687,6 +2794,8 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Navigation("Genres");
b.Navigation("Guids");
b.Navigation("Studios");
b.Navigation("Tags");
@@ -2700,6 +2809,8 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Navigation("Genres");
b.Navigation("Guids");
b.Navigation("Studios");
b.Navigation("Tags");
+24 -5
View File
@@ -4,15 +4,16 @@ using Refit;
namespace ErsatzTV.Infrastructure.Plex
{
[Headers("Accept: application/json")]
public interface IPlexServerApi
{
[Get("/library/sections")]
[Headers("Accept: application/json")]
public Task<PlexMediaContainerResponse<PlexMediaContainerDirectoryContent<PlexLibraryResponse>>> GetLibraries(
[Query] [AliasAs("X-Plex-Token")]
string token);
[Get("/library/sections/{key}/all")]
[Headers("Accept: application/json")]
public Task<PlexMediaContainerResponse<PlexMediaContainerMetadataContent<PlexMetadataResponse>>>
GetLibrarySectionContents(
string key,
@@ -20,15 +21,33 @@ namespace ErsatzTV.Infrastructure.Plex
string token);
[Get("/library/metadata/{key}")]
public Task<PlexMediaContainerResponse<PlexMediaContainerMetadataContent<PlexMetadataResponse>>>
GetMetadata(
[Headers("Accept: text/xml")]
public Task<PlexXmlVideoMetadataResponseContainer>
GetVideoMetadata(
string key,
[Query] [AliasAs("X-Plex-Token")]
string token);
[Get("/library/metadata/{key}")]
[Headers("Accept: text/xml")]
public Task<PlexXmlDirectoryMetadataResponseContainer>
GetDirectoryMetadata(
string key,
[Query] [AliasAs("X-Plex-Token")]
string token);
[Get("/library/metadata/{key}/children")]
public Task<PlexMediaContainerResponse<PlexMediaContainerMetadataContent<PlexMetadataResponse>>>
GetChildren(
[Headers("Accept: text/xml")]
public Task<PlexXmlSeasonsMetadataResponseContainer>
GetShowChildren(
string key,
[Query] [AliasAs("X-Plex-Token")]
string token);
[Get("/library/metadata/{key}/children")]
[Headers("Accept: text/xml")]
public Task<PlexXmlEpisodesMetadataResponseContainer>
GetSeasonChildren(
string key,
[Query] [AliasAs("X-Plex-Token")]
string token);
@@ -1,7 +1,10 @@
namespace ErsatzTV.Infrastructure.Plex.Models
using System.Xml.Serialization;
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexDirectorResponse
{
[XmlAttribute("tag")]
public string Tag { get; set; }
}
}
@@ -1,9 +1,16 @@
namespace ErsatzTV.Infrastructure.Plex.Models
using System.Xml.Serialization;
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexGenreResponse
{
[XmlAttribute("id")]
public int Id { get; set; }
[XmlAttribute("filter")]
public string Filter { get; set; }
[XmlAttribute("tag")]
public string Tag { get; set; }
}
}
@@ -0,0 +1,10 @@
using System.Xml.Serialization;
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexGuidResponse
{
[XmlAttribute("id")]
public string Id { get; set; }
}
}
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Xml.Serialization;
namespace ErsatzTV.Infrastructure.Plex.Models
{
@@ -16,4 +17,32 @@ namespace ErsatzTV.Infrastructure.Plex.Models
{
public List<T> Metadata { get; set; }
}
[XmlRoot("MediaContainer", Namespace = null)]
public class PlexXmlVideoMetadataResponseContainer
{
[XmlElement("Video")]
public PlexXmlMetadataResponse Metadata { get; set; }
}
[XmlRoot("MediaContainer", Namespace = null)]
public class PlexXmlDirectoryMetadataResponseContainer
{
[XmlElement("Directory")]
public PlexXmlMetadataResponse Metadata { get; set; }
}
[XmlRoot("MediaContainer", Namespace = null)]
public class PlexXmlSeasonsMetadataResponseContainer
{
[XmlElement("Directory")]
public List<PlexXmlMetadataResponse> Metadata { get; set; }
}
[XmlRoot("MediaContainer", Namespace = null)]
public class PlexXmlEpisodesMetadataResponseContainer
{
[XmlElement("Video")]
public List<PlexXmlMetadataResponse> Metadata { get; set; }
}
}
@@ -1,21 +1,50 @@
using System.Collections.Generic;
using System.Xml.Serialization;
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexMediaResponse
public class PlexMediaResponse<T>
{
[XmlAttribute("id")]
public int Id { get; set; }
[XmlAttribute("duration")]
public int Duration { get; set; }
[XmlAttribute("bitrate")]
public int Bitrate { get; set; }
[XmlAttribute("width")]
public int Width { get; set; }
[XmlAttribute("height")]
public int Height { get; set; }
[XmlAttribute("aspectRatio")]
public double AspectRatio { get; set; }
[XmlAttribute("audioChannels")]
public int AudioChannels { get; set; }
[XmlAttribute("audioCodec")]
public string AudioCodec { get; set; }
[XmlAttribute("videoCodec")]
public string VideoCodec { get; set; }
[XmlAttribute("videoResulution")]
public string VideoResolution { get; set; }
[XmlAttribute("videoProfile")]
public string VideoProfile { get; set; }
[XmlAttribute("container")]
public string Container { get; set; }
[XmlAttribute("videoFrameRate")]
public string VideoFrameRate { get; set; }
public List<PlexPartResponse> Part { get; set; }
[XmlElement("Part")]
public List<T> Part { get; set; }
}
}
@@ -1,26 +1,74 @@
using System.Collections.Generic;
using System.Xml.Serialization;
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexMetadataResponse
{
[XmlAttribute("key")]
public string Key { get; set; }
[XmlAttribute("title")]
public string Title { get; set; }
[XmlAttribute("contentRating")]
public string ContentRating { get; set; }
[XmlAttribute("summary")]
public string Summary { get; set; }
[XmlAttribute("year")]
public int Year { get; set; }
[XmlAttribute("tagline")]
public string Tagline { get; set; }
[XmlAttribute("thumb")]
public string Thumb { get; set; }
[XmlAttribute("art")]
public string Art { get; set; }
[XmlAttribute("originallyAvailableAt")]
public string OriginallyAvailableAt { get; set; }
[XmlAttribute("addedAt")]
public long AddedAt { get; set; }
[XmlAttribute("updatedAt")]
public long UpdatedAt { get; set; }
[XmlAttribute("index")]
public int Index { get; set; }
[XmlAttribute("studio")]
public string Studio { get; set; }
public List<PlexMediaResponse> Media { get; set; }
[XmlAttribute("rating")]
public double Rating { get; set; }
[XmlAttribute("audienceRating")]
public double AudienceRating { get; set; }
[XmlAttribute("audienceRatingImage")]
public string AudienceRatingImage { get; set; }
[XmlAttribute("ratingImage")]
public string RatingImage { get; set; }
[XmlIgnore]
public virtual List<PlexMediaResponse<PlexPartResponse>> Media { get; set; }
[XmlElement("Genre")]
public List<PlexGenreResponse> Genre { get; set; }
[XmlElement("Role")]
public List<PlexRoleResponse> Role { get; set; }
[XmlElement("Director")]
public List<PlexDirectorResponse> Director { get; set; }
[XmlElement("Writer")]
public List<PlexWriterResponse> Writer { get; set; }
}
}
@@ -1,6 +1,4 @@
using System.Collections.Generic;
namespace ErsatzTV.Infrastructure.Plex.Models
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexPartResponse
{
@@ -8,6 +6,5 @@ namespace ErsatzTV.Infrastructure.Plex.Models
public string Key { get; set; }
public int Duration { get; set; }
public string File { get; set; }
public List<PlexStreamResponse> Stream { get; set; }
}
}
@@ -1,9 +1,16 @@
namespace ErsatzTV.Infrastructure.Plex.Models
using System.Xml.Serialization;
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexRoleResponse
{
[XmlAttribute("tag")]
public string Tag { get; set; }
[XmlAttribute("role")]
public string Role { get; set; }
[XmlAttribute("thumb")]
public string Thumb { get; set; }
}
}
@@ -1,18 +1,43 @@
namespace ErsatzTV.Infrastructure.Plex.Models
using System.Xml.Serialization;
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexStreamResponse
{
[XmlAttribute("id")]
public int Id { get; set; }
[XmlAttribute("index")]
public int Index { get; set; }
[XmlAttribute("default")]
public bool Default { get; set; }
[XmlAttribute("forced")]
public bool Forced { get; set; }
[XmlAttribute("languageCode")]
public string LanguageCode { get; set; }
[XmlAttribute("streamType")]
public int StreamType { get; set; }
[XmlAttribute("codec")]
public string Codec { get; set; }
[XmlAttribute("profile")]
public string Profile { get; set; }
[XmlAttribute("channels")]
public int Channels { get; set; }
[XmlAttribute("anamorphic")]
public bool Anamorphic { get; set; }
[XmlAttribute("pixelAspectRatio")]
public string PixelAspectRatio { get; set; }
[XmlAttribute("scanType")]
public string ScanType { get; set; }
}
}
@@ -1,7 +1,10 @@
namespace ErsatzTV.Infrastructure.Plex.Models
using System.Xml.Serialization;
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexWriterResponse
{
[XmlAttribute("tag")]
public string Tag { get; set; }
}
}
@@ -0,0 +1,17 @@
using System.Collections.Generic;
using System.Xml.Serialization;
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexXmlMetadataResponse : PlexMetadataResponse
{
[XmlAttribute("guid")]
public string PlexGuid { get; set; }
[XmlElement("Media")]
public new List<PlexMediaResponse<PlexXmlPartResponse>> Media { get; set; }
[XmlElement("Guid")]
public List<PlexGuidResponse> Guid { get; set; }
}
}
@@ -0,0 +1,23 @@
using System.Collections.Generic;
using System.Xml.Serialization;
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexXmlPartResponse
{
[XmlAttribute("id")]
public int Id { get; set; }
[XmlAttribute("key")]
public string Key { get; set; }
[XmlAttribute("duration")]
public int Duration { get; set; }
[XmlAttribute("file")]
public string File { get; set; }
[XmlElement("Stream")]
public List<PlexStreamResponse> Stream { get; set; }
}
}
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Serialization;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
@@ -87,9 +88,9 @@ namespace ErsatzTV.Infrastructure.Plex
{
try
{
IPlexServerApi service = RestService.For<IPlexServerApi>(connection.Uri);
return await service.GetChildren(show.Key.Split("/").Reverse().Skip(1).Head(), token.AuthToken)
.Map(r => r.MediaContainer.Metadata)
IPlexServerApi service = XmlServiceFor(connection.Uri);
return await service.GetShowChildren(show.Key.Split("/").Reverse().Skip(1).Head(), token.AuthToken)
.Map(r => r.Metadata)
.Map(list => list.Map(metadata => ProjectToSeason(metadata, library.MediaSourceId)).ToList());
}
catch (Exception ex)
@@ -106,9 +107,9 @@ namespace ErsatzTV.Infrastructure.Plex
{
try
{
IPlexServerApi service = RestService.For<IPlexServerApi>(connection.Uri);
return await service.GetChildren(season.Key.Split("/").Reverse().Skip(1).Head(), token.AuthToken)
.Map(r => r.MediaContainer.Metadata.Filter(m => m.Media.Count > 0 && m.Media[0].Part.Count > 0))
IPlexServerApi service = XmlServiceFor(connection.Uri);
return await service.GetSeasonChildren(season.Key.Split("/").Reverse().Skip(1).Head(), token.AuthToken)
.Map(r => r.Metadata.Filter(m => m.Media.Count > 0 && m.Media[0].Part.Count > 0))
.Map(list => list.Map(metadata => ProjectToEpisode(metadata, library.MediaSourceId)).ToList());
}
catch (Exception ex)
@@ -125,12 +126,13 @@ namespace ErsatzTV.Infrastructure.Plex
{
try
{
IPlexServerApi service = RestService.For<IPlexServerApi>(connection.Uri);
return await service.GetMetadata(key, token.AuthToken)
IPlexServerApi service = XmlServiceFor(connection.Uri);
return await service.GetVideoMetadata(key, token.AuthToken)
.Map(Optional)
.Map(
r => r.MediaContainer.Metadata.Filter(m => m.Media.Count > 0 && m.Media[0].Part.Count > 0)
r => r.Filter(m => m.Metadata.Media.Count > 0 && m.Metadata.Media[0].Part.Count > 0)
.HeadOrNone())
.MapT(response => ProjectToMovieMetadata(response, library.MediaSourceId))
.MapT(response => ProjectToMovieMetadata(response.Metadata, library.MediaSourceId))
.Map(o => o.ToEither<BaseError>("Unable to locate metadata"));
}
catch (Exception ex)
@@ -147,10 +149,10 @@ namespace ErsatzTV.Infrastructure.Plex
{
try
{
IPlexServerApi service = RestService.For<IPlexServerApi>(connection.Uri);
return await service.GetMetadata(key, token.AuthToken)
.Map(r => r.MediaContainer.Metadata.HeadOrNone())
.MapT(response => ProjectToShowMetadata(response, library.MediaSourceId))
IPlexServerApi service = XmlServiceFor(connection.Uri);
return await service.GetDirectoryMetadata(key, token.AuthToken)
.Map(Optional)
.MapT(response => ProjectToShowMetadata(response.Metadata, library.MediaSourceId))
.Map(o => o.ToEither<BaseError>("Unable to locate metadata"));
}
catch (Exception ex)
@@ -159,20 +161,30 @@ namespace ErsatzTV.Infrastructure.Plex
}
}
public async Task<Either<BaseError, MediaVersion>> GetStatistics(
public async Task<Either<BaseError, Tuple<MovieMetadata, MediaVersion>>> GetMovieMetadataAndStatistics(
PlexLibrary library,
string key,
PlexConnection connection,
PlexServerAuthToken token)
{
try
{
IPlexServerApi service = RestService.For<IPlexServerApi>(connection.Uri);
return await service.GetMetadata(key, token.AuthToken)
IPlexServerApi service = XmlServiceFor(connection.Uri);
Option<PlexXmlVideoMetadataResponseContainer> maybeResponse = await service
.GetVideoMetadata(key, token.AuthToken)
.Map(Optional)
.Map(
r => r.MediaContainer.Metadata.Filter(m => m.Media.Count > 0 && m.Media[0].Part.Count > 0)
.HeadOrNone())
.BindT(ProjectToMediaVersion)
.Map(o => o.ToEither<BaseError>("Unable to locate metadata"));
r => r.Filter(m => m.Metadata.Media.Count > 0 && m.Metadata.Media[0].Part.Count > 0)
.HeadOrNone());
return maybeResponse.Match(
response =>
{
Option<MediaVersion> maybeVersion = ProjectToMediaVersion(response.Metadata);
return maybeVersion.Match<Either<BaseError, Tuple<MovieMetadata, MediaVersion>>>(
version => Tuple(ProjectToMovieMetadata(response.Metadata, library.MediaSourceId), version),
() => BaseError.New("Unable to locate metadata"));
},
() => BaseError.New("Unable to locate metadata"));
}
catch (Exception ex)
{
@@ -180,6 +192,57 @@ namespace ErsatzTV.Infrastructure.Plex
}
}
public async Task<Either<BaseError, Tuple<EpisodeMetadata, MediaVersion>>> GetEpisodeMetadataAndStatistics(
PlexLibrary library,
string key,
PlexConnection connection,
PlexServerAuthToken token)
{
try
{
IPlexServerApi service = XmlServiceFor(connection.Uri);
Option<PlexXmlVideoMetadataResponseContainer> maybeResponse = await service
.GetVideoMetadata(key, token.AuthToken)
.Map(Optional)
.Map(
r => r.Filter(m => m.Metadata.Media.Count > 0 && m.Metadata.Media[0].Part.Count > 0)
.HeadOrNone());
return maybeResponse.Match(
response =>
{
Option<MediaVersion> maybeVersion = ProjectToMediaVersion(response.Metadata);
return maybeVersion.Match<Either<BaseError, Tuple<EpisodeMetadata, MediaVersion>>>(
version => Tuple(
ProjectToEpisodeMetadata(response.Metadata, library.MediaSourceId),
version),
() => BaseError.New("Unable to locate metadata"));
},
() => BaseError.New("Unable to locate metadata"));
}
catch (Exception ex)
{
return BaseError.New(ex.ToString());
}
}
private static IPlexServerApi XmlServiceFor(string uri)
{
var overrides = new XmlAttributeOverrides();
var attrs = new XmlAttributes { XmlIgnore = true };
overrides.Add(typeof(PlexMetadataResponse), "Media", attrs);
return RestService.For<IPlexServerApi>(
uri,
new RefitSettings
{
ContentSerializer = new XmlContentSerializer(
new XmlContentSerializerSettings
{
XmlAttributeOverrides = overrides
})
});
}
private static Option<PlexLibrary> Project(PlexLibraryResponse response) =>
response.Type switch
{
@@ -205,7 +268,7 @@ namespace ErsatzTV.Infrastructure.Plex
private PlexMovie ProjectToMovie(PlexMetadataResponse response, int mediaSourceId)
{
PlexMediaResponse media = response.Media.Head();
PlexMediaResponse<PlexPartResponse> media = response.Media.Head();
PlexPartResponse part = media.Part.Head();
DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime;
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime;
@@ -268,6 +331,23 @@ namespace ErsatzTV.Infrastructure.Plex
Writers = Optional(response.Writer).Flatten().Map(w => new Writer { Name = w.Tag }).ToList()
};
if (response is PlexXmlMetadataResponse xml)
{
metadata.Guids = Optional(xml.Guid).Flatten().Map(g => new MetadataGuid { Guid = g.Id }).ToList();
if (!string.IsNullOrWhiteSpace(xml.PlexGuid))
{
string normalized = NormalizeGuid(xml.PlexGuid);
if (!string.IsNullOrWhiteSpace(normalized) && metadata.Guids.All(g => g.Guid != normalized))
{
metadata.Guids.Add(new MetadataGuid { Guid = normalized });
}
}
}
else
{
metadata.Guids = new List<MetadataGuid>();
}
if (!string.IsNullOrWhiteSpace(response.Studio))
{
metadata.Studios.Add(new Studio { Name = response.Studio });
@@ -311,7 +391,7 @@ namespace ErsatzTV.Infrastructure.Plex
return metadata;
}
private Option<MediaVersion> ProjectToMediaVersion(PlexMetadataResponse response)
private Option<MediaVersion> ProjectToMediaVersion(PlexXmlMetadataResponse response)
{
List<PlexStreamResponse> streams = response.Media.Head().Part.Head().Stream;
DateTime dateUpdated = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime;
@@ -418,6 +498,23 @@ namespace ErsatzTV.Infrastructure.Plex
.ToList()
};
if (response is PlexXmlMetadataResponse xml)
{
metadata.Guids = Optional(xml.Guid).Flatten().Map(g => new MetadataGuid { Guid = g.Id }).ToList();
if (!string.IsNullOrWhiteSpace(xml.PlexGuid))
{
string normalized = NormalizeGuid(xml.PlexGuid);
if (!string.IsNullOrWhiteSpace(normalized) && metadata.Guids.All(g => g.Guid != normalized))
{
metadata.Guids.Add(new MetadataGuid { Guid = normalized });
}
}
}
else
{
metadata.Guids = new List<MetadataGuid>();
}
if (!string.IsNullOrWhiteSpace(response.Studio))
{
metadata.Studios.Add(new Studio { Name = response.Studio });
@@ -461,7 +558,7 @@ namespace ErsatzTV.Infrastructure.Plex
return metadata;
}
private PlexSeason ProjectToSeason(PlexMetadataResponse response, int mediaSourceId)
private PlexSeason ProjectToSeason(PlexXmlMetadataResponse response, int mediaSourceId)
{
DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime;
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime;
@@ -476,6 +573,16 @@ namespace ErsatzTV.Infrastructure.Plex
DateUpdated = lastWriteTime
};
metadata.Guids = Optional(response.Guid).Flatten().Map(g => new MetadataGuid { Guid = g.Id }).ToList();
if (!string.IsNullOrWhiteSpace(response.PlexGuid))
{
string normalized = NormalizeGuid(response.PlexGuid);
if (!string.IsNullOrWhiteSpace(normalized) && metadata.Guids.All(g => g.Guid != normalized))
{
metadata.Guids.Add(new MetadataGuid { Guid = normalized });
}
}
if (!string.IsNullOrWhiteSpace(response.Thumb))
{
var path = $"plex/{mediaSourceId}{response.Thumb}";
@@ -516,47 +623,14 @@ namespace ErsatzTV.Infrastructure.Plex
return season;
}
private PlexEpisode ProjectToEpisode(PlexMetadataResponse response, int mediaSourceId)
private PlexEpisode ProjectToEpisode(PlexXmlMetadataResponse response, int mediaSourceId)
{
PlexMediaResponse media = response.Media.Head();
PlexPartResponse part = media.Part.Head();
PlexMediaResponse<PlexXmlPartResponse> media = response.Media.Head();
PlexXmlPartResponse part = media.Part.Head();
DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime;
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime;
var metadata = new EpisodeMetadata
{
MetadataKind = MetadataKind.External,
Title = response.Title,
SortTitle = _fallbackMetadataProvider.GetSortTitle(response.Title),
Plot = response.Summary,
Year = response.Year,
Tagline = response.Tagline,
DateAdded = dateAdded,
DateUpdated = lastWriteTime,
Actors = Optional(response.Role).Flatten().Map(r => ProjectToModel(r, dateAdded, lastWriteTime))
.ToList()
};
if (DateTime.TryParse(response.OriginallyAvailableAt, out DateTime releaseDate))
{
metadata.ReleaseDate = releaseDate;
}
if (!string.IsNullOrWhiteSpace(response.Thumb))
{
var path = $"plex/{mediaSourceId}{response.Thumb}";
var artwork = new Artwork
{
ArtworkKind = ArtworkKind.Thumbnail,
Path = path,
DateAdded = dateAdded,
DateUpdated = lastWriteTime
};
metadata.Artwork ??= new List<Artwork>();
metadata.Artwork.Add(artwork);
}
EpisodeMetadata metadata = ProjectToEpisodeMetadata(response, mediaSourceId);
var version = new MediaVersion
{
Name = "Main",
@@ -589,6 +663,67 @@ namespace ErsatzTV.Infrastructure.Plex
return episode;
}
private EpisodeMetadata ProjectToEpisodeMetadata(PlexMetadataResponse response, int mediaSourceId)
{
DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime;
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime;
var metadata = new EpisodeMetadata
{
MetadataKind = MetadataKind.External,
Title = response.Title,
SortTitle = _fallbackMetadataProvider.GetSortTitle(response.Title),
Plot = response.Summary,
Year = response.Year,
Tagline = response.Tagline,
DateAdded = dateAdded,
DateUpdated = lastWriteTime,
Actors = Optional(response.Role).Flatten().Map(r => ProjectToModel(r, dateAdded, lastWriteTime))
.ToList(),
Directors = Optional(response.Director).Flatten().Map(d => new Director { Name = d.Tag }).ToList(),
Writers = Optional(response.Writer).Flatten().Map(w => new Writer { Name = w.Tag }).ToList()
};
if (response is PlexXmlMetadataResponse xml)
{
metadata.Guids = Optional(xml.Guid).Flatten().Map(g => new MetadataGuid { Guid = g.Id }).ToList();
if (!string.IsNullOrWhiteSpace(xml.PlexGuid))
{
string normalized = NormalizeGuid(xml.PlexGuid);
if (!string.IsNullOrWhiteSpace(normalized) && metadata.Guids.All(g => g.Guid != normalized))
{
metadata.Guids.Add(new MetadataGuid { Guid = normalized });
}
}
}
else
{
metadata.Guids = new List<MetadataGuid>();
}
if (DateTime.TryParse(response.OriginallyAvailableAt, out DateTime releaseDate))
{
metadata.ReleaseDate = releaseDate;
}
if (!string.IsNullOrWhiteSpace(response.Thumb))
{
var path = $"plex/{mediaSourceId}{response.Thumb}";
var artwork = new Artwork
{
ArtworkKind = ArtworkKind.Thumbnail,
Path = path,
DateAdded = dateAdded,
DateUpdated = lastWriteTime
};
metadata.Artwork ??= new List<Artwork>();
metadata.Artwork.Add(artwork);
}
return metadata;
}
private Actor ProjectToModel(PlexRoleResponse role, DateTime dateAdded, DateTime lastWriteTime)
{
var actor = new Actor { Name = role.Tag, Role = role.Role };
@@ -605,5 +740,32 @@ namespace ErsatzTV.Infrastructure.Plex
return actor;
}
private string NormalizeGuid(string guid)
{
if (guid.StartsWith("plex://show") ||
guid.StartsWith("plex://season") ||
guid.StartsWith("plex://episode") ||
guid.StartsWith("plex://movie"))
{
return guid;
}
if (guid.StartsWith("com.plexapp.agents.thetvdb"))
{
string strip1 = guid.Replace("com.plexapp.agents.thetvdb://", string.Empty);
string strip2 = strip1.Split("?").Head();
return $"tvdb://{strip2}";
}
if (guid.StartsWith("com.plexapp.agents.themoviedb"))
{
string strip1 = guid.Replace("com.plexapp.agents.themoviedb://", string.Empty);
string strip2 = strip1.Split("?").Head();
return $"tmdb://{strip2}";
}
throw new NotSupportedException(guid);
}
}
}
+92 -2
View File
@@ -56,6 +56,7 @@ namespace ErsatzTV.Infrastructure.Search
private const string ShowType = "show";
private const string ArtistType = "artist";
private const string MusicVideoType = "music_video";
private const string EpisodeType = "episode";
private readonly List<CultureInfo> _cultureInfos;
private readonly ILogger<SearchIndex> _logger;
@@ -71,7 +72,7 @@ namespace ErsatzTV.Infrastructure.Search
_initialized = false;
}
public int Version => 12;
public int Version => 13;
public Task<bool> Initialize(ILocalFileSystem localFileSystem)
{
@@ -114,6 +115,9 @@ namespace ErsatzTV.Infrastructure.Search
case MusicVideo musicVideo:
UpdateMusicVideo(musicVideo);
break;
case Episode episode:
UpdateEpisode(episode);
break;
}
}
}
@@ -143,6 +147,9 @@ namespace ErsatzTV.Infrastructure.Search
case MusicVideo musicVideo:
UpdateMusicVideo(musicVideo);
break;
case Episode episode:
UpdateEpisode(episode);
break;
}
}
@@ -577,6 +584,83 @@ namespace ErsatzTV.Infrastructure.Search
}
}
private void UpdateEpisode(Episode episode)
{
Option<EpisodeMetadata> maybeMetadata = episode.EpisodeMetadata.HeadOrNone();
if (maybeMetadata.IsSome)
{
EpisodeMetadata metadata = maybeMetadata.ValueUnsafe();
try
{
var doc = new Document
{
new StringField(IdField, episode.Id.ToString(), Field.Store.YES),
new StringField(TypeField, EpisodeType, Field.Store.NO),
new TextField(TitleField, metadata.Title, Field.Store.NO),
new StringField(SortTitleField, metadata.SortTitle.ToLowerInvariant(), Field.Store.NO),
new TextField(LibraryNameField, episode.LibraryPath.Library.Name, Field.Store.NO),
new StringField(LibraryIdField, episode.LibraryPath.Library.Id.ToString(), Field.Store.NO),
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO),
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES)
};
AddLanguages(doc, episode.MediaVersions);
if (metadata.ReleaseDate.HasValue)
{
doc.Add(
new StringField(
ReleaseDateField,
metadata.ReleaseDate.Value.ToString("yyyyMMdd"),
Field.Store.NO));
}
if (!string.IsNullOrWhiteSpace(metadata.Plot))
{
doc.Add(new TextField(PlotField, metadata.Plot ?? string.Empty, Field.Store.NO));
}
foreach (Genre genre in metadata.Genres)
{
doc.Add(new TextField(GenreField, genre.Name, Field.Store.NO));
}
foreach (Tag tag in metadata.Tags)
{
doc.Add(new TextField(TagField, tag.Name, Field.Store.NO));
}
foreach (Studio studio in metadata.Studios)
{
doc.Add(new TextField(StudioField, studio.Name, Field.Store.NO));
}
foreach (Actor actor in metadata.Actors)
{
doc.Add(new TextField(ActorField, actor.Name, Field.Store.NO));
}
foreach (Director director in metadata.Directors)
{
doc.Add(new TextField(DirectorField, director.Name, Field.Store.NO));
}
foreach (Writer writer in metadata.Writers)
{
doc.Add(new TextField(WriterField, writer.Name, Field.Store.NO));
}
_writer.UpdateDocument(new Term(IdField, episode.Id.ToString()), doc);
}
catch (Exception ex)
{
metadata.Episode = null;
_logger.LogWarning(ex, "Error indexing episode with metadata {@Metadata}", metadata);
}
}
}
private SearchItem ProjectToSearchItem(Document doc) => new(Convert.ToInt32(doc.Get(IdField)));
private Query ParseQuery(string searchQuery, QueryParser parser)
@@ -595,7 +679,13 @@ namespace ErsatzTV.Infrastructure.Search
}
private static string GetTitleAndYear(Metadata metadata) =>
$"{metadata.Title}_{metadata.Year}".ToLowerInvariant();
metadata switch
{
EpisodeMetadata em =>
$"{em.Title}_{em.Year}_{em.Episode.Season.SeasonNumber}_{em.Episode.EpisodeNumber}"
.ToLowerInvariant(),
_ => $"{metadata.Title}_{metadata.Year}".ToLowerInvariant()
};
private static string GetJumpLetter(Metadata metadata)
{