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

2
ErsatzTV.Application/MediaCards/MovieCardResultsViewModel.cs

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

2
ErsatzTV.Application/MediaCards/OtherVideoCardResultsViewModel.cs

@ -5,4 +5,4 @@ namespace ErsatzTV.Application.MediaCards; @@ -5,4 +5,4 @@ namespace ErsatzTV.Application.MediaCards;
public record OtherVideoCardResultsViewModel(
int Count,
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 @@ -49,6 +49,6 @@ public class GetMusicVideoCardsHandler : IRequestHandler<GetMusicVideoCards, Mus
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 @@ -59,6 +59,6 @@ public class
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 @@ -35,6 +35,6 @@ public class
.GetPagedSeasons(request.TelevisionShowId, request.PageNumber, request.PageSize)
.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; @@ -5,4 +5,4 @@ namespace ErsatzTV.Application.MediaCards;
public record SongCardResultsViewModel(
int Count,
List<SongCardViewModel> Cards,
Option<SearchPageMap> PageMap);
SearchPageMap PageMap);

2
ErsatzTV.Application/MediaCards/TelevisionEpisodeCardResultsViewModel.cs

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

2
ErsatzTV.Application/MediaCards/TelevisionSeasonCardResultsViewModel.cs

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

2
ErsatzTV.Application/MediaCards/TelevisionShowCardResultsViewModel.cs

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

4
ErsatzTV.Application/MediaItems/LanguageCodeViewModel.cs

@ -0,0 +1,4 @@ @@ -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 @@ @@ -1,5 +1,3 @@
using System.Globalization;
namespace ErsatzTV.Application.MediaItems;
namespace ErsatzTV.Application.MediaItems;
public record GetAllLanguageCodes : IRequest<List<CultureInfo>>;
public record GetAllLanguageCodes : IRequest<List<LanguageCodeViewModel>>;

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

@ -3,13 +3,16 @@ using ErsatzTV.Core.Interfaces.Repositories; @@ -3,13 +3,16 @@ using ErsatzTV.Core.Interfaces.Repositories;
namespace ErsatzTV.Application.MediaItems;
public class GetAllLanguageCodesHandler : IRequestHandler<GetAllLanguageCodes, List<CultureInfo>>
public class GetAllLanguageCodesHandler : IRequestHandler<GetAllLanguageCodes, List<LanguageCodeViewModel>>
{
private readonly IMediaItemRepository _mediaItemRepository;
public GetAllLanguageCodesHandler(IMediaItemRepository mediaItemRepository) =>
_mediaItemRepository = mediaItemRepository;
public async Task<List<CultureInfo>> Handle(GetAllLanguageCodes request, CancellationToken cancellationToken) =>
await _mediaItemRepository.GetAllLanguageCodeCultures();
public async Task<List<LanguageCodeViewModel>> Handle(GetAllLanguageCodes request, CancellationToken cancellationToken)
{
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 @@ -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);
@ -52,6 +52,7 @@ internal static class Mapper @@ -52,6 +52,7 @@ internal static class Mapper
ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase)))
.Sequence()
.Flatten()
.Map(ci => ci.EnglishName)
.ToList();
}

2
ErsatzTV.Application/Movies/MovieViewModel.cs

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

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

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

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

@ -25,6 +25,6 @@ public interface ISearchIndex : IDisposable @@ -25,6 +25,6 @@ public interface ISearchIndex : IDisposable
List<MediaItem> items);
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();
}

2
ErsatzTV.Core/Search/SearchResult.cs

@ -2,5 +2,5 @@ @@ -2,5 +2,5 @@
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 @@ @@ -37,4 +37,6 @@
</ItemGroup>
</Project>

