Browse Source

add seasons to search index (#385)

* update trakt list items when re-adding existing list

* add seasons to search index
pull/386/head
Jason Dove 5 years ago committed by GitHub
parent
commit
a864d53327
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      CHANGELOG.md
  2. 22
      ErsatzTV.Application/MediaCards/Mapper.cs
  3. 3
      ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCardsHandler.cs
  4. 7
      ErsatzTV.Application/MediaCards/TelevisionSeasonCardResultsViewModel.cs
  5. 1
      ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollection.cs
  6. 15
      ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs
  7. 21
      ErsatzTV.Application/MediaCollections/Commands/TraktCommandBase.cs
  8. 1
      ErsatzTV.Application/Search/Queries/QuerySearchIndexAllItemsHandler.cs
  9. 8
      ErsatzTV.Application/Search/Queries/QuerySearchIndexSeasons.cs
  10. 55
      ErsatzTV.Application/Search/Queries/QuerySearchIndexSeasonsHandler.cs
  11. 1
      ErsatzTV.Application/Search/SearchResultAllItemsViewModel.cs
  12. 6
      ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs
  13. 12
      ErsatzTV.Core/Emby/EmbyTelevisionLibraryScanner.cs
  14. 2
      ErsatzTV.Core/Interfaces/Repositories/IEmbyTelevisionRepository.cs
  15. 2
      ErsatzTV.Core/Interfaces/Repositories/IJellyfinTelevisionRepository.cs
  16. 1
      ErsatzTV.Core/Interfaces/Repositories/ISearchRepository.cs
  17. 2
      ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs
  18. 12
      ErsatzTV.Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs
  19. 3
      ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs
  20. 20
      ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs
  21. 3
      ErsatzTV.Infrastructure/Data/Configurations/Collection/TraktListItemConfiguration.cs
  22. 4
      ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs
  23. 9
      ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs
  24. 30
      ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs
  25. 19
      ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs
  26. 39
      ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs
  27. 3284
      ErsatzTV.Infrastructure/Migrations/20210925181536_Add_TraktListItemDeleteSetNull.Designer.cs
  28. 37
      ErsatzTV.Infrastructure/Migrations/20210925181536_Add_TraktListItemDeleteSetNull.cs
  29. 3
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  30. 76
      ErsatzTV.Infrastructure/Search/SearchIndex.cs
  31. 10
      ErsatzTV/Pages/MultiSelectBase.cs
  32. 55
      ErsatzTV/Pages/Search.razor
  33. 157
      ErsatzTV/Pages/TelevisionSeasonSearchResults.razor
  34. 1
      ErsatzTV/Pages/TelevisionShowList.razor

2
CHANGELOG.md

@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- To re-download a Trakt list, simply add it again (no need to delete)
- See `Logs` for unmatched item details
- Trakt lists can only be scheduled by using Smart Collections
- Add seasons to search index
- This is needed because Trakt lists can contain seasons
### Fixed
- Fix local television scanner to properly update episode metadata when NFO files have been added/changed

22
ErsatzTV.Application/MediaCards/Mapper.cs

@ -21,7 +21,7 @@ namespace ErsatzTV.Application.MediaCards @@ -21,7 +21,7 @@ namespace ErsatzTV.Application.MediaCards
showMetadata.Year?.ToString(),
showMetadata.SortTitle,
GetPoster(showMetadata, maybeJellyfin, maybeEmby));
internal static TelevisionSeasonCardViewModel ProjectToViewModel(
Season season,
Option<JellyfinMediaSource> maybeJellyfin,
@ -37,6 +37,26 @@ namespace ErsatzTV.Application.MediaCards @@ -37,6 +37,26 @@ namespace ErsatzTV.Application.MediaCards
.IfNone(string.Empty),
season.SeasonNumber == 0 ? "S" : season.SeasonNumber.ToString());
internal static TelevisionSeasonCardViewModel ProjectToViewModel(
SeasonMetadata seasonMetadata,
Option<JellyfinMediaSource> maybeJellyfin,
Option<EmbyMediaSource> maybeEmby)
{
string showTitle = seasonMetadata.Season.Show.ShowMetadata.HeadOrNone().Match(
m => m.Title ?? string.Empty,
() => string.Empty);
return new TelevisionSeasonCardViewModel(
showTitle,
seasonMetadata.SeasonId,
seasonMetadata.Season.SeasonNumber,
showTitle,
GetSeasonName(seasonMetadata.Season.SeasonNumber),
$"{showTitle}_{seasonMetadata.Season.SeasonNumber:0000}",
GetPoster(seasonMetadata, maybeJellyfin, maybeEmby),
seasonMetadata.Season.SeasonNumber == 0 ? "S" : seasonMetadata.Season.SeasonNumber.ToString());
}
internal static TelevisionEpisodeCardViewModel ProjectToViewModel(
EpisodeMetadata episodeMetadata,
Option<JellyfinMediaSource> maybeJellyfin,

3
ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCardsHandler.cs

@ -7,6 +7,7 @@ using ErsatzTV.Core.Interfaces.Repositories; @@ -7,6 +7,7 @@ using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.MediaCards.Mapper;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.MediaCards.Queries
{
@ -41,7 +42,7 @@ namespace ErsatzTV.Application.MediaCards.Queries @@ -41,7 +42,7 @@ namespace ErsatzTV.Application.MediaCards.Queries
.GetPagedSeasons(request.TelevisionShowId, request.PageNumber, request.PageSize)
.Map(list => list.Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby)).ToList());
return new TelevisionSeasonCardResultsViewModel(count, results);
return new TelevisionSeasonCardResultsViewModel(count, results, None);
}
}
}

7
ErsatzTV.Application/MediaCards/TelevisionSeasonCardResultsViewModel.cs

