13 Commits

17 changed files with 242 additions and 115 deletions

View File

@@ -1,5 +1,5 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<AssemblyVersion>0.0.0.12</AssemblyVersion> <AssemblyVersion>0.1.1.1</AssemblyVersion>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@@ -13,13 +13,6 @@ using System.Linq;
namespace Jellyfin.Plugin.MediaCleaner.Controllers; 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")] [Route("radarr")]
public class RadarrController : Controller public class RadarrController : Controller
{ {

View File

@@ -5,35 +5,15 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System; using System;
using System.Web;
using System.Text.Json; using System.Text.Json;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.Json.Serialization;
using System.Globalization; using System.Globalization;
using Jellyfin.Plugin.MediaCleaner.Enums; using Jellyfin.Plugin.MediaCleaner.Enums;
using Jellyfin.Plugin.MediaCleaner.Helpers; using Jellyfin.Plugin.MediaCleaner.Helpers;
namespace Jellyfin.Plugin.MediaCleaner.Controllers; namespace Jellyfin.Plugin.MediaCleaner.Controllers;
public record SonarrSeries(
[property: JsonPropertyName("id")] int? Id,
[property: JsonPropertyName("title")] string? Title,
[property: JsonPropertyName("seasons")] IReadOnlyList<Season> Seasons
);
public record EpisodeDeletionDetails(
[property: JsonPropertyName("id")] int? EpisodeId,
[property: JsonPropertyName("episodeFileId")] int? EpisodeFileId,
[property: JsonPropertyName("seasonNumber")] int? SeasonNumber
);
public record EpisodeIdLists(IReadOnlyList<int> EpisodeIds, IReadOnlyList<int> EpisodeFileIds);
public record Season(
[property: JsonPropertyName("seasonNumber")] int? SeasonNumber
);
[Route("sonarr")] [Route("sonarr")]
public class SonarrController : Controller public class SonarrController : Controller
{ {
@@ -47,8 +27,8 @@ public class SonarrController : Controller
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
} }
private async Task<ObjectResult> GetSonarrSeriesInfo(SeriesInfo seriesInfo){ private async Task<ObjectResult> GetSeriesInfo(SeriesInfo seriesInfo, ServerType serverType){
HttpHelper httpHelper = new(ServerType.Sonarr); HttpHelper httpHelper = new(serverType);
var responseBody = await httpHelper.SendHttpRequestAsync( var responseBody = await httpHelper.SendHttpRequestAsync(
HttpMethod.Get, HttpMethod.Get,
$"/api/v3/series?tvdbId={Uri.EscapeDataString(seriesInfo.TvdbId ?? string.Empty)}" $"/api/v3/series?tvdbId={Uri.EscapeDataString(seriesInfo.TvdbId ?? string.Empty)}"
@@ -65,11 +45,11 @@ public class SonarrController : Controller
return Ok(series); return Ok(series);
} }
private async Task<ObjectResult> GetSonarrEpisodeInfo(SonarrSeries sonarrSeries){ private async Task<ObjectResult> GetEpisodeInfo(SonarrSeries sonarrSeries, ServerType serverType){
HttpHelper httpHelper = new(ServerType.Sonarr); HttpHelper httpHelper = new(serverType);
var responseBody = await httpHelper.SendHttpRequestAsync( var responseBody = await httpHelper.SendHttpRequestAsync(
HttpMethod.Get, HttpMethod.Get,
$"/api/v3/episode?seriesId={sonarrSeries.Id?.ToString(CultureInfo.InvariantCulture)}" $"/api/v3/episode?seriesId={sonarrSeries.Id.ToString(CultureInfo.InvariantCulture)}"
).ConfigureAwait(false); ).ConfigureAwait(false);
var episodesResponseObj = JsonSerializer.Deserialize<List<EpisodeDeletionDetails>>(responseBody.GetRawText()); var episodesResponseObj = JsonSerializer.Deserialize<List<EpisodeDeletionDetails>>(responseBody.GetRawText());
@@ -79,29 +59,70 @@ public class SonarrController : Controller
} }
var seasonNumbers = new HashSet<int>(sonarrSeries.Seasons var seasonNumbers = new HashSet<int>(sonarrSeries.Seasons
.Where(s => s.SeasonNumber.HasValue) .Select(s => s.SeasonNumber));
.Select(s => s.SeasonNumber!.Value));
var staleEpisodesResponseObj = episodesResponseObj var staleEpisodesResponseObj = episodesResponseObj
.Where(episodeDeletionDetail => episodeDeletionDetail.SeasonNumber != null && .Where(episodeDeletionDetail => seasonNumbers.Contains(episodeDeletionDetail.SeasonNumber))
seasonNumbers.Contains(episodeDeletionDetail.SeasonNumber.Value))
.ToList(); .ToList();
var episodeIds = staleEpisodesResponseObj var episodeIds = staleEpisodesResponseObj
.Where(episodeDeletionDetail => episodeDeletionDetail.HasFile)
.Select(episodeDeletionDetail => episodeDeletionDetail.EpisodeId) .Select(episodeDeletionDetail => episodeDeletionDetail.EpisodeId)
.Where(id => id.HasValue)
.Select(id => id!.Value)
.ToList(); .ToList();
var episodeFileIds = staleEpisodesResponseObj var episodeFileIds = staleEpisodesResponseObj
.Where(episodeDeletionDetail => episodeDeletionDetail.HasFile)
.Select(episodeDeletionDetail => episodeDeletionDetail.EpisodeFileId) .Select(episodeDeletionDetail => episodeDeletionDetail.EpisodeFileId)
.Where(id => id.HasValue)
.Select(id => id!.Value)
.ToList(); .ToList();
return Ok(new EpisodeIdLists(episodeIds, episodeFileIds)); return Ok(new EpisodeIdLists(episodeIds, episodeFileIds));
} }
[HttpPost("deleteSeriesFromAnimeSonarr")]
public async Task<IActionResult> DeleteSeriesFromAnimeSonarr([FromBody] SeriesInfo seriesInfo){
if (seriesInfo == null || string.IsNullOrEmpty(seriesInfo.TvdbId))
{
return BadRequest("Invalid series information provided.");
}
try
{
var sonarrSeriesInfoResult = await GetSeriesInfo(seriesInfo, ServerType.SonarrAnime).ConfigureAwait(false);
if(sonarrSeriesInfoResult.StatusCode != StatusCodes.Status200OK || sonarrSeriesInfoResult.Value is not SonarrSeries){
return sonarrSeriesInfoResult;
}
SonarrSeries retrievedSeries = (SonarrSeries)sonarrSeriesInfoResult.Value;
SonarrSeries staleSeries = new(
Id: retrievedSeries.Id,
Title: retrievedSeries.Title,
Seasons: [.. seriesInfo.Seasons.Select(season => new Season(SeasonNumber: int.Parse(season, CultureInfo.InvariantCulture)))],
Ended: retrievedSeries.Ended,
TvdbId: retrievedSeries.TvdbId
);
var episodesToPurgeResult = await GetEpisodeInfo(staleSeries, ServerType.SonarrAnime).ConfigureAwait(false);
if (episodesToPurgeResult.StatusCode != StatusCodes.Status200OK || episodesToPurgeResult.Value is not EpisodeIdLists)
{
return sonarrSeriesInfoResult;
}
EpisodeIdLists episodesToPurge = (EpisodeIdLists)episodesToPurgeResult.Value;
await UnmonitorSeasons(staleSeries, ServerType.SonarrAnime).ConfigureAwait(false);
await UnmonitorEpisodeIds(episodesToPurge.EpisodeIds, ServerType.SonarrAnime).ConfigureAwait(false);
await DeleteEpisodeFiles(episodesToPurge.EpisodeFileIds, ServerType.SonarrAnime).ConfigureAwait(false);
return Ok();
}
catch (HttpRequestException e)
{
return StatusCode(StatusCodes.Status500InternalServerError, $"An unexpected error occurred. {e.Message}");
}
}
[HttpPost("deleteSeriesFromSonarr")] [HttpPost("deleteSeriesFromSonarr")]
public async Task<IActionResult> DeleteSeriesFromSonarr([FromBody] SeriesInfo seriesInfo){ public async Task<IActionResult> DeleteSeriesFromSonarr([FromBody] SeriesInfo seriesInfo){
@@ -112,7 +133,7 @@ public class SonarrController : Controller
try try
{ {
var sonarrSeriesInfoResult = await GetSonarrSeriesInfo(seriesInfo).ConfigureAwait(false); var sonarrSeriesInfoResult = await GetSeriesInfo(seriesInfo, ServerType.Sonarr).ConfigureAwait(false);
if(sonarrSeriesInfoResult.StatusCode != StatusCodes.Status200OK || sonarrSeriesInfoResult.Value is not SonarrSeries){ if(sonarrSeriesInfoResult.StatusCode != StatusCodes.Status200OK || sonarrSeriesInfoResult.Value is not SonarrSeries){
return sonarrSeriesInfoResult; return sonarrSeriesInfoResult;
@@ -122,10 +143,12 @@ public class SonarrController : Controller
SonarrSeries staleSeries = new( SonarrSeries staleSeries = new(
Id: retrievedSeries.Id, Id: retrievedSeries.Id,
Title: retrievedSeries.Title, 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); var episodesToPurgeResult = await GetEpisodeInfo(staleSeries, ServerType.Sonarr).ConfigureAwait(false);
if (episodesToPurgeResult.StatusCode != StatusCodes.Status200OK || episodesToPurgeResult.Value is not EpisodeIdLists) if (episodesToPurgeResult.StatusCode != StatusCodes.Status200OK || episodesToPurgeResult.Value is not EpisodeIdLists)
{ {
return sonarrSeriesInfoResult; return sonarrSeriesInfoResult;
@@ -133,9 +156,9 @@ public class SonarrController : Controller
EpisodeIdLists episodesToPurge = (EpisodeIdLists)episodesToPurgeResult.Value; EpisodeIdLists episodesToPurge = (EpisodeIdLists)episodesToPurgeResult.Value;
await UnmonitorSeasons(staleSeries).ConfigureAwait(false); await UnmonitorSeasons(staleSeries, ServerType.Sonarr).ConfigureAwait(false);
await UnmonitorEpisodeIds(episodesToPurge.EpisodeIds).ConfigureAwait(false); await UnmonitorEpisodeIds(episodesToPurge.EpisodeIds, ServerType.Sonarr).ConfigureAwait(false);
await DeleteEpisodeFiles(episodesToPurge.EpisodeFileIds).ConfigureAwait(false); await DeleteEpisodeFiles(episodesToPurge.EpisodeFileIds, ServerType.Sonarr).ConfigureAwait(false);
return Ok(); return Ok();
} }
@@ -145,13 +168,13 @@ public class SonarrController : Controller
} }
} }
private async Task<ObjectResult> UnmonitorSeasons(SonarrSeries staleSeries){ private async Task<ObjectResult> UnmonitorSeasons(SonarrSeries staleSeries, ServerType serverType){
if (staleSeries == null) if (staleSeries == null)
{ {
return BadRequest("No stale series provided."); return BadRequest("No stale series provided.");
} }
HttpHelper httpHelper = new(ServerType.Sonarr); HttpHelper httpHelper = new(serverType);
var series = await httpHelper.SendHttpRequestAsync( var series = await httpHelper.SendHttpRequestAsync(
HttpMethod.Get, HttpMethod.Get,
$"/api/v3/series/{staleSeries.Id}" $"/api/v3/series/{staleSeries.Id}"
@@ -197,14 +220,14 @@ public class SonarrController : Controller
return Ok(responseBody); return Ok(responseBody);
} }
private async Task<ObjectResult> DeleteEpisodeFiles(IReadOnlyList<int> episodeFileIds) private async Task<ObjectResult> DeleteEpisodeFiles(IReadOnlyList<int> episodeFileIds, ServerType serverType)
{ {
if (episodeFileIds == null || episodeFileIds.Count == 0) if (episodeFileIds == null || episodeFileIds.Count == 0)
{ {
return BadRequest("No episode file IDs provided."); return BadRequest("No episode file IDs provided.");
} }
HttpHelper httpHelper = new(ServerType.Sonarr); HttpHelper httpHelper = new(serverType);
var responseBody = await httpHelper.SendHttpRequestAsync( var responseBody = await httpHelper.SendHttpRequestAsync(
HttpMethod.Delete, HttpMethod.Delete,
"/api/v3/episodefile/bulk", "/api/v3/episodefile/bulk",
@@ -214,7 +237,7 @@ public class SonarrController : Controller
return Ok(responseBody); return Ok(responseBody);
} }
private async Task<ObjectResult> UnmonitorEpisodeIds(IReadOnlyList<int> episodeIds) private async Task<ObjectResult> UnmonitorEpisodeIds(IReadOnlyList<int> episodeIds, ServerType serverType)
{ {
if (episodeIds == null || episodeIds.Count == 0) if (episodeIds == null || episodeIds.Count == 0)
{ {
@@ -222,7 +245,7 @@ public class SonarrController : Controller
} }
HttpHelper httpHelper = new(ServerType.Sonarr); HttpHelper httpHelper = new(serverType);
var responseBody = await httpHelper.SendHttpRequestAsync( var responseBody = await httpHelper.SendHttpRequestAsync(
HttpMethod.Put, HttpMethod.Put,
"/api/v3/episode/monitor", "/api/v3/episode/monitor",

View File

@@ -2,6 +2,8 @@ using Jellyfin.Plugin.MediaCleaner.Data;
using Jellyfin.Plugin.MediaCleaner; using Jellyfin.Plugin.MediaCleaner;
using Jellyfin.Plugin.MediaCleaner.Models; using Jellyfin.Plugin.MediaCleaner.Models;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using System.Collections.Generic;
namespace Jellyfin.Plugin.MediaCleaner.Controllers; namespace Jellyfin.Plugin.MediaCleaner.Controllers;
@@ -12,14 +14,29 @@ public class StateController(MediaCleanerState state) : Controller
private static Configuration Configuration => private static Configuration Configuration =>
Plugin.Instance!.Configuration; Plugin.Instance!.Configuration;
[HttpGet("getSeriesInfo")] [HttpGet("getTvSeriesInfo")]
public IActionResult GetSeriesInfo() => Ok(_state.GetSeriesInfo()); public async Task<IActionResult> GetTvSeriesInfo()
{
var tvSeriesInfo = await _state.GetTvSeriesInfo().ConfigureAwait(false);
return Ok(tvSeriesInfo);
}
[HttpGet("getAnimeSeriesInfo")]
public async Task<IActionResult> GetAnimeSeriesInfo()
{
var animeSeriesInfo = await _state.GetAnimeSeriesInfo().ConfigureAwait(false);
return Ok(animeSeriesInfo);
}
[HttpGet("getMovieInfo")] [HttpGet("getMovieInfo")]
public IActionResult GetMovieInfo() => Ok(_state.GetMovieInfo()); public IActionResult GetMovieInfo() => Ok(_state.GetMovieInfo());
[HttpGet("updateState")] [HttpGet("updateState")]
public IActionResult GetUpdateState() => Ok(_state.UpdateState()); public async Task<IActionResult> GetUpdateState()
{
await _state.UpdateState().ConfigureAwait(false);
return Ok();
}
[HttpGet("getMoviesTitle")] [HttpGet("getMoviesTitle")]
public IActionResult GetMoviesTitle() => public IActionResult GetMoviesTitle() =>

View File

@@ -1,38 +1,109 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Database.Implementations.ModelConfiguration; using Jellyfin.Plugin.MediaCleaner.Helpers;
using Jellyfin.Plugin.MediaCleaner;
using Jellyfin.Plugin.MediaCleaner.Models; using Jellyfin.Plugin.MediaCleaner.Models;
using Jellyfin.Plugin.MediaCleaner.ScheduledTasks;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Jellyfin.Plugin.MediaCleaner.Enums;
using System.Net.Http;
using System;
using System.Text.Json;
using System.Globalization;
using MediaBrowser.Model.Tasks;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Plugin.MediaCleaner.Data; namespace Jellyfin.Plugin.MediaCleaner.Data;
public class MediaCleanerState(ILogger<StaleMediaScanner> logger, ILibraryManager libraryManager) public class MediaCleanerState(ILogger<StaleMediaScanner> logger, ILibraryManager libraryManager, ITaskManager taskManager)
{ {
private readonly Lock _lock = new(); private readonly Lock _lock = new();
private IEnumerable<MediaInfo> _mediaInfo = []; private IEnumerable<MediaInfo> _mediaInfo = [];
private readonly StaleMediaScanner _staleMediaScanner = new(logger, libraryManager); private readonly StaleMediaScanner _staleMediaScanner = new(logger, libraryManager);
// private readonly ILibraryManager _libraryManager = libraryManager;
private readonly ITaskManager _taskManager = taskManager;
public async Task UpdateState() public async Task UpdateState()
{ {
// First re-scan library and then scan for stale media.
IScheduledTaskWorker? refreshLibraryWorker = _taskManager.ScheduledTasks.FirstOrDefault(task => task.ScheduledTask.Key == "RefreshLibrary");
if(refreshLibraryWorker != null)
{
await _taskManager.Execute(refreshLibraryWorker, new TaskOptions()).ConfigureAwait(false);
}
_mediaInfo = await _staleMediaScanner.ScanStaleMedia().ConfigureAwait(false); _mediaInfo = await _staleMediaScanner.ScanStaleMedia().ConfigureAwait(false);
return;
} }
public IEnumerable<SeriesInfo> GetSeriesInfo() public async Task<IEnumerable<SeriesInfo>> GetTvSeriesInfo()
{ {
// Filter only TV
// Get all series on tv sonarr server
HttpHelper httpHelper = new HttpHelper(ServerType.Sonarr);
JsonElement tvSeriesResponse = new JsonElement();
try
{
tvSeriesResponse = await httpHelper.SendHttpRequestAsync(HttpMethod.Get,"/api/v3/series").ConfigureAwait(false);
}
catch
{
return [];
}
var tvSeries = JsonSerializer.Deserialize<IEnumerable<SonarrSeries>>(tvSeriesResponse.GetRawText());
if(tvSeries == null)
{
return [];
}
lock (_lock) lock (_lock)
{ {
return _mediaInfo.OfType<SeriesInfo>(); var allSeries = _mediaInfo.OfType<SeriesInfo>();
var tvSeriesInfo = allSeries
.Where(series => tvSeries.Any(tv => tv.TvdbId == int.Parse(series.TvdbId, CultureInfo.InvariantCulture)));
return [.. tvSeriesInfo];
} }
} }
public IEnumerable<MovieInfo> GetMovieInfo() public async Task<IEnumerable<SeriesInfo>> GetAnimeSeriesInfo()
{
// Get all series on anime sonarr server
HttpHelper animeHttpHelper = new HttpHelper(ServerType.SonarrAnime);
JsonElement animeSeriesResponse = new JsonElement();
try
{
animeSeriesResponse = await animeHttpHelper.SendHttpRequestAsync(HttpMethod.Get,"/api/v3/series").ConfigureAwait(false);
}
catch
{
return [];
}
var animeSeries = JsonSerializer.Deserialize<List<SonarrSeries>>(animeSeriesResponse.GetRawText());
if(animeSeries == null)
{
return Enumerable.Empty<SeriesInfo>();
}
lock (_lock)
{
var allSeries = _mediaInfo.OfType<SeriesInfo>();
var animeSeriesInfo = allSeries
.Where(series => animeSeries.Any(anime => anime.TvdbId == int.Parse(series.TvdbId, CultureInfo.InvariantCulture)));
return animeSeriesInfo;
}
}
public IEnumerable<MovieInfo> GetMovieInfo()
{ {
lock (_lock) lock (_lock)
{ {

View File

@@ -1,12 +1,9 @@
using System; using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Plugin.MediaCleaner.Enums; using Jellyfin.Plugin.MediaCleaner.Enums;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.MediaCleaner.Helpers; namespace Jellyfin.Plugin.MediaCleaner.Helpers;

View File

@@ -0,0 +1,3 @@
namespace Jellyfin.Plugin.MediaCleaner.Models;
public record ConnectionTestRequest(string Address, string ApiKey);

View File

@@ -0,0 +1,10 @@
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,
[property: JsonPropertyName("hasFile")] bool HasFile
);

View File

@@ -0,0 +1,5 @@
using System.Collections.Generic;
namespace Jellyfin.Plugin.MediaCleaner.Models;
public record EpisodeIdLists(IReadOnlyList<int> EpisodeIds, IReadOnlyList<int> EpisodeFileIds);

View File

@@ -4,6 +4,6 @@ namespace Jellyfin.Plugin.MediaCleaner.Models;
public abstract class MediaInfo public abstract class MediaInfo
{ {
public required string? TmdbId { get; set; } public required string TmdbId { get; set; }
public required string Name { get; set; } public required string Name { get; set; }
} }

View File

@@ -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
);

View File

@@ -12,5 +12,5 @@ public class SeriesInfo : MediaInfo
{ {
public Guid SeriesId { get; set; } public Guid SeriesId { get; set; }
public IEnumerable<string> Seasons { get; set; } = []; public IEnumerable<string> Seasons { get; set; } = [];
public required string? TvdbId { get; set; } public required string TvdbId { get; set; }
} }

View File

@@ -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<Season> 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
);

View File

@@ -3,7 +3,7 @@
<div data-role="content"> <div data-role="content">
<div class="content-primary"> <div class="content-primary">
<link rel="stylesheet" href="/web/configurationpage?name=global.css" /> <link rel="stylesheet" href="/web/configurationpage?name=global.css" />
<div id="loading">Loading...</div> <div id="loading">Loading... This may take some time whilst we scan your library and retrieve data from Radarr and Sonarr to accurately fill tables.</div>
<div id="homepage" style="visibility: hidden;"> <div id="homepage" style="visibility: hidden;">
<button class="links" data-target="configurationpage?name=Configuration">Configuration</button> <button class="links" data-target="configurationpage?name=Configuration">Configuration</button>
<h2>Media Cleaner</h2> <h2>Media Cleaner</h2>
@@ -34,6 +34,7 @@
</table> </table>
<button id="seriesDeleteButton" class="delete-button raised button-submit emby-button" style="visibility: hidden;">Delete</button> <button id="seriesDeleteButton" class="delete-button raised button-submit emby-button" style="visibility: hidden;">Delete</button>
<br> <br>
<h3 id="animeSeriesTitle"></h3> <h3 id="animeSeriesTitle"></h3>
<table id="animeSeriesTable"> <table id="animeSeriesTable">
<thead> <thead>

View File

@@ -20,14 +20,24 @@ const refreshFrontEnd = async () => {
finishLoading(); finishLoading();
} }
const getMediaCleanerSeriesInfo = async () => { const getMediaCleanerTvSeriesInfo = async () => {
const response = await fetch("/mediacleaner/state/getSeriesInfo"); const response = await fetch("/mediacleaner/state/getTvSeriesInfo");
if(!response.ok){ if(!response.ok){
throw new Error(`Response status: ${response.status}`) 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 () => { const getMediaCleanerMovieInfo = async () => {
@@ -37,7 +47,7 @@ const getMediaCleanerMovieInfo = async () => {
throw new Error(`Response status: ${response.status}`) throw new Error(`Response status: ${response.status}`)
} }
return response.json(); return await response.json();
}; };
const updateMediaCleanerState = async () => { const updateMediaCleanerState = async () => {
@@ -46,8 +56,6 @@ const updateMediaCleanerState = async () => {
if(!response.ok){ if(!response.ok){
throw new Error(`Response status: ${response.status}`) throw new Error(`Response status: ${response.status}`)
} }
return response.json();
}; };
const getMediaCleanerAnimeSeriesTitle = async () => { const getMediaCleanerAnimeSeriesTitle = async () => {
@@ -83,7 +91,7 @@ const getMediaCleanerMoviesTitle = async () => {
const populateTables = async () => { const populateTables = async () => {
var moviesInfo = await getMediaCleanerMovieInfo(); var moviesInfo = await getMediaCleanerMovieInfo();
var seriesInfo = await getMediaCleanerSeriesInfo(); var seriesInfo = await getMediaCleanerTvSeriesInfo();
var animeSeriesInfo = await getMediaCleanerAnimeSeriesInfo(); var animeSeriesInfo = await getMediaCleanerAnimeSeriesInfo();
var seriesTable = document.getElementById("seriesTable"); var seriesTable = document.getElementById("seriesTable");
@@ -160,7 +168,7 @@ const populateTables = async () => {
} }
} }
else{ else{
var columnCount = animeSeriesTableBody.tHead.rows[0].cells.length; var columnCount = animeSeriesTable.tHead.rows[0].cells.length;
var row = animeSeriesTableBody.insertRow(-1); var row = animeSeriesTableBody.insertRow(-1);
var cell1 = row.insertCell(0); var cell1 = row.insertCell(0);
cell1.colSpan = columnCount; cell1.colSpan = columnCount;
@@ -226,7 +234,7 @@ const addClickHandlersToDeleteButtons = () => {
const deleteAnimeSeriesButtonElement = document.getElementById("animeSeriesDeleteButton"); const deleteAnimeSeriesButtonElement = document.getElementById("animeSeriesDeleteButton");
deleteMoviesButtonElement.addEventListener("click", deleteFromRadarr); deleteMoviesButtonElement.addEventListener("click", deleteFromRadarr);
deleteSeriesButtonElement.addEventListener("click", deleteFromSonarr); deleteSeriesButtonElement.addEventListener("click", deleteFromSonarr);
deleteAnimeSeriesButtonElement.addEventListener("click", deleteFromSonarrAnime); deleteAnimeSeriesButtonElement.addEventListener("click", deleteFromAnimeSonarr);
} }
const getCheckedMedia = (table) => { const getCheckedMedia = (table) => {
@@ -265,8 +273,8 @@ const deleteSeriesFromSonarrApi = async (series) => {
} }
} }
const deleteSeriesFromSonarrAnimeApi = async (series) => { const deleteSeriesFromAnimeSonarrApi = async (series) => {
const response = await fetch("/sonarr/deleteSeriesFromSonarrAnime", { const response = await fetch("/sonarr/deleteSeriesFromAnimeSonarr", {
method: "POST", method: "POST",
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -293,7 +301,7 @@ const deleteFromSonarr = () => {
const deleteFromAnimeSonarr = () => { const deleteFromAnimeSonarr = () => {
const selectedSeries = getCheckedMedia(animeSeriesTable); const selectedSeries = getCheckedMedia(animeSeriesTable);
selectedSeries.forEach(async series => await deleteSeriesFromSonarrApi(series)); selectedSeries.forEach(async series => await deleteSeriesFromAnimeSonarrApi(series));
refreshFrontEnd(); refreshFrontEnd();
} }

View File

@@ -103,7 +103,7 @@ public sealed class StaleMediaScanner
movie.ProviderIds.TryGetValue("Tmdb", out string? tmdbId); movie.ProviderIds.TryGetValue("Tmdb", out string? tmdbId);
return new MovieInfo return new MovieInfo
{ {
TmdbId = tmdbId, TmdbId = tmdbId ?? string.Empty,
Name = movie.Name Name = movie.Name
}; };
}); });
@@ -189,34 +189,6 @@ public sealed class StaleMediaScanner
List<BaseItem> staleSeasons = [.. GetStaleSeasonsWithShortCircuitOnNonStaleSeason(seasons)]; List<BaseItem> 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("-------------------------------------------------");
_loggingHelper.LogDebugInformation("End of scanning for series: {Series}", item); _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("Tvdb", out string? tvdbId);
series.ProviderIds.TryGetValue("Tmdb", out string? tmdbId); series.ProviderIds.TryGetValue("Tmdb", out string? tmdbId);
return new SeriesInfo return new SeriesInfo
{ {
SeriesId = series.Id, SeriesId = series.Id,
TmdbId = tmdbId, TmdbId = tmdbId ?? string.Empty,
TvdbId = tvdbId, TvdbId = tvdbId ?? string.Empty,
Name = series.Name, Name = series.Name,
Seasons = [.. seasons.Where(season => season.ParentId == series.Id).Select(season => season.Name.Replace("Season ", "", StringComparison.OrdinalIgnoreCase))] Seasons = [.. seasons.Where(season => season.ParentId == series.Id).Select(season => season.Name.Replace("Season ", "", StringComparison.OrdinalIgnoreCase))]
}; };

View File

@@ -6,7 +6,7 @@ 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. ✅ - A page that shows what media is currently flagged for removal. ✅
- Checkboxes to select media for removal within Jellyfin. ✅ - Checkboxes to select media for removal within Jellyfin. ✅
- 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)
Future features if I feel like it: Future features if I feel like it: