12 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
14 changed files with 296 additions and 290 deletions

View File

@@ -1,5 +1,5 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<AssemblyVersion>0.0.0.8</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

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Jellyfin.Plugin.MediaCleaner.Configuration; using Jellyfin.Plugin.MediaCleaner.Configuration;
using Jellyfin.Plugin.MediaCleaner.Models; using Jellyfin.Plugin.MediaCleaner.Models;
@@ -10,162 +11,25 @@ using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.MediaCleaner.Helpers; namespace Jellyfin.Plugin.MediaCleaner.Helpers;
public class LoggingHelper public class LoggingHelper(ILogger logger)
{ {
private readonly ILogger _logger; private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
public LoggingHelper(ILogger logger) [SuppressMessage("Microsoft.Performance", "CA2254:TemplateShouldBeConstant", Justification = "Message parameter is intentionally variable for flexible debug logging")]
public void LogDebugInformation(string message, params object?[] args)
{ {
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); 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 => private static PluginConfiguration Configuration =>
Plugin.Instance!.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 PrintStaleItemsInformation(IReadOnlyCollection<BaseItem> staleItems)
{
ArgumentNullException.ThrowIfNull(staleItems);
_logger.LogInformation("Total stale items: {ItemCount}", staleItems.Count);
_logger.LogInformation("Stale items found: {AllItems}", staleItems);
}
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 PrintDebugSeasonCreatedOutsideCutoff()
{
if(Configuration.DebugMode)
{
_logger.LogInformation("All episodes were created outside of media cutoff, season is possibly stale.");
}
}
public void PrintDebugEpisodesWithUserData(IReadOnlyCollection<BaseItem> episodesWithUserData)
{
if(Configuration.DebugMode){
_logger.LogInformation("Episodes with user data: {EpisodesWithUserData}", episodesWithUserData);
_logger.LogInformation("-------------------------------------------------");
}
}
public void PrintDebugNoUserDataAndOutsideCutoffEpisodeInfo(IReadOnlyCollection<BaseItem> episodes)
{
ArgumentNullException.ThrowIfNull(episodes);
if(Configuration.DebugMode){
_logger.LogInformation("No user data, and creation date is outside of media cutoff, Season is stale.");
_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

@@ -8,26 +8,20 @@ using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.MediaCleaner.Helpers; namespace Jellyfin.Plugin.MediaCleaner.Helpers;
public class MovieHelper public class MovieHelper(ILogger logger)
{ {
private readonly ILogger _logger; private readonly LoggingHelper _loggingHelper = new(logger);
public MovieHelper(ILogger logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
private static PluginConfiguration Configuration => private static PluginConfiguration Configuration =>
Plugin.Instance!.Configuration; Plugin.Instance!.Configuration;
public bool IsMovieStale(BaseItem movie) public bool IsMovieStale(BaseItem movie)
{ {
if (Configuration.DebugMode) ArgumentNullException.ThrowIfNull(movie, "IsMovieStale process recieved a null movie. Logic failure. Exception thrown.");
{
_logger.LogInformation("-------------------------------------------------"); _loggingHelper.LogDebugInformation("-------------------------------------------------");
_logger.LogInformation("Start of scanning for movie: {Movie}", movie); _loggingHelper.LogDebugInformation("Scanning movie: {Movie}", movie);
_logger.LogInformation("-------------------------------------------------"); _loggingHelper.LogDebugInformation("-------------------------------------------------");
}
bool movieIsStale = false; bool movieIsStale = false;
@@ -38,41 +32,41 @@ public class MovieHelper
{ {
var mostRecentUserData = movie.UserData.OrderByDescending(data => data.LastPlayedDate).First(data => data.LastPlayedDate != null); var mostRecentUserData = movie.UserData.OrderByDescending(data => data.LastPlayedDate).First(data => data.LastPlayedDate != null);
if (Configuration.DebugMode){ _loggingHelper.LogDebugInformation("Most recent user data: {Movie}", movie);
_logger.LogInformation("Most recent user data: {Movie}", movie);
foreach (var property in typeof(UserData).GetProperties()) foreach (var property in typeof(UserData).GetProperties())
{ {
_logger.LogInformation("{PropertyName}: {PropertyValue}", property.Name, property.GetValue(mostRecentUserData)); _loggingHelper.LogDebugInformation("{PropertyName}: {PropertyValue}", property.Name, property.GetValue(mostRecentUserData));
}
_logger.LogInformation("-------------------------------------------------");
} }
_loggingHelper.LogDebugInformation("-------------------------------------------------");
if (mostRecentUserData.LastPlayedDate < DateTime.Now.AddDays(-Configuration.StaleMediaCutoff)) if (mostRecentUserData.LastPlayedDate < DateTime.Now.AddDays(-Configuration.StaleMediaCutoff))
{ {
if (Configuration.DebugMode) _loggingHelper.LogDebugInformation("Most recent user data has last played date that is outside of cutoff.");
{ _loggingHelper.LogDebugInformation("Adding {Movie} to stale movies.", movie);
_logger.LogInformation("Most recent user data last played date is outside of cutoff. Adding {Movie} to stale movies.", movie); _loggingHelper.LogDebugInformation("With Last Played Date: {LastPlayedDate}", mostRecentUserData.LastPlayedDate);
}
movieIsStale = true; movieIsStale = true;
} }
} }
else if (createdOutsideCutoff)
if (createdOutsideCutoff && !hasUserData)
{ {
if (Configuration.DebugMode) _loggingHelper.LogDebugInformation("Movie has no user data and was created outside of cutoff: {DateCreated}.", movie.DateCreated);
{ _loggingHelper.LogDebugInformation("Adding {Movie} to stale movies.", movie);
_logger.LogInformation("Movie has no user data and was created outside of cutoff: {DateCreated}. Adding {Movie} to stale movies.", [movie.DateCreated, movie]);
}
movieIsStale = true; movieIsStale = true;
} }
if (Configuration.DebugMode) if (!createdOutsideCutoff && !hasUserData)
{ {
_logger.LogInformation("-------------------------------------------------"); _loggingHelper.LogDebugInformation("Movie has no user data and was not created outside of cutoff: {DateCreated}.", movie.DateCreated);
_logger.LogInformation("End of scanning for movie: {Movie}", movie); _loggingHelper.LogDebugInformation("Movie is not stale.");
_logger.LogInformation("-------------------------------------------------");
} }
_loggingHelper.LogDebugInformation("-------------------------------------------------");
_loggingHelper.LogDebugInformation("End of scanning for movie: {Movie}", movie);
return movieIsStale; return movieIsStale;
} }

View File

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

@@ -60,7 +60,10 @@ public sealed class StaleMediaTask : IScheduledTask
Task IScheduledTask.ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) Task IScheduledTask.ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{ {
_loggingHelper.StartLogging(); _loggingHelper.LogDebugInformation("--DEBUG MODE ACTIVE--");
_loggingHelper.LogInformation("-------------------------------------------------");
_loggingHelper.LogInformation("Starting stale media scan...");
_loggingHelper.LogInformation("-------------------------------------------------");
var query = new InternalItemsQuery var query = new InternalItemsQuery
{ {
@@ -70,21 +73,63 @@ public sealed class StaleMediaTask : IScheduledTask
List<BaseItem> allItems = [.. _libraryManager.GetItemsResult(query).Items]; List<BaseItem> allItems = [.. _libraryManager.GetItemsResult(query).Items];
_loggingHelper.PrintStaleItemsInformation(allItems); _loggingHelper.LogInformation("Total items to scan: {ItemCount}", allItems.Count);
_loggingHelper.LogDebugInformation("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)];
_loggingHelper.StartScanningSeriesItems(); _loggingHelper.LogDebugInformation("-------------------------------------------------");
List<BaseItem> staleEpisodes = [.. series.SelectMany(GetStaleEpisodes)]; _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.");
_loggingHelper.StartScanningMoviesItems();
List<BaseItem> staleMovies = [.. GetStaleMovies(movies)]; List<BaseItem> staleMovies = [.. GetStaleMovies(movies)];
_loggingHelper.PrintStaleMoviesInformation(staleMovies); _loggingHelper.LogDebugInformation("-------------------------------------------------");
_loggingHelper.PrintStaleEpisodesInformation(FindSeriesInfoFromEpisodes, staleEpisodes); _loggingHelper.LogDebugInformation("End of scan for movie items.");
_loggingHelper.EndLogging(); _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)
{
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!");
}
_loggingHelper.LogInformation("-------------------------------------------------");
_loggingHelper.LogInformation("Ending stale media scan...");
_loggingHelper.LogInformation("-------------------------------------------------");
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -93,91 +138,83 @@ public sealed class StaleMediaTask : IScheduledTask
{ {
List<BaseItem> staleMovies = []; List<BaseItem> staleMovies = [];
staleMovies.AddRange(movies.Where(_movieHelper.IsMovieStale)); try
{
staleMovies.AddRange(movies.Where(_movieHelper.IsMovieStale));
}
catch (ArgumentNullException ex)
{
_loggingHelper.LogInformation("Arguement Null Exception in GetStaleMovies!");
_loggingHelper.LogInformation(ex.Message);
}
return staleMovies; return staleMovies;
} }
private List<BaseItem> GetStaleEpisodes(BaseItem item) private List<BaseItem> GetStaleSeasons(BaseItem item)
{ {
List<BaseItem> staleEpisodes = []; _loggingHelper.LogDebugInformation("-------------------------------------------------");
_loggingHelper.LogDebugInformation("Debug data for series: {SeriesName}", item.Name);
_loggingHelper.LogDebugInformation("-------------------------------------------------");
// 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
}); });
_loggingHelper.PrintDebugDataForSeries(item); List<BaseItem> staleSeasons = [ ..seasons
.Where(season => {
foreach (var season in seasons)
{
// 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 seasonCreatedOutsideCutoff = episodes.All(episode => episode.DateCreated < DateTime.Now.AddDays(-Configuration.StaleMediaCutoff)); _loggingHelper.LogDebugInformation("Season debug information for {SeasonNumber}:", season);
_loggingHelper.PrintDebugSeasonInfo(); bool isSeasonDataStale = false;
if (seasonCreatedOutsideCutoff) try
{ {
_loggingHelper.PrintDebugSeasonCreatedOutsideCutoff(); isSeasonDataStale = _seriesHelper.IsSeasonDataStale(episodes);
}
catch (ArgumentNullException ex)
{
_loggingHelper.LogInformation("Arguement Null Exception in GetStaleSeasons!");
_loggingHelper.LogInformation(ex.Message);
} }
bool seasonHasUserData = episodes.Any(episode => episode.UserData.Count > 0); _loggingHelper.LogDebugInformation("End of season debug information for {SeasonNumber}.", season);
bool seasonIsStale = (seasonHasUserData && _seriesHelper.IsSeasonUserDataStale(episodes)) || seasonCreatedOutsideCutoff;
bool noUserDataAndOutsideCutoff = !seasonHasUserData && seasonCreatedOutsideCutoff;
if (seasonIsStale) return isSeasonDataStale;
{ })];
if (noUserDataAndOutsideCutoff)
{
_loggingHelper.PrintDebugNoUserDataAndOutsideCutoffEpisodeInfo(episodes);
}
staleEpisodes.AddRange(episodes);
}
}
_loggingHelper.PrintDebugEndOfScanningForSeries(item); _loggingHelper.LogDebugInformation("-------------------------------------------------");
_loggingHelper.LogDebugInformation("End of scanning for series: {Series}", item);
return staleEpisodes; return staleSeasons;
} }
private List<SeriesInfo> FindSeriesInfoFromEpisodes(IReadOnlyCollection<BaseItem> episodes) private IEnumerable<SeriesInfo> FindSeriesInfo(IReadOnlyCollection<BaseItem> seasons)
{ {
Guid[] seasonIds = [.. episodes.Select(episode => episode.ParentId).Distinct()];
var seasons = _libraryManager.GetItemList(new InternalItemsQuery
{
ItemIds = seasonIds
});
Guid[] seriesIds = [.. seasons.Select(season => season.ParentId).Distinct()]; Guid[] seriesIds = [.. seasons.Select(season => season.ParentId).Distinct()];
var series = _libraryManager.GetItemList(new InternalItemsQuery IReadOnlyCollection<BaseItem> series = _libraryManager.GetItemList(new InternalItemsQuery
{ {
ItemIds = seriesIds ItemIds = seriesIds
}).ToList(); });
List<string> seriesNames = [.. series.Select(series => series.Name).Distinct()]; IEnumerable<SeriesInfo> seriesInfoList = series.Select(series =>
List<SeriesInfo> seriesInfoList = [];
series.ForEach(series =>
{ {
seriesInfoList.Add(new SeriesInfo return new SeriesInfo
{ {
Id = series.Id, Id = series.Id,
SeriesName = series.Name, SeriesName = series.Name,
Seasons = [.. seasons.Where(season => season.ParentId == series.Id).Select(season => season.Name)] Seasons = [.. seasons.Where(season => season.ParentId == series.Id).Select(season => season.Name)]
}); };
}); });
return seriesInfoList; return seriesInfoList;
@@ -186,6 +223,7 @@ public sealed class StaleMediaTask : IScheduledTask
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)