// // Copyright (c) PlaceholderCompany. All rights reserved. // namespace Jellyfin.Api.Controllers; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.Entities.Views; using MediaBrowser.Common.Api; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; /// /// Exposes the PostgreSQL reporting views as read-only API endpoints. /// All endpoints require administrator privileges. /// [Route("DatabaseViews")] [Authorize(Policy = Policies.RequiresElevation)] public class DatabaseViewsController : BaseJellyfinApiController { private readonly IDbContextFactory _dbFactory; /// /// Initializes a new instance of the class. /// /// The EF Core context factory. public DatabaseViewsController(IDbContextFactory dbFactory) { _dbFactory = dbFactory; } /// /// Gets a flat list of all media streams (audio + video columns merged). /// Maps to library."MediaStreamInfos_v". /// /// Optional item ID filter. /// Optional stream type filter (0=Audio, 1=Video, 2=Subtitle). /// Record index to start at. /// Maximum records to return (1-1000, default 100). /// List of media stream rows. [HttpGet("MediaStreamInfos")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetMediaStreamInfos( [FromQuery] Guid? itemId = null, [FromQuery] int? streamType = null, [FromQuery] int startIndex = 0, [FromQuery] int limit = 100) { limit = Math.Clamp(limit, 1, 1000); await using var db = await _dbFactory.CreateDbContextAsync().ConfigureAwait(false); var query = db.MediaStreamInfoViews.AsNoTracking(); if (itemId.HasValue) { var id = itemId.Value; query = query.Where(x => x.ItemId.HasValue && x.ItemId.Value.Equals(id)); } if (streamType.HasValue) { query = query.Where(x => x.StreamType == streamType.Value); } IEnumerable results = await query .OrderBy(x => x.ItemId) .ThenBy(x => x.StreamIndex) .Skip(startIndex) .Take(limit) .ToListAsync() .ConfigureAwait(false); return Ok(results); } /// /// Gets video items joined with primary stream technical details. /// Maps to library."VideoItems_v". /// /// Optional partial type filter (e.g. "Movie", "Episode"). /// Optional minimum video height (e.g. 2160 for 4K). /// Optional HDR format ("Dolby Vision", "HDR10+", "HDR10", "HLG", "SDR"). /// Record index to start at. /// Maximum records to return (1-1000, default 100). /// List of video item rows. [HttpGet("VideoItems")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetVideoItems( [FromQuery] string? type = null, [FromQuery] int? minHeight = null, [FromQuery] string? hdrFormat = null, [FromQuery] int startIndex = 0, [FromQuery] int limit = 100) { limit = Math.Clamp(limit, 1, 1000); await using var db = await _dbFactory.CreateDbContextAsync().ConfigureAwait(false); var query = db.VideoItemViews.AsNoTracking(); if (!string.IsNullOrEmpty(type)) { query = query.Where(x => x.Type != null && x.Type.Contains(type)); } if (minHeight.HasValue) { query = query.Where(x => x.Height >= minHeight.Value); } if (!string.IsNullOrEmpty(hdrFormat)) { query = query.Where(x => x.HDRFormat == hdrFormat); } IEnumerable results = await query .OrderBy(x => x.Name) .Skip(startIndex) .Take(limit) .ToListAsync() .ConfigureAwait(false); return Ok(results); } /// /// Gets per-user playback history with calculated progress percentages. /// Maps to library."UserPlaybackHistory_v". /// /// Optional exact username filter. /// Optional item ID filter. /// When true, returns only items started but not fully played. /// Record index to start at. /// Maximum records to return (1-1000, default 100). /// List of playback history rows. [HttpGet("PlaybackHistory")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetPlaybackHistory( [FromQuery] string? username = null, [FromQuery] Guid? itemId = null, [FromQuery] bool inProgressOnly = false, [FromQuery] int startIndex = 0, [FromQuery] int limit = 100) { limit = Math.Clamp(limit, 1, 1000); await using var db = await _dbFactory.CreateDbContextAsync().ConfigureAwait(false); var query = db.UserPlaybackHistoryViews.AsNoTracking(); if (!string.IsNullOrEmpty(username)) { query = query.Where(x => x.Username == username); } if (itemId.HasValue) { var id = itemId.Value; query = query.Where(x => x.ItemId.HasValue && x.ItemId.Value.Equals(id)); } if (inProgressOnly) { query = query.Where(x => x.Played != true && x.ProgressPct > 0); } IEnumerable results = await query .OrderByDescending(x => x.LastPlayedDate) .Skip(startIndex) .Take(limit) .ToListAsync() .ConfigureAwait(false); return Ok(results); } /// /// Gets items with their external provider IDs (IMDb, TMDB, TVDB, etc.) in named columns. /// Maps to library."ItemProviders_v". /// /// Optional partial type filter. /// Optional exact IMDb ID filter. /// Optional exact TMDB ID filter. /// Optional exact TVDB ID filter. /// Record index to start at. /// Maximum records to return (1-1000, default 100). /// List of item provider rows. [HttpGet("ItemProviders")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetItemProviders( [FromQuery] string? type = null, [FromQuery] string? imdbId = null, [FromQuery] string? tmdbId = null, [FromQuery] string? tvdbId = null, [FromQuery] int startIndex = 0, [FromQuery] int limit = 100) { limit = Math.Clamp(limit, 1, 1000); await using var db = await _dbFactory.CreateDbContextAsync().ConfigureAwait(false); var query = db.ItemProviderViews.AsNoTracking(); if (!string.IsNullOrEmpty(type)) { query = query.Where(x => x.Type != null && x.Type.Contains(type)); } if (!string.IsNullOrEmpty(imdbId)) { query = query.Where(x => x.ImdbId == imdbId); } if (!string.IsNullOrEmpty(tmdbId)) { query = query.Where(x => x.TmdbId == tmdbId); } if (!string.IsNullOrEmpty(tvdbId)) { query = query.Where(x => x.TvdbId == tvdbId); } IEnumerable results = await query .OrderBy(x => x.Name) .Skip(startIndex) .Take(limit) .ToListAsync() .ConfigureAwait(false); return Ok(results); } /// /// Gets cast and crew entries linked to the items they appear in. /// Maps to library."ItemPeople_v". /// /// Optional exact person name filter. /// Optional person type filter (e.g. "Actor", "Director"). /// Optional item ID filter. /// Record index to start at. /// Maximum records to return (1-1000, default 100). /// List of item person rows. [HttpGet("ItemPeople")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetItemPeople( [FromQuery] string? personName = null, [FromQuery] string? personType = null, [FromQuery] Guid? itemId = null, [FromQuery] int startIndex = 0, [FromQuery] int limit = 100) { limit = Math.Clamp(limit, 1, 1000); await using var db = await _dbFactory.CreateDbContextAsync().ConfigureAwait(false); var query = db.ItemPersonViews.AsNoTracking(); if (!string.IsNullOrEmpty(personName)) { query = query.Where(x => x.PersonName == personName); } if (!string.IsNullOrEmpty(personType)) { query = query.Where(x => x.PersonType == personType); } if (itemId.HasValue) { var id = itemId.Value; query = query.Where(x => x.ItemId.HasValue && x.ItemId.Value.Equals(id)); } IEnumerable results = await query .OrderBy(x => x.PersonName) .ThenBy(x => x.SortOrder) .Skip(startIndex) .Take(limit) .ToListAsync() .ConfigureAwait(false); return Ok(results); } /// /// Gets aggregate library statistics - one row per item type. /// Maps to library."LibrarySummary_v". /// /// List of library summary rows, one per item type. [HttpGet("LibrarySummary")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetLibrarySummary() { await using var db = await _dbFactory.CreateDbContextAsync().ConfigureAwait(false); IEnumerable results = await db.LibrarySummaryViews .AsNoTracking() .OrderBy(x => x.Type) .ToListAsync() .ConfigureAwait(false); return Ok(results); } }