//
// 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);
}
}