add music videos library (#125)

* add music videos library

* add music video tables

* first pass at music video library scan

* support music videos in playouts

* display music videos in search results and collections

* fix music video thumbnails

* remove some obsolete fields
This commit is contained in:
Jason Dove
2021-04-02 18:28:45 -05:00
committed by GitHub
parent da3e05b231
commit 633586ddba
56 changed files with 7572 additions and 112 deletions
@@ -0,0 +1,23 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class MusicVideoConfiguration : IEntityTypeConfiguration<MusicVideo>
{
public void Configure(EntityTypeBuilder<MusicVideo> builder)
{
builder.ToTable("MusicVideo");
builder.HasMany(m => m.MusicVideoMetadata)
.WithOne(m => m.MusicVideo)
.HasForeignKey(m => m.MusicVideoId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(m => m.MediaVersions)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
}
}
}
@@ -0,0 +1,30 @@
using ErsatzTV.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class MusicVideoMetadataConfiguration : IEntityTypeConfiguration<MusicVideoMetadata>
{
public void Configure(EntityTypeBuilder<MusicVideoMetadata> builder)
{
builder.ToTable("MusicVideoMetadata");
builder.HasMany(mm => mm.Artwork)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(mm => mm.Genres)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(mm => mm.Tags)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(mm => mm.Studios)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
}
}
}
@@ -52,7 +52,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public Task<List<Library>> GetAll()
{
using TvContext context = _dbContextFactory.CreateDbContext();
return context.Libraries.ToListAsync();
return context.Libraries
.AsNoTracking()
.Include(l => l.MediaSource)
.ToListAsync();
}
public Task<Unit> UpdateLastScan(Library library) => _dbConnection.ExecuteAsync(
@@ -104,6 +104,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Movie).MovieMetadata)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as MusicVideo).MusicVideoMetadata)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Show).ShowMetadata)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Season).Show)
@@ -128,6 +130,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ThenInclude(i => (i as Movie).MovieMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as MusicVideo).MusicVideoMetadata)
.ThenInclude(mvm => mvm.Artwork)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Show).ShowMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(c => c.MediaItems)
@@ -196,6 +201,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
result.AddRange(await GetShowItems(collection));
result.AddRange(await GetSeasonItems(collection));
result.AddRange(await GetEpisodeItems(collection));
result.AddRange(await GetMusicVideoItems(collection));
return result.Distinct().ToList();
}
@@ -215,6 +221,21 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ToListAsync();
}
private async Task<List<MusicVideo>> GetMusicVideoItems(Collection collection)
{
IEnumerable<int> ids = await _dbConnection.QueryAsync<int>(
@"SELECT m.Id FROM CollectionItem ci
INNER JOIN MusicVideo m ON m.Id = ci.MediaItemId
WHERE ci.CollectionId = @CollectionId",
new { CollectionId = collection.Id });
return await _dbContext.MusicVideos
.Include(m => m.MusicVideoMetadata)
.Include(m => m.MediaVersions)
.Filter(m => ids.Contains(m.Id))
.ToListAsync();
}
private async Task<List<Episode>> GetShowItems(Collection collection)
{
IEnumerable<int> ids = await _dbConnection.QueryAsync<int>(
@@ -156,6 +156,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path)",
parameters)
.ToUnit(),
MusicVideoMetadata => _dbConnection.ExecuteAsync(
@"INSERT INTO Artwork (ArtworkKind, MusicVideoMetadataId, DateAdded, DateUpdated, Path)
VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path)",
parameters)
.ToUnit(),
_ => Task.FromResult(Unit.Default)
};
}
@@ -0,0 +1,181 @@
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;
using ErsatzTV.Core.Metadata;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude;
namespace ErsatzTV.Infrastructure.Data.Repositories
{
public class MusicVideoRepository : IMusicVideoRepository
{
private readonly IDbConnection _dbConnection;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public MusicVideoRepository(IDbContextFactory<TvContext> dbContextFactory, IDbConnection dbConnection)
{
_dbContextFactory = dbContextFactory;
_dbConnection = dbConnection;
}
public async Task<Option<MusicVideo>> GetByMetadata(LibraryPath libraryPath, MusicVideoMetadata metadata)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Option<int> maybeId = await dbContext.MusicVideoMetadata
.Where(s => s.Artist == metadata.Artist && s.Title == metadata.Title && s.Year == metadata.Year)
.Where(s => s.MusicVideo.LibraryPathId == libraryPath.Id)
.SingleOrDefaultAsync()
.Map(Optional)
.MapT(sm => sm.MusicVideoId);
return await maybeId.Match(
id =>
{
return dbContext.MusicVideos
.AsNoTracking()
.Include(mv => mv.MusicVideoMetadata)
.ThenInclude(mvm => mvm.Artwork)
.Include(mv => mv.MusicVideoMetadata)
.ThenInclude(mvm => mvm.Genres)
.Include(mv => mv.MusicVideoMetadata)
.ThenInclude(mvm => mvm.Tags)
.Include(mv => mv.MusicVideoMetadata)
.ThenInclude(mvm => mvm.Studios)
.Include(mv => mv.LibraryPath)
.ThenInclude(lp => lp.Library)
.Include(mv => mv.MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.OrderBy(mv => mv.Id)
.SingleOrDefaultAsync(mv => mv.Id == id)
.Map(Optional);
},
() => Option<MusicVideo>.None.AsTask());
}
public async Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> Add(
LibraryPath libraryPath,
string filePath,
MusicVideoMetadata metadata)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
try
{
metadata.DateAdded = DateTime.UtcNow;
metadata.Genres ??= new List<Genre>();
metadata.Tags ??= new List<Tag>();
metadata.Studios ??= new List<Studio>();
var musicVideo = new MusicVideo
{
LibraryPathId = libraryPath.Id,
MusicVideoMetadata = new List<MusicVideoMetadata> { metadata },
MediaVersions = new List<MediaVersion>
{
new()
{
MediaFiles = new List<MediaFile>
{
new() { Path = filePath }
},
Streams = new List<MediaStream>()
}
}
};
await dbContext.MusicVideos.AddAsync(musicVideo);
await dbContext.SaveChangesAsync();
await dbContext.Entry(musicVideo).Reference(s => s.LibraryPath).LoadAsync();
await dbContext.Entry(musicVideo.LibraryPath).Reference(lp => lp.Library).LoadAsync();
return new MediaItemScanResult<MusicVideo>(musicVideo) { IsAdded = true };
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
}
}
public Task<IEnumerable<string>> FindMusicVideoPaths(LibraryPath libraryPath) =>
_dbConnection.QueryAsync<string>(
@"SELECT MF.Path
FROM MediaFile MF
INNER JOIN MediaVersion MV on MF.MediaVersionId = MV.Id
INNER JOIN MusicVideo M on MV.MusicVideoId = M.Id
INNER JOIN MediaItem MI on M.Id = MI.Id
WHERE MI.LibraryPathId = @LibraryPathId",
new { LibraryPathId = libraryPath.Id });
public async Task<List<int>> DeleteByPath(LibraryPath libraryPath, string path)
{
List<int> ids = await _dbConnection.QueryAsync<int>(
@"SELECT M.Id
FROM MusicVideo M
INNER JOIN MediaItem MI on M.Id = MI.Id
INNER JOIN MediaVersion MV on M.Id = MV.EpisodeId
INNER JOIN MediaFile MF on MV.Id = MF.MediaVersionId
WHERE MI.LibraryPathId = @LibraryPathId AND MF.Path = @Path",
new { LibraryPathId = libraryPath.Id, Path = path }).Map(result => result.ToList());
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
foreach (int musicVideoId in ids)
{
MusicVideo musicVideo = await dbContext.MusicVideos.FindAsync(musicVideoId);
dbContext.MusicVideos.Remove(musicVideo);
}
await dbContext.SaveChangesAsync();
return ids;
}
public Task<bool> AddGenre(MusicVideoMetadata metadata, Genre genre) =>
_dbConnection.ExecuteAsync(
"INSERT INTO Genre (Name, MusicVideoMetadataId) VALUES (@Name, @MetadataId)",
new { genre.Name, MetadataId = metadata.Id }).Map(result => result > 0);
public Task<bool> AddTag(MusicVideoMetadata metadata, Tag tag) =>
_dbConnection.ExecuteAsync(
"INSERT INTO Tag (Name, MusicVideoMetadataId) VALUES (@Name, @MetadataId)",
new { tag.Name, MetadataId = metadata.Id }).Map(result => result > 0);
public Task<bool> AddStudio(MusicVideoMetadata metadata, Studio studio) =>
_dbConnection.ExecuteAsync(
"INSERT INTO Studio (Name, MusicVideoMetadataId) VALUES (@Name, @MetadataId)",
new { studio.Name, MetadataId = metadata.Id }).Map(result => result > 0);
public async Task<List<MusicVideoMetadata>> GetMusicVideosForCards(List<int> ids)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.MusicVideoMetadata
.AsNoTracking()
.Filter(mvm => ids.Contains(mvm.MusicVideoId))
.Include(mvm => mvm.Artwork)
.OrderBy(mvm => mvm.SortTitle)
.ToListAsync();
}
public async Task<Option<MusicVideo>> GetMusicVideo(int musicVideoId)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.MusicVideos
.Include(m => m.MusicVideoMetadata)
.ThenInclude(m => m.Artwork)
.Include(m => m.MusicVideoMetadata)
.ThenInclude(m => m.Genres)
.Include(m => m.MusicVideoMetadata)
.ThenInclude(m => m.Tags)
.Include(m => m.MusicVideoMetadata)
.ThenInclude(m => m.Studios)
.OrderBy(m => m.Id)
.SingleOrDefaultAsync(m => m.Id == musicVideoId)
.Map(Optional);
}
}
}
@@ -65,6 +65,12 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Movie).MediaVersions)
.ThenInclude(mv => mv.Streams)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).MediaVersions)
.ThenInclude(mv => mv.Streams)
.AsNoTracking()
.SingleOrDefaultAsync()
.Map(Optional);
@@ -89,6 +95,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Movie).MediaVersions)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).MusicVideoMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).MediaVersions)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Episode).EpisodeMetadata)
.ThenInclude(em => em.Artwork)
.Include(i => i.MediaItem)
@@ -27,6 +27,8 @@ namespace ErsatzTV.Infrastructure.Data
public DbSet<MediaFile> MediaFiles { get; set; }
public DbSet<Movie> Movies { get; set; }
public DbSet<MovieMetadata> MovieMetadata { get; set; }
public DbSet<MusicVideo> MusicVideos { get; set; }
public DbSet<MusicVideoMetadata> MusicVideoMetadata { get; set; }
public DbSet<Show> Shows { get; set; }
public DbSet<ShowMetadata> ShowMetadata { get; set; }
public DbSet<Season> Seasons { get; set; }
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_LocalLibrary_MusicVideos : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
// create local music videos library
migrationBuilder.Sql(
@"INSERT INTO Library (Name, MediaKind, MediaSourceId)
SELECT 'Music Videos', 3, Id FROM
(SELECT LMS.Id FROM LocalMediaSource LMS
INNER JOIN Library L on L.MediaSourceId = LMS.Id
INNER JOIN LocalLibrary LL on L.Id = LL.Id
WHERE L.Name = 'Movies')");
migrationBuilder.Sql("INSERT INTO LocalLibrary (Id) Values (last_insert_rowid())");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,228 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_MusicVideo_MusicVideoMetadata : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
"MusicVideoMetadataId",
"Tag",
"INTEGER",
nullable: true);
migrationBuilder.AddColumn<int>(
"MusicVideoMetadataId",
"Studio",
"INTEGER",
nullable: true);
migrationBuilder.AddColumn<int>(
"MusicVideoId",
"MediaVersion",
"INTEGER",
nullable: true);
migrationBuilder.AddColumn<int>(
"MusicVideoMetadataId",
"Genre",
"INTEGER",
nullable: true);
migrationBuilder.AddColumn<int>(
"MusicVideoMetadataId",
"Artwork",
"INTEGER",
nullable: true);
migrationBuilder.CreateTable(
"MusicVideo",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true)
},
constraints: table =>
{
table.PrimaryKey("PK_MusicVideo", x => x.Id);
table.ForeignKey(
"FK_MusicVideo_MediaItem_Id",
x => x.Id,
"MediaItem",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
"MusicVideoMetadata",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Album = table.Column<string>("TEXT", nullable: true),
Plot = table.Column<string>("TEXT", nullable: true),
Artist = table.Column<string>("TEXT", nullable: true),
MusicVideoId = table.Column<int>("INTEGER", nullable: false),
MetadataKind = table.Column<int>("INTEGER", nullable: false),
Title = table.Column<string>("TEXT", nullable: true),
OriginalTitle = table.Column<string>("TEXT", nullable: true),
SortTitle = table.Column<string>("TEXT", nullable: true),
Year = table.Column<int>("INTEGER", nullable: true),
ReleaseDate = table.Column<DateTime>("TEXT", nullable: true),
DateAdded = table.Column<DateTime>("TEXT", nullable: false),
DateUpdated = table.Column<DateTime>("TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MusicVideoMetadata", x => x.Id);
table.ForeignKey(
"FK_MusicVideoMetadata_MusicVideo_MusicVideoId",
x => x.MusicVideoId,
"MusicVideo",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
"IX_Tag_MusicVideoMetadataId",
"Tag",
"MusicVideoMetadataId");
migrationBuilder.CreateIndex(
"IX_Studio_MusicVideoMetadataId",
"Studio",
"MusicVideoMetadataId");
migrationBuilder.CreateIndex(
"IX_MediaVersion_MusicVideoId",
"MediaVersion",
"MusicVideoId");
migrationBuilder.CreateIndex(
"IX_Genre_MusicVideoMetadataId",
"Genre",
"MusicVideoMetadataId");
migrationBuilder.CreateIndex(
"IX_Artwork_MusicVideoMetadataId",
"Artwork",
"MusicVideoMetadataId");
migrationBuilder.CreateIndex(
"IX_MusicVideoMetadata_MusicVideoId",
"MusicVideoMetadata",
"MusicVideoId");
migrationBuilder.AddForeignKey(
"FK_Artwork_MusicVideoMetadata_MusicVideoMetadataId",
"Artwork",
"MusicVideoMetadataId",
"MusicVideoMetadata",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
"FK_Genre_MusicVideoMetadata_MusicVideoMetadataId",
"Genre",
"MusicVideoMetadataId",
"MusicVideoMetadata",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
"FK_MediaVersion_MusicVideo_MusicVideoId",
"MediaVersion",
"MusicVideoId",
"MusicVideo",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
"FK_Studio_MusicVideoMetadata_MusicVideoMetadataId",
"Studio",
"MusicVideoMetadataId",
"MusicVideoMetadata",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
"FK_Tag_MusicVideoMetadata_MusicVideoMetadataId",
"Tag",
"MusicVideoMetadataId",
"MusicVideoMetadata",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
"FK_Artwork_MusicVideoMetadata_MusicVideoMetadataId",
"Artwork");
migrationBuilder.DropForeignKey(
"FK_Genre_MusicVideoMetadata_MusicVideoMetadataId",
"Genre");
migrationBuilder.DropForeignKey(
"FK_MediaVersion_MusicVideo_MusicVideoId",
"MediaVersion");
migrationBuilder.DropForeignKey(
"FK_Studio_MusicVideoMetadata_MusicVideoMetadataId",
"Studio");
migrationBuilder.DropForeignKey(
"FK_Tag_MusicVideoMetadata_MusicVideoMetadataId",
"Tag");
migrationBuilder.DropTable(
"MusicVideoMetadata");
migrationBuilder.DropTable(
"MusicVideo");
migrationBuilder.DropIndex(
"IX_Tag_MusicVideoMetadataId",
"Tag");
migrationBuilder.DropIndex(
"IX_Studio_MusicVideoMetadataId",
"Studio");
migrationBuilder.DropIndex(
"IX_MediaVersion_MusicVideoId",
"MediaVersion");
migrationBuilder.DropIndex(
"IX_Genre_MusicVideoMetadataId",
"Genre");
migrationBuilder.DropIndex(
"IX_Artwork_MusicVideoMetadataId",
"Artwork");
migrationBuilder.DropColumn(
"MusicVideoMetadataId",
"Tag");
migrationBuilder.DropColumn(
"MusicVideoMetadataId",
"Studio");
migrationBuilder.DropColumn(
"MusicVideoId",
"MediaVersion");
migrationBuilder.DropColumn(
"MusicVideoMetadataId",
"Genre");
migrationBuilder.DropColumn(
"MusicVideoMetadataId",
"Artwork");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,43 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Remove_MediaVersion_Codecs : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
"AudioCodec",
"MediaVersion");
migrationBuilder.DropColumn(
"VideoCodec",
"MediaVersion");
migrationBuilder.DropColumn(
"VideoProfile",
"MediaVersion");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
"AudioCodec",
"MediaVersion",
"TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
"VideoCodec",
"MediaVersion",
"TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
"VideoProfile",
"MediaVersion",
"TEXT",
nullable: true);
}
}
}
@@ -42,6 +42,9 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<int?>("MovieMetadataId")
.HasColumnType("INTEGER");
b.Property<int?>("MusicVideoMetadataId")
.HasColumnType("INTEGER");
b.Property<string>("Path")
.HasColumnType("TEXT");
@@ -59,6 +62,8 @@ namespace ErsatzTV.Infrastructure.Migrations
b.HasIndex("MovieMetadataId");
b.HasIndex("MusicVideoMetadataId");
b.HasIndex("SeasonMetadataId");
b.HasIndex("ShowMetadataId");
@@ -294,6 +299,9 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<int?>("MovieMetadataId")
.HasColumnType("INTEGER");
b.Property<int?>("MusicVideoMetadataId")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
@@ -309,6 +317,8 @@ namespace ErsatzTV.Infrastructure.Migrations
b.HasIndex("MovieMetadataId");
b.HasIndex("MusicVideoMetadataId");
b.HasIndex("SeasonMetadataId");
b.HasIndex("ShowMetadataId");
@@ -472,9 +482,6 @@ namespace ErsatzTV.Infrastructure.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudioCodec")
.HasColumnType("TEXT");
b.Property<DateTime>("DateAdded")
.HasColumnType("TEXT");
@@ -496,18 +503,15 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<int?>("MovieId")
.HasColumnType("INTEGER");
b.Property<int?>("MusicVideoId")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("SampleAspectRatio")
.HasColumnType("TEXT");
b.Property<string>("VideoCodec")
.HasColumnType("TEXT");
b.Property<string>("VideoProfile")
.HasColumnType("TEXT");
b.Property<int>("VideoScanKind")
.HasColumnType("INTEGER");
@@ -520,6 +524,8 @@ namespace ErsatzTV.Infrastructure.Migrations
b.HasIndex("MovieId");
b.HasIndex("MusicVideoId");
b.ToTable("MediaVersion");
});
@@ -574,6 +580,57 @@ namespace ErsatzTV.Infrastructure.Migrations
b.ToTable("MovieMetadata");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.MusicVideoMetadata",
b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Album")
.HasColumnType("TEXT");
b.Property<string>("Artist")
.HasColumnType("TEXT");
b.Property<DateTime>("DateAdded")
.HasColumnType("TEXT");
b.Property<DateTime>("DateUpdated")
.HasColumnType("TEXT");
b.Property<int>("MetadataKind")
.HasColumnType("INTEGER");
b.Property<int>("MusicVideoId")
.HasColumnType("INTEGER");
b.Property<string>("OriginalTitle")
.HasColumnType("TEXT");
b.Property<string>("Plot")
.HasColumnType("TEXT");
b.Property<DateTime?>("ReleaseDate")
.HasColumnType("TEXT");
b.Property<string>("SortTitle")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<int?>("Year")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("MusicVideoId");
b.ToTable("MusicVideoMetadata");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.Playout",
b =>
@@ -913,6 +970,9 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<int?>("MovieMetadataId")
.HasColumnType("INTEGER");
b.Property<int?>("MusicVideoMetadataId")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
@@ -928,6 +988,8 @@ namespace ErsatzTV.Infrastructure.Migrations
b.HasIndex("MovieMetadataId");
b.HasIndex("MusicVideoMetadataId");
b.HasIndex("SeasonMetadataId");
b.HasIndex("ShowMetadataId");
@@ -949,6 +1011,9 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<int?>("MovieMetadataId")
.HasColumnType("INTEGER");
b.Property<int?>("MusicVideoMetadataId")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
@@ -964,6 +1029,8 @@ namespace ErsatzTV.Infrastructure.Migrations
b.HasIndex("MovieMetadataId");
b.HasIndex("MusicVideoMetadataId");
b.HasIndex("SeasonMetadataId");
b.HasIndex("ShowMetadataId");
@@ -1036,6 +1103,15 @@ namespace ErsatzTV.Infrastructure.Migrations
b.ToTable("Movie");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.MusicVideo",
b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaItem");
b.ToTable("MusicVideo");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.Season",
b =>
@@ -1207,6 +1283,11 @@ namespace ErsatzTV.Infrastructure.Migrations
.HasForeignKey("MovieMetadataId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null)
.WithMany("Artwork")
.HasForeignKey("MusicVideoMetadataId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null)
.WithMany("Artwork")
.HasForeignKey("SeasonMetadataId")
@@ -1291,6 +1372,11 @@ namespace ErsatzTV.Infrastructure.Migrations
.HasForeignKey("MovieMetadataId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null)
.WithMany("Genres")
.HasForeignKey("MusicVideoMetadataId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null)
.WithMany("Genres")
.HasForeignKey("SeasonMetadataId");
@@ -1379,6 +1465,11 @@ namespace ErsatzTV.Infrastructure.Migrations
.WithMany("MediaVersions")
.HasForeignKey("MovieId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.MusicVideo", null)
.WithMany("MediaVersions")
.HasForeignKey("MusicVideoId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity(
@@ -1394,6 +1485,19 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Navigation("Movie");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.MusicVideoMetadata",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.MusicVideo", "MusicVideo")
.WithMany("MusicVideoMetadata")
.HasForeignKey("MusicVideoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("MusicVideo");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.Playout",
b =>
@@ -1627,6 +1731,11 @@ namespace ErsatzTV.Infrastructure.Migrations
.HasForeignKey("MovieMetadataId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null)
.WithMany("Studios")
.HasForeignKey("MusicVideoMetadataId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null)
.WithMany("Studios")
.HasForeignKey("SeasonMetadataId");
@@ -1650,6 +1759,11 @@ namespace ErsatzTV.Infrastructure.Migrations
.HasForeignKey("MovieMetadataId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null)
.WithMany("Tags")
.HasForeignKey("MusicVideoMetadataId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null)
.WithMany("Tags")
.HasForeignKey("SeasonMetadataId");
@@ -1723,6 +1837,17 @@ namespace ErsatzTV.Infrastructure.Migrations
.IsRequired();
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.MusicVideo",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaItem", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.MusicVideo", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.Season",
b =>
@@ -1917,6 +2042,19 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Navigation("Tags");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.MusicVideoMetadata",
b =>
{
b.Navigation("Artwork");
b.Navigation("Genres");
b.Navigation("Studios");
b.Navigation("Tags");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.Playout",
b =>
@@ -1979,6 +2117,15 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Navigation("MovieMetadata");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.MusicVideo",
b =>
{
b.Navigation("MediaVersions");
b.Navigation("MusicVideoMetadata");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.Season",
b =>
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
namespace ErsatzTV.Infrastructure.Plex.Models
{
@@ -29,6 +29,7 @@ namespace ErsatzTV.Infrastructure.Search
private const string IdField = "id";
private const string TypeField = "type";
private const string ArtistField = "artist";
private const string TitleField = "title";
private const string SortTitleField = "sort_title";
private const string GenreField = "genre";
@@ -42,6 +43,7 @@ namespace ErsatzTV.Infrastructure.Search
private const string MovieType = "movie";
private const string ShowType = "show";
private const string MusicVideoType = "music_video";
private static bool _isRebuilding;
@@ -93,6 +95,9 @@ namespace ErsatzTV.Infrastructure.Search
case Show show:
UpdateShow(show, writer);
break;
case MusicVideo musicVideo:
UpdateMusicVideo(musicVideo, writer);
break;
}
}
}
@@ -121,6 +126,9 @@ namespace ErsatzTV.Infrastructure.Search
case Show show:
UpdateShow(show, writer);
break;
case MusicVideo musicVideo:
UpdateMusicVideo(musicVideo, writer);
break;
}
}
@@ -338,6 +346,66 @@ namespace ErsatzTV.Infrastructure.Search
}
}
private void UpdateMusicVideo(MusicVideo musicVideo, IndexWriter writer)
{
Option<MusicVideoMetadata> maybeMetadata = musicVideo.MusicVideoMetadata.HeadOrNone();
if (maybeMetadata.IsSome)
{
MusicVideoMetadata metadata = maybeMetadata.ValueUnsafe();
try
{
var doc = new Document
{
new StringField(IdField, musicVideo.Id.ToString(), Field.Store.YES),
new StringField(TypeField, MusicVideoType, Field.Store.NO),
new TextField(ArtistField, metadata.Artist, Field.Store.NO),
new TextField(TitleField, metadata.Title, Field.Store.NO),
new StringField(SortTitleField, metadata.SortTitle.ToLowerInvariant(), Field.Store.NO),
new TextField(LibraryNameField, musicVideo.LibraryPath.Library.Name, Field.Store.NO),
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO),
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES)
};
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));
}
writer.UpdateDocument(new Term(IdField, musicVideo.Id.ToString()), doc);
}
catch (Exception ex)
{
metadata.MusicVideo = null;
_logger.LogWarning(ex, "Error indexing music video with metadata {@Metadata}", metadata);
}
}
}
private SearchItem ProjectToSearchItem(Document doc) => new(Convert.ToInt32(doc.Get(IdField)));
private Query ParseQuery(string searchQuery, QueryParser parser)