From 872ab2ee458090869996513100fde3fe037ecda5 Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Sun, 6 Sep 2026 12:20:01 -0500 Subject: [PATCH] fix: season scanning fixes --- CHANGELOG.md | 3 +++ ErsatzTV.Application/Television/Mapper.cs | 7 ++++++- .../Repositories/EmbyTelevisionRepository.cs | 14 ++++++++++++++ .../JellyfinTelevisionRepository.cs | 19 +++++++++++++++++-- .../Repositories/PlexTelevisionRepository.cs | 14 ++++++++++++++ ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs | 5 ++++- .../Emby/Models/EmbyLibraryItemResponse.cs | 1 + ErsatzTV/Pages/Artist.razor | 2 +- ErsatzTV/Pages/Movie.razor | 2 +- ErsatzTV/Pages/TelevisionEpisodeList.razor | 2 +- ErsatzTV/Pages/TelevisionSeasonList.razor | 2 +- ErsatzTV/wwwroot/css/site.css | 5 +++++ 12 files changed, 68 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1a14879c..c75377ec3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/ErsatzTV.Application/Television/Mapper.cs b/ErsatzTV.Application/Television/Mapper.cs index a175dd4a0..ba223ba41 100644 --- a/ErsatzTV.Application/Television/Mapper.cs +++ b/ErsatzTV.Application/Television/Mapper.cs @@ -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)); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs index 88b68aff9..2dd8fb797 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs @@ -179,6 +179,20 @@ public class EmbyTelevisionRepository( foreach (EmbyEpisode embyEpisode in maybeExisting) { var result = new MediaItemScanResult(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); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs index b7f07b5df..0f21bbfb7 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs @@ -165,13 +165,27 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository Option 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 result; if (existingState.Etag != item.Etag || deepScan) { @@ -212,7 +226,8 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository .AsSplitQuery() .SingleAsync(s => s.Id == existingId, cancellationToken); - result = new MediaItemScanResult(existing) { IsAdded = false }; + result = new MediaItemScanResult(existing) + { IsAdded = false, IsUpdated = isUpdated }; } return result; diff --git a/ErsatzTV.Infrastructure/Data/Repositories/PlexTelevisionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/PlexTelevisionRepository.cs index 83a8d7d2c..ffd859c8e 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/PlexTelevisionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/PlexTelevisionRepository.cs @@ -355,6 +355,20 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository foreach (PlexEpisode plexEpisode in maybeExisting) { var result = new MediaItemScanResult(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)) diff --git a/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs b/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs index b375980e6..3909a0681 100644 --- a/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs +++ b/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs @@ -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.None + : maybeLibrary.Map(lib => ProjectToEpisode(lib, item)).Flatten()); public IAsyncEnumerable> GetCollectionLibraryItems(string address, string apiKey) { diff --git a/ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemResponse.cs b/ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemResponse.cs index 84f72b084..f897a0928 100644 --- a/ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemResponse.cs +++ b/ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemResponse.cs @@ -23,6 +23,7 @@ public class EmbyLibraryItemResponse public EmbyImageTagsResponse ImageTags { get; set; } public List BackdropImageTags { get; set; } public int? IndexNumber { get; set; } + public string SeasonId { get; set; } public string Type { get; set; } public IList Chapters { get; set; } } diff --git a/ErsatzTV/Pages/Artist.razor b/ErsatzTV/Pages/Artist.razor index be6674555..8c92b60d0 100644 --- a/ErsatzTV/Pages/Artist.razor +++ b/ErsatzTV/Pages/Artist.razor @@ -135,7 +135,7 @@ - + @foreach (MusicVideoCardViewModel musicVideo in _musicVideos.Cards) { diff --git a/ErsatzTV/Pages/Movie.razor b/ErsatzTV/Pages/Movie.razor index 0049a98cb..ac9bb0ccc 100644 --- a/ErsatzTV/Pages/Movie.razor +++ b/ErsatzTV/Pages/Movie.razor @@ -204,7 +204,7 @@ @if (_movie is not null && _movie.Actors.Any()) { - + Actors @foreach (ActorCardViewModel actor in _movie.Actors) diff --git a/ErsatzTV/Pages/TelevisionEpisodeList.razor b/ErsatzTV/Pages/TelevisionEpisodeList.razor index 6c71191d6..01abb138b 100644 --- a/ErsatzTV/Pages/TelevisionEpisodeList.razor +++ b/ErsatzTV/Pages/TelevisionEpisodeList.razor @@ -68,7 +68,7 @@ - + @foreach (TelevisionEpisodeCardViewModel episode in _data.Cards) { diff --git a/ErsatzTV/Pages/TelevisionSeasonList.razor b/ErsatzTV/Pages/TelevisionSeasonList.razor index 46c415e86..bd14e6fd4 100644 --- a/ErsatzTV/Pages/TelevisionSeasonList.razor +++ b/ErsatzTV/Pages/TelevisionSeasonList.razor @@ -151,7 +151,7 @@ - + Seasons @foreach (TelevisionSeasonCardViewModel card in _data.Cards) diff --git a/ErsatzTV/wwwroot/css/site.css b/ErsatzTV/wwwroot/css/site.css index a75f6d322..ea38d5319 100644 --- a/ErsatzTV/wwwroot/css/site.css +++ b/ErsatzTV/wwwroot/css/site.css @@ -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;