Browse Source

elasticsearch relative queries (#1373)

* remove unused code

* fix relative queries with elasticsearch

* fix some double page loads

* simplify language model
pull/1374/head
Jason Dove 2 years ago committed by GitHub
parent
commit
097c60169c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      ErsatzTV.Application/MediaCards/ArtistCardResultsViewModel.cs
  2. 2
      ErsatzTV.Application/MediaCards/MovieCardResultsViewModel.cs
  3. 2
      ErsatzTV.Application/MediaCards/MusicVideoCardResultsViewModel.cs
  4. 2
      ErsatzTV.Application/MediaCards/OtherVideoCardResultsViewModel.cs
  5. 2
      ErsatzTV.Application/MediaCards/Queries/GetMusicVideoCardsHandler.cs
  6. 2
      ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCardsHandler.cs
  7. 2
      ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCardsHandler.cs
  8. 2
      ErsatzTV.Application/MediaCards/SongCardResultsViewModel.cs
  9. 2
      ErsatzTV.Application/MediaCards/TelevisionEpisodeCardResultsViewModel.cs
  10. 2
      ErsatzTV.Application/MediaCards/TelevisionSeasonCardResultsViewModel.cs
  11. 2
      ErsatzTV.Application/MediaCards/TelevisionShowCardResultsViewModel.cs
  12. 4
      ErsatzTV.Application/MediaItems/LanguageCodeViewModel.cs
  13. 6
      ErsatzTV.Application/MediaItems/Queries/GetAllLanguageCodes.cs
  14. 9
      ErsatzTV.Application/MediaItems/Queries/GetAllLanguageCodesHandler.cs
  15. 3
      ErsatzTV.Application/Movies/Mapper.cs
  16. 2
      ErsatzTV.Application/Movies/MovieViewModel.cs
  17. 1
      ErsatzTV.Application/Search/Queries/QuerySearchIndexMoviesHandler.cs
  18. 2
      ErsatzTV.Core/Interfaces/Search/ISearchIndex.cs
  19. 2
      ErsatzTV.Core/Search/SearchResult.cs
  20. 2
      ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj
  21. 66
      ErsatzTV.Infrastructure/Search/CustomMultiFieldQueryParser.cs
  22. 159
      ErsatzTV.Infrastructure/Search/CustomQueryParser.cs
  23. 12
      ErsatzTV.Infrastructure/Search/ElasticSearchIndex.cs
  24. 34
      ErsatzTV.Infrastructure/Search/LuceneSearchIndex.cs
  25. 6
      ErsatzTV/Pages/ArtistList.razor
  26. 7
      ErsatzTV/Pages/ChannelEditor.razor
  27. 6
      ErsatzTV/Pages/EpisodeList.razor
  28. 86
      ErsatzTV/Pages/Movie.razor
  29. 44
      ErsatzTV/Pages/MovieList.razor
  30. 6
      ErsatzTV/Pages/MusicVideoList.razor
  31. 6
      ErsatzTV/Pages/OtherVideoList.razor
  32. 13
      ErsatzTV/Pages/ScheduleItemsEditor.razor
  33. 104
      ErsatzTV/Pages/Search.razor
  34. 4
      ErsatzTV/Pages/Settings.razor
  35. 6
      ErsatzTV/Pages/SongList.razor
  36. 6
      ErsatzTV/Pages/TelevisionSeasonSearchResults.razor
  37. 51
      ErsatzTV/Pages/TelevisionShowList.razor
  38. 1
      ErsatzTV/Pages/_Host.cshtml
  39. 7
      ErsatzTV/Startup.cs

2
ErsatzTV.Application/MediaCards/ArtistCardResultsViewModel.cs

@ -5,4 +5,4 @@ namespace ErsatzTV.Application.MediaCards;
public record ArtistCardResultsViewModel( public record ArtistCardResultsViewModel(
int Count, int Count,
List<ArtistCardViewModel> Cards, List<ArtistCardViewModel> Cards,
Option<SearchPageMap> PageMap); SearchPageMap PageMap);

2
ErsatzTV.Application/MediaCards/MovieCardResultsViewModel.cs

@ -2,4 +2,4 @@
namespace ErsatzTV.Application.MediaCards; namespace ErsatzTV.Application.MediaCards;
public record MovieCardResultsViewModel(int Count, List<MovieCardViewModel> Cards, Option<SearchPageMap> PageMap); public record MovieCardResultsViewModel(int Count, List<MovieCardViewModel> Cards, SearchPageMap PageMap);

2
ErsatzTV.Application/MediaCards/MusicVideoCardResultsViewModel.cs

@ -5,4 +5,4 @@ namespace ErsatzTV.Application.MediaCards;
public record MusicVideoCardResultsViewModel( public record MusicVideoCardResultsViewModel(
int Count, int Count,
List<MusicVideoCardViewModel> Cards, List<MusicVideoCardViewModel> Cards,
Option<SearchPageMap> PageMap); SearchPageMap PageMap);

2
ErsatzTV.Application/MediaCards/OtherVideoCardResultsViewModel.cs

@ -5,4 +5,4 @@ namespace ErsatzTV.Application.MediaCards;
public record OtherVideoCardResultsViewModel( public record OtherVideoCardResultsViewModel(
int Count, int Count,
List<OtherVideoCardViewModel> Cards, List<OtherVideoCardViewModel> Cards,
Option<SearchPageMap> PageMap); SearchPageMap PageMap);

2
ErsatzTV.Application/MediaCards/Queries/GetMusicVideoCardsHandler.cs

@ -49,6 +49,6 @@ public class GetMusicVideoCardsHandler : IRequestHandler<GetMusicVideoCards, Mus
results.Add(ProjectToViewModel(musicVideoMetadata, localPath)); results.Add(ProjectToViewModel(musicVideoMetadata, localPath));
} }
return new MusicVideoCardResultsViewModel(count, results, None); return new MusicVideoCardResultsViewModel(count, results, null);
} }
} }

2
ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCardsHandler.cs

@ -59,6 +59,6 @@ public class
results.Add(ProjectToViewModel(episodeMetadata, maybeJellyfin, maybeEmby, false, localPath)); results.Add(ProjectToViewModel(episodeMetadata, maybeJellyfin, maybeEmby, false, localPath));
} }
return new TelevisionEpisodeCardResultsViewModel(count, results, Option<SearchPageMap>.None); return new TelevisionEpisodeCardResultsViewModel(count, results, null);
} }
} }

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

@ -35,6 +35,6 @@ public class
.GetPagedSeasons(request.TelevisionShowId, request.PageNumber, request.PageSize) .GetPagedSeasons(request.TelevisionShowId, request.PageNumber, request.PageSize)
.Map(list => list.Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby)).ToList()); .Map(list => list.Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby)).ToList());
return new TelevisionSeasonCardResultsViewModel(count, results, None); return new TelevisionSeasonCardResultsViewModel(count, results, null);
} }
} }

2
ErsatzTV.Application/MediaCards/SongCardResultsViewModel.cs

@ -5,4 +5,4 @@ namespace ErsatzTV.Application.MediaCards;
public record SongCardResultsViewModel( public record SongCardResultsViewModel(
int Count, int Count,
List<SongCardViewModel> Cards, List<SongCardViewModel> Cards,
Option<SearchPageMap> PageMap); SearchPageMap PageMap);

2
ErsatzTV.Application/MediaCards/TelevisionEpisodeCardResultsViewModel.cs

@ -5,4 +5,4 @@ namespace ErsatzTV.Application.MediaCards;
public record TelevisionEpisodeCardResultsViewModel( public record TelevisionEpisodeCardResultsViewModel(
int Count, int Count,
List<TelevisionEpisodeCardViewModel> Cards, List<TelevisionEpisodeCardViewModel> Cards,
Option<SearchPageMap> PageMap); SearchPageMap PageMap);

2
ErsatzTV.Application/MediaCards/TelevisionSeasonCardResultsViewModel.cs

@ -5,4 +5,4 @@ namespace ErsatzTV.Application.MediaCards;
public record TelevisionSeasonCardResultsViewModel( public record TelevisionSeasonCardResultsViewModel(
int Count, int Count,
List<TelevisionSeasonCardViewModel> Cards, List<TelevisionSeasonCardViewModel> Cards,
Option<SearchPageMap> PageMap); SearchPageMap PageMap);

