aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations/Library
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Server.Implementations/Library')
-rw-r--r--Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs148
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs3066
-rw-r--r--Emby.Server.Implementations/Library/LocalTrailerPostScanTask.cs102
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs651
-rw-r--r--Emby.Server.Implementations/Library/MusicManager.cs157
-rw-r--r--Emby.Server.Implementations/Library/PathExtensions.cs45
-rw-r--r--Emby.Server.Implementations/Library/ResolverHelper.cs183
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs68
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs173
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs94
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs297
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs56
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs62
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs77
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs541
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs56
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs103
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs42
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs85
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs75
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs62
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs251
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs45
-rw-r--r--Emby.Server.Implementations/Library/SearchEngine.cs275
-rw-r--r--Emby.Server.Implementations/Library/UserViewManager.cs292
-rw-r--r--Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs44
-rw-r--r--Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs84
-rw-r--r--Emby.Server.Implementations/Library/Validators/GameGenresPostScanTask.cs45
-rw-r--r--Emby.Server.Implementations/Library/Validators/GameGenresValidator.cs74
-rw-r--r--Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs42
-rw-r--r--Emby.Server.Implementations/Library/Validators/GenresValidator.cs75
-rw-r--r--Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs45
-rw-r--r--Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs75
-rw-r--r--Emby.Server.Implementations/Library/Validators/PeopleValidator.cs172
-rw-r--r--Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs45
-rw-r--r--Emby.Server.Implementations/Library/Validators/StudiosValidator.cs74
-rw-r--r--Emby.Server.Implementations/Library/Validators/YearsPostScanTask.cs55
37 files changed, 7836 insertions, 0 deletions
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
new file mode 100644
index 000000000..2e69cd2ef
--- /dev/null
+++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
@@ -0,0 +1,148 @@
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Resolvers;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Server.Implementations.Library
+{
+ /// <summary>
+ /// Provides the core resolver ignore rules
+ /// </summary>
+ public class CoreResolutionIgnoreRule : IResolverIgnoreRule
+ {
+ private readonly IFileSystem _fileSystem;
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// Any folder named in this list will be ignored - can be added to at runtime for extensibility
+ /// </summary>
+ public static readonly List<string> IgnoreFolders = new List<string>
+ {
+ "metadata",
+ "ps3_update",
+ "ps3_vprm",
+ "extrafanart",
+ "extrathumbs",
+ ".actors",
+ ".wd_tv",
+
+ // Synology
+ "@eaDir",
+ "eaDir",
+ "#recycle"
+
+ };
+
+ public CoreResolutionIgnoreRule(IFileSystem fileSystem, ILibraryManager libraryManager)
+ {
+ _fileSystem = fileSystem;
+ _libraryManager = libraryManager;
+ }
+
+ /// <summary>
+ /// Shoulds the ignore.
+ /// </summary>
+ /// <param name="fileInfo">The file information.</param>
+ /// <param name="parent">The parent.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem parent)
+ {
+ var filename = fileInfo.Name;
+ var isHidden = fileInfo.IsHidden;
+ var path = fileInfo.FullName;
+
+ // Handle mac .DS_Store
+ // https://github.com/MediaBrowser/MediaBrowser/issues/427
+ if (filename.IndexOf("._", StringComparison.OrdinalIgnoreCase) == 0)
+ {
+ return true;
+ }
+
+ // Ignore hidden files and folders
+ if (isHidden)
+ {
+ if (parent == null)
+ {
+ var parentFolderName = Path.GetFileName(Path.GetDirectoryName(path));
+
+ if (string.Equals(parentFolderName, BaseItem.ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+ if (string.Equals(parentFolderName, BaseItem.ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+ }
+
+ // Sometimes these are marked hidden
+ if (_fileSystem.IsRootPath(path))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ if (fileInfo.IsDirectory)
+ {
+ // Ignore any folders in our list
+ if (IgnoreFolders.Contains(filename, StringComparer.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+
+ if (parent != null)
+ {
+ // Ignore trailer folders but allow it at the collection level
+ if (string.Equals(filename, BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase) &&
+ !(parent is AggregateFolder) && !(parent is UserRootFolder))
+ {
+ return true;
+ }
+
+ if (string.Equals(filename, BaseItem.ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+
+ if (string.Equals(filename, BaseItem.ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+ }
+ else
+ {
+ if (parent != null)
+ {
+ // Don't resolve these into audio files
+ if (string.Equals(_fileSystem.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename) && _libraryManager.IsAudioFile(filename))
+ {
+ return true;
+ }
+ }
+
+ // Ignore samples
+ var sampleFilename = " " + filename.Replace(".", " ", StringComparison.OrdinalIgnoreCase)
+ .Replace("-", " ", StringComparison.OrdinalIgnoreCase)
+ .Replace("_", " ", StringComparison.OrdinalIgnoreCase)
+ .Replace("!", " ", StringComparison.OrdinalIgnoreCase);
+
+ if (sampleFilename.IndexOf(" sample ", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
new file mode 100644
index 000000000..7ae00d94c
--- /dev/null
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -0,0 +1,3066 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Progress;
+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.Providers;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Querying;
+using MediaBrowser.Naming.Audio;
+using MediaBrowser.Naming.Common;
+using MediaBrowser.Naming.TV;
+using MediaBrowser.Naming.Video;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Server.Implementations.Library.Resolvers;
+using Emby.Server.Implementations.Library.Validators;
+using Emby.Server.Implementations.Logging;
+using Emby.Server.Implementations.ScheduledTasks;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Model.Channels;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.Library;
+using MediaBrowser.Model.Net;
+using SortOrder = MediaBrowser.Model.Entities.SortOrder;
+using VideoResolver = MediaBrowser.Naming.Video.VideoResolver;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Server.Implementations.Library
+{
+ /// <summary>
+ /// Class LibraryManager
+ /// </summary>
+ public class LibraryManager : ILibraryManager
+ {
+ /// <summary>
+ /// Gets or sets the postscan tasks.
+ /// </summary>
+ /// <value>The postscan tasks.</value>
+ private ILibraryPostScanTask[] PostscanTasks { get; set; }
+
+ /// <summary>
+ /// Gets the intro providers.
+ /// </summary>
+ /// <value>The intro providers.</value>
+ private IIntroProvider[] IntroProviders { get; set; }
+
+ /// <summary>
+ /// Gets the list of entity resolution ignore rules
+ /// </summary>
+ /// <value>The entity resolution ignore rules.</value>
+ private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
+
+ /// <summary>
+ /// Gets the list of BasePluginFolders added by plugins
+ /// </summary>
+ /// <value>The plugin folders.</value>
+ private IVirtualFolderCreator[] PluginFolderCreators { get; set; }
+
+ /// <summary>
+ /// Gets the list of currently registered entity resolvers
+ /// </summary>
+ /// <value>The entity resolvers enumerable.</value>
+ private IItemResolver[] EntityResolvers { get; set; }
+ private IMultiItemResolver[] MultiItemResolvers { get; set; }
+
+ /// <summary>
+ /// Gets or sets the comparers.
+ /// </summary>
+ /// <value>The comparers.</value>
+ private IBaseItemComparer[] Comparers { get; set; }
+
+ /// <summary>
+ /// Gets the active item repository
+ /// </summary>
+ /// <value>The item repository.</value>
+ public IItemRepository ItemRepository { get; set; }
+
+ /// <summary>
+ /// Occurs when [item added].
+ /// </summary>
+ public event EventHandler<ItemChangeEventArgs> ItemAdded;
+
+ /// <summary>
+ /// Occurs when [item updated].
+ /// </summary>
+ public event EventHandler<ItemChangeEventArgs> ItemUpdated;
+
+ /// <summary>
+ /// Occurs when [item removed].
+ /// </summary>
+ public event EventHandler<ItemChangeEventArgs> ItemRemoved;
+
+ /// <summary>
+ /// The _logger
+ /// </summary>
+ private readonly ILogger _logger;
+
+ /// <summary>
+ /// The _task manager
+ /// </summary>
+ private readonly ITaskManager _taskManager;
+
+ /// <summary>
+ /// The _user manager
+ /// </summary>
+ private readonly IUserManager _userManager;
+
+ /// <summary>
+ /// The _user data repository
+ /// </summary>
+ private readonly IUserDataManager _userDataRepository;
+
+ /// <summary>
+ /// Gets or sets the configuration manager.
+ /// </summary>
+ /// <value>The configuration manager.</value>
+ private IServerConfigurationManager ConfigurationManager { get; set; }
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ /// <value>The by reference items.</value>
+ private ConcurrentDictionary<Guid, BaseItem> ByReferenceItems { get; set; }
+
+ private readonly Func<ILibraryMonitor> _libraryMonitorFactory;
+ private readonly Func<IProviderManager> _providerManagerFactory;
+ private readonly Func<IUserViewManager> _userviewManager;
+ public bool IsScanRunning { get; private set; }
+
+ /// <summary>
+ /// The _library items cache
+ /// </summary>
+ private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
+ /// <summary>
+ /// Gets the library items cache.
+ /// </summary>
+ /// <value>The library items cache.</value>
+ private ConcurrentDictionary<Guid, BaseItem> LibraryItemsCache
+ {
+ get
+ {
+ return _libraryItemsCache;
+ }
+ }
+
+ private readonly IFileSystem _fileSystem;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LibraryManager" /> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="taskManager">The task manager.</param>
+ /// <param name="userManager">The user manager.</param>
+ /// <param name="configurationManager">The configuration manager.</param>
+ /// <param name="userDataRepository">The user data repository.</param>
+ public LibraryManager(ILogger logger, ITaskManager taskManager, IUserManager userManager, IServerConfigurationManager configurationManager, IUserDataManager userDataRepository, Func<ILibraryMonitor> libraryMonitorFactory, IFileSystem fileSystem, Func<IProviderManager> providerManagerFactory, Func<IUserViewManager> userviewManager)
+ {
+ _logger = logger;
+ _taskManager = taskManager;
+ _userManager = userManager;
+ ConfigurationManager = configurationManager;
+ _userDataRepository = userDataRepository;
+ _libraryMonitorFactory = libraryMonitorFactory;
+ _fileSystem = fileSystem;
+ _providerManagerFactory = providerManagerFactory;
+ _userviewManager = userviewManager;
+ ByReferenceItems = new ConcurrentDictionary<Guid, BaseItem>();
+ _libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>();
+
+ ConfigurationManager.ConfigurationUpdated += ConfigurationUpdated;
+
+ RecordConfigurationValues(configurationManager.Configuration);
+ }
+
+ /// <summary>
+ /// Adds the parts.
+ /// </summary>
+ /// <param name="rules">The rules.</param>
+ /// <param name="pluginFolders">The plugin folders.</param>
+ /// <param name="resolvers">The resolvers.</param>
+ /// <param name="introProviders">The intro providers.</param>
+ /// <param name="itemComparers">The item comparers.</param>
+ /// <param name="postscanTasks">The postscan tasks.</param>
+ public void AddParts(IEnumerable<IResolverIgnoreRule> rules,
+ IEnumerable<IVirtualFolderCreator> pluginFolders,
+ IEnumerable<IItemResolver> resolvers,
+ IEnumerable<IIntroProvider> introProviders,
+ IEnumerable<IBaseItemComparer> itemComparers,
+ IEnumerable<ILibraryPostScanTask> postscanTasks)
+ {
+ EntityResolutionIgnoreRules = rules.ToArray();
+ PluginFolderCreators = pluginFolders.ToArray();
+ EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
+ MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
+ IntroProviders = introProviders.ToArray();
+ Comparers = itemComparers.ToArray();
+
+ PostscanTasks = postscanTasks.OrderBy(i =>
+ {
+ var hasOrder = i as IHasOrder;
+
+ return hasOrder == null ? 0 : hasOrder.Order;
+
+ }).ToArray();
+ }
+
+ /// <summary>
+ /// The _root folder
+ /// </summary>
+ private volatile AggregateFolder _rootFolder;
+ /// <summary>
+ /// The _root folder sync lock
+ /// </summary>
+ private readonly object _rootFolderSyncLock = new object();
+ /// <summary>
+ /// Gets the root folder.
+ /// </summary>
+ /// <value>The root folder.</value>
+ public AggregateFolder RootFolder
+ {
+ get
+ {
+ if (_rootFolder == null)
+ {
+ lock (_rootFolderSyncLock)
+ {
+ if (_rootFolder == null)
+ {
+ _rootFolder = CreateRootFolder();
+ }
+ }
+ }
+ return _rootFolder;
+ }
+ }
+
+ /// <summary>
+ /// The _season zero display name
+ /// </summary>
+ private string _seasonZeroDisplayName;
+
+ private bool _wizardCompleted;
+ /// <summary>
+ /// Records the configuration values.
+ /// </summary>
+ /// <param name="configuration">The configuration.</param>
+ private void RecordConfigurationValues(ServerConfiguration configuration)
+ {
+ _seasonZeroDisplayName = configuration.SeasonZeroDisplayName;
+ _wizardCompleted = configuration.IsStartupWizardCompleted;
+ }
+
+ /// <summary>
+ /// Configurations the updated.
+ /// </summary>
+ /// <param name="sender">The sender.</param>
+ /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
+ void ConfigurationUpdated(object sender, EventArgs e)
+ {
+ var config = ConfigurationManager.Configuration;
+
+ var newSeasonZeroName = ConfigurationManager.Configuration.SeasonZeroDisplayName;
+ var seasonZeroNameChanged = !string.Equals(_seasonZeroDisplayName, newSeasonZeroName, StringComparison.Ordinal);
+ var wizardChanged = config.IsStartupWizardCompleted != _wizardCompleted;
+
+ RecordConfigurationValues(config);
+
+ if (seasonZeroNameChanged || wizardChanged)
+ {
+ _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
+ }
+
+ if (seasonZeroNameChanged)
+ {
+ Task.Run(async () =>
+ {
+ await UpdateSeasonZeroNames(newSeasonZeroName, CancellationToken.None).ConfigureAwait(false);
+
+ });
+ }
+ }
+
+ /// <summary>
+ /// Updates the season zero names.
+ /// </summary>
+ /// <param name="newName">The new name.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ private async Task UpdateSeasonZeroNames(string newName, CancellationToken cancellationToken)
+ {
+ var seasons = GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = new[] { typeof(Season).Name },
+ Recursive = true,
+ IndexNumber = 0
+
+ }).Cast<Season>()
+ .Where(i => !string.Equals(i.Name, newName, StringComparison.Ordinal))
+ .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);
+ }
+ }
+ }
+
+ public void RegisterItem(BaseItem item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+ RegisterItem(item.Id, item);
+ }
+
+ private void RegisterItem(Guid id, BaseItem item)
+ {
+ if (item is IItemByName)
+ {
+ if (!(item is MusicArtist))
+ {
+ return;
+ }
+ }
+
+ if (item.IsFolder)
+ {
+ if (!(item is ICollectionFolder) && !(item is UserView) && !(item is Channel) && !(item is AggregateFolder))
+ {
+ if (item.SourceType != SourceType.Library)
+ {
+ return;
+ }
+ }
+ }
+ else
+ {
+ if (item is Photo)
+ {
+ return;
+ }
+ }
+
+ LibraryItemsCache.AddOrUpdate(id, item, delegate { return item; });
+ }
+
+ public async Task DeleteItem(BaseItem item, DeleteOptions options)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ _logger.Debug("Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+ item.GetType().Name,
+ item.Name ?? "Unknown name",
+ item.Path ?? string.Empty,
+ item.Id);
+
+ var parent = item.Parent;
+
+ var locationType = item.LocationType;
+
+ var children = item.IsFolder
+ ? ((Folder)item).GetRecursiveChildren(false).ToList()
+ : new List<BaseItem>();
+
+ foreach (var metadataPath in GetMetadataPaths(item, children))
+ {
+ _logger.Debug("Deleting path {0}", metadataPath);
+
+ try
+ {
+ _fileSystem.DeleteDirectory(metadataPath, true);
+ }
+ catch (IOException)
+ {
+
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error deleting {0}", ex, metadataPath);
+ }
+ }
+
+ if (options.DeleteFileLocation && locationType != LocationType.Remote && locationType != LocationType.Virtual)
+ {
+ foreach (var path in item.GetDeletePaths().ToList())
+ {
+ if (_fileSystem.DirectoryExists(path))
+ {
+ _logger.Debug("Deleting path {0}", path);
+ _fileSystem.DeleteDirectory(path, true);
+ }
+ else if (_fileSystem.FileExists(path))
+ {
+ _logger.Debug("Deleting path {0}", path);
+ _fileSystem.DeleteFile(path);
+ }
+ }
+
+ if (parent != null)
+ {
+ await parent.ValidateChildren(new Progress<double>(), CancellationToken.None)
+ .ConfigureAwait(false);
+ }
+ }
+ else if (parent != null)
+ {
+ parent.RemoveChild(item);
+ }
+
+ await ItemRepository.DeleteItem(item.Id, CancellationToken.None).ConfigureAwait(false);
+ foreach (var child in children)
+ {
+ await ItemRepository.DeleteItem(child.Id, CancellationToken.None).ConfigureAwait(false);
+ }
+
+ BaseItem removed;
+ _libraryItemsCache.TryRemove(item.Id, out removed);
+
+ ReportItemRemoved(item);
+ }
+
+ private IEnumerable<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
+ {
+ var list = new List<string>
+ {
+ item.GetInternalMetadataPath()
+ };
+
+ list.AddRange(children.Select(i => i.GetInternalMetadataPath()));
+
+ return list;
+ }
+
+ /// <summary>
+ /// Resolves the item.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <param name="resolvers">The resolvers.</param>
+ /// <returns>BaseItem.</returns>
+ private BaseItem ResolveItem(ItemResolveArgs args, IItemResolver[] resolvers)
+ {
+ var item = (resolvers ?? EntityResolvers).Select(r => Resolve(args, r))
+ .FirstOrDefault(i => i != null);
+
+ if (item != null)
+ {
+ ResolverHelper.SetInitialItemValues(item, args, _fileSystem, this);
+ }
+
+ return item;
+ }
+
+ private BaseItem Resolve(ItemResolveArgs args, IItemResolver resolver)
+ {
+ try
+ {
+ return resolver.ResolvePath(args);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error in {0} resolving {1}", ex, resolver.GetType().Name, args.Path);
+ return null;
+ }
+ }
+
+ public Guid GetNewItemId(string key, Type type)
+ {
+ if (string.IsNullOrWhiteSpace(key))
+ {
+ throw new ArgumentNullException("key");
+ }
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ if (ConfigurationManager.Configuration.EnableLocalizedGuids && key.StartsWith(ConfigurationManager.ApplicationPaths.ProgramDataPath))
+ {
+ // Try to normalize paths located underneath program-data in an attempt to make them more portable
+ key = key.Substring(ConfigurationManager.ApplicationPaths.ProgramDataPath.Length)
+ .TrimStart(new[] { '/', '\\' })
+ .Replace("/", "\\");
+ }
+
+ if (!ConfigurationManager.Configuration.EnableCaseSensitiveItemIds)
+ {
+ key = key.ToLower();
+ }
+
+ key = type.FullName + key;
+
+ return key.GetMD5();
+ }
+
+ /// <summary>
+ /// Ensure supplied item has only one instance throughout
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>The proper instance to the item</returns>
+ 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;
+ }
+
+ public BaseItem ResolvePath(FileSystemMetadata fileInfo,
+ Folder parent = null)
+ {
+ return ResolvePath(fileInfo, new DirectoryService(_logger, _fileSystem), null, parent);
+ }
+
+ private BaseItem ResolvePath(FileSystemMetadata fileInfo,
+ IDirectoryService directoryService,
+ IItemResolver[] resolvers,
+ Folder parent = null,
+ string collectionType = null,
+ LibraryOptions libraryOptions = null)
+ {
+ if (fileInfo == null)
+ {
+ throw new ArgumentNullException("fileInfo");
+ }
+
+ var fullPath = fileInfo.FullName;
+
+ if (string.IsNullOrWhiteSpace(collectionType) && parent != null)
+ {
+ collectionType = GetContentTypeOverride(fullPath, true);
+ }
+
+ var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService)
+ {
+ Parent = parent,
+ Path = fullPath,
+ FileInfo = fileInfo,
+ CollectionType = collectionType,
+ LibraryOptions = libraryOptions
+ };
+
+ // Return null if ignore rules deem that we should do so
+ if (IgnoreFile(args.FileInfo, args.Parent))
+ {
+ 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;
+
+ var fileSystemDictionary = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, _fileSystem, _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 = NormalizeRootPathList(fileSystemDictionary.Values);
+
+ fileSystemDictionary = paths.ToDictionary(i => i.FullName);
+ }
+
+ args.FileSystemDictionary = fileSystemDictionary;
+ }
+
+ // Check to see if we should resolve based on our contents
+ if (args.IsDirectory && !ShouldResolvePathContents(args))
+ {
+ return null;
+ }
+
+ return ResolveItem(args, resolvers);
+ }
+
+ private readonly List<string> _ignoredPaths = new List<string>();
+
+ public void RegisterIgnoredPath(string path)
+ {
+ lock (_ignoredPaths)
+ {
+ _ignoredPaths.Add(path);
+ }
+ }
+ public void UnRegisterIgnoredPath(string path)
+ {
+ lock (_ignoredPaths)
+ {
+ _ignoredPaths.Remove(path);
+ }
+ }
+
+ public bool IgnoreFile(FileSystemMetadata file, BaseItem parent)
+ {
+ if (EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(file, parent)))
+ {
+ return true;
+ }
+
+ //lock (_ignoredPaths)
+ {
+ if (_ignoredPaths.Contains(file.FullName, StringComparer.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public IEnumerable<FileSystemMetadata> NormalizeRootPathList(IEnumerable<FileSystemMetadata> paths)
+ {
+ var originalList = paths.ToList();
+
+ var list = originalList.Where(i => i.IsDirectory)
+ .Select(i => _fileSystem.NormalizePath(i.FullName))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
+ var dupes = list.Where(subPath => !subPath.EndsWith(":\\", StringComparison.OrdinalIgnoreCase) && list.Any(i => _fileSystem.ContainsSubPath(i, subPath)))
+ .ToList();
+
+ foreach (var dupe in dupes)
+ {
+ _logger.Info("Found duplicate path: {0}", dupe);
+ }
+
+ var newList = list.Except(dupes, StringComparer.OrdinalIgnoreCase).Select(_fileSystem.GetDirectoryInfo).ToList();
+ newList.AddRange(originalList.Where(i => !i.IsDirectory));
+ return newList;
+ }
+
+ /// <summary>
+ /// Determines whether a path should be ignored based on its contents - called after the contents have been read
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ private static bool ShouldResolvePathContents(ItemResolveArgs args)
+ {
+ // Ignore any folders containing a file called .ignore
+ return !args.ContainsFileSystemEntryByName(".ignore");
+ }
+
+ public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, string collectionType)
+ {
+ return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers);
+ }
+
+ public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files,
+ IDirectoryService directoryService,
+ Folder parent,
+ LibraryOptions libraryOptions,
+ string collectionType,
+ IItemResolver[] resolvers)
+ {
+ var fileList = files.Where(i => !IgnoreFile(i, parent)).ToList();
+
+ if (parent != null)
+ {
+ var multiItemResolvers = resolvers == null ? MultiItemResolvers : resolvers.OfType<IMultiItemResolver>().ToArray();
+
+ foreach (var resolver in multiItemResolvers)
+ {
+ var result = resolver.ResolveMultiple(parent, fileList, collectionType, directoryService);
+
+ if (result != null && result.Items.Count > 0)
+ {
+ var items = new List<BaseItem>();
+ items.AddRange(result.Items);
+
+ foreach (var item in items)
+ {
+ ResolverHelper.SetInitialItemValues(item, parent, _fileSystem, this, directoryService);
+ }
+ items.AddRange(ResolveFileList(result.ExtraFiles, directoryService, parent, collectionType, resolvers, libraryOptions));
+ return items;
+ }
+ }
+ }
+
+ return ResolveFileList(fileList, directoryService, parent, collectionType, resolvers, libraryOptions);
+ }
+
+ private IEnumerable<BaseItem> ResolveFileList(IEnumerable<FileSystemMetadata> fileList,
+ IDirectoryService directoryService,
+ Folder parent,
+ string collectionType,
+ IItemResolver[] resolvers,
+ LibraryOptions libraryOptions)
+ {
+ return fileList.Select(f =>
+ {
+ try
+ {
+ return ResolvePath(f, directoryService, resolvers, parent, collectionType, libraryOptions);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error resolving path {0}", ex, f.FullName);
+ return null;
+ }
+ }).Where(i => i != null);
+ }
+
+ /// <summary>
+ /// Creates the root media folder
+ /// </summary>
+ /// <returns>AggregateFolder.</returns>
+ /// <exception cref="System.InvalidOperationException">Cannot create the root folder until plugins have loaded</exception>
+ public AggregateFolder CreateRootFolder()
+ {
+ var rootFolderPath = ConfigurationManager.ApplicationPaths.RootFolderPath;
+
+ _fileSystem.CreateDirectory(rootFolderPath);
+
+ var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ?? (AggregateFolder)ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath));
+
+ // Add in the plug-in folders
+ foreach (var child in PluginFolderCreators)
+ {
+ var folder = child.GetFolder();
+
+ if (folder != null)
+ {
+ if (folder.Id == Guid.Empty)
+ {
+ if (string.IsNullOrWhiteSpace(folder.Path))
+ {
+ folder.Id = GetNewItemId(folder.GetType().Name, folder.GetType());
+ }
+ else
+ {
+ folder.Id = GetNewItemId(folder.Path, folder.GetType());
+ }
+ }
+
+ var dbItem = GetItemById(folder.Id) as BasePluginFolder;
+
+ if (dbItem != null && string.Equals(dbItem.Path, folder.Path, StringComparison.OrdinalIgnoreCase))
+ {
+ folder = dbItem;
+ }
+
+ if (folder.ParentId != rootFolder.Id)
+ {
+ folder.ParentId = rootFolder.Id;
+ var task = folder.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None);
+ Task.WaitAll(task);
+ }
+
+ rootFolder.AddVirtualChild(folder);
+
+ RegisterItem(folder);
+ }
+ }
+
+ return rootFolder;
+ }
+
+ private volatile UserRootFolder _userRootFolder;
+ private readonly object _syncLock = new object();
+ public Folder GetUserRootFolder()
+ {
+ if (_userRootFolder == null)
+ {
+ lock (_syncLock)
+ {
+ if (_userRootFolder == null)
+ {
+ var userRootPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+
+ _fileSystem.CreateDirectory(userRootPath);
+
+ var tmpItem = GetItemById(GetNewItemId(userRootPath, typeof(UserRootFolder))) as UserRootFolder;
+
+ if (tmpItem == null)
+ {
+ tmpItem = (UserRootFolder)ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath));
+ }
+
+ _userRootFolder = tmpItem;
+ }
+ }
+ }
+
+ return _userRootFolder;
+ }
+
+ public BaseItem FindByPath(string path, bool? isFolder)
+ {
+ // If this returns multiple items it could be tricky figuring out which one is correct.
+ // In most cases, the newest one will be and the others obsolete but not yet cleaned up
+
+ var query = new InternalItemsQuery
+ {
+ Path = path,
+ IsFolder = isFolder,
+ SortBy = new[] { ItemSortBy.DateCreated },
+ SortOrder = SortOrder.Descending,
+ Limit = 1
+ };
+
+ return GetItemList(query)
+ .FirstOrDefault();
+ }
+
+ /// <summary>
+ /// Gets a Person
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>Task{Person}.</returns>
+ public Person GetPerson(string name)
+ {
+ return CreateItemByName<Person>(Person.GetPath(name), name);
+ }
+
+ /// <summary>
+ /// Gets a Studio
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>Task{Studio}.</returns>
+ public Studio GetStudio(string name)
+ {
+ return CreateItemByName<Studio>(Studio.GetPath(name), name);
+ }
+
+ /// <summary>
+ /// Gets a Genre
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>Task{Genre}.</returns>
+ public Genre GetGenre(string name)
+ {
+ return CreateItemByName<Genre>(Genre.GetPath(name), name);
+ }
+
+ /// <summary>
+ /// Gets the genre.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>Task{MusicGenre}.</returns>
+ public MusicGenre GetMusicGenre(string name)
+ {
+ return CreateItemByName<MusicGenre>(MusicGenre.GetPath(name), name);
+ }
+
+ /// <summary>
+ /// Gets the game genre.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>Task{GameGenre}.</returns>
+ public GameGenre GetGameGenre(string name)
+ {
+ return CreateItemByName<GameGenre>(GameGenre.GetPath(name), name);
+ }
+
+ /// <summary>
+ /// Gets a Year
+ /// </summary>
+ /// <param name="value">The value.</param>
+ /// <returns>Task{Year}.</returns>
+ /// <exception cref="System.ArgumentOutOfRangeException"></exception>
+ public Year GetYear(int value)
+ {
+ if (value <= 0)
+ {
+ throw new ArgumentOutOfRangeException("Years less than or equal to 0 are invalid.");
+ }
+
+ var name = value.ToString(CultureInfo.InvariantCulture);
+
+ return CreateItemByName<Year>(Year.GetPath(name), name);
+ }
+
+ /// <summary>
+ /// Gets a Genre
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>Task{Genre}.</returns>
+ public MusicArtist GetArtist(string name)
+ {
+ return CreateItemByName<MusicArtist>(MusicArtist.GetPath(name), name);
+ }
+
+ private T CreateItemByName<T>(string path, string name)
+ where T : BaseItem, new()
+ {
+ if (typeof(T) == typeof(MusicArtist))
+ {
+ var existing = GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = new[] { typeof(T).Name },
+ Name = name
+
+ }).Cast<MusicArtist>()
+ .OrderBy(i => i.IsAccessedByName ? 1 : 0)
+ .Cast<T>()
+ .FirstOrDefault();
+
+ if (existing != null)
+ {
+ return existing;
+ }
+ }
+
+ var id = GetNewItemId(path, typeof(T));
+
+ var item = GetItemById(id) as T;
+
+ if (item == null)
+ {
+ item = new T
+ {
+ Name = name,
+ Id = id,
+ DateCreated = DateTime.UtcNow,
+ DateModified = DateTime.UtcNow,
+ Path = path
+ };
+
+ var task = CreateItem(item, CancellationToken.None);
+ Task.WaitAll(task);
+ }
+
+ return item;
+ }
+
+ public IEnumerable<MusicArtist> GetAlbumArtists(IEnumerable<IHasAlbumArtist> items)
+ {
+ var names = items
+ .SelectMany(i => i.AlbumArtists)
+ .DistinctNames()
+ .Select(i =>
+ {
+ try
+ {
+ var artist = GetArtist(i);
+
+ return artist;
+ }
+ catch
+ {
+ // Already logged at lower levels
+ return null;
+ }
+ })
+ .Where(i => i != null);
+
+ return names;
+ }
+
+ public IEnumerable<MusicArtist> GetArtists(IEnumerable<IHasArtist> items)
+ {
+ var names = items
+ .SelectMany(i => i.AllArtists)
+ .DistinctNames()
+ .Select(i =>
+ {
+ try
+ {
+ var artist = GetArtist(i);
+
+ return artist;
+ }
+ catch
+ {
+ // Already logged at lower levels
+ return null;
+ }
+ })
+ .Where(i => i != null);
+
+ return names;
+ }
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ public Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ // Ensure the location is available.
+ _fileSystem.CreateDirectory(ConfigurationManager.ApplicationPaths.PeoplePath);
+
+ return new PeopleValidator(this, _logger, ConfigurationManager, _fileSystem).ValidatePeople(cancellationToken, progress);
+ }
+
+ /// <summary>
+ /// Reloads the root media folder
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ // Just run the scheduled task so that the user can see it
+ _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
+
+ return Task.FromResult(true);
+ }
+
+ /// <summary>
+ /// Queues the library scan.
+ /// </summary>
+ public void QueueLibraryScan()
+ {
+ // Just run the scheduled task so that the user can see it
+ _taskManager.QueueScheduledTask<RefreshMediaLibraryTask>();
+ }
+
+ /// <summary>
+ /// Validates the media library internal.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ IsScanRunning = true;
+ _libraryMonitorFactory().Stop();
+
+ try
+ {
+ await PerformLibraryValidation(progress, cancellationToken).ConfigureAwait(false);
+ }
+ finally
+ {
+ _libraryMonitorFactory().Start();
+ IsScanRunning = false;
+ }
+ }
+
+ private async Task PerformLibraryValidation(IProgress<double> 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<double>(), cancellationToken, new MetadataRefreshOptions(_fileSystem), recursive: false);
+
+ progress.Report(1);
+
+ var userRoot = GetUserRootFolder();
+
+ await userRoot.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+
+ await userRoot.ValidateChildren(new Progress<double>(), cancellationToken, new MetadataRefreshOptions(_fileSystem), recursive: false).ConfigureAwait(false);
+ progress.Report(2);
+
+ var innerProgress = new ActionableProgress<double>();
+
+ innerProgress.RegisterAction(pct => progress.Report(2 + pct * .73));
+
+ // Now validate the entire media library
+ await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(_fileSystem), recursive: true).ConfigureAwait(false);
+
+ progress.Report(75);
+
+ innerProgress = new ActionableProgress<double>();
+
+ innerProgress.RegisterAction(pct => progress.Report(75 + pct * .25));
+
+ // Run post-scan tasks
+ await RunPostScanTasks(innerProgress, cancellationToken).ConfigureAwait(false);
+
+ progress.Report(100);
+ }
+
+ /// <summary>
+ /// Runs the post scan tasks.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ private async Task RunPostScanTasks(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var tasks = PostscanTasks.ToList();
+
+ var numComplete = 0;
+ var numTasks = tasks.Count;
+
+ foreach (var task in tasks)
+ {
+ var innerProgress = new ActionableProgress<double>();
+
+ // Prevent access to modified closure
+ var currentNumComplete = numComplete;
+
+ innerProgress.RegisterAction(pct =>
+ {
+ double innerPercent = currentNumComplete * 100 + pct;
+ innerPercent /= numTasks;
+ progress.Report(innerPercent);
+ });
+
+ _logger.Debug("Running post-scan task {0}", task.GetType().Name);
+
+ try
+ {
+ await task.Run(innerProgress, cancellationToken).ConfigureAwait(false);
+ }
+ 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);
+ }
+
+ /// <summary>
+ /// Gets the default view.
+ /// </summary>
+ /// <returns>IEnumerable{VirtualFolderInfo}.</returns>
+ public IEnumerable<VirtualFolderInfo> GetVirtualFolders()
+ {
+ return GetView(ConfigurationManager.ApplicationPaths.DefaultUserViewsPath);
+ }
+
+ /// <summary>
+ /// Gets the view.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>IEnumerable{VirtualFolderInfo}.</returns>
+ private IEnumerable<VirtualFolderInfo> GetView(string path)
+ {
+ var topLibraryFolders = GetUserRootFolder().Children.ToList();
+
+ return _fileSystem.GetDirectoryPaths(path)
+ .Select(dir => GetVirtualFolderInfo(dir, topLibraryFolders));
+ }
+
+ private VirtualFolderInfo GetVirtualFolderInfo(string dir, List<BaseItem> allCollectionFolders)
+ {
+ var info = new VirtualFolderInfo
+ {
+ Name = Path.GetFileName(dir),
+
+ Locations = _fileSystem.GetFilePaths(dir, false)
+ .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
+ .Select(_fileSystem.ResolveShortcut)
+ .OrderBy(i => i)
+ .ToList(),
+
+ CollectionType = GetCollectionType(dir)
+ };
+
+ var libraryFolder = allCollectionFolders.FirstOrDefault(i => string.Equals(i.Path, dir, StringComparison.OrdinalIgnoreCase));
+
+ if (libraryFolder != null && libraryFolder.HasImage(ImageType.Primary))
+ {
+ info.PrimaryImageItemId = libraryFolder.Id.ToString("N");
+ }
+
+ if (libraryFolder != null)
+ {
+ info.ItemId = libraryFolder.Id.ToString("N");
+ info.LibraryOptions = GetLibraryOptions(libraryFolder);
+ }
+
+ return info;
+ }
+
+ private string GetCollectionType(string path)
+ {
+ return _fileSystem.GetFiles(path, false)
+ .Where(i => string.Equals(i.Extension, ".collection", StringComparison.OrdinalIgnoreCase))
+ .Select(i => _fileSystem.GetFileNameWithoutExtension(i))
+ .FirstOrDefault();
+ }
+
+ /// <summary>
+ /// Gets the item by id.
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <returns>BaseItem.</returns>
+ /// <exception cref="System.ArgumentNullException">id</exception>
+ public BaseItem GetItemById(Guid id)
+ {
+ if (id == Guid.Empty)
+ {
+ throw new ArgumentNullException("id");
+ }
+
+ BaseItem item;
+
+ if (LibraryItemsCache.TryGetValue(id, out item))
+ {
+ return item;
+ }
+
+ item = RetrieveItem(id);
+
+ //_logger.Debug("GetitemById {0}", id);
+
+ if (item != null)
+ {
+ RegisterItem(item);
+ }
+
+ return item;
+ }
+
+ public IEnumerable<BaseItem> GetItemList(InternalItemsQuery query)
+ {
+ if (query.Recursive && query.ParentId.HasValue)
+ {
+ var parent = GetItemById(query.ParentId.Value);
+ if (parent != null)
+ {
+ SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent });
+ query.ParentId = null;
+ }
+ }
+
+ if (query.User != null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+
+ return ItemRepository.GetItemList(query);
+ }
+
+ public IEnumerable<BaseItem> GetItemList(InternalItemsQuery query, IEnumerable<string> parentIds)
+ {
+ var parents = parentIds.Select(i => GetItemById(new Guid(i))).Where(i => i != null).ToList();
+
+ SetTopParentIdsOrAncestors(query, parents);
+
+ if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
+ {
+ if (query.User != null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+ }
+
+ return ItemRepository.GetItemList(query);
+ }
+
+ public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
+ {
+ if (query.User != null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+
+ if (query.EnableTotalRecordCount)
+ {
+ return ItemRepository.GetItems(query);
+ }
+
+ return new QueryResult<BaseItem>
+ {
+ Items = ItemRepository.GetItemList(query).ToArray()
+ };
+ }
+
+ public List<Guid> GetItemIds(InternalItemsQuery query)
+ {
+ if (query.User != null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+
+ return ItemRepository.GetItemIdsList(query);
+ }
+
+ public QueryResult<Tuple<BaseItem, ItemCounts>> GetStudios(InternalItemsQuery query)
+ {
+ if (query.User != null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+
+ SetTopParentOrAncestorIds(query);
+ return ItemRepository.GetStudios(query);
+ }
+
+ public QueryResult<Tuple<BaseItem, ItemCounts>> GetGenres(InternalItemsQuery query)
+ {
+ if (query.User != null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+
+ SetTopParentOrAncestorIds(query);
+ return ItemRepository.GetGenres(query);
+ }
+
+ public QueryResult<Tuple<BaseItem, ItemCounts>> GetGameGenres(InternalItemsQuery query)
+ {
+ if (query.User != null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+
+ SetTopParentOrAncestorIds(query);
+ return ItemRepository.GetGameGenres(query);
+ }
+
+ public QueryResult<Tuple<BaseItem, ItemCounts>> GetMusicGenres(InternalItemsQuery query)
+ {
+ if (query.User != null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+
+ SetTopParentOrAncestorIds(query);
+ return ItemRepository.GetMusicGenres(query);
+ }
+
+ public QueryResult<Tuple<BaseItem, ItemCounts>> GetAllArtists(InternalItemsQuery query)
+ {
+ if (query.User != null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+
+ SetTopParentOrAncestorIds(query);
+ return ItemRepository.GetAllArtists(query);
+ }
+
+ public QueryResult<Tuple<BaseItem, ItemCounts>> GetArtists(InternalItemsQuery query)
+ {
+ if (query.User != null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+
+ SetTopParentOrAncestorIds(query);
+ return ItemRepository.GetArtists(query);
+ }
+
+ private void SetTopParentOrAncestorIds(InternalItemsQuery query)
+ {
+ if (query.AncestorIds.Length == 0)
+ {
+ return;
+ }
+
+ var parents = query.AncestorIds.Select(i => GetItemById(new Guid(i))).ToList();
+
+ if (parents.All(i =>
+ {
+ if (i is ICollectionFolder || i is UserView)
+ {
+ return true;
+ }
+
+ //_logger.Debug("Query requires ancestor query due to type: " + i.GetType().Name);
+ return false;
+
+ }))
+ {
+ // Optimize by querying against top level views
+ query.TopParentIds = parents.SelectMany(i => GetTopParentsForQuery(i, query.User)).Select(i => i.Id.ToString("N")).ToArray();
+ query.AncestorIds = new string[] { };
+ }
+ }
+
+ public QueryResult<Tuple<BaseItem, ItemCounts>> GetAlbumArtists(InternalItemsQuery query)
+ {
+ if (query.User != null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+
+ SetTopParentOrAncestorIds(query);
+ return ItemRepository.GetAlbumArtists(query);
+ }
+
+ public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query)
+ {
+ if (query.Recursive && query.ParentId.HasValue)
+ {
+ var parent = GetItemById(query.ParentId.Value);
+ if (parent != null)
+ {
+ SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent });
+ query.ParentId = null;
+ }
+ }
+
+ if (query.User != null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+
+ if (query.EnableTotalRecordCount)
+ {
+ return ItemRepository.GetItems(query);
+ }
+
+ return new QueryResult<BaseItem>
+ {
+ Items = ItemRepository.GetItemList(query).ToArray()
+ };
+ }
+
+ private void SetTopParentIdsOrAncestors(InternalItemsQuery query, List<BaseItem> parents)
+ {
+ if (parents.All(i =>
+ {
+ if (i is ICollectionFolder || i is UserView)
+ {
+ return true;
+ }
+
+ //_logger.Debug("Query requires ancestor query due to type: " + i.GetType().Name);
+ return false;
+
+ }))
+ {
+ // Optimize by querying against top level views
+ query.TopParentIds = parents.SelectMany(i => GetTopParentsForQuery(i, query.User)).Select(i => i.Id.ToString("N")).ToArray();
+ }
+ else
+ {
+ // We need to be able to query from any arbitrary ancestor up the tree
+ query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).Select(i => i.ToString("N")).ToArray();
+ }
+ }
+
+ private void AddUserToQuery(InternalItemsQuery query, User user)
+ {
+ if (query.AncestorIds.Length == 0 &&
+ !query.ParentId.HasValue &&
+ query.ChannelIds.Length == 0 &&
+ query.TopParentIds.Length == 0 &&
+ string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey)
+ && query.ItemIds.Length == 0)
+ {
+ var userViews = _userviewManager().GetUserViews(new UserViewQuery
+ {
+ UserId = user.Id.ToString("N"),
+ IncludeHidden = true
+
+ }, CancellationToken.None).Result.ToList();
+
+ query.TopParentIds = userViews.SelectMany(i => GetTopParentsForQuery(i, user)).Select(i => i.Id.ToString("N")).ToArray();
+ }
+ }
+
+ private IEnumerable<BaseItem> GetTopParentsForQuery(BaseItem item, User user)
+ {
+ var view = item as UserView;
+
+ if (view != null)
+ {
+ if (string.Equals(view.ViewType, CollectionType.LiveTv))
+ {
+ return new[] { view };
+ }
+ if (string.Equals(view.ViewType, CollectionType.Channels))
+ {
+ var channelResult = BaseItem.ChannelManager.GetChannelsInternal(new ChannelQuery
+ {
+ UserId = user.Id.ToString("N")
+
+ }, CancellationToken.None).Result;
+
+ return channelResult.Items;
+ }
+
+ // Translate view into folders
+ if (view.DisplayParentId != Guid.Empty)
+ {
+ var displayParent = GetItemById(view.DisplayParentId);
+ if (displayParent != null)
+ {
+ return GetTopParentsForQuery(displayParent, user);
+ }
+ return new BaseItem[] { };
+ }
+ if (view.ParentId != Guid.Empty)
+ {
+ var displayParent = GetItemById(view.ParentId);
+ if (displayParent != null)
+ {
+ return GetTopParentsForQuery(displayParent, user);
+ }
+ return new BaseItem[] { };
+ }
+
+ // Handle grouping
+ if (user != null && !string.IsNullOrWhiteSpace(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType))
+ {
+ return user.RootFolder
+ .GetChildren(user, true)
+ .OfType<CollectionFolder>()
+ .Where(i => string.IsNullOrWhiteSpace(i.CollectionType) || string.Equals(i.CollectionType, view.ViewType, StringComparison.OrdinalIgnoreCase))
+ .Where(i => user.IsFolderGrouped(i.Id))
+ .SelectMany(i => GetTopParentsForQuery(i, user));
+ }
+ return new BaseItem[] { };
+ }
+
+ var collectionFolder = item as CollectionFolder;
+ if (collectionFolder != null)
+ {
+ return collectionFolder.GetPhysicalParents();
+ }
+
+ var topParent = item.GetTopParent();
+ if (topParent != null)
+ {
+ return new[] { topParent };
+ }
+ return new BaseItem[] { };
+ }
+
+ /// <summary>
+ /// Gets the intros.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="user">The user.</param>
+ /// <returns>IEnumerable{System.String}.</returns>
+ public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
+ {
+ var tasks = IntroProviders
+ .OrderBy(i => i.GetType().Name.IndexOf("Default", StringComparison.OrdinalIgnoreCase) == -1 ? 0 : 1)
+ .Take(1)
+ .Select(i => GetIntros(i, item, user));
+
+ var items = await Task.WhenAll(tasks).ConfigureAwait(false);
+
+ return items
+ .SelectMany(i => i.ToArray())
+ .Select(ResolveIntro)
+ .Where(i => i != null);
+ }
+
+ /// <summary>
+ /// Gets the intros.
+ /// </summary>
+ /// <param name="provider">The provider.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="user">The user.</param>
+ /// <returns>Task&lt;IEnumerable&lt;IntroInfo&gt;&gt;.</returns>
+ private async Task<IEnumerable<IntroInfo>> GetIntros(IIntroProvider provider, BaseItem item, User user)
+ {
+ try
+ {
+ return await provider.GetIntros(item, user).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error getting intros", ex);
+
+ return new List<IntroInfo>();
+ }
+ }
+
+ /// <summary>
+ /// Gets all intro files.
+ /// </summary>
+ /// <returns>IEnumerable{System.String}.</returns>
+ public IEnumerable<string> GetAllIntroFiles()
+ {
+ return IntroProviders.SelectMany(i =>
+ {
+ try
+ {
+ return i.GetAllIntroFiles().ToList();
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error getting intro files", ex);
+
+ return new List<string>();
+ }
+ });
+ }
+
+ /// <summary>
+ /// Resolves the intro.
+ /// </summary>
+ /// <param name="info">The info.</param>
+ /// <returns>Video.</returns>
+ private Video ResolveIntro(IntroInfo info)
+ {
+ Video video = null;
+
+ if (info.ItemId.HasValue)
+ {
+ // Get an existing item by Id
+ video = GetItemById(info.ItemId.Value) as Video;
+
+ if (video == null)
+ {
+ _logger.Error("Unable to locate item with Id {0}.", info.ItemId.Value);
+ }
+ }
+ else if (!string.IsNullOrEmpty(info.Path))
+ {
+ try
+ {
+ // Try to resolve the path into a video
+ video = ResolvePath(_fileSystem.GetFileSystemInfo(info.Path)) as Video;
+
+ if (video == null)
+ {
+ _logger.Error("Intro resolver returned null for {0}.", info.Path);
+ }
+ else
+ {
+ // Pull the saved db item that will include metadata
+ var dbItem = GetItemById(video.Id) as Video;
+
+ if (dbItem != null)
+ {
+ video = dbItem;
+ }
+ else
+ {
+ return null;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error resolving path {0}.", ex, info.Path);
+ }
+ }
+ else
+ {
+ _logger.Error("IntroProvider returned an IntroInfo with null Path and ItemId.");
+ }
+
+ return video;
+ }
+
+ /// <summary>
+ /// Sorts the specified sort by.
+ /// </summary>
+ /// <param name="items">The items.</param>
+ /// <param name="user">The user.</param>
+ /// <param name="sortBy">The sort by.</param>
+ /// <param name="sortOrder">The sort order.</param>
+ /// <returns>IEnumerable{BaseItem}.</returns>
+ public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<string> sortBy, SortOrder sortOrder)
+ {
+ var isFirst = true;
+
+ IOrderedEnumerable<BaseItem> orderedItems = null;
+
+ foreach (var orderBy in sortBy.Select(o => GetComparer(o, user)).Where(c => c != null))
+ {
+ if (isFirst)
+ {
+ orderedItems = sortOrder == SortOrder.Descending ? items.OrderByDescending(i => i, orderBy) : items.OrderBy(i => i, orderBy);
+ }
+ else
+ {
+ orderedItems = sortOrder == SortOrder.Descending ? orderedItems.ThenByDescending(i => i, orderBy) : orderedItems.ThenBy(i => i, orderBy);
+ }
+
+ isFirst = false;
+ }
+
+ return orderedItems ?? items;
+ }
+
+ /// <summary>
+ /// Gets the comparer.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="user">The user.</param>
+ /// <returns>IBaseItemComparer.</returns>
+ private IBaseItemComparer GetComparer(string name, User user)
+ {
+ var comparer = Comparers.FirstOrDefault(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase));
+
+ if (comparer != null)
+ {
+ // If it requires a user, create a new one, and assign the user
+ if (comparer is IUserBaseItemComparer)
+ {
+ var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType());
+
+ userComparer.User = user;
+ userComparer.UserManager = _userManager;
+ userComparer.UserDataRepository = _userDataRepository;
+
+ return userComparer;
+ }
+ }
+
+ return comparer;
+ }
+
+ /// <summary>
+ /// Creates the item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task CreateItem(BaseItem item, CancellationToken cancellationToken)
+ {
+ return CreateItems(new[] { item }, cancellationToken);
+ }
+
+ /// <summary>
+ /// Creates the items.
+ /// </summary>
+ /// <param name="items">The items.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task CreateItems(IEnumerable<BaseItem> items, CancellationToken cancellationToken)
+ {
+ var list = items.ToList();
+
+ await ItemRepository.SaveItems(list, cancellationToken).ConfigureAwait(false);
+
+ foreach (var item in list)
+ {
+ RegisterItem(item);
+ }
+
+ if (ItemAdded != null)
+ {
+ foreach (var item in list)
+ {
+ try
+ {
+ ItemAdded(this, new ItemChangeEventArgs { Item = item });
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error in ItemAdded event handler", ex);
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Updates the item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="updateReason">The update reason.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task UpdateItem(BaseItem item, ItemUpdateType updateReason, CancellationToken cancellationToken)
+ {
+ var locationType = item.LocationType;
+ if (locationType != LocationType.Remote && locationType != LocationType.Virtual)
+ {
+ await _providerManagerFactory().SaveMetadata(item, updateReason).ConfigureAwait(false);
+ }
+
+ item.DateLastSaved = DateTime.UtcNow;
+
+ var logName = item.LocationType == LocationType.Remote ? item.Name ?? item.Path : item.Path ?? item.Name;
+ _logger.Debug("Saving {0} to database.", logName);
+
+ await ItemRepository.SaveItem(item, cancellationToken).ConfigureAwait(false);
+
+ RegisterItem(item);
+
+ if (ItemUpdated != null)
+ {
+ try
+ {
+ ItemUpdated(this, new ItemChangeEventArgs
+ {
+ Item = item,
+ UpdateReason = updateReason
+ });
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error in ItemUpdated event handler", ex);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Reports the item removed.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ public void ReportItemRemoved(BaseItem item)
+ {
+ if (ItemRemoved != null)
+ {
+ try
+ {
+ ItemRemoved(this, new ItemChangeEventArgs { Item = item });
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error in ItemRemoved event handler", ex);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Retrieves the item.
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <returns>BaseItem.</returns>
+ public BaseItem RetrieveItem(Guid id)
+ {
+ return ItemRepository.RetrieveItem(id);
+ }
+
+ public IEnumerable<Folder> GetCollectionFolders(BaseItem item)
+ {
+ while (!(item.GetParent() is AggregateFolder) && item.GetParent() != null)
+ {
+ item = item.GetParent();
+ }
+
+ if (item == null)
+ {
+ return new List<Folder>();
+ }
+
+ return GetUserRootFolder().Children
+ .OfType<Folder>()
+ .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path, StringComparer.OrdinalIgnoreCase));
+ }
+
+ public LibraryOptions GetLibraryOptions(BaseItem item)
+ {
+ var collectionFolder = item as CollectionFolder;
+ if (collectionFolder == null)
+ {
+ collectionFolder = GetCollectionFolders(item)
+ .OfType<CollectionFolder>()
+ .FirstOrDefault();
+ }
+
+ var options = collectionFolder == null ? new LibraryOptions() : collectionFolder.GetLibraryOptions();
+
+ if (options.SchemaVersion < 3)
+ {
+ options.SaveLocalMetadata = ConfigurationManager.Configuration.SaveLocalMeta;
+ options.EnableInternetProviders = ConfigurationManager.Configuration.EnableInternetProviders;
+ }
+
+ if (options.SchemaVersion < 2)
+ {
+ var chapterOptions = ConfigurationManager.GetConfiguration<ChapterOptions>("chapters");
+ options.ExtractChapterImagesDuringLibraryScan = chapterOptions.ExtractDuringLibraryScan;
+
+ if (collectionFolder != null)
+ {
+ if (string.Equals(collectionFolder.CollectionType, "movies", StringComparison.OrdinalIgnoreCase))
+ {
+ options.EnableChapterImageExtraction = chapterOptions.EnableMovieChapterImageExtraction;
+ }
+ else if (string.Equals(collectionFolder.CollectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
+ {
+ options.EnableChapterImageExtraction = chapterOptions.EnableEpisodeChapterImageExtraction;
+ }
+ }
+ }
+
+ return options;
+ }
+
+ public string GetContentType(BaseItem item)
+ {
+ string configuredContentType = GetConfiguredContentType(item, false);
+ if (!string.IsNullOrWhiteSpace(configuredContentType))
+ {
+ return configuredContentType;
+ }
+ configuredContentType = GetConfiguredContentType(item, true);
+ if (!string.IsNullOrWhiteSpace(configuredContentType))
+ {
+ return configuredContentType;
+ }
+ return GetInheritedContentType(item);
+ }
+
+ public string GetInheritedContentType(BaseItem item)
+ {
+ var type = GetTopFolderContentType(item);
+
+ if (!string.IsNullOrWhiteSpace(type))
+ {
+ return type;
+ }
+
+ return item.GetParents()
+ .Select(GetConfiguredContentType)
+ .LastOrDefault(i => !string.IsNullOrWhiteSpace(i));
+ }
+
+ public string GetConfiguredContentType(BaseItem item)
+ {
+ return GetConfiguredContentType(item, false);
+ }
+
+ public string GetConfiguredContentType(string path)
+ {
+ return GetContentTypeOverride(path, false);
+ }
+
+ public string GetConfiguredContentType(BaseItem item, bool inheritConfiguredPath)
+ {
+ ICollectionFolder collectionFolder = item as ICollectionFolder;
+ if (collectionFolder != null)
+ {
+ return collectionFolder.CollectionType;
+ }
+ return GetContentTypeOverride(item.ContainingFolderPath, inheritConfiguredPath);
+ }
+
+ private string GetContentTypeOverride(string path, bool inherit)
+ {
+ var nameValuePair = ConfigurationManager.Configuration.ContentTypes.FirstOrDefault(i => string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase) || (inherit && !string.IsNullOrWhiteSpace(i.Name) && _fileSystem.ContainsSubPath(i.Name, path)));
+ if (nameValuePair != null)
+ {
+ return nameValuePair.Value;
+ }
+ return null;
+ }
+
+ private string GetTopFolderContentType(BaseItem item)
+ {
+ if (item == null)
+ {
+ return null;
+ }
+
+ while (!(item.GetParent() is AggregateFolder) && item.GetParent() != null)
+ {
+ item = item.GetParent();
+ }
+
+ return GetUserRootFolder().Children
+ .OfType<ICollectionFolder>()
+ .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path))
+ .Select(i => i.CollectionType)
+ .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i));
+ }
+
+ private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
+ //private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromMinutes(1);
+
+ public Task<UserView> GetNamedView(User user,
+ string name,
+ string viewType,
+ string sortName,
+ CancellationToken cancellationToken)
+ {
+ return GetNamedView(user, name, null, viewType, sortName, cancellationToken);
+ }
+
+ public async Task<UserView> GetNamedView(string name,
+ string viewType,
+ string sortName,
+ CancellationToken cancellationToken)
+ {
+ var path = Path.Combine(ConfigurationManager.ApplicationPaths.ItemsByNamePath, "views");
+
+ path = Path.Combine(path, _fileSystem.GetValidFilename(viewType));
+
+ var id = GetNewItemId(path + "_namedview_" + name, typeof(UserView));
+
+ var item = GetItemById(id) as UserView;
+
+ var refresh = false;
+
+ if (item == null || !string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase))
+ {
+ _fileSystem.CreateDirectory(path);
+
+ item = new UserView
+ {
+ Path = path,
+ Id = id,
+ DateCreated = DateTime.UtcNow,
+ Name = name,
+ ViewType = viewType,
+ ForcedSortName = sortName
+ };
+
+ await CreateItem(item, cancellationToken).ConfigureAwait(false);
+
+ refresh = true;
+ }
+
+ if (!refresh)
+ {
+ refresh = DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
+ }
+
+ if (!refresh && item.DisplayParentId != Guid.Empty)
+ {
+ var displayParent = GetItemById(item.DisplayParentId);
+ refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed;
+ }
+
+ if (refresh)
+ {
+ await item.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None).ConfigureAwait(false);
+ _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem)
+ {
+ // Not sure why this is necessary but need to figure it out
+ // View images are not getting utilized without this
+ ForceSave = true
+ });
+ }
+
+ return item;
+ }
+
+ public async Task<UserView> GetNamedView(User user,
+ string name,
+ string parentId,
+ string viewType,
+ string sortName,
+ CancellationToken cancellationToken)
+ {
+ var idValues = "38_namedview_" + name + user.Id.ToString("N") + (parentId ?? string.Empty) + (viewType ?? string.Empty);
+
+ var id = GetNewItemId(idValues, typeof(UserView));
+
+ var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N"));
+
+ var item = GetItemById(id) as UserView;
+
+ var isNew = false;
+
+ if (item == null)
+ {
+ _fileSystem.CreateDirectory(path);
+
+ item = new UserView
+ {
+ Path = path,
+ Id = id,
+ DateCreated = DateTime.UtcNow,
+ Name = name,
+ ViewType = viewType,
+ ForcedSortName = sortName,
+ UserId = user.Id
+ };
+
+ if (!string.IsNullOrWhiteSpace(parentId))
+ {
+ item.DisplayParentId = new Guid(parentId);
+ }
+
+ await CreateItem(item, cancellationToken).ConfigureAwait(false);
+
+ isNew = true;
+ }
+
+ var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
+
+ if (!refresh && item.DisplayParentId != Guid.Empty)
+ {
+ var displayParent = GetItemById(item.DisplayParentId);
+ refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed;
+ }
+
+ if (refresh)
+ {
+ _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem)
+ {
+ // Need to force save to increment DateLastSaved
+ ForceSave = true
+ });
+ }
+
+ return item;
+ }
+
+ public async Task<UserView> GetShadowView(BaseItem parent,
+ string viewType,
+ string sortName,
+ CancellationToken cancellationToken)
+ {
+ if (parent == null)
+ {
+ throw new ArgumentNullException("parent");
+ }
+
+ var name = parent.Name;
+ var parentId = parent.Id;
+
+ var idValues = "38_namedview_" + name + parentId + (viewType ?? string.Empty);
+
+ var id = GetNewItemId(idValues, typeof(UserView));
+
+ var path = parent.Path;
+
+ var item = GetItemById(id) as UserView;
+
+ var isNew = false;
+
+ if (item == null)
+ {
+ _fileSystem.CreateDirectory(path);
+
+ item = new UserView
+ {
+ Path = path,
+ Id = id,
+ DateCreated = DateTime.UtcNow,
+ Name = name,
+ ViewType = viewType,
+ ForcedSortName = sortName
+ };
+
+ item.DisplayParentId = parentId;
+
+ await CreateItem(item, cancellationToken).ConfigureAwait(false);
+
+ isNew = true;
+ }
+
+ var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
+
+ if (!refresh && item.DisplayParentId != Guid.Empty)
+ {
+ var displayParent = GetItemById(item.DisplayParentId);
+ refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed;
+ }
+
+ if (refresh)
+ {
+ _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem)
+ {
+ // Need to force save to increment DateLastSaved
+ ForceSave = true
+ });
+ }
+
+ return item;
+ }
+
+ public async Task<UserView> GetNamedView(string name,
+ string parentId,
+ string viewType,
+ string sortName,
+ string uniqueId,
+ CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentNullException("name");
+ }
+
+ var idValues = "37_namedview_" + name + (parentId ?? string.Empty) + (viewType ?? string.Empty);
+ if (!string.IsNullOrWhiteSpace(uniqueId))
+ {
+ idValues += uniqueId;
+ }
+
+ var id = GetNewItemId(idValues, typeof(UserView));
+
+ var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N"));
+
+ var item = GetItemById(id) as UserView;
+
+ var isNew = false;
+
+ if (item == null)
+ {
+ _fileSystem.CreateDirectory(path);
+
+ item = new UserView
+ {
+ Path = path,
+ Id = id,
+ DateCreated = DateTime.UtcNow,
+ Name = name,
+ ViewType = viewType,
+ ForcedSortName = sortName
+ };
+
+ if (!string.IsNullOrWhiteSpace(parentId))
+ {
+ item.DisplayParentId = new Guid(parentId);
+ }
+
+ await CreateItem(item, cancellationToken).ConfigureAwait(false);
+
+ isNew = true;
+ }
+
+ if (!string.Equals(viewType, item.ViewType, StringComparison.OrdinalIgnoreCase))
+ {
+ item.ViewType = viewType;
+ await item.UpdateToRepository(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
+ }
+
+ var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
+
+ if (!refresh && item.DisplayParentId != Guid.Empty)
+ {
+ var displayParent = GetItemById(item.DisplayParentId);
+ refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed;
+ }
+
+ if (refresh)
+ {
+ _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem)
+ {
+ // Need to force save to increment DateLastSaved
+ ForceSave = true
+ });
+ }
+
+ return item;
+ }
+
+ public bool IsVideoFile(string path, LibraryOptions libraryOptions)
+ {
+ var resolver = new VideoResolver(GetNamingOptions(libraryOptions), new PatternsLogger());
+ return resolver.IsVideoFile(path);
+ }
+
+ public bool IsVideoFile(string path)
+ {
+ return IsVideoFile(path, new LibraryOptions());
+ }
+
+ public bool IsAudioFile(string path, LibraryOptions libraryOptions)
+ {
+ var parser = new AudioFileParser(GetNamingOptions(libraryOptions));
+ return parser.IsAudioFile(path);
+ }
+
+ public bool IsAudioFile(string path)
+ {
+ return IsAudioFile(path, new LibraryOptions());
+ }
+
+ public int? GetSeasonNumberFromPath(string path)
+ {
+ return new SeasonPathParser(GetNamingOptions(), new RegexProvider()).Parse(path, true, true).SeasonNumber;
+ }
+
+ public bool FillMissingEpisodeNumbersFromPath(Episode episode)
+ {
+ var resolver = new EpisodeResolver(GetNamingOptions(),
+ new PatternsLogger());
+
+ var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd ||
+ episode.VideoType == VideoType.HdDvd;
+
+ var locationType = episode.LocationType;
+
+ var episodeInfo = locationType == LocationType.FileSystem || locationType == LocationType.Offline ?
+ resolver.Resolve(episode.Path, isFolder) :
+ new MediaBrowser.Naming.TV.EpisodeInfo();
+
+ if (episodeInfo == null)
+ {
+ episodeInfo = new MediaBrowser.Naming.TV.EpisodeInfo();
+ }
+
+ var changed = false;
+
+ if (episodeInfo.IsByDate)
+ {
+ if (episode.IndexNumber.HasValue)
+ {
+ episode.IndexNumber = null;
+ changed = true;
+ }
+
+ if (episode.IndexNumberEnd.HasValue)
+ {
+ episode.IndexNumberEnd = null;
+ changed = true;
+ }
+
+ if (!episode.PremiereDate.HasValue)
+ {
+ if (episodeInfo.Year.HasValue && episodeInfo.Month.HasValue && episodeInfo.Day.HasValue)
+ {
+ episode.PremiereDate = new DateTime(episodeInfo.Year.Value, episodeInfo.Month.Value, episodeInfo.Day.Value).ToUniversalTime();
+ }
+
+ if (episode.PremiereDate.HasValue)
+ {
+ changed = true;
+ }
+ }
+
+ if (!episode.ProductionYear.HasValue)
+ {
+ episode.ProductionYear = episodeInfo.Year;
+
+ if (episode.ProductionYear.HasValue)
+ {
+ changed = true;
+ }
+ }
+
+ if (!episode.ParentIndexNumber.HasValue)
+ {
+ var season = episode.Season;
+
+ if (season != null)
+ {
+ episode.ParentIndexNumber = season.IndexNumber;
+ }
+
+ if (episode.ParentIndexNumber.HasValue)
+ {
+ changed = true;
+ }
+ }
+ }
+ else
+ {
+ if (!episode.IndexNumber.HasValue)
+ {
+ episode.IndexNumber = episodeInfo.EpisodeNumber;
+
+ if (episode.IndexNumber.HasValue)
+ {
+ changed = true;
+ }
+ }
+
+ if (!episode.IndexNumberEnd.HasValue)
+ {
+ episode.IndexNumberEnd = episodeInfo.EndingEpsiodeNumber;
+
+ if (episode.IndexNumberEnd.HasValue)
+ {
+ changed = true;
+ }
+ }
+
+ if (!episode.ParentIndexNumber.HasValue)
+ {
+ episode.ParentIndexNumber = episodeInfo.SeasonNumber;
+
+ if (!episode.ParentIndexNumber.HasValue)
+ {
+ var season = episode.Season;
+
+ if (season != null)
+ {
+ episode.ParentIndexNumber = season.IndexNumber;
+ }
+ }
+
+ if (episode.ParentIndexNumber.HasValue)
+ {
+ changed = true;
+ }
+ }
+ }
+
+ return changed;
+ }
+
+ public NamingOptions GetNamingOptions()
+ {
+ return GetNamingOptions(new LibraryOptions());
+ }
+
+ public NamingOptions GetNamingOptions(LibraryOptions libraryOptions)
+ {
+ var options = new ExtendedNamingOptions();
+
+ // These cause apps to have problems
+ options.AudioFileExtensions.Remove(".m3u");
+ options.AudioFileExtensions.Remove(".wpl");
+
+ if (!libraryOptions.EnableArchiveMediaFiles)
+ {
+ options.AudioFileExtensions.Remove(".rar");
+ options.AudioFileExtensions.Remove(".zip");
+ }
+
+ if (!libraryOptions.EnableArchiveMediaFiles)
+ {
+ options.VideoFileExtensions.Remove(".rar");
+ options.VideoFileExtensions.Remove(".zip");
+ }
+
+ return options;
+ }
+
+ public ItemLookupInfo ParseName(string name)
+ {
+ var resolver = new VideoResolver(GetNamingOptions(), new PatternsLogger());
+
+ var result = resolver.CleanDateTime(name);
+ var cleanName = resolver.CleanString(result.Name);
+
+ return new ItemLookupInfo
+ {
+ Name = cleanName.Name,
+ Year = result.Year
+ };
+ }
+
+ public IEnumerable<Video> FindTrailers(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
+ {
+ var files = owner.DetectIsInMixedFolder() ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
+ .Where(i => string.Equals(i.Name, BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase))
+ .SelectMany(i => _fileSystem.GetFiles(i.FullName, false))
+ .ToList();
+
+ var videoListResolver = new VideoListResolver(GetNamingOptions(), new PatternsLogger());
+
+ var videos = videoListResolver.Resolve(fileSystemChildren);
+
+ var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files.First().Path, StringComparison.OrdinalIgnoreCase));
+
+ if (currentVideo != null)
+ {
+ files.AddRange(currentVideo.Extras.Where(i => string.Equals(i.ExtraType, "trailer", StringComparison.OrdinalIgnoreCase)).Select(i => _fileSystem.GetFileInfo(i.Path)));
+ }
+
+ var resolvers = new IItemResolver[]
+ {
+ new GenericVideoResolver<Trailer>(this)
+ };
+
+ return ResolvePaths(files, directoryService, null, new LibraryOptions(), null, resolvers)
+ .OfType<Trailer>()
+ .Select(video =>
+ {
+ // Try to retrieve it from the db. If we don't find it, use the resolved version
+ var dbItem = GetItemById(video.Id) as Trailer;
+
+ if (dbItem != null)
+ {
+ video = dbItem;
+ }
+
+ video.ExtraType = ExtraType.Trailer;
+ video.TrailerTypes = new List<TrailerType> { TrailerType.LocalTrailer };
+
+ return video;
+
+ // Sort them so that the list can be easily compared for changes
+ }).OrderBy(i => i.Path).ToList();
+ }
+
+ public IEnumerable<Video> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
+ {
+ var files = fileSystemChildren.Where(i => i.IsDirectory)
+ .Where(i => string.Equals(i.Name, "extras", StringComparison.OrdinalIgnoreCase) || string.Equals(i.Name, "specials", StringComparison.OrdinalIgnoreCase))
+ .SelectMany(i => _fileSystem.GetFiles(i.FullName, false))
+ .ToList();
+
+ var videoListResolver = new VideoListResolver(GetNamingOptions(), new PatternsLogger());
+
+ var videos = videoListResolver.Resolve(fileSystemChildren);
+
+ var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files.First().Path, StringComparison.OrdinalIgnoreCase));
+
+ if (currentVideo != null)
+ {
+ files.AddRange(currentVideo.Extras.Where(i => !string.Equals(i.ExtraType, "trailer", StringComparison.OrdinalIgnoreCase)).Select(i => _fileSystem.GetFileInfo(i.Path)));
+ }
+
+ return ResolvePaths(files, directoryService, null, new LibraryOptions(), null)
+ .OfType<Video>()
+ .Select(video =>
+ {
+ // Try to retrieve it from the db. If we don't find it, use the resolved version
+ var dbItem = GetItemById(video.Id) as Video;
+
+ if (dbItem != null)
+ {
+ video = dbItem;
+ }
+
+ SetExtraTypeFromFilename(video);
+
+ return video;
+
+ // Sort them so that the list can be easily compared for changes
+ }).OrderBy(i => i.Path).ToList();
+ }
+
+ public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
+ {
+ if (ownerItem != null)
+ {
+ var libraryOptions = GetLibraryOptions(ownerItem);
+ if (libraryOptions != null)
+ {
+ foreach (var pathInfo in libraryOptions.PathInfos)
+ {
+ if (string.IsNullOrWhiteSpace(pathInfo.NetworkPath))
+ {
+ continue;
+ }
+
+ var substitutionResult = SubstitutePathInternal(path, pathInfo.Path, pathInfo.NetworkPath);
+ if (substitutionResult.Item2)
+ {
+ return substitutionResult.Item1;
+ }
+ }
+ }
+ }
+
+ var metadataPath = ConfigurationManager.Configuration.MetadataPath;
+ var metadataNetworkPath = ConfigurationManager.Configuration.MetadataNetworkPath;
+
+ if (!string.IsNullOrWhiteSpace(metadataPath) && !string.IsNullOrWhiteSpace(metadataNetworkPath))
+ {
+ var metadataSubstitutionResult = SubstitutePathInternal(path, metadataPath, metadataNetworkPath);
+ if (metadataSubstitutionResult.Item2)
+ {
+ return metadataSubstitutionResult.Item1;
+ }
+ }
+
+ foreach (var map in ConfigurationManager.Configuration.PathSubstitutions)
+ {
+ var substitutionResult = SubstitutePathInternal(path, map.From, map.To);
+ if (substitutionResult.Item2)
+ {
+ return substitutionResult.Item1;
+ }
+ }
+
+ return path;
+ }
+
+ public string SubstitutePath(string path, string from, string to)
+ {
+ return SubstitutePathInternal(path, from, to).Item1;
+ }
+
+ private Tuple<string, bool> SubstitutePathInternal(string path, string from, string to)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ throw new ArgumentNullException("path");
+ }
+ if (string.IsNullOrWhiteSpace(from))
+ {
+ throw new ArgumentNullException("from");
+ }
+ if (string.IsNullOrWhiteSpace(to))
+ {
+ throw new ArgumentNullException("to");
+ }
+
+ from = from.Trim();
+ to = to.Trim();
+
+ var newPath = path.Replace(from, to, StringComparison.OrdinalIgnoreCase);
+ var changed = false;
+
+ if (!string.Equals(newPath, path))
+ {
+ if (to.IndexOf('/') != -1)
+ {
+ newPath = newPath.Replace('\\', '/');
+ }
+ else
+ {
+ newPath = newPath.Replace('/', '\\');
+ }
+
+ changed = true;
+ }
+
+ return new Tuple<string, bool>(newPath, changed);
+ }
+
+ private void SetExtraTypeFromFilename(Video item)
+ {
+ var resolver = new ExtraResolver(GetNamingOptions(), new PatternsLogger(), new RegexProvider());
+
+ var result = resolver.GetExtraInfo(item.Path);
+
+ if (string.Equals(result.ExtraType, "deletedscene", StringComparison.OrdinalIgnoreCase))
+ {
+ item.ExtraType = ExtraType.DeletedScene;
+ }
+ else if (string.Equals(result.ExtraType, "behindthescenes", StringComparison.OrdinalIgnoreCase))
+ {
+ item.ExtraType = ExtraType.BehindTheScenes;
+ }
+ else if (string.Equals(result.ExtraType, "interview", StringComparison.OrdinalIgnoreCase))
+ {
+ item.ExtraType = ExtraType.Interview;
+ }
+ else if (string.Equals(result.ExtraType, "scene", StringComparison.OrdinalIgnoreCase))
+ {
+ item.ExtraType = ExtraType.Scene;
+ }
+ else if (string.Equals(result.ExtraType, "sample", StringComparison.OrdinalIgnoreCase))
+ {
+ item.ExtraType = ExtraType.Sample;
+ }
+ else
+ {
+ item.ExtraType = ExtraType.Clip;
+ }
+ }
+
+ public List<PersonInfo> GetPeople(InternalPeopleQuery query)
+ {
+ return ItemRepository.GetPeople(query);
+ }
+
+ public List<PersonInfo> GetPeople(BaseItem item)
+ {
+ if (item.SupportsPeople)
+ {
+ var people = GetPeople(new InternalPeopleQuery
+ {
+ ItemId = item.Id
+ });
+
+ if (people.Count > 0)
+ {
+ return people;
+ }
+ }
+
+ return new List<PersonInfo>();
+ }
+
+ public List<Person> GetPeopleItems(InternalPeopleQuery query)
+ {
+ return ItemRepository.GetPeopleNames(query).Select(i =>
+ {
+ try
+ {
+ return GetPerson(i);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error getting person", ex);
+ return null;
+ }
+
+ }).Where(i => i != null).ToList();
+ }
+
+ public List<string> GetPeopleNames(InternalPeopleQuery query)
+ {
+ return ItemRepository.GetPeopleNames(query);
+ }
+
+ public Task UpdatePeople(BaseItem item, List<PersonInfo> people)
+ {
+ if (!item.SupportsPeople)
+ {
+ return Task.FromResult(true);
+ }
+
+ return ItemRepository.UpdatePeople(item.Id, people);
+ }
+
+ private readonly SemaphoreSlim _dynamicImageResourcePool = new SemaphoreSlim(1, 1);
+ public async Task<ItemImageInfo> ConvertImageToLocal(IHasImages item, ItemImageInfo image, int imageIndex)
+ {
+ foreach (var url in image.Path.Split('|'))
+ {
+ try
+ {
+ _logger.Debug("ConvertImageToLocal item {0} - image url: {1}", item.Id, url);
+
+ await _providerManagerFactory().SaveImage(item, url, _dynamicImageResourcePool, image.Type, imageIndex, CancellationToken.None).ConfigureAwait(false);
+
+ var newImage = item.GetImageInfo(image.Type, imageIndex);
+
+ if (newImage != null)
+ {
+ newImage.IsPlaceholder = image.IsPlaceholder;
+ }
+
+ await item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
+
+ return item.GetImageInfo(image.Type, imageIndex);
+ }
+ catch (HttpException ex)
+ {
+ if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
+ {
+ continue;
+ }
+ throw;
+ }
+ }
+
+ // Remove this image to prevent it from retrying over and over
+ item.RemoveImage(image);
+ await item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
+
+ throw new InvalidOperationException();
+ }
+
+ public void AddVirtualFolder(string name, string collectionType, LibraryOptions options, bool refreshLibrary)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentNullException("name");
+ }
+
+ name = _fileSystem.GetValidFilename(name);
+
+ var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+
+ var virtualFolderPath = Path.Combine(rootFolderPath, name);
+ while (_fileSystem.DirectoryExists(virtualFolderPath))
+ {
+ name += "1";
+ virtualFolderPath = Path.Combine(rootFolderPath, name);
+ }
+
+ var mediaPathInfos = options.PathInfos;
+ if (mediaPathInfos != null)
+ {
+ var invalidpath = mediaPathInfos.FirstOrDefault(i => !_fileSystem.DirectoryExists(i.Path));
+ if (invalidpath != null)
+ {
+ throw new ArgumentException("The specified path does not exist: " + invalidpath.Path + ".");
+ }
+ }
+
+ _libraryMonitorFactory().Stop();
+
+ try
+ {
+ _fileSystem.CreateDirectory(virtualFolderPath);
+
+ if (!string.IsNullOrEmpty(collectionType))
+ {
+ var path = Path.Combine(virtualFolderPath, collectionType + ".collection");
+
+ _fileSystem.WriteAllBytes(path, new byte[] {});
+ }
+
+ CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
+
+ if (mediaPathInfos != null)
+ {
+ foreach (var path in mediaPathInfos)
+ {
+ AddMediaPathInternal(name, path, false);
+ }
+ }
+ }
+ finally
+ {
+ Task.Run(() =>
+ {
+ // No need to start if scanning the library because it will handle it
+ if (refreshLibrary)
+ {
+ ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
+ }
+ else
+ {
+ // Need to add a delay here or directory watchers may still pick up the changes
+ var task = Task.Delay(1000);
+ // Have to block here to allow exceptions to bubble
+ Task.WaitAll(task);
+
+ _libraryMonitorFactory().Start();
+ }
+ });
+ }
+ }
+
+ private bool ValidateNetworkPath(string path)
+ {
+ //if (Environment.OSVersion.Platform == PlatformID.Win32NT)
+ //{
+ // // We can't validate protocol-based paths, so just allow them
+ // if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) == -1)
+ // {
+ // return _fileSystem.DirectoryExists(path);
+ // }
+ //}
+
+ // Without native support for unc, we cannot validate this when running under mono
+ return true;
+ }
+
+ private const string ShortcutFileExtension = ".mblink";
+ public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
+ {
+ AddMediaPathInternal(virtualFolderName, pathInfo, true);
+ }
+
+ private void AddMediaPathInternal(string virtualFolderName, MediaPathInfo pathInfo, bool saveLibraryOptions)
+ {
+ if (pathInfo == null)
+ {
+ throw new ArgumentNullException("path");
+ }
+
+ var path = pathInfo.Path;
+
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ throw new ArgumentNullException("path");
+ }
+
+ if (!_fileSystem.DirectoryExists(path))
+ {
+ throw new FileNotFoundException("The path does not exist.");
+ }
+
+ if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
+ {
+ throw new FileNotFoundException("The network path does not exist.");
+ }
+
+ var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+ var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
+
+ var shortcutFilename = _fileSystem.GetFileNameWithoutExtension(path);
+
+ var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
+
+ while (_fileSystem.FileExists(lnk))
+ {
+ shortcutFilename += "1";
+ lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
+ }
+
+ _fileSystem.CreateShortcut(lnk, path);
+
+ RemoveContentTypeOverrides(path);
+
+ if (saveLibraryOptions)
+ {
+ var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
+
+ var list = libraryOptions.PathInfos.ToList();
+ list.Add(pathInfo);
+ libraryOptions.PathInfos = list.ToArray();
+
+ SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
+
+ CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
+ }
+ }
+
+ public void UpdateMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
+ {
+ if (pathInfo == null)
+ {
+ throw new ArgumentNullException("path");
+ }
+
+ if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
+ {
+ throw new FileNotFoundException("The network path does not exist.");
+ }
+
+ var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+ var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
+
+ var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
+
+ SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
+
+ var list = libraryOptions.PathInfos.ToList();
+ foreach (var originalPathInfo in list)
+ {
+ if (string.Equals(pathInfo.Path, originalPathInfo.Path, StringComparison.Ordinal))
+ {
+ originalPathInfo.NetworkPath = pathInfo.NetworkPath;
+ break;
+ }
+ }
+
+ libraryOptions.PathInfos = list.ToArray();
+
+ CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
+ }
+
+ private void SyncLibraryOptionsToLocations(string virtualFolderPath, LibraryOptions options)
+ {
+ var topLibraryFolders = GetUserRootFolder().Children.ToList();
+ var info = GetVirtualFolderInfo(virtualFolderPath, topLibraryFolders);
+
+ if (info.Locations.Count > 0 && info.Locations.Count != options.PathInfos.Length)
+ {
+ var list = options.PathInfos.ToList();
+
+ foreach (var location in info.Locations)
+ {
+ if (!list.Any(i => string.Equals(i.Path, location, StringComparison.Ordinal)))
+ {
+ list.Add(new MediaPathInfo
+ {
+ Path = location
+ });
+ }
+ }
+
+ options.PathInfos = list.ToArray();
+ }
+ }
+
+ public void RemoveVirtualFolder(string name, bool refreshLibrary)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentNullException("name");
+ }
+
+ var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+
+ var path = Path.Combine(rootFolderPath, name);
+
+ if (!_fileSystem.DirectoryExists(path))
+ {
+ throw new FileNotFoundException("The media folder does not exist");
+ }
+
+ _libraryMonitorFactory().Stop();
+
+ try
+ {
+ _fileSystem.DeleteDirectory(path, true);
+ }
+ finally
+ {
+ Task.Run(() =>
+ {
+ // No need to start if scanning the library because it will handle it
+ if (refreshLibrary)
+ {
+ ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
+ }
+ else
+ {
+ // Need to add a delay here or directory watchers may still pick up the changes
+ var task = Task.Delay(1000);
+ // Have to block here to allow exceptions to bubble
+ Task.WaitAll(task);
+
+ _libraryMonitorFactory().Start();
+ }
+ });
+ }
+ }
+
+ private void RemoveContentTypeOverrides(string path)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ throw new ArgumentNullException("path");
+ }
+
+ var removeList = new List<NameValuePair>();
+
+ foreach (var contentType in ConfigurationManager.Configuration.ContentTypes)
+ {
+ if (string.Equals(path, contentType.Name, StringComparison.OrdinalIgnoreCase)
+ || _fileSystem.ContainsSubPath(path, contentType.Name))
+ {
+ removeList.Add(contentType);
+ }
+ }
+
+ if (removeList.Count > 0)
+ {
+ ConfigurationManager.Configuration.ContentTypes = ConfigurationManager.Configuration.ContentTypes
+ .Except(removeList)
+ .ToArray();
+
+ ConfigurationManager.SaveConfiguration();
+ }
+ }
+
+ public void RemoveMediaPath(string virtualFolderName, string mediaPath)
+ {
+ if (string.IsNullOrWhiteSpace(mediaPath))
+ {
+ throw new ArgumentNullException("mediaPath");
+ }
+
+ var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+ var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
+
+ if (!_fileSystem.DirectoryExists(virtualFolderPath))
+ {
+ throw new FileNotFoundException(string.Format("The media collection {0} does not exist", virtualFolderName));
+ }
+
+ var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
+ .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
+ .FirstOrDefault(f => _fileSystem.ResolveShortcut(f).Equals(mediaPath, StringComparison.OrdinalIgnoreCase));
+
+ if (!string.IsNullOrEmpty(shortcut))
+ {
+ _fileSystem.DeleteFile(shortcut);
+ }
+
+ var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
+
+ libraryOptions.PathInfos = libraryOptions
+ .PathInfos
+ .Where(i => !string.Equals(i.Path, mediaPath, StringComparison.Ordinal))
+ .ToArray();
+
+ CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
+ }
+ }
+} \ No newline at end of file
diff --git a/Emby.Server.Implementations/Library/LocalTrailerPostScanTask.cs b/Emby.Server.Implementations/Library/LocalTrailerPostScanTask.cs
new file mode 100644
index 000000000..7424ed5e5
--- /dev/null
+++ b/Emby.Server.Implementations/Library/LocalTrailerPostScanTask.cs
@@ -0,0 +1,102 @@
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+
+namespace Emby.Server.Implementations.Library
+{
+ public class LocalTrailerPostScanTask : ILibraryPostScanTask
+ {
+ private readonly ILibraryManager _libraryManager;
+ private readonly IChannelManager _channelManager;
+
+ public LocalTrailerPostScanTask(ILibraryManager libraryManager, IChannelManager channelManager)
+ {
+ _libraryManager = libraryManager;
+ _channelManager = channelManager;
+ }
+
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var items = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = new[] { typeof(BoxSet).Name, typeof(Game).Name, typeof(Movie).Name, typeof(Series).Name },
+ Recursive = true
+
+ }).OfType<IHasTrailers>().ToList();
+
+ var trailerTypes = Enum.GetNames(typeof(TrailerType))
+ .Select(i => (TrailerType)Enum.Parse(typeof(TrailerType), i, true))
+ .Except(new[] { TrailerType.LocalTrailer })
+ .ToArray();
+
+ var trailers = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = new[] { typeof(Trailer).Name },
+ TrailerTypes = trailerTypes,
+ Recursive = true
+
+ }).ToArray();
+
+ var numComplete = 0;
+
+ foreach (var item in items)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ await AssignTrailers(item, trailers).ConfigureAwait(false);
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= items.Count;
+ progress.Report(percent * 100);
+ }
+
+ progress.Report(100);
+ }
+
+ private async Task AssignTrailers(IHasTrailers item, BaseItem[] channelTrailers)
+ {
+ if (item is Game)
+ {
+ return;
+ }
+
+ var imdbId = item.GetProviderId(MetadataProviders.Imdb);
+ var tmdbId = item.GetProviderId(MetadataProviders.Tmdb);
+
+ var trailers = channelTrailers.Where(i =>
+ {
+ if (!string.IsNullOrWhiteSpace(imdbId) &&
+ string.Equals(imdbId, i.GetProviderId(MetadataProviders.Imdb), StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ if (!string.IsNullOrWhiteSpace(tmdbId) &&
+ string.Equals(tmdbId, i.GetProviderId(MetadataProviders.Tmdb), StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ return false;
+ });
+
+ var trailerIds = trailers.Select(i => i.Id)
+ .ToList();
+
+ if (!trailerIds.SequenceEqual(item.RemoteTrailerIds))
+ {
+ item.RemoteTrailerIds = trailerIds;
+
+ var baseItem = (BaseItem)item;
+ await baseItem.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None)
+ .ConfigureAwait(false);
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
new file mode 100644
index 000000000..93c406ebc
--- /dev/null
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -0,0 +1,651 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Serialization;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Threading;
+
+namespace Emby.Server.Implementations.Library
+{
+ public class MediaSourceManager : IMediaSourceManager, IDisposable
+ {
+ private readonly IItemRepository _itemRepo;
+ private readonly IUserManager _userManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IJsonSerializer _jsonSerializer;
+ private readonly IFileSystem _fileSystem;
+
+ private IMediaSourceProvider[] _providers;
+ private readonly ILogger _logger;
+ private readonly IUserDataManager _userDataManager;
+ private readonly ITimerFactory _timerFactory;
+
+ public MediaSourceManager(IItemRepository itemRepo, IUserManager userManager, ILibraryManager libraryManager, ILogger logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem, IUserDataManager userDataManager, ITimerFactory timerFactory)
+ {
+ _itemRepo = itemRepo;
+ _userManager = userManager;
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _jsonSerializer = jsonSerializer;
+ _fileSystem = fileSystem;
+ _userDataManager = userDataManager;
+ _timerFactory = timerFactory;
+ }
+
+ public void AddParts(IEnumerable<IMediaSourceProvider> providers)
+ {
+ _providers = providers.ToArray();
+ }
+
+ public IEnumerable<MediaStream> GetMediaStreams(MediaStreamQuery query)
+ {
+ var list = _itemRepo.GetMediaStreams(query)
+ .ToList();
+
+ foreach (var stream in list)
+ {
+ stream.SupportsExternalStream = StreamSupportsExternalStream(stream);
+ }
+
+ return list;
+ }
+
+ private bool StreamSupportsExternalStream(MediaStream stream)
+ {
+ if (stream.IsExternal)
+ {
+ return true;
+ }
+
+ if (stream.IsTextSubtitleStream)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ public IEnumerable<MediaStream> GetMediaStreams(string mediaSourceId)
+ {
+ var list = GetMediaStreams(new MediaStreamQuery
+ {
+ ItemId = new Guid(mediaSourceId)
+ });
+
+ return GetMediaStreamsForItem(list);
+ }
+
+ public IEnumerable<MediaStream> GetMediaStreams(Guid itemId)
+ {
+ var list = GetMediaStreams(new MediaStreamQuery
+ {
+ ItemId = itemId
+ });
+
+ return GetMediaStreamsForItem(list);
+ }
+
+ private IEnumerable<MediaStream> GetMediaStreamsForItem(IEnumerable<MediaStream> streams)
+ {
+ var list = streams.ToList();
+
+ var subtitleStreams = list
+ .Where(i => i.Type == MediaStreamType.Subtitle)
+ .ToList();
+
+ if (subtitleStreams.Count > 0)
+ {
+ foreach (var subStream in subtitleStreams)
+ {
+ subStream.SupportsExternalStream = StreamSupportsExternalStream(subStream);
+ }
+ }
+
+ return list;
+ }
+
+ public async Task<IEnumerable<MediaSourceInfo>> GetPlayackMediaSources(string id, string userId, bool enablePathSubstitution, string[] supportedLiveMediaTypes, CancellationToken cancellationToken)
+ {
+ var item = _libraryManager.GetItemById(id);
+
+ var hasMediaSources = (IHasMediaSources)item;
+ User user = null;
+
+ if (!string.IsNullOrWhiteSpace(userId))
+ {
+ user = _userManager.GetUserById(userId);
+ }
+
+ var mediaSources = GetStaticMediaSources(hasMediaSources, enablePathSubstitution, user);
+ var dynamicMediaSources = await GetDynamicMediaSources(hasMediaSources, cancellationToken).ConfigureAwait(false);
+
+ var list = new List<MediaSourceInfo>();
+
+ list.AddRange(mediaSources);
+
+ foreach (var source in dynamicMediaSources)
+ {
+ if (user != null)
+ {
+ SetUserProperties(hasMediaSources, source, user);
+ }
+ if (source.Protocol == MediaProtocol.File)
+ {
+ // TODO: Path substitution
+ if (!_fileSystem.FileExists(source.Path))
+ {
+ source.SupportsDirectStream = false;
+ }
+ }
+ else if (source.Protocol == MediaProtocol.Http)
+ {
+ // TODO: Allow this when the source is plain http, e.g. not HLS or Mpeg Dash
+ source.SupportsDirectStream = false;
+ }
+ else
+ {
+ source.SupportsDirectStream = false;
+ }
+
+ list.Add(source);
+ }
+
+ foreach (var source in list)
+ {
+ if (user != null)
+ {
+ if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+ {
+ if (!user.Policy.EnableAudioPlaybackTranscoding)
+ {
+ source.SupportsTranscoding = false;
+ }
+ }
+ }
+ }
+
+ return SortMediaSources(list).Where(i => i.Type != MediaSourceType.Placeholder);
+ }
+
+ private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(IHasMediaSources item, CancellationToken cancellationToken)
+ {
+ var tasks = _providers.Select(i => GetDynamicMediaSources(item, i, cancellationToken));
+ var results = await Task.WhenAll(tasks).ConfigureAwait(false);
+
+ return results.SelectMany(i => i.ToList());
+ }
+
+ private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(IHasMediaSources item, IMediaSourceProvider provider, CancellationToken cancellationToken)
+ {
+ try
+ {
+ var sources = await provider.GetMediaSources(item, cancellationToken).ConfigureAwait(false);
+ var list = sources.ToList();
+
+ foreach (var mediaSource in list)
+ {
+ SetKeyProperties(provider, mediaSource);
+ }
+
+ return list;
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error getting media sources", ex);
+ return new List<MediaSourceInfo>();
+ }
+ }
+
+ private void SetKeyProperties(IMediaSourceProvider provider, MediaSourceInfo mediaSource)
+ {
+ var prefix = provider.GetType().FullName.GetMD5().ToString("N") + LiveStreamIdDelimeter;
+
+ if (!string.IsNullOrWhiteSpace(mediaSource.OpenToken) && !mediaSource.OpenToken.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ mediaSource.OpenToken = prefix + mediaSource.OpenToken;
+ }
+
+ if (!string.IsNullOrWhiteSpace(mediaSource.LiveStreamId) && !mediaSource.LiveStreamId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ mediaSource.LiveStreamId = prefix + mediaSource.LiveStreamId;
+ }
+ }
+
+ public async Task<MediaSourceInfo> GetMediaSource(IHasMediaSources item, string mediaSourceId, string liveStreamId, bool enablePathSubstitution, CancellationToken cancellationToken)
+ {
+ if (!string.IsNullOrWhiteSpace(liveStreamId))
+ {
+ return await GetLiveStream(liveStreamId, cancellationToken).ConfigureAwait(false);
+ }
+ //await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ //try
+ //{
+ // var stream = _openStreams.Values.FirstOrDefault(i => string.Equals(i.MediaSource.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
+
+ // if (stream != null)
+ // {
+ // return stream.MediaSource;
+ // }
+ //}
+ //finally
+ //{
+ // _liveStreamSemaphore.Release();
+ //}
+
+ var sources = await GetPlayackMediaSources(item.Id.ToString("N"), null, enablePathSubstitution, new[] { MediaType.Audio, MediaType.Video },
+ CancellationToken.None).ConfigureAwait(false);
+
+ return sources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
+ }
+
+ public IEnumerable<MediaSourceInfo> GetStaticMediaSources(IHasMediaSources item, bool enablePathSubstitution, User user = null)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ if (!(item is Video))
+ {
+ return item.GetMediaSources(enablePathSubstitution);
+ }
+
+ var sources = item.GetMediaSources(enablePathSubstitution).ToList();
+
+ if (user != null)
+ {
+ foreach (var source in sources)
+ {
+ SetUserProperties(item, source, user);
+ }
+ }
+
+ return sources;
+ }
+
+ private void SetUserProperties(IHasUserData item, MediaSourceInfo source, User user)
+ {
+ var userData = item == null ? new UserItemData() : _userDataManager.GetUserData(user, item);
+
+ var allowRememberingSelection = item == null || item.EnableRememberingTrackSelections;
+
+ SetDefaultAudioStreamIndex(source, userData, user, allowRememberingSelection);
+ SetDefaultSubtitleStreamIndex(source, userData, user, allowRememberingSelection);
+ }
+
+ private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
+ {
+ if (userData.SubtitleStreamIndex.HasValue && user.Configuration.RememberSubtitleSelections && user.Configuration.SubtitleMode != SubtitlePlaybackMode.None && allowRememberingSelection)
+ {
+ var index = userData.SubtitleStreamIndex.Value;
+ // Make sure the saved index is still valid
+ if (index == -1 || source.MediaStreams.Any(i => i.Type == MediaStreamType.Subtitle && i.Index == index))
+ {
+ source.DefaultSubtitleStreamIndex = index;
+ return;
+ }
+ }
+
+ var preferredSubs = string.IsNullOrEmpty(user.Configuration.SubtitleLanguagePreference)
+ ? new List<string>() : new List<string> { user.Configuration.SubtitleLanguagePreference };
+
+ var defaultAudioIndex = source.DefaultAudioStreamIndex;
+ var audioLangage = defaultAudioIndex == null
+ ? null
+ : source.MediaStreams.Where(i => i.Type == MediaStreamType.Audio && i.Index == defaultAudioIndex).Select(i => i.Language).FirstOrDefault();
+
+ source.DefaultSubtitleStreamIndex = MediaStreamSelector.GetDefaultSubtitleStreamIndex(source.MediaStreams,
+ preferredSubs,
+ user.Configuration.SubtitleMode,
+ audioLangage);
+
+ MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs,
+ user.Configuration.SubtitleMode, audioLangage);
+ }
+
+ private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
+ {
+ if (userData.AudioStreamIndex.HasValue && user.Configuration.RememberAudioSelections && allowRememberingSelection)
+ {
+ var index = userData.AudioStreamIndex.Value;
+ // Make sure the saved index is still valid
+ if (source.MediaStreams.Any(i => i.Type == MediaStreamType.Audio && i.Index == index))
+ {
+ source.DefaultAudioStreamIndex = index;
+ return;
+ }
+ }
+
+ var preferredAudio = string.IsNullOrEmpty(user.Configuration.AudioLanguagePreference)
+ ? new string[] { }
+ : new[] { user.Configuration.AudioLanguagePreference };
+
+ source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.Configuration.PlayDefaultAudioTrack);
+ }
+
+ private IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
+ {
+ return sources.OrderBy(i =>
+ {
+ if (i.VideoType.HasValue && i.VideoType.Value == VideoType.VideoFile)
+ {
+ return 0;
+ }
+
+ return 1;
+
+ }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0)
+ .ThenByDescending(i =>
+ {
+ var stream = i.VideoStream;
+
+ return stream == null || stream.Width == null ? 0 : stream.Width.Value;
+ })
+ .ToList();
+ }
+
+ private readonly Dictionary<string, LiveStreamInfo> _openStreams = new Dictionary<string, LiveStreamInfo>(StringComparer.OrdinalIgnoreCase);
+ private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
+
+ public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, bool enableAutoClose, CancellationToken cancellationToken)
+ {
+ await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ try
+ {
+ var tuple = GetProvider(request.OpenToken);
+ var provider = tuple.Item1;
+
+ var mediaSourceTuple = await provider.OpenMediaSource(tuple.Item2, cancellationToken).ConfigureAwait(false);
+
+ var mediaSource = mediaSourceTuple.Item1;
+
+ if (string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
+ {
+ throw new InvalidOperationException(string.Format("{0} returned null LiveStreamId", provider.GetType().Name));
+ }
+
+ SetKeyProperties(provider, mediaSource);
+
+ var info = new LiveStreamInfo
+ {
+ Date = DateTime.UtcNow,
+ EnableCloseTimer = enableAutoClose,
+ Id = mediaSource.LiveStreamId,
+ MediaSource = mediaSource,
+ DirectStreamProvider = mediaSourceTuple.Item2
+ };
+
+ _openStreams[mediaSource.LiveStreamId] = info;
+
+ if (enableAutoClose)
+ {
+ StartCloseTimer();
+ }
+
+ var json = _jsonSerializer.SerializeToString(mediaSource);
+ _logger.Debug("Live stream opened: " + json);
+ var clone = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json);
+
+ if (!string.IsNullOrWhiteSpace(request.UserId))
+ {
+ var user = _userManager.GetUserById(request.UserId);
+ var item = string.IsNullOrWhiteSpace(request.ItemId)
+ ? null
+ : _libraryManager.GetItemById(request.ItemId);
+ SetUserProperties(item, clone, user);
+ }
+
+ return new LiveStreamResponse
+ {
+ MediaSource = clone
+ };
+ }
+ finally
+ {
+ _liveStreamSemaphore.Release();
+ }
+ }
+
+ public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrWhiteSpace(id))
+ {
+ throw new ArgumentNullException("id");
+ }
+
+ _logger.Debug("Getting already opened live stream {0}", id);
+
+ await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ try
+ {
+ LiveStreamInfo info;
+ if (_openStreams.TryGetValue(id, out info))
+ {
+ return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info.DirectStreamProvider);
+ }
+ else
+ {
+ throw new ResourceNotFoundException();
+ }
+ }
+ finally
+ {
+ _liveStreamSemaphore.Release();
+ }
+ }
+
+ public async Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken)
+ {
+ var result = await GetLiveStreamWithDirectStreamProvider(id, cancellationToken).ConfigureAwait(false);
+ return result.Item1;
+ }
+
+ public async Task PingLiveStream(string id, CancellationToken cancellationToken)
+ {
+ await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ try
+ {
+ LiveStreamInfo info;
+ if (_openStreams.TryGetValue(id, out info))
+ {
+ info.Date = DateTime.UtcNow;
+ }
+ else
+ {
+ _logger.Error("Failed to ping live stream {0}", id);
+ }
+ }
+ finally
+ {
+ _liveStreamSemaphore.Release();
+ }
+ }
+
+ private async Task CloseLiveStreamWithProvider(IMediaSourceProvider provider, string streamId)
+ {
+ _logger.Info("Closing live stream {0} with provider {1}", streamId, provider.GetType().Name);
+
+ try
+ {
+ await provider.CloseMediaSource(streamId).ConfigureAwait(false);
+ }
+ catch (NotImplementedException)
+ {
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error closing live stream {0}", ex, streamId);
+ }
+ }
+
+ public async Task CloseLiveStream(string id)
+ {
+ if (string.IsNullOrWhiteSpace(id))
+ {
+ throw new ArgumentNullException("id");
+ }
+
+ await _liveStreamSemaphore.WaitAsync().ConfigureAwait(false);
+
+ try
+ {
+ LiveStreamInfo current;
+
+ if (_openStreams.TryGetValue(id, out current))
+ {
+ _openStreams.Remove(id);
+ current.Closed = true;
+
+ if (current.MediaSource.RequiresClosing)
+ {
+ var tuple = GetProvider(id);
+
+ await CloseLiveStreamWithProvider(tuple.Item1, tuple.Item2).ConfigureAwait(false);
+ }
+
+ if (_openStreams.Count == 0)
+ {
+ StopCloseTimer();
+ }
+ }
+ }
+ finally
+ {
+ _liveStreamSemaphore.Release();
+ }
+ }
+
+ // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
+ private const char LiveStreamIdDelimeter = '_';
+
+ private Tuple<IMediaSourceProvider, string> GetProvider(string key)
+ {
+ if (string.IsNullOrWhiteSpace(key))
+ {
+ throw new ArgumentException("key");
+ }
+
+ var keys = key.Split(new[] { LiveStreamIdDelimeter }, 2);
+
+ var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N"), keys[0], StringComparison.OrdinalIgnoreCase));
+
+ var splitIndex = key.IndexOf(LiveStreamIdDelimeter);
+ var keyId = key.Substring(splitIndex + 1);
+
+ return new Tuple<IMediaSourceProvider, string>(provider, keyId);
+ }
+
+ private ITimer _closeTimer;
+ private readonly TimeSpan _openStreamMaxAge = TimeSpan.FromSeconds(180);
+
+ private void StartCloseTimer()
+ {
+ StopCloseTimer();
+
+ _closeTimer = _timerFactory.Create(CloseTimerCallback, null, _openStreamMaxAge, _openStreamMaxAge);
+ }
+
+ private void StopCloseTimer()
+ {
+ var timer = _closeTimer;
+
+ if (timer != null)
+ {
+ _closeTimer = null;
+ timer.Dispose();
+ }
+ }
+
+ private async void CloseTimerCallback(object state)
+ {
+ List<LiveStreamInfo> infos;
+ await _liveStreamSemaphore.WaitAsync().ConfigureAwait(false);
+
+ try
+ {
+ infos = _openStreams
+ .Values
+ .Where(i => i.EnableCloseTimer && DateTime.UtcNow - i.Date > _openStreamMaxAge)
+ .ToList();
+ }
+ finally
+ {
+ _liveStreamSemaphore.Release();
+ }
+
+ foreach (var info in infos)
+ {
+ if (!info.Closed)
+ {
+ try
+ {
+ await CloseLiveStream(info.Id).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error closing media source", ex);
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ StopCloseTimer();
+ Dispose(true);
+ }
+
+ private readonly object _disposeLock = new object();
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources.
+ /// </summary>
+ /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool dispose)
+ {
+ if (dispose)
+ {
+ lock (_disposeLock)
+ {
+ foreach (var key in _openStreams.Keys.ToList())
+ {
+ var task = CloseLiveStream(key);
+
+ Task.WaitAll(task);
+ }
+ }
+ }
+ }
+
+ private class LiveStreamInfo
+ {
+ public DateTime Date;
+ public bool EnableCloseTimer;
+ public string Id;
+ public bool Closed;
+ public MediaSourceInfo MediaSource;
+ public IDirectStreamProvider DirectStreamProvider;
+ }
+ }
+} \ No newline at end of file
diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs
new file mode 100644
index 000000000..7669dd0bf
--- /dev/null
+++ b/Emby.Server.Implementations/Library/MusicManager.cs
@@ -0,0 +1,157 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Playlists;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Emby.Server.Implementations.Library
+{
+ public class MusicManager : IMusicManager
+ {
+ private readonly ILibraryManager _libraryManager;
+
+ public MusicManager(ILibraryManager libraryManager)
+ {
+ _libraryManager = libraryManager;
+ }
+
+ public IEnumerable<Audio> GetInstantMixFromSong(Audio item, User user)
+ {
+ var list = new List<Audio>
+ {
+ item
+ };
+
+ return list.Concat(GetInstantMixFromGenres(item.Genres, user));
+ }
+
+ public IEnumerable<Audio> GetInstantMixFromArtist(MusicArtist artist, User user)
+ {
+ var genres = user.RootFolder
+ .GetRecursiveChildren(user, new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = new[] { typeof(Audio).Name }
+ })
+ .Cast<Audio>()
+ .Where(i => i.HasAnyArtist(artist.Name))
+ .SelectMany(i => i.Genres)
+ .Concat(artist.Genres)
+ .Distinct(StringComparer.OrdinalIgnoreCase);
+
+ return GetInstantMixFromGenres(genres, user);
+ }
+
+ public IEnumerable<Audio> GetInstantMixFromAlbum(MusicAlbum item, User user)
+ {
+ var genres = item
+ .GetRecursiveChildren(user, new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = new[] { typeof(Audio).Name }
+ })
+ .Cast<Audio>()
+ .SelectMany(i => i.Genres)
+ .Concat(item.Genres)
+ .DistinctNames();
+
+ return GetInstantMixFromGenres(genres, user);
+ }
+
+ public IEnumerable<Audio> GetInstantMixFromFolder(Folder item, User user)
+ {
+ var genres = item
+ .GetRecursiveChildren(user, new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = new[] {typeof(Audio).Name}
+ })
+ .Cast<Audio>()
+ .SelectMany(i => i.Genres)
+ .Concat(item.Genres)
+ .DistinctNames();
+
+ return GetInstantMixFromGenres(genres, user);
+ }
+
+ public IEnumerable<Audio> GetInstantMixFromPlaylist(Playlist item, User user)
+ {
+ var genres = item
+ .GetRecursiveChildren(user, new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = new[] { typeof(Audio).Name }
+ })
+ .Cast<Audio>()
+ .SelectMany(i => i.Genres)
+ .Concat(item.Genres)
+ .DistinctNames();
+
+ return GetInstantMixFromGenres(genres, user);
+ }
+
+ public IEnumerable<Audio> GetInstantMixFromGenres(IEnumerable<string> genres, User user)
+ {
+ var genreList = genres.ToList();
+
+ var inputItems = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = new[] { typeof(Audio).Name },
+
+ Genres = genreList.ToArray()
+
+ });
+
+ var genresDictionary = genreList.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
+
+ return inputItems
+ .Cast<Audio>()
+ .Select(i => new Tuple<Audio, int>(i, i.Genres.Count(genresDictionary.ContainsKey)))
+ .Where(i => i.Item2 > 0)
+ .OrderByDescending(i => i.Item2)
+ .ThenBy(i => Guid.NewGuid())
+ .Select(i => i.Item1)
+ .Take(100)
+ .OrderBy(i => Guid.NewGuid());
+ }
+
+ public IEnumerable<Audio> GetInstantMixFromItem(BaseItem item, User user)
+ {
+ var genre = item as MusicGenre;
+ if (genre != null)
+ {
+ return GetInstantMixFromGenres(new[] { item.Name }, user);
+ }
+
+ var playlist = item as Playlist;
+ if (playlist != null)
+ {
+ return GetInstantMixFromPlaylist(playlist, user);
+ }
+
+ var album = item as MusicAlbum;
+ if (album != null)
+ {
+ return GetInstantMixFromAlbum(album, user);
+ }
+
+ var artist = item as MusicArtist;
+ if (artist != null)
+ {
+ return GetInstantMixFromArtist(artist, user);
+ }
+
+ var song = item as Audio;
+ if (song != null)
+ {
+ return GetInstantMixFromSong(song, user);
+ }
+
+ var folder = item as Folder;
+ if (folder != null)
+ {
+ return GetInstantMixFromFolder(folder, user);
+ }
+
+ return new Audio[] { };
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs
new file mode 100644
index 000000000..28ed2f53c
--- /dev/null
+++ b/Emby.Server.Implementations/Library/PathExtensions.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Text.RegularExpressions;
+
+namespace Emby.Server.Implementations.Library
+{
+ public static class PathExtensions
+ {
+ /// <summary>
+ /// Gets the attribute value.
+ /// </summary>
+ /// <param name="str">The STR.</param>
+ /// <param name="attrib">The attrib.</param>
+ /// <returns>System.String.</returns>
+ /// <exception cref="System.ArgumentNullException">attrib</exception>
+ public static string GetAttributeValue(this string str, string attrib)
+ {
+ if (string.IsNullOrEmpty(str))
+ {
+ throw new ArgumentNullException("str");
+ }
+
+ if (string.IsNullOrEmpty(attrib))
+ {
+ throw new ArgumentNullException("attrib");
+ }
+
+ string srch = "[" + attrib + "=";
+ int start = str.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
+ if (start > -1)
+ {
+ start += srch.Length;
+ int end = str.IndexOf(']', start);
+ return str.Substring(start, end - start);
+ }
+ // for imdbid we also accept pattern matching
+ if (string.Equals(attrib, "imdbid", StringComparison.OrdinalIgnoreCase))
+ {
+ var m = Regex.Match(str, "tt\\d{7}", RegexOptions.IgnoreCase);
+ return m.Success ? m.Value : null;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs
new file mode 100644
index 000000000..1d3cacc1d
--- /dev/null
+++ b/Emby.Server.Implementations/Library/ResolverHelper.cs
@@ -0,0 +1,183 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using System;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Server.Implementations.Library
+{
+ /// <summary>
+ /// Class ResolverHelper
+ /// </summary>
+ public static class ResolverHelper
+ {
+ /// <summary>
+ /// Sets the initial item values.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="parent">The parent.</param>
+ /// <param name="fileSystem">The file system.</param>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="directoryService">The directory service.</param>
+ /// <exception cref="System.ArgumentException">Item must have a path</exception>
+ public static void SetInitialItemValues(BaseItem item, Folder parent, IFileSystem fileSystem, ILibraryManager libraryManager, IDirectoryService directoryService)
+ {
+ // This version of the below method has no ItemResolveArgs, so we have to require the path already being set
+ if (string.IsNullOrWhiteSpace(item.Path))
+ {
+ throw new ArgumentException("Item must have a Path");
+ }
+
+ // If the resolver didn't specify this
+ if (parent != null)
+ {
+ item.SetParent(parent);
+ }
+
+ item.Id = libraryManager.GetNewItemId(item.Path, item.GetType());
+
+ item.IsLocked = item.Path.IndexOf("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) != -1 ||
+ item.GetParents().Any(i => i.IsLocked);
+
+ // Make sure DateCreated and DateModified have values
+ var fileInfo = directoryService.GetFile(item.Path);
+ SetDateCreated(item, fileSystem, fileInfo);
+
+ EnsureName(item, fileInfo);
+ }
+
+ /// <summary>
+ /// Sets the initial item values.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="args">The args.</param>
+ /// <param name="fileSystem">The file system.</param>
+ /// <param name="libraryManager">The library manager.</param>
+ public static void SetInitialItemValues(BaseItem item, ItemResolveArgs args, IFileSystem fileSystem, ILibraryManager libraryManager)
+ {
+ // If the resolver didn't specify this
+ if (string.IsNullOrEmpty(item.Path))
+ {
+ item.Path = args.Path;
+ }
+
+ // If the resolver didn't specify this
+ if (args.Parent != null)
+ {
+ item.SetParent(args.Parent);
+ }
+
+ item.Id = libraryManager.GetNewItemId(item.Path, item.GetType());
+
+ // Make sure the item has a name
+ EnsureName(item, args.FileInfo);
+
+ item.IsLocked = item.Path.IndexOf("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) != -1 ||
+ item.GetParents().Any(i => i.IsLocked);
+
+ // Make sure DateCreated and DateModified have values
+ EnsureDates(fileSystem, item, args);
+ }
+
+ /// <summary>
+ /// Ensures the name.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="fileInfo">The file information.</param>
+ private static void EnsureName(BaseItem item, FileSystemMetadata fileInfo)
+ {
+ // If the subclass didn't supply a name, add it here
+ if (string.IsNullOrEmpty(item.Name) && !string.IsNullOrEmpty(item.Path))
+ {
+ item.Name = GetDisplayName(fileInfo.Name, fileInfo.IsDirectory);
+ }
+ }
+
+ /// <summary>
+ /// Gets the display name.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="isDirectory">if set to <c>true</c> [is directory].</param>
+ /// <returns>System.String.</returns>
+ private static string GetDisplayName(string path, bool isDirectory)
+ {
+ return isDirectory ? Path.GetFileName(path) : Path.GetFileNameWithoutExtension(path);
+ }
+
+ /// <summary>
+ /// The MB name regex
+ /// </summary>
+ private static readonly Regex MbNameRegex = new Regex(@"(\[.*?\])");
+
+ internal static string StripBrackets(string inputString)
+ {
+ var output = MbNameRegex.Replace(inputString, string.Empty).Trim();
+ return Regex.Replace(output, @"\s+", " ");
+ }
+
+ /// <summary>
+ /// Ensures DateCreated and DateModified have values
+ /// </summary>
+ /// <param name="fileSystem">The file system.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="args">The args.</param>
+ private static void EnsureDates(IFileSystem fileSystem, BaseItem item, ItemResolveArgs args)
+ {
+ if (fileSystem == null)
+ {
+ throw new ArgumentNullException("fileSystem");
+ }
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+ if (args == null)
+ {
+ throw new ArgumentNullException("args");
+ }
+
+ // See if a different path came out of the resolver than what went in
+ if (!string.Equals(args.Path, item.Path, StringComparison.OrdinalIgnoreCase))
+ {
+ var childData = args.IsDirectory ? args.GetFileSystemEntryByPath(item.Path) : null;
+
+ if (childData != null)
+ {
+ SetDateCreated(item, fileSystem, childData);
+ }
+ else
+ {
+ var fileData = fileSystem.GetFileSystemInfo(item.Path);
+
+ if (fileData.Exists)
+ {
+ SetDateCreated(item, fileSystem, fileData);
+ }
+ }
+ }
+ else
+ {
+ SetDateCreated(item, fileSystem, args.FileInfo);
+ }
+ }
+
+ private static void SetDateCreated(BaseItem item, IFileSystem fileSystem, FileSystemMetadata info)
+ {
+ var config = BaseItem.ConfigurationManager.GetMetadataConfiguration();
+
+ if (config.UseFileCreationTimeForDateAdded)
+ {
+ item.DateCreated = fileSystem.GetCreationTimeUtc(info);
+ }
+ else
+ {
+ item.DateCreated = DateTime.UtcNow;
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
new file mode 100644
index 000000000..d8805355a
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
@@ -0,0 +1,68 @@
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Model.Entities;
+using System;
+
+namespace Emby.Server.Implementations.Library.Resolvers.Audio
+{
+ /// <summary>
+ /// Class AudioResolver
+ /// </summary>
+ public class AudioResolver : ItemResolver<MediaBrowser.Controller.Entities.Audio.Audio>
+ {
+ private readonly ILibraryManager _libraryManager;
+
+ public AudioResolver(ILibraryManager libraryManager)
+ {
+ _libraryManager = libraryManager;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override ResolverPriority Priority
+ {
+ get { return ResolverPriority.Last; }
+ }
+
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>Entities.Audio.Audio.</returns>
+ protected override MediaBrowser.Controller.Entities.Audio.Audio Resolve(ItemResolveArgs args)
+ {
+ // Return audio if the path is a file and has a matching extension
+
+ if (!args.IsDirectory)
+ {
+ var libraryOptions = args.GetLibraryOptions();
+
+ if (_libraryManager.IsAudioFile(args.Path, libraryOptions))
+ {
+ var collectionType = args.GetCollectionType();
+
+ var isMixed = string.IsNullOrWhiteSpace(collectionType);
+
+ // For conflicting extensions, give priority to videos
+ if (isMixed && _libraryManager.IsVideoFile(args.Path, libraryOptions))
+ {
+ return null;
+ }
+
+ var isStandalone = args.Parent == null;
+
+ if (isStandalone ||
+ string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase) ||
+ isMixed)
+ {
+ return new MediaBrowser.Controller.Entities.Audio.Audio();
+ }
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
new file mode 100644
index 000000000..f8e105195
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
@@ -0,0 +1,173 @@
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Naming.Audio;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Emby.Server.Implementations.Logging;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.Configuration;
+
+namespace Emby.Server.Implementations.Library.Resolvers.Audio
+{
+ /// <summary>
+ /// Class MusicAlbumResolver
+ /// </summary>
+ public class MusicAlbumResolver : ItemResolver<MusicAlbum>
+ {
+ private readonly ILogger _logger;
+ private readonly IFileSystem _fileSystem;
+ private readonly ILibraryManager _libraryManager;
+
+ public MusicAlbumResolver(ILogger logger, IFileSystem fileSystem, ILibraryManager libraryManager)
+ {
+ _logger = logger;
+ _fileSystem = fileSystem;
+ _libraryManager = libraryManager;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override ResolverPriority Priority
+ {
+ get
+ {
+ // Behind special folder resolver
+ return ResolverPriority.Second;
+ }
+ }
+
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>MusicAlbum.</returns>
+ protected override MusicAlbum Resolve(ItemResolveArgs args)
+ {
+ if (!args.IsDirectory) return null;
+
+ // Avoid mis-identifying top folders
+ if (args.HasParent<MusicAlbum>()) return null;
+ if (args.Parent.IsRoot) return null;
+
+ var collectionType = args.GetCollectionType();
+
+ var isMusicMediaFolder = string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase);
+
+ // If there's a collection type and it's not music, don't allow it.
+ if (!isMusicMediaFolder)
+ {
+ return null;
+ }
+
+ return IsMusicAlbum(args) ? new MusicAlbum() : null;
+ }
+
+
+ /// <summary>
+ /// Determine if the supplied file data points to a music album
+ /// </summary>
+ public bool IsMusicAlbum(string path, IDirectoryService directoryService, LibraryOptions libraryOptions)
+ {
+ return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService, _logger, _fileSystem, libraryOptions, _libraryManager);
+ }
+
+ /// <summary>
+ /// Determine if the supplied resolve args should be considered a music album
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns><c>true</c> if [is music album] [the specified args]; otherwise, <c>false</c>.</returns>
+ private bool IsMusicAlbum(ItemResolveArgs args)
+ {
+ // Args points to an album if parent is an Artist folder or it directly contains music
+ if (args.IsDirectory)
+ {
+ //if (args.Parent is MusicArtist) return true; //saves us from testing children twice
+ if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService, _logger, _fileSystem, args.GetLibraryOptions(), _libraryManager)) return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Determine if the supplied list contains what we should consider music
+ /// </summary>
+ private bool ContainsMusic(IEnumerable<FileSystemMetadata> list,
+ bool allowSubfolders,
+ IDirectoryService directoryService,
+ ILogger logger,
+ IFileSystem fileSystem,
+ LibraryOptions libraryOptions,
+ ILibraryManager libraryManager)
+ {
+ var discSubfolderCount = 0;
+ var notMultiDisc = false;
+
+ foreach (var fileSystemInfo in list)
+ {
+ if (fileSystemInfo.IsDirectory)
+ {
+ if (allowSubfolders)
+ {
+ var path = fileSystemInfo.FullName;
+ var isMultiDisc = IsMultiDiscFolder(path, libraryOptions);
+
+ if (isMultiDisc)
+ {
+ var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryOptions, libraryManager);
+
+ if (hasMusic)
+ {
+ logger.Debug("Found multi-disc folder: " + path);
+ discSubfolderCount++;
+ }
+ }
+ else
+ {
+ var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryOptions, libraryManager);
+
+ if (hasMusic)
+ {
+ // If there are folders underneath with music that are not multidisc, then this can't be a multi-disc album
+ notMultiDisc = true;
+ }
+ }
+ }
+ }
+
+ var fullName = fileSystemInfo.FullName;
+
+ if (libraryManager.IsAudioFile(fullName, libraryOptions))
+ {
+ return true;
+ }
+ }
+
+ if (notMultiDisc)
+ {
+ return false;
+ }
+
+ return discSubfolderCount > 0;
+ }
+
+ private bool IsMultiDiscFolder(string path, LibraryOptions libraryOptions)
+ {
+ var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions(libraryOptions);
+
+ var parser = new AlbumParser(namingOptions, new PatternsLogger());
+ var result = parser.ParseMultiPart(path);
+
+ return result.IsMultiPart;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
new file mode 100644
index 000000000..2971405b9
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
@@ -0,0 +1,94 @@
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using System;
+using System.IO;
+using System.Linq;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.IO;
+
+namespace Emby.Server.Implementations.Library.Resolvers.Audio
+{
+ /// <summary>
+ /// Class MusicArtistResolver
+ /// </summary>
+ public class MusicArtistResolver : ItemResolver<MusicArtist>
+ {
+ private readonly ILogger _logger;
+ private readonly IFileSystem _fileSystem;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IServerConfigurationManager _config;
+
+ public MusicArtistResolver(ILogger logger, IFileSystem fileSystem, ILibraryManager libraryManager, IServerConfigurationManager config)
+ {
+ _logger = logger;
+ _fileSystem = fileSystem;
+ _libraryManager = libraryManager;
+ _config = config;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override ResolverPriority Priority
+ {
+ get
+ {
+ // Behind special folder resolver
+ return ResolverPriority.Second;
+ }
+ }
+
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>MusicArtist.</returns>
+ protected override MusicArtist Resolve(ItemResolveArgs args)
+ {
+ if (!args.IsDirectory) return null;
+
+ // Don't allow nested artists
+ if (args.HasParent<MusicArtist>() || args.HasParent<MusicAlbum>())
+ {
+ return null;
+ }
+
+ var collectionType = args.GetCollectionType();
+
+ var isMusicMediaFolder = string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase);
+
+ // If there's a collection type and it's not music, it can't be a series
+ if (!isMusicMediaFolder)
+ {
+ return null;
+ }
+
+ if (args.ContainsFileSystemEntryByName("artist.nfo"))
+ {
+ return new MusicArtist();
+ }
+
+ if (_config.Configuration.EnableSimpleArtistDetection)
+ {
+ return null;
+ }
+
+ // Avoid mis-identifying top folders
+ if (args.Parent.IsRoot) return null;
+
+ var directoryService = args.DirectoryService;
+
+ var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager);
+
+ // If we contain an album assume we are an artist folder
+ return args.FileSystemChildren.Where(i => i.IsDirectory).Any(i => albumResolver.IsMusicAlbum(i.FullName, directoryService, args.GetLibraryOptions())) ? new MusicArtist() : null;
+ }
+
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
new file mode 100644
index 000000000..b7819eb68
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
@@ -0,0 +1,297 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Naming.Video;
+using System;
+using System.IO;
+using Emby.Server.Implementations.Logging;
+
+namespace Emby.Server.Implementations.Library.Resolvers
+{
+ /// <summary>
+ /// Resolves a Path into a Video or Video subclass
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public abstract class BaseVideoResolver<T> : MediaBrowser.Controller.Resolvers.ItemResolver<T>
+ where T : Video, new()
+ {
+ protected readonly ILibraryManager LibraryManager;
+
+ protected BaseVideoResolver(ILibraryManager libraryManager)
+ {
+ LibraryManager = libraryManager;
+ }
+
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>`0.</returns>
+ protected override T Resolve(ItemResolveArgs args)
+ {
+ return ResolveVideo<T>(args, false);
+ }
+
+ /// <summary>
+ /// Resolves the video.
+ /// </summary>
+ /// <typeparam name="TVideoType">The type of the T video type.</typeparam>
+ /// <param name="args">The args.</param>
+ /// <param name="parseName">if set to <c>true</c> [parse name].</param>
+ /// <returns>``0.</returns>
+ protected TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
+ where TVideoType : Video, new()
+ {
+ var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
+
+ // If the path is a file check for a matching extensions
+ var parser = new MediaBrowser.Naming.Video.VideoResolver(namingOptions, new PatternsLogger());
+
+ if (args.IsDirectory)
+ {
+ TVideoType video = null;
+ VideoFileInfo videoInfo = null;
+
+ // Loop through each child file/folder and see if we find a video
+ foreach (var child in args.FileSystemChildren)
+ {
+ var filename = child.Name;
+
+ if (child.IsDirectory)
+ {
+ if (IsDvdDirectory(filename))
+ {
+ videoInfo = parser.ResolveDirectory(args.Path);
+
+ if (videoInfo == null)
+ {
+ return null;
+ }
+
+ video = new TVideoType
+ {
+ Path = args.Path,
+ VideoType = VideoType.Dvd,
+ ProductionYear = videoInfo.Year
+ };
+ break;
+ }
+ if (IsBluRayDirectory(filename))
+ {
+ videoInfo = parser.ResolveDirectory(args.Path);
+
+ if (videoInfo == null)
+ {
+ return null;
+ }
+
+ video = new TVideoType
+ {
+ Path = args.Path,
+ VideoType = VideoType.BluRay,
+ ProductionYear = videoInfo.Year
+ };
+ break;
+ }
+ }
+ else if (IsDvdFile(filename))
+ {
+ videoInfo = parser.ResolveDirectory(args.Path);
+
+ if (videoInfo == null)
+ {
+ return null;
+ }
+
+ video = new TVideoType
+ {
+ Path = args.Path,
+ VideoType = VideoType.Dvd,
+ ProductionYear = videoInfo.Year
+ };
+ break;
+ }
+ }
+
+ if (video != null)
+ {
+ video.Name = parseName ?
+ videoInfo.Name :
+ Path.GetFileName(args.Path);
+
+ Set3DFormat(video, videoInfo);
+ }
+
+ return video;
+ }
+ else
+ {
+ var videoInfo = parser.Resolve(args.Path, false, false);
+
+ if (videoInfo == null)
+ {
+ return null;
+ }
+
+ if (LibraryManager.IsVideoFile(args.Path, args.GetLibraryOptions()) || videoInfo.IsStub)
+ {
+ var path = args.Path;
+
+ var video = new TVideoType
+ {
+ Path = path,
+ IsInMixedFolder = true,
+ ProductionYear = videoInfo.Year
+ };
+
+ SetVideoType(video, videoInfo);
+
+ video.Name = parseName ?
+ videoInfo.Name :
+ Path.GetFileNameWithoutExtension(args.Path);
+
+ Set3DFormat(video, videoInfo);
+
+ return video;
+ }
+ }
+
+ return null;
+ }
+
+ protected void SetVideoType(Video video, VideoFileInfo videoInfo)
+ {
+ var extension = Path.GetExtension(video.Path);
+ video.VideoType = string.Equals(extension, ".iso", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(extension, ".img", StringComparison.OrdinalIgnoreCase) ?
+ VideoType.Iso :
+ VideoType.VideoFile;
+
+ video.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase);
+ video.IsPlaceHolder = videoInfo.IsStub;
+
+ if (videoInfo.IsStub)
+ {
+ if (string.Equals(videoInfo.StubType, "dvd", StringComparison.OrdinalIgnoreCase))
+ {
+ video.VideoType = VideoType.Dvd;
+ }
+ else if (string.Equals(videoInfo.StubType, "hddvd", StringComparison.OrdinalIgnoreCase))
+ {
+ video.VideoType = VideoType.HdDvd;
+ video.IsHD = true;
+ }
+ else if (string.Equals(videoInfo.StubType, "bluray", StringComparison.OrdinalIgnoreCase))
+ {
+ video.VideoType = VideoType.BluRay;
+ video.IsHD = true;
+ }
+ else if (string.Equals(videoInfo.StubType, "hdtv", StringComparison.OrdinalIgnoreCase))
+ {
+ video.IsHD = true;
+ }
+ }
+
+ SetIsoType(video);
+ }
+
+ protected void SetIsoType(Video video)
+ {
+ if (video.VideoType == VideoType.Iso)
+ {
+ if (video.Path.IndexOf("dvd", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ video.IsoType = IsoType.Dvd;
+ }
+ else if (video.Path.IndexOf("bluray", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ video.IsoType = IsoType.BluRay;
+ }
+ }
+ }
+
+ protected void Set3DFormat(Video video, bool is3D, string format3D)
+ {
+ if (is3D)
+ {
+ if (string.Equals(format3D, "fsbs", StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.FullSideBySide;
+ }
+ else if (string.Equals(format3D, "ftab", StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.FullTopAndBottom;
+ }
+ else if (string.Equals(format3D, "hsbs", StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.HalfSideBySide;
+ }
+ else if (string.Equals(format3D, "htab", StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.HalfTopAndBottom;
+ }
+ else if (string.Equals(format3D, "sbs", StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.HalfSideBySide;
+ }
+ else if (string.Equals(format3D, "sbs3d", StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.HalfSideBySide;
+ }
+ else if (string.Equals(format3D, "tab", StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.HalfTopAndBottom;
+ }
+ else if (string.Equals(format3D, "mvc", StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.MVC;
+ }
+ }
+ }
+
+ protected void Set3DFormat(Video video, VideoFileInfo videoInfo)
+ {
+ Set3DFormat(video, videoInfo.Is3D, videoInfo.Format3D);
+ }
+
+ protected void Set3DFormat(Video video)
+ {
+ var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
+
+ var resolver = new Format3DParser(namingOptions, new PatternsLogger());
+ var result = resolver.Parse(video.Path);
+
+ Set3DFormat(video, result.Is3D, result.Format3D);
+ }
+
+ /// <summary>
+ /// Determines whether [is DVD directory] [the specified directory name].
+ /// </summary>
+ /// <param name="directoryName">Name of the directory.</param>
+ /// <returns><c>true</c> if [is DVD directory] [the specified directory name]; otherwise, <c>false</c>.</returns>
+ protected bool IsDvdDirectory(string directoryName)
+ {
+ return string.Equals(directoryName, "video_ts", StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// Determines whether [is DVD file] [the specified name].
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns><c>true</c> if [is DVD file] [the specified name]; otherwise, <c>false</c>.</returns>
+ protected bool IsDvdFile(string name)
+ {
+ return string.Equals(name, "video_ts.ifo", StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// Determines whether [is blu ray directory] [the specified directory name].
+ /// </summary>
+ /// <param name="directoryName">Name of the directory.</param>
+ /// <returns><c>true</c> if [is blu ray directory] [the specified directory name]; otherwise, <c>false</c>.</returns>
+ protected bool IsBluRayDirectory(string directoryName)
+ {
+ return string.Equals(directoryName, "bdmv", StringComparison.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs
new file mode 100644
index 000000000..5e73baa5c
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs
@@ -0,0 +1,56 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Resolvers;
+
+namespace Emby.Server.Implementations.Library.Resolvers
+{
+ /// <summary>
+ /// Class FolderResolver
+ /// </summary>
+ public class FolderResolver : FolderResolver<Folder>
+ {
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override ResolverPriority Priority
+ {
+ get { return ResolverPriority.Last; }
+ }
+
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>Folder.</returns>
+ protected override Folder Resolve(ItemResolveArgs args)
+ {
+ if (args.IsDirectory)
+ {
+ return new Folder();
+ }
+
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Class FolderResolver
+ /// </summary>
+ /// <typeparam name="TItemType">The type of the T item type.</typeparam>
+ public abstract class FolderResolver<TItemType> : ItemResolver<TItemType>
+ where TItemType : Folder, new()
+ {
+ /// <summary>
+ /// Sets the initial item values.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="args">The args.</param>
+ protected override void SetInitialItemValues(TItemType item, ItemResolveArgs args)
+ {
+ base.SetInitialItemValues(item, args);
+
+ item.IsRoot = args.Parent == null;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs
new file mode 100644
index 000000000..b4a37be5f
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs
@@ -0,0 +1,62 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Resolvers;
+
+namespace Emby.Server.Implementations.Library.Resolvers
+{
+ /// <summary>
+ /// Class ItemResolver
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public abstract class ItemResolver<T> : IItemResolver
+ where T : BaseItem, new()
+ {
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>`0.</returns>
+ protected virtual T Resolve(ItemResolveArgs args)
+ {
+ return null;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public virtual ResolverPriority Priority
+ {
+ get
+ {
+ return ResolverPriority.First;
+ }
+ }
+
+ /// <summary>
+ /// Sets initial values on the newly resolved item
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="args">The args.</param>
+ protected virtual void SetInitialItemValues(T item, ItemResolveArgs args)
+ {
+ }
+
+ /// <summary>
+ /// Resolves the path.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>BaseItem.</returns>
+ BaseItem IItemResolver.ResolvePath(ItemResolveArgs args)
+ {
+ var item = Resolve(args);
+
+ if (item != null)
+ {
+ SetInitialItemValues(item, args);
+ }
+
+ return item;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
new file mode 100644
index 000000000..df441c5ed
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
@@ -0,0 +1,77 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+using System;
+using System.IO;
+
+namespace Emby.Server.Implementations.Library.Resolvers.Movies
+{
+ /// <summary>
+ /// Class BoxSetResolver
+ /// </summary>
+ public class BoxSetResolver : FolderResolver<BoxSet>
+ {
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>BoxSet.</returns>
+ protected override BoxSet Resolve(ItemResolveArgs args)
+ {
+ // It's a boxset if all of the following conditions are met:
+ // Is a Directory
+ // Contains [boxset] in the path
+ if (args.IsDirectory)
+ {
+ var filename = Path.GetFileName(args.Path);
+
+ if (string.IsNullOrEmpty(filename))
+ {
+ return null;
+ }
+
+ if (filename.IndexOf("[boxset]", StringComparison.OrdinalIgnoreCase) != -1 ||
+ args.ContainsFileSystemEntryByName("collection.xml"))
+ {
+ return new BoxSet
+ {
+ Path = args.Path,
+ Name = ResolverHelper.StripBrackets(Path.GetFileName(args.Path))
+ };
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Sets the initial item values.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="args">The args.</param>
+ protected override void SetInitialItemValues(BoxSet item, ItemResolveArgs args)
+ {
+ base.SetInitialItemValues(item, args);
+
+ SetProviderIdFromPath(item);
+ }
+
+ /// <summary>
+ /// Sets the provider id from path.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ private void SetProviderIdFromPath(BaseItem item)
+ {
+ //we need to only look at the name of this actual item (not parents)
+ var justName = Path.GetFileName(item.Path);
+
+ var id = justName.GetAttributeValue("tmdbid");
+
+ if (!string.IsNullOrEmpty(id))
+ {
+ item.SetProviderId(MetadataProviders.Tmdb, id);
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
new file mode 100644
index 000000000..d8c8b2024
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -0,0 +1,541 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Naming.Video;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Emby.Server.Implementations.Logging;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Server.Implementations.Library.Resolvers.Movies
+{
+ /// <summary>
+ /// Class MovieResolver
+ /// </summary>
+ public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
+ {
+ public MovieResolver(ILibraryManager libraryManager)
+ : base(libraryManager)
+ {
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override ResolverPriority Priority
+ {
+ get
+ {
+ // Give plugins a chance to catch iso's first
+ // Also since we have to loop through child files looking for videos,
+ // see if we can avoid some of that by letting other resolvers claim folders first
+ // Also run after series resolver
+ return ResolverPriority.Third;
+ }
+ }
+
+ public MultiItemResolverResult ResolveMultiple(Folder parent,
+ List<FileSystemMetadata> files,
+ string collectionType,
+ IDirectoryService directoryService)
+ {
+ var result = ResolveMultipleInternal(parent, files, collectionType, directoryService);
+
+ if (result != null)
+ {
+ foreach (var item in result.Items)
+ {
+ SetInitialItemValues((Video)item, null);
+ }
+ }
+
+ return result;
+ }
+
+ private MultiItemResolverResult ResolveMultipleInternal(Folder parent,
+ List<FileSystemMetadata> files,
+ string collectionType,
+ IDirectoryService directoryService)
+ {
+ if (IsInvalid(parent, collectionType))
+ {
+ return null;
+ }
+
+ if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
+ {
+ return ResolveVideos<MusicVideo>(parent, files, directoryService, false);
+ }
+
+ if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
+ {
+ return ResolveVideos<Video>(parent, files, directoryService, false);
+ }
+
+ if (string.IsNullOrEmpty(collectionType))
+ {
+ // Owned items should just use the plain video type
+ if (parent == null)
+ {
+ return ResolveVideos<Video>(parent, files, directoryService, false);
+ }
+
+ if (parent is Series || parent.GetParents().OfType<Series>().Any())
+ {
+ return null;
+ }
+
+ return ResolveVideos<Movie>(parent, files, directoryService, false);
+ }
+
+ if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
+ {
+ return ResolveVideos<Movie>(parent, files, directoryService, true);
+ }
+
+ return null;
+ }
+
+ private MultiItemResolverResult ResolveVideos<T>(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, bool suppportMultiEditions)
+ where T : Video, new()
+ {
+ var files = new List<FileSystemMetadata>();
+ var videos = new List<BaseItem>();
+ var leftOver = new List<FileSystemMetadata>();
+
+ // Loop through each child file/folder and see if we find a video
+ foreach (var child in fileSystemEntries)
+ {
+ if (child.IsDirectory)
+ {
+ leftOver.Add(child);
+ }
+ else if (IsIgnored(child.Name))
+ {
+
+ }
+ else
+ {
+ files.Add(child);
+ }
+ }
+
+ var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
+
+ var resolver = new VideoListResolver(namingOptions, new PatternsLogger());
+ var resolverResult = resolver.Resolve(files, suppportMultiEditions).ToList();
+
+ var result = new MultiItemResolverResult
+ {
+ ExtraFiles = leftOver,
+ Items = videos
+ };
+
+ var isInMixedFolder = resolverResult.Count > 1;
+
+ foreach (var video in resolverResult)
+ {
+ var firstVideo = video.Files.First();
+
+ var videoItem = new T
+ {
+ Path = video.Files[0].Path,
+ IsInMixedFolder = isInMixedFolder,
+ ProductionYear = video.Year,
+ Name = video.Name,
+ AdditionalParts = video.Files.Skip(1).Select(i => i.Path).ToList(),
+ LocalAlternateVersions = video.AlternateVersions.Select(i => i.Path).ToList()
+ };
+
+ SetVideoType(videoItem, firstVideo);
+ Set3DFormat(videoItem, firstVideo);
+
+ result.Items.Add(videoItem);
+ }
+
+ result.ExtraFiles.AddRange(files.Where(i => !ContainsFile(resolverResult, i)));
+
+ return result;
+ }
+
+ private bool ContainsFile(List<VideoInfo> result, FileSystemMetadata file)
+ {
+ return result.Any(i => ContainsFile(i, file));
+ }
+
+ private bool ContainsFile(VideoInfo result, FileSystemMetadata file)
+ {
+ return result.Files.Any(i => ContainsFile(i, file)) ||
+ result.AlternateVersions.Any(i => ContainsFile(i, file)) ||
+ result.Extras.Any(i => ContainsFile(i, file));
+ }
+
+ private bool ContainsFile(VideoFileInfo result, FileSystemMetadata file)
+ {
+ return string.Equals(result.Path, file.FullName, StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>Video.</returns>
+ protected override Video Resolve(ItemResolveArgs args)
+ {
+ var collectionType = args.GetCollectionType();
+
+ if (IsInvalid(args.Parent, collectionType))
+ {
+ return null;
+ }
+
+ // Find movies with their own folders
+ if (args.IsDirectory)
+ {
+ var files = args.FileSystemChildren
+ .Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
+ .ToList();
+
+ if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
+ {
+ return FindMovie<MusicVideo>(args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
+ }
+
+ if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
+ {
+ return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
+ }
+
+ if (string.IsNullOrEmpty(collectionType))
+ {
+ // Owned items will be caught by the plain video resolver
+ if (args.Parent == null)
+ {
+ //return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType);
+ return null;
+ }
+
+ if (args.HasParent<Series>())
+ {
+ return null;
+ }
+
+ {
+ return FindMovie<Movie>(args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
+ }
+ }
+
+ if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
+ {
+ return FindMovie<Movie>(args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
+ }
+
+ return null;
+ }
+
+ // Owned items will be caught by the plain video resolver
+ if (args.Parent == null)
+ {
+ return null;
+ }
+
+ Video item = null;
+
+ if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
+ {
+ item = ResolveVideo<MusicVideo>(args, false);
+ }
+
+ // To find a movie file, the collection type must be movies or boxsets
+ else if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
+ {
+ item = ResolveVideo<Movie>(args, true);
+ }
+
+ else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
+ {
+ item = ResolveVideo<Video>(args, false);
+ }
+ else if (string.IsNullOrEmpty(collectionType))
+ {
+ if (args.HasParent<Series>())
+ {
+ return null;
+ }
+
+ item = ResolveVideo<Video>(args, false);
+ }
+
+ if (item != null)
+ {
+ item.IsInMixedFolder = true;
+ }
+
+ return item;
+ }
+
+ private bool IsIgnored(string filename)
+ {
+ // Ignore samples
+ var sampleFilename = " " + filename.Replace(".", " ", StringComparison.OrdinalIgnoreCase)
+ .Replace("-", " ", StringComparison.OrdinalIgnoreCase)
+ .Replace("_", " ", StringComparison.OrdinalIgnoreCase)
+ .Replace("!", " ", StringComparison.OrdinalIgnoreCase);
+
+ if (sampleFilename.IndexOf(" sample ", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Sets the initial item values.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="args">The args.</param>
+ protected override void SetInitialItemValues(Video item, ItemResolveArgs args)
+ {
+ base.SetInitialItemValues(item, args);
+
+ SetProviderIdsFromPath(item);
+ }
+
+ /// <summary>
+ /// Sets the provider id from path.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ private void SetProviderIdsFromPath(Video item)
+ {
+ if (item is Movie || item is MusicVideo)
+ {
+ //we need to only look at the name of this actual item (not parents)
+ var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path) : Path.GetFileName(item.ContainingFolderPath);
+
+ if (!string.IsNullOrWhiteSpace(justName))
+ {
+ // check for tmdb id
+ var tmdbid = justName.GetAttributeValue("tmdbid");
+
+ if (!string.IsNullOrWhiteSpace(tmdbid))
+ {
+ item.SetProviderId(MetadataProviders.Tmdb, tmdbid);
+ }
+ }
+
+ if (!string.IsNullOrWhiteSpace(item.Path))
+ {
+ // check for imdb id - we use full media path, as we can assume, that this will match in any use case (wither id in parent dir or in file name)
+ var imdbid = item.Path.GetAttributeValue("imdbid");
+
+ if (!string.IsNullOrWhiteSpace(imdbid))
+ {
+ item.SetProviderId(MetadataProviders.Imdb, imdbid);
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Finds a movie based on a child file system entries
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <returns>Movie.</returns>
+ private T FindMovie<T>(string path, Folder parent, List<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, string collectionType, bool allowFilesAsFolders)
+ where T : Video, new()
+ {
+ var multiDiscFolders = new List<FileSystemMetadata>();
+
+ // Search for a folder rip
+ foreach (var child in fileSystemEntries)
+ {
+ var filename = child.Name;
+
+ if (child.IsDirectory)
+ {
+ if (IsDvdDirectory(filename))
+ {
+ var movie = new T
+ {
+ Path = path,
+ VideoType = VideoType.Dvd
+ };
+ Set3DFormat(movie);
+ return movie;
+ }
+ if (IsBluRayDirectory(filename))
+ {
+ var movie = new T
+ {
+ Path = path,
+ VideoType = VideoType.BluRay
+ };
+ Set3DFormat(movie);
+ return movie;
+ }
+
+ multiDiscFolders.Add(child);
+ }
+ else if (IsDvdFile(filename))
+ {
+ var movie = new T
+ {
+ Path = path,
+ VideoType = VideoType.Dvd
+ };
+ Set3DFormat(movie);
+ return movie;
+ }
+ }
+
+ if (allowFilesAsFolders)
+ {
+ // TODO: Allow GetMultiDiscMovie in here
+ var supportsMultiVersion = !string.Equals(collectionType, CollectionType.HomeVideos) &&
+ !string.Equals(collectionType, CollectionType.Photos) &&
+ !string.Equals(collectionType, CollectionType.MusicVideos);
+
+ var result = ResolveVideos<T>(parent, fileSystemEntries, directoryService, supportsMultiVersion);
+
+ if (result.Items.Count == 1)
+ {
+ var movie = (T)result.Items[0];
+ movie.IsInMixedFolder = false;
+ movie.Name = Path.GetFileName(movie.ContainingFolderPath);
+ return movie;
+ }
+
+ if (result.Items.Count == 0 && multiDiscFolders.Count > 0)
+ {
+ return GetMultiDiscMovie<T>(multiDiscFolders, directoryService);
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Gets the multi disc movie.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="multiDiscFolders">The folders.</param>
+ /// <param name="directoryService">The directory service.</param>
+ /// <returns>``0.</returns>
+ private T GetMultiDiscMovie<T>(List<FileSystemMetadata> multiDiscFolders, IDirectoryService directoryService)
+ where T : Video, new()
+ {
+ var videoTypes = new List<VideoType>();
+
+ var folderPaths = multiDiscFolders.Select(i => i.FullName).Where(i =>
+ {
+ var subFileEntries = directoryService.GetFileSystemEntries(i)
+ .ToList();
+
+ var subfolders = subFileEntries
+ .Where(e => e.IsDirectory)
+ .Select(d => d.Name)
+ .ToList();
+
+ if (subfolders.Any(IsDvdDirectory))
+ {
+ videoTypes.Add(VideoType.Dvd);
+ return true;
+ }
+ if (subfolders.Any(IsBluRayDirectory))
+ {
+ videoTypes.Add(VideoType.BluRay);
+ return true;
+ }
+
+ var subFiles = subFileEntries
+ .Where(e => !e.IsDirectory)
+ .Select(d => d.Name);
+
+ if (subFiles.Any(IsDvdFile))
+ {
+ videoTypes.Add(VideoType.Dvd);
+ return true;
+ }
+
+ return false;
+
+ }).OrderBy(i => i).ToList();
+
+ // If different video types were found, don't allow this
+ if (videoTypes.Distinct().Count() > 1)
+ {
+ return null;
+ }
+
+ if (folderPaths.Count == 0)
+ {
+ return null;
+ }
+
+ var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
+ var resolver = new StackResolver(namingOptions, new PatternsLogger());
+
+ var result = resolver.ResolveDirectories(folderPaths);
+
+ if (result.Stacks.Count != 1)
+ {
+ return null;
+ }
+
+ var returnVideo = new T
+ {
+ Path = folderPaths[0],
+
+ AdditionalParts = folderPaths.Skip(1).ToList(),
+
+ VideoType = videoTypes[0],
+
+ Name = result.Stacks[0].Name
+ };
+
+ SetIsoType(returnVideo);
+
+ return returnVideo;
+ }
+
+ private bool IsInvalid(Folder parent, string collectionType)
+ {
+ if (parent != null)
+ {
+ if (parent.IsRoot)
+ {
+ return true;
+ }
+ }
+
+ var validCollectionTypes = new[]
+ {
+ CollectionType.Movies,
+ CollectionType.HomeVideos,
+ CollectionType.MusicVideos,
+ CollectionType.Movies,
+ CollectionType.Photos
+ };
+
+ if (string.IsNullOrWhiteSpace(collectionType))
+ {
+ return false;
+ }
+
+ return !validCollectionTypes.Contains(collectionType, StringComparer.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
new file mode 100644
index 000000000..3d7ede879
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
@@ -0,0 +1,56 @@
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Model.Entities;
+using System;
+using System.IO;
+using System.Linq;
+
+namespace Emby.Server.Implementations.Library.Resolvers
+{
+ public class PhotoAlbumResolver : FolderResolver<PhotoAlbum>
+ {
+ private readonly IImageProcessor _imageProcessor;
+ public PhotoAlbumResolver(IImageProcessor imageProcessor)
+ {
+ _imageProcessor = imageProcessor;
+ }
+
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>Trailer.</returns>
+ protected override PhotoAlbum Resolve(ItemResolveArgs args)
+ {
+ // Must be an image file within a photo collection
+ if (args.IsDirectory && string.Equals(args.GetCollectionType(), CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
+ {
+ if (HasPhotos(args))
+ {
+ return new PhotoAlbum
+ {
+ Path = args.Path
+ };
+ }
+ }
+
+ return null;
+ }
+
+ private bool HasPhotos(ItemResolveArgs args)
+ {
+ return args.FileSystemChildren.Any(i => (!i.IsDirectory) && PhotoResolver.IsImageFile(i.FullName, _imageProcessor));
+ }
+
+ public override ResolverPriority Priority
+ {
+ get
+ {
+ // Behind special folder resolver
+ return ResolverPriority.Second;
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
new file mode 100644
index 000000000..df39e57ad
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
@@ -0,0 +1,103 @@
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+using System;
+using System.IO;
+using System.Linq;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.Configuration;
+
+namespace Emby.Server.Implementations.Library.Resolvers
+{
+ public class PhotoResolver : ItemResolver<Photo>
+ {
+ private readonly IImageProcessor _imageProcessor;
+ private readonly ILibraryManager _libraryManager;
+
+ public PhotoResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager)
+ {
+ _imageProcessor = imageProcessor;
+ _libraryManager = libraryManager;
+ }
+
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>Trailer.</returns>
+ protected override Photo Resolve(ItemResolveArgs args)
+ {
+ if (!args.IsDirectory)
+ {
+ // Must be an image file within a photo collection
+ var collectionType = args.GetCollectionType();
+
+
+ if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase) ||
+ (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.GetLibraryOptions().EnablePhotos))
+ {
+ if (IsImageFile(args.Path, _imageProcessor))
+ {
+ var filename = Path.GetFileNameWithoutExtension(args.Path);
+
+ // Make sure the image doesn't belong to a video file
+ if (args.DirectoryService.GetFiles(Path.GetDirectoryName(args.Path)).Any(i => IsOwnedByMedia(args.GetLibraryOptions(), i, filename)))
+ {
+ return null;
+ }
+
+ return new Photo
+ {
+ Path = args.Path
+ };
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private bool IsOwnedByMedia(LibraryOptions libraryOptions, FileSystemMetadata file, string imageFilename)
+ {
+ if (_libraryManager.IsVideoFile(file.FullName, libraryOptions) && imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file.Name), StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static readonly string[] IgnoreFiles =
+ {
+ "folder",
+ "thumb",
+ "landscape",
+ "fanart",
+ "backdrop",
+ "poster",
+ "cover"
+ };
+
+ internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
+ {
+ var filename = Path.GetFileNameWithoutExtension(path) ?? string.Empty;
+
+ if (IgnoreFiles.Contains(filename, StringComparer.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ if (IgnoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1))
+ {
+ return false;
+ }
+
+ return imageProcessor.SupportedInputFormats.Contains((Path.GetExtension(path) ?? string.Empty).TrimStart('.'), StringComparer.OrdinalIgnoreCase);
+ }
+
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
new file mode 100644
index 000000000..8c59cf20f
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
@@ -0,0 +1,42 @@
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Playlists;
+using System;
+using System.IO;
+
+namespace Emby.Server.Implementations.Library.Resolvers
+{
+ public class PlaylistResolver : FolderResolver<Playlist>
+ {
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>BoxSet.</returns>
+ protected override Playlist Resolve(ItemResolveArgs args)
+ {
+ // It's a boxset if all of the following conditions are met:
+ // Is a Directory
+ // Contains [playlist] in the path
+ if (args.IsDirectory)
+ {
+ var filename = Path.GetFileName(args.Path);
+
+ if (string.IsNullOrEmpty(filename))
+ {
+ return null;
+ }
+
+ if (filename.IndexOf("[playlist]", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return new Playlist
+ {
+ Path = args.Path,
+ Name = ResolverHelper.StripBrackets(Path.GetFileName(args.Path))
+ };
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
new file mode 100644
index 000000000..1bec1073d
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
@@ -0,0 +1,85 @@
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Resolvers;
+using System;
+using System.IO;
+using System.Linq;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Server.Implementations.Library.Resolvers
+{
+ class SpecialFolderResolver : FolderResolver<Folder>
+ {
+ private readonly IFileSystem _fileSystem;
+ private readonly IServerApplicationPaths _appPaths;
+
+ public SpecialFolderResolver(IFileSystem fileSystem, IServerApplicationPaths appPaths)
+ {
+ _fileSystem = fileSystem;
+ _appPaths = appPaths;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override ResolverPriority Priority
+ {
+ get { return ResolverPriority.First; }
+ }
+
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>Folder.</returns>
+ protected override Folder Resolve(ItemResolveArgs args)
+ {
+ if (args.IsDirectory)
+ {
+ if (args.IsPhysicalRoot)
+ {
+ return new AggregateFolder();
+ }
+ if (string.Equals(args.Path, _appPaths.DefaultUserViewsPath, StringComparison.OrdinalIgnoreCase))
+ {
+ return new UserRootFolder(); //if we got here and still a root - must be user root
+ }
+ if (args.IsVf)
+ {
+ return new CollectionFolder
+ {
+ CollectionType = GetCollectionType(args),
+ PhysicalLocationsList = args.PhysicalLocations.ToList()
+ };
+ }
+ }
+
+ return null;
+ }
+
+ private string GetCollectionType(ItemResolveArgs args)
+ {
+ return args.FileSystemChildren
+ .Where(i =>
+ {
+
+ try
+ {
+ return !i.IsDirectory &&
+ string.Equals(".collection", i.Extension, StringComparison.OrdinalIgnoreCase);
+ }
+ catch (IOException)
+ {
+ return false;
+ }
+
+ })
+ .Select(i => _fileSystem.GetFileNameWithoutExtension(i))
+ .FirstOrDefault();
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
new file mode 100644
index 000000000..2a4cc49b7
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
@@ -0,0 +1,75 @@
+using System;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using System.Linq;
+using MediaBrowser.Model.Entities;
+
+namespace Emby.Server.Implementations.Library.Resolvers.TV
+{
+ /// <summary>
+ /// Class EpisodeResolver
+ /// </summary>
+ public class EpisodeResolver : BaseVideoResolver<Episode>
+ {
+ public EpisodeResolver(ILibraryManager libraryManager) : base(libraryManager)
+ {
+ }
+
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>Episode.</returns>
+ protected override Episode Resolve(ItemResolveArgs args)
+ {
+ var parent = args.Parent;
+
+ if (parent == null)
+ {
+ return null;
+ }
+
+ var season = parent as Season;
+ // Just in case the user decided to nest episodes.
+ // Not officially supported but in some cases we can handle it.
+ if (season == null)
+ {
+ season = parent.GetParents().OfType<Season>().FirstOrDefault();
+ }
+
+ // If the parent is a Season or Series, then this is an Episode if the VideoResolver returns something
+ // Also handle flat tv folders
+ if (season != null ||
+ string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
+ args.HasParent<Series>())
+ {
+ var episode = ResolveVideo<Episode>(args, false);
+
+ if (episode != null)
+ {
+ var series = parent as Series;
+ if (series == null)
+ {
+ series = parent.GetParents().OfType<Series>().FirstOrDefault();
+ }
+
+ if (series != null)
+ {
+ episode.SeriesId = series.Id;
+ episode.SeriesName = series.Name;
+ episode.SeriesSortName = series.SortName;
+ }
+ if (season != null)
+ {
+ episode.SeasonId = season.Id;
+ episode.SeasonName = season.Name;
+ }
+ }
+
+ return episode;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
new file mode 100644
index 000000000..c065feda1
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
@@ -0,0 +1,62 @@
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Naming.Common;
+using MediaBrowser.Naming.TV;
+
+namespace Emby.Server.Implementations.Library.Resolvers.TV
+{
+ /// <summary>
+ /// Class SeasonResolver
+ /// </summary>
+ public class SeasonResolver : FolderResolver<Season>
+ {
+ /// <summary>
+ /// The _config
+ /// </summary>
+ private readonly IServerConfigurationManager _config;
+
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SeasonResolver"/> class.
+ /// </summary>
+ /// <param name="config">The config.</param>
+ public SeasonResolver(IServerConfigurationManager config, ILibraryManager libraryManager)
+ {
+ _config = config;
+ _libraryManager = libraryManager;
+ }
+
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>Season.</returns>
+ protected override Season Resolve(ItemResolveArgs args)
+ {
+ if (args.Parent is Series && args.IsDirectory)
+ {
+ var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
+ var series = ((Series)args.Parent);
+
+ var season = new Season
+ {
+ IndexNumber = new SeasonPathParser(namingOptions, new RegexProvider()).Parse(args.Path, true, true).SeasonNumber,
+ SeriesId = series.Id,
+ SeriesSortName = series.SortName,
+ SeriesName = series.Name
+ };
+
+ if (season.IndexNumber.HasValue && season.IndexNumber.Value == 0)
+ {
+ season.Name = _config.Configuration.SeasonZeroDisplayName;
+ }
+
+ return season;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
new file mode 100644
index 000000000..44eb0e3e2
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
@@ -0,0 +1,251 @@
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Naming.Common;
+using MediaBrowser.Naming.TV;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Emby.Server.Implementations.Logging;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.Configuration;
+
+namespace Emby.Server.Implementations.Library.Resolvers.TV
+{
+ /// <summary>
+ /// Class SeriesResolver
+ /// </summary>
+ public class SeriesResolver : FolderResolver<Series>
+ {
+ private readonly IFileSystem _fileSystem;
+ private readonly ILogger _logger;
+ private readonly ILibraryManager _libraryManager;
+
+ public SeriesResolver(IFileSystem fileSystem, ILogger logger, ILibraryManager libraryManager)
+ {
+ _fileSystem = fileSystem;
+ _logger = logger;
+ _libraryManager = libraryManager;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override ResolverPriority Priority
+ {
+ get
+ {
+ return ResolverPriority.Second;
+ }
+ }
+
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>Series.</returns>
+ protected override Series Resolve(ItemResolveArgs args)
+ {
+ if (args.IsDirectory)
+ {
+ if (args.HasParent<Series>() || args.HasParent<Season>())
+ {
+ return null;
+ }
+
+ var collectionType = args.GetCollectionType();
+ if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
+ {
+ //if (args.ContainsFileSystemEntryByName("tvshow.nfo"))
+ //{
+ // return new Series
+ // {
+ // Path = args.Path,
+ // Name = Path.GetFileName(args.Path)
+ // };
+ //}
+
+ var configuredContentType = _libraryManager.GetConfiguredContentType(args.Path);
+ if (!string.Equals(configuredContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
+ {
+ return new Series
+ {
+ Path = args.Path,
+ Name = Path.GetFileName(args.Path)
+ };
+ }
+ }
+ else if (string.IsNullOrWhiteSpace(collectionType))
+ {
+ if (args.ContainsFileSystemEntryByName("tvshow.nfo"))
+ {
+ if (args.Parent.IsRoot)
+ {
+ // For now, return null, but if we want to allow this in the future then add some additional checks to guard against a misplaced tvshow.nfo
+ return null;
+ }
+
+ return new Series
+ {
+ Path = args.Path,
+ Name = Path.GetFileName(args.Path)
+ };
+ }
+
+ if (args.Parent.IsRoot)
+ {
+ return null;
+ }
+
+ if (IsSeriesFolder(args.Path, args.FileSystemChildren, args.DirectoryService, _fileSystem, _logger, _libraryManager, args.GetLibraryOptions(), false))
+ {
+ return new Series
+ {
+ Path = args.Path,
+ Name = Path.GetFileName(args.Path)
+ };
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public static bool IsSeriesFolder(string path,
+ IEnumerable<FileSystemMetadata> fileSystemChildren,
+ IDirectoryService directoryService,
+ IFileSystem fileSystem,
+ ILogger logger,
+ ILibraryManager libraryManager,
+ LibraryOptions libraryOptions,
+ bool isTvContentType)
+ {
+ foreach (var child in fileSystemChildren)
+ {
+ //if ((attributes & FileAttributes.Hidden) == FileAttributes.Hidden)
+ //{
+ // //logger.Debug("Igoring series file or folder marked hidden: {0}", child.FullName);
+ // continue;
+ //}
+
+ // Can't enforce this because files saved by Bitcasa are always marked System
+ //if ((attributes & FileAttributes.System) == FileAttributes.System)
+ //{
+ // logger.Debug("Igoring series subfolder marked system: {0}", child.FullName);
+ // continue;
+ //}
+
+ if (child.IsDirectory)
+ {
+ if (IsSeasonFolder(child.FullName, isTvContentType, libraryManager))
+ {
+ //logger.Debug("{0} is a series because of season folder {1}.", path, child.FullName);
+ return true;
+ }
+ }
+ else
+ {
+ string fullName = child.FullName;
+ if (libraryManager.IsVideoFile(fullName, libraryOptions))
+ {
+ if (isTvContentType)
+ {
+ return true;
+ }
+
+ var namingOptions = ((LibraryManager)libraryManager).GetNamingOptions();
+
+ // In mixed folders we need to be conservative and avoid expressions that may result in false positives (e.g. movies with numbers in the title)
+ if (!isTvContentType)
+ {
+ namingOptions.EpisodeExpressions = namingOptions.EpisodeExpressions
+ .Where(i => i.IsNamed && !i.IsOptimistic)
+ .ToList();
+ }
+
+ var episodeResolver = new MediaBrowser.Naming.TV.EpisodeResolver(namingOptions, new PatternsLogger());
+ var episodeInfo = episodeResolver.Resolve(fullName, false, false);
+ if (episodeInfo != null && episodeInfo.EpisodeNumber.HasValue)
+ {
+ return true;
+ }
+ }
+ }
+ }
+
+ logger.Debug("{0} is not a series folder.", path);
+ return false;
+ }
+
+ /// <summary>
+ /// Determines whether [is place holder] [the specified path].
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns><c>true</c> if [is place holder] [the specified path]; otherwise, <c>false</c>.</returns>
+ /// <exception cref="System.ArgumentNullException">path</exception>
+ private static bool IsVideoPlaceHolder(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentNullException("path");
+ }
+
+ var extension = Path.GetExtension(path);
+
+ return string.Equals(extension, ".disc", StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// Determines whether [is season folder] [the specified path].
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="isTvContentType">if set to <c>true</c> [is tv content type].</param>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <returns><c>true</c> if [is season folder] [the specified path]; otherwise, <c>false</c>.</returns>
+ private static bool IsSeasonFolder(string path, bool isTvContentType, ILibraryManager libraryManager)
+ {
+ var namingOptions = ((LibraryManager)libraryManager).GetNamingOptions();
+
+ var seasonNumber = new SeasonPathParser(namingOptions, new RegexProvider()).Parse(path, isTvContentType, isTvContentType).SeasonNumber;
+
+ return seasonNumber.HasValue;
+ }
+
+ /// <summary>
+ /// Sets the initial item values.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="args">The args.</param>
+ protected override void SetInitialItemValues(Series item, ItemResolveArgs args)
+ {
+ base.SetInitialItemValues(item, args);
+
+ SetProviderIdFromPath(item, args.Path);
+ }
+
+ /// <summary>
+ /// Sets the provider id from path.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="path">The path.</param>
+ private void SetProviderIdFromPath(Series item, string path)
+ {
+ var justName = Path.GetFileName(path);
+
+ var id = justName.GetAttributeValue("tvdbid");
+
+ if (!string.IsNullOrEmpty(id))
+ {
+ item.SetProviderId(MetadataProviders.Tvdb, id);
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs
new file mode 100644
index 000000000..b5e1bf5f7
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs
@@ -0,0 +1,45 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Resolvers;
+
+namespace Emby.Server.Implementations.Library.Resolvers
+{
+ /// <summary>
+ /// Resolves a Path into a Video
+ /// </summary>
+ public class VideoResolver : BaseVideoResolver<Video>
+ {
+ public VideoResolver(ILibraryManager libraryManager)
+ : base(libraryManager)
+ {
+ }
+
+ protected override Video Resolve(ItemResolveArgs args)
+ {
+ if (args.Parent != null)
+ {
+ // The movie resolver will handle this
+ return null;
+ }
+
+ return base.Resolve(args);
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override ResolverPriority Priority
+ {
+ get { return ResolverPriority.Last; }
+ }
+ }
+
+ public class GenericVideoResolver<T> : BaseVideoResolver<T>
+ where T : Video, new ()
+ {
+ public GenericVideoResolver(ILibraryManager libraryManager) : base(libraryManager)
+ {
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
new file mode 100644
index 000000000..afdf65c06
--- /dev/null
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -0,0 +1,275 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Querying;
+using MediaBrowser.Model.Search;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Extensions;
+
+namespace Emby.Server.Implementations.Library
+{
+ /// <summary>
+ /// </summary>
+ public class SearchEngine : ISearchEngine
+ {
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly ILogger _logger;
+
+ public SearchEngine(ILogManager logManager, ILibraryManager libraryManager, IUserManager userManager)
+ {
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+
+ _logger = logManager.GetLogger("Lucene");
+ }
+
+ public async Task<QueryResult<SearchHintInfo>> GetSearchHints(SearchQuery query)
+ {
+ User user = null;
+
+ if (string.IsNullOrWhiteSpace(query.UserId))
+ {
+ }
+ else
+ {
+ user = _userManager.GetUserById(query.UserId);
+ }
+
+ var results = await GetSearchHints(query, user).ConfigureAwait(false);
+
+ var searchResultArray = results.ToArray();
+ results = searchResultArray;
+
+ var count = searchResultArray.Length;
+
+ if (query.StartIndex.HasValue)
+ {
+ results = results.Skip(query.StartIndex.Value);
+ }
+
+ if (query.Limit.HasValue)
+ {
+ results = results.Take(query.Limit.Value);
+ }
+
+ return new QueryResult<SearchHintInfo>
+ {
+ TotalRecordCount = count,
+
+ Items = results.ToArray()
+ };
+ }
+
+ private void AddIfMissing(List<string> list, string value)
+ {
+ if (!list.Contains(value, StringComparer.OrdinalIgnoreCase))
+ {
+ list.Add(value);
+ }
+ }
+
+ /// <summary>
+ /// Gets the search hints.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <param name="user">The user.</param>
+ /// <returns>IEnumerable{SearchHintResult}.</returns>
+ /// <exception cref="System.ArgumentNullException">searchTerm</exception>
+ private Task<IEnumerable<SearchHintInfo>> GetSearchHints(SearchQuery query, User user)
+ {
+ var searchTerm = query.SearchTerm;
+
+ if (searchTerm != null)
+ {
+ searchTerm = searchTerm.Trim().RemoveDiacritics();
+ }
+
+ if (string.IsNullOrWhiteSpace(searchTerm))
+ {
+ throw new ArgumentNullException("searchTerm");
+ }
+
+ var terms = GetWords(searchTerm);
+
+ var hints = new List<Tuple<BaseItem, string, int>>();
+
+ var excludeItemTypes = new List<string>();
+ var includeItemTypes = (query.IncludeItemTypes ?? new string[] { }).ToList();
+
+ excludeItemTypes.Add(typeof(Year).Name);
+ excludeItemTypes.Add(typeof(Folder).Name);
+
+ if (query.IncludeGenres && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Genre", StringComparer.OrdinalIgnoreCase)))
+ {
+ if (!query.IncludeMedia)
+ {
+ AddIfMissing(includeItemTypes, typeof(Genre).Name);
+ AddIfMissing(includeItemTypes, typeof(GameGenre).Name);
+ AddIfMissing(includeItemTypes, typeof(MusicGenre).Name);
+ }
+ }
+ else
+ {
+ AddIfMissing(excludeItemTypes, typeof(Genre).Name);
+ AddIfMissing(excludeItemTypes, typeof(GameGenre).Name);
+ AddIfMissing(excludeItemTypes, typeof(MusicGenre).Name);
+ }
+
+ if (query.IncludePeople && (includeItemTypes.Count == 0 || includeItemTypes.Contains("People", StringComparer.OrdinalIgnoreCase) || includeItemTypes.Contains("Person", StringComparer.OrdinalIgnoreCase)))
+ {
+ if (!query.IncludeMedia)
+ {
+ AddIfMissing(includeItemTypes, typeof(Person).Name);
+ }
+ }
+ else
+ {
+ AddIfMissing(excludeItemTypes, typeof(Person).Name);
+ }
+
+ if (query.IncludeStudios && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Studio", StringComparer.OrdinalIgnoreCase)))
+ {
+ if (!query.IncludeMedia)
+ {
+ AddIfMissing(includeItemTypes, typeof(Studio).Name);
+ }
+ }
+ else
+ {
+ AddIfMissing(excludeItemTypes, typeof(Studio).Name);
+ }
+
+ if (query.IncludeArtists && (includeItemTypes.Count == 0 || includeItemTypes.Contains("MusicArtist", StringComparer.OrdinalIgnoreCase)))
+ {
+ if (!query.IncludeMedia)
+ {
+ AddIfMissing(includeItemTypes, typeof(MusicArtist).Name);
+ }
+ }
+ else
+ {
+ AddIfMissing(excludeItemTypes, typeof(MusicArtist).Name);
+ }
+
+ AddIfMissing(excludeItemTypes, typeof(CollectionFolder).Name);
+
+ var mediaItems = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ {
+ NameContains = searchTerm,
+ ExcludeItemTypes = excludeItemTypes.ToArray(),
+ IncludeItemTypes = includeItemTypes.ToArray(),
+ Limit = query.Limit,
+ IncludeItemsByName = true,
+ IsVirtualItem = false
+ });
+
+ // Add search hints based on item name
+ hints.AddRange(mediaItems.Select(item =>
+ {
+ var index = GetIndex(item.Name, searchTerm, terms);
+
+ return new Tuple<BaseItem, string, int>(item, index.Item1, index.Item2);
+ }));
+
+ var returnValue = hints.Where(i => i.Item3 >= 0).OrderBy(i => i.Item3).Select(i => new SearchHintInfo
+ {
+ Item = i.Item1,
+ MatchedTerm = i.Item2
+ });
+
+ return Task.FromResult(returnValue);
+ }
+
+ /// <summary>
+ /// Gets the index.
+ /// </summary>
+ /// <param name="input">The input.</param>
+ /// <param name="searchInput">The search input.</param>
+ /// <param name="searchWords">The search input.</param>
+ /// <returns>System.Int32.</returns>
+ private Tuple<string, int> GetIndex(string input, string searchInput, List<string> searchWords)
+ {
+ if (string.IsNullOrWhiteSpace(input))
+ {
+ throw new ArgumentNullException("input");
+ }
+
+ input = input.RemoveDiacritics();
+
+ if (string.Equals(input, searchInput, StringComparison.OrdinalIgnoreCase))
+ {
+ return new Tuple<string, int>(searchInput, 0);
+ }
+
+ var index = input.IndexOf(searchInput, StringComparison.OrdinalIgnoreCase);
+
+ if (index == 0)
+ {
+ return new Tuple<string, int>(searchInput, 1);
+ }
+ if (index > 0)
+ {
+ return new Tuple<string, int>(searchInput, 2);
+ }
+
+ var items = GetWords(input);
+
+ for (var i = 0; i < searchWords.Count; i++)
+ {
+ var searchTerm = searchWords[i];
+
+ for (var j = 0; j < items.Count; j++)
+ {
+ var item = items[j];
+
+ if (string.Equals(item, searchTerm, StringComparison.OrdinalIgnoreCase))
+ {
+ return new Tuple<string, int>(searchTerm, 3 + (i + 1) * (j + 1));
+ }
+
+ index = item.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase);
+
+ if (index == 0)
+ {
+ return new Tuple<string, int>(searchTerm, 4 + (i + 1) * (j + 1));
+ }
+ if (index > 0)
+ {
+ return new Tuple<string, int>(searchTerm, 5 + (i + 1) * (j + 1));
+ }
+ }
+ }
+ return new Tuple<string, int>(null, -1);
+ }
+
+ /// <summary>
+ /// Gets the words.
+ /// </summary>
+ /// <param name="term">The term.</param>
+ /// <returns>System.String[][].</returns>
+ private List<string> GetWords(string term)
+ {
+ var stoplist = GetStopList().ToList();
+
+ return term.Split()
+ .Where(i => !string.IsNullOrWhiteSpace(i) && !stoplist.Contains(i, StringComparer.OrdinalIgnoreCase))
+ .ToList();
+ }
+
+ private IEnumerable<string> GetStopList()
+ {
+ return new[]
+ {
+ "the",
+ "a",
+ "of",
+ "an"
+ };
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
new file mode 100644
index 000000000..b93f565a3
--- /dev/null
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -0,0 +1,292 @@
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Channels;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Library;
+using MediaBrowser.Model.Querying;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Model.Globalization;
+
+namespace Emby.Server.Implementations.Library
+{
+ public class UserViewManager : IUserViewManager
+ {
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILocalizationManager _localizationManager;
+ private readonly IUserManager _userManager;
+
+ private readonly IChannelManager _channelManager;
+ private readonly ILiveTvManager _liveTvManager;
+ private readonly IServerConfigurationManager _config;
+
+ public UserViewManager(ILibraryManager libraryManager, ILocalizationManager localizationManager, IUserManager userManager, IChannelManager channelManager, ILiveTvManager liveTvManager, IServerConfigurationManager config)
+ {
+ _libraryManager = libraryManager;
+ _localizationManager = localizationManager;
+ _userManager = userManager;
+ _channelManager = channelManager;
+ _liveTvManager = liveTvManager;
+ _config = config;
+ }
+
+ public async Task<IEnumerable<Folder>> GetUserViews(UserViewQuery query, CancellationToken cancellationToken)
+ {
+ var user = _userManager.GetUserById(query.UserId);
+
+ var folders = user.RootFolder
+ .GetChildren(user, true)
+ .OfType<Folder>()
+ .ToList();
+
+ if (!query.IncludeHidden)
+ {
+ folders = folders.Where(i =>
+ {
+ var hidden = i as IHiddenFromDisplay;
+ return hidden == null || !hidden.IsHiddenFromUser(user);
+ }).ToList();
+ }
+
+ var plainFolderIds = user.Configuration.PlainFolderViews.Select(i => new Guid(i)).ToList();
+
+ var groupedFolders = new List<ICollectionFolder>();
+
+ var list = new List<Folder>();
+
+ foreach (var folder in folders)
+ {
+ var collectionFolder = folder as ICollectionFolder;
+ var folderViewType = collectionFolder == null ? null : collectionFolder.CollectionType;
+
+ if (UserView.IsUserSpecific(folder))
+ {
+ list.Add(await _libraryManager.GetNamedView(user, folder.Name, folder.Id.ToString("N"), folderViewType, null, cancellationToken).ConfigureAwait(false));
+ continue;
+ }
+
+ if (plainFolderIds.Contains(folder.Id) && UserView.IsEligibleForEnhancedView(folderViewType))
+ {
+ list.Add(folder);
+ continue;
+ }
+
+ if (collectionFolder != null && UserView.IsEligibleForGrouping(folder) && user.IsFolderGrouped(folder.Id))
+ {
+ groupedFolders.Add(collectionFolder);
+ continue;
+ }
+
+ if (query.PresetViews.Contains(folderViewType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ {
+ list.Add(await GetUserView(folder, folderViewType, string.Empty, cancellationToken).ConfigureAwait(false));
+ }
+ else
+ {
+ list.Add(folder);
+ }
+ }
+
+ foreach (var viewType in new[] { CollectionType.Movies, CollectionType.TvShows })
+ {
+ var parents = groupedFolders.Where(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(i.CollectionType))
+ .ToList();
+
+ if (parents.Count > 0)
+ {
+ list.Add(await GetUserView(parents, viewType, string.Empty, user, query.PresetViews, cancellationToken).ConfigureAwait(false));
+ }
+ }
+
+ if (_config.Configuration.EnableFolderView)
+ {
+ var name = _localizationManager.GetLocalizedString("ViewType" + CollectionType.Folders);
+ list.Add(await _libraryManager.GetNamedView(name, CollectionType.Folders, string.Empty, cancellationToken).ConfigureAwait(false));
+ }
+
+ if (query.IncludeExternalContent)
+ {
+ var channelResult = await _channelManager.GetChannelsInternal(new ChannelQuery
+ {
+ UserId = query.UserId
+
+ }, cancellationToken).ConfigureAwait(false);
+
+ var channels = channelResult.Items;
+
+ if (_config.Configuration.EnableChannelView && channels.Length > 0)
+ {
+ list.Add(await _channelManager.GetInternalChannelFolder(cancellationToken).ConfigureAwait(false));
+ }
+ else
+ {
+ list.AddRange(channels);
+ }
+
+ if (_liveTvManager.GetEnabledUsers().Select(i => i.Id.ToString("N")).Contains(query.UserId))
+ {
+ list.Add(await _liveTvManager.GetInternalLiveTvFolder(CancellationToken.None).ConfigureAwait(false));
+ }
+ }
+
+ var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
+
+ var orders = user.Configuration.OrderedViews.ToList();
+
+ return list
+ .OrderBy(i =>
+ {
+ var index = orders.IndexOf(i.Id.ToString("N"));
+
+ if (index == -1)
+ {
+ var view = i as UserView;
+ if (view != null)
+ {
+ if (view.DisplayParentId != Guid.Empty)
+ {
+ index = orders.IndexOf(view.DisplayParentId.ToString("N"));
+ }
+ }
+ }
+
+ return index == -1 ? int.MaxValue : index;
+ })
+ .ThenBy(sorted.IndexOf)
+ .ThenBy(i => i.SortName);
+ }
+
+ public Task<UserView> GetUserSubView(string name, string parentId, string type, string sortName, CancellationToken cancellationToken)
+ {
+ var uniqueId = parentId + "subview" + type;
+
+ return _libraryManager.GetNamedView(name, parentId, type, sortName, uniqueId, cancellationToken);
+ }
+
+ public Task<UserView> GetUserSubView(string parentId, string type, string sortName, CancellationToken cancellationToken)
+ {
+ var name = _localizationManager.GetLocalizedString("ViewType" + type);
+
+ return GetUserSubView(name, parentId, type, sortName, cancellationToken);
+ }
+
+ private async Task<Folder> GetUserView(List<ICollectionFolder> parents, string viewType, string sortName, User user, string[] presetViews, CancellationToken cancellationToken)
+ {
+ if (parents.Count == 1 && parents.All(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase)))
+ {
+ if (!presetViews.Contains(viewType, StringComparer.OrdinalIgnoreCase))
+ {
+ return (Folder)parents[0];
+ }
+
+ return await GetUserView((Folder)parents[0], viewType, string.Empty, cancellationToken).ConfigureAwait(false);
+ }
+
+ var name = _localizationManager.GetLocalizedString("ViewType" + viewType);
+ return await _libraryManager.GetNamedView(user, name, viewType, sortName, cancellationToken).ConfigureAwait(false);
+ }
+
+ public Task<UserView> GetUserView(Folder parent, string viewType, string sortName, CancellationToken cancellationToken)
+ {
+ return _libraryManager.GetShadowView(parent, viewType, sortName, cancellationToken);
+ }
+
+ public List<Tuple<BaseItem, List<BaseItem>>> GetLatestItems(LatestItemsQuery request)
+ {
+ var user = _userManager.GetUserById(request.UserId);
+
+ var libraryItems = GetItemsForLatestItems(user, request);
+
+ var list = new List<Tuple<BaseItem, List<BaseItem>>>();
+
+ foreach (var item in libraryItems)
+ {
+ // Only grab the index container for media
+ var container = item.IsFolder || !request.GroupItems ? null : item.LatestItemsIndexContainer;
+
+ if (container == null)
+ {
+ list.Add(new Tuple<BaseItem, List<BaseItem>>(null, new List<BaseItem> { item }));
+ }
+ else
+ {
+ var current = list.FirstOrDefault(i => i.Item1 != null && i.Item1.Id == container.Id);
+
+ if (current != null)
+ {
+ current.Item2.Add(item);
+ }
+ else
+ {
+ list.Add(new Tuple<BaseItem, List<BaseItem>>(container, new List<BaseItem> { item }));
+ }
+ }
+
+ if (list.Count >= request.Limit)
+ {
+ break;
+ }
+ }
+
+ return list;
+ }
+
+ private IEnumerable<BaseItem> GetItemsForLatestItems(User user, LatestItemsQuery request)
+ {
+ var parentId = request.ParentId;
+
+ var includeItemTypes = request.IncludeItemTypes;
+ var limit = request.Limit ?? 10;
+
+ var parentIds = string.IsNullOrEmpty(parentId)
+ ? new string[] { }
+ : new[] { parentId };
+
+ if (parentIds.Length == 0)
+ {
+ parentIds = user.RootFolder.GetChildren(user, true)
+ .OfType<Folder>()
+ .Select(i => i.Id.ToString("N"))
+ .Where(i => !user.Configuration.LatestItemsExcludes.Contains(i))
+ .ToArray();
+ }
+
+ if (parentIds.Length == 0)
+ {
+ return new List<BaseItem>();
+ }
+
+ var excludeItemTypes = includeItemTypes.Length == 0 ? new[]
+ {
+ typeof(Person).Name,
+ typeof(Studio).Name,
+ typeof(Year).Name,
+ typeof(GameGenre).Name,
+ typeof(MusicGenre).Name,
+ typeof(Genre).Name
+
+ } : new string[] { };
+
+ return _libraryManager.GetItemList(new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = includeItemTypes,
+ SortOrder = SortOrder.Descending,
+ SortBy = new[] { ItemSortBy.DateCreated },
+ IsFolder = includeItemTypes.Length == 0 ? false : (bool?)null,
+ ExcludeItemTypes = excludeItemTypes,
+ ExcludeLocationTypes = new[] { LocationType.Virtual },
+ Limit = limit * 5,
+ SourceTypes = parentIds.Length == 0 ? new[] { SourceType.Library } : new SourceType[] { },
+ IsPlayed = request.IsPlayed
+
+ }, parentIds);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs
new file mode 100644
index 000000000..4d718dbee
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs
@@ -0,0 +1,44 @@
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Persistence;
+
+namespace Emby.Server.Implementations.Library.Validators
+{
+ /// <summary>
+ /// Class ArtistsPostScanTask
+ /// </summary>
+ public class ArtistsPostScanTask : ILibraryPostScanTask
+ {
+ /// <summary>
+ /// The _library manager
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger _logger;
+ private readonly IItemRepository _itemRepo;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ public ArtistsPostScanTask(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
+
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ return new ArtistsValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
new file mode 100644
index 000000000..643c5970e
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
@@ -0,0 +1,84 @@
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Persistence;
+
+namespace Emby.Server.Implementations.Library.Validators
+{
+ /// <summary>
+ /// Class ArtistsValidator
+ /// </summary>
+ public class ArtistsValidator
+ {
+ /// <summary>
+ /// The _library manager
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// The _logger
+ /// </summary>
+ private readonly ILogger _logger;
+ private readonly IItemRepository _itemRepo;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ public ArtistsValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
+
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var names = _itemRepo.GetAllArtistNames();
+
+ var numComplete = 0;
+ var count = names.Count;
+
+ foreach (var name in names)
+ {
+ try
+ {
+ var item = _libraryManager.GetArtist(name);
+
+ await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ // Don't clutter the log
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error refreshing {0}", ex, name);
+ }
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= count;
+ percent *= 100;
+
+ progress.Report(percent);
+ }
+
+ progress.Report(100);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Validators/GameGenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/GameGenresPostScanTask.cs
new file mode 100644
index 000000000..ee6c4461c
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Validators/GameGenresPostScanTask.cs
@@ -0,0 +1,45 @@
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Persistence;
+
+namespace Emby.Server.Implementations.Library.Validators
+{
+ /// <summary>
+ /// Class GameGenresPostScanTask
+ /// </summary>
+ public class GameGenresPostScanTask : ILibraryPostScanTask
+ {
+ /// <summary>
+ /// The _library manager
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger _logger;
+ private readonly IItemRepository _itemRepo;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="GameGenresPostScanTask" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ public GameGenresPostScanTask(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
+
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ return new GameGenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Validators/GameGenresValidator.cs b/Emby.Server.Implementations/Library/Validators/GameGenresValidator.cs
new file mode 100644
index 000000000..b1820bb91
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Validators/GameGenresValidator.cs
@@ -0,0 +1,74 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Persistence;
+
+namespace Emby.Server.Implementations.Library.Validators
+{
+ class GameGenresValidator
+ {
+ /// <summary>
+ /// The _library manager
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// The _logger
+ /// </summary>
+ private readonly ILogger _logger;
+ private readonly IItemRepository _itemRepo;
+
+ public GameGenresValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
+
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var names = _itemRepo.GetGameGenreNames();
+
+ var numComplete = 0;
+ var count = names.Count;
+
+ foreach (var name in names)
+ {
+ try
+ {
+ var item = _libraryManager.GetGameGenre(name);
+
+ await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ // Don't clutter the log
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error refreshing {0}", ex, name);
+ }
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= count;
+ percent *= 100;
+
+ progress.Report(percent);
+ }
+
+ progress.Report(100);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs
new file mode 100644
index 000000000..be46decfb
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs
@@ -0,0 +1,42 @@
+using MediaBrowser.Controller.Library;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Logging;
+
+namespace Emby.Server.Implementations.Library.Validators
+{
+ public class GenresPostScanTask : ILibraryPostScanTask
+ {
+ /// <summary>
+ /// The _library manager
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger _logger;
+ private readonly IItemRepository _itemRepo;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ public GenresPostScanTask(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
+
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ return new GenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs
new file mode 100644
index 000000000..d8956f78a
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs
@@ -0,0 +1,75 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Persistence;
+
+namespace Emby.Server.Implementations.Library.Validators
+{
+ class GenresValidator
+ {
+ /// <summary>
+ /// The _library manager
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+ private readonly IItemRepository _itemRepo;
+
+ /// <summary>
+ /// The _logger
+ /// </summary>
+ private readonly ILogger _logger;
+
+ public GenresValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
+
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var names = _itemRepo.GetGenreNames();
+
+ var numComplete = 0;
+ var count = names.Count;
+
+ foreach (var name in names)
+ {
+ try
+ {
+ var item = _libraryManager.GetGenre(name);
+
+ await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ // Don't clutter the log
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error refreshing {0}", ex, name);
+ }
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= count;
+ percent *= 100;
+
+ progress.Report(percent);
+ }
+
+ progress.Report(100);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs
new file mode 100644
index 000000000..cd4021548
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs
@@ -0,0 +1,45 @@
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Persistence;
+
+namespace Emby.Server.Implementations.Library.Validators
+{
+ /// <summary>
+ /// Class MusicGenresPostScanTask
+ /// </summary>
+ public class MusicGenresPostScanTask : ILibraryPostScanTask
+ {
+ /// <summary>
+ /// The _library manager
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger _logger;
+ private readonly IItemRepository _itemRepo;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ public MusicGenresPostScanTask(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
+
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ return new MusicGenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs b/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs
new file mode 100644
index 000000000..983c881b7
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs
@@ -0,0 +1,75 @@
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Persistence;
+
+namespace Emby.Server.Implementations.Library.Validators
+{
+ class MusicGenresValidator
+ {
+ /// <summary>
+ /// The _library manager
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// The _logger
+ /// </summary>
+ private readonly ILogger _logger;
+ private readonly IItemRepository _itemRepo;
+
+ public MusicGenresValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
+
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var names = _itemRepo.GetMusicGenreNames();
+
+ var numComplete = 0;
+ var count = names.Count;
+
+ foreach (var name in names)
+ {
+ try
+ {
+ var item = _libraryManager.GetMusicGenre(name);
+
+ await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ // Don't clutter the log
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error refreshing {0}", ex, name);
+ }
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= count;
+ percent *= 100;
+
+ progress.Report(percent);
+ }
+
+ progress.Report(100);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
new file mode 100644
index 000000000..813f07fff
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
@@ -0,0 +1,172 @@
+using MediaBrowser.Common.Progress;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Server.Implementations.Library.Validators
+{
+ /// <summary>
+ /// Class PeopleValidator
+ /// </summary>
+ public class PeopleValidator
+ {
+ /// <summary>
+ /// The _library manager
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+ /// <summary>
+ /// The _logger
+ /// </summary>
+ private readonly ILogger _logger;
+
+ private readonly IServerConfigurationManager _config;
+ private readonly IFileSystem _fileSystem;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PeopleValidator" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="logger">The logger.</param>
+ public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _config = config;
+ _fileSystem = fileSystem;
+ }
+
+ private bool DownloadMetadata(PersonInfo i, PeopleMetadataOptions options)
+ {
+ if (i.IsType(PersonType.Actor))
+ {
+ return options.DownloadActorMetadata;
+ }
+ if (i.IsType(PersonType.Director))
+ {
+ return options.DownloadDirectorMetadata;
+ }
+ if (i.IsType(PersonType.Composer))
+ {
+ return options.DownloadComposerMetadata;
+ }
+ if (i.IsType(PersonType.Writer))
+ {
+ return options.DownloadWriterMetadata;
+ }
+ if (i.IsType(PersonType.Producer))
+ {
+ return options.DownloadProducerMetadata;
+ }
+ if (i.IsType(PersonType.GuestStar))
+ {
+ return options.DownloadGuestStarMetadata;
+ }
+
+ return options.DownloadOtherPeopleMetadata;
+ }
+
+ /// <summary>
+ /// Validates the people.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ public async Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ var innerProgress = new ActionableProgress<double>();
+
+ innerProgress.RegisterAction(pct => progress.Report(pct * .15));
+
+ var peopleOptions = _config.Configuration.PeopleMetadataOptions;
+
+ var people = _libraryManager.GetPeople(new InternalPeopleQuery());
+
+ var dict = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var person in people)
+ {
+ var isMetadataEnabled = DownloadMetadata(person, peopleOptions);
+
+ bool currentValue;
+ if (dict.TryGetValue(person.Name, out currentValue))
+ {
+ if (!currentValue && isMetadataEnabled)
+ {
+ dict[person.Name] = true;
+ }
+ }
+ else
+ {
+ dict[person.Name] = isMetadataEnabled;
+ }
+ }
+
+ var numComplete = 0;
+
+ _logger.Debug("Will refresh {0} people", dict.Count);
+
+ var numPeople = dict.Count;
+
+ foreach (var person in dict)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ var item = _libraryManager.GetPerson(person.Key);
+
+ var hasMetdata = !string.IsNullOrWhiteSpace(item.Overview);
+ var performFullRefresh = !hasMetdata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 30;
+
+ var defaultMetadataRefreshMode = performFullRefresh
+ ? MetadataRefreshMode.FullRefresh
+ : MetadataRefreshMode.Default;
+
+ var imageRefreshMode = performFullRefresh
+ ? ImageRefreshMode.FullRefresh
+ : ImageRefreshMode.Default;
+
+ var options = new MetadataRefreshOptions(_fileSystem)
+ {
+ MetadataRefreshMode = person.Value ? defaultMetadataRefreshMode : MetadataRefreshMode.ValidationOnly,
+ ImageRefreshMode = person.Value ? imageRefreshMode : ImageRefreshMode.ValidationOnly,
+ ForceSave = performFullRefresh
+ };
+
+ await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error validating IBN entry {0}", ex, person);
+ }
+
+ // Update progress
+ numComplete++;
+ double percent = numComplete;
+ percent /= numPeople;
+
+ progress.Report(100 * percent);
+ }
+
+ progress.Report(100);
+
+ _logger.Info("People validation complete");
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs
new file mode 100644
index 000000000..d23efb6d3
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs
@@ -0,0 +1,45 @@
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Persistence;
+
+namespace Emby.Server.Implementations.Library.Validators
+{
+ /// <summary>
+ /// Class MusicGenresPostScanTask
+ /// </summary>
+ public class StudiosPostScanTask : ILibraryPostScanTask
+ {
+ /// <summary>
+ /// The _library manager
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+
+ private readonly ILogger _logger;
+ private readonly IItemRepository _itemRepo;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ public StudiosPostScanTask(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
+
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ return new StudiosValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
new file mode 100644
index 000000000..6faab7bb9
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
@@ -0,0 +1,74 @@
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Persistence;
+
+namespace Emby.Server.Implementations.Library.Validators
+{
+ class StudiosValidator
+ {
+ /// <summary>
+ /// The _library manager
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+
+ private readonly IItemRepository _itemRepo;
+ /// <summary>
+ /// The _logger
+ /// </summary>
+ private readonly ILogger _logger;
+
+ public StudiosValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _itemRepo = itemRepo;
+ }
+
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var names = _itemRepo.GetStudioNames();
+
+ var numComplete = 0;
+ var count = names.Count;
+
+ foreach (var name in names)
+ {
+ try
+ {
+ var item = _libraryManager.GetStudio(name);
+
+ await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ // Don't clutter the log
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error refreshing {0}", ex, name);
+ }
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= count;
+ percent *= 100;
+
+ progress.Report(percent);
+ }
+
+ progress.Report(100);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Validators/YearsPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/YearsPostScanTask.cs
new file mode 100644
index 000000000..ae43c77f0
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Validators/YearsPostScanTask.cs
@@ -0,0 +1,55 @@
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Emby.Server.Implementations.Library.Validators
+{
+ public class YearsPostScanTask : ILibraryPostScanTask
+ {
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger _logger;
+
+ public YearsPostScanTask(ILibraryManager libraryManager, ILogger logger)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ }
+
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var yearNumber = 1900;
+ var maxYear = DateTime.UtcNow.Year + 3;
+ var count = maxYear - yearNumber + 1;
+ var numComplete = 0;
+
+ while (yearNumber < maxYear)
+ {
+ try
+ {
+ var year = _libraryManager.GetYear(yearNumber);
+
+ await year.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ // Don't clutter the log
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error refreshing year {0}", ex, yearNumber);
+ }
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= count;
+ percent *= 100;
+
+ progress.Report(percent);
+ yearNumber++;
+ }
+ }
+ }
+}