66
ErsatzTV.Infrastructure/Search/CustomMultiFieldQueryParser.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using Lucene.Net.Analysis;
using ErsatzTV.Core;
using Lucene.Net.Analysis;
using Lucene.Net.Index;
using Lucene.Net.QueryParsers.Classic;
using Lucene.Net.Search;
@ -9,6 +10,16 @@ namespace ErsatzTV.Infrastructure.Search; @@ -9,6 +10,16 @@ namespace ErsatzTV.Infrastructure.Search;
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(
LuceneVersion matchVersion,
string[] fields,
@ -32,7 +43,7 @@ public class CustomMultiFieldQueryParser : MultiFieldQueryParser @@ -32,7 +43,7 @@ public class CustomMultiFieldQueryParser : MultiFieldQueryParser
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();
NumericUtils.Int32ToPrefixCoded(val, 0, bytesRef);
@ -44,7 +55,7 @@ public class CustomMultiFieldQueryParser : MultiFieldQueryParser @@ -44,7 +55,7 @@ public class CustomMultiFieldQueryParser : MultiFieldQueryParser
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 dateString = start.ToString("yyyyMMdd");
@ -52,14 +63,14 @@ public class CustomMultiFieldQueryParser : MultiFieldQueryParser @@ -52,14 +63,14 @@ public class CustomMultiFieldQueryParser : MultiFieldQueryParser
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");
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 dateString = addedStart.ToString("yyyyMMdd");
@ -67,7 +78,7 @@ public class CustomMultiFieldQueryParser : MultiFieldQueryParser @@ -67,7 +78,7 @@ public class CustomMultiFieldQueryParser : MultiFieldQueryParser
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");
@ -84,7 +95,7 @@ public class CustomMultiFieldQueryParser : MultiFieldQueryParser @@ -84,7 +95,7 @@ public class CustomMultiFieldQueryParser : MultiFieldQueryParser
bool startInclusive,
bool endInclusive)
{
if (CustomQueryParser.NumericFields.Contains(field))
if (NumericFields.Contains(field))
{
if (part1 is null or "*" && int.TryParse(part2, out int max1))
{
@ -107,4 +118,45 @@ public class CustomMultiFieldQueryParser : MultiFieldQueryParser @@ -107,4 +118,45 @@ public class CustomMultiFieldQueryParser : MultiFieldQueryParser
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 @@ @@ -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; @@ -15,6 +15,7 @@ using ErsatzTV.Infrastructure.Search.Models;
using Microsoft.Extensions.Logging;
using ExistsResponse = Elastic.Clients.Elasticsearch.IndexManagement.ExistsResponse;
using MediaStream = ErsatzTV.Core.Domain.MediaStream;
using Query = Lucene.Net.Search.Query;
namespace ErsatzTV.Infrastructure.Search;
@ -152,17 +153,20 @@ public class ElasticSearchIndex : ISearchIndex @@ -152,17 +153,20 @@ public class ElasticSearchIndex : ISearchIndex
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 totalCount = 0;
Query parsedQuery = LuceneSearchIndex.ParseQuery(query);
SearchResponse<MinimalElasticSearchItem> response = await _client.SearchAsync<MinimalElasticSearchItem>(
s => s.Index(IndexName)
.Sort(ss => ss.Field(f => f.SortTitle, fs => fs.Order(SortOrder.Asc)))
.From(skip)
.Size(limit)
.QueryLuceneSyntax(query));
.QueryLuceneSyntax(parsedQuery.ToString()));
if (response.IsValidResponse)
{
items.AddRange(response.Documents);
@ -792,7 +796,7 @@ public class ElasticSearchIndex : ISearchIndex @@ -792,7 +796,7 @@ public class ElasticSearchIndex : ISearchIndex
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>(
s => s.Index(IndexName)
@ -803,7 +807,7 @@ public class ElasticSearchIndex : ISearchIndex @@ -803,7 +807,7 @@ public class ElasticSearchIndex : ISearchIndex
if (!response.IsValidResponse)
{
return Option<SearchPageMap>.None;
return null;
}
var letters = new List<char>

34
ErsatzTV.Infrastructure/Search/LuceneSearchIndex.cs

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

6
ErsatzTV/Pages/ArtistList.razor

@ -64,9 +64,9 @@ @@ -64,9 +64,9 @@
</FragmentLetterAnchor>
</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"
Query="@_query"/>
}
@ -77,7 +77,7 @@ @@ -77,7 +77,7 @@
[Parameter]
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;
protected override async Task OnParametersSetAsync()

7
ErsatzTV/Pages/ChannelEditor.razor

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

6
ErsatzTV/Pages/EpisodeList.razor

@ -64,9 +64,9 @@ @@ -64,9 +64,9 @@
</FragmentLetterAnchor>
</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"
Query="@_query"/>
}
@ -77,7 +77,7 @@ @@ -77,7 +77,7 @@
[Parameter]
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;
protected override async Task OnParametersSetAsync()

86
ErsatzTV/Pages/Movie.razor

@ -5,12 +5,14 @@ @@ -5,12 +5,14 @@
@using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.MediaItems
@using ErsatzTV.Application.MediaCards
@using LanguageExt.UnsafeValueAccess
@implements IDisposable
@inject IMediator _mediator
@inject IDialogService _dialog
@inject NavigationManager _navigationManager
@inject ILogger<Movie> _logger
@inject ISnackbar _snackbar
@inject IMediator Mediator
@inject IDialogService Dialog
@inject NavigationManager NavigationManager
@inject ILogger<Movie> Logger
@inject ISnackbar Snackbar
@inject PersistentComponentState ApplicationState
<MudContainer MaxWidth="MaxWidth.False" Style="padding: 0" Class="fanart-container">
<div class="fanart-tint"></div>
@ -129,11 +131,11 @@ @@ -129,11 +131,11 @@
{
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
<MudText GutterBottom="true">Languages:&nbsp;</MudText>
<MudLink Href="@(@$"language:""{_sortedLanguages.Head().EnglishName.ToLowerInvariant()}""".GetRelativeSearchQuery())">@_sortedLanguages.Head().EnglishName</MudLink>
@foreach (CultureInfo language in _sortedLanguages.Skip(1))
<MudLink Href="@(@$"language:""{_sortedLanguages.Head().ToLowerInvariant()}""".GetRelativeSearchQuery())">@_sortedLanguages.Head()</MudLink>
@foreach (string language in _sortedLanguages.Skip(1))
{
<MudText>,&nbsp;</MudText>
<MudLink Href="@(@$"language:""{language.EnglishName.ToLowerInvariant()}""".GetRelativeSearchQuery())">@language.EnglishName</MudLink>
<MudLink Href="@(@$"language:""{language.ToLowerInvariant()}""".GetRelativeSearchQuery())">@language</MudLink>
}
</div>
}
@ -217,13 +219,14 @@ @@ -217,13 +219,14 @@
@code {
private readonly CancellationTokenSource _cts = new();
private PersistingComponentStateSubscription _persistingSubscription;
[Parameter]
public int MovieId { get; set; }
private MovieViewModel _movie;
private List<string> _sortedContentRatings = new();
private List<CultureInfo> _sortedLanguages = new();
private List<string> _sortedLanguages = new();
private List<string> _sortedDirectors = new();
private List<string> _sortedWriters = new();
private List<string> _sortedStudios = new();
@ -232,54 +235,81 @@ @@ -232,54 +235,81 @@
public void Dispose()
{
_persistingSubscription.Dispose();
_cts.Cancel();
_cts.Dispose();
}
protected override Task OnParametersSetAsync() => RefreshData();
protected override Task OnInitializedAsync()
{
_persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData);
return base.OnInitializedAsync();
}
private async Task RefreshData() =>
await _mediator.Send(new GetMovieById(MovieId), _cts.Token).IfSomeAsync(vm =>
protected override async Task OnParametersSetAsync()
{
if (!ApplicationState.TryTakeFromJson("_movie", out MovieViewModel restored))
{
_movie = vm;
_sortedContentRatings = _movie.ContentRatings.OrderBy(cr => cr).ToList();
_sortedLanguages = _movie.Languages.OrderBy(ci => ci.EnglishName).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();
});
_movie = await RefreshData();
}
else
{
_movie = restored;
}
_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()
{
var parameters = new DialogParameters { { "EntityType", "movie" }, { "EntityName", _movie.Title } };
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;
if (!result.Canceled && result.Data is MediaCollectionViewModel collection)
{
await _mediator.Send(new AddMovieToCollection(collection.Id, MovieId), _cts.Token);
_navigationManager.NavigateTo($"media/collections/{collection.Id}");
await Mediator.Send(new AddMovieToCollection(collection.Id, MovieId), _cts.Token);
NavigationManager.NavigateTo($"media/collections/{collection.Id}");
}
}
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())
{
_snackbar.Add("Unexpected error loading media info");
_logger.LogError("Unexpected error loading media info: {Error}", error.Value);
Snackbar.Add("Unexpected error loading media info");
Logger.LogError("Unexpected error loading media info: {Error}", error.Value);
}
foreach (MediaItemInfo info in maybeInfo.RightToSeq())
{
var parameters = new DialogParameters { { "MediaItemInfo", info } };
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;
}
}

