feat: prioritise and blacklist media and overview in setting page of progress in adding info
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Plugin.JRay.Models;
|
||||
using Jellyfin.Plugin.JRay.Services;
|
||||
using Jellyfin.Plugin.JRay.Services.Interfaces;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Plugin.JRay.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Reports how much of the library has truth data (overall, by media type, and
|
||||
/// by genre), and supplies the genre/series option lists the config page's
|
||||
/// rule editor needs.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("Plugins/JRay/Coverage")]
|
||||
[Authorize(Roles = "Administrator")]
|
||||
public class CoverageController : ControllerBase
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ITruthDataService _truthDataService;
|
||||
private readonly IMediaPolicyStore _policyStore;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CoverageController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The Jellyfin library manager.</param>
|
||||
/// <param name="truthDataService">The truth data service.</param>
|
||||
/// <param name="policyStore">The media policy store.</param>
|
||||
public CoverageController(ILibraryManager libraryManager, ITruthDataService truthDataService, IMediaPolicyStore policyStore)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_truthDataService = truthDataService;
|
||||
_policyStore = policyStore;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the coverage report: totals plus breakdowns by media type and by genre.
|
||||
/// </summary>
|
||||
/// <returns>The coverage report.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<CoverageReport> GetCoverage()
|
||||
{
|
||||
var rules = _policyStore.GetRules();
|
||||
|
||||
var items = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { BaseItemKind.Movie, BaseItemKind.Episode },
|
||||
IsVirtualItem = false,
|
||||
Recursive = true,
|
||||
});
|
||||
|
||||
var report = new CoverageReport();
|
||||
var byMediaType = new Dictionary<string, CoverageCounts>(StringComparer.Ordinal);
|
||||
var byGenre = new Dictionary<string, CoverageCounts>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (string.IsNullOrEmpty(item.Path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var covered = _truthDataService.HasTruth(item.Id, item.Path);
|
||||
var action = PolicyResolver.Resolve(rules, item.Id, TasksController.GetSeriesId(item), item.Genres);
|
||||
|
||||
Accumulate(report.Total, covered, action);
|
||||
Accumulate(GetBucket(byMediaType, MediaTypeLabel(item)), covered, action);
|
||||
|
||||
foreach (var genre in item.Genres)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(genre))
|
||||
{
|
||||
Accumulate(GetBucket(byGenre, genre), covered, action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var kvp in byMediaType.OrderBy(k => k.Key, StringComparer.Ordinal))
|
||||
{
|
||||
report.ByMediaType.Add(new CoverageBreakdownRow { Label = kvp.Key, Counts = kvp.Value });
|
||||
}
|
||||
|
||||
foreach (var kvp in byGenre.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
report.ByGenre.Add(new CoverageBreakdownRow { Label = kvp.Key, Counts = kvp.Value });
|
||||
}
|
||||
|
||||
return Ok(report);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the distinct genres present on movies/episodes, for the rule editor.
|
||||
/// </summary>
|
||||
/// <returns>Genre options, sorted by name.</returns>
|
||||
[HttpGet("Genres")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<IEnumerable<PickerOption>> GetGenres()
|
||||
{
|
||||
var items = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { BaseItemKind.Movie, BaseItemKind.Episode },
|
||||
IsVirtualItem = false,
|
||||
Recursive = true,
|
||||
});
|
||||
|
||||
var genres = items
|
||||
.SelectMany(item => item.Genres)
|
||||
.Where(g => !string.IsNullOrWhiteSpace(g))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(g => g, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(g => new PickerOption { Value = g, Label = g });
|
||||
|
||||
return Ok(genres);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the series in the library, for the rule editor's series picker.
|
||||
/// </summary>
|
||||
/// <returns>Series options (id + title), sorted by title.</returns>
|
||||
[HttpGet("Series")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<IEnumerable<PickerOption>> GetSeries()
|
||||
{
|
||||
var series = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { BaseItemKind.Series },
|
||||
IsVirtualItem = false,
|
||||
Recursive = true,
|
||||
});
|
||||
|
||||
var options = series
|
||||
.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(s => new PickerOption { Value = s.Id.ToString("D"), Label = s.Name });
|
||||
|
||||
return Ok(options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches movies/episodes by name, for the rule editor's item picker.
|
||||
/// </summary>
|
||||
/// <param name="search">A name fragment to match (case-insensitive). Required.</param>
|
||||
/// <param name="limit">The maximum number of results (default 25, max 100).</param>
|
||||
/// <returns>Matching item options (id + name), sorted by name.</returns>
|
||||
[HttpGet("Items")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<IEnumerable<PickerOption>> SearchItems([FromQuery] string? search, [FromQuery] int limit = 25)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
return Ok(Array.Empty<PickerOption>());
|
||||
}
|
||||
|
||||
var items = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { BaseItemKind.Movie, BaseItemKind.Episode },
|
||||
IsVirtualItem = false,
|
||||
Recursive = true,
|
||||
SearchTerm = search,
|
||||
Limit = Math.Clamp(limit, 1, 100),
|
||||
});
|
||||
|
||||
var options = items
|
||||
.OrderBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(i => new PickerOption { Value = i.Id.ToString("D"), Label = ItemLabel(i) });
|
||||
|
||||
return Ok(options);
|
||||
}
|
||||
|
||||
private static string ItemLabel(BaseItem item)
|
||||
{
|
||||
// Give episodes a series-qualified label so identically-named episodes
|
||||
// (e.g. "Pilot") are distinguishable in the picker.
|
||||
if (item is Episode episode && !string.IsNullOrEmpty(episode.SeriesName))
|
||||
{
|
||||
return episode.SeriesName + " — " + item.Name;
|
||||
}
|
||||
|
||||
return item.Name;
|
||||
}
|
||||
|
||||
private static void Accumulate(CoverageCounts counts, bool covered, PolicyAction? action)
|
||||
{
|
||||
counts.Total++;
|
||||
|
||||
if (action == PolicyAction.Ignore)
|
||||
{
|
||||
counts.Ignored++;
|
||||
return;
|
||||
}
|
||||
|
||||
if (covered)
|
||||
{
|
||||
counts.Covered++;
|
||||
return;
|
||||
}
|
||||
|
||||
counts.Pending++;
|
||||
if (action == PolicyAction.Prioritise)
|
||||
{
|
||||
counts.Prioritised++;
|
||||
}
|
||||
}
|
||||
|
||||
private static CoverageCounts GetBucket(Dictionary<string, CoverageCounts> buckets, string key)
|
||||
{
|
||||
if (!buckets.TryGetValue(key, out var counts))
|
||||
{
|
||||
counts = new CoverageCounts();
|
||||
buckets[key] = counts;
|
||||
}
|
||||
|
||||
return counts;
|
||||
}
|
||||
|
||||
private static string MediaTypeLabel(BaseItem item)
|
||||
{
|
||||
return item.GetBaseItemKind() == BaseItemKind.Episode ? "TV" : "Film";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user