2
ErsatzTV.Application/MediaCards/TelevisionShowCardResultsViewModel.cs

@ -5,4 +5,4 @@ namespace ErsatzTV.Application.MediaCards;
public record TelevisionShowCardResultsViewModel( public record TelevisionShowCardResultsViewModel(
int Count, int Count,
List<TelevisionShowCardViewModel> Cards, List<TelevisionShowCardViewModel> Cards,
Option<SearchPageMap> PageMap); SearchPageMap PageMap);

4
ErsatzTV.Application/MediaItems/LanguageCodeViewModel.cs

@ -0,0 +1,4 @@
namespace ErsatzTV.Application.MediaItems;
// ReSharper disable once InconsistentNaming
public record LanguageCodeViewModel(string ThreeLetterISOLanguageName, string EnglishName);

6
ErsatzTV.Application/MediaItems/Queries/GetAllLanguageCodes.cs

@ -1,5 +1,3 @@
using System.Globalization; namespace ErsatzTV.Application.MediaItems;
namespace ErsatzTV.Application.MediaItems; public record GetAllLanguageCodes : IRequest<List<LanguageCodeViewModel>>;
public record GetAllLanguageCodes : IRequest<List<CultureInfo>>;

9
ErsatzTV.Application/MediaItems/Queries/GetAllLanguageCodesHandler.cs

@ -3,13 +3,16 @@ using ErsatzTV.Core.Interfaces.Repositories;
namespace ErsatzTV.Application.MediaItems; namespace ErsatzTV.Application.MediaItems;
public class GetAllLanguageCodesHandler : IRequestHandler<GetAllLanguageCodes, List<CultureInfo>> public class GetAllLanguageCodesHandler : IRequestHandler<GetAllLanguageCodes, List<LanguageCodeViewModel>>
{ {
private readonly IMediaItemRepository _mediaItemRepository; private readonly IMediaItemRepository _mediaItemRepository;
public GetAllLanguageCodesHandler(IMediaItemRepository mediaItemRepository) => public GetAllLanguageCodesHandler(IMediaItemRepository mediaItemRepository) =>
_mediaItemRepository = mediaItemRepository; _mediaItemRepository = mediaItemRepository;
public async Task<List<CultureInfo>> Handle(GetAllLanguageCodes request, CancellationToken cancellationToken) => public async Task<List<LanguageCodeViewModel>> Handle(GetAllLanguageCodes request, CancellationToken cancellationToken)
await _mediaItemRepository.GetAllLanguageCodeCultures(); {
List<CultureInfo> cultures = await _mediaItemRepository.GetAllLanguageCodeCultures();
return cultures.Map(c => new LanguageCodeViewModel(c.ThreeLetterISOLanguageName, c.EnglishName)).ToList();
}
} }

3
ErsatzTV.Application/Movies/Mapper.cs

@ -41,7 +41,7 @@ internal static class Mapper
}; };
} }
private static List<CultureInfo> LanguagesForMovie(List<string> languageCodes) private static List<string> LanguagesForMovie(List<string> languageCodes)
{ {
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures); CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
@ -52,6 +52,7 @@ internal static class Mapper
ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase))) ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase)))
.Sequence() .Sequence()
.Flatten() .Flatten()
.Map(ci => ci.EnglishName)
.ToList(); .ToList();
} }

2
ErsatzTV.Application/Movies/MovieViewModel.cs

@ -12,7 +12,7 @@ public record MovieViewModel(
List<string> Tags, List<string> Tags,
List<string> Studios, List<string> Studios,
List<string> ContentRatings, List<string> ContentRatings,
List<CultureInfo> Languages, List<string> Languages,
List<ActorCardViewModel> Actors, List<ActorCardViewModel> Actors,
List<string> Directors, List<string> Directors,
List<string> Writers, List<string> Writers,

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

@ -4,6 +4,7 @@ using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Search; using ErsatzTV.Core.Search;
using LanguageExt.UnsafeValueAccess;
using static ErsatzTV.Application.MediaCards.Mapper; using static ErsatzTV.Application.MediaCards.Mapper;
namespace ErsatzTV.Application.Search; namespace ErsatzTV.Application.Search;

2
ErsatzTV.Core/Interfaces/Search/ISearchIndex.cs

@ -25,6 +25,6 @@ public interface ISearchIndex : IDisposable
List<MediaItem> items); List<MediaItem> items);
Task<Unit> RemoveItems(IEnumerable<int> ids); Task<Unit> RemoveItems(IEnumerable<int> ids);
Task<SearchResult> Search(IClient client, string query, int skip, int limit, string searchField = ""); Task<SearchResult> Search(IClient client, string query, int skip, int limit);
void Commit(); void Commit();
} }

2
ErsatzTV.Core/Search/SearchResult.cs

@ -2,5 +2,5 @@
public record SearchResult(List<SearchItem> Items, int TotalCount) public record SearchResult(List<SearchItem> Items, int TotalCount)
{ {
public Option<SearchPageMap> PageMap { get; set; } public SearchPageMap PageMap { get; set; }
} }

2
ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj

@ -37,4 +37,6 @@
</ItemGroup> </ItemGroup>
</Project> </Project>

66
ErsatzTV.Infrastructure/Search/CustomMultiFieldQueryParser.cs

