20 Commits

Author SHA1 Message Date
66716a9bc9 Updated logging to be more concise when debug is not active 2026-01-25 09:15:26 -07:00
9bddc59fe2 Further improvement to logging formatting and readability 2026-01-25 09:11:45 -07:00
82441d5247 Improved logging futher and updated some methods to use LogDebugInformation and to handle exceptions 2026-01-25 08:58:46 -07:00
4fc8b4799d Continued to improve logging and fixed a few bugs introduced by refactor 2026-01-24 23:14:16 -07:00
9e324f14a7 Significantly refactored code to help with readability of logging and episode processing 2026-01-24 08:34:42 -07:00
d024035d07 Managed to figure out how to use javascript on the plugin page by utilizing data-controller as found in other repos. Unsure how this is used, but appears to be how you can attach a js file to a div. Also implemented a basic state api to build off of in future. 2026-01-20 20:32:37 -07:00
8d85194df5 Removed unused logging 2026-01-19 08:12:19 -07:00
d78d1069b1 Merge branch 'main' of https://gitea.silverhat.ca/T-Gander/jellyfin-plugin-mediacleaner 2026-01-19 08:11:10 -07:00
f7c463aba4 Simplified Stale Episodes logic 2026-01-19 08:10:54 -07:00
77f2873180 Update README.md 2026-01-19 08:03:56 -07:00
b2da7beb00 Renamed method to be clearer in intention 2026-01-19 08:01:49 -07:00
9696826406 Increment version 2026-01-18 20:37:58 -07:00
873b29985c Fixed stale episode logic. 2026-01-18 20:32:27 -07:00
30107010b1 Simplified some linq queries and fixed some debug logging getting through 2026-01-18 19:23:59 -07:00
5af49eb390 Increment version 2026-01-18 18:52:30 -07:00
ee44dd9ee6 Further enhanced logging and refactored StaleMediaTask to aid in maintainability 2026-01-06 23:06:29 -07:00
9604289684 Improved logging and correctly separated the debug logging. Logs should now make more sense and are easier to comprehend. 2026-01-05 22:00:53 -07:00
ddf84b5cb6 Fixed possible null LastPlayedDate if just marked as watched 2025-12-04 22:16:11 -07:00
ca1d1fcab1 V6 2025-12-04 21:48:45 -07:00
3b43d6807a Enabled deletion of detached user data 2025-12-04 21:34:15 -07:00
15 changed files with 433 additions and 174 deletions

View File

@@ -1,5 +1,5 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<AssemblyVersion>0.0.0.5</AssemblyVersion> <AssemblyVersion>0.0.0.10</AssemblyVersion>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@@ -0,0 +1,22 @@
using Jellyfin.Plugin.MediaCleaner.Data;
using Jellyfin.Plugin.MediaCleaner.Models;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Plugin.MediaCleaner.Controllers;
[Route("mediacleaner/state")]
public class StateController : Controller
{
private readonly PluginState _state;
public StateController(PluginState state) => _state = state;
[HttpGet]
public IActionResult Get() => Ok(_state.GetSeriesInfo());
[HttpPost("add")]
public IActionResult AddSeriesInfo([FromBody] SeriesInfo seriesInfo)
{
_state.AddSeriesInfo(seriesInfo);
return Ok();
}
}

View File

@@ -0,0 +1,29 @@
using System.Collections.Generic;
using Jellyfin.Plugin.MediaCleaner.Models;
namespace Jellyfin.Plugin.MediaCleaner.Data;
public class PluginState
{
private readonly object _lock = new();
private List<SeriesInfo> _seriesInfo = new List<SeriesInfo>
{
new SeriesInfo { SeriesName = "TestName", Id = System.Guid.NewGuid() }
};
public void AddSeriesInfo(SeriesInfo seriesInfo)
{
lock (_lock)
{
_seriesInfo.Add(seriesInfo);
}
}
public IEnumerable<SeriesInfo> GetSeriesInfo()
{
lock (_lock)
{
return _seriesInfo;
}
}
}

