mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* 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 fieldspull/128/head v0.0.27-prealpha
56 changed files with 7572 additions and 112 deletions
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic; |
||||
using ErsatzTV.Core.Search; |
||||
using LanguageExt; |
||||
|
||||
namespace ErsatzTV.Application.MediaCards |
||||
{ |
||||
public record MusicVideoCardResultsViewModel( |
||||
int Count, |
||||
List<MusicVideoCardViewModel> Cards, |
||||
Option<SearchPageMap> PageMap); |
||||
} |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
namespace ErsatzTV.Application.MediaCards |
||||
{ |
||||
public record MusicVideoCardViewModel |
||||
(int MusicVideoId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel( |
||||
MusicVideoId, |
||||
Title, |
||||
Subtitle, |
||||
SortTitle, |
||||
Poster) |
||||
{ |
||||
public int CustomIndex { get; set; } |
||||
} |
||||
} |
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
using ErsatzTV.Core; |
||||
using LanguageExt; |
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands |
||||
{ |
||||
public record AddMusicVideoToCollection |
||||
(int CollectionId, int MusicVideoId) : MediatR.IRequest<Either<BaseError, Unit>>; |
||||
} |
@ -0,0 +1,68 @@
@@ -0,0 +1,68 @@
|
||||
using System.Threading; |
||||
using System.Threading.Channels; |
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Application.Playouts.Commands; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using LanguageExt; |
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands |
||||
{ |
||||
public class |
||||
AddMusicVideoToCollectionHandler : MediatR.IRequestHandler<AddMusicVideoToCollection, Either<BaseError, Unit>> |
||||
{ |
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; |
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
||||
private readonly IMusicVideoRepository _musicVideoRepository; |
||||
|
||||
public AddMusicVideoToCollectionHandler( |
||||
IMediaCollectionRepository mediaCollectionRepository, |
||||
IMusicVideoRepository musicVideoRepository, |
||||
ChannelWriter<IBackgroundServiceRequest> channel) |
||||
{ |
||||
_mediaCollectionRepository = mediaCollectionRepository; |
||||
_musicVideoRepository = musicVideoRepository; |
||||
_channel = channel; |
||||
} |
||||
|
||||
public Task<Either<BaseError, Unit>> Handle( |
||||
AddMusicVideoToCollection request, |
||||
CancellationToken cancellationToken) => |
||||
Validate(request) |
||||
.MapT(_ => ApplyAddMusicVideoRequest(request)) |
||||
.Bind(v => v.ToEitherAsync()); |
||||
|
||||
private async Task<Unit> ApplyAddMusicVideoRequest(AddMusicVideoToCollection request) |
||||
{ |
||||
if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.MusicVideoId)) |
||||
{ |
||||
// rebuild all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository |
||||
.PlayoutIdsUsingCollection(request.CollectionId)) |
||||
{ |
||||
await _channel.WriteAsync(new BuildPlayout(playoutId, true)); |
||||
} |
||||
} |
||||
|
||||
return Unit.Default; |
||||
} |
||||
|
||||
private async Task<Validation<BaseError, Unit>> Validate(AddMusicVideoToCollection request) => |
||||
(await CollectionMustExist(request), await ValidateMusicVideo(request)) |
||||
.Apply((_, _) => Unit.Default); |
||||
|
||||
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddMusicVideoToCollection request) => |
||||
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId) |
||||
.MapT(_ => Unit.Default) |
||||
.Map(v => v.ToValidation<BaseError>("Collection does not exist.")); |
||||
|
||||
private Task<Validation<BaseError, Unit>> ValidateMusicVideo(AddMusicVideoToCollection request) => |
||||
LoadMusicVideo(request) |
||||
.MapT(_ => Unit.Default) |
||||
.Map(v => v.ToValidation<BaseError>("Music video does not exist")); |
||||
|
||||
private Task<Option<MusicVideo>> LoadMusicVideo(AddMusicVideoToCollection request) => |
||||
_musicVideoRepository.GetMusicVideo(request.MusicVideoId); |
||||
} |
||||
} |
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
using ErsatzTV.Application.MediaCards; |
||||
using MediatR; |
||||
|
||||
namespace ErsatzTV.Application.Search.Queries |
||||
{ |
||||
public record QuerySearchIndexMusicVideos |
||||
(string Query, int PageNumber, int PageSize) : IRequest<MusicVideoCardResultsViewModel>; |
||||
} |
@ -0,0 +1,44 @@
@@ -0,0 +1,44 @@
|
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using System.Threading; |
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Application.MediaCards; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using ErsatzTV.Core.Interfaces.Search; |
||||
using ErsatzTV.Core.Search; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
using static ErsatzTV.Application.MediaCards.Mapper; |
||||
|
||||
namespace ErsatzTV.Application.Search.Queries |
||||
{ |
||||
public class |
||||
QuerySearchIndexMusicVideosHandler : IRequestHandler<QuerySearchIndexMusicVideos, MusicVideoCardResultsViewModel |
||||
> |
||||
{ |
||||
private readonly IMusicVideoRepository _musicVideoRepository; |
||||
private readonly ISearchIndex _searchIndex; |
||||
|
||||
public QuerySearchIndexMusicVideosHandler(ISearchIndex searchIndex, IMusicVideoRepository musicVideoRepository) |
||||
{ |
||||
_searchIndex = searchIndex; |
||||
_musicVideoRepository = musicVideoRepository; |
||||
} |
||||
|
||||
public async Task<MusicVideoCardResultsViewModel> Handle( |
||||
QuerySearchIndexMusicVideos request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
SearchResult searchResult = await _searchIndex.Search( |
||||
request.Query, |
||||
(request.PageNumber - 1) * request.PageSize, |
||||
request.PageSize); |
||||
|
||||
List<MusicVideoCardViewModel> items = await _musicVideoRepository |
||||
.GetMusicVideosForCards(searchResult.Items.Map(i => i.Id).ToList()) |
||||
.Map(list => list.Map(ProjectToViewModel).ToList()); |
||||
|
||||
return new MusicVideoCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic; |
||||
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class MusicVideo : MediaItem |
||||
{ |
||||
public List<MusicVideoMetadata> MusicVideoMetadata { get; set; } |
||||
public List<MediaVersion> MediaVersions { get; set; } |
||||
} |
||||
} |
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class MusicVideoMetadata : Metadata |
||||
{ |
||||
public string Album { get; set; } |
||||
public string Plot { get; set; } |
||||
public string Artist { get; set; } |
||||
public int MusicVideoId { get; set; } |
||||
public MusicVideo MusicVideo { get; set; } |
||||
} |
||||
} |
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
using System; |
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Core.Domain; |
||||
using LanguageExt; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata |
||||
{ |
||||
public interface IMusicVideoFolderScanner |
||||
{ |
||||
Task<Either<BaseError, Unit>> ScanFolder(LibraryPath libraryPath, string ffprobePath, DateTimeOffset lastScan); |
||||
} |
||||
} |
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic; |
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Metadata; |
||||
using LanguageExt; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Repositories |
||||
{ |
||||
public interface IMusicVideoRepository |
||||
{ |
||||
Task<Option<MusicVideo>> GetByMetadata(LibraryPath libraryPath, MusicVideoMetadata metadata); |
||||
|
||||
Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> Add( |
||||
LibraryPath libraryPath, |
||||
string filePath, |
||||
MusicVideoMetadata metadata); |
||||
|
||||
Task<IEnumerable<string>> FindMusicVideoPaths(LibraryPath libraryPath); |
||||
Task<List<int>> DeleteByPath(LibraryPath libraryPath, string path); |
||||
Task<bool> AddGenre(MusicVideoMetadata metadata, Genre genre); |
||||
Task<bool> AddTag(MusicVideoMetadata metadata, Tag tag); |
||||
Task<bool> AddStudio(MusicVideoMetadata metadata, Studio studio); |
||||
Task<List<MusicVideoMetadata>> GetMusicVideosForCards(List<int> ids); |
||||
Task<Option<MusicVideo>> GetMusicVideo(int musicVideoId); |
||||
} |
||||
} |
@ -0,0 +1,225 @@
@@ -0,0 +1,225 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.IO; |
||||
using System.Linq; |
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Errors; |
||||
using ErsatzTV.Core.Interfaces.Images; |
||||
using ErsatzTV.Core.Interfaces.Metadata; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using ErsatzTV.Core.Interfaces.Search; |
||||
using LanguageExt; |
||||
using Microsoft.Extensions.Logging; |
||||
using static LanguageExt.Prelude; |
||||
|
||||
namespace ErsatzTV.Core.Metadata |
||||
{ |
||||
public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScanner |
||||
{ |
||||
private readonly ILocalFileSystem _localFileSystem; |
||||
private readonly ILocalMetadataProvider _localMetadataProvider; |
||||
private readonly ILogger<MusicVideoFolderScanner> _logger; |
||||
private readonly IMusicVideoRepository _musicVideoRepository; |
||||
private readonly ISearchIndex _searchIndex; |
||||
|
||||
public MusicVideoFolderScanner( |
||||
ILocalFileSystem localFileSystem, |
||||
ILocalStatisticsProvider localStatisticsProvider, |
||||
ILocalMetadataProvider localMetadataProvider, |
||||
IMetadataRepository metadataRepository, |
||||
IImageCache imageCache, |
||||
ISearchIndex searchIndex, |
||||
IMusicVideoRepository musicVideoRepository, |
||||
ILogger<MusicVideoFolderScanner> logger) : base( |
||||
localFileSystem, |
||||
localStatisticsProvider, |
||||
metadataRepository, |
||||
imageCache, |
||||
logger) |
||||
{ |
||||
_localFileSystem = localFileSystem; |
||||
_localMetadataProvider = localMetadataProvider; |
||||
_searchIndex = searchIndex; |
||||
_musicVideoRepository = musicVideoRepository; |
||||
_logger = logger; |
||||
} |
||||
|
||||
public async Task<Either<BaseError, Unit>> ScanFolder( |
||||
LibraryPath libraryPath, |
||||
string ffprobePath, |
||||
DateTimeOffset lastScan) |
||||
{ |
||||
if (!_localFileSystem.IsLibraryPathAccessible(libraryPath)) |
||||
{ |
||||
return new MediaSourceInaccessible(); |
||||
} |
||||
|
||||
var folderQueue = new Queue<string>(); |
||||
foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path).OrderBy(identity)) |
||||
{ |
||||
folderQueue.Enqueue(folder); |
||||
} |
||||
|
||||
while (folderQueue.Count > 0) |
||||
{ |
||||
string movieFolder = folderQueue.Dequeue(); |
||||
|
||||
var allFiles = _localFileSystem.ListFiles(movieFolder) |
||||
.Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f))) |
||||
.Filter( |
||||
f => !ExtraFiles.Any( |
||||
e => Path.GetFileNameWithoutExtension(f).EndsWith(e, StringComparison.OrdinalIgnoreCase))) |
||||
.ToList(); |
||||
|
||||
if (allFiles.Count == 0) |
||||
{ |
||||
foreach (string subdirectory in _localFileSystem.ListSubdirectories(movieFolder).OrderBy(identity)) |
||||
{ |
||||
folderQueue.Enqueue(subdirectory); |
||||
} |
||||
|
||||
continue; |
||||
} |
||||
|
||||
if (_localFileSystem.GetLastWriteTime(movieFolder) < lastScan) |
||||
{ |
||||
continue; |
||||
} |
||||
|
||||
foreach (string file in allFiles.OrderBy(identity)) |
||||
{ |
||||
// TODO: figure out how to rebuild playouts
|
||||
Either<BaseError, MediaItemScanResult<MusicVideo>> maybeMusicVideo = |
||||
await FindOrCreateMusicVideo(libraryPath, file) |
||||
.BindT(musicVideo => UpdateStatistics(musicVideo, ffprobePath)) |
||||
.BindT(UpdateMetadata) |
||||
.BindT(UpdateThumbnail); |
||||
|
||||
await maybeMusicVideo.Match( |
||||
async result => |
||||
{ |
||||
if (result.IsAdded) |
||||
{ |
||||
await _searchIndex.AddItems(new List<MediaItem> { result.Item }); |
||||
} |
||||
else if (result.IsUpdated) |
||||
{ |
||||
await _searchIndex.UpdateItems(new List<MediaItem> { result.Item }); |
||||
} |
||||
}, |
||||
error => |
||||
{ |
||||
_logger.LogWarning("Error processing music video at {Path}: {Error}", file, error.Value); |
||||
return Task.CompletedTask; |
||||
}); |
||||
} |
||||
} |
||||
|
||||
foreach (string path in await _musicVideoRepository.FindMusicVideoPaths(libraryPath)) |
||||
{ |
||||
if (!_localFileSystem.FileExists(path)) |
||||
{ |
||||
_logger.LogInformation("Removing missing music video at {Path}", path); |
||||
List<int> ids = await _musicVideoRepository.DeleteByPath(libraryPath, path); |
||||
await _searchIndex.RemoveItems(ids); |
||||
} |
||||
} |
||||
|
||||
return Unit.Default; |
||||
} |
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> FindOrCreateMusicVideo( |
||||
LibraryPath libraryPath, |
||||
string filePath) |
||||
{ |
||||
Option<MusicVideoMetadata> maybeMetadata = await _localMetadataProvider.GetMetadataForMusicVideo(filePath); |
||||
return await maybeMetadata.Match( |
||||
async metadata => |
||||
{ |
||||
Option<MusicVideo> maybeMusicVideo = |
||||
await _musicVideoRepository.GetByMetadata(libraryPath, metadata); |
||||
return await maybeMusicVideo.Match( |
||||
musicVideo => |
||||
Right<BaseError, MediaItemScanResult<MusicVideo>>( |
||||
new MediaItemScanResult<MusicVideo>(musicVideo)) |
||||
.AsTask(), |
||||
async () => await _musicVideoRepository.Add(libraryPath, filePath, metadata)); |
||||
}, |
||||
() => Left<BaseError, MediaItemScanResult<MusicVideo>>( |
||||
BaseError.New("Unable to locate metadata for music video")).AsTask()); |
||||
} |
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> UpdateMetadata( |
||||
MediaItemScanResult<MusicVideo> result) |
||||
{ |
||||
try |
||||
{ |
||||
MusicVideo musicVideo = result.Item; |
||||
return await LocateNfoFile(musicVideo).Match<Task<Either<BaseError, MediaItemScanResult<MusicVideo>>>>( |
||||
async nfoFile => |
||||
{ |
||||
bool shouldUpdate = Optional(musicVideo.MusicVideoMetadata).Flatten().HeadOrNone().Match( |
||||
m => m.MetadataKind == MetadataKind.Fallback || |
||||
m.DateUpdated < _localFileSystem.GetLastWriteTime(nfoFile), |
||||
true); |
||||
|
||||
if (shouldUpdate) |
||||
{ |
||||
_logger.LogDebug("Refreshing {Attribute} from {Path}", "Sidecar Metadata", nfoFile); |
||||
if (await _localMetadataProvider.RefreshSidecarMetadata(musicVideo, nfoFile)) |
||||
{ |
||||
result.IsUpdated = true; |
||||
} |
||||
} |
||||
|
||||
return result; |
||||
}, |
||||
() => Left<BaseError, MediaItemScanResult<MusicVideo>>( |
||||
BaseError.New("Unable to locate metadata for music video")).AsTask()); |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
return BaseError.New(ex.ToString()); |
||||
} |
||||
} |
||||
|
||||
private Option<string> LocateNfoFile(MusicVideo musicVideo) |
||||
{ |
||||
string path = musicVideo.MediaVersions.Head().MediaFiles.Head().Path; |
||||
return Optional(Path.ChangeExtension(path, "nfo")) |
||||
.Filter(s => _localFileSystem.FileExists(s)) |
||||
.HeadOrNone(); |
||||
} |
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> UpdateThumbnail( |
||||
MediaItemScanResult<MusicVideo> result) |
||||
{ |
||||
try |
||||
{ |
||||
MusicVideo musicVideo = result.Item; |
||||
await LocateThumbnail(musicVideo).IfSomeAsync( |
||||
async thumbnailFile => |
||||
{ |
||||
MusicVideoMetadata metadata = musicVideo.MusicVideoMetadata.Head(); |
||||
await RefreshArtwork(thumbnailFile, metadata, ArtworkKind.Thumbnail); |
||||
}); |
||||
|
||||
return result; |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
return BaseError.New(ex.ToString()); |
||||
} |
||||
} |
||||
|
||||
private Option<string> LocateThumbnail(MusicVideo musicVideo) |
||||
{ |
||||
string path = musicVideo.MediaVersions.Head().MediaFiles.Head().Path; |
||||
return ImageFileExtensions |
||||
.Map(ext => Path.ChangeExtension(path, ext)) |
||||
.Filter(f => _localFileSystem.FileExists(f)) |
||||
.HeadOrNone(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,23 @@
@@ -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 @@
@@ -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); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,181 @@
@@ -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); |
||||
} |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@
@@ -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 @@
@@ -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 @@
@@ -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); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,164 @@
@@ -0,0 +1,164 @@
|
||||
@page "/media/music/videos" |
||||
@page "/media/music/videos/page/{PageNumber:int}" |
||||
@using LanguageExt.UnsafeValueAccess |
||||
@using Microsoft.AspNetCore.WebUtilities |
||||
@using Microsoft.Extensions.Primitives |
||||
@using ErsatzTV.Application.MediaCards |
||||
@using ErsatzTV.Application.MediaCollections |
||||
@using ErsatzTV.Application.MediaCollections.Commands |
||||
@using ErsatzTV.Application.Search.Queries |
||||
@using Unit = LanguageExt.Unit |
||||
@inherits MultiSelectBase<MusicVideoList> |
||||
@inject NavigationManager NavigationManager |
||||
@inject ChannelWriter<IBackgroundServiceRequest> Channel |
||||
|
||||
<MudPaper Square="true" Style="display: flex; height: 64px; left: 240px; padding: 0; position: fixed; right: 0; z-index: 100;"> |
||||
<div style="display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%" class="ml-6 mr-6"> |
||||
@if (IsSelectMode()) |
||||
{ |
||||
<MudText Typo="Typo.h6" Color="Color.Primary">@SelectionLabel()</MudText> |
||||
<div style="margin-left: auto"> |
||||
<MudButton Variant="Variant.Filled" |
||||
Color="Color.Primary" |
||||
StartIcon="@Icons.Material.Filled.Add" |
||||
OnClick="@(_ => AddSelectionToCollection())"> |
||||
Add To Collection |
||||
</MudButton> |
||||
<MudButton Class="ml-3" |
||||
Variant="Variant.Filled" |
||||
Color="Color.Secondary" |
||||
StartIcon="@Icons.Material.Filled.Check" |
||||
OnClick="@(_ => ClearSelection())"> |
||||
Clear Selection |
||||
</MudButton> |
||||
</div> |
||||
} |
||||
else |
||||
{ |
||||
<MudText Style="margin-bottom: auto; margin-top: auto; width: 33%">@_query</MudText> |
||||
<div style="max-width: 300px; width: 33%;"> |
||||
<MudPaper Style="align-items: center; display: flex; justify-content: center;"> |
||||
<MudIconButton Icon="@Icons.Material.Outlined.ChevronLeft" |
||||
OnClick="@PrevPage" |
||||
Disabled="@(PageNumber <= 1)"> |
||||
</MudIconButton> |
||||
<MudText Style="flex-grow: 1" |
||||
Align="Align.Center"> |
||||
@Math.Min((PageNumber - 1) * PageSize + 1, _data.Count)-@Math.Min(_data.Count, PageNumber * PageSize) of @_data.Count |
||||
</MudText> |
||||
<MudIconButton Icon="@Icons.Material.Outlined.ChevronRight" |
||||
OnClick="@NextPage" Disabled="@(PageNumber * PageSize >= _data.Count)"> |
||||
</MudIconButton> |
||||
</MudPaper> |
||||
</div> |
||||
} |
||||
</div> |
||||
</MudPaper> |
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8" Style="margin-top: 64px"> |
||||
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid"> |
||||
@foreach (MusicVideoCardViewModel card in _data.Cards.Where(m => !string.IsNullOrWhiteSpace(m.Title)).OrderBy(m => m.SortTitle)) |
||||
{ |
||||
<MediaCard Data="@card" |
||||
Link="" |
||||
ArtworkKind="ArtworkKind.Thumbnail" |
||||
AddToCollectionClicked="@AddToCollection" |
||||
SelectClicked="@(e => SelectClicked(card, e))" |
||||
IsSelected="@IsSelected(card)" |
||||
IsSelectMode="@IsSelectMode()"/> |
||||
} |
||||
</MudContainer> |
||||
</MudContainer> |
||||
@if (_data.PageMap.IsSome) |
||||
{ |
||||
<LetterBar PageMap="@_data.PageMap.ValueUnsafe()" |
||||
BaseUri="/media/music/videos" |
||||
Query="@_query"/> |
||||
} |
||||
|
||||
@code { |
||||
private static int PageSize => 100; |
||||
|
||||
[Parameter] |
||||
public int PageNumber { get; set; } |
||||
|
||||
private MusicVideoCardResultsViewModel _data; |
||||
private string _query; |
||||
|
||||
protected override Task OnParametersSetAsync() |
||||
{ |
||||
if (PageNumber == 0) |
||||
{ |
||||
PageNumber = 1; |
||||
} |
||||
|
||||
string query = new Uri(NavigationManager.Uri).Query; |
||||
if (QueryHelpers.ParseQuery(query).TryGetValue("query", out StringValues value)) |
||||
{ |
||||
_query = value; |
||||
} |
||||
else |
||||
{ |
||||
_query = null; |
||||
} |
||||
|
||||
return RefreshData(); |
||||
} |
||||
|
||||
protected override async Task RefreshData() |
||||
{ |
||||
string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:music_video" : $"type:music_video AND ({_query})"; |
||||
_data = await Mediator.Send(new QuerySearchIndexMusicVideos(searchQuery, PageNumber, PageSize)); |
||||
} |
||||
|
||||
private void PrevPage() |
||||
{ |
||||
var uri = $"/media/music/videos/page/{PageNumber - 1}"; |
||||
if (!string.IsNullOrWhiteSpace(_query)) |
||||
{ |
||||
uri = QueryHelpers.AddQueryString(uri, "query", _query); |
||||
} |
||||
NavigationManager.NavigateTo(uri); |
||||
} |
||||
|
||||
private void NextPage() |
||||
{ |
||||
var uri = $"/media/music/videos/page/{PageNumber + 1}"; |
||||
if (!string.IsNullOrWhiteSpace(_query)) |
||||
{ |
||||
uri = QueryHelpers.AddQueryString(uri, "query", _query); |
||||
} |
||||
NavigationManager.NavigateTo(uri); |
||||
} |
||||
|
||||
private void SelectClicked(MediaCardViewModel card, MouseEventArgs e) |
||||
{ |
||||
List<MediaCardViewModel> GetSortedItems() => _data.Cards.OrderBy(m => m.SortTitle).ToList<MediaCardViewModel>(); |
||||
|
||||
SelectClicked(GetSortedItems, card, e); |
||||
} |
||||
|
||||
private async Task AddToCollection(MediaCardViewModel card) |
||||
{ |
||||
if (card is MusicVideoCardViewModel musicVideo) |
||||
{ |
||||
var parameters = new DialogParameters { { "EntityType", "music video" }, { "EntityName", musicVideo.Title } }; |
||||
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; |
||||
|
||||
IDialogReference dialog = Dialog.Show<AddToCollectionDialog>("Add To Collection", parameters, options); |
||||
DialogResult result = await dialog.Result; |
||||
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) |
||||
{ |
||||
var request = new AddMusicVideoToCollection(collection.Id, musicVideo.MusicVideoId); |
||||
Either<BaseError, Unit> addResult = await Mediator.Send(request); |
||||
addResult.Match( |
||||
Left: error => |
||||
{ |
||||
Snackbar.Add($"Unexpected error adding music video to collection: {error.Value}"); |
||||
Logger.LogError("Unexpected error adding music video to collection: {Error}", error.Value); |
||||
}, |
||||
Right: _ => Snackbar.Add($"Added {musicVideo.Title} to collection {collection.Name}", Severity.Success)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue