Compare commits
13 Commits
a676a8e8ec
...
v1.1.1-bet
| Author | SHA1 | Date | |
|---|---|---|---|
| dbc7325e85 | |||
| 1de8e31468 | |||
| a10769779a | |||
| c860141f5e | |||
| ddb3433bef | |||
| 3f5b59b0bd | |||
| b6242de064 | |||
| 2786d6c73d | |||
| 16c8338ffe | |||
| 87bf40dab9 | |||
| d5b97e0bf3 | |||
| de94fbd7ec | |||
| 98dfd51d3e |
@@ -1,5 +1,5 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<AssemblyVersion>0.0.0.12</AssemblyVersion>
|
||||
<AssemblyVersion>0.1.1.1</AssemblyVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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<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")]
|
||||
public class SonarrController : Controller
|
||||
{
|
||||
@@ -47,8 +27,8 @@ public class SonarrController : Controller
|
||||
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
}
|
||||
|
||||
private async Task<ObjectResult> GetSonarrSeriesInfo(SeriesInfo seriesInfo){
|
||||
HttpHelper httpHelper = new(ServerType.Sonarr);
|
||||
private async Task<ObjectResult> GetSeriesInfo(SeriesInfo seriesInfo, ServerType serverType){
|
||||
HttpHelper httpHelper = new(serverType);
|
||||
var responseBody = await httpHelper.SendHttpRequestAsync(
|
||||
HttpMethod.Get,
|
||||
$"/api/v3/series?tvdbId={Uri.EscapeDataString(seriesInfo.TvdbId ?? string.Empty)}"
|
||||
@@ -65,11 +45,11 @@ public class SonarrController : Controller
|
||||
return Ok(series);
|
||||
}
|
||||
|
||||
private async Task<ObjectResult> GetSonarrEpisodeInfo(SonarrSeries sonarrSeries){
|
||||
HttpHelper httpHelper = new(ServerType.Sonarr);
|
||||
private async Task<ObjectResult> GetEpisodeInfo(SonarrSeries sonarrSeries, ServerType serverType){
|
||||
HttpHelper httpHelper = new(serverType);
|
||||
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<List<EpisodeDeletionDetails>>(responseBody.GetRawText());
|
||||
@@ -79,29 +59,70 @@ public class SonarrController : Controller
|
||||
}
|
||||
|
||||
var seasonNumbers = new HashSet<int>(sonarrSeries.Seasons
|
||||
.Where(s => s.SeasonNumber.HasValue)
|
||||
.Select(s => s.SeasonNumber!.Value));
|
||||
.Select(s => s.SeasonNumber));
|
||||
|
||||
var staleEpisodesResponseObj = episodesResponseObj
|
||||
.Where(episodeDeletionDetail => episodeDeletionDetail.SeasonNumber != null &&
|
||||
seasonNumbers.Contains(episodeDeletionDetail.SeasonNumber.Value))
|
||||
.Where(episodeDeletionDetail => seasonNumbers.Contains(episodeDeletionDetail.SeasonNumber))
|
||||
.ToList();
|
||||
|
||||
var episodeIds = staleEpisodesResponseObj
|
||||
.Where(episodeDeletionDetail => episodeDeletionDetail.HasFile)
|
||||
.Select(episodeDeletionDetail => episodeDeletionDetail.EpisodeId)
|
||||
.Where(id => id.HasValue)
|
||||
.Select(id => id!.Value)
|
||||
.ToList();
|
||||
|
||||
var episodeFileIds = staleEpisodesResponseObj
|
||||
.Where(episodeDeletionDetail => episodeDeletionDetail.HasFile)
|
||||
.Select(episodeDeletionDetail => episodeDeletionDetail.EpisodeFileId)
|
||||
.Where(id => id.HasValue)
|
||||
.Select(id => id!.Value)
|
||||
.ToList();
|
||||
|
||||
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")]
|
||||
public async Task<IActionResult> DeleteSeriesFromSonarr([FromBody] SeriesInfo seriesInfo){
|
||||
|
||||
@@ -112,7 +133,7 @@ public class SonarrController : Controller
|
||||
|
||||
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){
|
||||
return sonarrSeriesInfoResult;
|
||||
@@ -122,10 +143,12 @@ 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);
|
||||
var episodesToPurgeResult = await GetEpisodeInfo(staleSeries, ServerType.Sonarr).ConfigureAwait(false);
|
||||
if (episodesToPurgeResult.StatusCode != StatusCodes.Status200OK || episodesToPurgeResult.Value is not EpisodeIdLists)
|
||||
{
|
||||
return sonarrSeriesInfoResult;
|
||||
@@ -133,9 +156,9 @@ public class SonarrController : Controller
|
||||
|
||||
EpisodeIdLists episodesToPurge = (EpisodeIdLists)episodesToPurgeResult.Value;
|
||||
|
||||
await UnmonitorSeasons(staleSeries).ConfigureAwait(false);
|
||||
await UnmonitorEpisodeIds(episodesToPurge.EpisodeIds).ConfigureAwait(false);
|
||||
await DeleteEpisodeFiles(episodesToPurge.EpisodeFileIds).ConfigureAwait(false);
|
||||
await UnmonitorSeasons(staleSeries, ServerType.Sonarr).ConfigureAwait(false);
|
||||
await UnmonitorEpisodeIds(episodesToPurge.EpisodeIds, ServerType.Sonarr).ConfigureAwait(false);
|
||||
await DeleteEpisodeFiles(episodesToPurge.EpisodeFileIds, ServerType.Sonarr).ConfigureAwait(false);
|
||||
|
||||
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)
|
||||
{
|
||||
return BadRequest("No stale series provided.");
|
||||
}
|
||||
|
||||
HttpHelper httpHelper = new(ServerType.Sonarr);
|
||||
HttpHelper httpHelper = new(serverType);
|
||||
var series = await httpHelper.SendHttpRequestAsync(
|
||||
HttpMethod.Get,
|
||||
$"/api/v3/series/{staleSeries.Id}"
|
||||
@@ -197,14 +220,14 @@ public class SonarrController : Controller
|
||||
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)
|
||||
{
|
||||
return BadRequest("No episode file IDs provided.");
|
||||
}
|
||||
|
||||
HttpHelper httpHelper = new(ServerType.Sonarr);
|
||||
HttpHelper httpHelper = new(serverType);
|
||||
var responseBody = await httpHelper.SendHttpRequestAsync(
|
||||
HttpMethod.Delete,
|
||||
"/api/v3/episodefile/bulk",
|
||||
@@ -214,7 +237,7 @@ public class SonarrController : Controller
|
||||
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)
|
||||
{
|
||||
@@ -222,7 +245,7 @@ public class SonarrController : Controller
|
||||
}
|
||||
|
||||
|
||||
HttpHelper httpHelper = new(ServerType.Sonarr);
|
||||
HttpHelper httpHelper = new(serverType);
|
||||
var responseBody = await httpHelper.SendHttpRequestAsync(
|
||||
HttpMethod.Put,
|
||||
"/api/v3/episode/monitor",
|
||||
|
||||
@@ -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,14 +14,29 @@ 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<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")]
|
||||
public IActionResult GetMovieInfo() => Ok(_state.GetMovieInfo());
|
||||
|
||||
[HttpGet("updateState")]
|
||||
public IActionResult GetUpdateState() => Ok(_state.UpdateState());
|
||||
public async Task<IActionResult> GetUpdateState()
|
||||
{
|
||||
await _state.UpdateState().ConfigureAwait(false);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet("getMoviesTitle")]
|
||||
public IActionResult GetMoviesTitle() =>
|
||||
|
||||
@@ -1,38 +1,109 @@
|
||||
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;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
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 IEnumerable<MediaInfo> _mediaInfo = [];
|
||||
private readonly StaleMediaScanner _staleMediaScanner = new(logger, libraryManager);
|
||||
// private readonly ILibraryManager _libraryManager = libraryManager;
|
||||
private readonly ITaskManager _taskManager = taskManager;
|
||||
|
||||
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);
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Plugin.MediaCleaner.Enums;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.MediaCleaner.Helpers;
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace Jellyfin.Plugin.MediaCleaner.Models;
|
||||
|
||||
public record ConnectionTestRequest(string Address, string ApiKey);
|
||||
@@ -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
|
||||
);
|
||||
5
Jellyfin.Plugin.MediaCleaner/Models/EpisodeIdLists.cs
Normal file
5
Jellyfin.Plugin.MediaCleaner/Models/EpisodeIdLists.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Jellyfin.Plugin.MediaCleaner.Models;
|
||||
|
||||
public record EpisodeIdLists(IReadOnlyList<int> EpisodeIds, IReadOnlyList<int> EpisodeFileIds);
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
8
Jellyfin.Plugin.MediaCleaner/Models/RadarrMovie.cs
Normal file
8
Jellyfin.Plugin.MediaCleaner/Models/RadarrMovie.cs
Normal 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
|
||||
);
|
||||
@@ -12,5 +12,5 @@ public class SeriesInfo : MediaInfo
|
||||
{
|
||||
public Guid SeriesId { get; set; }
|
||||
public IEnumerable<string> Seasons { get; set; } = [];
|
||||
public required string? TvdbId { get; set; }
|
||||
public required string TvdbId { get; set; }
|
||||
}
|
||||
|
||||
18
Jellyfin.Plugin.MediaCleaner/Models/SonarrSeries.cs
Normal file
18
Jellyfin.Plugin.MediaCleaner/Models/SonarrSeries.cs
Normal 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
|
||||
);
|
||||
@@ -3,7 +3,7 @@
|
||||
<div data-role="content">
|
||||
<div class="content-primary">
|
||||
<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;">
|
||||
<button class="links" data-target="configurationpage?name=Configuration">Configuration</button>
|
||||
<h2>Media Cleaner</h2>
|
||||
@@ -34,6 +34,7 @@
|
||||
</table>
|
||||
<button id="seriesDeleteButton" class="delete-button raised button-submit emby-button" style="visibility: hidden;">Delete</button>
|
||||
<br>
|
||||
|
||||
<h3 id="animeSeriesTitle"></h3>
|
||||
<table id="animeSeriesTable">
|
||||
<thead>
|
||||
|
||||
@@ -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 () => {
|
||||
@@ -46,8 +56,6 @@ const updateMediaCleanerState = async () => {
|
||||
if(!response.ok){
|
||||
throw new Error(`Response status: ${response.status}`)
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const getMediaCleanerAnimeSeriesTitle = async () => {
|
||||
@@ -83,7 +91,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 +168,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 +234,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) => {
|
||||
@@ -265,8 +273,8 @@ const deleteSeriesFromSonarrApi = async (series) => {
|
||||
}
|
||||
}
|
||||
|
||||
const deleteSeriesFromSonarrAnimeApi = async (series) => {
|
||||
const response = await fetch("/sonarr/deleteSeriesFromSonarrAnime", {
|
||||
const deleteSeriesFromAnimeSonarrApi = async (series) => {
|
||||
const response = await fetch("/sonarr/deleteSeriesFromAnimeSonarr", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -293,7 +301,7 @@ const deleteFromSonarr = () => {
|
||||
|
||||
const deleteFromAnimeSonarr = () => {
|
||||
const selectedSeries = getCheckedMedia(animeSeriesTable);
|
||||
selectedSeries.forEach(async series => await deleteSeriesFromSonarrApi(series));
|
||||
selectedSeries.forEach(async series => await deleteSeriesFromAnimeSonarrApi(series));
|
||||
refreshFrontEnd();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<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("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))]
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ Planned features:
|
||||
- Better logging to show more than just the count. ✅
|
||||
- A page that shows what media is currently flagged for removal. ✅
|
||||
- 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)
|
||||
|
||||
Future features if I feel like it:
|
||||
|
||||
Reference in New Issue
Block a user