View File

@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Jellyfin.Plugin.MediaCleaner.Configuration;
using Jellyfin.Plugin.MediaCleaner.Models;
using Jellyfin.Plugin.MediaCleaner.ScheduledTasks;
using MediaBrowser.Controller.Entities;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.MediaCleaner.Helpers;
public class LoggingHelper(ILogger logger)
{
private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
[SuppressMessage("Microsoft.Performance", "CA2254:TemplateShouldBeConstant", Justification = "Message parameter is intentionally variable for flexible debug logging")]
public void LogDebugInformation(string message, params object?[] args)
{
if (Configuration.DebugMode)
{
_logger.LogInformation(message, args);
}
}
[SuppressMessage("Microsoft.Performance", "CA2254:TemplateShouldBeConstant", Justification = "Message parameter is intentionally variable for flexible logging")]
public void LogInformation(string message, params object?[] args)
{
_logger.LogInformation(message, args);
}
private static PluginConfiguration Configuration =>
Plugin.Instance!.Configuration;
}

View File

@@ -0,0 +1,73 @@
using System;
using System.Linq;
using System.Threading;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Plugin.MediaCleaner.Configuration;
using MediaBrowser.Controller.Entities;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.MediaCleaner.Helpers;
public class MovieHelper(ILogger logger)
{
private readonly LoggingHelper _loggingHelper = new(logger);
private static PluginConfiguration Configuration =>
Plugin.Instance!.Configuration;
public bool IsMovieStale(BaseItem movie)
{
ArgumentNullException.ThrowIfNull(movie, "IsMovieStale process recieved a null movie. Logic failure. Exception thrown.");
_loggingHelper.LogDebugInformation("-------------------------------------------------");
_loggingHelper.LogDebugInformation("Scanning movie: {Movie}", movie);
_loggingHelper.LogDebugInformation("-------------------------------------------------");
bool movieIsStale = false;
bool createdOutsideCutoff = movie.DateCreated < DateTime.Now.AddDays(-Configuration.StaleMediaCutoff);
bool hasUserData = movie.UserData.Where(data => data.LastPlayedDate != null).ToList().Count > 0;
if (hasUserData)
{
var mostRecentUserData = movie.UserData.OrderByDescending(data => data.LastPlayedDate).First(data => data.LastPlayedDate != null);
_loggingHelper.LogDebugInformation("Most recent user data: {Movie}", movie);
foreach (var property in typeof(UserData).GetProperties())
{
_loggingHelper.LogDebugInformation("{PropertyName}: {PropertyValue}", property.Name, property.GetValue(mostRecentUserData));
}
_loggingHelper.LogDebugInformation("-------------------------------------------------");
if (mostRecentUserData.LastPlayedDate < DateTime.Now.AddDays(-Configuration.StaleMediaCutoff))
{
_loggingHelper.LogDebugInformation("Most recent user data has last played date that is outside of cutoff.");
_loggingHelper.LogDebugInformation("Adding {Movie} to stale movies.", movie);
_loggingHelper.LogDebugInformation("With Last Played Date: {LastPlayedDate}", mostRecentUserData.LastPlayedDate);
movieIsStale = true;
}
}
if (createdOutsideCutoff && !hasUserData)
{
_loggingHelper.LogDebugInformation("Movie has no user data and was created outside of cutoff: {DateCreated}.", movie.DateCreated);
_loggingHelper.LogDebugInformation("Adding {Movie} to stale movies.", movie);
movieIsStale = true;
}
if (!createdOutsideCutoff && !hasUserData)
{
_loggingHelper.LogDebugInformation("Movie has no user data and was not created outside of cutoff: {DateCreated}.", movie.DateCreated);
_loggingHelper.LogDebugInformation("Movie is not stale.");
}
_loggingHelper.LogDebugInformation("-------------------------------------------------");
_loggingHelper.LogDebugInformation("End of scanning for movie: {Movie}", movie);
return movieIsStale;
}
}

View File

