21 Commits

Author SHA1 Message Date
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
2c91846d81 Incremented version 2025-12-04 21:26:50 -07:00
c533eb4f90 Added cleaner for orphaned user data 2025-12-04 21:25:39 -07:00
2fdbae74e8 Updated userData search to filter out null last played dates from user data 2025-12-04 20:45:01 -07:00
08e26943c9 Added debugging 2025-12-04 20:24:24 -07:00
65dd638b09 Added logging 2025-12-03 23:30:58 -07:00
6b20bc0828 Fixed bug for nullable movie user data 2025-12-03 23:00:19 -07:00
db3a06cc67 Logic fix 2025-12-03 22:35:14 -07:00
9 changed files with 481 additions and 94 deletions

View File

@@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<AssemblyVersion>0.0.0.3</AssemblyVersion>
<AssemblyVersion>0.0.0.9</AssemblyVersion>
</PropertyGroup>
</Project>

View File

@@ -31,4 +31,9 @@ public class PluginConfiguration : BasePluginConfiguration
/// Gets or sets the cut off days before deleting unwatched files.
/// </summary>
public int StaleMediaCutoff { get; set; } = 90;
/// <summary>
/// Gets or sets debug mode.
/// </summary>
public bool DebugMode { get; set; }
}

View File

@@ -24,6 +24,12 @@
<input id="StaleMediaCutoff" name="StaleMediaCutoff" type="number" is="emby-input" style="width: 20%;"/>
<div class="fieldDescription">How many days to wait before marking files as stale</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="DebugMode" name="DebugMode" type="checkbox" is="emby-checkbox" />
<span>Debug Mode</span>
</label>
</div>
<!-- <div class="selectContainer">
<label class="selectLabel" for="Options">Several Options</label>
<select is="emby-select" id="Options" name="Options" class="emby-select-withcolor emby-select">
@@ -71,6 +77,7 @@
document.querySelector('#RadarrAPIKey').value = config.RadarrAPIKey;
document.querySelector('#SonarrAPIKey').value = config.SonarrAPIKey;
document.querySelector('#StaleMediaCutoff').value = config.StaleMediaCutoff;
document.querySelector('#DebugMode').checked = config.DebugMode;
Dashboard.hideLoadingMsg();
});
});
@@ -86,6 +93,7 @@
config.RadarrAPIKey = document.querySelector('#RadarrAPIKey').value;
config.SonarrAPIKey = document.querySelector('#SonarrAPIKey').value;
config.StaleMediaCutoff = document.querySelector('#StaleMediaCutoff').value;
config.DebugMode = document.querySelector('#DebugMode').checked;
ApiClient.updatePluginConfiguration(MediaCleanerConfig.pluginUniqueId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result);
});

View File

