Compare commits
12 Commits
v0.0.16
...
27da8c90f7
| Author | SHA1 | Date | |
|---|---|---|---|
| 27da8c90f7 | |||
| 96d67a8655 | |||
|
|
62a7547688 | ||
| 047dd82f3f | |||
|
|
76fb874b4d | ||
| 7fadc75c84 | |||
| d595b16573 | |||
| 20f603b4ee | |||
|
|
798cdcaf9c | ||
| af6ddeac0d | |||
| 87f1eff7df | |||
|
|
97a7bc5422 |
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "0.0.16"
|
||||
".": "0.0.18"
|
||||
}
|
||||
18
CHANGELOG.md
18
CHANGELOG.md
@@ -1,5 +1,23 @@
|
||||
# Changelog
|
||||
|
||||
## 0.0.18 (2026-03-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix: build error
|
||||
|
||||
|
||||
## 0.0.17 (2026-03-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix: removed notifications on reorganization
|
||||
|
||||
### Chores
|
||||
|
||||
* chore: update manifest for v0.0.16
|
||||
|
||||
|
||||
## 0.0.16 (2026-03-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<RootNamespace>Jellyfin.Plugin.SmartNotify</RootNamespace>
|
||||
<AssemblyVersion>0.0.16.0</AssemblyVersion>
|
||||
<FileVersion>0.0.16.0</FileVersion>
|
||||
<AssemblyVersion>0.0.18.0</AssemblyVersion>
|
||||
<FileVersion>0.0.18.0</FileVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Timers;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Plugin.SmartNotify.Services;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
@@ -47,6 +49,10 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable
|
||||
{
|
||||
_logger.LogInformation("SmartNotify background service starting");
|
||||
|
||||
// Pre-populate DB with all existing library items so they're recognized as "known".
|
||||
// This prevents mass notifications on first run or after DB reset.
|
||||
SeedExistingLibraryItems();
|
||||
|
||||
// Subscribe to library events
|
||||
_libraryManager.ItemAdded += OnItemAdded;
|
||||
_libraryManager.ItemRemoved += OnItemRemoved;
|
||||
@@ -62,6 +68,50 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seeds the database with all existing Episodes and Movies from the library.
|
||||
/// Runs once at startup — only records items not yet in the DB.
|
||||
/// </summary>
|
||||
private void SeedExistingLibraryItems()
|
||||
{
|
||||
try
|
||||
{
|
||||
var query = new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { BaseItemKind.Episode, BaseItemKind.Movie },
|
||||
IsVirtualItem = false,
|
||||
Recursive = true
|
||||
};
|
||||
|
||||
var existingItems = _libraryManager.GetItemList(query);
|
||||
var seeded = 0;
|
||||
var alreadyKnown = 0;
|
||||
|
||||
foreach (var item in existingItems)
|
||||
{
|
||||
if (!_historyService.IsKnownItem(item.Id))
|
||||
{
|
||||
_historyService.RecordItem(item);
|
||||
seeded++;
|
||||
}
|
||||
else
|
||||
{
|
||||
alreadyKnown++;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"[DEBUG Seed] Seeded {Seeded} new items, {AlreadyKnown} already known, {Total} total in library",
|
||||
seeded,
|
||||
alreadyKnown,
|
||||
existingItems.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error seeding existing library items");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -96,6 +146,60 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
// Debug: log all available metadata on the item at ItemAdded time
|
||||
if (item is Episode debugEp)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"[DEBUG ItemAdded] Episode: Name={Name}, Id={Id}, Path={Path}, " +
|
||||
"SeriesName={SeriesName}, SeriesId={SeriesId}, " +
|
||||
"Season={Season}, Episode={Episode}, " +
|
||||
"ProviderIds={ProviderIds}, " +
|
||||
"DateCreated={DateCreated}, PremiereDate={PremiereDate}",
|
||||
debugEp.Name,
|
||||
debugEp.Id,
|
||||
debugEp.Path,
|
||||
debugEp.SeriesName,
|
||||
debugEp.SeriesId,
|
||||
debugEp.ParentIndexNumber,
|
||||
debugEp.IndexNumber,
|
||||
debugEp.ProviderIds != null ? System.Text.Json.JsonSerializer.Serialize(debugEp.ProviderIds) : "null",
|
||||
debugEp.DateCreated,
|
||||
debugEp.PremiereDate);
|
||||
|
||||
// Also try to access the Series object directly
|
||||
try
|
||||
{
|
||||
var debugSeries = debugEp.Series;
|
||||
if (debugSeries != null)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"[DEBUG ItemAdded] Series object found: Name={Name}, Id={Id}, ProviderIds={ProviderIds}",
|
||||
debugSeries.Name,
|
||||
debugSeries.Id,
|
||||
debugSeries.ProviderIds != null ? System.Text.Json.JsonSerializer.Serialize(debugSeries.ProviderIds) : "null");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("[DEBUG ItemAdded] Series object is NULL for episode {Name}", debugEp.Name);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "[DEBUG ItemAdded] Failed to access Series object for {Name}", debugEp.Name);
|
||||
}
|
||||
}
|
||||
else if (item is Movie debugMovie)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"[DEBUG ItemAdded] Movie: Name={Name}, Id={Id}, Path={Path}, " +
|
||||
"ProviderIds={ProviderIds}, Year={Year}",
|
||||
debugMovie.Name,
|
||||
debugMovie.Id,
|
||||
debugMovie.Path,
|
||||
debugMovie.ProviderIds != null ? System.Text.Json.JsonSerializer.Serialize(debugMovie.ProviderIds) : "null",
|
||||
debugMovie.ProductionYear);
|
||||
}
|
||||
|
||||
_logger.LogDebug("Item added: {Name} (Type: {Type}, ID: {Id})", item.Name, item.GetType().Name, item.Id);
|
||||
|
||||
var config = Plugin.Instance?.Configuration;
|
||||
@@ -136,13 +240,11 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
// Check 0: Is this exact item (same Jellyfin ID) already known?
|
||||
// This catches metadata changes and library re-scans where no file actually changed.
|
||||
// Jellyfin fires ItemAdded again for existing items during metadata refreshes.
|
||||
// Check 0: Is this exact item (same Jellyfin ID) already known in our DB?
|
||||
if (_historyService.IsKnownItem(item.Id))
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Item {Name} (ID: {Id}) is already known - metadata change or re-scan, skipping notification",
|
||||
"Item {Name} (ID: {Id}) is already known in DB, skipping notification",
|
||||
item.Name,
|
||||
item.Id);
|
||||
_historyService.RecordItem(item);
|
||||
@@ -222,16 +324,32 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable
|
||||
notification.EpisodeNumber = episode.IndexNumber;
|
||||
notification.Year = episode.ProductionYear;
|
||||
|
||||
// Use series image if episode doesn't have its own
|
||||
// Use series provider IDs for external links — episode provider IDs
|
||||
// (e.g. AniDB episode ID) lead to wrong URLs when used with /anime/ paths
|
||||
if (episode.SeriesId != Guid.Empty)
|
||||
{
|
||||
var series = _libraryManager.GetItemById(episode.SeriesId);
|
||||
if (series?.ProviderIds != null && series.ProviderIds.Count > 0)
|
||||
{
|
||||
notification.ProviderIdsJson = JsonSerializer.Serialize(series.ProviderIds);
|
||||
}
|
||||
|
||||
var seriesImage = series?.GetImagePath(ImageType.Primary, 0);
|
||||
if (!string.IsNullOrEmpty(seriesImage))
|
||||
{
|
||||
notification.ImagePath = seriesImage;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"[DEBUG CreateNotification] Result: SeriesName={SeriesName}, SeriesId={SeriesId}, " +
|
||||
"S{Season}E{Episode}, ProviderIdsJson={ProviderIds}, ImagePath={ImagePath}",
|
||||
notification.SeriesName,
|
||||
notification.SeriesId,
|
||||
notification.SeasonNumber,
|
||||
notification.EpisodeNumber,
|
||||
notification.ProviderIdsJson,
|
||||
notification.ImagePath);
|
||||
}
|
||||
else if (item is Movie movie)
|
||||
{
|
||||
@@ -265,9 +383,45 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
if (item is not Episode episode)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"[DEBUG Refresh] Item {Id} is {Type} (not Episode), skipping",
|
||||
itemId,
|
||||
item?.GetType().Name ?? "NULL");
|
||||
return;
|
||||
}
|
||||
|
||||
// Debug: log what Jellyfin returns for this episode at refresh time
|
||||
_logger.LogInformation(
|
||||
"[DEBUG Refresh] Episode from library: Name={Name}, SeriesName={SeriesName}, SeriesId={SeriesId}, " +
|
||||
"Season={Season}, Episode={Episode}, ProviderIds={ProviderIds}",
|
||||
episode.Name,
|
||||
episode.SeriesName,
|
||||
episode.SeriesId,
|
||||
episode.ParentIndexNumber,
|
||||
episode.IndexNumber,
|
||||
episode.ProviderIds != null ? System.Text.Json.JsonSerializer.Serialize(episode.ProviderIds) : "null");
|
||||
|
||||
try
|
||||
{
|
||||
var debugSeries = episode.Series;
|
||||
if (debugSeries != null)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"[DEBUG Refresh] Series object: Name={Name}, Id={Id}, ProviderIds={ProviderIds}",
|
||||
debugSeries.Name,
|
||||
debugSeries.Id,
|
||||
debugSeries.ProviderIds != null ? System.Text.Json.JsonSerializer.Serialize(debugSeries.ProviderIds) : "null");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("[DEBUG Refresh] Series object is STILL NULL for {Name}", episode.Name);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "[DEBUG Refresh] Failed to access Series for {Name}", episode.Name);
|
||||
}
|
||||
|
||||
var changed = false;
|
||||
|
||||
if (string.IsNullOrEmpty(notification.SeriesName) || notification.SeriesName == "Unknown Series")
|
||||
|
||||
@@ -64,18 +64,25 @@ public class ItemHistoryService : IDisposable
|
||||
{
|
||||
if (item is Episode episode)
|
||||
{
|
||||
// For episodes: use series provider IDs + season + episode number
|
||||
// Prefer episode's own provider ID (e.g. AniDB episode ID).
|
||||
// This is stable even when seasons are reorganized in Jellyfin.
|
||||
var episodeKey = GetProviderKey(episode.ProviderIds);
|
||||
if (!string.IsNullOrEmpty(episodeKey))
|
||||
{
|
||||
return $"episode|{episodeKey}";
|
||||
}
|
||||
|
||||
// Fallback: series provider key + season + episode number
|
||||
var series = episode.Series;
|
||||
if (series == null)
|
||||
{
|
||||
_logger.LogDebug("Episode {Name} has no series, cannot generate content key", episode.Name);
|
||||
_logger.LogDebug("Episode {Name} has no series and no provider IDs, cannot generate content key", episode.Name);
|
||||
return null;
|
||||
}
|
||||
|
||||
var seriesKey = GetProviderKey(series.ProviderIds);
|
||||
if (string.IsNullOrEmpty(seriesKey))
|
||||
{
|
||||
// Fallback to series name if no provider IDs
|
||||
seriesKey = series.Name?.ToLowerInvariant().Trim() ?? "unknown";
|
||||
}
|
||||
|
||||
@@ -84,7 +91,7 @@ public class ItemHistoryService : IDisposable
|
||||
|
||||
if (episodeNum == 0)
|
||||
{
|
||||
_logger.LogDebug("Episode {Name} has no episode number", episode.Name);
|
||||
_logger.LogDebug("Episode {Name} has no episode number and no provider IDs", episode.Name);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,22 @@
|
||||
"category": "Notifications",
|
||||
"imageUrl": "",
|
||||
"versions": [
|
||||
{
|
||||
"version": "0.0.18.0",
|
||||
"changelog": "### Bug Fixes\n\n* fix: build error",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.tdpi.dev/TDPI/jellyfin-plugin-smartnotify/releases/download/v0.0.18/smartnotify_0.0.18.zip",
|
||||
"checksum": "4b5857ce309974f8e64ab81885d9b67d",
|
||||
"timestamp": "2026-03-03T13:53:53Z"
|
||||
},
|
||||
{
|
||||
"version": "0.0.16.0",
|
||||
"changelog": "### Bug Fixes\n\n* fix: ist das wirklich ein Fix und kein defix?\n\n### Chores\n\n* chore: update manifest for v0.0.15",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://git.tdpi.dev/TDPI/jellyfin-plugin-smartnotify/releases/download/v0.0.16/smartnotify_0.0.16.zip",
|
||||
"checksum": "fbe2b3ce339c92204961df605bfe276b",
|
||||
"timestamp": "2026-03-02T18:58:41Z"
|
||||
},
|
||||
{
|
||||
"version": "0.0.15.0",
|
||||
"changelog": "### Bug Fixes\n\n* fix: timestamps!\n\n### Chores\n\n* chore: update manifest for v0.0.14",
|
||||
|
||||
Reference in New Issue
Block a user