@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Entities.Libraries;
using Jellyfin.Plugin.MediaCleaner.Configuration;
using MediaBrowser.Controller.Entities;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.MediaCleaner.Helpers;
public class SeriesHelper(ILogger logger)
{
private readonly LoggingHelper _loggingHelper = new(logger);
private static PluginConfiguration Configuration =>
Plugin.Instance!.Configuration;
private List<BaseItem> ProcessEpisodes(IReadOnlyCollection<BaseItem> episodes)
{
List<BaseItem> staleEpisodes = [.. episodes
.Where(episode =>
{
bool episodeIsStale = false;
var staleCreationDate = episode.DateCreated < DateTime.Now.AddDays(-Configuration.StaleMediaCutoff);
var hasUserDataWithLastPlayedDate = episode.UserData.Any(data => data.LastPlayedDate != null);
_loggingHelper.LogDebugInformation("-------------------------------------------------");
_loggingHelper.LogDebugInformation("Debug data for episode: {Episode}", episode);
_loggingHelper.LogDebugInformation("-------------------------------------------------");
if (staleCreationDate && !hasUserDataWithLastPlayedDate){
_loggingHelper.LogDebugInformation("Creation date is stale, and no user data for episode {Episode}.", episode);
_loggingHelper.LogDebugInformation("Date created: {DateCreated}", episode.DateCreated);
episodeIsStale = true;
}
if (hasUserDataWithLastPlayedDate){
UserData mostRecentUserData = episode.UserData
.OrderByDescending(data => data.LastPlayedDate)
.First();
foreach (var property in typeof(UserData).GetProperties())
{
_loggingHelper.LogDebugInformation("{PropertyName}: {PropertyValue}", property.Name, property.GetValue(mostRecentUserData));
}
_loggingHelper.LogDebugInformation("-------------------------------------------------");
bool staleLastPlayedDate = mostRecentUserData.LastPlayedDate < DateTime.Now.AddDays(-Configuration.StaleMediaCutoff);
if (staleLastPlayedDate && staleCreationDate)
{
episodeIsStale = true;
_loggingHelper.LogDebugInformation("Most recent user data has a last played date of: {LastPlayedDate}.", [mostRecentUserData.LastPlayedDate]);
_loggingHelper.LogDebugInformation("Episode created {DateCreated}.", episode.DateCreated);
_loggingHelper.LogDebugInformation("Episode is marked as stale.");
}
}
return episodeIsStale;
})];
return staleEpisodes;
}
public bool IsSeasonDataStale(IReadOnlyList<BaseItem> episodes)
{
ArgumentNullException.ThrowIfNull(episodes, "IsSeasonDataStale process recieved null episodes. Logic failure. Exception thrown.");
bool seasonIsStale = false;
List<BaseItem> staleEpisodes = ProcessEpisodes(episodes);
if(staleEpisodes.Count == episodes.Count)
{
seasonIsStale = true;
_loggingHelper.LogDebugInformation("-------------------------------------------------");
_loggingHelper.LogDebugInformation("Stale episodes count matches season episode count. Season is stale.");
_loggingHelper.LogDebugInformation("-------------------------------------------------");
}
return seasonIsStale;
}
}

View File

@@ -22,8 +22,8 @@
<ItemGroup> <ItemGroup>
<None Remove="Configuration\settings.html" /> <None Remove="Configuration\settings.html" />
<EmbeddedResource Include="Configuration\settings.html" /> <EmbeddedResource Include="Configuration\settings.html" />
<None Remove="Pages\home.html" /> <None Remove="Pages\*" />
<EmbeddedResource Include="Pages\home.html" /> <EmbeddedResource Include="Pages\*" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>

View File

@@ -22,9 +22,5 @@ public class SeriesInfo
/// <summary> /// <summary>
/// Gets or sets seasons. /// Gets or sets seasons.
/// </summary> /// </summary>
#pragma warning disable CA2227 // Collection properties should be read only public IEnumerable<string> Seasons { get; set; } = [];
#pragma warning disable CA1002 // Do not expose generic lists
public List<string> Seasons { get; set; } = [];
#pragma warning restore CA1002 // Do not expose generic lists
#pragma warning restore CA2227 // Collection properties should be read only
} }

