add artists as owners of music videos (#154)
* clean up genre, tag, studio orphans * enforce foreign keys at connection level * wip * fix fragment scroll offset * fix see all link for music videos * add fake artist metadata * not null artist id * add artist scanning * remove improperly named music videos * code cleanup * add artists to search results and collections * clean up music video metadata / artist * add artist view * show music videos on artist page * add music video artwork placeholder
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Configurations
|
||||
{
|
||||
public class ArtistConfiguration : IEntityTypeConfiguration<Artist>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Artist> builder)
|
||||
{
|
||||
builder.ToTable("Artist");
|
||||
|
||||
builder.HasMany(a => a.MusicVideos)
|
||||
.WithOne(mv => mv.Artist)
|
||||
.HasForeignKey(mv => mv.ArtistId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasMany(a => a.ArtistMetadata)
|
||||
.WithOne(am => am.Artist)
|
||||
.HasForeignKey(am => am.ArtistId)
|
||||
.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 ArtistMetadataConfiguration : IEntityTypeConfiguration<ArtistMetadata>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ArtistMetadata> builder)
|
||||
{
|
||||
builder.ToTable("ArtistMetadata");
|
||||
|
||||
builder.HasMany(sm => sm.Artwork)
|
||||
.WithOne()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasMany(sm => sm.Genres)
|
||||
.WithOne()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasMany(sm => sm.Styles)
|
||||
.WithOne()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasMany(sm => sm.Moods)
|
||||
.WithOne()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
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 ArtistRepository : IArtistRepository
|
||||
{
|
||||
private readonly IDbConnection _dbConnection;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public ArtistRepository(IDbContextFactory<TvContext> dbContextFactory, IDbConnection dbConnection)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_dbConnection = dbConnection;
|
||||
}
|
||||
|
||||
public async Task<Option<Artist>> GetArtistByMetadata(int libraryPathId, ArtistMetadata metadata)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
Option<int> maybeId = await dbContext.ArtistMetadata
|
||||
.Where(
|
||||
s => s.Title == metadata.Title && (metadata.MetadataKind == MetadataKind.Fallback ||
|
||||
s.Disambiguation == metadata.Disambiguation))
|
||||
.Where(s => s.Artist.LibraryPathId == libraryPathId)
|
||||
.SingleOrDefaultAsync()
|
||||
.Map(Optional)
|
||||
.MapT(sm => sm.ArtistId);
|
||||
|
||||
return await maybeId.Match(
|
||||
id =>
|
||||
{
|
||||
return dbContext.Artists
|
||||
.AsNoTracking()
|
||||
.Include(s => s.ArtistMetadata)
|
||||
.ThenInclude(sm => sm.Artwork)
|
||||
.Include(s => s.ArtistMetadata)
|
||||
.ThenInclude(sm => sm.Genres)
|
||||
.Include(s => s.ArtistMetadata)
|
||||
.ThenInclude(sm => sm.Styles)
|
||||
.Include(s => s.ArtistMetadata)
|
||||
.ThenInclude(sm => sm.Moods)
|
||||
.Include(s => s.LibraryPath)
|
||||
.ThenInclude(lp => lp.Library)
|
||||
.OrderBy(s => s.Id)
|
||||
.SingleOrDefaultAsync(s => s.Id == id)
|
||||
.Map(Optional);
|
||||
},
|
||||
() => Option<Artist>.None.AsTask());
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, MediaItemScanResult<Artist>>> AddArtist(
|
||||
int libraryPathId,
|
||||
string artistFolder,
|
||||
ArtistMetadata metadata)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
|
||||
try
|
||||
{
|
||||
metadata.DateAdded = DateTime.UtcNow;
|
||||
metadata.Genres ??= new List<Genre>();
|
||||
metadata.Styles ??= new List<Style>();
|
||||
metadata.Moods ??= new List<Mood>();
|
||||
var artist = new Artist
|
||||
{
|
||||
LibraryPathId = libraryPathId,
|
||||
ArtistMetadata = new List<ArtistMetadata> { metadata }
|
||||
};
|
||||
|
||||
await dbContext.Artists.AddAsync(artist);
|
||||
await dbContext.SaveChangesAsync();
|
||||
await dbContext.Entry(artist).Reference(s => s.LibraryPath).LoadAsync();
|
||||
await dbContext.Entry(artist.LibraryPath).Reference(lp => lp.Library).LoadAsync();
|
||||
|
||||
return new MediaItemScanResult<Artist>(artist) { IsAdded = true };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BaseError.New(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<int>> DeleteEmptyArtists(LibraryPath libraryPath)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
List<Artist> artists = await dbContext.Artists
|
||||
.Filter(a => a.LibraryPathId == libraryPath.Id)
|
||||
.Filter(a => a.MusicVideos.Count == 0)
|
||||
.ToListAsync();
|
||||
var ids = artists.Map(a => a.Id).ToList();
|
||||
dbContext.Artists.RemoveRange(artists);
|
||||
await dbContext.SaveChangesAsync();
|
||||
return ids;
|
||||
}
|
||||
|
||||
public async Task<Option<Artist>> GetArtist(int artistId)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
return await dbContext.Artists
|
||||
.Include(m => m.ArtistMetadata)
|
||||
.ThenInclude(m => m.Artwork)
|
||||
.Include(m => m.ArtistMetadata)
|
||||
.ThenInclude(m => m.Genres)
|
||||
.Include(m => m.ArtistMetadata)
|
||||
.ThenInclude(m => m.Styles)
|
||||
.Include(m => m.ArtistMetadata)
|
||||
.ThenInclude(m => m.Moods)
|
||||
.OrderBy(m => m.Id)
|
||||
.SingleOrDefaultAsync(m => m.Id == artistId)
|
||||
.Map(Optional);
|
||||
}
|
||||
|
||||
public async Task<List<ArtistMetadata>> GetArtistsForCards(List<int> ids)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
return await dbContext.ArtistMetadata
|
||||
.AsNoTracking()
|
||||
.Filter(am => ids.Contains(am.ArtistId))
|
||||
.Include(am => am.Artwork)
|
||||
.OrderBy(am => am.SortTitle)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public Task<bool> AddGenre(ArtistMetadata metadata, Genre genre) =>
|
||||
_dbConnection.ExecuteAsync(
|
||||
"INSERT INTO Genre (Name, ArtistMetadataId) VALUES (@Name, @MetadataId)",
|
||||
new { genre.Name, MetadataId = metadata.Id }).Map(result => result > 0);
|
||||
|
||||
public Task<bool> AddStyle(ArtistMetadata metadata, Style style) =>
|
||||
_dbConnection.ExecuteAsync(
|
||||
"INSERT INTO Style (Name, ArtistMetadataId) VALUES (@Name, @MetadataId)",
|
||||
new { style.Name, MetadataId = metadata.Id }).Map(result => result > 0);
|
||||
|
||||
public Task<bool> AddMood(ArtistMetadata metadata, Mood mood) =>
|
||||
_dbConnection.ExecuteAsync(
|
||||
"INSERT INTO Mood (Name, ArtistMetadataId) VALUES (@Name, @MetadataId)",
|
||||
new { mood.Name, MetadataId = metadata.Id }).Map(result => result > 0);
|
||||
}
|
||||
}
|
||||
@@ -130,9 +130,15 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
.ThenInclude(i => (i as Movie).MovieMetadata)
|
||||
.ThenInclude(mm => mm.Artwork)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as Artist).ArtistMetadata)
|
||||
.ThenInclude(mvm => mvm.Artwork)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as MusicVideo).MusicVideoMetadata)
|
||||
.ThenInclude(mvm => mvm.Artwork)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as MusicVideo).Artist)
|
||||
.ThenInclude(a => a.ArtistMetadata)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as Show).ShowMetadata)
|
||||
.ThenInclude(sm => sm.Artwork)
|
||||
.Include(c => c.MediaItems)
|
||||
@@ -201,6 +207,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
result.AddRange(await GetShowItems(collection));
|
||||
result.AddRange(await GetSeasonItems(collection));
|
||||
result.AddRange(await GetEpisodeItems(collection));
|
||||
result.AddRange(await GetArtistItems(collection));
|
||||
result.AddRange(await GetMusicVideoItems(collection));
|
||||
|
||||
return result.Distinct().ToList();
|
||||
@@ -220,6 +227,25 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
.Filter(m => ids.Contains(m.Id))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
private async Task<List<MusicVideo>> GetArtistItems(Collection collection)
|
||||
{
|
||||
IEnumerable<int> ids = await _dbConnection.QueryAsync<int>(
|
||||
@"SELECT MusicVideo.Id FROM CollectionItem ci
|
||||
INNER JOIN Artist on Artist.Id = ci.MediaItemId
|
||||
INNER JOIN MusicVideo on Artist.Id = MusicVideo.ArtistId
|
||||
WHERE ci.CollectionId = @CollectionId",
|
||||
new { CollectionId = collection.Id });
|
||||
|
||||
return await _dbContext.MusicVideos
|
||||
.Include(m => m.Artist)
|
||||
.ThenInclude(a => a.ArtistMetadata)
|
||||
.Include(m => m.MusicVideoMetadata)
|
||||
.Include(m => m.MediaVersions)
|
||||
.Filter(m => ids.Contains(m.Id))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
private async Task<List<MusicVideo>> GetMusicVideoItems(Collection collection)
|
||||
{
|
||||
@@ -230,6 +256,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
new { CollectionId = collection.Id });
|
||||
|
||||
return await _dbContext.MusicVideos
|
||||
.Include(m => m.Artist)
|
||||
.ThenInclude(a => a.ArtistMetadata)
|
||||
.Include(m => m.MusicVideoMetadata)
|
||||
.Include(m => m.MediaVersions)
|
||||
.Filter(m => ids.Contains(m.Id))
|
||||
@@ -250,6 +278,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
.Include(e => e.EpisodeMetadata)
|
||||
.Include(e => e.MediaVersions)
|
||||
.Include(e => e.Season)
|
||||
.ThenInclude(s => s.Show)
|
||||
.ThenInclude(s => s.ShowMetadata)
|
||||
.Filter(e => ids.Contains(e.Id))
|
||||
.ToListAsync();
|
||||
}
|
||||
@@ -267,6 +297,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
.Include(e => e.EpisodeMetadata)
|
||||
.Include(e => e.MediaVersions)
|
||||
.Include(e => e.Season)
|
||||
.ThenInclude(s => s.Show)
|
||||
.ThenInclude(s => s.ShowMetadata)
|
||||
.Filter(e => ids.Contains(e.Id))
|
||||
.ToListAsync();
|
||||
}
|
||||
@@ -283,6 +315,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
.Include(e => e.EpisodeMetadata)
|
||||
.Include(e => e.MediaVersions)
|
||||
.Include(e => e.Season)
|
||||
.ThenInclude(s => s.Show)
|
||||
.ThenInclude(s => s.ShowMetadata)
|
||||
.Filter(e => ids.Contains(e.Id))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
@@ -156,6 +156,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path)",
|
||||
parameters)
|
||||
.ToUnit(),
|
||||
ArtistMetadata => _dbConnection.ExecuteAsync(
|
||||
@"INSERT INTO Artwork (ArtworkKind, ArtistMetadataId, DateAdded, DateUpdated, Path)
|
||||
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)",
|
||||
@@ -197,5 +202,13 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
public Task<bool> RemoveStudio(Studio studio) =>
|
||||
_dbConnection.ExecuteAsync("DELETE FROM Studio WHERE Id = @StudioId", new { StudioId = studio.Id })
|
||||
.Map(result => result > 0);
|
||||
|
||||
public Task<bool> RemoveStyle(Style style) =>
|
||||
_dbConnection.ExecuteAsync("DELETE FROM Style WHERE Id = @StyleId", new { StyleId = style.Id })
|
||||
.Map(result => result > 0);
|
||||
|
||||
public Task<bool> RemoveMood(Mood mood) =>
|
||||
_dbConnection.ExecuteAsync("DELETE FROM Mood WHERE Id = @MoodId", new { MoodId = mood.Id })
|
||||
.Map(result => result > 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> GetOrAdd(
|
||||
Artist artist,
|
||||
LibraryPath libraryPath,
|
||||
string path)
|
||||
{
|
||||
@@ -50,10 +51,22 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
.SingleOrDefaultAsync(i => i.MediaVersions.First().MediaFiles.First().Path == path);
|
||||
|
||||
return await maybeExisting.Match(
|
||||
mediaItem =>
|
||||
Right<BaseError, MediaItemScanResult<MusicVideo>>(
|
||||
new MediaItemScanResult<MusicVideo>(mediaItem) { IsAdded = false }).AsTask(),
|
||||
async () => await AddMusicVideo(dbContext, libraryPath.Id, path));
|
||||
async mediaItem =>
|
||||
{
|
||||
if (mediaItem.ArtistId != artist.Id)
|
||||
{
|
||||
await _dbConnection.ExecuteAsync(
|
||||
@"UPDATE MusicVideo SET ArtistId = @ArtistId WHERE Id = @Id",
|
||||
new { mediaItem.Id, ArtistId = artist.Id });
|
||||
|
||||
mediaItem.ArtistId = artist.Id;
|
||||
mediaItem.Artist = artist;
|
||||
}
|
||||
|
||||
return Right<BaseError, MediaItemScanResult<MusicVideo>>(
|
||||
new MediaItemScanResult<MusicVideo>(mediaItem) { IsAdded = false });
|
||||
},
|
||||
async () => await AddMusicVideo(dbContext, artist, libraryPath.Id, path));
|
||||
}
|
||||
|
||||
public Task<IEnumerable<string>> FindMusicVideoPaths(LibraryPath libraryPath) =>
|
||||
@@ -110,6 +123,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
return await dbContext.MusicVideoMetadata
|
||||
.AsNoTracking()
|
||||
.Filter(mvm => ids.Contains(mvm.MusicVideoId))
|
||||
.Include(mvm => mvm.MusicVideo)
|
||||
.ThenInclude(mv => mv.Artist)
|
||||
.ThenInclude(a => a.ArtistMetadata)
|
||||
.Include(mvm => mvm.Artwork)
|
||||
.OrderBy(mvm => mvm.SortTitle)
|
||||
.ToListAsync();
|
||||
@@ -132,8 +148,44 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
.Map(Optional);
|
||||
}
|
||||
|
||||
public Task<IEnumerable<string>> FindOrphanPaths(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
|
||||
AND NOT EXISTS (SELECT * FROM MusicVideoMetadata WHERE MusicVideoId = M.Id)",
|
||||
new { LibraryPathId = libraryPath.Id });
|
||||
|
||||
public Task<int> GetMusicVideoCount(int artistId) =>
|
||||
_dbConnection.QuerySingleAsync<int>(
|
||||
@"SELECT COUNT(*) FROM MusicVideo WHERE ArtistId = @ArtistId",
|
||||
new { ArtistId = artistId });
|
||||
|
||||
public async Task<List<MusicVideoMetadata>> GetPagedMusicVideos(int artistId, int pageNumber, int pageSize)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
return await dbContext.MusicVideoMetadata
|
||||
.AsNoTracking()
|
||||
.Include(m => m.Artwork)
|
||||
.Include(m => m.Genres)
|
||||
.Include(m => m.Tags)
|
||||
.Include(m => m.Studios)
|
||||
.Include(m => m.MusicVideo)
|
||||
.ThenInclude(mv => mv.Artist)
|
||||
.ThenInclude(a => a.ArtistMetadata)
|
||||
.Filter(m => m.MusicVideo.ArtistId == artistId)
|
||||
.OrderBy(m => m.SortTitle)
|
||||
.Skip((pageNumber - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
private static async Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> AddMusicVideo(
|
||||
TvContext dbContext,
|
||||
Artist artist,
|
||||
int libraryPathId,
|
||||
string path)
|
||||
{
|
||||
@@ -141,6 +193,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
{
|
||||
var musicVideo = new MusicVideo
|
||||
{
|
||||
ArtistId = artist.Id,
|
||||
LibraryPathId = libraryPathId,
|
||||
MediaVersions = new List<MediaVersion>
|
||||
{
|
||||
|
||||
@@ -101,6 +101,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as MusicVideo).MediaVersions)
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as MusicVideo).Artist)
|
||||
.ThenInclude(mm => mm.ArtistMetadata)
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as Episode).EpisodeMetadata)
|
||||
.ThenInclude(em => em.Artwork)
|
||||
.Include(i => i.MediaItem)
|
||||
|
||||
@@ -55,90 +55,17 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
.ThenInclude(mm => mm.Studios)
|
||||
.Include(mi => (mi as MusicVideo).MediaVersions)
|
||||
.ThenInclude(mm => mm.Streams)
|
||||
.Include(mi => (mi as Artist).ArtistMetadata)
|
||||
.ThenInclude(mm => mm.Genres)
|
||||
.Include(mi => (mi as Artist).ArtistMetadata)
|
||||
.ThenInclude(mm => mm.Styles)
|
||||
.Include(mi => (mi as Artist).ArtistMetadata)
|
||||
.ThenInclude(mm => mm.Moods)
|
||||
.OrderBy(mi => mi.Id)
|
||||
.SingleOrDefaultAsync(mi => mi.Id == id)
|
||||
.Map(Optional);
|
||||
}
|
||||
|
||||
public async Task<List<MediaItem>> SearchMediaItemsByTitle(string query)
|
||||
{
|
||||
List<int> ids = await _dbConnection.QueryAsync<int>(
|
||||
@"SELECT M.Id FROM Movie M
|
||||
INNER JOIN MovieMetadata MM on M.Id = MM.MovieId
|
||||
WHERE MM.Title LIKE @Query
|
||||
UNION
|
||||
SELECT S.Id FROM Show S
|
||||
INNER JOIN ShowMetadata SM on S.Id = SM.ShowId
|
||||
WHERE SM.Title LIKE @Query
|
||||
GROUP BY SM.Title, SM.Year",
|
||||
new { Query = $"%{query}%" })
|
||||
.Map(results => results.ToList());
|
||||
|
||||
await using TvContext context = _dbContextFactory.CreateDbContext();
|
||||
return await context.MediaItems
|
||||
.Filter(m => ids.Contains(m.Id))
|
||||
.Include(m => (m as Movie).MovieMetadata)
|
||||
.ThenInclude(mm => mm.Artwork)
|
||||
.Include(m => (m as Show).ShowMetadata)
|
||||
.ThenInclude(mm => mm.Artwork)
|
||||
.OfType<MediaItem>()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<MediaItem>> SearchMediaItemsByGenre(string genre)
|
||||
{
|
||||
List<int> ids = await _dbConnection.QueryAsync<int>(
|
||||
@"SELECT M.Id FROM Movie M
|
||||
INNER JOIN MovieMetadata MM on M.Id = MM.MovieId
|
||||
INNER JOIN Genre G on MM.Id = G.MovieMetadataId
|
||||
WHERE G.Name LIKE @Query
|
||||
UNION
|
||||
SELECT S.Id FROM Show S
|
||||
INNER JOIN ShowMetadata SM on S.Id = SM.ShowId
|
||||
INNER JOIN Genre G2 on SM.Id = G2.ShowMetadataId
|
||||
WHERE G2.Name LIKE @Query
|
||||
GROUP BY SM.Title, SM.Year",
|
||||
new { Query = genre })
|
||||
.Map(results => results.ToList());
|
||||
|
||||
await using TvContext context = _dbContextFactory.CreateDbContext();
|
||||
return await context.MediaItems
|
||||
.Filter(m => ids.Contains(m.Id))
|
||||
.Include(m => (m as Movie).MovieMetadata)
|
||||
.ThenInclude(mm => mm.Artwork)
|
||||
.Include(m => (m as Show).ShowMetadata)
|
||||
.ThenInclude(mm => mm.Artwork)
|
||||
.OfType<MediaItem>()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<MediaItem>> SearchMediaItemsByTag(string tag)
|
||||
{
|
||||
List<int> ids = await _dbConnection.QueryAsync<int>(
|
||||
@"SELECT M.Id FROM Movie M
|
||||
INNER JOIN MovieMetadata MM on M.Id = MM.MovieId
|
||||
INNER JOIN Tag T on MM.Id = T.MovieMetadataId
|
||||
WHERE T.Name LIKE @Query
|
||||
UNION
|
||||
SELECT S.Id FROM Show S
|
||||
INNER JOIN ShowMetadata SM on S.Id = SM.ShowId
|
||||
INNER JOIN Tag T2 on SM.Id = T2.ShowMetadataId
|
||||
WHERE T2.Name LIKE @Query
|
||||
GROUP BY SM.Title, SM.Year",
|
||||
new { Query = tag })
|
||||
.Map(results => results.ToList());
|
||||
|
||||
await using TvContext context = _dbContextFactory.CreateDbContext();
|
||||
return await context.MediaItems
|
||||
.Filter(m => ids.Contains(m.Id))
|
||||
.Include(m => (m as Movie).MovieMetadata)
|
||||
.ThenInclude(mm => mm.Artwork)
|
||||
.Include(m => (m as Show).ShowMetadata)
|
||||
.ThenInclude(mm => mm.Artwork)
|
||||
.OfType<MediaItem>()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public Task<List<string>> GetLanguagesForShow(Show show) =>
|
||||
_dbConnection.QueryAsync<string>(
|
||||
@"SELECT DISTINCT Language
|
||||
@@ -148,5 +75,15 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
INNER JOIN Season S ON E.SeasonId = S.Id
|
||||
WHERE MediaStreamKind = 2 AND S.ShowId = @ShowId",
|
||||
new { ShowId = show.Id }).Map(result => result.ToList());
|
||||
|
||||
public Task<List<string>> GetLanguagesForArtist(Artist artist) =>
|
||||
_dbConnection.QueryAsync<string>(
|
||||
@"SELECT DISTINCT Language
|
||||
FROM MediaStream
|
||||
INNER JOIN MediaVersion V ON MediaStream.MediaVersionId = V.Id
|
||||
INNER JOIN MusicVideo MV ON V.MusicVideoId = MV.Id
|
||||
INNER JOIN Artist A on MV.ArtistId = A.Id
|
||||
WHERE MediaStreamKind = 2 AND A.Id = @ArtistId",
|
||||
new { ArtistId = artist.Id }).Map(result => result.ToList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Artist> Artists { get; set; }
|
||||
public DbSet<ArtistMetadata> ArtistMetadata { get; set; }
|
||||
public DbSet<MusicVideo> MusicVideos { get; set; }
|
||||
public DbSet<MusicVideoMetadata> MusicVideoMetadata { get; set; }
|
||||
public DbSet<Show> Shows { get; set; }
|
||||
|
||||
Generated
+1964
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Delete_Orphan_GenreTagStudio : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(
|
||||
@"DELETE FROM Genre
|
||||
WHERE MovieMetadataId NOT IN (SELECT Id FROM MovieMetadata)
|
||||
OR ShowMetadataId NOT IN (SELECT Id FROM Show)
|
||||
OR SeasonMetadataId NOT IN (SELECT Id FROM Season)
|
||||
OR EpisodeMetadataId NOT IN (SELECT Id FROM Episode)
|
||||
OR MusicVideoMetadataId NOT IN (SELECT Id FROM MusicVideoMetadata)");
|
||||
|
||||
migrationBuilder.Sql(
|
||||
@"DELETE FROM Tag
|
||||
WHERE MovieMetadataId NOT IN (SELECT Id FROM MovieMetadata)
|
||||
OR ShowMetadataId NOT IN (SELECT Id FROM Show)
|
||||
OR SeasonMetadataId NOT IN (SELECT Id FROM Season)
|
||||
OR EpisodeMetadataId NOT IN (SELECT Id FROM Episode)
|
||||
OR MusicVideoMetadataId NOT IN (SELECT Id FROM MusicVideoMetadata)");
|
||||
|
||||
migrationBuilder.Sql(
|
||||
@"DELETE FROM Studio
|
||||
WHERE MovieMetadataId NOT IN (SELECT Id FROM MovieMetadata)
|
||||
OR ShowMetadataId NOT IN (SELECT Id FROM Show)
|
||||
OR SeasonMetadataId NOT IN (SELECT Id FROM Season)
|
||||
OR EpisodeMetadataId NOT IN (SELECT Id FROM Episode)
|
||||
OR MusicVideoMetadataId NOT IN (SELECT Id FROM MusicVideoMetadata)");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,284 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Add_Artist : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
"ArtistMetadataId",
|
||||
"Tag",
|
||||
"INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
"ArtistMetadataId",
|
||||
"Studio",
|
||||
"INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
"ArtistId",
|
||||
"MusicVideo",
|
||||
"INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
"ArtistMetadataId",
|
||||
"Genre",
|
||||
"INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
"ArtistMetadataId",
|
||||
"Artwork",
|
||||
"INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"Artist",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Artist", x => x.Id);
|
||||
table.ForeignKey(
|
||||
"FK_Artist_MediaItem_Id",
|
||||
x => x.Id,
|
||||
"MediaItem",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"ArtistMetadata",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Disambiguation = table.Column<string>("TEXT", nullable: true),
|
||||
Biography = table.Column<string>("TEXT", nullable: true),
|
||||
Formed = table.Column<string>("TEXT", nullable: true),
|
||||
ArtistId = table.Column<int>("INTEGER", nullable: true),
|
||||
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_ArtistMetadata", x => x.Id);
|
||||
table.ForeignKey(
|
||||
"FK_ArtistMetadata_Artist_ArtistId",
|
||||
x => x.ArtistId,
|
||||
"Artist",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"Mood",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Name = table.Column<string>("TEXT", nullable: true),
|
||||
ArtistMetadataId = table.Column<int>("INTEGER", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Mood", x => x.Id);
|
||||
table.ForeignKey(
|
||||
"FK_Mood_ArtistMetadata_ArtistMetadataId",
|
||||
x => x.ArtistMetadataId,
|
||||
"ArtistMetadata",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"Style",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Name = table.Column<string>("TEXT", nullable: true),
|
||||
ArtistMetadataId = table.Column<int>("INTEGER", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Style", x => x.Id);
|
||||
table.ForeignKey(
|
||||
"FK_Style_ArtistMetadata_ArtistMetadataId",
|
||||
x => x.ArtistMetadataId,
|
||||
"ArtistMetadata",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_Tag_ArtistMetadataId",
|
||||
"Tag",
|
||||
"ArtistMetadataId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_Studio_ArtistMetadataId",
|
||||
"Studio",
|
||||
"ArtistMetadataId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_MusicVideo_ArtistId",
|
||||
"MusicVideo",
|
||||
"ArtistId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_Genre_ArtistMetadataId",
|
||||
"Genre",
|
||||
"ArtistMetadataId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_Artwork_ArtistMetadataId",
|
||||
"Artwork",
|
||||
"ArtistMetadataId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_ArtistMetadata_ArtistId",
|
||||
"ArtistMetadata",
|
||||
"ArtistId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_Mood_ArtistMetadataId",
|
||||
"Mood",
|
||||
"ArtistMetadataId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_Style_ArtistMetadataId",
|
||||
"Style",
|
||||
"ArtistMetadataId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
"FK_Artwork_ArtistMetadata_ArtistMetadataId",
|
||||
"Artwork",
|
||||
"ArtistMetadataId",
|
||||
"ArtistMetadata",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
"FK_Genre_ArtistMetadata_ArtistMetadataId",
|
||||
"Genre",
|
||||
"ArtistMetadataId",
|
||||
"ArtistMetadata",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
"FK_MusicVideo_Artist_ArtistId",
|
||||
"MusicVideo",
|
||||
"ArtistId",
|
||||
"Artist",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
"FK_Studio_ArtistMetadata_ArtistMetadataId",
|
||||
"Studio",
|
||||
"ArtistMetadataId",
|
||||
"ArtistMetadata",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
"FK_Tag_ArtistMetadata_ArtistMetadataId",
|
||||
"Tag",
|
||||
"ArtistMetadataId",
|
||||
"ArtistMetadata",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
"FK_Artwork_ArtistMetadata_ArtistMetadataId",
|
||||
"Artwork");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
"FK_Genre_ArtistMetadata_ArtistMetadataId",
|
||||
"Genre");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
"FK_MusicVideo_Artist_ArtistId",
|
||||
"MusicVideo");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
"FK_Studio_ArtistMetadata_ArtistMetadataId",
|
||||
"Studio");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
"FK_Tag_ArtistMetadata_ArtistMetadataId",
|
||||
"Tag");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"Mood");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"Style");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"ArtistMetadata");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"Artist");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
"IX_Tag_ArtistMetadataId",
|
||||
"Tag");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
"IX_Studio_ArtistMetadataId",
|
||||
"Studio");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
"IX_MusicVideo_ArtistId",
|
||||
"MusicVideo");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
"IX_Genre_ArtistMetadataId",
|
||||
"Genre");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
"IX_Artwork_ArtistMetadataId",
|
||||
"Artwork");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
"ArtistMetadataId",
|
||||
"Tag");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
"ArtistMetadataId",
|
||||
"Studio");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
"ArtistId",
|
||||
"MusicVideo");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
"ArtistMetadataId",
|
||||
"Genre");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
"ArtistMetadataId",
|
||||
"Artwork");
|
||||
}
|
||||
}
|
||||
}
|
||||
+2165
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Add_ArtistMetadata : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
var now = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.FFFFFFF");
|
||||
|
||||
migrationBuilder.Sql(
|
||||
@"INSERT INTO MediaItem (LibraryPathId)
|
||||
SELECT LibraryPath.Id FROM LibraryPath INNER JOIN Library L on LibraryPath.LibraryId = L.Id
|
||||
WHERE MediaKind = 3");
|
||||
|
||||
migrationBuilder.Sql(
|
||||
@"INSERT INTO Artist (Id)
|
||||
SELECT MediaItem.Id FROM MediaItem
|
||||
INNER JOIN LibraryPath LP on MediaItem.LibraryPathId = LP.Id
|
||||
INNER JOIN Library L on LP.LibraryId = L.Id
|
||||
WHERE MediaKind = 3 AND NOT EXISTS (SELECT Id FROM MusicVideo WHERE Id = MediaItem.Id)");
|
||||
|
||||
migrationBuilder.Sql(
|
||||
$@"INSERT INTO ArtistMetadata (ArtistId, Title, DateAdded, DateUpdated, MetadataKind)
|
||||
SELECT Id, '[FAKE ARTIST]', '{now}', '{now}', 0 FROM Artist");
|
||||
|
||||
migrationBuilder.Sql(
|
||||
@"UPDATE MusicVideo SET ArtistId =
|
||||
(SELECT Artist.Id FROM Artist
|
||||
INNER JOIN MediaItem MIA on Artist.Id = MIA.Id
|
||||
INNER JOIN MediaItem MIMV on MusicVideo.Id = MIMV.Id
|
||||
INNER JOIN LibraryPath LPA on MIA.LibraryPathId = LPA.Id
|
||||
INNER JOIN LibraryPath LPMV on MIMV.LibraryPathId = LPMV.Id
|
||||
WHERE LPA.LibraryId = LPMV.LibraryId)");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
+2167
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,47 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Update_ArtistMetadata_FK : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
"ArtistId",
|
||||
"MusicVideo",
|
||||
"INTEGER",
|
||||
nullable: false,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "INTEGER",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
"ArtistId",
|
||||
"ArtistMetadata",
|
||||
"INTEGER",
|
||||
nullable: false,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "INTEGER",
|
||||
oldNullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
"ArtistId",
|
||||
"MusicVideo",
|
||||
"INTEGER",
|
||||
nullable: true,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "INTEGER");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
"ArtistId",
|
||||
"ArtistMetadata",
|
||||
"INTEGER",
|
||||
nullable: true,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "INTEGER");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+2164
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Delete_MusicVideoMetadata_Artist : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder) =>
|
||||
migrationBuilder.DropColumn(
|
||||
"Artist",
|
||||
"MusicVideoMetadata");
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder) =>
|
||||
migrationBuilder.AddColumn<string>(
|
||||
"Artist",
|
||||
"MusicVideoMetadata",
|
||||
"TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,57 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "5.0.4");
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.ArtistMetadata",
|
||||
b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ArtistId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Biography")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateUpdated")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Disambiguation")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Formed")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("MetadataKind")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("OriginalTitle")
|
||||
.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("ArtistId");
|
||||
|
||||
b.ToTable("ArtistMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.Artwork",
|
||||
b =>
|
||||
@@ -24,6 +75,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("ArtistMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ArtworkKind")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -56,6 +110,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ArtistMetadataId");
|
||||
|
||||
b.HasIndex("ChannelId");
|
||||
|
||||
b.HasIndex("EpisodeMetadataId");
|
||||
@@ -293,6 +349,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("ArtistMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("EpisodeMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -313,6 +372,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ArtistMetadataId");
|
||||
|
||||
b.HasIndex("EpisodeMetadataId");
|
||||
|
||||
b.HasIndex("MovieMetadataId");
|
||||
@@ -532,6 +593,27 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.ToTable("MediaVersion");
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.Mood",
|
||||
b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("ArtistMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ArtistMetadataId");
|
||||
|
||||
b.ToTable("Mood");
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.MovieMetadata",
|
||||
b =>
|
||||
@@ -594,9 +676,6 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<string>("Album")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Artist")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -967,6 +1046,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("ArtistMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("EpisodeMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -987,6 +1069,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ArtistMetadataId");
|
||||
|
||||
b.HasIndex("EpisodeMetadataId");
|
||||
|
||||
b.HasIndex("MovieMetadataId");
|
||||
@@ -1000,6 +1084,27 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.ToTable("Studio");
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.Style",
|
||||
b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("ArtistMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ArtistMetadataId");
|
||||
|
||||
b.ToTable("Style");
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.Tag",
|
||||
b =>
|
||||
@@ -1008,6 +1113,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("ArtistMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("EpisodeMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -1028,6 +1136,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ArtistMetadataId");
|
||||
|
||||
b.HasIndex("EpisodeMetadataId");
|
||||
|
||||
b.HasIndex("MovieMetadataId");
|
||||
@@ -1080,6 +1190,15 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.ToTable("PlexMediaFile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.Artist",
|
||||
b =>
|
||||
{
|
||||
b.HasBaseType("ErsatzTV.Core.Domain.MediaItem");
|
||||
|
||||
b.ToTable("Artist");
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.Episode",
|
||||
b =>
|
||||
@@ -1112,6 +1231,11 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
b.HasBaseType("ErsatzTV.Core.Domain.MediaItem");
|
||||
|
||||
b.Property<int>("ArtistId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasIndex("ArtistId");
|
||||
|
||||
b.ToTable("MusicVideo");
|
||||
});
|
||||
|
||||
@@ -1267,10 +1391,28 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.ToTable("PlexShow");
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.ArtistMetadata",
|
||||
b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.Artist", "Artist")
|
||||
.WithMany("ArtistMetadata")
|
||||
.HasForeignKey("ArtistId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Artist");
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.Artwork",
|
||||
b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null)
|
||||
.WithMany("Artwork")
|
||||
.HasForeignKey("ArtistMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.Channel", null)
|
||||
.WithMany("Artwork")
|
||||
.HasForeignKey("ChannelId")
|
||||
@@ -1366,6 +1508,11 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
"ErsatzTV.Core.Domain.Genre",
|
||||
b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null)
|
||||
.WithMany("Genres")
|
||||
.HasForeignKey("ArtistMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null)
|
||||
.WithMany("Genres")
|
||||
.HasForeignKey("EpisodeMetadataId");
|
||||
@@ -1475,6 +1622,16 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.Mood",
|
||||
b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null)
|
||||
.WithMany("Moods")
|
||||
.HasForeignKey("ArtistMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.MovieMetadata",
|
||||
b =>
|
||||
@@ -1725,6 +1882,10 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
"ErsatzTV.Core.Domain.Studio",
|
||||
b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null)
|
||||
.WithMany("Studios")
|
||||
.HasForeignKey("ArtistMetadataId");
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null)
|
||||
.WithMany("Studios")
|
||||
.HasForeignKey("EpisodeMetadataId");
|
||||
@@ -1749,10 +1910,24 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.Style",
|
||||
b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null)
|
||||
.WithMany("Styles")
|
||||
.HasForeignKey("ArtistMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.Tag",
|
||||
b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null)
|
||||
.WithMany("Tags")
|
||||
.HasForeignKey("ArtistMetadataId");
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null)
|
||||
.WithMany("Tags")
|
||||
.HasForeignKey("EpisodeMetadataId");
|
||||
@@ -1810,6 +1985,17 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.Artist",
|
||||
b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.MediaItem", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("ErsatzTV.Core.Domain.Artist", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.Episode",
|
||||
b =>
|
||||
@@ -1844,11 +2030,19 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
"ErsatzTV.Core.Domain.MusicVideo",
|
||||
b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.Artist", "Artist")
|
||||
.WithMany("MusicVideos")
|
||||
.HasForeignKey("ArtistId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.MediaItem", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("ErsatzTV.Core.Domain.MusicVideo", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Artist");
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
@@ -1991,6 +2185,23 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.ArtistMetadata",
|
||||
b =>
|
||||
{
|
||||
b.Navigation("Artwork");
|
||||
|
||||
b.Navigation("Genres");
|
||||
|
||||
b.Navigation("Moods");
|
||||
|
||||
b.Navigation("Studios");
|
||||
|
||||
b.Navigation("Styles");
|
||||
|
||||
b.Navigation("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.Channel",
|
||||
b =>
|
||||
@@ -2102,6 +2313,15 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Navigation("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.Artist",
|
||||
b =>
|
||||
{
|
||||
b.Navigation("ArtistMetadata");
|
||||
|
||||
b.Navigation("MusicVideos");
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.Episode",
|
||||
b =>
|
||||
|
||||
@@ -29,7 +29,6 @@ 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";
|
||||
@@ -41,9 +40,12 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
private const string ReleaseDateField = "release_date";
|
||||
private const string StudioField = "studio";
|
||||
private const string LanguageField = "language";
|
||||
private const string StyleField = "style";
|
||||
private const string MoodField = "mood";
|
||||
|
||||
private const string MovieType = "movie";
|
||||
private const string ShowType = "show";
|
||||
private const string ArtistType = "artist";
|
||||
private const string MusicVideoType = "music_video";
|
||||
|
||||
private readonly ILogger<SearchIndex> _logger;
|
||||
@@ -84,6 +86,9 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
case Show show:
|
||||
await UpdateShow(searchRepository, show);
|
||||
break;
|
||||
case Artist artist:
|
||||
await UpdateArtist(searchRepository, artist);
|
||||
break;
|
||||
case MusicVideo musicVideo:
|
||||
UpdateMusicVideo(musicVideo);
|
||||
break;
|
||||
@@ -110,6 +115,9 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
case Show show:
|
||||
await UpdateShow(searchRepository, show);
|
||||
break;
|
||||
case Artist artist:
|
||||
await UpdateArtist(searchRepository, artist);
|
||||
break;
|
||||
case MusicVideo musicVideo:
|
||||
UpdateMusicVideo(musicVideo);
|
||||
break;
|
||||
@@ -362,6 +370,57 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateArtist(ISearchRepository searchRepository, Artist artist)
|
||||
{
|
||||
Option<ArtistMetadata> maybeMetadata = artist.ArtistMetadata.HeadOrNone();
|
||||
if (maybeMetadata.IsSome)
|
||||
{
|
||||
ArtistMetadata metadata = maybeMetadata.ValueUnsafe();
|
||||
|
||||
try
|
||||
{
|
||||
var doc = new Document
|
||||
{
|
||||
new StringField(IdField, artist.Id.ToString(), Field.Store.YES),
|
||||
new StringField(TypeField, ArtistType, Field.Store.NO),
|
||||
new TextField(TitleField, metadata.Title, Field.Store.NO),
|
||||
new StringField(SortTitleField, metadata.SortTitle.ToLowerInvariant(), Field.Store.NO),
|
||||
new TextField(LibraryNameField, artist.LibraryPath.Library.Name, Field.Store.NO),
|
||||
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO),
|
||||
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES)
|
||||
};
|
||||
|
||||
List<string> languages = await searchRepository.GetLanguagesForArtist(artist);
|
||||
foreach (string lang in languages.Distinct().Filter(s => !string.IsNullOrWhiteSpace(s)))
|
||||
{
|
||||
doc.Add(new StringField(LanguageField, lang, Field.Store.NO));
|
||||
}
|
||||
|
||||
foreach (Genre genre in metadata.Genres)
|
||||
{
|
||||
doc.Add(new TextField(GenreField, genre.Name, Field.Store.NO));
|
||||
}
|
||||
|
||||
foreach (Style style in metadata.Styles)
|
||||
{
|
||||
doc.Add(new TextField(StyleField, style.Name, Field.Store.NO));
|
||||
}
|
||||
|
||||
foreach (Mood mood in metadata.Moods)
|
||||
{
|
||||
doc.Add(new TextField(MoodField, mood.Name, Field.Store.NO));
|
||||
}
|
||||
|
||||
_writer.UpdateDocument(new Term(IdField, artist.Id.ToString()), doc);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
metadata.Artist = null;
|
||||
_logger.LogWarning(ex, "Error indexing show with metadata {@Metadata}", metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateMusicVideo(MusicVideo musicVideo)
|
||||
{
|
||||
Option<MusicVideoMetadata> maybeMetadata = musicVideo.MusicVideoMetadata.HeadOrNone();
|
||||
@@ -375,7 +434,6 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
{
|
||||
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),
|
||||
|
||||
Reference in New Issue
Block a user