44
ErsatzTV/Pages/MovieList.razor

@ -1,12 +1,13 @@ @@ -1,12 +1,13 @@
@page "/media/movies"
@page "/media/movies/page/{PageNumber:int}"
@using ErsatzTV.Extensions
@using LanguageExt.UnsafeValueAccess
@using ErsatzTV.Application.MediaCards
@using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.Search
@inherits MultiSelectBase<MovieList>
@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;">
<div style="display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%" class="ml-6 mr-6">
@ -62,9 +63,9 @@ @@ -62,9 +63,9 @@
</FragmentLetterAnchor>
</MudContainer>
</MudContainer>
@if (_data.PageMap.IsSome)
@if (_data.PageMap is not null)
{
<LetterBar PageMap="@_data.PageMap.ValueUnsafe()"
<LetterBar PageMap="@_data.PageMap"
BaseUri="media/movies"
Query="@_query"/>
}
@ -75,8 +76,17 @@ @@ -75,8 +76,17 @@
[Parameter]
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 PersistingComponentStateSubscription _persistingSubscription;
protected override Task OnInitializedAsync()
{
_persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData);
_query = NavigationManager.Uri.GetSearchQuery();
return base.OnInitializedAsync();
}
protected override async Task OnParametersSetAsync()
{
@ -85,15 +95,33 @@ @@ -85,15 +95,33 @@
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);
await RefreshData();
return Task.CompletedTask;
}
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})";
_data = await Mediator.Send(new QuerySearchIndexMovies(searchQuery, PageNumber, PageSize), CancellationToken);
return await Mediator.Send(new QuerySearchIndexMovies(searchQuery, PageNumber, PageSize), CancellationToken);
}
private void PrevPage()