View File

@@ -1,4 +1,5 @@
<div data-role="page" class="page type-interior pluginConfigurationPage withTabs"> <div data-role="page" class="page type-interior pluginConfigurationPage withTabs"
data-controller="__plugin/media_cleaner_table.js">
<div data-role="content"> <div data-role="content">
<div class="content-primary"> <div class="content-primary">
<div> <div>
@@ -6,6 +7,15 @@
<a href="#configurationpage?name=Settings">Settings</a> <a href="#configurationpage?name=Settings">Settings</a>
</div> </div>
<h2>Media Cleaner</h2> <h2>Media Cleaner</h2>
<table id="seriesTable">
<thead>
<tr>
<th>ID</th>
<th>Series Name</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,27 @@
var table = document.getElementById("seriesTable");
const getMediaCleanerState = async () => {
const response = await fetch('/mediacleaner/state');
if(!response.ok){
throw new Error(`Response status: ${response.status}`)
}
return response.json();
}
var state = await getMediaCleanerState();
console.log("State: ", state);
for(let i = 0; i < state.length; i++){
var row = table.insertRow(-1);
var cell1 = row.insertCell(0);
var cell2 = row.insertCell(1);
var cell3 = row.insertCell(2);
cell1.innerHTML = state[i].Id;
cell2.innerHTML = state[i].SeriesName;
cell3.innerHTML = state[i].Seasons.length;
}

View File

@@ -2,11 +2,13 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using Jellyfin.Plugin.MediaCleaner.Configuration; using Jellyfin.Plugin.MediaCleaner.Configuration;
using Jellyfin.Plugin.MediaCleaner.Data;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Plugins;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.DependencyInjection;
namespace Jellyfin.Plugin.MediaCleaner; namespace Jellyfin.Plugin.MediaCleaner;
@@ -53,6 +55,11 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Pages.home.html", GetType().Namespace), EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Pages.home.html", GetType().Namespace),
EnableInMainMenu = true, EnableInMainMenu = true,
}, },
new PluginPageInfo
{
Name = "media_cleaner_table.js",
EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Pages.media_cleaner_table.js", GetType().Namespace),
}
]; ];
} }
} }

View File

@@ -0,0 +1,13 @@
using Jellyfin.Plugin.MediaCleaner.Data;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Plugins;
using Microsoft.Extensions.DependencyInjection;
namespace Jellyfin.Plugin.MediaCleaner;
public class PluginServiceRegistrator : IPluginServiceRegistrator
{
public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost)
{
serviceCollection.AddSingleton<PluginState>();
}
}

View File

@@ -55,22 +55,10 @@ public class CleanupUserDataTask : IScheduledTask
var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false)) await using (dbContext.ConfigureAwait(false))
{ {
var detachedUserData = dbContext.UserData.Where(e => e.ItemId == PlaceholderId).ToList(); var detachedUserData = dbContext.UserData.Where(e => e.ItemId == PlaceholderId);
_logger.LogInformation("There are {NoDetached} detached UserData entries.", detachedUserData.Count); _logger.LogInformation("Deleting {DetachedUserDataCount} detached UserData entries.", detachedUserData.Count());
foreach(var userData in detachedUserData)
{
if (userData.Item != null)
{
_logger.LogInformation("Detached user data for: {UserDataName}", userData.Item.Name);
}
else
{
_logger.LogInformation("Detached user data for: {UserDataName}", userData);
}
}
_logger.LogInformation("Detached user data count: {Count}.", detachedUserData.Count);
// await detachedUserData.ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); await detachedUserData.ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
} }
} }

View File

