5 Commits

Author SHA1 Message Date
fa52312228 Merge pull request 'chore(main): release 0.0.15' (#16) from release-please--branches--main into main
All checks were successful
Create Release PR / Create Release PR (push) Has been skipped
Build and Publish Plugin / Build Plugin + Update Manifest (release) Successful in 44s
Reviewed-on: #16
2026-03-01 18:48:56 +01:00
Gitea Actions
07cd31097a chore(main): release 0.0.15
All checks were successful
Create Release / Publish Release (pull_request) Successful in 7s
2026-03-01 17:45:54 +00:00
fe78850872 Merge branch 'main' of https://git.tdpi.dev/TDPI/jellyfin-plugin-smartnotify
All checks were successful
Create Release PR / Create Release PR (push) Successful in 11s
2026-03-01 18:45:45 +01:00
f2903719a3 fix: timestamps! 2026-03-01 18:45:43 +01:00
Gitea Actions
7fcc917ca5 chore: update manifest for v0.0.14
All checks were successful
Create Release PR / Create Release PR (push) Has been skipped
2026-03-01 17:36:26 +00:00
6 changed files with 149 additions and 12 deletions

View File

@@ -1,3 +1,3 @@
{
".": "0.0.14"
".": "0.0.15"
}

View File

@@ -1,5 +1,16 @@
# Changelog
## 0.0.15 (2026-03-01)
### Bug Fixes
* fix: timestamps!
### Chores
* chore: update manifest for v0.0.14
## 0.0.14 (2026-03-01)
### Bug Fixes

View File

@@ -3,8 +3,8 @@
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<RootNamespace>Jellyfin.Plugin.SmartNotify</RootNamespace>
<AssemblyVersion>0.0.14.0</AssemblyVersion>
<FileVersion>0.0.14.0</FileVersion>
<AssemblyVersion>0.0.15.0</AssemblyVersion>
<FileVersion>0.0.15.0</FileVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>

View File

@@ -231,6 +231,76 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable
_logger.LogDebug("Item removed: {Name} (ID: {Id})", e.Item.Name, e.Item.Id);
}
/// <summary>
/// Refreshes notification metadata from the library (series info may not be available at queue time).
/// </summary>
private void RefreshNotificationMetadata(PendingNotification notification)
{
if (!Guid.TryParse(notification.JellyfinItemId, out var itemId) || itemId == Guid.Empty)
{
return;
}
var item = _libraryManager.GetItemById(itemId);
if (item is not Episode episode)
{
return;
}
var changed = false;
if (string.IsNullOrEmpty(notification.SeriesName) || notification.SeriesName == "Unknown Series")
{
notification.SeriesName = episode.SeriesName;
changed = true;
}
if (string.IsNullOrEmpty(notification.SeriesId) || notification.SeriesId == Guid.Empty.ToString())
{
if (episode.SeriesId != Guid.Empty)
{
notification.SeriesId = episode.SeriesId.ToString();
changed = true;
}
}
if (!notification.SeasonNumber.HasValue && episode.ParentIndexNumber.HasValue)
{
notification.SeasonNumber = episode.ParentIndexNumber;
changed = true;
}
if (!notification.EpisodeNumber.HasValue && episode.IndexNumber.HasValue)
{
notification.EpisodeNumber = episode.IndexNumber;
changed = true;
}
// Refresh image if missing
if (string.IsNullOrEmpty(notification.ImagePath) && episode.SeriesId != Guid.Empty)
{
var series = _libraryManager.GetItemById(episode.SeriesId);
var seriesImage = series?.GetImagePath(ImageType.Primary, 0);
if (!string.IsNullOrEmpty(seriesImage))
{
notification.ImagePath = seriesImage;
changed = true;
}
}
if (changed)
{
_historyService.UpdateNotification(notification);
_logger.LogInformation(
"Refreshed metadata for {Name}: Series={SeriesName}, SeriesId={SeriesId}, S{Season}E{Episode}",
notification.Name,
notification.SeriesName,
notification.SeriesId,
notification.SeasonNumber,
notification.EpisodeNumber);
}
}
/// <summary>
/// Processes pending notifications (called by timer).
/// </summary>
@@ -261,21 +331,29 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable
groupingWindowMinutes,
cutoff);
// Refresh metadata for episodes that were queued before metadata was available
foreach (var notification in pendingNotifications.Where(n => n.ItemType == "Episode"))
{
RefreshNotificationMetadata(notification);
}
// Group episodes by series
var emptyGuid = Guid.Empty.ToString();
var episodesBySeries = pendingNotifications
.Where(n => n.ItemType == "Episode" && !string.IsNullOrEmpty(n.SeriesId))
.Where(n => n.ItemType == "Episode" && !string.IsNullOrEmpty(n.SeriesId) && n.SeriesId != emptyGuid)
.GroupBy(n => n.SeriesId!)
.ToList();
// Log unmatched notifications (neither episode with series nor movie)
var unmatchedCount = pendingNotifications.Count
- pendingNotifications.Count(n => n.ItemType == "Episode" && !string.IsNullOrEmpty(n.SeriesId))
- pendingNotifications.Count(n => n.ItemType == "Movie");
if (unmatchedCount > 0)
// Handle episodes that still have no series info (send individually as fallback)
var orphanEpisodes = pendingNotifications
.Where(n => n.ItemType == "Episode" && (string.IsNullOrEmpty(n.SeriesId) || n.SeriesId == emptyGuid))
.ToList();
if (orphanEpisodes.Count > 0)
{
_logger.LogWarning(
"{Count} notifications are neither episodes (with SeriesId) nor movies and will be skipped",
unmatchedCount);
"{Count} episode notifications have no series info even after refresh",
orphanEpisodes.Count);
}
// Process each series group
@@ -317,6 +395,31 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable
}
}
// Process orphan episodes (no series info - send individually)
foreach (var orphan in orphanEpisodes)
{
var oldAge = DateTime.UtcNow.AddMinutes(-groupingWindowMinutes);
if (orphan.QueuedAt > oldAge)
{
continue;
}
_logger.LogInformation("Sending individual episode notification for: {Name} (no series info)", orphan.Name);
var success = await _discordService.SendGroupedEpisodeNotificationAsync(
new[] { orphan },
CancellationToken.None);
if (success)
{
_historyService.RemoveNotifications(new[] { orphan.Id });
}
else
{
_logger.LogWarning("Discord send failed for orphan episode {Name}, keeping in queue for retry", orphan.Name);
}
}
// Process movies
var movies = pendingNotifications
.Where(n => n.ItemType == "Movie")

