Browse Source

starting database redesign

pull/31/head
Jason Dove 6 years ago
parent
commit
8783d5bae1
  1. 2
      ErsatzTV.Application/MediaCards/Mapper.cs
  2. 6
      ErsatzTV.Application/MediaCollections/Commands/AddMovieToSimpleMediaCollectionHandler.cs
  3. 2
      ErsatzTV.Application/MediaCollections/Commands/CreateSimpleMediaCollectionHandler.cs
  4. 27
      ErsatzTV.Application/MediaItems/Mapper.cs
  5. 2
      ErsatzTV.Application/MediaItems/MediaItemViewModel.cs
  6. 20
      ErsatzTV.Application/MediaSources/Commands/CreateLocalMediaSource.cs
  7. 154
      ErsatzTV.Application/MediaSources/Commands/CreateLocalMediaSourceHandler.cs
  8. 114
      ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs
  9. 8
      ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSource.cs
  10. 111
      ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSourceHandler.cs
  11. 6
      ErsatzTV.Application/MediaSources/LocalMediaSourceViewModel.cs
  12. 13
      ErsatzTV.Application/MediaSources/Mapper.cs
  13. 6
      ErsatzTV.Application/MediaSources/MediaSourceViewModel.cs
  14. 9
      ErsatzTV.Application/MediaSources/PlexMediaSourceViewModel.cs
  15. 2
      ErsatzTV.Application/Movies/Mapper.cs
  16. 2
      ErsatzTV.Application/Playouts/Mapper.cs
  17. 2
      ErsatzTV.Application/Plex/Commands/StartPlexPinFlow.cs
  18. 2
      ErsatzTV.Application/Plex/Commands/StartPlexPinFlowHandler.cs
  19. 2
      ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraries.cs
  20. 12
      ErsatzTV.Application/Plex/Commands/SynchronizePlexLibrariesHandler.cs
  21. 25
      ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryById.cs
  22. 144
      ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs
  23. 2
      ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSources.cs
  24. 8
      ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSourcesHandler.cs
  25. 2
      ErsatzTV.Application/Plex/Commands/TryCompletePlexPinFlow.cs
  26. 2
      ErsatzTV.Application/Plex/Commands/TryCompletePlexPinFlowHandler.cs
  27. 11
      ErsatzTV.Application/Plex/Commands/UpdatePlexLibraryPreferences.cs
  28. 32
      ErsatzTV.Application/Plex/Commands/UpdatePlexLibraryPreferencesHandler.cs
  29. 18
      ErsatzTV.Application/Plex/Mapper.cs
  30. 6
      ErsatzTV.Application/Plex/PlexLibraryViewModel.cs
  31. 6
      ErsatzTV.Application/Plex/PlexMediaSourceViewModel.cs
  32. 2
      ErsatzTV.Application/Plex/Queries/GetAllPlexMediaSources.cs
  33. 4
      ErsatzTV.Application/Plex/Queries/GetAllPlexMediaSourcesHandler.cs
  34. 7
      ErsatzTV.Application/Plex/Queries/GetPlexLibrariesBySourceId.cs
  35. 27
      ErsatzTV.Application/Plex/Queries/GetPlexLibrariesBySourceIdHandler.cs
  36. 7
      ErsatzTV.Application/Plex/Queries/GetPlexMediaSourceById.cs
  37. 23
      ErsatzTV.Application/Plex/Queries/GetPlexMediaSourceByIdHandler.cs
  38. 4
      ErsatzTV.Core.Tests/Fakes/FakeLocalFileSystem.cs
  39. 4
      ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs
  40. 2
      ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs
  41. 2
      ErsatzTV.Core.Tests/Metadata/FallbackMetadataProviderTests.cs
  42. 74
      ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs
  43. 34
      ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs
  44. 12
      ErsatzTV.Core/Domain/Collection/Collection.cs
  45. 10
      ErsatzTV.Core/Domain/Collection/CollectionItem.cs
  46. 18
      ErsatzTV.Core/Domain/Library/Library.cs
  47. 8
      ErsatzTV.Core/Domain/Library/LibraryMediaKind.cs
  48. 15
      ErsatzTV.Core/Domain/Library/LibraryPath.cs
  49. 6
      ErsatzTV.Core/Domain/Library/LocalLibrary.cs
  50. 8
      ErsatzTV.Core/Domain/Library/PlexLibrary.cs
  51. 10
      ErsatzTV.Core/Domain/LocalMediaSource.cs
  52. 17
      ErsatzTV.Core/Domain/MediaItem.cs
  53. 13
      ErsatzTV.Core/Domain/MediaItem/Episode.cs
  54. 11
      ErsatzTV.Core/Domain/MediaItem/MediaFile.cs
  55. 27
      ErsatzTV.Core/Domain/MediaItem/MediaItem.cs
  56. 22
      ErsatzTV.Core/Domain/MediaItem/MediaVersion.cs
  57. 18
      ErsatzTV.Core/Domain/MediaItem/Movie.cs
  58. 12
      ErsatzTV.Core/Domain/MediaItem/PlexMediaItemPart.cs
  59. 8
      ErsatzTV.Core/Domain/MediaItem/PlexMovie.cs
  60. 13
      ErsatzTV.Core/Domain/MediaItem/Season.cs
  61. 10
      ErsatzTV.Core/Domain/MediaItem/Show.cs
  62. 12
      ErsatzTV.Core/Domain/MediaSource.cs
  63. 6
      ErsatzTV.Core/Domain/MediaSource/LocalMediaSource.cs
  64. 11
      ErsatzTV.Core/Domain/MediaSource/MediaSource.cs
  65. 4
      ErsatzTV.Core/Domain/MediaSource/PlexConnection.cs
  66. 6
      ErsatzTV.Core/Domain/MediaSource/PlexMediaSource.cs
  67. 10
      ErsatzTV.Core/Domain/MediaSourceType.cs
  68. 9
      ErsatzTV.Core/Domain/MediaType.cs
  69. 11
      ErsatzTV.Core/Domain/Metadata/EpisodeMetadata.cs
  70. 16
      ErsatzTV.Core/Domain/Metadata/Metadata.cs
  71. 8
      ErsatzTV.Core/Domain/Metadata/MetadataKind.cs
  72. 11
      ErsatzTV.Core/Domain/Metadata/NewMovieMetadata.cs
  73. 11
      ErsatzTV.Core/Domain/Metadata/ShowMetadata.cs
  74. 11
      ErsatzTV.Core/Domain/MovieMediaItem.cs
  75. 2
      ErsatzTV.Core/Domain/MovieMetadata.cs
  76. 6
      ErsatzTV.Core/Domain/PlayoutProgramScheduleAnchor.cs
  77. 10
      ErsatzTV.Core/Domain/PlexMediaSourceLibrary.cs
  78. 6
      ErsatzTV.Core/Domain/ProgramScheduleItem.cs
  79. 2
      ErsatzTV.Core/Domain/SimpleMediaCollection.cs
  80. 2
      ErsatzTV.Core/Domain/TelevisionShowMetadata.cs
  81. 2
      ErsatzTV.Core/Domain/TelevisionShowSource.cs
  82. 7
      ErsatzTV.Core/ErsatzTV.Core.csproj.DotSettings
  83. 8
      ErsatzTV.Core/Interfaces/Locking/IEntityLocker.cs
  84. 2
      ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs
  85. 2
      ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs
  86. 2
      ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs
  87. 15
      ErsatzTV.Core/Interfaces/Plex/IPlexMovieLibraryScanner.cs
  88. 9
      ErsatzTV.Core/Interfaces/Plex/IPlexServerApiClient.cs
  89. 12
      ErsatzTV.Core/Interfaces/Repositories/ILibraryRepository.cs
  90. 5
      ErsatzTV.Core/Interfaces/Repositories/IMediaSourceRepository.cs
  91. 9
      ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs
  92. 2
      ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs
  93. 6
      ErsatzTV.Core/Iptv/ChannelGuide.cs
  94. 20
      ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs
  95. 4
      ErsatzTV.Core/Metadata/LocalFileSystem.cs
  96. 30
      ErsatzTV.Core/Metadata/LocalMetadataProvider.cs
  97. 20
      ErsatzTV.Core/Metadata/MovieFolderScanner.cs
  98. 35
      ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs
  99. 20
      ErsatzTV.Core/Plex/PlexMediaEntry.cs
  100. 19
      ErsatzTV.Core/Plex/PlexMetadataEntry.cs
  101. Some files were not shown because too many files have changed in this diff Show More

2
ErsatzTV.Application/MediaCards/Mapper.cs

@ -37,7 +37,7 @@ namespace ErsatzTV.Application.MediaCards @@ -37,7 +37,7 @@ namespace ErsatzTV.Application.MediaCards
televisionEpisode.Poster,
televisionEpisode.Metadata?.Episode.ToString());
internal static MovieCardViewModel ProjectToViewModel(MovieMediaItem movie) =>
internal static MovieCardViewModel ProjectToViewModel(Movie movie) =>
new(
movie.Id,
movie.Metadata?.Title,

6
ErsatzTV.Application/MediaCollections/Commands/AddMovieToSimpleMediaCollectionHandler.cs

@ -49,14 +49,14 @@ namespace ErsatzTV.Application.MediaCollections.Commands @@ -49,14 +49,14 @@ namespace ErsatzTV.Application.MediaCollections.Commands
_mediaCollectionRepository.GetSimpleMediaCollectionWithItems(updateSimpleMediaCollection.MediaCollectionId)
.Map(v => v.ToValidation<BaseError>("SimpleMediaCollection does not exist."));
private Task<Validation<BaseError, MovieMediaItem>> ValidateMovies(
private Task<Validation<BaseError, Movie>> ValidateMovies(
AddMovieToSimpleMediaCollection request) =>
LoadMovie(request)
.Map(v => v.ToValidation<BaseError>("MovieMediaItem does not exist"));
private Task<Option<MovieMediaItem>> LoadMovie(AddMovieToSimpleMediaCollection request) =>
private Task<Option<Movie>> LoadMovie(AddMovieToSimpleMediaCollection request) =>
_movieRepository.GetMovie(request.MovieId);
private record RequestParameters(SimpleMediaCollection Collection, MovieMediaItem MovieToAdd);
private record RequestParameters(SimpleMediaCollection Collection, Movie MovieToAdd);
}
}

2
ErsatzTV.Application/MediaCollections/Commands/CreateSimpleMediaCollectionHandler.cs

@ -33,7 +33,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands @@ -33,7 +33,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
name => new SimpleMediaCollection
{
Name = name,
Movies = new List<MovieMediaItem>(),
Movies = new List<Movie>(),
TelevisionShows = new List<TelevisionShow>(),
TelevisionEpisodes = new List<TelevisionEpisodeMediaItem>(),
TelevisionSeasons = new List<TelevisionSeason>()

27
ErsatzTV.Application/MediaItems/Mapper.cs

@ -9,29 +9,29 @@ namespace ErsatzTV.Application.MediaItems @@ -9,29 +9,29 @@ namespace ErsatzTV.Application.MediaItems
internal static MediaItemViewModel ProjectToViewModel(MediaItem mediaItem) =>
new(
mediaItem.Id,
mediaItem.MediaSourceId,
mediaItem.LibraryPathId,
mediaItem.Path);
internal static MediaItemSearchResultViewModel ProjectToSearchViewModel(MediaItem mediaItem) =>
mediaItem switch
{
TelevisionEpisodeMediaItem e => ProjectToSearchViewModel(e),
MovieMediaItem m => ProjectToSearchViewModel(m),
Movie m => ProjectToSearchViewModel(m),
_ => throw new ArgumentOutOfRangeException()
};
private static MediaItemSearchResultViewModel ProjectToSearchViewModel(TelevisionEpisodeMediaItem mediaItem) =>
new(
mediaItem.Id,
GetSourceName(mediaItem.Source),
GetLibraryName(mediaItem),
"TV Show",
GetDisplayTitle(mediaItem),
GetDisplayDuration(mediaItem));
private static MediaItemSearchResultViewModel ProjectToSearchViewModel(MovieMediaItem mediaItem) =>
private static MediaItemSearchResultViewModel ProjectToSearchViewModel(Movie mediaItem) =>
new(
mediaItem.Id,
GetSourceName(mediaItem.Source),
GetLibraryName(mediaItem),
"Movie",
GetDisplayTitle(mediaItem),
GetDisplayDuration(mediaItem));
@ -43,7 +43,7 @@ namespace ErsatzTV.Application.MediaItems @@ -43,7 +43,7 @@ namespace ErsatzTV.Application.MediaItems
TelevisionEpisodeMediaItem e => e.Metadata != null
? $"{e.Metadata.Title} - s{e.Metadata.Season:00}e{e.Metadata.Episode:00}"
: Path.GetFileName(e.Path),
MovieMediaItem m => m.Metadata?.Title ?? Path.GetFileName(m.Path),
Movie m => m.Metadata?.Title ?? Path.GetFileName(m.Path),
_ => string.Empty
};
@ -52,11 +52,14 @@ namespace ErsatzTV.Application.MediaItems @@ -52,11 +52,14 @@ namespace ErsatzTV.Application.MediaItems
mediaItem.Statistics.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
mediaItem.Statistics.Duration);
private static string GetSourceName(MediaSource source) =>
source switch
{
LocalMediaSource lms => lms.Folder,
_ => source.Name
};
// TODO: fix this
private static string GetLibraryName(MediaItem item) =>
"Library Name";
// source switch
// {
// LocalMediaSource lms => "Local Media Source",
// _ => "unknown source"
// };
}
}

