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.cs88
-rw-r--r--Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs105
-rw-r--r--Emby.Server.Implementations/Library/ExclusiveLiveStream.cs42
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs700
-rw-r--r--Emby.Server.Implementations/Library/LiveStreamHelper.cs181
-rw-r--r--Emby.Server.Implementations/Library/LocalTrailerPostScanTask.cs105
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs599
-rw-r--r--Emby.Server.Implementations/Library/MediaStreamSelector.cs217
-rw-r--r--Emby.Server.Implementations/Library/MusicManager.cs12
-rw-r--r--Emby.Server.Implementations/Library/ResolverHelper.cs24
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs8
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs33
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs5
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs8
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs36
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs3
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs27
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs16
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs22
-rw-r--r--Emby.Server.Implementations/Library/SearchEngine.cs152
-rw-r--r--Emby.Server.Implementations/Library/UserDataManager.cs106
-rw-r--r--Emby.Server.Implementations/Library/UserManager.cs369
-rw-r--r--Emby.Server.Implementations/Library/UserViewManager.cs110
-rw-r--r--Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs28
-rw-r--r--Emby.Server.Implementations/Library/Validators/PeopleValidator.cs28
-rw-r--r--Emby.Server.Implementations/Library/Validators/StudiosValidator.cs18
26 files changed, 1994 insertions, 1048 deletions
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
index 5c3e1dab1..59f0a9fc9 100644
--- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
+++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
@@ -22,10 +22,12 @@ namespace Emby.Server.Implementations.Library
private readonly ILibraryManager _libraryManager;
private readonly ILogger _logger;
+ private bool _ignoreDotPrefix;
+
/// <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>
+ public static readonly Dictionary<string, string> IgnoreFolders = new List<string>
{
"metadata",
"ps3_update",
@@ -41,15 +43,24 @@ namespace Emby.Server.Implementations.Library
"#recycle",
// Qnap
- "@Recycle"
+ "@Recycle",
+ ".@__thumb",
+ "$RECYCLE.BIN",
+ "System Volume Information",
+ ".grab",
+
+ // macos
+ ".AppleDouble"
+
+ }.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
- };
-
public CoreResolutionIgnoreRule(IFileSystem fileSystem, ILibraryManager libraryManager, ILogger logger)
{
_fileSystem = fileSystem;
_libraryManager = libraryManager;
_logger = logger;
+
+ _ignoreDotPrefix = Environment.OSVersion.Platform != PlatformID.Win32NT;
}
/// <summary>
@@ -67,46 +78,48 @@ namespace Emby.Server.Implementations.Library
}
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)
+ if (_ignoreDotPrefix)
{
- return true;
- }
-
- // Ignore hidden files and folders
- if (isHidden)
- {
- if (parent == null)
+ if (filename.IndexOf('.') == 0)
{
- var parentFolderName = Path.GetFileName(_fileSystem.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;
}
-
- return true;
}
+ // Ignore hidden files and folders
+ //if (fileInfo.IsHidden)
+ //{
+ // if (parent == null)
+ // {
+ // var parentFolderName = Path.GetFileName(_fileSystem.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))
+ if (IgnoreFolders.ContainsKey(filename))
{
return true;
}
@@ -141,6 +154,17 @@ namespace Emby.Server.Implementations.Library
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/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs
new file mode 100644
index 000000000..7c79a7c69
--- /dev/null
+++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Cryptography;
+
+namespace Emby.Server.Implementations.Library
+{
+ public class DefaultAuthenticationProvider : IAuthenticationProvider, IRequiresResolvedUser
+ {
+ private readonly ICryptoProvider _cryptographyProvider;
+ public DefaultAuthenticationProvider(ICryptoProvider crypto)
+ {
+ _cryptographyProvider = crypto;
+ }
+
+ public string Name => "Default";
+
+ public bool IsEnabled => true;
+
+ public Task<ProviderAuthenticationResult> Authenticate(string username, string password)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser)
+ {
+ if (resolvedUser == null)
+ {
+ throw new Exception("Invalid username or password");
+ }
+
+ var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase);
+
+ if (!success)
+ {
+ throw new Exception("Invalid username or password");
+ }
+
+ return Task.FromResult(new ProviderAuthenticationResult
+ {
+ Username = username
+ });
+ }
+
+ public Task<bool> HasPassword(User user)
+ {
+ var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user));
+ return Task.FromResult(hasConfiguredPassword);
+ }
+
+ private bool IsPasswordEmpty(User user, string passwordHash)
+ {
+ return string.Equals(passwordHash, GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase);
+ }
+
+ public Task ChangePassword(User user, string newPassword)
+ {
+ string newPasswordHash = null;
+
+ if (newPassword != null)
+ {
+ newPasswordHash = GetHashedString(user, newPassword);
+ }
+
+ if (string.IsNullOrWhiteSpace(newPasswordHash))
+ {
+ throw new ArgumentNullException("newPasswordHash");
+ }
+
+ user.Password = newPasswordHash;
+
+ return Task.CompletedTask;
+ }
+
+ public string GetPasswordHash(User user)
+ {
+ return string.IsNullOrEmpty(user.Password)
+ ? GetEmptyHashedString(user)
+ : user.Password;
+ }
+
+ public string GetEmptyHashedString(User user)
+ {
+ return GetHashedString(user, string.Empty);
+ }
+
+ /// <summary>
+ /// Gets the hashed string.
+ /// </summary>
+ public string GetHashedString(User user, string str)
+ {
+ var salt = user.Salt;
+ if (salt != null)
+ {
+ // return BCrypt.HashPassword(str, salt);
+ }
+
+ // legacy
+ return BitConverter.ToString(_cryptographyProvider.ComputeSHA1(Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
new file mode 100644
index 000000000..186ec63da
--- /dev/null
+++ b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Controller.Library;
+
+namespace Emby.Server.Implementations.Library
+{
+ public class ExclusiveLiveStream : ILiveStream
+ {
+ public int ConsumerCount { get; set; }
+ public string OriginalStreamId { get; set; }
+
+ public string TunerHostId => null;
+
+ public bool EnableStreamSharing { get; set; }
+ public MediaSourceInfo MediaSource { get; set; }
+
+ public string UniqueId { get; private set; }
+
+ private Func<Task> _closeFn;
+
+ public ExclusiveLiveStream(MediaSourceInfo mediaSource, Func<Task> closeFn)
+ {
+ MediaSource = mediaSource;
+ EnableStreamSharing = false;
+ _closeFn = closeFn;
+ ConsumerCount = 1;
+ UniqueId = Guid.NewGuid().ToString("N");
+ }
+
+ public Task Close()
+ {
+ return _closeFn();
+ }
+
+ public Task Open(CancellationToken openCancellationToken)
+ {
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 2934a5147..31af9370c 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -44,6 +44,9 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Tasks;
+using Emby.Server.Implementations.Playlists;
+using MediaBrowser.Providers.MediaInfo;
+using MediaBrowser.Controller;
namespace Emby.Server.Implementations.Library
{
@@ -71,12 +74,6 @@ namespace Emby.Server.Implementations.Library
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>
@@ -140,6 +137,7 @@ namespace Emby.Server.Implementations.Library
private readonly Func<IProviderManager> _providerManagerFactory;
private readonly Func<IUserViewManager> _userviewManager;
public bool IsScanRunning { get; private set; }
+ private IServerApplicationHost _appHost;
/// <summary>
/// The _library items cache
@@ -167,7 +165,7 @@ namespace Emby.Server.Implementations.Library
/// <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)
+ public LibraryManager(IServerApplicationHost appHost, 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;
@@ -178,6 +176,7 @@ namespace Emby.Server.Implementations.Library
_fileSystem = fileSystem;
_providerManagerFactory = providerManagerFactory;
_userviewManager = userviewManager;
+ _appHost = appHost;
_libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>();
ConfigurationManager.ConfigurationUpdated += ConfigurationUpdated;
@@ -195,14 +194,12 @@ namespace Emby.Server.Implementations.Library
/// <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();
@@ -302,7 +299,7 @@ namespace Emby.Server.Implementations.Library
}
else
{
- if (!(item is Video))
+ if (!(item is Video) && !(item is LiveTvChannel))
{
return;
}
@@ -311,13 +308,47 @@ namespace Emby.Server.Implementations.Library
LibraryItemsCache.AddOrUpdate(item.Id, item, delegate { return item; });
}
- public async Task DeleteItem(BaseItem item, DeleteOptions options)
+ public void DeleteItem(BaseItem item, DeleteOptions options)
+ {
+ DeleteItem(item, options, false);
+ }
+
+ public void DeleteItem(BaseItem item, DeleteOptions options, bool notifyParentItem)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ var parent = item.GetOwner() ?? item.GetParent();
+
+ DeleteItem(item, options, parent, notifyParentItem);
+ }
+
+ public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem)
{
if (item == null)
{
throw new ArgumentNullException("item");
}
+ if (item.SourceType == SourceType.Channel)
+ {
+ if (options.DeleteFromExternalProvider)
+ {
+ try
+ {
+ var task = BaseItem.ChannelManager.DeleteItem(item);
+ Task.WaitAll(task);
+ }
+ catch (ArgumentException)
+ {
+ // channel no longer installed
+ }
+ }
+ options.DeleteFileLocation = false;
+ }
+
if (item is LiveTvProgram)
{
_logger.Debug("Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
@@ -335,10 +366,6 @@ namespace Emby.Server.Implementations.Library
item.Id);
}
- var parent = item.IsOwnedItem ? item.GetOwner() : item.GetParent();
-
- var locationType = item.LocationType;
-
var children = item.IsFolder
? ((Folder)item).GetRecursiveChildren(false).ToList()
: new List<BaseItem>();
@@ -361,7 +388,7 @@ namespace Emby.Server.Implementations.Library
}
}
- if (options.DeleteFileLocation && locationType != LocationType.Remote && locationType != LocationType.Virtual)
+ if (options.DeleteFileLocation && item.IsFileProtocol)
{
// Assume only the first is required
// Add this flag to GetDeletePaths if required in the future
@@ -407,33 +434,10 @@ namespace Emby.Server.Implementations.Library
isRequiredForDelete = false;
}
-
- if (parent != null)
- {
- var parentFolder = parent as Folder;
- if (parentFolder != null)
- {
- await parentFolder.ValidateChildren(new SimpleProgress<double>(), CancellationToken.None, new MetadataRefreshOptions(_fileSystem), false).ConfigureAwait(false);
- }
- else
- {
- await parent.RefreshMetadata(new MetadataRefreshOptions(_fileSystem), CancellationToken.None).ConfigureAwait(false);
- }
- }
- }
- else if (parent != null)
- {
- var parentFolder = parent as Folder;
- if (parentFolder != null)
- {
- parentFolder.RemoveChild(item);
- }
- else
- {
- await parent.RefreshMetadata(new MetadataRefreshOptions(_fileSystem), CancellationToken.None).ConfigureAwait(false);
- }
}
+ item.SetParent(null);
+
ItemRepository.DeleteItem(item.Id, CancellationToken.None);
foreach (var child in children)
{
@@ -497,7 +501,7 @@ namespace Emby.Server.Implementations.Library
private Guid GetNewItemIdInternal(string key, Type type, bool forceCaseInsensitive)
{
- if (string.IsNullOrWhiteSpace(key))
+ if (string.IsNullOrEmpty(key))
{
throw new ArgumentNullException("key");
}
@@ -544,7 +548,7 @@ namespace Emby.Server.Implementations.Library
var fullPath = fileInfo.FullName;
- if (string.IsNullOrWhiteSpace(collectionType) && parent != null)
+ if (string.IsNullOrEmpty(collectionType) && parent != null)
{
collectionType = GetContentTypeOverride(fullPath, true);
}
@@ -572,7 +576,26 @@ namespace Emby.Server.Implementations.Library
// When resolving the root, we need it's grandchildren (children of user views)
var flattenFolderDepth = isPhysicalRoot ? 2 : 0;
- var files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, _fileSystem, _logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: isPhysicalRoot || args.IsVf);
+ FileSystemMetadata[] files;
+ var isVf = args.IsVf;
+
+ try
+ {
+ files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, _fileSystem, _appHost, _logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: isPhysicalRoot || isVf);
+ }
+ catch (Exception ex)
+ {
+ if (parent != null && parent.IsPhysicalRoot)
+ {
+ _logger.ErrorException("Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", ex, isPhysicalRoot, isVf);
+
+ files = new FileSystemMetadata[] { };
+ }
+ else
+ {
+ throw;
+ }
+ }
// Need to remove subpaths that may have been resolved from shortcuts
// Example: if \\server\movies exists, then strip out \\server\movies\action
@@ -717,42 +740,43 @@ namespace Emby.Server.Implementations.Library
}
// Add in the plug-in folders
- foreach (var child in PluginFolderCreators)
+ var path = Path.Combine(ConfigurationManager.ApplicationPaths.DataPath, "playlists");
+
+ _fileSystem.CreateDirectory(path);
+
+ Folder folder = new PlaylistsFolder
{
- var folder = child.GetFolder();
+ Path = path
+ };
- if (folder != null)
+ if (folder.Id.Equals(Guid.Empty))
+ {
+ if (string.IsNullOrEmpty(folder.Path))
{
- 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());
- }
- }
+ folder.Id = GetNewItemId(folder.GetType().Name, folder.GetType());
+ }
+ else
+ {
+ folder.Id = GetNewItemId(folder.Path, folder.GetType());
+ }
+ }
- var dbItem = GetItemById(folder.Id) as BasePluginFolder;
+ var dbItem = GetItemById(folder.Id) as BasePluginFolder;
- if (dbItem != null && string.Equals(dbItem.Path, folder.Path, StringComparison.OrdinalIgnoreCase))
- {
- folder = dbItem;
- }
+ if (dbItem != null && string.Equals(dbItem.Path, folder.Path, StringComparison.OrdinalIgnoreCase))
+ {
+ folder = dbItem;
+ }
- if (folder.ParentId != rootFolder.Id)
- {
- folder.ParentId = rootFolder.Id;
- folder.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None);
- }
+ if (folder.ParentId != rootFolder.Id)
+ {
+ folder.ParentId = rootFolder.Id;
+ folder.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None);
+ }
- rootFolder.AddVirtualChild(folder);
+ rootFolder.AddVirtualChild(folder);
- RegisterItem(folder);
- }
- }
+ RegisterItem(folder);
return rootFolder;
}
@@ -798,16 +822,18 @@ namespace Emby.Server.Implementations.Library
// 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
- if (string.IsNullOrWhiteSpace(path))
+ if (string.IsNullOrEmpty(path))
{
throw new ArgumentNullException("path");
}
+ //_logger.Info("FindByPath {0}", path);
+
var query = new InternalItemsQuery
{
Path = path,
IsFolder = isFolder,
- OrderBy = new[] { ItemSortBy.DateCreated }.Select(i => new Tuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
+ OrderBy = new[] { ItemSortBy.DateCreated }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
Limit = 1,
DtoOptions = new DtoOptions(true)
};
@@ -957,7 +983,7 @@ namespace Emby.Server.Implementations.Library
Path = path
};
- CreateItem(item, CancellationToken.None);
+ CreateItem(item, null);
}
return item;
@@ -997,7 +1023,7 @@ namespace Emby.Server.Implementations.Library
// Just run the scheduled task so that the user can see it
_taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
- return Task.FromResult(true);
+ return Task.CompletedTask;
}
/// <summary>
@@ -1031,48 +1057,45 @@ namespace Emby.Server.Implementations.Library
}
}
- private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken)
+ private async Task ValidateTopLibraryFolders(CancellationToken cancellationToken)
{
- _logger.Info("Validating media library");
-
- // Ensure these objects are lazy loaded.
- // Without this there is a deadlock that will need to be investigated
var rootChildren = RootFolder.Children.ToList();
rootChildren = GetUserRootFolder().Children.ToList();
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 SimpleProgress<double>(), cancellationToken, new MetadataRefreshOptions(_fileSystem), recursive: false);
- progress.Report(1);
-
await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false);
await GetUserRootFolder().ValidateChildren(new SimpleProgress<double>(), cancellationToken, new MetadataRefreshOptions(_fileSystem), recursive: false).ConfigureAwait(false);
- progress.Report(2);
// Quickly scan CollectionFolders for changes
foreach (var folder in GetUserRootFolder().Children.OfType<Folder>().ToList())
{
await folder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
- progress.Report(3);
+ }
+
+ private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ _logger.Info("Validating media library");
+
+ await ValidateTopLibraryFolders(cancellationToken).ConfigureAwait(false);
var innerProgress = new ActionableProgress<double>();
- innerProgress.RegisterAction(pct => progress.Report(3 + pct * .72));
+ innerProgress.RegisterAction(pct => progress.Report(pct * .96));
// Now validate the entire media library
await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(_fileSystem), recursive: true).ConfigureAwait(false);
- progress.Report(75);
+ progress.Report(96);
innerProgress = new ActionableProgress<double>();
- innerProgress.RegisterAction(pct => progress.Report(75 + pct * .25));
+ innerProgress.RegisterAction(pct => progress.Report(96 + (pct * .04)));
// Run post-scan tasks
await RunPostScanTasks(innerProgress, cancellationToken).ConfigureAwait(false);
@@ -1102,8 +1125,13 @@ namespace Emby.Server.Implementations.Library
innerProgress.RegisterAction(pct =>
{
- double innerPercent = currentNumComplete * 100 + pct;
+ double innerPercent = pct;
+ innerPercent /= 100;
+ innerPercent += currentNumComplete;
+
innerPercent /= numTasks;
+ innerPercent *= 100;
+
progress.Report(innerPercent);
});
@@ -1163,7 +1191,19 @@ namespace Emby.Server.Implementations.Library
Locations = _fileSystem.GetFilePaths(dir, false)
.Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
- .Select(_fileSystem.ResolveShortcut)
+ .Select(i =>
+ {
+ try
+ {
+ return _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(i));
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error resolving shortcut file {0}", ex, i);
+ return null;
+ }
+ })
+ .Where(i => i != null)
.OrderBy(i => i)
.ToArray(),
@@ -1197,7 +1237,7 @@ namespace Emby.Server.Implementations.Library
{
return _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false)
.Select(i => _fileSystem.GetFileNameWithoutExtension(i))
- .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i));
+ .FirstOrDefault(i => !string.IsNullOrEmpty(i));
}
/// <summary>
@@ -1208,7 +1248,7 @@ namespace Emby.Server.Implementations.Library
/// <exception cref="System.ArgumentNullException">id</exception>
public BaseItem GetItemById(Guid id)
{
- if (id == Guid.Empty)
+ if (id.Equals(Guid.Empty))
{
throw new ArgumentNullException("id");
}
@@ -1234,9 +1274,9 @@ namespace Emby.Server.Implementations.Library
public List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
{
- if (query.Recursive && query.ParentId.HasValue)
+ if (query.Recursive && !query.ParentId.Equals(Guid.Empty))
{
- var parent = GetItemById(query.ParentId.Value);
+ var parent = GetItemById(query.ParentId);
if (parent != null)
{
SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent });
@@ -1258,9 +1298,9 @@ namespace Emby.Server.Implementations.Library
public int GetCount(InternalItemsQuery query)
{
- if (query.Recursive && query.ParentId.HasValue)
+ if (query.Recursive && !query.ParentId.Equals(Guid.Empty))
{
- var parent = GetItemById(query.ParentId.Value);
+ var parent = GetItemById(query.ParentId);
if (parent != null)
{
SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent });
@@ -1391,7 +1431,7 @@ namespace Emby.Server.Implementations.Library
return;
}
- var parents = query.AncestorIds.Select(i => GetItemById(new Guid(i))).ToList();
+ var parents = query.AncestorIds.Select(i => GetItemById(i)).ToList();
if (parents.All(i =>
{
@@ -1406,13 +1446,13 @@ namespace Emby.Server.Implementations.Library
}))
{
// Optimize by querying against top level views
- query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).Select(i => i.ToString("N")).ToArray();
- query.AncestorIds = new string[] { };
+ query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
+ query.AncestorIds = Array.Empty<Guid>();
// Prevent searching in all libraries due to empty filter
if (query.TopParentIds.Length == 0)
{
- query.TopParentIds = new[] { Guid.NewGuid().ToString("N") };
+ query.TopParentIds = new[] { Guid.NewGuid() };
}
}
}
@@ -1430,9 +1470,9 @@ namespace Emby.Server.Implementations.Library
public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query)
{
- if (query.Recursive && query.ParentId.HasValue)
+ if (query.Recursive && !query.ParentId.Equals(Guid.Empty))
{
- var parent = GetItemById(query.ParentId.Value);
+ var parent = GetItemById(query.ParentId);
if (parent != null)
{
SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent });
@@ -1472,23 +1512,23 @@ namespace Emby.Server.Implementations.Library
}))
{
// Optimize by querying against top level views
- query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).Select(i => i.ToString("N")).ToArray();
+ query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
// Prevent searching in all libraries due to empty filter
if (query.TopParentIds.Length == 0)
{
- query.TopParentIds = new[] { Guid.NewGuid().ToString("N") };
+ query.TopParentIds = new[] { Guid.NewGuid() };
}
}
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();
+ query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).ToArray();
// Prevent searching in all libraries due to empty filter
if (query.AncestorIds.Length == 0)
{
- query.AncestorIds = new[] { Guid.NewGuid().ToString("N") };
+ query.AncestorIds = new[] { Guid.NewGuid() };
}
}
@@ -1498,22 +1538,21 @@ namespace Emby.Server.Implementations.Library
private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true)
{
if (query.AncestorIds.Length == 0 &&
- !query.ParentId.HasValue &&
+ query.ParentId.Equals(Guid.Empty) &&
query.ChannelIds.Length == 0 &&
query.TopParentIds.Length == 0 &&
- string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey) &&
- string.IsNullOrWhiteSpace(query.SeriesPresentationUniqueKey) &&
+ string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) &&
+ string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) &&
query.ItemIds.Length == 0)
{
var userViews = _userviewManager().GetUserViews(new UserViewQuery
{
- UserId = user.Id.ToString("N"),
+ UserId = user.Id,
IncludeHidden = true,
IncludeExternalContent = allowExternalContent
+ });
- }, CancellationToken.None).Result;
-
- query.TopParentIds = userViews.SelectMany(i => GetTopParentIdsForQuery(i, user)).Select(i => i.ToString("N")).ToArray();
+ query.TopParentIds = userViews.SelectMany(i => GetTopParentIdsForQuery(i, user)).ToArray();
}
}
@@ -1527,48 +1566,38 @@ namespace Emby.Server.Implementations.Library
{
return new[] { view.Id };
}
- 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.Select(i => i.Id);
- }
// Translate view into folders
- if (view.DisplayParentId != Guid.Empty)
+ if (!view.DisplayParentId.Equals(Guid.Empty))
{
var displayParent = GetItemById(view.DisplayParentId);
if (displayParent != null)
{
return GetTopParentIdsForQuery(displayParent, user);
}
- return new Guid[] { };
+ return Array.Empty<Guid>();
}
- if (view.ParentId != Guid.Empty)
+ if (!view.ParentId.Equals(Guid.Empty))
{
var displayParent = GetItemById(view.ParentId);
if (displayParent != null)
{
return GetTopParentIdsForQuery(displayParent, user);
}
- return new Guid[] { };
+ return Array.Empty<Guid>();
}
// Handle grouping
- if (user != null && !string.IsNullOrWhiteSpace(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType) && user.Configuration.GroupedFolders.Length > 0)
+ if (user != null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType) && user.Configuration.GroupedFolders.Length > 0)
{
- return user.RootFolder
+ return GetUserRootFolder()
.GetChildren(user, true)
.OfType<CollectionFolder>()
- .Where(i => string.IsNullOrWhiteSpace(i.CollectionType) || string.Equals(i.CollectionType, view.ViewType, StringComparison.OrdinalIgnoreCase))
+ .Where(i => string.IsNullOrEmpty(i.CollectionType) || string.Equals(i.CollectionType, view.ViewType, StringComparison.OrdinalIgnoreCase))
.Where(i => user.IsFolderGrouped(i.Id))
.SelectMany(i => GetTopParentIdsForQuery(i, user));
}
- return new Guid[] { };
+ return Array.Empty<Guid>();
}
var collectionFolder = item as CollectionFolder;
@@ -1582,7 +1611,7 @@ namespace Emby.Server.Implementations.Library
{
return new[] { topParent.Id };
}
- return new Guid[] { };
+ return Array.Empty<Guid>();
}
/// <summary>
@@ -1737,7 +1766,7 @@ namespace Emby.Server.Implementations.Library
return orderedItems ?? items;
}
- public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<Tuple<string, SortOrder>> orderByList)
+ public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ValueTuple<string, SortOrder>> orderByList)
{
var isFirst = true;
@@ -1802,9 +1831,9 @@ namespace Emby.Server.Implementations.Library
/// <param name="item">The item.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- public void CreateItem(BaseItem item, CancellationToken cancellationToken)
+ public void CreateItem(BaseItem item, BaseItem parent)
{
- CreateItems(new[] { item }, item.GetParent(), cancellationToken);
+ CreateItems(new[] { item }, parent, CancellationToken.None);
}
/// <summary>
@@ -1828,6 +1857,12 @@ namespace Emby.Server.Implementations.Library
{
foreach (var item in list)
{
+ // With the live tv guide this just creates too much noise
+ if (item.SourceType != SourceType.Library)
+ {
+ continue;
+ }
+
try
{
ItemAdded(this, new ItemChangeEventArgs
@@ -1854,46 +1889,65 @@ namespace Emby.Server.Implementations.Library
/// <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 void UpdateItem(BaseItem item, ItemUpdateType updateReason, CancellationToken cancellationToken)
+ public void UpdateItems(List<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
{
- var locationType = item.LocationType;
- if (locationType != LocationType.Remote && locationType != LocationType.Virtual)
+ foreach (var item in items)
{
- _providerManagerFactory().SaveMetadata(item, updateReason);
- }
+ if (item.IsFileProtocol)
+ {
+ _providerManagerFactory().SaveMetadata(item, updateReason);
+ }
- item.DateLastSaved = DateTime.UtcNow;
+ 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);
+ RegisterItem(item);
+ }
- ItemRepository.SaveItem(item, cancellationToken);
+ //var logName = item.LocationType == LocationType.Remote ? item.Name ?? item.Path : item.Path ?? item.Name;
+ //_logger.Debug("Saving {0} to database.", logName);
- RegisterItem(item);
+ ItemRepository.SaveItems(items, cancellationToken);
if (ItemUpdated != null)
{
- try
+ foreach (var item in items)
{
- ItemUpdated(this, new ItemChangeEventArgs
+ // With the live tv guide this just creates too much noise
+ if (item.SourceType != SourceType.Library)
{
- Item = item,
- Parent = item.GetParent(),
- UpdateReason = updateReason
- });
- }
- catch (Exception ex)
- {
- _logger.ErrorException("Error in ItemUpdated event handler", ex);
+ continue;
+ }
+
+ try
+ {
+ ItemUpdated(this, new ItemChangeEventArgs
+ {
+ Item = item,
+ Parent = parent,
+ UpdateReason = updateReason
+ });
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error in ItemUpdated 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 void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
+ {
+ UpdateItems(new List<BaseItem> { item }, parent, updateReason, cancellationToken);
+ }
+
+ /// <summary>
/// Reports the item removed.
/// </summary>
/// <param name="item">The item.</param>
@@ -1995,12 +2049,12 @@ namespace Emby.Server.Implementations.Library
public string GetContentType(BaseItem item)
{
string configuredContentType = GetConfiguredContentType(item, false);
- if (!string.IsNullOrWhiteSpace(configuredContentType))
+ if (!string.IsNullOrEmpty(configuredContentType))
{
return configuredContentType;
}
configuredContentType = GetConfiguredContentType(item, true);
- if (!string.IsNullOrWhiteSpace(configuredContentType))
+ if (!string.IsNullOrEmpty(configuredContentType))
{
return configuredContentType;
}
@@ -2011,14 +2065,14 @@ namespace Emby.Server.Implementations.Library
{
var type = GetTopFolderContentType(item);
- if (!string.IsNullOrWhiteSpace(type))
+ if (!string.IsNullOrEmpty(type))
{
return type;
}
return item.GetParents()
.Select(GetConfiguredContentType)
- .LastOrDefault(i => !string.IsNullOrWhiteSpace(i));
+ .LastOrDefault(i => !string.IsNullOrEmpty(i));
}
public string GetConfiguredContentType(BaseItem item)
@@ -2043,7 +2097,7 @@ namespace Emby.Server.Implementations.Library
private string GetContentTypeOverride(string path, bool inherit)
{
- var nameValuePair = ConfigurationManager.Configuration.ContentTypes.FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path) || (inherit && !string.IsNullOrWhiteSpace(i.Name) && _fileSystem.ContainsSubPath(i.Name, path)));
+ var nameValuePair = ConfigurationManager.Configuration.ContentTypes.FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path) || (inherit && !string.IsNullOrEmpty(i.Name) && _fileSystem.ContainsSubPath(i.Name, path)));
if (nameValuePair != null)
{
return nameValuePair.Value;
@@ -2058,16 +2112,21 @@ namespace Emby.Server.Implementations.Library
return null;
}
- while (!(item.GetParent() is AggregateFolder) && item.GetParent() != null)
+ while (!item.ParentId.Equals(Guid.Empty))
{
- item = item.GetParent();
+ var parent = item.GetParent();
+ if (parent == null || parent is AggregateFolder)
+ {
+ break;
+ }
+ item = parent;
}
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));
+ .FirstOrDefault(i => !string.IsNullOrEmpty(i));
}
private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
@@ -2076,18 +2135,16 @@ namespace Emby.Server.Implementations.Library
public UserView GetNamedView(User user,
string name,
string viewType,
- string sortName,
- CancellationToken cancellationToken)
+ string sortName)
{
- return GetNamedView(user, name, null, viewType, sortName, cancellationToken);
+ return GetNamedView(user, name, Guid.Empty, viewType, sortName);
}
public UserView GetNamedView(string name,
string viewType,
- string sortName,
- CancellationToken cancellationToken)
+ string sortName)
{
- var path = Path.Combine(ConfigurationManager.ApplicationPaths.ItemsByNamePath, "views");
+ var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, "views");
path = Path.Combine(path, _fileSystem.GetValidFilename(viewType));
@@ -2111,32 +2168,15 @@ namespace Emby.Server.Implementations.Library
ForcedSortName = sortName
};
- CreateItem(item, cancellationToken);
+ CreateItem(item, null);
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)
{
item.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None);
- _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
-
- }, RefreshPriority.Normal);
+ _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem), RefreshPriority.Normal);
}
return item;
@@ -2144,12 +2184,12 @@ namespace Emby.Server.Implementations.Library
public UserView GetNamedView(User user,
string name,
- string parentId,
+ Guid parentId,
string viewType,
- string sortName,
- CancellationToken cancellationToken)
+ string sortName)
{
- var idValues = "38_namedview_" + name + user.Id.ToString("N") + (parentId ?? string.Empty) + (viewType ?? string.Empty);
+ var parentIdString = parentId.Equals(Guid.Empty) ? null : parentId.ToString("N");
+ var idValues = "38_namedview_" + name + user.Id.ToString("N") + (parentIdString ?? string.Empty) + (viewType ?? string.Empty);
var id = GetNewItemId(idValues, typeof(UserView));
@@ -2174,19 +2214,16 @@ namespace Emby.Server.Implementations.Library
UserId = user.Id
};
- if (!string.IsNullOrWhiteSpace(parentId))
- {
- item.DisplayParentId = new Guid(parentId);
- }
+ item.DisplayParentId = parentId;
- CreateItem(item, cancellationToken);
+ CreateItem(item, null);
isNew = true;
}
var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
- if (!refresh && item.DisplayParentId != Guid.Empty)
+ if (!refresh && !item.DisplayParentId.Equals(Guid.Empty))
{
var displayParent = GetItemById(item.DisplayParentId);
refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed;
@@ -2207,8 +2244,7 @@ namespace Emby.Server.Implementations.Library
public UserView GetShadowView(BaseItem parent,
string viewType,
- string sortName,
- CancellationToken cancellationToken)
+ string sortName)
{
if (parent == null)
{
@@ -2244,14 +2280,14 @@ namespace Emby.Server.Implementations.Library
item.DisplayParentId = parentId;
- CreateItem(item, cancellationToken);
+ CreateItem(item, null);
isNew = true;
}
var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
- if (!refresh && item.DisplayParentId != Guid.Empty)
+ if (!refresh && !item.DisplayParentId.Equals(Guid.Empty))
{
var displayParent = GetItemById(item.DisplayParentId);
refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed;
@@ -2271,19 +2307,19 @@ namespace Emby.Server.Implementations.Library
}
public UserView GetNamedView(string name,
- string parentId,
+ Guid parentId,
string viewType,
string sortName,
- string uniqueId,
- CancellationToken cancellationToken)
+ string uniqueId)
{
- if (string.IsNullOrWhiteSpace(name))
+ if (string.IsNullOrEmpty(name))
{
throw new ArgumentNullException("name");
}
- var idValues = "37_namedview_" + name + (parentId ?? string.Empty) + (viewType ?? string.Empty);
- if (!string.IsNullOrWhiteSpace(uniqueId))
+ var parentIdString = parentId.Equals(Guid.Empty) ? null : parentId.ToString("N");
+ var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType ?? string.Empty);
+ if (!string.IsNullOrEmpty(uniqueId))
{
idValues += uniqueId;
}
@@ -2310,12 +2346,9 @@ namespace Emby.Server.Implementations.Library
ForcedSortName = sortName
};
- if (!string.IsNullOrWhiteSpace(parentId))
- {
- item.DisplayParentId = new Guid(parentId);
- }
+ item.DisplayParentId = parentId;
- CreateItem(item, cancellationToken);
+ CreateItem(item, null);
isNew = true;
}
@@ -2323,12 +2356,12 @@ namespace Emby.Server.Implementations.Library
if (!string.Equals(viewType, item.ViewType, StringComparison.OrdinalIgnoreCase))
{
item.ViewType = viewType;
- item.UpdateToRepository(ItemUpdateType.MetadataEdit, cancellationToken);
+ item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
}
var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
- if (!refresh && item.DisplayParentId != Guid.Empty)
+ if (!refresh && !item.DisplayParentId.Equals(Guid.Empty))
{
var displayParent = GetItemById(item.DisplayParentId);
refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed;
@@ -2346,6 +2379,13 @@ namespace Emby.Server.Implementations.Library
return item;
}
+ public void AddExternalSubtitleStreams(List<MediaStream> streams,
+ string videoPath,
+ string[] files)
+ {
+ new SubtitleResolver(BaseItem.LocalizationManager, _fileSystem).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files);
+ }
+
public bool IsVideoFile(string path, LibraryOptions libraryOptions)
{
var resolver = new VideoResolver(GetNamingOptions());
@@ -2370,19 +2410,25 @@ namespace Emby.Server.Implementations.Library
public int? GetSeasonNumberFromPath(string path)
{
- return new SeasonPathParser(GetNamingOptions(), new RegexProvider()).Parse(path, true, true).SeasonNumber;
+ return new SeasonPathParser(GetNamingOptions()).Parse(path, true, true).SeasonNumber;
}
- public bool FillMissingEpisodeNumbersFromPath(Episode episode)
+ public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
{
+ var series = episode.Series;
+ bool? isAbsoluteNaming = series == null ? false : string.Equals(series.DisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase);
+ if (!isAbsoluteNaming.Value)
+ {
+ // In other words, no filter applied
+ isAbsoluteNaming = null;
+ }
+
var resolver = new EpisodeResolver(GetNamingOptions());
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
- var locationType = episode.LocationType;
-
- var episodeInfo = locationType == LocationType.FileSystem || locationType == LocationType.Offline ?
- resolver.Resolve(episode.Path, isFolder) :
+ var episodeInfo = episode.IsFileProtocol ?
+ resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) :
new Emby.Naming.TV.EpisodeInfo();
if (episodeInfo == null)
@@ -2428,105 +2474,67 @@ namespace Emby.Server.Implementations.Library
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)
+ if (!episode.IndexNumber.HasValue || forceRefresh)
{
- episode.IndexNumber = episodeInfo.EpisodeNumber;
-
- if (episode.IndexNumber.HasValue)
+ if (episode.IndexNumber != episodeInfo.EpisodeNumber)
{
changed = true;
}
+ episode.IndexNumber = episodeInfo.EpisodeNumber;
}
- if (!episode.IndexNumberEnd.HasValue)
+ if (!episode.IndexNumberEnd.HasValue || forceRefresh)
{
- episode.IndexNumberEnd = episodeInfo.EndingEpsiodeNumber;
-
- if (episode.IndexNumberEnd.HasValue)
+ if (episode.IndexNumberEnd != episodeInfo.EndingEpsiodeNumber)
{
changed = true;
}
+ episode.IndexNumberEnd = episodeInfo.EndingEpsiodeNumber;
}
- if (!episode.ParentIndexNumber.HasValue)
+ if (!episode.ParentIndexNumber.HasValue || forceRefresh)
{
- episode.ParentIndexNumber = episodeInfo.SeasonNumber;
-
- if (!episode.ParentIndexNumber.HasValue)
- {
- var season = episode.Season;
-
- if (season != null)
- {
- episode.ParentIndexNumber = season.IndexNumber;
- }
- }
-
- if (episode.ParentIndexNumber.HasValue)
+ if (episode.ParentIndexNumber != episodeInfo.SeasonNumber)
{
changed = true;
}
+ episode.ParentIndexNumber = episodeInfo.SeasonNumber;
}
}
- return changed;
- }
-
- public NamingOptions GetNamingOptions()
- {
- return GetNamingOptions(true);
- }
-
- public NamingOptions GetNamingOptions(bool allowOptimisticEpisodeDetection)
- {
- if (!allowOptimisticEpisodeDetection)
+ if (!episode.ParentIndexNumber.HasValue)
{
- if (_namingOptionsWithoutOptimisticEpisodeDetection == null)
- {
- var namingOptions = new ExtendedNamingOptions();
+ var season = episode.Season;
- InitNamingOptions(namingOptions);
- namingOptions.EpisodeExpressions = namingOptions.EpisodeExpressions
- .Where(i => i.IsNamed && !i.IsOptimistic)
- .ToList();
-
- _namingOptionsWithoutOptimisticEpisodeDetection = namingOptions;
+ if (season != null)
+ {
+ episode.ParentIndexNumber = season.IndexNumber;
}
- return _namingOptionsWithoutOptimisticEpisodeDetection;
+ if (episode.ParentIndexNumber.HasValue)
+ {
+ changed = true;
+ }
}
+ return changed;
+ }
+
+ public NamingOptions GetNamingOptions()
+ {
return GetNamingOptionsInternal();
}
- private NamingOptions _namingOptionsWithoutOptimisticEpisodeDetection;
private NamingOptions _namingOptions;
private string[] _videoFileExtensions;
private NamingOptions GetNamingOptionsInternal()
{
if (_namingOptions == null)
{
- var options = new ExtendedNamingOptions();
-
- InitNamingOptions(options);
+ var options = new NamingOptions();
_namingOptions = options;
_videoFileExtensions = _namingOptions.VideoFileExtensions.ToArray();
@@ -2535,27 +2543,6 @@ namespace Emby.Server.Implementations.Library
return _namingOptions;
}
- private void InitNamingOptions(NamingOptions options)
- {
- // 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");
- }
-
- options.VideoFileExtensions.Add(".tp");
- }
-
public ItemLookupInfo ParseName(string name)
{
var resolver = new VideoResolver(GetNamingOptions());
@@ -2606,12 +2593,11 @@ namespace Emby.Server.Implementations.Library
{
video = dbItem;
}
- else
- {
- // item is new
- video.ExtraType = ExtraType.Trailer;
- }
- video.TrailerTypes = new List<TrailerType> { TrailerType.LocalTrailer };
+
+ video.ParentId = Guid.Empty;
+ video.OwnerId = owner.Id;
+ video.ExtraType = ExtraType.Trailer;
+ video.TrailerTypes = new [] { TrailerType.LocalTrailer };
return video;
@@ -2625,7 +2611,7 @@ namespace Emby.Server.Implementations.Library
{
var namingOptions = GetNamingOptions();
- var files = fileSystemChildren.Where(i => i.IsDirectory)
+ var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
.Where(i => ExtrasSubfolderNames.Contains(i.Name ?? string.Empty, StringComparer.OrdinalIgnoreCase))
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
.ToList();
@@ -2653,6 +2639,9 @@ namespace Emby.Server.Implementations.Library
video = dbItem;
}
+ video.ParentId = Guid.Empty;
+ video.OwnerId = owner.Id;
+
SetExtraTypeFromFilename(video);
return video;
@@ -2756,7 +2745,7 @@ namespace Emby.Server.Implementations.Library
private void SetExtraTypeFromFilename(Video item)
{
- var resolver = new ExtraResolver(GetNamingOptions(), new RegexProvider());
+ var resolver = new ExtraResolver(GetNamingOptions());
var result = resolver.GetExtraInfo(item.Path);
@@ -2841,7 +2830,7 @@ namespace Emby.Server.Implementations.Library
ItemRepository.UpdatePeople(item.Id, people);
}
- public async Task<ItemImageInfo> ConvertImageToLocal(IHasMetadata item, ItemImageInfo image, int imageIndex)
+ public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex)
{
foreach (var url in image.Path.Split('|'))
{
@@ -2872,7 +2861,7 @@ namespace Emby.Server.Implementations.Library
throw new InvalidOperationException();
}
- public void AddVirtualFolder(string name, string collectionType, LibraryOptions options, bool refreshLibrary)
+ public async Task AddVirtualFolder(string name, string collectionType, LibraryOptions options, bool refreshLibrary)
{
if (string.IsNullOrWhiteSpace(name))
{
@@ -2910,7 +2899,7 @@ namespace Emby.Server.Implementations.Library
{
var path = Path.Combine(virtualFolderPath, collectionType + ".collection");
- _fileSystem.WriteAllBytes(path, new byte[] { });
+ _fileSystem.WriteAllBytes(path, Array.Empty<byte>());
}
CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
@@ -2925,26 +2914,30 @@ namespace Emby.Server.Implementations.Library
}
finally
{
- Task.Run(() =>
+ if (refreshLibrary)
{
- // No need to start if scanning the library because it will handle it
- if (refreshLibrary)
- {
- ValidateMediaLibrary(new SimpleProgress<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);
+ await ValidateTopLibraryFolders(CancellationToken.None).ConfigureAwait(false);
- _libraryMonitorFactory().Start();
- }
- });
+ StartScanInBackground();
+ }
+ else
+ {
+ // Need to add a delay here or directory watchers may still pick up the changes
+ await Task.Delay(1000).ConfigureAwait(false);
+ _libraryMonitorFactory().Start();
+ }
}
}
+ private void StartScanInBackground()
+ {
+ Task.Run(() =>
+ {
+ // No need to start if scanning the library because it will handle it
+ ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None);
+ });
+ }
+
private bool ValidateNetworkPath(string path)
{
//if (Environment.OSVersion.Platform == PlatformID.Win32NT)
@@ -3003,7 +2996,7 @@ namespace Emby.Server.Implementations.Library
lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
}
- _fileSystem.CreateShortcut(lnk, path);
+ _fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
RemoveContentTypeOverrides(path);
@@ -3079,7 +3072,7 @@ namespace Emby.Server.Implementations.Library
}
}
- public void RemoveVirtualFolder(string name, bool refreshLibrary)
+ public async Task RemoveVirtualFolder(string name, bool refreshLibrary)
{
if (string.IsNullOrWhiteSpace(name))
{
@@ -3103,23 +3096,20 @@ namespace Emby.Server.Implementations.Library
}
finally
{
- Task.Run(() =>
+ CollectionFolder.OnCollectionFolderChange();
+
+ if (refreshLibrary)
{
- // No need to start if scanning the library because it will handle it
- if (refreshLibrary)
- {
- ValidateMediaLibrary(new SimpleProgress<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);
+ await ValidateTopLibraryFolders(CancellationToken.None).ConfigureAwait(false);
- _libraryMonitorFactory().Start();
- }
- });
+ StartScanInBackground();
+ }
+ else
+ {
+ // Need to add a delay here or directory watchers may still pick up the changes
+ await Task.Delay(1000).ConfigureAwait(false);
+ _libraryMonitorFactory().Start();
+ }
}
}
@@ -3157,7 +3147,7 @@ namespace Emby.Server.Implementations.Library
public void RemoveMediaPath(string virtualFolderName, string mediaPath)
{
- if (string.IsNullOrWhiteSpace(mediaPath))
+ if (string.IsNullOrEmpty(mediaPath))
{
throw new ArgumentNullException("mediaPath");
}
@@ -3172,7 +3162,7 @@ namespace Emby.Server.Implementations.Library
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));
+ .FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(shortcut))
{
diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
new file mode 100644
index 000000000..e027e133f
--- /dev/null
+++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
@@ -0,0 +1,181 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.MediaInfo;
+using System.Collections.Generic;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Common.Configuration;
+using System.IO;
+using MediaBrowser.Common.Extensions;
+
+namespace Emby.Server.Implementations.Library
+{
+ public class LiveStreamHelper
+ {
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly ILogger _logger;
+
+ private IJsonSerializer _json;
+ private IApplicationPaths _appPaths;
+
+ public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IJsonSerializer json, IApplicationPaths appPaths)
+ {
+ _mediaEncoder = mediaEncoder;
+ _logger = logger;
+ _json = json;
+ _appPaths = appPaths;
+ }
+
+ public async Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string cacheKey, bool addProbeDelay, CancellationToken cancellationToken)
+ {
+ var originalRuntime = mediaSource.RunTimeTicks;
+
+ var now = DateTime.UtcNow;
+
+ MediaInfo mediaInfo = null;
+ var cacheFilePath = string.IsNullOrEmpty(cacheKey) ? null : Path.Combine(_appPaths.CachePath, "mediainfo", cacheKey.GetMD5().ToString("N") + ".json");
+
+ if (!string.IsNullOrEmpty(cacheKey))
+ {
+ try
+ {
+ mediaInfo = _json.DeserializeFromFile<MediaInfo>(cacheFilePath);
+
+ //_logger.Debug("Found cached media info");
+ }
+ catch
+ {
+ }
+ }
+
+ if (mediaInfo == null)
+ {
+ if (addProbeDelay)
+ {
+ var delayMs = mediaSource.AnalyzeDurationMs ?? 0;
+ delayMs = Math.Max(3000, delayMs);
+ if (delayMs > 0)
+ {
+ _logger.Info("Waiting {0}ms before probing the live stream", delayMs);
+ await Task.Delay(delayMs, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ mediaSource.AnalyzeDurationMs = 3000;
+
+ mediaInfo = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
+ {
+ MediaSource = mediaSource,
+ MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
+ ExtractChapters = false
+
+ }, cancellationToken).ConfigureAwait(false);
+
+ if (cacheFilePath != null)
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
+ _json.SerializeToFile(mediaInfo, cacheFilePath);
+
+ //_logger.Debug("Saved media info to {0}", cacheFilePath);
+ }
+ }
+
+ var mediaStreams = mediaInfo.MediaStreams;
+
+ if (!string.IsNullOrEmpty(cacheKey))
+ {
+ var newList = new List<MediaStream>();
+ newList.AddRange(mediaStreams.Where(i => i.Type == MediaStreamType.Video).Take(1));
+ newList.AddRange(mediaStreams.Where(i => i.Type == MediaStreamType.Audio).Take(1));
+
+ foreach (var stream in newList)
+ {
+ stream.Index = -1;
+ stream.Language = null;
+ }
+
+ mediaStreams = newList;
+ }
+
+ _logger.Info("Live tv media info probe took {0} seconds", (DateTime.UtcNow - now).TotalSeconds.ToString(CultureInfo.InvariantCulture));
+
+ mediaSource.Bitrate = mediaInfo.Bitrate;
+ mediaSource.Container = mediaInfo.Container;
+ mediaSource.Formats = mediaInfo.Formats;
+ mediaSource.MediaStreams = mediaStreams;
+ mediaSource.RunTimeTicks = mediaInfo.RunTimeTicks;
+ mediaSource.Size = mediaInfo.Size;
+ mediaSource.Timestamp = mediaInfo.Timestamp;
+ mediaSource.Video3DFormat = mediaInfo.Video3DFormat;
+ mediaSource.VideoType = mediaInfo.VideoType;
+
+ mediaSource.DefaultSubtitleStreamIndex = null;
+
+ // Null this out so that it will be treated like a live stream
+ if (!originalRuntime.HasValue)
+ {
+ mediaSource.RunTimeTicks = null;
+ }
+
+ var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Audio);
+
+ if (audioStream == null || audioStream.Index == -1)
+ {
+ mediaSource.DefaultAudioStreamIndex = null;
+ }
+ else
+ {
+ mediaSource.DefaultAudioStreamIndex = audioStream.Index;
+ }
+
+ var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Video);
+ if (videoStream != null)
+ {
+ if (!videoStream.BitRate.HasValue)
+ {
+ var width = videoStream.Width ?? 1920;
+
+ if (width >= 3000)
+ {
+ videoStream.BitRate = 30000000;
+ }
+
+ else if (width >= 1900)
+ {
+ videoStream.BitRate = 20000000;
+ }
+
+ else if (width >= 1200)
+ {
+ videoStream.BitRate = 8000000;
+ }
+
+ else if (width >= 700)
+ {
+ videoStream.BitRate = 2000000;
+ }
+ }
+
+ // This is coming up false and preventing stream copy
+ videoStream.IsAVC = null;
+ }
+
+ mediaSource.AnalyzeDurationMs = 3000;
+
+ // Try to estimate this
+ mediaSource.InferTotalBitrate(true);
+ }
+
+ public Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, bool addProbeDelay, CancellationToken cancellationToken)
+ {
+ return AddMediaInfoWithProbe(mediaSource, isAudio, null, addProbeDelay, cancellationToken);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/LocalTrailerPostScanTask.cs b/Emby.Server.Implementations/Library/LocalTrailerPostScanTask.cs
deleted file mode 100644
index 4830da8fc..000000000
--- a/Emby.Server.Implementations/Library/LocalTrailerPostScanTask.cs
+++ /dev/null
@@ -1,105 +0,0 @@
-using MediaBrowser.Controller.Channels;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Entities;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Dto;
-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,
- DtoOptions = new DtoOptions(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,
- DtoOptions = new DtoOptions(false)
-
- });
-
- var numComplete = 0;
-
- foreach (var item in items)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- AssignTrailers(item, trailers);
-
- numComplete++;
- double percent = numComplete;
- percent /= items.Count;
- progress.Report(percent * 100);
- }
-
- progress.Report(100);
- }
-
- private void AssignTrailers(IHasTrailers item, IEnumerable<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)
- .ToArray();
-
- if (!trailerIds.SequenceEqual(item.RemoteTrailerIds))
- {
- item.RemoteTrailerIds = trailerIds;
-
- var baseItem = (BaseItem)item;
- baseItem.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None);
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 688da5764..0dc436800 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -1,5 +1,6 @@
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence;
@@ -16,6 +17,11 @@ using System.Threading.Tasks;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Threading;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Globalization;
+using System.IO;
+using System.Globalization;
+using MediaBrowser.Common.Configuration;
namespace Emby.Server.Implementations.Library
{
@@ -31,8 +37,11 @@ namespace Emby.Server.Implementations.Library
private readonly ILogger _logger;
private readonly IUserDataManager _userDataManager;
private readonly ITimerFactory _timerFactory;
+ private readonly Func<IMediaEncoder> _mediaEncoder;
+ private ILocalizationManager _localizationManager;
+ private IApplicationPaths _appPaths;
- public MediaSourceManager(IItemRepository itemRepo, IUserManager userManager, ILibraryManager libraryManager, ILogger logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem, IUserDataManager userDataManager, ITimerFactory timerFactory)
+ public MediaSourceManager(IItemRepository itemRepo, IApplicationPaths applicationPaths, ILocalizationManager localizationManager, IUserManager userManager, ILibraryManager libraryManager, ILogger logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem, IUserDataManager userDataManager, ITimerFactory timerFactory, Func<IMediaEncoder> mediaEncoder)
{
_itemRepo = itemRepo;
_userManager = userManager;
@@ -42,6 +51,9 @@ namespace Emby.Server.Implementations.Library
_fileSystem = fileSystem;
_userDataManager = userDataManager;
_timerFactory = timerFactory;
+ _mediaEncoder = mediaEncoder;
+ _localizationManager = localizationManager;
+ _appPaths = applicationPaths;
}
public void AddParts(IEnumerable<IMediaSourceProvider> providers)
@@ -109,20 +121,23 @@ namespace Emby.Server.Implementations.Library
return streams;
}
- public async Task<IEnumerable<MediaSourceInfo>> GetPlayackMediaSources(string id, string userId, bool enablePathSubstitution, string[] supportedLiveMediaTypes, CancellationToken cancellationToken)
+ public async Task<List<MediaSourceInfo>> GetPlayackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
{
- var item = _libraryManager.GetItemById(id);
+ var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
- var hasMediaSources = (IHasMediaSources)item;
- User user = null;
-
- if (!string.IsNullOrWhiteSpace(userId))
+ if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Audio || i.Type == MediaStreamType.Video))
{
- user = _userManager.GetUserById(userId);
+ await item.RefreshMetadata(new MediaBrowser.Controller.Providers.MetadataRefreshOptions(_fileSystem)
+ {
+ EnableRemoteContentProbe = true,
+ MetadataRefreshMode = MediaBrowser.Controller.Providers.MetadataRefreshMode.FullRefresh
+
+ }, cancellationToken).ConfigureAwait(false);
+
+ mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
}
- var mediaSources = GetStaticMediaSources(hasMediaSources, enablePathSubstitution, user);
- var dynamicMediaSources = await GetDynamicMediaSources(hasMediaSources, cancellationToken).ConfigureAwait(false);
+ var dynamicMediaSources = await GetDynamicMediaSources(item, cancellationToken).ConfigureAwait(false);
var list = new List<MediaSourceInfo>();
@@ -132,24 +147,13 @@ namespace Emby.Server.Implementations.Library
{
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;
+ SetDefaultAudioAndSubtitleStreamIndexes(item, source, user);
}
- else
+
+ // Validate that this is actually possible
+ if (source.SupportsDirectStream)
{
- source.SupportsDirectStream = false;
+ source.SupportsDirectStream = SupportsDirectStream(source.Path, source.Protocol);
}
list.Add(source);
@@ -169,10 +173,63 @@ namespace Emby.Server.Implementations.Library
}
}
- return SortMediaSources(list).Where(i => i.Type != MediaSourceType.Placeholder);
+ return SortMediaSources(list).Where(i => i.Type != MediaSourceType.Placeholder).ToList();
+ }
+
+ public MediaProtocol GetPathProtocol(string path)
+ {
+ if (path.StartsWith("Rtsp", StringComparison.OrdinalIgnoreCase))
+ {
+ return MediaProtocol.Rtsp;
+ }
+ if (path.StartsWith("Rtmp", StringComparison.OrdinalIgnoreCase))
+ {
+ return MediaProtocol.Rtmp;
+ }
+ if (path.StartsWith("Http", StringComparison.OrdinalIgnoreCase))
+ {
+ return MediaProtocol.Http;
+ }
+ if (path.StartsWith("rtp", StringComparison.OrdinalIgnoreCase))
+ {
+ return MediaProtocol.Rtp;
+ }
+ if (path.StartsWith("ftp", StringComparison.OrdinalIgnoreCase))
+ {
+ return MediaProtocol.Ftp;
+ }
+ if (path.StartsWith("udp", StringComparison.OrdinalIgnoreCase))
+ {
+ return MediaProtocol.Udp;
+ }
+
+ return _fileSystem.IsPathFile(path) ? MediaProtocol.File : MediaProtocol.Http;
}
- private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(IHasMediaSources item, CancellationToken cancellationToken)
+ public bool SupportsDirectStream(string path, MediaProtocol protocol)
+ {
+ if (protocol == MediaProtocol.File)
+ {
+ return true;
+ }
+
+ if (protocol == MediaProtocol.Http)
+ {
+ if (path != null)
+ {
+ if (path.IndexOf(".m3u", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(BaseItem item, CancellationToken cancellationToken)
{
var tasks = _providers.Select(i => GetDynamicMediaSources(item, i, cancellationToken));
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
@@ -180,7 +237,7 @@ namespace Emby.Server.Implementations.Library
return results.SelectMany(i => i.ToList());
}
- private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(IHasMediaSources item, IMediaSourceProvider provider, CancellationToken cancellationToken)
+ private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(BaseItem item, IMediaSourceProvider provider, CancellationToken cancellationToken)
{
try
{
@@ -207,78 +264,65 @@ namespace Emby.Server.Implementations.Library
{
var prefix = provider.GetType().FullName.GetMD5().ToString("N") + LiveStreamIdDelimeter;
- if (!string.IsNullOrWhiteSpace(mediaSource.OpenToken) && !mediaSource.OpenToken.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ if (!string.IsNullOrEmpty(mediaSource.OpenToken) && !mediaSource.OpenToken.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
mediaSource.OpenToken = prefix + mediaSource.OpenToken;
}
- if (!string.IsNullOrWhiteSpace(mediaSource.LiveStreamId) && !mediaSource.LiveStreamId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ if (!string.IsNullOrEmpty(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)
+ public async Task<MediaSourceInfo> GetMediaSource(BaseItem item, string mediaSourceId, string liveStreamId, bool enablePathSubstitution, CancellationToken cancellationToken)
{
- if (!string.IsNullOrWhiteSpace(liveStreamId))
+ if (!string.IsNullOrEmpty(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);
+ var sources = await GetPlayackMediaSources(item, null, false, enablePathSubstitution, cancellationToken).ConfigureAwait(false);
return sources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
}
- public List<MediaSourceInfo> GetStaticMediaSources(IHasMediaSources item, bool enablePathSubstitution, User user = null)
+ public List<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null)
{
if (item == null)
{
throw new ArgumentNullException("item");
}
- if (!(item is Video))
- {
- return item.GetMediaSources(enablePathSubstitution);
- }
+ var hasMediaSources = (IHasMediaSources)item;
- var sources = item.GetMediaSources(enablePathSubstitution);
+ var sources = hasMediaSources.GetMediaSources(enablePathSubstitution);
if (user != null)
{
foreach (var source in sources)
{
- SetUserProperties(item, source, user);
+ SetDefaultAudioAndSubtitleStreamIndexes(item, source, user);
}
}
return sources;
}
- private void SetUserProperties(IHasUserData item, MediaSourceInfo source, User user)
+ private string[] NormalizeLanguage(string language)
{
- var userData = item == null ? new UserItemData() : _userDataManager.GetUserData(user, item);
+ if (language != null)
+ {
+ var culture = _localizationManager.FindLanguageInfo(language);
+ if (culture != null)
+ {
+ return culture.ThreeLetterISOLanguageNames;
+ }
- var allowRememberingSelection = item == null || item.EnableRememberingTrackSelections;
+ return new string[] { language };
+ }
- SetDefaultAudioStreamIndex(source, userData, user, allowRememberingSelection);
- SetDefaultSubtitleStreamIndex(source, userData, user, allowRememberingSelection);
+ return Array.Empty<string>();
}
private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
@@ -293,9 +337,9 @@ namespace Emby.Server.Implementations.Library
return;
}
}
-
+
var preferredSubs = string.IsNullOrEmpty(user.Configuration.SubtitleLanguagePreference)
- ? new List<string>() : new List<string> { user.Configuration.SubtitleLanguagePreference };
+ ? Array.Empty<string>() : NormalizeLanguage(user.Configuration.SubtitleLanguagePreference);
var defaultAudioIndex = source.DefaultAudioStreamIndex;
var audioLangage = defaultAudioIndex == null
@@ -325,12 +369,37 @@ namespace Emby.Server.Implementations.Library
}
var preferredAudio = string.IsNullOrEmpty(user.Configuration.AudioLanguagePreference)
- ? new string[] { }
- : new[] { user.Configuration.AudioLanguagePreference };
+ ? Array.Empty<string>()
+ : NormalizeLanguage(user.Configuration.AudioLanguagePreference);
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.Configuration.PlayDefaultAudioTrack);
}
+ public void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user)
+ {
+ // Item would only be null if the app didn't supply ItemId as part of the live stream open request
+ var mediaType = item == null ? MediaType.Video : item.MediaType;
+
+ if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+ {
+ 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);
+ }
+ else if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+ {
+ var audio = source.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
+
+ if (audio != null)
+ {
+ source.DefaultAudioStreamIndex = audio.Index;
+ }
+ }
+ }
+
private IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
{
return sources.OrderBy(i =>
@@ -352,55 +421,157 @@ namespace Emby.Server.Implementations.Library
.ToList();
}
- private readonly Dictionary<string, LiveStreamInfo> _openStreams = new Dictionary<string, LiveStreamInfo>(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
- public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken)
+ public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
{
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+ MediaSourceInfo mediaSource;
+ ILiveStream liveStream;
+
try
{
var tuple = GetProvider(request.OpenToken);
var provider = tuple.Item1;
- var mediaSourceTuple = await provider.OpenMediaSource(tuple.Item2, request.EnableMediaProbe, cancellationToken).ConfigureAwait(false);
+ var currentLiveStreams = _openStreams.Values.ToList();
+
+ liveStream = await provider.OpenMediaSource(tuple.Item2, currentLiveStreams, cancellationToken).ConfigureAwait(false);
- var mediaSource = mediaSourceTuple.Item1;
+ mediaSource = liveStream.MediaSource;
- if (string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
+ // Validate that this is actually possible
+ if (mediaSource.SupportsDirectStream)
{
- throw new InvalidOperationException(string.Format("{0} returned null LiveStreamId", provider.GetType().Name));
+ mediaSource.SupportsDirectStream = SupportsDirectStream(mediaSource.Path, mediaSource.Protocol);
}
SetKeyProperties(provider, mediaSource);
- var info = new LiveStreamInfo
+ _openStreams[mediaSource.LiveStreamId] = liveStream;
+ }
+ finally
+ {
+ _liveStreamSemaphore.Release();
+ }
+
+ // TODO: Don't hardcode this
+ var isAudio = false;
+
+ try
+ {
+ if (mediaSource.MediaStreams.Any(i => i.Index != -1) || !mediaSource.SupportsProbing)
{
- Id = mediaSource.LiveStreamId,
- MediaSource = mediaSource,
- DirectStreamProvider = mediaSourceTuple.Item2
- };
-
- _openStreams[mediaSource.LiveStreamId] = info;
-
- 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);
+ AddMediaInfo(mediaSource, isAudio);
+ }
+ else
+ {
+ // hack - these two values were taken from LiveTVMediaSourceProvider
+ var cacheKey = request.OpenToken;
+
+ await new LiveStreamHelper(_mediaEncoder(), _logger, _jsonSerializer, _appPaths).AddMediaInfoWithProbe(mediaSource, isAudio, cacheKey, true, cancellationToken).ConfigureAwait(false);
}
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error probing live tv stream", ex);
+ AddMediaInfo(mediaSource, isAudio);
+ }
+
+ var json = _jsonSerializer.SerializeToString(mediaSource);
+ _logger.Info("Live stream opened: " + json);
+ var clone = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json);
+
+ if (!request.UserId.Equals(Guid.Empty))
+ {
+ var user = _userManager.GetUserById(request.UserId);
+ var item = request.ItemId.Equals(Guid.Empty)
+ ? null
+ : _libraryManager.GetItemById(request.ItemId);
+ SetDefaultAudioAndSubtitleStreamIndexes(item, clone, user);
+ }
+
+ return new Tuple<LiveStreamResponse, IDirectStreamProvider>(new LiveStreamResponse
+ {
+ MediaSource = clone
+
+ }, liveStream as IDirectStreamProvider);
+ }
+
+ private void AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio)
+ {
+ mediaSource.DefaultSubtitleStreamIndex = null;
+
+ // Null this out so that it will be treated like a live stream
+ if (mediaSource.IsInfiniteStream)
+ {
+ mediaSource.RunTimeTicks = null;
+ }
- return new LiveStreamResponse
+ var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Audio);
+
+ if (audioStream == null || audioStream.Index == -1)
+ {
+ mediaSource.DefaultAudioStreamIndex = null;
+ }
+ else
+ {
+ mediaSource.DefaultAudioStreamIndex = audioStream.Index;
+ }
+
+ var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Video);
+ if (videoStream != null)
+ {
+ if (!videoStream.BitRate.HasValue)
{
- MediaSource = clone
- };
+ var width = videoStream.Width ?? 1920;
+
+ if (width >= 3000)
+ {
+ videoStream.BitRate = 30000000;
+ }
+
+ else if (width >= 1900)
+ {
+ videoStream.BitRate = 20000000;
+ }
+
+ else if (width >= 1200)
+ {
+ videoStream.BitRate = 8000000;
+ }
+
+ else if (width >= 700)
+ {
+ videoStream.BitRate = 2000000;
+ }
+ }
+ }
+
+ // Try to estimate this
+ mediaSource.InferTotalBitrate();
+ }
+
+ public async Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken)
+ {
+ await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ try
+ {
+ var info = _openStreams.Values.FirstOrDefault(i =>
+ {
+ var liveStream = i as ILiveStream;
+ if (liveStream != null)
+ {
+ return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase);
+ }
+
+ return false;
+ });
+
+ return info as IDirectStreamProvider;
}
finally
{
@@ -408,23 +579,207 @@ namespace Emby.Server.Implementations.Library
}
}
+ public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken)
+ {
+ var result = await OpenLiveStreamInternal(request, cancellationToken).ConfigureAwait(false);
+ return result.Item1;
+ }
+
+ public async Task<MediaSourceInfo> GetLiveStreamMediaInfo(string id, CancellationToken cancellationToken)
+ {
+ var liveStreamInfo = await GetLiveStreamInfo(id, cancellationToken).ConfigureAwait(false);
+
+ var mediaSource = liveStreamInfo.MediaSource;
+
+ if (liveStreamInfo is IDirectStreamProvider)
+ {
+ var info = await _mediaEncoder().GetMediaInfo(new MediaInfoRequest
+ {
+ MediaSource = mediaSource,
+ ExtractChapters = false,
+ MediaType = DlnaProfileType.Video
+
+ }, cancellationToken).ConfigureAwait(false);
+
+ mediaSource.MediaStreams = info.MediaStreams;
+ mediaSource.Container = info.Container;
+ mediaSource.Bitrate = info.Bitrate;
+ }
+
+ return mediaSource;
+ }
+
+ public async Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string cacheKey, bool addProbeDelay, bool isLiveStream, CancellationToken cancellationToken)
+ {
+ var originalRuntime = mediaSource.RunTimeTicks;
+
+ var now = DateTime.UtcNow;
+
+ MediaInfo mediaInfo = null;
+ var cacheFilePath = string.IsNullOrEmpty(cacheKey) ? null : Path.Combine(_appPaths.CachePath, "mediainfo", cacheKey.GetMD5().ToString("N") + ".json");
+
+ if (!string.IsNullOrEmpty(cacheKey))
+ {
+ try
+ {
+ mediaInfo = _jsonSerializer.DeserializeFromFile<MediaInfo>(cacheFilePath);
+
+ //_logger.Debug("Found cached media info");
+ }
+ catch (Exception ex)
+ {
+ }
+ }
+
+ if (mediaInfo == null)
+ {
+ if (addProbeDelay)
+ {
+ var delayMs = mediaSource.AnalyzeDurationMs ?? 0;
+ delayMs = Math.Max(3000, delayMs);
+ await Task.Delay(delayMs, cancellationToken).ConfigureAwait(false);
+ }
+
+ if (isLiveStream)
+ {
+ mediaSource.AnalyzeDurationMs = 3000;
+ }
+
+ mediaInfo = await _mediaEncoder().GetMediaInfo(new MediaInfoRequest
+ {
+ MediaSource = mediaSource,
+ MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
+ ExtractChapters = false
+
+ }, cancellationToken).ConfigureAwait(false);
+
+ if (cacheFilePath != null)
+ {
+ _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(cacheFilePath));
+ _jsonSerializer.SerializeToFile(mediaInfo, cacheFilePath);
+
+ //_logger.Debug("Saved media info to {0}", cacheFilePath);
+ }
+ }
+
+ var mediaStreams = mediaInfo.MediaStreams;
+
+ if (isLiveStream && !string.IsNullOrEmpty(cacheKey))
+ {
+ var newList = new List<MediaStream>();
+ newList.AddRange(mediaStreams.Where(i => i.Type == MediaStreamType.Video).Take(1));
+ newList.AddRange(mediaStreams.Where(i => i.Type == MediaStreamType.Audio).Take(1));
+
+ foreach (var stream in newList)
+ {
+ stream.Index = -1;
+ stream.Language = null;
+ }
+
+ mediaStreams = newList;
+ }
+
+ _logger.Info("Live tv media info probe took {0} seconds", (DateTime.UtcNow - now).TotalSeconds.ToString(CultureInfo.InvariantCulture));
+
+ mediaSource.Bitrate = mediaInfo.Bitrate;
+ mediaSource.Container = mediaInfo.Container;
+ mediaSource.Formats = mediaInfo.Formats;
+ mediaSource.MediaStreams = mediaStreams;
+ mediaSource.RunTimeTicks = mediaInfo.RunTimeTicks;
+ mediaSource.Size = mediaInfo.Size;
+ mediaSource.Timestamp = mediaInfo.Timestamp;
+ mediaSource.Video3DFormat = mediaInfo.Video3DFormat;
+ mediaSource.VideoType = mediaInfo.VideoType;
+
+ mediaSource.DefaultSubtitleStreamIndex = null;
+
+ if (isLiveStream)
+ {
+ // Null this out so that it will be treated like a live stream
+ if (!originalRuntime.HasValue)
+ {
+ mediaSource.RunTimeTicks = null;
+ }
+ }
+
+ var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
+
+ if (audioStream == null || audioStream.Index == -1)
+ {
+ mediaSource.DefaultAudioStreamIndex = null;
+ }
+ else
+ {
+ mediaSource.DefaultAudioStreamIndex = audioStream.Index;
+ }
+
+ var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
+ if (videoStream != null)
+ {
+ if (!videoStream.BitRate.HasValue)
+ {
+ var width = videoStream.Width ?? 1920;
+
+ if (width >= 3000)
+ {
+ videoStream.BitRate = 30000000;
+ }
+
+ else if (width >= 1900)
+ {
+ videoStream.BitRate = 20000000;
+ }
+
+ else if (width >= 1200)
+ {
+ videoStream.BitRate = 8000000;
+ }
+
+ else if (width >= 700)
+ {
+ videoStream.BitRate = 2000000;
+ }
+ }
+
+ // This is coming up false and preventing stream copy
+ videoStream.IsAVC = null;
+ }
+
+ if (isLiveStream)
+ {
+ mediaSource.AnalyzeDurationMs = 3000;
+ }
+
+ // Try to estimate this
+ mediaSource.InferTotalBitrate(true);
+ }
+
public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken)
{
- if (string.IsNullOrWhiteSpace(id))
+ if (string.IsNullOrEmpty(id))
{
throw new ArgumentNullException("id");
}
- _logger.Debug("Getting already opened live stream {0}", id);
+ var info = await GetLiveStreamInfo(id, cancellationToken).ConfigureAwait(false);
+ return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider);
+ }
+
+ private async Task<ILiveStream> GetLiveStreamInfo(string id, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(id))
+ {
+ throw new ArgumentNullException("id");
+ }
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
- LiveStreamInfo info;
+ ILiveStream info;
if (_openStreams.TryGetValue(id, out info))
{
- return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info.DirectStreamProvider);
+ return info;
}
else
{
@@ -443,26 +798,9 @@ namespace Emby.Server.Implementations.Library
return result.Item1;
}
- 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))
+ if (string.IsNullOrEmpty(id))
{
throw new ArgumentNullException("id");
}
@@ -471,18 +809,22 @@ namespace Emby.Server.Implementations.Library
try
{
- LiveStreamInfo current;
+ ILiveStream liveStream;
- if (_openStreams.TryGetValue(id, out current))
+ if (_openStreams.TryGetValue(id, out liveStream))
{
- _openStreams.Remove(id);
- current.Closed = true;
+ liveStream.ConsumerCount--;
- if (current.MediaSource.RequiresClosing)
+ _logger.Info("Live stream {0} consumer count is now {1}", liveStream.OriginalStreamId, liveStream.ConsumerCount);
+
+ if (liveStream.ConsumerCount <= 0)
{
- var tuple = GetProvider(id);
+ _openStreams.Remove(id);
+
+ _logger.Info("Closing live stream {0}", id);
- await CloseLiveStreamWithProvider(tuple.Item1, tuple.Item2).ConfigureAwait(false);
+ await liveStream.Close().ConfigureAwait(false);
+ _logger.Info("Live stream {0} closed successfully", id);
}
}
}
@@ -497,7 +839,7 @@ namespace Emby.Server.Implementations.Library
private Tuple<IMediaSourceProvider, string> GetProvider(string key)
{
- if (string.IsNullOrWhiteSpace(key))
+ if (string.IsNullOrEmpty(key))
{
throw new ArgumentException("key");
}
@@ -518,7 +860,6 @@ namespace Emby.Server.Implementations.Library
public void Dispose()
{
Dispose(true);
- GC.SuppressFinalize(this);
}
private readonly object _disposeLock = new object();
@@ -541,13 +882,5 @@ namespace Emby.Server.Implementations.Library
}
}
}
-
- private class LiveStreamInfo
- {
- 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/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
new file mode 100644
index 000000000..5d4c5a452
--- /dev/null
+++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
@@ -0,0 +1,217 @@
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Emby.Server.Implementations.Library
+{
+ public static class MediaStreamSelector
+ {
+ public static int? GetDefaultAudioStreamIndex(List<MediaStream> streams, string[] preferredLanguages, bool preferDefaultTrack)
+ {
+ streams = GetSortedStreams(streams, MediaStreamType.Audio, preferredLanguages)
+ .ToList();
+
+ if (preferDefaultTrack)
+ {
+ var defaultStream = streams.FirstOrDefault(i => i.IsDefault);
+
+ if (defaultStream != null)
+ {
+ return defaultStream.Index;
+ }
+ }
+
+ var stream = streams.FirstOrDefault();
+
+ if (stream != null)
+ {
+ return stream.Index;
+ }
+
+ return null;
+ }
+
+ public static int? GetDefaultSubtitleStreamIndex(List<MediaStream> streams,
+ string[] preferredLanguages,
+ SubtitlePlaybackMode mode,
+ string audioTrackLanguage)
+ {
+ streams = GetSortedStreams(streams, MediaStreamType.Subtitle, preferredLanguages)
+ .ToList();
+
+ MediaStream stream = null;
+
+ if (mode == SubtitlePlaybackMode.None)
+ {
+ return null;
+ }
+
+ if (mode == SubtitlePlaybackMode.Default)
+ {
+ // Prefer embedded metadata over smart logic
+
+ stream = streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) ??
+ streams.FirstOrDefault(s => s.IsForced) ??
+ streams.FirstOrDefault(s => s.IsDefault);
+
+ // if the audio language is not understood by the user, load their preferred subs, if there are any
+ if (stream == null && !preferredLanguages.Contains(audioTrackLanguage, StringComparer.OrdinalIgnoreCase))
+ {
+ stream = streams.Where(s => !s.IsForced).FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase));
+ }
+ }
+ else if (mode == SubtitlePlaybackMode.Smart)
+ {
+ // Prefer smart logic over embedded metadata
+
+ // if the audio language is not understood by the user, load their preferred subs, if there are any
+ if (!preferredLanguages.Contains(audioTrackLanguage, StringComparer.OrdinalIgnoreCase))
+ {
+ stream = streams.Where(s => !s.IsForced).FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase)) ??
+ streams.FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase));
+ }
+ }
+ else if (mode == SubtitlePlaybackMode.Always)
+ {
+ // always load the most suitable full subtitles
+ stream = streams.FirstOrDefault(s => !s.IsForced);
+ }
+ else if (mode == SubtitlePlaybackMode.OnlyForced)
+ {
+ // always load the most suitable full subtitles
+ stream = streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) ??
+ streams.FirstOrDefault(s => s.IsForced);
+ }
+
+ // load forced subs if we have found no suitable full subtitles
+ stream = stream ?? streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase));
+
+ if (stream != null)
+ {
+ return stream.Index;
+ }
+
+ return null;
+ }
+
+ private static IEnumerable<MediaStream> GetSortedStreams(IEnumerable<MediaStream> streams, MediaStreamType type, string[] languagePreferences)
+ {
+ // Give some preferance to external text subs for better performance
+ return streams.Where(i => i.Type == type)
+ .OrderBy(i =>
+ {
+ var index = FindIndex(languagePreferences, i.Language);
+
+ return index == -1 ? 100 : index;
+ })
+ .ThenBy(i => GetBooleanOrderBy(i.IsDefault))
+ .ThenBy(i => GetBooleanOrderBy(i.SupportsExternalStream))
+ .ThenBy(i => GetBooleanOrderBy(i.IsTextSubtitleStream))
+ .ThenBy(i => GetBooleanOrderBy(i.IsExternal))
+ .ThenBy(i => i.Index);
+ }
+
+ public static void SetSubtitleStreamScores(List<MediaStream> streams,
+ string[] preferredLanguages,
+ SubtitlePlaybackMode mode,
+ string audioTrackLanguage)
+ {
+ if (mode == SubtitlePlaybackMode.None)
+ {
+ return;
+ }
+
+ streams = GetSortedStreams(streams, MediaStreamType.Subtitle, preferredLanguages)
+ .ToList();
+
+ var filteredStreams = new List<MediaStream>();
+
+ if (mode == SubtitlePlaybackMode.Default)
+ {
+ // Prefer embedded metadata over smart logic
+ filteredStreams = streams.Where(s => s.IsForced || s.IsDefault)
+ .ToList();
+ }
+ else if (mode == SubtitlePlaybackMode.Smart)
+ {
+ // Prefer smart logic over embedded metadata
+ if (!preferredLanguages.Contains(audioTrackLanguage, StringComparer.OrdinalIgnoreCase))
+ {
+ filteredStreams = streams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase))
+ .ToList();
+ }
+ }
+ else if (mode == SubtitlePlaybackMode.Always)
+ {
+ // always load the most suitable full subtitles
+ filteredStreams = streams.Where(s => !s.IsForced)
+ .ToList();
+ }
+ else if (mode == SubtitlePlaybackMode.OnlyForced)
+ {
+ // always load the most suitable full subtitles
+ filteredStreams = streams.Where(s => s.IsForced).ToList();
+ }
+
+ // load forced subs if we have found no suitable full subtitles
+ if (filteredStreams.Count == 0)
+ {
+ filteredStreams = streams
+ .Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
+ .ToList();
+ }
+
+ foreach (var stream in filteredStreams)
+ {
+ stream.Score = GetSubtitleScore(stream, preferredLanguages);
+ }
+ }
+
+ private static int FindIndex(string[] list, string value)
+ {
+ for (var i=0; i< list.Length; i++)
+ {
+ if (string.Equals(list[i], value, StringComparison.OrdinalIgnoreCase))
+ {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ private static int GetSubtitleScore(MediaStream stream, string[] languagePreferences)
+ {
+ var values = new List<int>();
+
+ var index = FindIndex(languagePreferences, stream.Language);
+
+ values.Add(index == -1 ? 0 : 100 - index);
+
+ values.Add(stream.IsForced ? 1 : 0);
+ values.Add(stream.IsDefault ? 1 : 0);
+ values.Add(stream.SupportsExternalStream ? 1 : 0);
+ values.Add(stream.IsTextSubtitleStream ? 1 : 0);
+ values.Add(stream.IsExternal ? 1 : 0);
+
+ values.Reverse();
+ var scale = 1;
+ var score = 0;
+
+ foreach (var value in values)
+ {
+ score += scale * (value + 1);
+ scale *= 10;
+ }
+
+ return score;
+ }
+
+ private static int GetBooleanOrderBy(bool value)
+ {
+ return value ? 0 : 1;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs
index 1cbf4235a..1319ee6f4 100644
--- a/Emby.Server.Implementations/Library/MusicManager.cs
+++ b/Emby.Server.Implementations/Library/MusicManager.cs
@@ -67,19 +67,19 @@ namespace Emby.Server.Implementations.Library
{
try
{
- return _libraryManager.GetMusicGenre(i).Id.ToString("N");
+ return _libraryManager.GetMusicGenre(i).Id;
}
catch
{
- return null;
+ return Guid.Empty;
}
- }).Where(i => i != null);
+ }).Where(i => !i.Equals(Guid.Empty)).ToArray();
return GetInstantMixFromGenreIds(genreIds, user, dtoOptions);
}
- public List<BaseItem> GetInstantMixFromGenreIds(IEnumerable<string> genreIds, User user, DtoOptions dtoOptions)
+ public List<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User user, DtoOptions dtoOptions)
{
return _libraryManager.GetItemList(new InternalItemsQuery(user)
{
@@ -89,7 +89,7 @@ namespace Emby.Server.Implementations.Library
Limit = 200,
- OrderBy = new [] { new Tuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending) },
+ OrderBy = new [] { new ValueTuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending) },
DtoOptions = dtoOptions
@@ -101,7 +101,7 @@ namespace Emby.Server.Implementations.Library
var genre = item as MusicGenre;
if (genre != null)
{
- return GetInstantMixFromGenreIds(new[] { item.Id.ToString("N") }, user, dtoOptions);
+ return GetInstantMixFromGenreIds(new[] { item.Id }, user, dtoOptions);
}
var playlist = item as Playlist;
diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs
index d0096de0c..14b28966a 100644
--- a/Emby.Server.Implementations/Library/ResolverHelper.cs
+++ b/Emby.Server.Implementations/Library/ResolverHelper.cs
@@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.Library
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))
+ if (string.IsNullOrEmpty(item.Path))
{
throw new ArgumentException("Item must have a Path");
}
@@ -108,17 +108,6 @@ namespace Emby.Server.Implementations.Library
}
/// <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>
@@ -140,7 +129,7 @@ namespace Emby.Server.Implementations.Library
}
// See if a different path came out of the resolver than what went in
- if (!string.Equals(args.Path, item.Path, StringComparison.OrdinalIgnoreCase))
+ if (!fileSystem.AreEqual(args.Path, item.Path))
{
var childData = args.IsDirectory ? args.GetFileSystemEntryByPath(item.Path) : null;
@@ -173,7 +162,14 @@ namespace Emby.Server.Implementations.Library
// directoryService.getFile may return null
if (info != null)
{
- item.DateCreated = fileSystem.GetCreationTimeUtc(info);
+ var dateCreated = fileSystem.GetCreationTimeUtc(info);
+
+ if (dateCreated.Equals(DateTime.MinValue))
+ {
+ dateCreated = DateTime.UtcNow;
+ }
+
+ item.DateCreated = dateCreated;
}
}
else
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
index d30aaa133..8872bd641 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
@@ -101,13 +101,15 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
if (LibraryManager.IsAudioFile(args.Path, libraryOptions))
{
- if (string.Equals(Path.GetExtension(args.Path), ".cue", StringComparison.OrdinalIgnoreCase))
+ var extension = Path.GetExtension(args.Path);
+
+ if (string.Equals(extension, ".cue", StringComparison.OrdinalIgnoreCase))
{
// if audio file exists of same name, return null
return null;
}
- var isMixedCollectionType = string.IsNullOrWhiteSpace(collectionType);
+ var isMixedCollectionType = string.IsNullOrEmpty(collectionType);
// For conflicting extensions, give priority to videos
if (isMixedCollectionType && LibraryManager.IsVideoFile(args.Path, libraryOptions))
@@ -134,6 +136,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
if (item != null)
{
+ item.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase);
+
item.IsInMixedFolder = true;
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
index b8ec41805..a33f101ae 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
@@ -52,14 +52,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// <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.
@@ -68,6 +61,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
return null;
}
+ if (!args.IsDirectory) return null;
+
+ // Avoid mis-identifying top folders
+ if (args.HasParent<MusicAlbum>()) return null;
+ if (args.Parent.IsRoot) return null;
+
return IsMusicAlbum(args) ? new MusicAlbum() : null;
}
@@ -117,24 +116,22 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
{
if (allowSubfolders)
{
+ if (notMultiDisc)
+ {
+ continue;
+ }
+
var path = fileSystemInfo.FullName;
- var isMultiDisc = IsMultiDiscFolder(path, libraryOptions);
+ var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryOptions, libraryManager);
- if (isMultiDisc)
+ if (hasMusic)
{
- var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryOptions, libraryManager);
-
- if (hasMusic)
+ if (IsMultiDiscFolder(path, libraryOptions))
{
logger.Debug("Found multi-disc folder: " + path);
discSubfolderCount++;
}
- }
- else
- {
- var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryOptions, libraryManager);
-
- if (hasMusic)
+ else
{
// If there are folders underneath with music that are not multidisc, then this can't be a multi-disc album
notMultiDisc = true;
diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
index 7e960f85e..556748183 100644
--- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
@@ -184,11 +184,6 @@ namespace Emby.Server.Implementations.Library.Resolvers
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;
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
index df441c5ed..b9aca1417 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
@@ -4,6 +4,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
using System;
using System.IO;
+using MediaBrowser.Model.Extensions;
namespace Emby.Server.Implementations.Library.Resolvers.Movies
{
@@ -30,14 +31,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
{
return null;
}
-
- if (filename.IndexOf("[boxset]", StringComparison.OrdinalIgnoreCase) != -1 ||
- args.ContainsFileSystemEntryByName("collection.xml"))
+
+ if (filename.IndexOf("[boxset]", StringComparison.OrdinalIgnoreCase) != -1 || args.ContainsFileSystemEntryByName("collection.xml"))
{
return new BoxSet
{
Path = args.Path,
- Name = ResolverHelper.StripBrackets(Path.GetFileName(args.Path))
+ Name = Path.GetFileName(args.Path).Replace("[boxset]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim()
};
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index d74235ec7..1394e3858 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -78,7 +78,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return ResolveVideos<Video>(parent, files, directoryService, false, collectionType, false);
}
- if (string.IsNullOrWhiteSpace(collectionType))
+ if (string.IsNullOrEmpty(collectionType))
{
// Owned items should just use the plain video type
if (parent == null)
@@ -113,7 +113,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
foreach (var child in fileSystemEntries)
{
// This is a hack but currently no better way to resolve a sometimes ambiguous situation
- if (string.IsNullOrWhiteSpace(collectionType))
+ if (string.IsNullOrEmpty(collectionType))
{
if (string.Equals(child.Name, "tvshow.nfo", StringComparison.OrdinalIgnoreCase) ||
string.Equals(child.Name, "season.nfo", StringComparison.OrdinalIgnoreCase))
@@ -126,6 +126,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
{
leftOver.Add(child);
}
+ else if (IsIgnored(child.Name))
+ {
+
+ }
else
{
files.Add(child);
@@ -172,6 +176,22 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return result;
}
+ 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;
+ }
+
private bool ContainsFile(List<VideoInfo> result, FileSystemMetadata file)
{
return result.Any(i => ContainsFile(i, file));
@@ -317,7 +337,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
//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))
+ if (!string.IsNullOrEmpty(justName))
{
// check for tmdb id
var tmdbid = justName.GetAttributeValue("tmdbid");
@@ -328,7 +348,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
}
}
- if (!string.IsNullOrWhiteSpace(item.Path))
+ if (!string.IsNullOrEmpty(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");
@@ -395,16 +415,14 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
Set3DFormat(movie);
return movie;
}
- else if (supportPhotos && !child.IsHidden && PhotoResolver.IsImageFile(child.FullName, _imageProcessor))
+ else if (supportPhotos && PhotoResolver.IsImageFile(child.FullName, _imageProcessor))
{
photos.Add(child);
}
}
// TODO: Allow GetMultiDiscMovie in here
- var supportsMultiVersion = !string.Equals(collectionType, CollectionType.HomeVideos) &&
- !string.Equals(collectionType, CollectionType.Photos) &&
- !string.Equals(collectionType, CollectionType.MusicVideos);
+ var supportsMultiVersion = true;
var result = ResolveVideos<T>(parent, fileSystemEntries, directoryService, supportsMultiVersion, collectionType, parseName) ??
new MultiItemResolverResult();
@@ -532,7 +550,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
}
}
- if (string.IsNullOrWhiteSpace(collectionType))
+ if (string.IsNullOrEmpty(collectionType))
{
return false;
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
index 48f5802a9..e3cce5f4b 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
@@ -94,7 +94,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
"backdrop",
"poster",
"cover",
- "logo"
+ "logo",
+ "default"
};
internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
index 8c59cf20f..e66c9f087 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
@@ -2,11 +2,20 @@
using MediaBrowser.Controller.Playlists;
using System;
using System.IO;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.Entities;
+using System.Linq;
namespace Emby.Server.Implementations.Library.Resolvers
{
public class PlaylistResolver : FolderResolver<Playlist>
{
+ private string[] SupportedCollectionTypes = new string[] {
+
+ string.Empty,
+ CollectionType.Music
+ };
+
/// <summary>
/// Resolves the specified args.
/// </summary>
@@ -31,10 +40,26 @@ namespace Emby.Server.Implementations.Library.Resolvers
return new Playlist
{
Path = args.Path,
- Name = ResolverHelper.StripBrackets(Path.GetFileName(args.Path))
+ Name = Path.GetFileName(args.Path).Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim()
};
}
}
+ else
+ {
+ if (SupportedCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ {
+ var extension = Path.GetExtension(args.Path);
+ if (Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ {
+ return new Playlist
+ {
+ Path = args.Path,
+ Name = Path.GetFileNameWithoutExtension(args.Path),
+ IsInMixedFolder = true
+ };
+ }
+ }
+ }
return null;
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
index 3bad69b56..d8343f7c6 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
@@ -50,24 +50,29 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var path = args.Path;
+ var seasonParserResult = new SeasonPathParser(namingOptions).Parse(path, true, true);
+
var season = new Season
{
- IndexNumber = new SeasonPathParser(namingOptions, new RegexProvider()).Parse(path, true, true).SeasonNumber,
+ IndexNumber = seasonParserResult.SeasonNumber,
SeriesId = series.Id,
SeriesName = series.Name
};
- if (season.IndexNumber.HasValue)
+ if (!season.IndexNumber.HasValue || !seasonParserResult.IsSeasonFolder)
{
var resolver = new Emby.Naming.TV.EpisodeResolver(namingOptions);
- var episodeInfo = resolver.Resolve(path, true);
+ var folderName = System.IO.Path.GetFileName(path);
+ var testPath = "\\\\test\\" + folderName;
+
+ var episodeInfo = resolver.Resolve(testPath, true);
if (episodeInfo != null)
{
if (episodeInfo.EpisodeNumber.HasValue && episodeInfo.SeasonNumber.HasValue)
{
- _logger.Info("Found folder underneath series with episode number: {0}. Season {1}. Episode {2}",
+ _logger.Debug("Found folder underneath series with episode number: {0}. Season {1}. Episode {2}",
path,
episodeInfo.SeasonNumber.Value,
episodeInfo.EpisodeNumber.Value);
@@ -75,7 +80,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null;
}
}
+ }
+ if (season.IndexNumber.HasValue)
+ {
var seasonNumber = season.IndexNumber.Value;
season.Name = seasonNumber == 0 ?
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
index a693e3b26..951f439c2 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
@@ -82,11 +82,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
};
}
}
- else if (string.IsNullOrWhiteSpace(collectionType))
+ else if (string.IsNullOrEmpty(collectionType))
{
if (args.ContainsFileSystemEntryByName("tvshow.nfo"))
{
- if (args.Parent.IsRoot)
+ if (args.Parent != null && 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;
@@ -99,7 +99,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
};
}
- if (args.Parent.IsRoot)
+ if (args.Parent != null && args.Parent.IsRoot)
{
return null;
}
@@ -160,11 +160,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return true;
}
- var allowOptimisticEpisodeDetection = isTvContentType;
- var namingOptions = ((LibraryManager)libraryManager).GetNamingOptions(allowOptimisticEpisodeDetection);
+ var namingOptions = ((LibraryManager)libraryManager).GetNamingOptions();
var episodeResolver = new Emby.Naming.TV.EpisodeResolver(namingOptions);
- var episodeInfo = episodeResolver.Resolve(fullName, false, false);
+ bool? isNamed = null;
+ bool? isOptimistic = null;
+
+ if (!isTvContentType)
+ {
+ isNamed = true;
+ isOptimistic = false;
+ }
+
+ var episodeInfo = episodeResolver.Resolve(fullName, false, isNamed, isOptimistic, null, false);
if (episodeInfo != null && episodeInfo.EpisodeNumber.HasValue)
{
return true;
@@ -206,7 +214,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
{
var namingOptions = ((LibraryManager)libraryManager).GetNamingOptions();
- var seasonNumber = new SeasonPathParser(namingOptions, new RegexProvider()).Parse(path, isTvContentType, isTvContentType).SeasonNumber;
+ var seasonNumber = new SeasonPathParser(namingOptions).Parse(path, isTvContentType, isTvContentType).SeasonNumber;
return seasonNumber.HasValue;
}
diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
index 8021399bd..7f04ac5bc 100644
--- a/Emby.Server.Implementations/Library/SearchEngine.cs
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -28,14 +28,14 @@ namespace Emby.Server.Implementations.Library
_libraryManager = libraryManager;
_userManager = userManager;
- _logger = logManager.GetLogger("Lucene");
+ _logger = logManager.GetLogger("SearchEngine");
}
- public async Task<QueryResult<SearchHintInfo>> GetSearchHints(SearchQuery query)
+ public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query)
{
User user = null;
- if (string.IsNullOrWhiteSpace(query.UserId))
+ if (query.UserId.Equals(Guid.Empty))
{
}
else
@@ -43,26 +43,22 @@ namespace Emby.Server.Implementations.Library
user = _userManager.GetUserById(query.UserId);
}
- var results = await GetSearchHints(query, user).ConfigureAwait(false);
-
- var searchResultArray = results.ToArray();
- results = searchResultArray;
-
- var count = searchResultArray.Length;
+ var results = GetSearchHints(query, user);
+ var totalRecordCount = results.Count;
if (query.StartIndex.HasValue)
{
- results = results.Skip(query.StartIndex.Value);
+ results = results.Skip(query.StartIndex.Value).ToList();
}
if (query.Limit.HasValue)
{
- results = results.Take(query.Limit.Value);
+ results = results.Take(query.Limit.Value).ToList();
}
return new QueryResult<SearchHintInfo>
{
- TotalRecordCount = count,
+ TotalRecordCount = totalRecordCount,
Items = results.ToArray()
};
@@ -83,24 +79,19 @@ namespace Emby.Server.Implementations.Library
/// <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)
+ private List<SearchHintInfo> GetSearchHints(SearchQuery query, User user)
{
var searchTerm = query.SearchTerm;
- if (searchTerm != null)
- {
- searchTerm = searchTerm.Trim().RemoveDiacritics();
- }
-
- if (string.IsNullOrWhiteSpace(searchTerm))
+ if (string.IsNullOrEmpty(searchTerm))
{
throw new ArgumentNullException("searchTerm");
}
- var terms = GetWords(searchTerm);
+ searchTerm = searchTerm.Trim().RemoveDiacritics();
var excludeItemTypes = query.ExcludeItemTypes.ToList();
- var includeItemTypes = (query.IncludeItemTypes ?? new string[] { }).ToList();
+ var includeItemTypes = (query.IncludeItemTypes ?? Array.Empty<string>()).ToList();
excludeItemTypes.Add(typeof(Year).Name);
excludeItemTypes.Add(typeof(Folder).Name);
@@ -169,13 +160,13 @@ namespace Emby.Server.Implementations.Library
var searchQuery = new InternalItemsQuery(user)
{
- NameContains = searchTerm,
+ SearchTerm = searchTerm,
ExcludeItemTypes = excludeItemTypes.ToArray(excludeItemTypes.Count),
IncludeItemTypes = includeItemTypes.ToArray(includeItemTypes.Count),
Limit = query.Limit,
- IncludeItemsByName = string.IsNullOrWhiteSpace(query.ParentId),
- ParentId = string.IsNullOrWhiteSpace(query.ParentId) ? (Guid?)null : new Guid(query.ParentId),
- OrderBy = new[] { new Tuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) },
+ IncludeItemsByName = string.IsNullOrEmpty(query.ParentId),
+ ParentId = string.IsNullOrEmpty(query.ParentId) ? Guid.Empty : new Guid(query.ParentId),
+ OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) },
Recursive = true,
IsKids = query.IsKids,
@@ -201,120 +192,25 @@ namespace Emby.Server.Implementations.Library
if (searchQuery.IncludeItemTypes.Length == 1 && string.Equals(searchQuery.IncludeItemTypes[0], "MusicArtist", StringComparison.OrdinalIgnoreCase))
{
- if (searchQuery.ParentId.HasValue)
+ if (!searchQuery.ParentId.Equals(Guid.Empty))
{
- searchQuery.AncestorIds = new string[] { searchQuery.ParentId.Value.ToString("N") };
+ searchQuery.AncestorIds = new[] { searchQuery.ParentId };
}
- searchQuery.ParentId = null;
+ searchQuery.ParentId = Guid.Empty;
searchQuery.IncludeItemsByName = true;
- searchQuery.IncludeItemTypes = new string[] { };
- mediaItems = _libraryManager.GetArtists(searchQuery).Items.Select(i => i.Item1).ToList();
+ searchQuery.IncludeItemTypes = Array.Empty<string>();
+ mediaItems = _libraryManager.GetAllArtists(searchQuery).Items.Select(i => i.Item1).ToList();
}
else
{
mediaItems = _libraryManager.GetItemList(searchQuery);
}
- var returnValue = mediaItems.Select(item =>
- {
- var index = GetIndex(item.Name, searchTerm, terms);
-
- return new Tuple<BaseItem, string, int>(item, index.Item1, index.Item2);
-
- }).OrderBy(i => i.Item3).ThenBy(i => i.Item1.SortName).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++)
+ return mediaItems.Select(i => new SearchHintInfo
{
- 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);
- }
+ Item = i
- /// <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"
- };
+ }).ToList();
}
}
}
diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index 7ef5ca35e..3714a7544 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/Emby.Server.Implementations/Library/UserDataManager.cs
@@ -12,7 +12,8 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Model.Querying;
+using MediaBrowser.Controller.Dto;
+using System.Globalization;
namespace Emby.Server.Implementations.Library
{
@@ -29,10 +30,13 @@ namespace Emby.Server.Implementations.Library
private readonly ILogger _logger;
private readonly IServerConfigurationManager _config;
- public UserDataManager(ILogManager logManager, IServerConfigurationManager config)
+ private Func<IUserManager> _userManager;
+
+ public UserDataManager(ILogManager logManager, IServerConfigurationManager config, Func<IUserManager> userManager)
{
_config = config;
_logger = logManager.GetLogger(GetType().Name);
+ _userManager = userManager;
}
/// <summary>
@@ -41,7 +45,14 @@ namespace Emby.Server.Implementations.Library
/// <value>The repository.</value>
public IUserDataRepository Repository { get; set; }
- public void SaveUserData(Guid userId, IHasUserData item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken)
+ public void SaveUserData(Guid userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken)
+ {
+ var user = _userManager().GetUserById(userId);
+
+ SaveUserData(user, item, userData, reason, cancellationToken);
+ }
+
+ public void SaveUserData(User user, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken)
{
if (userData == null)
{
@@ -51,15 +62,13 @@ namespace Emby.Server.Implementations.Library
{
throw new ArgumentNullException("item");
}
- if (userId == Guid.Empty)
- {
- throw new ArgumentNullException("userId");
- }
cancellationToken.ThrowIfCancellationRequested();
var keys = item.GetUserDataKeys();
+ var userId = user.InternalId;
+
foreach (var key in keys)
{
Repository.SaveUserData(userId, key, userData, cancellationToken);
@@ -73,7 +82,7 @@ namespace Emby.Server.Implementations.Library
Keys = keys,
UserData = userData,
SaveReason = reason,
- UserId = userId,
+ UserId = user.Id,
Item = item
}, _logger);
@@ -88,18 +97,9 @@ namespace Emby.Server.Implementations.Library
/// <returns></returns>
public void SaveAllUserData(Guid userId, UserItemData[] userData, CancellationToken cancellationToken)
{
- if (userData == null)
- {
- throw new ArgumentNullException("userData");
- }
- if (userId == Guid.Empty)
- {
- throw new ArgumentNullException("userId");
- }
+ var user = _userManager().GetUserById(userId);
- cancellationToken.ThrowIfCancellationRequested();
-
- Repository.SaveAllUserData(userId, userData, cancellationToken);
+ Repository.SaveAllUserData(user.InternalId, userData, cancellationToken);
}
/// <summary>
@@ -109,37 +109,30 @@ namespace Emby.Server.Implementations.Library
/// <returns></returns>
public List<UserItemData> GetAllUserData(Guid userId)
{
- if (userId == Guid.Empty)
- {
- throw new ArgumentNullException("userId");
- }
+ var user = _userManager().GetUserById(userId);
- return Repository.GetAllUserData(userId);
+ return Repository.GetAllUserData(user.InternalId);
}
public UserItemData GetUserData(Guid userId, Guid itemId, List<string> keys)
{
- if (userId == Guid.Empty)
- {
- throw new ArgumentNullException("userId");
- }
- if (keys == null)
- {
- throw new ArgumentNullException("keys");
- }
- if (keys.Count == 0)
- {
- throw new ArgumentException("UserData keys cannot be empty.");
- }
+ var user = _userManager().GetUserById(userId);
+
+ return GetUserData(user, itemId, keys);
+ }
+
+ public UserItemData GetUserData(User user, Guid itemId, List<string> keys)
+ {
+ var userId = user.InternalId;
var cacheKey = GetCacheKey(userId, itemId);
return _userData.GetOrAdd(cacheKey, k => GetUserDataInternal(userId, keys));
}
- private UserItemData GetUserDataInternal(Guid userId, List<string> keys)
+ private UserItemData GetUserDataInternal(long internalUserId, List<string> keys)
{
- var userData = Repository.GetUserData(userId, keys);
+ var userData = Repository.GetUserData(internalUserId, keys);
if (userData != null)
{
@@ -150,7 +143,6 @@ namespace Emby.Server.Implementations.Library
{
return new UserItemData
{
- UserId = userId,
Key = keys[0]
};
}
@@ -162,41 +154,41 @@ namespace Emby.Server.Implementations.Library
/// Gets the internal key.
/// </summary>
/// <returns>System.String.</returns>
- private string GetCacheKey(Guid userId, Guid itemId)
+ private string GetCacheKey(long internalUserId, Guid itemId)
{
- return userId.ToString("N") + itemId.ToString("N");
+ return internalUserId.ToString(CultureInfo.InvariantCulture) + "-" + itemId.ToString("N");
}
- public UserItemData GetUserData(IHasUserData user, IHasUserData item)
+ public UserItemData GetUserData(User user, BaseItem item)
{
- return GetUserData(user.Id, item);
+ return GetUserData(user, item.Id, item.GetUserDataKeys());
}
- public UserItemData GetUserData(string userId, IHasUserData item)
+ public UserItemData GetUserData(string userId, BaseItem item)
{
return GetUserData(new Guid(userId), item);
}
- public UserItemData GetUserData(Guid userId, IHasUserData item)
+ public UserItemData GetUserData(Guid userId, BaseItem item)
{
return GetUserData(userId, item.Id, item.GetUserDataKeys());
}
- public UserItemDataDto GetUserDataDto(IHasUserData item, User user)
+ public UserItemDataDto GetUserDataDto(BaseItem item, User user)
{
- var userData = GetUserData(user.Id, item);
+ var userData = GetUserData(user, item);
var dto = GetUserItemDataDto(userData);
- item.FillUserDataDtoValues(dto, userData, null, user, new ItemFields[] { });
+ item.FillUserDataDtoValues(dto, userData, null, user, new DtoOptions());
return dto;
}
- public UserItemDataDto GetUserDataDto(IHasUserData item, BaseItemDto itemDto, User user, ItemFields[] fields)
+ public UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto itemDto, User user, DtoOptions options)
{
- var userData = GetUserData(user.Id, item);
+ var userData = GetUserData(user, item);
var dto = GetUserItemDataDto(userData);
- item.FillUserDataDtoValues(dto, userData, itemDto, user, fields);
+ item.FillUserDataDtoValues(dto, userData, itemDto, user, options);
return dto;
}
@@ -230,13 +222,15 @@ namespace Emby.Server.Implementations.Library
{
var playedToCompletion = false;
- var positionTicks = reportedPositionTicks ?? item.RunTimeTicks ?? 0;
- var hasRuntime = item.RunTimeTicks.HasValue && item.RunTimeTicks > 0;
+ var runtimeTicks = item.GetRunTimeTicksForPlayState();
+
+ var positionTicks = reportedPositionTicks ?? runtimeTicks;
+ var hasRuntime = runtimeTicks > 0;
// If a position has been reported, and if we know the duration
if (positionTicks > 0 && hasRuntime)
{
- var pctIn = Decimal.Divide(positionTicks, item.RunTimeTicks.Value) * 100;
+ var pctIn = Decimal.Divide(positionTicks, runtimeTicks) * 100;
// Don't track in very beginning
if (pctIn < _config.Configuration.MinResumePct)
@@ -245,7 +239,7 @@ namespace Emby.Server.Implementations.Library
}
// If we're at the end, assume completed
- else if (pctIn > _config.Configuration.MaxResumePct || positionTicks >= item.RunTimeTicks.Value)
+ else if (pctIn > _config.Configuration.MaxResumePct || positionTicks >= runtimeTicks)
{
positionTicks = 0;
data.Played = playedToCompletion = true;
@@ -254,7 +248,7 @@ namespace Emby.Server.Implementations.Library
else
{
// Enforce MinResumeDuration
- var durationSeconds = TimeSpan.FromTicks(item.RunTimeTicks.Value).TotalSeconds;
+ var durationSeconds = TimeSpan.FromTicks(runtimeTicks).TotalSeconds;
if (durationSeconds < _config.Configuration.MinResumeDurationSeconds)
{
diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs
index 71c953b2c..b13a255aa 100644
--- a/Emby.Server.Implementations/Library/UserManager.cs
+++ b/Emby.Server.Implementations/Library/UserManager.cs
@@ -28,6 +28,11 @@ using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.IO;
+using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Security;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Controller.Plugins;
namespace Emby.Server.Implementations.Library
{
@@ -40,7 +45,9 @@ namespace Emby.Server.Implementations.Library
/// Gets the users.
/// </summary>
/// <value>The users.</value>
- public IEnumerable<User> Users { get; private set; }
+ public IEnumerable<User> Users { get { return _users; } }
+
+ private User[] _users;
/// <summary>
/// The _logger
@@ -72,6 +79,9 @@ namespace Emby.Server.Implementations.Library
private readonly IFileSystem _fileSystem;
private readonly ICryptoProvider _cryptographyProvider;
+ private IAuthenticationProvider[] _authenticationProviders;
+ private DefaultAuthenticationProvider _defaultAuthenticationProvider;
+
public UserManager(ILogger logger, IServerConfigurationManager configurationManager, IUserRepository userRepository, IXmlSerializer xmlSerializer, INetworkManager networkManager, Func<IImageProcessor> imageProcessorFactory, Func<IDtoService> dtoServiceFactory, Func<IConnectManager> connectFactory, IServerApplicationHost appHost, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ICryptoProvider cryptographyProvider)
{
_logger = logger;
@@ -86,16 +96,38 @@ namespace Emby.Server.Implementations.Library
_fileSystem = fileSystem;
_cryptographyProvider = cryptographyProvider;
ConfigurationManager = configurationManager;
- Users = new List<User>();
+ _users = Array.Empty<User>();
DeletePinFile();
}
+ public NameIdPair[] GetAuthenticationProviders()
+ {
+ return _authenticationProviders
+ .Where(i => i.IsEnabled)
+ .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1)
+ .ThenBy(i => i.Name)
+ .Select(i => new NameIdPair
+ {
+ Name = i.Name,
+ Id = GetAuthenticationProviderId(i)
+ })
+ .ToArray();
+ }
+
+ public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders)
+ {
+ _authenticationProviders = authenticationProviders.ToArray();
+
+ _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
+ }
+
#region UserUpdated Event
/// <summary>
/// Occurs when [user updated].
/// </summary>
public event EventHandler<GenericEventArgs<User>> UserUpdated;
+ public event EventHandler<GenericEventArgs<User>> UserPolicyUpdated;
public event EventHandler<GenericEventArgs<User>> UserConfigurationUpdated;
public event EventHandler<GenericEventArgs<User>> UserLockedOut;
@@ -132,7 +164,7 @@ namespace Emby.Server.Implementations.Library
/// <exception cref="System.ArgumentNullException"></exception>
public User GetUserById(Guid id)
{
- if (id == Guid.Empty)
+ if (id.Equals(Guid.Empty))
{
throw new ArgumentNullException("id");
}
@@ -162,7 +194,7 @@ namespace Emby.Server.Implementations.Library
public void Initialize()
{
- Users = LoadUsers();
+ _users = LoadUsers();
var users = Users.ToList();
@@ -218,7 +250,7 @@ namespace Emby.Server.Implementations.Library
return builder.ToString();
}
- public async Task<User> AuthenticateUser(string username, string password, string hashedPassword, string passwordMd5, string remoteEndPoint, bool isUserSession)
+ public async Task<User> AuthenticateUser(string username, string password, string hashedPassword, string remoteEndPoint, bool isUserSession)
{
if (string.IsNullOrWhiteSpace(username))
{
@@ -229,18 +261,16 @@ namespace Emby.Server.Implementations.Library
.FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
var success = false;
+ IAuthenticationProvider authenticationProvider = null;
if (user != null)
{
- if (password != null)
- {
- hashedPassword = GetHashedString(user, password);
- }
-
// Authenticate using local credentials if not a guest
if (!user.ConnectLinkType.HasValue || user.ConnectLinkType.Value != UserLinkType.Guest)
{
- success = AuthenticateLocalUser(user, password, hashedPassword, remoteEndPoint);
+ var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false);
+ authenticationProvider = authResult.Item1;
+ success = authResult.Item2;
}
// Maybe user accidently entered connect credentials. let's be flexible
@@ -248,7 +278,7 @@ namespace Emby.Server.Implementations.Library
{
try
{
- await _connectFactory().Authenticate(user.ConnectUserName, password, passwordMd5).ConfigureAwait(false);
+ await _connectFactory().Authenticate(user.ConnectUserName, password).ConfigureAwait(false);
success = true;
}
catch
@@ -257,13 +287,43 @@ namespace Emby.Server.Implementations.Library
}
}
}
+ else
+ {
+ // user is null
+ var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false);
+ authenticationProvider = authResult.Item1;
+ success = authResult.Item2;
+
+ if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider))
+ {
+ user = await CreateUser(username).ConfigureAwait(false);
+
+ var hasNewUserPolicy = authenticationProvider as IHasNewUserPolicy;
+ if (hasNewUserPolicy != null)
+ {
+ var policy = hasNewUserPolicy.GetNewUserPolicy();
+ UpdateUserPolicy(user, policy, true);
+ }
+ }
+ }
+
+ if (success && user != null && authenticationProvider != null)
+ {
+ var providerId = GetAuthenticationProviderId(authenticationProvider);
+
+ if (!string.Equals(providerId, user.Policy.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase))
+ {
+ user.Policy.AuthenticationProviderId = providerId;
+ UpdateUserPolicy(user, user.Policy, true);
+ }
+ }
// Try originally entered username
if (!success && (user == null || !string.Equals(user.ConnectUserName, username, StringComparison.OrdinalIgnoreCase)))
{
try
{
- var connectAuthResult = await _connectFactory().Authenticate(username, password, passwordMd5).ConfigureAwait(false);
+ var connectAuthResult = await _connectFactory().Authenticate(username, password).ConfigureAwait(false);
user = Users.FirstOrDefault(i => string.Equals(i.ConnectUserId, connectAuthResult.User.Id, StringComparison.OrdinalIgnoreCase));
@@ -285,6 +345,19 @@ namespace Emby.Server.Implementations.Library
throw new SecurityException(string.Format("The {0} account is currently disabled. Please consult with your administrator.", user.Name));
}
+ if (user != null)
+ {
+ if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint))
+ {
+ throw new SecurityException("Forbidden.");
+ }
+
+ if (!user.IsParentalScheduleAllowed())
+ {
+ throw new SecurityException("User is not allowed access at this time.");
+ }
+ }
+
// Update LastActivityDate and LastLoginDate, then save
if (success)
{
@@ -305,34 +378,106 @@ namespace Emby.Server.Implementations.Library
return success ? user : null;
}
- private bool AuthenticateLocalUser(User user, string password, string hashedPassword, string remoteEndPoint)
+ private string GetAuthenticationProviderId(IAuthenticationProvider provider)
{
- bool success;
+ return provider.GetType().FullName;
+ }
- if (password == null)
+ private IAuthenticationProvider GetAuthenticationProvider(User user)
+ {
+ return GetAuthenticationProviders(user).First();
+ }
+
+ private IAuthenticationProvider[] GetAuthenticationProviders(User user)
+ {
+ var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId;
+
+ var providers = _authenticationProviders.Where(i => i.IsEnabled).ToArray();
+
+ if (!string.IsNullOrEmpty(authenticationProviderId))
{
- // legacy
- success = string.Equals(GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
+ providers = providers.Where(i => string.Equals(authenticationProviderId, GetAuthenticationProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray();
}
- else
+
+ if (providers.Length == 0)
{
- success = string.Equals(GetPasswordHash(user), GetHashedString(user, password), StringComparison.OrdinalIgnoreCase);
+ providers = new IAuthenticationProvider[] { _defaultAuthenticationProvider };
}
- if (!success && _networkManager.IsInLocalNetwork(remoteEndPoint) && user.Configuration.EnableLocalPassword)
+ return providers;
+ }
+
+ private async Task<bool> AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser)
+ {
+ try
{
- if (password == null)
+ var requiresResolvedUser = provider as IRequiresResolvedUser;
+ if (requiresResolvedUser != null)
{
- // legacy
- success = string.Equals(GetLocalPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
+ await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false);
}
else
{
- success = string.Equals(GetLocalPasswordHash(user), GetHashedString(user, password), StringComparison.OrdinalIgnoreCase);
+ await provider.Authenticate(username, password).ConfigureAwait(false);
+ }
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error authenticating with provider {0}", ex, provider.Name);
+
+ return false;
+ }
+ }
+
+ private async Task<Tuple<IAuthenticationProvider, bool>> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint)
+ {
+ bool success = false;
+ IAuthenticationProvider authenticationProvider = null;
+
+ if (password != null && user != null)
+ {
+ // Doesn't look like this is even possible to be used, because of password == null checks below
+ hashedPassword = _defaultAuthenticationProvider.GetHashedString(user, password);
+ }
+
+ if (password == null)
+ {
+ // legacy
+ success = string.Equals(_defaultAuthenticationProvider.GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
+ }
+ else
+ {
+ foreach (var provider in GetAuthenticationProviders(user))
+ {
+ success = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
+
+ if (success)
+ {
+ authenticationProvider = provider;
+ break;
+ }
}
}
- return success;
+ if (user != null)
+ {
+ if (!success && _networkManager.IsInLocalNetwork(remoteEndPoint) && user.Configuration.EnableLocalPassword)
+ {
+ if (password == null)
+ {
+ // legacy
+ success = string.Equals(GetLocalPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
+ }
+ else
+ {
+ success = string.Equals(GetLocalPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase);
+ }
+ }
+ }
+
+ return new Tuple<IAuthenticationProvider, bool>(authenticationProvider, success);
}
private void UpdateInvalidLoginAttemptCount(User user, int newValue)
@@ -367,63 +512,41 @@ namespace Emby.Server.Implementations.Library
}
}
- private string GetPasswordHash(User user)
- {
- return string.IsNullOrEmpty(user.Password)
- ? GetEmptyHashedString(user)
- : user.Password;
- }
-
private string GetLocalPasswordHash(User user)
{
return string.IsNullOrEmpty(user.EasyPassword)
- ? GetEmptyHashedString(user)
+ ? _defaultAuthenticationProvider.GetEmptyHashedString(user)
: user.EasyPassword;
}
private bool IsPasswordEmpty(User user, string passwordHash)
{
- return string.Equals(passwordHash, GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase);
- }
-
- private string GetEmptyHashedString(User user)
- {
- return GetHashedString(user, string.Empty);
- }
-
- /// <summary>
- /// Gets the hashed string.
- /// </summary>
- private string GetHashedString(User user, string str)
- {
- var salt = user.Salt;
- if (salt != null)
- {
- // return BCrypt.HashPassword(str, salt);
- }
-
- // legacy
- return BitConverter.ToString(_cryptographyProvider.ComputeSHA1(Encoding.UTF8.GetBytes(str))).Replace("-", string.Empty);
+ return string.Equals(passwordHash, _defaultAuthenticationProvider.GetEmptyHashedString(user), StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Loads the users from the repository
/// </summary>
/// <returns>IEnumerable{User}.</returns>
- private List<User> LoadUsers()
+ private User[] LoadUsers()
{
- var users = UserRepository.RetrieveAllUsers().ToList();
+ var users = UserRepository.RetrieveAllUsers();
// There always has to be at least one user.
if (users.Count == 0)
{
- var name = MakeValidUsername(Environment.UserName);
+ var defaultName = Environment.UserName;
+ if (string.IsNullOrWhiteSpace(defaultName))
+ {
+ defaultName = "MyEmbyUser";
+ }
+ var name = MakeValidUsername(defaultName);
var user = InstantiateNewUser(name);
user.DateLastSaved = DateTime.UtcNow;
- UserRepository.SaveUser(user, CancellationToken.None);
+ UserRepository.CreateUser(user);
users.Add(user);
@@ -433,7 +556,7 @@ namespace Emby.Server.Implementations.Library
UpdateUserPolicy(user, user.Policy, false);
}
- return users;
+ return users.ToArray();
}
public UserDto GetUserDto(User user, string remoteEndPoint = null)
@@ -443,9 +566,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException("user");
}
- var passwordHash = GetPasswordHash(user);
-
- var hasConfiguredPassword = !IsPasswordEmpty(user, passwordHash);
+ var hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result;
var hasConfiguredEasyPassword = !IsPasswordEmpty(user, GetLocalPasswordHash(user));
var hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
@@ -454,7 +575,7 @@ namespace Emby.Server.Implementations.Library
var dto = new UserDto
{
- Id = user.Id.ToString("N"),
+ Id = user.Id,
Name = user.Name,
HasPassword = hasPassword,
HasConfiguredPassword = hasConfiguredPassword,
@@ -577,7 +698,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException("user");
}
- if (user.Id == Guid.Empty || !Users.Any(u => u.Id.Equals(user.Id)))
+ if (user.Id.Equals(Guid.Empty) || !Users.Any(u => u.Id.Equals(user.Id)))
{
throw new ArgumentException(string.Format("User with name '{0}' and Id {1} does not exist.", user.Name, user.Id));
}
@@ -585,7 +706,7 @@ namespace Emby.Server.Implementations.Library
user.DateModified = DateTime.UtcNow;
user.DateLastSaved = DateTime.UtcNow;
- UserRepository.SaveUser(user, CancellationToken.None);
+ UserRepository.UpdateUser(user);
OnUserUpdated(user);
}
@@ -626,11 +747,11 @@ namespace Emby.Server.Implementations.Library
var list = Users.ToList();
list.Add(user);
- Users = list;
+ _users = list.ToArray();
user.DateLastSaved = DateTime.UtcNow;
- UserRepository.SaveUser(user, CancellationToken.None);
+ UserRepository.CreateUser(user);
EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs<User> { Argument = user }, _logger);
@@ -658,7 +779,7 @@ namespace Emby.Server.Implementations.Library
if (user.ConnectLinkType.HasValue)
{
- await _connectFactory().RemoveConnect(user.Id.ToString("N")).ConfigureAwait(false);
+ await _connectFactory().RemoveConnect(user).ConfigureAwait(false);
}
var allUsers = Users.ToList();
@@ -684,7 +805,7 @@ namespace Emby.Server.Implementations.Library
{
var configPath = GetConfigurationFilePath(user);
- UserRepository.DeleteUser(user, CancellationToken.None);
+ UserRepository.DeleteUser(user);
try
{
@@ -697,7 +818,7 @@ namespace Emby.Server.Implementations.Library
DeleteUserPolicy(user);
- Users = allUsers.Where(i => i.Id != user.Id).ToList();
+ _users = allUsers.Where(i => i.Id != user.Id).ToArray();
OnUserDeleted(user);
}
@@ -711,9 +832,9 @@ namespace Emby.Server.Implementations.Library
/// Resets the password by clearing it.
/// </summary>
/// <returns>Task.</returns>
- public void ResetPassword(User user)
+ public Task ResetPassword(User user)
{
- ChangePassword(user, string.Empty, null);
+ return ChangePassword(user, string.Empty);
}
public void ResetEasyPassword(User user)
@@ -721,29 +842,19 @@ namespace Emby.Server.Implementations.Library
ChangeEasyPassword(user, string.Empty, null);
}
- public void ChangePassword(User user, string newPassword, string newPasswordHash)
+ public async Task ChangePassword(User user, string newPassword)
{
if (user == null)
{
throw new ArgumentNullException("user");
}
- if (newPassword != null)
- {
- newPasswordHash = GetHashedString(user, newPassword);
- }
-
- if (string.IsNullOrWhiteSpace(newPasswordHash))
- {
- throw new ArgumentNullException("newPasswordHash");
- }
-
if (user.ConnectLinkType.HasValue && user.ConnectLinkType.Value == UserLinkType.Guest)
{
throw new ArgumentException("Passwords for guests cannot be changed.");
}
- user.Password = newPasswordHash;
+ await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false);
UpdateUser(user);
@@ -759,7 +870,7 @@ namespace Emby.Server.Implementations.Library
if (newPassword != null)
{
- newPasswordHash = GetHashedString(user, newPassword);
+ newPasswordHash = _defaultAuthenticationProvider.GetHashedString(user, newPassword);
}
if (string.IsNullOrWhiteSpace(newPasswordHash))
@@ -801,7 +912,7 @@ namespace Emby.Server.Implementations.Library
private PasswordPinCreationResult _lastPasswordPinCreationResult;
private int _pinAttempts;
- private PasswordPinCreationResult CreatePasswordResetPin()
+ private async Task<PasswordPinCreationResult> CreatePasswordResetPin()
{
var num = new Random().Next(1, 9999);
@@ -815,7 +926,7 @@ namespace Emby.Server.Implementations.Library
var text = new StringBuilder();
- var localAddress = _appHost.GetLocalApiUrl(CancellationToken.None).Result ?? string.Empty;
+ var localAddress = (await _appHost.GetLocalApiUrl(CancellationToken.None).ConfigureAwait(false)) ?? string.Empty;
text.AppendLine("Use your web browser to visit:");
text.AppendLine(string.Empty);
@@ -844,7 +955,7 @@ namespace Emby.Server.Implementations.Library
return result;
}
- public ForgotPasswordResult StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
+ public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
{
DeletePinFile();
@@ -872,7 +983,7 @@ namespace Emby.Server.Implementations.Library
action = ForgotPasswordAction.PinCode;
}
- var result = CreatePasswordResetPin();
+ var result = await CreatePasswordResetPin().ConfigureAwait(false);
pinFile = result.PinFile;
expirationDate = result.ExpirationDate;
}
@@ -885,7 +996,7 @@ namespace Emby.Server.Implementations.Library
};
}
- public PinRedeemResult RedeemPasswordResetPin(string pin)
+ public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
{
DeletePinFile();
@@ -906,7 +1017,7 @@ namespace Emby.Server.Implementations.Library
foreach (var user in users)
{
- ResetPassword(user);
+ await ResetPassword(user).ConfigureAwait(false);
if (user.Policy.IsDisabled)
{
@@ -953,7 +1064,7 @@ namespace Emby.Server.Implementations.Library
public UserPolicy GetUserPolicy(User user)
{
- var path = GetPolifyFilePath(user);
+ var path = GetPolicyFilePath(user);
try
{
@@ -988,7 +1099,7 @@ namespace Emby.Server.Implementations.Library
}
private readonly object _policySyncLock = new object();
- public void UpdateUserPolicy(string userId, UserPolicy userPolicy)
+ public void UpdateUserPolicy(Guid userId, UserPolicy userPolicy)
{
var user = GetUserById(userId);
UpdateUserPolicy(user, userPolicy, true);
@@ -1003,7 +1114,7 @@ namespace Emby.Server.Implementations.Library
userPolicy = _jsonSerializer.DeserializeFromString<UserPolicy>(json);
}
- var path = GetPolifyFilePath(user);
+ var path = GetPolicyFilePath(user);
_fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path));
@@ -1013,12 +1124,15 @@ namespace Emby.Server.Implementations.Library
user.Policy = userPolicy;
}
- UpdateConfiguration(user, user.Configuration, true);
+ if (fireEvent)
+ {
+ EventHelper.FireEventIfNotNull(UserPolicyUpdated, this, new GenericEventArgs<User> { Argument = user }, _logger);
+ }
}
private void DeleteUserPolicy(User user)
{
- var path = GetPolifyFilePath(user);
+ var path = GetPolicyFilePath(user);
try
{
@@ -1037,7 +1151,7 @@ namespace Emby.Server.Implementations.Library
}
}
- private string GetPolifyFilePath(User user)
+ private string GetPolicyFilePath(User user)
{
return Path.Combine(user.ConfigurationDirectoryPath, "policy.xml");
}
@@ -1075,9 +1189,14 @@ namespace Emby.Server.Implementations.Library
}
private readonly object _configSyncLock = new object();
- public void UpdateConfiguration(string userId, UserConfiguration config)
+ public void UpdateConfiguration(Guid userId, UserConfiguration config)
{
var user = GetUserById(userId);
+ UpdateConfiguration(user, config);
+ }
+
+ public void UpdateConfiguration(User user, UserConfiguration config)
+ {
UpdateConfiguration(user, config, true);
}
@@ -1106,4 +1225,56 @@ namespace Emby.Server.Implementations.Library
}
}
}
+
+ public class DeviceAccessEntryPoint : IServerEntryPoint
+ {
+ private IUserManager _userManager;
+ private IAuthenticationRepository _authRepo;
+ private IDeviceManager _deviceManager;
+ private ISessionManager _sessionManager;
+
+ public DeviceAccessEntryPoint(IUserManager userManager, IAuthenticationRepository authRepo, IDeviceManager deviceManager, ISessionManager sessionManager)
+ {
+ _userManager = userManager;
+ _authRepo = authRepo;
+ _deviceManager = deviceManager;
+ _sessionManager = sessionManager;
+ }
+
+ public void Run()
+ {
+ _userManager.UserPolicyUpdated += _userManager_UserPolicyUpdated;
+ }
+
+ private void _userManager_UserPolicyUpdated(object sender, GenericEventArgs<User> e)
+ {
+ var user = e.Argument;
+ if (!user.Policy.EnableAllDevices)
+ {
+ UpdateDeviceAccess(user);
+ }
+ }
+
+ private void UpdateDeviceAccess(User user)
+ {
+ var existing = _authRepo.Get(new AuthenticationInfoQuery
+ {
+ UserId = user.Id
+
+ }).Items;
+
+ foreach (var authInfo in existing)
+ {
+ if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId))
+ {
+ _sessionManager.Logout(authInfo);
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+
+ }
+ }
} \ No newline at end of file
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index e97bf11c3..42f922710 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -39,24 +39,15 @@ namespace Emby.Server.Implementations.Library
_config = config;
}
- public async Task<Folder[]> GetUserViews(UserViewQuery query, CancellationToken cancellationToken)
+ public Folder[] GetUserViews(UserViewQuery query)
{
var user = _userManager.GetUserById(query.UserId);
- var folders = user.RootFolder
+ var folders = _libraryManager.GetUserRootFolder()
.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 groupedFolders = new List<ICollectionFolder>();
var list = new List<Folder>();
@@ -68,7 +59,7 @@ namespace Emby.Server.Implementations.Library
if (UserView.IsUserSpecific(folder))
{
- list.Add(_libraryManager.GetNamedView(user, folder.Name, folder.Id.ToString("N"), folderViewType, null, cancellationToken));
+ list.Add(_libraryManager.GetNamedView(user, folder.Name, folder.Id, folderViewType, null));
continue;
}
@@ -80,7 +71,7 @@ namespace Emby.Server.Implementations.Library
if (query.PresetViews.Contains(folderViewType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
{
- list.Add(GetUserView(folder, folderViewType, string.Empty, cancellationToken));
+ list.Add(GetUserView(folder, folderViewType, string.Empty));
}
else
{
@@ -90,7 +81,7 @@ namespace Emby.Server.Implementations.Library
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))
+ var parents = groupedFolders.Where(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase) || string.IsNullOrEmpty(i.CollectionType))
.ToList();
if (parents.Count > 0)
@@ -99,41 +90,38 @@ namespace Emby.Server.Implementations.Library
"TvShows" :
"Movies";
- list.Add(GetUserView(parents, viewType, localizationKey, string.Empty, user, query.PresetViews, cancellationToken));
+ list.Add(GetUserView(parents, viewType, localizationKey, string.Empty, user, query.PresetViews));
}
}
if (_config.Configuration.EnableFolderView)
{
var name = _localizationManager.GetLocalizedString("Folders");
- list.Add(_libraryManager.GetNamedView(name, CollectionType.Folders, string.Empty, cancellationToken));
+ list.Add(_libraryManager.GetNamedView(name, CollectionType.Folders, string.Empty));
}
if (query.IncludeExternalContent)
{
- var channelResult = await _channelManager.GetChannelsInternal(new ChannelQuery
+ var channelResult = _channelManager.GetChannelsInternal(new ChannelQuery
{
UserId = query.UserId
-
- }, cancellationToken).ConfigureAwait(false);
+ });
var channels = channelResult.Items;
- if (_config.Configuration.EnableChannelView && channels.Length > 0)
- {
- list.Add(_channelManager.GetInternalChannelFolder(cancellationToken));
- }
- else
- {
- list.AddRange(channels);
- }
+ list.AddRange(channels);
- if (_liveTvManager.GetEnabledUsers().Select(i => i.Id.ToString("N")).Contains(query.UserId))
+ if (_liveTvManager.GetEnabledUsers().Select(i => i.Id).Contains(query.UserId))
{
list.Add(_liveTvManager.GetInternalLiveTvFolder(CancellationToken.None));
}
}
+ if (!query.IncludeHidden)
+ {
+ list = list.Where(i => !user.Configuration.MyMediaExcludes.Contains(i.Id.ToString("N"))).ToList();
+ }
+
var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
var orders = user.Configuration.OrderedViews.ToList();
@@ -148,7 +136,7 @@ namespace Emby.Server.Implementations.Library
var view = i as UserView;
if (view != null)
{
- if (view.DisplayParentId != Guid.Empty)
+ if (!view.DisplayParentId.Equals(Guid.Empty))
{
index = orders.IndexOf(view.DisplayParentId.ToString("N"));
}
@@ -162,21 +150,21 @@ namespace Emby.Server.Implementations.Library
.ToArray();
}
- public UserView GetUserSubViewWithName(string name, string parentId, string type, string sortName, CancellationToken cancellationToken)
+ public UserView GetUserSubViewWithName(string name, Guid parentId, string type, string sortName)
{
var uniqueId = parentId + "subview" + type;
- return _libraryManager.GetNamedView(name, parentId, type, sortName, uniqueId, cancellationToken);
+ return _libraryManager.GetNamedView(name, parentId, type, sortName, uniqueId);
}
- public UserView GetUserSubView(string parentId, string type, string localizationKey, string sortName, CancellationToken cancellationToken)
+ public UserView GetUserSubView(Guid parentId, string type, string localizationKey, string sortName)
{
var name = _localizationManager.GetLocalizedString(localizationKey);
- return GetUserSubViewWithName(name, parentId, type, sortName, cancellationToken);
+ return GetUserSubViewWithName(name, parentId, type, sortName);
}
- private Folder GetUserView(List<ICollectionFolder> parents, string viewType, string localizationKey, string sortName, User user, string[] presetViews, CancellationToken cancellationToken)
+ private Folder GetUserView(List<ICollectionFolder> parents, string viewType, string localizationKey, string sortName, User user, string[] presetViews)
{
if (parents.Count == 1 && parents.All(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase)))
{
@@ -185,16 +173,16 @@ namespace Emby.Server.Implementations.Library
return (Folder)parents[0];
}
- return GetUserView((Folder)parents[0], viewType, string.Empty, cancellationToken);
+ return GetUserView((Folder)parents[0], viewType, string.Empty);
}
var name = _localizationManager.GetLocalizedString(localizationKey);
- return _libraryManager.GetNamedView(user, name, viewType, sortName, cancellationToken);
+ return _libraryManager.GetNamedView(user, name, viewType, sortName);
}
- public UserView GetUserView(Folder parent, string viewType, string sortName, CancellationToken cancellationToken)
+ public UserView GetUserView(Folder parent, string viewType, string sortName)
{
- return _libraryManager.GetShadowView(parent, viewType, sortName, cancellationToken);
+ return _libraryManager.GetShadowView(parent, viewType, sortName);
}
public List<Tuple<BaseItem, List<BaseItem>>> GetLatestItems(LatestItemsQuery request, DtoOptions options)
@@ -246,9 +234,26 @@ namespace Emby.Server.Implementations.Library
var parents = new List<BaseItem>();
- if (!string.IsNullOrWhiteSpace(parentId))
+ if (!parentId.Equals(Guid.Empty))
{
- var parent = _libraryManager.GetItemById(parentId) as Folder;
+ var parentItem = _libraryManager.GetItemById(parentId);
+ var parentItemChannel = parentItem as Channel;
+ if (parentItemChannel != null)
+ {
+ return _channelManager.GetLatestChannelItemsInternal(new InternalItemsQuery(user)
+ {
+ ChannelIds = new [] { parentId },
+ IsPlayed = request.IsPlayed,
+ StartIndex = request.StartIndex,
+ Limit = request.Limit,
+ IncludeItemTypes = request.IncludeItemTypes,
+ EnableTotalRecordCount = false
+
+
+ }, CancellationToken.None).Result.Items.ToList();
+ }
+
+ var parent = parentItem as Folder;
if (parent != null)
{
parents.Add(parent);
@@ -264,7 +269,7 @@ namespace Emby.Server.Implementations.Library
if (parents.Count == 0)
{
- parents = user.RootFolder.GetChildren(user, true)
+ parents = _libraryManager.GetUserRootFolder().GetChildren(user, true)
.Where(i => i is Folder)
.Where(i => !user.Configuration.LatestItemsExcludes.Contains(i.Id.ToString("N")))
.ToList();
@@ -275,6 +280,24 @@ namespace Emby.Server.Implementations.Library
return new List<BaseItem>();
}
+ if (includeItemTypes.Length == 0)
+ {
+ // Handle situations with the grouping setting, e.g. movies showing up in tv, etc.
+ // Thanks to mixed content libraries included in the UserView
+ var hasCollectionType = parents.OfType<UserView>().ToArray();
+ if (hasCollectionType.Length > 0)
+ {
+ if (hasCollectionType.All(i => string.Equals(i.CollectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase)))
+ {
+ includeItemTypes = new string[] { "Movie" };
+ }
+ else if (hasCollectionType.All(i => string.Equals(i.CollectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)))
+ {
+ includeItemTypes = new string[] { "Episode" };
+ }
+ }
+ }
+
var mediaTypes = new List<string>();
if (includeItemTypes.Length == 0)
@@ -285,6 +308,7 @@ namespace Emby.Server.Implementations.Library
{
case CollectionType.Books:
mediaTypes.Add(MediaType.Book);
+ mediaTypes.Add(MediaType.Audio);
break;
case CollectionType.Games:
mediaTypes.Add(MediaType.Game);
@@ -318,12 +342,12 @@ namespace Emby.Server.Implementations.Library
typeof(MusicGenre).Name,
typeof(Genre).Name
- } : new string[] { };
+ } : Array.Empty<string>();
var query = new InternalItemsQuery(user)
{
IncludeItemTypes = includeItemTypes,
- OrderBy = new[] { new Tuple<string, SortOrder>(ItemSortBy.DateCreated, SortOrder.Descending) },
+ OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DateCreated, SortOrder.Descending) },
IsFolder = includeItemTypes.Length == 0 ? false : (bool?)null,
ExcludeItemTypes = excludeItemTypes,
IsVirtualItem = false,
diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
index 1a53ad672..cd2aab4c8 100644
--- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
@@ -81,33 +81,27 @@ namespace Emby.Server.Implementations.Library.Validators
progress.Report(percent);
}
- names = names.Select(i => i.RemoveDiacritics()).DistinctNames().ToList();
-
- var artistEntities = _libraryManager.GetItemList(new InternalItemsQuery
+ var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(MusicArtist).Name }
-
+ IncludeItemTypes = new[] { typeof(MusicArtist).Name },
+ IsDeadArtist = true,
+ IsLocked = false
}).Cast<MusicArtist>().ToList();
- foreach (var artist in artistEntities)
+ foreach (var item in deadEntities)
{
- if (!artist.IsAccessedByName)
+ if (!item.IsAccessedByName)
{
continue;
}
+
+ _logger.Info("Deleting dead {2} {0} {1}.", item.Id.ToString("N"), item.Name, item.GetType().Name);
- var name = (artist.Name ?? string.Empty).RemoveDiacritics();
-
- if (!names.Contains(name, StringComparer.OrdinalIgnoreCase))
+ _libraryManager.DeleteItem(item, new DeleteOptions
{
- _logger.Info("Deleting dead artist {0} {1}.", artist.Id.ToString("N"), artist.Name);
+ DeleteFileLocation = false
- await _libraryManager.DeleteItem(artist, new DeleteOptions
- {
- DeleteFileLocation = false
-
- }).ConfigureAwait(false);
- }
+ }, false);
}
progress.Report(100);
diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
index 39630cf96..1f4e1de92 100644
--- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
@@ -1,18 +1,11 @@
-using MediaBrowser.Common.Progress;
-using MediaBrowser.Controller.Configuration;
+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.Controller.IO;
using MediaBrowser.Model.IO;
namespace Emby.Server.Implementations.Library.Validators
@@ -73,7 +66,7 @@ namespace Emby.Server.Implementations.Library.Validators
var options = new MetadataRefreshOptions(_fileSystem)
{
- ImageRefreshMode = ImageRefreshMode.ValidationOnly,
+ ImageRefreshMode = MetadataRefreshMode.ValidationOnly,
MetadataRefreshMode = MetadataRefreshMode.ValidationOnly
};
@@ -96,6 +89,23 @@ namespace Emby.Server.Implementations.Library.Validators
progress.Report(100 * percent);
}
+ var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = new[] { typeof(Person).Name },
+ IsDeadPerson = true,
+ IsLocked = false
+ });
+
+ foreach (var item in deadEntities)
+ {
+ _logger.Info("Deleting dead {2} {0} {1}.", item.Id.ToString("N"), item.Name, item.GetType().Name);
+
+ _libraryManager.DeleteItem(item, new DeleteOptions
+ {
+ DeleteFileLocation = false
+ }, false);
+ }
+
progress.Report(100);
_logger.Info("People validation complete");
diff --git a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
index 97b8ff0ac..f306309b3 100644
--- a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
@@ -68,6 +68,24 @@ namespace Emby.Server.Implementations.Library.Validators
progress.Report(percent);
}
+ var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = new[] { typeof(Studio).Name },
+ IsDeadStudio = true,
+ IsLocked = false
+ });
+
+ foreach (var item in deadEntities)
+ {
+ _logger.Info("Deleting dead {2} {0} {1}.", item.Id.ToString("N"), item.Name, item.GetType().Name);
+
+ _libraryManager.DeleteItem(item, new DeleteOptions
+ {
+ DeleteFileLocation = false
+
+ }, false);
+ }
+
progress.Report(100);
}
}