@ -1,4 +1,5 @@
using Lucene.Net.Analysis; using ErsatzTV.Core;
using Lucene.Net.Analysis;
using Lucene.Net.Index; using Lucene.Net.Index;
using Lucene.Net.QueryParsers.Classic; using Lucene.Net.QueryParsers.Classic;
using Lucene.Net.Search; using Lucene.Net.Search;
@ -9,6 +10,16 @@ namespace ErsatzTV.Infrastructure.Search;
public class CustomMultiFieldQueryParser : MultiFieldQueryParser public class CustomMultiFieldQueryParser : MultiFieldQueryParser
{ {
private static readonly List<string> NumericFields = new()
{
LuceneSearchIndex.MinutesField,
LuceneSearchIndex.HeightField,
LuceneSearchIndex.WidthField,
LuceneSearchIndex.SeasonNumberField,
LuceneSearchIndex.EpisodeNumberField,
LuceneSearchIndex.VideoBitDepthField
};
public CustomMultiFieldQueryParser( public CustomMultiFieldQueryParser(
LuceneVersion matchVersion, LuceneVersion matchVersion,
string[] fields, string[] fields,
@ -32,7 +43,7 @@ public class CustomMultiFieldQueryParser : MultiFieldQueryParser
return base.GetWildcardQuery(LuceneSearchIndex.ReleaseDateField, todayString); return base.GetWildcardQuery(LuceneSearchIndex.ReleaseDateField, todayString);
} }
if (CustomQueryParser.NumericFields.Contains(field) && int.TryParse(queryText, out int val)) if (NumericFields.Contains(field) && int.TryParse(queryText, out int val))
{ {
var bytesRef = new BytesRef(); var bytesRef = new BytesRef();
NumericUtils.Int32ToPrefixCoded(val, 0, bytesRef); NumericUtils.Int32ToPrefixCoded(val, 0, bytesRef);
@ -44,7 +55,7 @@ public class CustomMultiFieldQueryParser : MultiFieldQueryParser
protected override Query GetFieldQuery(string field, string queryText, int slop) protected override Query GetFieldQuery(string field, string queryText, int slop)
{ {
if (field == "released_inthelast" && CustomQueryParser.ParseStart(queryText, out DateTime start)) if (field == "released_inthelast" && ParseStart(queryText, out DateTime start))
{ {
var todayString = DateTime.UtcNow.ToString("yyyyMMdd"); var todayString = DateTime.UtcNow.ToString("yyyyMMdd");
var dateString = start.ToString("yyyyMMdd"); var dateString = start.ToString("yyyyMMdd");
@ -52,14 +63,14 @@ public class CustomMultiFieldQueryParser : MultiFieldQueryParser
return base.GetRangeQuery(LuceneSearchIndex.ReleaseDateField, dateString, todayString, true, true); return base.GetRangeQuery(LuceneSearchIndex.ReleaseDateField, dateString, todayString, true, true);
} }
if (field == "released_notinthelast" && CustomQueryParser.ParseStart(queryText, out DateTime finish)) if (field == "released_notinthelast" && ParseStart(queryText, out DateTime finish))
{ {
var dateString = finish.ToString("yyyyMMdd"); var dateString = finish.ToString("yyyyMMdd");
return base.GetRangeQuery(LuceneSearchIndex.ReleaseDateField, "00000000", dateString, false, false); return base.GetRangeQuery(LuceneSearchIndex.ReleaseDateField, "00000000", dateString, false, false);
} }
if (field == "added_inthelast" && CustomQueryParser.ParseStart(queryText, out DateTime addedStart)) if (field == "added_inthelast" && ParseStart(queryText, out DateTime addedStart))
{ {
var todayString = DateTime.UtcNow.ToString("yyyyMMdd"); var todayString = DateTime.UtcNow.ToString("yyyyMMdd");
var dateString = addedStart.ToString("yyyyMMdd"); var dateString = addedStart.ToString("yyyyMMdd");
@ -67,7 +78,7 @@ public class CustomMultiFieldQueryParser : MultiFieldQueryParser
return base.GetRangeQuery(LuceneSearchIndex.AddedDateField, dateString, todayString, true, true); return base.GetRangeQuery(LuceneSearchIndex.AddedDateField, dateString, todayString, true, true);
} }
if (field == "added_notinthelast" && CustomQueryParser.ParseStart(queryText, out DateTime addedFinish)) if (field == "added_notinthelast" && ParseStart(queryText, out DateTime addedFinish))
{ {
var dateString = addedFinish.ToString("yyyyMMdd"); var dateString = addedFinish.ToString("yyyyMMdd");
@ -84,7 +95,7 @@ public class CustomMultiFieldQueryParser : MultiFieldQueryParser
bool startInclusive, bool startInclusive,
bool endInclusive) bool endInclusive)
{ {
if (CustomQueryParser.NumericFields.Contains(field)) if (NumericFields.Contains(field))
{ {
if (part1 is null or "*" && int.TryParse(part2, out int max1)) if (part1 is null or "*" && int.TryParse(part2, out int max1))
{ {
@ -107,4 +118,45 @@ public class CustomMultiFieldQueryParser : MultiFieldQueryParser
return base.GetRangeQuery(field, part1, part2, startInclusive, endInclusive); return base.GetRangeQuery(field, part1, part2, startInclusive, endInclusive);
} }
private static bool ParseStart(string text, out DateTime start)
{
start = SystemTime.MinValueUtc;
try
{
if (int.TryParse(text.Split(" ")[0], out int number))
{
if (text.Contains("day"))
{
start = DateTime.Today.AddDays(number * -1);
return true;
}
if (text.Contains("week"))
{
start = DateTime.Today.AddDays(number * -7);
return true;
}
if (text.Contains("month"))
{
start = DateTime.Today.AddMonths(number * -1);
return true;
}
if (text.Contains("year"))
{
start = DateTime.Today.AddYears(number * -1);
return true;
}
}
}
catch
{
// do nothing
}
return false;
}
} }

159
ErsatzTV.Infrastructure/Search/CustomQueryParser.cs

@ -1,159 +0,0 @@
using ErsatzTV.Core;
using Lucene.Net.Analysis;
using Lucene.Net.Index;
using Lucene.Net.QueryParsers.Classic;
using Lucene.Net.Search;
using Lucene.Net.Util;
using Query = Lucene.Net.Search.Query;
namespace ErsatzTV.Infrastructure.Search;
public class CustomQueryParser : QueryParser
{
internal static readonly List<string> NumericFields = new()
{
LuceneSearchIndex.MinutesField,
LuceneSearchIndex.HeightField,
LuceneSearchIndex.WidthField,
LuceneSearchIndex.SeasonNumberField,
LuceneSearchIndex.EpisodeNumberField,
LuceneSearchIndex.VideoBitDepthField
};
public CustomQueryParser(LuceneVersion matchVersion, string f, Analyzer a) : base(matchVersion, f, a)
{
}
protected internal CustomQueryParser(ICharStream stream) : base(stream)
{
}
protected CustomQueryParser(QueryParserTokenManager tm) : base(tm)
{
}
protected override Query GetFieldQuery(string field, string queryText, bool quoted)
{
if (field == "released_onthisday")
{
var todayString = DateTime.Today.ToString("*MMdd");
return base.GetWildcardQuery(LuceneSearchIndex.ReleaseDateField, todayString);
}
if (NumericFields.Contains(field) && int.TryParse(queryText, out int val))
{
var bytesRef = new BytesRef();
NumericUtils.Int32ToPrefixCoded(val, 0, bytesRef);
return NewTermQuery(new Term(field, bytesRef));
}
return base.GetFieldQuery(field, queryText, quoted);
}
protected override Query GetFieldQuery(string field, string queryText, int slop)
{
if (field == "released_inthelast" && ParseStart(queryText, out DateTime start))
{
var todayString = DateTime.UtcNow.ToString("yyyyMMdd");
var dateString = start.ToString("yyyyMMdd");
return base.GetRangeQuery(LuceneSearchIndex.ReleaseDateField, dateString, todayString, true, true);
}
if (field == "released_notinthelast" && ParseStart(queryText, out DateTime finish))
{
var dateString = finish.ToString("yyyyMMdd");
return base.GetRangeQuery(LuceneSearchIndex.ReleaseDateField, "00000000", dateString, false, false);
}
if (field == "added_inthelast" && ParseStart(queryText, out DateTime addedStart))
{
var todayString = DateTime.UtcNow.ToString("yyyyMMdd");
var dateString = addedStart.ToString("yyyyMMdd");
return base.GetRangeQuery(LuceneSearchIndex.AddedDateField, dateString, todayString, true, true);
}
if (field == "added_notinthelast" && ParseStart(queryText, out DateTime addedFinish))
{
var dateString = addedFinish.ToString("yyyyMMdd");
return base.GetRangeQuery(LuceneSearchIndex.AddedDateField, "00000000", dateString, false, false);
}
return base.GetFieldQuery(field, queryText, slop);
}
protected override Query GetRangeQuery(
string field,
string part1,
string part2,
bool startInclusive,
bool endInclusive)
{
if (NumericFields.Contains(field))
{
if (part1 is null or "*" && int.TryParse(part2, out int max1))
{
return NumericRangeQuery.NewInt32Range(field, null, max1, startInclusive, endInclusive);
}
if (int.TryParse(part1, out int min))
{
if (part2 is null or "*")
{
return NumericRangeQuery.NewInt32Range(field, min, null, startInclusive, endInclusive);
}
if (int.TryParse(part2, out int max))
{
return NumericRangeQuery.NewInt32Range(field, min, max, startInclusive, endInclusive);
}
}
}
return base.GetRangeQuery(field, part1, part2, startInclusive, endInclusive);
}
internal static bool ParseStart(string text, out DateTime start)
{
start = SystemTime.MinValueUtc;
try
{
if (int.TryParse(text.Split(" ")[0], out int number))
{
if (text.Contains("day"))
{
start = DateTime.Today.AddDays(number * -1);
return true;
}
if (text.Contains("week"))
{
start = DateTime.Today.AddDays(number * -7);
return true;
}
if (text.Contains("month"))
{
start = DateTime.Today.AddMonths(number * -1);
return true;
}
if (text.Contains("year"))
{
start = DateTime.Today.AddYears(number * -1);
return true;
}
}
}
catch
{
// do nothing
}
return false;
}
}

12
ErsatzTV.Infrastructure/Search/ElasticSearchIndex.cs

@ -15,6 +15,7 @@ using ErsatzTV.Infrastructure.Search.Models;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using ExistsResponse = Elastic.Clients.Elasticsearch.IndexManagement.ExistsResponse; using ExistsResponse = Elastic.Clients.Elasticsearch.IndexManagement.ExistsResponse;
using MediaStream = ErsatzTV.Core.Domain.MediaStream; using MediaStream = ErsatzTV.Core.Domain.MediaStream;
using Query = Lucene.Net.Search.Query;
namespace ErsatzTV.Infrastructure.Search; namespace ErsatzTV.Infrastructure.Search;
@ -152,17 +153,20 @@ public class ElasticSearchIndex : ISearchIndex
return Unit.Default; return Unit.Default;
} }
public async Task<SearchResult> Search(IClient client, string query, int skip, int limit, string searchField = "") public async Task<SearchResult> Search(IClient client, string query, int skip, int limit)
{ {
var items = new List<MinimalElasticSearchItem>(); var items = new List<MinimalElasticSearchItem>();
var totalCount = 0; var totalCount = 0;
Query parsedQuery = LuceneSearchIndex.ParseQuery(query);
SearchResponse<MinimalElasticSearchItem> response = await _client.SearchAsync<MinimalElasticSearchItem>( SearchResponse<MinimalElasticSearchItem> response = await _client.SearchAsync<MinimalElasticSearchItem>(
s => s.Index(IndexName) s => s.Index(IndexName)
.Sort(ss => ss.Field(f => f.SortTitle, fs => fs.Order(SortOrder.Asc))) .Sort(ss => ss.Field(f => f.SortTitle, fs => fs.Order(SortOrder.Asc)))
.From(skip) .From(skip)
.Size(limit) .Size(limit)
.QueryLuceneSyntax(query)); .QueryLuceneSyntax(parsedQuery.ToString()));
if (response.IsValidResponse) if (response.IsValidResponse)
{ {
items.AddRange(response.Documents); items.AddRange(response.Documents);
@ -792,7 +796,7 @@ public class ElasticSearchIndex : ISearchIndex
return result; return result;
} }
private async Task<Option<SearchPageMap>> GetSearchPageMap(string query, int limit) private async Task<SearchPageMap> GetSearchPageMap(string query, int limit)
{ {
SearchResponse<MinimalElasticSearchItem> response = await _client.SearchAsync<MinimalElasticSearchItem>( SearchResponse<MinimalElasticSearchItem> response = await _client.SearchAsync<MinimalElasticSearchItem>(
s => s.Index(IndexName) s => s.Index(IndexName)
@ -803,7 +807,7 @@ public class ElasticSearchIndex : ISearchIndex
if (!response.IsValidResponse) if (!response.IsValidResponse)
{ {
return Option<SearchPageMap>.None; return null;
} }
var letters = new List<char> var letters = new List<char>

34
ErsatzTV.Infrastructure/Search/LuceneSearchIndex.cs

@ -189,14 +189,13 @@ public sealed class LuceneSearchIndex : ISearchIndex
return Task.FromResult(Unit.Default); return Task.FromResult(Unit.Default);
} }
public Task<SearchResult> Search(IClient client, string searchQuery, int skip, int limit, string searchField = "") public Task<SearchResult> Search(IClient client, string searchQuery, int skip, int limit)
{ {
var metadata = new Dictionary<string, string> var metadata = new Dictionary<string, string>
{ {
{ "searchQuery", searchQuery }, { "searchQuery", searchQuery },
{ "skip", skip.ToString() }, { "skip", skip.ToString() },
{ "limit", limit.ToString() }, { "limit", limit.ToString() }
{ "searchField", searchField }
}; };
client?.Breadcrumbs?.Leave("SearchIndex.Search", BreadcrumbType.State, metadata); client?.Breadcrumbs?.Leave("SearchIndex.Search", BreadcrumbType.State, metadata);
@ -210,18 +209,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
using DirectoryReader reader = _writer.GetReader(true); using DirectoryReader reader = _writer.GetReader(true);
var searcher = new IndexSearcher(reader); var searcher = new IndexSearcher(reader);
int hitsLimit = limit == 0 ? searcher.IndexReader.MaxDoc : skip + limit; int hitsLimit = limit == 0 ? searcher.IndexReader.MaxDoc : skip + limit;
using var analyzer = new StandardAnalyzer(AppLuceneVersion); Query query = ParseQuery(searchQuery);
var customAnalyzers = new Dictionary<string, Analyzer>
{
{ ContentRatingField, new KeywordAnalyzer() },
{ StateField, new KeywordAnalyzer() }
};
using var analyzerWrapper = new PerFieldAnalyzerWrapper(analyzer, customAnalyzers);
QueryParser parser = !string.IsNullOrWhiteSpace(searchField)
? new CustomQueryParser(AppLuceneVersion, searchField, analyzerWrapper)
: new CustomMultiFieldQueryParser(AppLuceneVersion, new[] { TitleField }, analyzerWrapper);
parser.AllowLeadingWildcard = true;
Query query = ParseQuery(searchQuery, parser);
// TODO: figure out if this is actually needed // TODO: figure out if this is actually needed
// var filter = new DuplicateFilter(TitleAndYearField); // var filter = new DuplicateFilter(TitleAndYearField);
var sort = new Sort(new SortField(SortTitleField, SortFieldType.STRING)); var sort = new Sort(new SortField(SortTitleField, SortFieldType.STRING));
@ -350,7 +338,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
} }
} }
private static Option<SearchPageMap> GetSearchPageMap( private static SearchPageMap GetSearchPageMap(
IndexSearcher searcher, IndexSearcher searcher,
Query query, Query query,
DuplicateFilter filter, DuplicateFilter filter,
@ -1145,6 +1133,20 @@ public sealed class LuceneSearchIndex : ISearchIndex
doc.Get(TypeField), doc.Get(TypeField),
Convert.ToInt32(doc.Get(IdField))); Convert.ToInt32(doc.Get(IdField)));
internal static Query ParseQuery(string query)
{
using var analyzer = new StandardAnalyzer(AppLuceneVersion);
var customAnalyzers = new Dictionary<string, Analyzer>
{
{ ContentRatingField, new KeywordAnalyzer() },
{ StateField, new KeywordAnalyzer() }
};
using var analyzerWrapper = new PerFieldAnalyzerWrapper(analyzer, customAnalyzers);
QueryParser parser = new CustomMultiFieldQueryParser(AppLuceneVersion, new[] { TitleField }, analyzerWrapper);
parser.AllowLeadingWildcard = true;
return ParseQuery(query, parser);
}
private static Query ParseQuery(string searchQuery, QueryParser parser) private static Query ParseQuery(string searchQuery, QueryParser parser)
{ {
Query query; Query query;

6
ErsatzTV/Pages/ArtistList.razor

@ -64,9 +64,9 @@
</FragmentLetterAnchor> </FragmentLetterAnchor>
</MudContainer> </MudContainer>
</MudContainer> </MudContainer>
@if (_data.PageMap.IsSome) @if (_data.PageMap is not null)
{ {
<LetterBar PageMap="@_data.PageMap.ValueUnsafe()" <LetterBar PageMap="@_data.PageMap"
BaseUri="media/music/artists" BaseUri="media/music/artists"
Query="@_query"/> Query="@_query"/>
} }
@ -77,7 +77,7 @@
[Parameter] [Parameter]
public int PageNumber { get; set; } public int PageNumber { get; set; }
private ArtistCardResultsViewModel _data = new(0, new List<ArtistCardViewModel>(), None); private ArtistCardResultsViewModel _data = new(0, new List<ArtistCardViewModel>(), null);
private string _query; private string _query;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()

7
ErsatzTV/Pages/ChannelEditor.razor

@ -7,7 +7,6 @@
@using ErsatzTV.Application.MediaItems @using ErsatzTV.Application.MediaItems
@using ErsatzTV.Application.Templates @using ErsatzTV.Application.Templates
@using ErsatzTV.Application.Watermarks @using ErsatzTV.Application.Watermarks
@using System.Globalization
@using ErsatzTV.Core.Domain.Filler @using ErsatzTV.Core.Domain.Filler
@using ErsatzTV.Application.Channels @using ErsatzTV.Application.Channels
@implements IDisposable @implements IDisposable
@ -50,7 +49,7 @@
For="@(() => _model.PreferredAudioLanguageCode)" For="@(() => _model.PreferredAudioLanguageCode)"
Clearable="true"> Clearable="true">
<MudSelectItem Value="@((string)null)">(none)</MudSelectItem> <MudSelectItem Value="@((string)null)">(none)</MudSelectItem>
@foreach (CultureInfo culture in _availableCultures) @foreach (LanguageCodeViewModel culture in _availableCultures)
{ {
<MudSelectItem Value="@culture.ThreeLetterISOLanguageName">@culture.EnglishName</MudSelectItem> <MudSelectItem Value="@culture.ThreeLetterISOLanguageName">@culture.EnglishName</MudSelectItem>
} }
@ -62,7 +61,7 @@
For="@(() => _model.PreferredSubtitleLanguageCode)" For="@(() => _model.PreferredSubtitleLanguageCode)"
Clearable="true"> Clearable="true">
<MudSelectItem Value="@((string)null)">(none)</MudSelectItem> <MudSelectItem Value="@((string)null)">(none)</MudSelectItem>
@foreach (CultureInfo culture in _availableCultures) @foreach (LanguageCodeViewModel culture in _availableCultures)
{ {
<MudSelectItem Value="@culture.ThreeLetterISOLanguageName">@culture.EnglishName</MudSelectItem> <MudSelectItem Value="@culture.ThreeLetterISOLanguageName">@culture.EnglishName</MudSelectItem>
} }
@ -148,7 +147,7 @@
private ValidationMessageStore _messageStore; private ValidationMessageStore _messageStore;
private List<FFmpegProfileViewModel> _ffmpegProfiles; private List<FFmpegProfileViewModel> _ffmpegProfiles;
private List<CultureInfo> _availableCultures; private List<LanguageCodeViewModel> _availableCultures;
private List<WatermarkViewModel> _watermarks; private List<WatermarkViewModel> _watermarks;
private List<FillerPresetViewModel> _fillerPresets; private List<FillerPresetViewModel> _fillerPresets;
private List<string> _musicVideoCreditsTemplates; private List<string> _musicVideoCreditsTemplates;

6
ErsatzTV/Pages/EpisodeList.razor

@ -64,9 +64,9 @@
</FragmentLetterAnchor> </FragmentLetterAnchor>
</MudContainer> </MudContainer>
</MudContainer> </MudContainer>
@if (_data.PageMap.IsSome) @if (_data.PageMap is not null)
{ {
<LetterBar PageMap="@_data.PageMap.ValueUnsafe()" <LetterBar PageMap="@_data.PageMap"
BaseUri="media/tv/episodes" BaseUri="media/tv/episodes"
Query="@_query"/> Query="@_query"/>
} }
@ -77,7 +77,7 @@
[Parameter] [Parameter]
public int PageNumber { get; set; } public int PageNumber { get; set; }
private TelevisionEpisodeCardResultsViewModel _data = new(0, new List<TelevisionEpisodeCardViewModel>(), None); private TelevisionEpisodeCardResultsViewModel _data = new(0, new List<TelevisionEpisodeCardViewModel>(), null);
private string _query; private string _query;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()

86
ErsatzTV/Pages/Movie.razor

@ -5,12 +5,14 @@
@using ErsatzTV.Application.MediaCollections @using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.MediaItems @using ErsatzTV.Application.MediaItems
@using ErsatzTV.Application.MediaCards @using ErsatzTV.Application.MediaCards
@using LanguageExt.UnsafeValueAccess
@implements IDisposable @implements IDisposable
@inject IMediator _mediator @inject IMediator Mediator
@inject IDialogService _dialog @inject IDialogService Dialog
@inject NavigationManager _navigationManager @inject NavigationManager NavigationManager
@inject ILogger<Movie> _logger @inject ILogger<Movie> Logger
@inject ISnackbar _snackbar @inject ISnackbar Snackbar
@inject PersistentComponentState ApplicationState
<MudContainer MaxWidth="MaxWidth.False" Style="padding: 0" Class="fanart-container"> <MudContainer MaxWidth="MaxWidth.False" Style="padding: 0" Class="fanart-container">
<div class="fanart-tint"></div> <div class="fanart-tint"></div>
@ -129,11 +131,11 @@
{ {
<div style="display: flex; flex-direction: row; flex-wrap: wrap"> <div style="display: flex; flex-direction: row; flex-wrap: wrap">
<MudText GutterBottom="true">Languages:&nbsp;</MudText> <MudText GutterBottom="true">Languages:&nbsp;</MudText>
<MudLink Href="@(@$"language:""{_sortedLanguages.Head().EnglishName.ToLowerInvariant()}""".GetRelativeSearchQuery())">@_sortedLanguages.Head().EnglishName</MudLink> <MudLink Href="@(@$"language:""{_sortedLanguages.Head().ToLowerInvariant()}""".GetRelativeSearchQuery())">@_sortedLanguages.Head()</MudLink>
@foreach (CultureInfo language in _sortedLanguages.Skip(1)) @foreach (string language in _sortedLanguages.Skip(1))
{ {
<MudText>,&nbsp;</MudText> <MudText>,&nbsp;</MudText>
<MudLink Href="@(@$"language:""{language.EnglishName.ToLowerInvariant()}""".GetRelativeSearchQuery())">@language.EnglishName</MudLink> <MudLink Href="@(@$"language:""{language.ToLowerInvariant()}""".GetRelativeSearchQuery())">@language</MudLink>
} }
</div> </div>
} }
@ -217,13 +219,14 @@
@code { @code {
private readonly CancellationTokenSource _cts = new(); private readonly CancellationTokenSource _cts = new();
private PersistingComponentStateSubscription _persistingSubscription;
[Parameter] [Parameter]
public int MovieId { get; set; } public int MovieId { get; set; }
private MovieViewModel _movie; private MovieViewModel _movie;
private List<string> _sortedContentRatings = new(); private List<string> _sortedContentRatings = new();
private List<CultureInfo> _sortedLanguages = new(); private List<string> _sortedLanguages = new();
private List<string> _sortedDirectors = new(); private List<string> _sortedDirectors = new();
private List<string> _sortedWriters = new(); private List<string> _sortedWriters = new();
private List<string> _sortedStudios = new(); private List<string> _sortedStudios = new();
@ -232,54 +235,81 @@
public void Dispose() public void Dispose()
{ {
_persistingSubscription.Dispose();
_cts.Cancel(); _cts.Cancel();
_cts.Dispose(); _cts.Dispose();
} }
protected override Task OnParametersSetAsync() => RefreshData(); protected override Task OnInitializedAsync()
{
_persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData);
private async Task RefreshData() => return base.OnInitializedAsync();
await _mediator.Send(new GetMovieById(MovieId), _cts.Token).IfSomeAsync(vm => }
protected override async Task OnParametersSetAsync()
{
if (!ApplicationState.TryTakeFromJson("_movie", out MovieViewModel restored))
{ {
_movie = vm; _movie = await RefreshData();
_sortedContentRatings = _movie.ContentRatings.OrderBy(cr => cr).ToList(); }
_sortedLanguages = _movie.Languages.OrderBy(ci => ci.EnglishName).ToList(); else
_sortedStudios = _movie.Studios.OrderBy(s => s).ToList(); {
_sortedGenres = _movie.Genres.OrderBy(g => g).ToList(); _movie = restored;
_sortedTags = _movie.Tags.OrderBy(t => t).ToList(); }
_sortedDirectors = _movie.Directors.OrderBy(d => d).ToList();
_sortedWriters = _movie.Writers.OrderBy(w => w).ToList(); _sortedContentRatings = _movie?.ContentRatings.OrderBy(cr => cr).ToList();
}); _sortedLanguages = _movie?.Languages.OrderBy(l => l).ToList();
_sortedStudios = _movie?.Studios.OrderBy(s => s).ToList();
_sortedGenres = _movie?.Genres.OrderBy(g => g).ToList();
_sortedTags = _movie?.Tags.OrderBy(t => t).ToList();
_sortedDirectors = _movie?.Directors.OrderBy(d => d).ToList();
_sortedWriters = _movie?.Writers.OrderBy(w => w).ToList();
}
private Task PersistData()
{
ApplicationState.PersistAsJson("_movie", _movie);
return Task.CompletedTask;
}
private async Task<MovieViewModel> RefreshData()
{
Option<MovieViewModel> vm = await Mediator.Send(new GetMovieById(MovieId), _cts.Token);
return vm.IsSome ? vm.ValueUnsafe() : null;
}
private async Task AddToCollection() private async Task AddToCollection()
{ {
var parameters = new DialogParameters { { "EntityType", "movie" }, { "EntityName", _movie.Title } }; var parameters = new DialogParameters { { "EntityType", "movie" }, { "EntityName", _movie.Title } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
IDialogReference dialog = await _dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options); IDialogReference dialog = await Dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options);
DialogResult result = await dialog.Result; DialogResult result = await dialog.Result;
if (!result.Canceled && result.Data is MediaCollectionViewModel collection) if (!result.Canceled && result.Data is MediaCollectionViewModel collection)
{ {
await _mediator.Send(new AddMovieToCollection(collection.Id, MovieId), _cts.Token); await Mediator.Send(new AddMovieToCollection(collection.Id, MovieId), _cts.Token);
_navigationManager.NavigateTo($"media/collections/{collection.Id}"); NavigationManager.NavigateTo($"media/collections/{collection.Id}");
} }
} }
private async Task ShowInfo() private async Task ShowInfo()
{ {
Either<BaseError, MediaItemInfo> maybeInfo = await _mediator.Send(new GetMediaItemInfo(MovieId)); Either<BaseError, MediaItemInfo> maybeInfo = await Mediator.Send(new GetMediaItemInfo(MovieId));
foreach (BaseError error in maybeInfo.LeftToSeq()) foreach (BaseError error in maybeInfo.LeftToSeq())
{ {
_snackbar.Add("Unexpected error loading media info"); Snackbar.Add("Unexpected error loading media info");
_logger.LogError("Unexpected error loading media info: {Error}", error.Value); Logger.LogError("Unexpected error loading media info: {Error}", error.Value);
} }
foreach (MediaItemInfo info in maybeInfo.RightToSeq()) foreach (MediaItemInfo info in maybeInfo.RightToSeq())
{ {
var parameters = new DialogParameters { { "MediaItemInfo", info } }; var parameters = new DialogParameters { { "MediaItemInfo", info } };
var options = new DialogOptions { CloseButton = true, CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true }; var options = new DialogOptions { CloseButton = true, CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
IDialogReference dialog = await _dialog.ShowAsync<MediaItemInfoDialog>(_movie.Title, parameters, options); IDialogReference dialog = await Dialog.ShowAsync<MediaItemInfoDialog>(_movie.Title, parameters, options);
DialogResult _ = await dialog.Result; DialogResult _ = await dialog.Result;
} }
} }

44
ErsatzTV/Pages/MovieList.razor

@ -1,12 +1,13 @@
@page "/media/movies" @page "/media/movies"
@page "/media/movies/page/{PageNumber:int}" @page "/media/movies/page/{PageNumber:int}"
@using ErsatzTV.Extensions @using ErsatzTV.Extensions
@using LanguageExt.UnsafeValueAccess
@using ErsatzTV.Application.MediaCards @using ErsatzTV.Application.MediaCards
@using ErsatzTV.Application.MediaCollections @using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.Search @using ErsatzTV.Application.Search
@inherits MultiSelectBase<MovieList> @inherits MultiSelectBase<MovieList>
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject PersistentComponentState ApplicationState
@implements IDisposable
<MudPaper Square="true" Style="display: flex; height: 64px; left: 240px; padding: 0; position: fixed; right: 0; z-index: 100;"> <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"> <div style="display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%" class="ml-6 mr-6">
@ -62,9 +63,9 @@
</FragmentLetterAnchor> </FragmentLetterAnchor>
</MudContainer> </MudContainer>
</MudContainer> </MudContainer>
@if (_data.PageMap.IsSome) @if (_data.PageMap is not null)
{ {
<LetterBar PageMap="@_data.PageMap.ValueUnsafe()" <LetterBar PageMap="@_data.PageMap"
BaseUri="media/movies" BaseUri="media/movies"
Query="@_query"/> Query="@_query"/>
} }
@ -75,8 +76,17 @@
[Parameter] [Parameter]
public int PageNumber { get; set; } public int PageNumber { get; set; }
private MovieCardResultsViewModel _data = new(0, new List<MovieCardViewModel>(), None); private MovieCardResultsViewModel _data = new(0, new List<MovieCardViewModel>(), null);
private string _query; private string _query;
private PersistingComponentStateSubscription _persistingSubscription;
protected override Task OnInitializedAsync()
{
_persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData);
_query = NavigationManager.Uri.GetSearchQuery();
return base.OnInitializedAsync();
}
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
@ -85,15 +95,33 @@
PageNumber = 1; PageNumber = 1;
} }
_query = NavigationManager.Uri.GetSearchQuery(); if (!ApplicationState.TryTakeFromJson("_data", out MovieCardResultsViewModel restored))
{
_data = await RefreshData();
}
else
{
_data = restored;
}
}
private Task PersistData()
{
ApplicationState.PersistAsJson("_data", _data);
return Task.CompletedTask;
}
await RefreshData(); void IDisposable.Dispose()
{
_persistingSubscription.Dispose();
base.Dispose();
} }
protected override async Task RefreshData() protected override async Task<MovieCardResultsViewModel> RefreshData()
{ {
string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:movie" : $"type:movie AND ({_query})"; string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:movie" : $"type:movie AND ({_query})";
_data = await Mediator.Send(new QuerySearchIndexMovies(searchQuery, PageNumber, PageSize), CancellationToken); return await Mediator.Send(new QuerySearchIndexMovies(searchQuery, PageNumber, PageSize), CancellationToken);
} }
private void PrevPage() private void PrevPage()

6
ErsatzTV/Pages/MusicVideoList.razor

@ -64,9 +64,9 @@
</FragmentLetterAnchor> </FragmentLetterAnchor>
</MudContainer> </MudContainer>
</MudContainer> </MudContainer>
@if (_data.PageMap.IsSome) @if (_data.PageMap is not null)
{ {
<LetterBar PageMap="@_data.PageMap.ValueUnsafe()" <LetterBar PageMap="@_data.PageMap"
BaseUri="media/music/videos" BaseUri="media/music/videos"
Query="@_query"/> Query="@_query"/>
} }
@ -77,7 +77,7 @@
[Parameter] [Parameter]
public int PageNumber { get; set; } public int PageNumber { get; set; }
private MusicVideoCardResultsViewModel _data = new(0, new List<MusicVideoCardViewModel>(), None); private MusicVideoCardResultsViewModel _data = new(0, new List<MusicVideoCardViewModel>(), null);
private string _query; private string _query;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()

6
ErsatzTV/Pages/OtherVideoList.razor

@ -64,9 +64,9 @@
</FragmentLetterAnchor> </FragmentLetterAnchor>
</MudContainer> </MudContainer>
</MudContainer> </MudContainer>
@if (_data.PageMap.IsSome) @if (_data.PageMap is not null)
{ {
<LetterBar PageMap="@_data.PageMap.ValueUnsafe()" <LetterBar PageMap="@_data.PageMap"
BaseUri="media/other/videos" BaseUri="media/other/videos"
Query="@_query"/> Query="@_query"/>
} }
@ -77,7 +77,7 @@
[Parameter] [Parameter]
public int PageNumber { get; set; } public int PageNumber { get; set; }
private OtherVideoCardResultsViewModel _data = new(0, new List<OtherVideoCardViewModel>(), None); private OtherVideoCardResultsViewModel _data = new(0, new List<OtherVideoCardViewModel>(), null);
private string _query; private string _query;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()

13
ErsatzTV/Pages/ScheduleItemsEditor.razor

@ -5,7 +5,6 @@
@using ErsatzTV.Application.ProgramSchedules @using ErsatzTV.Application.ProgramSchedules
@using ErsatzTV.Application.Search @using ErsatzTV.Application.Search
@using ErsatzTV.Application.Watermarks @using ErsatzTV.Application.Watermarks
@using System.Globalization
@using ErsatzTV.Core.Domain.Filler @using ErsatzTV.Core.Domain.Filler
@implements IDisposable @implements IDisposable
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@ -305,7 +304,7 @@
For="@(() => _selectedItem.PreferredAudioLanguageCode)" For="@(() => _selectedItem.PreferredAudioLanguageCode)"
Clearable="true"> Clearable="true">
<MudSelectItem Value="@((string)null)">(none)</MudSelectItem> <MudSelectItem Value="@((string)null)">(none)</MudSelectItem>
@foreach (CultureInfo culture in _availableCultures) @foreach (LanguageCodeViewModel culture in _availableCultures)
{ {
<MudSelectItem Value="@culture.ThreeLetterISOLanguageName">@culture.EnglishName</MudSelectItem> <MudSelectItem Value="@culture.ThreeLetterISOLanguageName">@culture.EnglishName</MudSelectItem>
} }
@ -317,7 +316,7 @@
For="@(() => _selectedItem.PreferredSubtitleLanguageCode)" For="@(() => _selectedItem.PreferredSubtitleLanguageCode)"
Clearable="true"> Clearable="true">
<MudSelectItem Value="@((string)null)">(none)</MudSelectItem> <MudSelectItem Value="@((string)null)">(none)</MudSelectItem>
@foreach (CultureInfo culture in _availableCultures) @foreach (LanguageCodeViewModel culture in _availableCultures)
{ {
<MudSelectItem Value="@culture.ThreeLetterISOLanguageName">@culture.EnglishName</MudSelectItem> <MudSelectItem Value="@culture.ThreeLetterISOLanguageName">@culture.EnglishName</MudSelectItem>
} }
@ -345,7 +344,7 @@
private ProgramScheduleItemsEditViewModel _schedule; private ProgramScheduleItemsEditViewModel _schedule;
private List<FillerPresetViewModel> _fillerPresets; private List<FillerPresetViewModel> _fillerPresets;
private List<WatermarkViewModel> _watermarks; private List<WatermarkViewModel> _watermarks;
private List<CultureInfo> _availableCultures; private List<LanguageCodeViewModel> _availableCultures;
private ProgramScheduleItemEditViewModel _selectedItem; private ProgramScheduleItemEditViewModel _selectedItem;
@ -355,7 +354,11 @@
_cts.Dispose(); _cts.Dispose();
} }
protected override async Task OnParametersSetAsync() => await LoadScheduleItems(); protected override async Task OnParametersSetAsync()
{
Logger.LogInformation("Loading schedule items");
await LoadScheduleItems();
}
private async Task LoadScheduleItems() private async Task LoadScheduleItems()
{ {

104
ErsatzTV/Pages/Search.razor

@ -6,6 +6,8 @@
@inherits MultiSelectBase<Search> @inherits MultiSelectBase<Search>
@inject NavigationManager _navigationManager @inject NavigationManager _navigationManager
@inject ChannelWriter<IBackgroundServiceRequest> _channel @inject ChannelWriter<IBackgroundServiceRequest> _channel
@inject PersistentComponentState ApplicationState
@implements IDisposable
<MudPaper Square="true" Style="display: flex; height: 64px; left: 240px; padding: 0; position: fixed; right: 0; z-index: 100;"> <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"> <div style="display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%" class="ml-6 mr-6">
@ -324,23 +326,109 @@
private OtherVideoCardResultsViewModel _otherVideos; private OtherVideoCardResultsViewModel _otherVideos;
private SongCardResultsViewModel _songs; private SongCardResultsViewModel _songs;
private ArtistCardResultsViewModel _artists; private ArtistCardResultsViewModel _artists;
private PersistingComponentStateSubscription _persistingSubscription;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
_persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData);
_query = _navigationManager.Uri.GetSearchQuery(); _query = _navigationManager.Uri.GetSearchQuery();
if (!string.IsNullOrWhiteSpace(_query)) if (!string.IsNullOrWhiteSpace(_query))
{ {
_movies = await Mediator.Send(new QuerySearchIndexMovies($"type:movie AND ({_query})", 1, 50), CancellationToken); if (!ApplicationState.TryTakeFromJson("_movies", out MovieCardResultsViewModel restoredMovies))
_shows = await Mediator.Send(new QuerySearchIndexShows($"type:show AND ({_query})", 1, 50), CancellationToken); {
_seasons = await Mediator.Send(new QuerySearchIndexSeasons($"type:season AND ({_query})", 1, 50), CancellationToken); _movies = await Mediator.Send(new QuerySearchIndexMovies($"type:movie AND ({_query})", 1, 50), CancellationToken);
_episodes = await Mediator.Send(new QuerySearchIndexEpisodes($"type:episode AND ({_query})", 1, 50), CancellationToken); }
_musicVideos = await Mediator.Send(new QuerySearchIndexMusicVideos($"type:music_video AND ({_query})", 1, 50), CancellationToken); else
_otherVideos = await Mediator.Send(new QuerySearchIndexOtherVideos($"type:other_video AND ({_query})", 1, 50), CancellationToken); {
_songs = await Mediator.Send(new QuerySearchIndexSongs($"type:song AND ({_query})", 1, 50), CancellationToken); _movies = restoredMovies;
_artists = await Mediator.Send(new QuerySearchIndexArtists($"type:artist AND ({_query})", 1, 50), CancellationToken); }
if (!ApplicationState.TryTakeFromJson("_shows", out TelevisionShowCardResultsViewModel restoredShows))
{
_shows = await Mediator.Send(new QuerySearchIndexShows($"type:show AND ({_query})", 1, 50), CancellationToken);
}
else
{
_shows = restoredShows;
}
if (!ApplicationState.TryTakeFromJson("_seasons", out TelevisionSeasonCardResultsViewModel restoredSeasons))
{
_seasons = await Mediator.Send(new QuerySearchIndexSeasons($"type:season AND ({_query})", 1, 50), CancellationToken);
}
else
{
_seasons = restoredSeasons;
}
if (!ApplicationState.TryTakeFromJson("_episodes", out TelevisionEpisodeCardResultsViewModel restoredEpisodes))
{
_episodes = await Mediator.Send(new QuerySearchIndexEpisodes($"type:episode AND ({_query})", 1, 50), CancellationToken);
}
else
{
_episodes = restoredEpisodes;
}
if (!ApplicationState.TryTakeFromJson("_musicVideos", out MusicVideoCardResultsViewModel restoredMusicVideos))
{
_musicVideos = await Mediator.Send(new QuerySearchIndexMusicVideos($"type:music_video AND ({_query})", 1, 50), CancellationToken);
}
else
{
_musicVideos = restoredMusicVideos;
}
if (!ApplicationState.TryTakeFromJson("_otherVideos", out OtherVideoCardResultsViewModel restoredOtherVideos))
{
_otherVideos = await Mediator.Send(new QuerySearchIndexOtherVideos($"type:other_video AND ({_query})", 1, 50), CancellationToken);
}
else
{
_otherVideos = restoredOtherVideos;
}
if (!ApplicationState.TryTakeFromJson("_songs", out SongCardResultsViewModel restoredSongs))
{
_songs = await Mediator.Send(new QuerySearchIndexSongs($"type:song AND ({_query})", 1, 50), CancellationToken);
}
else
{
_songs = restoredSongs;
}
if (!ApplicationState.TryTakeFromJson("_artists", out ArtistCardResultsViewModel restoredArtists))
{
_artists = await Mediator.Send(new QuerySearchIndexArtists($"type:artist AND ({_query})", 1, 50), CancellationToken);
}
else
{
_artists = restoredArtists;
}
} }
} }
private Task PersistData()
{
ApplicationState.PersistAsJson("_movies", _movies);
ApplicationState.PersistAsJson("_shows", _shows);
ApplicationState.PersistAsJson("_seasons", _seasons);
ApplicationState.PersistAsJson("_episodes", _episodes);
ApplicationState.PersistAsJson("_musicVideos", _musicVideos);
ApplicationState.PersistAsJson("_otherVideos", _otherVideos);
ApplicationState.PersistAsJson("_songs", _songs);
ApplicationState.PersistAsJson("_artists", _artists);
return Task.CompletedTask;
}
void IDisposable.Dispose()
{
_persistingSubscription.Dispose();
base.Dispose();
}
private void SelectClicked(MediaCardViewModel card, MouseEventArgs e) private void SelectClicked(MediaCardViewModel card, MouseEventArgs e)
{ {
List<MediaCardViewModel> GetSortedItems() List<MediaCardViewModel> GetSortedItems()

4
ErsatzTV/Pages/Settings.razor

@ -39,7 +39,7 @@
</MudSelect> </MudSelect>
</MudElement> </MudElement>
<MudSelect Class="mt-3" Label="Preferred Audio Language" @bind-Value="_ffmpegSettings.PreferredAudioLanguageCode" For="@(() => _ffmpegSettings.PreferredAudioLanguageCode)" Required="true" RequiredError="Preferred Language Code is required!"> <MudSelect Class="mt-3" Label="Preferred Audio Language" @bind-Value="_ffmpegSettings.PreferredAudioLanguageCode" For="@(() => _ffmpegSettings.PreferredAudioLanguageCode)" Required="true" RequiredError="Preferred Language Code is required!">
@foreach (CultureInfo culture in _availableCultures) @foreach (LanguageCodeViewModel culture in _availableCultures)
{ {
<MudSelectItem Value="@culture.ThreeLetterISOLanguageName">@culture.EnglishName</MudSelectItem> <MudSelectItem Value="@culture.ThreeLetterISOLanguageName">@culture.EnglishName</MudSelectItem>
} }
@ -254,7 +254,7 @@
private bool _playoutSuccess; private bool _playoutSuccess;
private List<FFmpegProfileViewModel> _ffmpegProfiles; private List<FFmpegProfileViewModel> _ffmpegProfiles;
private FFmpegSettingsViewModel _ffmpegSettings; private FFmpegSettingsViewModel _ffmpegSettings;
private List<CultureInfo> _availableCultures; private List<LanguageCodeViewModel> _availableCultures;
private List<WatermarkViewModel> _watermarks; private List<WatermarkViewModel> _watermarks;
private List<FillerPresetViewModel> _fillerPresets; private List<FillerPresetViewModel> _fillerPresets;
private List<ResolutionViewModel> _customResolutions; private List<ResolutionViewModel> _customResolutions;

6
ErsatzTV/Pages/SongList.razor

@ -64,9 +64,9 @@
</FragmentLetterAnchor> </FragmentLetterAnchor>
</MudContainer> </MudContainer>
</MudContainer> </MudContainer>
@if (_data.PageMap.IsSome) @if (_data.PageMap is not null)
{ {
<LetterBar PageMap="@_data.PageMap.ValueUnsafe()" <LetterBar PageMap="@_data.PageMap"
BaseUri="media/music/songs" BaseUri="media/music/songs"
Query="@_query"/> Query="@_query"/>
} }
@ -77,7 +77,7 @@
[Parameter] [Parameter]
public int PageNumber { get; set; } public int PageNumber { get; set; }
private SongCardResultsViewModel _data = new(0, new List<SongCardViewModel>(), None); private SongCardResultsViewModel _data = new(0, new List<SongCardViewModel>(), null);
private string _query; private string _query;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()

6
ErsatzTV/Pages/TelevisionSeasonSearchResults.razor

@ -63,9 +63,9 @@
</FragmentLetterAnchor> </FragmentLetterAnchor>
</MudContainer> </MudContainer>
</MudContainer> </MudContainer>
@if (_data.PageMap.IsSome) @if (_data.PageMap is not null)
{ {
<LetterBar PageMap="@_data.PageMap.ValueUnsafe()" <LetterBar PageMap="@_data.PageMap"
BaseUri="media/tv/seasons" BaseUri="media/tv/seasons"
Query="@_query"/> Query="@_query"/>
} }
@ -76,7 +76,7 @@
[Parameter] [Parameter]
public int PageNumber { get; set; } public int PageNumber { get; set; }
private TelevisionSeasonCardResultsViewModel _data = new(0, new List<TelevisionSeasonCardViewModel>(), None); private TelevisionSeasonCardResultsViewModel _data = new(0, new List<TelevisionSeasonCardViewModel>(), null);
private string _query; private string _query;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()

51
ErsatzTV/Pages/TelevisionShowList.razor

@ -1,13 +1,13 @@
@page "/media/tv/shows" @page "/media/tv/shows"
@page "/media/tv/shows/page/{PageNumber:int}" @page "/media/tv/shows/page/{PageNumber:int}"
@using ErsatzTV.Extensions @using ErsatzTV.Extensions
@using LanguageExt.UnsafeValueAccess
@using ErsatzTV.Application.MediaCards @using ErsatzTV.Application.MediaCards
@using ErsatzTV.Application.MediaCollections @using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.Search @using ErsatzTV.Application.Search
@inherits MultiSelectBase<TelevisionShowList> @inherits MultiSelectBase<TelevisionShowList>
@inject NavigationManager _navigationManager @inject NavigationManager NavigationManager
@inject ChannelWriter<IBackgroundServiceRequest> _channel @inject PersistentComponentState ApplicationState
@implements IDisposable
<MudPaper Square="true" Style="display: flex; height: 64px; left: 240px; padding: 0; position: fixed; right: 0; z-index: 100;"> <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"> <div style="display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%" class="ml-6 mr-6">
@ -63,9 +63,9 @@
</FragmentLetterAnchor> </FragmentLetterAnchor>
</MudContainer> </MudContainer>
</MudContainer> </MudContainer>
@if (_data.PageMap.IsSome) @if (_data.PageMap is not null)
{ {
<LetterBar PageMap="@_data.PageMap.ValueUnsafe()" <LetterBar PageMap="@_data.PageMap"
BaseUri="media/tv/shows" BaseUri="media/tv/shows"
Query="@_query"/> Query="@_query"/>
} }
@ -76,8 +76,17 @@
[Parameter] [Parameter]
public int PageNumber { get; set; } public int PageNumber { get; set; }
private TelevisionShowCardResultsViewModel _data = new(0, new List<TelevisionShowCardViewModel>(), None); private TelevisionShowCardResultsViewModel _data = new(0, new List<TelevisionShowCardViewModel>(), null);
private string _query; private string _query;
private PersistingComponentStateSubscription _persistingSubscription;
protected override Task OnInitializedAsync()
{
_persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData);
_query = NavigationManager.Uri.GetSearchQuery();
return base.OnInitializedAsync();
}
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
@ -86,15 +95,33 @@
PageNumber = 1; PageNumber = 1;
} }
_query = _navigationManager.Uri.GetSearchQuery(); if (!ApplicationState.TryTakeFromJson("_data", out TelevisionShowCardResultsViewModel restored))
{
_data = await RefreshData();
}
else
{
_data = restored;
}
}
private Task PersistData()
{
ApplicationState.PersistAsJson("_data", _data);
await RefreshData(); return Task.CompletedTask;
}
void IDisposable.Dispose()
{
_persistingSubscription.Dispose();
base.Dispose();
} }
protected override async Task RefreshData() protected override async Task<TelevisionShowCardResultsViewModel> RefreshData()
{ {
string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:show" : $"type:show AND ({_query})"; string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:show" : $"type:show AND ({_query})";
_data = await Mediator.Send(new QuerySearchIndexShows(searchQuery, PageNumber, PageSize), CancellationToken); return await Mediator.Send(new QuerySearchIndexShows(searchQuery, PageNumber, PageSize), CancellationToken);
} }
private void PrevPage() private void PrevPage()
@ -105,7 +132,7 @@
(string key, string value) = _query.EncodeQuery(); (string key, string value) = _query.EncodeQuery();
uri = $"{uri}?{key}={value}"; uri = $"{uri}?{key}={value}";
} }
_navigationManager.NavigateTo(uri); NavigationManager.NavigateTo(uri);
} }
private void NextPage() private void NextPage()
@ -116,7 +143,7 @@
(string key, string value) = _query.EncodeQuery(); (string key, string value) = _query.EncodeQuery();
uri = $"{uri}?{key}={value}"; uri = $"{uri}?{key}={value}";
} }
_navigationManager.NavigateTo(uri); NavigationManager.NavigateTo(uri);
} }
private void SelectClicked(MediaCardViewModel card, MouseEventArgs e) private void SelectClicked(MediaCardViewModel card, MouseEventArgs e)