@@ -0,0 +1,159 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
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
{
private readonly ILogger _logger;
public LoggingHelper(ILogger logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
private static PluginConfiguration Configuration =>
Plugin.Instance!.Configuration;
public void StartLogging()
{
if (Configuration.DebugMode)
{
_logger.LogInformation("--DEBUG MODE ACTIVE--");
}
_logger.LogInformation("-------------------------------------------------");
_logger.LogInformation("Starting stale media scan...");
}
public void EndLogging()
{
_logger.LogInformation("Ending stale media scan...");
_logger.LogInformation("-------------------------------------------------");
}
public void PrintDebugEndOfScanningForSeries(BaseItem item)
{
if (Configuration.DebugMode)
{
_logger.LogInformation("End of scanning for series: {Series}", item);
_logger.LogInformation("-------------------------------------------------");
}
}
public void StartScanningSeriesItems()
{
_logger.LogInformation("-------------------------------------------------");
_logger.LogInformation("Starting scan of series items.");
_logger.LogInformation("-------------------------------------------------");
}
public void StartScanningMoviesItems()
{
_logger.LogInformation("-------------------------------------------------");
_logger.LogInformation("Starting scan of movies items.");
_logger.LogInformation("-------------------------------------------------");
}
public void PrintItemsInformation(IReadOnlyCollection<BaseItem> items)
{
ArgumentNullException.ThrowIfNull(items);
_logger.LogInformation("Total items: {ItemCount}", items.Count);
_logger.LogInformation("Stale items found: {AllItems}", items);
}
public void PrintStaleMoviesInformation(IReadOnlyCollection<BaseItem> staleMovies)
{
ArgumentNullException.ThrowIfNull(staleMovies);
_logger.LogInformation("-------------------------------------------------");
_logger.LogInformation("Stale Movies found: {StaleMovies}", staleMovies.Count);
if (staleMovies.Count > 0 && Configuration.DebugMode)
{
foreach (var movieInfo in staleMovies)
{
_logger.LogInformation("Movie Info: ID: {Id} | Movie Name: {MovieName}", [movieInfo.Id, movieInfo.Name]);
}
}
}
public void PrintStaleEpisodesInformation(Func<IReadOnlyCollection<BaseItem>, List<SeriesInfo>> findSeriesInfoFromEpisodes, IReadOnlyCollection<BaseItem> staleEpisodes)
{
ArgumentNullException.ThrowIfNull(staleEpisodes);
ArgumentNullException.ThrowIfNull(findSeriesInfoFromEpisodes);
_logger.LogInformation("-------------------------------------------------");
_logger.LogInformation("Stale Episodes found: {StaleEpisodes}", staleEpisodes.Count);
if (staleEpisodes.Count > 0 && Configuration.DebugMode)
{
if (findSeriesInfoFromEpisodes == null)
{
throw new ArgumentNullException(nameof(findSeriesInfoFromEpisodes), "The method to find series information cannot be null.");
}
List<SeriesInfo> seriesInfoList = findSeriesInfoFromEpisodes(staleEpisodes);
foreach (var seriesInfo in seriesInfoList)
{
_logger.LogInformation("Series Info: ID: {Id} | Series Name: {SeriesName} | Stale Seasons: {Seasons}", [seriesInfo.Id, seriesInfo.SeriesName, string.Join(", ", seriesInfo.Seasons)]);
}
}
_logger.LogInformation("-------------------------------------------------");
}
public void PrintDebugDataForSeries(BaseItem item)
{
ArgumentNullException.ThrowIfNull(item);
if (Configuration.DebugMode)
{
_logger.LogInformation("-------------------------------------------------");
_logger.LogInformation("Debug data for series: {SeriesName}", item.Name);
_logger.LogInformation("-------------------------------------------------");
}
}
public void PrintDebugSeasonInfo()
{
if (Configuration.DebugMode)
{
_logger.LogInformation("Season debug information:");
}
}
public void PrintDebugEpisodesWithUserData(IReadOnlyCollection<BaseItem> episodesWithUserData)
{
if(Configuration.DebugMode){
_logger.LogInformation("Episodes with user data: {EpisodesWithUserData}", episodesWithUserData);
_logger.LogInformation("-------------------------------------------------");
}
}
public void PrintDebugEpisodeCreationInfo(IReadOnlyCollection<BaseItem> episodes)
{
ArgumentNullException.ThrowIfNull(episodes);
if(Configuration.DebugMode){
_logger.LogInformation("-------------------------------------------------");
_logger.LogInformation("Episode creation dates:");
_logger.LogInformation("-------------------------------------------------");
foreach(BaseItem episode in episodes)
{
_logger.LogInformation("Episode: {EpisodeName} | Date Created: {EpisodeDateCreated}", [episode.Name, episode.DateCreated]);
}
_logger.LogInformation("-------------------------------------------------");
}
}
}

View File

@@ -0,0 +1,79 @@
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
{
private readonly ILogger _logger;
public MovieHelper(ILogger logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
private static PluginConfiguration Configuration =>
Plugin.Instance!.Configuration;
public bool IsMovieStale(BaseItem movie)
{
if (Configuration.DebugMode)
{
_logger.LogInformation("-------------------------------------------------");
_logger.LogInformation("Start of scanning for movie: {Movie}", movie);
_logger.LogInformation("-------------------------------------------------");
}
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);
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))
{
if (Configuration.DebugMode)
{
_logger.LogInformation("Most recent user data last played date is outside of cutoff. Adding {Movie} to stale movies.", movie);
}
movieIsStale = true;
}
}
else if (createdOutsideCutoff)
{
if (Configuration.DebugMode)
{
_logger.LogInformation("Movie has no user data and was created outside of cutoff: {DateCreated}. Adding {Movie} to stale movies.", [movie.DateCreated, movie]);
}
movieIsStale = true;
}
if (Configuration.DebugMode)
{
_logger.LogInformation("-------------------------------------------------");
_logger.LogInformation("End of scanning for movie: {Movie}", movie);
_logger.LogInformation("-------------------------------------------------");
}
return movieIsStale;
}
}