@@ -5,6 +5,7 @@ using System.ComponentModel;
using System.Data.Common; using System.Data.Common;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Net;
using System.Reflection.Metadata.Ecma335; using System.Reflection.Metadata.Ecma335;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -12,6 +13,7 @@ using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Entities.Libraries; using Jellyfin.Database.Implementations.Entities.Libraries;
using Jellyfin.Plugin.MediaCleaner.Configuration; using Jellyfin.Plugin.MediaCleaner.Configuration;
using Jellyfin.Plugin.MediaCleaner.Helpers;
using Jellyfin.Plugin.MediaCleaner.Models; using Jellyfin.Plugin.MediaCleaner.Models;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@@ -26,20 +28,23 @@ namespace Jellyfin.Plugin.MediaCleaner.ScheduledTasks;
public sealed class StaleMediaTask : IScheduledTask public sealed class StaleMediaTask : IScheduledTask
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IUserManager _userManager;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly LoggingHelper _loggingHelper;
private readonly MovieHelper _movieHelper;
private readonly SeriesHelper _seriesHelper;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="StaleMediaTask"/> class. /// Initializes a new instance of the <see cref="StaleMediaTask"/> class.
/// </summary> /// </summary>
/// <param name="logger">Logger.</param> /// <param name="logger">Logger for StaleMediaTask.</param>
/// <param name="userManager">User manager.</param> /// <param name="libraryManager">Accesses jellyfin's library manager for media.</param>
/// <param name="libraryManager">.</param> public StaleMediaTask(ILogger<StaleMediaTask> logger, ILibraryManager libraryManager)
public StaleMediaTask(ILogger<StaleMediaTask> logger, IUserManager userManager, ILibraryManager libraryManager)
{ {
_logger = logger; _logger = logger;
_userManager = userManager;
_libraryManager = libraryManager; _libraryManager = libraryManager;
_loggingHelper = new LoggingHelper(_logger);
_movieHelper = new MovieHelper(_logger);
_seriesHelper = new SeriesHelper(_logger);
} }
private static PluginConfiguration Configuration => private static PluginConfiguration Configuration =>
@@ -55,44 +60,76 @@ public sealed class StaleMediaTask : IScheduledTask
Task IScheduledTask.ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) Task IScheduledTask.ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{ {
_loggingHelper.LogDebugInformation("--DEBUG MODE ACTIVE--");
_loggingHelper.LogInformation("-------------------------------------------------");
_loggingHelper.LogInformation("Starting stale media scan...");
_loggingHelper.LogInformation("-------------------------------------------------");
var query = new InternalItemsQuery var query = new InternalItemsQuery
{ {
IncludeItemTypes = [BaseItemKind.Movie, BaseItemKind.Series], IncludeItemTypes = [BaseItemKind.Movie, BaseItemKind.Series],
Recursive = true Recursive = true
}; };
List<BaseItem> allItems = [.. _libraryManager.GetItemsResult(query).Items]; List<BaseItem> allItems = [.. _libraryManager.GetItemsResult(query).Items];
if (Configuration.DebugMode) _loggingHelper.LogInformation("Total items to scan: {ItemCount}", allItems.Count);
{ _loggingHelper.LogDebugInformation("Items found: {AllItems}", allItems);
_logger.LogInformation("Total items found: {AllItems}", allItems);
}
List<BaseItem> series = [.. allItems.Where(item => item.GetBaseItemKind() == BaseItemKind.Series)]; List<BaseItem> series = [.. allItems.Where(item => item.GetBaseItemKind() == BaseItemKind.Series)];
List<BaseItem> movies = [.. allItems.Where(item => item.GetBaseItemKind() == BaseItemKind.Movie)]; List<BaseItem> movies = [.. allItems.Where(item => item.GetBaseItemKind() == BaseItemKind.Movie)];
List<BaseItem> staleEpisodes = [.. series.SelectMany(GetStaleEpisodes)]; _loggingHelper.LogDebugInformation("-------------------------------------------------");
_loggingHelper.LogDebugInformation("Starting scan of series items.");
List<BaseItem> staleSeasons = [.. series.SelectMany(GetStaleSeasons)];
_loggingHelper.LogDebugInformation("-------------------------------------------------");
_loggingHelper.LogDebugInformation("End of scan for series items.");
_loggingHelper.LogDebugInformation("-------------------------------------------------");
_loggingHelper.LogDebugInformation("Starting scan of movie items.");
List<BaseItem> staleMovies = [.. GetStaleMovies(movies)]; List<BaseItem> staleMovies = [.. GetStaleMovies(movies)];
_logger.LogInformation("Stale Movies found: {StaleMovies}", staleMovies.Count); _loggingHelper.LogDebugInformation("-------------------------------------------------");
_loggingHelper.LogDebugInformation("End of scan for movie items.");
_loggingHelper.LogInformation("-------------------------------------------------");
_loggingHelper.LogInformation("Stale seasons found: {StaleSeasons}", staleSeasons.Count);
if (staleSeasons.Count > 0)
{
IEnumerable<SeriesInfo> staleSeriesInfo = FindSeriesInfo(staleSeasons);
foreach (var seriesInfo in staleSeriesInfo)
{
_loggingHelper.LogInformation("Series Info: ID: {Id} | Series Name: {SeriesName} | Stale Seasons: {Seasons}", [seriesInfo.Id, seriesInfo.SeriesName, string.Join(", ", seriesInfo.Seasons)]);
}
}
else
{
_loggingHelper.LogInformation("No stale seasons found!");
}
_loggingHelper.LogInformation("-------------------------------------------------");
_loggingHelper.LogInformation("Stale Movies found: {StaleMovies}", staleMovies.Count);
if (staleMovies.Count > 0) if (staleMovies.Count > 0)
{ {
_logger.LogInformation("Movies: {Names}", string.Join(", ", staleMovies.Select(movie => movie.Name))); foreach (var movieInfo in staleMovies)
{
_loggingHelper.LogInformation("Movie Info: ID: {Id} | Movie Name: {MovieName}", [movieInfo.Id, movieInfo.Name]);
}
}
else
{
_loggingHelper.LogInformation("No stale movies found!");
} }
_logger.LogInformation("Stale Episodes found: {StaleEpisodes}", staleEpisodes.Count); _loggingHelper.LogInformation("-------------------------------------------------");
if (staleEpisodes.Count > 0) _loggingHelper.LogInformation("Ending stale media scan...");
{ _loggingHelper.LogInformation("-------------------------------------------------");
// Firstly figure out the seasons, and then the Series to find the name.
List<SeriesInfo> seriesInfoList = FindSeriesInfoFromEpisodes(staleEpisodes);
foreach (var seriesInfo in seriesInfoList)
{
if (Configuration.DebugMode)
{
_logger.LogInformation("Series Info: ID: {Id} | Series Name: {SeriesName} | Stale Seasons: {Seasons}", [seriesInfo.Id, seriesInfo.SeriesName, string.Join(", ", seriesInfo.Seasons)]);
}
}
}
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -101,159 +138,92 @@ public sealed class StaleMediaTask : IScheduledTask
{ {
List<BaseItem> staleMovies = []; List<BaseItem> staleMovies = [];
foreach (var movie in movies) try
{ {
bool movieIsStale = movie.DateCreated < DateTime.Now.AddDays(-Configuration.StaleMediaCutoff); staleMovies.AddRange(movies.Where(_movieHelper.IsMovieStale));
bool movieHasUserData = movie.UserData.Count > 0; }
if (movieHasUserData) catch (ArgumentNullException ex)
{ {
if (Configuration.DebugMode){ _loggingHelper.LogInformation("Arguement Null Exception in GetStaleMovies!");
_logger.LogInformation("Movie has user data: {Movie}", movie); _loggingHelper.LogInformation(ex.Message);
_logger.LogInformation("-------------------------------------------------");
}
var mostRecentUserData = movie.UserData.OrderByDescending(data => data.LastPlayedDate).Where(data => data.LastPlayedDate != null).First();
if (Configuration.DebugMode){
_logger.LogInformation("Most recent user data: {Movie}", movie);
foreach (var property in typeof(UserData).GetProperties())
{
_logger.LogInformation("{PropertyName}: {PropertyValue}", property.Name, property.GetValue(mostRecentUserData));
}
_logger.LogInformation("-------------------------------------------------");
}
if (mostRecentUserData.LastPlayedDate < DateTime.Now.AddDays(-Configuration.StaleMediaCutoff))
{
_logger.LogInformation("Most recent user data last played date is outside of cutoff. Adding to stale movies.");
staleMovies.Add(movie);
}
}
else if (movieIsStale)
{
_logger.LogInformation("Movie has no user data and was created outside of cutoff: {DateCreated}", movie.DateCreated);
staleMovies.Add(movie);
}
} }
return staleMovies; return staleMovies;
} }
private List<SeriesInfo> FindSeriesInfoFromEpisodes(List<BaseItem> episodes)
private List<BaseItem> GetStaleSeasons(BaseItem item)
{ {
Guid[] seasonIds = [.. episodes.Select(episode => episode.ParentId).Distinct()]; _loggingHelper.LogDebugInformation("-------------------------------------------------");
_loggingHelper.LogDebugInformation("Debug data for series: {SeriesName}", item.Name);
_loggingHelper.LogDebugInformation("-------------------------------------------------");
var seasons = _libraryManager.GetItemList(new InternalItemsQuery
{
ItemIds = seasonIds
});
Guid[] seriesIds = [.. seasons.Select(season => season.ParentId).Distinct()];
var series = _libraryManager.GetItemList(new InternalItemsQuery
{
ItemIds = seriesIds
}).ToList();
// Series Id, Series Name and Stale Seasons
List<string> seriesNames = [.. series.Select(series => series.Name).Distinct()];
List<SeriesInfo> seriesInfoList = [];
series.ForEach(series =>
{
seriesInfoList.Add(new SeriesInfo
{
Id = series.Id,
SeriesName = series.Name,
Seasons = [.. seasons.Where(season => season.ParentId == series.Id).Select(season => season.Name)]
});
});
return seriesInfoList;
}
private List<BaseItem> GetStaleEpisodes(BaseItem item)
{
List<BaseItem> staleEpisodes = [];
// Gets each season in a show
var seasons = _libraryManager.GetItemList(new InternalItemsQuery var seasons = _libraryManager.GetItemList(new InternalItemsQuery
{ {
ParentId = item.Id, ParentId = item.Id,
Recursive = false Recursive = false
}); });
foreach (var season in seasons) List<BaseItem> staleSeasons = [ ..seasons
{ .Where(season => {
// Gets each episode, to access user data.
var episodes = _libraryManager.GetItemList(new InternalItemsQuery var episodes = _libraryManager.GetItemList(new InternalItemsQuery
{ {
ParentId = season.Id, ParentId = season.Id,
Recursive = false Recursive = false
}); });
bool seasonHasUserData = episodes.Any(episode => episode.UserData.Count > 0);
if (seasonHasUserData && Configuration.DebugMode) _loggingHelper.LogDebugInformation("Season debug information for {SeasonNumber}:", season);
bool isSeasonDataStale = false;
try
{ {
_logger.LogInformation("Season has user data for episodes: {Episodes}", episodes); isSeasonDataStale = _seriesHelper.IsSeasonDataStale(episodes);
_logger.LogInformation("-------------------------------------------------");
} }
bool seasonIsStale = episodes.All(episode => episode.DateCreated < DateTime.Now.AddDays(-Configuration.StaleMediaCutoff)); catch (ArgumentNullException ex)
if (seasonIsStale && Configuration.DebugMode)
{ {
_logger.LogInformation("All episodes are outside media cutoff."); _loggingHelper.LogInformation("Arguement Null Exception in GetStaleSeasons!");
_logger.LogInformation("-------------------------------------------------"); _loggingHelper.LogInformation(ex.Message);
} }
if (seasonHasUserData) _loggingHelper.LogDebugInformation("End of season debug information for {SeasonNumber}.", season);
{
var episodesWithUserData = episodes.Where(episode => episode.UserData.Count > 0).ToList();
if(Configuration.DebugMode){ return isSeasonDataStale;
_logger.LogInformation("Episodes with user data: {EpisodesWithUserData}", episodesWithUserData); })];
_logger.LogInformation("-------------------------------------------------");
_loggingHelper.LogDebugInformation("-------------------------------------------------");
_loggingHelper.LogDebugInformation("End of scanning for series: {Series}", item);
return staleSeasons;
} }
foreach (var episode in episodesWithUserData) private IEnumerable<SeriesInfo> FindSeriesInfo(IReadOnlyCollection<BaseItem> seasons)
{ {
var mostRecentUserData = episode.UserData.OrderByDescending(data => data.LastPlayedDate).Where(data => data.LastPlayedDate != null).First(); Guid[] seriesIds = [.. seasons.Select(season => season.ParentId).Distinct()];
if(Configuration.DebugMode){
foreach (var property in typeof(UserData).GetProperties())
{
_logger.LogInformation("{PropertyName}: {PropertyValue}", property.Name, property.GetValue(mostRecentUserData));
}
_logger.LogInformation("-------------------------------------------------");
}
if (mostRecentUserData.LastPlayedDate < DateTime.Now.AddDays(-Configuration.StaleMediaCutoff))
{
if(Configuration.DebugMode){
_logger.LogInformation("Most Recent User Data Last Played Date is: {LastPlayedDate}. All Episodes are stale.", mostRecentUserData.LastPlayedDate);
_logger.LogInformation("-------------------------------------------------");
}
staleEpisodes.AddRange(episodes); IReadOnlyCollection<BaseItem> series = _libraryManager.GetItemList(new InternalItemsQuery
break;
}
}
}
// Check for episodes that have gone unwatched for stale media cutoff
else if (seasonIsStale)
{ {
if(Configuration.DebugMode){ ItemIds = seriesIds
_logger.LogInformation("No user data, adding all episodes as it is outside of cutoff."); });
_logger.LogInformation("-------------------------------------------------");
}
staleEpisodes.AddRange(episodes);
}
}
return staleEpisodes; IEnumerable<SeriesInfo> seriesInfoList = series.Select(series =>
{
return new SeriesInfo
{
Id = series.Id,
SeriesName = series.Name,
Seasons = [.. seasons.Where(season => season.ParentId == series.Id).Select(season => season.Name)]
};
});
return seriesInfoList;
} }
IEnumerable<TaskTriggerInfo> IScheduledTask.GetDefaultTriggers() IEnumerable<TaskTriggerInfo> IScheduledTask.GetDefaultTriggers()
{ {
// Run this task every 24 hours // Run this task every 24 hours
// Unnecessary, and will be removed once front end page is ready.
yield return new TaskTriggerInfo yield return new TaskTriggerInfo
{ {
Type = TaskTriggerInfoType.IntervalTrigger, Type = TaskTriggerInfoType.IntervalTrigger,

View File

@@ -1,9 +1,9 @@
The idea behind this plugin is to have an easy way to run a task to find all movies and shows in your media collection that users haven't viewed in a number of cutoff days. The idea behind this plugin is to have an easy way to run a task to find all movies and shows in your media collection that users haven't viewed in a number of cutoff days.
At the time of writing, the plugin is only capable of logging movies and shows that are stale (Unwatched for 90 days) by running a scheduled task. You will need to view your logs to know the number of stale files. At the time of writing, the plugin is only capable of logging movies and shows that are stale (Unwatched for a user set number of days) by running a scheduled task. You will need to view your logs to know the number of stale files and the names of said files.
Planned features: Planned features:
- Better logging to show more than just the count. - Better logging to show more than just the count.
- A page that shows what media is currently flagged for removal. And a button to confirm removal. - A page that shows what media is currently flagged for removal. And a button to confirm removal.
- Integration with sonarr and radarr apis to delete your media. - Integration with sonarr and radarr apis to delete your media.
- Whitelist for shows to ignore. (Seasonal shows) - Whitelist for shows to ignore. (Seasonal shows)