using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; using MediaBrowser.Common.ScheduledTasks; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Server.Implementations.Library.Validators; using MediaBrowser.Server.Implementations.ScheduledTasks; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using SortOrder = MediaBrowser.Model.Entities.SortOrder; namespace MediaBrowser.Server.Implementations.Library { /// /// Class LibraryManager /// public class LibraryManager : ILibraryManager { /// /// Gets or sets the postscan tasks. /// /// The postscan tasks. private IEnumerable PostscanTasks { get; set; } /// /// Gets or sets the prescan tasks. /// /// The prescan tasks. private IEnumerable PrescanTasks { get; set; } /// /// Gets or sets the people prescan tasks. /// /// The people prescan tasks. private IEnumerable PeoplePrescanTasks { get; set; } /// /// Gets the intro providers. /// /// The intro providers. private IEnumerable IntroProviders { get; set; } /// /// Gets the list of entity resolution ignore rules /// /// The entity resolution ignore rules. private IEnumerable EntityResolutionIgnoreRules { get; set; } /// /// Gets the list of BasePluginFolders added by plugins /// /// The plugin folders. private IEnumerable PluginFolderCreators { get; set; } /// /// Gets the list of currently registered entity resolvers /// /// The entity resolvers enumerable. private IEnumerable EntityResolvers { get; set; } /// /// Gets or sets the comparers. /// /// The comparers. private IEnumerable Comparers { get; set; } /// /// Gets the active item repository /// /// The item repository. public IItemRepository ItemRepository { get; set; } /// /// Occurs when [item added]. /// public event EventHandler ItemAdded; /// /// Occurs when [item updated]. /// public event EventHandler ItemUpdated; /// /// Occurs when [item removed]. /// public event EventHandler ItemRemoved; /// /// The _logger /// private readonly ILogger _logger; /// /// The _task manager /// private readonly ITaskManager _taskManager; /// /// The _user manager /// private readonly IUserManager _userManager; /// /// The _user data repository /// private readonly IUserDataManager _userDataRepository; /// /// Gets or sets the configuration manager. /// /// The configuration manager. private IServerConfigurationManager ConfigurationManager { get; set; } /// /// A collection of items that may be referenced from multiple physical places in the library /// (typically, multiple user roots). We store them here and be sure they all reference a /// single instance. /// /// The by reference items. private ConcurrentDictionary ByReferenceItems { get; set; } private IEnumerable _savers; private readonly Func _directoryWatchersFactory; /// /// The _library items cache /// private ConcurrentDictionary _libraryItemsCache; /// /// The _library items cache sync lock /// private object _libraryItemsCacheSyncLock = new object(); /// /// The _library items cache initialized /// private bool _libraryItemsCacheInitialized; /// /// Gets the library items cache. /// /// The library items cache. private ConcurrentDictionary LibraryItemsCache { get { LazyInitializer.EnsureInitialized(ref _libraryItemsCache, ref _libraryItemsCacheInitialized, ref _libraryItemsCacheSyncLock, CreateLibraryItemsCache); return _libraryItemsCache; } } /// /// The _user root folders /// private readonly ConcurrentDictionary _userRootFolders = new ConcurrentDictionary(); /// /// Initializes a new instance of the class. /// /// The logger. /// The task manager. /// The user manager. /// The configuration manager. /// The user data repository. public LibraryManager(ILogger logger, ITaskManager taskManager, IUserManager userManager, IServerConfigurationManager configurationManager, IUserDataManager userDataRepository, Func directoryWatchersFactory) { _logger = logger; _taskManager = taskManager; _userManager = userManager; ConfigurationManager = configurationManager; _userDataRepository = userDataRepository; _directoryWatchersFactory = directoryWatchersFactory; ByReferenceItems = new ConcurrentDictionary(); ConfigurationManager.ConfigurationUpdated += ConfigurationUpdated; RecordConfigurationValues(configurationManager.Configuration); } /// /// Adds the parts. /// /// The rules. /// The plugin folders. /// The resolvers. /// The intro providers. /// The item comparers. /// The prescan tasks. /// The postscan tasks. /// The people prescan tasks. /// The savers. public void AddParts(IEnumerable rules, IEnumerable pluginFolders, IEnumerable resolvers, IEnumerable introProviders, IEnumerable itemComparers, IEnumerable prescanTasks, IEnumerable postscanTasks, IEnumerable peoplePrescanTasks, IEnumerable savers) { EntityResolutionIgnoreRules = rules; PluginFolderCreators = pluginFolders; EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray(); IntroProviders = introProviders; Comparers = itemComparers; PrescanTasks = prescanTasks; PostscanTasks = postscanTasks; PeoplePrescanTasks = peoplePrescanTasks; _savers = savers; } /// /// The _root folder /// private AggregateFolder _rootFolder; /// /// The _root folder sync lock /// private object _rootFolderSyncLock = new object(); /// /// The _root folder initialized /// private bool _rootFolderInitialized; /// /// Gets the root folder. /// /// The root folder. public AggregateFolder RootFolder { get { LazyInitializer.EnsureInitialized(ref _rootFolder, ref _rootFolderInitialized, ref _rootFolderSyncLock, CreateRootFolder); return _rootFolder; } private set { _rootFolder = value; if (value == null) { _rootFolderInitialized = false; } } } /// /// The _items by name path /// private string _itemsByNamePath; /// /// The _season zero display name /// private string _seasonZeroDisplayName; /// /// Records the configuration values. /// /// The configuration. private void RecordConfigurationValues(ServerConfiguration configuration) { _seasonZeroDisplayName = ConfigurationManager.Configuration.SeasonZeroDisplayName; _itemsByNamePath = ConfigurationManager.ApplicationPaths.ItemsByNamePath; } /// /// Configurations the updated. /// /// The sender. /// The instance containing the event data. void ConfigurationUpdated(object sender, EventArgs e) { var config = ConfigurationManager.Configuration; var ibnPathChanged = !string.Equals(_itemsByNamePath, ConfigurationManager.ApplicationPaths.ItemsByNamePath, StringComparison.CurrentCulture); if (ibnPathChanged) { _itemsByName.Clear(); } var newSeasonZeroName = ConfigurationManager.Configuration.SeasonZeroDisplayName; var seasonZeroNameChanged = !string.Equals(_seasonZeroDisplayName, newSeasonZeroName, StringComparison.CurrentCulture); RecordConfigurationValues(config); Task.Run(async () => { if (seasonZeroNameChanged) { await UpdateSeasonZeroNames(newSeasonZeroName, CancellationToken.None).ConfigureAwait(false); } // Any number of configuration settings could change the way the library is refreshed, so do that now _taskManager.CancelIfRunningAndQueue(); }); } /// /// Updates the season zero names. /// /// The new name. /// The cancellation token. /// Task. private async Task UpdateSeasonZeroNames(string newName, CancellationToken cancellationToken) { var seasons = RootFolder.RecursiveChildren .OfType() .Where(i => i.IndexNumber.HasValue && i.IndexNumber.Value == 0 && !string.Equals(i.Name, newName, StringComparison.CurrentCulture)) .ToList(); foreach (var season in seasons) { season.Name = newName; try { await UpdateItem(season, ItemUpdateType.MetadataDownload, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { _logger.ErrorException("Error saving {0}", ex, season.Path); } } } /// /// Creates the library items cache. /// /// ConcurrentDictionary{GuidBaseItem}. private ConcurrentDictionary CreateLibraryItemsCache() { var items = RootFolder.GetRecursiveChildren(); items.Add(RootFolder); // Need to use Distinct because there could be multiple instances with the same id // due to sharing the default library var userRootFolders = _userManager.Users.Select(i => i.RootFolder) .Distinct() .ToList(); foreach (var folder in userRootFolders) { items.Add(folder); } // Get all user collection folders // Skip BasePluginFolders because we already got them from RootFolder.RecursiveChildren var userFolders = userRootFolders.SelectMany(i => i.Children) .Where(i => !(i is BasePluginFolder)) .ToList(); foreach (var folder in userFolders) { items.Add(folder); } var dictionary = new ConcurrentDictionary(); foreach (var item in items) { dictionary[item.Id] = item; } return dictionary; } /// /// Updates the item in library cache. /// /// The item. private void UpdateItemInLibraryCache(BaseItem item) { if (!(item is IItemByName)) { LibraryItemsCache.AddOrUpdate(item.Id, item, delegate { return item; }); } } /// /// Resolves the item. /// /// The args. /// BaseItem. public BaseItem ResolveItem(ItemResolveArgs args) { var item = EntityResolvers.Select(r => { try { return r.ResolvePath(args); } catch (Exception ex) { _logger.ErrorException("Error in {0} resolving {1}", ex, r.GetType().Name, args.Path); return null; } }).FirstOrDefault(i => i != null); if (item != null) { ResolverHelper.SetInitialItemValues(item, args); // Now handle the issue with posibly having the same item referenced from multiple physical // places within the library. Be sure we always end up with just one instance. if (item is IByReferenceItem) { item = GetOrAddByReferenceItem(item); } } return item; } /// /// Ensure supplied item has only one instance throughout /// /// The item. /// The proper instance to the item public BaseItem GetOrAddByReferenceItem(BaseItem item) { // Add this item to our list if not there already if (!ByReferenceItems.TryAdd(item.Id, item)) { // Already there - return the existing reference item = ByReferenceItems[item.Id]; } return item; } /// /// Resolves a path into a BaseItem /// /// The file info. /// The parent. /// BaseItem. /// public BaseItem ResolvePath(FileSystemInfo fileInfo, Folder parent = null) { if (fileInfo == null) { throw new ArgumentNullException("fileInfo"); } var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, this) { Parent = parent, Path = fileInfo.FullName, FileInfo = fileInfo }; // Return null if ignore rules deem that we should do so if (EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(args))) { return null; } // Gather child folder and files if (args.IsDirectory) { var isPhysicalRoot = args.IsPhysicalRoot; // When resolving the root, we need it's grandchildren (children of user views) var flattenFolderDepth = isPhysicalRoot ? 2 : 0; args.FileSystemDictionary = FileData.GetFilteredFileSystemEntries(args.Path, _logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: isPhysicalRoot || args.IsVf); // Need to remove subpaths that may have been resolved from shortcuts // Example: if \\server\movies exists, then strip out \\server\movies\action if (isPhysicalRoot) { var paths = args.FileSystemDictionary.Keys.ToList(); foreach (var subPath in paths .Where(subPath => !subPath.EndsWith(":\\", StringComparison.OrdinalIgnoreCase) && paths.Any(i => subPath.StartsWith(i.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)))) { _logger.Info("Ignoring duplicate path: {0}", subPath); args.FileSystemDictionary.Remove(subPath); } } } // Check to see if we should resolve based on our contents if (args.IsDirectory && !ShouldResolvePathContents(args)) { return null; } return ResolveItem(args); } /// /// Determines whether a path should be ignored based on its contents - called after the contents have been read /// /// The args. /// true if XXXX, false otherwise private static bool ShouldResolvePathContents(ItemResolveArgs args) { // Ignore any folders containing a file called .ignore return !args.ContainsFileSystemEntryByName(".ignore"); } /// /// Resolves a set of files into a list of BaseItem /// /// /// The files. /// The parent. /// List{``0}. public List ResolvePaths(IEnumerable files, Folder parent) where T : BaseItem { var list = new List(); Parallel.ForEach(files, f => { try { var item = ResolvePath(f, parent) as T; if (item != null) { lock (list) { list.Add(item); } } } catch (Exception ex) { _logger.ErrorException("Error resolving path {0}", ex, f.FullName); } }); return list; } /// /// Creates the root media folder /// /// AggregateFolder. /// Cannot create the root folder until plugins have loaded public AggregateFolder CreateRootFolder() { var rootFolderPath = ConfigurationManager.ApplicationPaths.RootFolderPath; if (!Directory.Exists(rootFolderPath)) { Directory.CreateDirectory(rootFolderPath); } var rootFolder = RetrieveItem(rootFolderPath.GetMBId(typeof(AggregateFolder))) as AggregateFolder ?? (AggregateFolder)ResolvePath(new DirectoryInfo(rootFolderPath)); // Add in the plug-in folders foreach (var child in PluginFolderCreators) { var folder = child.GetFolder(); if (folder.Id == Guid.Empty) { folder.Id = (folder.Path ?? folder.GetType().Name).GetMBId(folder.GetType()); } rootFolder.AddVirtualChild(folder); } return rootFolder; } /// /// Gets the user root folder. /// /// The user root path. /// UserRootFolder. public UserRootFolder GetUserRootFolder(string userRootPath) { return _userRootFolders.GetOrAdd(userRootPath, key => RetrieveItem(userRootPath.GetMBId(typeof(UserRootFolder))) as UserRootFolder ?? (UserRootFolder)ResolvePath(new DirectoryInfo(userRootPath))); } public Person GetPersonSync(string name) { return GetItemByName(ConfigurationManager.ApplicationPaths.PeoplePath, name); } /// /// Gets a Person /// /// The name. /// Task{Person}. public Person GetPerson(string name) { return GetItemByName(ConfigurationManager.ApplicationPaths.PeoplePath, name); } /// /// Gets a Studio /// /// The name. /// Task{Studio}. public Studio GetStudio(string name) { return GetItemByName(ConfigurationManager.ApplicationPaths.StudioPath, name); } /// /// Gets a Genre /// /// The name. /// Task{Genre}. public Genre GetGenre(string name) { return GetItemByName(ConfigurationManager.ApplicationPaths.GenrePath, name); } /// /// Gets the genre. /// /// The name. /// Task{MusicGenre}. public MusicGenre GetMusicGenre(string name) { return GetItemByName(ConfigurationManager.ApplicationPaths.MusicGenrePath, name); } /// /// Gets the game genre. /// /// The name. /// Task{GameGenre}. public GameGenre GetGameGenre(string name) { return GetItemByName(ConfigurationManager.ApplicationPaths.GameGenrePath, name); } /// /// Gets a Genre /// /// The name. /// Task{Genre}. public Artist GetArtist(string name) { return GetItemByName(ConfigurationManager.ApplicationPaths.ArtistsPath, name); } /// /// The us culture /// private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); /// /// Gets a Year /// /// The value. /// Task{Year}. /// public Year GetYear(int value) { if (value <= 0) { throw new ArgumentOutOfRangeException(); } return GetItemByName(ConfigurationManager.ApplicationPaths.YearPath, value.ToString(UsCulture)); } /// /// The images by name item cache /// private readonly ConcurrentDictionary _itemsByName = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); private T GetItemByName(string path, string name) where T : BaseItem, new() { if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException(); } if (string.IsNullOrEmpty(name)) { throw new ArgumentNullException(); } var validFilename = FileSystem.GetValidFilename(name); string subFolderPrefix = null; if (typeof(T) == typeof(Person) && ConfigurationManager.Configuration.EnablePeoplePrefixSubFolders) { subFolderPrefix = validFilename.Substring(0, 1); if (string.IsNullOrWhiteSpace(subFolderPrefix)) { subFolderPrefix = "0"; } } var key = string.IsNullOrEmpty(subFolderPrefix) ? Path.Combine(path, validFilename) : Path.Combine(path, subFolderPrefix, validFilename); BaseItem obj; if (!_itemsByName.TryGetValue(key, out obj)) { var tuple = CreateItemByName(key, name); obj = tuple.Item2; _itemsByName.AddOrUpdate(key, obj, (keyName, oldValue) => obj); } return obj as T; } /// /// Creates an IBN item based on a given path /// /// /// The path. /// The name. /// Task{``0}. /// Path not created: + path private Tuple CreateItemByName(string path, string name) where T : BaseItem, new() { var fileInfo = new DirectoryInfo(path); var isNew = false; if (!fileInfo.Exists) { Directory.CreateDirectory(path); fileInfo = new DirectoryInfo(path); if (!fileInfo.Exists) { throw new IOException("Path not created: " + path); } isNew = true; } var type = typeof(T); var id = path.GetMBId(type); var item = RetrieveItem(id) as T; if (item == null) { item = new T { Name = name, Id = id, DateCreated = fileInfo.CreationTimeUtc, DateModified = fileInfo.LastWriteTimeUtc, Path = path }; isNew = true; } // Set this now so we don't cause additional file system access during provider executions item.ResetResolveArgs(fileInfo); return new Tuple(isNew, item); } /// /// Validate and refresh the People sub-set of the IBN. /// The items are stored in the db but not loaded into memory until actually requested by an operation. /// /// The cancellation token. /// The progress. /// Task. public Task ValidatePeople(CancellationToken cancellationToken, IProgress progress) { return new PeopleValidator(this, PeoplePrescanTasks, _logger).ValidatePeople(cancellationToken, progress); } /// /// Validates the artists. /// /// The cancellation token. /// The progress. /// Task. public Task ValidateArtists(CancellationToken cancellationToken, IProgress progress) { return new ArtistsValidator(this, _userManager, _logger).Run(progress, cancellationToken); } /// /// Validates the music genres. /// /// The cancellation token. /// The progress. /// Task. public Task ValidateMusicGenres(CancellationToken cancellationToken, IProgress progress) { return new MusicGenresValidator(this, _userManager, _logger).Run(progress, cancellationToken); } /// /// Validates the game genres. /// /// The cancellation token. /// The progress. /// Task. public Task ValidateGameGenres(CancellationToken cancellationToken, IProgress progress) { return new GameGenresValidator(this, _userManager, _logger).Run(progress, cancellationToken); } /// /// Validates the studios. /// /// The cancellation token. /// The progress. /// Task. public Task ValidateStudios(CancellationToken cancellationToken, IProgress progress) { return new StudiosValidator(this, _userManager, _logger).Run(progress, cancellationToken); } /// /// Validates the genres. /// /// The cancellation token. /// The progress. /// Task. public Task ValidateGenres(CancellationToken cancellationToken, IProgress progress) { return new GenresValidator(this, _userManager, _logger).Run(progress, cancellationToken); } /// /// Reloads the root media folder /// /// The progress. /// The cancellation token. /// Task. public Task ValidateMediaLibrary(IProgress progress, CancellationToken cancellationToken) { // Just run the scheduled task so that the user can see it _taskManager.CancelIfRunningAndQueue(); return Task.FromResult(true); } /// /// Validates the media library internal. /// /// The progress. /// The cancellation token. /// Task. public async Task ValidateMediaLibraryInternal(IProgress progress, CancellationToken cancellationToken) { _logger.Info("Validating media library"); await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false); progress.Report(.5); // Start by just validating the children of the root, but go no further await RootFolder.ValidateChildren(new Progress(), cancellationToken, recursive: false); progress.Report(1); foreach (var folder in _userManager.Users.Select(u => u.RootFolder).Distinct()) { await ValidateCollectionFolders(folder, cancellationToken).ConfigureAwait(false); } progress.Report(2); var innerProgress = new ActionableProgress(); innerProgress.RegisterAction(pct => progress.Report(2 + pct * .13)); // Run prescan tasks await RunPrescanTasks(innerProgress, cancellationToken).ConfigureAwait(false); progress.Report(15); innerProgress = new ActionableProgress(); innerProgress.RegisterAction(pct => progress.Report(15 + pct * .6)); // Now validate the entire media library await RootFolder.ValidateChildren(innerProgress, cancellationToken, recursive: true).ConfigureAwait(false); progress.Report(75); innerProgress = new ActionableProgress(); innerProgress.RegisterAction(pct => progress.Report(75 + pct * .25)); // Run post-scan tasks await RunPostScanTasks(innerProgress, cancellationToken).ConfigureAwait(false); progress.Report(100); // Bad practice, i know. But we keep a lot in memory, unfortunately. GC.Collect(2, GCCollectionMode.Forced, true); GC.Collect(2, GCCollectionMode.Forced, true); } /// /// Runs the prescan tasks. /// /// The progress. /// The cancellation token. /// Task. private async Task RunPrescanTasks(IProgress progress, CancellationToken cancellationToken) { var tasks = PrescanTasks.ToList(); var numComplete = 0; var numTasks = tasks.Count; foreach (var task in tasks) { var innerProgress = new ActionableProgress(); // Prevent access to modified closure var currentNumComplete = numComplete; innerProgress.RegisterAction(pct => { double innerPercent = (currentNumComplete * 100) + pct; innerPercent /= numTasks; progress.Report(innerPercent); }); try { await task.Run(innerProgress, cancellationToken); } catch (OperationCanceledException) { _logger.Info("Pre-scan task cancelled: {0}", task.GetType().Name); } catch (Exception ex) { _logger.ErrorException("Error running pre-scan task", ex); } numComplete++; double percent = numComplete; percent /= numTasks; progress.Report(percent * 100); } progress.Report(100); } /// /// Runs the post scan tasks. /// /// The progress. /// The cancellation token. /// Task. private async Task RunPostScanTasks(IProgress progress, CancellationToken cancellationToken) { var tasks = PostscanTasks.ToList(); var numComplete = 0; var numTasks = tasks.Count; foreach (var task in tasks) { var innerProgress = new ActionableProgress(); // Prevent access to modified closure var currentNumComplete = numComplete; innerProgress.RegisterAction(pct => { double innerPercent = (currentNumComplete * 100) + pct; innerPercent /= numTasks; progress.Report(innerPercent); }); try { await task.Run(innerProgress, cancellationToken); } catch (OperationCanceledException) { _logger.Info("Post-scan task cancelled: {0}", task.GetType().Name); } catch (Exception ex) { _logger.ErrorException("Error running postscan task", ex); } numComplete++; double percent = numComplete; percent /= numTasks; progress.Report(percent * 100); } progress.Report(100); } /// /// Validates only the collection folders for a User and goes no further /// /// The user root folder. /// The cancellation token. /// Task. private async Task ValidateCollectionFolders(UserRootFolder userRootFolder, CancellationToken cancellationToken) { _logger.Info("Validating collection folders within {0}", userRootFolder.Path); await userRootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); await userRootFolder.ValidateChildren(new Progress(), cancellationToken, recursive: false).ConfigureAwait(false); } /// /// Gets the default view. /// /// IEnumerable{VirtualFolderInfo}. public IEnumerable GetDefaultVirtualFolders() { return GetView(ConfigurationManager.ApplicationPaths.DefaultUserViewsPath); } /// /// Gets the view. /// /// The user. /// IEnumerable{VirtualFolderInfo}. public IEnumerable GetVirtualFolders(User user) { return GetView(user.RootFolderPath); } /// /// Gets the view. /// /// The path. /// IEnumerable{VirtualFolderInfo}. private IEnumerable GetView(string path) { return Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly) .Select(dir => new VirtualFolderInfo { Name = Path.GetFileName(dir), Locations = Directory.EnumerateFiles(dir, "*.mblink", SearchOption.TopDirectoryOnly) .Select(FileSystem.ResolveShortcut) .OrderBy(i => i) .ToList(), CollectionType = GetCollectionType(dir) }); } private string GetCollectionType(string path) { return new DirectoryInfo(path).EnumerateFiles("*.collection", SearchOption.TopDirectoryOnly) .Select(i => Path.GetFileNameWithoutExtension(i.FullName)) .FirstOrDefault(); } /// /// Gets the item by id. /// /// The id. /// BaseItem. /// id public BaseItem GetItemById(Guid id) { if (id == Guid.Empty) { throw new ArgumentNullException("id"); } BaseItem item; if (LibraryItemsCache.TryGetValue(id, out item)) { return item; } return RetrieveItem(id); } /// /// Gets the intros. /// /// The item. /// The user. /// IEnumerable{System.String}. public IEnumerable