2
ErsatzTV.Application/MediaItems/MediaItemViewModel.cs

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
namespace ErsatzTV.Application.MediaItems
{
public record MediaItemViewModel(int Id, int MediaSourceId, string Path);
public record MediaItemViewModel(int Id, int LibraryPathId, string Path);
}

20
ErsatzTV.Application/MediaSources/Commands/CreateLocalMediaSource.cs

@ -1,10 +1,12 @@ @@ -1,10 +1,12 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
// using ErsatzTV.Core;
// using ErsatzTV.Core.Domain;
// using LanguageExt;
// using MediatR;
//
// namespace ErsatzTV.Application.MediaSources.Commands
// {
// public record CreateLocalMediaSource
// (string Name, MediaType MediaType, string Folder) : IRequest<Either<BaseError, MediaSourceViewModel>>;
// }
namespace ErsatzTV.Application.MediaSources.Commands
{
public record CreateLocalMediaSource
(string Name, MediaType MediaType, string Folder) : IRequest<Either<BaseError, MediaSourceViewModel>>;
}

154
ErsatzTV.Application/MediaSources/Commands/CreateLocalMediaSourceHandler.cs

@ -1,78 +1,80 @@ @@ -1,78 +1,80 @@
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 ErsatzTV.Application.MediaSources.Mapper;
using static LanguageExt.Prelude;
// 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 ErsatzTV.Application.MediaSources.Mapper;
// using static LanguageExt.Prelude;
//
// namespace ErsatzTV.Application.MediaSources.Commands
// {
// public class CreateLocalMediaSourceHandler : IRequestHandler<CreateLocalMediaSource,
// Either<BaseError, MediaSourceViewModel>>
// {
// private readonly IMediaSourceRepository _mediaSourceRepository;
//
// public CreateLocalMediaSourceHandler(IMediaSourceRepository mediaSourceRepository) =>
// _mediaSourceRepository = mediaSourceRepository;
//
// public Task<Either<BaseError, MediaSourceViewModel>> Handle(
// CreateLocalMediaSource request,
// CancellationToken cancellationToken) =>
// Validate(request).MapT(PersistLocalMediaSource).Bind(v => v.ToEitherAsync());
//
// private Task<MediaSourceViewModel> PersistLocalMediaSource(LocalMediaSource c) =>
// _mediaSourceRepository.Add(c).Map(ProjectToViewModel);
//
// private async Task<Validation<BaseError, LocalMediaSource>> Validate(CreateLocalMediaSource request) =>
// (await ValidateName(request), await ValidateFolder(request))
// .Apply(
// (name, folder) =>
// new LocalMediaSource
// {
// Name = name,
// MediaType = request.MediaType,
// Folder = folder
// });
//
// private async Task<Validation<BaseError, string>> ValidateName(CreateLocalMediaSource request)
// {
// List<string> allNames = await _mediaSourceRepository.GetAll()
// .Map(list => list.Map(c => c.Name).ToList());
//
// Validation<BaseError, string> result1 = request.NotEmpty(c => c.Name)
// .Bind(_ => request.NotLongerThan(50)(c => c.Name));
//
// var result2 = Optional(request.Name)
// .Filter(name => !allNames.Contains(name))
// .ToValidation<BaseError>("Media source name must be unique");
//
// return (result1, result2).Apply((_, _) => request.Name);
// }
//
// private async Task<Validation<BaseError, string>> ValidateFolder(CreateLocalMediaSource request)
// {
// List<string> allFolders = await _mediaSourceRepository.GetAll()
// .Map(list => list.OfType<LocalMediaSource>().Map(c => c.Folder).ToList());
//
//
// return Optional(request.Folder)
// .Filter(folder => allFolders.ForAll(f => !AreSubPaths(f, folder)))
// .ToValidation<BaseError>("Folder must not belong to another media source");
// }
//
// 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);
// }
// }
// }
namespace ErsatzTV.Application.MediaSources.Commands
{
public class CreateLocalMediaSourceHandler : IRequestHandler<CreateLocalMediaSource,
Either<BaseError, MediaSourceViewModel>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
public CreateLocalMediaSourceHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
public Task<Either<BaseError, MediaSourceViewModel>> Handle(
CreateLocalMediaSource request,
CancellationToken cancellationToken) =>
Validate(request).MapT(PersistLocalMediaSource).Bind(v => v.ToEitherAsync());
private Task<MediaSourceViewModel> PersistLocalMediaSource(LocalMediaSource c) =>
_mediaSourceRepository.Add(c).Map(ProjectToViewModel);
private async Task<Validation<BaseError, LocalMediaSource>> Validate(CreateLocalMediaSource request) =>
(await ValidateName(request), await ValidateFolder(request))
.Apply(
(name, folder) =>
new LocalMediaSource
{
Name = name,
MediaType = request.MediaType,
Folder = folder
});
private async Task<Validation<BaseError, string>> ValidateName(CreateLocalMediaSource request)
{
List<string> allNames = await _mediaSourceRepository.GetAll()
.Map(list => list.Map(c => c.Name).ToList());
Validation<BaseError, string> result1 = request.NotEmpty(c => c.Name)
.Bind(_ => request.NotLongerThan(50)(c => c.Name));
var result2 = Optional(request.Name)
.Filter(name => !allNames.Contains(name))
.ToValidation<BaseError>("Media source name must be unique");
return (result1, result2).Apply((_, _) => request.Name);
}
private async Task<Validation<BaseError, string>> ValidateFolder(CreateLocalMediaSource request)
{
List<string> allFolders = await _mediaSourceRepository.GetAll()
.Map(list => list.OfType<LocalMediaSource>().Map(c => c.Folder).ToList());
return Optional(request.Folder)
.Filter(folder => allFolders.ForAll(f => !AreSubPaths(f, folder)))
.ToValidation<BaseError>("Folder must not belong to another media source");
}
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);
}
}
}

114
ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs

@ -0,0 +1,114 @@ @@ -0,0 +1,114 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using Microsoft.Extensions.Logging;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.MediaSources.Commands
{
public class ScanLocalLibraryHandler : IRequestHandler<ForceScanLocalLibrary, Either<BaseError, string>>,
IRequestHandler<ScanLocalLibraryIfNeeded, Either<BaseError, string>>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IEntityLocker _entityLocker;
private readonly ILibraryRepository _libraryRepository;
private readonly ILogger<ScanLocalLibraryHandler> _logger;
private readonly IMovieFolderScanner _movieFolderScanner;
private readonly ITelevisionFolderScanner _televisionFolderScanner;
public ScanLocalLibraryHandler(
ILibraryRepository libraryRepository,
IConfigElementRepository configElementRepository,
IMovieFolderScanner movieFolderScanner,
ITelevisionFolderScanner televisionFolderScanner,
IEntityLocker entityLocker,
ILogger<ScanLocalLibraryHandler> logger)
{
_libraryRepository = libraryRepository;
_configElementRepository = configElementRepository;
_movieFolderScanner = movieFolderScanner;
_televisionFolderScanner = televisionFolderScanner;
_entityLocker = entityLocker;
_logger = logger;
}
public Task<Either<BaseError, string>> Handle(
ForceScanLocalLibrary request,
CancellationToken cancellationToken) => Handle(request);
public Task<Either<BaseError, string>> Handle(
ScanLocalLibraryIfNeeded request,
CancellationToken cancellationToken) => Handle(request);
private Task<Either<BaseError, string>>
Handle(IScanLocalLibrary request) =>
Validate(request)
.MapT(parameters => PerformScan(parameters).Map(_ => parameters.LocalLibrary.Name))
.Bind(v => v.ToEitherAsync());
private async Task<Unit> PerformScan(RequestParameters parameters)
{
(LocalLibrary localLibrary, string ffprobePath, bool forceScan) = parameters;
DateTimeOffset lastScan = localLibrary.LastScan ?? DateTime.MinValue;
if (forceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6))
{
foreach (LibraryPath libraryPath in localLibrary.Paths)
{
switch (localLibrary.MediaKind)
{
case LibraryMediaKind.Movies:
await _movieFolderScanner.ScanFolder(libraryPath, ffprobePath);
break;
case LibraryMediaKind.Shows:
await _televisionFolderScanner.ScanFolder(libraryPath, ffprobePath);
break;
}
}
localLibrary.LastScan = DateTimeOffset.Now;
await _libraryRepository.UpdateLastScan(localLibrary);
}
else
{
_logger.LogDebug(
"Skipping unforced scan of library {Name}",
localLibrary.Name);
}
_entityLocker.UnlockLibrary(localLibrary.Id);
return Unit.Default;
}
private async Task<Validation<BaseError, RequestParameters>> Validate(IScanLocalLibrary request) =>
(await LocalLibraryMustExist(request), await ValidateFFprobePath())
.Apply(
(library, ffprobePath) => new RequestParameters(
library,
ffprobePath,
request.ForceScan));
private Task<Validation<BaseError, LocalLibrary>> LocalLibraryMustExist(
IScanLocalLibrary request) =>
_libraryRepository.Get(request.LibraryId)
.Map(maybeLibrary => maybeLibrary.Map(ms => ms as LocalLibrary))
.Map(v => v.ToValidation<BaseError>($"Local library {request.LibraryId} does not exist."));
private Task<Validation<BaseError, string>> ValidateFFprobePath() =>
_configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath)
.FilterT(File.Exists)
.Map(
ffprobePath =>
ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system"));
private record RequestParameters(LocalLibrary LocalLibrary, string FFprobePath, bool ForceScan);
}
}

8
ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSource.cs

@ -4,18 +4,18 @@ using MediatR; @@ -4,18 +4,18 @@ using MediatR;
namespace ErsatzTV.Application.MediaSources.Commands
{
public interface IScanLocalMediaSource : IRequest<Either<BaseError, string>>, IBackgroundServiceRequest
public interface IScanLocalLibrary : IRequest<Either<BaseError, string>>, IBackgroundServiceRequest
{
int MediaSourceId { get; }
int LibraryId { get; }
bool ForceScan { get; }
}
public record ScanLocalMediaSourceIfNeeded(int MediaSourceId) : IScanLocalMediaSource
public record ScanLocalLibraryIfNeeded(int LibraryId) : IScanLocalLibrary
{
public bool ForceScan => false;
}
public record ForceScanLocalMediaSource(int MediaSourceId) : IScanLocalMediaSource
public record ForceScanLocalLibrary(int LibraryId) : IScanLocalLibrary
{
public bool ForceScan => true;
}

111
ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSourceHandler.cs

@ -1,111 +0,0 @@ @@ -1,111 +0,0 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using Microsoft.Extensions.Logging;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.MediaSources.Commands
{
public class ScanLocalMediaSourceHandler : IRequestHandler<ForceScanLocalMediaSource, Either<BaseError, string>>,
IRequestHandler<ScanLocalMediaSourceIfNeeded, Either<BaseError, string>>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IEntityLocker _entityLocker;
private readonly ILogger<ScanLocalMediaSourceHandler> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMovieFolderScanner _movieFolderScanner;
private readonly ITelevisionFolderScanner _televisionFolderScanner;
public ScanLocalMediaSourceHandler(
IMediaSourceRepository mediaSourceRepository,
IConfigElementRepository configElementRepository,
IMovieFolderScanner movieFolderScanner,
ITelevisionFolderScanner televisionFolderScanner,
IEntityLocker entityLocker,
ILogger<ScanLocalMediaSourceHandler> logger)
{
_mediaSourceRepository = mediaSourceRepository;
_configElementRepository = configElementRepository;
_movieFolderScanner = movieFolderScanner;
_televisionFolderScanner = televisionFolderScanner;
_entityLocker = entityLocker;
_logger = logger;
}
public Task<Either<BaseError, string>> Handle(
ForceScanLocalMediaSource request,
CancellationToken cancellationToken) =>
Handle((IScanLocalMediaSource) request, cancellationToken);
public Task<Either<BaseError, string>> Handle(
ScanLocalMediaSourceIfNeeded request,
CancellationToken cancellationToken) =>
Handle((IScanLocalMediaSource) request, cancellationToken);
private Task<Either<BaseError, string>>
Handle(IScanLocalMediaSource request, CancellationToken cancellationToken) =>
Validate(request)
.MapT(parameters => PerformScan(parameters).Map(_ => parameters.LocalMediaSource.Folder))
.Bind(v => v.ToEitherAsync());
private async Task<Unit> PerformScan(RequestParameters parameters)
{
DateTimeOffset lastScan = parameters.LocalMediaSource.LastScan ?? DateTime.MinValue;
if (parameters.ForceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6))
{
switch (parameters.LocalMediaSource.MediaType)
{
case MediaType.Movie:
await _movieFolderScanner.ScanFolder(parameters.LocalMediaSource, parameters.FFprobePath);
break;
case MediaType.TvShow:
await _televisionFolderScanner.ScanFolder(parameters.LocalMediaSource, parameters.FFprobePath);
break;
}
parameters.LocalMediaSource.LastScan = DateTimeOffset.Now;
await _mediaSourceRepository.Update(parameters.LocalMediaSource);
}
else
{
_logger.LogDebug(
"Skipping unforced scan of media source {Folder}",
parameters.LocalMediaSource.Folder);
}
_entityLocker.UnlockMediaSource(parameters.LocalMediaSource.Id);
return Unit.Default;
}
private async Task<Validation<BaseError, RequestParameters>> Validate(IScanLocalMediaSource request) =>
(await LocalMediaSourceMustExist(request), await ValidateFFprobePath())
.Apply(
(localMediaSource, ffprobePath) => new RequestParameters(
localMediaSource,
ffprobePath,
request.ForceScan));
private Task<Validation<BaseError, LocalMediaSource>> LocalMediaSourceMustExist(
IScanLocalMediaSource request) =>
_mediaSourceRepository.Get(request.MediaSourceId)
.Map(maybeMediaSource => maybeMediaSource.Map(ms => ms as LocalMediaSource))
.Map(v => v.ToValidation<BaseError>($"Local media source {request.MediaSourceId} does not exist."));
private Task<Validation<BaseError, string>> ValidateFFprobePath() =>
_configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath)
.FilterT(File.Exists)
.Map(
ffprobePath =>
ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system"));
private record RequestParameters(LocalMediaSource LocalMediaSource, string FFprobePath, bool ForceScan);
}
}