@ -1,6 +1,11 @@ @@ -1,6 +1,11 @@
using System.Collections.Generic;
using ErsatzTV.Core.Search;
using LanguageExt;
namespace ErsatzTV.Application.MediaCards
{
public record TelevisionSeasonCardResultsViewModel(int Count, List<TelevisionSeasonCardViewModel> Cards);
public record TelevisionSeasonCardResultsViewModel(
int Count,
List<TelevisionSeasonCardViewModel> Cards,
Option<SearchPageMap> PageMap);
}

1
ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollection.cs

@ -9,6 +9,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands @@ -9,6 +9,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
int CollectionId,
List<int> MovieIds,
List<int> ShowIds,
List<int> SeasonIds,
List<int> EpisodeIds,
List<int> ArtistIds,
List<int> MusicVideoIds) : MediatR.IRequest<Either<BaseError, Unit>>;

15
ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs

@ -52,6 +52,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands @@ -52,6 +52,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
{
var allItems = request.MovieIds
.Append(request.ShowIds)
.Append(request.SeasonIds)
.Append(request.EpisodeIds)
.Append(request.ArtistIds)
.Append(request.MusicVideoIds)
@ -77,12 +78,15 @@ namespace ErsatzTV.Application.MediaCollections.Commands @@ -77,12 +78,15 @@ namespace ErsatzTV.Application.MediaCollections.Commands
return Unit.Default;
}
private async Task<Validation<BaseError, Collection>> Validate(TvContext dbContext, AddItemsToCollection request) =>
private async Task<Validation<BaseError, Collection>> Validate(
TvContext dbContext,
AddItemsToCollection request) =>
(await CollectionMustExist(dbContext, request),
await ValidateMovies(request),
await ValidateShows(request),
await ValidateSeasons(request),
await ValidateEpisodes(request))
.Apply((collection, _, _, _) => collection);
.Apply((collection, _, _, _, _) => collection);
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
TvContext dbContext,
@ -106,6 +110,13 @@ namespace ErsatzTV.Application.MediaCollections.Commands @@ -106,6 +110,13 @@ namespace ErsatzTV.Application.MediaCollections.Commands
.MapT(_ => Unit.Default)
.Map(v => v.ToValidation<BaseError>("Show does not exist"));
private Task<Validation<BaseError, Unit>> ValidateSeasons(AddItemsToCollection request) =>
_televisionRepository.AllSeasonsExist(request.SeasonIds)
.Map(Optional)
.Filter(v => v == true)
.MapT(_ => Unit.Default)
.Map(v => v.ToValidation<BaseError>("Season does not exist"));
private Task<Validation<BaseError, Unit>> ValidateEpisodes(AddItemsToCollection request) =>
_televisionRepository.AllEpisodesExist(request.EpisodeIds)
.Map(Optional)

21
ErsatzTV.Application/MediaCollections/Commands/TraktCommandBase.cs

@ -82,12 +82,29 @@ namespace ErsatzTV.Application.MediaCollections.Commands @@ -82,12 +82,29 @@ namespace ErsatzTV.Application.MediaCollections.Commands
{
var toAdd = items.Filter(i => list.Items.All(i2 => i2.TraktId != i.TraktId)).ToList();
var toRemove = list.Items.Filter(i => items.All(i2 => i2.TraktId != i.TraktId)).ToList();
var toUpdate = list.Items.Filter(i => !toRemove.Contains(i)).ToList();
// TODO: do we need to update?
list.Items.RemoveAll(toRemove.Contains);
list.Items.AddRange(toAdd.Map(a => ProjectItem(list, a)));
foreach (TraktListItem existing in toUpdate)
{
Option<TraktListItem> maybeIncoming = list.Items.Find(i => i.TraktId == existing.TraktId);
foreach (TraktListItem incoming in maybeIncoming)
{
existing.Kind = incoming.Kind;
existing.Rank = incoming.Rank;
existing.Title = incoming.Title;
existing.Year = incoming.Year;
existing.Season = incoming.Season;
existing.Episode = incoming.Episode;
existing.Guids.Clear();
existing.Guids.AddRange(incoming.Guids);
existing.MediaItemId = null;
existing.MediaItem = null;
}
}
await dbContext.SaveChangesAsync();
return list;

1
ErsatzTV.Application/Search/Queries/QuerySearchIndexAllItemsHandler.cs

@ -21,6 +21,7 @@ namespace ErsatzTV.Application.Search.Queries @@ -21,6 +21,7 @@ namespace ErsatzTV.Application.Search.Queries
new(
await GetIds("movie", request.Query),
await GetIds("show", request.Query),
await GetIds("season", request.Query),
await GetIds("episode", request.Query),
await GetIds("artist", request.Query),
await GetIds("music_video", request.Query));

8
ErsatzTV.Application/Search/Queries/QuerySearchIndexSeasons.cs

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
using ErsatzTV.Application.MediaCards;
using MediatR;
namespace ErsatzTV.Application.Search.Queries
{
public record QuerySearchIndexSeasons
(string Query, int PageNumber, int PageSize) : IRequest<TelevisionSeasonCardResultsViewModel>;
}

55
ErsatzTV.Application/Search/Queries/QuerySearchIndexSeasonsHandler.cs

@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
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
QuerySearchIndexSeasonsHandler : IRequestHandler<QuerySearchIndexSeasons, TelevisionSeasonCardResultsViewModel>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ISearchIndex _searchIndex;
private readonly ITelevisionRepository _televisionRepository;
public QuerySearchIndexSeasonsHandler(
ISearchIndex searchIndex,
ITelevisionRepository televisionRepository,
IMediaSourceRepository mediaSourceRepository)
{
_searchIndex = searchIndex;
_televisionRepository = televisionRepository;
_mediaSourceRepository = mediaSourceRepository;
}
public async Task<TelevisionSeasonCardResultsViewModel> Handle(
QuerySearchIndexSeasons 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<TelevisionSeasonCardViewModel> items = await _televisionRepository
.GetSeasonsForCards(searchResult.Items.Map(i => i.Id).ToList())
.Map(list => list.Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby)).ToList());
return new TelevisionSeasonCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap);
}
}
}

