Compare commits

...
Author SHA1 Message Date
Jason DoveandGitHub f04c18c810 index release date for searching (#93) 2021-03-21 01:49:10 +00:00
Jason DoveandGitHub eca58dbe7f plex fixes (#92)
* fix updating plex path replacements

* fix adding/removing plex libraries

* fix adding/removing plex servers

* fix initial plex library sync after sign in

* code cleanup
2021-03-21 01:35:18 +00:00
Jason DoveandGitHub cf9479d2a9 log search indexing errors and continue indexing (#91) 2021-03-20 21:33:37 +00:00
Jason DoveandGitHub b6331331b0 use default ffmpeg profile with new channels (#90) 2021-03-20 20:56:15 +00:00
Jason DoveandGitHub ed365cfa43 keep search query in search field (#89)
* upgrade dependencies

* keep search query in search field
2021-03-20 20:45:02 +00:00
Jason Dove b3a1e71570 only search title by default, allow leading wildcards 2021-03-20 15:32:30 -05:00
11 changed files with 235 additions and 98 deletions
@@ -78,10 +78,10 @@ namespace ErsatzTV.Application.Plex.Commands
var existing = connectionParameters.PlexMediaSource.Libraries.OfType<PlexLibrary>().ToList();
var toAdd = libraries.Filter(library => existing.All(l => l.Key != library.Key)).ToList();
var toRemove = existing.Filter(library => libraries.All(l => l.Key != library.Key)).ToList();
connectionParameters.PlexMediaSource.Libraries.AddRange(toAdd);
toRemove.ForEach(c => connectionParameters.PlexMediaSource.Libraries.Remove(c));
return _mediaSourceRepository.Update(connectionParameters.PlexMediaSource);
return _mediaSourceRepository.UpdateLibraries(
connectionParameters.PlexMediaSource.Id,
toAdd,
toRemove);
},
error =>
{
@@ -66,23 +66,20 @@ namespace ErsatzTV.Application.Plex.Commands
{
existing.ProductVersion = server.ProductVersion;
existing.ServerName = server.ServerName;
MergeConnections(existing.Connections, server.Connections);
if (existing.Connections.Any() && existing.Connections.All(c => !c.IsActive))
{
existing.Connections.Head().IsActive = true;
}
return _mediaSourceRepository.Update(existing);
var toAdd = server.Connections
.Filter(connection => existing.Connections.All(c => c.Uri != connection.Uri)).ToList();
var toRemove = existing.Connections
.Filter(connection => server.Connections.All(c => c.Uri != connection.Uri)).ToList();
return _mediaSourceRepository.Update(existing, toAdd, toRemove);
},
async () =>
{
await _mediaSourceRepository.Add(server);
if (server.Connections.Any())
{
server.Connections.Head().IsActive = true;
}
await _mediaSourceRepository.Update(server);
await _mediaSourceRepository.Add(server);
});
}
@@ -6,7 +6,6 @@ using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Plex.Commands
{
@@ -35,20 +34,7 @@ namespace ErsatzTV.Application.Plex.Commands
var toRemove = plexMediaSource.PathReplacements.Filter(r => incoming.All(pr => pr.Id != r.Id)).ToList();
var toUpdate = incoming.Except(toAdd).ToList();
plexMediaSource.PathReplacements.AddRange(toAdd);
toRemove.ForEach(pr => plexMediaSource.PathReplacements.Remove(pr));
foreach (PlexPathReplacement pathReplacement in toUpdate)
{
Optional(plexMediaSource.PathReplacements.SingleOrDefault(pr => pr.Id == pathReplacement.Id))
.IfSome(
pr =>
{
pr.PlexPath = pathReplacement.PlexPath;
pr.LocalPath = pathReplacement.LocalPath;
});
}
return _mediaSourceRepository.Update(plexMediaSource).ToUnit();
return _mediaSourceRepository.UpdatePathReplacements(plexMediaSource.Id, toAdd, toUpdate, toRemove);
}
private static PlexPathReplacement Project(PlexPathReplacementItem vm) =>
@@ -20,7 +20,19 @@ namespace ErsatzTV.Core.Interfaces.Repositories
Task<List<PlexPathReplacement>> GetPlexPathReplacementsByLibraryId(int plexLibraryPathId);
Task<int> CountMediaItems(int id);
Task Update(LocalMediaSource localMediaSource);
Task Update(PlexMediaSource plexMediaSource);
Task Update(PlexMediaSource plexMediaSource, List<PlexConnection> toAdd, List<PlexConnection> toDelete);
Task<Unit> UpdateLibraries(
int plexMediaSourceId,
List<PlexLibrary> toAdd,
List<PlexLibrary> toDelete);
Task<Unit> UpdatePathReplacements(
int plexMediaSourceId,
List<PlexPathReplacement> toAdd,
List<PlexPathReplacement> toUpdate,
List<PlexPathReplacement> toDelete);
Task Update(PlexLibrary plexMediaSourceLibrary);
Task Delete(int mediaSourceId);
Task<Unit> DeleteAllPlex();
@@ -152,11 +152,94 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
await context.SaveChangesAsync();
}
public async Task Update(PlexMediaSource plexMediaSource)
public async Task Update(
PlexMediaSource plexMediaSource,
List<PlexConnection> toAdd,
List<PlexConnection> toDelete)
{
await using TvContext context = _dbContextFactory.CreateDbContext();
context.PlexMediaSources.Update(plexMediaSource);
await context.SaveChangesAsync();
await _dbConnection.ExecuteAsync(
@"UPDATE PlexMediaSource SET ProductVersion = @ProductVersion, ServerName = @ServerName WHERE Id = @Id",
new { plexMediaSource.ProductVersion, plexMediaSource.ServerName, plexMediaSource.Id });
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
foreach (PlexConnection add in toAdd)
{
add.PlexMediaSourceId = plexMediaSource.Id;
dbContext.Entry(add).State = EntityState.Added;
}
foreach (PlexConnection delete in toDelete)
{
dbContext.Entry(delete).State = EntityState.Deleted;
}
await dbContext.SaveChangesAsync();
PlexMediaSource pms = await dbContext.PlexMediaSources.FindAsync(plexMediaSource.Id);
await dbContext.Entry(pms).Collection(x => x.Connections).LoadAsync();
if (plexMediaSource.Connections.Any() && plexMediaSource.Connections.All(c => !c.IsActive))
{
plexMediaSource.Connections.Head().IsActive = true;
await dbContext.SaveChangesAsync();
}
}
public async Task<Unit> UpdateLibraries(
int plexMediaSourceId,
List<PlexLibrary> toAdd,
List<PlexLibrary> toDelete)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
foreach (PlexLibrary add in toAdd)
{
add.MediaSourceId = plexMediaSourceId;
dbContext.Entry(add).State = EntityState.Added;
}
foreach (PlexLibrary delete in toDelete)
{
dbContext.Entry(delete).State = EntityState.Deleted;
}
await dbContext.SaveChangesAsync();
return Unit.Default;
}
public async Task<Unit> UpdatePathReplacements(
int plexMediaSourceId,
List<PlexPathReplacement> toAdd,
List<PlexPathReplacement> toUpdate,
List<PlexPathReplacement> toDelete)
{
foreach (PlexPathReplacement add in toAdd)
{
await _dbConnection.ExecuteAsync(
@"INSERT INTO PlexPathReplacement
(PlexPath, LocalPath, PlexMediaSourceId)
VALUES (@PlexPath, @LocalPath, @PlexMediaSourceId)",
new { add.PlexPath, add.LocalPath, PlexMediaSourceId = plexMediaSourceId });
}
foreach (PlexPathReplacement update in toUpdate)
{
await _dbConnection.ExecuteAsync(
@"UPDATE PlexPathReplacement
SET PlexPath = @PlexPath, LocalPath = @LocalPath
WHERE Id = @Id",
new { update.PlexPath, update.LocalPath, update.Id });
}
foreach (PlexPathReplacement delete in toDelete)
{
await _dbConnection.ExecuteAsync(
@"DELETE FROM PlexPathReplacement WHERE Id = @Id",
new { delete.Id });
}
return Unit.Default;
}
public async Task Update(PlexLibrary plexMediaSourceLibrary)
@@ -16,7 +16,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.4" />
<PackageReference Include="Refit" Version="6.0.24" />
<PackageReference Include="Refit" Version="6.0.38" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.3" />
</ItemGroup>
@@ -51,17 +51,17 @@ namespace ErsatzTV.Infrastructure.Plex
.Append(httpsResources.Filter(resource => resource.HttpsRequired))
.ToList();
IEnumerable<PlexMediaSource> sources = allResources
IEnumerable<PlexMediaSource> sources = await allResources
.Filter(r => r.Provides.Split(",").Any(p => p == "server"))
.Filter(r => r.Owned) // TODO: maybe support non-owned servers in the future
.Map(
resource =>
async resource =>
{
var serverAuthToken = new PlexServerAuthToken(
resource.ClientIdentifier,
resource.AccessToken);
_plexSecretStore.UpsertServerAuthToken(serverAuthToken);
await _plexSecretStore.UpsertServerAuthToken(serverAuthToken);
List<PlexResourceConnection> sortedConnections = resource.HttpsRequired
? resource.Connections
: resource.Connections.OrderBy(c => c.Local ? 0 : 1).ToList();
@@ -76,7 +76,9 @@ namespace ErsatzTV.Infrastructure.Plex
};
return source;
});
})
.Sequence();
result.AddRange(sources);
}
+89 -50
View File
@@ -18,6 +18,7 @@ using Lucene.Net.Sandbox.Queries;
using Lucene.Net.Search;
using Lucene.Net.Store;
using Lucene.Net.Util;
using Microsoft.Extensions.Logging;
using Query = Lucene.Net.Search.Query;
namespace ErsatzTV.Infrastructure.Search
@@ -36,6 +37,7 @@ namespace ErsatzTV.Infrastructure.Search
private const string LibraryNameField = "library_name";
private const string TitleAndYearField = "title_and_year";
private const string JumpLetterField = "jump_letter";
private const string ReleaseDateField = "release_date";
private const string MovieType = "movie";
private const string ShowType = "show";
@@ -43,17 +45,21 @@ namespace ErsatzTV.Infrastructure.Search
private static bool _isRebuilding;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<SearchIndex> _logger;
private readonly string[] _searchFields = { TitleField, GenreField, TagField };
private readonly ISearchRepository _searchRepository;
public SearchIndex(ILocalFileSystem localFileSystem, ISearchRepository searchRepository)
public SearchIndex(
ILocalFileSystem localFileSystem,
ISearchRepository searchRepository,
ILogger<SearchIndex> logger)
{
_localFileSystem = localFileSystem;
_searchRepository = searchRepository;
_logger = logger;
}
public int Version => 1;
public int Version => 2;
public Task<bool> Initialize()
{
@@ -150,7 +156,8 @@ namespace ErsatzTV.Infrastructure.Search
using var analyzer = new StandardAnalyzer(AppLuceneVersion);
QueryParser parser = !string.IsNullOrWhiteSpace(searchField)
? new QueryParser(AppLuceneVersion, searchField, analyzer)
: new MultiFieldQueryParser(AppLuceneVersion, _searchFields, analyzer);
: new MultiFieldQueryParser(AppLuceneVersion, new[] { TitleField }, analyzer);
parser.AllowLeadingWildcard = true;
Query query = ParseQuery(searchQuery, parser);
var filter = new DuplicateFilter(TitleAndYearField);
var sort = new Sort(new SortField(SortTitleField, SortFieldType.STRING));
@@ -212,77 +219,109 @@ namespace ErsatzTV.Infrastructure.Search
return new SearchPageMap(map);
}
private static void UpdateMovie(Movie movie, IndexWriter writer)
private void UpdateMovie(Movie movie, IndexWriter writer)
{
Option<MovieMetadata> maybeMetadata = movie.MovieMetadata.HeadOrNone();
if (maybeMetadata.IsSome)
{
MovieMetadata metadata = maybeMetadata.ValueUnsafe();
var doc = new Document
try
{
new StringField(IdField, movie.Id.ToString(), Field.Store.YES),
new StringField(TypeField, MovieType, Field.Store.NO),
new TextField(TitleField, metadata.Title, Field.Store.NO),
new StringField(SortTitleField, metadata.SortTitle.ToLowerInvariant(), Field.Store.NO),
new TextField(LibraryNameField, movie.LibraryPath.Library.Name, Field.Store.NO),
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO),
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES)
};
var doc = new Document
{
new StringField(IdField, movie.Id.ToString(), Field.Store.YES),
new StringField(TypeField, MovieType, Field.Store.NO),
new TextField(TitleField, metadata.Title, Field.Store.NO),
new StringField(SortTitleField, metadata.SortTitle.ToLowerInvariant(), Field.Store.NO),
new TextField(LibraryNameField, movie.LibraryPath.Library.Name, Field.Store.NO),
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO),
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES)
};
if (!string.IsNullOrWhiteSpace(metadata.Plot))
{
doc.Add(new TextField(PlotField, metadata.Plot ?? string.Empty, Field.Store.NO));
if (metadata.ReleaseDate.HasValue)
{
doc.Add(
new StringField(
ReleaseDateField,
metadata.ReleaseDate.Value.ToString("yyyyMMdd"),
Field.Store.NO));
}
if (!string.IsNullOrWhiteSpace(metadata.Plot))
{
doc.Add(new TextField(PlotField, metadata.Plot ?? string.Empty, Field.Store.NO));
}
foreach (Genre genre in metadata.Genres)
{
doc.Add(new TextField(GenreField, genre.Name, Field.Store.NO));
}
foreach (Tag tag in metadata.Tags)
{
doc.Add(new TextField(TagField, tag.Name, Field.Store.NO));
}
writer.UpdateDocument(new Term(IdField, movie.Id.ToString()), doc);
}
foreach (Genre genre in metadata.Genres)
catch (Exception ex)
{
doc.Add(new TextField(GenreField, genre.Name, Field.Store.NO));
_logger.LogWarning(ex, "Error indexing movie with metadata {@Metadata}", metadata);
}
foreach (Tag tag in metadata.Tags)
{
doc.Add(new TextField(TagField, tag.Name, Field.Store.NO));
}
writer.UpdateDocument(new Term(IdField, movie.Id.ToString()), doc);
}
}
private static void UpdateShow(Show show, IndexWriter writer)
private void UpdateShow(Show show, IndexWriter writer)
{
Option<ShowMetadata> maybeMetadata = show.ShowMetadata.HeadOrNone();
if (maybeMetadata.IsSome)
{
ShowMetadata metadata = maybeMetadata.ValueUnsafe();
var doc = new Document
try
{
new StringField(IdField, show.Id.ToString(), Field.Store.YES),
new StringField(TypeField, ShowType, Field.Store.NO),
new TextField(TitleField, metadata.Title, Field.Store.NO),
new StringField(SortTitleField, metadata.SortTitle.ToLowerInvariant(), Field.Store.NO),
new TextField(LibraryNameField, show.LibraryPath.Library.Name, Field.Store.NO),
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO),
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES)
};
var doc = new Document
{
new StringField(IdField, show.Id.ToString(), Field.Store.YES),
new StringField(TypeField, ShowType, Field.Store.NO),
new TextField(TitleField, metadata.Title, Field.Store.NO),
new StringField(SortTitleField, metadata.SortTitle.ToLowerInvariant(), Field.Store.NO),
new TextField(LibraryNameField, show.LibraryPath.Library.Name, Field.Store.NO),
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO),
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES)
};
if (!string.IsNullOrWhiteSpace(metadata.Plot))
{
doc.Add(new TextField(PlotField, metadata.Plot ?? string.Empty, Field.Store.NO));
if (metadata.ReleaseDate.HasValue)
{
doc.Add(
new StringField(
ReleaseDateField,
metadata.ReleaseDate.Value.ToString("yyyyMMdd"),
Field.Store.NO));
}
if (!string.IsNullOrWhiteSpace(metadata.Plot))
{
doc.Add(new TextField(PlotField, metadata.Plot ?? string.Empty, Field.Store.NO));
}
foreach (Genre genre in metadata.Genres)
{
doc.Add(new TextField(GenreField, genre.Name, Field.Store.NO));
}
foreach (Tag tag in metadata.Tags)
{
doc.Add(new TextField(TagField, tag.Name, Field.Store.NO));
}
writer.UpdateDocument(new Term(IdField, show.Id.ToString()), doc);
}
foreach (Genre genre in metadata.Genres)
catch (Exception ex)
{
doc.Add(new TextField(GenreField, genre.Name, Field.Store.NO));
_logger.LogWarning(ex, "Error indexing show with metadata {@Metadata}", metadata);
}
foreach (Tag tag in metadata.Tags)
{
doc.Add(new TextField(TagField, tag.Name, Field.Store.NO));
}
writer.UpdateDocument(new Term(IdField, show.Id.ToString()), doc);
}
}
+5 -5
View File
@@ -11,8 +11,8 @@
<ItemGroup>
<PackageReference Include="Accelist.FluentValidation.Blazor" Version="4.0.0" />
<PackageReference Include="FluentValidation" Version="9.5.2" />
<PackageReference Include="FluentValidation.AspNetCore" Version="9.5.2" />
<PackageReference Include="FluentValidation" Version="9.5.3" />
<PackageReference Include="FluentValidation.AspNetCore" Version="9.5.3" />
<PackageReference Include="LanguageExt.Core" Version="3.4.15" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.4" />
@@ -20,14 +20,14 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MudBlazor" Version="5.0.5" />
<PackageReference Include="MudBlazor" Version="5.0.6" />
<PackageReference Include="Refit.HttpClientFactory" Version="6.0.24" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.AspNetCore" Version="4.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.SQLite" Version="5.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.1.1" />
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.1.1" />
</ItemGroup>
<ItemGroup>
+3 -1
View File
@@ -95,13 +95,15 @@
}
else
{
FFmpegSettingsViewModel ffmpegSettings = await Mediator.Send(new GetFFmpegSettings());
// TODO: command for new channel
IEnumerable<int> channelNumbers = await Mediator.Send(new GetAllChannels())
.Map(list => list.Map(c => int.TryParse(c.Number.Split(".").Head(), out int result) ? result : 0));
int maxNumber = Optional(channelNumbers).Flatten().DefaultIfEmpty(0).Max();
_model.Number = (maxNumber + 1).ToString();
_model.Name = "New Channel";
_model.FFmpegProfileId = _ffmpegProfiles.Head().Id;
_model.FFmpegProfileId = ffmpegSettings.DefaultFFmpegProfileId;
_model.StreamingMode = StreamingMode.TransportStream;
}
}
+20 -4
View File
@@ -1,4 +1,6 @@
@using System.Reflection
@using Microsoft.AspNetCore.WebUtilities
@using Microsoft.Extensions.Primitives
@using System.Web
@inherits LayoutComponentBase
@inject NavigationManager NavigationManager
@@ -15,7 +17,7 @@
</a>
</div>
<MudTextField T="string"
@ref="_textField"
@bind-Value="@_query"
AdornmentIcon="@Icons.Material.Filled.Search"
Adornment="Adornment.Start"
Variant="Variant.Outlined"
@@ -65,7 +67,7 @@
@code {
private static readonly string InfoVersion = Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "unknown";
private MudTextField<string> _textField;
private string _query;
private MudTheme _ersatzTvTheme => new()
{
@@ -89,12 +91,26 @@
}
};
protected override async Task OnParametersSetAsync()
{
await base.OnParametersSetAsync();
string query = new Uri(NavigationManager.Uri).Query;
if (QueryHelpers.ParseQuery(query).TryGetValue("query", out StringValues value))
{
_query = value;
}
else
{
_query = string.Empty;
}
}
private void OnSearchKeyDown(KeyboardEventArgs args)
{
if (args.Code == "Enter")
{
string query = HttpUtility.UrlEncode(_textField.Value);
_textField.Reset();
string query = HttpUtility.UrlEncode(_query);
NavigationManager.NavigateTo($"/search?query={query}", true);
StateHasChanged();
}