View File

@@ -38,7 +38,13 @@ public class ItemHistoryService : IDisposable
var dbPath = Path.Combine(pluginDataPath, "smartnotify.db");
_logger.LogInformation("SmartNotify database path: {Path}", dbPath);
_database = new LiteDatabase(dbPath);
// Use UTC for all DateTime to avoid container timezone vs Jellyfin time mismatches
var mapper = new BsonMapper { SerializeNullValues = false };
mapper.RegisterType<DateTime>(
serialize: value => new BsonValue(value.ToUniversalTime()),
deserialize: bson => bson.AsDateTime.ToUniversalTime());
_database = new LiteDatabase(dbPath, mapper);
_knownItems = _database.GetCollection<KnownMediaItem>("known_items");
_pendingNotifications = _database.GetCollection<PendingNotification>("pending_notifications");
@@ -295,6 +301,15 @@ public class ItemHistoryService : IDisposable
return _pendingNotifications.Find(x => x.QueuedAt <= olderThan);
}
/// <summary>
/// Updates an existing notification in the queue (e.g. after metadata refresh).
/// </summary>
/// <param name="notification">The notification to update.</param>
public void UpdateNotification(PendingNotification notification)
{
_pendingNotifications.Update(notification);
}
/// <summary>
/// Removes processed notifications.
/// </summary>

View File

@@ -8,6 +8,14 @@
"category": "Notifications",
"imageUrl": "",
"versions": [
{
"version": "0.0.14.0",
"changelog": "### Bug Fixes\n\n* fix: claude hat bugs im Kopf\n\n### Chores\n\n* chore: update manifest for v0.0.13",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.tdpi.dev/TDPI/jellyfin-plugin-smartnotify/releases/download/v0.0.14/smartnotify_0.0.14.zip",
"checksum": "428e0cc3a00c873381dc2f2f198f3907",
"timestamp": "2026-03-01T17:36:26Z"
},
{
"version": "0.0.13.0",
"changelog": "### Bug Fixes\n\n* fix: removed drecks versionen\n* fix: Bilder und dumme min TImings!\n\n### Chores\n\n* chore: update manifest for v0.0.12",