Browse Source

fix: season scanning fixes

pull/3005/head
Jason Dove 1 week ago
parent
commit
872ab2ee45
No known key found for this signature in database
  1. 3
      CHANGELOG.md
  2. 7
      ErsatzTV.Application/Television/Mapper.cs
  3. 14
      ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs
  4. 19
      ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs
  5. 14
      ErsatzTV.Infrastructure/Data/Repositories/PlexTelevisionRepository.cs
  6. 5
      ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs
  7. 1
      ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemResponse.cs
  8. 2
      ErsatzTV/Pages/Artist.razor
  9. 2
      ErsatzTV/Pages/Movie.razor
  10. 2
      ErsatzTV/Pages/TelevisionEpisodeList.razor
  11. 2
      ErsatzTV/Pages/TelevisionSeasonList.razor
  12. 5
      ErsatzTV/wwwroot/css/site.css

3
CHANGELOG.md

@ -35,6 +35,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -35,6 +35,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix seeking more than 24 hours into content
- Fix local movie and television folder scanner to only set etag when all files process successfully
- Previously, errors would be ignored so problematic files (e.g. malformed nfo files) would need to be touched for ETV to attempt reading them again
- Fix library collisions caused by Emby returning specials within normal seasons
- Fix Plex, Emby, Jellyfin episode updates when season has changed but episode is otherwise unchanged
- Fix episode title being hidden behind fanart in ETV UI, caused by missing season artwork
## [26.8.1] - 2026-08-29
### Security

7
ErsatzTV.Application/Television/Mapper.cs

@ -59,7 +59,12 @@ internal static class Mapper @@ -59,7 +59,12 @@ internal static class Mapper
.Map(m => m.Year?.ToString(CultureInfo.InvariantCulture) ?? string.Empty).IfNone(string.Empty),
season.SeasonNumber == 0 ? "Specials" : $"Season {season.SeasonNumber}",
season.SeasonMetadata.HeadOrNone().Map(m => GetPoster(m, maybeJellyfin, maybeEmby))
.IfNone(string.Empty),
.Filter(poster => !string.IsNullOrWhiteSpace(poster))
// media servers often have no artwork for a specials season
.IfNone(
() => season.Show.ShowMetadata.HeadOrNone()
.Map(m => GetPoster(m, maybeJellyfin, maybeEmby))
.IfNone(string.Empty)),
season.Show.ShowMetadata.HeadOrNone().Map(m => GetFanArt(m, maybeJellyfin, maybeEmby))
.IfNone(string.Empty));

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