View File

@@ -0,0 +1,83 @@
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
{
private readonly ILogger _logger;
private readonly LoggingHelper _loggingHelper;
public SeriesHelper(ILogger logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_loggingHelper = new LoggingHelper(logger);
}
private static PluginConfiguration Configuration =>
Plugin.Instance!.Configuration;
public bool IsSeasonUserDataStale(IReadOnlyList<BaseItem> episodes)
{
bool seasonIsStale = false;
List<BaseItem> staleEpisodes = [];
var episodesWithUserData = episodes.Where(episode => episode.UserData.Where(data => data.LastPlayedDate != null).ToList().Count > 0).ToList();
_loggingHelper.PrintDebugEpisodesWithUserData(episodesWithUserData);
foreach (var episode in episodesWithUserData)
{
bool episodeIsStale = false;
var mostRecentUserData = episode.UserData.OrderByDescending(data => data.LastPlayedDate).First(data => data.LastPlayedDate != null);
var staleLastPlayedDate = mostRecentUserData.LastPlayedDate < DateTime.Now.AddDays(-Configuration.StaleMediaCutoff);
var staleCreationDate = episode.DateCreated < DateTime.Now.AddDays(-Configuration.StaleMediaCutoff);
if(Configuration.DebugMode){
_logger.LogInformation("User data for episode: {Episode}", episode);
_logger.LogInformation("-------------------------------------------------");
foreach (var property in typeof(UserData).GetProperties())
{
_logger.LogInformation("{PropertyName}: {PropertyValue}", property.Name, property.GetValue(mostRecentUserData));
}
_logger.LogInformation("-------------------------------------------------");
}
if (staleLastPlayedDate && staleCreationDate)
{
episodeIsStale = true;
if(Configuration.DebugMode){
_logger.LogInformation("Most recent user data has a last played date of: {LastPlayedDate}.", [mostRecentUserData.LastPlayedDate]);
_logger.LogInformation("And episode created {DateCreated}.", episode.DateCreated);
}
}
if (episodeIsStale)
{
staleEpisodes.Add(episode);
if(Configuration.DebugMode){
_logger.LogInformation("Episode is stale.");
}
}
}
if(staleEpisodes.Count == episodes.Count)
{
seasonIsStale = true;
_logger.LogInformation("Stale episodes count matches all episode count. Season is stale.");
_logger.LogInformation("-------------------------------------------------");
}
return seasonIsStale;
}
}

View File

@@ -0,0 +1,70 @@
#pragma warning disable RS0030 // Do not use banned APIs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.MediaCleaner.ScheduledTasks;
/// <summary>
/// Task to clean up any detached userdata from the database.
/// </summary>
public class CleanupUserDataTask : IScheduledTask
{
private readonly ILocalizationManager _localization;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly ILogger<CleanupUserDataTask> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="CleanupUserDataTask"/> class.
/// </summary>
/// <param name="localization">The localisation Provider.</param>
/// <param name="dbProvider">The DB context factory.</param>
/// <param name="logger">A logger.</param>
public CleanupUserDataTask(ILocalizationManager localization, IDbContextFactory<JellyfinDbContext> dbProvider, ILogger<CleanupUserDataTask> logger)
{
_localization = localization;
_dbProvider = dbProvider;
_logger = logger;
}
/// <inheritdoc />
public string Name => _localization.GetLocalizedString("CleanupUserDataTask");
public Guid PlaceholderId => new("00000000-0000-0000-0000-000000000001");
/// <inheritdoc />
public string Description => _localization.GetLocalizedString("CleanupUserDataTaskDescription");
/// <inheritdoc />
public string Category => _localization.GetLocalizedString("Media");
/// <inheritdoc />
public string Key => nameof(CleanupUserDataTask);
/// <inheritdoc/>
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
var detachedUserData = dbContext.UserData.Where(e => e.ItemId == PlaceholderId);
_logger.LogInformation("Deleting {DetachedUserDataCount} detached UserData entries.", detachedUserData.Count());
await detachedUserData.ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
}
}
/// <inheritdoc/>
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
yield break;
}
}

