using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using System.Text.Json;
using allstarr.Models.Domain;
using allstarr.Models.Settings;
using allstarr.Models.Subsonic;
using allstarr.Services;
using allstarr.Services.Common;
using allstarr.Services.Local;
using allstarr.Services.Jellyfin;
using allstarr.Services.Subsonic;
using allstarr.Services.Lyrics;
using allstarr.Filters;
namespace allstarr.Controllers;
///
/// Jellyfin-compatible API controller. Merges local library with external providers
/// (Deezer, Qobuz, SquidWTF). Auth goes through Jellyfin.
///
[ApiController]
[Route("")]
public class JellyfinController : ControllerBase
{
private readonly JellyfinSettings _settings;
private readonly SpotifyImportSettings _spotifySettings;
private readonly IMusicMetadataService _metadataService;
private readonly ILocalLibraryService _localLibraryService;
private readonly IDownloadService _downloadService;
private readonly JellyfinResponseBuilder _responseBuilder;
private readonly JellyfinModelMapper _modelMapper;
private readonly JellyfinProxyService _proxyService;
private readonly PlaylistSyncService? _playlistSyncService;
private readonly RedisCacheService _cache;
private readonly ILogger _logger;
public JellyfinController(
IOptions settings,
IOptions spotifySettings,
IMusicMetadataService metadataService,
ILocalLibraryService localLibraryService,
IDownloadService downloadService,
JellyfinResponseBuilder responseBuilder,
JellyfinModelMapper modelMapper,
JellyfinProxyService proxyService,
RedisCacheService cache,
ILogger logger,
PlaylistSyncService? playlistSyncService = null)
{
_settings = settings.Value;
_spotifySettings = spotifySettings.Value;
_metadataService = metadataService;
_localLibraryService = localLibraryService;
_downloadService = downloadService;
_responseBuilder = responseBuilder;
_modelMapper = modelMapper;
_proxyService = proxyService;
_playlistSyncService = playlistSyncService;
_cache = cache;
_logger = logger;
if (string.IsNullOrWhiteSpace(_settings.Url))
{
throw new InvalidOperationException("JELLYFIN_URL environment variable is not set");
}
}
#region Search
///
/// Searches local Jellyfin library and external providers.
/// Dedupes artists, combines songs/albums. Works with /Items and /Users/{userId}/Items.
///
[HttpGet("Items", Order = 1)]
[HttpGet("Users/{userId}/Items", Order = 1)]
public async Task SearchItems(
[FromQuery] string? searchTerm,
[FromQuery] string? includeItemTypes,
[FromQuery] int limit = 20,
[FromQuery] int startIndex = 0,
[FromQuery] string? parentId = null,
[FromQuery] string? artistIds = null,
[FromQuery] string? sortBy = null,
[FromQuery] bool recursive = true,
string? userId = null)
{
_logger.LogInformation("=== SEARCHITEMS V2 CALLED === searchTerm={SearchTerm}, includeItemTypes={ItemTypes}, parentId={ParentId}, artistIds={ArtistIds}, userId={UserId}",
searchTerm, includeItemTypes, parentId, artistIds, userId);
// If filtering by artist, handle external artists
if (!string.IsNullOrWhiteSpace(artistIds))
{
var artistId = artistIds.Split(',')[0]; // Take first artist if multiple
var (isExternal, provider, externalId) = _localLibraryService.ParseSongId(artistId);
if (isExternal)
{
_logger.LogInformation("Fetching albums for external artist: {Provider}/{ExternalId}", provider, externalId);
return await GetExternalChildItems(provider!, externalId!, includeItemTypes);
}
}
// If no search term, proxy to Jellyfin for browsing
// If Jellyfin returns empty results, we'll just return empty (not mixing browse with external)
if (string.IsNullOrWhiteSpace(searchTerm) && string.IsNullOrWhiteSpace(parentId))
{
_logger.LogDebug("No search term or parentId, proxying to Jellyfin with full query string");
// Build the full endpoint path with query string
var endpoint = userId != null ? $"Users/{userId}/Items" : "Items";
if (Request.QueryString.HasValue)
{
endpoint = $"{endpoint}{Request.QueryString.Value}";
}
var browseResult = await _proxyService.GetJsonAsync(endpoint, null, Request.Headers);
if (browseResult == null)
{
_logger.LogInformation("Jellyfin returned null - likely 401 Unauthorized, returning 401 to client");
return Unauthorized(new { error = "Authentication required" });
}
// Update Spotify playlist counts if enabled and response contains playlists
if (_spotifySettings.Enabled && browseResult.RootElement.TryGetProperty("Items", out var _))
{
_logger.LogInformation("Browse result has Items, checking for Spotify playlists to update counts");
browseResult = await UpdateSpotifyPlaylistCounts(browseResult);
}
var result = JsonSerializer.Deserialize