@ -179,6 +179,20 @@ public class EmbyTelevisionRepository( @@ -179,6 +179,20 @@ public class EmbyTelevisionRepository(
foreach (EmbyEpisode embyEpisode in maybeExisting)
{
var result = new MediaItemScanResult<EmbyEpisode>(embyEpisode) { IsAdded = false };
// season id can change without etag changing
if (item.SeasonId != 0 && embyEpisode.SeasonId != item.SeasonId)
{
await dbContext.EmbyEpisodes
.Where(ee => ee.Id == embyEpisode.Id)
.ExecuteUpdateAsync(
setters => setters.SetProperty(ee => ee.SeasonId, item.SeasonId),
cancellationToken);
result.Item.SeasonId = item.SeasonId;
result.IsUpdated = true;
}
if (embyEpisode.Etag != item.Etag || deepScan)
{
await UpdateEpisode(dbContext, embyEpisode, item, cancellationToken);

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

@ -165,13 +165,27 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository @@ -165,13 +165,27 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
Option<dynamic> maybeExistingState = await dbContext.JellyfinEpisodes
.TagWithCallSite()
.Where(s => s.ItemId == item.ItemId)
.Select(s => new { s.Id, s.Etag })
.Select(s => new { s.Id, s.SeasonId, s.Etag })
.SingleOrDefaultAsync(cancellationToken);
foreach (dynamic existingState in maybeExistingState)
{
int existingId = existingState.Id;
var isUpdated = false;
// season id can change without etag changing
if (item.SeasonId != 0 && existingState.SeasonId != item.SeasonId)
{
await dbContext.JellyfinEpisodes
.Where(je => je.Id == existingId)
.ExecuteUpdateAsync(
setters => setters.SetProperty(ee => ee.SeasonId, item.SeasonId),
cancellationToken);
isUpdated = true;
}
MediaItemScanResult<JellyfinEpisode> result;
if (existingState.Etag != item.Etag || deepScan)
{
@ -212,7 +226,8 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository @@ -212,7 +226,8 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
.AsSplitQuery()
.SingleAsync(s => s.Id == existingId, cancellationToken);
result = new MediaItemScanResult<JellyfinEpisode>(existing) { IsAdded = false };
result = new MediaItemScanResult<JellyfinEpisode>(existing)
{ IsAdded = false, IsUpdated = isUpdated };
}
return result;

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

@ -355,6 +355,20 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository @@ -355,6 +355,20 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
foreach (PlexEpisode plexEpisode in maybeExisting)
{
var result = new MediaItemScanResult<PlexEpisode>(plexEpisode) { IsAdded = false };
// season id can change without etag changing
if (item.SeasonId != 0 && plexEpisode.SeasonId != item.SeasonId)
{
await dbContext.PlexEpisodes
.Where(pe => pe.Id == plexEpisode.Id)
.ExecuteUpdateAsync(
setters => setters.SetProperty(ee => ee.SeasonId, item.SeasonId),
cancellationToken);
result.Item.SeasonId = item.SeasonId;
result.IsUpdated = true;
}
if (plexEpisode.Etag != item.Etag || deepScan)
{
foreach (BaseError error in await UpdateEpisode(dbContext, plexEpisode, item, cancellationToken))

5
ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs

@ -129,7 +129,10 @@ public class EmbyApiClient : IEmbyApiClient @@ -129,7 +129,10 @@ public class EmbyApiClient : IEmbyApiClient
seasonId,
startIndex: skip,
limit: pageSize),
(maybeLibrary, item) => maybeLibrary.Map(lib => ProjectToEpisode(lib, item)).Flatten());
// ignore anything emby returns with the wrong season (i.e. specials)
(maybeLibrary, item) => item.SeasonId is not null && item.SeasonId != seasonId
? Option<EmbyEpisode>.None
: maybeLibrary.Map(lib => ProjectToEpisode(lib, item)).Flatten());
public IAsyncEnumerable<Tuple<EmbyCollection, int>> GetCollectionLibraryItems(string address, string apiKey)
{

1
ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemResponse.cs

@ -23,6 +23,7 @@ public class EmbyLibraryItemResponse @@ -23,6 +23,7 @@ public class EmbyLibraryItemResponse
public EmbyImageTagsResponse ImageTags { get; set; }
public List<string> BackdropImageTags { get; set; }
public int? IndexNumber { get; set; }
public string SeasonId { get; set; }
public string Type { get; set; }
public IList<EmbyChapterResponse> Chapters { get; set; }
}

2
ErsatzTV/Pages/Artist.razor

@ -135,7 +135,7 @@ @@ -135,7 +135,7 @@
</MudCardContent>
</MudCard>
</MudContainer>
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8 above-fanart">
<MudStack Row="false" Spacing="6">
@foreach (MusicVideoCardViewModel musicVideo in _musicVideos.Cards)
{

2
ErsatzTV/Pages/Movie.razor

@ -204,7 +204,7 @@ @@ -204,7 +204,7 @@
</MudContainer>
@if (_movie is not null && _movie.Actors.Any())
{
<MudContainer MaxWidth="MaxWidth.Large">
<MudContainer MaxWidth="MaxWidth.Large" Class="above-fanart">
<MudText Class="mb-4">Actors</MudText>
<MudStack Row="true" Wrap="Wrap.Wrap">
@foreach (ActorCardViewModel actor in _movie.Actors)

2
ErsatzTV/Pages/TelevisionEpisodeList.razor

@ -68,7 +68,7 @@ @@ -68,7 +68,7 @@
</MudStack>
</MudStack>
</MudContainer>
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8 above-fanart">
<MudStack Row="false" Spacing="6">
@foreach (TelevisionEpisodeCardViewModel episode in _data.Cards)
{

2
ErsatzTV/Pages/TelevisionSeasonList.razor

@ -151,7 +151,7 @@ @@ -151,7 +151,7 @@
</MudCardContent>
</MudCard>
</MudContainer>
<MudContainer MaxWidth="MaxWidth.Large">
<MudContainer MaxWidth="MaxWidth.Large" Class="above-fanart">
<MudText Class="mb-4">Seasons</MudText>
<MudStack Row="true" Wrap="Wrap.Wrap">
@foreach (TelevisionSeasonCardViewModel card in _data.Cards)

5
ErsatzTV/wwwroot/css/site.css

@ -116,6 +116,11 @@ div.ersatztv-light { @@ -116,6 +116,11 @@ div.ersatztv-light {
z-index: 1;
}
.above-fanart {
position: relative;
z-index: 10;
}
.media-item-title {
color: #fff;
text-shadow: 1px 1px 5px #000;

Loading…
Cancel
Save