Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cc61f3212 | ||
|
|
4cf44616a8 | ||
|
|
33aaadae68 | ||
|
|
fe3f8e391e | ||
|
|
1a68dd040a | ||
|
|
67761c1a14 | ||
|
|
1802f9d797 | ||
|
|
69354c9296 | ||
|
|
0021e21b50 | ||
|
|
cdf7765059 | ||
|
|
71658c448f | ||
|
|
3ecdd741a5 | ||
|
|
0daeb844b9 | ||
|
|
22da19845b |
+40
-3
@@ -5,9 +5,44 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.3-alpha] - 2021-10-12
|
||||
## [0.1.5-alpha] - 2021-10-18
|
||||
### Fixed
|
||||
- Fix startup bug for nvidia docker installations
|
||||
- Fix double scheduling; this could happen if the app was shutdown during a playout build
|
||||
- Fix updating Jellyfin and Emby TV seasons
|
||||
- Fix updating Jellyfin and Emby artwork
|
||||
- Fix Plex, Jellyfin, Emby worker crash attempting to sync library that no longer exists
|
||||
- Fix bug with `Duration` mode scheduling when media items are too long to fit in the requested duration
|
||||
- Fix bug with `Duration` mode scheduling with `Filler` tail mode where other duration items in the schedule would be skipped
|
||||
|
||||
### Added
|
||||
- Include music video thumbnails in channel guide (xmltv)
|
||||
|
||||
### Changed
|
||||
- Automatically find working Plex address on startup
|
||||
- Change default log level from `Debug` to `Information`
|
||||
- The `Debug` log level can be enabled in the `appsettings.json` file for non-docker installs
|
||||
- The `Debug` log level can be enabled by setting the environment variable `Serilog:MinimumLevel=Debug` for docker installs
|
||||
|
||||
## [0.1.4-alpha] - 2021-10-14
|
||||
### Fixed
|
||||
- Fix error message/offline stream continuity with channels that use HLS Segmenter
|
||||
- Fix removing items from search index when folders are removed from local libraries
|
||||
|
||||
### Added
|
||||
- Add `Other Video` local libraries
|
||||
- Other video items require no metadata or particular folder layout, and will have tags added for each containing folder
|
||||
- For Example, a video at `commercials/sd/1990/whatever.mkv` will have the tags `commercials`, `sd` and `1990`, and the title `whatever`
|
||||
- Add filler `Tail Mode` option to `Duration` playout mode (in addition to existing `Offline` option)
|
||||
- Filler collection will always be randomized (to fill as much time as possible)
|
||||
- Filler will be hidden from channel guide, but visible in playout details in ErsatzTV
|
||||
- Unfilled time will show offline image
|
||||
- Add `Guide Mode` option to all schedule items
|
||||
- `Normal` guide mode will show all scheduled items in the channel guide (xmltv)
|
||||
- `Filler` guide mode will hide all scheduled items from the channel guide, and extend the end time for the previous item in the guide
|
||||
|
||||
## [0.1.3-alpha] - 2021-10-13
|
||||
### Fixed
|
||||
- Fix startup bug for some docker installations
|
||||
|
||||
## [0.1.2-alpha] - 2021-10-12
|
||||
### Added
|
||||
@@ -695,7 +730,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Initial release to facilitate testing outside of Docker.
|
||||
|
||||
|
||||
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.3-alpha...HEAD
|
||||
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.5-alpha...HEAD
|
||||
[0.1.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.4-alpha...v0.1.5-alpha
|
||||
[0.1.4-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.3-alpha...v0.1.4-alpha
|
||||
[0.1.3-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.2-alpha...v0.1.3-alpha
|
||||
[0.1.2-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.1-alpha...v0.1.2-alpha
|
||||
[0.1.1-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.0-alpha...v0.1.1-alpha
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -8,6 +9,7 @@ using ErsatzTV.Application.MediaSources.Commands;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Locking;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
@@ -22,15 +24,18 @@ namespace ErsatzTV.Application.Libraries.Commands
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
|
||||
private readonly IEntityLocker _entityLocker;
|
||||
private readonly ISearchIndex _searchIndex;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public UpdateLocalLibraryHandler(
|
||||
ChannelWriter<IBackgroundServiceRequest> workerChannel,
|
||||
IEntityLocker entityLocker,
|
||||
ISearchIndex searchIndex,
|
||||
IDbContextFactory<TvContext> dbContextFactory)
|
||||
{
|
||||
_workerChannel = workerChannel;
|
||||
_entityLocker = entityLocker;
|
||||
_searchIndex = searchIndex;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
@@ -56,10 +61,21 @@ namespace ErsatzTV.Application.Libraries.Commands
|
||||
.Filter(ep => incoming.Paths.All(p => NormalizePath(p.Path) != NormalizePath(ep.Path)))
|
||||
.ToList();
|
||||
|
||||
var toRemoveIds = toRemove.Map(lp => lp.Id).ToList();
|
||||
|
||||
List<int> itemsToRemove = await dbContext.MediaItems
|
||||
.Filter(mi => toRemoveIds.Contains(mi.LibraryPathId))
|
||||
.Map(mi => mi.Id)
|
||||
.ToListAsync();
|
||||
|
||||
existing.Paths.RemoveAll(toRemove.Contains);
|
||||
existing.Paths.AddRange(toAdd);
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
await _searchIndex.RemoveItems(itemsToRemove);
|
||||
_searchIndex.Commit();
|
||||
}
|
||||
|
||||
if (toAdd.Count > 0 || toRemove.Count > 0 && _entityLocker.LockLibrary(existing.Id))
|
||||
{
|
||||
|
||||
@@ -9,7 +9,8 @@ namespace ErsatzTV.Application.MediaCards
|
||||
List<TelevisionSeasonCardViewModel> SeasonCards,
|
||||
List<TelevisionEpisodeCardViewModel> EpisodeCards,
|
||||
List<ArtistCardViewModel> ArtistCards,
|
||||
List<MusicVideoCardViewModel> MusicVideoCards)
|
||||
List<MusicVideoCardViewModel> MusicVideoCards,
|
||||
List<OtherVideoCardViewModel> OtherVideoCards)
|
||||
{
|
||||
public bool UseCustomPlaybackOrder { get; set; }
|
||||
}
|
||||
|
||||
@@ -103,6 +103,13 @@ namespace ErsatzTV.Application.MediaCards
|
||||
musicVideoMetadata.Album,
|
||||
GetThumbnail(musicVideoMetadata, None, None));
|
||||
|
||||
internal static OtherVideoCardViewModel ProjectToViewModel(OtherVideoMetadata otherVideoMetadata) =>
|
||||
new(
|
||||
otherVideoMetadata.OtherVideoId,
|
||||
otherVideoMetadata.Title,
|
||||
otherVideoMetadata.OriginalTitle,
|
||||
otherVideoMetadata.SortTitle);
|
||||
|
||||
internal static ArtistCardViewModel ProjectToViewModel(ArtistMetadata artistMetadata) =>
|
||||
new(
|
||||
artistMetadata.ArtistId,
|
||||
@@ -133,6 +140,8 @@ namespace ErsatzTV.Application.MediaCards
|
||||
.ToList(),
|
||||
collection.MediaItems.OfType<Artist>().Map(a => ProjectToViewModel(a.ArtistMetadata.Head())).ToList(),
|
||||
collection.MediaItems.OfType<MusicVideo>().Map(mv => ProjectToViewModel(mv.MusicVideoMetadata.Head()))
|
||||
.ToList(),
|
||||
collection.MediaItems.OfType<OtherVideo>().Map(mv => ProjectToViewModel(mv.OtherVideoMetadata.Head()))
|
||||
.ToList()) { UseCustomPlaybackOrder = collection.UseCustomPlaybackOrder };
|
||||
|
||||
internal static ActorCardViewModel ProjectToViewModel(
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core.Search;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCards
|
||||
{
|
||||
public record OtherVideoCardResultsViewModel(
|
||||
int Count,
|
||||
List<OtherVideoCardViewModel> Cards,
|
||||
Option<SearchPageMap> PageMap);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace ErsatzTV.Application.MediaCards
|
||||
{
|
||||
public record OtherVideoCardViewModel
|
||||
(
|
||||
int OtherVideoId,
|
||||
string Title,
|
||||
string Subtitle,
|
||||
string SortTitle) : MediaCardViewModel(
|
||||
OtherVideoId,
|
||||
Title,
|
||||
Subtitle,
|
||||
SortTitle,
|
||||
null)
|
||||
{
|
||||
public int CustomIndex { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -80,6 +80,9 @@ namespace ErsatzTV.Application.MediaCards.Queries
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as Episode).Season)
|
||||
.ThenInclude(s => s.SeasonMetadata)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as OtherVideo).OtherVideoMetadata)
|
||||
.ThenInclude(ovm => ovm.Artwork)
|
||||
.SelectOneAsync(c => c.Id, c => c.Id == request.Id)
|
||||
.Map(c => c.ToEither(BaseError.New("Unable to load collection")))
|
||||
.MapT(c => ProjectToViewModel(c, maybeJellyfin, maybeEmby));
|
||||
|
||||
@@ -12,5 +12,6 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
List<int> SeasonIds,
|
||||
List<int> EpisodeIds,
|
||||
List<int> ArtistIds,
|
||||
List<int> MusicVideoIds) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
List<int> MusicVideoIds,
|
||||
List<int> OtherVideoIds) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
.Append(request.EpisodeIds)
|
||||
.Append(request.ArtistIds)
|
||||
.Append(request.MusicVideoIds)
|
||||
.Append(request.OtherVideoIds)
|
||||
.ToList();
|
||||
|
||||
var toAddIds = allItems.Where(item => collection.MediaItems.All(mi => mi.Id != item)).ToList();
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
{
|
||||
public record AddOtherVideoToCollection
|
||||
(int CollectionId, int OtherVideoId) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Application.Playouts.Commands;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
{
|
||||
public class AddOtherVideoToCollectionHandler :
|
||||
MediatR.IRequestHandler<AddOtherVideoToCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
|
||||
public AddOtherVideoToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_channel = channel;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
AddOtherVideoToCollection request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(parameters => ApplyAddOtherVideoRequest(dbContext, parameters));
|
||||
}
|
||||
|
||||
private async Task<Unit> ApplyAddOtherVideoRequest(TvContext dbContext, Parameters parameters)
|
||||
{
|
||||
parameters.Collection.MediaItems.Add(parameters.OtherVideo);
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
// rebuild all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository
|
||||
.PlayoutIdsUsingCollection(parameters.Collection.Id))
|
||||
{
|
||||
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
|
||||
}
|
||||
}
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private static async Task<Validation<BaseError, Parameters>> Validate(
|
||||
TvContext dbContext,
|
||||
AddOtherVideoToCollection request) =>
|
||||
(await CollectionMustExist(dbContext, request), await ValidateOtherVideo(dbContext, request))
|
||||
.Apply((collection, episode) => new Parameters(collection, episode));
|
||||
|
||||
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
|
||||
TvContext dbContext,
|
||||
AddOtherVideoToCollection request) =>
|
||||
dbContext.Collections
|
||||
.Include(c => c.MediaItems)
|
||||
.SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId)
|
||||
.Map(o => o.ToValidation<BaseError>("Collection does not exist."));
|
||||
|
||||
private static Task<Validation<BaseError, OtherVideo>> ValidateOtherVideo(
|
||||
TvContext dbContext,
|
||||
AddOtherVideoToCollection request) =>
|
||||
dbContext.OtherVideos
|
||||
.SelectOneAsync(m => m.Id, e => e.Id == request.OtherVideoId)
|
||||
.Map(o => o.ToValidation<BaseError>("OtherVideo does not exist"));
|
||||
|
||||
private record Parameters(Collection Collection, OtherVideo OtherVideo);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
||||
private readonly IMediator _mediator;
|
||||
private readonly IMovieFolderScanner _movieFolderScanner;
|
||||
private readonly IMusicVideoFolderScanner _musicVideoFolderScanner;
|
||||
private readonly IOtherVideoFolderScanner _otherVideoFolderScanner;
|
||||
private readonly ITelevisionFolderScanner _televisionFolderScanner;
|
||||
|
||||
public ScanLocalLibraryHandler(
|
||||
@@ -34,6 +35,7 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
||||
IMovieFolderScanner movieFolderScanner,
|
||||
ITelevisionFolderScanner televisionFolderScanner,
|
||||
IMusicVideoFolderScanner musicVideoFolderScanner,
|
||||
IOtherVideoFolderScanner otherVideoFolderScanner,
|
||||
IEntityLocker entityLocker,
|
||||
IMediator mediator,
|
||||
ILogger<ScanLocalLibraryHandler> logger)
|
||||
@@ -43,6 +45,7 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
||||
_movieFolderScanner = movieFolderScanner;
|
||||
_televisionFolderScanner = televisionFolderScanner;
|
||||
_musicVideoFolderScanner = musicVideoFolderScanner;
|
||||
_otherVideoFolderScanner = otherVideoFolderScanner;
|
||||
_entityLocker = entityLocker;
|
||||
_mediator = mediator;
|
||||
_logger = logger;
|
||||
@@ -107,6 +110,13 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
||||
progressMin,
|
||||
progressMax);
|
||||
break;
|
||||
case LibraryMediaKind.OtherVideos:
|
||||
await _otherVideoFolderScanner.ScanFolder(
|
||||
libraryPath,
|
||||
ffprobePath,
|
||||
progressMin,
|
||||
progressMax);
|
||||
break;
|
||||
}
|
||||
|
||||
libraryPath.LastScan = DateTime.UtcNow;
|
||||
|
||||
@@ -37,6 +37,9 @@ namespace ErsatzTV.Application.Playouts
|
||||
return mv.MusicVideoMetadata.HeadOrNone()
|
||||
.Map(mvm => $"{artistName}{mvm.Title}")
|
||||
.IfNone("[unknown music video]");
|
||||
case OtherVideo ov:
|
||||
return ov.OtherVideoMetadata.HeadOrNone().Map(ovm => ovm.Title ?? string.Empty)
|
||||
.IfNone("[unknown video]");
|
||||
default:
|
||||
return string.Empty;
|
||||
}
|
||||
@@ -49,6 +52,7 @@ namespace ErsatzTV.Application.Playouts
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
MusicVideo mv => mv.MediaVersions.Head(),
|
||||
OtherVideo ov => ov.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Playouts.Queries
|
||||
{
|
||||
public record GetFuturePlayoutItemsById(int PlayoutId, int PageNum, int PageSize) : IRequest<PagedPlayoutItemsViewModel>;
|
||||
}
|
||||
+12
-4
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -11,21 +12,23 @@ using static ErsatzTV.Application.Playouts.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Playouts.Queries
|
||||
{
|
||||
public class GetPlayoutItemsByIdHandler : IRequestHandler<GetPlayoutItemsById, PagedPlayoutItemsViewModel>
|
||||
public class GetFuturePlayoutItemsByIdHandler : IRequestHandler<GetFuturePlayoutItemsById, PagedPlayoutItemsViewModel>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public GetPlayoutItemsByIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
public GetFuturePlayoutItemsByIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
_dbContextFactory = dbContextFactory;
|
||||
|
||||
public async Task<PagedPlayoutItemsViewModel> Handle(
|
||||
GetPlayoutItemsById request,
|
||||
GetFuturePlayoutItemsById request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
|
||||
int totalCount = await dbContext.PlayoutItems
|
||||
.CountAsync(i => i.PlayoutId == request.PlayoutId, cancellationToken);
|
||||
|
||||
DateTime now = DateTimeOffset.Now.UtcDateTime;
|
||||
|
||||
List<PlayoutItemViewModel> page = await dbContext.PlayoutItems
|
||||
.Include(i => i.MediaItem)
|
||||
@@ -49,7 +52,12 @@ namespace ErsatzTV.Application.Playouts.Queries
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as Episode).Season.Show)
|
||||
.ThenInclude(s => s.ShowMetadata)
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as OtherVideo).OtherVideoMetadata)
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as OtherVideo).MediaVersions)
|
||||
.Filter(i => i.PlayoutId == request.PlayoutId)
|
||||
.Filter(i => i.Finish >= now)
|
||||
.OrderBy(i => i.Start)
|
||||
.Skip(request.PageNum * request.PageSize)
|
||||
.Take(request.PageSize)
|
||||
@@ -1,7 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Playouts.Queries
|
||||
{
|
||||
public record GetPlayoutItemsById(int PlayoutId, int PageNum, int PageSize) : IRequest<PagedPlayoutItemsViewModel>;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ 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;
|
||||
@@ -23,16 +24,22 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
private readonly ILogger<SynchronizePlexMediaSourcesHandler> _logger;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly IPlexTvApiClient _plexTvApiClient;
|
||||
private readonly IPlexServerApiClient _plexServerApiClient;
|
||||
private readonly IPlexSecretStore _plexSecretStore;
|
||||
|
||||
public SynchronizePlexMediaSourcesHandler(
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
IPlexTvApiClient plexTvApiClient,
|
||||
IPlexServerApiClient plexServerApiClient,
|
||||
IPlexSecretStore plexSecretStore,
|
||||
ChannelWriter<IPlexBackgroundServiceRequest> channel,
|
||||
IEntityLocker entityLocker,
|
||||
ILogger<SynchronizePlexMediaSourcesHandler> logger)
|
||||
{
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_plexTvApiClient = plexTvApiClient;
|
||||
_plexServerApiClient = plexServerApiClient;
|
||||
_plexSecretStore = plexSecretStore;
|
||||
_channel = channel;
|
||||
_entityLocker = entityLocker;
|
||||
_logger = logger;
|
||||
@@ -69,32 +76,76 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
return allExisting;
|
||||
}
|
||||
|
||||
private Task SynchronizeServer(List<PlexMediaSource> allExisting, PlexMediaSource server)
|
||||
private async Task SynchronizeServer(List<PlexMediaSource> allExisting, PlexMediaSource server)
|
||||
{
|
||||
Option<PlexMediaSource> maybeExisting =
|
||||
allExisting.Find(s => s.ClientIdentifier == server.ClientIdentifier);
|
||||
return maybeExisting.Match(
|
||||
existing =>
|
||||
{
|
||||
existing.Platform = server.Platform;
|
||||
existing.PlatformVersion = server.PlatformVersion;
|
||||
existing.ProductVersion = server.ProductVersion;
|
||||
existing.ServerName = server.ServerName;
|
||||
var toAdd = server.Connections
|
||||
.Filter(connection => existing.Connections.All(c => c.Uri != connection.Uri)).ToList();
|
||||
var toRemove = existing.Connections
|
||||
.Filter(connection => server.Connections.All(c => c.Uri != connection.Uri)).ToList();
|
||||
return _mediaSourceRepository.Update(existing, server.Connections, toAdd, toRemove);
|
||||
},
|
||||
async () =>
|
||||
{
|
||||
if (server.Connections.Any())
|
||||
{
|
||||
server.Connections.Head().IsActive = true;
|
||||
}
|
||||
|
||||
await _mediaSourceRepository.Add(server);
|
||||
});
|
||||
foreach (PlexMediaSource existing in maybeExisting)
|
||||
{
|
||||
existing.Platform = server.Platform;
|
||||
existing.PlatformVersion = server.PlatformVersion;
|
||||
existing.ProductVersion = server.ProductVersion;
|
||||
existing.ServerName = server.ServerName;
|
||||
var toAdd = server.Connections
|
||||
.Filter(connection => existing.Connections.All(c => c.Uri != connection.Uri)).ToList();
|
||||
var toRemove = existing.Connections
|
||||
.Filter(connection => server.Connections.All(c => c.Uri != connection.Uri)).ToList();
|
||||
await _mediaSourceRepository.Update(existing, toAdd, toRemove);
|
||||
await FindConnectionToActivate(existing);
|
||||
}
|
||||
|
||||
if (maybeExisting.IsNone)
|
||||
{
|
||||
await _mediaSourceRepository.Add(server);
|
||||
await FindConnectionToActivate(server);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FindConnectionToActivate(PlexMediaSource server)
|
||||
{
|
||||
var prioritized = server.Connections.OrderBy(pc => pc.IsActive ? 0 : 1).ToList();
|
||||
foreach (PlexConnection connection in server.Connections)
|
||||
{
|
||||
connection.IsActive = false;
|
||||
}
|
||||
|
||||
Option<PlexServerAuthToken> maybeToken = await _plexSecretStore.GetServerAuthToken(server.ClientIdentifier);
|
||||
foreach (PlexServerAuthToken token in maybeToken)
|
||||
{
|
||||
foreach (PlexConnection connection in prioritized)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Attempting to locate to Plex at {Uri}", connection.Uri);
|
||||
if (await _plexServerApiClient.Ping(connection, token))
|
||||
{
|
||||
_logger.LogInformation("Located Plex at {Uri}", connection.Uri);
|
||||
connection.IsActive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (maybeToken.IsNone)
|
||||
{
|
||||
_logger.LogError(
|
||||
"Unable to activate Plex connection for server {Server} without auth token",
|
||||
server.ServerName);
|
||||
}
|
||||
|
||||
if (server.Connections.All(c => !c.IsActive))
|
||||
{
|
||||
_logger.LogError("Unable to locate Plex");
|
||||
server.Connections.Head().IsActive = true;
|
||||
}
|
||||
|
||||
await _mediaSourceRepository.Update(server, new List<PlexConnection>(), new List<PlexConnection>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,12 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
PlaybackOrder PlaybackOrder,
|
||||
int? MultipleCount,
|
||||
TimeSpan? PlayoutDuration,
|
||||
bool? OfflineTail,
|
||||
string CustomTitle) : IRequest<Either<BaseError, ProgramScheduleItemViewModel>>, IProgramScheduleItemRequest;
|
||||
TailMode TailMode,
|
||||
ProgramScheduleItemCollectionType TailCollectionType,
|
||||
int? TailCollectionId,
|
||||
int? TailMultiCollectionId,
|
||||
int? TailSmartCollectionId,
|
||||
int? TailMediaItemId,
|
||||
string CustomTitle,
|
||||
GuideMode GuideMode) : IRequest<Either<BaseError, ProgramScheduleItemViewModel>>, IProgramScheduleItemRequest;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,13 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
PlaybackOrder PlaybackOrder { get; }
|
||||
int? MultipleCount { get; }
|
||||
TimeSpan? PlayoutDuration { get; }
|
||||
bool? OfflineTail { get; }
|
||||
TailMode TailMode { get; }
|
||||
ProgramScheduleItemCollectionType TailCollectionType { get; }
|
||||
int? TailCollectionId { get; }
|
||||
int? TailMultiCollectionId { get; }
|
||||
int? TailSmartCollectionId { get; }
|
||||
int? TailMediaItemId { get; }
|
||||
string CustomTitle { get; }
|
||||
GuideMode GuideMode { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,11 +55,6 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
return BaseError.New("[PlayoutDuration] is required for playout mode 'duration'");
|
||||
}
|
||||
|
||||
if (item.OfflineTail is null)
|
||||
{
|
||||
return BaseError.New("[OfflineTail] is required for playout mode 'duration'");
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
return BaseError.New("[PlayoutMode] is invalid");
|
||||
@@ -140,7 +135,8 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
SmartCollectionId = item.SmartCollectionId,
|
||||
MediaItemId = item.MediaItemId,
|
||||
PlaybackOrder = item.PlaybackOrder,
|
||||
CustomTitle = item.CustomTitle
|
||||
CustomTitle = item.CustomTitle,
|
||||
GuideMode = item.GuideMode
|
||||
},
|
||||
PlayoutMode.One => new ProgramScheduleItemOne
|
||||
{
|
||||
@@ -153,7 +149,8 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
SmartCollectionId = item.SmartCollectionId,
|
||||
MediaItemId = item.MediaItemId,
|
||||
PlaybackOrder = item.PlaybackOrder,
|
||||
CustomTitle = item.CustomTitle
|
||||
CustomTitle = item.CustomTitle,
|
||||
GuideMode = item.GuideMode
|
||||
},
|
||||
PlayoutMode.Multiple => new ProgramScheduleItemMultiple
|
||||
{
|
||||
@@ -167,7 +164,8 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
MediaItemId = item.MediaItemId,
|
||||
PlaybackOrder = item.PlaybackOrder,
|
||||
Count = item.MultipleCount.GetValueOrDefault(),
|
||||
CustomTitle = item.CustomTitle
|
||||
CustomTitle = item.CustomTitle,
|
||||
GuideMode = item.GuideMode
|
||||
},
|
||||
PlayoutMode.Duration => new ProgramScheduleItemDuration
|
||||
{
|
||||
@@ -181,8 +179,14 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
MediaItemId = item.MediaItemId,
|
||||
PlaybackOrder = item.PlaybackOrder,
|
||||
PlayoutDuration = FixDuration(item.PlayoutDuration.GetValueOrDefault()),
|
||||
OfflineTail = item.OfflineTail.GetValueOrDefault(),
|
||||
CustomTitle = item.CustomTitle
|
||||
TailMode = item.TailMode,
|
||||
TailCollectionType = item.TailCollectionType,
|
||||
TailCollectionId = item.TailCollectionId,
|
||||
TailMultiCollectionId = item.TailMultiCollectionId,
|
||||
TailSmartCollectionId = item.TailSmartCollectionId,
|
||||
TailMediaItemId = item.TailMediaItemId,
|
||||
CustomTitle = item.CustomTitle,
|
||||
GuideMode = item.GuideMode
|
||||
},
|
||||
_ => throw new NotSupportedException($"Unsupported playout mode {item.PlayoutMode}")
|
||||
};
|
||||
|
||||
@@ -20,8 +20,14 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
PlaybackOrder PlaybackOrder,
|
||||
int? MultipleCount,
|
||||
TimeSpan? PlayoutDuration,
|
||||
bool? OfflineTail,
|
||||
string CustomTitle) : IProgramScheduleItemRequest;
|
||||
TailMode TailMode,
|
||||
ProgramScheduleItemCollectionType TailCollectionType,
|
||||
int? TailCollectionId,
|
||||
int? TailMultiCollectionId,
|
||||
int? TailSmartCollectionId,
|
||||
int? TailMediaItemId,
|
||||
string CustomTitle,
|
||||
GuideMode GuideMode) : IProgramScheduleItemRequest;
|
||||
|
||||
public record ReplaceProgramScheduleItems
|
||||
(int ProgramScheduleId, List<ReplaceProgramScheduleItem> Items) : IRequest<
|
||||
|
||||
@@ -40,8 +40,26 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
},
|
||||
duration.PlaybackOrder,
|
||||
duration.PlayoutDuration,
|
||||
duration.OfflineTail,
|
||||
duration.CustomTitle),
|
||||
duration.TailMode,
|
||||
duration.TailCollectionType,
|
||||
duration.TailCollection != null
|
||||
? MediaCollections.Mapper.ProjectToViewModel(duration.TailCollection)
|
||||
: null,
|
||||
duration.TailMultiCollection != null
|
||||
? MediaCollections.Mapper.ProjectToViewModel(duration.TailMultiCollection)
|
||||
: null,
|
||||
duration.TailSmartCollection != null
|
||||
? MediaCollections.Mapper.ProjectToViewModel(duration.TailSmartCollection)
|
||||
: null,
|
||||
duration.TailMediaItem switch
|
||||
{
|
||||
Show show => MediaItems.Mapper.ProjectToViewModel(show),
|
||||
Season season => MediaItems.Mapper.ProjectToViewModel(season),
|
||||
Artist artist => MediaItems.Mapper.ProjectToViewModel(artist),
|
||||
_ => null
|
||||
},
|
||||
duration.CustomTitle,
|
||||
duration.GuideMode),
|
||||
ProgramScheduleItemFlood flood =>
|
||||
new ProgramScheduleItemFloodViewModel(
|
||||
flood.Id,
|
||||
@@ -66,7 +84,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
_ => null
|
||||
},
|
||||
flood.PlaybackOrder,
|
||||
flood.CustomTitle),
|
||||
flood.CustomTitle,
|
||||
flood.GuideMode),
|
||||
ProgramScheduleItemMultiple multiple =>
|
||||
new ProgramScheduleItemMultipleViewModel(
|
||||
multiple.Id,
|
||||
@@ -92,7 +111,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
},
|
||||
multiple.PlaybackOrder,
|
||||
multiple.Count,
|
||||
multiple.CustomTitle),
|
||||
multiple.CustomTitle,
|
||||
multiple.GuideMode),
|
||||
ProgramScheduleItemOne one =>
|
||||
new ProgramScheduleItemOneViewModel(
|
||||
one.Id,
|
||||
@@ -117,7 +137,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
_ => null
|
||||
},
|
||||
one.PlaybackOrder,
|
||||
one.CustomTitle),
|
||||
one.CustomTitle,
|
||||
one.GuideMode),
|
||||
_ => throw new NotSupportedException(
|
||||
$"Unsupported program schedule item type {programScheduleItem.GetType().Name}")
|
||||
};
|
||||
|
||||
@@ -19,8 +19,14 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
NamedMediaItemViewModel mediaItem,
|
||||
PlaybackOrder playbackOrder,
|
||||
TimeSpan playoutDuration,
|
||||
bool offlineTail,
|
||||
string customTitle) : base(
|
||||
TailMode tailMode,
|
||||
ProgramScheduleItemCollectionType tailCollectionType,
|
||||
MediaCollectionViewModel tailCollection,
|
||||
MultiCollectionViewModel tailMultiCollection,
|
||||
SmartCollectionViewModel tailSmartCollection,
|
||||
NamedMediaItemViewModel tailMediaItem,
|
||||
string customTitle,
|
||||
GuideMode guideMode) : base(
|
||||
id,
|
||||
index,
|
||||
startType,
|
||||
@@ -32,13 +38,26 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
smartCollection,
|
||||
mediaItem,
|
||||
playbackOrder,
|
||||
customTitle)
|
||||
customTitle,
|
||||
guideMode)
|
||||
{
|
||||
PlayoutDuration = playoutDuration;
|
||||
OfflineTail = offlineTail;
|
||||
TailMode = tailMode;
|
||||
TailCollectionType = tailCollectionType;
|
||||
TailCollection = tailCollection;
|
||||
TailMultiCollection = tailMultiCollection;
|
||||
TailSmartCollection = tailSmartCollection;
|
||||
TailMediaItem = tailMediaItem;
|
||||
}
|
||||
|
||||
public TimeSpan PlayoutDuration { get; }
|
||||
public bool OfflineTail { get; }
|
||||
public TailMode TailMode { get; }
|
||||
public ProgramScheduleItemCollectionType TailCollectionType { get; }
|
||||
|
||||
public MediaCollectionViewModel TailCollection { get; }
|
||||
public MultiCollectionViewModel TailMultiCollection { get; }
|
||||
public SmartCollectionViewModel TailSmartCollection { get; }
|
||||
public NamedMediaItemViewModel TailMediaItem { get; }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
SmartCollectionViewModel smartCollection,
|
||||
NamedMediaItemViewModel mediaItem,
|
||||
PlaybackOrder playbackOrder,
|
||||
string customTitle) : base(
|
||||
string customTitle,
|
||||
GuideMode guideMode) : base(
|
||||
id,
|
||||
index,
|
||||
startType,
|
||||
@@ -30,7 +31,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
smartCollection,
|
||||
mediaItem,
|
||||
playbackOrder,
|
||||
customTitle)
|
||||
customTitle,
|
||||
guideMode)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
NamedMediaItemViewModel mediaItem,
|
||||
PlaybackOrder playbackOrder,
|
||||
int count,
|
||||
string customTitle) : base(
|
||||
string customTitle,
|
||||
GuideMode guideMode) : base(
|
||||
id,
|
||||
index,
|
||||
startType,
|
||||
@@ -31,7 +32,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
smartCollection,
|
||||
mediaItem,
|
||||
playbackOrder,
|
||||
customTitle) =>
|
||||
customTitle,
|
||||
guideMode) =>
|
||||
Count = count;
|
||||
|
||||
public int Count { get; }
|
||||
|
||||
@@ -18,7 +18,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
SmartCollectionViewModel smartCollection,
|
||||
NamedMediaItemViewModel mediaItem,
|
||||
PlaybackOrder playbackOrder,
|
||||
string customTitle) : base(
|
||||
string customTitle,
|
||||
GuideMode guideMode) : base(
|
||||
id,
|
||||
index,
|
||||
startType,
|
||||
@@ -30,7 +31,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
smartCollection,
|
||||
mediaItem,
|
||||
playbackOrder,
|
||||
customTitle)
|
||||
customTitle,
|
||||
guideMode)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
SmartCollectionViewModel SmartCollection,
|
||||
NamedMediaItemViewModel MediaItem,
|
||||
PlaybackOrder PlaybackOrder,
|
||||
string CustomTitle)
|
||||
string CustomTitle,
|
||||
GuideMode GuideMode)
|
||||
{
|
||||
public string Name => CollectionType switch
|
||||
{
|
||||
|
||||
@@ -30,6 +30,10 @@ namespace ErsatzTV.Application.ProgramSchedules.Queries
|
||||
.Include(i => i.MultiCollection)
|
||||
.Include(i => i.SmartCollection)
|
||||
.Include(i => i.MediaItem)
|
||||
.Include(i => (i as ProgramScheduleItemDuration).TailCollection)
|
||||
.Include(i => (i as ProgramScheduleItemDuration).TailMultiCollection)
|
||||
.Include(i => (i as ProgramScheduleItemDuration).TailSmartCollection)
|
||||
.Include(i => (i as ProgramScheduleItemDuration).TailMediaItem)
|
||||
.ThenInclude(i => (i as Movie).MovieMetadata)
|
||||
.ThenInclude(mm => mm.Artwork)
|
||||
.Include(i => i.MediaItem)
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Infrastructure.Search;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
@@ -19,12 +20,13 @@ namespace ErsatzTV.Application.Search.Queries
|
||||
QuerySearchIndexAllItems request,
|
||||
CancellationToken cancellationToken) =>
|
||||
new(
|
||||
await GetIds("movie", request.Query),
|
||||
await GetIds("show", request.Query),
|
||||
await GetIds("season", request.Query),
|
||||
await GetIds("episode", request.Query),
|
||||
await GetIds("artist", request.Query),
|
||||
await GetIds("music_video", request.Query));
|
||||
await GetIds(SearchIndex.MovieType, request.Query),
|
||||
await GetIds(SearchIndex.ShowType, request.Query),
|
||||
await GetIds(SearchIndex.SeasonType, request.Query),
|
||||
await GetIds(SearchIndex.EpisodeType, request.Query),
|
||||
await GetIds(SearchIndex.ArtistType, request.Query),
|
||||
await GetIds(SearchIndex.MusicVideoType, request.Query),
|
||||
await GetIds(SearchIndex.OtherVideoType, request.Query));
|
||||
|
||||
private Task<List<int>> GetIds(string type, string query) =>
|
||||
_searchIndex.Search($"type:{type} AND ({query})", 0, 0)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Search.Queries
|
||||
{
|
||||
public record QuerySearchIndexOtherVideos
|
||||
(string Query, int PageNumber, int PageSize) : IRequest<OtherVideoCardResultsViewModel>;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Search;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.MediaCards.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Search.Queries
|
||||
{
|
||||
public class
|
||||
QuerySearchIndexOtherVideosHandler : IRequestHandler<QuerySearchIndexOtherVideos,
|
||||
OtherVideoCardResultsViewModel>
|
||||
{
|
||||
private readonly IOtherVideoRepository _otherVideoRepository;
|
||||
private readonly ISearchIndex _searchIndex;
|
||||
|
||||
public QuerySearchIndexOtherVideosHandler(ISearchIndex searchIndex, IOtherVideoRepository otherVideoRepository)
|
||||
{
|
||||
_searchIndex = searchIndex;
|
||||
_otherVideoRepository = otherVideoRepository;
|
||||
}
|
||||
|
||||
public async Task<OtherVideoCardResultsViewModel> Handle(
|
||||
QuerySearchIndexOtherVideos request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
SearchResult searchResult = await _searchIndex.Search(
|
||||
request.Query,
|
||||
(request.PageNumber - 1) * request.PageSize,
|
||||
request.PageSize);
|
||||
|
||||
List<OtherVideoCardViewModel> items = await _otherVideoRepository
|
||||
.GetOtherVideosForCards(searchResult.Items.Map(i => i.Id).ToList())
|
||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
|
||||
return new OtherVideoCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,5 +8,6 @@ namespace ErsatzTV.Application.Search
|
||||
List<int> SeasonIds,
|
||||
List<int> EpisodeIds,
|
||||
List<int> ArtistIds,
|
||||
List<int> MusicVideoIds);
|
||||
List<int> MusicVideoIds,
|
||||
List<int> OtherVideoIds);
|
||||
}
|
||||
|
||||
+14
-4
@@ -75,6 +75,12 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as MusicVideo).MediaVersions)
|
||||
.ThenInclude(mv => mv.Streams)
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as OtherVideo).MediaVersions)
|
||||
.ThenInclude(ov => ov.MediaFiles)
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as OtherVideo).MediaVersions)
|
||||
.ThenInclude(ov => ov.Streams)
|
||||
.ForChannelAndTime(channel.Id, now)
|
||||
.Map(o => o.ToEither<BaseError>(new UnableToLocatePlayoutItem()))
|
||||
.BindT(ValidatePlayoutItemPath);
|
||||
@@ -87,6 +93,7 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
MusicVideo mv => mv.MediaVersions.Head(),
|
||||
OtherVideo ov => ov.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(playoutItemWithPath))
|
||||
};
|
||||
|
||||
@@ -111,7 +118,6 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
maybeGlobalWatermark,
|
||||
channel.FFmpegProfile.VaapiDriver,
|
||||
channel.FFmpegProfile.VaapiDevice,
|
||||
request.StartAtZero,
|
||||
request.HlsRealtime);
|
||||
|
||||
var result = new PlayoutItemProcessModel(process, playoutItemWithPath.PlayoutItem.FinishOffset);
|
||||
@@ -146,7 +152,8 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
ffmpegPath,
|
||||
channel,
|
||||
maybeDuration,
|
||||
"Channel is Offline");
|
||||
"Channel is Offline",
|
||||
request.HlsRealtime);
|
||||
|
||||
|
||||
return new PlayoutItemProcessModel(errorProcess, finish);
|
||||
@@ -165,7 +172,8 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
ffmpegPath,
|
||||
channel,
|
||||
maybeDuration,
|
||||
error.Value);
|
||||
error.Value,
|
||||
request.HlsRealtime);
|
||||
|
||||
return new PlayoutItemProcessModel(errorProcess, finish);
|
||||
}
|
||||
@@ -183,7 +191,8 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
ffmpegPath,
|
||||
channel,
|
||||
maybeDuration,
|
||||
"Channel is Offline");
|
||||
"Channel is Offline",
|
||||
request.HlsRealtime);
|
||||
|
||||
return new PlayoutItemProcessModel(errorProcess, finish);
|
||||
}
|
||||
@@ -217,6 +226,7 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
MusicVideo mv => mv.MediaVersions.Head(),
|
||||
OtherVideo ov => ov.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(playoutItem))
|
||||
};
|
||||
|
||||
|
||||
@@ -188,7 +188,6 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
None,
|
||||
VaapiDriver.Default,
|
||||
"/dev/dri/renderD128",
|
||||
false,
|
||||
false);
|
||||
|
||||
process.StartInfo.RedirectStandardError = true;
|
||||
|
||||
@@ -709,7 +709,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
CollectionId = fixedCollection.Id,
|
||||
StartTime = TimeSpan.FromHours(2),
|
||||
PlayoutDuration = TimeSpan.FromHours(2),
|
||||
OfflineTail = false, // immediately continue
|
||||
TailMode = TailMode.None, // immediately continue
|
||||
PlaybackOrder = PlaybackOrder.Chronological
|
||||
}
|
||||
};
|
||||
@@ -807,7 +807,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
CollectionId = dynamicCollection.Id,
|
||||
StartTime = null,
|
||||
PlayoutDuration = TimeSpan.FromHours(2),
|
||||
OfflineTail = false, // immediately continue
|
||||
TailMode = TailMode.None, // immediately continue
|
||||
PlaybackOrder = PlaybackOrder.Chronological
|
||||
}
|
||||
};
|
||||
@@ -1089,7 +1089,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlayoutDuration = TimeSpan.FromHours(3),
|
||||
OfflineTail = false,
|
||||
TailMode = TailMode.None,
|
||||
PlaybackOrder = PlaybackOrder.Chronological
|
||||
},
|
||||
new ProgramScheduleItemDuration
|
||||
@@ -1100,7 +1100,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
CollectionId = collectionTwo.Id,
|
||||
StartTime = null,
|
||||
PlayoutDuration = TimeSpan.FromHours(3),
|
||||
OfflineTail = false,
|
||||
TailMode = TailMode.None,
|
||||
PlaybackOrder = PlaybackOrder.Chronological
|
||||
}
|
||||
};
|
||||
@@ -1151,6 +1151,212 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
result.Anchor.NextScheduleItem.Should().Be(items[1]);
|
||||
result.Anchor.DurationFinish.Should().Be(HoursAfterMidnight(6).UtcDateTime);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Alternating_Duration_With_Filler_Should_Alternate_Schedule_Items()
|
||||
{
|
||||
var collectionOne = new Collection
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Duration Items 1",
|
||||
MediaItems = new List<MediaItem>
|
||||
{
|
||||
TestMovie(1, TimeSpan.FromMinutes(55), new DateTime(2020, 1, 1))
|
||||
}
|
||||
};
|
||||
|
||||
var collectionTwo = new Collection
|
||||
{
|
||||
Id = 2,
|
||||
Name = "Duration Items 2",
|
||||
MediaItems = new List<MediaItem>
|
||||
{
|
||||
TestMovie(2, TimeSpan.FromMinutes(55), new DateTime(2020, 1, 1))
|
||||
}
|
||||
};
|
||||
|
||||
var collectionThree = new Collection
|
||||
{
|
||||
Id = 3,
|
||||
Name = "Filler Items",
|
||||
MediaItems = new List<MediaItem>
|
||||
{
|
||||
TestMovie(3, TimeSpan.FromMinutes(5), new DateTime(2020, 1, 1))
|
||||
}
|
||||
};
|
||||
|
||||
var fakeRepository = new FakeMediaCollectionRepository(
|
||||
Map(
|
||||
(collectionOne.Id, collectionOne.MediaItems.ToList()),
|
||||
(collectionTwo.Id, collectionTwo.MediaItems.ToList()),
|
||||
(collectionThree.Id, collectionThree.MediaItems.ToList())));
|
||||
|
||||
var items = new List<ProgramScheduleItem>
|
||||
{
|
||||
new ProgramScheduleItemDuration
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlayoutDuration = TimeSpan.FromHours(3),
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailMode = TailMode.Filler,
|
||||
TailCollectionType = ProgramScheduleItemCollectionType.Collection,
|
||||
TailCollection = collectionThree,
|
||||
TailCollectionId = collectionThree.Id
|
||||
},
|
||||
new ProgramScheduleItemDuration
|
||||
{
|
||||
Id = 2,
|
||||
Index = 2,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id,
|
||||
StartTime = null,
|
||||
PlayoutDuration = TimeSpan.FromHours(3),
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailMode = TailMode.Filler,
|
||||
TailCollectionType = ProgramScheduleItemCollectionType.Collection,
|
||||
TailCollection = collectionThree,
|
||||
TailCollectionId = collectionThree.Id
|
||||
}
|
||||
};
|
||||
|
||||
var playout = new Playout
|
||||
{
|
||||
ProgramSchedule = new ProgramSchedule
|
||||
{
|
||||
Items = items
|
||||
},
|
||||
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
|
||||
};
|
||||
|
||||
var configRepo = new Mock<IConfigElementRepository>();
|
||||
var televisionRepo = new FakeTelevisionRepository();
|
||||
var artistRepo = new Mock<IArtistRepository>();
|
||||
var builder = new PlayoutBuilder(
|
||||
configRepo.Object,
|
||||
fakeRepository,
|
||||
televisionRepo,
|
||||
artistRepo.Object,
|
||||
_logger);
|
||||
|
||||
DateTimeOffset start = HoursAfterMidnight(0);
|
||||
DateTimeOffset finish = start + TimeSpan.FromHours(6);
|
||||
|
||||
Playout result = await builder.BuildPlayoutItems(playout, start, finish);
|
||||
|
||||
result.Items.Count.Should().Be(12);
|
||||
|
||||
result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromMinutes(0));
|
||||
result.Items[0].MediaItemId.Should().Be(1);
|
||||
result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromMinutes(55));
|
||||
result.Items[1].MediaItemId.Should().Be(1);
|
||||
result.Items[2].StartOffset.TimeOfDay.Should().Be(new TimeSpan(1, 50, 0));
|
||||
result.Items[2].MediaItemId.Should().Be(1);
|
||||
|
||||
result.Items[3].StartOffset.TimeOfDay.Should().Be(new TimeSpan(2, 45, 0));
|
||||
result.Items[3].MediaItemId.Should().Be(3);
|
||||
result.Items[4].StartOffset.TimeOfDay.Should().Be(new TimeSpan(2, 50, 0));
|
||||
result.Items[4].MediaItemId.Should().Be(3);
|
||||
result.Items[5].StartOffset.TimeOfDay.Should().Be(new TimeSpan(2, 55, 0));
|
||||
result.Items[5].MediaItemId.Should().Be(3);
|
||||
|
||||
result.Items[6].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3));
|
||||
result.Items[6].MediaItemId.Should().Be(2);
|
||||
result.Items[7].StartOffset.TimeOfDay.Should().Be(new TimeSpan(3, 55, 0));
|
||||
result.Items[7].MediaItemId.Should().Be(2);
|
||||
result.Items[8].StartOffset.TimeOfDay.Should().Be(new TimeSpan(4, 50, 0));
|
||||
result.Items[8].MediaItemId.Should().Be(2);
|
||||
|
||||
result.Items[9].StartOffset.TimeOfDay.Should().Be(new TimeSpan(5, 45, 0));
|
||||
result.Items[9].MediaItemId.Should().Be(3);
|
||||
result.Items[10].StartOffset.TimeOfDay.Should().Be(new TimeSpan(5, 50, 0));
|
||||
result.Items[10].MediaItemId.Should().Be(3);
|
||||
result.Items[11].StartOffset.TimeOfDay.Should().Be(new TimeSpan(5, 55, 0));
|
||||
result.Items[11].MediaItemId.Should().Be(3);
|
||||
|
||||
result.Anchor.NextScheduleItem.Should().Be(items[0]);
|
||||
result.Anchor.DurationFinish.Should().BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Duration_Should_Skip_Items_That_Are_Too_Long()
|
||||
{
|
||||
var collectionOne = new Collection
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Duration Items 1",
|
||||
MediaItems = new List<MediaItem>
|
||||
{
|
||||
TestMovie(1, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)),
|
||||
TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)),
|
||||
TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)),
|
||||
TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 1))
|
||||
}
|
||||
};
|
||||
|
||||
var fakeRepository =
|
||||
new FakeMediaCollectionRepository(Map((collectionOne.Id, collectionOne.MediaItems.ToList())));
|
||||
|
||||
var items = new List<ProgramScheduleItem>
|
||||
{
|
||||
new ProgramScheduleItemDuration
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlayoutDuration = TimeSpan.FromHours(1),
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailMode = TailMode.None,
|
||||
}
|
||||
};
|
||||
|
||||
var playout = new Playout
|
||||
{
|
||||
ProgramSchedule = new ProgramSchedule
|
||||
{
|
||||
Items = items
|
||||
},
|
||||
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
|
||||
};
|
||||
|
||||
var configRepo = new Mock<IConfigElementRepository>();
|
||||
var televisionRepo = new FakeTelevisionRepository();
|
||||
var artistRepo = new Mock<IArtistRepository>();
|
||||
var builder = new PlayoutBuilder(
|
||||
configRepo.Object,
|
||||
fakeRepository,
|
||||
televisionRepo,
|
||||
artistRepo.Object,
|
||||
_logger);
|
||||
|
||||
DateTimeOffset start = HoursAfterMidnight(0);
|
||||
DateTimeOffset finish = start + TimeSpan.FromHours(6);
|
||||
|
||||
Playout result = await builder.BuildPlayoutItems(playout, start, finish);
|
||||
|
||||
result.Items.Count.Should().Be(6);
|
||||
|
||||
result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(0));
|
||||
result.Items[0].MediaItemId.Should().Be(2);
|
||||
result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1));
|
||||
result.Items[1].MediaItemId.Should().Be(4);
|
||||
result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2));
|
||||
result.Items[2].MediaItemId.Should().Be(2);
|
||||
result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3));
|
||||
result.Items[3].MediaItemId.Should().Be(4);
|
||||
result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4));
|
||||
result.Items[4].MediaItemId.Should().Be(2);
|
||||
result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5));
|
||||
result.Items[5].MediaItemId.Should().Be(4);
|
||||
|
||||
result.Anchor.NextScheduleItem.Should().Be(items[0]);
|
||||
result.Anchor.DurationFinish.Should().BeNull();
|
||||
}
|
||||
|
||||
private static DateTimeOffset HoursAfterMidnight(int hours)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public enum GuideMode
|
||||
{
|
||||
Normal = 0,
|
||||
Filler = 1
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
{
|
||||
Movies = 1,
|
||||
Shows = 2,
|
||||
MusicVideos = 3
|
||||
MusicVideos = 3,
|
||||
OtherVideos = 4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public class OtherVideo : MediaItem
|
||||
{
|
||||
public List<OtherVideoMetadata> OtherVideoMetadata { get; set; }
|
||||
public List<MediaVersion> MediaVersions { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public class OtherVideoMetadata : Metadata
|
||||
{
|
||||
public int OtherVideoId { get; set; }
|
||||
public OtherVideo OtherVideo { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ namespace ErsatzTV.Core.Domain
|
||||
public int? MultipleRemaining { get; set; }
|
||||
public DateTime? DurationFinish { get; set; }
|
||||
public bool InFlood { get; set; }
|
||||
public bool InDurationFiller { get; set; }
|
||||
|
||||
public DateTimeOffset NextStartOffset => new DateTimeOffset(NextStart, TimeSpan.Zero).ToLocalTime();
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
[DebuggerDisplay("{MediaItemId} - {Start} - {Finish}")]
|
||||
public class PlayoutItem
|
||||
{
|
||||
public int Id { get; set; }
|
||||
@@ -9,12 +11,17 @@ namespace ErsatzTV.Core.Domain
|
||||
public MediaItem MediaItem { get; set; }
|
||||
public DateTime Start { get; set; }
|
||||
public DateTime Finish { get; set; }
|
||||
public DateTime? GuideFinish { get; set; }
|
||||
public string CustomTitle { get; set; }
|
||||
public bool CustomGroup { get; set; }
|
||||
public bool IsFiller { get; set; }
|
||||
public int PlayoutId { get; set; }
|
||||
public Playout Playout { get; set; }
|
||||
|
||||
public DateTimeOffset StartOffset => new DateTimeOffset(Start, TimeSpan.Zero).ToLocalTime();
|
||||
public DateTimeOffset FinishOffset => new DateTimeOffset(Finish, TimeSpan.Zero).ToLocalTime();
|
||||
public DateTimeOffset? GuideFinishOffset => GuideFinish.HasValue
|
||||
? new DateTimeOffset(GuideFinish.Value, TimeSpan.Zero).ToLocalTime()
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
public GuideMode GuideMode { get; set; }
|
||||
public string CustomTitle { get; set; }
|
||||
public int ProgramScheduleId { get; set; }
|
||||
public ProgramSchedule ProgramSchedule { get; set; }
|
||||
|
||||
@@ -5,6 +5,15 @@ namespace ErsatzTV.Core.Domain
|
||||
public class ProgramScheduleItemDuration : ProgramScheduleItem
|
||||
{
|
||||
public TimeSpan PlayoutDuration { get; set; }
|
||||
public bool OfflineTail { get; set; }
|
||||
public TailMode TailMode { get; set; }
|
||||
public ProgramScheduleItemCollectionType TailCollectionType { get; set; }
|
||||
public int? TailCollectionId { get; set; }
|
||||
public Collection TailCollection { get; set; }
|
||||
public int? TailMediaItemId { get; set; }
|
||||
public MediaItem TailMediaItem { get; set; }
|
||||
public int? TailMultiCollectionId { get; set; }
|
||||
public MultiCollection TailMultiCollection { get; set; }
|
||||
public int? TailSmartCollectionId { get; set; }
|
||||
public SmartCollection TailSmartCollection { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public enum TailMode
|
||||
{
|
||||
None = 0,
|
||||
Offline = 1,
|
||||
Slate = 2,
|
||||
Filler = 3
|
||||
}
|
||||
}
|
||||
@@ -230,7 +230,13 @@ namespace ErsatzTV.Core.Emby
|
||||
foreach (EmbySeason updated in await _televisionRepository.Update(incoming))
|
||||
{
|
||||
incoming.Show = show;
|
||||
await _searchIndex.UpdateItems(_searchRepository, new List<MediaItem> { updated });
|
||||
|
||||
foreach (MediaItem toIndex in await _searchRepository.GetItemToIndex(updated.Id))
|
||||
{
|
||||
await _searchIndex.UpdateItems(
|
||||
_searchRepository,
|
||||
new List<MediaItem> { toIndex });
|
||||
}
|
||||
}
|
||||
},
|
||||
async () =>
|
||||
|
||||
@@ -159,14 +159,24 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
return result;
|
||||
}
|
||||
|
||||
public FFmpegPlaybackSettings CalculateErrorSettings(FFmpegProfile ffmpegProfile) =>
|
||||
new()
|
||||
public FFmpegPlaybackSettings CalculateErrorSettings(FFmpegProfile ffmpegProfile)
|
||||
{
|
||||
string softwareCodec = ffmpegProfile.VideoCodec switch
|
||||
{
|
||||
{ } c when c.Contains("hevc") || c.Contains("265") => "libx265",
|
||||
{ } c when c.Contains("264") => "libx264",
|
||||
{ } c when c.Contains("mpeg2") => "mpeg2video",
|
||||
_ => "libx264"
|
||||
};
|
||||
|
||||
return new FFmpegPlaybackSettings
|
||||
{
|
||||
ThreadCount = ffmpegProfile.ThreadCount,
|
||||
FormatFlags = CommonFormatFlags,
|
||||
VideoCodec = "libx264",
|
||||
VideoCodec = softwareCodec,
|
||||
AudioCodec = ffmpegProfile.AudioCodec,
|
||||
};
|
||||
}
|
||||
|
||||
private static bool NeedToScale(FFmpegProfile ffmpegProfile, MediaVersion version) =>
|
||||
ffmpegProfile.Transcode && ffmpegProfile.NormalizeVideo &&
|
||||
|
||||
@@ -324,22 +324,29 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
return this;
|
||||
}
|
||||
|
||||
public FFmpegProcessBuilder WithHls(string channelNumber, MediaVersion mediaVersion, bool startAtZero)
|
||||
public FFmpegProcessBuilder WithHls(string channelNumber, Option<MediaVersion> mediaVersion)
|
||||
{
|
||||
const int SEGMENT_SECONDS = 4;
|
||||
|
||||
if (!int.TryParse(mediaVersion.RFrameRate, out int frameRate))
|
||||
|
||||
var frameRate = 24;
|
||||
|
||||
foreach (MediaVersion version in mediaVersion)
|
||||
{
|
||||
string[] split = (mediaVersion.RFrameRate ?? string.Empty).Split("/");
|
||||
if (int.TryParse(split[0], out int left) && int.TryParse(split[1], out int right))
|
||||
if (!int.TryParse(version.RFrameRate, out int fr))
|
||||
{
|
||||
frameRate = (int)Math.Round(left / (double)right);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Unable to detect framerate, using {FrameRate}", 24);
|
||||
frameRate = 24;
|
||||
string[] split = (version.RFrameRate ?? string.Empty).Split("/");
|
||||
if (int.TryParse(split[0], out int left) && int.TryParse(split[1], out int right))
|
||||
{
|
||||
fr = (int)Math.Round(left / (double)right);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Unable to detect framerate, using {FrameRate}", 24);
|
||||
fr = 24;
|
||||
}
|
||||
}
|
||||
|
||||
frameRate = fr;
|
||||
}
|
||||
|
||||
_arguments.AddRange(
|
||||
|
||||
@@ -41,7 +41,6 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
Option<ChannelWatermark> globalWatermark,
|
||||
VaapiDriver vaapiDriver,
|
||||
string vaapiDevice,
|
||||
bool startAtZero,
|
||||
bool hlsRealtime)
|
||||
{
|
||||
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, version);
|
||||
@@ -121,7 +120,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
{
|
||||
// HLS needs to segment and generate playlist
|
||||
case StreamingMode.HttpLiveStreamingSegmenter:
|
||||
return builder.WithHls(channel.Number, version, startAtZero)
|
||||
return builder.WithHls(channel.Number, version)
|
||||
.WithRealtimeOutput(hlsRealtime)
|
||||
.Build();
|
||||
default:
|
||||
@@ -131,7 +130,12 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
}
|
||||
}
|
||||
|
||||
public Process ForError(string ffmpegPath, Channel channel, Option<TimeSpan> duration, string errorMessage)
|
||||
public Process ForError(
|
||||
string ffmpegPath,
|
||||
Channel channel,
|
||||
Option<TimeSpan> duration,
|
||||
string errorMessage,
|
||||
bool hlsRealtime)
|
||||
{
|
||||
FFmpegPlaybackSettings playbackSettings =
|
||||
_playbackSettingsCalculator.CalculateErrorSettings(channel.FFmpegProfile);
|
||||
@@ -149,12 +153,22 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
.WithErrorText(desiredResolution, errorMessage)
|
||||
.WithPixfmt("yuv420p")
|
||||
.WithPlaybackArgs(playbackSettings)
|
||||
.WithMetadata(channel, None)
|
||||
.WithFormat("mpegts");
|
||||
.WithMetadata(channel, None);
|
||||
|
||||
duration.IfSome(d => builder = builder.WithDuration(d));
|
||||
|
||||
return builder.WithPipe().Build();
|
||||
switch (channel.StreamingMode)
|
||||
{
|
||||
// HLS needs to segment and generate playlist
|
||||
case StreamingMode.HttpLiveStreamingSegmenter:
|
||||
return builder.WithHls(channel.Number, None)
|
||||
.WithRealtimeOutput(hlsRealtime)
|
||||
.Build();
|
||||
default:
|
||||
return builder.WithFormat("mpegts")
|
||||
.WithPipe()
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
|
||||
public Process ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host)
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
List<EpisodeMetadata> GetFallbackMetadata(Episode episode);
|
||||
MovieMetadata GetFallbackMetadata(Movie movie);
|
||||
Option<MusicVideoMetadata> GetFallbackMetadata(MusicVideo musicVideo);
|
||||
Option<OtherVideoMetadata> GetFallbackMetadata(OtherVideo otherVideo);
|
||||
string GetSortTitle(string title);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
Task<bool> RefreshFallbackMetadata(Episode episode);
|
||||
Task<bool> RefreshFallbackMetadata(Artist artist, string artistFolder);
|
||||
Task<bool> RefreshFallbackMetadata(MusicVideo musicVideo);
|
||||
Task<bool> RefreshFallbackMetadata(OtherVideo otherVideo);
|
||||
Task<bool> RefreshFallbackMetadata(Show televisionShow, string showFolder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
{
|
||||
public interface IOtherVideoFolderScanner
|
||||
{
|
||||
Task<Either<BaseError, Unit>> ScanFolder(
|
||||
LibraryPath libraryPath,
|
||||
string ffprobePath,
|
||||
decimal progressMin,
|
||||
decimal progressMax);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,10 @@ namespace ErsatzTV.Core.Interfaces.Plex
|
||||
{
|
||||
public interface IPlexServerApiClient
|
||||
{
|
||||
Task<bool> Ping(
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token);
|
||||
|
||||
Task<Either<BaseError, List<PlexLibrary>>> GetLibraries(
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token);
|
||||
|
||||
@@ -18,7 +18,6 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
|
||||
Task Update(
|
||||
PlexMediaSource plexMediaSource,
|
||||
List<PlexConnection> prioritizedConnections,
|
||||
List<PlexConnection> toAdd,
|
||||
List<PlexConnection> toDelete);
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
{
|
||||
public interface IOtherVideoRepository
|
||||
{
|
||||
Task<Either<BaseError, MediaItemScanResult<OtherVideo>>> GetOrAdd(LibraryPath libraryPath, string path);
|
||||
Task<IEnumerable<string>> FindOtherVideoPaths(LibraryPath libraryPath);
|
||||
Task<List<int>> DeleteByPath(LibraryPath libraryPath, string path);
|
||||
Task<bool> AddTag(OtherVideoMetadata metadata, Tag tag);
|
||||
Task<List<OtherVideoMetadata>> GetOtherVideosForCards(List<int> ids);
|
||||
// Task<int> GetOtherVideoCount(int artistId);
|
||||
// Task<List<OtherVideoMetadata>> GetPagedOtherVideos(int artistId, int pageNumber, int pageSize);
|
||||
}
|
||||
}
|
||||
@@ -70,13 +70,19 @@ namespace ErsatzTV.Core.Iptv
|
||||
foreach ((Channel channel, List<PlayoutItem> sorted) in sortedChannelItems.OrderBy(kvp => kvp.Key.Number))
|
||||
{
|
||||
var i = 0;
|
||||
while (i < sorted.Count && sorted[i].IsFiller)
|
||||
{
|
||||
i++;
|
||||
}
|
||||
|
||||
while (i < sorted.Count)
|
||||
{
|
||||
PlayoutItem startItem = sorted[i];
|
||||
bool hasCustomTitle = !string.IsNullOrWhiteSpace(startItem.CustomTitle);
|
||||
|
||||
int finishIndex = i;
|
||||
while (hasCustomTitle && finishIndex + 1 < sorted.Count && sorted[finishIndex + 1].CustomGroup)
|
||||
while (finishIndex + 1 < sorted.Count && (hasCustomTitle && sorted[finishIndex + 1].CustomGroup ||
|
||||
sorted[finishIndex + 1].IsFiller))
|
||||
{
|
||||
finishIndex++;
|
||||
}
|
||||
@@ -97,10 +103,10 @@ namespace ErsatzTV.Core.Iptv
|
||||
PlayoutItem finishItem = sorted[finishIndex];
|
||||
i = finishIndex;
|
||||
|
||||
// ReSharper disable once StringLiteralTypo
|
||||
string start = startItem.StartOffset.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty);
|
||||
// ReSharper disable once StringLiteralTypo
|
||||
string stop = finishItem.FinishOffset.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty);
|
||||
string stop = startItem.GuideFinishOffset.HasValue
|
||||
? startItem.GuideFinishOffset.Value.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty)
|
||||
: finishItem.FinishOffset.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty);
|
||||
|
||||
string title = GetTitle(startItem);
|
||||
string subtitle = GetSubtitle(startItem);
|
||||
@@ -169,6 +175,39 @@ namespace ErsatzTV.Core.Iptv
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasCustomTitle && startItem.MediaItem is MusicVideo musicVideo)
|
||||
{
|
||||
foreach (MusicVideoMetadata metadata in musicVideo.MusicVideoMetadata.HeadOrNone())
|
||||
{
|
||||
if (metadata.Year.HasValue)
|
||||
{
|
||||
xml.WriteStartElement("date");
|
||||
xml.WriteString(metadata.Year.Value.ToString());
|
||||
xml.WriteEndElement(); // date
|
||||
}
|
||||
}
|
||||
|
||||
xml.WriteStartElement("category");
|
||||
xml.WriteAttributeString("lang", "en");
|
||||
xml.WriteString("Music");
|
||||
xml.WriteEndElement(); // category
|
||||
|
||||
foreach (MusicVideoMetadata metadata in musicVideo.MusicVideoMetadata.HeadOrNone())
|
||||
{
|
||||
string thumbnail = Optional(metadata.Artwork).Flatten()
|
||||
.Filter(a => a.ArtworkKind == ArtworkKind.Thumbnail)
|
||||
.HeadOrNone()
|
||||
.Match(a => GetArtworkUrl(a, ArtworkKind.Thumbnail), () => string.Empty);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(thumbnail))
|
||||
{
|
||||
xml.WriteStartElement("icon");
|
||||
xml.WriteAttributeString("src", thumbnail);
|
||||
xml.WriteEndElement(); // icon
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (startItem.MediaItem is Episode episode && (!hasCustomTitle || isSameCustomShow))
|
||||
{
|
||||
Option<ShowMetadata> maybeMetadata =
|
||||
|
||||
@@ -230,7 +230,13 @@ namespace ErsatzTV.Core.Jellyfin
|
||||
foreach (JellyfinSeason updated in await _televisionRepository.Update(incoming))
|
||||
{
|
||||
incoming.Show = show;
|
||||
await _searchIndex.UpdateItems(_searchRepository, new List<MediaItem> { updated });
|
||||
|
||||
foreach (MediaItem toIndex in await _searchRepository.GetItemToIndex(updated.Id))
|
||||
{
|
||||
await _searchIndex.UpdateItems(
|
||||
_searchRepository,
|
||||
new List<MediaItem> { toIndex });
|
||||
}
|
||||
}
|
||||
},
|
||||
async () =>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
@@ -88,6 +89,20 @@ namespace ErsatzTV.Core.Metadata
|
||||
|
||||
return GetMusicVideoMetadata(fileName, metadata);
|
||||
}
|
||||
|
||||
public Option<OtherVideoMetadata> GetFallbackMetadata(OtherVideo otherVideo)
|
||||
{
|
||||
string path = otherVideo.MediaVersions.Head().MediaFiles.Head().Path;
|
||||
string fileName = Path.GetFileNameWithoutExtension(path);
|
||||
var metadata = new OtherVideoMetadata
|
||||
{
|
||||
MetadataKind = MetadataKind.Fallback,
|
||||
Title = fileName ?? path,
|
||||
OtherVideo = otherVideo
|
||||
};
|
||||
|
||||
return GetOtherVideoMetadata(path, metadata);
|
||||
}
|
||||
|
||||
public string GetSortTitle(string title)
|
||||
{
|
||||
@@ -214,6 +229,43 @@ namespace ErsatzTV.Core.Metadata
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
private Option<OtherVideoMetadata> GetOtherVideoMetadata(string path, OtherVideoMetadata metadata)
|
||||
{
|
||||
try
|
||||
{
|
||||
string folder = Path.GetDirectoryName(path);
|
||||
if (folder == null)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
string libraryPath = metadata.OtherVideo.LibraryPath.Path;
|
||||
string parent = Optional(Directory.GetParent(libraryPath)).Match(
|
||||
di => di.FullName,
|
||||
() => libraryPath);
|
||||
|
||||
string diff = Path.GetRelativePath(parent, folder);
|
||||
|
||||
var tags = diff.Split(Path.DirectorySeparatorChar)
|
||||
.Map(t => new Tag { Name = t })
|
||||
.ToList();
|
||||
|
||||
metadata.Artwork = new List<Artwork>();
|
||||
metadata.Actors = new List<Actor>();
|
||||
metadata.Genres = new List<Genre>();
|
||||
metadata.Tags = tags;
|
||||
metadata.Studios = new List<Studio>();
|
||||
metadata.DateUpdated = DateTime.UtcNow;
|
||||
metadata.OriginalTitle = Path.GetRelativePath(libraryPath, path);
|
||||
|
||||
return metadata;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
private ShowMetadata GetTelevisionShowMetadata(string fileName, ShowMetadata metadata)
|
||||
{
|
||||
|
||||
@@ -73,6 +73,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
MusicVideo mv => mv.MediaVersions.Head(),
|
||||
OtherVideo ov => ov.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
|
||||
};
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
private readonly IMetadataRepository _metadataRepository;
|
||||
private readonly IMovieRepository _movieRepository;
|
||||
private readonly IMusicVideoRepository _musicVideoRepository;
|
||||
private readonly IOtherVideoRepository _otherVideoRepository;
|
||||
private readonly ITelevisionRepository _televisionRepository;
|
||||
|
||||
public LocalMetadataProvider(
|
||||
@@ -38,6 +39,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
ITelevisionRepository televisionRepository,
|
||||
IArtistRepository artistRepository,
|
||||
IMusicVideoRepository musicVideoRepository,
|
||||
IOtherVideoRepository otherVideoRepository,
|
||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||
ILocalFileSystem localFileSystem,
|
||||
IEpisodeNfoReader episodeNfoReader,
|
||||
@@ -48,6 +50,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
_televisionRepository = televisionRepository;
|
||||
_artistRepository = artistRepository;
|
||||
_musicVideoRepository = musicVideoRepository;
|
||||
_otherVideoRepository = otherVideoRepository;
|
||||
_fallbackMetadataProvider = fallbackMetadataProvider;
|
||||
_localFileSystem = localFileSystem;
|
||||
_episodeNfoReader = episodeNfoReader;
|
||||
@@ -136,6 +139,11 @@ namespace ErsatzTV.Core.Metadata
|
||||
public Task<bool> RefreshFallbackMetadata(Artist artist, string artistFolder) =>
|
||||
ApplyMetadataUpdate(artist, _fallbackMetadataProvider.GetFallbackMetadataForArtist(artistFolder));
|
||||
|
||||
public Task<bool> RefreshFallbackMetadata(OtherVideo otherVideo) =>
|
||||
_fallbackMetadataProvider.GetFallbackMetadata(otherVideo).Match(
|
||||
metadata => ApplyMetadataUpdate(otherVideo, metadata),
|
||||
() => Task.FromResult(false));
|
||||
|
||||
public Task<bool> RefreshFallbackMetadata(MusicVideo musicVideo) =>
|
||||
_fallbackMetadataProvider.GetFallbackMetadata(musicVideo).Match(
|
||||
metadata => ApplyMetadataUpdate(musicVideo, metadata),
|
||||
@@ -614,6 +622,45 @@ namespace ErsatzTV.Core.Metadata
|
||||
|
||||
return await _metadataRepository.Add(metadata);
|
||||
});
|
||||
|
||||
private Task<bool> ApplyMetadataUpdate(OtherVideo otherVideo, OtherVideoMetadata metadata) =>
|
||||
Optional(otherVideo.OtherVideoMetadata).Flatten().HeadOrNone().Match(
|
||||
async existing =>
|
||||
{
|
||||
existing.Title = metadata.Title;
|
||||
|
||||
if (existing.DateAdded == SystemTime.MinValueUtc)
|
||||
{
|
||||
existing.DateAdded = metadata.DateAdded;
|
||||
}
|
||||
|
||||
existing.DateUpdated = metadata.DateUpdated;
|
||||
existing.MetadataKind = metadata.MetadataKind;
|
||||
existing.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
|
||||
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
|
||||
: metadata.SortTitle;
|
||||
existing.OriginalTitle = metadata.OriginalTitle;
|
||||
|
||||
bool updated = await UpdateMetadataCollections(
|
||||
existing,
|
||||
metadata,
|
||||
(_, _) => Task.FromResult(false),
|
||||
_otherVideoRepository.AddTag,
|
||||
(_, _) => Task.FromResult(false),
|
||||
(_, _) => Task.FromResult(false));
|
||||
|
||||
return await _metadataRepository.Update(existing) || updated;
|
||||
},
|
||||
async () =>
|
||||
{
|
||||
metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
|
||||
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
|
||||
: metadata.SortTitle;
|
||||
metadata.OtherVideoId = otherVideo.Id;
|
||||
otherVideo.OtherVideoMetadata = new List<OtherVideoMetadata> { metadata };
|
||||
|
||||
return await _metadataRepository.Add(metadata);
|
||||
});
|
||||
|
||||
private async Task<Option<ShowMetadata>> LoadTelevisionShowMetadata(string nfoFileName)
|
||||
{
|
||||
|
||||
@@ -39,6 +39,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
Movie m => m.MediaVersions.Head().MediaFiles.Head().Path,
|
||||
Episode e => e.MediaVersions.Head().MediaFiles.Head().Path,
|
||||
MusicVideo mv => mv.MediaVersions.Head().MediaFiles.Head().Path,
|
||||
OtherVideo ov => ov.MediaVersions.Head().MediaFiles.Head().Path,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
|
||||
};
|
||||
|
||||
@@ -82,6 +83,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
MusicVideo mv => mv.MediaVersions.Head(),
|
||||
OtherVideo ov => ov.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Errors;
|
||||
using ErsatzTV.Core.Interfaces.Images;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static LanguageExt.Prelude;
|
||||
using Unit = LanguageExt.Unit;
|
||||
|
||||
namespace ErsatzTV.Core.Metadata
|
||||
{
|
||||
public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScanner
|
||||
{
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
private readonly ILocalMetadataProvider _localMetadataProvider;
|
||||
private readonly IMediator _mediator;
|
||||
private readonly ISearchIndex _searchIndex;
|
||||
private readonly ISearchRepository _searchRepository;
|
||||
private readonly IOtherVideoRepository _otherVideoRepository;
|
||||
private readonly ILibraryRepository _libraryRepository;
|
||||
private readonly ILogger<OtherVideoFolderScanner> _logger;
|
||||
|
||||
public OtherVideoFolderScanner(
|
||||
ILocalFileSystem localFileSystem,
|
||||
ILocalStatisticsProvider localStatisticsProvider,
|
||||
ILocalMetadataProvider localMetadataProvider,
|
||||
IMetadataRepository metadataRepository,
|
||||
IImageCache imageCache,
|
||||
IMediator mediator,
|
||||
ISearchIndex searchIndex,
|
||||
ISearchRepository searchRepository,
|
||||
IOtherVideoRepository otherVideoRepository,
|
||||
ILibraryRepository libraryRepository,
|
||||
ILogger<OtherVideoFolderScanner> logger) : base(
|
||||
localFileSystem,
|
||||
localStatisticsProvider,
|
||||
metadataRepository,
|
||||
imageCache,
|
||||
logger)
|
||||
{
|
||||
_localFileSystem = localFileSystem;
|
||||
_localMetadataProvider = localMetadataProvider;
|
||||
_mediator = mediator;
|
||||
_searchIndex = searchIndex;
|
||||
_searchRepository = searchRepository;
|
||||
_otherVideoRepository = otherVideoRepository;
|
||||
_libraryRepository = libraryRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> ScanFolder(
|
||||
LibraryPath libraryPath,
|
||||
string ffprobePath,
|
||||
decimal progressMin,
|
||||
decimal progressMax)
|
||||
{
|
||||
decimal progressSpread = progressMax - progressMin;
|
||||
|
||||
if (!_localFileSystem.IsLibraryPathAccessible(libraryPath))
|
||||
{
|
||||
return new MediaSourceInaccessible();
|
||||
}
|
||||
|
||||
var foldersCompleted = 0;
|
||||
|
||||
var folderQueue = new Queue<string>();
|
||||
foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path)
|
||||
.Filter(ShouldIncludeFolder)
|
||||
.OrderBy(identity))
|
||||
{
|
||||
folderQueue.Enqueue(folder);
|
||||
}
|
||||
|
||||
while (folderQueue.Count > 0)
|
||||
{
|
||||
decimal percentCompletion = (decimal)foldersCompleted / (foldersCompleted + folderQueue.Count);
|
||||
await _mediator.Publish(
|
||||
new LibraryScanProgress(libraryPath.LibraryId, progressMin + percentCompletion * progressSpread));
|
||||
|
||||
string otherVideoFolder = folderQueue.Dequeue();
|
||||
foldersCompleted++;
|
||||
|
||||
var filesForEtag = _localFileSystem.ListFiles(otherVideoFolder).ToList();
|
||||
|
||||
var allFiles = filesForEtag
|
||||
.Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f)))
|
||||
.Filter(f => !Path.GetFileName(f).StartsWith("._"))
|
||||
.ToList();
|
||||
|
||||
foreach (string subdirectory in _localFileSystem.ListSubdirectories(otherVideoFolder)
|
||||
.Filter(ShouldIncludeFolder)
|
||||
.OrderBy(identity))
|
||||
{
|
||||
folderQueue.Enqueue(subdirectory);
|
||||
}
|
||||
|
||||
string etag = FolderEtag.Calculate(otherVideoFolder, _localFileSystem);
|
||||
Option<LibraryFolder> knownFolder = libraryPath.LibraryFolders
|
||||
.Filter(f => f.Path == otherVideoFolder)
|
||||
.HeadOrNone();
|
||||
|
||||
// skip folder if etag matches
|
||||
if (!allFiles.Any() || await knownFolder.Map(f => f.Etag ?? string.Empty).IfNoneAsync(string.Empty) == etag)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"UPDATE: Etag has changed for folder {Folder}",
|
||||
otherVideoFolder);
|
||||
|
||||
foreach (string file in allFiles.OrderBy(identity))
|
||||
{
|
||||
_logger.LogDebug("Other video found at {File}", file);
|
||||
|
||||
Either<BaseError, MediaItemScanResult<OtherVideo>> maybeVideo = await _otherVideoRepository
|
||||
.GetOrAdd(libraryPath, file)
|
||||
.BindT(video => UpdateStatistics(video, ffprobePath))
|
||||
.BindT(UpdateMetadata);
|
||||
|
||||
await maybeVideo.Match(
|
||||
async result =>
|
||||
{
|
||||
if (result.IsAdded)
|
||||
{
|
||||
await _searchIndex.AddItems(_searchRepository, new List<MediaItem> { result.Item });
|
||||
}
|
||||
else if (result.IsUpdated)
|
||||
{
|
||||
await _searchIndex.UpdateItems(_searchRepository, new List<MediaItem> { result.Item });
|
||||
}
|
||||
|
||||
await _libraryRepository.SetEtag(libraryPath, knownFolder, otherVideoFolder, etag);
|
||||
},
|
||||
error =>
|
||||
{
|
||||
_logger.LogWarning("Error processing other video at {Path}: {Error}", file, error.Value);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string path in await _otherVideoRepository.FindOtherVideoPaths(libraryPath))
|
||||
{
|
||||
if (!_localFileSystem.FileExists(path))
|
||||
{
|
||||
_logger.LogInformation("Removing missing other video at {Path}", path);
|
||||
List<int> otherVideoIds = await _otherVideoRepository.DeleteByPath(libraryPath, path);
|
||||
await _searchIndex.RemoveItems(otherVideoIds);
|
||||
}
|
||||
else if (Path.GetFileName(path).StartsWith("._"))
|
||||
{
|
||||
_logger.LogInformation("Removing dot underscore file at {Path}", path);
|
||||
List<int> otherVideoIds = await _otherVideoRepository.DeleteByPath(libraryPath, path);
|
||||
await _searchIndex.RemoveItems(otherVideoIds);
|
||||
}
|
||||
}
|
||||
|
||||
_searchIndex.Commit();
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<OtherVideo>>> UpdateMetadata(
|
||||
MediaItemScanResult<OtherVideo> result)
|
||||
{
|
||||
try
|
||||
{
|
||||
OtherVideo otherVideo = result.Item;
|
||||
if (!Optional(otherVideo.OtherVideoMetadata).Flatten().Any())
|
||||
{
|
||||
otherVideo.OtherVideoMetadata ??= new List<OtherVideoMetadata>();
|
||||
|
||||
string path = otherVideo.MediaVersions.Head().MediaFiles.Head().Path;
|
||||
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", path);
|
||||
if (await _localMetadataProvider.RefreshFallbackMetadata(otherVideo))
|
||||
{
|
||||
result.IsUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ namespace ErsatzTV.Core.Scheduling
|
||||
bool rebuild = false)
|
||||
{
|
||||
var collectionKeys = playout.ProgramSchedule.Items
|
||||
.Map(CollectionKeyForItem)
|
||||
.SelectMany(CollectionKeysForItem)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
@@ -118,14 +118,14 @@ namespace ErsatzTV.Core.Scheduling
|
||||
{
|
||||
bool isZero = item switch
|
||||
{
|
||||
Movie m => await m.MediaVersions.Map(v => v.Duration).HeadOrNone().IfNoneAsync(TimeSpan.Zero) ==
|
||||
TimeSpan.Zero,
|
||||
Movie m => await m.MediaVersions.Map(v => v.Duration).HeadOrNone()
|
||||
.IfNoneAsync(TimeSpan.Zero) == TimeSpan.Zero,
|
||||
Episode e => await e.MediaVersions.Map(v => v.Duration).HeadOrNone()
|
||||
.IfNoneAsync(TimeSpan.Zero) ==
|
||||
TimeSpan.Zero,
|
||||
.IfNoneAsync(TimeSpan.Zero) == TimeSpan.Zero,
|
||||
MusicVideo mv => await mv.MediaVersions.Map(v => v.Duration).HeadOrNone()
|
||||
.IfNoneAsync(TimeSpan.Zero) ==
|
||||
TimeSpan.Zero,
|
||||
.IfNoneAsync(TimeSpan.Zero) == TimeSpan.Zero,
|
||||
OtherVideo ov => await ov.MediaVersions.Map(v => v.Duration).HeadOrNone()
|
||||
.IfNoneAsync(TimeSpan.Zero) == TimeSpan.Zero,
|
||||
_ => true
|
||||
};
|
||||
|
||||
@@ -160,12 +160,14 @@ namespace ErsatzTV.Core.Scheduling
|
||||
c => c.Value.Any(
|
||||
mi => mi switch
|
||||
{
|
||||
Movie m => m.MediaVersions.HeadOrNone().Map(mv => mv.Duration).IfNone(TimeSpan.Zero) ==
|
||||
TimeSpan.Zero,
|
||||
Episode e => e.MediaVersions.HeadOrNone().Map(mv => mv.Duration).IfNone(TimeSpan.Zero) ==
|
||||
TimeSpan.Zero,
|
||||
MusicVideo mv => mv.MediaVersions.HeadOrNone().Map(v => v.Duration).IfNone(TimeSpan.Zero) ==
|
||||
TimeSpan.Zero,
|
||||
Movie m => m.MediaVersions.HeadOrNone().Map(mv => mv.Duration)
|
||||
.IfNone(TimeSpan.Zero) == TimeSpan.Zero,
|
||||
Episode e => e.MediaVersions.HeadOrNone().Map(mv => mv.Duration)
|
||||
.IfNone(TimeSpan.Zero) == TimeSpan.Zero,
|
||||
MusicVideo mv => mv.MediaVersions.HeadOrNone().Map(v => v.Duration)
|
||||
.IfNone(TimeSpan.Zero) == TimeSpan.Zero,
|
||||
OtherVideo ov => ov.MediaVersions.HeadOrNone().Map(v => v.Duration)
|
||||
.IfNone(TimeSpan.Zero) == TimeSpan.Zero,
|
||||
_ => true
|
||||
})).Map(c => c.Key);
|
||||
if (zeroDurationCollection.IsSome)
|
||||
@@ -192,9 +194,11 @@ namespace ErsatzTV.Core.Scheduling
|
||||
var collectionEnumerators = new Dictionary<CollectionKey, IMediaCollectionEnumerator>();
|
||||
foreach ((CollectionKey collectionKey, List<MediaItem> mediaItems) in collectionMediaItems)
|
||||
{
|
||||
PlaybackOrder playbackOrder = sortedScheduleItems
|
||||
.First(item => CollectionKeyForItem(item) == collectionKey)
|
||||
.PlaybackOrder;
|
||||
// use configured playback order for primary collection, shuffle for filler
|
||||
Option<ProgramScheduleItem> maybeScheduleItem = sortedScheduleItems
|
||||
.FirstOrDefault(item => CollectionKeyForItem(item) == collectionKey);
|
||||
PlaybackOrder playbackOrder = maybeScheduleItem
|
||||
.Match(item => item.PlaybackOrder, () => PlaybackOrder.Shuffle);
|
||||
IMediaCollectionEnumerator enumerator =
|
||||
await GetMediaCollectionEnumerator(playout, collectionKey, mediaItems, playbackOrder);
|
||||
collectionEnumerators.Add(collectionKey, enumerator);
|
||||
@@ -211,6 +215,15 @@ namespace ErsatzTV.Core.Scheduling
|
||||
playout.Channel.Number,
|
||||
playout.Channel.Name,
|
||||
currentTime);
|
||||
|
||||
// removing any items scheduled past the start anchor
|
||||
// this could happen if the app was closed after scheduling items
|
||||
// but before saving the anchor
|
||||
int removed = playout.Items.RemoveAll(pi => pi.StartOffset >= currentTime);
|
||||
if (removed > 0)
|
||||
{
|
||||
_logger.LogWarning("Removed {Count} schedule items beyond current start anchor", removed);
|
||||
}
|
||||
|
||||
// start with the previously-decided schedule item
|
||||
int index = sortedScheduleItems.IndexOf(startAnchor.NextScheduleItem);
|
||||
@@ -219,6 +232,7 @@ namespace ErsatzTV.Core.Scheduling
|
||||
Option<int> multipleRemaining = Optional(startAnchor.MultipleRemaining);
|
||||
Option<DateTimeOffset> durationFinish = startAnchor.DurationFinishOffset;
|
||||
bool inFlood = startAnchor.InFlood;
|
||||
bool inDurationFiller = startAnchor.InDurationFiller;
|
||||
|
||||
bool customGroup = multipleRemaining.IsSome || durationFinish.IsSome;
|
||||
|
||||
@@ -234,16 +248,33 @@ namespace ErsatzTV.Core.Scheduling
|
||||
currentTime,
|
||||
multipleRemaining.IsSome,
|
||||
durationFinish.IsSome,
|
||||
inFlood);
|
||||
inFlood,
|
||||
inDurationFiller);
|
||||
|
||||
Option<CollectionKey> maybeTailCollectionKey = Option<CollectionKey>.None;
|
||||
if (inDurationFiller && scheduleItem is ProgramScheduleItemDuration
|
||||
{
|
||||
TailMode: TailMode.Filler
|
||||
})
|
||||
{
|
||||
maybeTailCollectionKey = TailCollectionKeyForItem(scheduleItem);
|
||||
}
|
||||
|
||||
IMediaCollectionEnumerator enumerator = collectionEnumerators[CollectionKeyForItem(scheduleItem)];
|
||||
foreach (CollectionKey tailCollectionKey in maybeTailCollectionKey)
|
||||
{
|
||||
enumerator = collectionEnumerators[tailCollectionKey];
|
||||
}
|
||||
|
||||
await enumerator.Current.IfSomeAsync(
|
||||
mediaItem =>
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Scheduling media item: {ScheduleItemNumber} / {CollectionType} / {MediaItemId} - {MediaItemTitle} / {StartTime}",
|
||||
scheduleItem.Index,
|
||||
scheduleItem.CollectionType,
|
||||
inDurationFiller
|
||||
? (scheduleItem as ProgramScheduleItemDuration)?.TailCollectionType
|
||||
: scheduleItem.CollectionType,
|
||||
mediaItem.Id,
|
||||
DisplayTitle(mediaItem),
|
||||
itemStartTime);
|
||||
@@ -253,6 +284,7 @@ namespace ErsatzTV.Core.Scheduling
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
MusicVideo mv => mv.MediaVersions.Head(),
|
||||
OtherVideo mv => mv.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
|
||||
};
|
||||
|
||||
@@ -261,7 +293,8 @@ namespace ErsatzTV.Core.Scheduling
|
||||
MediaItemId = mediaItem.Id,
|
||||
Start = itemStartTime.UtcDateTime,
|
||||
Finish = itemStartTime.UtcDateTime + version.Duration,
|
||||
CustomGroup = customGroup
|
||||
CustomGroup = customGroup,
|
||||
IsFiller = inDurationFiller || scheduleItem.GuideMode == GuideMode.Filler
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(scheduleItem.CustomTitle))
|
||||
@@ -269,9 +302,20 @@ namespace ErsatzTV.Core.Scheduling
|
||||
playoutItem.CustomTitle = scheduleItem.CustomTitle;
|
||||
}
|
||||
|
||||
currentTime = itemStartTime + version.Duration;
|
||||
enumerator.MoveNext();
|
||||
|
||||
if (scheduleItem is ProgramScheduleItemDuration d &&
|
||||
version.Duration > d.PlayoutDuration)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Skipping playout item {Title} with duration {Duration} that is longer than schedule item duration {PlayoutDuration}",
|
||||
DisplayTitle(mediaItem),
|
||||
version.Duration,
|
||||
d.PlayoutDuration);
|
||||
return;
|
||||
}
|
||||
|
||||
currentTime = itemStartTime + version.Duration;
|
||||
playout.Items.Add(playoutItem);
|
||||
|
||||
switch (scheduleItem)
|
||||
@@ -323,6 +367,7 @@ namespace ErsatzTV.Core.Scheduling
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
MusicVideo mv => mv.MediaVersions.Head(),
|
||||
OtherVideo ov => ov.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(peekMediaItem))
|
||||
};
|
||||
|
||||
@@ -363,6 +408,7 @@ namespace ErsatzTV.Core.Scheduling
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
MusicVideo mv => mv.MediaVersions.Head(),
|
||||
OtherVideo ov => ov.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(peekMediaItem))
|
||||
};
|
||||
|
||||
@@ -383,14 +429,38 @@ namespace ErsatzTV.Core.Scheduling
|
||||
"Advancing to next schedule item after playout mode {PlayoutMode}",
|
||||
"Duration");
|
||||
index++;
|
||||
customGroup = false;
|
||||
|
||||
if (duration.OfflineTail)
|
||||
if (duration.TailMode == TailMode.Offline)
|
||||
{
|
||||
durationFinish.Do(f => currentTime = f);
|
||||
}
|
||||
|
||||
durationFinish = None;
|
||||
if (duration.TailMode != TailMode.Filler || inDurationFiller)
|
||||
{
|
||||
if (duration.TailMode != TailMode.None)
|
||||
{
|
||||
durationFinish.Do(f => currentTime = f);
|
||||
}
|
||||
|
||||
durationFinish = None;
|
||||
inDurationFiller = false;
|
||||
customGroup = false;
|
||||
}
|
||||
else if (duration.TailMode == TailMode.Filler &&
|
||||
WillFinishFillerInTime(
|
||||
scheduleItem,
|
||||
currentTime,
|
||||
durationFinish,
|
||||
collectionEnumerators))
|
||||
{
|
||||
// if we're starting filler, we don't actually need to move
|
||||
// to the next schedule item yet
|
||||
index--;
|
||||
|
||||
inDurationFiller = true;
|
||||
durationFinish.Do(
|
||||
f => playoutItem.GuideFinish = f.UtcDateTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -406,7 +476,8 @@ namespace ErsatzTV.Core.Scheduling
|
||||
playout.ProgramScheduleAnchors = BuildProgramScheduleAnchors(playout, collectionEnumerators);
|
||||
|
||||
// remove any items outside the desired range
|
||||
playout.Items.RemoveAll(old => old.FinishOffset < playoutStart || old.StartOffset > playoutFinish);
|
||||
playout.Items.RemoveAll(
|
||||
old => old.FinishOffset < playoutStart.AddHours(-4) || old.StartOffset > playoutFinish);
|
||||
|
||||
DateTimeOffset minCurrentTime = currentTime;
|
||||
if (playout.Items.Any())
|
||||
@@ -425,12 +496,62 @@ namespace ErsatzTV.Core.Scheduling
|
||||
NextStart = GetStartTimeAfter(nextScheduleItem, minCurrentTime).UtcDateTime,
|
||||
MultipleRemaining = multipleRemaining.IsSome ? multipleRemaining.ValueUnsafe() : null,
|
||||
DurationFinish = durationFinish.IsSome ? durationFinish.ValueUnsafe().UtcDateTime : null,
|
||||
InFlood = inFlood
|
||||
InFlood = inFlood,
|
||||
InDurationFiller = inDurationFiller
|
||||
};
|
||||
|
||||
return playout;
|
||||
}
|
||||
|
||||
private static bool WillFinishFillerInTime(
|
||||
ProgramScheduleItem scheduleItem,
|
||||
DateTimeOffset currentTime,
|
||||
Option<DateTimeOffset> durationFinish,
|
||||
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators)
|
||||
{
|
||||
Option<CollectionKey> maybeTailCollectionKey = Option<CollectionKey>.None;
|
||||
if (scheduleItem is ProgramScheduleItemDuration
|
||||
{
|
||||
TailMode: TailMode.Filler
|
||||
})
|
||||
{
|
||||
maybeTailCollectionKey = TailCollectionKeyForItem(scheduleItem);
|
||||
}
|
||||
|
||||
foreach (CollectionKey collectionKey in maybeTailCollectionKey)
|
||||
{
|
||||
IMediaCollectionEnumerator enumerator = collectionEnumerators[collectionKey];
|
||||
Option<int> firstId = enumerator.Current.Map(i => i.Id);
|
||||
while (true)
|
||||
{
|
||||
foreach (MediaItem peekMediaItem in enumerator.Current)
|
||||
{
|
||||
MediaVersion peekVersion = peekMediaItem switch
|
||||
{
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
MusicVideo mv => mv.MediaVersions.Head(),
|
||||
OtherVideo ov => ov.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(peekMediaItem))
|
||||
};
|
||||
|
||||
if (currentTime + peekVersion.Duration <= durationFinish.IfNone(SystemTime.MinValueUtc))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
enumerator.MoveNext();
|
||||
if (enumerator.Current.Map(i => i.Id) == firstId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static PlayoutAnchor FindStartAnchor(
|
||||
Playout playout,
|
||||
DateTimeOffset start,
|
||||
@@ -465,14 +586,16 @@ namespace ErsatzTV.Core.Scheduling
|
||||
DateTimeOffset start,
|
||||
bool inMultiple = false,
|
||||
bool inDuration = false,
|
||||
bool inFlood = false)
|
||||
bool inFlood = false,
|
||||
bool inDurationFiller = false)
|
||||
{
|
||||
switch (item.StartType)
|
||||
{
|
||||
case StartType.Fixed:
|
||||
if (item is ProgramScheduleItemMultiple && inMultiple ||
|
||||
item is ProgramScheduleItemDuration && inDuration ||
|
||||
item is ProgramScheduleItemFlood && inFlood)
|
||||
item is ProgramScheduleItemFlood && inFlood ||
|
||||
item is ProgramScheduleItemDuration && inDurationFiller)
|
||||
{
|
||||
return start;
|
||||
}
|
||||
@@ -641,11 +764,22 @@ namespace ErsatzTV.Core.Scheduling
|
||||
return mv.MusicVideoMetadata.HeadOrNone()
|
||||
.Map(mvm => $"{artistName}{mvm.Title}")
|
||||
.IfNone("[unknown music video]");
|
||||
case OtherVideo ov:
|
||||
return ov.OtherVideoMetadata.HeadOrNone().Match(
|
||||
ovm => ovm.Title ?? string.Empty,
|
||||
() => "[unknown video]");
|
||||
default:
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<CollectionKey> CollectionKeysForItem(ProgramScheduleItem item)
|
||||
{
|
||||
var result = new List<CollectionKey> { CollectionKeyForItem(item) };
|
||||
result.AddRange(TailCollectionKeyForItem(item));
|
||||
return result;
|
||||
}
|
||||
|
||||
private static CollectionKey CollectionKeyForItem(ProgramScheduleItem item) =>
|
||||
item.CollectionType switch
|
||||
{
|
||||
@@ -682,6 +816,49 @@ namespace ErsatzTV.Core.Scheduling
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(item))
|
||||
};
|
||||
|
||||
private static Option<CollectionKey> TailCollectionKeyForItem(ProgramScheduleItem item)
|
||||
{
|
||||
if (item is ProgramScheduleItemDuration { TailMode: TailMode.Filler } duration)
|
||||
{
|
||||
return duration.TailCollectionType switch
|
||||
{
|
||||
ProgramScheduleItemCollectionType.Collection => new CollectionKey
|
||||
{
|
||||
CollectionType = duration.TailCollectionType,
|
||||
CollectionId = duration.TailCollectionId
|
||||
},
|
||||
ProgramScheduleItemCollectionType.TelevisionShow => new CollectionKey
|
||||
{
|
||||
CollectionType = duration.TailCollectionType,
|
||||
MediaItemId = duration.TailMediaItemId
|
||||
},
|
||||
ProgramScheduleItemCollectionType.TelevisionSeason => new CollectionKey
|
||||
{
|
||||
CollectionType = duration.TailCollectionType,
|
||||
MediaItemId = duration.TailMediaItemId
|
||||
},
|
||||
ProgramScheduleItemCollectionType.Artist => new CollectionKey
|
||||
{
|
||||
CollectionType = duration.TailCollectionType,
|
||||
MediaItemId = duration.TailMediaItemId
|
||||
},
|
||||
ProgramScheduleItemCollectionType.MultiCollection => new CollectionKey
|
||||
{
|
||||
CollectionType = duration.TailCollectionType,
|
||||
MultiCollectionId = duration.TailMultiCollectionId
|
||||
},
|
||||
ProgramScheduleItemCollectionType.SmartCollection => new CollectionKey
|
||||
{
|
||||
CollectionType = duration.TailCollectionType,
|
||||
SmartCollectionId = duration.TailSmartCollectionId
|
||||
},
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(item))
|
||||
};
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
private class CollectionKey : Record<CollectionKey>
|
||||
{
|
||||
public ProgramScheduleItemCollectionType CollectionType { get; set; }
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Configurations
|
||||
{
|
||||
public class OtherVideoConfiguration : IEntityTypeConfiguration<OtherVideo>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<OtherVideo> builder)
|
||||
{
|
||||
builder.ToTable("OtherVideo");
|
||||
|
||||
builder.HasMany(m => m.OtherVideoMetadata)
|
||||
.WithOne(m => m.OtherVideo)
|
||||
.HasForeignKey(m => m.OtherVideoId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasMany(m => m.MediaVersions)
|
||||
.WithOne()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Configurations
|
||||
{
|
||||
public class OtherVideoMetadataConfiguration : IEntityTypeConfiguration<OtherVideoMetadata>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<OtherVideoMetadata> builder)
|
||||
{
|
||||
builder.ToTable("OtherVideoMetadata");
|
||||
|
||||
builder.HasMany(mm => mm.Artwork)
|
||||
.WithOne()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasMany(mm => mm.Tags)
|
||||
.WithOne()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
-1
@@ -6,7 +6,27 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
|
||||
{
|
||||
public class ProgramScheduleItemDurationConfiguration : IEntityTypeConfiguration<ProgramScheduleItemDuration>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ProgramScheduleItemDuration> builder) =>
|
||||
public void Configure(EntityTypeBuilder<ProgramScheduleItemDuration> builder)
|
||||
{
|
||||
builder.ToTable("ProgramScheduleDurationItem");
|
||||
|
||||
builder.HasOne(i => i.TailCollection)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.TailCollectionId)
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired(false);
|
||||
|
||||
builder.HasOne(i => i.TailMediaItem)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.TailMediaItemId)
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired(false);
|
||||
|
||||
builder.HasOne(i => i.TailMultiCollection)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.TailMultiCollectionId)
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +233,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
}
|
||||
|
||||
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork.Filter(a => !paths.Contains(a.Path)))
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork
|
||||
.Filter(a => !paths.Contains(a.Path))
|
||||
.ToList())
|
||||
{
|
||||
metadata.Artwork.Remove(artworkToRemove);
|
||||
}
|
||||
@@ -370,7 +372,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
}
|
||||
|
||||
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork.Filter(a => !paths.Contains(a.Path)))
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork
|
||||
.Filter(a => !paths.Contains(a.Path))
|
||||
.ToList())
|
||||
{
|
||||
metadata.Artwork.Remove(artworkToRemove);
|
||||
}
|
||||
@@ -526,7 +530,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
}
|
||||
|
||||
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork.Filter(a => !paths.Contains(a.Path)))
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork
|
||||
.Filter(a => !paths.Contains(a.Path))
|
||||
.ToList())
|
||||
{
|
||||
metadata.Artwork.Remove(artworkToRemove);
|
||||
}
|
||||
|
||||
@@ -250,7 +250,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
}
|
||||
|
||||
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork.Filter(a => !paths.Contains(a.Path)))
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork
|
||||
.Filter(a => !paths.Contains(a.Path))
|
||||
.ToList())
|
||||
{
|
||||
metadata.Artwork.Remove(artworkToRemove);
|
||||
}
|
||||
@@ -370,7 +372,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
}
|
||||
|
||||
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork.Filter(a => !paths.Contains(a.Path)))
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork
|
||||
.Filter(a => !paths.Contains(a.Path))
|
||||
.ToList())
|
||||
{
|
||||
metadata.Artwork.Remove(artworkToRemove);
|
||||
}
|
||||
@@ -527,7 +531,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
}
|
||||
|
||||
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork.Filter(a => !paths.Contains(a.Path)))
|
||||
foreach (Artwork artworkToRemove in metadata.Artwork
|
||||
.Filter(a => !paths.Contains(a.Path))
|
||||
.ToList())
|
||||
{
|
||||
metadata.Artwork.Remove(artworkToRemove);
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
result.AddRange(await GetEpisodeItems(dbContext, collectionId));
|
||||
result.AddRange(await GetArtistItems(dbContext, collectionId));
|
||||
result.AddRange(await GetMusicVideoItems(dbContext, collectionId));
|
||||
result.AddRange(await GetOtherVideoItems(dbContext, collectionId));
|
||||
|
||||
return result.Distinct().ToList();
|
||||
}
|
||||
@@ -79,6 +80,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
result.AddRange(await GetEpisodeItems(dbContext, collectionId));
|
||||
result.AddRange(await GetArtistItems(dbContext, collectionId));
|
||||
result.AddRange(await GetMusicVideoItems(dbContext, collectionId));
|
||||
result.AddRange(await GetOtherVideoItems(dbContext, collectionId));
|
||||
}
|
||||
|
||||
foreach (int smartCollectionId in multiCollection.SmartCollections.Map(c => c.Id))
|
||||
@@ -137,6 +139,12 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
.Map(i => i.Id)
|
||||
.ToList();
|
||||
result.AddRange(await GetEpisodeItems(dbContext, episodeIds));
|
||||
|
||||
var otherVideoIds = searchResults.Items
|
||||
.Filter(i => i.Type == SearchIndex.OtherVideoType)
|
||||
.Map(i => i.Id)
|
||||
.ToList();
|
||||
result.AddRange(await GetOtherVideoItems(dbContext, otherVideoIds));
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -410,6 +418,24 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
.Include(m => m.MediaVersions)
|
||||
.Filter(m => musicVideoIds.Contains(m.Id))
|
||||
.ToListAsync();
|
||||
|
||||
private async Task<List<OtherVideo>> GetOtherVideoItems(TvContext dbContext, int collectionId)
|
||||
{
|
||||
IEnumerable<int> ids = await _dbConnection.QueryAsync<int>(
|
||||
@"SELECT o.Id FROM CollectionItem ci
|
||||
INNER JOIN OtherVideo o ON o.Id = ci.MediaItemId
|
||||
WHERE ci.CollectionId = @CollectionId",
|
||||
new { CollectionId = collectionId });
|
||||
|
||||
return await GetOtherVideoItems(dbContext, ids);
|
||||
}
|
||||
|
||||
private static Task<List<OtherVideo>> GetOtherVideoItems(TvContext dbContext, IEnumerable<int> otherVideoIds) =>
|
||||
dbContext.OtherVideos
|
||||
.Include(m => m.OtherVideoMetadata)
|
||||
.Include(m => m.MediaVersions)
|
||||
.Filter(m => otherVideoIds.Contains(m.Id))
|
||||
.ToListAsync();
|
||||
|
||||
private async Task<List<Episode>> GetShowItems(TvContext dbContext, int collectionId)
|
||||
{
|
||||
|
||||
@@ -81,7 +81,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
|
||||
public async Task<Option<PlexMediaSource>> GetPlexByLibraryId(int plexLibraryId)
|
||||
{
|
||||
int? id = await _dbConnection.QuerySingleAsync<int?>(
|
||||
int? id = await _dbConnection.QuerySingleOrDefaultAsync<int?>(
|
||||
@"SELECT L.MediaSourceId FROM Library L
|
||||
INNER JOIN PlexLibrary PL on L.Id = PL.Id
|
||||
WHERE L.Id = @PlexLibraryId",
|
||||
@@ -113,55 +113,30 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
|
||||
public async Task Update(
|
||||
PlexMediaSource plexMediaSource,
|
||||
List<PlexConnection> sortedConnections,
|
||||
List<PlexConnection> toAdd,
|
||||
List<PlexConnection> toDelete)
|
||||
{
|
||||
await _dbConnection.ExecuteAsync(
|
||||
@"UPDATE PlexMediaSource SET
|
||||
ProductVersion = @ProductVersion,
|
||||
Platform = @Platform,
|
||||
PlatformVersion = @PlatformVersion,
|
||||
ServerName = @ServerName
|
||||
WHERE Id = @Id",
|
||||
new
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
|
||||
dbContext.Entry(plexMediaSource).State = EntityState.Modified;
|
||||
|
||||
if (toAdd.Any() || toDelete.Any())
|
||||
{
|
||||
plexMediaSource.Connections.Clear();
|
||||
await dbContext.Entry(plexMediaSource).Collection(pms => pms.Connections).LoadAsync();
|
||||
|
||||
plexMediaSource.Connections.AddRange(toAdd);
|
||||
plexMediaSource.Connections.RemoveAll(toDelete.Contains);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (PlexConnection connection in plexMediaSource.Connections)
|
||||
{
|
||||
plexMediaSource.ProductVersion,
|
||||
plexMediaSource.Platform,
|
||||
plexMediaSource.PlatformVersion,
|
||||
plexMediaSource.ServerName,
|
||||
plexMediaSource.Id
|
||||
});
|
||||
|
||||
foreach (PlexConnection add in toAdd)
|
||||
{
|
||||
await _dbConnection.ExecuteAsync(
|
||||
@"INSERT INTO PlexConnection (IsActive, Uri, PlexMediaSourceId)
|
||||
VALUES (0, @Uri, @PlexMediaSourceId)",
|
||||
new { add.Uri, PlexMediaSourceId = plexMediaSource.Id });
|
||||
dbContext.Entry(connection).State = EntityState.Modified;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (PlexConnection delete in toDelete)
|
||||
{
|
||||
await _dbConnection.ExecuteAsync(
|
||||
@"DELETE FROM PlexConnection WHERE Id = @Id",
|
||||
new { delete.Id });
|
||||
}
|
||||
|
||||
int activeCount = await _dbConnection.QuerySingleAsync<int>(
|
||||
@"SELECT COUNT(*) FROM PlexConnection WHERE IsActive = 1 AND PlexMediaSourceId = @PlexMediaSourceId",
|
||||
new { PlexMediaSourceId = plexMediaSource.Id });
|
||||
if (activeCount == 0)
|
||||
{
|
||||
Option<PlexConnection> toActivate =
|
||||
sortedConnections.FirstOrDefault(c => toDelete.All(d => d.Id != c.Id));
|
||||
|
||||
// update on uri because connections from Plex API don't have our local ids
|
||||
await toActivate.IfSomeAsync(
|
||||
async c => await _dbConnection.ExecuteAsync(
|
||||
@"UPDATE PlexConnection SET IsActive = 1 WHERE Uri = @Uri",
|
||||
new { c.Uri }));
|
||||
}
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<List<int>> UpdateLibraries(
|
||||
@@ -587,7 +562,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
|
||||
public async Task<Option<JellyfinMediaSource>> GetJellyfinByLibraryId(int jellyfinLibraryId)
|
||||
{
|
||||
int? id = await _dbConnection.QuerySingleAsync<int?>(
|
||||
int? id = await _dbConnection.QuerySingleOrDefaultAsync<int?>(
|
||||
@"SELECT L.MediaSourceId FROM Library L
|
||||
INNER JOIN JellyfinLibrary PL on L.Id = PL.Id
|
||||
WHERE L.Id = @JellyfinLibraryId",
|
||||
@@ -770,7 +745,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
|
||||
public async Task<Option<EmbyMediaSource>> GetEmbyByLibraryId(int embyLibraryId)
|
||||
{
|
||||
int? id = await _dbConnection.QuerySingleAsync<int?>(
|
||||
int? id = await _dbConnection.QuerySingleOrDefaultAsync<int?>(
|
||||
@"SELECT L.MediaSourceId FROM Library L
|
||||
INNER JOIN EmbyLibrary PL on L.Id = PL.Id
|
||||
WHERE L.Id = @EmbyLibraryId",
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Dapper;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using LanguageExt;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
{
|
||||
public class OtherVideoRepository : IOtherVideoRepository
|
||||
{
|
||||
private readonly IDbConnection _dbConnection;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public OtherVideoRepository(IDbConnection dbConnection, IDbContextFactory<TvContext> dbContextFactory)
|
||||
{
|
||||
_dbConnection = dbConnection;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, MediaItemScanResult<OtherVideo>>> GetOrAdd(
|
||||
LibraryPath libraryPath,
|
||||
string path)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
Option<OtherVideo> maybeExisting = await dbContext.OtherVideos
|
||||
.AsNoTracking()
|
||||
.Include(ov => ov.OtherVideoMetadata)
|
||||
.ThenInclude(ovm => ovm.Artwork)
|
||||
.Include(ov => ov.OtherVideoMetadata)
|
||||
.ThenInclude(ovm => ovm.Tags)
|
||||
.Include(ov => ov.LibraryPath)
|
||||
.ThenInclude(lp => lp.Library)
|
||||
.Include(ov => ov.MediaVersions)
|
||||
.ThenInclude(ov => ov.MediaFiles)
|
||||
.Include(ov => ov.MediaVersions)
|
||||
.ThenInclude(ov => ov.Streams)
|
||||
.Include(ov => ov.TraktListItems)
|
||||
.ThenInclude(tli => tli.TraktList)
|
||||
.OrderBy(i => i.MediaVersions.First().MediaFiles.First().Path)
|
||||
.SingleOrDefaultAsync(i => i.MediaVersions.First().MediaFiles.First().Path == path);
|
||||
|
||||
return await maybeExisting.Match(
|
||||
mediaItem =>
|
||||
Right<BaseError, MediaItemScanResult<OtherVideo>>(
|
||||
new MediaItemScanResult<OtherVideo>(mediaItem) { IsAdded = false }).AsTask(),
|
||||
async () => await AddOtherVideo(dbContext, libraryPath.Id, path));
|
||||
}
|
||||
|
||||
public Task<IEnumerable<string>> FindOtherVideoPaths(LibraryPath libraryPath) =>
|
||||
_dbConnection.QueryAsync<string>(
|
||||
@"SELECT MF.Path
|
||||
FROM MediaFile MF
|
||||
INNER JOIN MediaVersion MV on MF.MediaVersionId = MV.Id
|
||||
INNER JOIN OtherVideo O on MV.OtherVideoId = O.Id
|
||||
INNER JOIN MediaItem MI on O.Id = MI.Id
|
||||
WHERE MI.LibraryPathId = @LibraryPathId",
|
||||
new { LibraryPathId = libraryPath.Id });
|
||||
|
||||
public async Task<List<int>> DeleteByPath(LibraryPath libraryPath, string path)
|
||||
{
|
||||
List<int> ids = await _dbConnection.QueryAsync<int>(
|
||||
@"SELECT O.Id
|
||||
FROM OtherVideo O
|
||||
INNER JOIN MediaItem MI on O.Id = MI.Id
|
||||
INNER JOIN MediaVersion MV on O.Id = MV.OtherVideoId
|
||||
INNER JOIN MediaFile MF on MV.Id = MF.MediaVersionId
|
||||
WHERE MI.LibraryPathId = @LibraryPathId AND MF.Path = @Path",
|
||||
new { LibraryPathId = libraryPath.Id, Path = path }).Map(result => result.ToList());
|
||||
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
foreach (int otherVideoId in ids)
|
||||
{
|
||||
OtherVideo othervide = await dbContext.OtherVideos.FindAsync(otherVideoId);
|
||||
dbContext.OtherVideos.Remove(othervide);
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
public Task<bool> AddTag(OtherVideoMetadata metadata, Tag tag) =>
|
||||
_dbConnection.ExecuteAsync(
|
||||
"INSERT INTO Tag (Name, OtherVideoMetadataId) VALUES (@Name, @MetadataId)",
|
||||
new { tag.Name, MetadataId = metadata.Id }).Map(result => result > 0);
|
||||
|
||||
public async Task<List<OtherVideoMetadata>> GetOtherVideosForCards(List<int> ids)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
return await dbContext.OtherVideoMetadata
|
||||
.AsNoTracking()
|
||||
.Filter(ovm => ids.Contains(ovm.OtherVideoId))
|
||||
.Include(ovm => ovm.OtherVideo)
|
||||
.Include(ovm => ovm.Artwork)
|
||||
.OrderBy(ovm => ovm.SortTitle)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
private static async Task<Either<BaseError, MediaItemScanResult<OtherVideo>>> AddOtherVideo(
|
||||
TvContext dbContext,
|
||||
int libraryPathId,
|
||||
string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
var otherVideo = new OtherVideo
|
||||
{
|
||||
LibraryPathId = libraryPathId,
|
||||
MediaVersions = new List<MediaVersion>
|
||||
{
|
||||
new()
|
||||
{
|
||||
MediaFiles = new List<MediaFile>
|
||||
{
|
||||
new() { Path = path }
|
||||
},
|
||||
Streams = new List<MediaStream>()
|
||||
}
|
||||
},
|
||||
TraktListItems = new List<TraktListItem>()
|
||||
};
|
||||
|
||||
await dbContext.OtherVideos.AddAsync(otherVideo);
|
||||
await dbContext.SaveChangesAsync();
|
||||
await dbContext.Entry(otherVideo).Reference(m => m.LibraryPath).LoadAsync();
|
||||
await dbContext.Entry(otherVideo.LibraryPath).Reference(lp => lp.Library).LoadAsync();
|
||||
return new MediaItemScanResult<OtherVideo>(otherVideo) { IsAdded = true };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BaseError.New(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,6 +97,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
.ThenInclude(mm => mm.Styles)
|
||||
.Include(mi => (mi as Artist).ArtistMetadata)
|
||||
.ThenInclude(mm => mm.Moods)
|
||||
.Include(mi => (mi as OtherVideo).OtherVideoMetadata)
|
||||
.ThenInclude(mm => mm.Tags)
|
||||
.Include(mi => (mi as OtherVideo).MediaVersions)
|
||||
.ThenInclude(mm => mm.Streams)
|
||||
.Include(mi => mi.TraktListItems)
|
||||
.ThenInclude(tli => tli.TraktList)
|
||||
.OrderBy(mi => mi.Id)
|
||||
|
||||
@@ -39,6 +39,8 @@ namespace ErsatzTV.Infrastructure.Data
|
||||
public DbSet<ArtistMetadata> ArtistMetadata { get; set; }
|
||||
public DbSet<MusicVideo> MusicVideos { get; set; }
|
||||
public DbSet<MusicVideoMetadata> MusicVideoMetadata { get; set; }
|
||||
public DbSet<OtherVideo> OtherVideos { get; set; }
|
||||
public DbSet<OtherVideoMetadata> OtherVideoMetadata { get; set; }
|
||||
public DbSet<Show> Shows { get; set; }
|
||||
public DbSet<ShowMetadata> ShowMetadata { get; set; }
|
||||
public DbSet<Season> Seasons { get; set; }
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00014" />
|
||||
<PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00014" />
|
||||
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00014" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.10">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.11" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.11">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.11" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.63">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
+3343
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,158 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Add_DurationTail : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "OfflineTail",
|
||||
table: "ProgramScheduleDurationItem",
|
||||
newName: "TailMode");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "TailCollectionId",
|
||||
table: "ProgramScheduleDurationItem",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "TailCollectionType",
|
||||
table: "ProgramScheduleDurationItem",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "TailMediaItemId",
|
||||
table: "ProgramScheduleDurationItem",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "TailMultiCollectionId",
|
||||
table: "ProgramScheduleDurationItem",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "TailSmartCollectionId",
|
||||
table: "ProgramScheduleDurationItem",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ProgramScheduleDurationItem_TailCollectionId",
|
||||
table: "ProgramScheduleDurationItem",
|
||||
column: "TailCollectionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ProgramScheduleDurationItem_TailMediaItemId",
|
||||
table: "ProgramScheduleDurationItem",
|
||||
column: "TailMediaItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ProgramScheduleDurationItem_TailMultiCollectionId",
|
||||
table: "ProgramScheduleDurationItem",
|
||||
column: "TailMultiCollectionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ProgramScheduleDurationItem_TailSmartCollectionId",
|
||||
table: "ProgramScheduleDurationItem",
|
||||
column: "TailSmartCollectionId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_ProgramScheduleDurationItem_Collection_TailCollectionId",
|
||||
table: "ProgramScheduleDurationItem",
|
||||
column: "TailCollectionId",
|
||||
principalTable: "Collection",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_ProgramScheduleDurationItem_MediaItem_TailMediaItemId",
|
||||
table: "ProgramScheduleDurationItem",
|
||||
column: "TailMediaItemId",
|
||||
principalTable: "MediaItem",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_ProgramScheduleDurationItem_MultiCollection_TailMultiCollectionId",
|
||||
table: "ProgramScheduleDurationItem",
|
||||
column: "TailMultiCollectionId",
|
||||
principalTable: "MultiCollection",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_ProgramScheduleDurationItem_SmartCollection_TailSmartCollectionId",
|
||||
table: "ProgramScheduleDurationItem",
|
||||
column: "TailSmartCollectionId",
|
||||
principalTable: "SmartCollection",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_ProgramScheduleDurationItem_Collection_TailCollectionId",
|
||||
table: "ProgramScheduleDurationItem");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_ProgramScheduleDurationItem_MediaItem_TailMediaItemId",
|
||||
table: "ProgramScheduleDurationItem");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_ProgramScheduleDurationItem_MultiCollection_TailMultiCollectionId",
|
||||
table: "ProgramScheduleDurationItem");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_ProgramScheduleDurationItem_SmartCollection_TailSmartCollectionId",
|
||||
table: "ProgramScheduleDurationItem");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ProgramScheduleDurationItem_TailCollectionId",
|
||||
table: "ProgramScheduleDurationItem");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ProgramScheduleDurationItem_TailMediaItemId",
|
||||
table: "ProgramScheduleDurationItem");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ProgramScheduleDurationItem_TailMultiCollectionId",
|
||||
table: "ProgramScheduleDurationItem");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ProgramScheduleDurationItem_TailSmartCollectionId",
|
||||
table: "ProgramScheduleDurationItem");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TailCollectionId",
|
||||
table: "ProgramScheduleDurationItem");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TailCollectionType",
|
||||
table: "ProgramScheduleDurationItem");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TailMediaItemId",
|
||||
table: "ProgramScheduleDurationItem");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TailMultiCollectionId",
|
||||
table: "ProgramScheduleDurationItem");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TailSmartCollectionId",
|
||||
table: "ProgramScheduleDurationItem");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "TailMode",
|
||||
table: "ProgramScheduleDurationItem",
|
||||
newName: "OfflineTail");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+3346
File diff suppressed because it is too large
Load Diff
+23
@@ -0,0 +1,23 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Add_PlayoutAnchor_InDurationFiller : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "Anchor_InDurationFiller",
|
||||
table: "Playout",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Anchor_InDurationFiller",
|
||||
table: "Playout");
|
||||
}
|
||||
}
|
||||
}
|
||||
+3349
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Add_PlayoutItem_IsFiller : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsFiller",
|
||||
table: "PlayoutItem",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsFiller",
|
||||
table: "PlayoutItem");
|
||||
}
|
||||
}
|
||||
}
|
||||
+3352
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Add_PlayoutItem_GuideFinish : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "GuideFinish",
|
||||
table: "PlayoutItem",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "GuideFinish",
|
||||
table: "PlayoutItem");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+3352
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Add_LocalLibrary_OtherVideos : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// create local other videos library
|
||||
migrationBuilder.Sql(
|
||||
@"INSERT INTO Library (Name, MediaKind, MediaSourceId)
|
||||
SELECT 'Other Videos', 4, Id FROM
|
||||
(SELECT LMS.Id FROM LocalMediaSource LMS LIMIT 1)");
|
||||
migrationBuilder.Sql("INSERT INTO LocalLibrary (Id) Values (last_insert_rowid())");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+3507
File diff suppressed because it is too large
Load Diff
+287
@@ -0,0 +1,287 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Add_OtherVideo_OtherVideoMetadata : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "OtherVideoMetadataId",
|
||||
table: "Tag",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "OtherVideoMetadataId",
|
||||
table: "Studio",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "OtherVideoMetadataId",
|
||||
table: "MetadataGuid",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "OtherVideoId",
|
||||
table: "MediaVersion",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "OtherVideoMetadataId",
|
||||
table: "Genre",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "OtherVideoMetadataId",
|
||||
table: "Artwork",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "OtherVideoMetadataId",
|
||||
table: "Actor",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "OtherVideo",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OtherVideo", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_OtherVideo_MediaItem_Id",
|
||||
column: x => x.Id,
|
||||
principalTable: "MediaItem",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "OtherVideoMetadata",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
OtherVideoId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
MetadataKind = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Title = table.Column<string>(type: "TEXT", nullable: true),
|
||||
OriginalTitle = table.Column<string>(type: "TEXT", nullable: true),
|
||||
SortTitle = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Year = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
ReleaseDate = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
DateUpdated = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OtherVideoMetadata", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_OtherVideoMetadata_OtherVideo_OtherVideoId",
|
||||
column: x => x.OtherVideoId,
|
||||
principalTable: "OtherVideo",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Tag_OtherVideoMetadataId",
|
||||
table: "Tag",
|
||||
column: "OtherVideoMetadataId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Studio_OtherVideoMetadataId",
|
||||
table: "Studio",
|
||||
column: "OtherVideoMetadataId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MetadataGuid_OtherVideoMetadataId",
|
||||
table: "MetadataGuid",
|
||||
column: "OtherVideoMetadataId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MediaVersion_OtherVideoId",
|
||||
table: "MediaVersion",
|
||||
column: "OtherVideoId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Genre_OtherVideoMetadataId",
|
||||
table: "Genre",
|
||||
column: "OtherVideoMetadataId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Artwork_OtherVideoMetadataId",
|
||||
table: "Artwork",
|
||||
column: "OtherVideoMetadataId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Actor_OtherVideoMetadataId",
|
||||
table: "Actor",
|
||||
column: "OtherVideoMetadataId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OtherVideoMetadata_OtherVideoId",
|
||||
table: "OtherVideoMetadata",
|
||||
column: "OtherVideoId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Actor_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "Actor",
|
||||
column: "OtherVideoMetadataId",
|
||||
principalTable: "OtherVideoMetadata",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Artwork_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "Artwork",
|
||||
column: "OtherVideoMetadataId",
|
||||
principalTable: "OtherVideoMetadata",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Genre_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "Genre",
|
||||
column: "OtherVideoMetadataId",
|
||||
principalTable: "OtherVideoMetadata",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_MediaVersion_OtherVideo_OtherVideoId",
|
||||
table: "MediaVersion",
|
||||
column: "OtherVideoId",
|
||||
principalTable: "OtherVideo",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_MetadataGuid_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "MetadataGuid",
|
||||
column: "OtherVideoMetadataId",
|
||||
principalTable: "OtherVideoMetadata",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Studio_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "Studio",
|
||||
column: "OtherVideoMetadataId",
|
||||
principalTable: "OtherVideoMetadata",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Tag_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "Tag",
|
||||
column: "OtherVideoMetadataId",
|
||||
principalTable: "OtherVideoMetadata",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Actor_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "Actor");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Artwork_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "Artwork");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Genre_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "Genre");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_MediaVersion_OtherVideo_OtherVideoId",
|
||||
table: "MediaVersion");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_MetadataGuid_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "MetadataGuid");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Studio_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "Studio");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Tag_OtherVideoMetadata_OtherVideoMetadataId",
|
||||
table: "Tag");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "OtherVideoMetadata");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "OtherVideo");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Tag_OtherVideoMetadataId",
|
||||
table: "Tag");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Studio_OtherVideoMetadataId",
|
||||
table: "Studio");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_MetadataGuid_OtherVideoMetadataId",
|
||||
table: "MetadataGuid");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_MediaVersion_OtherVideoId",
|
||||
table: "MediaVersion");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Genre_OtherVideoMetadataId",
|
||||
table: "Genre");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Artwork_OtherVideoMetadataId",
|
||||
table: "Artwork");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Actor_OtherVideoMetadataId",
|
||||
table: "Actor");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OtherVideoMetadataId",
|
||||
table: "Tag");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OtherVideoMetadataId",
|
||||
table: "Studio");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OtherVideoMetadataId",
|
||||
table: "MetadataGuid");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OtherVideoId",
|
||||
table: "MediaVersion");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OtherVideoMetadataId",
|
||||
table: "Genre");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OtherVideoMetadataId",
|
||||
table: "Artwork");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OtherVideoMetadataId",
|
||||
table: "Actor");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+3510
File diff suppressed because it is too large
Load Diff
+24
@@ -0,0 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Add_ProgramScheduleItem_GuideMode : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "GuideMode",
|
||||
table: "ProgramScheduleItem",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "GuideMode",
|
||||
table: "ProgramScheduleItem");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "5.0.10");
|
||||
.HasAnnotation("ProductVersion", "5.0.11");
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b =>
|
||||
{
|
||||
@@ -43,6 +43,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<int?>("Order")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("OtherVideoMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Role")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -65,6 +68,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
|
||||
b.HasIndex("MusicVideoMetadataId");
|
||||
|
||||
b.HasIndex("OtherVideoMetadataId");
|
||||
|
||||
b.HasIndex("SeasonMetadataId");
|
||||
|
||||
b.HasIndex("ShowMetadataId");
|
||||
@@ -151,6 +156,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<int?>("MusicVideoMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("OtherVideoMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -172,6 +180,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
|
||||
b.HasIndex("MusicVideoMetadataId");
|
||||
|
||||
b.HasIndex("OtherVideoMetadataId");
|
||||
|
||||
b.HasIndex("SeasonMetadataId");
|
||||
|
||||
b.HasIndex("ShowMetadataId");
|
||||
@@ -525,6 +535,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("OtherVideoMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("SeasonMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -541,6 +554,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
|
||||
b.HasIndex("MusicVideoMetadataId");
|
||||
|
||||
b.HasIndex("OtherVideoMetadataId");
|
||||
|
||||
b.HasIndex("SeasonMetadataId");
|
||||
|
||||
b.HasIndex("ShowMetadataId");
|
||||
@@ -815,6 +830,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("OtherVideoId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("RFrameRate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -835,6 +853,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
|
||||
b.HasIndex("MusicVideoId");
|
||||
|
||||
b.HasIndex("OtherVideoId");
|
||||
|
||||
b.ToTable("MediaVersion");
|
||||
});
|
||||
|
||||
@@ -859,6 +879,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<int?>("MusicVideoMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("OtherVideoMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("SeasonMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -875,6 +898,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
|
||||
b.HasIndex("MusicVideoMetadataId");
|
||||
|
||||
b.HasIndex("OtherVideoMetadataId");
|
||||
|
||||
b.HasIndex("SeasonMetadataId");
|
||||
|
||||
b.HasIndex("ShowMetadataId");
|
||||
@@ -1055,6 +1080,46 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.ToTable("MusicVideoMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideoMetadata", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateUpdated")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("MetadataKind")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("OriginalTitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("OtherVideoId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("ReleaseDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SortTitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("Year")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OtherVideoId");
|
||||
|
||||
b.ToTable("OtherVideoMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -1097,6 +1162,12 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<DateTime>("Finish")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("GuideFinish")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsFiller")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MediaItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -1241,6 +1312,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<string>("CustomTitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("GuideMode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Index")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -1430,6 +1504,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("OtherVideoMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("SeasonMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -1446,6 +1523,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
|
||||
b.HasIndex("MusicVideoMetadataId");
|
||||
|
||||
b.HasIndex("OtherVideoMetadataId");
|
||||
|
||||
b.HasIndex("SeasonMetadataId");
|
||||
|
||||
b.HasIndex("ShowMetadataId");
|
||||
@@ -1493,6 +1572,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("OtherVideoMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("SeasonMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -1509,6 +1591,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
|
||||
b.HasIndex("MusicVideoMetadataId");
|
||||
|
||||
b.HasIndex("OtherVideoMetadataId");
|
||||
|
||||
b.HasIndex("SeasonMetadataId");
|
||||
|
||||
b.HasIndex("ShowMetadataId");
|
||||
@@ -1727,6 +1811,13 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.ToTable("MusicVideo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideo", b =>
|
||||
{
|
||||
b.HasBaseType("ErsatzTV.Core.Domain.MediaItem");
|
||||
|
||||
b.ToTable("OtherVideo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b =>
|
||||
{
|
||||
b.HasBaseType("ErsatzTV.Core.Domain.MediaItem");
|
||||
@@ -1808,12 +1899,35 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem");
|
||||
|
||||
b.Property<bool>("OfflineTail")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<TimeSpan>("PlayoutDuration")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("TailCollectionId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("TailCollectionType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("TailMediaItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("TailMode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("TailMultiCollectionId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("TailSmartCollectionId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasIndex("TailCollectionId");
|
||||
|
||||
b.HasIndex("TailMediaItemId");
|
||||
|
||||
b.HasIndex("TailMultiCollectionId");
|
||||
|
||||
b.HasIndex("TailSmartCollectionId");
|
||||
|
||||
b.ToTable("ProgramScheduleDurationItem");
|
||||
});
|
||||
|
||||
@@ -2010,6 +2124,10 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.WithMany("Actors")
|
||||
.HasForeignKey("MusicVideoMetadataId");
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null)
|
||||
.WithMany("Actors")
|
||||
.HasForeignKey("OtherVideoMetadataId");
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null)
|
||||
.WithMany("Actors")
|
||||
.HasForeignKey("SeasonMetadataId");
|
||||
@@ -2060,6 +2178,11 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.HasForeignKey("MusicVideoMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null)
|
||||
.WithMany("Artwork")
|
||||
.HasForeignKey("OtherVideoMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null)
|
||||
.WithMany("Artwork")
|
||||
.HasForeignKey("SeasonMetadataId")
|
||||
@@ -2185,6 +2308,10 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.HasForeignKey("MusicVideoMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null)
|
||||
.WithMany("Genres")
|
||||
.HasForeignKey("OtherVideoMetadataId");
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null)
|
||||
.WithMany("Genres")
|
||||
.HasForeignKey("SeasonMetadataId");
|
||||
@@ -2299,6 +2426,11 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.WithMany("MediaVersions")
|
||||
.HasForeignKey("MusicVideoId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.OtherVideo", null)
|
||||
.WithMany("MediaVersions")
|
||||
.HasForeignKey("OtherVideoId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.MetadataGuid", b =>
|
||||
@@ -2321,6 +2453,10 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.WithMany("Guids")
|
||||
.HasForeignKey("MusicVideoMetadataId");
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null)
|
||||
.WithMany("Guids")
|
||||
.HasForeignKey("OtherVideoMetadataId");
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null)
|
||||
.WithMany("Guids")
|
||||
.HasForeignKey("SeasonMetadataId")
|
||||
@@ -2400,6 +2536,17 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Navigation("MusicVideo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideoMetadata", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.OtherVideo", "OtherVideo")
|
||||
.WithMany("OtherVideoMetadata")
|
||||
.HasForeignKey("OtherVideoId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("OtherVideo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.Channel", "Channel")
|
||||
@@ -2422,6 +2569,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b1.Property<DateTime?>("DurationFinish")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<bool>("InDurationFiller")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<bool>("InFlood")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -2647,6 +2797,10 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.HasForeignKey("MusicVideoMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null)
|
||||
.WithMany("Studios")
|
||||
.HasForeignKey("OtherVideoMetadataId");
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null)
|
||||
.WithMany("Studios")
|
||||
.HasForeignKey("SeasonMetadataId");
|
||||
@@ -2685,6 +2839,11 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.HasForeignKey("MusicVideoMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null)
|
||||
.WithMany("Tags")
|
||||
.HasForeignKey("OtherVideoMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null)
|
||||
.WithMany("Tags")
|
||||
.HasForeignKey("SeasonMetadataId");
|
||||
@@ -2834,6 +2993,15 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Navigation("Artist");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideo", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.MediaItem", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("ErsatzTV.Core.Domain.OtherVideo", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.MediaItem", null)
|
||||
@@ -2903,6 +3071,33 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.Collection", "TailCollection")
|
||||
.WithMany()
|
||||
.HasForeignKey("TailCollectionId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.MediaItem", "TailMediaItem")
|
||||
.WithMany()
|
||||
.HasForeignKey("TailMediaItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.MultiCollection", "TailMultiCollection")
|
||||
.WithMany()
|
||||
.HasForeignKey("TailMultiCollectionId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "TailSmartCollection")
|
||||
.WithMany()
|
||||
.HasForeignKey("TailSmartCollectionId");
|
||||
|
||||
b.Navigation("TailCollection");
|
||||
|
||||
b.Navigation("TailMediaItem");
|
||||
|
||||
b.Navigation("TailMultiCollection");
|
||||
|
||||
b.Navigation("TailSmartCollection");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b =>
|
||||
@@ -3164,6 +3359,21 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Navigation("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideoMetadata", b =>
|
||||
{
|
||||
b.Navigation("Actors");
|
||||
|
||||
b.Navigation("Artwork");
|
||||
|
||||
b.Navigation("Genres");
|
||||
|
||||
b.Navigation("Guids");
|
||||
|
||||
b.Navigation("Studios");
|
||||
|
||||
b.Navigation("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b =>
|
||||
{
|
||||
b.Navigation("Items");
|
||||
@@ -3251,6 +3461,13 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Navigation("MusicVideoMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideo", b =>
|
||||
{
|
||||
b.Navigation("MediaVersions");
|
||||
|
||||
b.Navigation("OtherVideoMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b =>
|
||||
{
|
||||
b.Navigation("Episodes");
|
||||
|
||||
@@ -6,6 +6,12 @@ namespace ErsatzTV.Infrastructure.Plex
|
||||
{
|
||||
public interface IPlexServerApi
|
||||
{
|
||||
[Get("/")]
|
||||
[Headers("Accept: application/json")]
|
||||
public Task Ping(
|
||||
[Query] [AliasAs("X-Plex-Token")]
|
||||
string token);
|
||||
|
||||
[Get("/library/sections")]
|
||||
[Headers("Accept: application/json")]
|
||||
public Task<PlexMediaContainerResponse<PlexMediaContainerDirectoryContent<PlexLibraryResponse>>> GetLibraries(
|
||||
|
||||
@@ -30,6 +30,28 @@ namespace ErsatzTV.Infrastructure.Plex
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<bool> Ping(
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
IPlexServerApi service = RestService.For<IPlexServerApi>(
|
||||
new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(connection.Uri),
|
||||
Timeout = TimeSpan.FromSeconds(5)
|
||||
});
|
||||
|
||||
await service.Ping(token.AuthToken);
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, List<PlexLibrary>>> GetLibraries(
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token)
|
||||
|
||||
@@ -60,6 +60,7 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
public const string ArtistType = "artist";
|
||||
public const string MusicVideoType = "music_video";
|
||||
public const string EpisodeType = "episode";
|
||||
public const string OtherVideoType = "other_video";
|
||||
private readonly List<CultureInfo> _cultureInfos;
|
||||
|
||||
private readonly ILogger<SearchIndex> _logger;
|
||||
@@ -121,6 +122,9 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
case Episode episode:
|
||||
await UpdateEpisode(searchRepository, episode);
|
||||
break;
|
||||
case OtherVideo otherVideo:
|
||||
await UpdateOtherVideo(searchRepository, otherVideo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,6 +222,9 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
case Episode episode:
|
||||
await UpdateEpisode(searchRepository, episode);
|
||||
break;
|
||||
case OtherVideo otherVideo:
|
||||
await UpdateOtherVideo(searchRepository, otherVideo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -747,6 +754,44 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateOtherVideo(ISearchRepository searchRepository, OtherVideo otherVideo)
|
||||
{
|
||||
Option<OtherVideoMetadata> maybeMetadata = otherVideo.OtherVideoMetadata.HeadOrNone();
|
||||
if (maybeMetadata.IsSome)
|
||||
{
|
||||
OtherVideoMetadata metadata = maybeMetadata.ValueUnsafe();
|
||||
|
||||
try
|
||||
{
|
||||
var doc = new Document
|
||||
{
|
||||
new StringField(IdField, otherVideo.Id.ToString(), Field.Store.YES),
|
||||
new StringField(TypeField, OtherVideoType, Field.Store.YES),
|
||||
new TextField(TitleField, metadata.Title, Field.Store.NO),
|
||||
new StringField(SortTitleField, metadata.SortTitle.ToLowerInvariant(), Field.Store.NO),
|
||||
new TextField(LibraryNameField, otherVideo.LibraryPath.Library.Name, Field.Store.NO),
|
||||
new StringField(LibraryIdField, otherVideo.LibraryPath.Library.Id.ToString(), Field.Store.NO),
|
||||
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO),
|
||||
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES)
|
||||
};
|
||||
|
||||
await AddLanguages(searchRepository, doc, otherVideo.MediaVersions);
|
||||
|
||||
foreach (Tag tag in metadata.Tags)
|
||||
{
|
||||
doc.Add(new TextField(TagField, tag.Name, Field.Store.NO));
|
||||
}
|
||||
|
||||
_writer.UpdateDocument(new Term(IdField, otherVideo.Id.ToString()), doc);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
metadata.OtherVideo = null;
|
||||
_logger.LogWarning(ex, "Error indexing other video with metadata {@Metadata}", metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SearchItem ProjectToSearchItem(Document doc) => new(
|
||||
doc.Get(TypeField),
|
||||
@@ -773,6 +818,7 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
EpisodeMetadata em =>
|
||||
$"{em.Title}_{em.Year}_{em.Episode.Season.SeasonNumber}_{em.EpisodeNumber}"
|
||||
.ToLowerInvariant(),
|
||||
OtherVideoMetadata ovm => $"{ovm.OriginalTitle}".ToLowerInvariant(),
|
||||
_ => $"{metadata.Title}_{metadata.Year}".ToLowerInvariant()
|
||||
};
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
<PackageReference Include="Markdig" Version="0.26.0" />
|
||||
<PackageReference Include="MediatR.Courier.DependencyInjection" Version="3.0.1" />
|
||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.10">
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.11" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.11">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -213,7 +213,7 @@
|
||||
DialogResult result = await dialog.Result;
|
||||
if (!result.Cancelled && result.Data is ProgramScheduleViewModel schedule)
|
||||
{
|
||||
await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.Artist, null, null, null, ArtistId, PlaybackOrder.Shuffle, null, null, null, null));
|
||||
await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.Artist, null, null, null, ArtistId, PlaybackOrder.Shuffle, null, null, TailMode.None, ProgramScheduleItemCollectionType.Collection, null, null, null, null, null, GuideMode.Normal));
|
||||
_navigationManager.NavigateTo($"/schedules/{schedule.Id}/items");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,10 @@
|
||||
{
|
||||
<MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#music_videos")">@_data.MusicVideoCards.Count Music Videos</MudLink>
|
||||
}
|
||||
@if (_data.OtherVideoCards.Any())
|
||||
{
|
||||
<MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#other_videos")">@_data.OtherVideoCards.Count Other Videos</MudLink>
|
||||
}
|
||||
@if (SupportsCustomOrdering())
|
||||
{
|
||||
<div style="margin-left: auto">
|
||||
@@ -220,6 +224,30 @@
|
||||
}
|
||||
</MudContainer>
|
||||
}
|
||||
|
||||
@if (_data.OtherVideoCards.Any())
|
||||
{
|
||||
<MudText GutterBottom="true"
|
||||
Typo="Typo.h4"
|
||||
Style="scroll-margin-top: 160px"
|
||||
UserAttributes="@(new Dictionary<string, object> { { "id", "other_videos" } })">
|
||||
Other Videos
|
||||
</MudText>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
|
||||
@foreach (OtherVideoCardViewModel card in _data.OtherVideoCards.OrderBy(e => e.SortTitle))
|
||||
{
|
||||
<MediaCard Data="@card"
|
||||
Link=""
|
||||
ArtworkKind="ArtworkKind.Thumbnail"
|
||||
DeleteClicked="@RemoveOtherVideoFromCollection"
|
||||
SelectColor="@Color.Error"
|
||||
SelectClicked="@(e => SelectClicked(card, e))"
|
||||
IsSelected="@IsSelected(card)"
|
||||
IsSelectMode="@IsSelectMode()"/>
|
||||
}
|
||||
</MudContainer>
|
||||
}
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
@@ -278,6 +306,7 @@
|
||||
.Append(_data.EpisodeCards.OrderBy(ep => ep.Aired))
|
||||
.Append(_data.ArtistCards.OrderBy(a => a.SortTitle))
|
||||
.Append(_data.MusicVideoCards.OrderBy(mv => mv.SortTitle))
|
||||
.Append(_data.OtherVideoCards.OrderBy(ov => ov.SortTitle))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
@@ -361,6 +390,19 @@
|
||||
|
||||
await RemoveItemsWithConfirmation("episode", $"{episode.ShowTitle} - {episode.Title}", request);
|
||||
}
|
||||
|
||||
private async Task RemoveOtherVideoFromCollection(MediaCardViewModel vm)
|
||||
{
|
||||
if (vm is OtherVideoCardViewModel otherVideo)
|
||||
{
|
||||
var request = new RemoveItemsFromCollection(Id)
|
||||
{
|
||||
MediaItemIds = new List<int> { otherVideo.OtherVideoId }
|
||||
};
|
||||
|
||||
await RemoveItemsWithConfirmation("other video", $"{otherVideo.Title}", request);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveItemsWithConfirmation(
|
||||
string entityType,
|
||||
|
||||
@@ -98,7 +98,8 @@ namespace ErsatzTV.Pages
|
||||
_selectedItems.OfType<TelevisionSeasonCardViewModel>().Map(s => s.TelevisionSeasonId).ToList(),
|
||||
_selectedItems.OfType<TelevisionEpisodeCardViewModel>().Map(e => e.EpisodeId).ToList(),
|
||||
_selectedItems.OfType<ArtistCardViewModel>().Map(a => a.ArtistId).ToList(),
|
||||
_selectedItems.OfType<MusicVideoCardViewModel>().Map(mv => mv.MusicVideoId).ToList());
|
||||
_selectedItems.OfType<MusicVideoCardViewModel>().Map(mv => mv.MusicVideoId).ToList(),
|
||||
_selectedItems.OfType<OtherVideoCardViewModel>().Map(ov => ov.OtherVideoId).ToList());
|
||||
|
||||
protected async Task AddItemsToCollection(
|
||||
List<int> movieIds,
|
||||
@@ -107,10 +108,11 @@ namespace ErsatzTV.Pages
|
||||
List<int> episodeIds,
|
||||
List<int> artistIds,
|
||||
List<int> musicVideoIds,
|
||||
List<int> otherVideoIds,
|
||||
string entityName = "selected items")
|
||||
{
|
||||
int count = movieIds.Count + showIds.Count + seasonIds.Count + episodeIds.Count + artistIds.Count +
|
||||
musicVideoIds.Count;
|
||||
musicVideoIds.Count + otherVideoIds.Count;
|
||||
|
||||
var parameters = new DialogParameters
|
||||
{ { "EntityType", count.ToString() }, { "EntityName", entityName } };
|
||||
@@ -127,7 +129,8 @@ namespace ErsatzTV.Pages
|
||||
seasonIds,
|
||||
episodeIds,
|
||||
artistIds,
|
||||
musicVideoIds);
|
||||
musicVideoIds,
|
||||
otherVideoIds);
|
||||
|
||||
Either<BaseError, Unit> addResult = await Mediator.Send(request);
|
||||
addResult.Match(
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
@page "/media/other/videos"
|
||||
@page "/media/other/videos/page/{PageNumber:int}"
|
||||
@using LanguageExt.UnsafeValueAccess
|
||||
@using ErsatzTV.Application.MediaCards
|
||||
@using ErsatzTV.Application.MediaCollections
|
||||
@using ErsatzTV.Application.MediaCollections.Commands
|
||||
@using ErsatzTV.Application.Search.Queries
|
||||
@using ErsatzTV.Extensions
|
||||
@using Unit = LanguageExt.Unit
|
||||
@inherits MultiSelectBase<OtherVideoList>
|
||||
@inject NavigationManager _navigationManager
|
||||
@inject ChannelWriter<IBackgroundServiceRequest> _channel
|
||||
|
||||
<MudPaper Square="true" Style="display: flex; height: 64px; left: 240px; padding: 0; position: fixed; right: 0; z-index: 100;">
|
||||
<div style="display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%" class="ml-6 mr-6">
|
||||
@if (IsSelectMode())
|
||||
{
|
||||
<MudText Typo="Typo.h6" Color="Color.Primary">@SelectionLabel()</MudText>
|
||||
<div style="margin-left: auto">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="@(_ => AddSelectionToCollection())">
|
||||
Add To Collection
|
||||
</MudButton>
|
||||
<MudButton Class="ml-3"
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Secondary"
|
||||
StartIcon="@Icons.Material.Filled.Check"
|
||||
OnClick="@(_ => ClearSelection())">
|
||||
Clear Selection
|
||||
</MudButton>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Style="margin-bottom: auto; margin-top: auto; width: 33%">@_query</MudText>
|
||||
<div style="max-width: 300px; width: 33%;">
|
||||
<MudPaper Style="align-items: center; display: flex; justify-content: center;">
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.ChevronLeft"
|
||||
OnClick="@PrevPage"
|
||||
Disabled="@(PageNumber <= 1)">
|
||||
</MudIconButton>
|
||||
<MudText Style="flex-grow: 1"
|
||||
Align="Align.Center">
|
||||
@Math.Min((PageNumber - 1) * PageSize + 1, _data.Count)-@Math.Min(_data.Count, PageNumber * PageSize) of @_data.Count
|
||||
</MudText>
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.ChevronRight"
|
||||
OnClick="@NextPage" Disabled="@(PageNumber * PageSize >= _data.Count)">
|
||||
</MudIconButton>
|
||||
</MudPaper>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</MudPaper>
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8" Style="margin-top: 64px">
|
||||
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
|
||||
<FragmentLetterAnchor TCard="OtherVideoCardViewModel" Cards="@_data.Cards">
|
||||
<MediaCard Data="@context"
|
||||
Link=""
|
||||
ArtworkKind="ArtworkKind.Thumbnail"
|
||||
AddToCollectionClicked="@AddToCollection"
|
||||
SelectClicked="@(e => SelectClicked(context, e))"
|
||||
IsSelected="@IsSelected(context)"
|
||||
IsSelectMode="@IsSelectMode()"/>
|
||||
</FragmentLetterAnchor>
|
||||
</MudContainer>
|
||||
</MudContainer>
|
||||
@if (_data.PageMap.IsSome)
|
||||
{
|
||||
<LetterBar PageMap="@_data.PageMap.ValueUnsafe()"
|
||||
BaseUri="/media/other/videos"
|
||||
Query="@_query"/>
|
||||
}
|
||||
|
||||
@code {
|
||||
private static int PageSize => 100;
|
||||
|
||||
[Parameter]
|
||||
public int PageNumber { get; set; }
|
||||
|
||||
private OtherVideoCardResultsViewModel _data;
|
||||
private string _query;
|
||||
|
||||
protected override Task OnParametersSetAsync()
|
||||
{
|
||||
if (PageNumber == 0)
|
||||
{
|
||||
PageNumber = 1;
|
||||
}
|
||||
|
||||
_query = _navigationManager.Uri.GetSearchQuery();
|
||||
return RefreshData();
|
||||
}
|
||||
|
||||
protected override async Task RefreshData()
|
||||
{
|
||||
string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:other_video" : $"type:other_video AND ({_query})";
|
||||
_data = await Mediator.Send(new QuerySearchIndexOtherVideos(searchQuery, PageNumber, PageSize));
|
||||
}
|
||||
|
||||
private void PrevPage()
|
||||
{
|
||||
var uri = $"/media/other/videos/page/{PageNumber - 1}";
|
||||
if (!string.IsNullOrWhiteSpace(_query))
|
||||
{
|
||||
(string key, string value) = _query.EncodeQuery();
|
||||
uri = $"{uri}?{key}={value}";
|
||||
}
|
||||
_navigationManager.NavigateTo(uri);
|
||||
}
|
||||
|
||||
private void NextPage()
|
||||
{
|
||||
var uri = $"/media/other/videos/page/{PageNumber + 1}";
|
||||
if (!string.IsNullOrWhiteSpace(_query))
|
||||
{
|
||||
(string key, string value) = _query.EncodeQuery();
|
||||
uri = $"{uri}?{key}={value}";
|
||||
}
|
||||
_navigationManager.NavigateTo(uri);
|
||||
}
|
||||
|
||||
private void SelectClicked(MediaCardViewModel card, MouseEventArgs e)
|
||||
{
|
||||
List<MediaCardViewModel> GetSortedItems()
|
||||
{
|
||||
return _data.Cards.OrderBy(m => m.SortTitle).ToList<MediaCardViewModel>();
|
||||
}
|
||||
|
||||
SelectClicked(GetSortedItems, card, e);
|
||||
}
|
||||
|
||||
private async Task AddToCollection(MediaCardViewModel card)
|
||||
{
|
||||
if (card is OtherVideoCardViewModel otherVideo)
|
||||
{
|
||||
var parameters = new DialogParameters { { "EntityType", "other video" }, { "EntityName", otherVideo.Title } };
|
||||
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
|
||||
|
||||
IDialogReference dialog = Dialog.Show<AddToCollectionDialog>("Add To Collection", parameters, options);
|
||||
DialogResult result = await dialog.Result;
|
||||
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
|
||||
{
|
||||
var request = new AddOtherVideoToCollection(collection.Id, otherVideo.OtherVideoId);
|
||||
Either<BaseError, Unit> addResult = await Mediator.Send(request);
|
||||
addResult.Match(
|
||||
Left: error =>
|
||||
{
|
||||
Snackbar.Add($"Unexpected error adding other video to collection: {error.Value}");
|
||||
Logger.LogError("Unexpected error adding other video to collection: {Error}", error.Value);
|
||||
},
|
||||
Right: _ => Snackbar.Add($"Added {otherVideo.Title} to collection {collection.Name}", Severity.Success));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -182,7 +182,7 @@
|
||||
if (_selectedPlayoutId.HasValue)
|
||||
{
|
||||
PagedPlayoutItemsViewModel data =
|
||||
await _mediator.Send(new GetPlayoutItemsById(_selectedPlayoutId.Value, state.Page, state.PageSize));
|
||||
await _mediator.Send(new GetFuturePlayoutItemsById(_selectedPlayoutId.Value, state.Page, state.PageSize));
|
||||
return new TableData<PlayoutItemViewModel>
|
||||
{
|
||||
TotalItems = data.TotalCount,
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
}
|
||||
</MudSelect>
|
||||
<MudTimePicker Class="mt-3" Label="Start Time" @bind-Time="@_selectedItem.StartTime" For="@(() => _selectedItem.StartTime)" Disabled="@(_selectedItem.StartType == StartType.Dynamic)"/>
|
||||
<MudSelect Label="Collection Type" @bind-Value="_selectedItem.CollectionType" For="@(() => _selectedItem.CollectionType)">
|
||||
<MudSelect Class="mt-3" Label="Collection Type" @bind-Value="_selectedItem.CollectionType" For="@(() => _selectedItem.CollectionType)">
|
||||
@foreach (ProgramScheduleItemCollectionType collectionType in Enum.GetValues<ProgramScheduleItemCollectionType>())
|
||||
{
|
||||
<MudSelectItem Value="@collectionType">@collectionType</MudSelectItem>
|
||||
@@ -154,7 +154,7 @@
|
||||
SearchFunc="@SearchArtists"
|
||||
ToStringFunc="@(s => s?.Name)"/>
|
||||
}
|
||||
<MudSelect Class="mt-3" Label="Playback Order" @bind-Value="@_selectedItem.PlaybackOrder" For="@(() => _selectedItem.PlaybackOrder)">
|
||||
<MudSelect Class="mt-3" Label="Playback Order" @bind-Value="@_selectedItem.PlaybackOrder" For="@(() => _selectedItem.PlaybackOrder)">
|
||||
@switch (_selectedItem.CollectionType)
|
||||
{
|
||||
case ProgramScheduleItemCollectionType.MultiCollection:
|
||||
@@ -183,10 +183,82 @@
|
||||
</MudSelect>
|
||||
<MudTextField Class="mt-3" Label="Multiple Count" @bind-Value="@_selectedItem.MultipleCount" For="@(() => _selectedItem.MultipleCount)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Multiple)"/>
|
||||
<MudTimePicker Class="mt-3" Label="Playout Duration" @bind-Time="@_selectedItem.PlayoutDuration" For="@(() => _selectedItem.PlayoutDuration)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration)"/>
|
||||
<MudElement HtmlTag="div" Class="mt-3">
|
||||
<MudSwitch Label="Offline Tail" @bind-Checked="@_selectedItem.OfflineTail" For="@(() => _selectedItem.OfflineTail)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration)"/>
|
||||
</MudElement>
|
||||
<MudSelect Class="mt-3" Label="Tail Mode" @bind-Value="@_selectedItem.TailMode" For="@(() => _selectedItem.TailMode)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration)">
|
||||
<MudSelectItem Value="@TailMode.None">(none)</MudSelectItem>
|
||||
<MudSelectItem Value="@TailMode.Offline">Offline</MudSelectItem>
|
||||
<MudSelectItem Value="@TailMode.Filler">Filler</MudSelectItem>
|
||||
</MudSelect>
|
||||
<MudSelect Class="mt-3" Label="Tail Collection Type" @bind-Value="_selectedItem.TailCollectionType" For="@(() => _selectedItem.TailCollectionType)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration || _selectedItem.TailMode != TailMode.Filler)">
|
||||
@foreach (ProgramScheduleItemCollectionType collectionType in Enum.GetValues<ProgramScheduleItemCollectionType>())
|
||||
{
|
||||
<MudSelectItem Value="@collectionType">@collectionType</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
@if (_selectedItem.TailCollectionType == ProgramScheduleItemCollectionType.Collection)
|
||||
{
|
||||
<MudAutocomplete Class="mt-3"
|
||||
T="MediaCollectionViewModel"
|
||||
Label="Collection"
|
||||
@bind-value="_selectedItem.TailCollection"
|
||||
SearchFunc="@SearchMediaCollections"
|
||||
ToStringFunc="@(c => c?.Name)"
|
||||
Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration || _selectedItem.TailMode != TailMode.Filler)"/>
|
||||
}
|
||||
@if (_selectedItem.TailCollectionType == ProgramScheduleItemCollectionType.MultiCollection)
|
||||
{
|
||||
<MudAutocomplete Class="mt-3"
|
||||
T="MultiCollectionViewModel"
|
||||
Label="Multi Collection"
|
||||
@bind-value="_selectedItem.TailMultiCollection"
|
||||
SearchFunc="@SearchMultiCollections"
|
||||
ToStringFunc="@(c => c?.Name)"
|
||||
Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration || _selectedItem.TailMode != TailMode.Filler)"/>
|
||||
}
|
||||
@if (_selectedItem.TailCollectionType == ProgramScheduleItemCollectionType.SmartCollection)
|
||||
{
|
||||
<MudAutocomplete Class="mt-3"
|
||||
T="SmartCollectionViewModel"
|
||||
Label="Smart Collection"
|
||||
@bind-value="_selectedItem.TailSmartCollection"
|
||||
SearchFunc="@SearchSmartCollections"
|
||||
ToStringFunc="@(c => c?.Name)"
|
||||
Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration || _selectedItem.TailMode != TailMode.Filler)"/>
|
||||
}
|
||||
@if (_selectedItem.TailCollectionType == ProgramScheduleItemCollectionType.TelevisionShow)
|
||||
{
|
||||
<MudAutocomplete Class="mt-3"
|
||||
T="NamedMediaItemViewModel"
|
||||
Label="Television Show"
|
||||
@bind-value="_selectedItem.TailMediaItem"
|
||||
SearchFunc="@SearchTelevisionShows"
|
||||
ToStringFunc="@(s => s?.Name)"
|
||||
Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration || _selectedItem.TailMode != TailMode.Filler)"/>
|
||||
}
|
||||
@if (_selectedItem.TailCollectionType == ProgramScheduleItemCollectionType.TelevisionSeason)
|
||||
{
|
||||
<MudAutocomplete Class="mt-3"
|
||||
T="NamedMediaItemViewModel"
|
||||
Label="Television Season"
|
||||
@bind-value="_selectedItem.TailMediaItem"
|
||||
SearchFunc="@SearchTelevisionSeasons"
|
||||
ToStringFunc="@(s => s?.Name)"
|
||||
Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration || _selectedItem.TailMode != TailMode.Filler)"/>
|
||||
}
|
||||
@if (_selectedItem.TailCollectionType == ProgramScheduleItemCollectionType.Artist)
|
||||
{
|
||||
<MudAutocomplete Class="mt-3"
|
||||
T="NamedMediaItemViewModel"
|
||||
Label="Artist"
|
||||
@bind-value="_selectedItem.TailMediaItem"
|
||||
SearchFunc="@SearchArtists"
|
||||
ToStringFunc="@(s => s?.Name)"
|
||||
Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration || _selectedItem.TailMode != TailMode.Filler)"/>
|
||||
}
|
||||
<MudTextField Class="mt-3" Label="Custom Title" @bind-Value="@_selectedItem.CustomTitle" For="@(() => _selectedItem.CustomTitle)"/>
|
||||
<MudSelect Class="mt-3" Label="Guide Mode" @bind-Value="@_selectedItem.GuideMode" For="@(() => _selectedItem.GuideMode)">
|
||||
<MudSelectItem Value="@GuideMode.Normal">Normal</MudSelectItem>
|
||||
<MudSelectItem Value="@GuideMode.Filler">Filler</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</EditForm>
|
||||
@@ -274,7 +346,8 @@
|
||||
SmartCollection = item.SmartCollection,
|
||||
MediaItem = item.MediaItem,
|
||||
PlaybackOrder = item.PlaybackOrder,
|
||||
CustomTitle = item.CustomTitle
|
||||
CustomTitle = item.CustomTitle,
|
||||
GuideMode = item.GuideMode
|
||||
};
|
||||
|
||||
switch (item)
|
||||
@@ -284,7 +357,12 @@
|
||||
break;
|
||||
case ProgramScheduleItemDurationViewModel duration:
|
||||
result.PlayoutDuration = duration.PlayoutDuration;
|
||||
result.OfflineTail = duration.OfflineTail;
|
||||
result.TailMode = duration.TailMode;
|
||||
result.TailCollectionType = duration.TailCollectionType;
|
||||
result.TailCollection = duration.TailCollection;
|
||||
result.TailMultiCollection = duration.TailMultiCollection;
|
||||
result.TailSmartCollection = duration.TailSmartCollection;
|
||||
result.TailMediaItem = duration.TailMediaItem;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -359,8 +437,14 @@
|
||||
item.PlaybackOrder,
|
||||
item.MultipleCount,
|
||||
item.PlayoutDuration,
|
||||
item.PlayoutMode == PlayoutMode.Duration ? item.OfflineTail.IfNone(false) : null,
|
||||
item.CustomTitle)).ToList();
|
||||
item.TailMode,
|
||||
item.TailCollectionType,
|
||||
item.TailCollection?.Id,
|
||||
item.TailMultiCollection?.Id,
|
||||
item.TailSmartCollection?.Id,
|
||||
item.TailMediaItem?.MediaItemId,
|
||||
item.CustomTitle,
|
||||
item.GuideMode)).ToList();
|
||||
|
||||
Seq<BaseError> errorMessages = await _mediator.Send(new ReplaceProgramScheduleItems(Id, items)).Map(e => e.LeftToSeq());
|
||||
|
||||
|
||||
@@ -63,6 +63,11 @@
|
||||
{
|
||||
<MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#music_videos")" Style="margin-bottom: auto; margin-top: auto">@_musicVideos.Count Music Videos</MudLink>
|
||||
}
|
||||
|
||||
if (_otherVideos?.Count > 0)
|
||||
{
|
||||
<MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#other_videos")" Style="margin-bottom: auto; margin-top: auto">@_otherVideos.Count Other Videos</MudLink>
|
||||
}
|
||||
<div style="margin-left: auto">
|
||||
<MudTooltip Text="Add All To Collection">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
@@ -249,6 +254,34 @@
|
||||
}
|
||||
</MudContainer>
|
||||
}
|
||||
|
||||
@if (_otherVideos?.Count > 0)
|
||||
{
|
||||
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
|
||||
<MudText Typo="Typo.h4"
|
||||
Style="scroll-margin-top: 160px"
|
||||
UserAttributes="@(new Dictionary<string, object> { { "id", "other_videos" } })">
|
||||
Other Videos
|
||||
</MudText>
|
||||
@if (_otherVideos.Count > 50)
|
||||
{
|
||||
<MudLink Href="@GetOtherVideosLink()" Class="ml-4">See All >></MudLink>
|
||||
}
|
||||
</div>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
|
||||
@foreach (OtherVideoCardViewModel card in _otherVideos.Cards.OrderBy(s => s.SortTitle))
|
||||
{
|
||||
<MediaCard Data="@card"
|
||||
Link=""
|
||||
ArtworkKind="ArtworkKind.Thumbnail"
|
||||
AddToCollectionClicked="@AddToCollection"
|
||||
SelectClicked="@(e => SelectClicked(card, e))"
|
||||
IsSelected="@IsSelected(card)"
|
||||
IsSelectMode="@IsSelectMode()"/>
|
||||
}
|
||||
</MudContainer>
|
||||
}
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
@@ -258,6 +291,7 @@
|
||||
private TelevisionSeasonCardResultsViewModel _seasons;
|
||||
private TelevisionEpisodeCardResultsViewModel _episodes;
|
||||
private MusicVideoCardResultsViewModel _musicVideos;
|
||||
private OtherVideoCardResultsViewModel _otherVideos;
|
||||
private ArtistCardResultsViewModel _artists;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -270,6 +304,7 @@
|
||||
_seasons = await Mediator.Send(new QuerySearchIndexSeasons($"type:season AND ({_query})", 1, 50));
|
||||
_episodes = await Mediator.Send(new QuerySearchIndexEpisodes($"type:episode AND ({_query})", 1, 50));
|
||||
_musicVideos = await Mediator.Send(new QuerySearchIndexMusicVideos($"type:music_video AND ({_query})", 1, 50));
|
||||
_otherVideos = await Mediator.Send(new QuerySearchIndexOtherVideos($"type:other_video AND ({_query})", 1, 50));
|
||||
_artists = await Mediator.Send(new QuerySearchIndexArtists($"type:artist AND ({_query})", 1, 50));
|
||||
}
|
||||
}
|
||||
@@ -284,6 +319,7 @@
|
||||
.Append(_episodes.Cards.OrderBy(ep => ep.SortTitle))
|
||||
.Append(_artists.Cards.OrderBy(a => a.SortTitle))
|
||||
.Append(_musicVideos.Cards.OrderBy(mv => mv.SortTitle))
|
||||
.Append(_otherVideos.Cards.OrderBy(ov => ov.SortTitle))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
@@ -396,6 +432,27 @@
|
||||
Right: _ => Snackbar.Add($"Added {musicVideo.Title} to collection {collection.Name}", Severity.Success));
|
||||
}
|
||||
}
|
||||
|
||||
if (card is OtherVideoCardViewModel otherVideo)
|
||||
{
|
||||
var parameters = new DialogParameters { { "EntityType", "other video" }, { "EntityName", otherVideo.Title } };
|
||||
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
|
||||
|
||||
IDialogReference dialog = Dialog.Show<AddToCollectionDialog>("Add To Collection", parameters, options);
|
||||
DialogResult result = await dialog.Result;
|
||||
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
|
||||
{
|
||||
var request = new AddOtherVideoToCollection(collection.Id, otherVideo.OtherVideoId);
|
||||
Either<BaseError, Unit> addResult = await Mediator.Send(request);
|
||||
addResult.Match(
|
||||
Left: error =>
|
||||
{
|
||||
Snackbar.Add($"Unexpected error adding other video to collection: {error.Value}");
|
||||
Logger.LogError("Unexpected error adding other video to collection: {Error}", error.Value);
|
||||
},
|
||||
Right: _ => Snackbar.Add($"Added {otherVideo.Title} to collection {collection.Name}", Severity.Success));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetMoviesLink()
|
||||
@@ -463,6 +520,17 @@
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
private string GetOtherVideosLink()
|
||||
{
|
||||
var uri = "/media/other/videos/page/1";
|
||||
if (!string.IsNullOrWhiteSpace(_query))
|
||||
{
|
||||
(string key, string value) = _query.EncodeQuery();
|
||||
uri = $"{uri}?{key}={value}";
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
private async Task AddAllToCollection(MouseEventArgs _)
|
||||
{
|
||||
@@ -474,6 +542,7 @@
|
||||
results.EpisodeIds,
|
||||
results.ArtistIds,
|
||||
results.MusicVideoIds,
|
||||
results.OtherVideoIds,
|
||||
"search results");
|
||||
}
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@
|
||||
DialogResult result = await dialog.Result;
|
||||
if (!result.Cancelled && result.Data is ProgramScheduleViewModel schedule)
|
||||
{
|
||||
await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionSeason, null, null, null, SeasonId, PlaybackOrder.Shuffle, null, null, null, null));
|
||||
await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionSeason, null, null, null, SeasonId, PlaybackOrder.Shuffle, null, null, TailMode.None, ProgramScheduleItemCollectionType.Collection, null, null, null, null, null, GuideMode.Normal));
|
||||
_navigationManager.NavigateTo($"/schedules/{schedule.Id}/items");
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user