6
ErsatzTV/Pages/MusicVideoList.razor

@ -64,9 +64,9 @@ @@ -64,9 +64,9 @@
</FragmentLetterAnchor>
</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"
Query="@_query"/>
}
@ -77,7 +77,7 @@ @@ -77,7 +77,7 @@
[Parameter]
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;
protected override async Task OnParametersSetAsync()

6
ErsatzTV/Pages/OtherVideoList.razor

@ -64,9 +64,9 @@ @@ -64,9 +64,9 @@
</FragmentLetterAnchor>
</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"
Query="@_query"/>
}
@ -77,7 +77,7 @@ @@ -77,7 +77,7 @@
[Parameter]
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;
protected override async Task OnParametersSetAsync()

13
ErsatzTV/Pages/ScheduleItemsEditor.razor

@ -5,7 +5,6 @@ @@ -5,7 +5,6 @@
@using ErsatzTV.Application.ProgramSchedules
@using ErsatzTV.Application.Search
@using ErsatzTV.Application.Watermarks
@using System.Globalization
@using ErsatzTV.Core.Domain.Filler
@implements IDisposable
@inject NavigationManager NavigationManager
@ -305,7 +304,7 @@ @@ -305,7 +304,7 @@
For="@(() => _selectedItem.PreferredAudioLanguageCode)"
Clearable="true">
<MudSelectItem Value="@((string)null)">(none)</MudSelectItem>
@foreach (CultureInfo culture in _availableCultures)
@foreach (LanguageCodeViewModel culture in _availableCultures)
{
<MudSelectItem Value="@culture.ThreeLetterISOLanguageName">@culture.EnglishName</MudSelectItem>
}
@ -317,7 +316,7 @@ @@ -317,7 +316,7 @@
For="@(() => _selectedItem.PreferredSubtitleLanguageCode)"
Clearable="true">
<MudSelectItem Value="@((string)null)">(none)</MudSelectItem>
@foreach (CultureInfo culture in _availableCultures)
@foreach (LanguageCodeViewModel culture in _availableCultures)
{
<MudSelectItem Value="@culture.ThreeLetterISOLanguageName">@culture.EnglishName</MudSelectItem>
}
@ -345,7 +344,7 @@ @@ -345,7 +344,7 @@
private ProgramScheduleItemsEditViewModel _schedule;
private List<FillerPresetViewModel> _fillerPresets;
private List<WatermarkViewModel> _watermarks;
private List<CultureInfo> _availableCultures;
private List<LanguageCodeViewModel> _availableCultures;
private ProgramScheduleItemEditViewModel _selectedItem;
@ -355,7 +354,11 @@ @@ -355,7 +354,11 @@
_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()
{

104
ErsatzTV/Pages/Search.razor

@ -6,6 +6,8 @@ @@ -6,6 +6,8 @@
@inherits MultiSelectBase<Search>
@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;">
<div style="display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%" class="ml-6 mr-6">
@ -324,22 +326,108 @@ @@ -324,22 +326,108 @@
private OtherVideoCardResultsViewModel _otherVideos;
private SongCardResultsViewModel _songs;
private ArtistCardResultsViewModel _artists;
private PersistingComponentStateSubscription _persistingSubscription;
protected override async Task OnInitializedAsync()
{
_persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData);
_query = _navigationManager.Uri.GetSearchQuery();
if (!string.IsNullOrWhiteSpace(_query))
{
_movies = await Mediator.Send(new QuerySearchIndexMovies($"type:movie AND ({_query})", 1, 50), CancellationToken);
_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);
_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);
_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);
_artists = await Mediator.Send(new QuerySearchIndexArtists($"type:artist AND ({_query})", 1, 50), CancellationToken);
if (!ApplicationState.TryTakeFromJson("_movies", out MovieCardResultsViewModel restoredMovies))
{
_movies = await Mediator.Send(new QuerySearchIndexMovies($"type:movie AND ({_query})", 1, 50), CancellationToken);
}
else
{
_movies = restoredMovies;
}
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)
{

4
ErsatzTV/Pages/Settings.razor

@ -39,7 +39,7 @@ @@ -39,7 +39,7 @@
</MudSelect>
</MudElement>
<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>
}
@ -254,7 +254,7 @@ @@ -254,7 +254,7 @@
private bool _playoutSuccess;
private List<FFmpegProfileViewModel> _ffmpegProfiles;
private FFmpegSettingsViewModel _ffmpegSettings;
private List<CultureInfo> _availableCultures;
private List<LanguageCodeViewModel> _availableCultures;
private List<WatermarkViewModel> _watermarks;
private List<FillerPresetViewModel> _fillerPresets;
private List<ResolutionViewModel> _customResolutions;