1
ErsatzTV.Application/Search/SearchResultAllItemsViewModel.cs

@ -5,6 +5,7 @@ namespace ErsatzTV.Application.Search @@ -5,6 +5,7 @@ namespace ErsatzTV.Application.Search
public record SearchResultAllItemsViewModel(
List<int> MovieIds,
List<int> ShowIds,
List<int> SeasonIds,
List<int> EpisodeIds,
List<int> ArtistIds,
List<int> MusicVideoIds);

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

@ -11,6 +11,8 @@ namespace ErsatzTV.Core.Tests.Fakes @@ -11,6 +11,8 @@ namespace ErsatzTV.Core.Tests.Fakes
public class FakeTelevisionRepository : ITelevisionRepository
{
public Task<bool> AllShowsExist(List<int> showIds) => throw new NotSupportedException();
public Task<bool> AllSeasonsExist(List<int> seasonIds) => throw new NotSupportedException();
public Task<bool> AllEpisodesExist(List<int> episodeIds) => throw new NotSupportedException();
public Task<List<Show>> GetAllShows() => throw new NotSupportedException();
@ -18,6 +20,8 @@ namespace ErsatzTV.Core.Tests.Fakes @@ -18,6 +20,8 @@ namespace ErsatzTV.Core.Tests.Fakes
public Task<Option<Show>> GetShow(int showId) => throw new NotSupportedException();
public Task<List<ShowMetadata>> GetShowsForCards(List<int> ids) => throw new NotSupportedException();
public Task<List<SeasonMetadata>> GetSeasonsForCards(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();
@ -33,8 +37,6 @@ namespace ErsatzTV.Core.Tests.Fakes @@ -33,8 +37,6 @@ namespace ErsatzTV.Core.Tests.Fakes
public Task<List<Episode>> GetSeasonItems(int seasonId) => throw new NotSupportedException();
public Task<Option<Episode>> GetEpisode(int episodeId) => throw new NotSupportedException();
public Task<int> GetEpisodeCount(int seasonId) => throw new NotSupportedException();
public Task<List<EpisodeMetadata>> GetPagedEpisodes(int seasonId, int pageNumber, int pageSize) =>

12
ErsatzTV.Core/Emby/EmbyTelevisionLibraryScanner.cs

@ -227,7 +227,11 @@ namespace ErsatzTV.Core.Emby @@ -227,7 +227,11 @@ namespace ErsatzTV.Core.Emby
incoming.ShowId = show.Id;
incoming.LibraryPathId = library.Paths.Head().Id;
await _televisionRepository.Update(incoming);
foreach (EmbySeason updated in await _televisionRepository.Update(incoming))
{
incoming.Show = show;
await _searchIndex.UpdateItems(_searchRepository, new List<MediaItem> { updated });
}
},
async () =>
{
@ -238,7 +242,11 @@ namespace ErsatzTV.Core.Emby @@ -238,7 +242,11 @@ namespace ErsatzTV.Core.Emby
show.ShowMetadata.Head().Title,
incoming.SeasonMetadata.Head().Title);
await _televisionRepository.AddSeason(show, incoming);
if (await _televisionRepository.AddSeason(show, incoming))
{
incoming.Show = show;
await _searchIndex.AddItems(_searchRepository, new List<MediaItem> { incoming });
}
});
List<EmbyItemEtag> existingEpisodes =

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

@ -14,7 +14,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -14,7 +14,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
Task<bool> AddShow(EmbyShow show);
Task<Option<EmbyShow>> Update(EmbyShow show);
Task<bool> AddSeason(EmbyShow show, EmbySeason season);
Task<Unit> Update(EmbySeason season);
Task<Option<EmbySeason>> Update(EmbySeason season);
Task<bool> AddEpisode(EmbySeason season, EmbyEpisode episode);
Task<Option<EmbyEpisode>> Update(EmbyEpisode episode);
Task<List<int>> RemoveMissingShows(EmbyLibrary library, List<string> showIds);

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

@ -14,7 +14,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -14,7 +14,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
Task<bool> AddShow(JellyfinShow show);
Task<Option<JellyfinShow>> Update(JellyfinShow show);
Task<bool> AddSeason(JellyfinShow show, JellyfinSeason season);
Task<Unit> Update(JellyfinSeason season);
Task<Option<JellyfinSeason>> Update(JellyfinSeason season);
Task<bool> AddEpisode(JellyfinSeason season, JellyfinEpisode episode);
Task<Option<JellyfinEpisode>> Update(JellyfinEpisode episode);
Task<List<int>> RemoveMissingShows(JellyfinLibrary library, List<string> showIds);

1
ErsatzTV.Core/Interfaces/Repositories/ISearchRepository.cs

@ -10,6 +10,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -10,6 +10,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
Task<List<int>> GetItemIdsToIndex();
Task<Option<MediaItem>> GetItemToIndex(int id);
Task<List<string>> GetLanguagesForShow(Show show);
Task<List<string>> GetLanguagesForSeason(Season season);
Task<List<string>> GetLanguagesForArtist(Artist artist);
Task<List<string>> GetAllLanguageCodes(List<string> mediaCodes);
}

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

