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:
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
+2862
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+2886
File diff suppressed because it is too large
Load Diff
+75
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user