Browse Source

prevent unnecessary search index updates (#1592)

* add sub_language and sub_language_tag fields to search index

* prevent unnecessary search index updates

* update changelog

* update dependencies
pull/1594/head
Jason Dove 1 year ago committed by GitHub
parent
commit
abed22b560
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 2
      ErsatzTV.Application/ErsatzTV.Application.csproj
  3. 2
      ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj
  4. 2
      ErsatzTV.Core/ErsatzTV.Core.csproj
  5. 3
      ErsatzTV.Core/Interfaces/Repositories/ISearchRepository.cs
  6. 2
      ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj
  7. 2
      ErsatzTV.Infrastructure.Tests/ErsatzTV.Infrastructure.Tests.csproj
  8. 7
      ErsatzTV.Infrastructure/Data/Repositories/Caching/CachingSearchRepository.cs
  9. 25
      ErsatzTV.Infrastructure/Data/Repositories/EmbyMovieRepository.cs
  10. 47
      ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs
  11. 25
      ErsatzTV.Infrastructure/Data/Repositories/JellyfinMovieRepository.cs
  12. 47
      ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs
  13. 25
      ErsatzTV.Infrastructure/Data/Repositories/PlexMovieRepository.cs
  14. 41
      ErsatzTV.Infrastructure/Data/Repositories/PlexTelevisionRepository.cs
  15. 38
      ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs
  16. 6
      ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj
  17. 53
      ErsatzTV.Infrastructure/Search/ElasticSearchIndex.cs
  18. 48
      ErsatzTV.Infrastructure/Search/LuceneSearchIndex.cs
  19. 6
      ErsatzTV.Infrastructure/Search/Models/ElasticSearchItem.cs
  20. 2
      ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj
  21. 22
      ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs
  22. 22
      ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs
  23. 4
      ErsatzTV.Scanner/ErsatzTV.Scanner.csproj
  24. 6
      ErsatzTV/ErsatzTV.csproj

4
CHANGELOG.md

@ -13,19 +13,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `Extract and use embedded (text) subtitles` - `Extract and use embedded (text) subtitles`
- Default value: `false` - Default value: `false`
- When enabled, embedded text subtitles will be periodically extracted, and considered for playback - When enabled, embedded text subtitles will be periodically extracted, and considered for playback
- Add `sub_language` and `sub_language_tag` fields to search index
### Fixed ### Fixed
- Fix antiforgery error caused by reusing existing browser tabs across docker container restarts - Fix antiforgery error caused by reusing existing browser tabs across docker container restarts
- Data protection keys will now be persisted under ErsatzTV's config folder instead of being recreated at startup - Data protection keys will now be persisted under ErsatzTV's config folder instead of being recreated at startup
- Fix bug updating/replacing Jellyfin movies - Fix bug updating/replacing Jellyfin movies
- A deep scan can be used to fix all movies, otherwise any future updates made to JF movies will correctly sync to ETV - A deep scan can be used to fix all movies, otherwise any future updates made to JF movies will correctly sync to ETV
- Automatically generate JWT tokens to allow channel previews of protected streams - Automatically generate JWT tokens to allow channel previews of protected streams
### Changed ### Changed
- Log search index updates under scanner category at debug level, to indicate a potential cause for the UI being out of date - Log search index updates under scanner category at debug level, to indicate a potential cause for the UI being out of date
- Batch search index updates to keep pace with library scans - Batch search index updates to keep pace with library scans
- Previously, search index updates would slowly process over minutes/hours after library scans completed - Previously, search index updates would slowly process over minutes/hours after library scans completed
- Search index updates should now complete at the same time as library scans - Search index updates should now complete at the same time as library scans
- Do not unnecessarily update the search index during media server library scans
## [0.8.5-beta] - 2024-01-30 ## [0.8.5-beta] - 2024-01-30
### Added ### Added

2
ErsatzTV.Application/ErsatzTV.Application.csproj

@ -10,7 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Bugsnag" Version="3.1.0" /> <PackageReference Include="Bugsnag" Version="3.1.0" />
<PackageReference Include="CliWrap" Version="3.6.4" /> <PackageReference Include="CliWrap" Version="3.6.6" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" /> <PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="MediatR" Version="12.2.0" /> <PackageReference Include="MediatR" Version="12.2.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />

2
ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj

@ -8,7 +8,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Bugsnag" Version="3.1.0" /> <PackageReference Include="Bugsnag" Version="3.1.0" />
<PackageReference Include="CliWrap" Version="3.6.4" /> <PackageReference Include="CliWrap" Version="3.6.6" />
<PackageReference Include="FluentAssertions" Version="6.12.0" /> <PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="LanguageExt.Core" Version="4.4.7" /> <PackageReference Include="LanguageExt.Core" Version="4.4.7" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />

2
ErsatzTV.Core/ErsatzTV.Core.csproj

@ -10,7 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Bugsnag" Version="3.1.0" /> <PackageReference Include="Bugsnag" Version="3.1.0" />
<PackageReference Include="Destructurama.Attributed" Version="3.1.0" /> <PackageReference Include="Destructurama.Attributed" Version="3.2.0" />
<PackageReference Include="Flurl" Version="4.0.0" /> <PackageReference Include="Flurl" Version="4.0.0" />
<PackageReference Include="LanguageExt.Core" Version="4.4.7" /> <PackageReference Include="LanguageExt.Core" Version="4.4.7" />
<PackageReference Include="LanguageExt.Transformers" Version="4.4.7" /> <PackageReference Include="LanguageExt.Transformers" Version="4.4.7" />

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

@ -6,8 +6,11 @@ public interface ISearchRepository
{ {
Task<Option<MediaItem>> GetItemToIndex(int id); Task<Option<MediaItem>> GetItemToIndex(int id);
Task<List<string>> GetLanguagesForShow(Show show); Task<List<string>> GetLanguagesForShow(Show show);
Task<List<string>> GetSubLanguagesForShow(Show show);
Task<List<string>> GetLanguagesForSeason(Season season); Task<List<string>> GetLanguagesForSeason(Season season);
Task<List<string>> GetSubLanguagesForSeason(Season season);
Task<List<string>> GetLanguagesForArtist(Artist artist); Task<List<string>> GetLanguagesForArtist(Artist artist);
Task<List<string>> GetSubLanguagesForArtist(Artist artist);
Task<List<string>> GetAllLanguageCodes(List<string> mediaCodes); Task<List<string>> GetAllLanguageCodes(List<string> mediaCodes);
IAsyncEnumerable<MediaItem> GetAllMediaItems(); IAsyncEnumerable<MediaItem> GetAllMediaItems();
} }

2
ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj

@ -9,7 +9,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CliWrap" Version="3.6.4" /> <PackageReference Include="CliWrap" Version="3.6.6" />
<PackageReference Include="LanguageExt.Core" Version="4.4.7" /> <PackageReference Include="LanguageExt.Core" Version="4.4.7" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />

2
ErsatzTV.Infrastructure.Tests/ErsatzTV.Infrastructure.Tests.csproj

@ -14,7 +14,7 @@
<PackageReference Include="NSubstitute" Version="5.1.0" /> <PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="NUnit" Version="4.0.1" /> <PackageReference Include="NUnit" Version="4.0.1" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit.Analyzers" Version="3.10.0"> <PackageReference Include="NUnit.Analyzers" Version="4.0.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>

7
ErsatzTV.Infrastructure/Data/Repositories/Caching/CachingSearchRepository.cs

@ -17,11 +17,18 @@ public class CachingSearchRepository : ICachingSearchRepository
public Task<Option<MediaItem>> GetItemToIndex(int id) => _searchRepository.GetItemToIndex(id); public Task<Option<MediaItem>> GetItemToIndex(int id) => _searchRepository.GetItemToIndex(id);
public Task<List<string>> GetLanguagesForShow(Show show) => _searchRepository.GetLanguagesForShow(show); public Task<List<string>> GetLanguagesForShow(Show show) => _searchRepository.GetLanguagesForShow(show);
public Task<List<string>> GetSubLanguagesForShow(Show show) => _searchRepository.GetSubLanguagesForShow(show);
public Task<List<string>> GetLanguagesForSeason(Season season) => _searchRepository.GetLanguagesForSeason(season); public Task<List<string>> GetLanguagesForSeason(Season season) => _searchRepository.GetLanguagesForSeason(season);
public Task<List<string>> GetSubLanguagesForSeason(Season season) =>
_searchRepository.GetSubLanguagesForSeason(season);
public Task<List<string>> GetLanguagesForArtist(Artist artist) => _searchRepository.GetLanguagesForArtist(artist); public Task<List<string>> GetLanguagesForArtist(Artist artist) => _searchRepository.GetLanguagesForArtist(artist);
public Task<List<string>> GetSubLanguagesForArtist(Artist artist) =>
_searchRepository.GetSubLanguagesForArtist(artist);
public async Task<List<string>> GetAllLanguageCodes(List<string> mediaCodes) public async Task<List<string>> GetAllLanguageCodes(List<string> mediaCodes)
{ {
if (!_cache.ContainsKey(mediaCodes)) if (!_cache.ContainsKey(mediaCodes))

25
ErsatzTV.Infrastructure/Data/Repositories/EmbyMovieRepository.cs

@ -37,6 +37,11 @@ public class EmbyMovieRepository : IEmbyMovieRepository
public async Task<Option<int>> FlagNormal(EmbyLibrary library, EmbyMovie movie) public async Task<Option<int>> FlagNormal(EmbyLibrary library, EmbyMovie movie)
{ {
if (movie.State is MediaItemState.Normal)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
movie.State = MediaItemState.Normal; movie.State = MediaItemState.Normal;
@ -51,7 +56,7 @@ public class EmbyMovieRepository : IEmbyMovieRepository
foreach (int id in maybeId) foreach (int id in maybeId)
{ {
return await dbContext.Connection.ExecuteAsync( return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 0 WHERE Id = @Id", "UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
new { Id = id }).Map(count => count > 0 ? Some(id) : None); new { Id = id }).Map(count => count > 0 ? Some(id) : None);
} }
@ -60,6 +65,11 @@ public class EmbyMovieRepository : IEmbyMovieRepository
public async Task<Option<int>> FlagUnavailable(EmbyLibrary library, EmbyMovie movie) public async Task<Option<int>> FlagUnavailable(EmbyLibrary library, EmbyMovie movie)
{ {
if (movie.State is MediaItemState.Unavailable)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
movie.State = MediaItemState.Unavailable; movie.State = MediaItemState.Unavailable;
@ -74,7 +84,7 @@ public class EmbyMovieRepository : IEmbyMovieRepository
foreach (int id in maybeId) foreach (int id in maybeId)
{ {
return await dbContext.Connection.ExecuteAsync( return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 2 WHERE Id = @Id", "UPDATE MediaItem SET State = 2 WHERE Id = @Id AND State != 2",
new { Id = id }).Map(count => count > 0 ? Some(id) : None); new { Id = id }).Map(count => count > 0 ? Some(id) : None);
} }
@ -83,6 +93,11 @@ public class EmbyMovieRepository : IEmbyMovieRepository
public async Task<Option<int>> FlagRemoteOnly(EmbyLibrary library, EmbyMovie movie) public async Task<Option<int>> FlagRemoteOnly(EmbyLibrary library, EmbyMovie movie)
{ {
if (movie.State is MediaItemState.RemoteOnly)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
movie.State = MediaItemState.RemoteOnly; movie.State = MediaItemState.RemoteOnly;
@ -97,7 +112,7 @@ public class EmbyMovieRepository : IEmbyMovieRepository
foreach (int id in maybeId) foreach (int id in maybeId)
{ {
return await dbContext.Connection.ExecuteAsync( return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 3 WHERE Id = @Id", "UPDATE MediaItem SET State = 3 WHERE Id = @Id AND State != 3",
new { Id = id }).Map(count => count > 0 ? Some(id) : None); new { Id = id }).Map(count => count > 0 ? Some(id) : None);
} }
@ -108,7 +123,7 @@ public class EmbyMovieRepository : IEmbyMovieRepository
{ {
if (movieItemIds.Count == 0) if (movieItemIds.Count == 0)
{ {
return new List<int>(); return [];
} }
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
@ -123,7 +138,7 @@ public class EmbyMovieRepository : IEmbyMovieRepository
.Map(result => result.ToList()); .Map(result => result.ToList());
await dbContext.Connection.ExecuteAsync( await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids", "UPDATE MediaItem SET State = 1 WHERE Id IN @Ids AND State != 1",
new { Ids = ids }); new { Ids = ids });
return ids; return ids;

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

@ -207,6 +207,11 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
public async Task<Option<int>> FlagNormal(EmbyLibrary library, EmbyEpisode episode) public async Task<Option<int>> FlagNormal(EmbyLibrary library, EmbyEpisode episode)
{ {
if (episode.State is MediaItemState.Normal)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
episode.State = MediaItemState.Normal; episode.State = MediaItemState.Normal;
@ -221,7 +226,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
foreach (int id in maybeId) foreach (int id in maybeId)
{ {
return await dbContext.Connection.ExecuteAsync( return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 0 WHERE Id = @Id", "UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
new { Id = id }).Map(count => count > 0 ? Some(id) : None); new { Id = id }).Map(count => count > 0 ? Some(id) : None);
} }
@ -230,6 +235,11 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
public async Task<Option<int>> FlagNormal(EmbyLibrary library, EmbySeason season) public async Task<Option<int>> FlagNormal(EmbyLibrary library, EmbySeason season)
{ {
if (season.State is MediaItemState.Normal)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
season.State = MediaItemState.Normal; season.State = MediaItemState.Normal;
@ -244,7 +254,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
foreach (int id in maybeId) foreach (int id in maybeId)
{ {
return await dbContext.Connection.ExecuteAsync( return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 0 WHERE Id = @Id", "UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
new { Id = id }).Map(count => count > 0 ? Some(id) : None); new { Id = id }).Map(count => count > 0 ? Some(id) : None);
} }
@ -253,6 +263,11 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
public async Task<Option<int>> FlagNormal(EmbyLibrary library, EmbyShow show) public async Task<Option<int>> FlagNormal(EmbyLibrary library, EmbyShow show)
{ {
if (show.State is MediaItemState.Normal)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
show.State = MediaItemState.Normal; show.State = MediaItemState.Normal;
@ -267,7 +282,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
foreach (int id in maybeId) foreach (int id in maybeId)
{ {
return await dbContext.Connection.ExecuteAsync( return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 0 WHERE Id = @Id", "UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
new { Id = id }).Map(count => count > 0 ? Some(id) : None); new { Id = id }).Map(count => count > 0 ? Some(id) : None);
} }
@ -278,7 +293,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
{ {
if (showItemIds.Count == 0) if (showItemIds.Count == 0)
{ {
return new List<int>(); return [];
} }
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
@ -293,7 +308,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
.Map(result => result.ToList()); .Map(result => result.ToList());
await dbContext.Connection.ExecuteAsync( await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids", "UPDATE MediaItem SET State = 1 WHERE Id IN @Ids AND State != 1",
new { Ids = ids }); new { Ids = ids });
return ids; return ids;
@ -303,7 +318,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
{ {
if (seasonItemIds.Count == 0) if (seasonItemIds.Count == 0)
{ {
return new List<int>(); return [];
} }
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
@ -318,7 +333,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
.Map(result => result.ToList()); .Map(result => result.ToList());
await dbContext.Connection.ExecuteAsync( await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids", "UPDATE MediaItem SET State = 1 WHERE Id IN @Ids AND State != 1",
new { Ids = ids }); new { Ids = ids });
return ids; return ids;
@ -328,7 +343,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
{ {
if (episodeItemIds.Count == 0) if (episodeItemIds.Count == 0)
{ {
return new List<int>(); return [];
} }
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
@ -343,7 +358,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
.Map(result => result.ToList()); .Map(result => result.ToList());
await dbContext.Connection.ExecuteAsync( await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids", "UPDATE MediaItem SET State = 1 WHERE Id IN @Ids AND State != 1",
new { Ids = ids }); new { Ids = ids });
return ids; return ids;
@ -351,6 +366,11 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
public async Task<Option<int>> FlagUnavailable(EmbyLibrary library, EmbyEpisode episode) public async Task<Option<int>> FlagUnavailable(EmbyLibrary library, EmbyEpisode episode)
{ {
if (episode.State is MediaItemState.Unavailable)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
episode.State = MediaItemState.Unavailable; episode.State = MediaItemState.Unavailable;
@ -365,7 +385,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
foreach (int id in maybeId) foreach (int id in maybeId)
{ {
return await dbContext.Connection.ExecuteAsync( return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 2 WHERE Id = @Id", "UPDATE MediaItem SET State = 2 WHERE Id = @Id AND State != 2",
new { Id = id }).Map(count => count > 0 ? Some(id) : None); new { Id = id }).Map(count => count > 0 ? Some(id) : None);
} }
@ -374,6 +394,11 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
public async Task<Option<int>> FlagRemoteOnly(EmbyLibrary library, EmbyEpisode episode) public async Task<Option<int>> FlagRemoteOnly(EmbyLibrary library, EmbyEpisode episode)
{ {
if (episode.State is MediaItemState.RemoteOnly)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
episode.State = MediaItemState.RemoteOnly; episode.State = MediaItemState.RemoteOnly;
@ -388,7 +413,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
foreach (int id in maybeId) foreach (int id in maybeId)
{ {
return await dbContext.Connection.ExecuteAsync( return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 3 WHERE Id = @Id", "UPDATE MediaItem SET State = 3 WHERE Id = @Id AND State != 3",
new { Id = id }).Map(count => count > 0 ? Some(id) : None); new { Id = id }).Map(count => count > 0 ? Some(id) : None);
} }

25
ErsatzTV.Infrastructure/Data/Repositories/JellyfinMovieRepository.cs

@ -39,6 +39,11 @@ public class JellyfinMovieRepository : IJellyfinMovieRepository
public async Task<Option<int>> FlagNormal(JellyfinLibrary library, JellyfinMovie movie) public async Task<Option<int>> FlagNormal(JellyfinLibrary library, JellyfinMovie movie)
{ {
if (movie.State is MediaItemState.Normal)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
movie.State = MediaItemState.Normal; movie.State = MediaItemState.Normal;
@ -53,7 +58,7 @@ public class JellyfinMovieRepository : IJellyfinMovieRepository
foreach (int id in maybeId) foreach (int id in maybeId)
{ {
return await dbContext.Connection.ExecuteAsync( return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 0 WHERE Id = @Id", "UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
new { Id = id }).Map(count => count > 0 ? Some(id) : None); new { Id = id }).Map(count => count > 0 ? Some(id) : None);
} }
@ -62,6 +67,11 @@ public class JellyfinMovieRepository : IJellyfinMovieRepository
public async Task<Option<int>> FlagUnavailable(JellyfinLibrary library, JellyfinMovie movie) public async Task<Option<int>> FlagUnavailable(JellyfinLibrary library, JellyfinMovie movie)
{ {
if (movie.State is MediaItemState.Unavailable)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
movie.State = MediaItemState.Unavailable; movie.State = MediaItemState.Unavailable;
@ -76,7 +86,7 @@ public class JellyfinMovieRepository : IJellyfinMovieRepository
foreach (int id in maybeId) foreach (int id in maybeId)
{ {
return await dbContext.Connection.ExecuteAsync( return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 2 WHERE Id = @Id", "UPDATE MediaItem SET State = 2 WHERE Id = @Id AND State != 2",
new { Id = id }).Map(count => count > 0 ? Some(id) : None); new { Id = id }).Map(count => count > 0 ? Some(id) : None);
} }
@ -85,6 +95,11 @@ public class JellyfinMovieRepository : IJellyfinMovieRepository
public async Task<Option<int>> FlagRemoteOnly(JellyfinLibrary library, JellyfinMovie movie) public async Task<Option<int>> FlagRemoteOnly(JellyfinLibrary library, JellyfinMovie movie)
{ {
if (movie.State is MediaItemState.RemoteOnly)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
movie.State = MediaItemState.RemoteOnly; movie.State = MediaItemState.RemoteOnly;
@ -99,7 +114,7 @@ public class JellyfinMovieRepository : IJellyfinMovieRepository
foreach (int id in maybeId) foreach (int id in maybeId)
{ {
return await dbContext.Connection.ExecuteAsync( return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 3 WHERE Id = @Id", "UPDATE MediaItem SET State = 3 WHERE Id = @Id AND State != 3",
new { Id = id }).Map(count => count > 0 ? Some(id) : None); new { Id = id }).Map(count => count > 0 ? Some(id) : None);
} }
@ -110,7 +125,7 @@ public class JellyfinMovieRepository : IJellyfinMovieRepository
{ {
if (movieItemIds.Count == 0) if (movieItemIds.Count == 0)
{ {
return new List<int>(); return [];
} }
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
@ -125,7 +140,7 @@ public class JellyfinMovieRepository : IJellyfinMovieRepository
.Map(result => result.ToList()); .Map(result => result.ToList());
await dbContext.Connection.ExecuteAsync( await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids", "UPDATE MediaItem SET State = 1 WHERE Id IN @Ids AND State != 1",
new { Ids = ids }); new { Ids = ids });
return ids; return ids;

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

@ -211,6 +211,11 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
public async Task<Option<int>> FlagNormal(JellyfinLibrary library, JellyfinEpisode episode) public async Task<Option<int>> FlagNormal(JellyfinLibrary library, JellyfinEpisode episode)
{ {
if (episode.State is MediaItemState.Normal)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
episode.State = MediaItemState.Normal; episode.State = MediaItemState.Normal;
@ -225,7 +230,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
foreach (int id in maybeId) foreach (int id in maybeId)
{ {
return await dbContext.Connection.ExecuteAsync( return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 0 WHERE Id = @Id", "UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
new { Id = id }).Map(count => count > 0 ? Some(id) : None); new { Id = id }).Map(count => count > 0 ? Some(id) : None);
} }
@ -234,6 +239,11 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
public async Task<Option<int>> FlagNormal(JellyfinLibrary library, JellyfinSeason season) public async Task<Option<int>> FlagNormal(JellyfinLibrary library, JellyfinSeason season)
{ {
if (season.State is MediaItemState.Normal)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
season.State = MediaItemState.Normal; season.State = MediaItemState.Normal;
@ -248,7 +258,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
foreach (int id in maybeId) foreach (int id in maybeId)
{ {
return await dbContext.Connection.ExecuteAsync( return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 0 WHERE Id = @Id", "UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
new { Id = id }).Map(count => count > 0 ? Some(id) : None); new { Id = id }).Map(count => count > 0 ? Some(id) : None);
} }
@ -257,6 +267,11 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
public async Task<Option<int>> FlagNormal(JellyfinLibrary library, JellyfinShow show) public async Task<Option<int>> FlagNormal(JellyfinLibrary library, JellyfinShow show)
{ {
if (show.State is MediaItemState.Normal)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
show.State = MediaItemState.Normal; show.State = MediaItemState.Normal;
@ -271,7 +286,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
foreach (int id in maybeId) foreach (int id in maybeId)
{ {
return await dbContext.Connection.ExecuteAsync( return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 0 WHERE Id = @Id", "UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
new { Id = id }).Map(count => count > 0 ? Some(id) : None); new { Id = id }).Map(count => count > 0 ? Some(id) : None);
} }
@ -282,7 +297,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
{ {
if (showItemIds.Count == 0) if (showItemIds.Count == 0)
{ {
return new List<int>(); return [];
} }
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
@ -297,7 +312,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
.Map(result => result.ToList()); .Map(result => result.ToList());
await dbContext.Connection.ExecuteAsync( await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids", "UPDATE MediaItem SET State = 1 WHERE Id IN @Ids AND State != 1",
new { Ids = ids }); new { Ids = ids });
return ids; return ids;
@ -307,7 +322,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
{ {
if (seasonItemIds.Count == 0) if (seasonItemIds.Count == 0)
{ {
return new List<int>(); return [];
} }
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
@ -322,7 +337,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
.Map(result => result.ToList()); .Map(result => result.ToList());
await dbContext.Connection.ExecuteAsync( await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids", "UPDATE MediaItem SET State = 1 WHERE Id IN @Ids AND State != 1",
new { Ids = ids }); new { Ids = ids });
return ids; return ids;
@ -332,7 +347,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
{ {
if (episodeItemIds.Count == 0) if (episodeItemIds.Count == 0)
{ {
return new List<int>(); return [];
} }
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
@ -347,7 +362,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
.Map(result => result.ToList()); .Map(result => result.ToList());
await dbContext.Connection.ExecuteAsync( await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids", "UPDATE MediaItem SET State = 1 WHERE Id IN @Ids AND State != 1",
new { Ids = ids }); new { Ids = ids });
return ids; return ids;
@ -355,6 +370,11 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
public async Task<Option<int>> FlagUnavailable(JellyfinLibrary library, JellyfinEpisode episode) public async Task<Option<int>> FlagUnavailable(JellyfinLibrary library, JellyfinEpisode episode)
{ {
if (episode.State is MediaItemState.Unavailable)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
episode.State = MediaItemState.Unavailable; episode.State = MediaItemState.Unavailable;
@ -369,7 +389,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
foreach (int id in maybeId) foreach (int id in maybeId)
{ {
return await dbContext.Connection.ExecuteAsync( return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 2 WHERE Id = @Id", "UPDATE MediaItem SET State = 2 WHERE Id = @Id AND State != 2",
new { Id = id }).Map(count => count > 0 ? Some(id) : None); new { Id = id }).Map(count => count > 0 ? Some(id) : None);
} }
@ -378,6 +398,11 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
public async Task<Option<int>> FlagRemoteOnly(JellyfinLibrary library, JellyfinEpisode episode) public async Task<Option<int>> FlagRemoteOnly(JellyfinLibrary library, JellyfinEpisode episode)
{ {
if (episode.State is MediaItemState.RemoteOnly)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
episode.State = MediaItemState.RemoteOnly; episode.State = MediaItemState.RemoteOnly;
@ -392,7 +417,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
foreach (int id in maybeId) foreach (int id in maybeId)
{ {
return await dbContext.Connection.ExecuteAsync( return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 3 WHERE Id = @Id", "UPDATE MediaItem SET State = 3 WHERE Id = @Id AND State != 3",
new { Id = id }).Map(count => count > 0 ? Some(id) : None); new { Id = id }).Map(count => count > 0 ? Some(id) : None);
} }

25
ErsatzTV.Infrastructure/Data/Repositories/PlexMovieRepository.cs

@ -37,6 +37,11 @@ public class PlexMovieRepository : IPlexMovieRepository
public async Task<Option<int>> FlagNormal(PlexLibrary library, PlexMovie movie) public async Task<Option<int>> FlagNormal(PlexLibrary library, PlexMovie movie)
{ {
if (movie.State is MediaItemState.Normal)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
movie.State = MediaItemState.Normal; movie.State = MediaItemState.Normal;
@ -51,7 +56,7 @@ public class PlexMovieRepository : IPlexMovieRepository
foreach (int id in maybeId) foreach (int id in maybeId)
{ {
return await dbContext.Connection.ExecuteAsync( return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 0 WHERE Id = @Id", "UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
new { Id = id }).Map(count => count > 0 ? Some(id) : None); new { Id = id }).Map(count => count > 0 ? Some(id) : None);
} }
@ -60,6 +65,11 @@ public class PlexMovieRepository : IPlexMovieRepository
public async Task<Option<int>> FlagUnavailable(PlexLibrary library, PlexMovie movie) public async Task<Option<int>> FlagUnavailable(PlexLibrary library, PlexMovie movie)
{ {
if (movie.State is MediaItemState.Unavailable)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
movie.State = MediaItemState.Unavailable; movie.State = MediaItemState.Unavailable;
@ -74,7 +84,7 @@ public class PlexMovieRepository : IPlexMovieRepository
foreach (int id in maybeId) foreach (int id in maybeId)
{ {
return await dbContext.Connection.ExecuteAsync( return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 2 WHERE Id = @Id", "UPDATE MediaItem SET State = 2 WHERE Id = @Id AND State != 2",
new { Id = id }).Map(count => count > 0 ? Some(id) : None); new { Id = id }).Map(count => count > 0 ? Some(id) : None);
} }
@ -83,6 +93,11 @@ public class PlexMovieRepository : IPlexMovieRepository
public async Task<Option<int>> FlagRemoteOnly(PlexLibrary library, PlexMovie movie) public async Task<Option<int>> FlagRemoteOnly(PlexLibrary library, PlexMovie movie)
{ {
if (movie.State is MediaItemState.RemoteOnly)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
movie.State = MediaItemState.RemoteOnly; movie.State = MediaItemState.RemoteOnly;
@ -97,7 +112,7 @@ public class PlexMovieRepository : IPlexMovieRepository
foreach (int id in maybeId) foreach (int id in maybeId)
{ {
return await dbContext.Connection.ExecuteAsync( return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 3 WHERE Id = @Id", "UPDATE MediaItem SET State = 3 WHERE Id = @Id AND State != 3",
new { Id = id }).Map(count => count > 0 ? Some(id) : None); new { Id = id }).Map(count => count > 0 ? Some(id) : None);
} }
@ -108,7 +123,7 @@ public class PlexMovieRepository : IPlexMovieRepository
{ {
if (movieItemIds.Count == 0) if (movieItemIds.Count == 0)
{ {
return new List<int>(); return [];
} }
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
@ -123,7 +138,7 @@ public class PlexMovieRepository : IPlexMovieRepository
.Map(result => result.ToList()); .Map(result => result.ToList());
await dbContext.Connection.ExecuteAsync( await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids", "UPDATE MediaItem SET State = 1 WHERE Id IN @Ids AND State != 1",
new { Ids = ids }); new { Ids = ids });
return ids; return ids;

41
ErsatzTV.Infrastructure/Data/Repositories/PlexTelevisionRepository.cs

@ -26,6 +26,11 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
public async Task<Option<int>> FlagNormal(PlexLibrary library, PlexEpisode episode) public async Task<Option<int>> FlagNormal(PlexLibrary library, PlexEpisode episode)
{ {
if (episode.State is MediaItemState.Normal)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
episode.State = MediaItemState.Normal; episode.State = MediaItemState.Normal;
@ -40,7 +45,7 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
foreach (int id in maybeId) foreach (int id in maybeId)
{ {
return await dbContext.Connection.ExecuteAsync( return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 0 WHERE Id = @Id", "UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
new { Id = id }).Map(count => count > 0 ? Some(id) : None); new { Id = id }).Map(count => count > 0 ? Some(id) : None);
} }
@ -49,6 +54,11 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
public async Task<Option<int>> FlagNormal(PlexLibrary library, PlexSeason season) public async Task<Option<int>> FlagNormal(PlexLibrary library, PlexSeason season)
{ {
if (season.State is MediaItemState.Normal)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
season.State = MediaItemState.Normal; season.State = MediaItemState.Normal;
@ -63,7 +73,7 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
foreach (int id in maybeId) foreach (int id in maybeId)
{ {
return await dbContext.Connection.ExecuteAsync( return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 0 WHERE Id = @Id", "UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
new { Id = id }).Map(count => count > 0 ? Some(id) : None); new { Id = id }).Map(count => count > 0 ? Some(id) : None);
} }
@ -72,6 +82,11 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
public async Task<Option<int>> FlagNormal(PlexLibrary library, PlexShow show) public async Task<Option<int>> FlagNormal(PlexLibrary library, PlexShow show)
{ {
if (show.State is MediaItemState.Normal)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
show.State = MediaItemState.Normal; show.State = MediaItemState.Normal;
@ -86,7 +101,7 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
foreach (int id in maybeId) foreach (int id in maybeId)
{ {
return await dbContext.Connection.ExecuteAsync( return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 0 WHERE Id = @Id", "UPDATE MediaItem SET State = 0 WHERE Id = @Id AND State != 0",
new { Id = id }).Map(count => count > 0 ? Some(id) : None); new { Id = id }).Map(count => count > 0 ? Some(id) : None);
} }
@ -95,6 +110,11 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
public async Task<Option<int>> FlagUnavailable(PlexLibrary library, PlexEpisode episode) public async Task<Option<int>> FlagUnavailable(PlexLibrary library, PlexEpisode episode)
{ {
if (episode.State is MediaItemState.Unavailable)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
episode.State = MediaItemState.Unavailable; episode.State = MediaItemState.Unavailable;
@ -109,7 +129,7 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
foreach (int id in maybeId) foreach (int id in maybeId)
{ {
return await dbContext.Connection.ExecuteAsync( return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 2 WHERE Id = @Id", "UPDATE MediaItem SET State = 2 WHERE Id = @Id AND State != 2",
new { Id = id }).Map(count => count > 0 ? Some(id) : None); new { Id = id }).Map(count => count > 0 ? Some(id) : None);
} }
@ -118,6 +138,11 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
public async Task<Option<int>> FlagRemoteOnly(PlexLibrary library, PlexEpisode episode) public async Task<Option<int>> FlagRemoteOnly(PlexLibrary library, PlexEpisode episode)
{ {
if (episode.State is MediaItemState.RemoteOnly)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
episode.State = MediaItemState.RemoteOnly; episode.State = MediaItemState.RemoteOnly;
@ -132,7 +157,7 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
foreach (int id in maybeId) foreach (int id in maybeId)
{ {
return await dbContext.Connection.ExecuteAsync( return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 3 WHERE Id = @Id", "UPDATE MediaItem SET State = 3 WHERE Id = @Id AND State != 3",
new { Id = id }).Map(count => count > 0 ? Some(id) : None); new { Id = id }).Map(count => count > 0 ? Some(id) : None);
} }
@ -321,7 +346,7 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
{ {
if (showItemIds.Count == 0) if (showItemIds.Count == 0)
{ {
return new List<int>(); return [];
} }
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
@ -346,7 +371,7 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
{ {
if (seasonItemIds.Count == 0) if (seasonItemIds.Count == 0)
{ {
return new List<int>(); return [];
} }
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
@ -371,7 +396,7 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
{ {
if (episodeItemIds.Count == 0) if (episodeItemIds.Count == 0)
{ {
return new List<int>(); return [];
} }
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();

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

@ -157,6 +157,19 @@ public class SearchRepository : ISearchRepository
WHERE MediaStreamKind = 2 AND S.ShowId = @ShowId", WHERE MediaStreamKind = 2 AND S.ShowId = @ShowId",
new { ShowId = show.Id }).Map(result => result.ToList()); new { ShowId = show.Id }).Map(result => result.ToList());
} }
public async Task<List<string>> GetSubLanguagesForShow(Show show)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Connection.QueryAsync<string>(
@"SELECT DISTINCT Language
FROM MediaStream
INNER JOIN MediaVersion MV ON MediaStream.MediaVersionId = MV.Id
INNER JOIN Episode E ON MV.EpisodeId = E.Id
INNER JOIN Season S ON E.SeasonId = S.Id
WHERE MediaStreamKind = 3 AND S.ShowId = @ShowId",
new { ShowId = show.Id }).Map(result => result.ToList());
}
public async Task<List<string>> GetLanguagesForSeason(Season season) public async Task<List<string>> GetLanguagesForSeason(Season season)
{ {
@ -169,6 +182,18 @@ public class SearchRepository : ISearchRepository
WHERE MediaStreamKind = 2 AND E.SeasonId = @SeasonId", WHERE MediaStreamKind = 2 AND E.SeasonId = @SeasonId",
new { SeasonId = season.Id }).Map(result => result.ToList()); new { SeasonId = season.Id }).Map(result => result.ToList());
} }
public async Task<List<string>> GetSubLanguagesForSeason(Season season)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Connection.QueryAsync<string>(
@"SELECT DISTINCT Language
FROM MediaStream
INNER JOIN MediaVersion MV ON MediaStream.MediaVersionId = MV.Id
INNER JOIN Episode E ON MV.EpisodeId = E.Id
WHERE MediaStreamKind = 3 AND E.SeasonId = @SeasonId",
new { SeasonId = season.Id }).Map(result => result.ToList());
}
public async Task<List<string>> GetLanguagesForArtist(Artist artist) public async Task<List<string>> GetLanguagesForArtist(Artist artist)
{ {
@ -182,6 +207,19 @@ public class SearchRepository : ISearchRepository
WHERE MediaStreamKind = 2 AND A.Id = @ArtistId", WHERE MediaStreamKind = 2 AND A.Id = @ArtistId",
new { ArtistId = artist.Id }).Map(result => result.ToList()); new { ArtistId = artist.Id }).Map(result => result.ToList());
} }
public async Task<List<string>> GetSubLanguagesForArtist(Artist artist)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Connection.QueryAsync<string>(
@"SELECT DISTINCT Language
FROM MediaStream
INNER JOIN MediaVersion V ON MediaStream.MediaVersionId = V.Id
INNER JOIN MusicVideo MV ON V.MusicVideoId = MV.Id
INNER JOIN Artist A on MV.ArtistId = A.Id
WHERE MediaStreamKind = 3 AND A.Id = @ArtistId",
new { ArtistId = artist.Id }).Map(result => result.ToList());
}
public virtual async Task<List<string>> GetAllLanguageCodes(List<string> mediaCodes) public virtual async Task<List<string>> GetAllLanguageCodes(List<string> mediaCodes)
{ {

6
ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj

@ -11,10 +11,10 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Blurhash.ImageSharp" Version="3.0.0" /> <PackageReference Include="Blurhash.ImageSharp" Version="3.0.0" />
<PackageReference Include="CliWrap" Version="3.6.4" /> <PackageReference Include="CliWrap" Version="3.6.6" />
<PackageReference Include="Dapper" Version="2.1.28" /> <PackageReference Include="Dapper" Version="2.1.28" />
<PackageReference Include="Elastic.Clients.Elasticsearch" Version="8.11.0" /> <PackageReference Include="Elastic.Clients.Elasticsearch" Version="8.12.0" />
<PackageReference Include="Jint" Version="3.0.0-beta-2059" /> <PackageReference Include="Jint" Version="3.0.0" />
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00016" /> <PackageReference Include="Lucene.Net" Version="4.8.0-beta00016" />
<PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00016" /> <PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00016" />
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00016" /> <PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00016" />

53
ErsatzTV.Infrastructure/Search/ElasticSearchIndex.cs

@ -46,7 +46,7 @@ public class ElasticSearchIndex : ISearchIndex
return exists.IsValidResponse; return exists.IsValidResponse;
} }
public int Version => 38; public int Version => 39;
public async Task<bool> Initialize( public async Task<bool> Initialize(
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
@ -211,6 +211,9 @@ public class ElasticSearchIndex : ISearchIndex
.Keyword(t => t.State, t => t.Store(false)) .Keyword(t => t.State, t => t.Store(false))
.Text(t => t.MetadataKind, t => t.Store(false)) .Text(t => t.MetadataKind, t => t.Store(false))
.Text(t => t.Language, t => t.Store(false)) .Text(t => t.Language, t => t.Store(false))
.Text(t => t.LanguageTag, t => t.Store(false))
.Text(t => t.SubLanguage, t => t.Store(false))
.Text(t => t.SubLanguageTag, t => t.Store(false))
.IntegerNumber(t => t.Height, t => t.Store(false)) .IntegerNumber(t => t.Height, t => t.Store(false))
.IntegerNumber(t => t.Width, t => t.Store(false)) .IntegerNumber(t => t.Width, t => t.Store(false))
.Keyword(t => t.VideoCodec, t => t.Store(false)) .Keyword(t => t.VideoCodec, t => t.Store(false))
@ -294,6 +297,8 @@ public class ElasticSearchIndex : ISearchIndex
MetadataKind = metadata.MetadataKind.ToString(), MetadataKind = metadata.MetadataKind.ToString(),
Language = await GetLanguages(searchRepository, movie.MediaVersions), Language = await GetLanguages(searchRepository, movie.MediaVersions),
LanguageTag = GetLanguageTags(movie.MediaVersions), LanguageTag = GetLanguageTags(movie.MediaVersions),
SubLanguage = await GetSubLanguages(searchRepository, movie.MediaVersions),
SubLanguageTag = GetSubLanguageTags(movie.MediaVersions),
ContentRating = GetContentRatings(metadata.ContentRating), ContentRating = GetContentRatings(metadata.ContentRating),
ReleaseDate = GetReleaseDate(metadata.ReleaseDate), ReleaseDate = GetReleaseDate(metadata.ReleaseDate),
AddedDate = GetAddedDate(metadata.DateAdded), AddedDate = GetAddedDate(metadata.DateAdded),
@ -345,6 +350,8 @@ public class ElasticSearchIndex : ISearchIndex
MetadataKind = metadata.MetadataKind.ToString(), MetadataKind = metadata.MetadataKind.ToString(),
Language = await GetLanguages(searchRepository, await searchRepository.GetLanguagesForShow(show)), Language = await GetLanguages(searchRepository, await searchRepository.GetLanguagesForShow(show)),
LanguageTag = await searchRepository.GetLanguagesForShow(show), LanguageTag = await searchRepository.GetLanguagesForShow(show),
SubLanguage = await GetLanguages(searchRepository, await searchRepository.GetSubLanguagesForShow(show)),
SubLanguageTag = await searchRepository.GetSubLanguagesForShow(show),
ContentRating = GetContentRatings(metadata.ContentRating), ContentRating = GetContentRatings(metadata.ContentRating),
ReleaseDate = GetReleaseDate(metadata.ReleaseDate), ReleaseDate = GetReleaseDate(metadata.ReleaseDate),
AddedDate = GetAddedDate(metadata.DateAdded), AddedDate = GetAddedDate(metadata.DateAdded),
@ -405,6 +412,10 @@ public class ElasticSearchIndex : ISearchIndex
searchRepository, searchRepository,
await searchRepository.GetLanguagesForSeason(season)), await searchRepository.GetLanguagesForSeason(season)),
LanguageTag = await searchRepository.GetLanguagesForSeason(season), LanguageTag = await searchRepository.GetLanguagesForSeason(season),
SubLanguage = await GetLanguages(
searchRepository,
await searchRepository.GetSubLanguagesForSeason(season)),
SubLanguageTag = await searchRepository.GetSubLanguagesForSeason(season),
ContentRating = GetContentRatings(showMetadata.ContentRating), ContentRating = GetContentRatings(showMetadata.ContentRating),
ReleaseDate = GetReleaseDate(metadata.ReleaseDate), ReleaseDate = GetReleaseDate(metadata.ReleaseDate),
AddedDate = GetAddedDate(metadata.DateAdded), AddedDate = GetAddedDate(metadata.DateAdded),
@ -450,6 +461,10 @@ public class ElasticSearchIndex : ISearchIndex
searchRepository, searchRepository,
await searchRepository.GetLanguagesForArtist(artist)), await searchRepository.GetLanguagesForArtist(artist)),
LanguageTag = await searchRepository.GetLanguagesForArtist(artist), LanguageTag = await searchRepository.GetLanguagesForArtist(artist),
SubLanguage = await GetLanguages(
searchRepository,
await searchRepository.GetSubLanguagesForArtist(artist)),
SubLanguageTag = await searchRepository.GetSubLanguagesForArtist(artist),
AddedDate = GetAddedDate(metadata.DateAdded), AddedDate = GetAddedDate(metadata.DateAdded),
Genre = metadata.Genres.Map(g => g.Name).ToList(), Genre = metadata.Genres.Map(g => g.Name).ToList(),
Style = metadata.Styles.Map(t => t.Name).ToList(), Style = metadata.Styles.Map(t => t.Name).ToList(),
@ -491,6 +506,8 @@ public class ElasticSearchIndex : ISearchIndex
MetadataKind = metadata.MetadataKind.ToString(), MetadataKind = metadata.MetadataKind.ToString(),
Language = await GetLanguages(searchRepository, musicVideo.MediaVersions), Language = await GetLanguages(searchRepository, musicVideo.MediaVersions),
LanguageTag = GetLanguageTags(musicVideo.MediaVersions), LanguageTag = GetLanguageTags(musicVideo.MediaVersions),
SubLanguage = await GetSubLanguages(searchRepository, musicVideo.MediaVersions),
SubLanguageTag = GetSubLanguageTags(musicVideo.MediaVersions),
ReleaseDate = GetReleaseDate(metadata.ReleaseDate), ReleaseDate = GetReleaseDate(metadata.ReleaseDate),
AddedDate = GetAddedDate(metadata.DateAdded), AddedDate = GetAddedDate(metadata.DateAdded),
Album = metadata.Album ?? string.Empty, Album = metadata.Album ?? string.Empty,
@ -570,6 +587,8 @@ public class ElasticSearchIndex : ISearchIndex
EpisodeNumber = metadata.EpisodeNumber, EpisodeNumber = metadata.EpisodeNumber,
Language = await GetLanguages(searchRepository, episode.MediaVersions), Language = await GetLanguages(searchRepository, episode.MediaVersions),
LanguageTag = GetLanguageTags(episode.MediaVersions), LanguageTag = GetLanguageTags(episode.MediaVersions),
SubLanguage = await GetSubLanguages(searchRepository, episode.MediaVersions),
SubLanguageTag = GetSubLanguageTags(episode.MediaVersions),
ReleaseDate = GetReleaseDate(metadata.ReleaseDate), ReleaseDate = GetReleaseDate(metadata.ReleaseDate),
AddedDate = GetAddedDate(metadata.DateAdded), AddedDate = GetAddedDate(metadata.DateAdded),
Plot = metadata.Plot ?? string.Empty, Plot = metadata.Plot ?? string.Empty,
@ -629,6 +648,8 @@ public class ElasticSearchIndex : ISearchIndex
MetadataKind = metadata.MetadataKind.ToString(), MetadataKind = metadata.MetadataKind.ToString(),
Language = await GetLanguages(searchRepository, otherVideo.MediaVersions), Language = await GetLanguages(searchRepository, otherVideo.MediaVersions),
LanguageTag = GetLanguageTags(otherVideo.MediaVersions), LanguageTag = GetLanguageTags(otherVideo.MediaVersions),
SubLanguage = await GetSubLanguages(searchRepository, otherVideo.MediaVersions),
SubLanguageTag = GetSubLanguageTags(otherVideo.MediaVersions),
ContentRating = GetContentRatings(metadata.ContentRating), ContentRating = GetContentRatings(metadata.ContentRating),
ReleaseDate = GetReleaseDate(metadata.ReleaseDate), ReleaseDate = GetReleaseDate(metadata.ReleaseDate),
AddedDate = GetAddedDate(metadata.DateAdded), AddedDate = GetAddedDate(metadata.DateAdded),
@ -678,6 +699,8 @@ public class ElasticSearchIndex : ISearchIndex
MetadataKind = metadata.MetadataKind.ToString(), MetadataKind = metadata.MetadataKind.ToString(),
Language = await GetLanguages(searchRepository, song.MediaVersions), Language = await GetLanguages(searchRepository, song.MediaVersions),
LanguageTag = GetLanguageTags(song.MediaVersions), LanguageTag = GetLanguageTags(song.MediaVersions),
SubLanguage = await GetSubLanguages(searchRepository, song.MediaVersions),
SubLanguageTag = GetSubLanguageTags(song.MediaVersions),
AddedDate = GetAddedDate(metadata.DateAdded), AddedDate = GetAddedDate(metadata.DateAdded),
Album = metadata.Album ?? string.Empty, Album = metadata.Album ?? string.Empty,
Artist = !string.IsNullOrWhiteSpace(metadata.Artist) ? new List<string> { metadata.Artist } : null, Artist = !string.IsNullOrWhiteSpace(metadata.Artist) ? new List<string> { metadata.Artist } : null,
@ -744,6 +767,26 @@ public class ElasticSearchIndex : ISearchIndex
return result; return result;
} }
private async Task<List<string>> GetSubLanguages(
ISearchRepository searchRepository,
IEnumerable<MediaVersion> mediaVersions)
{
var result = new List<string>();
foreach (MediaVersion version in mediaVersions.HeadOrNone())
{
var mediaCodes = version.Streams
.Filter(ms => ms.MediaStreamKind == MediaStreamKind.Subtitle)
.Map(ms => ms.Language)
.Distinct()
.ToList();
result.AddRange(await GetLanguages(searchRepository, mediaCodes));
}
return result;
}
private async Task<List<string>> GetLanguages(ISearchRepository searchRepository, List<string> mediaCodes) private async Task<List<string>> GetLanguages(ISearchRepository searchRepository, List<string> mediaCodes)
{ {
@ -769,6 +812,14 @@ public class ElasticSearchIndex : ISearchIndex
.Distinct() .Distinct()
.ToList(); .ToList();
private static List<string> GetSubLanguageTags(IEnumerable<MediaVersion> mediaVersions) =>
mediaVersions
.Map(mv => mv.Streams.Filter(ms => ms.MediaStreamKind == MediaStreamKind.Subtitle).Map(ms => ms.Language))
.Flatten()
.Filter(s => !string.IsNullOrWhiteSpace(s))
.Distinct()
.ToList();
private static void AddStatistics(ElasticSearchItem doc, IEnumerable<MediaVersion> mediaVersions) private static void AddStatistics(ElasticSearchItem doc, IEnumerable<MediaVersion> mediaVersions)
{ {
foreach (MediaVersion version in mediaVersions.HeadOrNone()) foreach (MediaVersion version in mediaVersions.HeadOrNone())

48
ErsatzTV.Infrastructure/Search/LuceneSearchIndex.cs

@ -46,6 +46,8 @@ public sealed class LuceneSearchIndex : ISearchIndex
internal const string StudioField = "studio"; internal const string StudioField = "studio";
internal const string LanguageField = "language"; internal const string LanguageField = "language";
internal const string LanguageTagField = "language_tag"; internal const string LanguageTagField = "language_tag";
internal const string SubLanguageField = "sub_language";
internal const string SubLanguageTagField = "sub_language_tag";
internal const string StyleField = "style"; internal const string StyleField = "style";
internal const string MoodField = "mood"; internal const string MoodField = "mood";
internal const string ActorField = "actor"; internal const string ActorField = "actor";
@ -109,7 +111,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
return Task.FromResult(directoryExists && fileExists); return Task.FromResult(directoryExists && fileExists);
} }
public int Version => 38; public int Version => 39;
public async Task<bool> Initialize( public async Task<bool> Initialize(
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
@ -501,7 +503,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
private async Task AddLanguages( private async Task AddLanguages(
ISearchRepository searchRepository, ISearchRepository searchRepository,
Document doc, Document doc,
IEnumerable<MediaVersion> mediaVersions) ICollection<MediaVersion> mediaVersions)
{ {
var mediaCodes = mediaVersions var mediaCodes = mediaVersions
.Map(mv => mv.Streams.Filter(ms => ms.MediaStreamKind == MediaStreamKind.Audio).Map(ms => ms.Language)) .Map(mv => mv.Streams.Filter(ms => ms.MediaStreamKind == MediaStreamKind.Audio).Map(ms => ms.Language))
@ -511,6 +513,15 @@ public sealed class LuceneSearchIndex : ISearchIndex
.ToList(); .ToList();
await AddLanguages(searchRepository, doc, mediaCodes); await AddLanguages(searchRepository, doc, mediaCodes);
var subMediaCodes = mediaVersions
.Map(mv => mv.Streams.Filter(ms => ms.MediaStreamKind == MediaStreamKind.Subtitle).Map(ms => ms.Language))
.Flatten()
.Filter(c => !string.IsNullOrWhiteSpace(c))
.Distinct()
.ToList();
await AddSubLanguages(searchRepository, doc, subMediaCodes);
} }
private async Task AddLanguages(ISearchRepository searchRepository, Document doc, List<string> mediaCodes) private async Task AddLanguages(ISearchRepository searchRepository, Document doc, List<string> mediaCodes)
@ -536,6 +547,30 @@ public sealed class LuceneSearchIndex : ISearchIndex
doc.Add(new TextField(LanguageField, englishName, Field.Store.NO)); doc.Add(new TextField(LanguageField, englishName, Field.Store.NO));
} }
} }
private async Task AddSubLanguages(ISearchRepository searchRepository, Document doc, List<string> mediaCodes)
{
foreach (string code in mediaCodes.Where(c => !string.IsNullOrWhiteSpace(c)).Distinct())
{
doc.Add(new TextField(SubLanguageTagField, code, Field.Store.NO));
}
var englishNames = new System.Collections.Generic.HashSet<string>();
foreach (string code in await searchRepository.GetAllLanguageCodes(mediaCodes))
{
Option<CultureInfo> maybeCultureInfo = _cultureInfos.Find(
ci => string.Equals(ci.ThreeLetterISOLanguageName, code, StringComparison.OrdinalIgnoreCase));
foreach (CultureInfo cultureInfo in maybeCultureInfo)
{
englishNames.Add(cultureInfo.EnglishName);
}
}
foreach (string englishName in englishNames)
{
doc.Add(new TextField(SubLanguageField, englishName, Field.Store.NO));
}
}
private async Task UpdateShow(ISearchRepository searchRepository, Show show) private async Task UpdateShow(ISearchRepository searchRepository, Show show)
{ {
@ -566,6 +601,9 @@ public sealed class LuceneSearchIndex : ISearchIndex
List<string> languages = await searchRepository.GetLanguagesForShow(show); List<string> languages = await searchRepository.GetLanguagesForShow(show);
await AddLanguages(searchRepository, doc, languages); await AddLanguages(searchRepository, doc, languages);
List<string> subLanguages = await searchRepository.GetSubLanguagesForShow(show);
await AddSubLanguages(searchRepository, doc, subLanguages);
if (!string.IsNullOrWhiteSpace(metadata.ContentRating)) if (!string.IsNullOrWhiteSpace(metadata.ContentRating))
{ {
foreach (string contentRating in (metadata.ContentRating ?? string.Empty).Split("/") foreach (string contentRating in (metadata.ContentRating ?? string.Empty).Split("/")
@ -690,6 +728,9 @@ public sealed class LuceneSearchIndex : ISearchIndex
List<string> languages = await searchRepository.GetLanguagesForSeason(season); List<string> languages = await searchRepository.GetLanguagesForSeason(season);
await AddLanguages(searchRepository, doc, languages); await AddLanguages(searchRepository, doc, languages);
List<string> subLanguages = await searchRepository.GetSubLanguagesForSeason(season);
await AddSubLanguages(searchRepository, doc, subLanguages);
if (!string.IsNullOrWhiteSpace(showMetadata.ContentRating)) if (!string.IsNullOrWhiteSpace(showMetadata.ContentRating))
{ {
foreach (string contentRating in (showMetadata.ContentRating ?? string.Empty).Split("/") foreach (string contentRating in (showMetadata.ContentRating ?? string.Empty).Split("/")
@ -768,6 +809,9 @@ public sealed class LuceneSearchIndex : ISearchIndex
List<string> languages = await searchRepository.GetLanguagesForArtist(artist); List<string> languages = await searchRepository.GetLanguagesForArtist(artist);
await AddLanguages(searchRepository, doc, languages); await AddLanguages(searchRepository, doc, languages);
List<string> subLanguages = await searchRepository.GetSubLanguagesForArtist(artist);
await AddSubLanguages(searchRepository, doc, subLanguages);
doc.Add( doc.Add(
new StringField( new StringField(
AddedDateField, AddedDateField,

6
ErsatzTV.Infrastructure/Search/Models/ElasticSearchItem.cs

@ -31,6 +31,12 @@ public class ElasticSearchItem : MinimalElasticSearchItem
[JsonPropertyName(LuceneSearchIndex.LanguageTagField)] [JsonPropertyName(LuceneSearchIndex.LanguageTagField)]
public List<string> LanguageTag { get; set; } public List<string> LanguageTag { get; set; }
[JsonPropertyName(LuceneSearchIndex.SubLanguageField)]
public List<string> SubLanguage { get; set; }
[JsonPropertyName(LuceneSearchIndex.SubLanguageTagField)]
public List<string> SubLanguageTag { get; set; }
[JsonPropertyName(LuceneSearchIndex.MinutesField)] [JsonPropertyName(LuceneSearchIndex.MinutesField)]
public int Minutes { get; set; } public int Minutes { get; set; }

2
ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj

@ -15,7 +15,7 @@
<PackageReference Include="NSubstitute" Version="5.1.0" /> <PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="NUnit" Version="4.0.1" /> <PackageReference Include="NUnit" Version="4.0.1" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit.Analyzers" Version="3.10.0"> <PackageReference Include="NUnit.Analyzers" Version="4.0.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>

22
ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs

@ -307,20 +307,26 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
{ {
if (ServerSupportsRemoteStreaming) if (ServerSupportsRemoteStreaming)
{ {
foreach (int id in await movieRepository.FlagRemoteOnly(library, incoming)) if (existingState is not MediaItemState.RemoteOnly)
{ {
await _mediator.Publish( foreach (int id in await movieRepository.FlagRemoteOnly(library, incoming))
new ScannerProgressUpdate(library.Id, null, null, new[] { id }, Array.Empty<int>()), {
CancellationToken.None); await _mediator.Publish(
new ScannerProgressUpdate(library.Id, null, null, new[] { id }, Array.Empty<int>()),
CancellationToken.None);
}
} }
} }
else else
{ {
foreach (int id in await movieRepository.FlagUnavailable(library, incoming)) if (existingState is not MediaItemState.Unavailable)
{ {
await _mediator.Publish( foreach (int id in await movieRepository.FlagUnavailable(library, incoming))
new ScannerProgressUpdate(library.Id, null, null, new[] { id }, Array.Empty<int>()), {
CancellationToken.None); await _mediator.Publish(
new ScannerProgressUpdate(library.Id, null, null, new[] { id }, Array.Empty<int>()),
CancellationToken.None);
}
} }
} }
} }

22
ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs

@ -584,20 +584,26 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
{ {
if (ServerSupportsRemoteStreaming) if (ServerSupportsRemoteStreaming)
{ {
foreach (int id in await televisionRepository.FlagRemoteOnly(library, incoming)) if (existingState is not MediaItemState.RemoteOnly)
{ {
await _mediator.Publish( foreach (int id in await televisionRepository.FlagRemoteOnly(library, incoming))
new ScannerProgressUpdate(library.Id, null, null, new[] { id }, Array.Empty<int>()), {
CancellationToken.None); await _mediator.Publish(
new ScannerProgressUpdate(library.Id, null, null, new[] { id }, Array.Empty<int>()),
CancellationToken.None);
}
} }
} }
else else
{ {
foreach (int id in await televisionRepository.FlagUnavailable(library, incoming)) if (existingState is not MediaItemState.Unavailable)
{ {
await _mediator.Publish( foreach (int id in await televisionRepository.FlagUnavailable(library, incoming))
new ScannerProgressUpdate(library.Id, null, null, new[] { id }, Array.Empty<int>()), {
CancellationToken.None); await _mediator.Publish(
new ScannerProgressUpdate(library.Id, null, null, new[] { id }, Array.Empty<int>()),
CancellationToken.None);
}
} }
} }
} }

4
ErsatzTV.Scanner/ErsatzTV.Scanner.csproj

@ -20,7 +20,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CliWrap" Version="3.6.4" /> <PackageReference Include="CliWrap" Version="3.6.6" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" /> <PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="LanguageExt.Core" Version="4.4.7" /> <PackageReference Include="LanguageExt.Core" Version="4.4.7" />
<PackageReference Include="MediatR" Version="12.2.0" /> <PackageReference Include="MediatR" Version="12.2.0" />
@ -28,7 +28,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageReference Include="Serilog" Version="3.1.1" /> <PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" /> <PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Formatting.Compact" Version="2.0.0" /> <PackageReference Include="Serilog.Formatting.Compact" Version="2.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" /> <PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />

6
ErsatzTV/ErsatzTV.csproj

@ -21,7 +21,7 @@
<PackageReference Include="FluentValidation" Version="11.9.0" /> <PackageReference Include="FluentValidation" Version="11.9.0" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" /> <PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
<PackageReference Include="Heron.MudCalendar" Version="1.1.0" /> <PackageReference Include="Heron.MudCalendar" Version="1.1.0" />
<PackageReference Include="HtmlSanitizer" Version="8.0.811" /> <PackageReference Include="HtmlSanitizer" Version="8.0.838" />
<PackageReference Include="LanguageExt.Core" Version="4.4.7" /> <PackageReference Include="LanguageExt.Core" Version="4.4.7" />
<PackageReference Include="Markdig" Version="0.34.0" /> <PackageReference Include="Markdig" Version="0.34.0" />
<PackageReference Include="MediatR.Courier.DependencyInjection" Version="5.0.0" /> <PackageReference Include="MediatR.Courier.DependencyInjection" Version="5.0.0" />
@ -37,11 +37,11 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="MudBlazor" Version="6.12.0" /> <PackageReference Include="MudBlazor" Version="6.15.0" />
<PackageReference Include="NaturalSort.Extension" Version="4.2.0" /> <PackageReference Include="NaturalSort.Extension" Version="4.2.0" />
<PackageReference Include="Refit.HttpClientFactory" Version="7.0.0" /> <PackageReference Include="Refit.HttpClientFactory" Version="7.0.0" />
<PackageReference Include="Serilog" Version="3.1.1" /> <PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.0" /> <PackageReference Include="Serilog.Settings.Configuration" Version="8.0.0" />
<PackageReference Include="System.IO.FileSystem.Primitives" Version="4.3.0" /> <PackageReference Include="System.IO.FileSystem.Primitives" Version="4.3.0" />
<PackageReference Include="System.Text.Encoding.Extensions" Version="4.3.0" /> <PackageReference Include="System.Text.Encoding.Extensions" Version="4.3.0" />

Loading…
Cancel
Save