@ -9,10 +9,12 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -9,10 +9,12 @@ namespace ErsatzTV.Core.Interfaces.Repositories
public interface ITelevisionRepository
{
Task<bool> AllShowsExist(List<int> showIds);
Task<bool> AllSeasonsExist(List<int> seasonIds);
Task<bool> AllEpisodesExist(List<int> episodeIds);
Task<List<Show>> GetAllShows();
Task<Option<Show>> GetShow(int showId);
Task<List<ShowMetadata>> GetShowsForCards(List<int> ids);
Task<List<SeasonMetadata>> GetSeasonsForCards(List<int> ids);
Task<List<EpisodeMetadata>> GetEpisodesForCards(List<int> ids);
Task<List<Episode>> GetShowItems(int showId);
Task<List<Season>> GetAllSeasons();

12
ErsatzTV.Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs

@ -227,7 +227,11 @@ namespace ErsatzTV.Core.Jellyfin @@ -227,7 +227,11 @@ namespace ErsatzTV.Core.Jellyfin
incoming.ShowId = show.Id;
incoming.LibraryPathId = library.Paths.Head().Id;
await _televisionRepository.Update(incoming);
foreach (JellyfinSeason updated in await _televisionRepository.Update(incoming))
{
incoming.Show = show;
await _searchIndex.UpdateItems(_searchRepository, new List<MediaItem> { updated });
}
},
async () =>
{
@ -238,7 +242,11 @@ namespace ErsatzTV.Core.Jellyfin @@ -238,7 +242,11 @@ namespace ErsatzTV.Core.Jellyfin
show.ShowMetadata.Head().Title,
incoming.SeasonMetadata.Head().Title);
await _televisionRepository.AddSeason(show, incoming);
if (await _televisionRepository.AddSeason(show, incoming))
{
incoming.Show = show;
await _searchIndex.AddItems(_searchRepository, new List<MediaItem> { incoming });
}
});
List<JellyfinItemEtag> existingEpisodes =

3
ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs

@ -184,6 +184,9 @@ namespace ErsatzTV.Core.Metadata @@ -184,6 +184,9 @@ namespace ErsatzTV.Core.Metadata
{
await ScanEpisodes(libraryPath, ffprobePath, season, seasonFolder);
await _libraryRepository.SetEtag(libraryPath, knownFolder, seasonFolder, etag);
season.Show = show;
await _searchIndex.UpdateItems(_searchRepository, new List<MediaItem> { season });
},
error =>
{

20
ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs

@ -314,13 +314,19 @@ namespace ErsatzTV.Core.Plex @@ -314,13 +314,19 @@ namespace ErsatzTV.Core.Plex
.BindT(existing => UpdateMetadataAndArtwork(existing, incoming));
await maybeSeason.Match(
async season => await ScanEpisodes(
library,
pathReplacements,
season,
connection,
token,
ffprobePath),
async season =>
{
await ScanEpisodes(
library,
pathReplacements,
season,
connection,
token,
ffprobePath);
season.Show = show;
await _searchIndex.AddItems(_searchRepository, new List<MediaItem> { season });
},
error =>
{
_logger.LogWarning(

3
ErsatzTV.Infrastructure/Data/Configurations/Collection/TraktListItemConfiguration.cs

@ -11,7 +11,8 @@ namespace ErsatzTV.Infrastructure.Data.Configurations @@ -11,7 +11,8 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
builder.ToTable("TraktListItem");
builder.HasOne(i => i.MediaItem)
.WithMany(mi => mi.TraktListItems);
.WithMany(mi => mi.TraktListItems)
.OnDelete(DeleteBehavior.SetNull);
builder.HasMany(i => i.Guids)
.WithOne(g => g.TraktListItem)

4
ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs

@ -269,7 +269,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -269,7 +269,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
}
}
public async Task<Unit> Update(EmbySeason season)
public async Task<Option<EmbySeason>> Update(EmbySeason season)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Option<EmbySeason> maybeExisting = await dbContext.EmbySeasons
@ -378,7 +378,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -378,7 +378,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
await dbContext.SaveChangesAsync();
return Unit.Default;
return maybeExisting;
}
public async Task<bool> AddEpisode(EmbySeason season, EmbyEpisode episode)

9
ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs

@ -286,7 +286,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -286,7 +286,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
}
}
public async Task<Unit> Update(JellyfinSeason season)
public async Task<Option<JellyfinSeason>> Update(JellyfinSeason season)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Option<JellyfinSeason> maybeExisting = await dbContext.JellyfinSeasons
@ -374,11 +374,12 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -374,11 +374,12 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
{
metadata.Artwork.Remove(artworkToRemove);
}
}
await dbContext.SaveChangesAsync();
await dbContext.SaveChangesAsync();
await dbContext.Entry(existing.LibraryPath).Reference(lp => lp.Library).LoadAsync();
}
return Unit.Default;
return maybeExisting;
}
public async Task<bool> AddEpisode(JellyfinSeason season, JellyfinEpisode episode)

30
ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs

@ -365,6 +365,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -365,6 +365,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
WHERE l.Id IN @ids)",
new { ids = libraryIds });
List<int> seasonIds = await _dbConnection.QueryAsync<int>(
@"SELECT m.Id FROM MediaItem m
INNER JOIN PlexSeason ps ON ps.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(
@"DELETE FROM MediaItem WHERE Id IN
(SELECT m.Id FROM MediaItem m
@ -391,7 +399,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -391,7 +399,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
WHERE l.Id IN @ids)",
new { ids = libraryIds });
return movieIds.Append(showIds).Append(episodeIds).ToList();
return movieIds.Append(showIds).Append(seasonIds).Append(episodeIds).ToList();
}
public Task EnablePlexLibrarySync(IEnumerable<int> libraryIds) =>
@ -530,6 +538,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -530,6 +538,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
WHERE l.Id IN @ids)",
new { ids = libraryIds });
List<int> seasonIds = await _dbConnection.QueryAsync<int>(
@"SELECT m.Id FROM MediaItem m
INNER JOIN JellyfinSeason js ON js.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(
@"DELETE FROM MediaItem WHERE Id IN
(SELECT m.Id FROM MediaItem m
@ -556,7 +572,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -556,7 +572,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
WHERE l.Id IN @ids)",
new { ids = libraryIds });
return movieIds.Append(showIds).Append(episodeIds).ToList();
return movieIds.Append(showIds).Append(seasonIds).Append(episodeIds).ToList();
}
public Task<Option<JellyfinLibrary>> GetJellyfinLibrary(int jellyfinLibraryId)
@ -928,6 +944,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -928,6 +944,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
WHERE l.Id IN @ids)",
new { ids = libraryIds });
List<int> seasonIds = await _dbConnection.QueryAsync<int>(
@"SELECT m.Id FROM MediaItem m
INNER JOIN EmbySeason es ON es.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(
@"DELETE FROM MediaItem WHERE Id IN
(SELECT m.Id FROM MediaItem m
@ -954,7 +978,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -954,7 +978,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
WHERE l.Id IN @ids)",
new { ids = libraryIds });
return movieIds.Append(showIds).Append(episodeIds).ToList();
return movieIds.Append(showIds).Append(seasonIds).Append(episodeIds).ToList();
}
}
}

19
ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs

@ -65,6 +65,16 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -65,6 +65,16 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.Include(mi => (mi as Episode).MediaVersions)
.ThenInclude(em => em.Streams)
.Include(mi => (mi as Episode).Season)
.Include(mi => (mi as Season).SeasonMetadata)
.ThenInclude(sm => sm.Genres)
.Include(mi => (mi as Season).SeasonMetadata)
.ThenInclude(sm => sm.Tags)
.Include(mi => (mi as Season).SeasonMetadata)
.ThenInclude(sm => sm.Studios)
.Include(mi => (mi as Season).SeasonMetadata)
.ThenInclude(sm => sm.Actors)
.Include(mi => (mi as Season).Show)
.ThenInclude(sm => sm.ShowMetadata)
.Include(mi => (mi as Show).ShowMetadata)
.ThenInclude(mm => mm.Genres)
.Include(mi => (mi as Show).ShowMetadata)
@ -104,6 +114,15 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -104,6 +114,15 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
WHERE MediaStreamKind = 2 AND S.ShowId = @ShowId",
new { ShowId = show.Id }).Map(result => result.ToList());
public Task<List<string>> GetLanguagesForSeason(Season season) =>
_dbConnection.QueryAsync<string>(
@"SELECT DISTINCT Language
FROM MediaStream
INNER JOIN MediaVersion MV ON MediaStream.MediaVersionId = MV.Id
INNER JOIN Episode E ON MV.EpisodeId = E.Id
WHERE MediaStreamKind = 2 AND E.SeasonId = @SeasonId",
new { SeasonId = season.Id }).Map(result => result.ToList());
public Task<List<string>> GetLanguagesForArtist(Artist artist) =>
_dbConnection.QueryAsync<string>(
@"SELECT DISTINCT Language

39
ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs

@ -31,6 +31,12 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -31,6 +31,12 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
new { ShowIds = showIds })
.Map(c => c == showIds.Count);
public Task<bool> AllSeasonsExist(List<int> seasonIds) =>
_dbConnection.QuerySingleAsync<int>(
"SELECT COUNT(*) FROM Season WHERE Id in @SeasonIds",
new { SeasonIds = seasonIds })
.Map(c => c == seasonIds.Count);
public Task<bool> AllEpisodesExist(List<int> episodeIds) =>
_dbConnection.QuerySingleAsync<int>(
"SELECT COUNT(*) FROM Episode WHERE Id in @EpisodeIds",
@ -80,6 +86,23 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -80,6 +86,23 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ToListAsync();
}
public async Task<List<SeasonMetadata>> GetSeasonsForCards(List<int> ids)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.SeasonMetadata
.AsNoTracking()
.Filter(s => ids.Contains(s.SeasonId))
.Include(s => s.Season.Show)
.ThenInclude(s => s.ShowMetadata)
.Include(sm => sm.Artwork)
.ToListAsync()
.Map(
list => list
.OrderBy(s => s.Season.Show.ShowMetadata.HeadOrNone().Match(sm => sm.SortTitle, () => string.Empty))
.ThenBy(s => s.Season.SeasonNumber)
.ToList());
}
public async Task<List<EpisodeMetadata>> GetEpisodesForCards(List<int> ids)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
@ -274,6 +297,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -274,6 +297,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ThenInclude(sm => sm.Artwork)
.Include(s => s.SeasonMetadata)
.ThenInclude(sm => sm.Guids)
.Include(s => s.LibraryPath)
.ThenInclude(lp => lp.Library)
.Include(s => s.TraktListItems)
.ThenInclude(tli => tli.TraktList)
.OrderBy(s => s.ShowId)
.ThenBy(s => s.SeasonNumber)
.SingleOrDefaultAsync(s => s.ShowId == show.Id && s.SeasonNumber == seasonNumber);
@ -439,6 +466,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -439,6 +466,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ThenInclude(sm => sm.Artwork)
.Include(i => i.SeasonMetadata)
.ThenInclude(sm => sm.Guids)
.Include(s => s.LibraryPath)
.ThenInclude(l => l.Library)
.Include(s => s.TraktListItems)
.ThenInclude(tli => tli.TraktList)
.OrderBy(i => i.Key)
.SingleOrDefaultAsync(i => i.Key == item.Key);
@ -678,10 +709,15 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -678,10 +709,15 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
DateAdded = DateTime.UtcNow,
Guids = new List<MetadataGuid>()
}
}
},
TraktListItems = new List<TraktListItem>()
};
await dbContext.Seasons.AddAsync(season);
await dbContext.SaveChangesAsync();
await dbContext.Entry(season).Reference(s => s.LibraryPath).LoadAsync();
await dbContext.Entry(season.LibraryPath).Reference(lp => lp.Library).LoadAsync();
return season;
}
catch (Exception ex)
@ -782,6 +818,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -782,6 +818,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
await dbContext.PlexSeasons.AddAsync(item);
await dbContext.SaveChangesAsync();
await dbContext.Entry(item).Reference(i => i.LibraryPath).LoadAsync();
await dbContext.Entry(item.LibraryPath).Reference(lp => lp.Library).LoadAsync();
return item;
}
catch (Exception ex)

3284
ErsatzTV.Infrastructure/Migrations/20210925181536_Add_TraktListItemDeleteSetNull.Designer.cs generated

File diff suppressed because it is too large Load Diff

37
ErsatzTV.Infrastructure/Migrations/20210925181536_Add_TraktListItemDeleteSetNull.cs

@ -0,0 +1,37 @@ @@ -0,0 +1,37 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_TraktListItemDeleteSetNull : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_TraktListItem_MediaItem_MediaItemId",
table: "TraktListItem");
migrationBuilder.AddForeignKey(
name: "FK_TraktListItem_MediaItem_MediaItemId",
table: "TraktListItem",
column: "MediaItemId",
principalTable: "MediaItem",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_TraktListItem_MediaItem_MediaItemId",
table: "TraktListItem");
migrationBuilder.AddForeignKey(
name: "FK_TraktListItem_MediaItem_MediaItemId",
table: "TraktListItem",
column: "MediaItemId",
principalTable: "MediaItem",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
}
}

3
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -2690,7 +2690,8 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -2690,7 +2690,8 @@ namespace ErsatzTV.Infrastructure.Migrations
{
b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem")
.WithMany("TraktListItems")
.HasForeignKey("MediaItemId");
.HasForeignKey("MediaItemId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("ErsatzTV.Core.Domain.TraktList", "TraktList")
.WithMany("Items")

76
ErsatzTV.Infrastructure/Search/SearchIndex.cs

@ -55,6 +55,7 @@ namespace ErsatzTV.Infrastructure.Search @@ -55,6 +55,7 @@ namespace ErsatzTV.Infrastructure.Search
public const string MovieType = "movie";
public const string ShowType = "show";
public const string SeasonType = "season";
public const string ArtistType = "artist";
public const string MusicVideoType = "music_video";
public const string EpisodeType = "episode";
@ -73,7 +74,7 @@ namespace ErsatzTV.Infrastructure.Search @@ -73,7 +74,7 @@ namespace ErsatzTV.Infrastructure.Search
_initialized = false;
}
public int Version => 15;
public int Version => 16;
public Task<bool> Initialize(ILocalFileSystem localFileSystem)
{
@ -107,6 +108,9 @@ namespace ErsatzTV.Infrastructure.Search @@ -107,6 +108,9 @@ namespace ErsatzTV.Infrastructure.Search
case Show show:
await UpdateShow(searchRepository, show);
break;
case Season season:
await UpdateSeason(searchRepository, season);
break;
case Artist artist:
await UpdateArtist(searchRepository, artist);
break;
@ -201,6 +205,9 @@ namespace ErsatzTV.Infrastructure.Search @@ -201,6 +205,9 @@ namespace ErsatzTV.Infrastructure.Search
case Show show:
await UpdateShow(searchRepository, show);
break;
case Season season:
await UpdateSeason(searchRepository, season);
break;
case Artist artist:
await UpdateArtist(searchRepository, artist);
break;
@ -470,6 +477,71 @@ namespace ErsatzTV.Infrastructure.Search @@ -470,6 +477,71 @@ namespace ErsatzTV.Infrastructure.Search
}
}
}
private async Task UpdateSeason(ISearchRepository searchRepository, Season season)
{
Option<SeasonMetadata> maybeMetadata = season.SeasonMetadata.HeadOrNone();
Option<ShowMetadata> maybeShowMetadata = season.Show.ShowMetadata.HeadOrNone();
if (maybeMetadata.IsSome && maybeShowMetadata.IsSome)
{
SeasonMetadata metadata = maybeMetadata.ValueUnsafe();
ShowMetadata showMetadata = maybeShowMetadata.ValueUnsafe();
try
{
var seasonTitle = $"{showMetadata.Title} - S{season.SeasonNumber}";
string sortTitle = $"{showMetadata.SortTitle}_{season.SeasonNumber:0000}"
.ToLowerInvariant();
string titleAndYear = $"{showMetadata.Title}_{showMetadata.Year}_{season.SeasonNumber}"
.ToLowerInvariant();
var doc = new Document
{
new StringField(IdField, season.Id.ToString(), Field.Store.YES),
new StringField(TypeField, SeasonType, Field.Store.YES),
new TextField(TitleField, seasonTitle, Field.Store.NO),
new StringField(SortTitleField, sortTitle, Field.Store.NO),
new TextField(LibraryNameField, season.LibraryPath.Library.Name, Field.Store.NO),
new StringField(LibraryIdField, season.LibraryPath.Library.Id.ToString(), Field.Store.NO),
new StringField(TitleAndYearField, titleAndYear, Field.Store.NO),
new StringField(JumpLetterField, GetJumpLetter(showMetadata), Field.Store.YES)
};
List<string> languages = await searchRepository.GetLanguagesForSeason(season);
await AddLanguages(searchRepository, doc, languages);
if (!string.IsNullOrWhiteSpace(showMetadata.ContentRating))
{
foreach (string contentRating in (showMetadata.ContentRating ?? string.Empty).Split("/")
.Map(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x)))
{
doc.Add(new StringField(ContentRatingField, contentRating, Field.Store.NO));
}
}
if (metadata.ReleaseDate.HasValue)
{
doc.Add(
new StringField(
ReleaseDateField,
metadata.ReleaseDate.Value.ToString("yyyyMMdd"),
Field.Store.NO));
}
foreach (TraktListItem item in season.TraktListItems)
{
doc.Add(new StringField(TraktListField, item.TraktList.TraktId.ToString(), Field.Store.NO));
}
_writer.UpdateDocument(new Term(IdField, season.Id.ToString()), doc);
}
catch (Exception ex)
{
metadata.Season = null;
_logger.LogWarning(ex, "Error indexing show with metadata {@Metadata}", metadata);
}
}
}
private async Task UpdateArtist(ISearchRepository searchRepository, Artist artist)
{
@ -697,7 +769,7 @@ namespace ErsatzTV.Infrastructure.Search @@ -697,7 +769,7 @@ namespace ErsatzTV.Infrastructure.Search
.ToLowerInvariant(),
_ => $"{metadata.Title}_{metadata.Year}".ToLowerInvariant()
};
private static string GetJumpLetter(Metadata metadata)
{
char c = metadata.SortTitle.ToLowerInvariant().Head();

10
ErsatzTV/Pages/MultiSelectBase.cs

@ -78,9 +78,7 @@ namespace ErsatzTV.Pages @@ -78,9 +78,7 @@ namespace ErsatzTV.Pages
int finish = sorted.IndexOf(card);
if (start > finish)
{
int temp = start;
start = finish;
finish = temp;
(start, finish) = (finish, start);
}
for (int i = start; i < finish; i++)
@ -97,6 +95,7 @@ namespace ErsatzTV.Pages @@ -97,6 +95,7 @@ namespace ErsatzTV.Pages
protected Task AddSelectionToCollection() => AddItemsToCollection(
_selectedItems.OfType<MovieCardViewModel>().Map(m => m.MovieId).ToList(),
_selectedItems.OfType<TelevisionShowCardViewModel>().Map(s => s.TelevisionShowId).ToList(),
_selectedItems.OfType<TelevisionSeasonCardViewModel>().Map(s => s.TelevisionSeasonId).ToList(),
_selectedItems.OfType<TelevisionEpisodeCardViewModel>().Map(e => e.EpisodeId).ToList(),
_selectedItems.OfType<ArtistCardViewModel>().Map(a => a.ArtistId).ToList(),
_selectedItems.OfType<MusicVideoCardViewModel>().Map(mv => mv.MusicVideoId).ToList());
@ -104,12 +103,14 @@ namespace ErsatzTV.Pages @@ -104,12 +103,14 @@ namespace ErsatzTV.Pages
protected async Task AddItemsToCollection(
List<int> movieIds,
List<int> showIds,
List<int> seasonIds,
List<int> episodeIds,
List<int> artistIds,
List<int> musicVideoIds,
string entityName = "selected items")
{
int count = movieIds.Count + showIds.Count + episodeIds.Count + artistIds.Count + musicVideoIds.Count;
int count = movieIds.Count + showIds.Count + seasonIds.Count + episodeIds.Count + artistIds.Count +
musicVideoIds.Count;
var parameters = new DialogParameters
{ { "EntityType", count.ToString() }, { "EntityName", entityName } };
@ -123,6 +124,7 @@ namespace ErsatzTV.Pages @@ -123,6 +124,7 @@ namespace ErsatzTV.Pages
collection.Id,
movieIds,
showIds,
seasonIds,
episodeIds,
artistIds,
musicVideoIds);

55
ErsatzTV/Pages/Search.razor

@ -44,6 +44,11 @@ @@ -44,6 +44,11 @@
<MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#shows")" Style="margin-bottom: auto; margin-top: auto">@_shows.Count Shows</MudLink>
}
if (_seasons?.Count > 0)
{
<MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#seasons")" Style="margin-bottom: auto; margin-top: auto">@_seasons.Count Seasons</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>
@ -133,6 +138,33 @@ @@ -133,6 +138,33 @@
}
</MudContainer>
}
@if (_seasons?.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", "seasons" } })">
Seasons
</MudText>
@if (_seasons.Count > 50)
{
<MudLink Href="@GetSeasonsLink()" Class="ml-4">See All >></MudLink>
}
</div>
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
@foreach (TelevisionSeasonCardViewModel card in _seasons.Cards.OrderBy(s => s.SortTitle))
{
<MediaCard Data="@card"
Link="@($"/media/tv/shows/{card.TelevisionSeasonId}")"
AddToCollectionClicked="@AddToCollection"
SelectClicked="@(e => SelectClicked(card, e))"
IsSelected="@IsSelected(card)"
IsSelectMode="@IsSelectMode()"/>
}
</MudContainer>
}
@if (_episodes?.Count > 0)
{
@ -223,6 +255,7 @@ @@ -223,6 +255,7 @@
private string _query;
private MovieCardResultsViewModel _movies;
private TelevisionShowCardResultsViewModel _shows;
private TelevisionSeasonCardResultsViewModel _seasons;
private TelevisionEpisodeCardResultsViewModel _episodes;
private MusicVideoCardResultsViewModel _musicVideos;
private ArtistCardResultsViewModel _artists;
@ -234,6 +267,7 @@ @@ -234,6 +267,7 @@
{
_movies = await Mediator.Send(new QuerySearchIndexMovies($"type:movie AND ({_query})", 1, 50));
_shows = await Mediator.Send(new QuerySearchIndexShows($"type:show AND ({_query})", 1, 50));
_seasons = await Mediator.Send(new QuerySearchIndexSeasons($"type:season AND ({_query})", 1, 50));
_episodes = await Mediator.Send(new QuerySearchIndexEpisodes($"type:episode AND ({_query})", 1, 50));
_musicVideos = await Mediator.Send(new QuerySearchIndexMusicVideos($"type:music_video AND ({_query})", 1, 50));
_artists = await Mediator.Send(new QuerySearchIndexArtists($"type:artist AND ({_query})", 1, 50));
@ -246,6 +280,7 @@ @@ -246,6 +280,7 @@
{
return _movies.Cards.OrderBy(m => m.SortTitle)
.Append<MediaCardViewModel>(_shows.Cards.OrderBy(s => s.SortTitle))
.Append(_seasons.Cards.OrderBy(s => s.SortTitle))
.Append(_episodes.Cards.OrderBy(ep => ep.SortTitle))
.Append(_artists.Cards.OrderBy(a => a.SortTitle))
.Append(_musicVideos.Cards.OrderBy(mv => mv.SortTitle))
@ -384,6 +419,17 @@ @@ -384,6 +419,17 @@
}
return uri;
}
private string GetSeasonsLink()
{
var uri = "/media/tv/seasons/page/1";
if (!string.IsNullOrWhiteSpace(_query))
{
(string key, string value) = _query.EncodeQuery();
uri = $"{uri}?{key}={value}";
}
return uri;
}
private string GetEpisodesLink()
{
@ -421,7 +467,14 @@ @@ -421,7 +467,14 @@
private async Task AddAllToCollection(MouseEventArgs _)
{
SearchResultAllItemsViewModel results = await Mediator.Send(new QuerySearchIndexAllItems(_query));
await AddItemsToCollection(results.MovieIds, results.ShowIds, results.EpisodeIds, results.ArtistIds, results.MusicVideoIds, "search results");
await AddItemsToCollection(
results.MovieIds,
results.ShowIds,
results.SeasonIds,
results.EpisodeIds,
results.ArtistIds,
results.MusicVideoIds,
"search results");
}
private async Task SaveAsSmartCollection(MouseEventArgs _)

157
ErsatzTV/Pages/TelevisionSeasonSearchResults.razor

@ -0,0 +1,157 @@ @@ -0,0 +1,157 @@
@page "/media/tv/seasons"
@page "/media/tv/seasons/page/{PageNumber:int}"
@using LanguageExt.UnsafeValueAccess
@using ErsatzTV.Application.MediaCards
@using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.MediaCollections.Commands
@using ErsatzTV.Application.Search.Queries
@using ErsatzTV.Extensions
@using Unit = LanguageExt.Unit
@inherits MultiSelectBase<TelevisionShowList>
@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="TelevisionSeasonCardViewModel" Cards="@_data.Cards">
<MediaCard Data="@context"
Link="@($"/media/tv/seasons/{context.TelevisionSeasonId}")"
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/seasons"
Query="@_query"/>
}
@code {
private static int PageSize => 100;
[Parameter]
public int PageNumber { get; set; }
private TelevisionSeasonCardResultsViewModel _data;
private string _query;
protected override Task OnParametersSetAsync()
{
if (PageNumber == 0)
{
PageNumber = 1;
}
_query = _navigationManager.Uri.GetSearchQuery();
return RefreshData();
}
protected override async Task RefreshData()
{
string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:season" : $"type:season AND ({_query})";
_data = await Mediator.Send(new QuerySearchIndexSeasons(searchQuery, PageNumber, PageSize));
}
private void PrevPage()
{
var uri = $"/media/tv/seasons/page/{PageNumber - 1}";
if (!string.IsNullOrWhiteSpace(_query))
{
(string key, string value) = _query.EncodeQuery();
uri = $"{uri}?{key}={value}";
}
_navigationManager.NavigateTo(uri);
}
private void NextPage()
{
var uri = $"/media/tv/seasons/page/{PageNumber + 1}";
if (!string.IsNullOrWhiteSpace(_query))
{
(string key, string value) = _query.EncodeQuery();
uri = $"{uri}?{key}={value}";
}
_navigationManager.NavigateTo(uri);
}
private void SelectClicked(MediaCardViewModel card, MouseEventArgs e)
{
List<MediaCardViewModel> GetSortedItems()
{
return _data.Cards.OrderBy(m => m.SortTitle).ToList<MediaCardViewModel>();
}
SelectClicked(GetSortedItems, card, e);
}
private async Task AddToCollection(MediaCardViewModel card)
{
if (card is TelevisionSeasonCardViewModel season)
{
var parameters = new DialogParameters { { "EntityType", "season" }, { "EntityName", season.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 AddSeasonToCollection(collection.Id, season.TelevisionSeasonId);
Either<BaseError, Unit> addResult = await Mediator.Send(request);
addResult.Match(
Left: error =>
{
Snackbar.Add($"Unexpected error adding season to collection: {error.Value}");
Logger.LogError("Unexpected error adding season to collection: {Error}", error.Value);
},
Right: _ => Snackbar.Add($"Added {season.Title} to collection {collection.Name}", Severity.Success));
}
}
}
}

1
ErsatzTV/Pages/TelevisionShowList.razor

@ -1,7 +1,6 @@ @@ -1,7 +1,6 @@
@page "/media/tv/shows"
@page "/media/tv/shows/page/{PageNumber:int}"
@using LanguageExt.UnsafeValueAccess
@using Microsoft.AspNetCore.WebUtilities
@using ErsatzTV.Application.MediaCards
@using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.MediaCollections.Commands

Loading…
Cancel
Save