6
ErsatzTV/Pages/SongList.razor

@ -64,9 +64,9 @@ @@ -64,9 +64,9 @@
</FragmentLetterAnchor>
</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"
Query="@_query"/>
}
@ -77,7 +77,7 @@ @@ -77,7 +77,7 @@
[Parameter]
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;
protected override async Task OnParametersSetAsync()

6
ErsatzTV/Pages/TelevisionSeasonSearchResults.razor

@ -63,9 +63,9 @@ @@ -63,9 +63,9 @@
</FragmentLetterAnchor>
</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"
Query="@_query"/>
}
@ -76,7 +76,7 @@ @@ -76,7 +76,7 @@
[Parameter]
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;
protected override async Task OnParametersSetAsync()

51
ErsatzTV/Pages/TelevisionShowList.razor

@ -1,13 +1,13 @@ @@ -1,13 +1,13 @@
@page "/media/tv/shows"
@page "/media/tv/shows/page/{PageNumber:int}"
@using ErsatzTV.Extensions
@using LanguageExt.UnsafeValueAccess
@using ErsatzTV.Application.MediaCards
@using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.Search
@inherits MultiSelectBase<TelevisionShowList>
@inject NavigationManager _navigationManager
@inject ChannelWriter<IBackgroundServiceRequest> _channel
@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;">
<div style="display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%" class="ml-6 mr-6">
@ -63,9 +63,9 @@ @@ -63,9 +63,9 @@
</FragmentLetterAnchor>
</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"
Query="@_query"/>
}
@ -76,8 +76,17 @@ @@ -76,8 +76,17 @@
[Parameter]
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 PersistingComponentStateSubscription _persistingSubscription;
protected override Task OnInitializedAsync()
{
_persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData);
_query = NavigationManager.Uri.GetSearchQuery();
return base.OnInitializedAsync();
}
protected override async Task OnParametersSetAsync()
{
@ -86,15 +95,33 @@ @@ -86,15 +95,33 @@
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})";
_data = await Mediator.Send(new QuerySearchIndexShows(searchQuery, PageNumber, PageSize), CancellationToken);
return await Mediator.Send(new QuerySearchIndexShows(searchQuery, PageNumber, PageSize), CancellationToken);
}
private void PrevPage()
@ -105,7 +132,7 @@ @@ -105,7 +132,7 @@
(string key, string value) = _query.EncodeQuery();
uri = $"{uri}?{key}={value}";
}
_navigationManager.NavigateTo(uri);
NavigationManager.NavigateTo(uri);
}
private void NextPage()
@ -116,7 +143,7 @@ @@ -116,7 +143,7 @@
(string key, string value) = _query.EncodeQuery();
uri = $"{uri}?{key}={value}";
}
_navigationManager.NavigateTo(uri);
NavigationManager.NavigateTo(uri);
}
private void SelectClicked(MediaCardViewModel card, MouseEventArgs e)

1
ErsatzTV/Pages/_Host.cshtml

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

7
ErsatzTV/Startup.cs

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

Loading…
Cancel
Save