6
ErsatzTV.Application/MediaSources/LocalMediaSourceViewModel.cs

@ -1,7 +1,5 @@ @@ -1,7 +1,5 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.MediaSources
namespace ErsatzTV.Application.MediaSources
{
public record LocalMediaSourceViewModel(int Id, string Name, string Folder)
: MediaSourceViewModel(Id, Name, MediaSourceType.Local);
: MediaSourceViewModel(Id, Name);
}

13
ErsatzTV.Application/MediaSources/Mapper.cs

@ -1,7 +1,5 @@ @@ -1,7 +1,5 @@
using System;
using System.Linq;
using ErsatzTV.Core.Domain;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.MediaSources
{
@ -10,15 +8,10 @@ namespace ErsatzTV.Application.MediaSources @@ -10,15 +8,10 @@ namespace ErsatzTV.Application.MediaSources
internal static MediaSourceViewModel ProjectToViewModel(MediaSource mediaSource) =>
mediaSource switch
{
LocalMediaSource lms => new LocalMediaSourceViewModel(lms.Id, lms.Name, lms.Folder),
PlexMediaSource pms => ProjectToViewModel(pms),
// TODO: address this...
LocalMediaSource lms => new LocalMediaSourceViewModel(lms.Id, "LMS Name", "LMS Folder"),
PlexMediaSource pms => Plex.Mapper.ProjectToViewModel(pms),
_ => throw new NotSupportedException($"Unsupported media source {mediaSource.GetType().Name}")
};
internal static PlexMediaSourceViewModel ProjectToViewModel(PlexMediaSource plexMediaSource) =>
new(
plexMediaSource.Id,
plexMediaSource.Name,
Optional(plexMediaSource.Connections.SingleOrDefault(c => c.IsActive)).Match(c => c.Uri, string.Empty));
}
}

6
ErsatzTV.Application/MediaSources/MediaSourceViewModel.cs

@ -1,6 +1,4 @@ @@ -1,6 +1,4 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.MediaSources
namespace ErsatzTV.Application.MediaSources
{
public record MediaSourceViewModel(int Id, string Name, MediaSourceType SourceType);
public record MediaSourceViewModel(int Id, string Name);
}

9
ErsatzTV.Application/MediaSources/PlexMediaSourceViewModel.cs

@ -1,9 +0,0 @@ @@ -1,9 +0,0 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.MediaSources
{
public record PlexMediaSourceViewModel(int Id, string Name, string Address) : MediaSourceViewModel(
Id,
Name,
MediaSourceType.Plex);
}

2
ErsatzTV.Application/Movies/Mapper.cs

@ -4,7 +4,7 @@ namespace ErsatzTV.Application.Movies @@ -4,7 +4,7 @@ namespace ErsatzTV.Application.Movies
{
internal static class Mapper
{
internal static MovieViewModel ProjectToViewModel(MovieMediaItem movie) =>
internal static MovieViewModel ProjectToViewModel(Movie movie) =>
new(movie.Metadata.Title, movie.Metadata.Year?.ToString(), movie.Metadata.Plot, movie.Poster);
}
}

2
ErsatzTV.Application/Playouts/Mapper.cs

@ -27,7 +27,7 @@ namespace ErsatzTV.Application.Playouts @@ -27,7 +27,7 @@ namespace ErsatzTV.Application.Playouts
TelevisionEpisodeMediaItem e => e.Metadata != null
? $"{e.Metadata.Title} - s{e.Metadata.Season:00}e{e.Metadata.Episode:00}"
: Path.GetFileName(e.Path),
MovieMediaItem m => m.Metadata?.Title ?? Path.GetFileName(m.Path),
Movie m => m.Metadata?.Title ?? Path.GetFileName(m.Path),
_ => string.Empty
};

2
ErsatzTV.Application/MediaSources/Commands/StartPlexPinFlow.cs → ErsatzTV.Application/Plex/Commands/StartPlexPinFlow.cs

@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaSources.Commands
namespace ErsatzTV.Application.Plex.Commands
{
public record StartPlexPinFlow : IRequest<Either<BaseError, string>>;
}

2
ErsatzTV.Application/MediaSources/Commands/StartPlexPinFlowHandler.cs → ErsatzTV.Application/Plex/Commands/StartPlexPinFlowHandler.cs

