Database redesign (#31)

* starting database redesign

* set season and episode numbers

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

* get movie cards from new table

* copy show/episode metadata

* remove old movie metadata type

* rename new movie metadata type

* code cleanup

* start to remove old television classes

* remove old television tables from database

* fix playout building

* fix collection views

* fix show/season views

* clean up movie metadata table

* fix scanner tests

* add libraries ui

* code cleanup

* fix movie scanning/metadata

* add library scan button to ui

* delete library path from ui

* temp disable movie scanning

* remove orphan media items and prevent duplicate paths

* attach artwork to metadata

* fix split show/season display

* fix television artwork

* store year distinct from release date

* fix collections ui

* code cleanup

* add library paths from ui

* fix adding to collections from ui

* fix schedule items loading

* schedule editing works again

* remove some todos

* more cleanup

* fix unit tests

* fix episode sorting

* fix deleting show library paths

* remove unused class

* fix playout list in ui

* fix log viewer

* start to use version/file instead of statistics

* clean up old columns

* fix playout display (time zone)

* fix playback

* fix channel guide time zone

* cascade more deletes

* fix compiler warnings

* fix adding new seasons

* use artwork for channel logo

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

* log database migration

* update homepage docs for libraries

* fix adding new channel with logo

* fix episode numbers in epg
This commit is contained in:
Jason Dove
2021-02-28 17:48:01 +00:00
committed by GitHub
parent e25b9edd01
commit f392bab118
392 changed files with 52108 additions and 4079 deletions
@@ -0,0 +1,9 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Libraries.Commands
{
public record CreateLocalLibraryPath
(int LibraryId, string Path) : IRequest<Either<BaseError, LocalLibraryPathViewModel>>;
}
@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static LanguageExt.Prelude;
using static ErsatzTV.Application.Libraries.Mapper;
namespace ErsatzTV.Application.Libraries.Commands
{
public class CreateLocalLibraryPathHandler : IRequestHandler<CreateLocalLibraryPath,
Either<BaseError, LocalLibraryPathViewModel>>
{
private readonly ILibraryRepository _libraryRepository;
public CreateLocalLibraryPathHandler(ILibraryRepository mediaSourceRepository) =>
_libraryRepository = mediaSourceRepository;
public Task<Either<BaseError, LocalLibraryPathViewModel>> Handle(
CreateLocalLibraryPath request,
CancellationToken cancellationToken) =>
Validate(request).MapT(PersistLocalLibraryPath).Bind(v => v.ToEitherAsync());
private Task<LocalLibraryPathViewModel> PersistLocalLibraryPath(LibraryPath p) =>
_libraryRepository.Add(p).Map(ProjectToViewModel);
private Task<Validation<BaseError, LibraryPath>> Validate(CreateLocalLibraryPath request) =>
ValidateFolder(request)
.MapT(
folder =>
new LibraryPath
{
LibraryId = request.LibraryId,
Path = folder
});
private async Task<Validation<BaseError, string>> ValidateFolder(CreateLocalLibraryPath request)
{
List<string> allPaths = await _libraryRepository.GetLocalPaths(request.LibraryId)
.Map(list => list.Map(c => c.Path).ToList());
return Optional(request.Path)
.Filter(folder => allPaths.ForAll(f => !AreSubPaths(f, folder)))
.ToValidation<BaseError>("Path must not belong to another library path");
}
private static bool AreSubPaths(string path1, string path2)
{
string one = path1 + Path.DirectorySeparatorChar;
string two = path2 + Path.DirectorySeparatorChar;
return one == two || one.StartsWith(two, StringComparison.OrdinalIgnoreCase) ||
two.StartsWith(one, StringComparison.OrdinalIgnoreCase);
}
}
}
@@ -0,0 +1,7 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.Libraries.Commands
{
public record DeleteLocalLibraryPath(int LocalLibraryPathId) : MediatR.IRequest<Either<BaseError, Unit>>;
}
@@ -0,0 +1,34 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
namespace ErsatzTV.Application.Libraries.Commands
{
public class
DeleteLocalLibraryPathHandler : MediatR.IRequestHandler<DeleteLocalLibraryPath, Either<BaseError, Unit>>
{
private readonly ILibraryRepository _libraryRepository;
public DeleteLocalLibraryPathHandler(ILibraryRepository libraryRepository) =>
_libraryRepository = libraryRepository;
public Task<Either<BaseError, Unit>> Handle(
DeleteLocalLibraryPath request,
CancellationToken cancellationToken) =>
MediaSourceMustExist(request)
.MapT(DoDeletion)
.Bind(t => t.ToEitherAsync());
private Task<Unit> DoDeletion(LibraryPath libraryPath) =>
_libraryRepository.DeleteLocalPath(libraryPath.Id).ToUnit();
private async Task<Validation<BaseError, LibraryPath>> MediaSourceMustExist(DeleteLocalLibraryPath request) =>
(await _libraryRepository.GetPath(request.LocalLibraryPathId))
.HeadOrNone()
.ToValidation<BaseError>(
$"Local library path {request.LocalLibraryPathId} does not exist.");
}
}
@@ -0,0 +1,4 @@
namespace ErsatzTV.Application.Libraries
{
public record LocalLibraryPathViewModel(int Id, int LibraryId, string Path);
}
@@ -0,0 +1,6 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Libraries
{
public record LocalLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind);
}
+13
View File
@@ -0,0 +1,13 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Libraries
{
internal static class Mapper
{
public static LocalLibraryViewModel ProjectToViewModel(LocalLibrary library) =>
new(library.Id, library.Name, library.MediaKind);
public static LocalLibraryPathViewModel ProjectToViewModel(LibraryPath libraryPath) =>
new(libraryPath.Id, libraryPath.LibraryId, libraryPath.Path);
}
}
@@ -0,0 +1,6 @@
using MediatR;
namespace ErsatzTV.Application.Libraries.Queries
{
public record CountMediaItemsByLibraryPath(int LibraryPathId) : IRequest<int>;
}
@@ -0,0 +1,18 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using MediatR;
namespace ErsatzTV.Application.Libraries.Queries
{
public class CountMediaItemsByLibraryPathHandler : IRequestHandler<CountMediaItemsByLibraryPath, int>
{
private readonly ILibraryRepository _libraryRepository;
public CountMediaItemsByLibraryPathHandler(ILibraryRepository libraryRepository) =>
_libraryRepository = libraryRepository;
public Task<int> Handle(CountMediaItemsByLibraryPath request, CancellationToken cancellationToken) =>
_libraryRepository.CountMediaItemsByPath(request.LibraryPathId);
}
}
@@ -0,0 +1,7 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.Libraries.Queries
{
public record GetLocalLibraries : IRequest<List<LocalLibraryViewModel>>;
}
@@ -0,0 +1,22 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Libraries.Mapper;
namespace ErsatzTV.Application.Libraries.Queries
{
public class GetLocalLibrariesHandler : IRequestHandler<GetLocalLibraries, List<LocalLibraryViewModel>>
{
private readonly ILibraryRepository _libraryRepository;
public GetLocalLibrariesHandler(ILibraryRepository libraryRepository) => _libraryRepository = libraryRepository;
public Task<List<LocalLibraryViewModel>>
Handle(GetLocalLibraries request, CancellationToken cancellationToken) =>
_libraryRepository.GetAllLocal().Map(list => list.Map(ProjectToViewModel).ToList());
}
}
@@ -0,0 +1,7 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Libraries.Queries
{
public record GetLocalLibraryById(int LibraryId) : IRequest<Option<LocalLibraryViewModel>>;
}
@@ -0,0 +1,22 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Libraries.Mapper;
namespace ErsatzTV.Application.Libraries.Queries
{
public class GetLocalLibraryByIdHandler : IRequestHandler<GetLocalLibraryById, Option<LocalLibraryViewModel>>
{
private readonly ILibraryRepository _libraryRepository;
public GetLocalLibraryByIdHandler(ILibraryRepository libraryRepository) =>
_libraryRepository = libraryRepository;
public Task<Option<LocalLibraryViewModel>> Handle(
GetLocalLibraryById request,
CancellationToken cancellationToken) =>
_libraryRepository.GetLocal(request.LibraryId).MapT(ProjectToViewModel);
}
}
@@ -0,0 +1,7 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.Libraries.Queries
{
public record GetLocalLibraryPaths(int LocalLibraryId) : IRequest<List<LocalLibraryPathViewModel>>;
}
@@ -0,0 +1,25 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Libraries.Mapper;
namespace ErsatzTV.Application.Libraries.Queries
{
public class GetLocalLibraryPathsHandler : IRequestHandler<GetLocalLibraryPaths, List<LocalLibraryPathViewModel>>
{
private readonly ILibraryRepository _libraryRepository;
public GetLocalLibraryPathsHandler(ILibraryRepository libraryRepository) =>
_libraryRepository = libraryRepository;
public Task<List<LocalLibraryPathViewModel>> Handle(
GetLocalLibraryPaths request,
CancellationToken cancellationToken) =>
_libraryRepository.GetLocalPaths(request.LocalLibraryId)
.Map(list => list.Map(ProjectToViewModel).ToList());
}
}