From 87bf40dab9c7b7f1896df33f3e49d2a36e7b41ed Mon Sep 17 00:00:00 2001 From: Thomas Gander Date: Sun, 8 Mar 2026 23:39:17 -0600 Subject: [PATCH] Refactored sonarr controller to be able to return anime or tv series based on the media found in the server. Also updated models --- .../Controllers/RadarrController.cs | 7 --- .../Controllers/SonarrController.cs | 26 ++------- .../Controllers/StateController.cs | 17 +++++- .../Data/MediaCleanerState.cs | 56 ++++++++++++++++--- .../Models/ConnectionTestRequest.cs | 3 + .../Models/EpisodeDeletionDetails.cs | 9 +++ .../Models/EpisodeIdLists.cs | 5 ++ .../Models/MediaInfo.cs | 2 +- .../Models/RadarrMovie.cs | 8 +++ .../Models/SeriesInfo.cs | 2 +- .../Models/SonarrSeries.cs | 18 ++++++ Jellyfin.Plugin.MediaCleaner/Pages/home.html | 1 + Jellyfin.Plugin.MediaCleaner/Pages/home.js | 24 +++++--- .../StaleMediaScanner.cs | 35 ++---------- 14 files changed, 134 insertions(+), 79 deletions(-) create mode 100644 Jellyfin.Plugin.MediaCleaner/Models/ConnectionTestRequest.cs create mode 100644 Jellyfin.Plugin.MediaCleaner/Models/EpisodeDeletionDetails.cs create mode 100644 Jellyfin.Plugin.MediaCleaner/Models/EpisodeIdLists.cs create mode 100644 Jellyfin.Plugin.MediaCleaner/Models/RadarrMovie.cs create mode 100644 Jellyfin.Plugin.MediaCleaner/Models/SonarrSeries.cs diff --git a/Jellyfin.Plugin.MediaCleaner/Controllers/RadarrController.cs b/Jellyfin.Plugin.MediaCleaner/Controllers/RadarrController.cs index bf78246..8285d15 100644 --- a/Jellyfin.Plugin.MediaCleaner/Controllers/RadarrController.cs +++ b/Jellyfin.Plugin.MediaCleaner/Controllers/RadarrController.cs @@ -13,13 +13,6 @@ using System.Linq; namespace Jellyfin.Plugin.MediaCleaner.Controllers; -public record ConnectionTestRequest(string Address, string ApiKey); - -public record RadarrMovie( - [property: JsonPropertyName("id")] int? Id, - [property: JsonPropertyName("title")] string? Title -); - [Route("radarr")] public class RadarrController : Controller { diff --git a/Jellyfin.Plugin.MediaCleaner/Controllers/SonarrController.cs b/Jellyfin.Plugin.MediaCleaner/Controllers/SonarrController.cs index ac57b75..d7b6fa9 100644 --- a/Jellyfin.Plugin.MediaCleaner/Controllers/SonarrController.cs +++ b/Jellyfin.Plugin.MediaCleaner/Controllers/SonarrController.cs @@ -5,35 +5,15 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using System.Net.Http.Headers; using System; -using System.Web; using System.Text.Json; using System.Collections.Generic; using System.Linq; -using System.Text.Json.Serialization; using System.Globalization; using Jellyfin.Plugin.MediaCleaner.Enums; using Jellyfin.Plugin.MediaCleaner.Helpers; namespace Jellyfin.Plugin.MediaCleaner.Controllers; -public record SonarrSeries( - [property: JsonPropertyName("id")] int? Id, - [property: JsonPropertyName("title")] string? Title, - [property: JsonPropertyName("seasons")] IReadOnlyList Seasons -); - -public record EpisodeDeletionDetails( - [property: JsonPropertyName("id")] int? EpisodeId, - [property: JsonPropertyName("episodeFileId")] int? EpisodeFileId, - [property: JsonPropertyName("seasonNumber")] int? SeasonNumber -); - -public record EpisodeIdLists(IReadOnlyList EpisodeIds, IReadOnlyList EpisodeFileIds); - -public record Season( - [property: JsonPropertyName("seasonNumber")] int? SeasonNumber -); - [Route("sonarr")] public class SonarrController : Controller { @@ -69,7 +49,7 @@ public class SonarrController : Controller HttpHelper httpHelper = new(ServerType.Sonarr); var responseBody = await httpHelper.SendHttpRequestAsync( HttpMethod.Get, - $"/api/v3/episode?seriesId={sonarrSeries.Id?.ToString(CultureInfo.InvariantCulture)}" + $"/api/v3/episode?seriesId={sonarrSeries.Id.ToString(CultureInfo.InvariantCulture)}" ).ConfigureAwait(false); var episodesResponseObj = JsonSerializer.Deserialize>(responseBody.GetRawText()); @@ -122,7 +102,9 @@ public class SonarrController : Controller SonarrSeries staleSeries = new( Id: retrievedSeries.Id, Title: retrievedSeries.Title, - Seasons: [.. seriesInfo.Seasons.Select(season => new Season(SeasonNumber: int.Parse(season, CultureInfo.InvariantCulture)))] + Seasons: [.. seriesInfo.Seasons.Select(season => new Season(SeasonNumber: int.Parse(season, CultureInfo.InvariantCulture)))], + Ended: retrievedSeries.Ended, + TvdbId: retrievedSeries.TvdbId ); var episodesToPurgeResult = await GetSonarrEpisodeInfo(staleSeries).ConfigureAwait(false); diff --git a/Jellyfin.Plugin.MediaCleaner/Controllers/StateController.cs b/Jellyfin.Plugin.MediaCleaner/Controllers/StateController.cs index af8e2bc..28f9684 100644 --- a/Jellyfin.Plugin.MediaCleaner/Controllers/StateController.cs +++ b/Jellyfin.Plugin.MediaCleaner/Controllers/StateController.cs @@ -2,6 +2,8 @@ using Jellyfin.Plugin.MediaCleaner.Data; using Jellyfin.Plugin.MediaCleaner; using Jellyfin.Plugin.MediaCleaner.Models; using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using System.Collections.Generic; namespace Jellyfin.Plugin.MediaCleaner.Controllers; @@ -12,8 +14,19 @@ public class StateController(MediaCleanerState state) : Controller private static Configuration Configuration => Plugin.Instance!.Configuration; - [HttpGet("getSeriesInfo")] - public IActionResult GetSeriesInfo() => Ok(_state.GetSeriesInfo()); + [HttpGet("getTvSeriesInfo")] + public async Task GetTvSeriesInfo() + { + var tvSeriesInfo = await _state.GetTvSeriesInfo().ConfigureAwait(false); + return Ok(tvSeriesInfo); + } + + [HttpGet("getAnimeSeriesInfo")] + public async Task GetAnimeSeriesInfo() + { + var animeSeriesInfo = await _state.GetAnimeSeriesInfo().ConfigureAwait(false); + return Ok(animeSeriesInfo); + } [HttpGet("getMovieInfo")] public IActionResult GetMovieInfo() => Ok(_state.GetMovieInfo()); diff --git a/Jellyfin.Plugin.MediaCleaner/Data/MediaCleanerState.cs b/Jellyfin.Plugin.MediaCleaner/Data/MediaCleanerState.cs index 4c168df..3d12c1e 100644 --- a/Jellyfin.Plugin.MediaCleaner/Data/MediaCleanerState.cs +++ b/Jellyfin.Plugin.MediaCleaner/Data/MediaCleanerState.cs @@ -1,15 +1,16 @@ -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.Helpers; using Jellyfin.Plugin.MediaCleaner.Models; -using Jellyfin.Plugin.MediaCleaner.ScheduledTasks; using MediaBrowser.Controller.Library; using Microsoft.Extensions.Logging; +using Jellyfin.Plugin.MediaCleaner.Enums; +using System.Net.Http; +using System; +using System.Text.Json; +using System.Globalization; namespace Jellyfin.Plugin.MediaCleaner.Data; @@ -24,15 +25,54 @@ public class MediaCleanerState(ILogger logger, ILibraryManage _mediaInfo = await _staleMediaScanner.ScanStaleMedia().ConfigureAwait(false); } - public IEnumerable GetSeriesInfo() + public async Task> GetTvSeriesInfo() { + // Filter only TV + // Get all series on tv sonarr server + HttpHelper tvHttpHelper = new HttpHelper(ServerType.Sonarr); + var tvSeriesResponse = await tvHttpHelper.SendHttpRequestAsync(HttpMethod.Get,"/api/v3/series").ConfigureAwait(false); + var tvSeries = JsonSerializer.Deserialize>(tvSeriesResponse.GetRawText()); + + if(tvSeries == null) + { + return []; + } + lock (_lock) { - return _mediaInfo.OfType(); + var allSeries = _mediaInfo.OfType(); + + var tvSeriesInfo = allSeries + .Where(series => tvSeries.Any(tv => tv.TvdbId == int.Parse(series.TvdbId, CultureInfo.InvariantCulture))); + + return [.. tvSeriesInfo]; } } - public IEnumerable GetMovieInfo() + public async Task> GetAnimeSeriesInfo() + { + // Get all series on anime sonarr server + HttpHelper animeHttpHelper = new HttpHelper(ServerType.SonarrAnime); + var animeSeriesResponse = await animeHttpHelper.SendHttpRequestAsync(HttpMethod.Get,"/api/v3/series").ConfigureAwait(false); + var animeSeries = JsonSerializer.Deserialize>(animeSeriesResponse.GetRawText()); + + if(animeSeries == null) + { + return Enumerable.Empty(); + } + + lock (_lock) + { + var allSeries = _mediaInfo.OfType(); + + var animeSeriesInfo = allSeries + .Where(series => animeSeries.Any(anime => anime.TvdbId == int.Parse(series.TvdbId, CultureInfo.InvariantCulture))); + + return animeSeriesInfo; + } + } + + public IEnumerable GetMovieInfo() { lock (_lock) { diff --git a/Jellyfin.Plugin.MediaCleaner/Models/ConnectionTestRequest.cs b/Jellyfin.Plugin.MediaCleaner/Models/ConnectionTestRequest.cs new file mode 100644 index 0000000..05b261e --- /dev/null +++ b/Jellyfin.Plugin.MediaCleaner/Models/ConnectionTestRequest.cs @@ -0,0 +1,3 @@ +namespace Jellyfin.Plugin.MediaCleaner.Models; + +public record ConnectionTestRequest(string Address, string ApiKey); diff --git a/Jellyfin.Plugin.MediaCleaner/Models/EpisodeDeletionDetails.cs b/Jellyfin.Plugin.MediaCleaner/Models/EpisodeDeletionDetails.cs new file mode 100644 index 0000000..0d1feec --- /dev/null +++ b/Jellyfin.Plugin.MediaCleaner/Models/EpisodeDeletionDetails.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Jellyfin.Plugin.MediaCleaner.Models; + +public record EpisodeDeletionDetails( + [property: JsonPropertyName("id")] int? EpisodeId, + [property: JsonPropertyName("episodeFileId")] int? EpisodeFileId, + [property: JsonPropertyName("seasonNumber")] int? SeasonNumber +); diff --git a/Jellyfin.Plugin.MediaCleaner/Models/EpisodeIdLists.cs b/Jellyfin.Plugin.MediaCleaner/Models/EpisodeIdLists.cs new file mode 100644 index 0000000..17036f1 --- /dev/null +++ b/Jellyfin.Plugin.MediaCleaner/Models/EpisodeIdLists.cs @@ -0,0 +1,5 @@ +using System.Collections.Generic; + +namespace Jellyfin.Plugin.MediaCleaner.Models; + +public record EpisodeIdLists(IReadOnlyList EpisodeIds, IReadOnlyList EpisodeFileIds); diff --git a/Jellyfin.Plugin.MediaCleaner/Models/MediaInfo.cs b/Jellyfin.Plugin.MediaCleaner/Models/MediaInfo.cs index 2de2c00..ead3dec 100644 --- a/Jellyfin.Plugin.MediaCleaner/Models/MediaInfo.cs +++ b/Jellyfin.Plugin.MediaCleaner/Models/MediaInfo.cs @@ -4,6 +4,6 @@ namespace Jellyfin.Plugin.MediaCleaner.Models; public abstract class MediaInfo { - public required string? TmdbId { get; set; } + public required string TmdbId { get; set; } public required string Name { get; set; } } diff --git a/Jellyfin.Plugin.MediaCleaner/Models/RadarrMovie.cs b/Jellyfin.Plugin.MediaCleaner/Models/RadarrMovie.cs new file mode 100644 index 0000000..dac8049 --- /dev/null +++ b/Jellyfin.Plugin.MediaCleaner/Models/RadarrMovie.cs @@ -0,0 +1,8 @@ +using System.Text.Json.Serialization; + +namespace Jellyfin.Plugin.MediaCleaner.Models; + +public record RadarrMovie( + [property: JsonPropertyName("id")] int? Id, + [property: JsonPropertyName("title")] string? Title +); diff --git a/Jellyfin.Plugin.MediaCleaner/Models/SeriesInfo.cs b/Jellyfin.Plugin.MediaCleaner/Models/SeriesInfo.cs index 47c8d11..b3d9705 100644 --- a/Jellyfin.Plugin.MediaCleaner/Models/SeriesInfo.cs +++ b/Jellyfin.Plugin.MediaCleaner/Models/SeriesInfo.cs @@ -12,5 +12,5 @@ public class SeriesInfo : MediaInfo { public Guid SeriesId { get; set; } public IEnumerable Seasons { get; set; } = []; - public required string? TvdbId { get; set; } + public required string TvdbId { get; set; } } diff --git a/Jellyfin.Plugin.MediaCleaner/Models/SonarrSeries.cs b/Jellyfin.Plugin.MediaCleaner/Models/SonarrSeries.cs new file mode 100644 index 0000000..fc4a330 --- /dev/null +++ b/Jellyfin.Plugin.MediaCleaner/Models/SonarrSeries.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Jellyfin.Plugin.MediaCleaner.Models; + +public record SonarrSeries( + [property: JsonPropertyName("id")] int Id, + [property: JsonPropertyName("title")] string? Title, + [property: JsonPropertyName("seasons")] IReadOnlyList Seasons, + [property: JsonPropertyName("ended")] bool Ended, + [property: JsonPropertyName("tvdbId")] int TvdbId + // [property: JsonPropertyName("tmdbId")] int TmdbId, + // [property: JsonPropertyName("imdbId")] int ImdbId +); + +public record Season( + [property: JsonPropertyName("seasonNumber")] int? SeasonNumber +); diff --git a/Jellyfin.Plugin.MediaCleaner/Pages/home.html b/Jellyfin.Plugin.MediaCleaner/Pages/home.html index dacf46a..8ee7deb 100644 --- a/Jellyfin.Plugin.MediaCleaner/Pages/home.html +++ b/Jellyfin.Plugin.MediaCleaner/Pages/home.html @@ -34,6 +34,7 @@
+

diff --git a/Jellyfin.Plugin.MediaCleaner/Pages/home.js b/Jellyfin.Plugin.MediaCleaner/Pages/home.js index 578e99f..4c1dd34 100644 --- a/Jellyfin.Plugin.MediaCleaner/Pages/home.js +++ b/Jellyfin.Plugin.MediaCleaner/Pages/home.js @@ -20,14 +20,24 @@ const refreshFrontEnd = async () => { finishLoading(); } -const getMediaCleanerSeriesInfo = async () => { - const response = await fetch("/mediacleaner/state/getSeriesInfo"); +const getMediaCleanerTvSeriesInfo = async () => { + const response = await fetch("/mediacleaner/state/getTvSeriesInfo"); if(!response.ok){ throw new Error(`Response status: ${response.status}`) } - return response.json(); + return await response.json(); +}; + +const getMediaCleanerAnimeSeriesInfo = async () => { + const response = await fetch("/mediacleaner/state/getAnimeSeriesInfo"); + + if(!response.ok){ + throw new Error(`Response status: ${response.status}`) + } + + return await response.json(); }; const getMediaCleanerMovieInfo = async () => { @@ -37,7 +47,7 @@ const getMediaCleanerMovieInfo = async () => { throw new Error(`Response status: ${response.status}`) } - return response.json(); + return await response.json(); }; const updateMediaCleanerState = async () => { @@ -83,7 +93,7 @@ const getMediaCleanerMoviesTitle = async () => { const populateTables = async () => { var moviesInfo = await getMediaCleanerMovieInfo(); - var seriesInfo = await getMediaCleanerSeriesInfo(); + var seriesInfo = await getMediaCleanerTvSeriesInfo(); var animeSeriesInfo = await getMediaCleanerAnimeSeriesInfo(); var seriesTable = document.getElementById("seriesTable"); @@ -160,7 +170,7 @@ const populateTables = async () => { } } else{ - var columnCount = animeSeriesTableBody.tHead.rows[0].cells.length; + var columnCount = animeSeriesTable.tHead.rows[0].cells.length; var row = animeSeriesTableBody.insertRow(-1); var cell1 = row.insertCell(0); cell1.colSpan = columnCount; @@ -226,7 +236,7 @@ const addClickHandlersToDeleteButtons = () => { const deleteAnimeSeriesButtonElement = document.getElementById("animeSeriesDeleteButton"); deleteMoviesButtonElement.addEventListener("click", deleteFromRadarr); deleteSeriesButtonElement.addEventListener("click", deleteFromSonarr); - deleteAnimeSeriesButtonElement.addEventListener("click", deleteFromSonarrAnime); + deleteAnimeSeriesButtonElement.addEventListener("click", deleteFromAnimeSonarr); } const getCheckedMedia = (table) => { diff --git a/Jellyfin.Plugin.MediaCleaner/StaleMediaScanner.cs b/Jellyfin.Plugin.MediaCleaner/StaleMediaScanner.cs index 8207e26..0c45f0e 100644 --- a/Jellyfin.Plugin.MediaCleaner/StaleMediaScanner.cs +++ b/Jellyfin.Plugin.MediaCleaner/StaleMediaScanner.cs @@ -103,7 +103,7 @@ public sealed class StaleMediaScanner movie.ProviderIds.TryGetValue("Tmdb", out string? tmdbId); return new MovieInfo { - TmdbId = tmdbId, + TmdbId = tmdbId ?? string.Empty, Name = movie.Name }; }); @@ -189,34 +189,6 @@ public sealed class StaleMediaScanner List staleSeasons = [.. GetStaleSeasonsWithShortCircuitOnNonStaleSeason(seasons)]; - // [ ..seasons - // .Where(season => { - // var episodes = _libraryManager.GetItemList(new InternalItemsQuery - // { - // ParentId = season.Id, - // Recursive = false - // }); - - // _loggingHelper.LogDebugInformation("Season debug information for {SeasonNumber}:", season); - - // 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); - - // return isSeasonDataStale; - // })]; - - _loggingHelper.LogDebugInformation("-------------------------------------------------"); _loggingHelper.LogDebugInformation("End of scanning for series: {Series}", item); @@ -236,11 +208,12 @@ public sealed class StaleMediaScanner { series.ProviderIds.TryGetValue("Tvdb", out string? tvdbId); series.ProviderIds.TryGetValue("Tmdb", out string? tmdbId); + return new SeriesInfo { SeriesId = series.Id, - TmdbId = tmdbId, - TvdbId = tvdbId, + TmdbId = tmdbId ?? string.Empty, + TvdbId = tvdbId ?? string.Empty, Name = series.Name, Seasons = [.. seasons.Where(season => season.ParentId == series.Id).Select(season => season.Name.Replace("Season ", "", StringComparison.OrdinalIgnoreCase))] };