using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LibraryDtos; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Net; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Controllers { /// /// Library Controller. /// [Route("")] public class LibraryController : BaseJellyfinApiController { private readonly IProviderManager _providerManager; private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; private readonly IDtoService _dtoService; private readonly IAuthorizationContext _authContext; private readonly IActivityManager _activityManager; private readonly ILocalizationManager _localization; private readonly ILibraryMonitor _libraryMonitor; private readonly ILogger _logger; private readonly IServerConfigurationManager _serverConfigurationManager; /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. public LibraryController( IProviderManager providerManager, ILibraryManager libraryManager, IUserManager userManager, IDtoService dtoService, IAuthorizationContext authContext, IActivityManager activityManager, ILocalizationManager localization, ILibraryMonitor libraryMonitor, ILogger logger, IServerConfigurationManager serverConfigurationManager) { _providerManager = providerManager; _libraryManager = libraryManager; _userManager = userManager; _dtoService = dtoService; _authContext = authContext; _activityManager = activityManager; _localization = localization; _libraryMonitor = libraryMonitor; _logger = logger; _serverConfigurationManager = serverConfigurationManager; } /// /// Get the original file of an item. /// /// The item id. /// File stream returned. /// Item not found. /// A with the original file. [HttpGet("Items/{itemId}/File")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesFile("video/*", "audio/*")] public ActionResult GetFile([FromRoute, Required] Guid itemId) { var item = _libraryManager.GetItemById(itemId); if (item == null) { return NotFound(); } return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), true); } /// /// Gets critic review for an item. /// /// Critic reviews returned. /// The list of critic reviews. [HttpGet("Items/{itemId}/CriticReviews")] [Authorize(Policy = Policies.DefaultAuthorization)] [Obsolete("This endpoint is obsolete.")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetCriticReviews() { return new QueryResult(); } /// /// Get theme songs for an item. /// /// The item id. /// Optional. Filter by user id, and attach user data. /// Optional. Determines whether or not parent items should be searched for theme media. /// Theme songs returned. /// Item not found. /// The item theme songs. [HttpGet("Items/{itemId}/ThemeSongs")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetThemeSongs( [FromRoute, Required] Guid itemId, [FromQuery] Guid? userId, [FromQuery] bool inheritFromParent = false) { var user = userId is null || userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var item = itemId.Equals(default) ? (userId is null || userId.Value.Equals(default) ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); if (item == null) { return NotFound("Item not found."); } IEnumerable themeItems; while (true) { themeItems = item.GetThemeSongs(); if (themeItems.Any() || !inheritFromParent) { break; } var parent = item.GetParent(); if (parent == null) { break; } item = parent; } var dtoOptions = new DtoOptions().AddClientFields(Request); var items = themeItems .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)) .ToArray(); return new ThemeMediaResult { Items = items, TotalRecordCount = items.Length, OwnerId = item.Id }; } /// /// Get theme videos for an item. /// /// The item id. /// Optional. Filter by user id, and attach user data. /// Optional. Determines whether or not parent items should be searched for theme media. /// Theme videos returned. /// Item not found. /// The item theme videos. [HttpGet("Items/{itemId}/ThemeVideos")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetThemeVideos( [FromRoute, Required] Guid itemId, [FromQuery] Guid? userId, [FromQuery] bool inheritFromParent = false) { var user = userId is null || userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var item = itemId.Equals(default) ? (userId is null || userId.Value.Equals(default) ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); if (item == null) { return NotFound("Item not found."); } IEnumerable themeItems; while (true) { themeItems = item.GetThemeVideos(); if (themeItems.Any() || !inheritFromParent) { break; } var parent = item.GetParent(); if (parent == null) { break; } item = parent; } var dtoOptions = new DtoOptions().AddClientFields(Request); var items = themeItems .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)) .ToArray(); return new ThemeMediaResult { Items = items, TotalRecordCount = items.Length, OwnerId = item.Id }; } /// /// Get theme songs and videos for an item. /// /// The item id. /// Optional. Filter by user id, and attach user data. /// Optional. Determines whether or not parent items should be searched for theme media. /// Theme songs and videos returned. /// Item not found. /// The item theme videos. [HttpGet("Items/{itemId}/ThemeMedia")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetThemeMedia( [FromRoute, Required] Guid itemId, [FromQuery] Guid? userId, [FromQuery] bool inheritFromParent = false) { var themeSongs = GetThemeSongs( itemId, userId, inheritFromParent); var themeVideos = GetThemeVideos( itemId, userId, inheritFromParent); return new AllThemeMediaResult { ThemeSongsResult = themeSongs?.Value, ThemeVideosResult = themeVideos?.Value, SoundtrackSongsResult = new ThemeMediaResult() }; } /// /// Starts a library scan. /// /// Library scan started. /// A . [HttpPost("Library/Refresh")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task RefreshLibrary() { try { await _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "Error refreshing library"); } return NoContent(); } /// /// Deletes an item from the library and filesystem. /// /// The item id. /// Item deleted. /// Unauthorized access. /// A . [HttpDelete("Items/{itemId}")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task DeleteItem(Guid itemId) { var item = _libraryManager.GetItemById(itemId); var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); var user = auth.User; if (!item.CanDelete(user)) { return Unauthorized("Unauthorized access"); } _libraryManager.DeleteItem( item, new DeleteOptions { DeleteFileLocation = true }, true); return NoContent(); } /// /// Deletes items from the library and filesystem. /// /// The item ids. /// Items deleted. /// Unauthorized access. /// A . [HttpDelete("Items")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) { if (ids.Length == 0) { return NoContent(); } foreach (var i in ids) { var item = _libraryManager.GetItemById(i); var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); var user = auth.User; if (!item.CanDelete(user)) { if (ids.Length > 1) { return Unauthorized("Unauthorized access"); } continue; } _libraryManager.DeleteItem( item, new DeleteOptions { DeleteFileLocation = true }, true); } return NoContent(); } /// /// Get item counts. /// /// Optional. Get counts from a specific user's library. /// Optional. Get counts of favorite items. /// Item counts returned. /// Item counts. [HttpGet("Items/Counts")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetItemCounts( [FromQuery] Guid? userId, [FromQuery] bool? isFavorite) { var user = userId is null || userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var counts = new ItemCounts { AlbumCount = GetCount(BaseItemKind.MusicAlbum, user, isFavorite), EpisodeCount = GetCount(BaseItemKind.Episode, user, isFavorite), MovieCount = GetCount(BaseItemKind.Movie, user, isFavorite), SeriesCount = GetCount(BaseItemKind.Series, user, isFavorite), SongCount = GetCount(BaseItemKind.Audio, user, isFavorite), MusicVideoCount = GetCount(BaseItemKind.MusicVideo, user, isFavorite), BoxSetCount = GetCount(BaseItemKind.BoxSet, user, isFavorite), BookCount = GetCount(BaseItemKind.Book, user, isFavorite) }; return counts; } /// /// Gets all parents of an item. /// /// The item id. /// Optional. Filter by user id, and attach user data. /// Item parents returned. /// Item not found. /// Item parents. [HttpGet("Items/{itemId}/Ancestors")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetAncestors([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId) { var item = _libraryManager.GetItemById(itemId); if (item == null) { return NotFound("Item not found"); } var baseItemDtos = new List(); var user = userId is null || userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions().AddClientFields(Request); BaseItem? parent = item.GetParent(); while (parent != null) { if (user != null) { parent = TranslateParentItem(parent, user); } baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user)); parent = parent?.GetParent(); } return baseItemDtos; } /// /// Gets a list of physical paths from virtual folders. /// /// Physical paths returned. /// List of physical paths. [HttpGet("Library/PhysicalPaths")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetPhysicalPaths() { return Ok(_libraryManager.RootFolder.Children .SelectMany(c => c.PhysicalLocations)); } /// /// Gets all user media folders. /// /// Optional. Filter by folders that are marked hidden, or not. /// Media folders returned. /// List of user media folders. [HttpGet("Library/MediaFolders")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetMediaFolders([FromQuery] bool? isHidden) { var items = _libraryManager.GetUserRootFolder().Children.Concat(_libraryManager.RootFolder.VirtualChildren).OrderBy(i => i.SortName).ToList(); if (isHidden.HasValue) { var val = isHidden.Value; items = items.Where(i => i.IsHidden == val).ToList(); } var dtoOptions = new DtoOptions().AddClientFields(Request); var resultArray = _dtoService.GetBaseItemDtos(items, dtoOptions); return new QueryResult(resultArray); } /// /// Reports that new episodes of a series have been added by an external source. /// /// The tvdbId. /// Report success. /// A . [HttpPost("Library/Series/Added", Name = "PostAddedSeries")] [HttpPost("Library/Series/Updated")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult PostUpdatedSeries([FromQuery] string? tvdbId) { var series = _libraryManager.GetItemList(new InternalItemsQuery { IncludeItemTypes = new[] { BaseItemKind.Series }, DtoOptions = new DtoOptions(false) { EnableImages = false } }).Where(i => string.Equals(tvdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Tvdb), StringComparison.OrdinalIgnoreCase)).ToArray(); foreach (var item in series) { _libraryMonitor.ReportFileSystemChanged(item.Path); } return NoContent(); } /// /// Reports that new movies have been added by an external source. /// /// The tmdbId. /// The imdbId. /// Report success. /// A . [HttpPost("Library/Movies/Added", Name = "PostAddedMovies")] [HttpPost("Library/Movies/Updated")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult PostUpdatedMovies([FromQuery] string? tmdbId, [FromQuery] string? imdbId) { var movies = _libraryManager.GetItemList(new InternalItemsQuery { IncludeItemTypes = new[] { BaseItemKind.Movie }, DtoOptions = new DtoOptions(false) { EnableImages = false } }); if (!string.IsNullOrWhiteSpace(imdbId)) { movies = movies.Where(i => string.Equals(imdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb), StringComparison.OrdinalIgnoreCase)).ToList(); } else if (!string.IsNullOrWhiteSpace(tmdbId)) { movies = movies.Where(i => string.Equals(tmdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Tmdb), StringComparison.OrdinalIgnoreCase)).ToList(); } else { movies = new List(); } foreach (var item in movies) { _libraryMonitor.ReportFileSystemChanged(item.Path); } return NoContent(); } /// /// Reports that new movies have been added by an external source. /// /// The update paths. /// Report success. /// A . [HttpPost("Library/Media/Updated")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto dto) { foreach (var item in dto.Updates) { _libraryMonitor.ReportFileSystemChanged(item.Path ?? throw new ArgumentException("Item path can't be null.")); } return NoContent(); } /// /// Downloads item media. /// /// The item id. /// Media downloaded. /// Item not found. /// A containing the media stream. /// User can't download or item can't be downloaded. [HttpGet("Items/{itemId}/Download")] [Authorize(Policy = Policies.Download)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesFile("video/*", "audio/*")] public async Task GetDownload([FromRoute, Required] Guid itemId) { var item = _libraryManager.GetItemById(itemId); if (item == null) { return NotFound(); } var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); var user = auth.User; if (user != null) { if (!item.CanDownload(user)) { throw new ArgumentException("Item does not support downloading"); } } else { if (!item.CanDownload()) { throw new ArgumentException("Item does not support downloading"); } } if (user != null) { await LogDownloadAsync(item, user, auth).ConfigureAwait(false); } var path = item.Path; // Quotes are valid in linux. They'll possibly cause issues here var filename = (Path.GetFileName(path) ?? string.Empty).Replace("\"", string.Empty, StringComparison.Ordinal); if (!string.IsNullOrWhiteSpace(filename)) { // Kestrel doesn't support non-ASCII characters in headers if (Regex.IsMatch(filename, @"[^\p{IsBasicLatin}]")) { // Manually encoding non-ASCII characters, following https://tools.ietf.org/html/rfc5987#section-3.2.2 filename = WebUtility.UrlEncode(filename); } } // TODO determine non-ASCII validity. return PhysicalFile(path, MimeTypes.GetMimeType(path), filename, true); } /// /// Gets similar items. /// /// The item id. /// Exclude artist ids. /// Optional. Filter by user id, and attach user data. /// Optional. The maximum number of records to return. /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls. /// Similar items returned. /// A containing the similar items. [HttpGet("Artists/{itemId}/Similar", Name = "GetSimilarArtists")] [HttpGet("Items/{itemId}/Similar")] [HttpGet("Albums/{itemId}/Similar", Name = "GetSimilarAlbums")] [HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows")] [HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies")] [HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetSimilarItems( [FromRoute, Required] Guid itemId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds, [FromQuery] Guid? userId, [FromQuery] int? limit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) { var item = itemId.Equals(default) ? (userId is null || userId.Value.Equals(default) ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); if (item is Episode || (item is IItemByName && item is not MusicArtist)) { return new QueryResult(); } var user = userId is null || userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request); var program = item as IHasProgramAttributes; bool? isMovie = item is Movie || (program != null && program.IsMovie) || item is Trailer; bool? isSeries = item is Series || (program != null && program.IsSeries); var includeItemTypes = new List(); if (isMovie.Value) { includeItemTypes.Add(BaseItemKind.Movie); if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) { includeItemTypes.Add(BaseItemKind.Trailer); includeItemTypes.Add(BaseItemKind.LiveTvProgram); } } else if (isSeries.Value) { includeItemTypes.Add(BaseItemKind.Series); } else { // For non series and movie types these columns are typically null // isSeries = null; isMovie = null; includeItemTypes.Add(item.GetBaseItemKind()); } var query = new InternalItemsQuery(user) { Genres = item.Genres, Limit = limit, IncludeItemTypes = includeItemTypes.ToArray(), SimilarTo = item, DtoOptions = dtoOptions, EnableTotalRecordCount = !isMovie ?? true, EnableGroupByMetadataKey = isMovie ?? false, MinSimilarityScore = 2 // A remnant from album/artist scoring }; // ExcludeArtistIds if (excludeArtistIds.Length != 0) { query.ExcludeArtistIds = excludeArtistIds; } List itemsResult = _libraryManager.GetItemList(query); var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user); return new QueryResult( query.StartIndex, itemsResult.Count, returnList); } /// /// Gets the library options info. /// /// Library content type. /// Whether this is a new library. /// Library options info returned. /// Library options info. [HttpGet("Libraries/AvailableOptions")] [Authorize(Policy = Policies.FirstTimeSetupOrDefault)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetLibraryOptionsInfo( [FromQuery] string? libraryContentType, [FromQuery] bool isNewLibrary = false) { var result = new LibraryOptionsResultDto(); var types = GetRepresentativeItemTypes(libraryContentType); var typesList = types.ToList(); var plugins = _providerManager.GetAllMetadataPlugins() .Where(i => types.Contains(i.ItemType, StringComparison.OrdinalIgnoreCase)) .OrderBy(i => typesList.IndexOf(i.ItemType)) .ToList(); result.MetadataSavers = plugins .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataSaver)) .Select(i => new LibraryOptionInfoDto { Name = i.Name, DefaultEnabled = IsSaverEnabledByDefault(i.Name, types, isNewLibrary) }) .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) .Select(x => x.First()) .ToArray(); result.MetadataReaders = plugins .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.LocalMetadataProvider)) .Select(i => new LibraryOptionInfoDto { Name = i.Name, DefaultEnabled = true }) .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) .Select(x => x.First()) .ToArray(); result.SubtitleFetchers = plugins .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.SubtitleFetcher)) .Select(i => new LibraryOptionInfoDto { Name = i.Name, DefaultEnabled = true }) .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) .Select(x => x.First()) .ToArray(); var typeOptions = new List(); foreach (var type in types) { TypeOptions.DefaultImageOptions.TryGetValue(type, out var defaultImageOptions); typeOptions.Add(new LibraryTypeOptionsDto { Type = type, MetadataFetchers = plugins .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataFetcher)) .Select(i => new LibraryOptionInfoDto { Name = i.Name, DefaultEnabled = IsMetadataFetcherEnabledByDefault(i.Name, type, isNewLibrary) }) .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) .Select(x => x.First()) .ToArray(), ImageFetchers = plugins .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.ImageFetcher)) .Select(i => new LibraryOptionInfoDto { Name = i.Name, DefaultEnabled = IsImageFetcherEnabledByDefault(i.Name, type, isNewLibrary) }) .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) .Select(x => x.First()) .ToArray(), SupportedImageTypes = plugins .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) .SelectMany(i => i.SupportedImageTypes ?? Array.Empty()) .Distinct() .ToArray(), DefaultImageOptions = defaultImageOptions ?? Array.Empty() }); } result.TypeOptions = typeOptions.ToArray(); return result; } private int GetCount(BaseItemKind itemKind, User? user, bool? isFavorite) { var query = new InternalItemsQuery(user) { IncludeItemTypes = new[] { itemKind }, Limit = 0, Recursive = true, IsVirtualItem = false, IsFavorite = isFavorite, DtoOptions = new DtoOptions(false) { EnableImages = false } }; return _libraryManager.GetItemsResult(query).TotalRecordCount; } private BaseItem? TranslateParentItem(BaseItem item, User user) { return item.GetParent() is AggregateFolder ? _libraryManager.GetUserRootFolder().GetChildren(user, true) .FirstOrDefault(i => i.PhysicalLocations.Contains(item.Path)) : item; } private async Task LogDownloadAsync(BaseItem item, User user, AuthorizationInfo auth) { try { await _activityManager.CreateAsync(new ActivityLog( string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name), "UserDownloadingContent", auth.UserId) { ShortOverview = string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("AppDeviceValues"), auth.Client, auth.Device), }).ConfigureAwait(false); } catch { // Logged at lower levels } } private static string[] GetRepresentativeItemTypes(string? contentType) { return contentType switch { CollectionType.BoxSets => new[] { "BoxSet" }, CollectionType.Playlists => new[] { "Playlist" }, CollectionType.Movies => new[] { "Movie" }, CollectionType.TvShows => new[] { "Series", "Season", "Episode" }, CollectionType.Books => new[] { "Book" }, CollectionType.Music => new[] { "MusicArtist", "MusicAlbum", "Audio", "MusicVideo" }, CollectionType.HomeVideos => new[] { "Video", "Photo" }, CollectionType.Photos => new[] { "Video", "Photo" }, CollectionType.MusicVideos => new[] { "MusicVideo" }, _ => new[] { "Series", "Season", "Episode", "Movie" } }; } private bool IsSaverEnabledByDefault(string name, string[] itemTypes, bool isNewLibrary) { if (isNewLibrary) { return false; } var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions .Where(i => itemTypes.Contains(i.ItemType ?? string.Empty, StringComparison.OrdinalIgnoreCase)) .ToArray(); return metadataOptions.Length == 0 || metadataOptions.Any(i => !i.DisabledMetadataSavers.Contains(name, StringComparison.OrdinalIgnoreCase)); } private bool IsMetadataFetcherEnabledByDefault(string name, string type, bool isNewLibrary) { if (isNewLibrary) { if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase)) { return !(string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase) || string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase) || string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase)); } return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase) || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase) || string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase); } var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) .ToArray(); return metadataOptions.Length == 0 || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase)); } private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary) { if (isNewLibrary) { if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase)) { return !string.Equals(type, "Series", StringComparison.OrdinalIgnoreCase) && !string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase) && !string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase) && !string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase); } return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase) || string.Equals(name, "Screen Grabber", StringComparison.OrdinalIgnoreCase) || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase) || string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase); } var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) .ToArray(); if (metadataOptions.Length == 0) { return true; } return metadataOptions.Any(i => !i.DisabledImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase)); } } }