merge latest develop (#230)
* sync guids/provider ids (#227) * sync guids from plex * cleanup * sync local guids * sync jellyfin and emby guids * add episodes to search index (#228) * sync episode directors and writers * display episode writers and directors * remove missing episodes from search index * show episodes in search results * fix emby and jellyfin episode updates * fix updating plex episodes * don't delete channel logos on startup * add episodes page; fix adding episodes to collection * cleanup * multi-part episode grouping fixes (#229)
This commit is contained in:
@@ -39,7 +39,8 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
internal static TelevisionEpisodeCardViewModel ProjectToViewModel(
|
internal static TelevisionEpisodeCardViewModel ProjectToViewModel(
|
||||||
EpisodeMetadata episodeMetadata,
|
EpisodeMetadata episodeMetadata,
|
||||||
Option<JellyfinMediaSource> maybeJellyfin,
|
Option<JellyfinMediaSource> maybeJellyfin,
|
||||||
Option<EmbyMediaSource> maybeEmby) =>
|
Option<EmbyMediaSource> maybeEmby,
|
||||||
|
bool isSearchResult) =>
|
||||||
new(
|
new(
|
||||||
episodeMetadata.EpisodeId,
|
episodeMetadata.EpisodeId,
|
||||||
episodeMetadata.ReleaseDate ?? DateTime.MinValue,
|
episodeMetadata.ReleaseDate ?? DateTime.MinValue,
|
||||||
@@ -48,12 +49,21 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
() => string.Empty),
|
() => string.Empty),
|
||||||
episodeMetadata.Episode.Season.ShowId,
|
episodeMetadata.Episode.Season.ShowId,
|
||||||
episodeMetadata.Episode.SeasonId,
|
episodeMetadata.Episode.SeasonId,
|
||||||
|
episodeMetadata.Episode.Season.SeasonNumber,
|
||||||
episodeMetadata.Episode.EpisodeNumber,
|
episodeMetadata.Episode.EpisodeNumber,
|
||||||
episodeMetadata.Title,
|
episodeMetadata.Title,
|
||||||
|
episodeMetadata.SortTitle,
|
||||||
episodeMetadata.Episode.EpisodeMetadata.HeadOrNone().Match(
|
episodeMetadata.Episode.EpisodeMetadata.HeadOrNone().Match(
|
||||||
em => em.Plot ?? string.Empty,
|
em => em.Plot ?? string.Empty,
|
||||||
() => string.Empty),
|
() => string.Empty),
|
||||||
GetThumbnail(episodeMetadata, maybeJellyfin, maybeEmby));
|
isSearchResult
|
||||||
|
? GetPoster(
|
||||||
|
episodeMetadata.Episode.Season.SeasonMetadata.Head(),
|
||||||
|
maybeJellyfin,
|
||||||
|
maybeEmby)
|
||||||
|
: GetThumbnail(episodeMetadata, maybeJellyfin, maybeEmby),
|
||||||
|
episodeMetadata.Directors.Map(d => d.Name).ToList(),
|
||||||
|
episodeMetadata.Writers.Map(w => w.Name).ToList());
|
||||||
|
|
||||||
internal static MovieCardViewModel ProjectToViewModel(
|
internal static MovieCardViewModel ProjectToViewModel(
|
||||||
MovieMetadata movieMetadata,
|
MovieMetadata movieMetadata,
|
||||||
@@ -101,7 +111,7 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
collection.MediaItems.OfType<Season>().Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby))
|
collection.MediaItems.OfType<Season>().Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby))
|
||||||
.ToList(),
|
.ToList(),
|
||||||
collection.MediaItems.OfType<Episode>()
|
collection.MediaItems.OfType<Episode>()
|
||||||
.Map(e => ProjectToViewModel(e.EpisodeMetadata.Head(), maybeJellyfin, maybeEmby))
|
.Map(e => ProjectToViewModel(e.EpisodeMetadata.Head(), maybeJellyfin, maybeEmby, false))
|
||||||
.ToList(),
|
.ToList(),
|
||||||
collection.MediaItems.OfType<Artist>().Map(a => ProjectToViewModel(a.ArtistMetadata.Head())).ToList(),
|
collection.MediaItems.OfType<Artist>().Map(a => ProjectToViewModel(a.ArtistMetadata.Head())).ToList(),
|
||||||
collection.MediaItems.OfType<MusicVideo>().Map(mv => ProjectToViewModel(mv.MusicVideoMetadata.Head()))
|
collection.MediaItems.OfType<MusicVideo>().Map(mv => ProjectToViewModel(mv.MusicVideoMetadata.Head()))
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Search;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using static ErsatzTV.Application.MediaCards.Mapper;
|
using static ErsatzTV.Application.MediaCards.Mapper;
|
||||||
@@ -39,9 +40,9 @@ namespace ErsatzTV.Application.MediaCards.Queries
|
|||||||
|
|
||||||
List<TelevisionEpisodeCardViewModel> results = await _televisionRepository
|
List<TelevisionEpisodeCardViewModel> results = await _televisionRepository
|
||||||
.GetPagedEpisodes(request.TelevisionSeasonId, request.PageNumber, request.PageSize)
|
.GetPagedEpisodes(request.TelevisionSeasonId, request.PageNumber, request.PageSize)
|
||||||
.Map(list => list.Map(e => ProjectToViewModel(e, maybeJellyfin, maybeEmby)).ToList());
|
.Map(list => list.Map(e => ProjectToViewModel(e, maybeJellyfin, maybeEmby, false)).ToList());
|
||||||
|
|
||||||
return new TelevisionEpisodeCardResultsViewModel(count, results);
|
return new TelevisionEpisodeCardResultsViewModel(count, results, Option<SearchPageMap>.None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using ErsatzTV.Core.Search;
|
||||||
|
using LanguageExt;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCards
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record TelevisionEpisodeCardResultsViewModel(int Count, List<TelevisionEpisodeCardViewModel> Cards);
|
public record TelevisionEpisodeCardResultsViewModel(
|
||||||
|
int Count,
|
||||||
|
List<TelevisionEpisodeCardViewModel> Cards,
|
||||||
|
Option<SearchPageMap> PageMap);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCards
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
@@ -9,13 +10,17 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
string ShowTitle,
|
string ShowTitle,
|
||||||
int ShowId,
|
int ShowId,
|
||||||
int SeasonId,
|
int SeasonId,
|
||||||
|
int Season,
|
||||||
int Episode,
|
int Episode,
|
||||||
string Title,
|
string Title,
|
||||||
|
string SortTitle,
|
||||||
string Plot,
|
string Plot,
|
||||||
string Poster) : MediaCardViewModel(
|
string Poster,
|
||||||
|
List<string> Directors,
|
||||||
|
List<string> Writers) : MediaCardViewModel(
|
||||||
EpisodeId,
|
EpisodeId,
|
||||||
Title,
|
Title,
|
||||||
$"Episode {Episode}",
|
$"Episode {Episode}",
|
||||||
$"Episode {Episode}",
|
SortTitle,
|
||||||
Poster);
|
Poster);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
int CollectionId,
|
int CollectionId,
|
||||||
List<int> MovieIds,
|
List<int> MovieIds,
|
||||||
List<int> ShowIds,
|
List<int> ShowIds,
|
||||||
|
List<int> EpisodeIds,
|
||||||
List<int> ArtistIds,
|
List<int> ArtistIds,
|
||||||
List<int> MusicVideoIds) : MediatR.IRequest<Either<BaseError, Unit>>;
|
List<int> MusicVideoIds) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
{
|
{
|
||||||
var allItems = request.MovieIds
|
var allItems = request.MovieIds
|
||||||
.Append(request.ShowIds)
|
.Append(request.ShowIds)
|
||||||
|
.Append(request.EpisodeIds)
|
||||||
.Append(request.ArtistIds)
|
.Append(request.ArtistIds)
|
||||||
.Append(request.MusicVideoIds)
|
.Append(request.MusicVideoIds)
|
||||||
.ToList();
|
.ToList();
|
||||||
@@ -59,8 +60,9 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Validation<BaseError, Unit>> Validate(AddItemsToCollection request) =>
|
private async Task<Validation<BaseError, Unit>> Validate(AddItemsToCollection request) =>
|
||||||
(await CollectionMustExist(request), await ValidateMovies(request), await ValidateShows(request))
|
(await CollectionMustExist(request), await ValidateMovies(request), await ValidateShows(request),
|
||||||
.Apply((_, _, _) => Unit.Default);
|
await ValidateEpisodes(request))
|
||||||
|
.Apply((_, _, _, _) => Unit.Default);
|
||||||
|
|
||||||
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddItemsToCollection request) =>
|
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddItemsToCollection request) =>
|
||||||
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId)
|
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId)
|
||||||
@@ -80,5 +82,12 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
.Filter(v => v == true)
|
.Filter(v => v == true)
|
||||||
.MapT(_ => Unit.Default)
|
.MapT(_ => Unit.Default)
|
||||||
.Map(v => v.ToValidation<BaseError>("Show does not exist"));
|
.Map(v => v.ToValidation<BaseError>("Show does not exist"));
|
||||||
|
|
||||||
|
private Task<Validation<BaseError, Unit>> ValidateEpisodes(AddItemsToCollection request) =>
|
||||||
|
_televisionRepository.AllEpisodesExist(request.EpisodeIds)
|
||||||
|
.Map(Optional)
|
||||||
|
.Filter(v => v == true)
|
||||||
|
.MapT(_ => Unit.Default)
|
||||||
|
.Map(v => v.ToValidation<BaseError>("Show does not exist"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,18 +17,16 @@ namespace ErsatzTV.Application.Search.Queries
|
|||||||
|
|
||||||
public async Task<SearchResultAllItemsViewModel> Handle(
|
public async Task<SearchResultAllItemsViewModel> Handle(
|
||||||
QuerySearchIndexAllItems request,
|
QuerySearchIndexAllItems request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken) =>
|
||||||
{
|
new(
|
||||||
List<int> movieIds = await _searchIndex.Search($"type:movie AND ({request.Query})", 0, 0)
|
await GetIds("movie", request.Query),
|
||||||
.Map(result => result.Items.Map(i => i.Id).ToList());
|
await GetIds("show", request.Query),
|
||||||
List<int> showIds = await _searchIndex.Search($"type:show AND ({request.Query})", 0, 0)
|
await GetIds("episode", request.Query),
|
||||||
.Map(result => result.Items.Map(i => i.Id).ToList());
|
await GetIds("artist", request.Query),
|
||||||
List<int> artistIds = await _searchIndex.Search($"type:artist AND ({request.Query})", 0, 0)
|
await GetIds("music_video", request.Query));
|
||||||
.Map(result => result.Items.Map(i => i.Id).ToList());
|
|
||||||
List<int> musicVideoIds = await _searchIndex.Search($"type:music_video AND ({request.Query})", 0, 0)
|
|
||||||
.Map(result => result.Items.Map(i => i.Id).ToList());
|
|
||||||
|
|
||||||
return new SearchResultAllItemsViewModel(movieIds, showIds, artistIds, musicVideoIds);
|
private Task<List<int>> GetIds(string type, string query) =>
|
||||||
}
|
_searchIndex.Search($"type:{type} AND ({query})", 0, 0)
|
||||||
|
.Map(result => result.Items.Map(i => i.Id).ToList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using ErsatzTV.Application.MediaCards;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Search.Queries
|
||||||
|
{
|
||||||
|
public record QuerySearchIndexEpisodes
|
||||||
|
(string Query, int PageNumber, int PageSize) : IRequest<TelevisionEpisodeCardResultsViewModel>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Application.MediaCards;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
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
|
||||||
|
QuerySearchIndexEpisodesHandler : IRequestHandler<QuerySearchIndexEpisodes,
|
||||||
|
TelevisionEpisodeCardResultsViewModel>
|
||||||
|
{
|
||||||
|
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||||
|
private readonly ISearchIndex _searchIndex;
|
||||||
|
private readonly ITelevisionRepository _televisionRepository;
|
||||||
|
|
||||||
|
public QuerySearchIndexEpisodesHandler(
|
||||||
|
ISearchIndex searchIndex,
|
||||||
|
ITelevisionRepository televisionRepository,
|
||||||
|
IMediaSourceRepository mediaSourceRepository)
|
||||||
|
{
|
||||||
|
_searchIndex = searchIndex;
|
||||||
|
_televisionRepository = televisionRepository;
|
||||||
|
_mediaSourceRepository = mediaSourceRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TelevisionEpisodeCardResultsViewModel> Handle(
|
||||||
|
QuerySearchIndexEpisodes request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
SearchResult searchResult = await _searchIndex.Search(
|
||||||
|
request.Query,
|
||||||
|
(request.PageNumber - 1) * request.PageSize,
|
||||||
|
request.PageSize);
|
||||||
|
|
||||||
|
Option<JellyfinMediaSource> maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin()
|
||||||
|
.Map(list => list.HeadOrNone());
|
||||||
|
|
||||||
|
Option<EmbyMediaSource> maybeEmby = await _mediaSourceRepository.GetAllEmby()
|
||||||
|
.Map(list => list.HeadOrNone());
|
||||||
|
|
||||||
|
List<TelevisionEpisodeCardViewModel> items = await _televisionRepository
|
||||||
|
.GetEpisodesForCards(searchResult.Items.Map(i => i.Id).ToList())
|
||||||
|
.Map(list => list.Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby, true)).ToList());
|
||||||
|
|
||||||
|
return new TelevisionEpisodeCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ namespace ErsatzTV.Application.Search
|
|||||||
public record SearchResultAllItemsViewModel(
|
public record SearchResultAllItemsViewModel(
|
||||||
List<int> MovieIds,
|
List<int> MovieIds,
|
||||||
List<int> ShowIds,
|
List<int> ShowIds,
|
||||||
|
List<int> EpisodeIds,
|
||||||
List<int> ArtistIds,
|
List<int> ArtistIds,
|
||||||
List<int> MusicVideoIds);
|
List<int> MusicVideoIds);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,17 +11,14 @@ namespace ErsatzTV.Core.Tests.Fakes
|
|||||||
public class FakeTelevisionRepository : ITelevisionRepository
|
public class FakeTelevisionRepository : ITelevisionRepository
|
||||||
{
|
{
|
||||||
public Task<bool> AllShowsExist(List<int> showIds) => throw new NotSupportedException();
|
public Task<bool> AllShowsExist(List<int> showIds) => throw new NotSupportedException();
|
||||||
|
public Task<bool> AllEpisodesExist(List<int> episodeIds) => throw new NotSupportedException();
|
||||||
|
|
||||||
public Task<List<Show>> GetAllShows() => throw new NotSupportedException();
|
public Task<List<Show>> GetAllShows() => throw new NotSupportedException();
|
||||||
|
|
||||||
public Task<Option<Show>> GetShow(int showId) => throw new NotSupportedException();
|
public Task<Option<Show>> GetShow(int showId) => throw new NotSupportedException();
|
||||||
|
|
||||||
public Task<int> GetShowCount() => throw new NotSupportedException();
|
|
||||||
|
|
||||||
public Task<List<ShowMetadata>> GetPagedShows(int pageNumber, int pageSize) =>
|
|
||||||
throw new NotSupportedException();
|
|
||||||
|
|
||||||
public Task<List<ShowMetadata>> GetShowsForCards(List<int> ids) => throw new NotSupportedException();
|
public Task<List<ShowMetadata>> GetShowsForCards(List<int> ids) => throw new NotSupportedException();
|
||||||
|
public Task<List<EpisodeMetadata>> GetEpisodesForCards(List<int> ids) => throw new NotSupportedException();
|
||||||
|
|
||||||
public Task<List<Episode>> GetShowItems(int showId) => throw new NotSupportedException();
|
public Task<List<Episode>> GetShowItems(int showId) => throw new NotSupportedException();
|
||||||
|
|
||||||
@@ -89,10 +86,18 @@ namespace ErsatzTV.Core.Tests.Fakes
|
|||||||
public Task<Unit> RemoveMissingPlexSeasons(string showKey, List<string> seasonKeys) =>
|
public Task<Unit> RemoveMissingPlexSeasons(string showKey, List<string> seasonKeys) =>
|
||||||
throw new NotSupportedException();
|
throw new NotSupportedException();
|
||||||
|
|
||||||
public Task<Unit> RemoveMissingPlexEpisodes(string seasonKey, List<string> episodeKeys) =>
|
public Task<List<int>> RemoveMissingPlexEpisodes(string seasonKey, List<string> episodeKeys) =>
|
||||||
throw new NotSupportedException();
|
throw new NotSupportedException();
|
||||||
|
|
||||||
public Task<Unit> SetEpisodeNumber(Episode episode, int episodeNumber) => throw new NotSupportedException();
|
public Task<Unit> SetEpisodeNumber(Episode episode, int episodeNumber) => throw new NotSupportedException();
|
||||||
|
public Task<bool> AddDirector(EpisodeMetadata metadata, Director director) => throw new NotSupportedException();
|
||||||
|
|
||||||
|
public Task<bool> AddWriter(EpisodeMetadata metadata, Writer writer) => throw new NotSupportedException();
|
||||||
|
|
||||||
|
public Task<int> GetShowCount() => throw new NotSupportedException();
|
||||||
|
|
||||||
|
public Task<List<ShowMetadata>> GetPagedShows(int pageNumber, int pageSize) =>
|
||||||
|
throw new NotSupportedException();
|
||||||
|
|
||||||
public Task<bool> Update(Show show) => throw new NotSupportedException();
|
public Task<bool> Update(Show show) => throw new NotSupportedException();
|
||||||
|
|
||||||
|
|||||||
@@ -9,14 +9,17 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
public class MultiPartEpisodeGrouperTests
|
public class MultiPartEpisodeGrouperTests
|
||||||
{
|
{
|
||||||
[Test]
|
[Test]
|
||||||
public void NotGrouped_Grouped_NotGrouped()
|
[TestCase("Episode 1", "Episode 2 (1)", "Episode 3 (2)", "Episode 4")]
|
||||||
|
[TestCase("Episode 1 - More", "Episode 2 (1) - Title", "Episode 3 (2) - After", "Episode 4 - Dash")]
|
||||||
|
[TestCase("Episode 1", "Episode 2 Part 1", "Episode 3 Part 2", "Episode 4")]
|
||||||
|
public void NotGrouped_Grouped_NotGrouped(string one, string two, string three, string four)
|
||||||
{
|
{
|
||||||
var mediaItems = new List<MediaItem>
|
var mediaItems = new List<MediaItem>
|
||||||
{
|
{
|
||||||
NamedEpisode("Episode 1"),
|
NamedEpisode(one),
|
||||||
NamedEpisode("Episode 2 (1)"),
|
NamedEpisode(two),
|
||||||
NamedEpisode("Episode 3 (2)"),
|
NamedEpisode(three),
|
||||||
NamedEpisode("Episode 4")
|
NamedEpisode(four)
|
||||||
};
|
};
|
||||||
|
|
||||||
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems);
|
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems);
|
||||||
@@ -29,13 +32,16 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Grouped_NotGrouped()
|
[TestCase("Episode 1 (1)", "Episode 2 (2)", "Episode 3")]
|
||||||
|
[TestCase("Episode 1 (1) - More", "Episode 2 (2) - Title", "Episode 3 - After")]
|
||||||
|
[TestCase("Episode 1 Part 1", "Episode 2 Part 2", "Episode 3")]
|
||||||
|
public void Grouped_NotGrouped(string one, string two, string three)
|
||||||
{
|
{
|
||||||
var mediaItems = new List<MediaItem>
|
var mediaItems = new List<MediaItem>
|
||||||
{
|
{
|
||||||
NamedEpisode("Episode 1 (1)"),
|
NamedEpisode(one),
|
||||||
NamedEpisode("Episode 2 (2)"),
|
NamedEpisode(two),
|
||||||
NamedEpisode("Episode 3")
|
NamedEpisode(three)
|
||||||
};
|
};
|
||||||
|
|
||||||
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems);
|
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems);
|
||||||
@@ -47,15 +53,23 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Grouped_NotGrouped_Grouped()
|
[TestCase("Episode 1 (1)", "Episode 2 (2)", "Episode 3", "Episode 4 (1)", "Episode 5 (2)")]
|
||||||
|
[TestCase(
|
||||||
|
"Episode 1 (1) - More",
|
||||||
|
"Episode 2 (2) - Title",
|
||||||
|
"Episode 3 - After",
|
||||||
|
"Episode 4 (1) - Dash",
|
||||||
|
"Episode 5 (2) - Again")]
|
||||||
|
[TestCase("Episode 1 Part 1", "Episode 2 Part 2", "Episode 3", "Episode 4 Part 1", "Episode 5 Part 2")]
|
||||||
|
public void Grouped_NotGrouped_Grouped(string one, string two, string three, string four, string five)
|
||||||
{
|
{
|
||||||
var mediaItems = new List<MediaItem>
|
var mediaItems = new List<MediaItem>
|
||||||
{
|
{
|
||||||
NamedEpisode("Episode 1 (1)"),
|
NamedEpisode(one),
|
||||||
NamedEpisode("Episode 2 (2)"),
|
NamedEpisode(two),
|
||||||
NamedEpisode("Episode 3"),
|
NamedEpisode(three),
|
||||||
NamedEpisode("Episode 4 (1)"),
|
NamedEpisode(four),
|
||||||
NamedEpisode("Episode 5 (2)")
|
NamedEpisode(five)
|
||||||
};
|
};
|
||||||
|
|
||||||
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems);
|
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems);
|
||||||
@@ -69,14 +83,17 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Grouped_Grouped()
|
[TestCase("Episode 1 (1)", "Episode 2 (2)", "Episode 3 (1)", "Episode 4 (2)")]
|
||||||
|
[TestCase("Episode 1 (1) - More", "Episode 2 (2) - Title", "Episode 3 (1) - After", "Episode 4 (2) - Dash")]
|
||||||
|
[TestCase("Episode 1 Part 1", "Episode 2 Part 2", "Episode 3 Part 1", "Episode 4 Part 2")]
|
||||||
|
public void Grouped_Grouped(string one, string two, string three, string four)
|
||||||
{
|
{
|
||||||
var mediaItems = new List<MediaItem>
|
var mediaItems = new List<MediaItem>
|
||||||
{
|
{
|
||||||
NamedEpisode("Episode 1 (1)"),
|
NamedEpisode(one),
|
||||||
NamedEpisode("Episode 2 (2)"),
|
NamedEpisode(two),
|
||||||
NamedEpisode("Episode 3 (1)"),
|
NamedEpisode(three),
|
||||||
NamedEpisode("Episode 4 (2)")
|
NamedEpisode(four)
|
||||||
};
|
};
|
||||||
|
|
||||||
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems);
|
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems);
|
||||||
@@ -88,6 +105,75 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
result[1].Additional[0].Should().Be(mediaItems[3]);
|
result[1].Additional[0].Should().Be(mediaItems[3]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[TestCase("Episode 1", "Episode 2 (2)", "Episode 3 (1)", "Episode 4 (2)")]
|
||||||
|
[TestCase("Episode 1 - More", "Episode 2 (2) - Title", "Episode 3 (1) - After", "Episode 4 (2) - Dash")]
|
||||||
|
[TestCase("Episode 1", "Episode 2 Part 2", "Episode 3 Part 1", "Episode 4 Part 2")]
|
||||||
|
public void Part2_Without_Part1(string one, string two, string three, string four)
|
||||||
|
{
|
||||||
|
var mediaItems = new List<MediaItem>
|
||||||
|
{
|
||||||
|
NamedEpisode(one),
|
||||||
|
NamedEpisode(two),
|
||||||
|
NamedEpisode(three),
|
||||||
|
NamedEpisode(four)
|
||||||
|
};
|
||||||
|
|
||||||
|
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems);
|
||||||
|
|
||||||
|
result.Count.Should().Be(3);
|
||||||
|
result[0].First.Should().Be(mediaItems[0]);
|
||||||
|
result[1].First.Should().Be(mediaItems[1]);
|
||||||
|
result[2].First.Should().Be(mediaItems[2]);
|
||||||
|
result[2].Additional[0].Should().Be(mediaItems[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[TestCase("Episode 1 (1)", "Episode 3 (3)", "Episode 4", "Episode 5")]
|
||||||
|
[TestCase("Episode 1 (1) - More", "Episode 3 (3) - Title", "Episode 4 - After", "Episode 5 - Dash")]
|
||||||
|
[TestCase("Episode 1 Part 1", "Episode 3 Part 3", "Episode 4", "Episode 5")]
|
||||||
|
public void Skip_Part(string one, string two, string three, string four)
|
||||||
|
{
|
||||||
|
var mediaItems = new List<MediaItem>
|
||||||
|
{
|
||||||
|
NamedEpisode(one),
|
||||||
|
NamedEpisode(two),
|
||||||
|
NamedEpisode(three),
|
||||||
|
NamedEpisode(four)
|
||||||
|
};
|
||||||
|
|
||||||
|
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems);
|
||||||
|
|
||||||
|
result.Count.Should().Be(4);
|
||||||
|
result[0].First.Should().Be(mediaItems[0]);
|
||||||
|
result[1].First.Should().Be(mediaItems[1]);
|
||||||
|
result[2].First.Should().Be(mediaItems[2]);
|
||||||
|
result[3].First.Should().Be(mediaItems[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[TestCase("Episode 1 (1)", "Episode 3 (1)", "Episode 4 (2)", "Episode 5")]
|
||||||
|
[TestCase("Episode 1 (1) - More", "Episode 3 (1) - Title", "Episode 4 (2) - After", "Episode 5 - Dash")]
|
||||||
|
[TestCase("Episode 1 Part 1", "Episode 3 Part 1", "Episode 4 Part 2", "Episode 5")]
|
||||||
|
public void Repeat_Part(string one, string two, string three, string four)
|
||||||
|
{
|
||||||
|
var mediaItems = new List<MediaItem>
|
||||||
|
{
|
||||||
|
NamedEpisode(one),
|
||||||
|
NamedEpisode(two),
|
||||||
|
NamedEpisode(three),
|
||||||
|
NamedEpisode(four)
|
||||||
|
};
|
||||||
|
|
||||||
|
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems);
|
||||||
|
|
||||||
|
result.Count.Should().Be(3);
|
||||||
|
result[0].First.Should().Be(mediaItems[0]);
|
||||||
|
result[1].First.Should().Be(mediaItems[1]);
|
||||||
|
result[1].Additional[0].Should().Be(mediaItems[2]);
|
||||||
|
result[2].First.Should().Be(mediaItems[3]);
|
||||||
|
}
|
||||||
|
|
||||||
private static Episode NamedEpisode(string title) =>
|
private static Episode NamedEpisode(string title) =>
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace ErsatzTV.Core.Domain
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Domain
|
||||||
{
|
{
|
||||||
public class EpisodeMetadata : Metadata
|
public class EpisodeMetadata : Metadata
|
||||||
{
|
{
|
||||||
@@ -7,5 +9,7 @@
|
|||||||
public string Tagline { get; set; }
|
public string Tagline { get; set; }
|
||||||
public int EpisodeId { get; set; }
|
public int EpisodeId { get; set; }
|
||||||
public Episode Episode { get; set; }
|
public Episode Episode { get; set; }
|
||||||
|
public List<Director> Directors { get; set; }
|
||||||
|
public List<Writer> Writers { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,5 +19,6 @@ namespace ErsatzTV.Core.Domain
|
|||||||
public List<Tag> Tags { get; set; }
|
public List<Tag> Tags { get; set; }
|
||||||
public List<Studio> Studios { get; set; }
|
public List<Studio> Studios { get; set; }
|
||||||
public List<Actor> Actors { get; set; }
|
public List<Actor> Actors { get; set; }
|
||||||
|
public List<MetadataGuid> Guids { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace ErsatzTV.Core.Domain
|
||||||
|
{
|
||||||
|
public class MetadataGuid
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Guid { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -120,8 +120,6 @@ namespace ErsatzTV.Core.Emby
|
|||||||
decimal percentCompletion = (decimal) shows.IndexOf(incoming) / shows.Count;
|
decimal percentCompletion = (decimal) shows.IndexOf(incoming) / shows.Count;
|
||||||
await _mediator.Publish(new LibraryScanProgress(library.Id, percentCompletion));
|
await _mediator.Publish(new LibraryScanProgress(library.Id, percentCompletion));
|
||||||
|
|
||||||
var changed = false;
|
|
||||||
|
|
||||||
Option<EmbyItemEtag> maybeExisting = existingShows.Find(ie => ie.ItemId == incoming.ItemId);
|
Option<EmbyItemEtag> maybeExisting = existingShows.Find(ie => ie.ItemId == incoming.ItemId);
|
||||||
await maybeExisting.Match(
|
await maybeExisting.Match(
|
||||||
async existing =>
|
async existing =>
|
||||||
@@ -135,7 +133,6 @@ namespace ErsatzTV.Core.Emby
|
|||||||
"UPDATE: Etag has changed for show {Show}",
|
"UPDATE: Etag has changed for show {Show}",
|
||||||
incoming.ShowMetadata.Head().Title);
|
incoming.ShowMetadata.Head().Title);
|
||||||
|
|
||||||
changed = true;
|
|
||||||
incoming.LibraryPathId = library.Paths.Head().Id;
|
incoming.LibraryPathId = library.Paths.Head().Id;
|
||||||
|
|
||||||
Option<EmbyShow> updated = await _televisionRepository.Update(incoming);
|
Option<EmbyShow> updated = await _televisionRepository.Update(incoming);
|
||||||
@@ -148,7 +145,6 @@ namespace ErsatzTV.Core.Emby
|
|||||||
},
|
},
|
||||||
async () =>
|
async () =>
|
||||||
{
|
{
|
||||||
changed = true;
|
|
||||||
incoming.LibraryPathId = library.Paths.Head().Id;
|
incoming.LibraryPathId = library.Paths.Head().Id;
|
||||||
|
|
||||||
// _logger.LogDebug("INSERT: Item id is new for show {Show}", incoming.ShowMetadata.Head().Title);
|
// _logger.LogDebug("INSERT: Item id is new for show {Show}", incoming.ShowMetadata.Head().Title);
|
||||||
@@ -159,50 +155,45 @@ namespace ErsatzTV.Core.Emby
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (changed)
|
List<EmbyItemEtag> existingSeasons =
|
||||||
{
|
await _televisionRepository.GetExistingSeasons(library, incoming.ItemId);
|
||||||
List<EmbyItemEtag> existingSeasons =
|
|
||||||
await _televisionRepository.GetExistingSeasons(library, incoming.ItemId);
|
|
||||||
|
|
||||||
Either<BaseError, List<EmbySeason>> maybeSeasons =
|
Either<BaseError, List<EmbySeason>> maybeSeasons =
|
||||||
await _embyApiClient.GetSeasonLibraryItems(
|
await _embyApiClient.GetSeasonLibraryItems(
|
||||||
|
address,
|
||||||
|
apiKey,
|
||||||
|
library.MediaSourceId,
|
||||||
|
incoming.ItemId);
|
||||||
|
|
||||||
|
await maybeSeasons.Match(
|
||||||
|
async seasons =>
|
||||||
|
{
|
||||||
|
await ProcessSeasons(
|
||||||
address,
|
address,
|
||||||
apiKey,
|
apiKey,
|
||||||
library.MediaSourceId,
|
library,
|
||||||
incoming.ItemId);
|
ffprobePath,
|
||||||
|
pathReplacements,
|
||||||
|
incoming,
|
||||||
|
existingSeasons,
|
||||||
|
seasons);
|
||||||
|
|
||||||
await maybeSeasons.Match(
|
var incomingSeasonIds = seasons.Map(s => s.ItemId).ToList();
|
||||||
async seasons =>
|
var seasonIds = existingSeasons
|
||||||
{
|
.Filter(i => !incomingSeasonIds.Contains(i.ItemId))
|
||||||
await ProcessSeasons(
|
.Map(m => m.ItemId)
|
||||||
address,
|
.ToList();
|
||||||
apiKey,
|
await _televisionRepository.RemoveMissingSeasons(library, seasonIds);
|
||||||
library,
|
},
|
||||||
ffprobePath,
|
error =>
|
||||||
pathReplacements,
|
{
|
||||||
incoming,
|
_logger.LogWarning(
|
||||||
existingSeasons,
|
"Error synchronizing emby library {Path}: {Error}",
|
||||||
seasons);
|
library.Name,
|
||||||
|
error.Value);
|
||||||
|
|
||||||
await _searchIndex.UpdateItems(_searchRepository, new List<MediaItem> { incoming });
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
var incomingSeasonIds = seasons.Map(s => s.ItemId).ToList();
|
|
||||||
var seasonIds = existingSeasons
|
|
||||||
.Filter(i => !incomingSeasonIds.Contains(i.ItemId))
|
|
||||||
.Map(m => m.ItemId)
|
|
||||||
.ToList();
|
|
||||||
await _televisionRepository.RemoveMissingSeasons(library, seasonIds);
|
|
||||||
},
|
|
||||||
error =>
|
|
||||||
{
|
|
||||||
_logger.LogWarning(
|
|
||||||
"Error synchronizing emby library {Path}: {Error}",
|
|
||||||
library.Name,
|
|
||||||
error.Value);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,8 +209,6 @@ namespace ErsatzTV.Core.Emby
|
|||||||
{
|
{
|
||||||
foreach (EmbySeason incoming in seasons)
|
foreach (EmbySeason incoming in seasons)
|
||||||
{
|
{
|
||||||
var changed = false;
|
|
||||||
|
|
||||||
Option<EmbyItemEtag> maybeExisting = existingSeasons.Find(ie => ie.ItemId == incoming.ItemId);
|
Option<EmbyItemEtag> maybeExisting = existingSeasons.Find(ie => ie.ItemId == incoming.ItemId);
|
||||||
await maybeExisting.Match(
|
await maybeExisting.Match(
|
||||||
async existing =>
|
async existing =>
|
||||||
@@ -234,7 +223,6 @@ namespace ErsatzTV.Core.Emby
|
|||||||
show.ShowMetadata.Head().Title,
|
show.ShowMetadata.Head().Title,
|
||||||
incoming.SeasonMetadata.Head().Title);
|
incoming.SeasonMetadata.Head().Title);
|
||||||
|
|
||||||
changed = true;
|
|
||||||
incoming.ShowId = show.Id;
|
incoming.ShowId = show.Id;
|
||||||
incoming.LibraryPathId = library.Paths.Head().Id;
|
incoming.LibraryPathId = library.Paths.Head().Id;
|
||||||
|
|
||||||
@@ -242,7 +230,6 @@ namespace ErsatzTV.Core.Emby
|
|||||||
},
|
},
|
||||||
async () =>
|
async () =>
|
||||||
{
|
{
|
||||||
changed = true;
|
|
||||||
incoming.ShowId = show.Id;
|
incoming.ShowId = show.Id;
|
||||||
incoming.LibraryPathId = library.Paths.Head().Id;
|
incoming.LibraryPathId = library.Paths.Head().Id;
|
||||||
|
|
||||||
@@ -254,68 +241,68 @@ namespace ErsatzTV.Core.Emby
|
|||||||
await _televisionRepository.AddSeason(incoming);
|
await _televisionRepository.AddSeason(incoming);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (changed)
|
List<EmbyItemEtag> existingEpisodes =
|
||||||
{
|
await _televisionRepository.GetExistingEpisodes(library, incoming.ItemId);
|
||||||
List<EmbyItemEtag> existingEpisodes =
|
|
||||||
await _televisionRepository.GetExistingEpisodes(library, incoming.ItemId);
|
|
||||||
|
|
||||||
Either<BaseError, List<EmbyEpisode>> maybeEpisodes =
|
Either<BaseError, List<EmbyEpisode>> maybeEpisodes =
|
||||||
await _embyApiClient.GetEpisodeLibraryItems(
|
await _embyApiClient.GetEpisodeLibraryItems(
|
||||||
address,
|
address,
|
||||||
apiKey,
|
apiKey,
|
||||||
library.MediaSourceId,
|
library.MediaSourceId,
|
||||||
incoming.ItemId);
|
incoming.ItemId);
|
||||||
|
|
||||||
await maybeEpisodes.Match(
|
await maybeEpisodes.Match(
|
||||||
async episodes =>
|
async episodes =>
|
||||||
|
{
|
||||||
|
var validEpisodes = new List<EmbyEpisode>();
|
||||||
|
foreach (EmbyEpisode episode in episodes)
|
||||||
{
|
{
|
||||||
var validEpisodes = new List<EmbyEpisode>();
|
string localPath = _pathReplacementService.GetReplacementEmbyPath(
|
||||||
foreach (EmbyEpisode episode in episodes)
|
|
||||||
{
|
|
||||||
string localPath = _pathReplacementService.GetReplacementEmbyPath(
|
|
||||||
pathReplacements,
|
|
||||||
episode.MediaVersions.Head().MediaFiles.Head().Path,
|
|
||||||
false);
|
|
||||||
|
|
||||||
if (!_localFileSystem.FileExists(localPath))
|
|
||||||
{
|
|
||||||
_logger.LogWarning(
|
|
||||||
"Skipping emby episode that does not exist at {Path}",
|
|
||||||
localPath);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
validEpisodes.Add(episode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await ProcessEpisodes(
|
|
||||||
show.ShowMetadata.Head().Title,
|
|
||||||
incoming.SeasonMetadata.Head().Title,
|
|
||||||
library,
|
|
||||||
ffprobePath,
|
|
||||||
pathReplacements,
|
pathReplacements,
|
||||||
incoming,
|
episode.MediaVersions.Head().MediaFiles.Head().Path,
|
||||||
existingEpisodes,
|
false);
|
||||||
validEpisodes);
|
|
||||||
|
|
||||||
var incomingEpisodeIds = episodes.Map(s => s.ItemId).ToList();
|
if (!_localFileSystem.FileExists(localPath))
|
||||||
var episodeIds = existingEpisodes
|
{
|
||||||
.Filter(i => !incomingEpisodeIds.Contains(i.ItemId))
|
_logger.LogWarning(
|
||||||
.Map(m => m.ItemId)
|
"Skipping emby episode that does not exist at {Path}",
|
||||||
.ToList();
|
localPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
validEpisodes.Add(episode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await ProcessEpisodes(
|
||||||
|
show.ShowMetadata.Head().Title,
|
||||||
|
incoming.SeasonMetadata.Head().Title,
|
||||||
|
library,
|
||||||
|
ffprobePath,
|
||||||
|
pathReplacements,
|
||||||
|
incoming,
|
||||||
|
existingEpisodes,
|
||||||
|
validEpisodes);
|
||||||
|
|
||||||
|
var incomingEpisodeIds = episodes.Map(s => s.ItemId).ToList();
|
||||||
|
var episodeIds = existingEpisodes
|
||||||
|
.Filter(i => !incomingEpisodeIds.Contains(i.ItemId))
|
||||||
|
.Map(m => m.ItemId)
|
||||||
|
.ToList();
|
||||||
|
List<int> missingEpisodeIds =
|
||||||
await _televisionRepository.RemoveMissingEpisodes(library, episodeIds);
|
await _televisionRepository.RemoveMissingEpisodes(library, episodeIds);
|
||||||
},
|
await _searchIndex.RemoveItems(missingEpisodeIds);
|
||||||
error =>
|
_searchIndex.Commit();
|
||||||
{
|
},
|
||||||
_logger.LogWarning(
|
error =>
|
||||||
"Error synchronizing emby library {Path}: {Error}",
|
{
|
||||||
library.Name,
|
_logger.LogWarning(
|
||||||
error.Value);
|
"Error synchronizing emby library {Path}: {Error}",
|
||||||
|
library.Name,
|
||||||
|
error.Value);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,7 +341,13 @@ namespace ErsatzTV.Core.Emby
|
|||||||
incoming.SeasonId = season.Id;
|
incoming.SeasonId = season.Id;
|
||||||
incoming.LibraryPathId = library.Paths.Head().Id;
|
incoming.LibraryPathId = library.Paths.Head().Id;
|
||||||
|
|
||||||
await _televisionRepository.Update(incoming);
|
Option<EmbyEpisode> updated = await _televisionRepository.Update(incoming);
|
||||||
|
if (updated.IsSome)
|
||||||
|
{
|
||||||
|
await _searchIndex.UpdateItems(
|
||||||
|
_searchRepository,
|
||||||
|
new List<MediaItem> { updated.ValueUnsafe() });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -379,7 +372,10 @@ namespace ErsatzTV.Core.Emby
|
|||||||
seasonName,
|
seasonName,
|
||||||
incoming.EpisodeNumber);
|
incoming.EpisodeNumber);
|
||||||
|
|
||||||
await _televisionRepository.AddEpisode(incoming);
|
if (await _televisionRepository.AddEpisode(incoming))
|
||||||
|
{
|
||||||
|
await _searchIndex.AddItems(_searchRepository, new List<MediaItem> { incoming });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Plex;
|
using ErsatzTV.Core.Plex;
|
||||||
@@ -46,7 +47,14 @@ namespace ErsatzTV.Core.Interfaces.Plex
|
|||||||
PlexConnection connection,
|
PlexConnection connection,
|
||||||
PlexServerAuthToken token);
|
PlexServerAuthToken token);
|
||||||
|
|
||||||
Task<Either<BaseError, MediaVersion>> GetStatistics(
|
Task<Either<BaseError, Tuple<MovieMetadata, MediaVersion>>> GetMovieMetadataAndStatistics(
|
||||||
|
PlexLibrary library,
|
||||||
|
string key,
|
||||||
|
PlexConnection connection,
|
||||||
|
PlexServerAuthToken token);
|
||||||
|
|
||||||
|
Task<Either<BaseError, Tuple<EpisodeMetadata, MediaVersion>>> GetEpisodeMetadataAndStatistics(
|
||||||
|
PlexLibrary library,
|
||||||
string key,
|
string key,
|
||||||
PlexConnection connection,
|
PlexConnection connection,
|
||||||
PlexServerAuthToken token);
|
PlexServerAuthToken token);
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
|||||||
Task<bool> AddSeason(EmbySeason season);
|
Task<bool> AddSeason(EmbySeason season);
|
||||||
Task<Unit> Update(EmbySeason season);
|
Task<Unit> Update(EmbySeason season);
|
||||||
Task<bool> AddEpisode(EmbyEpisode episode);
|
Task<bool> AddEpisode(EmbyEpisode episode);
|
||||||
Task<Unit> Update(EmbyEpisode episode);
|
Task<Option<EmbyEpisode>> Update(EmbyEpisode episode);
|
||||||
Task<List<int>> RemoveMissingShows(EmbyLibrary library, List<string> showIds);
|
Task<List<int>> RemoveMissingShows(EmbyLibrary library, List<string> showIds);
|
||||||
Task<Unit> RemoveMissingSeasons(EmbyLibrary library, List<string> seasonIds);
|
Task<Unit> RemoveMissingSeasons(EmbyLibrary library, List<string> seasonIds);
|
||||||
Task<Unit> RemoveMissingEpisodes(EmbyLibrary library, List<string> episodeIds);
|
Task<List<int>> RemoveMissingEpisodes(EmbyLibrary library, List<string> episodeIds);
|
||||||
Task<Unit> DeleteEmptySeasons(EmbyLibrary library);
|
Task<Unit> DeleteEmptySeasons(EmbyLibrary library);
|
||||||
Task<List<int>> DeleteEmptyShows(EmbyLibrary library);
|
Task<List<int>> DeleteEmptyShows(EmbyLibrary library);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
|||||||
Task<bool> AddSeason(JellyfinSeason season);
|
Task<bool> AddSeason(JellyfinSeason season);
|
||||||
Task<Unit> Update(JellyfinSeason season);
|
Task<Unit> Update(JellyfinSeason season);
|
||||||
Task<bool> AddEpisode(JellyfinEpisode episode);
|
Task<bool> AddEpisode(JellyfinEpisode episode);
|
||||||
Task<Unit> Update(JellyfinEpisode episode);
|
Task<Option<JellyfinEpisode>> Update(JellyfinEpisode episode);
|
||||||
Task<List<int>> RemoveMissingShows(JellyfinLibrary library, List<string> showIds);
|
Task<List<int>> RemoveMissingShows(JellyfinLibrary library, List<string> showIds);
|
||||||
Task<Unit> RemoveMissingSeasons(JellyfinLibrary library, List<string> seasonIds);
|
Task<Unit> RemoveMissingSeasons(JellyfinLibrary library, List<string> seasonIds);
|
||||||
Task<Unit> RemoveMissingEpisodes(JellyfinLibrary library, List<string> episodeIds);
|
Task<List<int>> RemoveMissingEpisodes(JellyfinLibrary library, List<string> episodeIds);
|
||||||
Task<Unit> DeleteEmptySeasons(JellyfinLibrary library);
|
Task<Unit> DeleteEmptySeasons(JellyfinLibrary library);
|
||||||
Task<List<int>> DeleteEmptyShows(JellyfinLibrary library);
|
Task<List<int>> DeleteEmptyShows(JellyfinLibrary library);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,5 +28,9 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
|||||||
Task<Unit> SetContentRating(ShowMetadata metadata, string contentRating);
|
Task<Unit> SetContentRating(ShowMetadata metadata, string contentRating);
|
||||||
Task<Unit> MarkAsExternal(MovieMetadata metadata);
|
Task<Unit> MarkAsExternal(MovieMetadata metadata);
|
||||||
Task<Unit> SetContentRating(MovieMetadata metadata, string contentRating);
|
Task<Unit> SetContentRating(MovieMetadata metadata, string contentRating);
|
||||||
|
Task<bool> RemoveGuid(MetadataGuid guid);
|
||||||
|
Task<bool> AddGuid(Domain.Metadata metadata, MetadataGuid guid);
|
||||||
|
Task<bool> RemoveDirector(Director director);
|
||||||
|
Task<bool> RemoveWriter(Writer writer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,9 +33,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
|||||||
Task<List<int>> RemoveMissingEmbyMovies(EmbyLibrary library, List<string> movieIds);
|
Task<List<int>> RemoveMissingEmbyMovies(EmbyLibrary library, List<string> movieIds);
|
||||||
Task<bool> AddEmby(EmbyMovie movie);
|
Task<bool> AddEmby(EmbyMovie movie);
|
||||||
Task<Option<EmbyMovie>> UpdateEmby(EmbyMovie movie);
|
Task<Option<EmbyMovie>> UpdateEmby(EmbyMovie movie);
|
||||||
Task<bool> RemoveDirector(Director director);
|
|
||||||
Task<bool> AddDirector(MovieMetadata metadata, Director director);
|
Task<bool> AddDirector(MovieMetadata metadata, Director director);
|
||||||
Task<bool> RemoveWriter(Writer writer);
|
|
||||||
Task<bool> AddWriter(MovieMetadata metadata, Writer writer);
|
Task<bool> AddWriter(MovieMetadata metadata, Writer writer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
|||||||
public interface ITelevisionRepository
|
public interface ITelevisionRepository
|
||||||
{
|
{
|
||||||
Task<bool> AllShowsExist(List<int> showIds);
|
Task<bool> AllShowsExist(List<int> showIds);
|
||||||
|
Task<bool> AllEpisodesExist(List<int> episodeIds);
|
||||||
Task<List<Show>> GetAllShows();
|
Task<List<Show>> GetAllShows();
|
||||||
Task<Option<Show>> GetShow(int showId);
|
Task<Option<Show>> GetShow(int showId);
|
||||||
Task<int> GetShowCount();
|
|
||||||
Task<List<ShowMetadata>> GetPagedShows(int pageNumber, int pageSize);
|
|
||||||
Task<List<ShowMetadata>> GetShowsForCards(List<int> ids);
|
Task<List<ShowMetadata>> GetShowsForCards(List<int> ids);
|
||||||
|
Task<List<EpisodeMetadata>> GetEpisodesForCards(List<int> ids);
|
||||||
Task<List<Episode>> GetShowItems(int showId);
|
Task<List<Episode>> GetShowItems(int showId);
|
||||||
Task<List<Season>> GetAllSeasons();
|
Task<List<Season>> GetAllSeasons();
|
||||||
Task<Option<Season>> GetSeason(int seasonId);
|
Task<Option<Season>> GetSeason(int seasonId);
|
||||||
@@ -46,7 +46,9 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
|||||||
Task<bool> AddActor(EpisodeMetadata metadata, Actor actor);
|
Task<bool> AddActor(EpisodeMetadata metadata, Actor actor);
|
||||||
Task<List<int>> RemoveMissingPlexShows(PlexLibrary library, List<string> showKeys);
|
Task<List<int>> RemoveMissingPlexShows(PlexLibrary library, List<string> showKeys);
|
||||||
Task<Unit> RemoveMissingPlexSeasons(string showKey, List<string> seasonKeys);
|
Task<Unit> RemoveMissingPlexSeasons(string showKey, List<string> seasonKeys);
|
||||||
Task<Unit> RemoveMissingPlexEpisodes(string seasonKey, List<string> episodeKeys);
|
Task<List<int>> RemoveMissingPlexEpisodes(string seasonKey, List<string> episodeKeys);
|
||||||
Task<Unit> SetEpisodeNumber(Episode episode, int episodeNumber);
|
Task<Unit> SetEpisodeNumber(Episode episode, int episodeNumber);
|
||||||
|
Task<bool> AddDirector(EpisodeMetadata metadata, Director director);
|
||||||
|
Task<bool> AddWriter(EpisodeMetadata metadata, Writer writer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,8 +120,6 @@ namespace ErsatzTV.Core.Jellyfin
|
|||||||
decimal percentCompletion = (decimal) shows.IndexOf(incoming) / shows.Count;
|
decimal percentCompletion = (decimal) shows.IndexOf(incoming) / shows.Count;
|
||||||
await _mediator.Publish(new LibraryScanProgress(library.Id, percentCompletion));
|
await _mediator.Publish(new LibraryScanProgress(library.Id, percentCompletion));
|
||||||
|
|
||||||
var changed = false;
|
|
||||||
|
|
||||||
Option<JellyfinItemEtag> maybeExisting = existingShows.Find(ie => ie.ItemId == incoming.ItemId);
|
Option<JellyfinItemEtag> maybeExisting = existingShows.Find(ie => ie.ItemId == incoming.ItemId);
|
||||||
await maybeExisting.Match(
|
await maybeExisting.Match(
|
||||||
async existing =>
|
async existing =>
|
||||||
@@ -135,7 +133,6 @@ namespace ErsatzTV.Core.Jellyfin
|
|||||||
"UPDATE: Etag has changed for show {Show}",
|
"UPDATE: Etag has changed for show {Show}",
|
||||||
incoming.ShowMetadata.Head().Title);
|
incoming.ShowMetadata.Head().Title);
|
||||||
|
|
||||||
changed = true;
|
|
||||||
incoming.LibraryPathId = library.Paths.Head().Id;
|
incoming.LibraryPathId = library.Paths.Head().Id;
|
||||||
|
|
||||||
Option<JellyfinShow> updated = await _televisionRepository.Update(incoming);
|
Option<JellyfinShow> updated = await _televisionRepository.Update(incoming);
|
||||||
@@ -148,7 +145,6 @@ namespace ErsatzTV.Core.Jellyfin
|
|||||||
},
|
},
|
||||||
async () =>
|
async () =>
|
||||||
{
|
{
|
||||||
changed = true;
|
|
||||||
incoming.LibraryPathId = library.Paths.Head().Id;
|
incoming.LibraryPathId = library.Paths.Head().Id;
|
||||||
|
|
||||||
// _logger.LogDebug("INSERT: Item id is new for show {Show}", incoming.ShowMetadata.Head().Title);
|
// _logger.LogDebug("INSERT: Item id is new for show {Show}", incoming.ShowMetadata.Head().Title);
|
||||||
@@ -159,50 +155,45 @@ namespace ErsatzTV.Core.Jellyfin
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (changed)
|
List<JellyfinItemEtag> existingSeasons =
|
||||||
{
|
await _televisionRepository.GetExistingSeasons(library, incoming.ItemId);
|
||||||
List<JellyfinItemEtag> existingSeasons =
|
|
||||||
await _televisionRepository.GetExistingSeasons(library, incoming.ItemId);
|
|
||||||
|
|
||||||
Either<BaseError, List<JellyfinSeason>> maybeSeasons =
|
Either<BaseError, List<JellyfinSeason>> maybeSeasons =
|
||||||
await _jellyfinApiClient.GetSeasonLibraryItems(
|
await _jellyfinApiClient.GetSeasonLibraryItems(
|
||||||
|
address,
|
||||||
|
apiKey,
|
||||||
|
library.MediaSourceId,
|
||||||
|
incoming.ItemId);
|
||||||
|
|
||||||
|
await maybeSeasons.Match(
|
||||||
|
async seasons =>
|
||||||
|
{
|
||||||
|
await ProcessSeasons(
|
||||||
address,
|
address,
|
||||||
apiKey,
|
apiKey,
|
||||||
library.MediaSourceId,
|
library,
|
||||||
incoming.ItemId);
|
ffprobePath,
|
||||||
|
pathReplacements,
|
||||||
|
incoming,
|
||||||
|
existingSeasons,
|
||||||
|
seasons);
|
||||||
|
|
||||||
await maybeSeasons.Match(
|
var incomingSeasonIds = seasons.Map(s => s.ItemId).ToList();
|
||||||
async seasons =>
|
var seasonIds = existingSeasons
|
||||||
{
|
.Filter(i => !incomingSeasonIds.Contains(i.ItemId))
|
||||||
await ProcessSeasons(
|
.Map(m => m.ItemId)
|
||||||
address,
|
.ToList();
|
||||||
apiKey,
|
await _televisionRepository.RemoveMissingSeasons(library, seasonIds);
|
||||||
library,
|
},
|
||||||
ffprobePath,
|
error =>
|
||||||
pathReplacements,
|
{
|
||||||
incoming,
|
_logger.LogWarning(
|
||||||
existingSeasons,
|
"Error synchronizing jellyfin library {Path}: {Error}",
|
||||||
seasons);
|
library.Name,
|
||||||
|
error.Value);
|
||||||
|
|
||||||
await _searchIndex.UpdateItems(_searchRepository, new List<MediaItem> { incoming });
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
var incomingSeasonIds = seasons.Map(s => s.ItemId).ToList();
|
|
||||||
var seasonIds = existingSeasons
|
|
||||||
.Filter(i => !incomingSeasonIds.Contains(i.ItemId))
|
|
||||||
.Map(m => m.ItemId)
|
|
||||||
.ToList();
|
|
||||||
await _televisionRepository.RemoveMissingSeasons(library, seasonIds);
|
|
||||||
},
|
|
||||||
error =>
|
|
||||||
{
|
|
||||||
_logger.LogWarning(
|
|
||||||
"Error synchronizing jellyfin library {Path}: {Error}",
|
|
||||||
library.Name,
|
|
||||||
error.Value);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,8 +209,6 @@ namespace ErsatzTV.Core.Jellyfin
|
|||||||
{
|
{
|
||||||
foreach (JellyfinSeason incoming in seasons)
|
foreach (JellyfinSeason incoming in seasons)
|
||||||
{
|
{
|
||||||
var changed = false;
|
|
||||||
|
|
||||||
Option<JellyfinItemEtag> maybeExisting = existingSeasons.Find(ie => ie.ItemId == incoming.ItemId);
|
Option<JellyfinItemEtag> maybeExisting = existingSeasons.Find(ie => ie.ItemId == incoming.ItemId);
|
||||||
await maybeExisting.Match(
|
await maybeExisting.Match(
|
||||||
async existing =>
|
async existing =>
|
||||||
@@ -234,7 +223,6 @@ namespace ErsatzTV.Core.Jellyfin
|
|||||||
show.ShowMetadata.Head().Title,
|
show.ShowMetadata.Head().Title,
|
||||||
incoming.SeasonMetadata.Head().Title);
|
incoming.SeasonMetadata.Head().Title);
|
||||||
|
|
||||||
changed = true;
|
|
||||||
incoming.ShowId = show.Id;
|
incoming.ShowId = show.Id;
|
||||||
incoming.LibraryPathId = library.Paths.Head().Id;
|
incoming.LibraryPathId = library.Paths.Head().Id;
|
||||||
|
|
||||||
@@ -242,7 +230,6 @@ namespace ErsatzTV.Core.Jellyfin
|
|||||||
},
|
},
|
||||||
async () =>
|
async () =>
|
||||||
{
|
{
|
||||||
changed = true;
|
|
||||||
incoming.ShowId = show.Id;
|
incoming.ShowId = show.Id;
|
||||||
incoming.LibraryPathId = library.Paths.Head().Id;
|
incoming.LibraryPathId = library.Paths.Head().Id;
|
||||||
|
|
||||||
@@ -254,68 +241,69 @@ namespace ErsatzTV.Core.Jellyfin
|
|||||||
await _televisionRepository.AddSeason(incoming);
|
await _televisionRepository.AddSeason(incoming);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (changed)
|
List<JellyfinItemEtag> existingEpisodes =
|
||||||
{
|
await _televisionRepository.GetExistingEpisodes(library, incoming.ItemId);
|
||||||
List<JellyfinItemEtag> existingEpisodes =
|
|
||||||
await _televisionRepository.GetExistingEpisodes(library, incoming.ItemId);
|
|
||||||
|
|
||||||
Either<BaseError, List<JellyfinEpisode>> maybeEpisodes =
|
Either<BaseError, List<JellyfinEpisode>> maybeEpisodes =
|
||||||
await _jellyfinApiClient.GetEpisodeLibraryItems(
|
await _jellyfinApiClient.GetEpisodeLibraryItems(
|
||||||
address,
|
address,
|
||||||
apiKey,
|
apiKey,
|
||||||
library.MediaSourceId,
|
library.MediaSourceId,
|
||||||
incoming.ItemId);
|
incoming.ItemId);
|
||||||
|
|
||||||
await maybeEpisodes.Match(
|
await maybeEpisodes.Match(
|
||||||
async episodes =>
|
async episodes =>
|
||||||
|
{
|
||||||
|
var validEpisodes = new List<JellyfinEpisode>();
|
||||||
|
foreach (JellyfinEpisode episode in episodes)
|
||||||
{
|
{
|
||||||
var validEpisodes = new List<JellyfinEpisode>();
|
string localPath = _pathReplacementService.GetReplacementJellyfinPath(
|
||||||
foreach (JellyfinEpisode episode in episodes)
|
|
||||||
{
|
|
||||||
string localPath = _pathReplacementService.GetReplacementJellyfinPath(
|
|
||||||
pathReplacements,
|
|
||||||
episode.MediaVersions.Head().MediaFiles.Head().Path,
|
|
||||||
false);
|
|
||||||
|
|
||||||
if (!_localFileSystem.FileExists(localPath))
|
|
||||||
{
|
|
||||||
_logger.LogWarning(
|
|
||||||
"Skipping jellyfin episode that does not exist at {Path}",
|
|
||||||
localPath);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
validEpisodes.Add(episode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await ProcessEpisodes(
|
|
||||||
show.ShowMetadata.Head().Title,
|
|
||||||
incoming.SeasonMetadata.Head().Title,
|
|
||||||
library,
|
|
||||||
ffprobePath,
|
|
||||||
pathReplacements,
|
pathReplacements,
|
||||||
incoming,
|
episode.MediaVersions.Head().MediaFiles.Head().Path,
|
||||||
existingEpisodes,
|
false);
|
||||||
validEpisodes);
|
|
||||||
|
|
||||||
var incomingEpisodeIds = episodes.Map(s => s.ItemId).ToList();
|
if (!_localFileSystem.FileExists(localPath))
|
||||||
var episodeIds = existingEpisodes
|
{
|
||||||
.Filter(i => !incomingEpisodeIds.Contains(i.ItemId))
|
_logger.LogWarning(
|
||||||
.Map(m => m.ItemId)
|
"Skipping jellyfin episode that does not exist at {Path}",
|
||||||
.ToList();
|
localPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
validEpisodes.Add(episode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await ProcessEpisodes(
|
||||||
|
show.ShowMetadata.Head().Title,
|
||||||
|
incoming.SeasonMetadata.Head().Title,
|
||||||
|
library,
|
||||||
|
ffprobePath,
|
||||||
|
pathReplacements,
|
||||||
|
incoming,
|
||||||
|
existingEpisodes,
|
||||||
|
validEpisodes);
|
||||||
|
|
||||||
|
var incomingEpisodeIds = episodes.Map(s => s.ItemId).ToList();
|
||||||
|
var episodeIds = existingEpisodes
|
||||||
|
.Filter(i => !incomingEpisodeIds.Contains(i.ItemId))
|
||||||
|
.Map(m => m.ItemId)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
List<int> missingEpisodeIds =
|
||||||
await _televisionRepository.RemoveMissingEpisodes(library, episodeIds);
|
await _televisionRepository.RemoveMissingEpisodes(library, episodeIds);
|
||||||
},
|
await _searchIndex.RemoveItems(missingEpisodeIds);
|
||||||
error =>
|
_searchIndex.Commit();
|
||||||
{
|
},
|
||||||
_logger.LogWarning(
|
error =>
|
||||||
"Error synchronizing jellyfin library {Path}: {Error}",
|
{
|
||||||
library.Name,
|
_logger.LogWarning(
|
||||||
error.Value);
|
"Error synchronizing jellyfin library {Path}: {Error}",
|
||||||
|
library.Name,
|
||||||
|
error.Value);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,7 +342,13 @@ namespace ErsatzTV.Core.Jellyfin
|
|||||||
incoming.SeasonId = season.Id;
|
incoming.SeasonId = season.Id;
|
||||||
incoming.LibraryPathId = library.Paths.Head().Id;
|
incoming.LibraryPathId = library.Paths.Head().Id;
|
||||||
|
|
||||||
await _televisionRepository.Update(incoming);
|
Option<JellyfinEpisode> updated = await _televisionRepository.Update(incoming);
|
||||||
|
if (updated.IsSome)
|
||||||
|
{
|
||||||
|
await _searchIndex.UpdateItems(
|
||||||
|
_searchRepository,
|
||||||
|
new List<MediaItem> { updated.ValueUnsafe() });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -379,7 +373,10 @@ namespace ErsatzTV.Core.Jellyfin
|
|||||||
seasonName,
|
seasonName,
|
||||||
incoming.EpisodeNumber);
|
incoming.EpisodeNumber);
|
||||||
|
|
||||||
await _televisionRepository.AddEpisode(incoming);
|
if (await _televisionRepository.AddEpisode(incoming))
|
||||||
|
{
|
||||||
|
await _searchIndex.AddItems(_searchRepository, new List<MediaItem> { incoming });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -212,6 +212,66 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
(_, _) => Task.FromResult(false),
|
(_, _) => Task.FromResult(false),
|
||||||
_televisionRepository.AddActor);
|
_televisionRepository.AddActor);
|
||||||
|
|
||||||
|
foreach (Director director in existing.Directors
|
||||||
|
.Filter(d => metadata.Directors.All(d2 => d2.Name != d.Name)).ToList())
|
||||||
|
{
|
||||||
|
existing.Directors.Remove(director);
|
||||||
|
if (await _metadataRepository.RemoveDirector(director))
|
||||||
|
{
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Director director in metadata.Directors
|
||||||
|
.Filter(d => existing.Directors.All(d2 => d2.Name != d.Name)).ToList())
|
||||||
|
{
|
||||||
|
existing.Directors.Add(director);
|
||||||
|
if (await _televisionRepository.AddDirector(existing, director))
|
||||||
|
{
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Writer writer in existing.Writers
|
||||||
|
.Filter(w => metadata.Writers.All(w2 => w2.Name != w.Name)).ToList())
|
||||||
|
{
|
||||||
|
existing.Writers.Remove(writer);
|
||||||
|
if (await _metadataRepository.RemoveWriter(writer))
|
||||||
|
{
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Writer writer in metadata.Writers
|
||||||
|
.Filter(w => existing.Writers.All(w2 => w2.Name != w.Name)).ToList())
|
||||||
|
{
|
||||||
|
existing.Writers.Add(writer);
|
||||||
|
if (await _televisionRepository.AddWriter(existing, writer))
|
||||||
|
{
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (MetadataGuid guid in existing.Guids
|
||||||
|
.Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid)).ToList())
|
||||||
|
{
|
||||||
|
existing.Guids.Remove(guid);
|
||||||
|
if (await _metadataRepository.RemoveGuid(guid))
|
||||||
|
{
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (MetadataGuid guid in metadata.Guids
|
||||||
|
.Filter(g => existing.Guids.All(g2 => g2.Guid != g.Guid)).ToList())
|
||||||
|
{
|
||||||
|
existing.Guids.Add(guid);
|
||||||
|
if (await _metadataRepository.AddGuid(existing, guid))
|
||||||
|
{
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return await _metadataRepository.Update(existing) || updated;
|
return await _metadataRepository.Update(existing) || updated;
|
||||||
},
|
},
|
||||||
async () =>
|
async () =>
|
||||||
@@ -261,17 +321,17 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
_movieRepository.AddActor);
|
_movieRepository.AddActor);
|
||||||
|
|
||||||
foreach (Director director in existing.Directors
|
foreach (Director director in existing.Directors
|
||||||
.Filter(g => metadata.Directors.All(g2 => g2.Name != g.Name)).ToList())
|
.Filter(d => metadata.Directors.All(d2 => d2.Name != d.Name)).ToList())
|
||||||
{
|
{
|
||||||
existing.Directors.Remove(director);
|
existing.Directors.Remove(director);
|
||||||
if (await _movieRepository.RemoveDirector(director))
|
if (await _metadataRepository.RemoveDirector(director))
|
||||||
{
|
{
|
||||||
updated = true;
|
updated = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (Director director in metadata.Directors
|
foreach (Director director in metadata.Directors
|
||||||
.Filter(g => existing.Directors.All(g2 => g2.Name != g.Name)).ToList())
|
.Filter(d => existing.Directors.All(d2 => d2.Name != d.Name)).ToList())
|
||||||
{
|
{
|
||||||
existing.Directors.Add(director);
|
existing.Directors.Add(director);
|
||||||
if (await _movieRepository.AddDirector(existing, director))
|
if (await _movieRepository.AddDirector(existing, director))
|
||||||
@@ -281,17 +341,17 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach (Writer writer in existing.Writers
|
foreach (Writer writer in existing.Writers
|
||||||
.Filter(g => metadata.Writers.All(g2 => g2.Name != g.Name)).ToList())
|
.Filter(w => metadata.Writers.All(w2 => w2.Name != w.Name)).ToList())
|
||||||
{
|
{
|
||||||
existing.Writers.Remove(writer);
|
existing.Writers.Remove(writer);
|
||||||
if (await _movieRepository.RemoveWriter(writer))
|
if (await _metadataRepository.RemoveWriter(writer))
|
||||||
{
|
{
|
||||||
updated = true;
|
updated = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (Writer writer in metadata.Writers
|
foreach (Writer writer in metadata.Writers
|
||||||
.Filter(g => existing.Writers.All(g2 => g2.Name != g.Name)).ToList())
|
.Filter(w => existing.Writers.All(w2 => w2.Name != w.Name)).ToList())
|
||||||
{
|
{
|
||||||
existing.Writers.Add(writer);
|
existing.Writers.Add(writer);
|
||||||
if (await _movieRepository.AddWriter(existing, writer))
|
if (await _movieRepository.AddWriter(existing, writer))
|
||||||
@@ -300,6 +360,26 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (MetadataGuid guid in existing.Guids
|
||||||
|
.Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid)).ToList())
|
||||||
|
{
|
||||||
|
existing.Guids.Remove(guid);
|
||||||
|
if (await _metadataRepository.RemoveGuid(guid))
|
||||||
|
{
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (MetadataGuid guid in metadata.Guids
|
||||||
|
.Filter(g => existing.Guids.All(g2 => g2.Guid != g.Guid)).ToList())
|
||||||
|
{
|
||||||
|
existing.Guids.Add(guid);
|
||||||
|
if (await _metadataRepository.AddGuid(existing, guid))
|
||||||
|
{
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return await _metadataRepository.Update(existing) || updated;
|
return await _metadataRepository.Update(existing) || updated;
|
||||||
},
|
},
|
||||||
async () =>
|
async () =>
|
||||||
@@ -345,6 +425,26 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
_televisionRepository.AddStudio,
|
_televisionRepository.AddStudio,
|
||||||
_televisionRepository.AddActor);
|
_televisionRepository.AddActor);
|
||||||
|
|
||||||
|
foreach (MetadataGuid guid in existing.Guids
|
||||||
|
.Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid)).ToList())
|
||||||
|
{
|
||||||
|
existing.Guids.Remove(guid);
|
||||||
|
if (await _metadataRepository.RemoveGuid(guid))
|
||||||
|
{
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (MetadataGuid guid in metadata.Guids
|
||||||
|
.Filter(g => existing.Guids.All(g2 => g2.Guid != g.Guid)).ToList())
|
||||||
|
{
|
||||||
|
existing.Guids.Add(guid);
|
||||||
|
if (await _metadataRepository.AddGuid(existing, guid))
|
||||||
|
{
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return await _metadataRepository.Update(existing) || updated;
|
return await _metadataRepository.Update(existing) || updated;
|
||||||
},
|
},
|
||||||
async () =>
|
async () =>
|
||||||
@@ -522,7 +622,10 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
Genres = nfo.Genres.Map(g => new Genre { Name = g }).ToList(),
|
Genres = nfo.Genres.Map(g => new Genre { Name = g }).ToList(),
|
||||||
Tags = nfo.Tags.Map(t => new Tag { Name = t }).ToList(),
|
Tags = nfo.Tags.Map(t => new Tag { Name = t }).ToList(),
|
||||||
Studios = nfo.Studios.Map(s => new Studio { Name = s }).ToList(),
|
Studios = nfo.Studios.Map(s => new Studio { Name = s }).ToList(),
|
||||||
Actors = Actors(nfo.Actors, dateAdded, dateUpdated)
|
Actors = Actors(nfo.Actors, dateAdded, dateUpdated),
|
||||||
|
Guids = nfo.UniqueIds
|
||||||
|
.Map(id => new MetadataGuid { Guid = $"{id.Type}://{id.Guid}" })
|
||||||
|
.ToList()
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
None);
|
None);
|
||||||
@@ -582,7 +685,12 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
Title = nfo.Title,
|
Title = nfo.Title,
|
||||||
ReleaseDate = GetAired(0, nfo.Aired),
|
ReleaseDate = GetAired(0, nfo.Aired),
|
||||||
Plot = nfo.Plot,
|
Plot = nfo.Plot,
|
||||||
Actors = Actors(nfo.Actors, dateAdded, dateUpdated)
|
Actors = Actors(nfo.Actors, dateAdded, dateUpdated),
|
||||||
|
Guids = nfo.UniqueIds
|
||||||
|
.Map(id => new MetadataGuid { Guid = $"{id.Type}://{id.Guid}" })
|
||||||
|
.ToList(),
|
||||||
|
Directors = nfo.Directors.Map(d => new Director { Name = d }).ToList(),
|
||||||
|
Writers = nfo.Writers.Map(w => new Writer { Name = w }).ToList()
|
||||||
};
|
};
|
||||||
return Tuple(metadata, nfo.Episode);
|
return Tuple(metadata, nfo.Episode);
|
||||||
},
|
},
|
||||||
@@ -624,7 +732,10 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
Studios = nfo.Studios.Map(s => new Studio { Name = s }).ToList(),
|
Studios = nfo.Studios.Map(s => new Studio { Name = s }).ToList(),
|
||||||
Actors = Actors(nfo.Actors, dateAdded, dateUpdated),
|
Actors = Actors(nfo.Actors, dateAdded, dateUpdated),
|
||||||
Directors = nfo.Directors.Map(d => new Director { Name = d }).ToList(),
|
Directors = nfo.Directors.Map(d => new Director { Name = d }).ToList(),
|
||||||
Writers = nfo.Writers.Map(w => new Writer { Name = w }).ToList()
|
Writers = nfo.Writers.Map(w => new Writer { Name = w }).ToList(),
|
||||||
|
Guids = nfo.UniqueIds
|
||||||
|
.Map(id => new MetadataGuid { Guid = $"{id.Type}://{id.Guid}" })
|
||||||
|
.ToList()
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
None);
|
None);
|
||||||
|
|||||||
@@ -45,5 +45,8 @@ namespace ErsatzTV.Core.Metadata.Nfo
|
|||||||
|
|
||||||
[XmlElement("director")]
|
[XmlElement("director")]
|
||||||
public List<string> Directors { get; set; }
|
public List<string> Directors { get; set; }
|
||||||
|
|
||||||
|
[XmlElement("uniqueid")]
|
||||||
|
public List<UniqueIdNfo> UniqueIds { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,5 +29,14 @@ namespace ErsatzTV.Core.Metadata.Nfo
|
|||||||
|
|
||||||
[XmlElement("actor")]
|
[XmlElement("actor")]
|
||||||
public List<ActorNfo> Actors { get; set; }
|
public List<ActorNfo> Actors { get; set; }
|
||||||
|
|
||||||
|
[XmlElement("credits")]
|
||||||
|
public List<string> Writers { get; set; }
|
||||||
|
|
||||||
|
[XmlElement("director")]
|
||||||
|
public List<string> Directors { get; set; }
|
||||||
|
|
||||||
|
[XmlElement("uniqueid")]
|
||||||
|
public List<UniqueIdNfo> UniqueIds { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,5 +38,8 @@ namespace ErsatzTV.Core.Metadata.Nfo
|
|||||||
|
|
||||||
[XmlElement("actor")]
|
[XmlElement("actor")]
|
||||||
public List<ActorNfo> Actors { get; set; }
|
public List<ActorNfo> Actors { get; set; }
|
||||||
|
|
||||||
|
[XmlElement("uniqueid")]
|
||||||
|
public List<UniqueIdNfo> UniqueIds { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Metadata.Nfo
|
||||||
|
{
|
||||||
|
public class UniqueIdNfo
|
||||||
|
{
|
||||||
|
[XmlAttribute("default")]
|
||||||
|
public bool Default { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("type")]
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
[XmlText]
|
||||||
|
public string Guid { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -209,10 +209,20 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
.BindT(UpdateMetadata)
|
.BindT(UpdateMetadata)
|
||||||
.BindT(UpdateThumbnail);
|
.BindT(UpdateThumbnail);
|
||||||
|
|
||||||
maybeEpisode.IfLeft(
|
await maybeEpisode.Match(
|
||||||
error => _logger.LogWarning("Error processing episode at {Path}: {Error}", file, error.Value));
|
async episode =>
|
||||||
|
{
|
||||||
|
await _searchIndex.UpdateItems(_searchRepository, new List<MediaItem> { episode });
|
||||||
|
},
|
||||||
|
error =>
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Error processing episode at {Path}: {Error}", file, error.Value);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: remove missing episodes?
|
||||||
|
|
||||||
return Unit.Default;
|
return Unit.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
@@ -63,7 +64,7 @@ namespace ErsatzTV.Core.Plex
|
|||||||
// TODO: figure out how to rebuild playlists
|
// TODO: figure out how to rebuild playlists
|
||||||
Either<BaseError, MediaItemScanResult<PlexMovie>> maybeMovie = await _movieRepository
|
Either<BaseError, MediaItemScanResult<PlexMovie>> maybeMovie = await _movieRepository
|
||||||
.GetOrAdd(library, incoming)
|
.GetOrAdd(library, incoming)
|
||||||
.BindT(existing => UpdateStatistics(existing, incoming, connection, token))
|
.BindT(existing => UpdateStatistics(existing, incoming, library, connection, token))
|
||||||
.BindT(existing => UpdateMetadata(existing, incoming, library, connection, token))
|
.BindT(existing => UpdateMetadata(existing, incoming, library, connection, token))
|
||||||
.BindT(existing => UpdateArtwork(existing, incoming));
|
.BindT(existing => UpdateArtwork(existing, incoming));
|
||||||
|
|
||||||
@@ -114,6 +115,7 @@ namespace ErsatzTV.Core.Plex
|
|||||||
private async Task<Either<BaseError, MediaItemScanResult<PlexMovie>>> UpdateStatistics(
|
private async Task<Either<BaseError, MediaItemScanResult<PlexMovie>>> UpdateStatistics(
|
||||||
MediaItemScanResult<PlexMovie> result,
|
MediaItemScanResult<PlexMovie> result,
|
||||||
PlexMovie incoming,
|
PlexMovie incoming,
|
||||||
|
PlexLibrary library,
|
||||||
PlexConnection connection,
|
PlexConnection connection,
|
||||||
PlexServerAuthToken token)
|
PlexServerAuthToken token)
|
||||||
{
|
{
|
||||||
@@ -123,12 +125,18 @@ namespace ErsatzTV.Core.Plex
|
|||||||
|
|
||||||
if (incomingVersion.DateUpdated > existingVersion.DateUpdated || !existingVersion.Streams.Any())
|
if (incomingVersion.DateUpdated > existingVersion.DateUpdated || !existingVersion.Streams.Any())
|
||||||
{
|
{
|
||||||
Either<BaseError, MediaVersion> maybeStatistics =
|
Either<BaseError, Tuple<MovieMetadata, MediaVersion>> maybeStatistics =
|
||||||
await _plexServerApiClient.GetStatistics(incoming.Key.Split("/").Last(), connection, token);
|
await _plexServerApiClient.GetMovieMetadataAndStatistics(
|
||||||
|
library,
|
||||||
|
incoming.Key.Split("/").Last(),
|
||||||
|
connection,
|
||||||
|
token);
|
||||||
|
|
||||||
await maybeStatistics.Match(
|
await maybeStatistics.Match(
|
||||||
async mediaVersion =>
|
async tuple =>
|
||||||
{
|
{
|
||||||
|
(MovieMetadata _, MediaVersion mediaVersion) = tuple;
|
||||||
|
|
||||||
_logger.LogDebug(
|
_logger.LogDebug(
|
||||||
"Refreshing {Attribute} from {Path}",
|
"Refreshing {Attribute} from {Path}",
|
||||||
"Plex Statistics",
|
"Plex Statistics",
|
||||||
@@ -259,7 +267,7 @@ namespace ErsatzTV.Core.Plex
|
|||||||
.ToList())
|
.ToList())
|
||||||
{
|
{
|
||||||
existingMetadata.Directors.Remove(director);
|
existingMetadata.Directors.Remove(director);
|
||||||
if (await _movieRepository.RemoveDirector(director))
|
if (await _metadataRepository.RemoveDirector(director))
|
||||||
{
|
{
|
||||||
result.IsUpdated = true;
|
result.IsUpdated = true;
|
||||||
}
|
}
|
||||||
@@ -281,7 +289,7 @@ namespace ErsatzTV.Core.Plex
|
|||||||
.ToList())
|
.ToList())
|
||||||
{
|
{
|
||||||
existingMetadata.Writers.Remove(writer);
|
existingMetadata.Writers.Remove(writer);
|
||||||
if (await _movieRepository.RemoveWriter(writer))
|
if (await _metadataRepository.RemoveWriter(writer))
|
||||||
{
|
{
|
||||||
result.IsUpdated = true;
|
result.IsUpdated = true;
|
||||||
}
|
}
|
||||||
@@ -298,6 +306,28 @@ namespace ErsatzTV.Core.Plex
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (MetadataGuid guid in existingMetadata.Guids
|
||||||
|
.Filter(g => fullMetadata.Guids.All(g2 => g2.Guid != g.Guid))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
existingMetadata.Guids.Remove(guid);
|
||||||
|
if (await _metadataRepository.RemoveGuid(guid))
|
||||||
|
{
|
||||||
|
result.IsUpdated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (MetadataGuid guid in fullMetadata.Guids
|
||||||
|
.Filter(g => existingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
existingMetadata.Guids.Add(guid);
|
||||||
|
if (await _metadataRepository.AddGuid(existingMetadata, guid))
|
||||||
|
{
|
||||||
|
result.IsUpdated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (fullMetadata.SortTitle != existingMetadata.SortTitle)
|
if (fullMetadata.SortTitle != existingMetadata.SortTitle)
|
||||||
{
|
{
|
||||||
existingMetadata.SortTitle = fullMetadata.SortTitle;
|
existingMetadata.SortTitle = fullMetadata.SortTitle;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
@@ -70,17 +71,7 @@ namespace ErsatzTV.Core.Plex
|
|||||||
await maybeShow.Match(
|
await maybeShow.Match(
|
||||||
async result =>
|
async result =>
|
||||||
{
|
{
|
||||||
if (result.IsAdded || incoming.ShowMetadata.Head().DateUpdated >
|
await ScanSeasons(library, result.Item, connection, token);
|
||||||
result.Item.ShowMetadata.Head().DateUpdated)
|
|
||||||
{
|
|
||||||
await ScanSeasons(library, result.Item, connection, token);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogDebug(
|
|
||||||
"Skipping Plex show that has not been updated: {Show}",
|
|
||||||
incoming.ShowMetadata.Head().Title);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.IsAdded)
|
if (result.IsAdded)
|
||||||
{
|
{
|
||||||
@@ -227,6 +218,28 @@ namespace ErsatzTV.Core.Plex
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (MetadataGuid guid in existingMetadata.Guids
|
||||||
|
.Filter(g => fullMetadata.Guids.All(g2 => g2.Guid != g.Guid))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
existingMetadata.Guids.Remove(guid);
|
||||||
|
if (await _metadataRepository.RemoveGuid(guid))
|
||||||
|
{
|
||||||
|
result.IsUpdated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (MetadataGuid guid in fullMetadata.Guids
|
||||||
|
.Filter(g => existingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
existingMetadata.Guids.Add(guid);
|
||||||
|
if (await _metadataRepository.AddGuid(existingMetadata, guid))
|
||||||
|
{
|
||||||
|
result.IsUpdated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (result.IsUpdated)
|
if (result.IsUpdated)
|
||||||
{
|
{
|
||||||
await _metadataRepository.MarkAsUpdated(existingMetadata, fullMetadata.DateUpdated);
|
await _metadataRepository.MarkAsUpdated(existingMetadata, fullMetadata.DateUpdated);
|
||||||
@@ -278,7 +291,7 @@ namespace ErsatzTV.Core.Plex
|
|||||||
// TODO: figure out how to rebuild playlists
|
// TODO: figure out how to rebuild playlists
|
||||||
Either<BaseError, PlexSeason> maybeSeason = await _televisionRepository
|
Either<BaseError, PlexSeason> maybeSeason = await _televisionRepository
|
||||||
.GetOrAddPlexSeason(plexMediaSourceLibrary, incoming)
|
.GetOrAddPlexSeason(plexMediaSourceLibrary, incoming)
|
||||||
.BindT(existing => UpdateArtwork(existing, incoming));
|
.BindT(existing => UpdateMetadataAndArtwork(existing, incoming));
|
||||||
|
|
||||||
await maybeSeason.Match(
|
await maybeSeason.Match(
|
||||||
async season => await ScanEpisodes(plexMediaSourceLibrary, season, connection, token),
|
async season => await ScanEpisodes(plexMediaSourceLibrary, season, connection, token),
|
||||||
@@ -308,13 +321,31 @@ namespace ErsatzTV.Core.Plex
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Either<BaseError, PlexSeason>> UpdateArtwork(PlexSeason existing, PlexSeason incoming)
|
private async Task<Either<BaseError, PlexSeason>> UpdateMetadataAndArtwork(
|
||||||
|
PlexSeason existing,
|
||||||
|
PlexSeason incoming)
|
||||||
{
|
{
|
||||||
SeasonMetadata existingMetadata = existing.SeasonMetadata.Head();
|
SeasonMetadata existingMetadata = existing.SeasonMetadata.Head();
|
||||||
SeasonMetadata incomingMetadata = incoming.SeasonMetadata.Head();
|
SeasonMetadata incomingMetadata = incoming.SeasonMetadata.Head();
|
||||||
|
|
||||||
if (incomingMetadata.DateUpdated > existingMetadata.DateUpdated)
|
if (incomingMetadata.DateUpdated > existingMetadata.DateUpdated)
|
||||||
{
|
{
|
||||||
|
foreach (MetadataGuid guid in existingMetadata.Guids
|
||||||
|
.Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
existingMetadata.Guids.Remove(guid);
|
||||||
|
await _metadataRepository.RemoveGuid(guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (MetadataGuid guid in incomingMetadata.Guids
|
||||||
|
.Filter(g => existingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
existingMetadata.Guids.Add(guid);
|
||||||
|
await _metadataRepository.AddGuid(existingMetadata, guid);
|
||||||
|
}
|
||||||
|
|
||||||
await UpdateArtworkIfNeeded(existingMetadata, incomingMetadata, ArtworkKind.Poster);
|
await UpdateArtworkIfNeeded(existingMetadata, incomingMetadata, ArtworkKind.Poster);
|
||||||
await _metadataRepository.MarkAsUpdated(existingMetadata, incomingMetadata.DateUpdated);
|
await _metadataRepository.MarkAsUpdated(existingMetadata, incomingMetadata.DateUpdated);
|
||||||
}
|
}
|
||||||
@@ -344,18 +375,34 @@ namespace ErsatzTV.Core.Plex
|
|||||||
// TODO: figure out how to rebuild playlists
|
// TODO: figure out how to rebuild playlists
|
||||||
Either<BaseError, PlexEpisode> maybeEpisode = await _televisionRepository
|
Either<BaseError, PlexEpisode> maybeEpisode = await _televisionRepository
|
||||||
.GetOrAddPlexEpisode(plexMediaSourceLibrary, incoming)
|
.GetOrAddPlexEpisode(plexMediaSourceLibrary, incoming)
|
||||||
.BindT(existing => UpdateStatistics(existing, incoming, connection, token))
|
.BindT(
|
||||||
|
existing => UpdateMetadataAndStatistics(
|
||||||
|
existing,
|
||||||
|
incoming,
|
||||||
|
plexMediaSourceLibrary,
|
||||||
|
connection,
|
||||||
|
token))
|
||||||
.BindT(existing => UpdateArtwork(existing, incoming));
|
.BindT(existing => UpdateArtwork(existing, incoming));
|
||||||
|
|
||||||
maybeEpisode.IfLeft(
|
await maybeEpisode.Match(
|
||||||
error => _logger.LogWarning(
|
async episode =>
|
||||||
"Error processing plex episode at {Key}: {Error}",
|
{
|
||||||
incoming.Key,
|
await _searchIndex.UpdateItems(_searchRepository, new List<MediaItem> { episode });
|
||||||
error.Value));
|
},
|
||||||
|
error =>
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Error processing plex episode at {Key}: {Error}",
|
||||||
|
incoming.Key,
|
||||||
|
error.Value);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var episodeKeys = episodeEntries.Map(s => s.Key).ToList();
|
var episodeKeys = episodeEntries.Map(s => s.Key).ToList();
|
||||||
await _televisionRepository.RemoveMissingPlexEpisodes(season.Key, episodeKeys);
|
List<int> ids = await _televisionRepository.RemoveMissingPlexEpisodes(season.Key, episodeKeys);
|
||||||
|
await _searchIndex.RemoveItems(ids);
|
||||||
|
_searchIndex.Commit();
|
||||||
|
|
||||||
return Unit.Default;
|
return Unit.Default;
|
||||||
},
|
},
|
||||||
@@ -370,9 +417,10 @@ namespace ErsatzTV.Core.Plex
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Either<BaseError, PlexEpisode>> UpdateStatistics(
|
private async Task<Either<BaseError, PlexEpisode>> UpdateMetadataAndStatistics(
|
||||||
PlexEpisode existing,
|
PlexEpisode existing,
|
||||||
PlexEpisode incoming,
|
PlexEpisode incoming,
|
||||||
|
PlexLibrary library,
|
||||||
PlexConnection connection,
|
PlexConnection connection,
|
||||||
PlexServerAuthToken token)
|
PlexServerAuthToken token)
|
||||||
{
|
{
|
||||||
@@ -381,12 +429,36 @@ namespace ErsatzTV.Core.Plex
|
|||||||
|
|
||||||
if (incomingVersion.DateUpdated > existingVersion.DateUpdated || !existingVersion.Streams.Any())
|
if (incomingVersion.DateUpdated > existingVersion.DateUpdated || !existingVersion.Streams.Any())
|
||||||
{
|
{
|
||||||
Either<BaseError, MediaVersion> maybeStatistics =
|
Either<BaseError, Tuple<EpisodeMetadata, MediaVersion>> maybeStatistics =
|
||||||
await _plexServerApiClient.GetStatistics(incoming.Key.Split("/").Last(), connection, token);
|
await _plexServerApiClient.GetEpisodeMetadataAndStatistics(
|
||||||
|
library,
|
||||||
|
incoming.Key.Split("/").Last(),
|
||||||
|
connection,
|
||||||
|
token);
|
||||||
|
|
||||||
await maybeStatistics.Match(
|
await maybeStatistics.Match(
|
||||||
async mediaVersion =>
|
async tuple =>
|
||||||
{
|
{
|
||||||
|
(EpisodeMetadata incomingMetadata, MediaVersion mediaVersion) = tuple;
|
||||||
|
|
||||||
|
EpisodeMetadata existingMetadata = existing.EpisodeMetadata.Head();
|
||||||
|
|
||||||
|
foreach (MetadataGuid guid in existingMetadata.Guids
|
||||||
|
.Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
existingMetadata.Guids.Remove(guid);
|
||||||
|
await _metadataRepository.RemoveGuid(guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (MetadataGuid guid in incomingMetadata.Guids
|
||||||
|
.Filter(g => existingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
existingMetadata.Guids.Add(guid);
|
||||||
|
await _metadataRepository.AddGuid(existingMetadata, guid);
|
||||||
|
}
|
||||||
|
|
||||||
existingVersion.SampleAspectRatio = mediaVersion.SampleAspectRatio;
|
existingVersion.SampleAspectRatio = mediaVersion.SampleAspectRatio;
|
||||||
existingVersion.VideoScanKind = mediaVersion.VideoScanKind;
|
existingVersion.VideoScanKind = mediaVersion.VideoScanKind;
|
||||||
existingVersion.DateUpdated = mediaVersion.DateUpdated;
|
existingVersion.DateUpdated = mediaVersion.DateUpdated;
|
||||||
@@ -399,7 +471,9 @@ namespace ErsatzTV.Core.Plex
|
|||||||
return Right<BaseError, PlexEpisode>(existing);
|
return Right<BaseError, PlexEpisode>(existing);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Either<BaseError, PlexEpisode>> UpdateArtwork(PlexEpisode existing, PlexEpisode incoming)
|
private async Task<Either<BaseError, PlexEpisode>> UpdateArtwork(
|
||||||
|
PlexEpisode existing,
|
||||||
|
PlexEpisode incoming)
|
||||||
{
|
{
|
||||||
EpisodeMetadata existingMetadata = existing.EpisodeMetadata.Head();
|
EpisodeMetadata existingMetadata = existing.EpisodeMetadata.Head();
|
||||||
EpisodeMetadata incomingMetadata = incoming.EpisodeMetadata.Head();
|
EpisodeMetadata incomingMetadata = incoming.EpisodeMetadata.Head();
|
||||||
|
|||||||
@@ -15,15 +15,27 @@ namespace ErsatzTV.Core.Scheduling
|
|||||||
var groups = new List<GroupedMediaItem>();
|
var groups = new List<GroupedMediaItem>();
|
||||||
GroupedMediaItem group = null;
|
GroupedMediaItem group = null;
|
||||||
var lastNumber = 0;
|
var lastNumber = 0;
|
||||||
|
|
||||||
|
void AddUngrouped(MediaItem item)
|
||||||
|
{
|
||||||
|
if (group != null && lastNumber != 0)
|
||||||
|
{
|
||||||
|
groups.Add(group);
|
||||||
|
group = null;
|
||||||
|
lastNumber = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
groups.Add(new GroupedMediaItem(item, null));
|
||||||
|
}
|
||||||
|
|
||||||
foreach (MediaItem item in sortedMediaItems)
|
foreach (MediaItem item in sortedMediaItems)
|
||||||
{
|
{
|
||||||
if (item is Episode e)
|
if (item is Episode e)
|
||||||
{
|
{
|
||||||
const string PATTERN = @"^.*\((\d+)\)( - .*)?$";
|
string numberString = FindPartNumber(e);
|
||||||
Match match = Regex.Match(e.EpisodeMetadata.Head().Title, PATTERN);
|
if (numberString != null)
|
||||||
if (match.Success)
|
|
||||||
{
|
{
|
||||||
var number = int.Parse(match.Groups[1].Value);
|
var number = int.Parse(numberString);
|
||||||
if (number <= lastNumber && group != null)
|
if (number <= lastNumber && group != null)
|
||||||
{
|
{
|
||||||
groups.Add(group);
|
groups.Add(group);
|
||||||
@@ -48,28 +60,20 @@ namespace ErsatzTV.Core.Scheduling
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// this should never happen
|
// this should never happen
|
||||||
throw new InvalidOperationException("Bad shuffle state");
|
throw new InvalidOperationException(
|
||||||
|
$"Bad shuffle state; unexpected number {number} after {lastNumber} with no existing group");
|
||||||
}
|
}
|
||||||
|
|
||||||
lastNumber = number;
|
lastNumber = number;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// this should never happen
|
AddUngrouped(item);
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"Bad shuffle state; unexpected number {number} after {lastNumber}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (group != null && lastNumber != 0)
|
AddUngrouped(item);
|
||||||
{
|
|
||||||
groups.Add(group);
|
|
||||||
group = null;
|
|
||||||
lastNumber = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
groups.Add(new GroupedMediaItem(item, null));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -86,6 +90,20 @@ namespace ErsatzTV.Core.Scheduling
|
|||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string FindPartNumber(Episode e)
|
||||||
|
{
|
||||||
|
const string PATTERN = @"^.*\((\d+)\)( - .*)?$";
|
||||||
|
Match match = Regex.Match(e.EpisodeMetadata.Head().Title, PATTERN);
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
return match.Groups[1].Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const string PATTERN_2 = @"^.*Part (\d+)$";
|
||||||
|
Match match2 = Regex.Match(e.EpisodeMetadata.Head().Title, PATTERN_2);
|
||||||
|
return match2.Success ? match2.Groups[1].Value : null;
|
||||||
|
}
|
||||||
|
|
||||||
public static IList<MediaItem> FlattenGroups(GroupedMediaItem[] copy, int mediaItemCount)
|
public static IList<MediaItem> FlattenGroups(GroupedMediaItem[] copy, int mediaItemCount)
|
||||||
{
|
{
|
||||||
var result = new MediaItem[mediaItemCount];
|
var result = new MediaItem[mediaItemCount];
|
||||||
|
|||||||
@@ -17,6 +17,18 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
|
|||||||
builder.HasMany(em => em.Actors)
|
builder.HasMany(em => em.Actors)
|
||||||
.WithOne()
|
.WithOne()
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasMany(mm => mm.Directors)
|
||||||
|
.WithOne()
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasMany(mm => mm.Writers)
|
||||||
|
.WithOne()
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasMany(mm => mm.Guids)
|
||||||
|
.WithOne()
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Data.Configurations
|
||||||
|
{
|
||||||
|
public class MetadataGuidConfiguration : IEntityTypeConfiguration<MetadataGuid>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<MetadataGuid> builder) => builder.ToTable("MetadataGuid");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,10 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
|
|||||||
builder.HasMany(mm => mm.Writers)
|
builder.HasMany(mm => mm.Writers)
|
||||||
.WithOne()
|
.WithOne()
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasMany(mm => mm.Guids)
|
||||||
|
.WithOne()
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
|
|||||||
builder.HasMany(em => em.Artwork)
|
builder.HasMany(em => em.Artwork)
|
||||||
.WithOne()
|
.WithOne()
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasMany(mm => mm.Guids)
|
||||||
|
.WithOne()
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
|
|||||||
builder.HasMany(sm => sm.Actors)
|
builder.HasMany(sm => sm.Actors)
|
||||||
.WithOne()
|
.WithOne()
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasMany(mm => mm.Guids)
|
||||||
|
.WithOne()
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
WHERE A.ArtistMetadataId IS NULL AND A.EpisodeMetadataId IS NULL
|
WHERE A.ArtistMetadataId IS NULL AND A.EpisodeMetadataId IS NULL
|
||||||
AND A.SeasonMetadataId IS NULL AND A.ShowMetadataId IS NULL
|
AND A.SeasonMetadataId IS NULL AND A.ShowMetadataId IS NULL
|
||||||
AND A.MovieMetadataId IS NULL AND A.MusicVideoMetadataId IS NULL
|
AND A.MovieMetadataId IS NULL AND A.MusicVideoMetadataId IS NULL
|
||||||
|
AND A.ChannelId IS NULL
|
||||||
AND NOT EXISTS (SELECT * FROM Actor WHERE Actor.ArtworkId = A.Id)")
|
AND NOT EXISTS (SELECT * FROM Actor WHERE Actor.ArtworkId = A.Id)")
|
||||||
.Map(result => result.ToList());
|
.Map(result => result.ToList());
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.ThenInclude(mm => mm.Actors)
|
.ThenInclude(mm => mm.Actors)
|
||||||
.Include(m => m.ShowMetadata)
|
.Include(m => m.ShowMetadata)
|
||||||
.ThenInclude(mm => mm.Artwork)
|
.ThenInclude(mm => mm.Artwork)
|
||||||
|
.Include(m => m.ShowMetadata)
|
||||||
|
.ThenInclude(mm => mm.Guids)
|
||||||
.Filter(m => m.ItemId == show.ItemId)
|
.Filter(m => m.ItemId == show.ItemId)
|
||||||
.OrderBy(m => m.ItemId)
|
.OrderBy(m => m.ItemId)
|
||||||
.SingleOrDefaultAsync();
|
.SingleOrDefaultAsync();
|
||||||
@@ -177,6 +179,21 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
metadata.Actors.Add(actor);
|
metadata.Actors.Add(actor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// guids
|
||||||
|
foreach (MetadataGuid guid in metadata.Guids
|
||||||
|
.Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
metadata.Guids.Remove(guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (MetadataGuid guid in incomingMetadata.Guids
|
||||||
|
.Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
metadata.Guids.Add(guid);
|
||||||
|
}
|
||||||
|
|
||||||
metadata.ReleaseDate = incomingMetadata.ReleaseDate;
|
metadata.ReleaseDate = incomingMetadata.ReleaseDate;
|
||||||
|
|
||||||
// poster
|
// poster
|
||||||
@@ -246,6 +263,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.Include(m => m.LibraryPath)
|
.Include(m => m.LibraryPath)
|
||||||
.Include(m => m.SeasonMetadata)
|
.Include(m => m.SeasonMetadata)
|
||||||
.ThenInclude(mm => mm.Artwork)
|
.ThenInclude(mm => mm.Artwork)
|
||||||
|
.Include(m => m.SeasonMetadata)
|
||||||
|
.ThenInclude(mm => mm.Guids)
|
||||||
.Filter(m => m.ItemId == season.ItemId)
|
.Filter(m => m.ItemId == season.ItemId)
|
||||||
.OrderBy(m => m.ItemId)
|
.OrderBy(m => m.ItemId)
|
||||||
.SingleOrDefaultAsync();
|
.SingleOrDefaultAsync();
|
||||||
@@ -322,6 +341,21 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
fanArt.DateUpdated = incomingFanArt.DateUpdated;
|
fanArt.DateUpdated = incomingFanArt.DateUpdated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// guids
|
||||||
|
foreach (MetadataGuid guid in metadata.Guids
|
||||||
|
.Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
metadata.Guids.Remove(guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (MetadataGuid guid in incomingMetadata.Guids
|
||||||
|
.Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
metadata.Guids.Add(guid);
|
||||||
|
}
|
||||||
|
|
||||||
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
|
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)))
|
||||||
{
|
{
|
||||||
@@ -345,20 +379,37 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
await dbContext.Entry(episode).Reference(m => m.LibraryPath).LoadAsync();
|
await dbContext.Entry(episode).Reference(m => m.LibraryPath).LoadAsync();
|
||||||
await dbContext.Entry(episode.LibraryPath).Reference(lp => lp.Library).LoadAsync();
|
await dbContext.Entry(episode.LibraryPath).Reference(lp => lp.Library).LoadAsync();
|
||||||
|
await dbContext.Entry(episode).Reference(e => e.Season).LoadAsync();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Unit> Update(EmbyEpisode episode)
|
public async Task<Option<EmbyEpisode>> Update(EmbyEpisode episode)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||||
Option<EmbyEpisode> maybeExisting = await dbContext.EmbyEpisodes
|
Option<EmbyEpisode> maybeExisting = await dbContext.EmbyEpisodes
|
||||||
.Include(m => m.LibraryPath)
|
.Include(m => m.LibraryPath)
|
||||||
|
.ThenInclude(lp => lp.Library)
|
||||||
.Include(m => m.MediaVersions)
|
.Include(m => m.MediaVersions)
|
||||||
.ThenInclude(mv => mv.MediaFiles)
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
.Include(m => m.MediaVersions)
|
.Include(m => m.MediaVersions)
|
||||||
.ThenInclude(mv => mv.Streams)
|
.ThenInclude(mv => mv.Streams)
|
||||||
.Include(m => m.EpisodeMetadata)
|
.Include(m => m.EpisodeMetadata)
|
||||||
.ThenInclude(mm => mm.Artwork)
|
.ThenInclude(mm => mm.Artwork)
|
||||||
|
.Include(m => m.EpisodeMetadata)
|
||||||
|
.ThenInclude(mm => mm.Guids)
|
||||||
|
.Include(m => m.EpisodeMetadata)
|
||||||
|
.ThenInclude(mm => mm.Genres)
|
||||||
|
.Include(m => m.EpisodeMetadata)
|
||||||
|
.ThenInclude(mm => mm.Tags)
|
||||||
|
.Include(m => m.EpisodeMetadata)
|
||||||
|
.ThenInclude(mm => mm.Studios)
|
||||||
|
.Include(m => m.EpisodeMetadata)
|
||||||
|
.ThenInclude(mm => mm.Actors)
|
||||||
|
.Include(m => m.EpisodeMetadata)
|
||||||
|
.ThenInclude(mm => mm.Directors)
|
||||||
|
.Include(m => m.EpisodeMetadata)
|
||||||
|
.ThenInclude(mm => mm.Writers)
|
||||||
|
.Include(m => m.Season)
|
||||||
.Filter(m => m.ItemId == episode.ItemId)
|
.Filter(m => m.ItemId == episode.ItemId)
|
||||||
.OrderBy(m => m.ItemId)
|
.OrderBy(m => m.ItemId)
|
||||||
.SingleOrDefaultAsync();
|
.SingleOrDefaultAsync();
|
||||||
@@ -402,6 +453,51 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
thumbnail.DateUpdated = incomingThumbnail.DateUpdated;
|
thumbnail.DateUpdated = incomingThumbnail.DateUpdated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// directors
|
||||||
|
foreach (Director director in metadata.Directors
|
||||||
|
.Filter(d => incomingMetadata.Directors.All(d2 => d2.Name != d.Name))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
metadata.Directors.Remove(director);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Director director in incomingMetadata.Directors
|
||||||
|
.Filter(d => metadata.Directors.All(d2 => d2.Name != d.Name))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
metadata.Directors.Add(director);
|
||||||
|
}
|
||||||
|
|
||||||
|
// writers
|
||||||
|
foreach (Writer writer in metadata.Writers
|
||||||
|
.Filter(w => incomingMetadata.Writers.All(w2 => w2.Name != w.Name))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
metadata.Writers.Remove(writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Writer writer in incomingMetadata.Writers
|
||||||
|
.Filter(w => metadata.Writers.All(w2 => w2.Name != w.Name))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
metadata.Writers.Add(writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// guids
|
||||||
|
foreach (MetadataGuid guid in metadata.Guids
|
||||||
|
.Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
metadata.Guids.Remove(guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (MetadataGuid guid in incomingMetadata.Guids
|
||||||
|
.Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
metadata.Guids.Add(guid);
|
||||||
|
}
|
||||||
|
|
||||||
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
|
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)))
|
||||||
{
|
{
|
||||||
@@ -422,7 +518,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
return Unit.Default;
|
return maybeExisting;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<int>> RemoveMissingShows(EmbyLibrary library, List<string> showIds)
|
public async Task<List<int>> RemoveMissingShows(EmbyLibrary library, List<string> showIds)
|
||||||
@@ -454,14 +550,25 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
WHERE LP.LibraryId = @LibraryId AND js.ItemId IN @SeasonIds)",
|
WHERE LP.LibraryId = @LibraryId AND js.ItemId IN @SeasonIds)",
|
||||||
new { LibraryId = library.Id, SeasonIds = seasonIds }).ToUnit();
|
new { LibraryId = library.Id, SeasonIds = seasonIds }).ToUnit();
|
||||||
|
|
||||||
public Task<Unit> RemoveMissingEpisodes(EmbyLibrary library, List<string> episodeIds) =>
|
public async Task<List<int>> RemoveMissingEpisodes(EmbyLibrary library, List<string> episodeIds)
|
||||||
_dbConnection.ExecuteAsync(
|
{
|
||||||
|
List<int> ids = await _dbConnection.QueryAsync<int>(
|
||||||
|
@"SELECT m.Id FROM MediaItem m
|
||||||
|
INNER JOIN EmbyEpisode ee ON ee.Id = m.Id
|
||||||
|
INNER JOIN LibraryPath lp ON lp.Id = m.LibraryPathId
|
||||||
|
WHERE lp.LibraryId = @LibraryId AND ee.ItemId IN @EpisodeIds",
|
||||||
|
new { LibraryId = library.Id, EpisodeIds = episodeIds }).Map(result => result.ToList());
|
||||||
|
|
||||||
|
await _dbConnection.ExecuteAsync(
|
||||||
@"DELETE FROM MediaItem WHERE Id IN
|
@"DELETE FROM MediaItem WHERE Id IN
|
||||||
(SELECT m.Id FROM MediaItem m
|
(SELECT m.Id FROM MediaItem m
|
||||||
INNER JOIN EmbyEpisode je ON je.Id = m.Id
|
INNER JOIN EmbyEpisode ee ON ee.Id = m.Id
|
||||||
INNER JOIN LibraryPath LP on m.LibraryPathId = LP.Id
|
INNER JOIN LibraryPath LP on m.LibraryPathId = LP.Id
|
||||||
WHERE LP.LibraryId = @LibraryId AND je.ItemId IN @EpisodeIds)",
|
WHERE LP.LibraryId = @LibraryId AND ee.ItemId IN @EpisodeIds)",
|
||||||
new { LibraryId = library.Id, EpisodeIds = episodeIds }).ToUnit();
|
new { LibraryId = library.Id, EpisodeIds = episodeIds });
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Unit> DeleteEmptySeasons(EmbyLibrary library)
|
public async Task<Unit> DeleteEmptySeasons(EmbyLibrary library)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.ThenInclude(mm => mm.Actors)
|
.ThenInclude(mm => mm.Actors)
|
||||||
.Include(m => m.ShowMetadata)
|
.Include(m => m.ShowMetadata)
|
||||||
.ThenInclude(mm => mm.Artwork)
|
.ThenInclude(mm => mm.Artwork)
|
||||||
|
.Include(m => m.ShowMetadata)
|
||||||
|
.ThenInclude(mm => mm.Guids)
|
||||||
.Filter(m => m.ItemId == show.ItemId)
|
.Filter(m => m.ItemId == show.ItemId)
|
||||||
.OrderBy(m => m.ItemId)
|
.OrderBy(m => m.ItemId)
|
||||||
.SingleOrDefaultAsync();
|
.SingleOrDefaultAsync();
|
||||||
@@ -177,6 +179,21 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
metadata.Actors.Add(actor);
|
metadata.Actors.Add(actor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// guids
|
||||||
|
foreach (MetadataGuid guid in metadata.Guids
|
||||||
|
.Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
metadata.Guids.Remove(guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (MetadataGuid guid in incomingMetadata.Guids
|
||||||
|
.Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
metadata.Guids.Add(guid);
|
||||||
|
}
|
||||||
|
|
||||||
metadata.ReleaseDate = incomingMetadata.ReleaseDate;
|
metadata.ReleaseDate = incomingMetadata.ReleaseDate;
|
||||||
|
|
||||||
// poster
|
// poster
|
||||||
@@ -263,6 +280,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.Include(m => m.LibraryPath)
|
.Include(m => m.LibraryPath)
|
||||||
.Include(m => m.SeasonMetadata)
|
.Include(m => m.SeasonMetadata)
|
||||||
.ThenInclude(mm => mm.Artwork)
|
.ThenInclude(mm => mm.Artwork)
|
||||||
|
.Include(m => m.SeasonMetadata)
|
||||||
|
.ThenInclude(mm => mm.Guids)
|
||||||
.Filter(m => m.ItemId == season.ItemId)
|
.Filter(m => m.ItemId == season.ItemId)
|
||||||
.OrderBy(m => m.ItemId)
|
.OrderBy(m => m.ItemId)
|
||||||
.SingleOrDefaultAsync();
|
.SingleOrDefaultAsync();
|
||||||
@@ -322,6 +341,21 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
fanArt.DateUpdated = incomingFanArt.DateUpdated;
|
fanArt.DateUpdated = incomingFanArt.DateUpdated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// guids
|
||||||
|
foreach (MetadataGuid guid in metadata.Guids
|
||||||
|
.Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
metadata.Guids.Remove(guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (MetadataGuid guid in incomingMetadata.Guids
|
||||||
|
.Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
metadata.Guids.Add(guid);
|
||||||
|
}
|
||||||
|
|
||||||
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
|
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)))
|
||||||
{
|
{
|
||||||
@@ -345,20 +379,37 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
await dbContext.Entry(episode).Reference(m => m.LibraryPath).LoadAsync();
|
await dbContext.Entry(episode).Reference(m => m.LibraryPath).LoadAsync();
|
||||||
await dbContext.Entry(episode.LibraryPath).Reference(lp => lp.Library).LoadAsync();
|
await dbContext.Entry(episode.LibraryPath).Reference(lp => lp.Library).LoadAsync();
|
||||||
|
await dbContext.Entry(episode).Reference(e => e.Season).LoadAsync();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Unit> Update(JellyfinEpisode episode)
|
public async Task<Option<JellyfinEpisode>> Update(JellyfinEpisode episode)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||||
Option<JellyfinEpisode> maybeExisting = await dbContext.JellyfinEpisodes
|
Option<JellyfinEpisode> maybeExisting = await dbContext.JellyfinEpisodes
|
||||||
.Include(m => m.LibraryPath)
|
.Include(m => m.LibraryPath)
|
||||||
|
.ThenInclude(lp => lp.Library)
|
||||||
.Include(m => m.MediaVersions)
|
.Include(m => m.MediaVersions)
|
||||||
.ThenInclude(mv => mv.MediaFiles)
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
.Include(m => m.MediaVersions)
|
.Include(m => m.MediaVersions)
|
||||||
.ThenInclude(mv => mv.Streams)
|
.ThenInclude(mv => mv.Streams)
|
||||||
.Include(m => m.EpisodeMetadata)
|
.Include(m => m.EpisodeMetadata)
|
||||||
.ThenInclude(mm => mm.Artwork)
|
.ThenInclude(mm => mm.Artwork)
|
||||||
|
.Include(m => m.EpisodeMetadata)
|
||||||
|
.ThenInclude(mm => mm.Guids)
|
||||||
|
.Include(m => m.EpisodeMetadata)
|
||||||
|
.ThenInclude(mm => mm.Genres)
|
||||||
|
.Include(m => m.EpisodeMetadata)
|
||||||
|
.ThenInclude(mm => mm.Tags)
|
||||||
|
.Include(m => m.EpisodeMetadata)
|
||||||
|
.ThenInclude(mm => mm.Studios)
|
||||||
|
.Include(m => m.EpisodeMetadata)
|
||||||
|
.ThenInclude(mm => mm.Actors)
|
||||||
|
.Include(m => m.EpisodeMetadata)
|
||||||
|
.ThenInclude(mm => mm.Directors)
|
||||||
|
.Include(m => m.EpisodeMetadata)
|
||||||
|
.ThenInclude(mm => mm.Writers)
|
||||||
|
.Include(m => m.Season)
|
||||||
.Filter(m => m.ItemId == episode.ItemId)
|
.Filter(m => m.ItemId == episode.ItemId)
|
||||||
.OrderBy(m => m.ItemId)
|
.OrderBy(m => m.ItemId)
|
||||||
.SingleOrDefaultAsync();
|
.SingleOrDefaultAsync();
|
||||||
@@ -402,6 +453,51 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
thumbnail.DateUpdated = incomingThumbnail.DateUpdated;
|
thumbnail.DateUpdated = incomingThumbnail.DateUpdated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// directors
|
||||||
|
foreach (Director director in metadata.Directors
|
||||||
|
.Filter(d => incomingMetadata.Directors.All(d2 => d2.Name != d.Name))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
metadata.Directors.Remove(director);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Director director in incomingMetadata.Directors
|
||||||
|
.Filter(d => metadata.Directors.All(d2 => d2.Name != d.Name))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
metadata.Directors.Add(director);
|
||||||
|
}
|
||||||
|
|
||||||
|
// writers
|
||||||
|
foreach (Writer writer in metadata.Writers
|
||||||
|
.Filter(w => incomingMetadata.Writers.All(w2 => w2.Name != w.Name))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
metadata.Writers.Remove(writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Writer writer in incomingMetadata.Writers
|
||||||
|
.Filter(w => metadata.Writers.All(w2 => w2.Name != w.Name))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
metadata.Writers.Add(writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// guids
|
||||||
|
foreach (MetadataGuid guid in metadata.Guids
|
||||||
|
.Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
metadata.Guids.Remove(guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (MetadataGuid guid in incomingMetadata.Guids
|
||||||
|
.Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
metadata.Guids.Add(guid);
|
||||||
|
}
|
||||||
|
|
||||||
var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList();
|
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)))
|
||||||
{
|
{
|
||||||
@@ -422,7 +518,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
return Unit.Default;
|
return maybeExisting;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<int>> RemoveMissingShows(JellyfinLibrary library, List<string> showIds)
|
public async Task<List<int>> RemoveMissingShows(JellyfinLibrary library, List<string> showIds)
|
||||||
@@ -454,14 +550,25 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
WHERE LP.LibraryId = @LibraryId AND js.ItemId IN @SeasonIds)",
|
WHERE LP.LibraryId = @LibraryId AND js.ItemId IN @SeasonIds)",
|
||||||
new { LibraryId = library.Id, SeasonIds = seasonIds }).ToUnit();
|
new { LibraryId = library.Id, SeasonIds = seasonIds }).ToUnit();
|
||||||
|
|
||||||
public Task<Unit> RemoveMissingEpisodes(JellyfinLibrary library, List<string> episodeIds) =>
|
public async Task<List<int>> RemoveMissingEpisodes(JellyfinLibrary library, List<string> episodeIds)
|
||||||
_dbConnection.ExecuteAsync(
|
{
|
||||||
|
List<int> ids = await _dbConnection.QueryAsync<int>(
|
||||||
|
@"SELECT m.Id FROM MediaItem m
|
||||||
|
INNER JOIN JellyfinEpisode je ON je.Id = m.Id
|
||||||
|
INNER JOIN LibraryPath lp ON lp.Id = m.LibraryPathId
|
||||||
|
WHERE lp.LibraryId = @LibraryId AND je.ItemId IN @EpisodeIds",
|
||||||
|
new { LibraryId = library.Id, EpisodeIds = episodeIds }).Map(result => result.ToList());
|
||||||
|
|
||||||
|
await _dbConnection.ExecuteAsync(
|
||||||
@"DELETE FROM MediaItem WHERE Id IN
|
@"DELETE FROM MediaItem WHERE Id IN
|
||||||
(SELECT m.Id FROM MediaItem m
|
(SELECT m.Id FROM MediaItem m
|
||||||
INNER JOIN JellyfinEpisode je ON je.Id = m.Id
|
INNER JOIN JellyfinEpisode je ON je.Id = m.Id
|
||||||
INNER JOIN LibraryPath LP on m.LibraryPathId = LP.Id
|
INNER JOIN LibraryPath LP on m.LibraryPathId = LP.Id
|
||||||
WHERE LP.LibraryId = @LibraryId AND je.ItemId IN @EpisodeIds)",
|
WHERE LP.LibraryId = @LibraryId AND je.ItemId IN @EpisodeIds)",
|
||||||
new { LibraryId = library.Id, EpisodeIds = episodeIds }).ToUnit();
|
new { LibraryId = library.Id, EpisodeIds = episodeIds });
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Unit> DeleteEmptySeasons(JellyfinLibrary library)
|
public async Task<Unit> DeleteEmptySeasons(JellyfinLibrary library)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -155,9 +155,18 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.ThenInclude(i => (i as Episode).EpisodeMetadata)
|
.ThenInclude(i => (i as Episode).EpisodeMetadata)
|
||||||
.ThenInclude(em => em.Artwork)
|
.ThenInclude(em => em.Artwork)
|
||||||
.Include(c => c.MediaItems)
|
.Include(c => c.MediaItems)
|
||||||
|
.ThenInclude(i => (i as Episode).EpisodeMetadata)
|
||||||
|
.ThenInclude(em => em.Directors)
|
||||||
|
.Include(c => c.MediaItems)
|
||||||
|
.ThenInclude(i => (i as Episode).EpisodeMetadata)
|
||||||
|
.ThenInclude(em => em.Writers)
|
||||||
|
.Include(c => c.MediaItems)
|
||||||
.ThenInclude(i => (i as Episode).Season)
|
.ThenInclude(i => (i as Episode).Season)
|
||||||
.ThenInclude(s => s.Show)
|
.ThenInclude(s => s.Show)
|
||||||
.ThenInclude(s => s.ShowMetadata)
|
.ThenInclude(s => s.ShowMetadata)
|
||||||
|
.Include(c => c.MediaItems)
|
||||||
|
.ThenInclude(i => (i as Episode).Season)
|
||||||
|
.ThenInclude(s => s.SeasonMetadata)
|
||||||
.OrderBy(c => c.Id)
|
.OrderBy(c => c.Id)
|
||||||
.SingleOrDefaultAsync(c => c.Id == id)
|
.SingleOrDefaultAsync(c => c.Id == id)
|
||||||
.Map(Optional);
|
.Map(Optional);
|
||||||
|
|||||||
@@ -354,10 +354,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
List<int> movieIds = await context.PlexMovies.Map(pm => pm.Id).ToListAsync();
|
List<int> movieIds = await context.PlexMovies.Map(pm => pm.Id).ToListAsync();
|
||||||
List<int> showIds = await context.PlexShows.Map(ps => ps.Id).ToListAsync();
|
List<int> showIds = await context.PlexShows.Map(ps => ps.Id).ToListAsync();
|
||||||
|
List<int> episodeIds = await context.PlexEpisodes.Map(pe => pe.Id).ToListAsync();
|
||||||
|
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
return movieIds.Append(showIds).ToList();
|
return movieIds.Append(showIds).Append(episodeIds).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<int>> DeletePlex(PlexMediaSource plexMediaSource)
|
public async Task<List<int>> DeletePlex(PlexMediaSource plexMediaSource)
|
||||||
@@ -404,6 +405,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
WHERE l.Id IN @ids)",
|
WHERE l.Id IN @ids)",
|
||||||
new { ids = libraryIds });
|
new { ids = libraryIds });
|
||||||
|
|
||||||
|
List<int> episodeIds = await _dbConnection.QueryAsync<int>(
|
||||||
|
@"SELECT m.Id FROM MediaItem m
|
||||||
|
INNER JOIN PlexEpisode pe ON pe.Id = m.Id
|
||||||
|
INNER JOIN LibraryPath lp ON lp.Id = m.LibraryPathId
|
||||||
|
INNER JOIN Library l ON l.Id = lp.LibraryId
|
||||||
|
WHERE l.Id IN @ids",
|
||||||
|
new { ids = libraryIds }).Map(result => result.ToList());
|
||||||
|
|
||||||
await _dbConnection.ExecuteAsync(
|
await _dbConnection.ExecuteAsync(
|
||||||
@"DELETE FROM MediaItem WHERE Id IN
|
@"DELETE FROM MediaItem WHERE Id IN
|
||||||
(SELECT m.Id FROM MediaItem m
|
(SELECT m.Id FROM MediaItem m
|
||||||
@@ -439,7 +448,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
WHERE l.Id IN @ids)",
|
WHERE l.Id IN @ids)",
|
||||||
new { ids = libraryIds });
|
new { ids = libraryIds });
|
||||||
|
|
||||||
return movieIds.Append(showIds).ToList();
|
return movieIds.Append(showIds).Append(episodeIds).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task EnablePlexLibrarySync(IEnumerable<int> libraryIds) =>
|
public Task EnablePlexLibrarySync(IEnumerable<int> libraryIds) =>
|
||||||
@@ -561,6 +570,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
WHERE l.Id IN @ids)",
|
WHERE l.Id IN @ids)",
|
||||||
new { ids = libraryIds });
|
new { ids = libraryIds });
|
||||||
|
|
||||||
|
List<int> episodeIds = await _dbConnection.QueryAsync<int>(
|
||||||
|
@"SELECT m.Id FROM MediaItem m
|
||||||
|
INNER JOIN JellyfinEpisode pe ON pe.Id = m.Id
|
||||||
|
INNER JOIN LibraryPath lp ON lp.Id = m.LibraryPathId
|
||||||
|
INNER JOIN Library l ON l.Id = lp.LibraryId
|
||||||
|
WHERE l.Id IN @ids",
|
||||||
|
new { ids = libraryIds }).Map(result => result.ToList());
|
||||||
|
|
||||||
await _dbConnection.ExecuteAsync(
|
await _dbConnection.ExecuteAsync(
|
||||||
@"DELETE FROM MediaItem WHERE Id IN
|
@"DELETE FROM MediaItem WHERE Id IN
|
||||||
(SELECT m.Id FROM MediaItem m
|
(SELECT m.Id FROM MediaItem m
|
||||||
@@ -596,7 +613,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
WHERE l.Id IN @ids)",
|
WHERE l.Id IN @ids)",
|
||||||
new { ids = libraryIds });
|
new { ids = libraryIds });
|
||||||
|
|
||||||
return movieIds.Append(showIds).ToList();
|
return movieIds.Append(showIds).Append(episodeIds).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<Option<JellyfinLibrary>> GetJellyfinLibrary(int jellyfinLibraryId)
|
public Task<Option<JellyfinLibrary>> GetJellyfinLibrary(int jellyfinLibraryId)
|
||||||
@@ -708,9 +725,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.Map(ps => ps.Id)
|
.Map(ps => ps.Id)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
List<int> episodeIds = await context.JellyfinEpisodes
|
||||||
|
.Where(m => libraryIds.Contains(m.LibraryPath.LibraryId))
|
||||||
|
.Map(ps => ps.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
return movieIds.Append(showIds).ToList();
|
return movieIds.Append(showIds).Append(episodeIds).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Unit> UpsertEmby(string address, string serverName, string operatingSystem)
|
public async Task<Unit> UpsertEmby(string address, string serverName, string operatingSystem)
|
||||||
@@ -904,9 +926,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.Map(ps => ps.Id)
|
.Map(ps => ps.Id)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
List<int> episodeIds = await context.EmbyEpisodes
|
||||||
|
.Where(m => libraryIds.Contains(m.LibraryPath.LibraryId))
|
||||||
|
.Map(ps => ps.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
return movieIds.Append(showIds).ToList();
|
return movieIds.Append(showIds).Append(episodeIds).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<Unit> EnableEmbyLibrarySync(IEnumerable<int> libraryIds) =>
|
public Task<Unit> EnableEmbyLibrarySync(IEnumerable<int> libraryIds) =>
|
||||||
@@ -941,6 +968,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
WHERE l.Id IN @ids)",
|
WHERE l.Id IN @ids)",
|
||||||
new { ids = libraryIds });
|
new { ids = libraryIds });
|
||||||
|
|
||||||
|
List<int> episodeIds = await _dbConnection.QueryAsync<int>(
|
||||||
|
@"SELECT m.Id FROM MediaItem m
|
||||||
|
INNER JOIN EmbyEpisode pe ON pe.Id = m.Id
|
||||||
|
INNER JOIN LibraryPath lp ON lp.Id = m.LibraryPathId
|
||||||
|
INNER JOIN Library l ON l.Id = lp.LibraryId
|
||||||
|
WHERE l.Id IN @ids",
|
||||||
|
new { ids = libraryIds }).Map(result => result.ToList());
|
||||||
|
|
||||||
await _dbConnection.ExecuteAsync(
|
await _dbConnection.ExecuteAsync(
|
||||||
@"DELETE FROM MediaItem WHERE Id IN
|
@"DELETE FROM MediaItem WHERE Id IN
|
||||||
(SELECT m.Id FROM MediaItem m
|
(SELECT m.Id FROM MediaItem m
|
||||||
@@ -976,7 +1011,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
WHERE l.Id IN @ids)",
|
WHERE l.Id IN @ids)",
|
||||||
new { ids = libraryIds });
|
new { ids = libraryIds });
|
||||||
|
|
||||||
return movieIds.Append(showIds).ToList();
|
return movieIds.Append(showIds).Append(episodeIds).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (MetadataGuid guid in Optional(metadata.Guids).Flatten())
|
||||||
|
{
|
||||||
|
dbContext.Entry(guid).State = EntityState.Added;
|
||||||
|
}
|
||||||
|
|
||||||
if (metadata is MovieMetadata movieMetadata)
|
if (metadata is MovieMetadata movieMetadata)
|
||||||
{
|
{
|
||||||
foreach (Director director in Optional(movieMetadata.Directors).Flatten())
|
foreach (Director director in Optional(movieMetadata.Directors).Flatten())
|
||||||
@@ -88,6 +93,19 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (metadata is EpisodeMetadata episodeMetadata)
|
||||||
|
{
|
||||||
|
foreach (Director director in Optional(episodeMetadata.Directors).Flatten())
|
||||||
|
{
|
||||||
|
dbContext.Entry(director).State = EntityState.Added;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Writer writer in Optional(episodeMetadata.Writers).Flatten())
|
||||||
|
{
|
||||||
|
dbContext.Entry(writer).State = EntityState.Added;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return await dbContext.SaveChangesAsync() > 0;
|
return await dbContext.SaveChangesAsync() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,6 +279,44 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
@"UPDATE MovieMetadata SET ContentRating = @ContentRating WHERE Id = @Id",
|
@"UPDATE MovieMetadata SET ContentRating = @ContentRating WHERE Id = @Id",
|
||||||
new { metadata.Id, ContentRating = contentRating }).ToUnit();
|
new { metadata.Id, ContentRating = contentRating }).ToUnit();
|
||||||
|
|
||||||
|
public Task<bool> RemoveGuid(MetadataGuid guid) =>
|
||||||
|
_dbConnection.ExecuteAsync("DELETE FROM MetadataGuid WHERE Id = @GuidId", new { GuidId = guid.Id })
|
||||||
|
.Map(result => result > 0);
|
||||||
|
|
||||||
|
public Task<bool> AddGuid(Metadata metadata, MetadataGuid guid) =>
|
||||||
|
metadata switch
|
||||||
|
{
|
||||||
|
MovieMetadata =>
|
||||||
|
_dbConnection.ExecuteAsync(
|
||||||
|
"INSERT INTO MetadataGuid (Guid, MovieMetadataId) VALUES (@Guid, @MetadataId)",
|
||||||
|
new { guid.Guid, MetadataId = metadata.Id }).Map(result => result > 0),
|
||||||
|
ShowMetadata =>
|
||||||
|
_dbConnection.ExecuteAsync(
|
||||||
|
"INSERT INTO MetadataGuid (Guid, ShowMetadataId) VALUES (@Guid, @MetadataId)",
|
||||||
|
new { guid.Guid, MetadataId = metadata.Id }).Map(result => result > 0),
|
||||||
|
SeasonMetadata =>
|
||||||
|
_dbConnection.ExecuteAsync(
|
||||||
|
"INSERT INTO MetadataGuid (Guid, SeasonMetadataId) VALUES (@Guid, @MetadataId)",
|
||||||
|
new { guid.Guid, MetadataId = metadata.Id }).Map(result => result > 0),
|
||||||
|
EpisodeMetadata =>
|
||||||
|
_dbConnection.ExecuteAsync(
|
||||||
|
"INSERT INTO MetadataGuid (Guid, EpisodeMetadataId) VALUES (@Guid, @MetadataId)",
|
||||||
|
new { guid.Guid, MetadataId = metadata.Id }).Map(result => result > 0),
|
||||||
|
ArtistMetadata =>
|
||||||
|
_dbConnection.ExecuteAsync(
|
||||||
|
"INSERT INTO MetadataGuid (Guid, ArtistMetadataId) VALUES (@Guid, @MetadataId)",
|
||||||
|
new { guid.Guid, MetadataId = metadata.Id }).Map(result => result > 0),
|
||||||
|
_ => throw new NotSupportedException()
|
||||||
|
};
|
||||||
|
|
||||||
|
public Task<bool> RemoveDirector(Director director) =>
|
||||||
|
_dbConnection.ExecuteAsync("DELETE FROM Director WHERE Id = @DirectorId", new { DirectorId = director.Id })
|
||||||
|
.Map(result => result > 0);
|
||||||
|
|
||||||
|
public Task<bool> RemoveWriter(Writer writer) =>
|
||||||
|
_dbConnection.ExecuteAsync("DELETE FROM Writer WHERE Id = @WriterId", new { WriterId = writer.Id })
|
||||||
|
.Map(result => result > 0);
|
||||||
|
|
||||||
public Task<bool> RemoveGenre(Genre genre) =>
|
public Task<bool> RemoveGenre(Genre genre) =>
|
||||||
_dbConnection.ExecuteAsync("DELETE FROM Genre WHERE Id = @GenreId", new { GenreId = genre.Id })
|
_dbConnection.ExecuteAsync("DELETE FROM Genre WHERE Id = @GenreId", new { GenreId = genre.Id })
|
||||||
.Map(result => result > 0);
|
.Map(result => result > 0);
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.ThenInclude(mm => mm.Directors)
|
.ThenInclude(mm => mm.Directors)
|
||||||
.Include(i => i.MovieMetadata)
|
.Include(i => i.MovieMetadata)
|
||||||
.ThenInclude(mm => mm.Writers)
|
.ThenInclude(mm => mm.Writers)
|
||||||
|
.Include(i => i.MovieMetadata)
|
||||||
|
.ThenInclude(mm => mm.Guids)
|
||||||
.Include(i => i.MediaVersions)
|
.Include(i => i.MediaVersions)
|
||||||
.ThenInclude(mv => mv.MediaFiles)
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
.Include(i => i.MediaVersions)
|
.Include(i => i.MediaVersions)
|
||||||
@@ -328,6 +330,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.ThenInclude(mm => mm.Directors)
|
.ThenInclude(mm => mm.Directors)
|
||||||
.Include(m => m.MovieMetadata)
|
.Include(m => m.MovieMetadata)
|
||||||
.ThenInclude(mm => mm.Writers)
|
.ThenInclude(mm => mm.Writers)
|
||||||
|
.Include(m => m.MovieMetadata)
|
||||||
|
.ThenInclude(mm => mm.Guids)
|
||||||
.Filter(m => m.ItemId == movie.ItemId)
|
.Filter(m => m.ItemId == movie.ItemId)
|
||||||
.OrderBy(m => m.ItemId)
|
.OrderBy(m => m.ItemId)
|
||||||
.SingleOrDefaultAsync();
|
.SingleOrDefaultAsync();
|
||||||
@@ -447,6 +451,21 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
metadata.Writers.Add(writer);
|
metadata.Writers.Add(writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// guids
|
||||||
|
foreach (MetadataGuid guid in metadata.Guids
|
||||||
|
.Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
metadata.Guids.Remove(guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (MetadataGuid guid in incomingMetadata.Guids
|
||||||
|
.Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
metadata.Guids.Add(guid);
|
||||||
|
}
|
||||||
|
|
||||||
metadata.ReleaseDate = incomingMetadata.ReleaseDate;
|
metadata.ReleaseDate = incomingMetadata.ReleaseDate;
|
||||||
|
|
||||||
// poster
|
// poster
|
||||||
@@ -565,6 +584,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.ThenInclude(mm => mm.Writers)
|
.ThenInclude(mm => mm.Writers)
|
||||||
.Include(m => m.MovieMetadata)
|
.Include(m => m.MovieMetadata)
|
||||||
.ThenInclude(mm => mm.Artwork)
|
.ThenInclude(mm => mm.Artwork)
|
||||||
|
.Include(m => m.MovieMetadata)
|
||||||
|
.ThenInclude(mm => mm.Guids)
|
||||||
.Filter(m => m.ItemId == movie.ItemId)
|
.Filter(m => m.ItemId == movie.ItemId)
|
||||||
.OrderBy(m => m.ItemId)
|
.OrderBy(m => m.ItemId)
|
||||||
.SingleOrDefaultAsync();
|
.SingleOrDefaultAsync();
|
||||||
@@ -684,6 +705,21 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
metadata.Writers.Add(writer);
|
metadata.Writers.Add(writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// guids
|
||||||
|
foreach (MetadataGuid guid in metadata.Guids
|
||||||
|
.Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
metadata.Guids.Remove(guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (MetadataGuid guid in incomingMetadata.Guids
|
||||||
|
.Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid))
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
metadata.Guids.Add(guid);
|
||||||
|
}
|
||||||
|
|
||||||
metadata.ReleaseDate = incomingMetadata.ReleaseDate;
|
metadata.ReleaseDate = incomingMetadata.ReleaseDate;
|
||||||
|
|
||||||
// poster
|
// poster
|
||||||
@@ -737,19 +773,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
return maybeExisting;
|
return maybeExisting;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<bool> RemoveDirector(Director director) =>
|
|
||||||
_dbConnection.ExecuteAsync("DELETE FROM Director WHERE Id = @DirectorId", new { DirectorId = director.Id })
|
|
||||||
.Map(result => result > 0);
|
|
||||||
|
|
||||||
public Task<bool> AddDirector(MovieMetadata metadata, Director director) =>
|
public Task<bool> AddDirector(MovieMetadata metadata, Director director) =>
|
||||||
_dbConnection.ExecuteAsync(
|
_dbConnection.ExecuteAsync(
|
||||||
"INSERT INTO Director (Name, MovieMetadataId) VALUES (@Name, @MetadataId)",
|
"INSERT INTO Director (Name, MovieMetadataId) VALUES (@Name, @MetadataId)",
|
||||||
new { director.Name, MetadataId = metadata.Id }).Map(result => result > 0);
|
new { director.Name, MetadataId = metadata.Id }).Map(result => result > 0);
|
||||||
|
|
||||||
public Task<bool> RemoveWriter(Writer writer) =>
|
|
||||||
_dbConnection.ExecuteAsync("DELETE FROM Writer WHERE Id = @WriterId", new { WriterId = writer.Id })
|
|
||||||
.Map(result => result > 0);
|
|
||||||
|
|
||||||
public Task<bool> AddWriter(MovieMetadata metadata, Writer writer) =>
|
public Task<bool> AddWriter(MovieMetadata metadata, Writer writer) =>
|
||||||
_dbConnection.ExecuteAsync(
|
_dbConnection.ExecuteAsync(
|
||||||
"INSERT INTO Writer (Name, MovieMetadataId) VALUES (@Name, @MetadataId)",
|
"INSERT INTO Writer (Name, MovieMetadataId) VALUES (@Name, @MetadataId)",
|
||||||
|
|||||||
@@ -47,6 +47,21 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.ThenInclude(mm => mm.Writers)
|
.ThenInclude(mm => mm.Writers)
|
||||||
.Include(mi => (mi as Movie).MediaVersions)
|
.Include(mi => (mi as Movie).MediaVersions)
|
||||||
.ThenInclude(mm => mm.Streams)
|
.ThenInclude(mm => mm.Streams)
|
||||||
|
.Include(mi => (mi as Episode).EpisodeMetadata)
|
||||||
|
.ThenInclude(em => em.Genres)
|
||||||
|
.Include(mi => (mi as Episode).EpisodeMetadata)
|
||||||
|
.ThenInclude(em => em.Tags)
|
||||||
|
.Include(mi => (mi as Episode).EpisodeMetadata)
|
||||||
|
.ThenInclude(em => em.Studios)
|
||||||
|
.Include(mi => (mi as Episode).EpisodeMetadata)
|
||||||
|
.ThenInclude(em => em.Actors)
|
||||||
|
.Include(mi => (mi as Episode).EpisodeMetadata)
|
||||||
|
.ThenInclude(em => em.Directors)
|
||||||
|
.Include(mi => (mi as Episode).EpisodeMetadata)
|
||||||
|
.ThenInclude(em => em.Writers)
|
||||||
|
.Include(mi => (mi as Episode).MediaVersions)
|
||||||
|
.ThenInclude(em => em.Streams)
|
||||||
|
.Include(mi => (mi as Episode).Season)
|
||||||
.Include(mi => (mi as Show).ShowMetadata)
|
.Include(mi => (mi as Show).ShowMetadata)
|
||||||
.ThenInclude(mm => mm.Genres)
|
.ThenInclude(mm => mm.Genres)
|
||||||
.Include(mi => (mi as Show).ShowMetadata)
|
.Include(mi => (mi as Show).ShowMetadata)
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
new { ShowIds = showIds })
|
new { ShowIds = showIds })
|
||||||
.Map(c => c == showIds.Count);
|
.Map(c => c == showIds.Count);
|
||||||
|
|
||||||
|
public Task<bool> AllEpisodesExist(List<int> episodeIds) =>
|
||||||
|
_dbConnection.QuerySingleAsync<int>(
|
||||||
|
"SELECT COUNT(*) FROM Episode WHERE Id in @EpisodeIds",
|
||||||
|
new { EpisodeIds = episodeIds })
|
||||||
|
.Map(c => c == episodeIds.Count);
|
||||||
|
|
||||||
public async Task<List<Show>> GetAllShows()
|
public async Task<List<Show>> GetAllShows()
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||||
@@ -63,31 +69,6 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.Map(Optional);
|
.Map(Optional);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> GetShowCount()
|
|
||||||
{
|
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
|
||||||
return await dbContext.ShowMetadata
|
|
||||||
.AsNoTracking()
|
|
||||||
.GroupBy(sm => new { sm.Title, sm.Year })
|
|
||||||
.CountAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<ShowMetadata>> GetPagedShows(int pageNumber, int pageSize)
|
|
||||||
{
|
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
|
||||||
return await dbContext.ShowMetadata.FromSqlRaw(
|
|
||||||
@"SELECT * FROM ShowMetadata WHERE Id IN
|
|
||||||
(SELECT MIN(Id) FROM ShowMetadata GROUP BY Title, Year, MetadataKind HAVING MetadataKind = MAX(MetadataKind))
|
|
||||||
ORDER BY SortTitle
|
|
||||||
LIMIT {0} OFFSET {1}",
|
|
||||||
pageSize,
|
|
||||||
(pageNumber - 1) * pageSize)
|
|
||||||
.AsNoTracking()
|
|
||||||
.Include(mm => mm.Artwork)
|
|
||||||
.OrderBy(mm => mm.SortTitle)
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<ShowMetadata>> GetShowsForCards(List<int> ids)
|
public async Task<List<ShowMetadata>> GetShowsForCards(List<int> ids)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||||
@@ -99,6 +80,27 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<EpisodeMetadata>> GetEpisodesForCards(List<int> ids)
|
||||||
|
{
|
||||||
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||||
|
return await dbContext.EpisodeMetadata
|
||||||
|
.AsNoTracking()
|
||||||
|
.Filter(em => ids.Contains(em.EpisodeId))
|
||||||
|
.Include(em => em.Artwork)
|
||||||
|
.Include(em => em.Directors)
|
||||||
|
.Include(em => em.Writers)
|
||||||
|
.Include(em => em.Episode)
|
||||||
|
.ThenInclude(e => e.Season)
|
||||||
|
.ThenInclude(s => s.SeasonMetadata)
|
||||||
|
.ThenInclude(sm => sm.Artwork)
|
||||||
|
.Include(em => em.Episode)
|
||||||
|
.ThenInclude(e => e.Season)
|
||||||
|
.ThenInclude(s => s.Show)
|
||||||
|
.ThenInclude(s => s.ShowMetadata)
|
||||||
|
.OrderBy(em => em.SortTitle)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<Season>> GetAllSeasons()
|
public async Task<List<Season>> GetAllSeasons()
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||||
@@ -187,6 +189,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Filter(em => em.Episode.SeasonId == seasonId)
|
.Filter(em => em.Episode.SeasonId == seasonId)
|
||||||
.Include(em => em.Artwork)
|
.Include(em => em.Artwork)
|
||||||
|
.Include(em => em.Directors)
|
||||||
|
.Include(em => em.Writers)
|
||||||
.Include(em => em.Episode)
|
.Include(em => em.Episode)
|
||||||
.ThenInclude(e => e.Season)
|
.ThenInclude(e => e.Season)
|
||||||
.ThenInclude(s => s.Show)
|
.ThenInclude(s => s.Show)
|
||||||
@@ -225,6 +229,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.Include(s => s.ShowMetadata)
|
.Include(s => s.ShowMetadata)
|
||||||
.ThenInclude(sm => sm.Actors)
|
.ThenInclude(sm => sm.Actors)
|
||||||
.ThenInclude(a => a.Artwork)
|
.ThenInclude(a => a.Artwork)
|
||||||
|
.Include(s => s.ShowMetadata)
|
||||||
|
.ThenInclude(sm => sm.Guids)
|
||||||
.Include(s => s.LibraryPath)
|
.Include(s => s.LibraryPath)
|
||||||
.ThenInclude(lp => lp.Library)
|
.ThenInclude(lp => lp.Library)
|
||||||
.OrderBy(s => s.Id)
|
.OrderBy(s => s.Id)
|
||||||
@@ -248,6 +254,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
metadata.Tags ??= new List<Tag>();
|
metadata.Tags ??= new List<Tag>();
|
||||||
metadata.Studios ??= new List<Studio>();
|
metadata.Studios ??= new List<Studio>();
|
||||||
metadata.Actors ??= new List<Actor>();
|
metadata.Actors ??= new List<Actor>();
|
||||||
|
metadata.Guids ??= new List<MetadataGuid>();
|
||||||
var show = new Show
|
var show = new Show
|
||||||
{
|
{
|
||||||
LibraryPathId = libraryPathId,
|
LibraryPathId = libraryPathId,
|
||||||
@@ -274,6 +281,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
Option<Season> maybeExisting = await dbContext.Seasons
|
Option<Season> maybeExisting = await dbContext.Seasons
|
||||||
.Include(s => s.SeasonMetadata)
|
.Include(s => s.SeasonMetadata)
|
||||||
.ThenInclude(sm => sm.Artwork)
|
.ThenInclude(sm => sm.Artwork)
|
||||||
|
.Include(s => s.SeasonMetadata)
|
||||||
|
.ThenInclude(sm => sm.Guids)
|
||||||
.OrderBy(s => s.ShowId)
|
.OrderBy(s => s.ShowId)
|
||||||
.ThenBy(s => s.SeasonNumber)
|
.ThenBy(s => s.SeasonNumber)
|
||||||
.SingleOrDefaultAsync(s => s.ShowId == show.Id && s.SeasonNumber == seasonNumber);
|
.SingleOrDefaultAsync(s => s.ShowId == show.Id && s.SeasonNumber == seasonNumber);
|
||||||
@@ -293,12 +302,27 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.Include(i => i.EpisodeMetadata)
|
.Include(i => i.EpisodeMetadata)
|
||||||
.ThenInclude(em => em.Artwork)
|
.ThenInclude(em => em.Artwork)
|
||||||
.Include(i => i.EpisodeMetadata)
|
.Include(i => i.EpisodeMetadata)
|
||||||
|
.ThenInclude(em => em.Genres)
|
||||||
|
.Include(i => i.EpisodeMetadata)
|
||||||
|
.ThenInclude(em => em.Tags)
|
||||||
|
.Include(i => i.EpisodeMetadata)
|
||||||
|
.ThenInclude(em => em.Studios)
|
||||||
|
.Include(i => i.EpisodeMetadata)
|
||||||
.ThenInclude(em => em.Actors)
|
.ThenInclude(em => em.Actors)
|
||||||
.ThenInclude(a => a.Artwork)
|
.ThenInclude(a => a.Artwork)
|
||||||
|
.Include(i => i.EpisodeMetadata)
|
||||||
|
.ThenInclude(em => em.Guids)
|
||||||
|
.Include(i => i.EpisodeMetadata)
|
||||||
|
.ThenInclude(mm => mm.Directors)
|
||||||
|
.Include(i => i.EpisodeMetadata)
|
||||||
|
.ThenInclude(mm => mm.Writers)
|
||||||
.Include(i => i.MediaVersions)
|
.Include(i => i.MediaVersions)
|
||||||
.ThenInclude(mv => mv.MediaFiles)
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
.Include(i => i.MediaVersions)
|
.Include(i => i.MediaVersions)
|
||||||
.ThenInclude(mv => mv.Streams)
|
.ThenInclude(mv => mv.Streams)
|
||||||
|
.Include(i => i.LibraryPath)
|
||||||
|
.ThenInclude(lp => lp.Library)
|
||||||
|
.Include(i => i.Season)
|
||||||
.OrderBy(i => i.MediaVersions.First().MediaFiles.First().Path)
|
.OrderBy(i => i.MediaVersions.First().MediaFiles.First().Path)
|
||||||
.SingleOrDefaultAsync(i => i.MediaVersions.First().MediaFiles.First().Path == path);
|
.SingleOrDefaultAsync(i => i.MediaVersions.First().MediaFiles.First().Path == path);
|
||||||
|
|
||||||
@@ -398,6 +422,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.ThenInclude(a => a.Artwork)
|
.ThenInclude(a => a.Artwork)
|
||||||
.Include(i => i.ShowMetadata)
|
.Include(i => i.ShowMetadata)
|
||||||
.ThenInclude(sm => sm.Artwork)
|
.ThenInclude(sm => sm.Artwork)
|
||||||
|
.Include(i => i.ShowMetadata)
|
||||||
|
.ThenInclude(sm => sm.Guids)
|
||||||
.Include(i => i.LibraryPath)
|
.Include(i => i.LibraryPath)
|
||||||
.ThenInclude(lp => lp.Library)
|
.ThenInclude(lp => lp.Library)
|
||||||
.OrderBy(i => i.Key)
|
.OrderBy(i => i.Key)
|
||||||
@@ -415,7 +441,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
Option<PlexSeason> maybeExisting = await dbContext.PlexSeasons
|
Option<PlexSeason> maybeExisting = await dbContext.PlexSeasons
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(i => i.SeasonMetadata)
|
.Include(i => i.SeasonMetadata)
|
||||||
.ThenInclude(mm => mm.Artwork)
|
.ThenInclude(sm => sm.Artwork)
|
||||||
|
.Include(i => i.SeasonMetadata)
|
||||||
|
.ThenInclude(sm => sm.Guids)
|
||||||
.OrderBy(i => i.Key)
|
.OrderBy(i => i.Key)
|
||||||
.SingleOrDefaultAsync(i => i.Key == item.Key);
|
.SingleOrDefaultAsync(i => i.Key == item.Key);
|
||||||
|
|
||||||
@@ -431,6 +459,16 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(i => i.EpisodeMetadata)
|
.Include(i => i.EpisodeMetadata)
|
||||||
.ThenInclude(mm => mm.Artwork)
|
.ThenInclude(mm => mm.Artwork)
|
||||||
|
.Include(i => i.EpisodeMetadata)
|
||||||
|
.ThenInclude(mm => mm.Genres)
|
||||||
|
.Include(i => i.EpisodeMetadata)
|
||||||
|
.ThenInclude(mm => mm.Tags)
|
||||||
|
.Include(i => i.EpisodeMetadata)
|
||||||
|
.ThenInclude(mm => mm.Studios)
|
||||||
|
.Include(i => i.EpisodeMetadata)
|
||||||
|
.ThenInclude(mm => mm.Directors)
|
||||||
|
.Include(i => i.EpisodeMetadata)
|
||||||
|
.ThenInclude(mm => mm.Writers)
|
||||||
.Include(i => i.MediaVersions)
|
.Include(i => i.MediaVersions)
|
||||||
.ThenInclude(mv => mv.MediaFiles)
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
.Include(i => i.MediaVersions)
|
.Include(i => i.MediaVersions)
|
||||||
@@ -438,6 +476,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.Include(e => e.EpisodeMetadata)
|
.Include(e => e.EpisodeMetadata)
|
||||||
.ThenInclude(em => em.Actors)
|
.ThenInclude(em => em.Actors)
|
||||||
.ThenInclude(a => a.Artwork)
|
.ThenInclude(a => a.Artwork)
|
||||||
|
.Include(e => e.EpisodeMetadata)
|
||||||
|
.ThenInclude(em => em.Guids)
|
||||||
|
.Include(i => i.LibraryPath)
|
||||||
|
.ThenInclude(lp => lp.Library)
|
||||||
|
.Include(e => e.Season)
|
||||||
.OrderBy(i => i.Key)
|
.OrderBy(i => i.Key)
|
||||||
.SingleOrDefaultAsync(i => i.Key == item.Key);
|
.SingleOrDefaultAsync(i => i.Key == item.Key);
|
||||||
|
|
||||||
@@ -456,15 +499,27 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
WHERE P.Key = @ShowKey AND ps.Key not in @Keys)",
|
WHERE P.Key = @ShowKey AND ps.Key not in @Keys)",
|
||||||
new { ShowKey = showKey, Keys = seasonKeys }).ToUnit();
|
new { ShowKey = showKey, Keys = seasonKeys }).ToUnit();
|
||||||
|
|
||||||
public Task<Unit> RemoveMissingPlexEpisodes(string seasonKey, List<string> episodeKeys) =>
|
public async Task<List<int>> RemoveMissingPlexEpisodes(string seasonKey, List<string> episodeKeys)
|
||||||
_dbConnection.ExecuteAsync(
|
{
|
||||||
|
List<int> ids = await _dbConnection.QueryAsync<int>(
|
||||||
|
@"SELECT m.Id FROM MediaItem m
|
||||||
|
INNER JOIN Episode e ON m.Id = e.Id
|
||||||
|
INNER JOIN PlexEpisode pe ON pe.Id = m.Id
|
||||||
|
INNER JOIN PlexSeason P on P.Id = e.SeasonId
|
||||||
|
WHERE P.Key = @SeasonKey AND pe.Key not in @Keys",
|
||||||
|
new { SeasonKey = seasonKey, Keys = episodeKeys }).Map(result => result.ToList());
|
||||||
|
|
||||||
|
await _dbConnection.ExecuteAsync(
|
||||||
@"DELETE FROM MediaItem WHERE Id IN
|
@"DELETE FROM MediaItem WHERE Id IN
|
||||||
(SELECT m.Id FROM MediaItem m
|
(SELECT m.Id FROM MediaItem m
|
||||||
INNER JOIN Episode e ON m.Id = e.Id
|
INNER JOIN Episode e ON m.Id = e.Id
|
||||||
INNER JOIN PlexEpisode pe ON pe.Id = m.Id
|
INNER JOIN PlexEpisode pe ON pe.Id = m.Id
|
||||||
INNER JOIN PlexSeason P on P.Id = e.SeasonId
|
INNER JOIN PlexSeason P on P.Id = e.SeasonId
|
||||||
WHERE P.Key = @SeasonKey AND pe.Key not in @Keys)",
|
WHERE P.Key = @SeasonKey AND pe.Key not in @Keys)",
|
||||||
new { SeasonKey = seasonKey, Keys = episodeKeys }).ToUnit();
|
new { SeasonKey = seasonKey, Keys = episodeKeys });
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Unit> SetEpisodeNumber(Episode episode, int episodeNumber)
|
public async Task<Unit> SetEpisodeNumber(Episode episode, int episodeNumber)
|
||||||
{
|
{
|
||||||
@@ -475,6 +530,16 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
return Unit.Default;
|
return Unit.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<bool> AddDirector(EpisodeMetadata metadata, Director director) =>
|
||||||
|
_dbConnection.ExecuteAsync(
|
||||||
|
"INSERT INTO Director (Name, EpisodeMetadataId) VALUES (@Name, @MetadataId)",
|
||||||
|
new { director.Name, MetadataId = metadata.Id }).Map(result => result > 0);
|
||||||
|
|
||||||
|
public Task<bool> AddWriter(EpisodeMetadata metadata, Writer writer) =>
|
||||||
|
_dbConnection.ExecuteAsync(
|
||||||
|
"INSERT INTO Writer (Name, EpisodeMetadataId) VALUES (@Name, @MetadataId)",
|
||||||
|
new { writer.Name, MetadataId = metadata.Id }).Map(result => result > 0);
|
||||||
|
|
||||||
public async Task<List<Episode>> GetShowItems(int showId)
|
public async Task<List<Episode>> GetShowItems(int showId)
|
||||||
{
|
{
|
||||||
IEnumerable<int> ids = await _dbConnection.QueryAsync<int>(
|
IEnumerable<int> ids = await _dbConnection.QueryAsync<int>(
|
||||||
@@ -613,7 +678,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
{
|
{
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
DateAdded = DateTime.UtcNow
|
DateAdded = DateTime.UtcNow,
|
||||||
|
Guids = new List<MetadataGuid>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -651,7 +717,13 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
DateAdded = DateTime.UtcNow,
|
DateAdded = DateTime.UtcNow,
|
||||||
DateUpdated = DateTime.MinValue,
|
DateUpdated = DateTime.MinValue,
|
||||||
MetadataKind = MetadataKind.Fallback,
|
MetadataKind = MetadataKind.Fallback,
|
||||||
Actors = new List<Actor>()
|
Actors = new List<Actor>(),
|
||||||
|
Guids = new List<MetadataGuid>(),
|
||||||
|
Writers = new List<Writer>(),
|
||||||
|
Directors = new List<Director>(),
|
||||||
|
Genres = new List<Genre>(),
|
||||||
|
Tags = new List<Tag>(),
|
||||||
|
Studios = new List<Studio>()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
MediaVersions = new List<MediaVersion>
|
MediaVersions = new List<MediaVersion>
|
||||||
@@ -668,6 +740,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
};
|
};
|
||||||
await dbContext.Episodes.AddAsync(episode);
|
await dbContext.Episodes.AddAsync(episode);
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
|
await dbContext.Entry(episode).Reference(i => i.LibraryPath).LoadAsync();
|
||||||
|
await dbContext.Entry(episode.LibraryPath).Reference(lp => lp.Library).LoadAsync();
|
||||||
|
await dbContext.Entry(episode).Reference(e => e.Season).LoadAsync();
|
||||||
return episode;
|
return episode;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -730,10 +805,19 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
}
|
}
|
||||||
|
|
||||||
item.LibraryPathId = library.Paths.Head().Id;
|
item.LibraryPathId = library.Paths.Head().Id;
|
||||||
|
EpisodeMetadata metadata = item.EpisodeMetadata.Head();
|
||||||
|
metadata.Genres ??= new List<Genre>();
|
||||||
|
metadata.Tags ??= new List<Tag>();
|
||||||
|
metadata.Studios ??= new List<Studio>();
|
||||||
|
metadata.Actors ??= new List<Actor>();
|
||||||
|
metadata.Directors ??= new List<Director>();
|
||||||
|
metadata.Writers ??= new List<Writer>();
|
||||||
|
|
||||||
await dbContext.PlexEpisodes.AddAsync(item);
|
await dbContext.PlexEpisodes.AddAsync(item);
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
await dbContext.Entry(item).Reference(i => i.LibraryPath).LoadAsync();
|
await dbContext.Entry(item).Reference(i => i.LibraryPath).LoadAsync();
|
||||||
|
await dbContext.Entry(item.LibraryPath).Reference(lp => lp.Library).LoadAsync();
|
||||||
|
await dbContext.Entry(item).Reference(e => e.Season).LoadAsync();
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -244,7 +244,8 @@ namespace ErsatzTV.Infrastructure.Emby
|
|||||||
Actors = Optional(item.People).Flatten().Collect(r => ProjectToActor(r, dateAdded)).ToList(),
|
Actors = Optional(item.People).Flatten().Collect(r => ProjectToActor(r, dateAdded)).ToList(),
|
||||||
Directors = Optional(item.People).Flatten().Collect(r => ProjectToDirector(r)).ToList(),
|
Directors = Optional(item.People).Flatten().Collect(r => ProjectToDirector(r)).ToList(),
|
||||||
Writers = Optional(item.People).Flatten().Collect(r => ProjectToWriter(r)).ToList(),
|
Writers = Optional(item.People).Flatten().Collect(r => ProjectToWriter(r)).ToList(),
|
||||||
Artwork = new List<Artwork>()
|
Artwork = new List<Artwork>(),
|
||||||
|
Guids = GuidsFromProviderIds(item.ProviderIds)
|
||||||
};
|
};
|
||||||
|
|
||||||
// set order on actors
|
// set order on actors
|
||||||
@@ -365,7 +366,8 @@ namespace ErsatzTV.Infrastructure.Emby
|
|||||||
Tags = Optional(item.Tags).Flatten().Map(t => new Tag { Name = t }).ToList(),
|
Tags = Optional(item.Tags).Flatten().Map(t => new Tag { Name = t }).ToList(),
|
||||||
Studios = Optional(item.Studios).Flatten().Map(s => new Studio { Name = s.Name }).ToList(),
|
Studios = Optional(item.Studios).Flatten().Map(s => new Studio { Name = s.Name }).ToList(),
|
||||||
Actors = Optional(item.People).Flatten().Collect(r => ProjectToActor(r, dateAdded)).ToList(),
|
Actors = Optional(item.People).Flatten().Collect(r => ProjectToActor(r, dateAdded)).ToList(),
|
||||||
Artwork = new List<Artwork>()
|
Artwork = new List<Artwork>(),
|
||||||
|
Guids = GuidsFromProviderIds(item.ProviderIds)
|
||||||
};
|
};
|
||||||
|
|
||||||
// set order on actors
|
// set order on actors
|
||||||
@@ -429,7 +431,8 @@ namespace ErsatzTV.Infrastructure.Emby
|
|||||||
SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name),
|
SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name),
|
||||||
Year = item.ProductionYear,
|
Year = item.ProductionYear,
|
||||||
DateAdded = dateAdded,
|
DateAdded = dateAdded,
|
||||||
Artwork = new List<Artwork>()
|
Artwork = new List<Artwork>(),
|
||||||
|
Guids = GuidsFromProviderIds(item.ProviderIds)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(item.ImageTags.Primary))
|
if (!string.IsNullOrWhiteSpace(item.ImageTags.Primary))
|
||||||
@@ -540,7 +543,10 @@ namespace ErsatzTV.Infrastructure.Emby
|
|||||||
Tags = new List<Tag>(),
|
Tags = new List<Tag>(),
|
||||||
Studios = new List<Studio>(),
|
Studios = new List<Studio>(),
|
||||||
Actors = new List<Actor>(),
|
Actors = new List<Actor>(),
|
||||||
Artwork = new List<Artwork>()
|
Artwork = new List<Artwork>(),
|
||||||
|
Guids = GuidsFromProviderIds(item.ProviderIds),
|
||||||
|
Directors = Optional(item.People).Flatten().Collect(r => ProjectToDirector(r)).ToList(),
|
||||||
|
Writers = Optional(item.People).Flatten().Collect(r => ProjectToWriter(r)).ToList()
|
||||||
};
|
};
|
||||||
|
|
||||||
if (DateTime.TryParse(item.PremiereDate, out DateTime releaseDate))
|
if (DateTime.TryParse(item.PremiereDate, out DateTime releaseDate))
|
||||||
@@ -561,5 +567,30 @@ namespace ErsatzTV.Infrastructure.Emby
|
|||||||
|
|
||||||
return metadata;
|
return metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<MetadataGuid> GuidsFromProviderIds(EmbyProviderIdsResponse providerIds)
|
||||||
|
{
|
||||||
|
var result = new List<MetadataGuid>();
|
||||||
|
|
||||||
|
if (providerIds != null)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(providerIds.Imdb))
|
||||||
|
{
|
||||||
|
result.Add(new MetadataGuid { Guid = $"imdb://{providerIds.Imdb}" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(providerIds.Tmdb))
|
||||||
|
{
|
||||||
|
result.Add(new MetadataGuid { Guid = $"tmdb://{providerIds.Tmdb}" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(providerIds.Tvdb))
|
||||||
|
{
|
||||||
|
result.Add(new MetadataGuid { Guid = $"tvdb://{providerIds.Tvdb}" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ namespace ErsatzTV.Infrastructure.Emby
|
|||||||
string parentId,
|
string parentId,
|
||||||
[Query]
|
[Query]
|
||||||
string fields =
|
string fields =
|
||||||
"Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,ProductionYear,PremiereDate,MediaSources,OfficialRating",
|
"Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,ProductionYear,PremiereDate,MediaSources,OfficialRating,ProviderIds",
|
||||||
[Query]
|
[Query]
|
||||||
string includeItemTypes = "Movie",
|
string includeItemTypes = "Movie",
|
||||||
[Query]
|
[Query]
|
||||||
@@ -42,7 +42,7 @@ namespace ErsatzTV.Infrastructure.Emby
|
|||||||
string parentId,
|
string parentId,
|
||||||
[Query]
|
[Query]
|
||||||
string fields =
|
string fields =
|
||||||
"Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,ProductionYear,PremiereDate,MediaSources,OfficialRating",
|
"Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,ProductionYear,PremiereDate,MediaSources,OfficialRating,ProviderIds",
|
||||||
[Query]
|
[Query]
|
||||||
string includeItemTypes = "Series",
|
string includeItemTypes = "Series",
|
||||||
[Query]
|
[Query]
|
||||||
@@ -55,10 +55,12 @@ namespace ErsatzTV.Infrastructure.Emby
|
|||||||
[Query]
|
[Query]
|
||||||
string parentId,
|
string parentId,
|
||||||
[Query]
|
[Query]
|
||||||
string fields = "Path,DateCreated,Etag,Taglines",
|
string fields = "Path,DateCreated,Etag,Taglines,ProviderIds",
|
||||||
[Query]
|
[Query]
|
||||||
string includeItemTypes = "Season",
|
string includeItemTypes = "Season",
|
||||||
[Query]
|
[Query]
|
||||||
|
string excludeLocationTypes = "Virtual",
|
||||||
|
[Query]
|
||||||
bool recursive = true);
|
bool recursive = true);
|
||||||
|
|
||||||
[Get("/Items")]
|
[Get("/Items")]
|
||||||
@@ -68,10 +70,13 @@ namespace ErsatzTV.Infrastructure.Emby
|
|||||||
[Query]
|
[Query]
|
||||||
string parentId,
|
string parentId,
|
||||||
[Query]
|
[Query]
|
||||||
string fields = "Path,DateCreated,Etag,Overview,ProductionYear,PremiereDate,MediaSources,LocationType",
|
string fields =
|
||||||
|
"Path,DateCreated,Etag,Overview,ProductionYear,PremiereDate,MediaSources,LocationType,ProviderIds,People",
|
||||||
[Query]
|
[Query]
|
||||||
string includeItemTypes = "Episode",
|
string includeItemTypes = "Episode",
|
||||||
[Query]
|
[Query]
|
||||||
|
string excludeLocationTypes = "Virtual",
|
||||||
|
[Query]
|
||||||
bool recursive = true);
|
bool recursive = true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ namespace ErsatzTV.Infrastructure.Emby.Models
|
|||||||
public List<string> Genres { get; set; }
|
public List<string> Genres { get; set; }
|
||||||
public List<string> Tags { get; set; }
|
public List<string> Tags { get; set; }
|
||||||
public int ProductionYear { get; set; }
|
public int ProductionYear { get; set; }
|
||||||
|
public EmbyProviderIdsResponse ProviderIds { get; set; }
|
||||||
public string PremiereDate { get; set; }
|
public string PremiereDate { get; set; }
|
||||||
public List<EmbyMediaStreamResponse> MediaStreams { get; set; }
|
public List<EmbyMediaStreamResponse> MediaStreams { get; set; }
|
||||||
public List<EmbyMediaSourceResponse> MediaSources { get; set; }
|
public List<EmbyMediaSourceResponse> MediaSources { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace ErsatzTV.Infrastructure.Emby.Models
|
||||||
|
{
|
||||||
|
public class EmbyProviderIdsResponse
|
||||||
|
{
|
||||||
|
public string Imdb { get; set; }
|
||||||
|
public string Tmdb { get; set; }
|
||||||
|
public string Tvdb { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,7 +34,8 @@ namespace ErsatzTV.Infrastructure.Jellyfin
|
|||||||
[Query]
|
[Query]
|
||||||
string parentId,
|
string parentId,
|
||||||
[Query]
|
[Query]
|
||||||
string fields = "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,OfficialRating",
|
string fields =
|
||||||
|
"Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,OfficialRating,ProviderIds",
|
||||||
[Query]
|
[Query]
|
||||||
string includeItemTypes = "Movie",
|
string includeItemTypes = "Movie",
|
||||||
[Query]
|
[Query]
|
||||||
@@ -49,7 +50,8 @@ namespace ErsatzTV.Infrastructure.Jellyfin
|
|||||||
[Query]
|
[Query]
|
||||||
string parentId,
|
string parentId,
|
||||||
[Query]
|
[Query]
|
||||||
string fields = "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,OfficialRating",
|
string fields =
|
||||||
|
"Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,OfficialRating,ProviderIds",
|
||||||
[Query]
|
[Query]
|
||||||
string includeItemTypes = "Series",
|
string includeItemTypes = "Series",
|
||||||
[Query]
|
[Query]
|
||||||
@@ -64,7 +66,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin
|
|||||||
[Query]
|
[Query]
|
||||||
string parentId,
|
string parentId,
|
||||||
[Query]
|
[Query]
|
||||||
string fields = "Path,DateCreated,Etag,Taglines",
|
string fields = "Path,DateCreated,Etag,Taglines,ProviderIds",
|
||||||
[Query]
|
[Query]
|
||||||
string includeItemTypes = "Season",
|
string includeItemTypes = "Season",
|
||||||
[Query]
|
[Query]
|
||||||
@@ -79,7 +81,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin
|
|||||||
[Query]
|
[Query]
|
||||||
string parentId,
|
string parentId,
|
||||||
[Query]
|
[Query]
|
||||||
string fields = "Path,DateCreated,Etag,Overview",
|
string fields = "Path,DateCreated,Etag,Overview,ProviderIds,People",
|
||||||
[Query]
|
[Query]
|
||||||
string includeItemTypes = "Episode",
|
string includeItemTypes = "Episode",
|
||||||
[Query]
|
[Query]
|
||||||
|
|||||||
@@ -290,7 +290,8 @@ namespace ErsatzTV.Infrastructure.Jellyfin
|
|||||||
Actors = Optional(item.People).Flatten().Collect(r => ProjectToActor(r, dateAdded)).ToList(),
|
Actors = Optional(item.People).Flatten().Collect(r => ProjectToActor(r, dateAdded)).ToList(),
|
||||||
Directors = Optional(item.People).Flatten().Collect(r => ProjectToDirector(r)).ToList(),
|
Directors = Optional(item.People).Flatten().Collect(r => ProjectToDirector(r)).ToList(),
|
||||||
Writers = Optional(item.People).Flatten().Collect(r => ProjectToWriter(r)).ToList(),
|
Writers = Optional(item.People).Flatten().Collect(r => ProjectToWriter(r)).ToList(),
|
||||||
Artwork = new List<Artwork>()
|
Artwork = new List<Artwork>(),
|
||||||
|
Guids = GuidsFromProviderIds(item.ProviderIds)
|
||||||
};
|
};
|
||||||
|
|
||||||
// set order on actors
|
// set order on actors
|
||||||
@@ -416,7 +417,8 @@ namespace ErsatzTV.Infrastructure.Jellyfin
|
|||||||
Tags = Optional(item.Tags).Flatten().Map(t => new Tag { Name = t }).ToList(),
|
Tags = Optional(item.Tags).Flatten().Map(t => new Tag { Name = t }).ToList(),
|
||||||
Studios = Optional(item.Studios).Flatten().Map(s => new Studio { Name = s.Name }).ToList(),
|
Studios = Optional(item.Studios).Flatten().Map(s => new Studio { Name = s.Name }).ToList(),
|
||||||
Actors = Optional(item.People).Flatten().Collect(r => ProjectToActor(r, dateAdded)).ToList(),
|
Actors = Optional(item.People).Flatten().Collect(r => ProjectToActor(r, dateAdded)).ToList(),
|
||||||
Artwork = new List<Artwork>()
|
Artwork = new List<Artwork>(),
|
||||||
|
Guids = GuidsFromProviderIds(item.ProviderIds)
|
||||||
};
|
};
|
||||||
|
|
||||||
// set order on actors
|
// set order on actors
|
||||||
@@ -485,7 +487,8 @@ namespace ErsatzTV.Infrastructure.Jellyfin
|
|||||||
SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name),
|
SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name),
|
||||||
Year = item.ProductionYear,
|
Year = item.ProductionYear,
|
||||||
DateAdded = dateAdded,
|
DateAdded = dateAdded,
|
||||||
Artwork = new List<Artwork>()
|
Artwork = new List<Artwork>(),
|
||||||
|
Guids = GuidsFromProviderIds(item.ProviderIds)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(item.ImageTags.Primary))
|
if (!string.IsNullOrWhiteSpace(item.ImageTags.Primary))
|
||||||
@@ -596,7 +599,10 @@ namespace ErsatzTV.Infrastructure.Jellyfin
|
|||||||
Tags = new List<Tag>(),
|
Tags = new List<Tag>(),
|
||||||
Studios = new List<Studio>(),
|
Studios = new List<Studio>(),
|
||||||
Actors = new List<Actor>(),
|
Actors = new List<Actor>(),
|
||||||
Artwork = new List<Artwork>()
|
Artwork = new List<Artwork>(),
|
||||||
|
Guids = GuidsFromProviderIds(item.ProviderIds),
|
||||||
|
Directors = Optional(item.People).Flatten().Collect(r => ProjectToDirector(r)).ToList(),
|
||||||
|
Writers = Optional(item.People).Flatten().Collect(r => ProjectToWriter(r)).ToList()
|
||||||
};
|
};
|
||||||
|
|
||||||
if (DateTime.TryParse(item.PremiereDate, out DateTime releaseDate))
|
if (DateTime.TryParse(item.PremiereDate, out DateTime releaseDate))
|
||||||
@@ -617,5 +623,30 @@ namespace ErsatzTV.Infrastructure.Jellyfin
|
|||||||
|
|
||||||
return metadata;
|
return metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<MetadataGuid> GuidsFromProviderIds(JellyfinProviderIdsResponse providerIds)
|
||||||
|
{
|
||||||
|
var result = new List<MetadataGuid>();
|
||||||
|
|
||||||
|
if (providerIds != null)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(providerIds.Imdb))
|
||||||
|
{
|
||||||
|
result.Add(new MetadataGuid { Guid = $"imdb://{providerIds.Imdb}" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(providerIds.Tmdb))
|
||||||
|
{
|
||||||
|
result.Add(new MetadataGuid { Guid = $"tmdb://{providerIds.Tmdb}" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(providerIds.Tvdb))
|
||||||
|
{
|
||||||
|
result.Add(new MetadataGuid { Guid = $"tvdb://{providerIds.Tvdb}" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin.Models
|
|||||||
public List<string> Genres { get; set; }
|
public List<string> Genres { get; set; }
|
||||||
public List<string> Tags { get; set; }
|
public List<string> Tags { get; set; }
|
||||||
public int ProductionYear { get; set; }
|
public int ProductionYear { get; set; }
|
||||||
|
public JellyfinProviderIdsResponse ProviderIds { get; set; }
|
||||||
public string PremiereDate { get; set; }
|
public string PremiereDate { get; set; }
|
||||||
public List<JellyfinMediaStreamResponse> MediaStreams { get; set; }
|
public List<JellyfinMediaStreamResponse> MediaStreams { get; set; }
|
||||||
public string LocationType { get; set; }
|
public string LocationType { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace ErsatzTV.Infrastructure.Jellyfin.Models
|
||||||
|
{
|
||||||
|
public class JellyfinProviderIdsResponse
|
||||||
|
{
|
||||||
|
public string Imdb { get; set; }
|
||||||
|
public string Tmdb { get; set; }
|
||||||
|
public string Tvdb { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
+2862
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,124 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
public partial class Add_MetadataGuid : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
// local and plex
|
||||||
|
migrationBuilder.Sql("UPDATE MovieMetadata SET DateUpdated = '0001-01-01 00:00:00'");
|
||||||
|
migrationBuilder.Sql("UPDATE ShowMetadata SET DateUpdated = '0001-01-01 00:00:00'");
|
||||||
|
migrationBuilder.Sql("UPDATE SeasonMetadata SET DateUpdated = '0001-01-01 00:00:00'");
|
||||||
|
migrationBuilder.Sql("UPDATE EpisodeMetadata SET DateUpdated = '0001-01-01 00:00:00'");
|
||||||
|
migrationBuilder.Sql(
|
||||||
|
@"UPDATE LibraryFolder SET Etag = NULL WHERE LibraryPathId IN
|
||||||
|
(SELECT LibraryPathId FROM LibraryPath LP
|
||||||
|
INNER JOIN Library L on LP.LibraryId = L.Id
|
||||||
|
WHERE L.MediaKind = 1)");
|
||||||
|
|
||||||
|
// emby
|
||||||
|
migrationBuilder.Sql("UPDATE EmbyMovie SET Etag = NULL");
|
||||||
|
migrationBuilder.Sql("UPDATE EmbyShow SET Etag = NULL");
|
||||||
|
migrationBuilder.Sql("UPDATE EmbySeason SET Etag = NULL");
|
||||||
|
migrationBuilder.Sql("UPDATE EmbyEpisode SET Etag = NULL");
|
||||||
|
|
||||||
|
// jellyfin
|
||||||
|
migrationBuilder.Sql("UPDATE JellyfinMovie SET Etag = NULL");
|
||||||
|
migrationBuilder.Sql("UPDATE JellyfinShow SET Etag = NULL");
|
||||||
|
migrationBuilder.Sql("UPDATE JellyfinSeason SET Etag = NULL");
|
||||||
|
migrationBuilder.Sql("UPDATE JellyfinEpisode SET Etag = NULL");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "MetadataGuid",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Guid = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
ArtistMetadataId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||||
|
EpisodeMetadataId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||||
|
MovieMetadataId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||||
|
MusicVideoMetadataId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||||
|
SeasonMetadataId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||||
|
ShowMetadataId = table.Column<int>(type: "INTEGER", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_MetadataGuid", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_MetadataGuid_ArtistMetadata_ArtistMetadataId",
|
||||||
|
column: x => x.ArtistMetadataId,
|
||||||
|
principalTable: "ArtistMetadata",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_MetadataGuid_EpisodeMetadata_EpisodeMetadataId",
|
||||||
|
column: x => x.EpisodeMetadataId,
|
||||||
|
principalTable: "EpisodeMetadata",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_MetadataGuid_MovieMetadata_MovieMetadataId",
|
||||||
|
column: x => x.MovieMetadataId,
|
||||||
|
principalTable: "MovieMetadata",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_MetadataGuid_MusicVideoMetadata_MusicVideoMetadataId",
|
||||||
|
column: x => x.MusicVideoMetadataId,
|
||||||
|
principalTable: "MusicVideoMetadata",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_MetadataGuid_SeasonMetadata_SeasonMetadataId",
|
||||||
|
column: x => x.SeasonMetadataId,
|
||||||
|
principalTable: "SeasonMetadata",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_MetadataGuid_ShowMetadata_ShowMetadataId",
|
||||||
|
column: x => x.ShowMetadataId,
|
||||||
|
principalTable: "ShowMetadata",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_MetadataGuid_ArtistMetadataId",
|
||||||
|
table: "MetadataGuid",
|
||||||
|
column: "ArtistMetadataId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_MetadataGuid_EpisodeMetadataId",
|
||||||
|
table: "MetadataGuid",
|
||||||
|
column: "EpisodeMetadataId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_MetadataGuid_MovieMetadataId",
|
||||||
|
table: "MetadataGuid",
|
||||||
|
column: "MovieMetadataId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_MetadataGuid_MusicVideoMetadataId",
|
||||||
|
table: "MetadataGuid",
|
||||||
|
column: "MusicVideoMetadataId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_MetadataGuid_SeasonMetadataId",
|
||||||
|
table: "MetadataGuid",
|
||||||
|
column: "SeasonMetadataId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_MetadataGuid_ShowMetadataId",
|
||||||
|
table: "MetadataGuid",
|
||||||
|
column: "ShowMetadataId");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "MetadataGuid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+2886
File diff suppressed because it is too large
Load Diff
+75
@@ -0,0 +1,75 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
public partial class Add_EpisodeMetadataDirectorsWriters : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "EpisodeMetadataId",
|
||||||
|
table: "Writer",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "EpisodeMetadataId",
|
||||||
|
table: "Director",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Writer_EpisodeMetadataId",
|
||||||
|
table: "Writer",
|
||||||
|
column: "EpisodeMetadataId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Director_EpisodeMetadataId",
|
||||||
|
table: "Director",
|
||||||
|
column: "EpisodeMetadataId");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Director_EpisodeMetadata_EpisodeMetadataId",
|
||||||
|
table: "Director",
|
||||||
|
column: "EpisodeMetadataId",
|
||||||
|
principalTable: "EpisodeMetadata",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Writer_EpisodeMetadata_EpisodeMetadataId",
|
||||||
|
table: "Writer",
|
||||||
|
column: "EpisodeMetadataId",
|
||||||
|
principalTable: "EpisodeMetadata",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Director_EpisodeMetadata_EpisodeMetadataId",
|
||||||
|
table: "Director");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Writer_EpisodeMetadata_EpisodeMetadataId",
|
||||||
|
table: "Writer");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Writer_EpisodeMetadataId",
|
||||||
|
table: "Writer");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Director_EpisodeMetadataId",
|
||||||
|
table: "Director");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "EpisodeMetadataId",
|
||||||
|
table: "Writer");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "EpisodeMetadataId",
|
||||||
|
table: "Director");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -274,6 +274,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("EpisodeMetadataId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int?>("MovieMetadataId")
|
b.Property<int?>("MovieMetadataId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
@@ -282,6 +285,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("EpisodeMetadataId");
|
||||||
|
|
||||||
b.HasIndex("MovieMetadataId");
|
b.HasIndex("MovieMetadataId");
|
||||||
|
|
||||||
b.ToTable("Director");
|
b.ToTable("Director");
|
||||||
@@ -740,6 +745,50 @@ namespace ErsatzTV.Infrastructure.Migrations
|
|||||||
b.ToTable("MediaVersion");
|
b.ToTable("MediaVersion");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ErsatzTV.Core.Domain.MetadataGuid", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("ArtistMetadataId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("EpisodeMetadataId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Guid")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("MovieMetadataId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("MusicVideoMetadataId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("SeasonMetadataId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("ShowMetadataId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ArtistMetadataId");
|
||||||
|
|
||||||
|
b.HasIndex("EpisodeMetadataId");
|
||||||
|
|
||||||
|
b.HasIndex("MovieMetadataId");
|
||||||
|
|
||||||
|
b.HasIndex("MusicVideoMetadataId");
|
||||||
|
|
||||||
|
b.HasIndex("SeasonMetadataId");
|
||||||
|
|
||||||
|
b.HasIndex("ShowMetadataId");
|
||||||
|
|
||||||
|
b.ToTable("MetadataGuid");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Mood", b =>
|
modelBuilder.Entity("ErsatzTV.Core.Domain.Mood", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -1281,6 +1330,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("EpisodeMetadataId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int?>("MovieMetadataId")
|
b.Property<int?>("MovieMetadataId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
@@ -1289,6 +1341,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("EpisodeMetadataId");
|
||||||
|
|
||||||
b.HasIndex("MovieMetadataId");
|
b.HasIndex("MovieMetadataId");
|
||||||
|
|
||||||
b.ToTable("Writer");
|
b.ToTable("Writer");
|
||||||
@@ -1770,6 +1824,11 @@ namespace ErsatzTV.Infrastructure.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Director", b =>
|
modelBuilder.Entity("ErsatzTV.Core.Domain.Director", b =>
|
||||||
{
|
{
|
||||||
|
b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null)
|
||||||
|
.WithMany("Directors")
|
||||||
|
.HasForeignKey("EpisodeMetadataId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null)
|
b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null)
|
||||||
.WithMany("Directors")
|
.WithMany("Directors")
|
||||||
.HasForeignKey("MovieMetadataId")
|
.HasForeignKey("MovieMetadataId")
|
||||||
@@ -1957,6 +2016,37 @@ namespace ErsatzTV.Infrastructure.Migrations
|
|||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ErsatzTV.Core.Domain.MetadataGuid", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null)
|
||||||
|
.WithMany("Guids")
|
||||||
|
.HasForeignKey("ArtistMetadataId");
|
||||||
|
|
||||||
|
b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null)
|
||||||
|
.WithMany("Guids")
|
||||||
|
.HasForeignKey("EpisodeMetadataId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null)
|
||||||
|
.WithMany("Guids")
|
||||||
|
.HasForeignKey("MovieMetadataId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null)
|
||||||
|
.WithMany("Guids")
|
||||||
|
.HasForeignKey("MusicVideoMetadataId");
|
||||||
|
|
||||||
|
b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null)
|
||||||
|
.WithMany("Guids")
|
||||||
|
.HasForeignKey("SeasonMetadataId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null)
|
||||||
|
.WithMany("Guids")
|
||||||
|
.HasForeignKey("ShowMetadataId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Mood", b =>
|
modelBuilder.Entity("ErsatzTV.Core.Domain.Mood", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null)
|
b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null)
|
||||||
@@ -2255,6 +2345,11 @@ namespace ErsatzTV.Infrastructure.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Writer", b =>
|
modelBuilder.Entity("ErsatzTV.Core.Domain.Writer", b =>
|
||||||
{
|
{
|
||||||
|
b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null)
|
||||||
|
.WithMany("Writers")
|
||||||
|
.HasForeignKey("EpisodeMetadataId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null)
|
b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null)
|
||||||
.WithMany("Writers")
|
.WithMany("Writers")
|
||||||
.HasForeignKey("MovieMetadataId")
|
.HasForeignKey("MovieMetadataId")
|
||||||
@@ -2572,6 +2667,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.Navigation("Genres");
|
b.Navigation("Genres");
|
||||||
|
|
||||||
|
b.Navigation("Guids");
|
||||||
|
|
||||||
b.Navigation("Moods");
|
b.Navigation("Moods");
|
||||||
|
|
||||||
b.Navigation("Studios");
|
b.Navigation("Studios");
|
||||||
@@ -2599,11 +2696,17 @@ namespace ErsatzTV.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.Navigation("Artwork");
|
b.Navigation("Artwork");
|
||||||
|
|
||||||
|
b.Navigation("Directors");
|
||||||
|
|
||||||
b.Navigation("Genres");
|
b.Navigation("Genres");
|
||||||
|
|
||||||
|
b.Navigation("Guids");
|
||||||
|
|
||||||
b.Navigation("Studios");
|
b.Navigation("Studios");
|
||||||
|
|
||||||
b.Navigation("Tags");
|
b.Navigation("Tags");
|
||||||
|
|
||||||
|
b.Navigation("Writers");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b =>
|
modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b =>
|
||||||
@@ -2645,6 +2748,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.Navigation("Genres");
|
b.Navigation("Genres");
|
||||||
|
|
||||||
|
b.Navigation("Guids");
|
||||||
|
|
||||||
b.Navigation("Studios");
|
b.Navigation("Studios");
|
||||||
|
|
||||||
b.Navigation("Tags");
|
b.Navigation("Tags");
|
||||||
@@ -2660,6 +2765,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.Navigation("Genres");
|
b.Navigation("Genres");
|
||||||
|
|
||||||
|
b.Navigation("Guids");
|
||||||
|
|
||||||
b.Navigation("Studios");
|
b.Navigation("Studios");
|
||||||
|
|
||||||
b.Navigation("Tags");
|
b.Navigation("Tags");
|
||||||
@@ -2687,6 +2794,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.Navigation("Genres");
|
b.Navigation("Genres");
|
||||||
|
|
||||||
|
b.Navigation("Guids");
|
||||||
|
|
||||||
b.Navigation("Studios");
|
b.Navigation("Studios");
|
||||||
|
|
||||||
b.Navigation("Tags");
|
b.Navigation("Tags");
|
||||||
@@ -2700,6 +2809,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.Navigation("Genres");
|
b.Navigation("Genres");
|
||||||
|
|
||||||
|
b.Navigation("Guids");
|
||||||
|
|
||||||
b.Navigation("Studios");
|
b.Navigation("Studios");
|
||||||
|
|
||||||
b.Navigation("Tags");
|
b.Navigation("Tags");
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ using Refit;
|
|||||||
|
|
||||||
namespace ErsatzTV.Infrastructure.Plex
|
namespace ErsatzTV.Infrastructure.Plex
|
||||||
{
|
{
|
||||||
[Headers("Accept: application/json")]
|
|
||||||
public interface IPlexServerApi
|
public interface IPlexServerApi
|
||||||
{
|
{
|
||||||
[Get("/library/sections")]
|
[Get("/library/sections")]
|
||||||
|
[Headers("Accept: application/json")]
|
||||||
public Task<PlexMediaContainerResponse<PlexMediaContainerDirectoryContent<PlexLibraryResponse>>> GetLibraries(
|
public Task<PlexMediaContainerResponse<PlexMediaContainerDirectoryContent<PlexLibraryResponse>>> GetLibraries(
|
||||||
[Query] [AliasAs("X-Plex-Token")]
|
[Query] [AliasAs("X-Plex-Token")]
|
||||||
string token);
|
string token);
|
||||||
|
|
||||||
[Get("/library/sections/{key}/all")]
|
[Get("/library/sections/{key}/all")]
|
||||||
|
[Headers("Accept: application/json")]
|
||||||
public Task<PlexMediaContainerResponse<PlexMediaContainerMetadataContent<PlexMetadataResponse>>>
|
public Task<PlexMediaContainerResponse<PlexMediaContainerMetadataContent<PlexMetadataResponse>>>
|
||||||
GetLibrarySectionContents(
|
GetLibrarySectionContents(
|
||||||
string key,
|
string key,
|
||||||
@@ -20,15 +21,33 @@ namespace ErsatzTV.Infrastructure.Plex
|
|||||||
string token);
|
string token);
|
||||||
|
|
||||||
[Get("/library/metadata/{key}")]
|
[Get("/library/metadata/{key}")]
|
||||||
public Task<PlexMediaContainerResponse<PlexMediaContainerMetadataContent<PlexMetadataResponse>>>
|
[Headers("Accept: text/xml")]
|
||||||
GetMetadata(
|
public Task<PlexXmlVideoMetadataResponseContainer>
|
||||||
|
GetVideoMetadata(
|
||||||
|
string key,
|
||||||
|
[Query] [AliasAs("X-Plex-Token")]
|
||||||
|
string token);
|
||||||
|
|
||||||
|
[Get("/library/metadata/{key}")]
|
||||||
|
[Headers("Accept: text/xml")]
|
||||||
|
public Task<PlexXmlDirectoryMetadataResponseContainer>
|
||||||
|
GetDirectoryMetadata(
|
||||||
string key,
|
string key,
|
||||||
[Query] [AliasAs("X-Plex-Token")]
|
[Query] [AliasAs("X-Plex-Token")]
|
||||||
string token);
|
string token);
|
||||||
|
|
||||||
[Get("/library/metadata/{key}/children")]
|
[Get("/library/metadata/{key}/children")]
|
||||||
public Task<PlexMediaContainerResponse<PlexMediaContainerMetadataContent<PlexMetadataResponse>>>
|
[Headers("Accept: text/xml")]
|
||||||
GetChildren(
|
public Task<PlexXmlSeasonsMetadataResponseContainer>
|
||||||
|
GetShowChildren(
|
||||||
|
string key,
|
||||||
|
[Query] [AliasAs("X-Plex-Token")]
|
||||||
|
string token);
|
||||||
|
|
||||||
|
[Get("/library/metadata/{key}/children")]
|
||||||
|
[Headers("Accept: text/xml")]
|
||||||
|
public Task<PlexXmlEpisodesMetadataResponseContainer>
|
||||||
|
GetSeasonChildren(
|
||||||
string key,
|
string key,
|
||||||
[Query] [AliasAs("X-Plex-Token")]
|
[Query] [AliasAs("X-Plex-Token")]
|
||||||
string token);
|
string token);
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
namespace ErsatzTV.Infrastructure.Plex.Models
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Plex.Models
|
||||||
{
|
{
|
||||||
public class PlexDirectorResponse
|
public class PlexDirectorResponse
|
||||||
{
|
{
|
||||||
|
[XmlAttribute("tag")]
|
||||||
public string Tag { get; set; }
|
public string Tag { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
namespace ErsatzTV.Infrastructure.Plex.Models
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Plex.Models
|
||||||
{
|
{
|
||||||
public class PlexGenreResponse
|
public class PlexGenreResponse
|
||||||
{
|
{
|
||||||
|
[XmlAttribute("id")]
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("filter")]
|
||||||
public string Filter { get; set; }
|
public string Filter { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("tag")]
|
||||||
public string Tag { get; set; }
|
public string Tag { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Plex.Models
|
||||||
|
{
|
||||||
|
public class PlexGuidResponse
|
||||||
|
{
|
||||||
|
[XmlAttribute("id")]
|
||||||
|
public string Id { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
namespace ErsatzTV.Infrastructure.Plex.Models
|
namespace ErsatzTV.Infrastructure.Plex.Models
|
||||||
{
|
{
|
||||||
@@ -16,4 +17,32 @@ namespace ErsatzTV.Infrastructure.Plex.Models
|
|||||||
{
|
{
|
||||||
public List<T> Metadata { get; set; }
|
public List<T> Metadata { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[XmlRoot("MediaContainer", Namespace = null)]
|
||||||
|
public class PlexXmlVideoMetadataResponseContainer
|
||||||
|
{
|
||||||
|
[XmlElement("Video")]
|
||||||
|
public PlexXmlMetadataResponse Metadata { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[XmlRoot("MediaContainer", Namespace = null)]
|
||||||
|
public class PlexXmlDirectoryMetadataResponseContainer
|
||||||
|
{
|
||||||
|
[XmlElement("Directory")]
|
||||||
|
public PlexXmlMetadataResponse Metadata { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[XmlRoot("MediaContainer", Namespace = null)]
|
||||||
|
public class PlexXmlSeasonsMetadataResponseContainer
|
||||||
|
{
|
||||||
|
[XmlElement("Directory")]
|
||||||
|
public List<PlexXmlMetadataResponse> Metadata { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[XmlRoot("MediaContainer", Namespace = null)]
|
||||||
|
public class PlexXmlEpisodesMetadataResponseContainer
|
||||||
|
{
|
||||||
|
[XmlElement("Video")]
|
||||||
|
public List<PlexXmlMetadataResponse> Metadata { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,50 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
namespace ErsatzTV.Infrastructure.Plex.Models
|
namespace ErsatzTV.Infrastructure.Plex.Models
|
||||||
{
|
{
|
||||||
public class PlexMediaResponse
|
public class PlexMediaResponse<T>
|
||||||
{
|
{
|
||||||
|
[XmlAttribute("id")]
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("duration")]
|
||||||
public int Duration { get; set; }
|
public int Duration { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("bitrate")]
|
||||||
public int Bitrate { get; set; }
|
public int Bitrate { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("width")]
|
||||||
public int Width { get; set; }
|
public int Width { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("height")]
|
||||||
public int Height { get; set; }
|
public int Height { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("aspectRatio")]
|
||||||
public double AspectRatio { get; set; }
|
public double AspectRatio { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("audioChannels")]
|
||||||
public int AudioChannels { get; set; }
|
public int AudioChannels { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("audioCodec")]
|
||||||
public string AudioCodec { get; set; }
|
public string AudioCodec { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("videoCodec")]
|
||||||
public string VideoCodec { get; set; }
|
public string VideoCodec { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("videoResulution")]
|
||||||
|
public string VideoResolution { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("videoProfile")]
|
||||||
public string VideoProfile { get; set; }
|
public string VideoProfile { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("container")]
|
||||||
public string Container { get; set; }
|
public string Container { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("videoFrameRate")]
|
||||||
public string VideoFrameRate { get; set; }
|
public string VideoFrameRate { get; set; }
|
||||||
public List<PlexPartResponse> Part { get; set; }
|
|
||||||
|
[XmlElement("Part")]
|
||||||
|
public List<T> Part { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,74 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
namespace ErsatzTV.Infrastructure.Plex.Models
|
namespace ErsatzTV.Infrastructure.Plex.Models
|
||||||
{
|
{
|
||||||
public class PlexMetadataResponse
|
public class PlexMetadataResponse
|
||||||
{
|
{
|
||||||
|
[XmlAttribute("key")]
|
||||||
public string Key { get; set; }
|
public string Key { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("title")]
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("contentRating")]
|
||||||
public string ContentRating { get; set; }
|
public string ContentRating { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("summary")]
|
||||||
public string Summary { get; set; }
|
public string Summary { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("year")]
|
||||||
public int Year { get; set; }
|
public int Year { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("tagline")]
|
||||||
public string Tagline { get; set; }
|
public string Tagline { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("thumb")]
|
||||||
public string Thumb { get; set; }
|
public string Thumb { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("art")]
|
||||||
public string Art { get; set; }
|
public string Art { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("originallyAvailableAt")]
|
||||||
public string OriginallyAvailableAt { get; set; }
|
public string OriginallyAvailableAt { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("addedAt")]
|
||||||
public long AddedAt { get; set; }
|
public long AddedAt { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("updatedAt")]
|
||||||
public long UpdatedAt { get; set; }
|
public long UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("index")]
|
||||||
public int Index { get; set; }
|
public int Index { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("studio")]
|
||||||
public string Studio { get; set; }
|
public string Studio { get; set; }
|
||||||
public List<PlexMediaResponse> Media { get; set; }
|
|
||||||
|
[XmlAttribute("rating")]
|
||||||
|
public double Rating { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("audienceRating")]
|
||||||
|
public double AudienceRating { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("audienceRatingImage")]
|
||||||
|
public string AudienceRatingImage { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("ratingImage")]
|
||||||
|
public string RatingImage { get; set; }
|
||||||
|
|
||||||
|
[XmlIgnore]
|
||||||
|
public virtual List<PlexMediaResponse<PlexPartResponse>> Media { get; set; }
|
||||||
|
|
||||||
|
[XmlElement("Genre")]
|
||||||
public List<PlexGenreResponse> Genre { get; set; }
|
public List<PlexGenreResponse> Genre { get; set; }
|
||||||
|
|
||||||
|
[XmlElement("Role")]
|
||||||
public List<PlexRoleResponse> Role { get; set; }
|
public List<PlexRoleResponse> Role { get; set; }
|
||||||
|
|
||||||
|
[XmlElement("Director")]
|
||||||
public List<PlexDirectorResponse> Director { get; set; }
|
public List<PlexDirectorResponse> Director { get; set; }
|
||||||
|
|
||||||
|
[XmlElement("Writer")]
|
||||||
public List<PlexWriterResponse> Writer { get; set; }
|
public List<PlexWriterResponse> Writer { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using System.Collections.Generic;
|
namespace ErsatzTV.Infrastructure.Plex.Models
|
||||||
|
|
||||||
namespace ErsatzTV.Infrastructure.Plex.Models
|
|
||||||
{
|
{
|
||||||
public class PlexPartResponse
|
public class PlexPartResponse
|
||||||
{
|
{
|
||||||
@@ -8,6 +6,5 @@ namespace ErsatzTV.Infrastructure.Plex.Models
|
|||||||
public string Key { get; set; }
|
public string Key { get; set; }
|
||||||
public int Duration { get; set; }
|
public int Duration { get; set; }
|
||||||
public string File { get; set; }
|
public string File { get; set; }
|
||||||
public List<PlexStreamResponse> Stream { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
namespace ErsatzTV.Infrastructure.Plex.Models
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Plex.Models
|
||||||
{
|
{
|
||||||
public class PlexRoleResponse
|
public class PlexRoleResponse
|
||||||
{
|
{
|
||||||
|
[XmlAttribute("tag")]
|
||||||
public string Tag { get; set; }
|
public string Tag { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("role")]
|
||||||
public string Role { get; set; }
|
public string Role { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("thumb")]
|
||||||
public string Thumb { get; set; }
|
public string Thumb { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,43 @@
|
|||||||
namespace ErsatzTV.Infrastructure.Plex.Models
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Plex.Models
|
||||||
{
|
{
|
||||||
public class PlexStreamResponse
|
public class PlexStreamResponse
|
||||||
{
|
{
|
||||||
|
[XmlAttribute("id")]
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("index")]
|
||||||
public int Index { get; set; }
|
public int Index { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("default")]
|
||||||
public bool Default { get; set; }
|
public bool Default { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("forced")]
|
||||||
public bool Forced { get; set; }
|
public bool Forced { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("languageCode")]
|
||||||
public string LanguageCode { get; set; }
|
public string LanguageCode { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("streamType")]
|
||||||
public int StreamType { get; set; }
|
public int StreamType { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("codec")]
|
||||||
public string Codec { get; set; }
|
public string Codec { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("profile")]
|
||||||
public string Profile { get; set; }
|
public string Profile { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("channels")]
|
||||||
public int Channels { get; set; }
|
public int Channels { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("anamorphic")]
|
||||||
public bool Anamorphic { get; set; }
|
public bool Anamorphic { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("pixelAspectRatio")]
|
||||||
public string PixelAspectRatio { get; set; }
|
public string PixelAspectRatio { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("scanType")]
|
||||||
public string ScanType { get; set; }
|
public string ScanType { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
namespace ErsatzTV.Infrastructure.Plex.Models
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Plex.Models
|
||||||
{
|
{
|
||||||
public class PlexWriterResponse
|
public class PlexWriterResponse
|
||||||
{
|
{
|
||||||
|
[XmlAttribute("tag")]
|
||||||
public string Tag { get; set; }
|
public string Tag { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Plex.Models
|
||||||
|
{
|
||||||
|
public class PlexXmlMetadataResponse : PlexMetadataResponse
|
||||||
|
{
|
||||||
|
[XmlAttribute("guid")]
|
||||||
|
public string PlexGuid { get; set; }
|
||||||
|
|
||||||
|
[XmlElement("Media")]
|
||||||
|
public new List<PlexMediaResponse<PlexXmlPartResponse>> Media { get; set; }
|
||||||
|
|
||||||
|
[XmlElement("Guid")]
|
||||||
|
public List<PlexGuidResponse> Guid { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Plex.Models
|
||||||
|
{
|
||||||
|
public class PlexXmlPartResponse
|
||||||
|
{
|
||||||
|
[XmlAttribute("id")]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("key")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("duration")]
|
||||||
|
public int Duration { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute("file")]
|
||||||
|
public string File { get; set; }
|
||||||
|
|
||||||
|
[XmlElement("Stream")]
|
||||||
|
public List<PlexStreamResponse> Stream { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Xml.Serialization;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
@@ -87,9 +88,9 @@ namespace ErsatzTV.Infrastructure.Plex
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
IPlexServerApi service = RestService.For<IPlexServerApi>(connection.Uri);
|
IPlexServerApi service = XmlServiceFor(connection.Uri);
|
||||||
return await service.GetChildren(show.Key.Split("/").Reverse().Skip(1).Head(), token.AuthToken)
|
return await service.GetShowChildren(show.Key.Split("/").Reverse().Skip(1).Head(), token.AuthToken)
|
||||||
.Map(r => r.MediaContainer.Metadata)
|
.Map(r => r.Metadata)
|
||||||
.Map(list => list.Map(metadata => ProjectToSeason(metadata, library.MediaSourceId)).ToList());
|
.Map(list => list.Map(metadata => ProjectToSeason(metadata, library.MediaSourceId)).ToList());
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -106,9 +107,9 @@ namespace ErsatzTV.Infrastructure.Plex
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
IPlexServerApi service = RestService.For<IPlexServerApi>(connection.Uri);
|
IPlexServerApi service = XmlServiceFor(connection.Uri);
|
||||||
return await service.GetChildren(season.Key.Split("/").Reverse().Skip(1).Head(), token.AuthToken)
|
return await service.GetSeasonChildren(season.Key.Split("/").Reverse().Skip(1).Head(), token.AuthToken)
|
||||||
.Map(r => r.MediaContainer.Metadata.Filter(m => m.Media.Count > 0 && m.Media[0].Part.Count > 0))
|
.Map(r => r.Metadata.Filter(m => m.Media.Count > 0 && m.Media[0].Part.Count > 0))
|
||||||
.Map(list => list.Map(metadata => ProjectToEpisode(metadata, library.MediaSourceId)).ToList());
|
.Map(list => list.Map(metadata => ProjectToEpisode(metadata, library.MediaSourceId)).ToList());
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -125,12 +126,13 @@ namespace ErsatzTV.Infrastructure.Plex
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
IPlexServerApi service = RestService.For<IPlexServerApi>(connection.Uri);
|
IPlexServerApi service = XmlServiceFor(connection.Uri);
|
||||||
return await service.GetMetadata(key, token.AuthToken)
|
return await service.GetVideoMetadata(key, token.AuthToken)
|
||||||
|
.Map(Optional)
|
||||||
.Map(
|
.Map(
|
||||||
r => r.MediaContainer.Metadata.Filter(m => m.Media.Count > 0 && m.Media[0].Part.Count > 0)
|
r => r.Filter(m => m.Metadata.Media.Count > 0 && m.Metadata.Media[0].Part.Count > 0)
|
||||||
.HeadOrNone())
|
.HeadOrNone())
|
||||||
.MapT(response => ProjectToMovieMetadata(response, library.MediaSourceId))
|
.MapT(response => ProjectToMovieMetadata(response.Metadata, library.MediaSourceId))
|
||||||
.Map(o => o.ToEither<BaseError>("Unable to locate metadata"));
|
.Map(o => o.ToEither<BaseError>("Unable to locate metadata"));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -147,10 +149,10 @@ namespace ErsatzTV.Infrastructure.Plex
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
IPlexServerApi service = RestService.For<IPlexServerApi>(connection.Uri);
|
IPlexServerApi service = XmlServiceFor(connection.Uri);
|
||||||
return await service.GetMetadata(key, token.AuthToken)
|
return await service.GetDirectoryMetadata(key, token.AuthToken)
|
||||||
.Map(r => r.MediaContainer.Metadata.HeadOrNone())
|
.Map(Optional)
|
||||||
.MapT(response => ProjectToShowMetadata(response, library.MediaSourceId))
|
.MapT(response => ProjectToShowMetadata(response.Metadata, library.MediaSourceId))
|
||||||
.Map(o => o.ToEither<BaseError>("Unable to locate metadata"));
|
.Map(o => o.ToEither<BaseError>("Unable to locate metadata"));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -159,20 +161,30 @@ namespace ErsatzTV.Infrastructure.Plex
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Either<BaseError, MediaVersion>> GetStatistics(
|
public async Task<Either<BaseError, Tuple<MovieMetadata, MediaVersion>>> GetMovieMetadataAndStatistics(
|
||||||
|
PlexLibrary library,
|
||||||
string key,
|
string key,
|
||||||
PlexConnection connection,
|
PlexConnection connection,
|
||||||
PlexServerAuthToken token)
|
PlexServerAuthToken token)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
IPlexServerApi service = RestService.For<IPlexServerApi>(connection.Uri);
|
IPlexServerApi service = XmlServiceFor(connection.Uri);
|
||||||
return await service.GetMetadata(key, token.AuthToken)
|
Option<PlexXmlVideoMetadataResponseContainer> maybeResponse = await service
|
||||||
|
.GetVideoMetadata(key, token.AuthToken)
|
||||||
|
.Map(Optional)
|
||||||
.Map(
|
.Map(
|
||||||
r => r.MediaContainer.Metadata.Filter(m => m.Media.Count > 0 && m.Media[0].Part.Count > 0)
|
r => r.Filter(m => m.Metadata.Media.Count > 0 && m.Metadata.Media[0].Part.Count > 0)
|
||||||
.HeadOrNone())
|
.HeadOrNone());
|
||||||
.BindT(ProjectToMediaVersion)
|
return maybeResponse.Match(
|
||||||
.Map(o => o.ToEither<BaseError>("Unable to locate metadata"));
|
response =>
|
||||||
|
{
|
||||||
|
Option<MediaVersion> maybeVersion = ProjectToMediaVersion(response.Metadata);
|
||||||
|
return maybeVersion.Match<Either<BaseError, Tuple<MovieMetadata, MediaVersion>>>(
|
||||||
|
version => Tuple(ProjectToMovieMetadata(response.Metadata, library.MediaSourceId), version),
|
||||||
|
() => BaseError.New("Unable to locate metadata"));
|
||||||
|
},
|
||||||
|
() => BaseError.New("Unable to locate metadata"));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -180,6 +192,57 @@ namespace ErsatzTV.Infrastructure.Plex
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Either<BaseError, Tuple<EpisodeMetadata, MediaVersion>>> GetEpisodeMetadataAndStatistics(
|
||||||
|
PlexLibrary library,
|
||||||
|
string key,
|
||||||
|
PlexConnection connection,
|
||||||
|
PlexServerAuthToken token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IPlexServerApi service = XmlServiceFor(connection.Uri);
|
||||||
|
Option<PlexXmlVideoMetadataResponseContainer> maybeResponse = await service
|
||||||
|
.GetVideoMetadata(key, token.AuthToken)
|
||||||
|
.Map(Optional)
|
||||||
|
.Map(
|
||||||
|
r => r.Filter(m => m.Metadata.Media.Count > 0 && m.Metadata.Media[0].Part.Count > 0)
|
||||||
|
.HeadOrNone());
|
||||||
|
return maybeResponse.Match(
|
||||||
|
response =>
|
||||||
|
{
|
||||||
|
Option<MediaVersion> maybeVersion = ProjectToMediaVersion(response.Metadata);
|
||||||
|
return maybeVersion.Match<Either<BaseError, Tuple<EpisodeMetadata, MediaVersion>>>(
|
||||||
|
version => Tuple(
|
||||||
|
ProjectToEpisodeMetadata(response.Metadata, library.MediaSourceId),
|
||||||
|
version),
|
||||||
|
() => BaseError.New("Unable to locate metadata"));
|
||||||
|
},
|
||||||
|
() => BaseError.New("Unable to locate metadata"));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BaseError.New(ex.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IPlexServerApi XmlServiceFor(string uri)
|
||||||
|
{
|
||||||
|
var overrides = new XmlAttributeOverrides();
|
||||||
|
var attrs = new XmlAttributes { XmlIgnore = true };
|
||||||
|
overrides.Add(typeof(PlexMetadataResponse), "Media", attrs);
|
||||||
|
|
||||||
|
return RestService.For<IPlexServerApi>(
|
||||||
|
uri,
|
||||||
|
new RefitSettings
|
||||||
|
{
|
||||||
|
ContentSerializer = new XmlContentSerializer(
|
||||||
|
new XmlContentSerializerSettings
|
||||||
|
{
|
||||||
|
XmlAttributeOverrides = overrides
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static Option<PlexLibrary> Project(PlexLibraryResponse response) =>
|
private static Option<PlexLibrary> Project(PlexLibraryResponse response) =>
|
||||||
response.Type switch
|
response.Type switch
|
||||||
{
|
{
|
||||||
@@ -205,7 +268,7 @@ namespace ErsatzTV.Infrastructure.Plex
|
|||||||
|
|
||||||
private PlexMovie ProjectToMovie(PlexMetadataResponse response, int mediaSourceId)
|
private PlexMovie ProjectToMovie(PlexMetadataResponse response, int mediaSourceId)
|
||||||
{
|
{
|
||||||
PlexMediaResponse media = response.Media.Head();
|
PlexMediaResponse<PlexPartResponse> media = response.Media.Head();
|
||||||
PlexPartResponse part = media.Part.Head();
|
PlexPartResponse part = media.Part.Head();
|
||||||
DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime;
|
DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime;
|
||||||
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime;
|
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime;
|
||||||
@@ -268,6 +331,23 @@ namespace ErsatzTV.Infrastructure.Plex
|
|||||||
Writers = Optional(response.Writer).Flatten().Map(w => new Writer { Name = w.Tag }).ToList()
|
Writers = Optional(response.Writer).Flatten().Map(w => new Writer { Name = w.Tag }).ToList()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (response is PlexXmlMetadataResponse xml)
|
||||||
|
{
|
||||||
|
metadata.Guids = Optional(xml.Guid).Flatten().Map(g => new MetadataGuid { Guid = g.Id }).ToList();
|
||||||
|
if (!string.IsNullOrWhiteSpace(xml.PlexGuid))
|
||||||
|
{
|
||||||
|
string normalized = NormalizeGuid(xml.PlexGuid);
|
||||||
|
if (!string.IsNullOrWhiteSpace(normalized) && metadata.Guids.All(g => g.Guid != normalized))
|
||||||
|
{
|
||||||
|
metadata.Guids.Add(new MetadataGuid { Guid = normalized });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
metadata.Guids = new List<MetadataGuid>();
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(response.Studio))
|
if (!string.IsNullOrWhiteSpace(response.Studio))
|
||||||
{
|
{
|
||||||
metadata.Studios.Add(new Studio { Name = response.Studio });
|
metadata.Studios.Add(new Studio { Name = response.Studio });
|
||||||
@@ -311,7 +391,7 @@ namespace ErsatzTV.Infrastructure.Plex
|
|||||||
return metadata;
|
return metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Option<MediaVersion> ProjectToMediaVersion(PlexMetadataResponse response)
|
private Option<MediaVersion> ProjectToMediaVersion(PlexXmlMetadataResponse response)
|
||||||
{
|
{
|
||||||
List<PlexStreamResponse> streams = response.Media.Head().Part.Head().Stream;
|
List<PlexStreamResponse> streams = response.Media.Head().Part.Head().Stream;
|
||||||
DateTime dateUpdated = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime;
|
DateTime dateUpdated = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime;
|
||||||
@@ -418,6 +498,23 @@ namespace ErsatzTV.Infrastructure.Plex
|
|||||||
.ToList()
|
.ToList()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (response is PlexXmlMetadataResponse xml)
|
||||||
|
{
|
||||||
|
metadata.Guids = Optional(xml.Guid).Flatten().Map(g => new MetadataGuid { Guid = g.Id }).ToList();
|
||||||
|
if (!string.IsNullOrWhiteSpace(xml.PlexGuid))
|
||||||
|
{
|
||||||
|
string normalized = NormalizeGuid(xml.PlexGuid);
|
||||||
|
if (!string.IsNullOrWhiteSpace(normalized) && metadata.Guids.All(g => g.Guid != normalized))
|
||||||
|
{
|
||||||
|
metadata.Guids.Add(new MetadataGuid { Guid = normalized });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
metadata.Guids = new List<MetadataGuid>();
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(response.Studio))
|
if (!string.IsNullOrWhiteSpace(response.Studio))
|
||||||
{
|
{
|
||||||
metadata.Studios.Add(new Studio { Name = response.Studio });
|
metadata.Studios.Add(new Studio { Name = response.Studio });
|
||||||
@@ -461,7 +558,7 @@ namespace ErsatzTV.Infrastructure.Plex
|
|||||||
return metadata;
|
return metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlexSeason ProjectToSeason(PlexMetadataResponse response, int mediaSourceId)
|
private PlexSeason ProjectToSeason(PlexXmlMetadataResponse response, int mediaSourceId)
|
||||||
{
|
{
|
||||||
DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime;
|
DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime;
|
||||||
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime;
|
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime;
|
||||||
@@ -476,6 +573,16 @@ namespace ErsatzTV.Infrastructure.Plex
|
|||||||
DateUpdated = lastWriteTime
|
DateUpdated = lastWriteTime
|
||||||
};
|
};
|
||||||
|
|
||||||
|
metadata.Guids = Optional(response.Guid).Flatten().Map(g => new MetadataGuid { Guid = g.Id }).ToList();
|
||||||
|
if (!string.IsNullOrWhiteSpace(response.PlexGuid))
|
||||||
|
{
|
||||||
|
string normalized = NormalizeGuid(response.PlexGuid);
|
||||||
|
if (!string.IsNullOrWhiteSpace(normalized) && metadata.Guids.All(g => g.Guid != normalized))
|
||||||
|
{
|
||||||
|
metadata.Guids.Add(new MetadataGuid { Guid = normalized });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(response.Thumb))
|
if (!string.IsNullOrWhiteSpace(response.Thumb))
|
||||||
{
|
{
|
||||||
var path = $"plex/{mediaSourceId}{response.Thumb}";
|
var path = $"plex/{mediaSourceId}{response.Thumb}";
|
||||||
@@ -516,47 +623,14 @@ namespace ErsatzTV.Infrastructure.Plex
|
|||||||
return season;
|
return season;
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlexEpisode ProjectToEpisode(PlexMetadataResponse response, int mediaSourceId)
|
private PlexEpisode ProjectToEpisode(PlexXmlMetadataResponse response, int mediaSourceId)
|
||||||
{
|
{
|
||||||
PlexMediaResponse media = response.Media.Head();
|
PlexMediaResponse<PlexXmlPartResponse> media = response.Media.Head();
|
||||||
PlexPartResponse part = media.Part.Head();
|
PlexXmlPartResponse part = media.Part.Head();
|
||||||
DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime;
|
DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime;
|
||||||
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime;
|
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime;
|
||||||
|
|
||||||
var metadata = new EpisodeMetadata
|
EpisodeMetadata metadata = ProjectToEpisodeMetadata(response, mediaSourceId);
|
||||||
{
|
|
||||||
MetadataKind = MetadataKind.External,
|
|
||||||
Title = response.Title,
|
|
||||||
SortTitle = _fallbackMetadataProvider.GetSortTitle(response.Title),
|
|
||||||
Plot = response.Summary,
|
|
||||||
Year = response.Year,
|
|
||||||
Tagline = response.Tagline,
|
|
||||||
DateAdded = dateAdded,
|
|
||||||
DateUpdated = lastWriteTime,
|
|
||||||
Actors = Optional(response.Role).Flatten().Map(r => ProjectToModel(r, dateAdded, lastWriteTime))
|
|
||||||
.ToList()
|
|
||||||
};
|
|
||||||
|
|
||||||
if (DateTime.TryParse(response.OriginallyAvailableAt, out DateTime releaseDate))
|
|
||||||
{
|
|
||||||
metadata.ReleaseDate = releaseDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(response.Thumb))
|
|
||||||
{
|
|
||||||
var path = $"plex/{mediaSourceId}{response.Thumb}";
|
|
||||||
var artwork = new Artwork
|
|
||||||
{
|
|
||||||
ArtworkKind = ArtworkKind.Thumbnail,
|
|
||||||
Path = path,
|
|
||||||
DateAdded = dateAdded,
|
|
||||||
DateUpdated = lastWriteTime
|
|
||||||
};
|
|
||||||
|
|
||||||
metadata.Artwork ??= new List<Artwork>();
|
|
||||||
metadata.Artwork.Add(artwork);
|
|
||||||
}
|
|
||||||
|
|
||||||
var version = new MediaVersion
|
var version = new MediaVersion
|
||||||
{
|
{
|
||||||
Name = "Main",
|
Name = "Main",
|
||||||
@@ -589,6 +663,67 @@ namespace ErsatzTV.Infrastructure.Plex
|
|||||||
return episode;
|
return episode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private EpisodeMetadata ProjectToEpisodeMetadata(PlexMetadataResponse response, int mediaSourceId)
|
||||||
|
{
|
||||||
|
DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime;
|
||||||
|
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime;
|
||||||
|
|
||||||
|
var metadata = new EpisodeMetadata
|
||||||
|
{
|
||||||
|
MetadataKind = MetadataKind.External,
|
||||||
|
Title = response.Title,
|
||||||
|
SortTitle = _fallbackMetadataProvider.GetSortTitle(response.Title),
|
||||||
|
Plot = response.Summary,
|
||||||
|
Year = response.Year,
|
||||||
|
Tagline = response.Tagline,
|
||||||
|
DateAdded = dateAdded,
|
||||||
|
DateUpdated = lastWriteTime,
|
||||||
|
Actors = Optional(response.Role).Flatten().Map(r => ProjectToModel(r, dateAdded, lastWriteTime))
|
||||||
|
.ToList(),
|
||||||
|
Directors = Optional(response.Director).Flatten().Map(d => new Director { Name = d.Tag }).ToList(),
|
||||||
|
Writers = Optional(response.Writer).Flatten().Map(w => new Writer { Name = w.Tag }).ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (response is PlexXmlMetadataResponse xml)
|
||||||
|
{
|
||||||
|
metadata.Guids = Optional(xml.Guid).Flatten().Map(g => new MetadataGuid { Guid = g.Id }).ToList();
|
||||||
|
if (!string.IsNullOrWhiteSpace(xml.PlexGuid))
|
||||||
|
{
|
||||||
|
string normalized = NormalizeGuid(xml.PlexGuid);
|
||||||
|
if (!string.IsNullOrWhiteSpace(normalized) && metadata.Guids.All(g => g.Guid != normalized))
|
||||||
|
{
|
||||||
|
metadata.Guids.Add(new MetadataGuid { Guid = normalized });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
metadata.Guids = new List<MetadataGuid>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DateTime.TryParse(response.OriginallyAvailableAt, out DateTime releaseDate))
|
||||||
|
{
|
||||||
|
metadata.ReleaseDate = releaseDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(response.Thumb))
|
||||||
|
{
|
||||||
|
var path = $"plex/{mediaSourceId}{response.Thumb}";
|
||||||
|
var artwork = new Artwork
|
||||||
|
{
|
||||||
|
ArtworkKind = ArtworkKind.Thumbnail,
|
||||||
|
Path = path,
|
||||||
|
DateAdded = dateAdded,
|
||||||
|
DateUpdated = lastWriteTime
|
||||||
|
};
|
||||||
|
|
||||||
|
metadata.Artwork ??= new List<Artwork>();
|
||||||
|
metadata.Artwork.Add(artwork);
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
private Actor ProjectToModel(PlexRoleResponse role, DateTime dateAdded, DateTime lastWriteTime)
|
private Actor ProjectToModel(PlexRoleResponse role, DateTime dateAdded, DateTime lastWriteTime)
|
||||||
{
|
{
|
||||||
var actor = new Actor { Name = role.Tag, Role = role.Role };
|
var actor = new Actor { Name = role.Tag, Role = role.Role };
|
||||||
@@ -605,5 +740,32 @@ namespace ErsatzTV.Infrastructure.Plex
|
|||||||
|
|
||||||
return actor;
|
return actor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string NormalizeGuid(string guid)
|
||||||
|
{
|
||||||
|
if (guid.StartsWith("plex://show") ||
|
||||||
|
guid.StartsWith("plex://season") ||
|
||||||
|
guid.StartsWith("plex://episode") ||
|
||||||
|
guid.StartsWith("plex://movie"))
|
||||||
|
{
|
||||||
|
return guid;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (guid.StartsWith("com.plexapp.agents.thetvdb"))
|
||||||
|
{
|
||||||
|
string strip1 = guid.Replace("com.plexapp.agents.thetvdb://", string.Empty);
|
||||||
|
string strip2 = strip1.Split("?").Head();
|
||||||
|
return $"tvdb://{strip2}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (guid.StartsWith("com.plexapp.agents.themoviedb"))
|
||||||
|
{
|
||||||
|
string strip1 = guid.Replace("com.plexapp.agents.themoviedb://", string.Empty);
|
||||||
|
string strip2 = strip1.Split("?").Head();
|
||||||
|
return $"tmdb://{strip2}";
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new NotSupportedException(guid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ namespace ErsatzTV.Infrastructure.Search
|
|||||||
private const string ShowType = "show";
|
private const string ShowType = "show";
|
||||||
private const string ArtistType = "artist";
|
private const string ArtistType = "artist";
|
||||||
private const string MusicVideoType = "music_video";
|
private const string MusicVideoType = "music_video";
|
||||||
|
private const string EpisodeType = "episode";
|
||||||
private readonly List<CultureInfo> _cultureInfos;
|
private readonly List<CultureInfo> _cultureInfos;
|
||||||
|
|
||||||
private readonly ILogger<SearchIndex> _logger;
|
private readonly ILogger<SearchIndex> _logger;
|
||||||
@@ -71,7 +72,7 @@ namespace ErsatzTV.Infrastructure.Search
|
|||||||
_initialized = false;
|
_initialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int Version => 12;
|
public int Version => 13;
|
||||||
|
|
||||||
public Task<bool> Initialize(ILocalFileSystem localFileSystem)
|
public Task<bool> Initialize(ILocalFileSystem localFileSystem)
|
||||||
{
|
{
|
||||||
@@ -114,6 +115,9 @@ namespace ErsatzTV.Infrastructure.Search
|
|||||||
case MusicVideo musicVideo:
|
case MusicVideo musicVideo:
|
||||||
UpdateMusicVideo(musicVideo);
|
UpdateMusicVideo(musicVideo);
|
||||||
break;
|
break;
|
||||||
|
case Episode episode:
|
||||||
|
UpdateEpisode(episode);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,6 +147,9 @@ namespace ErsatzTV.Infrastructure.Search
|
|||||||
case MusicVideo musicVideo:
|
case MusicVideo musicVideo:
|
||||||
UpdateMusicVideo(musicVideo);
|
UpdateMusicVideo(musicVideo);
|
||||||
break;
|
break;
|
||||||
|
case Episode episode:
|
||||||
|
UpdateEpisode(episode);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -577,6 +584,83 @@ namespace ErsatzTV.Infrastructure.Search
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateEpisode(Episode episode)
|
||||||
|
{
|
||||||
|
Option<EpisodeMetadata> maybeMetadata = episode.EpisodeMetadata.HeadOrNone();
|
||||||
|
if (maybeMetadata.IsSome)
|
||||||
|
{
|
||||||
|
EpisodeMetadata metadata = maybeMetadata.ValueUnsafe();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var doc = new Document
|
||||||
|
{
|
||||||
|
new StringField(IdField, episode.Id.ToString(), Field.Store.YES),
|
||||||
|
new StringField(TypeField, EpisodeType, Field.Store.NO),
|
||||||
|
new TextField(TitleField, metadata.Title, Field.Store.NO),
|
||||||
|
new StringField(SortTitleField, metadata.SortTitle.ToLowerInvariant(), Field.Store.NO),
|
||||||
|
new TextField(LibraryNameField, episode.LibraryPath.Library.Name, Field.Store.NO),
|
||||||
|
new StringField(LibraryIdField, episode.LibraryPath.Library.Id.ToString(), Field.Store.NO),
|
||||||
|
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO),
|
||||||
|
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES)
|
||||||
|
};
|
||||||
|
|
||||||
|
AddLanguages(doc, episode.MediaVersions);
|
||||||
|
|
||||||
|
if (metadata.ReleaseDate.HasValue)
|
||||||
|
{
|
||||||
|
doc.Add(
|
||||||
|
new StringField(
|
||||||
|
ReleaseDateField,
|
||||||
|
metadata.ReleaseDate.Value.ToString("yyyyMMdd"),
|
||||||
|
Field.Store.NO));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(metadata.Plot))
|
||||||
|
{
|
||||||
|
doc.Add(new TextField(PlotField, metadata.Plot ?? string.Empty, Field.Store.NO));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Genre genre in metadata.Genres)
|
||||||
|
{
|
||||||
|
doc.Add(new TextField(GenreField, genre.Name, Field.Store.NO));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Tag tag in metadata.Tags)
|
||||||
|
{
|
||||||
|
doc.Add(new TextField(TagField, tag.Name, Field.Store.NO));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Studio studio in metadata.Studios)
|
||||||
|
{
|
||||||
|
doc.Add(new TextField(StudioField, studio.Name, Field.Store.NO));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Actor actor in metadata.Actors)
|
||||||
|
{
|
||||||
|
doc.Add(new TextField(ActorField, actor.Name, Field.Store.NO));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Director director in metadata.Directors)
|
||||||
|
{
|
||||||
|
doc.Add(new TextField(DirectorField, director.Name, Field.Store.NO));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Writer writer in metadata.Writers)
|
||||||
|
{
|
||||||
|
doc.Add(new TextField(WriterField, writer.Name, Field.Store.NO));
|
||||||
|
}
|
||||||
|
|
||||||
|
_writer.UpdateDocument(new Term(IdField, episode.Id.ToString()), doc);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
metadata.Episode = null;
|
||||||
|
_logger.LogWarning(ex, "Error indexing episode with metadata {@Metadata}", metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private SearchItem ProjectToSearchItem(Document doc) => new(Convert.ToInt32(doc.Get(IdField)));
|
private SearchItem ProjectToSearchItem(Document doc) => new(Convert.ToInt32(doc.Get(IdField)));
|
||||||
|
|
||||||
private Query ParseQuery(string searchQuery, QueryParser parser)
|
private Query ParseQuery(string searchQuery, QueryParser parser)
|
||||||
@@ -595,7 +679,13 @@ namespace ErsatzTV.Infrastructure.Search
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static string GetTitleAndYear(Metadata metadata) =>
|
private static string GetTitleAndYear(Metadata metadata) =>
|
||||||
$"{metadata.Title}_{metadata.Year}".ToLowerInvariant();
|
metadata switch
|
||||||
|
{
|
||||||
|
EpisodeMetadata em =>
|
||||||
|
$"{em.Title}_{em.Year}_{em.Episode.Season.SeasonNumber}_{em.Episode.EpisodeNumber}"
|
||||||
|
.ToLowerInvariant(),
|
||||||
|
_ => $"{metadata.Title}_{metadata.Year}".ToLowerInvariant()
|
||||||
|
};
|
||||||
|
|
||||||
private static string GetJumpLetter(Metadata metadata)
|
private static string GetJumpLetter(Metadata metadata)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
@page "/media/tv/episodes"
|
||||||
|
@page "/media/tv/episodes/page/{PageNumber:int}"
|
||||||
|
@using LanguageExt.UnsafeValueAccess
|
||||||
|
@using Microsoft.AspNetCore.WebUtilities
|
||||||
|
@using Microsoft.Extensions.Primitives
|
||||||
|
@using ErsatzTV.Application.MediaCards
|
||||||
|
@using ErsatzTV.Application.MediaCollections
|
||||||
|
@using ErsatzTV.Application.MediaCollections.Commands
|
||||||
|
@using ErsatzTV.Application.Search.Queries
|
||||||
|
@using Unit = LanguageExt.Unit
|
||||||
|
@inherits MultiSelectBase<EpisodeList>
|
||||||
|
@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="TelevisionEpisodeCardViewModel" Cards="@_data.Cards">
|
||||||
|
<MediaCard Data="@context"
|
||||||
|
Link="@($"/media/tv/seasons/{context.SeasonId}#episode-{context.EpisodeId}")"
|
||||||
|
Subtitle="@($"{context.ShowTitle} - S{context.Season} E{context.Episode}")"
|
||||||
|
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/tv/episodes"
|
||||||
|
Query="@_query"/>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private static int PageSize => 100;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int PageNumber { get; set; }
|
||||||
|
|
||||||
|
private TelevisionEpisodeCardResultsViewModel _data;
|
||||||
|
private string _query;
|
||||||
|
|
||||||
|
protected override Task OnParametersSetAsync()
|
||||||
|
{
|
||||||
|
if (PageNumber == 0)
|
||||||
|
{
|
||||||
|
PageNumber = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
string query = new Uri(_navigationManager.Uri).Query;
|
||||||
|
if (QueryHelpers.ParseQuery(query).TryGetValue("query", out StringValues value))
|
||||||
|
{
|
||||||
|
_query = value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_query = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RefreshData();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task RefreshData()
|
||||||
|
{
|
||||||
|
string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:episode" : $"type:episode AND ({_query})";
|
||||||
|
_data = await Mediator.Send(new QuerySearchIndexEpisodes(searchQuery, PageNumber, PageSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PrevPage()
|
||||||
|
{
|
||||||
|
var uri = $"/media/tv/episodes/page/{PageNumber - 1}";
|
||||||
|
if (!string.IsNullOrWhiteSpace(_query))
|
||||||
|
{
|
||||||
|
uri = QueryHelpers.AddQueryString(uri, "query", _query);
|
||||||
|
}
|
||||||
|
_navigationManager.NavigateTo(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NextPage()
|
||||||
|
{
|
||||||
|
var uri = $"/media/tv/episodes/page/{PageNumber + 1}";
|
||||||
|
if (!string.IsNullOrWhiteSpace(_query))
|
||||||
|
{
|
||||||
|
uri = QueryHelpers.AddQueryString(uri, "query", _query);
|
||||||
|
}
|
||||||
|
_navigationManager.NavigateTo(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SelectClicked(MediaCardViewModel card, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
List<MediaCardViewModel> GetSortedItems()
|
||||||
|
{
|
||||||
|
return _data.Cards.OrderBy(m => m.SortTitle).ToList<MediaCardViewModel>();
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectClicked(GetSortedItems, card, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddToCollection(MediaCardViewModel card)
|
||||||
|
{
|
||||||
|
if (card is TelevisionEpisodeCardViewModel episode)
|
||||||
|
{
|
||||||
|
var parameters = new DialogParameters { { "EntityType", "episode" }, { "EntityName", episode.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 AddEpisodeToCollection(collection.Id, episode.EpisodeId);
|
||||||
|
Either<BaseError, Unit> addResult = await Mediator.Send(request);
|
||||||
|
addResult.Match(
|
||||||
|
Left: error =>
|
||||||
|
{
|
||||||
|
Snackbar.Add($"Unexpected error adding episode to collection: {error.Value}");
|
||||||
|
Logger.LogError("Unexpected error adding episode to collection: {Error}", error.Value);
|
||||||
|
},
|
||||||
|
Right: _ => Snackbar.Add($"Added {episode.Title} to collection {collection.Name}", Severity.Success));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -97,17 +97,19 @@ namespace ErsatzTV.Pages
|
|||||||
protected Task AddSelectionToCollection() => AddItemsToCollection(
|
protected Task AddSelectionToCollection() => AddItemsToCollection(
|
||||||
_selectedItems.OfType<MovieCardViewModel>().Map(m => m.MovieId).ToList(),
|
_selectedItems.OfType<MovieCardViewModel>().Map(m => m.MovieId).ToList(),
|
||||||
_selectedItems.OfType<TelevisionShowCardViewModel>().Map(s => s.TelevisionShowId).ToList(),
|
_selectedItems.OfType<TelevisionShowCardViewModel>().Map(s => s.TelevisionShowId).ToList(),
|
||||||
|
_selectedItems.OfType<TelevisionEpisodeCardViewModel>().Map(e => e.EpisodeId).ToList(),
|
||||||
_selectedItems.OfType<ArtistCardViewModel>().Map(a => a.ArtistId).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());
|
||||||
|
|
||||||
protected async Task AddItemsToCollection(
|
protected async Task AddItemsToCollection(
|
||||||
List<int> movieIds,
|
List<int> movieIds,
|
||||||
List<int> showIds,
|
List<int> showIds,
|
||||||
|
List<int> episodeIds,
|
||||||
List<int> artistIds,
|
List<int> artistIds,
|
||||||
List<int> musicVideoIds,
|
List<int> musicVideoIds,
|
||||||
string entityName = "selected items")
|
string entityName = "selected items")
|
||||||
{
|
{
|
||||||
int count = movieIds.Count + showIds.Count + artistIds.Count + musicVideoIds.Count;
|
int count = movieIds.Count + showIds.Count + episodeIds.Count + artistIds.Count + musicVideoIds.Count;
|
||||||
|
|
||||||
var parameters = new DialogParameters
|
var parameters = new DialogParameters
|
||||||
{ { "EntityType", count.ToString() }, { "EntityName", entityName } };
|
{ { "EntityType", count.ToString() }, { "EntityName", entityName } };
|
||||||
@@ -117,7 +119,13 @@ namespace ErsatzTV.Pages
|
|||||||
DialogResult result = await dialog.Result;
|
DialogResult result = await dialog.Result;
|
||||||
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
|
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
|
||||||
{
|
{
|
||||||
var request = new AddItemsToCollection(collection.Id, movieIds, showIds, artistIds, musicVideoIds);
|
var request = new AddItemsToCollection(
|
||||||
|
collection.Id,
|
||||||
|
movieIds,
|
||||||
|
showIds,
|
||||||
|
episodeIds,
|
||||||
|
artistIds,
|
||||||
|
musicVideoIds);
|
||||||
|
|
||||||
Either<BaseError, Unit> addResult = await Mediator.Send(request);
|
Either<BaseError, Unit> addResult = await Mediator.Send(request);
|
||||||
addResult.Match(
|
addResult.Match(
|
||||||
|
|||||||
@@ -45,6 +45,11 @@
|
|||||||
<MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#shows")" Style="margin-bottom: auto; margin-top: auto">@_shows.Count Shows</MudLink>
|
<MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#shows")" Style="margin-bottom: auto; margin-top: auto">@_shows.Count Shows</MudLink>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_episodes.Count > 0)
|
||||||
|
{
|
||||||
|
<MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#episodes")" Style="margin-bottom: auto; margin-top: auto">@_episodes.Count Episodes</MudLink>
|
||||||
|
}
|
||||||
|
|
||||||
if (_artists.Count > 0)
|
if (_artists.Count > 0)
|
||||||
{
|
{
|
||||||
<MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#artists")" Style="margin-bottom: auto; margin-top: auto">@_artists.Count Artists</MudLink>
|
<MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#artists")" Style="margin-bottom: auto; margin-top: auto">@_artists.Count Artists</MudLink>
|
||||||
@@ -120,6 +125,34 @@
|
|||||||
</MudContainer>
|
</MudContainer>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (_episodes.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", "episodes" } })">
|
||||||
|
Episodes
|
||||||
|
</MudText>
|
||||||
|
@if (_episodes.Count > 50)
|
||||||
|
{
|
||||||
|
<MudLink Href="@GetEpisodesLink()" Class="ml-4">See All >></MudLink>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
|
||||||
|
@foreach (TelevisionEpisodeCardViewModel card in _episodes.Cards.OrderBy(s => s.SortTitle))
|
||||||
|
{
|
||||||
|
<MediaCard Data="@card"
|
||||||
|
AddToCollectionClicked="@AddToCollection"
|
||||||
|
Link="@($"/media/tv/seasons/{card.SeasonId}#episode-{card.EpisodeId}")"
|
||||||
|
Subtitle="@($"{card.ShowTitle} - S{card.Season} E{card.Episode}")"
|
||||||
|
SelectClicked="@(e => SelectClicked(card, e))"
|
||||||
|
IsSelected="@IsSelected(card)"
|
||||||
|
IsSelectMode="@IsSelectMode()"/>
|
||||||
|
}
|
||||||
|
</MudContainer>
|
||||||
|
}
|
||||||
|
|
||||||
@if (_artists.Count > 0)
|
@if (_artists.Count > 0)
|
||||||
{
|
{
|
||||||
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
|
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
|
||||||
@@ -181,6 +214,7 @@
|
|||||||
private string _query;
|
private string _query;
|
||||||
private MovieCardResultsViewModel _movies;
|
private MovieCardResultsViewModel _movies;
|
||||||
private TelevisionShowCardResultsViewModel _shows;
|
private TelevisionShowCardResultsViewModel _shows;
|
||||||
|
private TelevisionEpisodeCardResultsViewModel _episodes;
|
||||||
private MusicVideoCardResultsViewModel _musicVideos;
|
private MusicVideoCardResultsViewModel _musicVideos;
|
||||||
private ArtistCardResultsViewModel _artists;
|
private ArtistCardResultsViewModel _artists;
|
||||||
|
|
||||||
@@ -194,6 +228,7 @@
|
|||||||
|
|
||||||
_movies = await Mediator.Send(new QuerySearchIndexMovies($"type:movie AND ({_query})", 1, 50));
|
_movies = await Mediator.Send(new QuerySearchIndexMovies($"type:movie AND ({_query})", 1, 50));
|
||||||
_shows = await Mediator.Send(new QuerySearchIndexShows($"type:show AND ({_query})", 1, 50));
|
_shows = await Mediator.Send(new QuerySearchIndexShows($"type:show 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));
|
_musicVideos = await Mediator.Send(new QuerySearchIndexMusicVideos($"type:music_video AND ({_query})", 1, 50));
|
||||||
_artists = await Mediator.Send(new QuerySearchIndexArtists($"type:artist AND ({_query})", 1, 50));
|
_artists = await Mediator.Send(new QuerySearchIndexArtists($"type:artist AND ({_query})", 1, 50));
|
||||||
}
|
}
|
||||||
@@ -205,6 +240,7 @@
|
|||||||
{
|
{
|
||||||
return _movies.Cards.OrderBy(m => m.SortTitle)
|
return _movies.Cards.OrderBy(m => m.SortTitle)
|
||||||
.Append<MediaCardViewModel>(_shows.Cards.OrderBy(s => s.SortTitle))
|
.Append<MediaCardViewModel>(_shows.Cards.OrderBy(s => s.SortTitle))
|
||||||
|
.Append(_episodes.Cards.OrderBy(ep => ep.SortTitle))
|
||||||
.Append(_artists.Cards.OrderBy(a => a.SortTitle))
|
.Append(_artists.Cards.OrderBy(a => a.SortTitle))
|
||||||
.Append(_musicVideos.Cards.OrderBy(mv => mv.SortTitle))
|
.Append(_musicVideos.Cards.OrderBy(mv => mv.SortTitle))
|
||||||
.ToList();
|
.ToList();
|
||||||
@@ -257,6 +293,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (card is TelevisionEpisodeCardViewModel episode)
|
||||||
|
{
|
||||||
|
var parameters = new DialogParameters { { "EntityType", "episode" }, { "EntityName", episode.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 AddEpisodeToCollection(collection.Id, episode.EpisodeId);
|
||||||
|
Either<BaseError, Unit> addResult = await Mediator.Send(request);
|
||||||
|
addResult.Match(
|
||||||
|
Left: error =>
|
||||||
|
{
|
||||||
|
Snackbar.Add($"Unexpected error adding episode to collection: {error.Value}");
|
||||||
|
Logger.LogError("Unexpected error adding episode to collection: {Error}", error.Value);
|
||||||
|
},
|
||||||
|
Right: _ => Snackbar.Add($"Added {episode.Title} to collection {collection.Name}", Severity.Success));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (card is ArtistCardViewModel artist)
|
if (card is ArtistCardViewModel artist)
|
||||||
{
|
{
|
||||||
var parameters = new DialogParameters { { "EntityType", "artist" }, { "EntityName", artist.Title } };
|
var parameters = new DialogParameters { { "EntityType", "artist" }, { "EntityName", artist.Title } };
|
||||||
@@ -320,6 +377,16 @@
|
|||||||
return uri;
|
return uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GetEpisodesLink()
|
||||||
|
{
|
||||||
|
var uri = "/media/tv/episodes/page/1";
|
||||||
|
if (!string.IsNullOrWhiteSpace(_query))
|
||||||
|
{
|
||||||
|
uri = QueryHelpers.AddQueryString(uri, "query", _query);
|
||||||
|
}
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
|
||||||
private string GetArtistsLink()
|
private string GetArtistsLink()
|
||||||
{
|
{
|
||||||
var uri = "/media/music/artists/page/1";
|
var uri = "/media/music/artists/page/1";
|
||||||
@@ -343,7 +410,7 @@
|
|||||||
private async Task AddAllToCollection(MouseEventArgs _)
|
private async Task AddAllToCollection(MouseEventArgs _)
|
||||||
{
|
{
|
||||||
SearchResultAllItemsViewModel results = await Mediator.Send(new QuerySearchIndexAllItems(_query));
|
SearchResultAllItemsViewModel results = await Mediator.Send(new QuerySearchIndexAllItems(_query));
|
||||||
await AddItemsToCollection(results.MovieIds, results.ShowIds, results.ArtistIds, results.MusicVideoIds, "search results");
|
await AddItemsToCollection(results.MovieIds, results.ShowIds, results.EpisodeIds, results.ArtistIds, results.MusicVideoIds, "search results");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -106,6 +106,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</MudCardContent>
|
</MudCardContent>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="pl-3 pt-3">
|
||||||
|
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
|
||||||
|
<MudText GutterBottom="true">Released: @episode.Aired.ToShortDateString()</MudText>
|
||||||
|
</div>
|
||||||
|
@if (episode.Directors.Any())
|
||||||
|
{
|
||||||
|
var sorted = episode.Directors.OrderBy(w => w).ToList();
|
||||||
|
|
||||||
|
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
|
||||||
|
<MudText GutterBottom="true">Directors: </MudText>
|
||||||
|
<MudLink Href="@($"/search?query=director%3a%22{Uri.EscapeDataString(sorted.Head())}%22")">@sorted.Head()</MudLink>
|
||||||
|
@foreach (string director in sorted.Skip(1))
|
||||||
|
{
|
||||||
|
<MudText>, </MudText>
|
||||||
|
<MudLink Href="@($"/search?query=director%3a%22{Uri.EscapeDataString(director)}%22")">@director</MudLink>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (episode.Writers.Any())
|
||||||
|
{
|
||||||
|
var sorted = episode.Writers.OrderBy(w => w).ToList();
|
||||||
|
|
||||||
|
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
|
||||||
|
<MudText GutterBottom="true">Writers: </MudText>
|
||||||
|
<MudLink Href="@($"/search?query=writer%3a%22{Uri.EscapeDataString(sorted.Head())}%22")">@sorted.Head()</MudLink>
|
||||||
|
@foreach (string writer in sorted.Skip(1))
|
||||||
|
{
|
||||||
|
<MudText>, </MudText>
|
||||||
|
<MudLink Href="@($"/search?query=writer%3a%22{Uri.EscapeDataString(writer)}%22")">@writer</MudLink>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</MudCard>
|
</MudCard>
|
||||||
}
|
}
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
|
|||||||
Reference in New Issue
Block a user