10 Commits

14 changed files with 259 additions and 291 deletions

View File

@@ -1,5 +1,5 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<AssemblyVersion>0.0.0.8</AssemblyVersion> <AssemblyVersion>0.0.0.9</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,17 @@ 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) _loggingHelper.LogDebugInformation("Start of scanning for movie: {Movie}", movie);
{ _loggingHelper.LogDebugInformation("-------------------------------------------------");
_logger.LogInformation("-------------------------------------------------");
_logger.LogInformation("Start of scanning for movie: {Movie}", movie);
_logger.LogInformation("-------------------------------------------------");
}
bool movieIsStale = false; bool movieIsStale = false;
@@ -38,40 +29,33 @@ 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) else if (createdOutsideCutoff)
{ {
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) _loggingHelper.LogDebugInformation("-------------------------------------------------");
{ _loggingHelper.LogDebugInformation("End of scanning for movie: {Movie}", movie);
_logger.LogInformation("-------------------------------------------------");
_logger.LogInformation("End of scanning for movie: {Movie}", movie);
_logger.LogInformation("-------------------------------------------------");
}
return movieIsStale; return movieIsStale;
} }

View File

@@ -11,71 +11,80 @@ 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)
{
if(episodes == null)
{
ArgumentNullException.ThrowIfNull(episodes);
} }
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,50 @@ 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: {ItemCount}", allItems.Count);
_loggingHelper.LogInformation("Stale 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.LogInformation("-------------------------------------------------");
List<BaseItem> staleEpisodes = [.. series.SelectMany(GetStaleEpisodes)]; _loggingHelper.LogInformation("Starting scan of series items.");
_loggingHelper.LogInformation("-------------------------------------------------");
List<BaseItem> staleSeasons = [.. series.SelectMany(GetStaleSeasons)];
_loggingHelper.LogInformation("Starting scan of movies items.");
_loggingHelper.LogInformation("-------------------------------------------------");
_loggingHelper.StartScanningMoviesItems();
List<BaseItem> staleMovies = [.. GetStaleMovies(movies)]; List<BaseItem> staleMovies = [.. GetStaleMovies(movies)];
_loggingHelper.PrintStaleMoviesInformation(staleMovies); _loggingHelper.LogInformation("-------------------------------------------------");
_loggingHelper.PrintStaleEpisodesInformation(FindSeriesInfoFromEpisodes, staleEpisodes); _loggingHelper.LogInformation("Stale Movies found: {StaleMovies}", staleMovies.Count);
_loggingHelper.EndLogging(); if (staleMovies.Count > 0 && Configuration.DebugMode)
{
foreach (var movieInfo in staleMovies)
{
_loggingHelper.LogDebugInformation("Movie Info: ID: {Id} | Movie Name: {MovieName}", [movieInfo.Id, movieInfo.Name]);
}
}
_loggingHelper.LogInformation("-------------------------------------------------");
_loggingHelper.LogInformation("Stale seasons found: {StaleSeasons}", staleSeasons.Count);
if (staleSeasons.Count > 0 && Configuration.DebugMode)
{
IEnumerable<SeriesInfo> staleSeriesInfo = FindSeriesInfo(staleSeasons);
foreach (var seriesInfo in staleSeriesInfo)
{
_loggingHelper.LogDebugInformation("Series Info: ID: {Id} | Series Name: {SeriesName} | Stale Seasons: {Seasons}", [seriesInfo.Id, seriesInfo.SeriesName, string.Join(", ", seriesInfo.Seasons)]);
}
}
_loggingHelper.LogInformation("-------------------------------------------------");
_loggingHelper.LogInformation("Ending stale media scan...");
_loggingHelper.LogInformation("-------------------------------------------------");
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -99,9 +131,10 @@ public sealed class StaleMediaTask : IScheduledTask
} }
private List<BaseItem> GetStaleEpisodes(BaseItem item) private List<BaseItem> GetStaleSeasons(BaseItem item)
{ {
List<BaseItem> staleEpisodes = []; _loggingHelper.LogDebugInformation("Debug data for series: {SeriesName}", item.Name);
_loggingHelper.LogDebugInformation("-------------------------------------------------");
// Gets each season in a show // Gets each season in a show
var seasons = _libraryManager.GetItemList(new InternalItemsQuery var seasons = _libraryManager.GetItemList(new InternalItemsQuery
@@ -110,74 +143,48 @@ public sealed class StaleMediaTask : IScheduledTask
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 = _seriesHelper.IsSeasonDataStale(episodes);
if (seasonCreatedOutsideCutoff) _loggingHelper.LogDebugInformation("End of season debug information for {SeasonNumber}.", season);
{
_loggingHelper.PrintDebugSeasonCreatedOutsideCutoff();
}
bool seasonHasUserData = episodes.Any(episode => episode.UserData.Count > 0); return isSeasonDataStale;
bool seasonIsStale = (seasonHasUserData && _seriesHelper.IsSeasonUserDataStale(episodes)) || seasonCreatedOutsideCutoff; })];
bool noUserDataAndOutsideCutoff = !seasonHasUserData && seasonCreatedOutsideCutoff;
if (seasonIsStale)
{
if (noUserDataAndOutsideCutoff)
{
_loggingHelper.PrintDebugNoUserDataAndOutsideCutoffEpisodeInfo(episodes);
}
staleEpisodes.AddRange(episodes); _loggingHelper.LogDebugInformation("-------------------------------------------------");
} _loggingHelper.LogDebugInformation("End of scanning for series: {Series}", item);
} _loggingHelper.LogDebugInformation("-------------------------------------------------");
_loggingHelper.PrintDebugEndOfScanningForSeries(item); return staleSeasons;
return staleEpisodes;
} }
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;

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)