From 82441d52471b7ea8ebfc7cf3b4c2d73e8354f71d Mon Sep 17 00:00:00 2001 From: Thomas Gander Date: Sun, 25 Jan 2026 08:58:46 -0700 Subject: [PATCH 01/11] Improved logging futher and updated some methods to use LogDebugInformation and to handle exceptions --- Directory.Build.props | 2 +- .../Helpers/MovieHelper.cs | 11 ++++- .../Helpers/SeriesHelper.cs | 5 +-- .../ScheduledTasks/StaleMediaTask.cs | 44 +++++++++++++++---- 4 files changed, 47 insertions(+), 15 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 12d33c7..a334026 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - 0.0.0.9 + 0.0.0.10 diff --git a/Jellyfin.Plugin.MediaCleaner/Helpers/MovieHelper.cs b/Jellyfin.Plugin.MediaCleaner/Helpers/MovieHelper.cs index 429ed1c..7621892 100644 --- a/Jellyfin.Plugin.MediaCleaner/Helpers/MovieHelper.cs +++ b/Jellyfin.Plugin.MediaCleaner/Helpers/MovieHelper.cs @@ -17,6 +17,8 @@ public class MovieHelper(ILogger logger) public bool IsMovieStale(BaseItem movie) { + ArgumentNullException.ThrowIfNull(movie, "IsMovieStale process recieved a null movie. Logic failure. Exception thrown."); + _loggingHelper.LogDebugInformation("Start of scanning for movie: {Movie}", movie); _loggingHelper.LogDebugInformation("-------------------------------------------------"); @@ -47,13 +49,20 @@ public class MovieHelper(ILogger logger) movieIsStale = true; } } - else if (createdOutsideCutoff) + + 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); diff --git a/Jellyfin.Plugin.MediaCleaner/Helpers/SeriesHelper.cs b/Jellyfin.Plugin.MediaCleaner/Helpers/SeriesHelper.cs index 51d2950..cbd8c3d 100644 --- a/Jellyfin.Plugin.MediaCleaner/Helpers/SeriesHelper.cs +++ b/Jellyfin.Plugin.MediaCleaner/Helpers/SeriesHelper.cs @@ -69,10 +69,7 @@ public class SeriesHelper(ILogger logger) public bool IsSeasonDataStale(IReadOnlyList episodes) { - if(episodes == null) - { - ArgumentNullException.ThrowIfNull(episodes); - } + ArgumentNullException.ThrowIfNull(episodes, "IsSeasonDataStale process recieved null episodes. Logic failure. Exception thrown."); bool seasonIsStale = false; diff --git a/Jellyfin.Plugin.MediaCleaner/ScheduledTasks/StaleMediaTask.cs b/Jellyfin.Plugin.MediaCleaner/ScheduledTasks/StaleMediaTask.cs index 7c9f3d1..8400349 100644 --- a/Jellyfin.Plugin.MediaCleaner/ScheduledTasks/StaleMediaTask.cs +++ b/Jellyfin.Plugin.MediaCleaner/ScheduledTasks/StaleMediaTask.cs @@ -74,7 +74,7 @@ public sealed class StaleMediaTask : IScheduledTask List allItems = [.. _libraryManager.GetItemsResult(query).Items]; _loggingHelper.LogInformation("Total items: {ItemCount}", allItems.Count); - _loggingHelper.LogInformation("Stale items found: {AllItems}", allItems); + _loggingHelper.LogDebugInformation("Items found: {AllItems}", allItems); List series = [.. allItems.Where(item => item.GetBaseItemKind() == BaseItemKind.Series)]; List movies = [.. allItems.Where(item => item.GetBaseItemKind() == BaseItemKind.Movie)]; @@ -85,6 +85,7 @@ public sealed class StaleMediaTask : IScheduledTask List staleSeasons = [.. series.SelectMany(GetStaleSeasons)]; + _loggingHelper.LogInformation("-------------------------------------------------"); _loggingHelper.LogInformation("Starting scan of movies items."); _loggingHelper.LogInformation("-------------------------------------------------"); @@ -93,26 +94,34 @@ public sealed class StaleMediaTask : IScheduledTask _loggingHelper.LogInformation("-------------------------------------------------"); _loggingHelper.LogInformation("Stale Movies found: {StaleMovies}", staleMovies.Count); - if (staleMovies.Count > 0 && Configuration.DebugMode) + if (staleMovies.Count > 0) { foreach (var movieInfo in staleMovies) { - _loggingHelper.LogDebugInformation("Movie Info: ID: {Id} | Movie Name: {MovieName}", [movieInfo.Id, movieInfo.Name]); + _loggingHelper.LogInformation("Movie Info: ID: {Id} | Movie Name: {MovieName}", [movieInfo.Id, movieInfo.Name]); } } + else + { + _loggingHelper.LogInformation("No stale movies found!"); + } _loggingHelper.LogInformation("-------------------------------------------------"); _loggingHelper.LogInformation("Stale seasons found: {StaleSeasons}", staleSeasons.Count); - if (staleSeasons.Count > 0 && Configuration.DebugMode) + if (staleSeasons.Count > 0) { IEnumerable 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("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("Ending stale media scan..."); @@ -125,7 +134,15 @@ public sealed class StaleMediaTask : IScheduledTask { List 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; } @@ -136,7 +153,6 @@ public sealed class StaleMediaTask : IScheduledTask _loggingHelper.LogDebugInformation("Debug data for series: {SeriesName}", item.Name); _loggingHelper.LogDebugInformation("-------------------------------------------------"); - // Gets each season in a show var seasons = _libraryManager.GetItemList(new InternalItemsQuery { ParentId = item.Id, @@ -153,7 +169,17 @@ public sealed class StaleMediaTask : IScheduledTask _loggingHelper.LogDebugInformation("Season debug information for {SeasonNumber}:", season); - bool isSeasonDataStale = _seriesHelper.IsSeasonDataStale(episodes); + bool isSeasonDataStale = false; + + try + { + isSeasonDataStale = _seriesHelper.IsSeasonDataStale(episodes); + } + catch (ArgumentNullException ex) + { + _loggingHelper.LogInformation("Arguement Null Exception in GetStaleSeasons!"); + _loggingHelper.LogInformation(ex.Message); + } _loggingHelper.LogDebugInformation("End of season debug information for {SeasonNumber}.", season); @@ -163,7 +189,6 @@ public sealed class StaleMediaTask : IScheduledTask _loggingHelper.LogDebugInformation("-------------------------------------------------"); _loggingHelper.LogDebugInformation("End of scanning for series: {Series}", item); - _loggingHelper.LogDebugInformation("-------------------------------------------------"); return staleSeasons; } @@ -193,6 +218,7 @@ public sealed class StaleMediaTask : IScheduledTask IEnumerable IScheduledTask.GetDefaultTriggers() { // Run this task every 24 hours + // Unnecessary, and will be removed once front end page is ready. yield return new TaskTriggerInfo { Type = TaskTriggerInfoType.IntervalTrigger, From 9bddc59fe25592e87460d913b68426aaa4463cce Mon Sep 17 00:00:00 2001 From: Thomas Gander Date: Sun, 25 Jan 2026 09:11:45 -0700 Subject: [PATCH 02/11] Further improvement to logging formatting and readability --- .../Helpers/MovieHelper.cs | 3 +- .../ScheduledTasks/StaleMediaTask.cs | 35 +++++++++++-------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/Jellyfin.Plugin.MediaCleaner/Helpers/MovieHelper.cs b/Jellyfin.Plugin.MediaCleaner/Helpers/MovieHelper.cs index 7621892..86a9c06 100644 --- a/Jellyfin.Plugin.MediaCleaner/Helpers/MovieHelper.cs +++ b/Jellyfin.Plugin.MediaCleaner/Helpers/MovieHelper.cs @@ -19,7 +19,8 @@ public class MovieHelper(ILogger logger) { ArgumentNullException.ThrowIfNull(movie, "IsMovieStale process recieved a null movie. Logic failure. Exception thrown."); - _loggingHelper.LogDebugInformation("Start of scanning for movie: {Movie}", movie); + _loggingHelper.LogDebugInformation("-------------------------------------------------"); + _loggingHelper.LogDebugInformation("Scanning movie: {Movie}", movie); _loggingHelper.LogDebugInformation("-------------------------------------------------"); bool movieIsStale = false; diff --git a/Jellyfin.Plugin.MediaCleaner/ScheduledTasks/StaleMediaTask.cs b/Jellyfin.Plugin.MediaCleaner/ScheduledTasks/StaleMediaTask.cs index 8400349..d7dfb28 100644 --- a/Jellyfin.Plugin.MediaCleaner/ScheduledTasks/StaleMediaTask.cs +++ b/Jellyfin.Plugin.MediaCleaner/ScheduledTasks/StaleMediaTask.cs @@ -81,30 +81,19 @@ public sealed class StaleMediaTask : IScheduledTask _loggingHelper.LogInformation("-------------------------------------------------"); _loggingHelper.LogInformation("Starting scan of series items."); - _loggingHelper.LogInformation("-------------------------------------------------"); List staleSeasons = [.. series.SelectMany(GetStaleSeasons)]; _loggingHelper.LogInformation("-------------------------------------------------"); - _loggingHelper.LogInformation("Starting scan of movies items."); + _loggingHelper.LogInformation("End of scan for series items."); + _loggingHelper.LogInformation("-------------------------------------------------"); + _loggingHelper.LogInformation("Starting scan of movie items."); List staleMovies = [.. GetStaleMovies(movies)]; _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("End of scan for movie items."); _loggingHelper.LogInformation("-------------------------------------------------"); _loggingHelper.LogInformation("Stale seasons found: {StaleSeasons}", staleSeasons.Count); @@ -123,6 +112,21 @@ public sealed class StaleMediaTask : IScheduledTask _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("-------------------------------------------------"); @@ -150,6 +154,7 @@ public sealed class StaleMediaTask : IScheduledTask private List GetStaleSeasons(BaseItem item) { + _loggingHelper.LogDebugInformation("-------------------------------------------------"); _loggingHelper.LogDebugInformation("Debug data for series: {SeriesName}", item.Name); _loggingHelper.LogDebugInformation("-------------------------------------------------"); From 66716a9bc965ac2fba5575ba3dc53e25889424a1 Mon Sep 17 00:00:00 2001 From: Thomas Gander Date: Sun, 25 Jan 2026 09:15:26 -0700 Subject: [PATCH 03/11] Updated logging to be more concise when debug is not active --- .../ScheduledTasks/StaleMediaTask.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Jellyfin.Plugin.MediaCleaner/ScheduledTasks/StaleMediaTask.cs b/Jellyfin.Plugin.MediaCleaner/ScheduledTasks/StaleMediaTask.cs index d7dfb28..13a8c9d 100644 --- a/Jellyfin.Plugin.MediaCleaner/ScheduledTasks/StaleMediaTask.cs +++ b/Jellyfin.Plugin.MediaCleaner/ScheduledTasks/StaleMediaTask.cs @@ -73,27 +73,27 @@ public sealed class StaleMediaTask : IScheduledTask List allItems = [.. _libraryManager.GetItemsResult(query).Items]; - _loggingHelper.LogInformation("Total items: {ItemCount}", allItems.Count); + _loggingHelper.LogInformation("Total items to scan: {ItemCount}", allItems.Count); _loggingHelper.LogDebugInformation("Items found: {AllItems}", allItems); List series = [.. allItems.Where(item => item.GetBaseItemKind() == BaseItemKind.Series)]; List movies = [.. allItems.Where(item => item.GetBaseItemKind() == BaseItemKind.Movie)]; - _loggingHelper.LogInformation("-------------------------------------------------"); - _loggingHelper.LogInformation("Starting scan of series items."); + _loggingHelper.LogDebugInformation("-------------------------------------------------"); + _loggingHelper.LogDebugInformation("Starting scan of series items."); List staleSeasons = [.. series.SelectMany(GetStaleSeasons)]; - _loggingHelper.LogInformation("-------------------------------------------------"); - _loggingHelper.LogInformation("End of scan for series items."); + _loggingHelper.LogDebugInformation("-------------------------------------------------"); + _loggingHelper.LogDebugInformation("End of scan for series items."); - _loggingHelper.LogInformation("-------------------------------------------------"); - _loggingHelper.LogInformation("Starting scan of movie items."); + _loggingHelper.LogDebugInformation("-------------------------------------------------"); + _loggingHelper.LogDebugInformation("Starting scan of movie items."); List staleMovies = [.. GetStaleMovies(movies)]; - _loggingHelper.LogInformation("-------------------------------------------------"); - _loggingHelper.LogInformation("End of scan for movie items."); + _loggingHelper.LogDebugInformation("-------------------------------------------------"); + _loggingHelper.LogDebugInformation("End of scan for movie items."); _loggingHelper.LogInformation("-------------------------------------------------"); _loggingHelper.LogInformation("Stale seasons found: {StaleSeasons}", staleSeasons.Count); From 04ef815a9bd57f7f742a29b1a6099b7bbc6ccc5e Mon Sep 17 00:00:00 2001 From: Thomas Gander Date: Sun, 25 Jan 2026 14:52:47 -0700 Subject: [PATCH 04/11] Converted StaleMediaTask to StaleMediaScanner for use in plugin home page. Also added a generic Media Info class that can be filtered to return the data you desire. --- .../Controllers/StateController.cs | 32 ++++--- .../Data/MediaCleanerState.cs | 49 +++++++++++ .../Data/PluginState.cs | 29 ------ .../Models/MediaInfo.cs | 9 ++ .../Models/MovieInfo.cs | 14 +++ .../Models/SeriesInfo.cs | 16 +--- Jellyfin.Plugin.MediaCleaner/Pages/home.html | 27 ++++-- .../Pages/media_cleaner_table.js | 88 +++++++++++++++---- .../PluginServiceRegistrator.cs | 2 +- ...StaleMediaTask.cs => StaleMediaScanner.cs} | 58 ++++++------ 10 files changed, 213 insertions(+), 111 deletions(-) create mode 100644 Jellyfin.Plugin.MediaCleaner/Data/MediaCleanerState.cs delete mode 100644 Jellyfin.Plugin.MediaCleaner/Data/PluginState.cs create mode 100644 Jellyfin.Plugin.MediaCleaner/Models/MediaInfo.cs create mode 100644 Jellyfin.Plugin.MediaCleaner/Models/MovieInfo.cs rename Jellyfin.Plugin.MediaCleaner/{ScheduledTasks/StaleMediaTask.cs => StaleMediaScanner.cs} (83%) diff --git a/Jellyfin.Plugin.MediaCleaner/Controllers/StateController.cs b/Jellyfin.Plugin.MediaCleaner/Controllers/StateController.cs index c66c6be..5e3833b 100644 --- a/Jellyfin.Plugin.MediaCleaner/Controllers/StateController.cs +++ b/Jellyfin.Plugin.MediaCleaner/Controllers/StateController.cs @@ -1,22 +1,32 @@ using Jellyfin.Plugin.MediaCleaner.Data; +using Jellyfin.Plugin.MediaCleaner; using Jellyfin.Plugin.MediaCleaner.Models; using Microsoft.AspNetCore.Mvc; +using Jellyfin.Plugin.MediaCleaner.Configuration; namespace Jellyfin.Plugin.MediaCleaner.Controllers; [Route("mediacleaner/state")] -public class StateController : Controller +public class StateController(MediaCleanerState state) : Controller { - private readonly PluginState _state; - public StateController(PluginState state) => _state = state; + private readonly MediaCleanerState _state = state; + private static PluginConfiguration Configuration => + Plugin.Instance!.Configuration; - [HttpGet] - public IActionResult Get() => Ok(_state.GetSeriesInfo()); + [HttpGet("getSeriesInfo")] + public IActionResult GetSeriesInfo() => Ok(_state.GetSeriesInfo()); - [HttpPost("add")] - public IActionResult AddSeriesInfo([FromBody] SeriesInfo seriesInfo) - { - _state.AddSeriesInfo(seriesInfo); - return Ok(); - } + [HttpGet("getMovieInfo")] + public IActionResult GetMovieInfo() => Ok(_state.GetMovieInfo()); + + [HttpGet("updateState")] + public IActionResult GetUpdateState() => Ok(_state.UpdateState()); + + [HttpGet("getMoviesTitle")] + public IActionResult GetMoviesTitle() => + Ok($"Stale Movies (Unwatched for and created over {Configuration.StaleMediaCutoff} Days ago.)"); + + [HttpGet("getSeriesTitle")] + public IActionResult GetSeriesTitle() => + Ok($"Stale Series (Unwatched for and created over {Configuration.StaleMediaCutoff} Days ago.)"); } diff --git a/Jellyfin.Plugin.MediaCleaner/Data/MediaCleanerState.cs b/Jellyfin.Plugin.MediaCleaner/Data/MediaCleanerState.cs new file mode 100644 index 0000000..7fa61db --- /dev/null +++ b/Jellyfin.Plugin.MediaCleaner/Data/MediaCleanerState.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Database.Implementations.ModelConfiguration; +using Jellyfin.Plugin.MediaCleaner; +using Jellyfin.Plugin.MediaCleaner.Models; +using Jellyfin.Plugin.MediaCleaner.ScheduledTasks; +using MediaBrowser.Controller.Library; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Plugin.MediaCleaner.Data; + +public class MediaCleanerState +{ + private readonly Lock _lock = new(); + private IEnumerable _mediaInfo = []; + private ILogger _logger; + private readonly StaleMediaScanner _staleMediaScanner; + + public MediaCleanerState(ILogger logger, ILibraryManager libraryManager) + { + _logger = logger; + _staleMediaScanner = new(logger, libraryManager); + } + + public async Task UpdateState() + { + _mediaInfo = await _staleMediaScanner.ScanStaleMedia().ConfigureAwait(false); + } + + public IEnumerable GetSeriesInfo() + { + lock (_lock) + { + return _mediaInfo.OfType(); + } + } + + public IEnumerable GetMovieInfo() + { + lock (_lock) + { + return _mediaInfo.OfType(); + } + } +} diff --git a/Jellyfin.Plugin.MediaCleaner/Data/PluginState.cs b/Jellyfin.Plugin.MediaCleaner/Data/PluginState.cs deleted file mode 100644 index 53f038f..0000000 --- a/Jellyfin.Plugin.MediaCleaner/Data/PluginState.cs +++ /dev/null @@ -1,29 +0,0 @@ -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 = new List - { - new SeriesInfo { SeriesName = "TestName", Id = System.Guid.NewGuid() } - }; - - public void AddSeriesInfo(SeriesInfo seriesInfo) - { - lock (_lock) - { - _seriesInfo.Add(seriesInfo); - } - } - - public IEnumerable GetSeriesInfo() - { - lock (_lock) - { - return _seriesInfo; - } - } -} diff --git a/Jellyfin.Plugin.MediaCleaner/Models/MediaInfo.cs b/Jellyfin.Plugin.MediaCleaner/Models/MediaInfo.cs new file mode 100644 index 0000000..7ae7881 --- /dev/null +++ b/Jellyfin.Plugin.MediaCleaner/Models/MediaInfo.cs @@ -0,0 +1,9 @@ +using System; + +namespace Jellyfin.Plugin.MediaCleaner.Models; + +public abstract class MediaInfo +{ + public required Guid Id { get; set; } + public required string Name { get; set; } +} diff --git a/Jellyfin.Plugin.MediaCleaner/Models/MovieInfo.cs b/Jellyfin.Plugin.MediaCleaner/Models/MovieInfo.cs new file mode 100644 index 0000000..c70717f --- /dev/null +++ b/Jellyfin.Plugin.MediaCleaner/Models/MovieInfo.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Jellyfin.Plugin.MediaCleaner; + +namespace Jellyfin.Plugin.MediaCleaner.Models; + +/// +/// Contains Movie information. +/// +public class MovieInfo : MediaInfo +{ + +} diff --git a/Jellyfin.Plugin.MediaCleaner/Models/SeriesInfo.cs b/Jellyfin.Plugin.MediaCleaner/Models/SeriesInfo.cs index 59ff805..1a49baa 100644 --- a/Jellyfin.Plugin.MediaCleaner/Models/SeriesInfo.cs +++ b/Jellyfin.Plugin.MediaCleaner/Models/SeriesInfo.cs @@ -1,26 +1,14 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using Jellyfin.Plugin.MediaCleaner; namespace Jellyfin.Plugin.MediaCleaner.Models; /// /// Contains series information. /// -public class SeriesInfo +public class SeriesInfo : MediaInfo { - /// - /// Gets or sets series identifier. - /// - public Guid Id { get; set; } - - /// - /// Gets or sets series name. - /// - public string SeriesName { get; set; } = string.Empty; - - /// - /// Gets or sets seasons. - /// public IEnumerable Seasons { get; set; } = []; } diff --git a/Jellyfin.Plugin.MediaCleaner/Pages/home.html b/Jellyfin.Plugin.MediaCleaner/Pages/home.html index e5971e0..3e8b22e 100644 --- a/Jellyfin.Plugin.MediaCleaner/Pages/home.html +++ b/Jellyfin.Plugin.MediaCleaner/Pages/home.html @@ -3,18 +3,29 @@

Media Cleaner

+ +

+ + + + + + +
Name
+
+
+ +

- - - - - - - + + + + + +
IDSeries Name
NameSeasons
diff --git a/Jellyfin.Plugin.MediaCleaner/Pages/media_cleaner_table.js b/Jellyfin.Plugin.MediaCleaner/Pages/media_cleaner_table.js index a56f7ba..25caa16 100644 --- a/Jellyfin.Plugin.MediaCleaner/Pages/media_cleaner_table.js +++ b/Jellyfin.Plugin.MediaCleaner/Pages/media_cleaner_table.js @@ -1,9 +1,10 @@ -var table = document.getElementById("seriesTable"); +var moviesTitle = document.getElementById("moviesTitle"); +var seriesTitle = document.getElementById("seriesTitle"); +var moviesTable = document.getElementById("moviesTable"); +var seriesTable = document.getElementById("seriesTable"); - - -const getMediaCleanerState = async () => { - const response = await fetch('/mediacleaner/state'); +const getMediaCleanerSeriesInfo = async () => { + const response = await fetch("/mediacleaner/state/getSeriesInfo"); if(!response.ok){ throw new Error(`Response status: ${response.status}`) @@ -12,16 +13,73 @@ const getMediaCleanerState = async () => { return response.json(); } -var state = await getMediaCleanerState(); +const getMediaCleanerMovieInfo = async () => { + const response = await fetch("/mediacleaner/state/getMovieInfo"); -console.log("State: ", state); + if(!response.ok){ + throw new Error(`Response status: ${response.status}`) + } -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; + return response.json(); } + +const updateMediaCleanerState = async () => { + const response = await fetch("/mediacleaner/state/updateState"); + + if(!response.ok){ + throw new Error(`Response status: ${response.status}`) + } + + return response.json(); +} + +const getMediaCleanerSeriesTitle = async () => { + const response = await fetch("/mediacleaner/state/getSeriesTitle"); + + if(!response.ok){ + throw new Error(`Response status: ${response.status}`); + } + + return response.json(); +} + +const getMediaCleanerMoviesTitle = async () => { + const response = await fetch("/mediacleaner/state/getMoviesTitle"); + + if(!response.ok){ + throw new Error(`Response status: ${response.status}`); + } + + return response.json(); +} + +const populateMoviesTable = () => { + for(let i = 0; i < moviesInfo.length; i++){ + var row = moviesTable.insertRow(-1); + var cell1 = row.insertCell(0); + var cell2 = row.insertCell(1); + cell1.innerHTML = moviesInfo[i].Name; + cell2.innerHTML = ""; + } +} + +const populateSeriesTable = () => { + for(let i = 0; i < seriesInfo.length; i++){ + var row = seriesTable.insertRow(-1); + var cell1 = row.insertCell(0); + var cell2 = row.insertCell(1); + var cell3 = row.insertCell(2); + cell1.innerHTML = seriesInfo[i].Name; + cell2.innerHTML = seriesInfo[i].Seasons.map(season => season.replace("Season ", "")).join(", "); + cell3.innerHTML = ""; + } +} + +moviesTitle.innerHTML = await getMediaCleanerMoviesTitle(); +seriesTitle.innerHTML = await getMediaCleanerSeriesTitle(); + +await updateMediaCleanerState(); +var moviesInfo = await getMediaCleanerMovieInfo(); +var seriesInfo = await getMediaCleanerSeriesInfo(); +populateMoviesTable(); +populateSeriesTable(); diff --git a/Jellyfin.Plugin.MediaCleaner/PluginServiceRegistrator.cs b/Jellyfin.Plugin.MediaCleaner/PluginServiceRegistrator.cs index e5b2db9..079f689 100644 --- a/Jellyfin.Plugin.MediaCleaner/PluginServiceRegistrator.cs +++ b/Jellyfin.Plugin.MediaCleaner/PluginServiceRegistrator.cs @@ -8,6 +8,6 @@ public class PluginServiceRegistrator : IPluginServiceRegistrator { public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost) { - serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); } } diff --git a/Jellyfin.Plugin.MediaCleaner/ScheduledTasks/StaleMediaTask.cs b/Jellyfin.Plugin.MediaCleaner/StaleMediaScanner.cs similarity index 83% rename from Jellyfin.Plugin.MediaCleaner/ScheduledTasks/StaleMediaTask.cs rename to Jellyfin.Plugin.MediaCleaner/StaleMediaScanner.cs index 13a8c9d..fde16f9 100644 --- a/Jellyfin.Plugin.MediaCleaner/ScheduledTasks/StaleMediaTask.cs +++ b/Jellyfin.Plugin.MediaCleaner/StaleMediaScanner.cs @@ -14,18 +14,20 @@ 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; using Jellyfin.Plugin.MediaCleaner.Models; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Tasks; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace Jellyfin.Plugin.MediaCleaner.ScheduledTasks; +namespace Jellyfin.Plugin.MediaCleaner; /// /// A task to scan media for stale files. /// -public sealed class StaleMediaTask : IScheduledTask +public sealed class StaleMediaScanner { private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; @@ -38,7 +40,7 @@ public sealed class StaleMediaTask : IScheduledTask /// /// Logger for StaleMediaTask. /// Accesses jellyfin's library manager for media. - public StaleMediaTask(ILogger logger, ILibraryManager libraryManager) + public StaleMediaScanner(ILogger logger, ILibraryManager libraryManager) { _logger = logger; _libraryManager = libraryManager; @@ -47,18 +49,7 @@ public sealed class StaleMediaTask : IScheduledTask _seriesHelper = new SeriesHelper(_logger); } - private static PluginConfiguration Configuration => - Plugin.Instance!.Configuration; - - string IScheduledTask.Name => "Scan Stale Media"; - - string IScheduledTask.Key => "Stale Media"; - - string IScheduledTask.Description => "Scan Stale Media"; - - string IScheduledTask.Category => "Media"; - - Task IScheduledTask.ExecuteAsync(IProgress progress, CancellationToken cancellationToken) + public async Task> ScanStaleMedia() { _loggingHelper.LogDebugInformation("--DEBUG MODE ACTIVE--"); _loggingHelper.LogInformation("-------------------------------------------------"); @@ -98,13 +89,15 @@ public sealed class StaleMediaTask : IScheduledTask _loggingHelper.LogInformation("-------------------------------------------------"); _loggingHelper.LogInformation("Stale seasons found: {StaleSeasons}", staleSeasons.Count); + IEnumerable staleSeriesInfo = []; + if (staleSeasons.Count > 0) { - IEnumerable staleSeriesInfo = FindSeriesInfo(staleSeasons); + staleSeriesInfo = FindSeriesInfo(staleSeasons); - foreach (var seriesInfo in staleSeriesInfo) + foreach (SeriesInfo seriesInfo in staleSeriesInfo.Cast()) { - _loggingHelper.LogInformation("Series Info: ID: {Id} | Series Name: {SeriesName} | Stale Seasons: {Seasons}", [seriesInfo.Id, seriesInfo.SeriesName, string.Join(", ", seriesInfo.Seasons)]); + _loggingHelper.LogInformation("Series Info: ID: {Id} | Series Name: {SeriesName} | Stale Seasons: {Seasons}", [seriesInfo.Id, seriesInfo.Name, string.Join(", ", seriesInfo.Seasons)]); } } else @@ -115,9 +108,17 @@ public sealed class StaleMediaTask : IScheduledTask _loggingHelper.LogInformation("-------------------------------------------------"); _loggingHelper.LogInformation("Stale Movies found: {StaleMovies}", staleMovies.Count); + IEnumerable staleMoviesInfo = []; + if (staleMovies.Count > 0) { - foreach (var movieInfo in staleMovies) + staleMoviesInfo = staleMovies.Select(movie => new MovieInfo + { + Id = movie.Id, + Name = movie.Name + }); + + foreach (MovieInfo movieInfo in staleMoviesInfo.Cast()) { _loggingHelper.LogInformation("Movie Info: ID: {Id} | Movie Name: {MovieName}", [movieInfo.Id, movieInfo.Name]); } @@ -131,7 +132,9 @@ public sealed class StaleMediaTask : IScheduledTask _loggingHelper.LogInformation("Ending stale media scan..."); _loggingHelper.LogInformation("-------------------------------------------------"); - return Task.CompletedTask; + IEnumerable mediaInfo = staleSeriesInfo.Concat(staleMoviesInfo); + + return mediaInfo; } private List GetStaleMovies(List movies) @@ -198,7 +201,7 @@ public sealed class StaleMediaTask : IScheduledTask return staleSeasons; } - private IEnumerable FindSeriesInfo(IReadOnlyCollection seasons) + private IEnumerable FindSeriesInfo(IReadOnlyCollection seasons) { Guid[] seriesIds = [.. seasons.Select(season => season.ParentId).Distinct()]; @@ -212,22 +215,11 @@ public sealed class StaleMediaTask : IScheduledTask return new SeriesInfo { Id = series.Id, - SeriesName = series.Name, + Name = series.Name, Seasons = [.. seasons.Where(season => season.ParentId == series.Id).Select(season => season.Name)] }; }); return seriesInfoList; } - - IEnumerable IScheduledTask.GetDefaultTriggers() - { - // Run this task every 24 hours - // Unnecessary, and will be removed once front end page is ready. - yield return new TaskTriggerInfo - { - Type = TaskTriggerInfoType.IntervalTrigger, - IntervalTicks = TimeSpan.FromHours(24).Ticks - }; - } } From ebe24e2630e241435bcfe24484e6dcf552fa4ddd Mon Sep 17 00:00:00 2001 From: Thomas Gander Date: Sun, 25 Jan 2026 15:07:55 -0700 Subject: [PATCH 05/11] Added barebones UI for Media Cleaner --- .../Data/MediaCleanerState.cs | 11 +- .../Pages/media_cleaner_table.js | 124 +++++++++--------- 2 files changed, 67 insertions(+), 68 deletions(-) diff --git a/Jellyfin.Plugin.MediaCleaner/Data/MediaCleanerState.cs b/Jellyfin.Plugin.MediaCleaner/Data/MediaCleanerState.cs index 7fa61db..4c168df 100644 --- a/Jellyfin.Plugin.MediaCleaner/Data/MediaCleanerState.cs +++ b/Jellyfin.Plugin.MediaCleaner/Data/MediaCleanerState.cs @@ -13,18 +13,11 @@ using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.MediaCleaner.Data; -public class MediaCleanerState +public class MediaCleanerState(ILogger logger, ILibraryManager libraryManager) { private readonly Lock _lock = new(); private IEnumerable _mediaInfo = []; - private ILogger _logger; - private readonly StaleMediaScanner _staleMediaScanner; - - public MediaCleanerState(ILogger logger, ILibraryManager libraryManager) - { - _logger = logger; - _staleMediaScanner = new(logger, libraryManager); - } + private readonly StaleMediaScanner _staleMediaScanner = new(logger, libraryManager); public async Task UpdateState() { diff --git a/Jellyfin.Plugin.MediaCleaner/Pages/media_cleaner_table.js b/Jellyfin.Plugin.MediaCleaner/Pages/media_cleaner_table.js index 25caa16..1b3d261 100644 --- a/Jellyfin.Plugin.MediaCleaner/Pages/media_cleaner_table.js +++ b/Jellyfin.Plugin.MediaCleaner/Pages/media_cleaner_table.js @@ -1,85 +1,91 @@ -var moviesTitle = document.getElementById("moviesTitle"); -var seriesTitle = document.getElementById("seriesTitle"); -var moviesTable = document.getElementById("moviesTable"); -var seriesTable = document.getElementById("seriesTable"); +document.addEventListener('pageshow', async () => { + var moviesTitle = document.getElementById("moviesTitle"); + var seriesTitle = document.getElementById("seriesTitle"); + var moviesTable = document.getElementById("moviesTable"); + var seriesTable = document.getElementById("seriesTable"); -const getMediaCleanerSeriesInfo = async () => { - const response = await fetch("/mediacleaner/state/getSeriesInfo"); + const getMediaCleanerSeriesInfo = async () => { + const response = await fetch("/mediacleaner/state/getSeriesInfo"); - if(!response.ok){ - throw new Error(`Response status: ${response.status}`) + if(!response.ok){ + throw new Error(`Response status: ${response.status}`) + } + + return response.json(); } - return response.json(); -} + const getMediaCleanerMovieInfo = async () => { + const response = await fetch("/mediacleaner/state/getMovieInfo"); -const getMediaCleanerMovieInfo = async () => { - const response = await fetch("/mediacleaner/state/getMovieInfo"); + if(!response.ok){ + throw new Error(`Response status: ${response.status}`) + } - if(!response.ok){ - throw new Error(`Response status: ${response.status}`) + return response.json(); } - return response.json(); -} + const updateMediaCleanerState = async () => { + const response = await fetch("/mediacleaner/state/updateState"); -const updateMediaCleanerState = async () => { - const response = await fetch("/mediacleaner/state/updateState"); + if(!response.ok){ + throw new Error(`Response status: ${response.status}`) + } - if(!response.ok){ - throw new Error(`Response status: ${response.status}`) + return response.json(); } - return response.json(); -} + const getMediaCleanerSeriesTitle = async () => { + const response = await fetch("/mediacleaner/state/getSeriesTitle"); -const getMediaCleanerSeriesTitle = async () => { - const response = await fetch("/mediacleaner/state/getSeriesTitle"); + if(!response.ok){ + throw new Error(`Response status: ${response.status}`); + } - if(!response.ok){ - throw new Error(`Response status: ${response.status}`); + return response.json(); } - return response.json(); -} + const getMediaCleanerMoviesTitle = async () => { + const response = await fetch("/mediacleaner/state/getMoviesTitle"); -const getMediaCleanerMoviesTitle = async () => { - const response = await fetch("/mediacleaner/state/getMoviesTitle"); + if(!response.ok){ + throw new Error(`Response status: ${response.status}`); + } - if(!response.ok){ - throw new Error(`Response status: ${response.status}`); + return response.json(); } - return response.json(); -} + const populateMoviesTable = () => { + moviesTable.innerHTML = ''; -const populateMoviesTable = () => { - for(let i = 0; i < moviesInfo.length; i++){ - var row = moviesTable.insertRow(-1); - var cell1 = row.insertCell(0); - var cell2 = row.insertCell(1); - cell1.innerHTML = moviesInfo[i].Name; - cell2.innerHTML = ""; + for(let i = 0; i < moviesInfo.length; i++){ + var row = moviesTable.insertRow(-1); + var cell1 = row.insertCell(0); + var cell2 = row.insertCell(1); + cell1.innerHTML = moviesInfo[i].Name; + cell2.innerHTML = ""; + } } -} -const populateSeriesTable = () => { - for(let i = 0; i < seriesInfo.length; i++){ - var row = seriesTable.insertRow(-1); - var cell1 = row.insertCell(0); - var cell2 = row.insertCell(1); - var cell3 = row.insertCell(2); - cell1.innerHTML = seriesInfo[i].Name; - cell2.innerHTML = seriesInfo[i].Seasons.map(season => season.replace("Season ", "")).join(", "); - cell3.innerHTML = ""; + const populateSeriesTable = () => { + seriesTable.innerHTML = ''; + + for(let i = 0; i < seriesInfo.length; i++){ + var row = seriesTable.insertRow(-1); + var cell1 = row.insertCell(0); + var cell2 = row.insertCell(1); + var cell3 = row.insertCell(2); + cell1.innerHTML = seriesInfo[i].Name; + cell2.innerHTML = seriesInfo[i].Seasons.map(season => season.replace("Season ", "")).join(", "); + cell3.innerHTML = ""; + } } -} -moviesTitle.innerHTML = await getMediaCleanerMoviesTitle(); -seriesTitle.innerHTML = await getMediaCleanerSeriesTitle(); + moviesTitle.innerHTML = await getMediaCleanerMoviesTitle(); + seriesTitle.innerHTML = await getMediaCleanerSeriesTitle(); -await updateMediaCleanerState(); -var moviesInfo = await getMediaCleanerMovieInfo(); -var seriesInfo = await getMediaCleanerSeriesInfo(); -populateMoviesTable(); -populateSeriesTable(); + await updateMediaCleanerState(); + var moviesInfo = await getMediaCleanerMovieInfo(); + var seriesInfo = await getMediaCleanerSeriesInfo(); + populateMoviesTable(); + populateSeriesTable(); +}); From 56403b5722f5885228a064588d2e9e042915842e Mon Sep 17 00:00:00 2001 From: Thomas Gander Date: Sun, 25 Jan 2026 15:34:59 -0700 Subject: [PATCH 06/11] Updated plugin to refresh page and state whenever the page is shown --- Jellyfin.Plugin.MediaCleaner/Pages/home.html | 2 ++ .../Pages/media_cleaner_table.js | 19 +++++++++---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Jellyfin.Plugin.MediaCleaner/Pages/home.html b/Jellyfin.Plugin.MediaCleaner/Pages/home.html index 3e8b22e..3b7d39c 100644 --- a/Jellyfin.Plugin.MediaCleaner/Pages/home.html +++ b/Jellyfin.Plugin.MediaCleaner/Pages/home.html @@ -14,6 +14,7 @@ Name +

@@ -26,6 +27,7 @@ Seasons + diff --git a/Jellyfin.Plugin.MediaCleaner/Pages/media_cleaner_table.js b/Jellyfin.Plugin.MediaCleaner/Pages/media_cleaner_table.js index 1b3d261..0e689c3 100644 --- a/Jellyfin.Plugin.MediaCleaner/Pages/media_cleaner_table.js +++ b/Jellyfin.Plugin.MediaCleaner/Pages/media_cleaner_table.js @@ -54,23 +54,23 @@ document.addEventListener('pageshow', async () => { return response.json(); } - const populateMoviesTable = () => { - moviesTable.innerHTML = ''; + const populateTables = () => { + var seriesTableBody = seriesTable.getElementsByTagName('tbody')[0]; + seriesTableBody.replaceChildren(); + + var moviesTableBody = moviesTable.getElementsByTagName('tbody')[0]; + moviesTableBody.replaceChildren(); for(let i = 0; i < moviesInfo.length; i++){ - var row = moviesTable.insertRow(-1); + var row = moviesTableBody.insertRow(-1); var cell1 = row.insertCell(0); var cell2 = row.insertCell(1); cell1.innerHTML = moviesInfo[i].Name; cell2.innerHTML = ""; } - } - - const populateSeriesTable = () => { - seriesTable.innerHTML = ''; for(let i = 0; i < seriesInfo.length; i++){ - var row = seriesTable.insertRow(-1); + var row = seriesTableBody.insertRow(-1); var cell1 = row.insertCell(0); var cell2 = row.insertCell(1); var cell3 = row.insertCell(2); @@ -86,6 +86,5 @@ document.addEventListener('pageshow', async () => { await updateMediaCleanerState(); var moviesInfo = await getMediaCleanerMovieInfo(); var seriesInfo = await getMediaCleanerSeriesInfo(); - populateMoviesTable(); - populateSeriesTable(); + populateTables(); }); From e99ce9656384484fdd13d6a5f4b0b6ca07133439 Mon Sep 17 00:00:00 2001 From: Thomas Gander Date: Sun, 25 Jan 2026 22:55:00 -0700 Subject: [PATCH 07/11] Added styling for tables and also converted settings anchor to a button to enable styling --- Jellyfin.Plugin.MediaCleaner/Pages/home.html | 56 +++++++------- .../Pages/media_cleaner_table.js | 75 +++++++++++++++++-- 2 files changed, 99 insertions(+), 32 deletions(-) diff --git a/Jellyfin.Plugin.MediaCleaner/Pages/home.html b/Jellyfin.Plugin.MediaCleaner/Pages/home.html index 3b7d39c..1bd9e08 100644 --- a/Jellyfin.Plugin.MediaCleaner/Pages/home.html +++ b/Jellyfin.Plugin.MediaCleaner/Pages/home.html @@ -1,34 +1,36 @@ -
-
- Settings +
Loading...
+ -

Media Cleaner

- -

- - - - - - - -
Name
-
-
- -

- - - - - - - - -
NameSeasons
diff --git a/Jellyfin.Plugin.MediaCleaner/Pages/media_cleaner_table.js b/Jellyfin.Plugin.MediaCleaner/Pages/media_cleaner_table.js index 0e689c3..5e1e94c 100644 --- a/Jellyfin.Plugin.MediaCleaner/Pages/media_cleaner_table.js +++ b/Jellyfin.Plugin.MediaCleaner/Pages/media_cleaner_table.js @@ -12,7 +12,7 @@ document.addEventListener('pageshow', async () => { } return response.json(); - } + }; const getMediaCleanerMovieInfo = async () => { const response = await fetch("/mediacleaner/state/getMovieInfo"); @@ -22,7 +22,7 @@ document.addEventListener('pageshow', async () => { } return response.json(); - } + }; const updateMediaCleanerState = async () => { const response = await fetch("/mediacleaner/state/updateState"); @@ -32,7 +32,7 @@ document.addEventListener('pageshow', async () => { } return response.json(); - } + }; const getMediaCleanerSeriesTitle = async () => { const response = await fetch("/mediacleaner/state/getSeriesTitle"); @@ -42,7 +42,7 @@ document.addEventListener('pageshow', async () => { } return response.json(); - } + }; const getMediaCleanerMoviesTitle = async () => { const response = await fetch("/mediacleaner/state/getMoviesTitle"); @@ -52,14 +52,17 @@ document.addEventListener('pageshow', async () => { } return response.json(); - } + }; const populateTables = () => { var seriesTableBody = seriesTable.getElementsByTagName('tbody')[0]; seriesTableBody.replaceChildren(); + seriesTableBody.style.border = "1px solid"; + seriesTableBody.style.borderCollapse = "collapse"; var moviesTableBody = moviesTable.getElementsByTagName('tbody')[0]; moviesTableBody.replaceChildren(); + moviesTableBody.style.border = "1px solid"; for(let i = 0; i < moviesInfo.length; i++){ var row = moviesTableBody.insertRow(-1); @@ -77,7 +80,66 @@ document.addEventListener('pageshow', async () => { cell1.innerHTML = seriesInfo[i].Name; cell2.innerHTML = seriesInfo[i].Seasons.map(season => season.replace("Season ", "")).join(", "); cell3.innerHTML = ""; + cell3.className = "actions-cell"; } + }; + + const styleTables = () => { + const tables = document.querySelectorAll("table"); + const cells = document.querySelectorAll("th, td"); + + tables.forEach(table => { + table.style.border = "1px solid"; + table.style.borderCollapse = "collapse"; + }) + + cells.forEach(cell => { + cell.style.border = "1px solid"; + cell.style.padding = "0.5rem 0.75rem"; + cell.style.textAlign = "left" + }); + }; + + const styleLinks = () => { + const linkbtns = document.querySelectorAll("button.links") + linkbtns.forEach(btn => { + btn.style.background = "#0f0f0f"; + btn.style.border = '1px solid'; + btn.style.padding = '1rem 2rem'; + btn.style.fontSize = "1.2rem" + btn.style.color = "white"; + btn.style.textDecoration = 'none'; + btn.style.cursor = 'pointer'; + btn.style.lineHeight = 'inherit'; + btn.style.verticalAlign = 'baseline'; + btn.style.transition = 'background 0.3s ease'; + + const hoverBg = '#2a2a2a'; + const normalBg = btn.style.background; + + btn.addEventListener("click", () => { + const target = btn.dataset.target; + if (!target) return; + window.location.hash = target; + }) + + btn.addEventListener('mouseenter', () => { + btn.style.background = hoverBg; + }); + + btn.addEventListener('mouseleave', () => { + btn.style.background = normalBg; + }); + }) + } + + const showElements = () => { + const loadingElement = document.querySelector("#loading"); + const elements = document.querySelectorAll("div #page"); + elements.forEach(element => { + element.style.visibility = "visible"; + }) + loadingElement.style.visibility = "hidden"; } moviesTitle.innerHTML = await getMediaCleanerMoviesTitle(); @@ -87,4 +149,7 @@ document.addEventListener('pageshow', async () => { var moviesInfo = await getMediaCleanerMovieInfo(); var seriesInfo = await getMediaCleanerSeriesInfo(); populateTables(); + styleTables(); + styleLinks(); + showElements(); }); From a69f9df4e1dcbe201e1a50f644ece8dd021e6bff Mon Sep 17 00:00:00 2001 From: Thomas Gander Date: Sun, 25 Jan 2026 23:02:05 -0700 Subject: [PATCH 08/11] Disabled buttons with no click handlers and added comments. Also updated Settings button styling --- .../Pages/media_cleaner_table.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Plugin.MediaCleaner/Pages/media_cleaner_table.js b/Jellyfin.Plugin.MediaCleaner/Pages/media_cleaner_table.js index 5e1e94c..6a8a6a2 100644 --- a/Jellyfin.Plugin.MediaCleaner/Pages/media_cleaner_table.js +++ b/Jellyfin.Plugin.MediaCleaner/Pages/media_cleaner_table.js @@ -69,7 +69,9 @@ document.addEventListener('pageshow', async () => { var cell1 = row.insertCell(0); var cell2 = row.insertCell(1); cell1.innerHTML = moviesInfo[i].Name; - cell2.innerHTML = ""; + // Will need to be enabled once radarr and sonarr integration is enabled. + // Maybe change this to an element to remove hard coding. + cell2.innerHTML = ""; } for(let i = 0; i < seriesInfo.length; i++){ @@ -79,7 +81,9 @@ document.addEventListener('pageshow', async () => { var cell3 = row.insertCell(2); cell1.innerHTML = seriesInfo[i].Name; cell2.innerHTML = seriesInfo[i].Seasons.map(season => season.replace("Season ", "")).join(", "); - cell3.innerHTML = ""; + // Will need to be enabled once radarr and sonarr integration is enabled. + // Maybe change this to an element to remove hard coding. + cell3.innerHTML = ""; cell3.className = "actions-cell"; } }; @@ -105,7 +109,7 @@ document.addEventListener('pageshow', async () => { linkbtns.forEach(btn => { btn.style.background = "#0f0f0f"; btn.style.border = '1px solid'; - btn.style.padding = '1rem 2rem'; + btn.style.padding = '0.8rem 1.8rem'; btn.style.fontSize = "1.2rem" btn.style.color = "white"; btn.style.textDecoration = 'none'; From 669f59db15f411df58aa575712273d6864193098 Mon Sep 17 00:00:00 2001 From: Thomas Gander Date: Mon, 26 Jan 2026 08:15:53 -0700 Subject: [PATCH 09/11] Managed to move js styling to css file and inject it during load. --- Jellyfin.Plugin.MediaCleaner/Pages/home.css | 27 ++++++++ Jellyfin.Plugin.MediaCleaner/Pages/home.html | 2 +- .../Pages/media_cleaner_table.js | 67 ++++++++----------- Jellyfin.Plugin.MediaCleaner/Plugin.cs | 5 ++ 4 files changed, 62 insertions(+), 39 deletions(-) create mode 100644 Jellyfin.Plugin.MediaCleaner/Pages/home.css diff --git a/Jellyfin.Plugin.MediaCleaner/Pages/home.css b/Jellyfin.Plugin.MediaCleaner/Pages/home.css new file mode 100644 index 0000000..9409dd8 --- /dev/null +++ b/Jellyfin.Plugin.MediaCleaner/Pages/home.css @@ -0,0 +1,27 @@ +table { + border: 1px solid; + border-collapse: collapse; +} + +td, th { + border: 1px solid; + padding: 0.5rem 0.75rem; + text-align: left; +} + +.links { + background-color: #0f0f0f; + border: 1px solid; + padding: 0.8rem 1.8rem; + font-size: 1.2rem; + color: #ffffff; + text-decoration: none; + cursor: pointer; + line-height: inherit; + vertical-align: baseline; + transition: background-color 0.3s ease; +} + +.links:hover { + background-color: #2a2a2a; +} diff --git a/Jellyfin.Plugin.MediaCleaner/Pages/home.html b/Jellyfin.Plugin.MediaCleaner/Pages/home.html index 1bd9e08..2e439f3 100644 --- a/Jellyfin.Plugin.MediaCleaner/Pages/home.html +++ b/Jellyfin.Plugin.MediaCleaner/Pages/home.html @@ -4,7 +4,7 @@
Loading...