Database redesign (#31)

* starting database redesign

* set season and episode numbers

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

* get movie cards from new table

* copy show/episode metadata

* remove old movie metadata type

* rename new movie metadata type

* code cleanup

* start to remove old television classes

* remove old television tables from database

* fix playout building

* fix collection views

* fix show/season views

* clean up movie metadata table

* fix scanner tests

* add libraries ui

* code cleanup

* fix movie scanning/metadata

* add library scan button to ui

* delete library path from ui

* temp disable movie scanning

* remove orphan media items and prevent duplicate paths

* attach artwork to metadata

* fix split show/season display

* fix television artwork

* store year distinct from release date

* fix collections ui

* code cleanup

* add library paths from ui

* fix adding to collections from ui

* fix schedule items loading

* schedule editing works again

* remove some todos

* more cleanup

* fix unit tests

* fix episode sorting

* fix deleting show library paths

* remove unused class

* fix playout list in ui

* fix log viewer

* start to use version/file instead of statistics

* clean up old columns

* fix playout display (time zone)

* fix playback

* fix channel guide time zone

* cascade more deletes

* fix compiler warnings

* fix adding new seasons

* use artwork for channel logo

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

* log database migration

* update homepage docs for libraries

* fix adding new channel with logo

* fix episode numbers in epg
This commit is contained in:
Jason Dove
2021-02-28 17:48:01 +00:00
committed by GitHub
parent e25b9edd01
commit f392bab118
392 changed files with 52108 additions and 4079 deletions
@@ -8,6 +8,8 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
{
public void Configure(EntityTypeBuilder<Channel> builder)
{
builder.ToTable("Channel");
builder.HasIndex(c => c.Number)
.IsUnique();
@@ -15,6 +17,10 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
.WithOne(p => p.Channel)
.HasForeignKey(p => p.ChannelId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(c => c.Artwork)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
}
}
}
@@ -0,0 +1,27 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class CollectionConfiguration : IEntityTypeConfiguration<Collection>
{
public void Configure(EntityTypeBuilder<Collection> builder)
{
builder.ToTable("Collection");
builder.HasMany(c => c.MediaItems)
.WithMany(m => m.Collections)
.UsingEntity<CollectionItem>(
j => j.HasOne(ci => ci.MediaItem)
.WithMany(mi => mi.CollectionItems)
.HasForeignKey(ci => ci.MediaItemId)
.OnDelete(DeleteBehavior.Cascade),
j => j.HasOne(ci => ci.Collection)
.WithMany(c => c.CollectionItems)
.HasForeignKey(ci => ci.CollectionId)
.OnDelete(DeleteBehavior.Cascade),
j => j.HasKey(ci => new { ci.CollectionId, ci.MediaItemId }));
}
}
}
@@ -6,8 +6,12 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class ConfigElementConfiguration : IEntityTypeConfiguration<ConfigElement>
{
public void Configure(EntityTypeBuilder<ConfigElement> builder) =>
public void Configure(EntityTypeBuilder<ConfigElement> builder)
{
builder.ToTable("ConfigElement");
builder.HasIndex(ce => ce.Key)
.IsUnique();
}
}
}
@@ -0,0 +1,11 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class FFmpegProfileConfiguration : IEntityTypeConfiguration<FFmpegProfile>
{
public void Configure(EntityTypeBuilder<FFmpegProfile> builder) => builder.ToTable("FFmpegProfile");
}
}
@@ -0,0 +1,12 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class LibraryConfiguration : IEntityTypeConfiguration<Library>
{
public void Configure(EntityTypeBuilder<Library> builder) =>
builder.ToTable("Library");
}
}
@@ -0,0 +1,19 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class LibraryPathConfiguration : IEntityTypeConfiguration<LibraryPath>
{
public void Configure(EntityTypeBuilder<LibraryPath> builder)
{
builder.ToTable("LibraryPath");
builder.HasMany(p => p.MediaItems)
.WithOne(i => i.LibraryPath)
.HasForeignKey(i => i.LibraryPathId)
.OnDelete(DeleteBehavior.Cascade);
}
}
}
@@ -0,0 +1,12 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class LocalLibraryConfiguration : IEntityTypeConfiguration<LocalLibrary>
{
public void Configure(EntityTypeBuilder<LocalLibrary> builder) =>
builder.ToTable("LocalLibrary");
}
}
@@ -0,0 +1,12 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class PlexLibraryConfiguration : IEntityTypeConfiguration<PlexLibrary>
{
public void Configure(EntityTypeBuilder<PlexLibrary> builder) =>
builder.ToTable("PlexLibrary");
}
}
@@ -1,12 +0,0 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class MediaCollectionConfiguration : IEntityTypeConfiguration<MediaCollection>
{
public void Configure(EntityTypeBuilder<MediaCollection> builder) =>
builder.HasIndex(c => c.Name).IsUnique();
}
}
@@ -0,0 +1,23 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class EpisodeConfiguration : IEntityTypeConfiguration<Episode>
{
public void Configure(EntityTypeBuilder<Episode> builder)
{
builder.ToTable("Episode");
builder.HasMany(e => e.EpisodeMetadata)
.WithOne(m => m.Episode)
.HasForeignKey(m => m.EpisodeId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(e => e.MediaVersions)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
}
}
}
@@ -0,0 +1,17 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class MediaFileConfiguration : IEntityTypeConfiguration<MediaFile>
{
public void Configure(EntityTypeBuilder<MediaFile> builder)
{
builder.ToTable("MediaFile");
builder.HasIndex(f => f.Path)
.IsUnique();
}
}
}
@@ -6,10 +6,6 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class MediaItemConfiguration : IEntityTypeConfiguration<MediaItem>
{
public void Configure(EntityTypeBuilder<MediaItem> builder)
{
builder.ToTable("MediaItems");
builder.OwnsOne(c => c.Statistics).WithOwner();
}
public void Configure(EntityTypeBuilder<MediaItem> builder) => builder.ToTable("MediaItem");
}
}
@@ -0,0 +1,19 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class MediaVersionConfiguration : IEntityTypeConfiguration<MediaVersion>
{
public void Configure(EntityTypeBuilder<MediaVersion> builder)
{
builder.ToTable("MediaVersion");
builder.HasMany(v => v.MediaFiles)
.WithOne(f => f.MediaVersion)
.HasForeignKey(f => f.MediaVersionId)
.OnDelete(DeleteBehavior.Cascade);
}
}
}
@@ -0,0 +1,23 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class MovieConfiguration : IEntityTypeConfiguration<Movie>
{
public void Configure(EntityTypeBuilder<Movie> builder)
{
builder.ToTable("Movie");
builder.HasMany(m => m.MovieMetadata)
.WithOne(m => m.Movie)
.HasForeignKey(m => m.MovieId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(m => m.MediaVersions)
.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 PlexMovieMediaItemConfiguration : IEntityTypeConfiguration<PlexMovie>
{
public void Configure(EntityTypeBuilder<PlexMovie> builder) => builder.ToTable("PlexMovie");
}
}
@@ -0,0 +1,24 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class SeasonConfiguration : IEntityTypeConfiguration<Season>
{
public void Configure(EntityTypeBuilder<Season> builder)
{
builder.ToTable("Season");
builder.HasMany(s => s.Episodes)
.WithOne(e => e.Season)
.HasForeignKey(e => e.SeasonId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(s => s.SeasonMetadata)
.WithOne(s => s.Season)
.HasForeignKey(s => s.SeasonId)
.OnDelete(DeleteBehavior.Cascade);
}
}
}
@@ -0,0 +1,24 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class ShowConfiguration : IEntityTypeConfiguration<Show>
{
public void Configure(EntityTypeBuilder<Show> builder)
{
builder.ToTable("Show");
builder.HasMany(s => s.Seasons)
.WithOne(s => s.Show)
.HasForeignKey(s => s.ShowId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(s => s.ShowMetadata)
.WithOne(s => s.Show)
.HasForeignKey(s => s.ShowId)
.OnDelete(DeleteBehavior.Cascade);
}
}
}
@@ -7,6 +7,6 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
public class LocalMediaSourceConfiguration : IEntityTypeConfiguration<LocalMediaSource>
{
public void Configure(EntityTypeBuilder<LocalMediaSource> builder) =>
builder.ToTable("LocalMediaSources");
builder.ToTable("LocalMediaSource");
}
}
@@ -6,7 +6,14 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class MediaSourceConfiguration : IEntityTypeConfiguration<MediaSource>
{
public void Configure(EntityTypeBuilder<MediaSource> builder) =>
builder.HasIndex(ms => ms.Name).IsUnique();
public void Configure(EntityTypeBuilder<MediaSource> builder)
{
builder.ToTable("MediaSource");
builder.HasMany(s => s.Libraries)
.WithOne(l => l.MediaSource)
.HasForeignKey(l => l.MediaSourceId)
.OnDelete(DeleteBehavior.Cascade);
}
}
}
@@ -0,0 +1,12 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class PlexConnectionConfiguration : IEntityTypeConfiguration<PlexConnection>
{
public void Configure(EntityTypeBuilder<PlexConnection> builder) =>
builder.ToTable("PlexConnection");
}
}
@@ -6,6 +6,14 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class PlexMediaSourceConfiguration : IEntityTypeConfiguration<PlexMediaSource>
{
public void Configure(EntityTypeBuilder<PlexMediaSource> builder) => builder.ToTable("PlexMediaSources");
public void Configure(EntityTypeBuilder<PlexMediaSource> builder)
{
builder.ToTable("PlexMediaSource");
builder.HasMany(s => s.Connections)
.WithOne(c => c.PlexMediaSource)
.HasForeignKey(c => c.PlexMediaSourceId)
.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 ArtworkConfiguration : IEntityTypeConfiguration<Artwork>
{
public void Configure(EntityTypeBuilder<Artwork> builder) => builder.ToTable("Artwork");
}
}
@@ -0,0 +1,18 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class EpisodeMetadataConfiguration : IEntityTypeConfiguration<EpisodeMetadata>
{
public void Configure(EntityTypeBuilder<EpisodeMetadata> builder)
{
builder.ToTable("EpisodeMetadata");
builder.HasMany(em => em.Artwork)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
}
}
}
@@ -6,7 +6,13 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class MovieMetadataConfiguration : IEntityTypeConfiguration<MovieMetadata>
{
public void Configure(EntityTypeBuilder<MovieMetadata> builder) =>
public void Configure(EntityTypeBuilder<MovieMetadata> builder)
{
builder.ToTable("MovieMetadata");
builder.HasMany(mm => mm.Artwork)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
}
}
}
@@ -0,0 +1,18 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class SeasonMetadataConfiguration : IEntityTypeConfiguration<SeasonMetadata>
{
public void Configure(EntityTypeBuilder<SeasonMetadata> builder)
{
builder.ToTable("SeasonMetadata");
builder.HasMany(em => em.Artwork)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
}
}
}
@@ -0,0 +1,18 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class ShowMetadataConfiguration : IEntityTypeConfiguration<ShowMetadata>
{
public void Configure(EntityTypeBuilder<ShowMetadata> builder)
{
builder.ToTable("ShowMetadata");
builder.HasMany(sm => sm.Artwork)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
}
}
}
@@ -1,19 +0,0 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class MovieMediaItemConfiguration : IEntityTypeConfiguration<MovieMediaItem>
{
public void Configure(EntityTypeBuilder<MovieMediaItem> builder)
{
builder.ToTable("Movies");
builder.HasOne(i => i.Metadata)
.WithOne(m => m.Movie)
.HasForeignKey<MovieMetadata>(m => m.MovieId)
.OnDelete(DeleteBehavior.Cascade);
}
}
}
@@ -8,6 +8,8 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
{
public void Configure(EntityTypeBuilder<Playout> builder)
{
builder.ToTable("Playout");
builder.HasMany(p => p.Items)
.WithOne(pi => pi.Playout)
.HasForeignKey(pi => pi.PlayoutId)
@@ -0,0 +1,20 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class PlayoutItemConfiguration : IEntityTypeConfiguration<PlayoutItem>
{
public void Configure(EntityTypeBuilder<PlayoutItem> builder)
{
builder.ToTable("PlayoutItem");
builder.HasOne(pi => pi.MediaItem)
.WithMany()
.HasForeignKey(pi => pi.MediaItemId)
.OnDelete(DeleteBehavior.Cascade);
}
}
}
@@ -6,8 +6,24 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class PlayoutProgramScheduleAnchorConfiguration : IEntityTypeConfiguration<PlayoutProgramScheduleAnchor>
{
public void Configure(EntityTypeBuilder<PlayoutProgramScheduleAnchor> builder) =>
public void Configure(EntityTypeBuilder<PlayoutProgramScheduleAnchor> builder)
{
builder.ToTable("PlayoutProgramScheduleAnchor");
builder.OwnsOne(a => a.EnumeratorState)
.WithOwner();
builder.HasOne(i => i.Collection)
.WithMany()
.HasForeignKey(i => i.CollectionId)
.OnDelete(DeleteBehavior.Cascade)
.IsRequired(false);
builder.HasOne(i => i.MediaItem)
.WithMany()
.HasForeignKey(i => i.MediaItemId)
.OnDelete(DeleteBehavior.Cascade)
.IsRequired(false);
}
}
}
@@ -1,12 +0,0 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class PlexMediaSourceConnectionConfiguration : IEntityTypeConfiguration<PlexMediaSourceConnection>
{
public void Configure(EntityTypeBuilder<PlexMediaSourceConnection> builder) =>
builder.ToTable("PlexMediaSourceConnections");
}
}
@@ -1,12 +0,0 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class PlexMediaSourceLibraryConfiguration : IEntityTypeConfiguration<PlexMediaSourceLibrary>
{
public void Configure(EntityTypeBuilder<PlexMediaSourceLibrary> builder) =>
builder.ToTable("PlexMediaSourceLibraries");
}
}
@@ -8,6 +8,8 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
{
public void Configure(EntityTypeBuilder<ProgramSchedule> builder)
{
builder.ToTable("ProgramSchedule");
builder.HasIndex(ps => ps.Name)
.IsUnique();
@@ -18,7 +20,8 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
builder.HasMany(ps => ps.Playouts)
.WithOne(p => p.ProgramSchedule)
.HasForeignKey(p => p.ProgramScheduleId);
.HasForeignKey(p => p.ProgramScheduleId)
.OnDelete(DeleteBehavior.Cascade);
}
}
}
@@ -8,21 +8,18 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
{
public void Configure(EntityTypeBuilder<ProgramScheduleItem> builder)
{
builder.ToTable("ProgramScheduleItems");
builder.ToTable("ProgramScheduleItem");
builder.HasOne(i => i.MediaCollection)
builder.HasOne(i => i.Collection)
.WithMany()
.HasForeignKey(i => i.MediaCollectionId)
.HasForeignKey(i => i.CollectionId)
.OnDelete(DeleteBehavior.Cascade)
.IsRequired(false);
builder.HasOne(i => i.TelevisionShow)
builder.HasOne(i => i.MediaItem)
.WithMany()
.HasForeignKey(i => i.TelevisionShowId)
.IsRequired(false);
builder.HasOne(i => i.TelevisionSeason)
.WithMany()
.HasForeignKey(i => i.TelevisionSeasonId)
.HasForeignKey(i => i.MediaItemId)
.OnDelete(DeleteBehavior.Cascade)
.IsRequired(false);
}
}
@@ -7,6 +7,6 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
public class ProgramScheduleItemDurationConfiguration : IEntityTypeConfiguration<ProgramScheduleItemDuration>
{
public void Configure(EntityTypeBuilder<ProgramScheduleItemDuration> builder) =>
builder.ToTable("ProgramScheduleDurationItems");
builder.ToTable("ProgramScheduleDurationItem");
}
}
@@ -7,6 +7,6 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
public class ProgramScheduleItemFloodConfiguration : IEntityTypeConfiguration<ProgramScheduleItemFlood>
{
public void Configure(EntityTypeBuilder<ProgramScheduleItemFlood> builder) =>
builder.ToTable("ProgramScheduleFloodItems");
builder.ToTable("ProgramScheduleFloodItem");
}
}
@@ -7,6 +7,6 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
public class ProgramScheduleItemMultipleConfiguration : IEntityTypeConfiguration<ProgramScheduleItemMultiple>
{
public void Configure(EntityTypeBuilder<ProgramScheduleItemMultiple> builder) =>
builder.ToTable("ProgramScheduleMultipleItems");
builder.ToTable("ProgramScheduleMultipleItem");
}
}
@@ -7,6 +7,6 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
public class ProgramScheduleItemOneConfiguration : IEntityTypeConfiguration<ProgramScheduleItemOne>
{
public void Configure(EntityTypeBuilder<ProgramScheduleItemOne> builder) =>
builder.ToTable("ProgramScheduleOneItems");
builder.ToTable("ProgramScheduleOneItem");
}
}
@@ -0,0 +1,11 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class ResolutionConfiguration : IEntityTypeConfiguration<Resolution>
{
public void Configure(EntityTypeBuilder<Resolution> builder) => builder.ToTable("Resolution");
}
}
@@ -1,30 +0,0 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class SimpleMediaCollectionConfiguration : IEntityTypeConfiguration<SimpleMediaCollection>
{
public void Configure(EntityTypeBuilder<SimpleMediaCollection> builder)
{
builder.ToTable("SimpleMediaCollections");
builder.HasMany(c => c.Movies)
.WithMany(m => m.SimpleMediaCollections)
.UsingEntity(join => join.ToTable("SimpleMediaCollectionMovies"));
builder.HasMany(c => c.TelevisionShows)
.WithMany(s => s.SimpleMediaCollections)
.UsingEntity(join => join.ToTable("SimpleMediaCollectionShows"));
builder.HasMany(c => c.TelevisionSeasons)
.WithMany(s => s.SimpleMediaCollections)
.UsingEntity(join => join.ToTable("SimpleMediaCollectionSeasons"));
builder.HasMany(c => c.TelevisionEpisodes)
.WithMany(e => e.SimpleMediaCollections)
.UsingEntity(join => join.ToTable("SimpleMediaCollectionEpisodes"));
}
}
}
@@ -1,19 +0,0 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class TelevisionEpisodeMediaItemConfiguration : IEntityTypeConfiguration<TelevisionEpisodeMediaItem>
{
public void Configure(EntityTypeBuilder<TelevisionEpisodeMediaItem> builder)
{
builder.ToTable("TelevisionEpisodes");
builder.HasOne(i => i.Metadata)
.WithOne(m => m.TelevisionEpisode)
.HasForeignKey<TelevisionEpisodeMetadata>(m => m.TelevisionEpisodeId)
.OnDelete(DeleteBehavior.Cascade);
}
}
}
@@ -1,12 +0,0 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class TelevisionEpisodeMetadataConfiguration : IEntityTypeConfiguration<TelevisionEpisodeMetadata>
{
public void Configure(EntityTypeBuilder<TelevisionEpisodeMetadata> builder) =>
builder.ToTable("TelevisionEpisodeMetadata");
}
}
@@ -1,19 +0,0 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class TelevisionSeasonConfiguration : IEntityTypeConfiguration<TelevisionSeason>
{
public void Configure(EntityTypeBuilder<TelevisionSeason> builder)
{
builder.ToTable("TelevisionSeasons");
builder.HasMany(season => season.Episodes)
.WithOne(episode => episode.Season)
.HasForeignKey(episode => episode.SeasonId)
.OnDelete(DeleteBehavior.Cascade);
}
}
}
@@ -1,27 +0,0 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class TelevisionShowConfiguration : IEntityTypeConfiguration<TelevisionShow>
{
public void Configure(EntityTypeBuilder<TelevisionShow> builder)
{
builder.ToTable("TelevisionShows");
builder.HasOne(show => show.Metadata)
.WithOne(metadata => metadata.TelevisionShow)
.HasForeignKey<TelevisionShowMetadata>(metadata => metadata.TelevisionShowId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(show => show.Seasons)
.WithOne(season => season.TelevisionShow);
builder.HasMany(show => show.Sources)
.WithOne(source => source.TelevisionShow)
.HasForeignKey(source => source.TelevisionShowId)
.OnDelete(DeleteBehavior.Cascade);
}
}
}
@@ -1,12 +0,0 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class TelevisionShowMetadataConfiguration : IEntityTypeConfiguration<TelevisionShowMetadata>
{
public void Configure(EntityTypeBuilder<TelevisionShowMetadata> builder) =>
builder.ToTable("TelevisionShowMetadata");
}
}
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
@@ -22,27 +23,39 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
}
public Task<Option<Channel>> Get(int id) =>
_dbContext.Channels.SingleOrDefaultAsync(c => c.Id == id).Map(Optional);
_dbContext.Channels
.Include(c => c.Artwork)
.OrderBy(c => c.Id)
.SingleOrDefaultAsync(c => c.Id == id)
.Map(Optional);
public Task<Option<Channel>> GetByNumber(int number) =>
_dbContext.Channels
.Include(c => c.FFmpegProfile)
.ThenInclude(p => p.Resolution)
.OrderBy(c => c.Number)
.SingleOrDefaultAsync(c => c.Number == number)
.Map(Optional);
public Task<List<Channel>> GetAll() => _dbContext.Channels.ToListAsync();
public Task<List<Channel>> GetAll() =>
_dbContext.Channels
.Include(c => c.Artwork)
.ToListAsync();
public Task<List<Channel>> GetAllForGuide() =>
_dbContext.Channels
.Include(c => c.Playouts)
.ThenInclude(p => p.Items)
.ThenInclude(i => i.MediaItem)
.ThenInclude(i => (i as TelevisionEpisodeMediaItem).Metadata)
.ThenInclude(i => (i as Episode).EpisodeMetadata)
.Include(c => c.Playouts)
.ThenInclude(p => p.Items)
.ThenInclude(i => i.MediaItem)
.ThenInclude(i => (i as MovieMediaItem).Metadata)
.ThenInclude(i => (i as Episode).Season)
.Include(c => c.Playouts)
.ThenInclude(p => p.Items)
.ThenInclude(i => i.MediaItem)
.ThenInclude(i => (i as Movie).MovieMetadata)
.ToListAsync();
public async Task Update(Channel channel)
@@ -1,9 +1,11 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude;
namespace ErsatzTV.Infrastructure.Data.Repositories
{
@@ -20,8 +22,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
return configElement;
}
public async Task<Option<ConfigElement>> Get(ConfigElementKey key) =>
await _dbContext.ConfigElements.SingleOrDefaultAsync(ce => ce.Key == key.Key);
public Task<Option<ConfigElement>> Get(ConfigElementKey key) =>
_dbContext.ConfigElements
.OrderBy(ce => ce.Key)
.SingleOrDefaultAsync(ce => ce.Key == key.Key)
.Map(Optional);
public Task<Option<T>> GetValue<T>(ConfigElementKey key) =>
Get(key).MapT(ce => (T) Convert.ChangeType(ce.Value, typeof(T)));
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
@@ -23,6 +24,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public async Task<Option<FFmpegProfile>> Get(int id) =>
await _dbContext.FFmpegProfiles
.Include(p => p.Resolution)
.OrderBy(p => p.Id)
.SingleOrDefaultAsync(p => p.Id == id);
public Task<List<FFmpegProfile>> GetAll() =>
@@ -0,0 +1,95 @@
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using Dapper;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude;
namespace ErsatzTV.Infrastructure.Data.Repositories
{
public class LibraryRepository : ILibraryRepository
{
private readonly IDbConnection _dbConnection;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public LibraryRepository(IDbContextFactory<TvContext> dbContextFactory, IDbConnection dbConnection)
{
_dbContextFactory = dbContextFactory;
_dbConnection = dbConnection;
}
public async Task<LibraryPath> Add(LibraryPath libraryPath)
{
await using TvContext context = _dbContextFactory.CreateDbContext();
await context.LibraryPaths.AddAsync(libraryPath);
await context.SaveChangesAsync();
return libraryPath;
}
public Task<Option<Library>> Get(int libraryId)
{
using TvContext context = _dbContextFactory.CreateDbContext();
return context.Libraries
.Include(l => l.Paths)
.OrderBy(l => l.Id)
.SingleOrDefaultAsync(l => l.Id == libraryId)
.Map(Optional);
}
public Task<Option<LocalLibrary>> GetLocal(int libraryId)
{
using TvContext context = _dbContextFactory.CreateDbContext();
return context.LocalLibraries
.OrderBy(l => l.Id)
.SingleOrDefaultAsync(l => l.Id == libraryId)
.Map(Optional);
}
public Task<List<LocalLibrary>> GetAllLocal()
{
using TvContext context = _dbContextFactory.CreateDbContext();
return context.LocalLibraries.ToListAsync();
}
public Task<Unit> UpdateLastScan(Library library) => _dbConnection.ExecuteAsync(
"UPDATE Library SET LastScan = @LastScan WHERE Id = @Id",
new { library.LastScan, library.Id }).ToUnit();
public Task<List<LibraryPath>> GetLocalPaths(int libraryId)
{
using TvContext context = _dbContextFactory.CreateDbContext();
return context.LocalLibraries
.Include(l => l.Paths)
.OrderBy(l => l.Id)
.SingleOrDefaultAsync(l => l.Id == libraryId)
.Map(Optional)
.Match(l => l.Paths, () => new List<LibraryPath>());
}
public Task<Option<LibraryPath>> GetPath(int libraryPathId)
{
using TvContext context = _dbContextFactory.CreateDbContext();
return context.LibraryPaths
.OrderBy(lp => lp.Id)
.SingleOrDefaultAsync(lp => lp.Id == libraryPathId)
.Map(Optional);
}
public Task<int> CountMediaItemsByPath(int libraryPathId) =>
_dbConnection.QuerySingleAsync<int>(
@"SELECT COUNT(*) FROM MediaItem WHERE LibraryPathId = @LibraryPathId",
new { LibraryPathId = libraryPathId });
public async Task DeleteLocalPath(int libraryPathId)
{
await using TvContext context = _dbContextFactory.CreateDbContext();
LibraryPath libraryPath = await context.LibraryPaths.FindAsync(libraryPathId);
context.LibraryPaths.Remove(libraryPath);
await context.SaveChangesAsync();
}
}
}
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
@@ -23,174 +22,191 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
_dbConnection = dbConnection;
}
public async Task<SimpleMediaCollection> Add(SimpleMediaCollection collection)
public async Task<Collection> Add(Collection collection)
{
await _dbContext.SimpleMediaCollections.AddAsync(collection);
await _dbContext.Collections.AddAsync(collection);
await _dbContext.SaveChangesAsync();
return collection;
}
public Task<Option<MediaCollection>> Get(int id) =>
_dbContext.MediaCollections.SingleOrDefaultAsync(c => c.Id == id).Map(Optional);
public Task<Option<SimpleMediaCollection>> GetSimpleMediaCollection(int id) =>
_dbContext.SimpleMediaCollections
public async Task<Unit> AddMediaItem(int collectionId, int mediaItemId)
{
Option<Collection> maybeCollection = await _dbContext.Collections
.Include(c => c.MediaItems)
.OrderBy(c => c.Id)
.SingleOrDefaultAsync(c => c.Id == collectionId)
.Map(Optional);
await maybeCollection.IfSomeAsync(
async collection =>
{
if (collection.MediaItems.All(i => i.Id != mediaItemId))
{
Option<MediaItem> maybeMediaItem = await _dbContext.MediaItems
.OrderBy(i => i.Id)
.SingleOrDefaultAsync(i => i.Id == mediaItemId)
.Map(Optional);
await maybeMediaItem.IfSomeAsync(
async mediaItem =>
{
collection.MediaItems.Add(mediaItem);
await _dbContext.SaveChangesAsync();
});
}
});
return Unit.Default;
}
public Task<Option<Collection>> Get(int id) =>
_dbContext.Collections
.OrderBy(c => c.Id)
.SingleOrDefaultAsync(c => c.Id == id)
.Map(Optional);
public Task<Option<SimpleMediaCollection>> GetSimpleMediaCollectionWithItems(int id) =>
_dbContext.SimpleMediaCollections
.Include(s => s.Movies)
.ThenInclude(m => m.Source)
.Include(s => s.TelevisionShows)
.ThenInclude(s => s.Metadata)
.Include(s => s.TelevisionSeasons)
.Include(s => s.TelevisionEpisodes)
.ThenInclude(s => s.Metadata)
public Task<Option<Collection>> GetCollectionWithItems(int id) =>
_dbContext.Collections
.Include(c => c.MediaItems)
.ThenInclude(i => i.LibraryPath)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Movie).MovieMetadata)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Show).ShowMetadata)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Season).Show)
.ThenInclude(s => s.ShowMetadata)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Episode).EpisodeMetadata)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Episode).Season)
.ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata)
.OrderBy(c => c.Id)
.SingleOrDefaultAsync(c => c.Id == id)
.Map(Optional);
public Task<Option<SimpleMediaCollection>> GetSimpleMediaCollectionWithItemsUntracked(int id) =>
_dbContext.SimpleMediaCollections
public Task<Option<Collection>> GetCollectionWithItemsUntracked(int id) =>
_dbContext.Collections
.AsNoTracking()
.Include(s => s.Movies)
.ThenInclude(i => i.Source)
.Include(s => s.Movies)
.ThenInclude(m => m.Metadata)
.Include(s => s.TelevisionShows)
.ThenInclude(s => s.Metadata)
.Include(s => s.TelevisionSeasons)
.ThenInclude(s => s.TelevisionShow)
.ThenInclude(s => s.Metadata)
.Include(s => s.TelevisionEpisodes)
.ThenInclude(s => s.Metadata)
.Include(s => s.TelevisionEpisodes)
.ThenInclude(e => e.Season)
.ThenInclude(s => s.TelevisionShow)
.ThenInclude(s => s.Metadata)
.Include(c => c.MediaItems)
.ThenInclude(i => i.LibraryPath)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Movie).MovieMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Show).ShowMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Season).SeasonMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Season).Show)
.ThenInclude(s => s.ShowMetadata)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Episode).EpisodeMetadata)
.ThenInclude(em => em.Artwork)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Episode).Season)
.ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata)
.OrderBy(c => c.Id)
.SingleOrDefaultAsync(c => c.Id == id)
.Map(Optional);
public Task<List<SimpleMediaCollection>> GetSimpleMediaCollections() =>
_dbContext.SimpleMediaCollections.ToListAsync();
public Task<List<MediaCollection>> GetAll() =>
_dbContext.MediaCollections.ToListAsync();
public Task<List<Collection>> GetAll() =>
_dbContext.Collections.ToListAsync();
public Task<Option<List<MediaItem>>> GetItems(int id) =>
Get(id).MapT(
collection => collection switch
{
SimpleMediaCollection s => SimpleItems(s),
_ => throw new NotSupportedException($"Unsupported collection type {collection.GetType().Name}")
}).Bind(x => x.Sequence());
Get(id).MapT(GetItemsForCollection).Bind(x => x.Sequence());
public Task<Option<List<MediaItem>>> GetSimpleMediaCollectionItems(int id) =>
GetSimpleMediaCollection(id).MapT(SimpleItems).Bind(x => x.Sequence());
public Task Update(SimpleMediaCollection collection)
public Task Update(Collection collection)
{
_dbContext.SimpleMediaCollections.Update(collection);
_dbContext.Collections.Update(collection);
return _dbContext.SaveChangesAsync();
}
public async Task Delete(int mediaCollectionId)
public async Task Delete(int collectionId)
{
MediaCollection mediaCollection = await _dbContext.MediaCollections.FindAsync(mediaCollectionId);
_dbContext.MediaCollections.Remove(mediaCollection);
Collection mediaCollection = await _dbContext.Collections.FindAsync(collectionId);
_dbContext.Collections.Remove(mediaCollection);
await _dbContext.SaveChangesAsync();
}
private async Task<List<MediaItem>> SimpleItems(SimpleMediaCollection collection)
private async Task<List<MediaItem>> GetItemsForCollection(Collection collection)
{
var result = new List<MediaItem>();
await _dbContext.Entry(collection).Collection(c => c.Movies).LoadAsync();
result.AddRange(collection.Movies);
result.AddRange(await GetTelevisionShowItems(collection));
result.AddRange(await GetTelevisionSeasonItems(collection));
result.AddRange(await GetTelevisionEpisodeItems(collection));
result.AddRange(await GetMovieItems(collection));
result.AddRange(await GetShowItems(collection));
result.AddRange(await GetSeasonItems(collection));
result.AddRange(await GetEpisodeItems(collection));
return result.Distinct().ToList();
}
private async Task<List<TelevisionEpisodeMediaItem>> GetTelevisionShowItems(SimpleMediaCollection collection)
private async Task<List<Movie>> GetMovieItems(Collection collection)
{
var parameters = new { CollectionId = collection.Id };
return await _dbConnection
.QueryAsync<TelevisionEpisodeMediaItem, MediaItemStatistics, TelevisionEpisodeMetadata,
TelevisionEpisodeMediaItem>(
@"select tmi.Id, tmi.SeasonId, mi.MediaSourceId, mi.LastWriteTime, mi.Path, mi.Poster, mi.PosterLastWriteTime,
mi.Statistics_AudioCodec as AudioCodec, mi.Statistics_DisplayAspectRatio as DisplayAspectRatio, mi.Statistics_Duration as Duration, mi.Statistics_Height as Height, mi.Statistics_LastWriteTime as LastWriteTime, mi.Statistics_SampleAspectRatio as SampleAspectRatio,
mi.Statistics_VideoCodec as VideoCodec, mi.Statistics_VideoScanType as VideoScanType, mi.Statistics_Width as Width,
tem.TelevisionEpisodeId, tem.Id, tem.Season, tem.Episode, tem.Plot, tem.Aired, tem.Source, tem.LastWriteTime, tem.Title, tem.SortTitle
from TelevisionEpisodes tmi
inner join MediaItems mi on tmi.Id = mi.Id
inner join TelevisionEpisodeMetadata tem on tem.TelevisionEpisodeId = tmi.Id
inner join TelevisionSeasons tsn on tsn.Id = tmi.SeasonId
inner join TelevisionShows ts on ts.Id = tsn.TelevisionShowId
inner join SimpleMediaCollectionShows s on s.TelevisionShowsId = ts.Id
where s.SimpleMediaCollectionsId = @CollectionId",
(episode, statistics, metadata) =>
{
episode.Statistics = statistics;
episode.Metadata = metadata;
return episode;
},
parameters,
splitOn: "AudioCodec,TelevisionEpisodeId").Map(result => result.ToList());
IEnumerable<int> ids = await _dbConnection.QueryAsync<int>(
@"SELECT m.Id FROM CollectionItem ci
INNER JOIN Movie m ON m.Id = ci.MediaItemId
WHERE ci.CollectionId = @CollectionId",
new { CollectionId = collection.Id });
return await _dbContext.Movies
.Include(m => m.MovieMetadata)
.Include(m => m.MediaVersions)
.Filter(m => ids.Contains(m.Id))
.ToListAsync();
}
private async Task<List<TelevisionEpisodeMediaItem>> GetTelevisionSeasonItems(SimpleMediaCollection collection)
private async Task<List<Episode>> GetShowItems(Collection collection)
{
var parameters = new { CollectionId = collection.Id };
return await _dbConnection
.QueryAsync<TelevisionEpisodeMediaItem, MediaItemStatistics, TelevisionEpisodeMetadata,
TelevisionEpisodeMediaItem>(
@"select tmi.Id, tmi.SeasonId, mi.MediaSourceId, mi.LastWriteTime, mi.Path, mi.Poster, mi.PosterLastWriteTime,
mi.Statistics_AudioCodec as AudioCodec, mi.Statistics_DisplayAspectRatio as DisplayAspectRatio, mi.Statistics_Duration as Duration, mi.Statistics_Height as Height, mi.Statistics_LastWriteTime as LastWriteTime, mi.Statistics_SampleAspectRatio as SampleAspectRatio,
mi.Statistics_VideoCodec as VideoCodec, mi.Statistics_VideoScanType as VideoScanType, mi.Statistics_Width as Width,
tem.TelevisionEpisodeId, tem.Id, tem.Season, tem.Episode, tem.Plot, tem.Aired, tem.Source, tem.LastWriteTime, tem.Title, tem.SortTitle
from TelevisionEpisodes tmi
inner join MediaItems mi on tmi.Id = mi.Id
inner join TelevisionEpisodeMetadata tem on tem.TelevisionEpisodeId = tmi.Id
inner join TelevisionSeasons tsn on tsn.Id = tmi.SeasonId
inner join SimpleMediaCollectionSeasons s on s.TelevisionSeasonsId = tsn.Id
where s.SimpleMediaCollectionsId = @CollectionId",
(episode, statistics, metadata) =>
{
episode.Statistics = statistics;
episode.Metadata = metadata;
return episode;
},
parameters,
splitOn: "AudioCodec,TelevisionEpisodeId").Map(result => result.ToList());
IEnumerable<int> ids = await _dbConnection.QueryAsync<int>(
@"SELECT Episode.Id FROM CollectionItem ci
INNER JOIN Show ON Show.Id = ci.MediaItemId
INNER JOIN Season ON Season.ShowId = Show.Id
INNER JOIN Episode ON Episode.SeasonId = Season.Id
WHERE ci.CollectionId = @CollectionId",
new { CollectionId = collection.Id });
return await _dbContext.Episodes
.Include(e => e.EpisodeMetadata)
.Include(e => e.MediaVersions)
.Filter(e => ids.Contains(e.Id))
.ToListAsync();
}
private async Task<List<TelevisionEpisodeMediaItem>> GetTelevisionEpisodeItems(SimpleMediaCollection collection)
private async Task<List<Episode>> GetSeasonItems(Collection collection)
{
var parameters = new { CollectionId = collection.Id };
return await _dbConnection
.QueryAsync<TelevisionEpisodeMediaItem, MediaItemStatistics, TelevisionEpisodeMetadata,
TelevisionEpisodeMediaItem>(
@"select tmi.Id, tmi.SeasonId, mi.MediaSourceId, mi.LastWriteTime, mi.Path, mi.Poster, mi.PosterLastWriteTime,
mi.Statistics_AudioCodec as AudioCodec, mi.Statistics_DisplayAspectRatio as DisplayAspectRatio, mi.Statistics_Duration as Duration, mi.Statistics_Height as Height, mi.Statistics_LastWriteTime as LastWriteTime, mi.Statistics_SampleAspectRatio as SampleAspectRatio,
mi.Statistics_VideoCodec as VideoCodec, mi.Statistics_VideoScanType as VideoScanType, mi.Statistics_Width as Width,
tem.TelevisionEpisodeId, tem.Id, tem.Season, tem.Episode, tem.Plot, tem.Aired, tem.Source, tem.LastWriteTime, tem.Title, tem.SortTitle
from TelevisionEpisodes tmi
inner join MediaItems mi on tmi.Id = mi.Id
inner join TelevisionEpisodeMetadata tem on tem.TelevisionEpisodeId = tmi.Id
inner join SimpleMediaCollectionEpisodes s on s.TelevisionEpisodesId = tmi.Id
where s.SimpleMediaCollectionsId = @CollectionId",
(episode, statistics, metadata) =>
{
episode.Statistics = statistics;
episode.Metadata = metadata;
return episode;
},
parameters,
splitOn: "AudioCodec,TelevisionEpisodeId").Map(result => result.ToList());
IEnumerable<int> ids = await _dbConnection.QueryAsync<int>(
@"SELECT Episode.Id FROM CollectionItem ci
INNER JOIN Season ON Season.Id = ci.MediaItemId
INNER JOIN Episode ON Episode.SeasonId = Season.Id
WHERE ci.CollectionId = @CollectionId",
new { CollectionId = collection.Id });
return await _dbContext.Episodes
.Include(e => e.EpisodeMetadata)
.Include(e => e.MediaVersions)
.Filter(e => ids.Contains(e.Id))
.ToListAsync();
}
private async Task<List<Episode>> GetEpisodeItems(Collection collection)
{
IEnumerable<int> ids = await _dbConnection.QueryAsync<int>(
@"SELECT Episode.Id FROM CollectionItem ci
INNER JOIN Episode ON Episode.Id = ci.MediaItemId
WHERE ci.CollectionId = @CollectionId",
new { CollectionId = collection.Id });
return await _dbContext.Episodes
.Include(e => e.EpisodeMetadata)
.Include(e => e.MediaVersions)
.Filter(e => ids.Contains(e.Id))
.ToListAsync();
}
}
}
@@ -17,32 +17,33 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public Task<Option<MediaItem>> Get(int id) =>
_dbContext.MediaItems
.Include(i => i.Source)
.Include(i => i.LibraryPath)
.OrderBy(i => i.Id)
.SingleOrDefaultAsync(i => i.Id == id)
.Map(Optional);
public Task<List<MediaItem>> GetAll() => _dbContext.MediaItems.ToListAsync();
public Task<List<MediaItem>> Search(string searchString)
{
IQueryable<TelevisionEpisodeMediaItem> episodeData =
from c in _dbContext.TelevisionEpisodeMediaItems.Include(c => c.Source) select c;
if (!string.IsNullOrEmpty(searchString))
{
episodeData = episodeData.Where(c => EF.Functions.Like(c.Metadata.Title, $"%{searchString}%"));
}
IQueryable<MovieMediaItem> movieData =
from c in _dbContext.MovieMediaItems.Include(c => c.Source) select c;
if (!string.IsNullOrEmpty(searchString))
{
movieData = movieData.Where(c => EF.Functions.Like(c.Metadata.Title, $"%{searchString}%"));
}
return episodeData.OfType<MediaItem>().Concat(movieData.OfType<MediaItem>()).ToListAsync();
}
public Task<List<MediaItem>> Search(string searchString) =>
// TODO: fix this when we need to search
// IQueryable<TelevisionEpisodeMediaItem> episodeData =
// from c in _dbContext.TelevisionEpisodeMediaItems.Include(c => c.LibraryPath) select c;
//
// if (!string.IsNullOrEmpty(searchString))
// {
// episodeData = episodeData.Where(c => EF.Functions.Like(c.Metadata.Title, $"%{searchString}%"));
// }
//
// IQueryable<Movie> movieData =
// from c in _dbContext.Movies.Include(c => c.LibraryPath) select c;
//
// // if (!string.IsNullOrEmpty(searchString))
// // {
// // movieData = movieData.Where(c => EF.Functions.Like(c.Metadata.Title, $"%{searchString}%"));
// // }
//
// return episodeData.OfType<MediaItem>().Concat(movieData.OfType<MediaItem>()).ToListAsync();
new List<MediaItem>().AsTask();
public async Task<bool> Update(MediaItem mediaItem)
{
@@ -1,6 +1,8 @@
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using Dapper;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
@@ -11,70 +13,134 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
{
public class MediaSourceRepository : IMediaSourceRepository
{
private readonly TvContext _dbContext;
private readonly IDbConnection _dbConnection;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public MediaSourceRepository(TvContext dbContext) => _dbContext = dbContext;
public MediaSourceRepository(
IDbContextFactory<TvContext> dbContextFactory,
IDbConnection dbConnection)
{
_dbContextFactory = dbContextFactory;
_dbConnection = dbConnection;
}
public async Task<LocalMediaSource> Add(LocalMediaSource localMediaSource)
{
await _dbContext.LocalMediaSources.AddAsync(localMediaSource);
await _dbContext.SaveChangesAsync();
await using TvContext context = _dbContextFactory.CreateDbContext();
await context.LocalMediaSources.AddAsync(localMediaSource);
await context.SaveChangesAsync();
return localMediaSource;
}
public async Task<PlexMediaSource> Add(PlexMediaSource plexMediaSource)
{
await _dbContext.PlexMediaSources.AddAsync(plexMediaSource);
await _dbContext.SaveChangesAsync();
await using TvContext context = _dbContextFactory.CreateDbContext();
await context.PlexMediaSources.AddAsync(plexMediaSource);
await context.SaveChangesAsync();
return plexMediaSource;
}
public async Task<List<MediaSource>> GetAll()
{
List<MediaSource> all = await _dbContext.MediaSources.ToListAsync();
await using TvContext context = _dbContextFactory.CreateDbContext();
List<MediaSource> all = await context.MediaSources.ToListAsync();
foreach (PlexMediaSource plex in all.OfType<PlexMediaSource>())
{
await _dbContext.Entry(plex).Collection(p => p.Connections).LoadAsync();
await context.Entry(plex).Collection(p => p.Connections).LoadAsync();
}
return all;
}
public Task<List<PlexMediaSource>> GetAllPlex() =>
_dbContext.PlexMediaSources
public Task<List<PlexMediaSource>> GetAllPlex()
{
using TvContext context = _dbContextFactory.CreateDbContext();
return context.PlexMediaSources
.Include(p => p.Connections)
.ToListAsync();
}
public Task<Option<MediaSource>> Get(int id) =>
_dbContext.MediaSources
public Task<List<PlexLibrary>> GetPlexLibraries(int plexMediaSourceId)
{
using TvContext context = _dbContextFactory.CreateDbContext();
return context.PlexLibraries
.Filter(l => l.MediaSourceId == plexMediaSourceId)
.ToListAsync();
}
public Task<Option<PlexLibrary>> GetPlexLibrary(int plexLibraryId)
{
using TvContext context = _dbContextFactory.CreateDbContext();
return context.PlexLibraries
.OrderBy(l => l.Id) // https://github.com/dotnet/efcore/issues/22579
.SingleOrDefaultAsync(l => l.Id == plexLibraryId)
.Map(Optional);
}
public Task<Option<MediaSource>> Get(int id)
{
using TvContext context = _dbContextFactory.CreateDbContext();
return context.MediaSources
.OrderBy(s => s.Id) // https://github.com/dotnet/efcore/issues/22579
.SingleOrDefaultAsync(s => s.Id == id)
.Map(Optional);
}
public Task<Option<PlexMediaSource>> GetPlex(int id) =>
_dbContext.PlexMediaSources
public Task<Option<PlexMediaSource>> GetPlex(int id)
{
using TvContext context = _dbContextFactory.CreateDbContext();
return context.PlexMediaSources
.Include(p => p.Connections)
.Include(p => p.Libraries)
.OrderBy(s => s.Id) // https://github.com/dotnet/efcore/issues/22579
.SingleOrDefaultAsync(p => p.Id == id)
.Map(Optional);
}
public Task<int> CountMediaItems(int id) =>
_dbContext.MediaItems.CountAsync(i => i.MediaSourceId == id);
public Task<int> CountMediaItems(int id)
{
using TvContext context = _dbContextFactory.CreateDbContext();
return context.MediaItems
.CountAsync(i => i.LibraryPath.Library.MediaSourceId == id);
}
public Task Update(LocalMediaSource localMediaSource) =>
_dbContext.LocalMediaSources.Update(localMediaSource)
.AsTask()
.Bind(_ => _dbContext.SaveChangesAsync());
public async Task Update(LocalMediaSource localMediaSource)
{
await using TvContext context = _dbContextFactory.CreateDbContext();
context.LocalMediaSources.Update(localMediaSource);
await context.SaveChangesAsync();
}
public Task Update(PlexMediaSource plexMediaSource) =>
_dbContext.PlexMediaSources.Update(plexMediaSource)
.AsTask()
.Bind(_ => _dbContext.SaveChangesAsync());
public async Task Update(PlexMediaSource plexMediaSource)
{
await using TvContext context = _dbContextFactory.CreateDbContext();
context.PlexMediaSources.Update(plexMediaSource);
await context.SaveChangesAsync();
}
public async Task Update(PlexLibrary plexMediaSourceLibrary)
{
await using TvContext context = _dbContextFactory.CreateDbContext();
context.PlexLibraries.Update(plexMediaSourceLibrary);
await context.SaveChangesAsync();
}
public async Task Delete(int id)
{
MediaSource mediaSource = await _dbContext.MediaSources.FindAsync(id);
_dbContext.MediaSources.Remove(mediaSource);
await _dbContext.SaveChangesAsync();
await using TvContext context = _dbContextFactory.CreateDbContext();
MediaSource mediaSource = await context.MediaSources.FindAsync(id);
context.MediaSources.Remove(mediaSource);
await context.SaveChangesAsync();
}
public Task DisablePlexLibrarySync(IEnumerable<int> libraryIds) =>
_dbConnection.ExecuteAsync(
"UPDATE PlexMediaSourceLibraries SET ShouldSyncItems = 0 WHERE Id in @ids",
new { ids = libraryIds });
public Task EnablePlexLibrarySync(IEnumerable<int> libraryIds) =>
_dbConnection.ExecuteAsync(
"UPDATE PlexMediaSourceLibraries SET ShouldSyncItems = 1 WHERE Id in @ids",
new { ids = libraryIds });
}
}
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using Dapper;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
@@ -13,54 +15,102 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
{
public class MovieRepository : IMovieRepository
{
private readonly IDbConnection _dbConnection;
private readonly TvContext _dbContext;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public MovieRepository(TvContext dbContext) => _dbContext = dbContext;
public Task<Option<MovieMediaItem>> GetMovie(int movieId) =>
_dbContext.MovieMediaItems
.Include(m => m.Metadata)
.SingleOrDefaultAsync(m => m.Id == movieId)
.Map(Optional);
public async Task<Either<BaseError, MovieMediaItem>> GetOrAdd(int mediaSourceId, string path)
public MovieRepository(
TvContext dbContext,
IDbContextFactory<TvContext> dbContextFactory,
IDbConnection dbConnection)
{
Option<MovieMediaItem> maybeExisting = await _dbContext.MovieMediaItems
.Include(i => i.Metadata)
.SingleOrDefaultAsync(i => i.Path == path);
return await maybeExisting.Match(
mediaItem => Right<BaseError, MovieMediaItem>(mediaItem).AsTask(),
async () => await AddMovie(mediaSourceId, path));
_dbContext = dbContext;
_dbContextFactory = dbContextFactory;
_dbConnection = dbConnection;
}
public async Task<bool> Update(MovieMediaItem movie)
public async Task<Option<Movie>> GetMovie(int movieId)
{
_dbContext.MovieMediaItems.Update(movie);
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.Movies
.Include(m => m.MovieMetadata)
.ThenInclude(m => m.Artwork)
.OrderBy(m => m.Id)
.SingleOrDefaultAsync(m => m.Id == movieId)
.Map(Optional);
}
public async Task<Either<BaseError, Movie>> GetOrAdd(LibraryPath libraryPath, string path)
{
Option<Movie> maybeExisting = await _dbContext.Movies
.Include(i => i.MovieMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(i => i.LibraryPath)
.Include(i => i.MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.OrderBy(i => i.MediaVersions.First().MediaFiles.First().Path)
.SingleOrDefaultAsync(i => i.MediaVersions.First().MediaFiles.First().Path == path);
return await maybeExisting.Match(
mediaItem => Right<BaseError, Movie>(mediaItem).AsTask(),
async () => await AddMovie(libraryPath.Id, path));
}
public async Task<Either<BaseError, PlexMovie>> GetOrAdd(PlexLibrary library, PlexMovie item)
{
Option<PlexMovie> maybeExisting = await _dbContext.PlexMovieMediaItems
.Include(i => i.MovieMetadata)
.Include(i => i.Part)
.OrderBy(i => i.Key)
.SingleOrDefaultAsync(i => i.Key == item.Key);
return await maybeExisting.Match(
plexMovie => Right<BaseError, PlexMovie>(plexMovie).AsTask(),
async () => await AddPlexMovie(library, item));
}
public async Task<bool> Update(Movie movie)
{
_dbContext.Movies.Update(movie);
return await _dbContext.SaveChangesAsync() > 0;
}
public Task<int> GetMovieCount() =>
_dbContext.MovieMediaItems
.AsNoTracking()
.CountAsync();
_dbConnection.QuerySingleAsync<int>(@"SELECT COUNT(DISTINCT MovieId) FROM MovieMetadata");
public Task<List<MovieMediaItem>> GetPagedMovies(int pageNumber, int pageSize) =>
_dbContext.MovieMediaItems
.Include(s => s.Metadata)
.OrderBy(s => s.Metadata.SortTitle)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.AsNoTracking()
public Task<List<MovieMetadata>> GetPagedMovies(int pageNumber, int pageSize) =>
_dbContext.MovieMetadata.FromSqlRaw(
@"SELECT * FROM MovieMetadata WHERE Id IN
(SELECT Id FROM MovieMetadata GROUP BY MovieId, MetadataKind HAVING MetadataKind = MAX(MetadataKind))
ORDER BY SortTitle
LIMIT {0} OFFSET {1}",
pageSize,
(pageNumber - 1) * pageSize)
.Include(mm => mm.Artwork)
.OrderBy(mm => mm.SortTitle)
.ToListAsync();
private async Task<Either<BaseError, MovieMediaItem>> AddMovie(int mediaSourceId, string path)
private async Task<Either<BaseError, Movie>> AddMovie(int libraryPathId, string path)
{
try
{
var movie = new MovieMediaItem { MediaSourceId = mediaSourceId, Path = path };
await _dbContext.MovieMediaItems.AddAsync(movie);
var movie = new Movie
{
LibraryPathId = libraryPathId,
MediaVersions = new List<MediaVersion>
{
new()
{
MediaFiles = new List<MediaFile>
{
new() { Path = path }
}
}
}
};
await _dbContext.Movies.AddAsync(movie);
await _dbContext.SaveChangesAsync();
await _dbContext.Entry(movie).Reference(m => m.LibraryPath).LoadAsync();
return movie;
}
catch (Exception ex)
@@ -68,5 +118,24 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
return BaseError.New(ex.Message);
}
}
private async Task<Either<BaseError, PlexMovie>> AddPlexMovie(
PlexLibrary library,
PlexMovie item)
{
try
{
item.LibraryPathId = library.Paths.Head().Id;
await _dbContext.PlexMovieMediaItems.AddAsync(item);
await _dbContext.SaveChangesAsync();
await _dbContext.Entry(item).Reference(i => i.LibraryPath).LoadAsync();
return item;
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
}
}
}
}
@@ -5,7 +5,6 @@ using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude;
@@ -25,63 +24,57 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
}
public Task<Option<Playout>> Get(int id) =>
_dbContext.Playouts.SingleOrDefaultAsync(p => p.Id == id).Map(Optional);
_dbContext.Playouts
.OrderBy(p => p.Id)
.SingleOrDefaultAsync(p => p.Id == id)
.Map(Optional);
public async Task<Option<Playout>> GetFull(int id) =>
await _dbContext.Playouts
.Include(p => p.Channel)
.Include(p => p.Items)
.Include(p => p.ProgramScheduleAnchors)
.ThenInclude(a => a.MediaItem)
.Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.MediaCollection)
.ThenInclude(psi => psi.Collection)
.Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.TelevisionShow)
.ThenInclude(psi => psi.MediaItem)
.OrderBy(p => p.Id) // https://github.com/dotnet/efcore/issues/22579#issuecomment-694772289
.SingleOrDefaultAsync(p => p.Id == id);
public async Task<Option<PlayoutItem>> GetPlayoutItem(int channelId, DateTimeOffset now)
{
var p1 = new SqliteParameter("channelId", channelId);
var p2 = new SqliteParameter("now", now);
return await _dbContext.PlayoutItems
.FromSqlRaw(
"select i.* from playoutitems i inner join playouts p on i.playoutid = p.id where p.channelid = @channelId and i.start <= @now and i.finish > @now",
p1,
p2)
public async Task<Option<PlayoutItem>> GetPlayoutItem(int channelId, DateTimeOffset now) =>
await _dbContext.PlayoutItems
.Where(pi => pi.Playout.ChannelId == channelId)
.Where(pi => pi.Start <= now.UtcDateTime && pi.Finish > now.UtcDateTime)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Episode).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Movie).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.AsNoTracking()
.SingleOrDefaultAsync();
}
public Task<List<PlayoutItem>> GetPlayoutItems(int playoutId) =>
_dbContext.PlayoutItems
.Include(i => i.MediaItem)
.ThenInclude(m => (m as MovieMediaItem).Metadata)
.ThenInclude(mi => (mi as Movie).MovieMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(i => i.MediaItem)
.ThenInclude(m => (m as TelevisionEpisodeMediaItem).Metadata)
.ThenInclude(mi => (mi as Movie).MediaVersions)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Episode).EpisodeMetadata)
.ThenInclude(em => em.Artwork)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Episode).MediaVersions)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Episode).Season)
.ThenInclude(s => s.SeasonMetadata)
.Filter(i => i.PlayoutId == playoutId)
.ToListAsync();
public Task<List<int>> GetPlayoutIdsForMediaItems(Seq<MediaItem> mediaItems)
{
var ids = string.Join(", ", mediaItems.Map(mi => mi.Id));
return _dbContext.Playouts.FromSqlRaw(
@"SELECT DISTINCT p.* FROM Playouts p
INNER JOIN ProgramScheduleItems psi on psi.ProgramScheduleId = p.ProgramScheduleId
INNER JOIN SimpleMediaCollections smc on smc.Id = psi.MediaCollectionId
INNER JOIN MediaItemSimpleMediaCollection mismc on mismc.SimpleMediaCollectionsId = smc.Id
WHERE mismc.ItemsId in ({0})
UNION
SELECT DISTINCT p.* FROM Playouts p
INNER JOIN ProgramScheduleItems psi on psi.ProgramScheduleId = p.ProgramScheduleId
INNER JOIN TelevisionMediaCollections tmc on tmc.Id = psi.MediaCollectionId
INNER JOIN MediaItems mi on mi.Metadata_Title = tmc.ShowTitle and (tmc.SeasonNumber is null or tmc.SeasonNumber = mi.Metadata_SeasonNumber)
WHERE mi.Id in ({0})",
ids).Select(p => p.Id).ToListAsync();
}
public Task<List<Playout>> GetAll() =>
_dbContext.Playouts
.Include(p => p.Channel)
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
@@ -22,12 +23,16 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
}
public Task<Option<ProgramSchedule>> Get(int id) =>
_dbContext.ProgramSchedules.SingleOrDefaultAsync(s => s.Id == id).Map(Optional);
_dbContext.ProgramSchedules
.OrderBy(s => s.Id)
.SingleOrDefaultAsync(s => s.Id == id)
.Map(Optional);
public async Task<Option<ProgramSchedule>> GetWithPlayouts(int id) =>
await _dbContext.ProgramSchedules
.Include(ps => ps.Items)
.Include(ps => ps.Playouts)
.OrderBy(ps => ps.Id)
.SingleOrDefaultAsync(ps => ps.Id == id);
public Task<List<ProgramSchedule>> GetAll() =>
@@ -37,7 +42,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
{
_dbContext.ProgramSchedules.Update(programSchedule);
await _dbContext.SaveChangesAsync();
await _dbContext.Entry(programSchedule).Collection(s => s.Items).Query().Include(i => i.MediaCollection)
await _dbContext.Entry(programSchedule).Collection(s => s.Items).Query().Include(i => i.Collection)
.LoadAsync();
await _dbContext.Entry(programSchedule).Collection(s => s.Playouts).LoadAsync();
}
@@ -57,12 +62,20 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
{
await _dbContext.Entry(programSchedule).Collection(s => s.Items).LoadAsync();
await _dbContext.Entry(programSchedule).Collection(s => s.Items).Query()
.Include(i => i.MediaCollection)
.Include(i => i.TelevisionShow)
.ThenInclude(s => s.Metadata)
.Include(i => i.TelevisionSeason)
.ThenInclude(s => s.TelevisionShow)
.ThenInclude(s => s.Metadata)
.Include(i => i.Collection)
.Include(i => i.MediaItem)
.ThenInclude(i => (i as Movie).MovieMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(i => i.MediaItem)
.ThenInclude(i => (i as Season).SeasonMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(i => i.MediaItem)
.ThenInclude(i => (i as Season).Show)
.ThenInclude(s => s.ShowMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(i => i.MediaItem)
.ThenInclude(i => (i as Show).ShowMetadata)
.ThenInclude(sm => sm.Artwork)
.LoadAsync();
return programSchedule.Items;
}).Sequence();
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
@@ -15,7 +16,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public ResolutionRepository(TvContext dbContext) => _dbContext = dbContext;
public Task<Option<Resolution>> Get(int id) =>
_dbContext.Resolutions.SingleOrDefaultAsync(r => r.Id == id).Map(Optional);
_dbContext.Resolutions
.OrderBy(r => r.Id)
.SingleOrDefaultAsync(r => r.Id == id)
.Map(Optional);
public Task<List<Resolution>> GetAll() =>
_dbContext.Resolutions.ToListAsync();
@@ -24,170 +24,172 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
_dbConnection = dbConnection;
}
public async Task<bool> Update(TelevisionShow show)
public async Task<bool> Update(Show show)
{
_dbContext.TelevisionShows.Update(show);
_dbContext.Shows.Update(show);
return await _dbContext.SaveChangesAsync() > 0;
}
public async Task<bool> Update(TelevisionSeason season)
public async Task<bool> Update(Season season)
{
_dbContext.TelevisionSeasons.Update(season);
_dbContext.Seasons.Update(season);
return await _dbContext.SaveChangesAsync() > 0;
}
public async Task<bool> Update(TelevisionEpisodeMediaItem episode)
public async Task<bool> Update(Episode episode)
{
_dbContext.TelevisionEpisodeMediaItems.Update(episode);
_dbContext.Episodes.Update(episode);
return await _dbContext.SaveChangesAsync() > 0;
}
public Task<List<TelevisionShow>> GetAllShows() =>
_dbContext.TelevisionShows
public Task<List<Show>> GetAllShows() =>
_dbContext.Shows
.AsNoTracking()
.Include(s => s.Metadata)
.Include(s => s.ShowMetadata)
.ThenInclude(sm => sm.Artwork)
.ToListAsync();
public Task<Option<TelevisionShow>> GetShow(int televisionShowId) =>
_dbContext.TelevisionShows
public Task<Option<Show>> GetShow(int showId) =>
_dbContext.Shows
.AsNoTracking()
.Filter(s => s.Id == televisionShowId)
.Include(s => s.Metadata)
.Filter(s => s.Id == showId)
.Include(s => s.ShowMetadata)
.ThenInclude(sm => sm.Artwork)
.OrderBy(s => s.Id)
.SingleOrDefaultAsync()
.Map(Optional);
public Task<int> GetShowCount() =>
_dbContext.TelevisionShows
_dbContext.ShowMetadata
.AsNoTracking()
.GroupBy(sm => new { sm.Title, sm.Year })
.CountAsync();
public Task<List<TelevisionShow>> GetPagedShows(int pageNumber, int pageSize) =>
_dbContext.TelevisionShows
public Task<List<ShowMetadata>> GetPagedShows(int pageNumber, int pageSize) =>
_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)
.Include(mm => mm.Artwork)
.OrderBy(mm => mm.SortTitle)
.ToListAsync();
public Task<List<Season>> GetAllSeasons() =>
_dbContext.Seasons
.AsNoTracking()
.Include(s => s.Metadata)
.OrderBy(s => s.Metadata == null ? string.Empty : s.Metadata.SortTitle)
.Include(s => s.SeasonMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(s => s.Show)
.ThenInclude(s => s.ShowMetadata)
.ThenInclude(sm => sm.Artwork)
.ToListAsync();
public Task<Option<Season>> GetSeason(int seasonId) =>
_dbContext.Seasons
.AsNoTracking()
.Include(s => s.SeasonMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(s => s.Show)
.ThenInclude(s => s.ShowMetadata)
.OrderBy(s => s.Id)
.SingleOrDefaultAsync(s => s.Id == seasonId)
.Map(Optional);
public Task<int> GetSeasonCount(int showId) =>
_dbContext.Seasons
.AsNoTracking()
.CountAsync(s => s.ShowId == showId);
public async Task<List<Season>> GetPagedSeasons(int televisionShowId, int pageNumber, int pageSize)
{
List<int> showIds = await _dbConnection.QueryAsync<int>(
@"SELECT m1.ShowId
FROM ShowMetadata m1
LEFT OUTER JOIN ShowMetadata m2 ON m2.ShowId = @ShowId
WHERE m1.Title = m2.Title AND m1.Year = m2.Year",
new { ShowId = televisionShowId })
.Map(results => results.ToList());
return await _dbContext.Seasons
.AsNoTracking()
.Where(s => showIds.Contains(s.ShowId))
.Include(s => s.SeasonMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(s => s.Show)
.ThenInclude(s => s.ShowMetadata)
.OrderBy(s => s.SeasonNumber)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
}
public Task<List<TelevisionSeason>> GetAllSeasons() =>
_dbContext.TelevisionSeasons
public Task<Option<Episode>> GetEpisode(int episodeId) =>
_dbContext.Episodes
.AsNoTracking()
.Include(s => s.TelevisionShow)
.ThenInclude(s => s.Metadata)
.ToListAsync();
public Task<Option<TelevisionSeason>> GetSeason(int televisionSeasonId) =>
_dbContext.TelevisionSeasons
.AsNoTracking()
.Include(s => s.TelevisionShow)
.ThenInclude(s => s.Metadata)
.SingleOrDefaultAsync(s => s.Id == televisionSeasonId)
.Map(Optional);
public Task<int> GetSeasonCount(int televisionShowId) =>
_dbContext.TelevisionSeasons
.AsNoTracking()
.Where(s => s.TelevisionShowId == televisionShowId)
.CountAsync();
public Task<List<TelevisionSeason>> GetPagedSeasons(int televisionShowId, int pageNumber, int pageSize) =>
_dbContext.TelevisionSeasons
.AsNoTracking()
.Where(s => s.TelevisionShowId == televisionShowId)
.Include(s => s.TelevisionShow)
.ThenInclude(s => s.Metadata)
.OrderBy(s => s.Number)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
public Task<Option<TelevisionEpisodeMediaItem>> GetEpisode(int televisionEpisodeId) =>
_dbContext.TelevisionEpisodeMediaItems
.AsNoTracking()
.Include(s => s.Season)
.Include(s => s.Metadata)
.SingleOrDefaultAsync(s => s.Id == televisionEpisodeId)
.Map(Optional);
public Task<int> GetEpisodeCount(int televisionSeasonId) =>
_dbContext.TelevisionEpisodeMediaItems
.AsNoTracking()
.Where(e => e.SeasonId == televisionSeasonId)
.CountAsync();
public Task<List<TelevisionEpisodeMediaItem>> GetPagedEpisodes(
int televisionSeasonId,
int pageNumber,
int pageSize) =>
_dbContext.TelevisionEpisodeMediaItems
.AsNoTracking()
.Include(e => e.Metadata)
.Include(e => e.Season)
.ThenInclude(s => s.TelevisionShow)
.ThenInclude(s => s.Metadata)
.Where(e => e.SeasonId == televisionSeasonId)
.OrderBy(s => s.Metadata.Episode)
.Include(e => e.EpisodeMetadata)
.ThenInclude(em => em.Artwork)
.OrderBy(s => s.Id)
.SingleOrDefaultAsync(s => s.Id == episodeId)
.Map(Optional);
public Task<int> GetEpisodeCount(int seasonId) =>
_dbContext.Episodes
.AsNoTracking()
.CountAsync(e => e.SeasonId == seasonId);
public Task<List<EpisodeMetadata>> GetPagedEpisodes(int seasonId, int pageNumber, int pageSize) =>
_dbContext.EpisodeMetadata
.AsNoTracking()
.Filter(em => em.Episode.SeasonId == seasonId)
.Include(em => em.Artwork)
.Include(em => em.Episode)
.ThenInclude(e => e.Season)
.ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata)
.OrderBy(em => em.Episode.EpisodeNumber)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
public async Task<Option<TelevisionShow>> GetShowByPath(int mediaSourceId, string path)
public async Task<Option<Show>> GetShowByMetadata(int libraryPathId, ShowMetadata metadata)
{
Option<int> maybeShowId = await _dbContext.LocalTelevisionShowSources
.SingleOrDefaultAsync(s => s.MediaSourceId == mediaSourceId && s.Path == path)
.Map(Optional)
.MapT(s => s.TelevisionShowId);
return await maybeShowId.Match<Task<Option<TelevisionShow>>>(
async id => await _dbContext.TelevisionShows
.Include(s => s.Metadata)
.Include(s => s.Sources)
.SingleOrDefaultAsync(s => s.Id == id),
() => Task.FromResult(Option<TelevisionShow>.None));
}
public async Task<Option<TelevisionShow>> GetShowByMetadata(TelevisionShowMetadata metadata)
{
Option<TelevisionShow> maybeShow = await _dbContext.TelevisionShows
.Include(s => s.Metadata)
.Where(s => s.Metadata.Title == metadata.Title && s.Metadata.Year == metadata.Year)
Option<int> maybeId = await _dbContext.ShowMetadata
.Where(s => s.Title == metadata.Title && s.Year == metadata.Year)
.Where(s => s.Show.LibraryPathId == libraryPathId)
.SingleOrDefaultAsync()
.Map(Optional);
.Map(Optional)
.MapT(sm => sm.ShowId);
await maybeShow.IfSomeAsync(
async show =>
return await maybeId.Match(
id =>
{
await _dbContext.Entry(show).Reference(s => s.Metadata).LoadAsync();
await _dbContext.Entry(show).Collection(s => s.Sources).LoadAsync();
});
return maybeShow;
return _dbContext.Shows
.Include(s => s.ShowMetadata)
.ThenInclude(sm => sm.Artwork)
.OrderBy(s => s.Id)
.SingleOrDefaultAsync(s => s.Id == id)
.Map(Optional);
},
() => Option<Show>.None.AsTask());
}
public async Task<Either<BaseError, TelevisionShow>> AddShow(
int localMediaSourceId,
string showFolder,
TelevisionShowMetadata metadata)
public async Task<Either<BaseError, Show>> AddShow(int libraryPathId, string showFolder, ShowMetadata metadata)
{
try
{
var show = new TelevisionShow
metadata.DateAdded = DateTime.UtcNow;
var show = new Show
{
Sources = new List<TelevisionShowSource>(),
Metadata = metadata,
Seasons = new List<TelevisionSeason>()
LibraryPathId = libraryPathId,
ShowMetadata = new List<ShowMetadata> { metadata },
Seasons = new List<Season>()
};
show.Sources.Add(
new LocalTelevisionShowSource
{
MediaSourceId = localMediaSourceId,
Path = showFolder,
TelevisionShow = show
});
await _dbContext.TelevisionShows.AddAsync(show);
await _dbContext.Shows.AddAsync(show);
await _dbContext.SaveChangesAsync();
return show;
@@ -198,122 +200,86 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
}
}
public async Task<Either<BaseError, TelevisionSeason>> GetOrAddSeason(
TelevisionShow show,
string path,
int seasonNumber)
public async Task<Either<BaseError, Season>> GetOrAddSeason(Show show, int libraryPathId, int seasonNumber)
{
Option<TelevisionSeason> maybeExisting = await _dbContext.TelevisionSeasons
.SingleOrDefaultAsync(i => i.Path == path);
Option<Season> maybeExisting = await _dbContext.Seasons
.Include(s => s.SeasonMetadata)
.ThenInclude(sm => sm.Artwork)
.SingleOrDefaultAsync(s => s.ShowId == show.Id && s.SeasonNumber == seasonNumber);
return await maybeExisting.Match(
season => Right<BaseError, TelevisionSeason>(season).AsTask(),
() => AddSeason(show, path, seasonNumber));
season => Right<BaseError, Season>(season).AsTask(),
() => AddSeason(show, libraryPathId, seasonNumber));
}
public async Task<Either<BaseError, TelevisionEpisodeMediaItem>> GetOrAddEpisode(
TelevisionSeason season,
int mediaSourceId,
public async Task<Either<BaseError, Episode>> GetOrAddEpisode(
Season season,
LibraryPath libraryPath,
string path)
{
Option<TelevisionEpisodeMediaItem> maybeExisting = await _dbContext.TelevisionEpisodeMediaItems
.Include(i => i.Metadata)
.Include(i => i.Statistics)
.SingleOrDefaultAsync(i => i.Path == path);
Option<Episode> maybeExisting = await _dbContext.Episodes
.Include(i => i.EpisodeMetadata)
.ThenInclude(em => em.Artwork)
.Include(i => i.MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.OrderBy(i => i.MediaVersions.First().MediaFiles.First().Path)
.SingleOrDefaultAsync(i => i.MediaVersions.First().MediaFiles.First().Path == path);
return await maybeExisting.Match(
episode => Right<BaseError, TelevisionEpisodeMediaItem>(episode).AsTask(),
() => AddEpisode(season, mediaSourceId, path));
episode => Right<BaseError, Episode>(episode).AsTask(),
() => AddEpisode(season, libraryPath.Id, path));
}
public Task<Unit> DeleteMissingSources(int localMediaSourceId, List<string> allFolders) =>
_dbContext.LocalTelevisionShowSources
.Where(s => s.MediaSourceId == localMediaSourceId && !allFolders.Contains(s.Path))
.ToListAsync()
.Bind(
list =>
{
_dbContext.LocalTelevisionShowSources.RemoveRange(list);
return _dbContext.SaveChangesAsync();
})
.ToUnit();
public Task<Unit> DeleteEmptyShows() =>
_dbContext.TelevisionShows
.Where(s => s.Sources.Count == 0)
_dbContext.Shows
.Where(s => s.Seasons.Count == 0)
.ToListAsync()
.Bind(
list =>
{
_dbContext.TelevisionShows.RemoveRange(list);
_dbContext.Shows.RemoveRange(list);
return _dbContext.SaveChangesAsync();
})
.ToUnit();
public async Task<List<TelevisionEpisodeMediaItem>> GetShowItems(int televisionShowId)
public async Task<List<Episode>> GetShowItems(int showId)
{
var parameters = new { ShowId = televisionShowId };
return await _dbConnection
.QueryAsync<TelevisionEpisodeMediaItem, MediaItemStatistics, TelevisionEpisodeMetadata,
TelevisionEpisodeMediaItem>(
@"select tmi.Id, tmi.SeasonId, mi.MediaSourceId, mi.LastWriteTime, mi.Path, mi.Poster, mi.PosterLastWriteTime,
mi.Statistics_AudioCodec as AudioCodec, mi.Statistics_DisplayAspectRatio as DisplayAspectRatio, mi.Statistics_Duration as Duration, mi.Statistics_Height as Height, mi.Statistics_LastWriteTime as LastWriteTime, mi.Statistics_SampleAspectRatio as SampleAspectRatio,
mi.Statistics_VideoCodec as VideoCodec, mi.Statistics_VideoScanType as VideoScanType, mi.Statistics_Width as Width,
tem.TelevisionEpisodeId, tem.Id, tem.Season, tem.Episode, tem.Plot, tem.Aired, tem.Source, tem.LastWriteTime, tem.Title, tem.SortTitle
from TelevisionEpisodes tmi
inner join MediaItems mi on tmi.Id = mi.Id
inner join TelevisionEpisodeMetadata tem on tem.TelevisionEpisodeId = tmi.Id
inner join TelevisionSeasons tsn on tsn.Id = tmi.SeasonId
inner join TelevisionShows ts on ts.Id = tsn.TelevisionShowId
where ts.Id = @ShowId",
(episode, statistics, metadata) =>
{
episode.Statistics = statistics;
episode.Metadata = metadata;
return episode;
},
parameters,
splitOn: "AudioCodec,TelevisionEpisodeId").Map(result => result.ToList());
IEnumerable<int> ids = await _dbConnection.QueryAsync<int>(
@"SELECT Episode.Id FROM Show
INNER JOIN Season ON Season.ShowId = Show.Id
INNER JOIN Episode ON Episode.SeasonId = Season.Id
WHERE Show.Id = @ShowId",
new { ShowId = showId });
return await _dbContext.Episodes
.Include(e => e.EpisodeMetadata)
.Include(e => e.MediaVersions)
.Include(e => e.Season)
.Filter(e => ids.Contains(e.Id))
.ToListAsync();
}
public async Task<List<TelevisionEpisodeMediaItem>> GetSeasonItems(int televisionSeasonId)
{
var parameters = new { SeasonId = televisionSeasonId };
return await _dbConnection
.QueryAsync<TelevisionEpisodeMediaItem, MediaItemStatistics, TelevisionEpisodeMetadata,
TelevisionEpisodeMediaItem>(
@"select tmi.Id, tmi.SeasonId, mi.MediaSourceId, mi.LastWriteTime, mi.Path, mi.Poster, mi.PosterLastWriteTime,
mi.Statistics_AudioCodec as AudioCodec, mi.Statistics_DisplayAspectRatio as DisplayAspectRatio, mi.Statistics_Duration as Duration, mi.Statistics_Height as Height, mi.Statistics_LastWriteTime as LastWriteTime, mi.Statistics_SampleAspectRatio as SampleAspectRatio,
mi.Statistics_VideoCodec as VideoCodec, mi.Statistics_VideoScanType as VideoScanType, mi.Statistics_Width as Width,
tem.TelevisionEpisodeId, tem.Id, tem.Season, tem.Episode, tem.Plot, tem.Aired, tem.Source, tem.LastWriteTime, tem.Title, tem.SortTitle
from TelevisionEpisodes tmi
inner join MediaItems mi on tmi.Id = mi.Id
inner join TelevisionEpisodeMetadata tem on tem.TelevisionEpisodeId = tmi.Id
inner join TelevisionSeasons tsn on tsn.Id = tmi.SeasonId
where tsn.Id = @SeasonId",
(episode, statistics, metadata) =>
{
episode.Statistics = statistics;
episode.Metadata = metadata;
return episode;
},
parameters,
splitOn: "AudioCodec,TelevisionEpisodeId").Map(result => result.ToList());
}
public Task<List<Episode>> GetSeasonItems(int seasonId) =>
_dbContext.Episodes
.Include(e => e.EpisodeMetadata)
.Include(e => e.MediaVersions)
.Include(e => e.Season)
.Filter(e => e.SeasonId == seasonId)
.ToListAsync();
private async Task<Either<BaseError, TelevisionSeason>> AddSeason(
TelevisionShow show,
string path,
int seasonNumber)
private async Task<Either<BaseError, Season>> AddSeason(Show show, int libraryPathId, int seasonNumber)
{
try
{
var season = new TelevisionSeason
var season = new Season
{
TelevisionShowId = show.Id, Path = path, Number = seasonNumber,
Episodes = new List<TelevisionEpisodeMediaItem>()
LibraryPathId = libraryPathId,
ShowId = show.Id,
SeasonNumber = seasonNumber,
Episodes = new List<Episode>(),
SeasonMetadata = new List<SeasonMetadata>()
};
await _dbContext.TelevisionSeasons.AddAsync(season);
await _dbContext.Seasons.AddAsync(season);
await _dbContext.SaveChangesAsync();
return season;
}
@@ -323,20 +289,27 @@ where tsn.Id = @SeasonId",
}
}
private async Task<Either<BaseError, TelevisionEpisodeMediaItem>> AddEpisode(
TelevisionSeason season,
int mediaSourceId,
string path)
private async Task<Either<BaseError, Episode>> AddEpisode(Season season, int libraryPathId, string path)
{
try
{
var episode = new TelevisionEpisodeMediaItem
var episode = new Episode
{
MediaSourceId = mediaSourceId,
LibraryPathId = libraryPathId,
SeasonId = season.Id,
Path = path
EpisodeMetadata = new List<EpisodeMetadata>(),
MediaVersions = new List<MediaVersion>
{
new()
{
MediaFiles = new List<MediaFile>
{
new() { Path = path }
}
}
}
};
await _dbContext.TelevisionEpisodeMediaItems.AddAsync(episode);
await _dbContext.Episodes.AddAsync(episode);
await _dbContext.SaveChangesAsync();
return episode;
}
@@ -14,19 +14,18 @@ namespace ErsatzTV.Infrastructure.Data
public class DateTimeOffsetHandler : SqliteTypeHandler<DateTimeOffset>
{
public override DateTimeOffset Parse(object value)
=> DateTimeOffset.Parse((string)value);
=> DateTimeOffset.Parse((string) value);
}
public class GuidHandler : SqliteTypeHandler<Guid>
{
public override Guid Parse(object value)
=> Guid.Parse((string)value);
=> Guid.Parse((string) value);
}
public class TimeSpanHandler : SqliteTypeHandler<TimeSpan>
{
public override TimeSpan Parse(object value)
=> TimeSpan.Parse((string)value);
=> TimeSpan.Parse((string) value);
}
}
+15 -8
View File
@@ -17,21 +17,28 @@ namespace ErsatzTV.Infrastructure.Data
public DbSet<MediaSource> MediaSources { get; set; }
public DbSet<LocalMediaSource> LocalMediaSources { get; set; }
public DbSet<PlexMediaSource> PlexMediaSources { get; set; }
public DbSet<Library> Libraries { get; set; }
public DbSet<LocalLibrary> LocalLibraries { get; set; }
public DbSet<LibraryPath> LibraryPaths { get; set; }
public DbSet<PlexLibrary> PlexLibraries { get; set; }
public DbSet<MediaItem> MediaItems { get; set; }
public DbSet<MovieMediaItem> MovieMediaItems { get; set; }
public DbSet<TelevisionEpisodeMediaItem> TelevisionEpisodeMediaItems { get; set; }
public DbSet<MediaCollection> MediaCollections { get; set; }
public DbSet<SimpleMediaCollection> SimpleMediaCollections { get; set; }
public DbSet<MediaVersion> MediaVersions { get; set; }
public DbSet<MediaFile> MediaFiles { get; set; }
public DbSet<Movie> Movies { get; set; }
public DbSet<MovieMetadata> MovieMetadata { get; set; }
public DbSet<Show> Shows { get; set; }
public DbSet<ShowMetadata> ShowMetadata { get; set; }
public DbSet<Season> Seasons { get; set; }
public DbSet<Episode> Episodes { get; set; }
public DbSet<EpisodeMetadata> EpisodeMetadata { get; set; }
public DbSet<PlexMovie> PlexMovieMediaItems { get; set; }
public DbSet<Collection> Collections { get; set; }
public DbSet<ProgramSchedule> ProgramSchedules { get; set; }
public DbSet<Playout> Playouts { get; set; }
public DbSet<PlayoutItem> PlayoutItems { get; set; }
public DbSet<PlayoutProgramScheduleAnchor> PlayoutProgramScheduleItemAnchors { get; set; }
public DbSet<FFmpegProfile> FFmpegProfiles { get; set; }
public DbSet<Resolution> Resolutions { get; set; }
public DbSet<TelevisionShow> TelevisionShows { get; set; }
public DbSet<LocalTelevisionShowSource> LocalTelevisionShowSources { get; set; }
public DbSet<TelevisionShowMetadata> TelevisionShowMetadata { get; set; }
public DbSet<TelevisionSeason> TelevisionSeasons { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) =>
optionsBuilder.UseLoggerFactory(_loggerFactory);