View File

@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Data.Common;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Reflection.Metadata.Ecma335;
using System.Threading;
using System.Threading.Tasks;
@@ -11,6 +13,7 @@ using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Entities.Libraries;
using Jellyfin.Plugin.MediaCleaner.Configuration;
using Jellyfin.Plugin.MediaCleaner.Helpers;
using Jellyfin.Plugin.MediaCleaner.Models;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -25,20 +28,23 @@ namespace Jellyfin.Plugin.MediaCleaner.ScheduledTasks;
public sealed class StaleMediaTask : IScheduledTask
{
private readonly ILogger _logger;
private readonly IUserManager _userManager;
private readonly ILibraryManager _libraryManager;
private readonly LoggingHelper _loggingHelper;
private readonly MovieHelper _movieHelper;
private readonly SeriesHelper _seriesHelper;
/// <summary>
/// Initializes a new instance of the <see cref="StaleMediaTask"/> class.
/// </summary>
/// <param name="logger">Logger.</param>
/// <param name="userManager">User manager.</param>
/// <param name="libraryManager">.</param>
public StaleMediaTask(ILogger<StaleMediaTask> logger, IUserManager userManager, ILibraryManager libraryManager)
/// <param name="logger">Logger for StaleMediaTask.</param>
/// <param name="libraryManager">Accesses jellyfin's library manager for media.</param>
public StaleMediaTask(ILogger<StaleMediaTask> logger, ILibraryManager libraryManager)
{
_logger = logger;
_userManager = userManager;
_libraryManager = libraryManager;
_loggingHelper = new LoggingHelper(_logger);
_movieHelper = new MovieHelper(_logger);
_seriesHelper = new SeriesHelper(_logger);
}
private static PluginConfiguration Configuration =>
@@ -54,38 +60,31 @@ public sealed class StaleMediaTask : IScheduledTask
Task IScheduledTask.ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
_loggingHelper.StartLogging();
var query = new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.Movie, BaseItemKind.Series],
Recursive = true
};
List<BaseItem> allItems = [.. _libraryManager.GetItemsResult(query).Items];
_logger.LogInformation("Total items found: {AllItems}", allItems);
_loggingHelper.PrintItemsInformation(allItems);
List<BaseItem> series = [.. allItems.Where(item => item.GetBaseItemKind() == BaseItemKind.Series)];
List<BaseItem> movies = [.. allItems.Where(item => item.GetBaseItemKind() == BaseItemKind.Movie && item.UserData.Count > 0)];
List<BaseItem> movies = [.. allItems.Where(item => item.GetBaseItemKind() == BaseItemKind.Movie)];
_loggingHelper.StartScanningSeriesItems();
List<BaseItem> staleEpisodes = [.. series.SelectMany(GetStaleEpisodes)];
_loggingHelper.StartScanningMoviesItems();
List<BaseItem> staleMovies = [.. GetStaleMovies(movies)];
_logger.LogInformation("Stale Movies found: {StaleMovies}", staleMovies.Count);
if (staleMovies.Count > 0)
{
_logger.LogInformation("Movies: {Names}", string.Join(", ", staleMovies.Select(movie => movie.Name)));
}
_loggingHelper.PrintStaleMoviesInformation(staleMovies);
_loggingHelper.PrintStaleEpisodesInformation(FindSeriesInfoFromEpisodes, staleEpisodes);
_logger.LogInformation("Stale Episodes found: {StaleEpisodes}", staleEpisodes.Count);
if (staleEpisodes.Count > 0)
{
// Firstly figure out the seasons, and then the Series to find the name.
List<SeriesInfo> seriesInfoList = FindSeriesInfoFromEpisodes(staleEpisodes);
foreach (var seriesInfo in seriesInfoList)
{
_logger.LogInformation("Series Info: ID: {Id} | Series Name: {SeriesName} | Stale Seasons: {Seasons}", [seriesInfo.Id, seriesInfo.SeriesName, string.Join(", ", seriesInfo.Seasons)]);
}
}
_loggingHelper.EndLogging();
return Task.CompletedTask;
}
@@ -94,25 +93,56 @@ public sealed class StaleMediaTask : IScheduledTask
{
List<BaseItem> staleMovies = [];
foreach (var movie in movies)
{
bool movieIsStale = movie.DateCreated > DateTime.Now.AddDays(-Configuration.StaleMediaCutoff);
var mostRecentUserData = movie.UserData.OrderByDescending(data => data.LastPlayedDate).First();
if (mostRecentUserData.LastPlayedDate < DateTime.Now.AddDays(-Configuration.StaleMediaCutoff))
{
staleMovies.Add(movie);
}
else if (movieIsStale)
{
staleMovies.Add(movie);
}
}
staleMovies.AddRange(movies.Where(_movieHelper.IsMovieStale));
return staleMovies;
}
private List<SeriesInfo> FindSeriesInfoFromEpisodes(List<BaseItem> episodes)
private List<BaseItem> GetStaleEpisodes(BaseItem item)
{
List<BaseItem> staleEpisodes = [];
// Gets each season in a show
var seasons = _libraryManager.GetItemList(new InternalItemsQuery
{
ParentId = item.Id,
Recursive = false
});
_loggingHelper.PrintDebugDataForSeries(item);
foreach (var season in seasons)
{
// Gets each episode, to access user data.
var episodes = _libraryManager.GetItemList(new InternalItemsQuery
{
ParentId = season.Id,
Recursive = false
});
_loggingHelper.PrintDebugSeasonInfo();
bool seasonHasUserData = episodes.Any(episode => episode.UserData.Count > 0);
bool seasonIsStale = seasonHasUserData && _seriesHelper.IsSeasonUserDataStale(episodes);
if (seasonIsStale)
{
if (!seasonHasUserData)
{
_loggingHelper.PrintDebugEpisodeCreationInfo(episodes);
}
staleEpisodes.AddRange(episodes);
}
}
_loggingHelper.PrintDebugEndOfScanningForSeries(item);
return staleEpisodes;
}
private List<SeriesInfo> FindSeriesInfoFromEpisodes(IReadOnlyCollection<BaseItem> episodes)
{
Guid[] seasonIds = [.. episodes.Select(episode => episode.ParentId).Distinct()];
@@ -128,7 +158,6 @@ public sealed class StaleMediaTask : IScheduledTask
ItemIds = seriesIds
}).ToList();
// Series Id, Series Name and Stale Seasons
List<string> seriesNames = [.. series.Select(series => series.Name).Distinct()];
List<SeriesInfo> seriesInfoList = [];
@@ -136,62 +165,16 @@ public sealed class StaleMediaTask : IScheduledTask
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)]
});
{
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
{
ParentId = item.Id,
Recursive = false
});
foreach (var season in seasons)
{
// Gets each episode, to access user data.
var episodes = _libraryManager.GetItemList(new InternalItemsQuery
{
ParentId = season.Id,
Recursive = false
});
bool seasonHasUserData = episodes.Any(episode => episode.UserData.Count > 0);
bool seasonIsStale = episodes.All(episode => episode.DateCreated > DateTime.Now.AddDays(-Configuration.StaleMediaCutoff));
if (seasonHasUserData)
{
var episodesWithUserData = episodes.Where(episode => episode.UserData.Count > 0).ToList();
foreach (var episode in episodesWithUserData)
{
var mostRecentUserData = episode.UserData.OrderByDescending(data => data.LastPlayedDate).First();
if (mostRecentUserData.LastPlayedDate < DateTime.Now.AddDays(-Configuration.StaleMediaCutoff))
{
staleEpisodes.AddRange(episodes);
break;
}
}
}
// Check for episodes that have gone unwatched for stale media cutoff
else if (seasonIsStale)
{
staleEpisodes.AddRange(episodes);
}
}
return staleEpisodes;
}
IEnumerable<TaskTriggerInfo> IScheduledTask.GetDefaultTriggers()
{
// Run this task every 24 hours

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.
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:
- 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.
- Integration with sonarr and radarr apis to delete your media.
- Whitelist for shows to ignore. (Seasonal shows)