using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Jellyfin.Data.Enums; using Jellyfin.Plugin.MediaCleaner.Helpers; using Jellyfin.Plugin.MediaCleaner.Models; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.MediaCleaner; /// /// A task to scan media for stale files. /// public sealed class StaleMediaScanner { private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly LoggingHelper _loggingHelper; private readonly MovieHelper _movieHelper; private readonly SeriesHelper _seriesHelper; /// /// Initializes a new instance of the class. /// /// Logger for StaleMediaTask. /// Accesses jellyfin's library manager for media. public StaleMediaScanner(ILogger logger, ILibraryManager libraryManager) { _logger = logger; _libraryManager = libraryManager; _loggingHelper = new LoggingHelper(_logger); _movieHelper = new MovieHelper(_logger); _seriesHelper = new SeriesHelper(_logger); } public Task> ScanStaleMedia() { _loggingHelper.LogDebugInformation("--DEBUG MODE ACTIVE--"); _loggingHelper.LogInformation("-------------------------------------------------"); _loggingHelper.LogInformation("Starting stale media scan..."); _loggingHelper.LogInformation("-------------------------------------------------"); var query = new InternalItemsQuery { IncludeItemTypes = [BaseItemKind.Movie, BaseItemKind.Series], Recursive = true }; List allItems = [.. _libraryManager.GetItemsResult(query).Items]; _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.LogDebugInformation("-------------------------------------------------"); _loggingHelper.LogDebugInformation("Starting scan of series items."); List staleSeasons = [.. series.SelectMany(GetStaleSeasons)]; _loggingHelper.LogDebugInformation("-------------------------------------------------"); _loggingHelper.LogDebugInformation("End of scan for series items."); _loggingHelper.LogDebugInformation("-------------------------------------------------"); _loggingHelper.LogDebugInformation("Starting scan of movie items."); List staleMovies = [.. GetStaleMovies(movies)]; _loggingHelper.LogDebugInformation("-------------------------------------------------"); _loggingHelper.LogDebugInformation("End of scan for movie items."); _loggingHelper.LogInformation("-------------------------------------------------"); _loggingHelper.LogInformation("Stale seasons found: {StaleSeasons}", staleSeasons.Count); IEnumerable staleSeriesInfo = []; if (staleSeasons.Count > 0) { staleSeriesInfo = FindSeriesInfo(staleSeasons); foreach (SeriesInfo seriesInfo in staleSeriesInfo.Cast()) { _loggingHelper.LogInformation("Series Info: TmbdID: {Id} | Series Name: {SeriesName} | Stale Seasons: {Seasons}", [seriesInfo.TmdbId, seriesInfo.Name, string.Join(", ", seriesInfo.Seasons)]); } } else { _loggingHelper.LogInformation("No stale seasons found!"); } _loggingHelper.LogInformation("-------------------------------------------------"); _loggingHelper.LogInformation("Stale Movies found: {StaleMovies}", staleMovies.Count); IEnumerable staleMoviesInfo = []; if (staleMovies.Count > 0) { staleMoviesInfo = staleMovies.Select(movie => { movie.ProviderIds.TryGetValue("Tmdb", out string? tmdbId); return new MovieInfo { TmdbId = tmdbId, Name = movie.Name }; }); foreach (MovieInfo movieInfo in staleMoviesInfo.Cast()) { _loggingHelper.LogInformation("Movie Info: TmdbID: {Id} | Movie Name: {MovieName}", [movieInfo.TmdbId, movieInfo.Name]); } } else { _loggingHelper.LogInformation("No stale movies found!"); } _loggingHelper.LogInformation("-------------------------------------------------"); _loggingHelper.LogInformation("Ending stale media scan..."); _loggingHelper.LogInformation("-------------------------------------------------"); IEnumerable mediaInfo = staleSeriesInfo.Concat(staleMoviesInfo); return Task.FromResult(mediaInfo); } private List GetStaleMovies(List movies) { List staleMovies = []; try { staleMovies.AddRange(movies.Where(_movieHelper.IsMovieStale)); } catch (ArgumentNullException ex) { _loggingHelper.LogInformation("Arguement Null Exception in GetStaleMovies!"); _loggingHelper.LogInformation(ex.Message); } return staleMovies; } private IEnumerable GetStaleSeasonsWithShortCircuitOnNonStaleSeason(IEnumerable seasons) { foreach (BaseItem season in seasons) { 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("Argument Null Exception in GetStaleSeasons!"); _loggingHelper.LogInformation(ex.Message); } _loggingHelper.LogDebugInformation("End of season debug information for {SeasonNumber}.", season); if (!isSeasonDataStale) yield break; yield return season; } } private List GetStaleSeasons(BaseItem item) { _loggingHelper.LogDebugInformation("-------------------------------------------------"); _loggingHelper.LogDebugInformation("Debug data for series: {SeriesName}", item.Name); _loggingHelper.LogDebugInformation("-------------------------------------------------"); var seasons = _libraryManager.GetItemList(new InternalItemsQuery { ParentId = item.Id, Recursive = false }); 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); return staleSeasons; } private IEnumerable FindSeriesInfo(IReadOnlyCollection seasons) { Guid[] seriesIds = [.. seasons.Select(season => season.ParentId).Distinct()]; IReadOnlyCollection series = _libraryManager.GetItemList(new InternalItemsQuery { ItemIds = seriesIds }); IEnumerable seriesInfoList = series.Select(series => { series.ProviderIds.TryGetValue("Tvdb", out string? tvdbId); series.ProviderIds.TryGetValue("Tmdb", out string? tmdbId); return new SeriesInfo { SeriesId = series.Id, TmdbId = tmdbId, TvdbId = tvdbId, Name = series.Name, Seasons = [.. seasons.Where(season => season.ParentId == series.Id).Select(season => season.Name.Replace("Season ", "", StringComparison.OrdinalIgnoreCase))] }; }); return seriesInfoList; } }