1
ErsatzTV/Pages/_Host.cshtml

@ -87,5 +87,6 @@
} }
}; };
</script> </script>
<persist-component-state />
</body> </body>
</html> </html>

7
ErsatzTV/Startup.cs

@ -295,7 +295,8 @@ public class Startup
} }
}); });
services.AddServerSideBlazor(); services.AddServerSideBlazor()
.AddHubOptions(hubOptions => hubOptions.MaximumReceiveMessageSize = 1024 * 1024);
services.AddMudServices(); services.AddMudServices();
@ -520,12 +521,16 @@ public class Startup
if (SearchHelper.IsElasticSearchEnabled) if (SearchHelper.IsElasticSearchEnabled)
{ {
Log.Logger.Information("Using Elasticsearch (external) search index backend");
ElasticSearchIndex.Uri = new Uri(SearchHelper.ElasticSearchUri); ElasticSearchIndex.Uri = new Uri(SearchHelper.ElasticSearchUri);
ElasticSearchIndex.IndexName = SearchHelper.ElasticSearchIndexName; ElasticSearchIndex.IndexName = SearchHelper.ElasticSearchIndexName;
services.AddSingleton<ISearchIndex, ElasticSearchIndex>(); services.AddSingleton<ISearchIndex, ElasticSearchIndex>();
} }
else else
{ {
Log.Logger.Information("Using Lucene (embedded) search index backend");
services.AddSingleton<ISearchIndex, LuceneSearchIndex>(); services.AddSingleton<ISearchIndex, LuceneSearchIndex>();
} }

Loading…
Cancel
Save