@ -7,7 +7,7 @@ using LanguageExt; @@ -7,7 +7,7 @@ using LanguageExt;
using MediatR;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.MediaSources.Commands
namespace ErsatzTV.Application.Plex.Commands
{
public class StartPlexPinFlowHandler : IRequestHandler<StartPlexPinFlow, Either<BaseError, string>>
{

2
ErsatzTV.Application/MediaSources/Commands/SynchronizePlexLibraries.cs → ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraries.cs

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.MediaSources.Commands
namespace ErsatzTV.Application.Plex.Commands
{
public record SynchronizePlexLibraries(int PlexMediaSourceId) : MediatR.IRequest<Either<BaseError, Unit>>;
}

12
ErsatzTV.Application/MediaSources/Commands/SynchronizePlexLibrariesHandler.cs → ErsatzTV.Application/Plex/Commands/SynchronizePlexLibrariesHandler.cs

@ -10,7 +10,7 @@ using ErsatzTV.Core.Plex; @@ -10,7 +10,7 @@ using ErsatzTV.Core.Plex;
using LanguageExt;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Application.MediaSources.Commands
namespace ErsatzTV.Application.Plex.Commands
{
public class
SynchronizePlexLibrariesHandler : MediatR.IRequestHandler<SynchronizePlexLibraries, Either<BaseError, Unit>>
@ -51,7 +51,7 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -51,7 +51,7 @@ namespace ErsatzTV.Application.MediaSources.Commands
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
PlexMediaSource plexMediaSource)
{
Option<PlexMediaSourceConnection> maybeConnection =
Option<PlexConnection> maybeConnection =
plexMediaSource.Connections.SingleOrDefault(c => c.IsActive);
return maybeConnection.Map(connection => new ConnectionParameters(plexMediaSource, connection))
.ToValidation<BaseError>("Plex media source requires an active connection");
@ -68,14 +68,14 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -68,14 +68,14 @@ namespace ErsatzTV.Application.MediaSources.Commands
private async Task<Unit> SynchronizeLibraries(ConnectionParameters connectionParameters)
{
Either<BaseError, List<PlexMediaSourceLibrary>> maybeLibraries = await _plexServerApiClient.GetLibraries(
Either<BaseError, List<PlexLibrary>> maybeLibraries = await _plexServerApiClient.GetLibraries(
connectionParameters.ActiveConnection,
connectionParameters.PlexServerAuthToken);
await maybeLibraries.Match(
libraries =>
{
List<PlexMediaSourceLibrary> existing = connectionParameters.PlexMediaSource.Libraries;
var existing = connectionParameters.PlexMediaSource.Libraries.OfType<PlexLibrary>().ToList();
var toAdd = libraries.Filter(library => existing.All(l => l.Key != library.Key)).ToList();
var toRemove = existing.Filter(library => libraries.All(l => l.Key != library.Key)).ToList();
existing.AddRange(toAdd);
@ -87,7 +87,7 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -87,7 +87,7 @@ namespace ErsatzTV.Application.MediaSources.Commands
{
_logger.LogWarning(
"Unable to synchronize libraries from plex server {PlexServer}: {Error}",
connectionParameters.PlexMediaSource.Name,
connectionParameters.PlexMediaSource.ServerName,
error.Value);
return Task.CompletedTask;
@ -98,7 +98,7 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -98,7 +98,7 @@ namespace ErsatzTV.Application.MediaSources.Commands
private record ConnectionParameters(
PlexMediaSource PlexMediaSource,
PlexMediaSourceConnection ActiveConnection)
PlexConnection ActiveConnection)
{
public PlexServerAuthToken PlexServerAuthToken { get; set; }
}

25
ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryById.cs

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Plex.Commands
{
public interface ISynchronizePlexLibraryById : IRequest<Either<BaseError, string>>, IBackgroundServiceRequest
{
int PlexMediaSourceId { get; }
int PlexLibraryId { get; }
bool ForceScan { get; }
}
public record SynchronizePlexLibraryByIdIfNeeded
(int PlexMediaSourceId, int PlexLibraryId) : ISynchronizePlexLibraryById
{
public bool ForceScan => false;
}
public record ForceSynchronizePlexLibraryById
(int PlexMediaSourceId, int PlexLibraryId) : ISynchronizePlexLibraryById
{
public bool ForceScan => true;
}
}

144
ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs

@ -0,0 +1,144 @@ @@ -0,0 +1,144 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Plex;
using LanguageExt;
using MediatR;
using Microsoft.Extensions.Logging;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.Plex.Commands
{
public class
SynchronizePlexLibraryByIdHandler : IRequestHandler<ForceSynchronizePlexLibraryById, Either<BaseError, string>>,
IRequestHandler<SynchronizePlexLibraryByIdIfNeeded, Either<BaseError, string>>
{
private readonly IEntityLocker _entityLocker;
private readonly ILogger<SynchronizePlexLibraryByIdHandler> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IPlexMovieLibraryScanner _plexMovieLibraryScanner;
private readonly IPlexSecretStore _plexSecretStore;
public SynchronizePlexLibraryByIdHandler(
IMediaSourceRepository mediaSourceRepository,
IPlexSecretStore plexSecretStore,
IPlexMovieLibraryScanner plexMovieLibraryScanner,
IEntityLocker entityLocker,
ILogger<SynchronizePlexLibraryByIdHandler> logger)
{
_mediaSourceRepository = mediaSourceRepository;
_plexSecretStore = plexSecretStore;
_plexMovieLibraryScanner = plexMovieLibraryScanner;
_entityLocker = entityLocker;
_logger = logger;
}
public Task<Either<BaseError, string>> Handle(
ForceSynchronizePlexLibraryById request,
CancellationToken cancellationToken) => Handle(request);
public Task<Either<BaseError, string>> Handle(
SynchronizePlexLibraryByIdIfNeeded request,
CancellationToken cancellationToken) => Handle(request);
private Task<Either<BaseError, string>>
Handle(ISynchronizePlexLibraryById request) =>
Validate(request)
.MapT(parameters => Synchronize(parameters).Map(_ => parameters.Library.Name))
.Bind(v => v.ToEitherAsync());
private async Task<Unit> Synchronize(RequestParameters parameters)
{
DateTimeOffset lastScan = parameters.Library.LastScan ?? DateTime.MinValue;
if (parameters.ForceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6))
{
switch (parameters.Library.MediaKind)
{
case LibraryMediaKind.Movies:
await _plexMovieLibraryScanner.ScanLibrary(
parameters.ConnectionParameters.ActiveConnection,
parameters.ConnectionParameters.PlexServerAuthToken,
parameters.Library);
break;
case LibraryMediaKind.Shows:
// TODO: plex tv scanner
// await _televisionFolderScanner.ScanFolder(parameters.LocalMediaSource, parameters.FFprobePath);
break;
}
parameters.Library.LastScan = DateTimeOffset.Now;
await _mediaSourceRepository.Update(parameters.Library);
}
else
{
_logger.LogDebug(
"Skipping unforced scan of plex media library {Name}",
parameters.Library.Name);
}
// _entityLocker.UnlockMediaSource(parameters.MediaSource.Id);
return Unit.Default;
}
private async Task<Validation<BaseError, RequestParameters>> Validate(ISynchronizePlexLibraryById request) =>
(await ValidateConnection(request), await PlexLibraryMustExist(request))
.Apply(
(connectionParameters, plexLibrary) => new RequestParameters(
connectionParameters,
plexLibrary,
request.ForceScan
));
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection(
ISynchronizePlexLibraryById request) =>
PlexMediaSourceMustExist(request)
.BindT(MediaSourceMustHaveActiveConnection)
.BindT(MediaSourceMustHaveToken);
private Task<Validation<BaseError, PlexMediaSource>> PlexMediaSourceMustExist(
ISynchronizePlexLibraryById request) =>
_mediaSourceRepository.GetPlex(request.PlexMediaSourceId)
.Map(v => v.ToValidation<BaseError>($"Plex media source {request.PlexMediaSourceId} does not exist."));
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
PlexMediaSource plexMediaSource)
{
Option<PlexConnection> maybeConnection =
plexMediaSource.Connections.SingleOrDefault(c => c.IsActive);
return maybeConnection.Map(connection => new ConnectionParameters(plexMediaSource, connection))
.ToValidation<BaseError>("Plex media source requires an active connection");
}
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveToken(
ConnectionParameters connectionParameters)
{
Option<PlexServerAuthToken> maybeToken = await
_plexSecretStore.GetServerAuthToken(connectionParameters.PlexMediaSource.ClientIdentifier);
return maybeToken.Map(token => connectionParameters with { PlexServerAuthToken = token })
.ToValidation<BaseError>("Plex media source requires a token");
}
private Task<Validation<BaseError, PlexLibrary>> PlexLibraryMustExist(
ISynchronizePlexLibraryById request) =>
_mediaSourceRepository.GetPlexLibrary(request.PlexLibraryId)
.Map(v => v.ToValidation<BaseError>($"Plex library {request.PlexLibraryId} does not exist."));
private record RequestParameters(
ConnectionParameters ConnectionParameters,
PlexLibrary Library,
bool ForceScan);
private record ConnectionParameters(
PlexMediaSource PlexMediaSource,
PlexConnection ActiveConnection)
{
public PlexServerAuthToken PlexServerAuthToken { get; set; }
}
}
}

2
ErsatzTV.Application/MediaSources/Commands/SynchronizePlexMediaSources.cs → ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSources.cs

@ -4,7 +4,7 @@ using ErsatzTV.Core.Domain; @@ -4,7 +4,7 @@ using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaSources.Commands
namespace ErsatzTV.Application.Plex.Commands
{
public record
SynchronizePlexMediaSources : IRequest<Either<BaseError, List<PlexMediaSource>>>, IPlexBackgroundServiceRequest;

8
ErsatzTV.Application/MediaSources/Commands/SynchronizePlexMediaSourcesHandler.cs → ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSourcesHandler.cs

@ -9,7 +9,7 @@ using ErsatzTV.Core.Interfaces.Repositories; @@ -9,7 +9,7 @@ using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaSources.Commands
namespace ErsatzTV.Application.Plex.Commands
{
public class
SynchronizePlexMediaSourcesHandler : IRequestHandler<SynchronizePlexMediaSources,
@ -50,7 +50,7 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -50,7 +50,7 @@ namespace ErsatzTV.Application.MediaSources.Commands
existing =>
{
existing.ProductVersion = server.ProductVersion;
existing.Name = server.Name;
existing.ServerName = server.ServerName;
MergeConnections(existing.Connections, server.Connections);
if (existing.Connections.Any() && existing.Connections.All(c => !c.IsActive))
{
@ -72,8 +72,8 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -72,8 +72,8 @@ namespace ErsatzTV.Application.MediaSources.Commands
}
private void MergeConnections(
List<PlexMediaSourceConnection> existing,
List<PlexMediaSourceConnection> incoming)
List<PlexConnection> existing,
List<PlexConnection> incoming)
{
var toAdd = incoming.Filter(connection => existing.All(c => c.Uri != connection.Uri)).ToList();
var toRemove = existing.Filter(connection => incoming.All(c => c.Uri != connection.Uri)).ToList();

2
ErsatzTV.Application/MediaSources/Commands/TryCompletePlexPinFlow.cs → ErsatzTV.Application/Plex/Commands/TryCompletePlexPinFlow.cs

@ -3,7 +3,7 @@ using ErsatzTV.Core.Plex; @@ -3,7 +3,7 @@ using ErsatzTV.Core.Plex;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaSources.Commands
namespace ErsatzTV.Application.Plex.Commands
{
public record TryCompletePlexPinFlow(PlexAuthPin AuthPin) : IRequest<Either<BaseError, bool>>,
IPlexBackgroundServiceRequest;

2
ErsatzTV.Application/MediaSources/Commands/TryCompletePlexPinFlowHandler.cs → ErsatzTV.Application/Plex/Commands/TryCompletePlexPinFlowHandler.cs

@ -7,7 +7,7 @@ using ErsatzTV.Core.Interfaces.Plex; @@ -7,7 +7,7 @@ using ErsatzTV.Core.Interfaces.Plex;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaSources.Commands
namespace ErsatzTV.Application.Plex.Commands
{
public class TryCompletePlexPinFlowHandler : IRequestHandler<TryCompletePlexPinFlow, Either<BaseError, bool>>
{

11
ErsatzTV.Application/Plex/Commands/UpdatePlexLibraryPreferences.cs

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
using System.Collections.Generic;
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.Plex.Commands
{
public record UpdatePlexLibraryPreferences
(List<PlexLibraryPreference> Preferences) : MediatR.IRequest<Either<BaseError, Unit>>;
public record PlexLibraryPreference(int Id, bool ShouldSyncItems);
}

32
ErsatzTV.Application/Plex/Commands/UpdatePlexLibraryPreferencesHandler.cs

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
namespace ErsatzTV.Application.Plex.Commands
{
public class
UpdatePlexLibraryPreferencesHandler : MediatR.IRequestHandler<UpdatePlexLibraryPreferences,
Either<BaseError, Unit>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
public UpdatePlexLibraryPreferencesHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
public async Task<Either<BaseError, Unit>> Handle(
UpdatePlexLibraryPreferences request,
CancellationToken cancellationToken)
{
IEnumerable<int> toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id);
await _mediaSourceRepository.DisablePlexLibrarySync(toDisable);
IEnumerable<int> toEnable = request.Preferences.Filter(p => p.ShouldSyncItems).Map(p => p.Id);
await _mediaSourceRepository.EnablePlexLibrarySync(toEnable);
return Unit.Default;
}
}
}

18
ErsatzTV.Application/Plex/Mapper.cs

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
using System.Linq;
using ErsatzTV.Core.Domain;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Plex
{
internal static class Mapper
{
internal static PlexMediaSourceViewModel ProjectToViewModel(PlexMediaSource plexMediaSource) =>
new(
plexMediaSource.Id,
plexMediaSource.ServerName,
Optional(plexMediaSource.Connections.SingleOrDefault(c => c.IsActive)).Match(c => c.Uri, string.Empty));
internal static PlexLibraryViewModel ProjectToViewModel(PlexLibrary library) =>
new(library.Id, library.Name, library.MediaKind, library.ShouldSyncItems);
}
}

6
ErsatzTV.Application/Plex/PlexLibraryViewModel.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Plex
{
public record PlexLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind, bool ShouldSyncItems);
}

6
ErsatzTV.Application/Plex/PlexMediaSourceViewModel.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using ErsatzTV.Application.MediaSources;
namespace ErsatzTV.Application.Plex
{
public record PlexMediaSourceViewModel(int Id, string Name, string Address) : MediaSourceViewModel(Id, Name);
}

2
ErsatzTV.Application/MediaSources/Queries/GetAllPlexMediaSources.cs → ErsatzTV.Application/Plex/Queries/GetAllPlexMediaSources.cs

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.MediaSources.Queries
namespace ErsatzTV.Application.Plex.Queries
{
public record GetAllPlexMediaSources : IRequest<List<PlexMediaSourceViewModel>>;
}

4
ErsatzTV.Application/MediaSources/Queries/GetAllPlexMediaSourcesHandler.cs → ErsatzTV.Application/Plex/Queries/GetAllPlexMediaSourcesHandler.cs

@ -5,9 +5,9 @@ using System.Threading.Tasks; @@ -5,9 +5,9 @@ using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.MediaSources.Mapper;
using static ErsatzTV.Application.Plex.Mapper;
namespace ErsatzTV.Application.MediaSources.Queries
namespace ErsatzTV.Application.Plex.Queries
{
public class GetAllPlexMediaSourcesHandler : IRequestHandler<GetAllPlexMediaSources, List<PlexMediaSourceViewModel>>
{

7
ErsatzTV.Application/Plex/Queries/GetPlexLibrariesBySourceId.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.Plex.Queries
{
public record GetPlexLibrariesBySourceId(int PlexMediaSourceId) : IRequest<List<PlexLibraryViewModel>>;
}

27
ErsatzTV.Application/Plex/Queries/GetPlexLibrariesBySourceIdHandler.cs

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
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.Plex.Mapper;
namespace ErsatzTV.Application.Plex.Queries
{
public class
GetPlexLibrariesBySourceIdHandler : IRequestHandler<GetPlexLibrariesBySourceId,
List<PlexLibraryViewModel>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
public GetPlexLibrariesBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
public Task<List<PlexLibraryViewModel>> Handle(
GetPlexLibrariesBySourceId request,
CancellationToken cancellationToken) =>
_mediaSourceRepository.GetPlexLibraries(request.PlexMediaSourceId)
.Map(list => list.Map(ProjectToViewModel).ToList());
}
}

7
ErsatzTV.Application/Plex/Queries/GetPlexMediaSourceById.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Plex.Queries
{
public record GetPlexMediaSourceById(int PlexMediaSourceId) : IRequest<Option<PlexMediaSourceViewModel>>;
}

23
ErsatzTV.Application/Plex/Queries/GetPlexMediaSourceByIdHandler.cs

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Plex.Mapper;
namespace ErsatzTV.Application.Plex.Queries
{
public class
GetPlexMediaSourceByIdHandler : IRequestHandler<GetPlexMediaSourceById, Option<PlexMediaSourceViewModel>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
public GetPlexMediaSourceByIdHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
public Task<Option<PlexMediaSourceViewModel>> Handle(
GetPlexMediaSourceById request,
CancellationToken cancellationToken) =>
_mediaSourceRepository.GetPlex(request.PlexMediaSourceId).MapT(ProjectToViewModel);
}
}

4
ErsatzTV.Core.Tests/Fakes/FakeLocalFileSystem.cs

@ -41,8 +41,8 @@ namespace ErsatzTV.Core.Tests.Fakes @@ -41,8 +41,8 @@ namespace ErsatzTV.Core.Tests.Fakes
.Map(f => f.LastWriteTime)
.IfNone(DateTime.MinValue);
public bool IsMediaSourceAccessible(LocalMediaSource localMediaSource) =>
_files.Any(f => f.Path.StartsWith(localMediaSource.Folder));
public bool IsLibraryPathAccessible(LibraryPath libraryPath) =>
_files.Any(f => f.Path.StartsWith(libraryPath.Path + Path.DirectorySeparatorChar));
public IEnumerable<string> ListSubdirectories(string folder) =>
_folders.Map(f => f.Path).Filter(f => f.StartsWith(folder) && Directory.GetParent(f)?.FullName == folder);

4
ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs

@ -11,9 +11,9 @@ namespace ErsatzTV.Core.Tests.Fakes @@ -11,9 +11,9 @@ namespace ErsatzTV.Core.Tests.Fakes
{
public class FakeMediaCollectionRepository : IMediaCollectionRepository
{
private readonly Map<int, List<MovieMediaItem>> _data;
private readonly Map<int, List<Movie>> _data;
public FakeMediaCollectionRepository(Map<int, List<MovieMediaItem>> data) => _data = data;
public FakeMediaCollectionRepository(Map<int, List<Movie>> data) => _data = data;
public Task<SimpleMediaCollection> Add(SimpleMediaCollection collection) => throw new NotSupportedException();

2
ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs

@ -67,7 +67,7 @@ namespace ErsatzTV.Core.Tests.Fakes @@ -67,7 +67,7 @@ namespace ErsatzTV.Core.Tests.Fakes
public Task<Either<BaseError, TelevisionEpisodeMediaItem>> GetOrAddEpisode(
TelevisionSeason season,
int mediaSourceId,
LibraryPath libraryPath,
string path) => throw new NotSupportedException();
public Task<Unit> DeleteMissingSources(int localMediaSourceId, List<string> allFolders) =>

2
ErsatzTV.Core.Tests/Metadata/FallbackMetadataProviderTests.cs

@ -39,7 +39,7 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -39,7 +39,7 @@ namespace ErsatzTV.Core.Tests.Metadata
{
TelevisionEpisodeMetadata metadata = FallbackMetadataProvider.GetFallbackMetadata(
new TelevisionEpisodeMediaItem
{ Path = path, Source = new LocalMediaSource { MediaType = MediaType.TvShow } });
{ Path = path, LibraryPath = new LibraryPath() });
metadata.Title.Should().Be(title);
metadata.Season.Should().Be(season);

74
ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs

@ -40,10 +40,10 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -40,10 +40,10 @@ namespace ErsatzTV.Core.Tests.Metadata
public void SetUp()
{
_movieRepository = new Mock<IMovieRepository>();
_movieRepository.Setup(x => x.GetOrAdd(It.IsAny<int>(), It.IsAny<string>()))
_movieRepository.Setup(x => x.GetOrAdd(It.IsAny<LibraryPath>(), It.IsAny<string>()))
.Returns(
(int _, string path) =>
Right<BaseError, MovieMediaItem>(new MovieMediaItem { Path = path }).AsTask());
(LibraryPath _, string path) =>
Right<BaseError, Movie>(new Movie { Path = path }).AsTask());
_localStatisticsProvider = new Mock<ILocalStatisticsProvider>();
_localMetadataProvider = new Mock<ILocalMetadataProvider>();
@ -65,9 +65,9 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -65,9 +65,9 @@ namespace ErsatzTV.Core.Tests.Metadata
MovieFolderScanner service = GetService(
new FakeFileEntry(Path.Combine(FakeRoot, Path.Combine("Movie (2020)", "Movie (2020).mkv")))
);
var source = new LocalMediaSource { Folder = BadFakeRoot };
var libraryPath = new LibraryPath { Path = BadFakeRoot };
Either<BaseError, Unit> result = await service.ScanFolder(source, FFprobePath);
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
result.IsLeft.Should().BeTrue();
result.IfLeft(error => error.Should().BeOfType<MediaSourceInaccessible>());
@ -85,14 +85,14 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -85,14 +85,14 @@ namespace ErsatzTV.Core.Tests.Metadata
MovieFolderScanner service = GetService(
new FakeFileEntry(moviePath)
);
var source = new LocalMediaSource { Id = 1, Folder = FakeRoot };
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
Either<BaseError, Unit> result = await service.ScanFolder(source, FFprobePath);
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
result.IsRight.Should().BeTrue();
_movieRepository.Verify(x => x.GetOrAdd(It.IsAny<int>(), It.IsAny<string>()), Times.Once);
_movieRepository.Verify(x => x.GetOrAdd(1, moviePath), Times.Once);
_movieRepository.Verify(x => x.GetOrAdd(It.IsAny<LibraryPath>(), It.IsAny<string>()), Times.Once);
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
_localStatisticsProvider.Verify(
x => x.RefreshStatistics(FFprobePath, It.Is<MediaItem>(i => i.Path == moviePath)),
@ -118,14 +118,14 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -118,14 +118,14 @@ namespace ErsatzTV.Core.Tests.Metadata
new FakeFileEntry(moviePath),
new FakeFileEntry(metadataPath)
);
var source = new LocalMediaSource { Id = 1, Folder = FakeRoot };
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
Either<BaseError, Unit> result = await service.ScanFolder(source, FFprobePath);
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
result.IsRight.Should().BeTrue();
_movieRepository.Verify(x => x.GetOrAdd(It.IsAny<int>(), It.IsAny<string>()), Times.Once);
_movieRepository.Verify(x => x.GetOrAdd(1, moviePath), Times.Once);
_movieRepository.Verify(x => x.GetOrAdd(It.IsAny<LibraryPath>(), It.IsAny<string>()), Times.Once);
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
_localStatisticsProvider.Verify(
x => x.RefreshStatistics(FFprobePath, It.Is<MediaItem>(i => i.Path == moviePath)),
@ -151,14 +151,14 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -151,14 +151,14 @@ namespace ErsatzTV.Core.Tests.Metadata
new FakeFileEntry(moviePath),
new FakeFileEntry(metadataPath)
);
var source = new LocalMediaSource { Id = 1, Folder = FakeRoot };
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
Either<BaseError, Unit> result = await service.ScanFolder(source, FFprobePath);
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
result.IsRight.Should().BeTrue();
_movieRepository.Verify(x => x.GetOrAdd(It.IsAny<int>(), It.IsAny<string>()), Times.Once);
_movieRepository.Verify(x => x.GetOrAdd(1, moviePath), Times.Once);
_movieRepository.Verify(x => x.GetOrAdd(It.IsAny<LibraryPath>(), It.IsAny<string>()), Times.Once);
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
_localStatisticsProvider.Verify(
x => x.RefreshStatistics(FFprobePath, It.Is<MediaItem>(i => i.Path == moviePath)),
@ -188,14 +188,14 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -188,14 +188,14 @@ namespace ErsatzTV.Core.Tests.Metadata
new FakeFileEntry(moviePath),
new FakeFileEntry(posterPath)
);
var source = new LocalMediaSource { Id = 1, Folder = FakeRoot };
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
Either<BaseError, Unit> result = await service.ScanFolder(source, FFprobePath);
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
result.IsRight.Should().BeTrue();
_movieRepository.Verify(x => x.GetOrAdd(It.IsAny<int>(), It.IsAny<string>()), Times.Once);
_movieRepository.Verify(x => x.GetOrAdd(1, moviePath), Times.Once);
_movieRepository.Verify(x => x.GetOrAdd(It.IsAny<LibraryPath>(), It.IsAny<string>()), Times.Once);
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
_localStatisticsProvider.Verify(
x => x.RefreshStatistics(FFprobePath, It.Is<MediaItem>(i => i.Path == moviePath)),
@ -229,14 +229,14 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -229,14 +229,14 @@ namespace ErsatzTV.Core.Tests.Metadata
new FakeFileEntry(moviePath),
new FakeFileEntry(posterPath)
);
var source = new LocalMediaSource { Id = 1, Folder = FakeRoot };
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
Either<BaseError, Unit> result = await service.ScanFolder(source, FFprobePath);
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
result.IsRight.Should().BeTrue();
_movieRepository.Verify(x => x.GetOrAdd(It.IsAny<int>(), It.IsAny<string>()), Times.Once);
_movieRepository.Verify(x => x.GetOrAdd(1, moviePath), Times.Once);
_movieRepository.Verify(x => x.GetOrAdd(It.IsAny<LibraryPath>(), It.IsAny<string>()), Times.Once);
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
_localStatisticsProvider.Verify(
x => x.RefreshStatistics(FFprobePath, It.Is<MediaItem>(i => i.Path == moviePath)),
@ -269,14 +269,14 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -269,14 +269,14 @@ namespace ErsatzTV.Core.Tests.Metadata
Path.GetDirectoryName(moviePath) ?? string.Empty,
$"Movie (2020)-{extraFile}{videoExtension}"))
);
var source = new LocalMediaSource { Id = 1, Folder = FakeRoot };
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
Either<BaseError, Unit> result = await service.ScanFolder(source, FFprobePath);
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
result.IsRight.Should().BeTrue();
_movieRepository.Verify(x => x.GetOrAdd(It.IsAny<int>(), It.IsAny<string>()), Times.Once);
_movieRepository.Verify(x => x.GetOrAdd(1, moviePath), Times.Once);
_movieRepository.Verify(x => x.GetOrAdd(It.IsAny<LibraryPath>(), It.IsAny<string>()), Times.Once);
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
_localStatisticsProvider.Verify(
x => x.RefreshStatistics(FFprobePath, It.Is<MediaItem>(i => i.Path == moviePath)),
@ -305,14 +305,14 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -305,14 +305,14 @@ namespace ErsatzTV.Core.Tests.Metadata
Path.GetDirectoryName(moviePath) ?? string.Empty,
Path.Combine(extraFolder, $"Movie (2020){videoExtension}")))
);
var source = new LocalMediaSource { Id = 1, Folder = FakeRoot };
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
Either<BaseError, Unit> result = await service.ScanFolder(source, FFprobePath);
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
result.IsRight.Should().BeTrue();
_movieRepository.Verify(x => x.GetOrAdd(It.IsAny<int>(), It.IsAny<string>()), Times.Once);
_movieRepository.Verify(x => x.GetOrAdd(1, moviePath), Times.Once);
_movieRepository.Verify(x => x.GetOrAdd(It.IsAny<LibraryPath>(), It.IsAny<string>()), Times.Once);
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
_localStatisticsProvider.Verify(
x => x.RefreshStatistics(FFprobePath, It.Is<MediaItem>(i => i.Path == moviePath)),
@ -335,14 +335,14 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -335,14 +335,14 @@ namespace ErsatzTV.Core.Tests.Metadata
MovieFolderScanner service = GetService(
new FakeFileEntry(moviePath)
);
var source = new LocalMediaSource { Id = 1, Folder = FakeRoot };
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
Either<BaseError, Unit> result = await service.ScanFolder(source, FFprobePath);
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
result.IsRight.Should().BeTrue();
_movieRepository.Verify(x => x.GetOrAdd(It.IsAny<int>(), It.IsAny<string>()), Times.Once);
_movieRepository.Verify(x => x.GetOrAdd(1, moviePath), Times.Once);
_movieRepository.Verify(x => x.GetOrAdd(It.IsAny<LibraryPath>(), It.IsAny<string>()), Times.Once);
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
_localStatisticsProvider.Verify(
x => x.RefreshStatistics(FFprobePath, It.Is<MediaItem>(i => i.Path == moviePath)),

34
ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs

@ -40,7 +40,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -40,7 +40,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
[Test]
public async Task InitialFlood_Should_StartAtMidnight()
{
var mediaItems = new List<MovieMediaItem>
var mediaItems = new List<Movie>
{
TestMovie(1, TimeSpan.FromHours(6), DateTime.Today)
};
@ -59,7 +59,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -59,7 +59,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
[Test]
public async Task InitialFlood_Should_StartAtMidnight_With_LateStart()
{
var mediaItems = new List<MovieMediaItem>
var mediaItems = new List<Movie>
{
TestMovie(1, TimeSpan.FromHours(6), DateTime.Today)
};
@ -79,7 +79,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -79,7 +79,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
[Test]
public async Task ChronologicalContent_Should_CreateChronologicalItems()
{
var mediaItems = new List<MovieMediaItem>
var mediaItems = new List<Movie>
{
TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)),
TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1))
@ -105,7 +105,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -105,7 +105,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
[Test]
public async Task ChronologicalFlood_Should_AnchorAndMaintainExistingPlayout()
{
var mediaItems = new List<MovieMediaItem>
var mediaItems = new List<Movie>
{
TestMovie(1, TimeSpan.FromHours(6), DateTime.Today),
TestMovie(2, TimeSpan.FromHours(6), DateTime.Today.AddHours(1))
@ -142,7 +142,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -142,7 +142,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
[Test]
public async Task ChronologicalFlood_Should_AnchorAndReturnNewPlayoutItems()
{
var mediaItems = new List<MovieMediaItem>
var mediaItems = new List<Movie>
{
TestMovie(1, TimeSpan.FromHours(6), DateTime.Today),
TestMovie(2, TimeSpan.FromHours(6), DateTime.Today.AddHours(1))
@ -180,7 +180,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -180,7 +180,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
[Test]
public async Task ShuffleFloodRebuild_Should_IgnoreAnchors()
{
var mediaItems = new List<MovieMediaItem>
var mediaItems = new List<Movie>
{
TestMovie(1, TimeSpan.FromHours(1), DateTime.Today),
TestMovie(2, TimeSpan.FromHours(1), DateTime.Today.AddHours(1)),
@ -223,7 +223,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -223,7 +223,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
[Test]
public async Task ShuffleFlood_Should_MaintainRandomSeed()
{
var mediaItems = new List<MovieMediaItem>
var mediaItems = new List<Movie>
{
TestMovie(1, TimeSpan.FromHours(1), DateTime.Today),
TestMovie(2, TimeSpan.FromHours(1), DateTime.Today.AddHours(1)),
@ -259,7 +259,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -259,7 +259,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
{
Id = 1,
Name = "Flood Items",
Movies = new List<MovieMediaItem>
Movies = new List<Movie>
{
TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)),
TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1))
@ -270,7 +270,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -270,7 +270,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
{
Id = 2,
Name = "Fixed Items",
Movies = new List<MovieMediaItem>
Movies = new List<Movie>
{
TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1))
}
@ -337,7 +337,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -337,7 +337,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
{
Id = 1,
Name = "Flood Items",
Movies = new List<MovieMediaItem>
Movies = new List<Movie>
{
TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)),
TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1))
@ -348,7 +348,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -348,7 +348,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
{
Id = 2,
Name = "Fixed Items",
Movies = new List<MovieMediaItem>
Movies = new List<Movie>
{
TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)),
TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 2))
@ -422,7 +422,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -422,7 +422,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
{
Id = 1,
Name = "Flood Items",
Movies = new List<MovieMediaItem>
Movies = new List<Movie>
{
TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)),
TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1))
@ -433,7 +433,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -433,7 +433,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
{
Id = 2,
Name = "Fixed Items",
Movies = new List<MovieMediaItem>
Movies = new List<Movie>
{
TestMovie(3, TimeSpan.FromHours(0.75), new DateTime(2020, 1, 1)),
TestMovie(4, TimeSpan.FromHours(1.5), new DateTime(2020, 1, 2))
@ -511,7 +511,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -511,7 +511,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
{
Id = 1,
Name = "Multiple Items",
Movies = new List<MovieMediaItem>
Movies = new List<Movie>
{
TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)),
TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1))
@ -522,7 +522,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -522,7 +522,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
{
Id = 2,
Name = "Dynamic Items",
Movies = new List<MovieMediaItem>
Movies = new List<Movie>
{
TestMovie(3, TimeSpan.FromHours(0.75), new DateTime(2020, 1, 1)),
TestMovie(4, TimeSpan.FromHours(1.5), new DateTime(2020, 1, 2))
@ -607,7 +607,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -607,7 +607,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
StartTime = null
};
private static MovieMediaItem TestMovie(int id, TimeSpan duration, DateTime aired) =>
private static Movie TestMovie(int id, TimeSpan duration, DateTime aired) =>
new()
{
Id = id,
@ -615,7 +615,7 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -615,7 +615,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
Statistics = new MediaItemStatistics { Duration = duration }
};
private TestData TestDataFloodForItems(List<MovieMediaItem> mediaItems, PlaybackOrder playbackOrder)
private TestData TestDataFloodForItems(List<Movie> mediaItems, PlaybackOrder playbackOrder)
{
var mediaCollection = new SimpleMediaCollection
{

12
ErsatzTV.Core/Domain/Collection/Collection.cs

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace ErsatzTV.Core.Domain
{
public class Collection
{
public int Id { get; set; }
public string Name { get; set; }
public List<MediaItem> MediaItems { get; set; }
public List<CollectionItem> CollectionItems { get; set; }
}
}

10
ErsatzTV.Core/Domain/Collection/CollectionItem.cs

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
namespace ErsatzTV.Core.Domain
{
public class CollectionItem
{
public int CollectionId { get; set; }
public Collection Collection { get; set; }
public int MediaItemId { get; set; }
public MediaItem MediaItem { get; set; }
}
}

18
ErsatzTV.Core/Domain/Library/Library.cs

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
namespace ErsatzTV.Core.Domain
{
public abstract class Library
{
public int Id { get; set; }
public string Name { get; set; }
public LibraryMediaKind MediaKind { get; set; }
public DateTimeOffset? LastScan { get; set; }
public int MediaSourceId { get; set; }
public MediaSource MediaSource { get; set; }
public List<LibraryPath> Paths { get; set; }
}
}

8
ErsatzTV.Core/Domain/Library/LibraryMediaKind.cs

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
namespace ErsatzTV.Core.Domain
{
public enum LibraryMediaKind
{
Movies = 1,
Shows = 2
}
}

15
ErsatzTV.Core/Domain/Library/LibraryPath.cs

@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
using System.Collections.Generic;
namespace ErsatzTV.Core.Domain
{
public class LibraryPath
{
public int Id { get; set; }
public string Path { get; set; }
public int LibraryId { get; set; }
public Library Library { get; set; }
public List<MediaItem> MediaItems { get; set; }
}
}

6
ErsatzTV.Core/Domain/Library/LocalLibrary.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
namespace ErsatzTV.Core.Domain
{
public class LocalLibrary : Library
{
}
}

8
ErsatzTV.Core/Domain/Library/PlexLibrary.cs

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
namespace ErsatzTV.Core.Domain
{
public class PlexLibrary : Library
{
public string Key { get; set; }
public bool ShouldSyncItems { get; set; }
}
}

10
ErsatzTV.Core/Domain/LocalMediaSource.cs

@ -1,10 +0,0 @@ @@ -1,10 +0,0 @@
namespace ErsatzTV.Core.Domain
{
public class LocalMediaSource : MediaSource
{
public LocalMediaSource() => SourceType = MediaSourceType.Local;
public MediaType MediaType { get; set; }
public string Folder { get; set; }
}
}

17
ErsatzTV.Core/Domain/MediaItem.cs

@ -1,17 +0,0 @@ @@ -1,17 +0,0 @@
using System;
using ErsatzTV.Core.Interfaces.Domain;
namespace ErsatzTV.Core.Domain
{
public class MediaItem : IHasAPoster
{
public int Id { get; set; }
public int MediaSourceId { get; set; }
public MediaSource Source { get; set; }
public MediaItemStatistics Statistics { get; set; }
public DateTime? LastWriteTime { get; set; }
public string Path { get; set; }
public string Poster { get; set; }
public DateTime? PosterLastWriteTime { get; set; }
}
}

13
ErsatzTV.Core/Domain/MediaItem/Episode.cs

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
using System.Collections.Generic;
namespace ErsatzTV.Core.Domain
{
public class Episode : MediaItem
{
public int EpisodeNumber { get; set; }
public int SeasonId { get; set; }
public Season Season { get; set; }
public List<EpisodeMetadata> EpisodeMetadata { get; set; }
public List<MediaVersion> MediaVersions { get; set; }
}
}

11
ErsatzTV.Core/Domain/MediaItem/MediaFile.cs

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
namespace ErsatzTV.Core.Domain
{
public class MediaFile
{
public int Id { get; set; }
public string Path { get; set; }
public int MediaVersionId { get; set; }
public MediaVersion MediaVersion { get; set; }
}
}

27
ErsatzTV.Core/Domain/MediaItem/MediaItem.cs

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using ErsatzTV.Core.Interfaces.Domain;
namespace ErsatzTV.Core.Domain
{
public class MediaItem : IHasAPoster
{
public int Id { get; set; }
public MediaItemStatistics Statistics { get; set; }
public DateTime? LastWriteTime { get; set; }
public int LibraryPathId { get; set; }
public LibraryPath LibraryPath { get; set; }
public string Path { get; set; }
public string Poster { get; set; }
public DateTime? PosterLastWriteTime { get; set; }
// temporary fields to help migrations...
public int TelevisionShowId { get; set; }
public int TelevisionSeasonId { get; set; }
public int TelevisionEpisodeId { get; set; }
public List<Collection> Collections { get; set; }
public List<CollectionItem> CollectionItems { get; set; }
}
}

22
ErsatzTV.Core/Domain/MediaItem/MediaVersion.cs

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
namespace ErsatzTV.Core.Domain
{
public class MediaVersion
{
public int Id { get; set; }
public string Name { get; set; }
public List<MediaFile> MediaFiles { get; set; }
public TimeSpan Duration { get; set; }
public string SampleAspectRatio { get; set; }
public string DisplayAspectRatio { get; set; }
public string VideoCodec { get; set; }
public string AudioCodec { get; set; }
public bool IsInterlaced { get; set; }
public int Width { get; set; }
public int Height { get; set; }
}
}

18
ErsatzTV.Core/Domain/MediaItem/Movie.cs

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
using System.Collections.Generic;
namespace ErsatzTV.Core.Domain
{
public class Movie : MediaItem
{
// TODO: remove MetadataId
public int MetadataId { get; set; }
// TODO: remove Metadata
public MovieMetadata Metadata { get; set; }
public List<NewMovieMetadata> MovieMetadata { get; set; }
public List<MediaVersion> MediaVersions { get; set; }
// TODO: remove SimpleMediaCollections
public List<SimpleMediaCollection> SimpleMediaCollections { get; set; }
}
}

12
ErsatzTV.Core/Domain/MediaItem/PlexMediaItemPart.cs

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
namespace ErsatzTV.Core.Domain
{
public class PlexMediaItemPart
{
public int Id { get; set; }
public int PlexId { get; set; }
public string Key { get; set; }
public int Duration { get; set; }
public string File { get; set; }
public int Size { get; set; }
}
}

8
ErsatzTV.Core/Domain/MediaItem/PlexMovie.cs

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
namespace ErsatzTV.Core.Domain
{
public class PlexMovie : Movie
{
public string Key { get; set; }
public PlexMediaItemPart Part { get; set; }
}
}

13
ErsatzTV.Core/Domain/MediaItem/Season.cs

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
using System.Collections.Generic;
namespace ErsatzTV.Core.Domain
{
public class Season : MediaItem
{
public int SeasonNumber { get; set; }
public int ShowId { get; set; }
public Show Show { get; set; }
public List<Episode> Episodes { get; set; }
}
}

10
ErsatzTV.Core/Domain/MediaItem/Show.cs

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace ErsatzTV.Core.Domain
{
public class Show : MediaItem
{
public List<Season> Seasons { get; set; }
public List<ShowMetadata> ShowMetadata { get; set; }
}
}

12
ErsatzTV.Core/Domain/MediaSource.cs

@ -1,12 +0,0 @@ @@ -1,12 +0,0 @@
using System;
namespace ErsatzTV.Core.Domain
{
public abstract class MediaSource
{
public int Id { get; set; }
public MediaSourceType SourceType { get; set; }
public string Name { get; set; }
public DateTimeOffset? LastScan { get; set; }
}
}

6
ErsatzTV.Core/Domain/MediaSource/LocalMediaSource.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
namespace ErsatzTV.Core.Domain
{
public class LocalMediaSource : MediaSource
{
}
}

11
ErsatzTV.Core/Domain/MediaSource/MediaSource.cs

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace ErsatzTV.Core.Domain
{
public abstract class MediaSource
{
public int Id { get; set; }
public List<Library> Libraries { get; set; }
}
}

4
ErsatzTV.Core/Domain/PlexMediaSourceConnection.cs → ErsatzTV.Core/Domain/MediaSource/PlexConnection.cs

@ -1,9 +1,11 @@ @@ -1,9 +1,11 @@
namespace ErsatzTV.Core.Domain
{
public class PlexMediaSourceConnection
public class PlexConnection
{
public int Id { get; set; }
public bool IsActive { get; set; }
public string Uri { get; set; }
public int PlexMediaSourceId { get; set; }
public PlexMediaSource PlexMediaSource { get; set; }
}
}

6
ErsatzTV.Core/Domain/PlexMediaSource.cs → ErsatzTV.Core/Domain/MediaSource/PlexMediaSource.cs

@ -4,13 +4,11 @@ namespace ErsatzTV.Core.Domain @@ -4,13 +4,11 @@ namespace ErsatzTV.Core.Domain
{
public class PlexMediaSource : MediaSource
{
public PlexMediaSource() => SourceType = MediaSourceType.Plex;
public string ServerName { get; set; }
public string ProductVersion { get; set; }
public string ClientIdentifier { get; set; }
// public bool IsOwned { get; set; }
public List<PlexMediaSourceConnection> Connections { get; set; }
public List<PlexMediaSourceLibrary> Libraries { get; set; }
public List<PlexConnection> Connections { get; set; }
}
}

10
ErsatzTV.Core/Domain/MediaSourceType.cs

@ -1,10 +0,0 @@ @@ -1,10 +0,0 @@
namespace ErsatzTV.Core.Domain
{
public enum MediaSourceType
{
None = 0,
Local = 1,
Plex = 2,
Jellyfin = 3
}
}

9
ErsatzTV.Core/Domain/MediaType.cs

@ -1,9 +0,0 @@ @@ -1,9 +0,0 @@
namespace ErsatzTV.Core.Domain
{
public enum MediaType
{
Other = 0,
TvShow = 1,
Movie = 2
}
}

11
ErsatzTV.Core/Domain/Metadata/EpisodeMetadata.cs

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
namespace ErsatzTV.Core.Domain
{
public class EpisodeMetadata : Metadata
{
public string Outline { get; set; }
public string Plot { get; set; }
public string Tagline { get; set; }
public int EpisodeId { get; set; }
public Episode Episode { get; set; }
}
}

16
ErsatzTV.Core/Domain/Metadata/Metadata.cs

@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
using System;
namespace ErsatzTV.Core.Domain
{
public class Metadata
{
public int Id { get; set; }
public MetadataKind MetadataKind { get; set; }
public string Title { get; set; }
public string OriginalTitle { get; set; }
public string SortTitle { get; set; }
public DateTimeOffset? ReleaseDate { get; set; }
public DateTimeOffset DateAdded { get; set; }
public DateTimeOffset DateUpdated { get; set; }
}
}

8
ErsatzTV.Core/Domain/Metadata/MetadataKind.cs

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
namespace ErsatzTV.Core.Domain
{
public enum MetadataKind
{
Fallback,
Sidecar
}
}

11
ErsatzTV.Core/Domain/Metadata/NewMovieMetadata.cs

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
namespace ErsatzTV.Core.Domain
{
public class NewMovieMetadata : Metadata
{
public string Outline { get; set; }
public string Plot { get; set; }
public string Tagline { get; set; }
public int MovieId { get; set; }
public Movie Movie { get; set; }
}
}

11
ErsatzTV.Core/Domain/Metadata/ShowMetadata.cs

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
namespace ErsatzTV.Core.Domain
{
public class ShowMetadata : Metadata
{
public string Outline { get; set; }
public string Plot { get; set; }
public string Tagline { get; set; }
public int ShowId { get; set; }
public Show Show { get; set; }
}
}

11
ErsatzTV.Core/Domain/MovieMediaItem.cs

@ -1,11 +0,0 @@ @@ -1,11 +0,0 @@
using System.Collections.Generic;
namespace ErsatzTV.Core.Domain
{
public class MovieMediaItem : MediaItem
{
public int MetadataId { get; set; }
public MovieMetadata Metadata { get; set; }
public List<SimpleMediaCollection> SimpleMediaCollections { get; set; }
}
}

2
ErsatzTV.Core/Domain/MovieMetadata.cs

@ -6,7 +6,7 @@ namespace ErsatzTV.Core.Domain @@ -6,7 +6,7 @@ namespace ErsatzTV.Core.Domain
{
public int Id { get; set; }
public int MovieId { get; set; }
public MovieMediaItem Movie { get; set; }
public Movie Movie { get; set; }
public int? Year { get; set; }
public DateTime? Premiered { get; set; }
public string Plot { get; set; }

6
ErsatzTV.Core/Domain/PlayoutProgramScheduleAnchor.cs

@ -8,7 +8,13 @@ @@ -8,7 +8,13 @@
public int ProgramScheduleId { get; set; }
public ProgramSchedule ProgramSchedule { get; set; }
public ProgramScheduleItemCollectionType CollectionType { get; set; }
// TODO: remove this
public int CollectionId { get; set; }
// TODO: rename this
public int? NewCollectionId { get; set; }
public Collection Collection { get; set; }
public int? MediaItemId { get; set; }
public MediaItem MediaItem { get; set; }
public MediaCollectionEnumeratorState EnumeratorState { get; set; }
}
}

10
ErsatzTV.Core/Domain/PlexMediaSourceLibrary.cs

@ -1,10 +0,0 @@ @@ -1,10 +0,0 @@
namespace ErsatzTV.Core.Domain
{
public class PlexMediaSourceLibrary
{
public int Id { get; set; }
public string Key { get; init; }
public string Name { get; init; }
public MediaType MediaType { get; init; }
}
}

6
ErsatzTV.Core/Domain/ProgramScheduleItem.cs

@ -9,6 +9,7 @@ namespace ErsatzTV.Core.Domain @@ -9,6 +9,7 @@ namespace ErsatzTV.Core.Domain
public StartType StartType => StartTime.HasValue ? StartType.Fixed : StartType.Dynamic;
public TimeSpan? StartTime { get; set; }
public ProgramScheduleItemCollectionType CollectionType { get; set; }
// TODO: remove these
public int? MediaCollectionId { get; set; }
public MediaCollection MediaCollection { get; set; }
public int? TelevisionShowId { get; set; }
@ -17,5 +18,10 @@ namespace ErsatzTV.Core.Domain @@ -17,5 +18,10 @@ namespace ErsatzTV.Core.Domain
public TelevisionSeason TelevisionSeason { get; set; }
public int ProgramScheduleId { get; set; }
public ProgramSchedule ProgramSchedule { get; set; }
public int? CollectionId { get; set; }
public Collection Collection { get; set; }
public int? MediaItemId { get; set; }
public MediaItem MediaItem { get; set; }
}
}

2
ErsatzTV.Core/Domain/SimpleMediaCollection.cs

@ -4,7 +4,7 @@ namespace ErsatzTV.Core.Domain @@ -4,7 +4,7 @@ namespace ErsatzTV.Core.Domain
{
public class SimpleMediaCollection : MediaCollection
{
public List<MovieMediaItem> Movies { get; set; }
public List<Movie> Movies { get; set; }
public List<TelevisionShow> TelevisionShows { get; set; }
public List<TelevisionSeason> TelevisionSeasons { get; set; }
public List<TelevisionEpisodeMediaItem> TelevisionEpisodes { get; set; }

2
ErsatzTV.Core/Domain/TelevisionShowMetadata.cs

@ -7,6 +7,8 @@ namespace ErsatzTV.Core.Domain @@ -7,6 +7,8 @@ namespace ErsatzTV.Core.Domain
public int Id { get; set; }
public int TelevisionShowId { get; set; }
public TelevisionShow TelevisionShow { get; set; }
// public int ShowId { get; set; }
// public Show Show { get; set; }
public MetadataSource Source { get; set; }
public DateTime? LastWriteTime { get; set; }
public string Title { get; set; }

2
ErsatzTV.Core/Domain/TelevisionShowSource.cs

@ -1,5 +1,7 @@ @@ -1,5 +1,7 @@
namespace ErsatzTV.Core.Domain
{
// used for shows split across sources (folders)
// won't need to convert, since shows will be linked via metadata
public class TelevisionShowSource
{
public int Id { get; set; }

7
ErsatzTV.Core/ErsatzTV.Core.csproj.DotSettings

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=domain_005Ccollection/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=domain_005Clibrary/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=domain_005Cmediaitem/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=domain_005Cmediasource/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=domain_005Cmediasources/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=domain_005Cmetadata/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

8
ErsatzTV.Core/Interfaces/Locking/IEntityLocker.cs

@ -4,9 +4,9 @@ namespace ErsatzTV.Core.Interfaces.Locking @@ -4,9 +4,9 @@ namespace ErsatzTV.Core.Interfaces.Locking
{
public interface IEntityLocker
{
event EventHandler OnMediaSourceChanged;
bool LockMediaSource(int mediaSourceId);
bool UnlockMediaSource(int mediaSourceId);
bool IsMediaSourceLocked(int mediaSourceId);
event EventHandler OnLibraryChanged;
bool LockLibrary(int libraryId);
bool UnlockLibrary(int libraryId);
bool IsLibraryLocked(int libraryId);
}
}

2
ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs

@ -8,7 +8,7 @@ namespace ErsatzTV.Core.Interfaces.Metadata @@ -8,7 +8,7 @@ namespace ErsatzTV.Core.Interfaces.Metadata
public interface ILocalFileSystem
{
DateTime GetLastWriteTime(string path);
bool IsMediaSourceAccessible(LocalMediaSource localMediaSource);
bool IsLibraryPathAccessible(LibraryPath libraryPath);
IEnumerable<string> ListSubdirectories(string folder);
IEnumerable<string> ListFiles(string folder);
bool FileExists(string path);

2
ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs

@ -6,6 +6,6 @@ namespace ErsatzTV.Core.Interfaces.Metadata @@ -6,6 +6,6 @@ namespace ErsatzTV.Core.Interfaces.Metadata
{
public interface IMovieFolderScanner
{
Task<Either<BaseError, Unit>> ScanFolder(LocalMediaSource localMediaSource, string ffprobePath);
Task<Either<BaseError, Unit>> ScanFolder(LibraryPath libraryPath, string ffprobePath);
}
}

2
ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs

@ -6,6 +6,6 @@ namespace ErsatzTV.Core.Interfaces.Metadata @@ -6,6 +6,6 @@ namespace ErsatzTV.Core.Interfaces.Metadata
{
public interface ITelevisionFolderScanner
{
Task<Unit> ScanFolder(LocalMediaSource localMediaSource, string ffprobePath);
Task<Either<BaseError, Unit>> ScanFolder(LibraryPath libraryPath, string ffprobePath);
}
}

15
ErsatzTV.Core/Interfaces/Plex/IPlexMovieLibraryScanner.cs

@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Plex;
using LanguageExt;
namespace ErsatzTV.Core.Interfaces.Plex
{
public interface IPlexMovieLibraryScanner
{
Task<Either<BaseError, Unit>> ScanLibrary(
PlexConnection connection,
PlexServerAuthToken token,
PlexLibrary plexMediaSourceLibrary);
}
}

9
ErsatzTV.Core/Interfaces/Plex/IPlexServerApiClient.cs

@ -8,8 +8,13 @@ namespace ErsatzTV.Core.Interfaces.Plex @@ -8,8 +8,13 @@ namespace ErsatzTV.Core.Interfaces.Plex
{
public interface IPlexServerApiClient
{
Task<Either<BaseError, List<PlexMediaSourceLibrary>>> GetLibraries(
PlexMediaSourceConnection connection,
Task<Either<BaseError, List<PlexLibrary>>> GetLibraries(
PlexConnection connection,
PlexServerAuthToken token);
Task<Either<BaseError, List<PlexMovie>>> GetLibraryContents(
PlexLibrary library,
PlexConnection connection,
PlexServerAuthToken token);
}
}

12
ErsatzTV.Core/Interfaces/Repositories/ILibraryRepository.cs

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using LanguageExt;
namespace ErsatzTV.Core.Interfaces.Repositories
{
public interface ILibraryRepository
{
Task<Option<Library>> Get(int libraryId);
Task<Unit> UpdateLastScan(Library library);
}
}

5
ErsatzTV.Core/Interfaces/Repositories/IMediaSourceRepository.cs

@ -11,11 +11,16 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -11,11 +11,16 @@ namespace ErsatzTV.Core.Interfaces.Repositories
Task<PlexMediaSource> Add(PlexMediaSource plexMediaSource);
Task<List<MediaSource>> GetAll();
Task<List<PlexMediaSource>> GetAllPlex();
Task<List<PlexLibrary>> GetPlexLibraries(int plexMediaSourceId);
Task<Option<PlexLibrary>> GetPlexLibrary(int plexLibraryId);
Task<Option<MediaSource>> Get(int id);
Task<Option<PlexMediaSource>> GetPlex(int id);
Task<int> CountMediaItems(int id);
Task Update(LocalMediaSource localMediaSource);
Task Update(PlexMediaSource plexMediaSource);
Task Update(PlexLibrary plexMediaSourceLibrary);
Task Delete(int id);
Task DisablePlexLibrarySync(IEnumerable<int> libraryIds);
Task EnablePlexLibrarySync(IEnumerable<int> libraryIds);
}
}

9
ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs

@ -7,10 +7,11 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -7,10 +7,11 @@ namespace ErsatzTV.Core.Interfaces.Repositories
{
public interface IMovieRepository
{
Task<Option<MovieMediaItem>> GetMovie(int movieId);
Task<Either<BaseError, MovieMediaItem>> GetOrAdd(int mediaSourceId, string path);
Task<bool> Update(MovieMediaItem movie);
Task<Option<Movie>> GetMovie(int movieId);
Task<Either<BaseError, Movie>> GetOrAdd(LibraryPath libraryPath, string path);
Task<Either<BaseError, PlexMovie>> GetOrAdd(PlexLibrary library, PlexMovie item);
Task<bool> Update(Movie movie);
Task<int> GetMovieCount();
Task<List<MovieMediaItem>> GetPagedMovies(int pageNumber, int pageSize);
Task<List<Movie>> GetPagedMovies(int pageNumber, int pageSize);
}
}

2
ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs

@ -38,7 +38,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -38,7 +38,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
Task<Either<BaseError, TelevisionEpisodeMediaItem>> GetOrAddEpisode(
TelevisionSeason season,
int mediaSourceId,
LibraryPath libraryPath,
string path);
Task<Unit> DeleteMissingSources(int localMediaSourceId, List<string> allFolders);

6
ErsatzTV.Core/Iptv/ChannelGuide.cs

@ -60,21 +60,21 @@ namespace ErsatzTV.Core.Iptv @@ -60,21 +60,21 @@ namespace ErsatzTV.Core.Iptv
string title = playoutItem.MediaItem switch
{
MovieMediaItem m => m.Metadata?.Title ?? m.Path,
Movie m => m.Metadata?.Title ?? m.Path,
TelevisionEpisodeMediaItem e => e.Metadata?.Title ?? e.Path,
_ => "[unknown]"
};
string description = playoutItem.MediaItem switch
{
MovieMediaItem m => m.Metadata?.Plot,
Movie m => m.Metadata?.Plot,
TelevisionEpisodeMediaItem e => e.Metadata?.Plot,
_ => string.Empty
};
string contentRating = playoutItem.MediaItem switch
{
MovieMediaItem m => m.Metadata?.ContentRating,
Movie m => m.Metadata?.ContentRating,
_ => string.Empty
};

20
ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs

@ -24,10 +24,11 @@ namespace ErsatzTV.Core.Metadata @@ -24,10 +24,11 @@ namespace ErsatzTV.Core.Metadata
if (fileName != null)
{
if (!(mediaItem.Source is LocalMediaSource))
{
return metadata;
}
// TODO: ensure local?
// if (!(mediaItem.LibraryPath is LocalMediaSource))
// {
// return metadata;
// }
return GetEpisodeMetadata(fileName, metadata);
}
@ -35,17 +36,18 @@ namespace ErsatzTV.Core.Metadata @@ -35,17 +36,18 @@ namespace ErsatzTV.Core.Metadata
return metadata;
}
public static MovieMetadata GetFallbackMetadata(MovieMediaItem mediaItem)
public static MovieMetadata GetFallbackMetadata(Movie mediaItem)
{
string fileName = Path.GetFileName(mediaItem.Path);
var metadata = new MovieMetadata { Source = MetadataSource.Fallback, Title = fileName ?? mediaItem.Path };
if (fileName != null)
{
if (!(mediaItem.Source is LocalMediaSource))
{
return metadata;
}
// TODO: ensure local?
// if (!(mediaItem.Source is LocalMediaSource))
// {
// return metadata;
// }
return GetMovieMetadata(fileName, metadata);
}

4
ErsatzTV.Core/Metadata/LocalFileSystem.cs

@ -13,8 +13,8 @@ namespace ErsatzTV.Core.Metadata @@ -13,8 +13,8 @@ namespace ErsatzTV.Core.Metadata
public DateTime GetLastWriteTime(string path) =>
Try(File.GetLastWriteTimeUtc(path)).IfFail(() => DateTime.MinValue);
public bool IsMediaSourceAccessible(LocalMediaSource localMediaSource) =>
Directory.Exists(localMediaSource.Folder);
public bool IsLibraryPathAccessible(LibraryPath libraryPath) =>
Directory.Exists(libraryPath.Path);
public IEnumerable<string> ListSubdirectories(string folder) =>
Try(Directory.EnumerateDirectories(folder)).IfFail(new List<string>());

30
ErsatzTV.Core/Metadata/LocalMetadataProvider.cs

@ -57,7 +57,7 @@ namespace ErsatzTV.Core.Metadata @@ -57,7 +57,7 @@ namespace ErsatzTV.Core.Metadata
{
TelevisionEpisodeMediaItem e => LoadMetadata(e, path)
.Bind(maybeMetadata => maybeMetadata.IfSomeAsync(metadata => ApplyMetadataUpdate(e, metadata))),
MovieMediaItem m => LoadMetadata(m, path)
Movie m => LoadMetadata(m, path)
.Bind(maybeMetadata => maybeMetadata.IfSomeAsync(metadata => ApplyMetadataUpdate(m, metadata))),
_ => Task.FromResult(Unit.Default)
};
@ -71,7 +71,7 @@ namespace ErsatzTV.Core.Metadata @@ -71,7 +71,7 @@ namespace ErsatzTV.Core.Metadata
{
TelevisionEpisodeMediaItem e => ApplyMetadataUpdate(e, FallbackMetadataProvider.GetFallbackMetadata(e))
.ToUnit(),
MovieMediaItem m => ApplyMetadataUpdate(m, FallbackMetadataProvider.GetFallbackMetadata(m)).ToUnit(),
Movie m => ApplyMetadataUpdate(m, FallbackMetadataProvider.GetFallbackMetadata(m)).ToUnit(),
_ => Task.FromResult(Unit.Default)
};
@ -94,7 +94,7 @@ namespace ErsatzTV.Core.Metadata @@ -94,7 +94,7 @@ namespace ErsatzTV.Core.Metadata
await _televisionRepository.Update(mediaItem);
}
private async Task ApplyMetadataUpdate(MovieMediaItem mediaItem, MovieMetadata metadata)
private async Task ApplyMetadataUpdate(Movie mediaItem, MovieMetadata metadata)
{
mediaItem.Metadata ??= new MovieMetadata();
mediaItem.Metadata.Source = metadata.Source;
@ -124,7 +124,7 @@ namespace ErsatzTV.Core.Metadata @@ -124,7 +124,7 @@ namespace ErsatzTV.Core.Metadata
await _televisionRepository.Update(televisionShow);
}
private async Task<Option<MovieMetadata>> LoadMetadata(MovieMediaItem mediaItem, string nfoFileName)
private async Task<Option<MovieMetadata>> LoadMetadata(Movie mediaItem, string nfoFileName)
{
if (nfoFileName == null || !File.Exists(nfoFileName))
{
@ -132,11 +132,11 @@ namespace ErsatzTV.Core.Metadata @@ -132,11 +132,11 @@ namespace ErsatzTV.Core.Metadata
return None;
}
if (!(mediaItem.Source is LocalMediaSource))
{
_logger.LogDebug("Media source {Name} is not a local media source", mediaItem.Source.Name);
return None;
}
// if (!(mediaItem.Source is LocalMediaSource))
// {
// _logger.LogDebug("Media source {Name} is not a local media source", mediaItem.Source.Name);
// return None;
// }
return await LoadMovieMetadata(mediaItem, nfoFileName);
}
@ -151,11 +151,11 @@ namespace ErsatzTV.Core.Metadata @@ -151,11 +151,11 @@ namespace ErsatzTV.Core.Metadata
return None;
}
if (!(mediaItem.Source is LocalMediaSource))
{
_logger.LogDebug("Media source {Name} is not a local media source", mediaItem.Source.Name);
return None;
}
// if (!(mediaItem.Source is LocalMediaSource))
// {
// _logger.LogDebug("Media source {Name} is not a local media source", mediaItem.Source.Name);
// return None;
// }
return await LoadEpisodeMetadata(mediaItem, nfoFileName);
}
@ -225,7 +225,7 @@ namespace ErsatzTV.Core.Metadata @@ -225,7 +225,7 @@ namespace ErsatzTV.Core.Metadata
}
}
private async Task<Option<MovieMetadata>> LoadMovieMetadata(MovieMediaItem mediaItem, string nfoFileName)
private async Task<Option<MovieMetadata>> LoadMovieMetadata(Movie mediaItem, string nfoFileName)
{
try
{

20
ErsatzTV.Core/Metadata/MovieFolderScanner.cs

@ -37,15 +37,15 @@ namespace ErsatzTV.Core.Metadata @@ -37,15 +37,15 @@ namespace ErsatzTV.Core.Metadata
_logger = logger;
}
public async Task<Either<BaseError, Unit>> ScanFolder(LocalMediaSource localMediaSource, string ffprobePath)
public async Task<Either<BaseError, Unit>> ScanFolder(LibraryPath libraryPath, string ffprobePath)
{
if (!_localFileSystem.IsMediaSourceAccessible(localMediaSource))
if (!_localFileSystem.IsLibraryPathAccessible(libraryPath))
{
return new MediaSourceInaccessible();
}
var folderQueue = new Queue<string>();
foreach (string folder in _localFileSystem.ListSubdirectories(localMediaSource.Folder).OrderBy(identity))
foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path).OrderBy(identity))
{
folderQueue.Enqueue(folder);
}
@ -73,10 +73,10 @@ namespace ErsatzTV.Core.Metadata @@ -73,10 +73,10 @@ namespace ErsatzTV.Core.Metadata
foreach (string file in allFiles.OrderBy(identity))
{
// TODO: optimize dbcontext use here, do we need tracking? can we make partial updates with dapper?
// TODO: figure out how to rebuild playlists
Either<BaseError, MovieMediaItem> x = await _movieRepository.GetOrAdd(localMediaSource.Id, file);
Either<BaseError, MovieMediaItem> maybeMovie = await x.AsTask()
Either<BaseError, Movie> maybeMovie = await _movieRepository
.GetOrAdd(libraryPath, file)
.BindT(movie => UpdateStatistics(movie, ffprobePath).MapT(_ => movie))
.BindT(UpdateMetadata)
.BindT(UpdatePoster);
@ -89,7 +89,7 @@ namespace ErsatzTV.Core.Metadata @@ -89,7 +89,7 @@ namespace ErsatzTV.Core.Metadata
return Unit.Default;
}
private async Task<Either<BaseError, MovieMediaItem>> UpdateMetadata(MovieMediaItem movie)
private async Task<Either<BaseError, Movie>> UpdateMetadata(Movie movie)
{
try
{
@ -121,7 +121,7 @@ namespace ErsatzTV.Core.Metadata @@ -121,7 +121,7 @@ namespace ErsatzTV.Core.Metadata
}
}
private async Task<Either<BaseError, MovieMediaItem>> UpdatePoster(MovieMediaItem movie)
private async Task<Either<BaseError, Movie>> UpdatePoster(Movie movie)
{
try
{
@ -145,7 +145,7 @@ namespace ErsatzTV.Core.Metadata @@ -145,7 +145,7 @@ namespace ErsatzTV.Core.Metadata
}
}
private Option<string> LocateNfoFile(MovieMediaItem movie)
private Option<string> LocateNfoFile(Movie movie)
{
string movieAsNfo = Path.ChangeExtension(movie.Path, "nfo");
string movieNfo = Path.Combine(Path.GetDirectoryName(movie.Path) ?? string.Empty, "movie.nfo");
@ -154,7 +154,7 @@ namespace ErsatzTV.Core.Metadata @@ -154,7 +154,7 @@ namespace ErsatzTV.Core.Metadata
.HeadOrNone();
}
private Option<string> LocatePoster(MovieMediaItem movie)
private Option<string> LocatePoster(Movie movie)
{
string folder = Path.GetDirectoryName(movie.Path) ?? string.Empty;
IEnumerable<string> possibleMoviePosters = ImageFileExtensions.Collect(

35
ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs

@ -3,6 +3,7 @@ using System.IO; @@ -3,6 +3,7 @@ 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;
@ -37,17 +38,14 @@ namespace ErsatzTV.Core.Metadata @@ -37,17 +38,14 @@ namespace ErsatzTV.Core.Metadata
_logger = logger;
}
public async Task<Unit> ScanFolder(LocalMediaSource localMediaSource, string ffprobePath)
public async Task<Either<BaseError, Unit>> ScanFolder(LibraryPath libraryPath, string ffprobePath)
{
if (!_localFileSystem.IsMediaSourceAccessible(localMediaSource))
if (!_localFileSystem.IsLibraryPathAccessible(libraryPath))
{
_logger.LogWarning(
"Media source is not accessible or missing; skipping scan of {Folder}",
localMediaSource.Folder);
return Unit.Default;
return new MediaSourceInaccessible();
}
var allShowFolders = _localFileSystem.ListSubdirectories(localMediaSource.Folder)
var allShowFolders = _localFileSystem.ListSubdirectories(libraryPath.Path)
.Filter(ShouldIncludeFolder)
.OrderBy(identity)
.ToList();
@ -56,27 +54,28 @@ namespace ErsatzTV.Core.Metadata @@ -56,27 +54,28 @@ namespace ErsatzTV.Core.Metadata
{
// TODO: check all sources for latest metadata?
Either<BaseError, TelevisionShow> maybeShow =
await FindOrCreateShow(localMediaSource.Id, showFolder)
await FindOrCreateShow(libraryPath.Id, showFolder)
.BindT(show => UpdateMetadataForShow(show, showFolder))
.BindT(show => UpdatePosterForShow(show, showFolder));
await maybeShow.Match(
show => ScanSeasons(localMediaSource, ffprobePath, show, showFolder),
show => ScanSeasons(libraryPath, ffprobePath, show, showFolder),
_ => Task.FromResult(Unit.Default));
}
await _televisionRepository.DeleteMissingSources(localMediaSource.Id, allShowFolders);
// TODO: remove this?
// await _televisionRepository.DeleteMissingSources(libraryPath.Id, allShowFolders);
await _televisionRepository.DeleteEmptyShows();
return Unit.Default;
}
private async Task<Either<BaseError, TelevisionShow>> FindOrCreateShow(
int localMediaSourceId,
int libraryPathId,
string showFolder)
{
Option<TelevisionShow> maybeShowByPath =
await _televisionRepository.GetShowByPath(localMediaSourceId, showFolder);
await _televisionRepository.GetShowByPath(libraryPathId, showFolder);
return await maybeShowByPath.Match(
show => Right<BaseError, TelevisionShow>(show).AsTask(),
async () =>
@ -89,19 +88,19 @@ namespace ErsatzTV.Core.Metadata @@ -89,19 +88,19 @@ namespace ErsatzTV.Core.Metadata
show.Sources.Add(
new LocalTelevisionShowSource
{
MediaSourceId = localMediaSourceId,
MediaSourceId = libraryPathId,
Path = showFolder,
TelevisionShow = show
});
await _televisionRepository.Update(show);
return Right<BaseError, TelevisionShow>(show);
},
async () => await _televisionRepository.AddShow(localMediaSourceId, showFolder, metadata));
async () => await _televisionRepository.AddShow(libraryPathId, showFolder, metadata));
});
}
private async Task<Unit> ScanSeasons(
LocalMediaSource localMediaSource,
LibraryPath libraryPath,
string ffprobePath,
TelevisionShow show,
string showPath)
@ -118,7 +117,7 @@ namespace ErsatzTV.Core.Metadata @@ -118,7 +117,7 @@ namespace ErsatzTV.Core.Metadata
.BindT(UpdatePoster);
await maybeSeason.Match(
season => ScanEpisodes(localMediaSource, ffprobePath, season),
season => ScanEpisodes(libraryPath, ffprobePath, season),
_ => Task.FromResult(Unit.Default));
});
}
@ -127,7 +126,7 @@ namespace ErsatzTV.Core.Metadata @@ -127,7 +126,7 @@ namespace ErsatzTV.Core.Metadata
}
private async Task<Unit> ScanEpisodes(
LocalMediaSource localMediaSource,
LibraryPath libraryPath,
string ffprobePath,
TelevisionSeason season)
{
@ -136,7 +135,7 @@ namespace ErsatzTV.Core.Metadata @@ -136,7 +135,7 @@ namespace ErsatzTV.Core.Metadata
{
// TODO: figure out how to rebuild playlists
Either<BaseError, TelevisionEpisodeMediaItem> maybeEpisode = await _televisionRepository
.GetOrAddEpisode(season, localMediaSource.Id, file)
.GetOrAddEpisode(season, libraryPath, file)
.BindT(episode => UpdateStatistics(episode, ffprobePath).MapT(_ => episode))
.BindT(UpdateMetadata)
.BindT(UpdateThumbnail);

20
ErsatzTV.Core/Plex/PlexMediaEntry.cs

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
using System.Collections.Generic;
namespace ErsatzTV.Core.Plex
{
public class PlexMediaEntry
{
public int Id { get; set; }
public int Duration { get; set; }
public int Bitrate { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public double AspectRatio { get; set; }
public int AudioChannels { get; set; }
public string AudioCodec { get; set; }
public string VideoCodec { get; set; }
public string Container { get; set; }
public string VideoFrameRate { get; set; }
public List<PlexPartEntry> Part { get; set; }
}
}

19
ErsatzTV.Core/Plex/PlexMetadataEntry.cs

@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
using System.Collections.Generic;
namespace ErsatzTV.Core.Plex
{
public class PlexMetadataEntry
{
public string Key { get; set; }
public string Title { get; set; }
public string Summary { get; set; }
public int Year { get; set; }
public string Tagline { get; set; }
public string Thumb { get; set; }
public string Art { get; set; }
public string OriginallyAvailableAt { get; set; }
public int AddedAt { get; set; }
public int UpdatedAt { get; set; }
public List<PlexMediaEntry> Media { get; set; }
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save