diff options
| author | Luke <luke.pulverenti@gmail.com> | 2015-10-26 18:50:19 -0400 |
|---|---|---|
| committer | Luke <luke.pulverenti@gmail.com> | 2015-10-26 18:50:19 -0400 |
| commit | 35778ebc02e5931142a1fe31a256b7488a07c5c2 (patch) | |
| tree | ced0290be8820f5e507b51ca4c5165212b1879d1 /MediaBrowser.Server.Implementations | |
| parent | c0dc8d055bfd4d2f58591083beb9e9128357aad6 (diff) | |
| parent | 8d77308593c3b16b733b0109323770d9dfe7e166 (diff) | |
Merge pull request #1222 from MediaBrowser/dev
3.0.5768.7
Diffstat (limited to 'MediaBrowser.Server.Implementations')
131 files changed, 3218 insertions, 2369 deletions
diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelItemImageProvider.cs b/MediaBrowser.Server.Implementations/Channels/ChannelItemImageProvider.cs deleted file mode 100644 index 042beb7e5..000000000 --- a/MediaBrowser.Server.Implementations/Channels/ChannelItemImageProvider.cs +++ /dev/null @@ -1,82 +0,0 @@ -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Channels; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Logging; -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Server.Implementations.Channels -{ - public class ChannelItemImageProvider : IDynamicImageProvider, IHasItemChangeMonitor - { - private readonly IHttpClient _httpClient; - private readonly ILogger _logger; - - public ChannelItemImageProvider(IHttpClient httpClient, ILogger logger) - { - _httpClient = httpClient; - _logger = logger; - } - - public IEnumerable<ImageType> GetSupportedImages(IHasImages item) - { - return new[] { ImageType.Primary }; - } - - public async Task<DynamicImageResponse> GetImage(IHasImages item, ImageType type, CancellationToken cancellationToken) - { - var channelItem = (IChannelItem)item; - - var imageResponse = new DynamicImageResponse(); - - if (!string.IsNullOrEmpty(channelItem.OriginalImageUrl)) - { - var options = new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = channelItem.OriginalImageUrl - }; - - var response = await _httpClient.GetResponse(options).ConfigureAwait(false); - - if (response.ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) - { - imageResponse.HasImage = true; - imageResponse.Stream = response.Content; - imageResponse.SetFormatFromMimeType(response.ContentType); - } - else - { - _logger.Error("Provider did not return an image content type."); - } - } - - return imageResponse; - } - - public string Name - { - get { return "Channel Image Provider"; } - } - - public bool Supports(IHasImages item) - { - return item is IChannelItem; - } - - public bool HasChanged(IHasMetadata item, MetadataStatus status, IDirectoryService directoryService) - { - var channelItem = item as IChannelItem; - - if (channelItem != null) - { - return !channelItem.HasImage(ImageType.Primary) && !string.IsNullOrWhiteSpace(channelItem.OriginalImageUrl); - } - return false; - } - } -} diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs b/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs index f670176e6..ba759dcb9 100644 --- a/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs +++ b/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs @@ -25,13 +25,13 @@ using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; +using CommonIO; namespace MediaBrowser.Server.Implementations.Channels { public class ChannelManager : IChannelManager, IDisposable { private IChannel[] _channels; - private IChannelFactory[] _factories; private readonly IUserManager _userManager; private readonly IUserDataManager _userDataManager; @@ -42,6 +42,7 @@ namespace MediaBrowser.Server.Implementations.Channels private readonly IFileSystem _fileSystem; private readonly IJsonSerializer _jsonSerializer; private readonly IHttpClient _httpClient; + private readonly IProviderManager _providerManager; private readonly ILocalizationManager _localization; private readonly ConcurrentDictionary<Guid, bool> _refreshedItems = new ConcurrentDictionary<Guid, bool>(); @@ -51,7 +52,7 @@ namespace MediaBrowser.Server.Implementations.Channels private Timer _refreshTimer; private Timer _clearDownloadCountsTimer; - public ChannelManager(IUserManager userManager, IDtoService dtoService, ILibraryManager libraryManager, ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem, IUserDataManager userDataManager, IJsonSerializer jsonSerializer, ILocalizationManager localization, IHttpClient httpClient) + public ChannelManager(IUserManager userManager, IDtoService dtoService, ILibraryManager libraryManager, ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem, IUserDataManager userDataManager, IJsonSerializer jsonSerializer, ILocalizationManager localization, IHttpClient httpClient, IProviderManager providerManager) { _userManager = userManager; _dtoService = dtoService; @@ -63,6 +64,7 @@ namespace MediaBrowser.Server.Implementations.Channels _jsonSerializer = jsonSerializer; _localization = localization; _httpClient = httpClient; + _providerManager = providerManager; _refreshTimer = new Timer(s => _refreshedItems.Clear(), null, TimeSpan.FromHours(3), TimeSpan.FromHours(3)); _clearDownloadCountsTimer = new Timer(s => _downloadCounts.Clear(), null, TimeSpan.FromHours(24), TimeSpan.FromHours(24)); @@ -76,10 +78,9 @@ namespace MediaBrowser.Server.Implementations.Channels } } - public void AddParts(IEnumerable<IChannel> channels, IEnumerable<IChannelFactory> factories) + public void AddParts(IEnumerable<IChannel> channels) { - _channels = channels.Where(i => !(i is IFactoryChannel)).ToArray(); - _factories = factories.ToArray(); + _channels = channels.ToArray(); } public string ChannelDownloadPath @@ -99,20 +100,7 @@ namespace MediaBrowser.Server.Implementations.Channels private IEnumerable<IChannel> GetAllChannels() { - return _factories - .SelectMany(i => - { - try - { - return i.GetChannels().ToList(); - } - catch (Exception ex) - { - _logger.ErrorException("Error getting channel list", ex); - return new List<IChannel>(); - } - }) - .Concat(_channels) + return _channels .OrderBy(i => i.Name); } @@ -130,8 +118,18 @@ namespace MediaBrowser.Server.Implementations.Channels if (query.SupportsLatestItems.HasValue) { var val = query.SupportsLatestItems.Value; - channels = channels.Where(i => (GetChannelProvider(i) is ISupportsLatestMedia) == val) - .ToList(); + channels = channels.Where(i => + { + try + { + return (GetChannelProvider(i) is ISupportsLatestMedia) == val; + } + catch + { + return false; + } + + }).ToList(); } if (query.IsFavorite.HasValue) { @@ -142,8 +140,23 @@ namespace MediaBrowser.Server.Implementations.Channels if (user != null) { - channels = channels.Where(i => GetChannelProvider(i).IsEnabledFor(user.Id.ToString("N")) && i.IsVisible(user)) - .ToList(); + channels = channels.Where(i => + { + if (!i.IsVisible(user)) + { + return false; + } + + try + { + return GetChannelProvider(i).IsEnabledFor(user.Id.ToString("N")); + } + catch + { + return false; + } + + }).ToList(); } var all = channels; @@ -203,9 +216,7 @@ namespace MediaBrowser.Server.Implementations.Channels try { - var item = await GetChannel(channelInfo, cancellationToken).ConfigureAwait(false); - - _libraryManager.RegisterItem(item); + await GetChannel(channelInfo, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -233,8 +244,6 @@ namespace MediaBrowser.Server.Implementations.Channels if (item == null) { item = GetChannel(channel, CancellationToken.None).Result; - - _libraryManager.RegisterItem(item); } return item; @@ -318,7 +327,7 @@ namespace MediaBrowser.Server.Implementations.Channels try { - var files = new DirectoryInfo(parentPath).EnumerateFiles("*", SearchOption.TopDirectoryOnly); + var files = _fileSystem.GetFiles(parentPath); if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) { @@ -403,18 +412,15 @@ namespace MediaBrowser.Server.Implementations.Channels var path = Channel.GetInternalMetadataPath(_config.ApplicationPaths.InternalMetadataPath, id); - var fileInfo = new DirectoryInfo(path); - var isNew = false; - if (!fileInfo.Exists) + if (!_fileSystem.DirectoryExists(path)) { _logger.Debug("Creating directory {0}", path); - Directory.CreateDirectory(path); - fileInfo = new DirectoryInfo(path); + _fileSystem.CreateDirectory(path); - if (!fileInfo.Exists) + if (!_fileSystem.DirectoryExists(path)) { throw new IOException("Path not created: " + path); } @@ -423,6 +429,7 @@ namespace MediaBrowser.Server.Implementations.Channels } var item = _libraryManager.GetItemById(id) as Channel; + var channelId = channelInfo.Name.GetMD5().ToString("N"); if (item == null) { @@ -430,25 +437,30 @@ namespace MediaBrowser.Server.Implementations.Channels { Name = channelInfo.Name, Id = id, - DateCreated = _fileSystem.GetCreationTimeUtc(fileInfo), - DateModified = _fileSystem.GetLastWriteTimeUtc(fileInfo), - Path = path + DateCreated = _fileSystem.GetCreationTimeUtc(path), + DateModified = _fileSystem.GetLastWriteTimeUtc(path), + Path = path, + ChannelId = channelId }; isNew = true; } + if (!string.Equals(item.ChannelId, channelId, StringComparison.OrdinalIgnoreCase)) + { + isNew = true; + } + item.OfficialRating = GetOfficialRating(channelInfo.ParentalRating); item.Overview = channelInfo.Description; item.HomePageUrl = channelInfo.HomePageUrl; - item.OriginalChannelName = channelInfo.Name; - if (string.IsNullOrEmpty(item.Name)) + if (string.IsNullOrWhiteSpace(item.Name)) { item.Name = channelInfo.Name; } - - await item.RefreshMetadata(new MetadataRefreshOptions + + await item.RefreshMetadata(new MetadataRefreshOptions(_fileSystem) { ForceSave = isNew @@ -481,9 +493,14 @@ namespace MediaBrowser.Server.Implementations.Channels public IEnumerable<ChannelFeatures> GetAllChannelFeatures() { - return GetAllChannels() - .Select(GetChannelEntity) - .OrderBy(i => i.SortName) + var inputItems = _libraryManager.GetItems(new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Channel).Name }, + SortBy = new[] { ItemSortBy.SortName } + + }).Items; + + return inputItems .Select(i => GetChannelFeatures(i.Id.ToString("N"))); } @@ -660,7 +677,7 @@ namespace MediaBrowser.Server.Implementations.Channels var internalItems = await Task.WhenAll(itemTasks).ConfigureAwait(false); internalItems = ApplyFilters(internalItems, query.Filters, user).ToArray(); - await RefreshIfNeeded(internalItems, new Progress<double>(), cancellationToken).ConfigureAwait(false); + RefreshIfNeeded(internalItems); if (query.StartIndex.HasValue) { @@ -825,7 +842,7 @@ namespace MediaBrowser.Server.Implementations.Channels var internalResult = await GetAllMediaInternal(query, cancellationToken).ConfigureAwait(false); - await RefreshIfNeeded(internalResult.Items, new Progress<double>(), cancellationToken).ConfigureAwait(false); + RefreshIfNeeded(internalResult.Items); var dtoOptions = new DtoOptions(); @@ -965,7 +982,7 @@ namespace MediaBrowser.Server.Implementations.Channels } } - return await GetReturnItems(internalItems, providerTotalRecordCount, user, query, progress, cancellationToken).ConfigureAwait(false); + return await GetReturnItems(internalItems, providerTotalRecordCount, user, query).ConfigureAwait(false); } public async Task<QueryResult<BaseItemDto>> GetChannelItems(ChannelItemQuery query, CancellationToken cancellationToken) @@ -1082,7 +1099,7 @@ namespace MediaBrowser.Server.Implementations.Channels { try { - Directory.CreateDirectory(Path.GetDirectoryName(path)); + _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); _jsonSerializer.SerializeToFile(result, path); } @@ -1134,9 +1151,7 @@ namespace MediaBrowser.Server.Implementations.Channels private async Task<QueryResult<BaseItem>> GetReturnItems(IEnumerable<BaseItem> items, int? totalCountFromProvider, User user, - ChannelItemQuery query, - IProgress<double> progress, - CancellationToken cancellationToken) + ChannelItemQuery query) { items = ApplyFilters(items, query.Filters, user); @@ -1159,7 +1174,7 @@ namespace MediaBrowser.Server.Implementations.Channels } var returnItemArray = all.ToArray(); - await RefreshIfNeeded(returnItemArray, progress, cancellationToken).ConfigureAwait(false); + RefreshIfNeeded(returnItemArray); return new QueryResult<BaseItem> { @@ -1191,7 +1206,7 @@ namespace MediaBrowser.Server.Implementations.Channels _logger.ErrorException("Error retrieving channel item from database", ex); } - if (item == null || !string.Equals(item.DataVersion, channnelDataVersion, StringComparison.Ordinal)) + if (item == null || !string.Equals(item.ExternalEtag, channnelDataVersion, StringComparison.Ordinal)) { item = new T(); isNew = true; @@ -1201,7 +1216,7 @@ namespace MediaBrowser.Server.Implementations.Channels isNew = false; } - item.DataVersion = channnelDataVersion; + item.ExternalEtag = channnelDataVersion; item.Id = id; return item; } @@ -1240,17 +1255,18 @@ namespace MediaBrowser.Server.Implementations.Channels item.ProviderIds = info.ProviderIds; item.OfficialRating = info.OfficialRating; - item.DateCreated = info.DateCreated.HasValue ? - info.DateCreated.Value : - DateTime.UtcNow; + item.DateCreated = info.DateCreated ?? DateTime.UtcNow; } var channelItem = (IChannelItem)item; - channelItem.OriginalImageUrl = info.ImageUrl; - channelItem.ExternalId = info.Id; channelItem.ChannelId = internalChannelId.ToString("N"); - channelItem.ChannelItemType = info.Type; + + if (!string.Equals(channelItem.ExternalId, info.Id, StringComparison.OrdinalIgnoreCase)) + { + isNew = true; + } + channelItem.ExternalId = info.Id; if (isNew) { @@ -1270,48 +1286,52 @@ namespace MediaBrowser.Server.Implementations.Channels item.Path = mediaSource == null ? null : mediaSource.Path; } + if (!string.IsNullOrWhiteSpace(info.ImageUrl)) + { + item.SetImagePath(ImageType.Primary, info.ImageUrl); + } + if (isNew) { await _libraryManager.CreateItem(item, cancellationToken).ConfigureAwait(false); - _libraryManager.RegisterItem(item); - await _libraryManager.UpdatePeople(item, info.People ?? new List<PersonInfo>()).ConfigureAwait(false); + if (info.People != null && info.People.Count > 0) + { + await _libraryManager.UpdatePeople(item, info.People ?? new List<PersonInfo>()).ConfigureAwait(false); + } } return item; } - private async Task RefreshIfNeeded(BaseItem[] programs, IProgress<double> progress, CancellationToken cancellationToken) + private void RefreshIfNeeded(BaseItem[] programs) { - var numComplete = 0; - var numItems = programs.Length; - foreach (var program in programs) { - await RefreshIfNeeded(program, cancellationToken).ConfigureAwait(false); - - numComplete++; - double percent = numComplete; - percent /= numItems; - progress.Report(percent * 100); + RefreshIfNeeded(program); } } - private readonly Task _cachedTask = Task.FromResult(true); - private Task RefreshIfNeeded(BaseItem program, CancellationToken cancellationToken) + private void RefreshIfNeeded(BaseItem program) { if (!_refreshedItems.ContainsKey(program.Id)) { _refreshedItems.TryAdd(program.Id, true); - return program.RefreshMetadata(cancellationToken); + _providerManager.QueueRefresh(program.Id, new MetadataRefreshOptions(_fileSystem)); } - return _cachedTask; } internal IChannel GetChannelProvider(Channel channel) { - return GetAllChannels().First(i => string.Equals(i.Name, channel.OriginalChannelName, StringComparison.OrdinalIgnoreCase)); + var result = GetAllChannels().FirstOrDefault(i => string.Equals(i.Name.GetMD5().ToString("N"), channel.ChannelId, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Name, channel.Name, StringComparison.OrdinalIgnoreCase)); + + if (result == null) + { + throw new ResourceNotFoundException("No channel provider found for channel " + channel.Name); + } + + return result; } private IEnumerable<BaseItem> ApplyFilters(IEnumerable<BaseItem> items, IEnumerable<ItemFilter> filters, User user) @@ -1462,7 +1482,7 @@ namespace MediaBrowser.Server.Implementations.Channels options.RequestHeaders[header.Key] = header.Value; } - Directory.CreateDirectory(Path.GetDirectoryName(destination)); + _fileSystem.CreateDirectory(Path.GetDirectoryName(destination)); // Determine output extension var response = await _httpClient.GetTempFileResponse(options).ConfigureAwait(false); @@ -1500,7 +1520,7 @@ namespace MediaBrowser.Server.Implementations.Channels throw new ApplicationException("Unexpected response type encountered: " + response.ContentType); } - File.Copy(response.TempFilePath, destination, true); + _fileSystem.CopyFile(response.TempFilePath, destination, true); try { diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelPostScanTask.cs b/MediaBrowser.Server.Implementations/Channels/ChannelPostScanTask.cs index 976fa5615..2e9d42f49 100644 --- a/MediaBrowser.Server.Implementations/Channels/ChannelPostScanTask.cs +++ b/MediaBrowser.Server.Implementations/Channels/ChannelPostScanTask.cs @@ -87,6 +87,7 @@ namespace MediaBrowser.Server.Implementations.Channels const int currentRefreshLevel = 1; var maxRefreshLevel = features.AutoRefreshLevels ?? 0; + maxRefreshLevel = Math.Max(maxRefreshLevel, 2); if (maxRefreshLevel > 0) { @@ -143,7 +144,7 @@ namespace MediaBrowser.Server.Implementations.Channels private async Task CleanChannel(Guid id, CancellationToken cancellationToken) { - _logger.Debug("Cleaning channel {0} from database", id); + _logger.Info("Cleaning channel {0} from database", id); // Delete all channel items var allIds = _libraryManager.GetItemIds(new InternalItemsQuery diff --git a/MediaBrowser.Server.Implementations/Collections/CollectionImageProvider.cs b/MediaBrowser.Server.Implementations/Collections/CollectionImageProvider.cs index f5ddbdb78..9ea457284 100644 --- a/MediaBrowser.Server.Implementations/Collections/CollectionImageProvider.cs +++ b/MediaBrowser.Server.Implementations/Collections/CollectionImageProvider.cs @@ -10,8 +10,10 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Server.Implementations.Photos; using MoreLinq; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; +using CommonIO; namespace MediaBrowser.Server.Implementations.Collections { @@ -21,6 +23,17 @@ namespace MediaBrowser.Server.Implementations.Collections { } + protected override bool Supports(IHasImages item) + { + // Right now this is the only way to prevent this image from getting created ahead of internet image providers + if (!item.IsLocked) + { + return false; + } + + return base.Supports(item); + } + protected override Task<List<BaseItem>> GetItemsWithImages(IHasImages item) { var playlist = (BoxSet)item; @@ -62,7 +75,27 @@ namespace MediaBrowser.Server.Implementations.Collections .DistinctBy(i => i.Id) .ToList(); - return Task.FromResult(GetFinalItems(items)); + return Task.FromResult(GetFinalItems(items, 2)); + } + + protected override async Task<string> CreateImage(IHasImages item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + { + var image = itemsWithImages + .Where(i => i.HasImage(ImageType.Primary) && i.GetImageInfo(ImageType.Primary, 0).IsLocalFile && Path.HasExtension(i.GetImagePath(ImageType.Primary))) + .Select(i => i.GetImagePath(ImageType.Primary)) + .FirstOrDefault(); + + if (string.IsNullOrWhiteSpace(image)) + { + return null; + } + + var ext = Path.GetExtension(image); + + var outputPath = Path.ChangeExtension(outputPathWithoutExtension, ext); + File.Copy(image, outputPath); + + return outputPath; } } } diff --git a/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs b/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs index 04f82db6f..5c98e401f 100644 --- a/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs +++ b/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs @@ -12,6 +12,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; namespace MediaBrowser.Server.Implementations.Collections { @@ -21,17 +22,19 @@ namespace MediaBrowser.Server.Implementations.Collections private readonly IFileSystem _fileSystem; private readonly ILibraryMonitor _iLibraryMonitor; private readonly ILogger _logger; + private readonly IProviderManager _providerManager; public event EventHandler<CollectionCreatedEventArgs> CollectionCreated; public event EventHandler<CollectionModifiedEventArgs> ItemsAddedToCollection; public event EventHandler<CollectionModifiedEventArgs> ItemsRemovedFromCollection; - public CollectionManager(ILibraryManager libraryManager, IFileSystem fileSystem, ILibraryMonitor iLibraryMonitor, ILogger logger) + public CollectionManager(ILibraryManager libraryManager, IFileSystem fileSystem, ILibraryMonitor iLibraryMonitor, ILogger logger, IProviderManager providerManager) { _libraryManager = libraryManager; _fileSystem = fileSystem; _iLibraryMonitor = iLibraryMonitor; _logger = logger; + _providerManager = providerManager; } public Folder GetCollectionsFolder(string userId) @@ -70,7 +73,7 @@ namespace MediaBrowser.Server.Implementations.Collections try { - Directory.CreateDirectory(path); + _fileSystem.CreateDirectory(path); var collection = new BoxSet { @@ -88,12 +91,18 @@ namespace MediaBrowser.Server.Implementations.Collections await parentFolder.AddChild(collection, CancellationToken.None).ConfigureAwait(false); - await collection.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService()), CancellationToken.None) - .ConfigureAwait(false); - if (options.ItemIdList.Count > 0) { - await AddToCollection(collection.Id, options.ItemIdList, false); + await AddToCollection(collection.Id, options.ItemIdList, false, new MetadataRefreshOptions(_fileSystem) + { + // The initial adding of items is going to create a local metadata file + // This will cause internet metadata to be skipped as a result + MetadataRefreshMode = MetadataRefreshMode.FullRefresh + }); + } + else + { + _providerManager.QueueRefresh(collection.Id, new MetadataRefreshOptions(_fileSystem)); } EventHelper.FireEventIfNotNull(CollectionCreated, this, new CollectionCreatedEventArgs @@ -141,10 +150,10 @@ namespace MediaBrowser.Server.Implementations.Collections public Task AddToCollection(Guid collectionId, IEnumerable<Guid> ids) { - return AddToCollection(collectionId, ids, true); + return AddToCollection(collectionId, ids, true, new MetadataRefreshOptions(_fileSystem)); } - private async Task AddToCollection(Guid collectionId, IEnumerable<Guid> ids, bool fireEvent) + private async Task AddToCollection(Guid collectionId, IEnumerable<Guid> ids, bool fireEvent, MetadataRefreshOptions refreshOptions) { var collection = _libraryManager.GetItemById(collectionId) as BoxSet; @@ -168,29 +177,31 @@ namespace MediaBrowser.Server.Implementations.Collections itemList.Add(item); - if (currentLinkedChildren.Any(i => i.Id == itemId)) + if (currentLinkedChildren.All(i => i.Id != itemId)) { - throw new ArgumentException("Item already exists in collection"); + list.Add(LinkedChild.Create(item)); } - - list.Add(LinkedChild.Create(item)); } - collection.LinkedChildren.AddRange(list); + if (list.Count > 0) + { + collection.LinkedChildren.AddRange(list); - collection.UpdateRatingToContent(); + collection.UpdateRatingToContent(); - await collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); - await collection.RefreshMetadata(CancellationToken.None).ConfigureAwait(false); + await collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); - if (fireEvent) - { - EventHelper.FireEventIfNotNull(ItemsAddedToCollection, this, new CollectionModifiedEventArgs + _providerManager.QueueRefresh(collection.Id, refreshOptions); + + if (fireEvent) { - Collection = collection, - ItemsChanged = itemList + EventHelper.FireEventIfNotNull(ItemsAddedToCollection, this, new CollectionModifiedEventArgs + { + Collection = collection, + ItemsChanged = itemList - }, _logger); + }, _logger); + } } } @@ -225,8 +236,8 @@ namespace MediaBrowser.Server.Implementations.Collections } } - var shortcutFiles = Directory - .EnumerateFiles(collection.Path, "*", SearchOption.TopDirectoryOnly) + var shortcutFiles = _fileSystem + .GetFilePaths(collection.Path) .Where(i => _fileSystem.IsShortcut(i)) .ToList(); @@ -263,7 +274,7 @@ namespace MediaBrowser.Server.Implementations.Collections collection.UpdateRatingToContent(); await collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); - await collection.RefreshMetadata(CancellationToken.None).ConfigureAwait(false); + _providerManager.QueueRefresh(collection.Id, new MetadataRefreshOptions(_fileSystem)); EventHelper.FireEventIfNotNull(ItemsRemovedFromCollection, this, new CollectionModifiedEventArgs { diff --git a/MediaBrowser.Server.Implementations/Collections/CollectionsDynamicFolder.cs b/MediaBrowser.Server.Implementations/Collections/CollectionsDynamicFolder.cs index 915a27c11..85b143a40 100644 --- a/MediaBrowser.Server.Implementations/Collections/CollectionsDynamicFolder.cs +++ b/MediaBrowser.Server.Implementations/Collections/CollectionsDynamicFolder.cs @@ -1,23 +1,27 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities; using System.IO; +using CommonIO; +using MediaBrowser.Common.IO; namespace MediaBrowser.Server.Implementations.Collections { public class CollectionsDynamicFolder : IVirtualFolderCreator { private readonly IApplicationPaths _appPaths; + private IFileSystem _fileSystem; - public CollectionsDynamicFolder(IApplicationPaths appPaths) + public CollectionsDynamicFolder(IApplicationPaths appPaths, IFileSystem fileSystem) { _appPaths = appPaths; + _fileSystem = fileSystem; } public BasePluginFolder GetFolder() { var path = Path.Combine(_appPaths.DataPath, "collections"); - Directory.CreateDirectory(path); + _fileSystem.CreateDirectory(path); return new ManualCollectionsFolder { diff --git a/MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs b/MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs index bf199503b..a7d3854e7 100644 --- a/MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -15,6 +15,7 @@ using MediaBrowser.Model.Serialization; using System; using System.IO; using System.Linq; +using CommonIO; namespace MediaBrowser.Server.Implementations.Configuration { @@ -23,14 +24,16 @@ namespace MediaBrowser.Server.Implementations.Configuration /// </summary> public class ServerConfigurationManager : BaseConfigurationManager, IServerConfigurationManager { + /// <summary> /// Initializes a new instance of the <see cref="ServerConfigurationManager" /> class. /// </summary> /// <param name="applicationPaths">The application paths.</param> /// <param name="logManager">The log manager.</param> /// <param name="xmlSerializer">The XML serializer.</param> - public ServerConfigurationManager(IApplicationPaths applicationPaths, ILogManager logManager, IXmlSerializer xmlSerializer) - : base(applicationPaths, logManager, xmlSerializer) + /// <param name="fileSystem">The file system.</param> + public ServerConfigurationManager(IApplicationPaths applicationPaths, ILogManager logManager, IXmlSerializer xmlSerializer, IFileSystem fileSystem) + : base(applicationPaths, logManager, xmlSerializer, fileSystem) { UpdateItemsByNamePath(); UpdateMetadataPath(); @@ -198,10 +201,12 @@ namespace MediaBrowser.Server.Implementations.Configuration && !string.Equals(Configuration.ItemsByNamePath ?? string.Empty, newPath)) { // Validate - if (!Directory.Exists(newPath)) + if (!FileSystem.DirectoryExists(newPath)) { throw new DirectoryNotFoundException(string.Format("{0} does not exist.", newPath)); } + + EnsureWriteAccess(newPath); } } @@ -218,10 +223,12 @@ namespace MediaBrowser.Server.Implementations.Configuration && !string.Equals(Configuration.MetadataPath ?? string.Empty, newPath)) { // Validate - if (!Directory.Exists(newPath)) + if (!FileSystem.DirectoryExists(newPath)) { throw new DirectoryNotFoundException(string.Format("{0} does not exist.", newPath)); } + + EnsureWriteAccess(newPath); } } diff --git a/MediaBrowser.Server.Implementations/Connect/ConnectEntryPoint.cs b/MediaBrowser.Server.Implementations/Connect/ConnectEntryPoint.cs index 8a659fb65..9ad04ebbd 100644 --- a/MediaBrowser.Server.Implementations/Connect/ConnectEntryPoint.cs +++ b/MediaBrowser.Server.Implementations/Connect/ConnectEntryPoint.cs @@ -10,6 +10,8 @@ using System.IO; using System.Net; using System.Text; using System.Threading; +using CommonIO; +using MediaBrowser.Common.IO; namespace MediaBrowser.Server.Implementations.Connect { @@ -23,8 +25,9 @@ namespace MediaBrowser.Server.Implementations.Connect private readonly INetworkManager _networkManager; private readonly IApplicationHost _appHost; + private readonly IFileSystem _fileSystem; - public ConnectEntryPoint(IHttpClient httpClient, IApplicationPaths appPaths, ILogger logger, INetworkManager networkManager, IConnectManager connectManager, IApplicationHost appHost) + public ConnectEntryPoint(IHttpClient httpClient, IApplicationPaths appPaths, ILogger logger, INetworkManager networkManager, IConnectManager connectManager, IApplicationHost appHost, IFileSystem fileSystem) { _httpClient = httpClient; _appPaths = appPaths; @@ -32,6 +35,7 @@ namespace MediaBrowser.Server.Implementations.Connect _networkManager = networkManager; _connectManager = connectManager; _appHost = appHost; + _fileSystem = fileSystem; } public void Run() @@ -45,14 +49,23 @@ namespace MediaBrowser.Server.Implementations.Connect private async void TimerCallback(object state) { + var index = 0; + foreach (var ipLookupUrl in _ipLookups) { try { + // Sometimes whatismyipaddress might fail, but it won't do us any good having users raise alarms over it. + var logErrors = index > 0; + +#if DEBUG + logErrors = true; +#endif using (var stream = await _httpClient.Get(new HttpRequestOptions { Url = ipLookupUrl, - UserAgent = "Emby Server/" + _appHost.ApplicationVersion + UserAgent = "Emby/" + _appHost.ApplicationVersion, + LogErrors = logErrors }).ConfigureAwait(false)) { @@ -60,7 +73,7 @@ namespace MediaBrowser.Server.Implementations.Connect { var address = await reader.ReadToEndAsync().ConfigureAwait(false); - if (IsValid(address)) + if (IsValid(address, ipLookupUrl)) { ((ConnectManager)_connectManager).OnWanAddressResolved(address); CacheAddress(address); @@ -76,6 +89,8 @@ namespace MediaBrowser.Server.Implementations.Connect { _logger.ErrorException("Error getting connection info", ex); } + + index++; } } @@ -90,8 +105,8 @@ namespace MediaBrowser.Server.Implementations.Connect try { - Directory.CreateDirectory(Path.GetDirectoryName(path)); - File.WriteAllText(path, address, Encoding.UTF8); + _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); + _fileSystem.WriteAllText(path, address, Encoding.UTF8); } catch (Exception ex) { @@ -105,9 +120,9 @@ namespace MediaBrowser.Server.Implementations.Connect try { - var endpoint = File.ReadAllText(path, Encoding.UTF8); + var endpoint = _fileSystem.ReadAllText(path, Encoding.UTF8); - if (IsValid(endpoint)) + if (IsValid(endpoint, "cache")) { ((ConnectManager)_connectManager).OnWanAddressResolved(endpoint); } @@ -122,14 +137,14 @@ namespace MediaBrowser.Server.Implementations.Connect } } - private bool IsValid(string address) + private bool IsValid(string address, string source) { IPAddress ipAddress; var valid = IPAddress.TryParse(address, out ipAddress); if (!valid) { - _logger.Error("{0} is not a valid ip address", address); + _logger.Error("{0} is not a valid ip address. Source: {1}", address, source); } return valid; diff --git a/MediaBrowser.Server.Implementations/Connect/ConnectManager.cs b/MediaBrowser.Server.Implementations/Connect/ConnectManager.cs index 7cd96c5f3..f1de09d56 100644 --- a/MediaBrowser.Server.Implementations/Connect/ConnectManager.cs +++ b/MediaBrowser.Server.Implementations/Connect/ConnectManager.cs @@ -23,6 +23,8 @@ using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; +using CommonIO; +using MediaBrowser.Common.IO; namespace MediaBrowser.Server.Implementations.Connect { @@ -40,6 +42,7 @@ namespace MediaBrowser.Server.Implementations.Connect private readonly IUserManager _userManager; private readonly IProviderManager _providerManager; private readonly ISecurityManager _securityManager; + private readonly IFileSystem _fileSystem; private ConnectData _data = new ConnectData(); @@ -104,7 +107,7 @@ namespace MediaBrowser.Server.Implementations.Connect IEncryptionManager encryption, IHttpClient httpClient, IServerApplicationHost appHost, - IServerConfigurationManager config, IUserManager userManager, IProviderManager providerManager, ISecurityManager securityManager) + IServerConfigurationManager config, IUserManager userManager, IProviderManager providerManager, ISecurityManager securityManager, IFileSystem fileSystem) { _logger = logger; _appPaths = appPaths; @@ -116,6 +119,7 @@ namespace MediaBrowser.Server.Implementations.Connect _userManager = userManager; _providerManager = providerManager; _securityManager = securityManager; + _fileSystem = fileSystem; _userManager.UserConfigurationUpdated += _userManager_UserConfigurationUpdated; _config.ConfigurationUpdated += _config_ConfigurationUpdated; @@ -315,7 +319,7 @@ namespace MediaBrowser.Server.Implementations.Connect try { - Directory.CreateDirectory(Path.GetDirectoryName(path)); + _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); var json = _json.SerializeToString(_data); @@ -323,7 +327,7 @@ namespace MediaBrowser.Server.Implementations.Connect lock (_dataFileLock) { - File.WriteAllText(path, encrypted, Encoding.UTF8); + _fileSystem.WriteAllText(path, encrypted, Encoding.UTF8); } } catch (Exception ex) @@ -340,7 +344,7 @@ namespace MediaBrowser.Server.Implementations.Connect { lock (_dataFileLock) { - var encrypted = File.ReadAllText(path, Encoding.UTF8); + var encrypted = _fileSystem.ReadAllText(path, Encoding.UTF8); var json = _encryption.DecryptString(encrypted); @@ -932,7 +936,7 @@ namespace MediaBrowser.Server.Implementations.Connect { var length = response.ContentLength; - if (length != new FileInfo(user.GetImageInfo(ImageType.Primary, 0).Path).Length) + if (length != _fileSystem.GetFileInfo(user.GetImageInfo(ImageType.Primary, 0).Path).Length) { changed = true; } @@ -943,7 +947,7 @@ namespace MediaBrowser.Server.Implementations.Connect { await _providerManager.SaveImage(user, imageUrl, _connectImageSemaphore, ImageType.Primary, null, CancellationToken.None).ConfigureAwait(false); - await user.RefreshMetadata(new MetadataRefreshOptions + await user.RefreshMetadata(new MetadataRefreshOptions(_fileSystem) { ForceSave = true, @@ -1076,20 +1080,25 @@ namespace MediaBrowser.Server.Implementations.Connect var url = GetConnectUrl("keyAssociation"); - url += "?serverId=" + ConnectServerId; - url += "&supporterKey=" + _securityManager.SupporterKey; - var options = new HttpRequestOptions { Url = url, CancellationToken = CancellationToken.None }; + var postData = new Dictionary<string, string> + { + {"serverId", ConnectServerId}, + {"supporterKey", _securityManager.SupporterKey} + }; + + options.SetPostData(postData); + SetServerAccessToken(options); SetApplicationHeader(options); // No need to examine the response - using (var stream = (await _httpClient.SendAsync(options, "GET").ConfigureAwait(false)).Content) + using (var stream = (await _httpClient.SendAsync(options, "POST").ConfigureAwait(false)).Content) { return _json.DeserializeFromStream<ConnectSupporterSummary>(stream); } @@ -1104,16 +1113,21 @@ namespace MediaBrowser.Server.Implementations.Connect var url = GetConnectUrl("keyAssociation"); - url += "?serverId=" + ConnectServerId; - url += "&supporterKey=" + _securityManager.SupporterKey; - url += "&userId=" + id; - var options = new HttpRequestOptions { Url = url, CancellationToken = CancellationToken.None }; + var postData = new Dictionary<string, string> + { + {"serverId", ConnectServerId}, + {"supporterKey", _securityManager.SupporterKey}, + {"userId", id} + }; + + options.SetPostData(postData); + SetServerAccessToken(options); SetApplicationHeader(options); @@ -1132,16 +1146,21 @@ namespace MediaBrowser.Server.Implementations.Connect var url = GetConnectUrl("keyAssociation"); - url += "?serverId=" + ConnectServerId; - url += "&supporterKey=" + _securityManager.SupporterKey; - url += "&userId=" + id; - var options = new HttpRequestOptions { Url = url, CancellationToken = CancellationToken.None }; + var postData = new Dictionary<string, string> + { + {"serverId", ConnectServerId}, + {"supporterKey", _securityManager.SupporterKey}, + {"userId", id} + }; + + options.SetPostData(postData); + SetServerAccessToken(options); SetApplicationHeader(options); diff --git a/MediaBrowser.Server.Implementations/Devices/CameraUploadsFolder.cs b/MediaBrowser.Server.Implementations/Devices/CameraUploadsFolder.cs index 566f4c5f4..6d7265972 100644 --- a/MediaBrowser.Server.Implementations/Devices/CameraUploadsFolder.cs +++ b/MediaBrowser.Server.Implementations/Devices/CameraUploadsFolder.cs @@ -3,6 +3,8 @@ using MediaBrowser.Controller.Entities; using System; using System.IO; using System.Linq; +using CommonIO; +using MediaBrowser.Common.IO; namespace MediaBrowser.Server.Implementations.Devices { @@ -51,17 +53,19 @@ namespace MediaBrowser.Server.Implementations.Devices public class CameraUploadsDynamicFolder : IVirtualFolderCreator { private readonly IApplicationPaths _appPaths; + private readonly IFileSystem _fileSystem; - public CameraUploadsDynamicFolder(IApplicationPaths appPaths) + public CameraUploadsDynamicFolder(IApplicationPaths appPaths, IFileSystem fileSystem) { _appPaths = appPaths; + _fileSystem = fileSystem; } public BasePluginFolder GetFolder() { var path = Path.Combine(_appPaths.DataPath, "camerauploads"); - Directory.CreateDirectory(path); + _fileSystem.CreateDirectory(path); return new CameraUploadsFolder { diff --git a/MediaBrowser.Server.Implementations/Devices/DeviceManager.cs b/MediaBrowser.Server.Implementations/Devices/DeviceManager.cs index 0173f2784..0b2c082a8 100644 --- a/MediaBrowser.Server.Implementations/Devices/DeviceManager.cs +++ b/MediaBrowser.Server.Implementations/Devices/DeviceManager.cs @@ -17,6 +17,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using CommonIO; namespace MediaBrowser.Server.Implementations.Devices { @@ -157,7 +158,7 @@ namespace MediaBrowser.Server.Implementations.Devices _libraryMonitor.ReportFileSystemChangeBeginning(path); - Directory.CreateDirectory(Path.GetDirectoryName(path)); + _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); try { @@ -264,6 +265,11 @@ namespace MediaBrowser.Server.Implementations.Devices return true; } + if (policy.IsAdministrator) + { + return true; + } + return ListHelper.ContainsIgnoreCase(policy.EnabledDevices, id); } } @@ -290,4 +296,4 @@ namespace MediaBrowser.Server.Implementations.Devices return config.GetConfiguration<DevicesOptions>("devices"); } } -} +}
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Devices/DeviceRepository.cs b/MediaBrowser.Server.Implementations/Devices/DeviceRepository.cs index 6d324b1ab..43b1e693c 100644 --- a/MediaBrowser.Server.Implementations/Devices/DeviceRepository.cs +++ b/MediaBrowser.Server.Implementations/Devices/DeviceRepository.cs @@ -12,6 +12,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using CommonIO; namespace MediaBrowser.Server.Implementations.Devices { @@ -47,7 +48,7 @@ namespace MediaBrowser.Server.Implementations.Devices public Task SaveDevice(DeviceInfo device) { var path = Path.Combine(GetDevicePath(device.Id), "device.json"); - Directory.CreateDirectory(Path.GetDirectoryName(path)); + _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); lock (_syncLock) { @@ -111,8 +112,8 @@ namespace MediaBrowser.Server.Implementations.Devices try { - return Directory - .EnumerateFiles(path, "*", SearchOption.AllDirectories) + return _fileSystem + .GetFilePaths(path, true) .Where(i => string.Equals(Path.GetFileName(i), "device.json", StringComparison.OrdinalIgnoreCase)) .ToList() .Select(i => @@ -178,7 +179,7 @@ namespace MediaBrowser.Server.Implementations.Devices public void AddCameraUpload(string deviceId, LocalFileInfo file) { var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json"); - Directory.CreateDirectory(Path.GetDirectoryName(path)); + _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); lock (_syncLock) { diff --git a/MediaBrowser.Server.Implementations/Dto/DtoService.cs b/MediaBrowser.Server.Implementations/Dto/DtoService.cs index edfef38fd..20e1eb543 100644 --- a/MediaBrowser.Server.Implementations/Dto/DtoService.cs +++ b/MediaBrowser.Server.Implementations/Dto/DtoService.cs @@ -26,6 +26,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using CommonIO; namespace MediaBrowser.Server.Implementations.Dto { @@ -397,7 +398,7 @@ namespace MediaBrowser.Server.Implementations.Dto else if (item is LiveTvProgram) { - _livetvManager().AddInfoToProgramDto(item, dto, user); + _livetvManager().AddInfoToProgramDto(item, dto, fields.Contains(ItemFields.ChannelInfo), user); } return dto; @@ -908,12 +909,6 @@ namespace MediaBrowser.Server.Implementations.Dto dto.DisplayMediaType = item.DisplayMediaType; } - // Leave null if false - if (item.IsUnidentified) - { - dto.IsUnidentified = item.IsUnidentified; - } - if (fields.Contains(ItemFields.Settings)) { dto.LockedFields = item.LockedFields; @@ -1047,14 +1042,10 @@ namespace MediaBrowser.Server.Implementations.Dto dto.IsFolder = item.IsFolder; dto.MediaType = item.MediaType; dto.LocationType = item.LocationType; + dto.IsHD = item.IsHD; - var hasLang = item as IHasPreferredMetadataLanguage; - - if (hasLang != null) - { - dto.PreferredMetadataCountryCode = hasLang.PreferredMetadataCountryCode; - dto.PreferredMetadataLanguage = hasLang.PreferredMetadataLanguage; - } + dto.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode; + dto.PreferredMetadataLanguage = item.PreferredMetadataLanguage; var hasCriticRating = item as IHasCriticRating; if (hasCriticRating != null) @@ -1318,7 +1309,11 @@ namespace MediaBrowser.Server.Implementations.Dto dto.VideoType = video.VideoType; dto.Video3DFormat = video.Video3DFormat; dto.IsoType = video.IsoType; - dto.IsHD = video.IsHD; + + if (video.HasSubtitles) + { + dto.HasSubtitles = video.HasSubtitles; + } if (video.AdditionalParts.Count != 0) { @@ -1433,8 +1428,6 @@ namespace MediaBrowser.Server.Implementations.Dto dto.AirTime = series.AirTime; dto.SeriesStatus = series.Status; - dto.SeasonCount = series.SeasonCount; - if (fields.Contains(ItemFields.Settings)) { dto.DisplaySpecialsWithSeasons = series.DisplaySpecialsWithSeasons; @@ -1590,7 +1583,7 @@ namespace MediaBrowser.Server.Implementations.Dto { foreach (var map in _config.Configuration.PathSubstitutions) { - path = _fileSystem.SubstitutePath(path, map.From, map.To); + path = _libraryManager.SubstitutePath(path, map.From, map.To); } } @@ -1755,13 +1748,11 @@ namespace MediaBrowser.Server.Implementations.Dto { var imageInfo = item.GetImageInfo(ImageType.Primary, 0); - if (imageInfo == null) + if (imageInfo == null || !imageInfo.IsLocalFile) { return; } - var path = imageInfo.Path; - ImageSize size; try diff --git a/MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs index fa5841bb8..a2ffa9aff 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ b/MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs @@ -147,7 +147,8 @@ namespace MediaBrowser.Server.Implementations.EntryPoints // It also can fail with others like 727-ExternalPortOnlySupportsWildcard, 728-NoPortMapsAvailable // and those errors (upnp errors) could be useful for diagnosting. - _logger.ErrorException("Error creating port forwarding rules", ex); + // Commenting out because users are reporting problems out of our control + //_logger.ErrorException("Error creating port forwarding rules", ex); } } diff --git a/MediaBrowser.Server.Implementations/EntryPoints/Notifications/RemoteNotifications.cs b/MediaBrowser.Server.Implementations/EntryPoints/Notifications/RemoteNotifications.cs index d5b7f5b36..9a20505a5 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/Notifications/RemoteNotifications.cs +++ b/MediaBrowser.Server.Implementations/EntryPoints/Notifications/RemoteNotifications.cs @@ -13,6 +13,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications { @@ -60,7 +61,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications { var dataPath = Path.Combine(_appPaths.DataPath, "remotenotifications.json"); - var lastRunTime = File.Exists(dataPath) ? _fileSystem.GetLastWriteTimeUtc(dataPath) : DateTime.MinValue; + var lastRunTime = _fileSystem.FileExists(dataPath) ? _fileSystem.GetLastWriteTimeUtc(dataPath) : DateTime.MinValue; try { @@ -88,7 +89,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications { var notifications = _json.DeserializeFromStream<RemoteNotification[]>(stream); - File.WriteAllText(dataPath, string.Empty); + _fileSystem.WriteAllText(dataPath, string.Empty); await CreateNotifications(notifications, lastRunTime).ConfigureAwait(false); } diff --git a/MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs b/MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs index 06b72e4ef..e36813d11 100644 --- a/MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs +++ b/MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs @@ -17,6 +17,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; namespace MediaBrowser.Server.Implementations.FileOrganization { @@ -182,7 +183,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization _logger.Info("Sorting file {0} to new path {1}", sourcePath, newPath); result.TargetPath = newPath; - var fileExists = File.Exists(result.TargetPath); + var fileExists = _fileSystem.FileExists(result.TargetPath); var otherDuplicatePaths = GetOtherDuplicatePaths(result.TargetPath, series, seasonNumber, episodeNumber, endingEpiosdeNumber); if (!overwriteExisting) @@ -256,7 +257,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization if (!string.IsNullOrWhiteSpace(originalFilenameWithoutExtension) && !string.IsNullOrWhiteSpace(directory)) { // Get all related files, e.g. metadata, images, etc - var files = Directory.EnumerateFiles(directory, "*", SearchOption.TopDirectoryOnly) + var files = _fileSystem.GetFilePaths(directory) .Where(i => (Path.GetFileNameWithoutExtension(i) ?? string.Empty).StartsWith(originalFilenameWithoutExtension, StringComparison.OrdinalIgnoreCase)) .ToList(); @@ -272,7 +273,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization var destination = Path.Combine(directory, filename); - File.Move(file, destination); + _fileSystem.MoveFile(file, destination); } } } @@ -313,7 +314,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization try { - var filesOfOtherExtensions = Directory.EnumerateFiles(folder, "*", SearchOption.TopDirectoryOnly) + var filesOfOtherExtensions = _fileSystem.GetFilePaths(folder) .Where(i => _libraryManager.IsVideoFile(i) && string.Equals(_fileSystem.GetFileNameWithoutExtension(i), targetFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)); episodePaths.AddRange(filesOfOtherExtensions); @@ -332,19 +333,19 @@ namespace MediaBrowser.Server.Implementations.FileOrganization { _libraryMonitor.ReportFileSystemChangeBeginning(result.TargetPath); - Directory.CreateDirectory(Path.GetDirectoryName(result.TargetPath)); + _fileSystem.CreateDirectory(Path.GetDirectoryName(result.TargetPath)); - var targetAlreadyExists = File.Exists(result.TargetPath); + var targetAlreadyExists = _fileSystem.FileExists(result.TargetPath); try { if (targetAlreadyExists || options.CopyOriginalFile) { - File.Copy(result.OriginalPath, result.TargetPath, true); + _fileSystem.CopyFile(result.OriginalPath, result.TargetPath, true); } else { - File.Move(result.OriginalPath, result.TargetPath); + _fileSystem.MoveFile(result.OriginalPath, result.TargetPath); } result.Status = FileSortingStatus.Success; @@ -435,7 +436,26 @@ namespace MediaBrowser.Server.Implementations.FileOrganization var newPath = GetSeasonFolderPath(series, seasonNumber, options); - var episodeFileName = GetEpisodeFileName(sourcePath, series.Name, seasonNumber, episodeNumber, endingEpisodeNumber, episode.Name, options); + // MAX_PATH - trailing <NULL> charachter - drive component: 260 - 1 - 3 = 256 + // Usually newPath would include the drive component, but use 256 to be sure + var maxFilenameLength = 256 - newPath.Length; + + if (!newPath.EndsWith(@"\")) + { + // Remove 1 for missing backslash combining path and filename + maxFilenameLength--; + } + + // Remove additional 4 chars to prevent PathTooLongException for downloaded subtitles (eg. filename.ext.eng.srt) + maxFilenameLength -= 4; + + var episodeFileName = GetEpisodeFileName(sourcePath, series.Name, seasonNumber, episodeNumber, endingEpisodeNumber, episode.Name, options, maxFilenameLength); + + if (string.IsNullOrEmpty(episodeFileName)) + { + // cause failure + return string.Empty; + } newPath = Path.Combine(newPath, episodeFileName); @@ -481,7 +501,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization return Path.Combine(path, _fileSystem.GetValidFilename(seasonFolderName)); } - private string GetEpisodeFileName(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int? endingEpisodeNumber, string episodeTitle, TvFileOrganizationOptions options) + private string GetEpisodeFileName(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int? endingEpisodeNumber, string episodeTitle, TvFileOrganizationOptions options, int? maxLength) { seriesName = _fileSystem.GetValidFilename(seriesName).Trim(); episodeTitle = _fileSystem.GetValidFilename(episodeTitle).Trim(); @@ -497,9 +517,9 @@ namespace MediaBrowser.Server.Implementations.FileOrganization .Replace("%0s", seasonNumber.ToString("00", _usCulture)) .Replace("%00s", seasonNumber.ToString("000", _usCulture)) .Replace("%ext", sourceExtension) - .Replace("%en", episodeTitle) - .Replace("%e.n", episodeTitle.Replace(" ", ".")) - .Replace("%e_n", episodeTitle.Replace(" ", "_")); + .Replace("%en", "%#1") + .Replace("%e.n", "%#2") + .Replace("%e_n", "%#3"); if (endingEpisodeNumber.HasValue) { @@ -508,9 +528,37 @@ namespace MediaBrowser.Server.Implementations.FileOrganization .Replace("%00ed", endingEpisodeNumber.Value.ToString("000", _usCulture)); } - return result.Replace("%e", episodeNumber.ToString(_usCulture)) + result = result.Replace("%e", episodeNumber.ToString(_usCulture)) .Replace("%0e", episodeNumber.ToString("00", _usCulture)) .Replace("%00e", episodeNumber.ToString("000", _usCulture)); + + if (maxLength.HasValue && result.Contains("%#")) + { + // Substract 3 for the temp token length (%#1, %#2 or %#3) + int maxRemainingTitleLength = maxLength.Value - result.Length + 3; + string shortenedEpisodeTitle = string.Empty; + + if (maxRemainingTitleLength > 5) + { + // A title with fewer than 5 letters wouldn't be of much value + shortenedEpisodeTitle = episodeTitle.Substring(0, Math.Min(maxRemainingTitleLength, episodeTitle.Length)); + } + + result = result.Replace("%#1", shortenedEpisodeTitle) + .Replace("%#2", shortenedEpisodeTitle.Replace(" ", ".")) + .Replace("%#3", shortenedEpisodeTitle.Replace(" ", "_")); + } + + if (maxLength.HasValue && result.Length > maxLength.Value) + { + // There may be cases where reducing the title length may still not be sufficient to + // stay below maxLength + var msg = string.Format("Unable to generate an episode file name shorter than {0} characters to constrain to the max path limit", maxLength); + _logger.Warn(msg); + return string.Empty; + } + + return result; } private bool IsSameEpisode(string sourcePath, string newPath) diff --git a/MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs b/MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs index a6116ab09..839a85adb 100644 --- a/MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs +++ b/MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs @@ -13,6 +13,7 @@ using System; using System.IO; using System.Threading; using System.Threading.Tasks; +using CommonIO; namespace MediaBrowser.Server.Implementations.FileOrganization { diff --git a/MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs b/MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs index f993e1fc3..f1fe5539f 100644 --- a/MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs +++ b/MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using CommonIO; namespace MediaBrowser.Server.Implementations.FileOrganization { diff --git a/MediaBrowser.Server.Implementations/FileOrganization/TvFolderOrganizer.cs b/MediaBrowser.Server.Implementations/FileOrganization/TvFolderOrganizer.cs index 0caa8c26e..3e5296639 100644 --- a/MediaBrowser.Server.Implementations/FileOrganization/TvFolderOrganizer.cs +++ b/MediaBrowser.Server.Implementations/FileOrganization/TvFolderOrganizer.cs @@ -11,6 +11,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; namespace MediaBrowser.Server.Implementations.FileOrganization { @@ -35,17 +36,33 @@ namespace MediaBrowser.Server.Implementations.FileOrganization _providerManager = providerManager; } - public async Task Organize(TvFileOrganizationOptions options, CancellationToken cancellationToken, IProgress<double> progress) + private bool EnableOrganization(FileSystemMetadata fileInfo, TvFileOrganizationOptions options) { var minFileBytes = options.MinFileSizeMb * 1024 * 1024; + try + { + return _libraryManager.IsVideoFile(fileInfo.FullName) && fileInfo.Length >= minFileBytes; + } + catch (Exception ex) + { + _logger.ErrorException("Error organizing file {0}", ex, fileInfo.Name); + } + + return false; + } + + public async Task Organize(TvFileOrganizationOptions options, CancellationToken cancellationToken, IProgress<double> progress) + { var watchLocations = options.WatchLocations.ToList(); var eligibleFiles = watchLocations.SelectMany(GetFilesToOrganize) .OrderBy(_fileSystem.GetCreationTimeUtc) - .Where(i => _libraryManager.IsVideoFile(i.FullName) && i.Length >= minFileBytes) + .Where(i => EnableOrganization(i, options)) .ToList(); + var processedFolders = new HashSet<string>(); + progress.Report(10); if (eligibleFiles.Count > 0) @@ -59,7 +76,11 @@ namespace MediaBrowser.Server.Implementations.FileOrganization try { - await organizer.OrganizeEpisodeFile(file.FullName, options, options.OverwriteExistingEpisodes, cancellationToken).ConfigureAwait(false); + var result = await organizer.OrganizeEpisodeFile(file.FullName, options, options.OverwriteExistingEpisodes, cancellationToken).ConfigureAwait(false); + if (result.Status == FileSortingStatus.Success && !processedFolders.Contains(file.DirectoryName, StringComparer.OrdinalIgnoreCase)) + { + processedFolders.Add(file.DirectoryName); + } } catch (Exception ex) { @@ -77,7 +98,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization cancellationToken.ThrowIfCancellationRequested(); progress.Report(99); - foreach (var path in watchLocations) + foreach (var path in processedFolders) { var deleteExtensions = options.LeftOverFileExtensionsToDelete .Select(i => i.Trim().TrimStart('.')) @@ -92,9 +113,9 @@ namespace MediaBrowser.Server.Implementations.FileOrganization if (options.DeleteEmptyFolders) { - foreach (var subfolder in GetDirectories(path).ToList()) + if (!IsWatchFolder(path, watchLocations)) { - DeleteEmptyFolders(subfolder); + DeleteEmptyFolders(path); } } } @@ -103,44 +124,22 @@ namespace MediaBrowser.Server.Implementations.FileOrganization } /// <summary> - /// Gets the directories. - /// </summary> - /// <param name="path">The path.</param> - /// <returns>IEnumerable{System.String}.</returns> - private IEnumerable<string> GetDirectories(string path) - { - try - { - return Directory - .EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly) - .ToList(); - } - catch (IOException ex) - { - _logger.ErrorException("Error getting files from {0}", ex, path); - - return new List<string>(); - } - } - - /// <summary> /// Gets the files to organize. /// </summary> /// <param name="path">The path.</param> /// <returns>IEnumerable{FileInfo}.</returns> - private IEnumerable<FileInfo> GetFilesToOrganize(string path) + private List<FileSystemMetadata> GetFilesToOrganize(string path) { try { - return new DirectoryInfo(path) - .EnumerateFiles("*", SearchOption.AllDirectories) + return _fileSystem.GetFiles(path, true) .ToList(); } catch (IOException ex) { _logger.ErrorException("Error getting files from {0}", ex, path); - return new List<FileInfo>(); + return new List<FileSystemMetadata>(); } } @@ -151,8 +150,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization /// <param name="extensions">The extensions.</param> private void DeleteLeftOverFiles(string path, IEnumerable<string> extensions) { - var eligibleFiles = new DirectoryInfo(path) - .EnumerateFiles("*", SearchOption.AllDirectories) + var eligibleFiles = _fileSystem.GetFiles(path, true) .Where(i => extensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase)) .ToList(); @@ -177,19 +175,19 @@ namespace MediaBrowser.Server.Implementations.FileOrganization { try { - foreach (var d in Directory.EnumerateDirectories(path)) + foreach (var d in _fileSystem.GetDirectoryPaths(path)) { DeleteEmptyFolders(d); } - var entries = Directory.EnumerateFileSystemEntries(path); + var entries = _fileSystem.GetFileSystemEntryPaths(path); if (!entries.Any()) { try { _logger.Debug("Deleting empty directory {0}", path); - Directory.Delete(path); + _fileSystem.DeleteDirectory(path, false); } catch (UnauthorizedAccessException) { } catch (DirectoryNotFoundException) { } @@ -197,5 +195,15 @@ namespace MediaBrowser.Server.Implementations.FileOrganization } catch (UnauthorizedAccessException) { } } + + /// <summary> + /// Determines if a given folder path is contained in a folder list + /// </summary> + /// <param name="path">The folder path to check.</param> + /// <param name="watchLocations">A list of folders.</param> + private bool IsWatchFolder(string path, IEnumerable<string> watchLocations) + { + return watchLocations.Contains(path, StringComparer.OrdinalIgnoreCase); + } } } diff --git a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs index 3795f4b15..e8bb40ba1 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -79,6 +79,8 @@ namespace MediaBrowser.Server.Implementations.HttpServer _containerAdapter = new ContainerAdapter(applicationHost); } + public string GlobalResponse { get; set; } + public override void Configure(Container container) { HostConfig.Instance.DefaultRedirectPath = DefaultRedirectPath; @@ -90,7 +92,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer {typeof (FileNotFoundException), 404}, {typeof (DirectoryNotFoundException), 404}, {typeof (SecurityException), 401}, - {typeof (UnauthorizedAccessException), 401} + {typeof (UnauthorizedAccessException), 500} }; HostConfig.Instance.DebugMode = true; @@ -336,6 +338,13 @@ namespace MediaBrowser.Server.Implementations.HttpServer return Task.FromResult(true); } + if (!string.IsNullOrWhiteSpace(GlobalResponse)) + { + httpRes.Write(GlobalResponse); + httpRes.ContentType = "text/plain"; + return Task.FromResult(true); + } + var handler = HttpHandlerFactory.GetHandler(httpReq); var remoteIp = httpReq.RemoteIp; diff --git a/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs b/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs index 961d58eb6..080441dda 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -12,6 +12,7 @@ using System.IO; using System.Net; using System.Text; using System.Threading.Tasks; +using CommonIO; using MimeTypes = MediaBrowser.Model.Net.MimeTypes; namespace MediaBrowser.Server.Implementations.HttpServer @@ -476,7 +477,8 @@ namespace MediaBrowser.Server.Implementations.HttpServer return new StreamWriter(stream, contentType, _logger) { - OnComplete = options.OnComplete + OnComplete = options.OnComplete, + OnError = options.OnError }; } diff --git a/MediaBrowser.Server.Implementations/HttpServer/LoggerUtils.cs b/MediaBrowser.Server.Implementations/HttpServer/LoggerUtils.cs index 5558c24d7..f038591aa 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/LoggerUtils.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/LoggerUtils.cs @@ -27,7 +27,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer var msg = "HTTP Response " + statusCode + " to " + endPoint + responseTime; - logger.LogMultiline(msg, LogSeverity.Debug, log); + logger.LogMultiline(msg, LogSeverity.Info, log); } } } diff --git a/MediaBrowser.Server.Implementations/HttpServer/Security/AuthService.cs b/MediaBrowser.Server.Implementations/HttpServer/Security/AuthService.cs index b4da40702..f6b14fcab 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/Security/AuthService.cs @@ -220,7 +220,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.Security { if (string.IsNullOrWhiteSpace(token)) { - throw new SecurityException("Access token is invalid or expired."); + throw new SecurityException("Access token is required."); } var info = GetTokenInfo(request); diff --git a/MediaBrowser.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/MediaBrowser.Server.Implementations/HttpServer/Security/AuthorizationContext.cs index 80892b96c..509a00ff9 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/Security/AuthorizationContext.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/Security/AuthorizationContext.cs @@ -62,52 +62,15 @@ namespace MediaBrowser.Server.Implementations.HttpServer.Security auth.TryGetValue("Version", out version); } - var token = httpReq.Headers["X-MediaBrowser-Token"]; + var token = httpReq.Headers["X-Emby-Token"]; if (string.IsNullOrWhiteSpace(token)) { - token = httpReq.QueryString["api_key"]; + token = httpReq.Headers["X-MediaBrowser-Token"]; } - - // Hack until iOS is updated - // TODO: Remove - if (string.IsNullOrWhiteSpace(client)) - { - var userAgent = httpReq.Headers["User-Agent"] ?? string.Empty; - - if (userAgent.IndexOf("mediabrowserios", StringComparison.OrdinalIgnoreCase) != -1 || - userAgent.IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 || - userAgent.IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1) - { - client = "iOS"; - } - - else if (userAgent.IndexOf("crKey", StringComparison.OrdinalIgnoreCase) != -1) - { - client = "Chromecast"; - } - } - - // Hack until iOS is updated - // TODO: Remove - if (string.IsNullOrWhiteSpace(device)) + if (string.IsNullOrWhiteSpace(token)) { - var userAgent = httpReq.Headers["User-Agent"] ?? string.Empty; - - if (userAgent.IndexOf("iPhone", StringComparison.OrdinalIgnoreCase) != -1) - { - device = "iPhone"; - } - - else if (userAgent.IndexOf("iPad", StringComparison.OrdinalIgnoreCase) != -1) - { - device = "iPad"; - } - - else if (userAgent.IndexOf("crKey", StringComparison.OrdinalIgnoreCase) != -1) - { - device = "Chromecast"; - } + token = httpReq.QueryString["api_key"]; } var info = new AuthorizationInfo diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs index fdb27d40d..f8178c115 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs @@ -191,7 +191,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp var type = request.IsWebSocketRequest ? "Web Socket" : "HTTP " + request.HttpMethod; - logger.LogMultiline(type + " " + request.Url, LogSeverity.Debug, log); + logger.LogMultiline(type + " " + request.Url, LogSeverity.Info, log); } private void HandleError(Exception ex, HttpListenerContext context) diff --git a/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs b/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs index daa5b86d9..a756f4aa8 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs @@ -36,6 +36,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer } public Action OnComplete { get; set; } + public Action OnError { get; set; } /// <summary> /// Initializes a new instance of the <see cref="StreamWriter" /> class. @@ -102,6 +103,11 @@ namespace MediaBrowser.Server.Implementations.HttpServer { Logger.ErrorException("Error streaming data", ex); + if (OnError != null) + { + OnError(); + } + throw; } finally diff --git a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs index 5bd26ce18..e107ea9f1 100644 --- a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs +++ b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs @@ -15,6 +15,8 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; +using MediaBrowser.Controller; namespace MediaBrowser.Server.Implementations.IO { @@ -86,7 +88,7 @@ namespace MediaBrowser.Server.Implementations.IO // This is an arbitraty amount of time, but delay it because file system writes often trigger events long after the file was actually written to. // Seeing long delays in some situations, especially over the network, sometimes up to 45 seconds // But if we make this delay too high, we risk missing legitimate changes, such as user adding a new file, or hand-editing metadata - await Task.Delay(20000).ConfigureAwait(false); + await Task.Delay(25000).ConfigureAwait(false); string val; _tempIgnoredPaths.TryRemove(path, out val); @@ -113,11 +115,12 @@ namespace MediaBrowser.Server.Implementations.IO private IServerConfigurationManager ConfigurationManager { get; set; } private readonly IFileSystem _fileSystem; + private readonly IServerApplicationHost _appHost; /// <summary> /// Initializes a new instance of the <see cref="LibraryMonitor" /> class. /// </summary> - public LibraryMonitor(ILogManager logManager, ITaskManager taskManager, ILibraryManager libraryManager, IServerConfigurationManager configurationManager, IFileSystem fileSystem) + public LibraryMonitor(ILogManager logManager, ITaskManager taskManager, ILibraryManager libraryManager, IServerConfigurationManager configurationManager, IFileSystem fileSystem, IServerApplicationHost appHost) { if (taskManager == null) { @@ -129,6 +132,7 @@ namespace MediaBrowser.Server.Implementations.IO Logger = logManager.GetLogger(GetType().Name); ConfigurationManager = configurationManager; _fileSystem = fileSystem; + _appHost = appHost; SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged; } @@ -156,7 +160,7 @@ namespace MediaBrowser.Server.Implementations.IO switch (ConfigurationManager.Configuration.EnableLibraryMonitor) { case AutoOnOff.Auto: - return Environment.OSVersion.Platform == PlatformID.Win32NT; + return _appHost.SupportsLibraryMonitor; case AutoOnOff.Enabled: return true; default: @@ -645,7 +649,7 @@ namespace MediaBrowser.Server.Implementations.IO if (item != null) { // If the item has been deleted find the first valid parent that still exists - while (!Directory.Exists(item.Path) && !File.Exists(item.Path)) + while (!_fileSystem.DirectoryExists(item.Path) && !_fileSystem.FileExists(item.Path)) { item = item.Parent; diff --git a/MediaBrowser.Server.Implementations/Intros/DefaultIntroProvider.cs b/MediaBrowser.Server.Implementations/Intros/DefaultIntroProvider.cs index db9841f9d..ec94e16db 100644 --- a/MediaBrowser.Server.Implementations/Intros/DefaultIntroProvider.cs +++ b/MediaBrowser.Server.Implementations/Intros/DefaultIntroProvider.cs @@ -15,6 +15,8 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; +using MediaBrowser.Common.IO; namespace MediaBrowser.Server.Implementations.Intros { @@ -25,14 +27,16 @@ namespace MediaBrowser.Server.Implementations.Intros private readonly ILocalizationManager _localization; private readonly IConfigurationManager _serverConfig; private readonly ILibraryManager _libraryManager; + private readonly IFileSystem _fileSystem; - public DefaultIntroProvider(ISecurityManager security, IChannelManager channelManager, ILocalizationManager localization, IConfigurationManager serverConfig, ILibraryManager libraryManager) + public DefaultIntroProvider(ISecurityManager security, IChannelManager channelManager, ILocalizationManager localization, IConfigurationManager serverConfig, ILibraryManager libraryManager, IFileSystem fileSystem) { _security = security; _channelManager = channelManager; _localization = localization; _serverConfig = serverConfig; _libraryManager = libraryManager; + _fileSystem = fileSystem; } public async Task<IEnumerable<IntroInfo>> GetIntros(BaseItem item, User user) @@ -79,8 +83,16 @@ namespace MediaBrowser.Server.Implementations.Intros if (config.EnableIntrosFromMoviesInLibrary) { - var itemsWithTrailers = user.RootFolder - .GetRecursiveChildren(user, i => + var inputItems = _libraryManager.GetItems(new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Movie).Name }, + + User = user + + }).Items; + + var itemsWithTrailers = inputItems + .Where(i => { var hasTrailers = i as IHasTrailers; @@ -232,7 +244,7 @@ namespace MediaBrowser.Server.Implementations.Intros return new List<string>(); } - return Directory.EnumerateFiles(options.CustomIntroPath, "*", SearchOption.AllDirectories) + return _fileSystem.GetFilePaths(options.CustomIntroPath, true) .Where(_libraryManager.IsVideoFile); } diff --git a/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index be8c1cfbd..9035d6479 100644 --- a/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using CommonIO; namespace MediaBrowser.Server.Implementations.Library { diff --git a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs index cc9d9551c..92acd08d1 100644 --- a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs +++ b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs @@ -34,6 +34,8 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; +using MediaBrowser.Model.Extensions; using MoreLinq; using SortOrder = MediaBrowser.Model.Entities.SortOrder; @@ -354,6 +356,10 @@ namespace MediaBrowser.Server.Implementations.Library return; } } + //if (!(item is Folder)) + //{ + // return; + //} LibraryItemsCache.AddOrUpdate(id, item, delegate { return item; }); } @@ -395,12 +401,12 @@ namespace MediaBrowser.Server.Implementations.Library { foreach (var path in item.GetDeletePaths().ToList()) { - if (Directory.Exists(path)) + if (_fileSystem.DirectoryExists(path)) { _logger.Debug("Deleting path {0}", path); _fileSystem.DeleteDirectory(path, true); } - else if (File.Exists(path)) + else if (_fileSystem.FileExists(path)) { _logger.Debug("Deleting path {0}", path); _fileSystem.DeleteFile(path); @@ -545,13 +551,13 @@ namespace MediaBrowser.Server.Implementations.Library return item; } - public BaseItem ResolvePath(FileSystemInfo fileInfo, + public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null) { - return ResolvePath(fileInfo, new DirectoryService(_logger), parent); + return ResolvePath(fileInfo, new DirectoryService(_logger, _fileSystem), parent); } - private BaseItem ResolvePath(FileSystemInfo fileInfo, IDirectoryService directoryService, Folder parent = null, string collectionType = null) + private BaseItem ResolvePath(FileSystemMetadata fileInfo, IDirectoryService directoryService, Folder parent = null, string collectionType = null) { if (fileInfo == null) { @@ -595,7 +601,7 @@ namespace MediaBrowser.Server.Implementations.Library { var paths = NormalizeRootPathList(fileSystemDictionary.Keys); - fileSystemDictionary = paths.Select(i => (FileSystemInfo)new DirectoryInfo(i)).ToDictionary(i => i.FullName); + fileSystemDictionary = paths.Select(_fileSystem.GetDirectoryInfo).ToDictionary(i => i.FullName); } args.FileSystemDictionary = fileSystemDictionary; @@ -638,7 +644,7 @@ namespace MediaBrowser.Server.Implementations.Library return !args.ContainsFileSystemEntryByName(".ignore"); } - public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemInfo> files, IDirectoryService directoryService, Folder parent, string collectionType) + public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, Folder parent, string collectionType) { var fileList = files.ToList(); @@ -666,7 +672,7 @@ namespace MediaBrowser.Server.Implementations.Library return ResolveFileList(fileList, directoryService, parent, collectionType); } - private IEnumerable<BaseItem> ResolveFileList(IEnumerable<FileSystemInfo> fileList, IDirectoryService directoryService, Folder parent, string collectionType) + private IEnumerable<BaseItem> ResolveFileList(IEnumerable<FileSystemMetadata> fileList, IDirectoryService directoryService, Folder parent, string collectionType) { return fileList.Select(f => { @@ -691,9 +697,9 @@ namespace MediaBrowser.Server.Implementations.Library { var rootFolderPath = ConfigurationManager.ApplicationPaths.RootFolderPath; - Directory.CreateDirectory(rootFolderPath); + _fileSystem.CreateDirectory(rootFolderPath); - var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ?? (AggregateFolder)ResolvePath(new DirectoryInfo(rootFolderPath)); + var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ?? (AggregateFolder)ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)); // Add in the plug-in folders foreach (var child in PluginFolderCreators) @@ -742,13 +748,13 @@ namespace MediaBrowser.Server.Implementations.Library { var userRootPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath; - Directory.CreateDirectory(userRootPath); + _fileSystem.CreateDirectory(userRootPath); var tmpItem = GetItemById(GetNewItemId(userRootPath, typeof(UserRootFolder))) as UserRootFolder; if (tmpItem == null) { - tmpItem = (UserRootFolder)ResolvePath(new DirectoryInfo(userRootPath)); + tmpItem = (UserRootFolder)ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath)); } _userRootFolder = tmpItem; @@ -1007,9 +1013,9 @@ namespace MediaBrowser.Server.Implementations.Library public Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress) { // Ensure the location is available. - Directory.CreateDirectory(ConfigurationManager.ApplicationPaths.PeoplePath); + _fileSystem.CreateDirectory(ConfigurationManager.ApplicationPaths.PeoplePath); - return new PeopleValidator(this, _logger, ConfigurationManager).ValidatePeople(cancellationToken, progress); + return new PeopleValidator(this, _logger, ConfigurationManager, _fileSystem).ValidatePeople(cancellationToken, progress); } /// <summary> @@ -1064,7 +1070,7 @@ namespace MediaBrowser.Server.Implementations.Library progress.Report(.5); // Start by just validating the children of the root, but go no further - await RootFolder.ValidateChildren(new Progress<double>(), cancellationToken, new MetadataRefreshOptions(), recursive: false); + await RootFolder.ValidateChildren(new Progress<double>(), cancellationToken, new MetadataRefreshOptions(_fileSystem), recursive: false); progress.Report(1); @@ -1072,7 +1078,7 @@ namespace MediaBrowser.Server.Implementations.Library await userRoot.RefreshMetadata(cancellationToken).ConfigureAwait(false); - await userRoot.ValidateChildren(new Progress<double>(), cancellationToken, new MetadataRefreshOptions(), recursive: false).ConfigureAwait(false); + await userRoot.ValidateChildren(new Progress<double>(), cancellationToken, new MetadataRefreshOptions(_fileSystem), recursive: false).ConfigureAwait(false); progress.Report(2); var innerProgress = new ActionableProgress<double>(); @@ -1080,7 +1086,7 @@ namespace MediaBrowser.Server.Implementations.Library innerProgress.RegisterAction(pct => progress.Report(2 + pct * .73)); // Now validate the entire media library - await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(), recursive: true).ConfigureAwait(false); + await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(_fileSystem), recursive: true).ConfigureAwait(false); progress.Report(75); @@ -1165,23 +1171,45 @@ namespace MediaBrowser.Server.Implementations.Library /// <returns>IEnumerable{VirtualFolderInfo}.</returns> private IEnumerable<VirtualFolderInfo> GetView(string path) { - return Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly) - .Select(dir => new VirtualFolderInfo - { - Name = Path.GetFileName(dir), + var topLibraryFolders = GetUserRootFolder().Children.ToList(); - Locations = Directory.EnumerateFiles(dir, "*.mblink", SearchOption.TopDirectoryOnly) - .Select(_fileSystem.ResolveShortcut) - .OrderBy(i => i) - .ToList(), + return _fileSystem.GetDirectoryPaths(path) + .Select(dir => GetVirtualFolderInfo(dir, topLibraryFolders)); + } - CollectionType = GetCollectionType(dir) - }); + private VirtualFolderInfo GetVirtualFolderInfo(string dir, List<BaseItem> collectionFolders) + { + var info = new VirtualFolderInfo + { + Name = Path.GetFileName(dir), + + Locations = Directory.EnumerateFiles(dir, "*.mblink", SearchOption.TopDirectoryOnly) + .Select(_fileSystem.ResolveShortcut) + .OrderBy(i => i) + .ToList(), + + CollectionType = GetCollectionType(dir) + }; + + var libraryFolder = collectionFolders.FirstOrDefault(i => string.Equals(i.Path, dir, StringComparison.OrdinalIgnoreCase)); + + if (libraryFolder != null && libraryFolder.HasImage(ImageType.Primary)) + { + info.PrimaryImageItemId = libraryFolder.Id.ToString("N"); + } + + if (libraryFolder != null) + { + info.ItemId = libraryFolder.Id.ToString("N"); + } + + return info; } private string GetCollectionType(string path) { - return new DirectoryInfo(path).EnumerateFiles("*.collection", SearchOption.TopDirectoryOnly) + return _fileSystem.GetFiles(path, false) + .Where(i => string.Equals(i.Extension, ".collection", StringComparison.OrdinalIgnoreCase)) .Select(i => _fileSystem.GetFileNameWithoutExtension(i)) .FirstOrDefault(); } @@ -1638,6 +1666,7 @@ namespace MediaBrowser.Server.Implementations.Library } private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24); + //private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromMinutes(1); public Task<UserView> GetNamedView(User user, string name, @@ -1645,12 +1674,7 @@ namespace MediaBrowser.Server.Implementations.Library string sortName, CancellationToken cancellationToken) { - if (ConfigurationManager.Configuration.EnableUserSpecificUserViews) - { - return GetNamedViewInternal(user, name, null, viewType, sortName, null, cancellationToken); - } - - return GetNamedView(name, viewType, sortName, cancellationToken); + return GetNamedViewInternal(user, name, null, viewType, sortName, null, cancellationToken); } public async Task<UserView> GetNamedView(string name, @@ -1671,7 +1695,7 @@ namespace MediaBrowser.Server.Implementations.Library if (item == null || !string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase)) { - Directory.CreateDirectory(path); + _fileSystem.CreateDirectory(path); item = new UserView { @@ -1696,13 +1720,19 @@ namespace MediaBrowser.Server.Implementations.Library if (!refresh) { - refresh = (DateTime.UtcNow - item.DateLastSaved) >= _viewRefreshInterval; + refresh = (DateTime.UtcNow - item.DateLastRefreshed) >= _viewRefreshInterval; + } + + if (!refresh && item.DisplayParentId != Guid.Empty) + { + var displayParent = GetItemById(item.DisplayParentId); + refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; } if (refresh) { await item.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None).ConfigureAwait(false); - _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions + _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 @@ -1758,7 +1788,7 @@ namespace MediaBrowser.Server.Implementations.Library if (item == null) { - Directory.CreateDirectory(path); + _fileSystem.CreateDirectory(path); item = new UserView { @@ -1767,12 +1797,13 @@ namespace MediaBrowser.Server.Implementations.Library DateCreated = DateTime.UtcNow, Name = name, ViewType = viewType, - ForcedSortName = sortName + ForcedSortName = sortName, + UserId = user.Id }; if (!string.IsNullOrWhiteSpace(parentId)) { - item.ParentId = new Guid(parentId); + item.DisplayParentId = new Guid(parentId); } await CreateItem(item, cancellationToken).ConfigureAwait(false); @@ -1780,17 +1811,29 @@ namespace MediaBrowser.Server.Implementations.Library isNew = true; } + if (!item.UserId.HasValue) + { + item.UserId = user.Id; + await item.UpdateToRepository(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); + } + if (!string.Equals(viewType, item.ViewType, StringComparison.OrdinalIgnoreCase)) { item.ViewType = viewType; await item.UpdateToRepository(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); } - var refresh = isNew || (DateTime.UtcNow - item.DateLastSaved) >= _viewRefreshInterval; + var refresh = isNew || (DateTime.UtcNow - item.DateLastRefreshed) >= _viewRefreshInterval; + + if (!refresh && item.DisplayParentId != Guid.Empty) + { + var displayParent = GetItemById(item.DisplayParentId); + refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; + } if (refresh) { - _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions + _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem) { // Need to force save to increment DateLastSaved ForceSave = true @@ -1800,6 +1843,81 @@ namespace MediaBrowser.Server.Implementations.Library return item; } + public async Task<UserView> GetShadowView(BaseItem parent, + string viewType, + string sortName, + string uniqueId, + CancellationToken cancellationToken) + { + if (parent == null) + { + throw new ArgumentNullException("parent"); + } + + var name = parent.Name; + var parentId = parent.Id; + + var idValues = "37_namedview_" + name + parentId + (viewType ?? string.Empty); + if (!string.IsNullOrWhiteSpace(uniqueId)) + { + idValues += uniqueId; + } + + var id = GetNewItemId(idValues, typeof(UserView)); + + var path = parent.Path; + + var item = GetItemById(id) as UserView; + + var isNew = false; + + if (item == null) + { + _fileSystem.CreateDirectory(path); + + item = new UserView + { + Path = path, + Id = id, + DateCreated = DateTime.UtcNow, + Name = name, + ViewType = viewType, + ForcedSortName = sortName + }; + + item.DisplayParentId = parentId; + + await CreateItem(item, cancellationToken).ConfigureAwait(false); + + isNew = true; + } + + if (!string.Equals(viewType, item.ViewType, StringComparison.OrdinalIgnoreCase)) + { + item.ViewType = viewType; + await item.UpdateToRepository(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); + } + + var refresh = isNew || (DateTime.UtcNow - item.DateLastRefreshed) >= _viewRefreshInterval; + + if (!refresh && item.DisplayParentId != Guid.Empty) + { + var displayParent = GetItemById(item.DisplayParentId); + refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; + } + + if (refresh) + { + _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem) + { + // Need to force save to increment DateLastSaved + ForceSave = true + }); + } + + return item; + } + public async Task<UserView> GetNamedView(string name, string parentId, string viewType, @@ -1828,7 +1946,7 @@ namespace MediaBrowser.Server.Implementations.Library if (item == null) { - Directory.CreateDirectory(path); + _fileSystem.CreateDirectory(path); item = new UserView { @@ -1842,7 +1960,7 @@ namespace MediaBrowser.Server.Implementations.Library if (!string.IsNullOrWhiteSpace(parentId)) { - item.ParentId = new Guid(parentId); + item.DisplayParentId = new Guid(parentId); } await CreateItem(item, cancellationToken).ConfigureAwait(false); @@ -1856,11 +1974,17 @@ namespace MediaBrowser.Server.Implementations.Library await item.UpdateToRepository(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); } - var refresh = isNew || (DateTime.UtcNow - item.DateLastSaved) >= _viewRefreshInterval; + var refresh = isNew || (DateTime.UtcNow - item.DateLastRefreshed) >= _viewRefreshInterval; + + if (!refresh && item.DisplayParentId != Guid.Empty) + { + var displayParent = GetItemById(item.DisplayParentId); + refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; + } if (refresh) { - _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions + _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem) { // Need to force save to increment DateLastSaved ForceSave = true @@ -2043,11 +2167,11 @@ namespace MediaBrowser.Server.Implementations.Library }; } - public IEnumerable<Video> FindTrailers(BaseItem owner, List<FileSystemInfo> fileSystemChildren, IDirectoryService directoryService) + public IEnumerable<Video> FindTrailers(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) { - var files = fileSystemChildren.OfType<DirectoryInfo>() + var files = fileSystemChildren.Where(i => i.IsDirectory) .Where(i => string.Equals(i.Name, BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase)) - .SelectMany(i => i.EnumerateFiles("*", SearchOption.TopDirectoryOnly)) + .SelectMany(i => _fileSystem.GetFiles(i.FullName, false)) .ToList(); var videoListResolver = new VideoListResolver(GetNamingOptions(), new PatternsLogger()); @@ -2063,7 +2187,7 @@ namespace MediaBrowser.Server.Implementations.Library if (currentVideo != null) { - files.AddRange(currentVideo.Extras.Where(i => string.Equals(i.ExtraType, "trailer", StringComparison.OrdinalIgnoreCase)).Select(i => new FileInfo(i.Path))); + files.AddRange(currentVideo.Extras.Where(i => string.Equals(i.ExtraType, "trailer", StringComparison.OrdinalIgnoreCase)).Select(i => _fileSystem.GetFileInfo(i.Path))); } return ResolvePaths(files, directoryService, null, null) @@ -2086,11 +2210,11 @@ namespace MediaBrowser.Server.Implementations.Library }).OrderBy(i => i.Path).ToList(); } - public IEnumerable<Video> FindExtras(BaseItem owner, List<FileSystemInfo> fileSystemChildren, IDirectoryService directoryService) + public IEnumerable<Video> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) { - var files = fileSystemChildren.OfType<DirectoryInfo>() + var files = fileSystemChildren.Where(i => i.IsDirectory) .Where(i => string.Equals(i.Name, "extras", StringComparison.OrdinalIgnoreCase) || string.Equals(i.Name, "specials", StringComparison.OrdinalIgnoreCase)) - .SelectMany(i => i.EnumerateFiles("*", SearchOption.TopDirectoryOnly)) + .SelectMany(i => _fileSystem.GetFiles(i.FullName, false)) .ToList(); var videoListResolver = new VideoListResolver(GetNamingOptions(), new PatternsLogger()); @@ -2106,7 +2230,7 @@ namespace MediaBrowser.Server.Implementations.Library if (currentVideo != null) { - files.AddRange(currentVideo.Extras.Where(i => !string.Equals(i.ExtraType, "trailer", StringComparison.OrdinalIgnoreCase)).Select(i => new FileInfo(i.Path))); + files.AddRange(currentVideo.Extras.Where(i => !string.Equals(i.ExtraType, "trailer", StringComparison.OrdinalIgnoreCase)).Select(i => _fileSystem.GetFileInfo(i.Path))); } return ResolvePaths(files, directoryService, null, null) @@ -2129,6 +2253,38 @@ namespace MediaBrowser.Server.Implementations.Library }).OrderBy(i => i.Path).ToList(); } + public string SubstitutePath(string path, string from, string to) + { + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentNullException("path"); + } + if (string.IsNullOrWhiteSpace(from)) + { + throw new ArgumentNullException("from"); + } + if (string.IsNullOrWhiteSpace(to)) + { + throw new ArgumentNullException("to"); + } + + var newPath = path.Replace(from, to, StringComparison.OrdinalIgnoreCase); + + if (!string.Equals(newPath, path)) + { + if (to.IndexOf('/') != -1) + { + newPath = newPath.Replace('\\', '/'); + } + else + { + newPath = newPath.Replace('/', '\\'); + } + } + + return newPath; + } + private void SetExtraTypeFromFilename(Video item) { var resolver = new ExtraResolver(GetNamingOptions(), new PatternsLogger(), new RegexProvider()); @@ -2181,7 +2337,7 @@ namespace MediaBrowser.Server.Implementations.Library } } - return item.People ?? new List<PersonInfo>(); + return new List<PersonInfo>(); } public List<Person> GetPeopleItems(InternalPeopleQuery query) @@ -2222,5 +2378,17 @@ namespace MediaBrowser.Server.Implementations.Library return ItemRepository.UpdatePeople(item.Id, people); } + + private readonly SemaphoreSlim _dynamicImageResourcePool = new SemaphoreSlim(1,1); + public async Task<ItemImageInfo> ConvertImageToLocal(IHasImages item, ItemImageInfo image, int imageIndex) + { + _logger.Debug("ConvertImageToLocal item {0}", item.Id); + + await _providerManagerFactory().SaveImage(item, image.Path, _dynamicImageResourcePool, image.Type, imageIndex, CancellationToken.None).ConfigureAwait(false); + + await item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); + + return item.GetImageInfo(image.Type, imageIndex); + } } } diff --git a/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs b/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs index 63067bf5a..9694965c7 100644 --- a/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs +++ b/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs @@ -15,6 +15,8 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; +using MediaBrowser.Common.IO; namespace MediaBrowser.Server.Implementations.Library { @@ -24,17 +26,19 @@ namespace MediaBrowser.Server.Implementations.Library private readonly IUserManager _userManager; private readonly ILibraryManager _libraryManager; private readonly IJsonSerializer _jsonSerializer; + private readonly IFileSystem _fileSystem; private IMediaSourceProvider[] _providers; private readonly ILogger _logger; - public MediaSourceManager(IItemRepository itemRepo, IUserManager userManager, ILibraryManager libraryManager, ILogger logger, IJsonSerializer jsonSerializer) + public MediaSourceManager(IItemRepository itemRepo, IUserManager userManager, ILibraryManager libraryManager, ILogger logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem) { _itemRepo = itemRepo; _userManager = userManager; _libraryManager = libraryManager; _logger = logger; _jsonSerializer = jsonSerializer; + _fileSystem = fileSystem; } public void AddParts(IEnumerable<IMediaSourceProvider> providers) @@ -77,10 +81,6 @@ namespace MediaBrowser.Server.Implementations.Library { return false; } - if (string.Equals(stream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)) - { - return false; - } return true; } @@ -105,6 +105,18 @@ namespace MediaBrowser.Server.Implementations.Library return GetMediaStreamsForItem(list); } + private int GetMaxAllowedBitrateForExternalSubtitleStream() + { + // This is abitrary but at some point it becomes too slow to extract subtitles on the fly + // We need to learn more about when this is the case vs. when it isn't + if (Environment.ProcessorCount >= 8) + { + return 10000000; + } + + return 2000000; + } + private IEnumerable<MediaStream> GetMediaStreamsForItem(IEnumerable<MediaStream> streams) { var list = streams.ToList(); @@ -117,9 +129,7 @@ namespace MediaBrowser.Server.Implementations.Library { var videoStream = list.FirstOrDefault(i => i.Type == MediaStreamType.Video); - // This is abitrary but at some point it becomes too slow to extract subtitles on the fly - // We need to learn more about when this is the case vs. when it isn't - const int maxAllowedBitrateForExternalSubtitleStream = 10000000; + int maxAllowedBitrateForExternalSubtitleStream = GetMaxAllowedBitrateForExternalSubtitleStream(); var videoBitrate = videoStream == null ? maxAllowedBitrateForExternalSubtitleStream : videoStream.BitRate ?? maxAllowedBitrateForExternalSubtitleStream; @@ -170,7 +180,7 @@ namespace MediaBrowser.Server.Implementations.Library if (source.Protocol == MediaProtocol.File) { // TODO: Path substitution - if (!File.Exists(source.Path)) + if (!_fileSystem.FileExists(source.Path)) { source.SupportsDirectStream = false; } @@ -582,4 +592,4 @@ namespace MediaBrowser.Server.Implementations.Library public MediaSourceInfo MediaSource; } } -} +}
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Library/MusicManager.cs b/MediaBrowser.Server.Implementations/Library/MusicManager.cs index 683e6c5cc..aee101ef4 100644 --- a/MediaBrowser.Server.Implementations/Library/MusicManager.cs +++ b/MediaBrowser.Server.Implementations/Library/MusicManager.cs @@ -78,10 +78,19 @@ namespace MediaBrowser.Server.Implementations.Library public IEnumerable<Audio> GetInstantMixFromGenres(IEnumerable<string> genres, User user) { - var inputItems = user.RootFolder - .GetRecursiveChildren(user, i => i is Audio); + var genreList = genres.ToList(); - var genresDictionary = genres.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); + var inputItems = _libraryManager.GetItems(new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Audio).Name }, + + Genres = genreList.ToArray(), + + User = user + + }).Items; + + var genresDictionary = genreList.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); return inputItems .Cast<Audio>() @@ -131,7 +140,7 @@ namespace MediaBrowser.Server.Implementations.Library { return GetInstantMixFromFolder(folder, user); } - + return new Audio[] { }; } } diff --git a/MediaBrowser.Server.Implementations/Library/ResolverHelper.cs b/MediaBrowser.Server.Implementations/Library/ResolverHelper.cs index dac658095..100241d90 100644 --- a/MediaBrowser.Server.Implementations/Library/ResolverHelper.cs +++ b/MediaBrowser.Server.Implementations/Library/ResolverHelper.cs @@ -6,6 +6,7 @@ using System; using System.IO; using System.Linq; using System.Text.RegularExpressions; +using CommonIO; namespace MediaBrowser.Server.Implementations.Library { @@ -88,7 +89,7 @@ namespace MediaBrowser.Server.Implementations.Library /// </summary> /// <param name="item">The item.</param> /// <param name="fileInfo">The file information.</param> - private static void EnsureName(BaseItem item, FileSystemInfo fileInfo) + private static void EnsureName(BaseItem item, FileSystemMetadata fileInfo) { // If the subclass didn't supply a name, add it here if (string.IsNullOrEmpty(item.Name) && !string.IsNullOrEmpty(item.Path)) @@ -179,7 +180,7 @@ namespace MediaBrowser.Server.Implementations.Library } } - private static void SetDateCreated(BaseItem item, IFileSystem fileSystem, FileSystemInfo info) + private static void SetDateCreated(BaseItem item, IFileSystem fileSystem, FileSystemMetadata info) { var config = BaseItem.ConfigurationManager.GetMetadataConfiguration(); diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index 0abdc4296..26e767c20 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -10,6 +10,7 @@ using MediaBrowser.Server.Implementations.Logging; using System; using System.Collections.Generic; using System.IO; +using CommonIO; namespace MediaBrowser.Server.Implementations.Library.Resolvers.Audio { @@ -107,7 +108,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Audio /// <param name="fileSystem">The file system.</param> /// <param name="libraryManager">The library manager.</param> /// <returns><c>true</c> if the specified list contains music; otherwise, <c>false</c>.</returns> - private bool ContainsMusic(IEnumerable<FileSystemInfo> list, + private bool ContainsMusic(IEnumerable<FileSystemMetadata> list, bool allowSubfolders, IDirectoryService directoryService, ILogger logger, diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs index 9f3f24865..97a31990e 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs @@ -9,6 +9,7 @@ using MediaBrowser.Model.Logging; using System; using System.IO; using System.Linq; +using CommonIO; namespace MediaBrowser.Server.Implementations.Library.Resolvers.Audio { diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index 343b6d3a4..fd74b68b8 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -179,10 +179,16 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers else if (string.Equals(videoInfo.StubType, "hddvd", StringComparison.OrdinalIgnoreCase)) { video.VideoType = VideoType.HdDvd; + video.IsHD = true; } else if (string.Equals(videoInfo.StubType, "bluray", StringComparison.OrdinalIgnoreCase)) { video.VideoType = VideoType.BluRay; + video.IsHD = true; + } + else if (string.Equals(videoInfo.StubType, "hdtv", StringComparison.OrdinalIgnoreCase)) + { + video.IsHD = true; } } } diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index ff38e057b..3252db505 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -13,6 +13,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using CommonIO; +using MediaBrowser.Common.IO; namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies { @@ -43,7 +45,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies } public MultiItemResolverResult ResolveMultiple(Folder parent, - List<FileSystemInfo> files, + List<FileSystemMetadata> files, string collectionType, IDirectoryService directoryService) { @@ -61,7 +63,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies } private MultiItemResolverResult ResolveMultipleInternal(Folder parent, - List<FileSystemInfo> files, + List<FileSystemMetadata> files, string collectionType, IDirectoryService directoryService) { @@ -72,12 +74,12 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase)) { - return ResolveVideos<MusicVideo>(parent, files, directoryService, collectionType, false); + return ResolveVideos<MusicVideo>(parent, files, directoryService, false); } if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase)) { - return ResolveVideos<Video>(parent, files, directoryService, collectionType, false); + return ResolveVideos<Video>(parent, files, directoryService, false); } if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)) @@ -90,7 +92,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies // Owned items should just use the plain video type if (parent == null) { - return ResolveVideos<Video>(parent, files, directoryService, collectionType, false); + return ResolveVideos<Video>(parent, files, directoryService, false); } if (parent is Series || parent.Parents.OfType<Series>().Any()) @@ -98,23 +100,23 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies return null; } - return ResolveVideos<Movie>(parent, files, directoryService, collectionType, false); + return ResolveVideos<Movie>(parent, files, directoryService, false); } if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase)) { - return ResolveVideos<Movie>(parent, files, directoryService, collectionType, true); + return ResolveVideos<Movie>(parent, files, directoryService, true); } return null; } - private MultiItemResolverResult ResolveVideos<T>(Folder parent, IEnumerable<FileSystemInfo> fileSystemEntries, IDirectoryService directoryService, string collectionType, bool suppportMultiEditions) + private MultiItemResolverResult ResolveVideos<T>(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, bool suppportMultiEditions) where T : Video, new() { - var files = new List<FileSystemInfo>(); + var files = new List<FileSystemMetadata>(); var videos = new List<BaseItem>(); - var leftOver = new List<FileSystemInfo>(); + var leftOver = new List<FileSystemMetadata>(); // Loop through each child file/folder and see if we find a video foreach (var child in fileSystemEntries) @@ -343,10 +345,10 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies /// <param name="directoryService">The directory service.</param> /// <param name="collectionType">Type of the collection.</param> /// <returns>Movie.</returns> - private T FindMovie<T>(string path, Folder parent, List<FileSystemInfo> fileSystemEntries, IDirectoryService directoryService, string collectionType) + private T FindMovie<T>(string path, Folder parent, List<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, string collectionType) where T : Video, new() { - var multiDiscFolders = new List<FileSystemInfo>(); + var multiDiscFolders = new List<FileSystemMetadata>(); // Search for a folder rip foreach (var child in fileSystemEntries) @@ -394,7 +396,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies !string.Equals(collectionType, CollectionType.Photos) && !string.Equals(collectionType, CollectionType.MusicVideos); - var result = ResolveVideos<T>(parent, fileSystemEntries, directoryService, collectionType, supportsMultiVersion); + var result = ResolveVideos<T>(parent, fileSystemEntries, directoryService, supportsMultiVersion); if (result.Items.Count == 1) { @@ -419,7 +421,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies /// <param name="multiDiscFolders">The folders.</param> /// <param name="directoryService">The directory service.</param> /// <returns>``0.</returns> - private T GetMultiDiscMovie<T>(List<FileSystemInfo> multiDiscFolders, IDirectoryService directoryService) + private T GetMultiDiscMovie<T>(List<FileSystemMetadata> multiDiscFolders, IDirectoryService directoryService) where T : Video, new() { var videoTypes = new List<VideoType>(); @@ -492,7 +494,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies }; } - private bool IsInvalid(Folder parent, string collectionType, IEnumerable<FileSystemInfo> files) + private bool IsInvalid(Folder parent, string collectionType, IEnumerable<FileSystemMetadata> files) { if (parent != null) { @@ -504,7 +506,6 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies var validCollectionTypes = new[] { - string.Empty, CollectionType.Movies, CollectionType.HomeVideos, CollectionType.MusicVideos, @@ -512,7 +513,12 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies CollectionType.Photos }; - return !validCollectionTypes.Contains(collectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + if (string.IsNullOrWhiteSpace(collectionType)) + { + return false; + } + + return !validCollectionTypes.Contains(collectionType, StringComparer.OrdinalIgnoreCase); } } } diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs index 31b2d8b0f..cde44122e 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs @@ -6,6 +6,7 @@ using MediaBrowser.Controller.Resolvers; using System; using System.IO; using System.Linq; +using CommonIO; namespace MediaBrowser.Server.Implementations.Library.Resolvers { diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index c5565eb53..19bd4a1a3 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using CommonIO; namespace MediaBrowser.Server.Implementations.Library.Resolvers.TV { @@ -99,7 +100,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.TV } public static bool IsSeriesFolder(string path, - IEnumerable<FileSystemInfo> fileSystemChildren, + IEnumerable<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService, IFileSystem fileSystem, ILogger logger, diff --git a/MediaBrowser.Server.Implementations/Library/SearchEngine.cs b/MediaBrowser.Server.Implementations/Library/SearchEngine.cs index d4ff89b4f..d6aff1192 100644 --- a/MediaBrowser.Server.Implementations/Library/SearchEngine.cs +++ b/MediaBrowser.Server.Implementations/Library/SearchEngine.cs @@ -155,18 +155,20 @@ namespace MediaBrowser.Server.Implementations.Library AddIfMissing(excludeItemTypes, typeof(MusicArtist).Name); } + AddIfMissing(excludeItemTypes, typeof(CollectionFolder).Name); + var mediaItems = _libraryManager.GetItems(new InternalItemsQuery { NameContains = searchTerm, ExcludeItemTypes = excludeItemTypes.ToArray(), IncludeItemTypes = includeItemTypes.ToArray(), MaxParentalRating = user == null ? null : user.Policy.MaxParentalRating, - Limit = query.Limit.HasValue ? query.Limit * 3 : null + Limit = (query.Limit.HasValue ? (int?)(query.Limit.Value * 3) : null), }).Items; // Add search hints based on item name - hints.AddRange(mediaItems.Where(i => IncludeInSearch(i) && IsVisible(i, user) && !(i is CollectionFolder)).Select(item => + hints.AddRange(mediaItems.Where(i => IncludeInSearch(i) && IsVisible(i, user)).Select(item => { var index = GetIndex(item.Name, searchTerm, terms); diff --git a/MediaBrowser.Server.Implementations/Library/UserManager.cs b/MediaBrowser.Server.Implementations/Library/UserManager.cs index 5012f2479..3c29cf15d 100644 --- a/MediaBrowser.Server.Implementations/Library/UserManager.cs +++ b/MediaBrowser.Server.Implementations/Library/UserManager.cs @@ -30,6 +30,7 @@ using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; +using CommonIO; namespace MediaBrowser.Server.Implementations.Library { @@ -454,7 +455,7 @@ namespace MediaBrowser.Server.Implementations.Library /// <returns>Task.</returns> public Task RefreshUsersMetadata(CancellationToken cancellationToken) { - var tasks = Users.Select(user => user.RefreshMetadata(new MetadataRefreshOptions(), cancellationToken)).ToList(); + var tasks = Users.Select(user => user.RefreshMetadata(new MetadataRefreshOptions(_fileSystem), cancellationToken)).ToList(); return Task.WhenAll(tasks); } @@ -706,7 +707,8 @@ namespace MediaBrowser.Server.Implementations.Library Id = Guid.NewGuid(), DateCreated = DateTime.UtcNow, DateModified = DateTime.UtcNow, - UsesIdForConfigurationPath = true + UsesIdForConfigurationPath = true, + EnableUserViews = true }; } @@ -745,7 +747,7 @@ namespace MediaBrowser.Server.Implementations.Library text.AppendLine(string.Empty); text.AppendLine("The pin code will expire at " + expiration.ToLocalTime().ToShortDateString() + " " + expiration.ToLocalTime().ToShortTimeString()); - File.WriteAllText(path, text.ToString(), Encoding.UTF8); + _fileSystem.WriteAllText(path, text.ToString(), Encoding.UTF8); var result = new PasswordPinCreationResult { @@ -919,7 +921,7 @@ namespace MediaBrowser.Server.Implementations.Library var path = GetPolifyFilePath(user); - Directory.CreateDirectory(Path.GetDirectoryName(path)); + _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); lock (_policySyncLock) { @@ -1006,7 +1008,7 @@ namespace MediaBrowser.Server.Implementations.Library config = _jsonSerializer.DeserializeFromString<UserConfiguration>(json); } - Directory.CreateDirectory(Path.GetDirectoryName(path)); + _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); lock (_configSyncLock) { diff --git a/MediaBrowser.Server.Implementations/Library/UserViewManager.cs b/MediaBrowser.Server.Implementations/Library/UserViewManager.cs index 43f77ec49..c2938475c 100644 --- a/MediaBrowser.Server.Implementations/Library/UserViewManager.cs +++ b/MediaBrowser.Server.Implementations/Library/UserViewManager.cs @@ -27,19 +27,15 @@ namespace MediaBrowser.Server.Implementations.Library private readonly IChannelManager _channelManager; private readonly ILiveTvManager _liveTvManager; - private readonly IPlaylistManager _playlists; - private readonly ICollectionManager _collectionManager; private readonly IServerConfigurationManager _config; - public UserViewManager(ILibraryManager libraryManager, ILocalizationManager localizationManager, IUserManager userManager, IChannelManager channelManager, ILiveTvManager liveTvManager, IPlaylistManager playlists, ICollectionManager collectionManager, IServerConfigurationManager config) + public UserViewManager(ILibraryManager libraryManager, ILocalizationManager localizationManager, IUserManager userManager, IChannelManager channelManager, ILiveTvManager liveTvManager, IServerConfigurationManager config) { _libraryManager = libraryManager; _localizationManager = localizationManager; _userManager = userManager; _channelManager = channelManager; _liveTvManager = liveTvManager; - _playlists = playlists; - _collectionManager = collectionManager; _config = config; } @@ -65,20 +61,26 @@ namespace MediaBrowser.Server.Implementations.Library var list = new List<Folder>(); - if (_config.Configuration.EnableUserSpecificUserViews) + var enableUserViews = _config.Configuration.EnableUserViews || user.EnableUserViews; + + if (enableUserViews) { foreach (var folder in standaloneFolders) { var collectionFolder = folder as ICollectionFolder; var folderViewType = collectionFolder == null ? null : collectionFolder.CollectionType; - if (plainFolderIds.Contains(folder.Id)) + if (UserView.IsUserSpecific(folder)) { - list.Add(await GetUserView(folder.Id, folder.Name, folderViewType, false, string.Empty, user, cancellationToken).ConfigureAwait(false)); + list.Add(await GetUserView(folder.Id, folder.Name, folderViewType, true, string.Empty, user, cancellationToken).ConfigureAwait(false)); + } + else if (plainFolderIds.Contains(folder.Id)) + { + list.Add(await GetUserView(folder, folderViewType, false, string.Empty, cancellationToken).ConfigureAwait(false)); } else if (!string.IsNullOrWhiteSpace(folderViewType)) { - list.Add(await GetUserView(folder.Id, folder.Name, folderViewType, true, string.Empty, user, cancellationToken).ConfigureAwait(false)); + list.Add(await GetUserView(folder, folderViewType, true, string.Empty, cancellationToken).ConfigureAwait(false)); } else { @@ -88,7 +90,29 @@ namespace MediaBrowser.Server.Implementations.Library } else { - list.AddRange(standaloneFolders); + // TODO: Deprecate this whole block + foreach (var folder in standaloneFolders) + { + var collectionFolder = folder as ICollectionFolder; + var folderViewType = collectionFolder == null ? null : collectionFolder.CollectionType; + + if (UserView.IsUserSpecific(folder)) + { + list.Add(await GetUserView(folder.Id, folder.Name, folderViewType, true, string.Empty, user, cancellationToken).ConfigureAwait(false)); + } + else if (plainFolderIds.Contains(folder.Id)) + { + list.Add(await GetUserView(folder.Id, folder.Name, folderViewType, false, string.Empty, user, cancellationToken).ConfigureAwait(false)); + } + else if (!string.IsNullOrWhiteSpace(folderViewType)) + { + list.Add(await GetUserView(folder.Id, folder.Name, folderViewType, true, string.Empty, user, cancellationToken).ConfigureAwait(false)); + } + else + { + list.Add(folder); + } + } } var parents = foldersWithViewTypes.Where(i => string.Equals(i.GetViewType(user), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(i.GetViewType(user))) @@ -96,7 +120,7 @@ namespace MediaBrowser.Server.Implementations.Library if (parents.Count > 0) { - list.Add(await GetUserView(parents, list, CollectionType.TvShows, string.Empty, user, cancellationToken).ConfigureAwait(false)); + list.Add(await GetUserView(parents, list, CollectionType.TvShows, string.Empty, user, enableUserViews, cancellationToken).ConfigureAwait(false)); } parents = foldersWithViewTypes.Where(i => string.Equals(i.GetViewType(user), CollectionType.Music, StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(i.GetViewType(user))) @@ -104,7 +128,7 @@ namespace MediaBrowser.Server.Implementations.Library if (parents.Count > 0) { - list.Add(await GetUserView(parents, list, CollectionType.Music, string.Empty, user, cancellationToken).ConfigureAwait(false)); + list.Add(await GetUserView(parents, list, CollectionType.Music, string.Empty, user, enableUserViews, cancellationToken).ConfigureAwait(false)); } parents = foldersWithViewTypes.Where(i => string.Equals(i.GetViewType(user), CollectionType.Movies, StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(i.GetViewType(user))) @@ -112,7 +136,7 @@ namespace MediaBrowser.Server.Implementations.Library if (parents.Count > 0) { - list.Add(await GetUserView(parents, list, CollectionType.Movies, string.Empty, user, cancellationToken).ConfigureAwait(false)); + list.Add(await GetUserView(parents, list, CollectionType.Movies, string.Empty, user, enableUserViews, cancellationToken).ConfigureAwait(false)); } parents = foldersWithViewTypes.Where(i => string.Equals(i.GetViewType(user), CollectionType.Games, StringComparison.OrdinalIgnoreCase)) @@ -120,23 +144,7 @@ namespace MediaBrowser.Server.Implementations.Library if (parents.Count > 0) { - list.Add(await GetUserView(parents, list, CollectionType.Games, string.Empty, user, cancellationToken).ConfigureAwait(false)); - } - - parents = foldersWithViewTypes.Where(i => string.Equals(i.GetViewType(user), CollectionType.BoxSets, StringComparison.OrdinalIgnoreCase)) - .ToList(); - - if (parents.Count > 0) - { - list.Add(await GetUserView(parents, list, CollectionType.BoxSets, string.Empty, user, cancellationToken).ConfigureAwait(false)); - } - - parents = foldersWithViewTypes.Where(i => string.Equals(i.GetViewType(user), CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) - .ToList(); - - if (parents.Count > 0) - { - list.Add(await GetUserView(parents, list, CollectionType.Playlists, string.Empty, user, cancellationToken).ConfigureAwait(false)); + list.Add(await GetUserView(parents, list, CollectionType.Games, string.Empty, user, enableUserViews, cancellationToken).ConfigureAwait(false)); } if (user.Configuration.DisplayFoldersView) @@ -201,40 +209,18 @@ namespace MediaBrowser.Server.Implementations.Library return GetUserSubView(name, parentId, type, sortName, cancellationToken); } - public async Task<UserView> GetUserView(List<ICollectionFolder> parents, List<Folder> currentViews, string viewType, string sortName, User user, CancellationToken cancellationToken) + private async Task<UserView> GetUserView(List<ICollectionFolder> parents, List<Folder> currentViews, string viewType, string sortName, User user, bool enableUserViews, CancellationToken cancellationToken) { - var name = _localizationManager.GetLocalizedString("ViewType" + viewType); - var enableUserSpecificViews = _config.Configuration.EnableUserSpecificUserViews; - - if (parents.Count == 1 && parents.All(i => string.Equals((enableUserSpecificViews ? i.CollectionType : i.GetViewType(user)), viewType, StringComparison.OrdinalIgnoreCase))) + if (parents.Count == 1 && parents.All(i => string.Equals((enableUserViews ? i.GetViewType(user) : i.CollectionType), viewType, StringComparison.OrdinalIgnoreCase))) { - if (!string.IsNullOrWhiteSpace(parents[0].Name)) - { - name = parents[0].Name; - } - var parentId = parents[0].Id; var enableRichView = !user.Configuration.PlainFolderViews.Contains(parentId.ToString("N"), StringComparer.OrdinalIgnoreCase); - if (!enableRichView || currentViews.OfType<UserView>().Any(i => string.Equals(i.ViewType, viewType, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase))) - { - return await GetUserView(parentId, name, viewType, enableRichView, sortName, user, cancellationToken).ConfigureAwait(false); - } - - if (enableUserSpecificViews) - { - var view = await _libraryManager.GetNamedView(user, name, viewType, sortName, cancellationToken).ConfigureAwait(false); - - if (view.ParentId != parentId) - { - view.ParentId = parentId; - await view.UpdateToRepository(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); - } - return view; - } + return await GetUserView((Folder)parents[0], viewType, enableRichView, string.Empty, cancellationToken).ConfigureAwait(false); } + var name = _localizationManager.GetLocalizedString("ViewType" + viewType); return await _libraryManager.GetNamedView(user, name, viewType, sortName, cancellationToken).ConfigureAwait(false); } @@ -244,6 +230,13 @@ namespace MediaBrowser.Server.Implementations.Library return _libraryManager.GetNamedView(user, name, parentId.ToString("N"), viewType, sortName, null, cancellationToken); } + public Task<UserView> GetUserView(Folder parent, string viewType, bool enableRichView, string sortName, CancellationToken cancellationToken) + { + viewType = enableRichView ? viewType : null; + + return _libraryManager.GetShadowView(parent, viewType, sortName, null, cancellationToken); + } + public List<Tuple<BaseItem, List<BaseItem>>> GetLatestItems(LatestItemsQuery request) { var user = _userManager.GetUserById(request.UserId); diff --git a/MediaBrowser.Server.Implementations/Library/Validators/PeopleValidator.cs b/MediaBrowser.Server.Implementations/Library/Validators/PeopleValidator.cs index a4c43af5d..26cde925e 100644 --- a/MediaBrowser.Server.Implementations/Library/Validators/PeopleValidator.cs +++ b/MediaBrowser.Server.Implementations/Library/Validators/PeopleValidator.cs @@ -11,6 +11,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; +using MediaBrowser.Common.IO; namespace MediaBrowser.Server.Implementations.Library.Validators { @@ -29,17 +31,19 @@ namespace MediaBrowser.Server.Implementations.Library.Validators private readonly ILogger _logger; private readonly IServerConfigurationManager _config; + private readonly IFileSystem _fileSystem; /// <summary> /// Initializes a new instance of the <see cref="PeopleValidator" /> class. /// </summary> /// <param name="libraryManager">The library manager.</param> /// <param name="logger">The logger.</param> - public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IServerConfigurationManager config) + public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem) { _libraryManager = libraryManager; _logger = logger; _config = config; + _fileSystem = fileSystem; } private bool DownloadMetadata(PersonInfo i, PeopleMetadataOptions options) @@ -120,15 +124,19 @@ namespace MediaBrowser.Server.Implementations.Library.Validators var item = _libraryManager.GetPerson(person.Key); validIds.Add(item.Id); - - var options = new MetadataRefreshOptions + + var options = new MetadataRefreshOptions(_fileSystem) { - MetadataRefreshMode = person.Value ? MetadataRefreshMode.Default : MetadataRefreshMode.ValidationOnly, - ImageRefreshMode = person.Value ? ImageRefreshMode.Default : ImageRefreshMode.ValidationOnly + MetadataRefreshMode = person.Value ? MetadataRefreshMode.Default : MetadataRefreshMode.ValidationOnly, + ImageRefreshMode = person.Value ? ImageRefreshMode.Default : ImageRefreshMode.ValidationOnly }; await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false); } + catch (OperationCanceledException) + { + throw; + } catch (Exception ex) { _logger.ErrorException("Error validating IBN entry {0}", ex, person); diff --git a/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs index f205da70d..24d38a63e 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs @@ -39,58 +39,24 @@ namespace MediaBrowser.Server.Implementations.LiveTv var imageResponse = new DynamicImageResponse(); - if (!string.IsNullOrEmpty(liveTvItem.ProviderImagePath)) - { - imageResponse.Path = liveTvItem.ProviderImagePath; - imageResponse.HasImage = true; - } - else if (!string.IsNullOrEmpty(liveTvItem.ProviderImageUrl)) - { - var options = new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = liveTvItem.ProviderImageUrl, - - // Some image hosts require a user agent to be specified. - UserAgent = "Emby Server/" + _appHost.ApplicationVersion - }; + var service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, liveTvItem.ServiceName, StringComparison.OrdinalIgnoreCase)); - var response = await _httpClient.GetResponse(options).ConfigureAwait(false); - - var contentType = response.ContentType; - - if (contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) - { - imageResponse.HasImage = true; - imageResponse.Stream = response.Content; - imageResponse.SetFormatFromMimeType(contentType); - } - else - { - _logger.Error("Provider did not return an image content type."); - } - } - else if (liveTvItem.HasProviderImage ?? true) + if (service != null) { - var service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, liveTvItem.ServiceName, StringComparison.OrdinalIgnoreCase)); - - if (service != null) + try { - try - { - var response = await service.GetChannelImageAsync(liveTvItem.ExternalId, cancellationToken).ConfigureAwait(false); + var response = await service.GetChannelImageAsync(liveTvItem.ExternalId, cancellationToken).ConfigureAwait(false); - if (response != null) - { - imageResponse.HasImage = true; - imageResponse.Stream = response.Stream; - imageResponse.Format = response.Format; - } - } - catch (NotImplementedException) + if (response != null) { + imageResponse.HasImage = true; + imageResponse.Stream = response.Stream; + imageResponse.Format = response.Format; } } + catch (NotImplementedException) + { + } } return imageResponse; diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index b89d8a8d1..2ac06cda8 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -8,7 +8,9 @@ using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.FileOrganization; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Events; @@ -24,6 +26,8 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; +using MediaBrowser.Common.Extensions; namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { @@ -47,10 +51,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV private readonly ILibraryManager _libraryManager; private readonly IProviderManager _providerManager; private readonly IFileOrganizationService _organizationService; + private readonly IMediaEncoder _mediaEncoder; public static EmbyTV Current; - public EmbyTV(IApplicationHost appHost, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IServerConfigurationManager config, ILiveTvManager liveTvManager, IFileSystem fileSystem, ISecurityManager security, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager, IFileOrganizationService organizationService) + public EmbyTV(IApplicationHost appHost, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IServerConfigurationManager config, ILiveTvManager liveTvManager, IFileSystem fileSystem, ISecurityManager security, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager, IFileOrganizationService organizationService, IMediaEncoder mediaEncoder) { Current = this; @@ -64,12 +69,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV _libraryMonitor = libraryMonitor; _providerManager = providerManager; _organizationService = organizationService; + _mediaEncoder = mediaEncoder; _liveTvManager = (LiveTvManager)liveTvManager; _jsonSerializer = jsonSerializer; - _recordingProvider = new ItemDataProvider<RecordingInfo>(jsonSerializer, _logger, Path.Combine(DataPath, "recordings"), (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase)); - _seriesTimerProvider = new SeriesTimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers")); - _timerProvider = new TimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "timers")); + _recordingProvider = new ItemDataProvider<RecordingInfo>(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "recordings"), (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase)); + _seriesTimerProvider = new SeriesTimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers")); + _timerProvider = new TimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "timers")); _timerProvider.TimerFired += _timerProvider_TimerFired; } @@ -126,6 +132,33 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV return status; } + public async Task RefreshSeriesTimers(CancellationToken cancellationToken, IProgress<double> progress) + { + var timers = await GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false); + + List<ChannelInfo> channels = null; + + foreach (var timer in timers) + { + List<ProgramInfo> epgData; + + if (timer.RecordAnyChannel) + { + if (channels == null) + { + channels = (await GetChannelsAsync(true, CancellationToken.None).ConfigureAwait(false)).ToList(); + } + var channelIds = channels.Select(i => i.Id).ToList(); + epgData = GetEpgDataForChannels(channelIds); + } + else + { + epgData = GetEpgDataForChannel(timer.ChannelId); + } + await UpdateTimersForSeriesTimer(epgData, timer).ConfigureAwait(false); + } + } + private List<ChannelInfo> _channelCache = null; private async Task<IEnumerable<ChannelInfo>> GetChannelsAsync(bool enableCache, CancellationToken cancellationToken) { @@ -235,7 +268,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV try { - File.Delete(remove.Path); + _fileSystem.DeleteFile(remove.Path); } catch (DirectoryNotFoundException) { @@ -247,6 +280,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } _recordingProvider.Delete(remove); } + else + { + throw new ResourceNotFoundException("Recording not found: " + recordingId); + } } public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken) @@ -327,9 +364,31 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV throw new NotImplementedException(); } - public Task<IEnumerable<RecordingInfo>> GetRecordingsAsync(CancellationToken cancellationToken) + public async Task<IEnumerable<RecordingInfo>> GetRecordingsAsync(CancellationToken cancellationToken) { - return Task.FromResult((IEnumerable<RecordingInfo>)_recordingProvider.GetAll()); + var recordings = _recordingProvider.GetAll().ToList(); + var updated = false; + + foreach (var recording in recordings) + { + if (recording.Status == RecordingStatus.InProgress) + { + if (string.IsNullOrWhiteSpace(recording.TimerId) || !_activeRecordings.ContainsKey(recording.TimerId)) + { + recording.Status = RecordingStatus.Cancelled; + recording.DateLastUpdated = DateTime.UtcNow; + _recordingProvider.Update(recording); + updated = true; + } + } + } + + if (updated) + { + recordings = _recordingProvider.GetAll().ToList(); + } + + return recordings; } public Task<IEnumerable<TimerInfo>> GetTimersAsync(CancellationToken cancellationToken) @@ -364,39 +423,16 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV return Task.FromResult((IEnumerable<SeriesTimerInfo>)_seriesTimerProvider.GetAll()); } - public async Task RefreshSeriesTimers(CancellationToken cancellationToken, IProgress<double> progress) - { - var timers = await GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false); - - List<ChannelInfo> channels = null; - - foreach (var timer in timers) - { - List<ProgramInfo> epgData; - - if (timer.RecordAnyChannel) - { - if (channels == null) - { - channels = (await GetChannelsAsync(true, CancellationToken.None).ConfigureAwait(false)).ToList(); - } - var channelIds = channels.Select(i => i.Id).ToList(); - epgData = GetEpgDataForChannels(channelIds); - } - else - { - epgData = GetEpgDataForChannel(timer.ChannelId); - } - await UpdateTimersForSeriesTimer(epgData, timer).ConfigureAwait(false); - } - } - public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) { try { return await GetProgramsAsyncInternal(channelId, startDateUtc, endDateUtc, cancellationToken).ConfigureAwait(false); } + catch (OperationCanceledException) + { + throw; + } catch (Exception ex) { _logger.ErrorException("Error getting programs", ex); @@ -411,7 +447,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV foreach (var provider in GetListingProviders()) { - var programs = await provider.Item1.GetProgramsAsync(provider.Item2, channel.Number, startDateUtc, endDateUtc, cancellationToken) + var programs = await provider.Item1.GetProgramsAsync(provider.Item2, channel.Number, channel.Name, startDateUtc, endDateUtc, cancellationToken) .ConfigureAwait(false); var list = programs.ToList(); @@ -457,20 +493,36 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV foreach (var hostInstance in _liveTvManager.TunerHosts) { - MediaSourceInfo mediaSourceInfo = null; try { - mediaSourceInfo = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false); + var result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false); + + result.Item2.Release(); + + return result.Item1; } catch (Exception e) { _logger.ErrorException("Error getting channel stream", e); } + } + + throw new ApplicationException("Tuner not found."); + } + + private async Task<Tuple<MediaSourceInfo, SemaphoreSlim>> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken) + { + _logger.Info("Streaming Channel " + channelId); - if (mediaSourceInfo != null) + foreach (var hostInstance in _liveTvManager.TunerHosts) + { + try { - mediaSourceInfo.Id = Guid.NewGuid().ToString("N"); - return mediaSourceInfo; + return await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false); + } + catch (Exception e) + { + _logger.ErrorException("Error getting channel stream", e); } } @@ -527,11 +579,19 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV try { + var recordingEndDate = timer.EndDate.AddSeconds(timer.PostPaddingSeconds); + + if (recordingEndDate <= DateTime.UtcNow) + { + _logger.Warn("Recording timer fired for timer {0}, Id: {1}, but the program has already ended.", timer.Name, timer.Id); + return; + } + var cancellationTokenSource = new CancellationTokenSource(); if (_activeRecordings.TryAdd(timer.Id, cancellationTokenSource)) { - await RecordStream(timer, cancellationTokenSource.Token).ConfigureAwait(false); + await RecordStream(timer, recordingEndDate, cancellationTokenSource.Token).ConfigureAwait(false); } } catch (OperationCanceledException) @@ -544,58 +604,62 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } } - private async Task RecordStream(TimerInfo timer, CancellationToken cancellationToken) + private async Task RecordStream(TimerInfo timer, DateTime recordingEndDate, CancellationToken cancellationToken) { if (timer == null) { throw new ArgumentNullException("timer"); } - var mediaStreamInfo = await GetChannelStream(timer.ChannelId, null, CancellationToken.None); - var duration = (timer.EndDate - DateTime.UtcNow).Add(TimeSpan.FromSeconds(timer.PostPaddingSeconds)); - - HttpRequestOptions httpRequestOptions = new HttpRequestOptions() + if (string.IsNullOrWhiteSpace(timer.ProgramId)) { - Url = mediaStreamInfo.Path - }; + throw new InvalidOperationException("timer.ProgramId is null. Cannot record."); + } var info = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId); + + if (info == null) + { + throw new InvalidOperationException(string.Format("Program with Id {0} not found", timer.ProgramId)); + } + var recordPath = RecordingPath; if (info.IsMovie) { - recordPath = Path.Combine(recordPath, "Movies", _fileSystem.GetValidFilename(info.Name)); + recordPath = Path.Combine(recordPath, "Movies", _fileSystem.GetValidFilename(info.Name).Trim()); } else if (info.IsSeries) { - recordPath = Path.Combine(recordPath, "Series", _fileSystem.GetValidFilename(info.Name)); + recordPath = Path.Combine(recordPath, "Series", _fileSystem.GetValidFilename(info.Name).Trim()); } else if (info.IsKids) { - recordPath = Path.Combine(recordPath, "Kids", _fileSystem.GetValidFilename(info.Name)); + recordPath = Path.Combine(recordPath, "Kids", _fileSystem.GetValidFilename(info.Name).Trim()); } else if (info.IsSports) { - recordPath = Path.Combine(recordPath, "Sports", _fileSystem.GetValidFilename(info.Name)); + recordPath = Path.Combine(recordPath, "Sports", _fileSystem.GetValidFilename(info.Name).Trim()); } else { - recordPath = Path.Combine(recordPath, "Other", _fileSystem.GetValidFilename(info.Name)); + recordPath = Path.Combine(recordPath, "Other", _fileSystem.GetValidFilename(info.Name).Trim()); } - var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer, info)) + ".ts"; + var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer, info)).Trim() + ".ts"; recordPath = Path.Combine(recordPath, recordingFileName); - Directory.CreateDirectory(Path.GetDirectoryName(recordPath)); + _fileSystem.CreateDirectory(Path.GetDirectoryName(recordPath)); - var recording = _recordingProvider.GetAll().FirstOrDefault(x => string.Equals(x.ProgramId, info.Id, StringComparison.OrdinalIgnoreCase)); + var recordingId = info.Id.GetMD5().ToString("N"); + var recording = _recordingProvider.GetAll().FirstOrDefault(x => string.Equals(x.Id, recordingId, StringComparison.OrdinalIgnoreCase)); if (recording == null) { recording = new RecordingInfo { ChannelId = info.ChannelId, - Id = Guid.NewGuid().ToString("N"), + Id = recordingId, StartDate = info.StartDate, EndDate = info.EndDate, Genres = info.Genres, @@ -611,7 +675,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV Name = info.Name, EpisodeTitle = info.EpisodeTitle, ProgramId = info.Id, - HasImage = info.HasImage, ImagePath = info.ImagePath, ImageUrl = info.ImageUrl, OriginalAirDate = info.OriginalAirDate, @@ -624,30 +687,58 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV _recordingProvider.Add(recording); } - recording.Path = recordPath; - recording.Status = RecordingStatus.InProgress; - recording.DateLastUpdated = DateTime.UtcNow; - _recordingProvider.Update(recording); - - _logger.Info("Beginning recording."); - try { - httpRequestOptions.BufferContent = false; - var durationToken = new CancellationTokenSource(duration); - var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; - httpRequestOptions.CancellationToken = linkedToken; - _logger.Info("Writing file to path: " + recordPath); - using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET")) + var result = await GetChannelStreamInternal(timer.ChannelId, null, CancellationToken.None); + var mediaStreamInfo = result.Item1; + var isResourceOpen = true; + + // Unfortunately due to the semaphore we have to have a nested try/finally + try + { + // HDHR doesn't seem to release the tuner right away after first probing with ffmpeg + await Task.Delay(3000, cancellationToken).ConfigureAwait(false); + + var duration = recordingEndDate - DateTime.UtcNow; + + HttpRequestOptions httpRequestOptions = new HttpRequestOptions() + { + Url = mediaStreamInfo.Path + }; + + recording.Path = recordPath; + recording.Status = RecordingStatus.InProgress; + recording.DateLastUpdated = DateTime.UtcNow; + _recordingProvider.Update(recording); + + _logger.Info("Beginning recording."); + + httpRequestOptions.BufferContent = false; + var durationToken = new CancellationTokenSource(duration); + var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; + httpRequestOptions.CancellationToken = linkedToken; + _logger.Info("Writing file to path: " + recordPath); + using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET")) + { + using (var output = _fileSystem.GetFileStream(recordPath, FileMode.Create, FileAccess.Write, FileShare.Read)) + { + result.Item2.Release(); + isResourceOpen = false; + + await response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, linkedToken); + } + } + + recording.Status = RecordingStatus.Completed; + _logger.Info("Recording completed"); + } + finally { - using (var output = File.Open(recordPath, FileMode.Create, FileAccess.Write, FileShare.Read)) + if (isResourceOpen) { - await response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, linkedToken); + result.Item2.Release(); } } - - recording.Status = RecordingStatus.Completed; - _logger.Info("Recording completed"); } catch (OperationCanceledException) { @@ -794,14 +885,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV private string GetChannelEpgCachePath(string channelId) { - return Path.Combine(DataPath, "epg", channelId + ".json"); + return Path.Combine(_config.CommonApplicationPaths.CachePath, "embytvepg", channelId + ".json"); } private readonly object _epgLock = new object(); private void SaveEpgDataForChannel(string channelId, List<ProgramInfo> epgData) { var path = GetChannelEpgCachePath(channelId); - Directory.CreateDirectory(Path.GetDirectoryName(path)); + _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); lock (_epgLock) { _jsonSerializer.SerializeToFile(epgData, path); @@ -848,4 +939,4 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV }); } } -} +}
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs index 75dec5f97..d89c1c0cb 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using CommonIO; +using MediaBrowser.Common.IO; namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { @@ -16,13 +18,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV protected readonly ILogger Logger; private readonly string _dataPath; protected readonly Func<T, T, bool> EqualityComparer; + private readonly IFileSystem _fileSystem; - public ItemDataProvider(IJsonSerializer jsonSerializer, ILogger logger, string dataPath, Func<T, T, bool> equalityComparer) + public ItemDataProvider(IFileSystem fileSystem, IJsonSerializer jsonSerializer, ILogger logger, string dataPath, Func<T, T, bool> equalityComparer) { Logger = logger; _dataPath = dataPath; EqualityComparer = equalityComparer; _jsonSerializer = jsonSerializer; + _fileSystem = fileSystem; } public IReadOnlyList<T> GetAll() @@ -69,7 +73,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV private void UpdateList(List<T> newList) { var file = _dataPath + ".json"; - Directory.CreateDirectory(Path.GetDirectoryName(file)); + _fileSystem.CreateDirectory(Path.GetDirectoryName(file)); lock (_fileDataLock) { diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs index 5b83d63b1..3ee808bb5 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs @@ -67,10 +67,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } } - else if (info.ProductionYear != null) + else if (info.IsMovie && info.ProductionYear != null) { name += " (" + info.ProductionYear + ")"; } + else + { + name += " " + info.StartDate.ToString("yyyy-MM-dd"); + } return name; } diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs index eab278eb4..6d88c7c0a 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs @@ -2,13 +2,15 @@ using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; using System; +using CommonIO; +using MediaBrowser.Common.IO; namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { public class SeriesTimerManager : ItemDataProvider<SeriesTimerInfo> { - public SeriesTimerManager(IJsonSerializer jsonSerializer, ILogger logger, string dataPath) - : base(jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase)) + public SeriesTimerManager(IFileSystem fileSystem, IJsonSerializer jsonSerializer, ILogger logger, string dataPath) + : base(fileSystem, jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase)) { } diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs index 3ae38f382..94ad5e5ce 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs @@ -7,6 +7,8 @@ using System; using System.Collections.Concurrent; using System.Linq; using System.Threading; +using CommonIO; +using MediaBrowser.Common.IO; namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { @@ -16,8 +18,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV public event EventHandler<GenericEventArgs<TimerInfo>> TimerFired; - public TimerManager(IJsonSerializer jsonSerializer, ILogger logger, string dataPath) - : base(jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase)) + public TimerManager(IFileSystem fileSystem, IJsonSerializer jsonSerializer, ILogger logger, string dataPath) + : base(fileSystem, jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase)) { } diff --git a/MediaBrowser.Server.Implementations/LiveTv/Listings/Emby/EmbyListings.cs b/MediaBrowser.Server.Implementations/LiveTv/Listings/Emby/EmbyListings.cs index e446ff469..ae441b44e 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/Listings/Emby/EmbyListings.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/Listings/Emby/EmbyListings.cs @@ -31,7 +31,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings.Emby get { return "emby"; } } - public Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelNumber, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) + public Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelNumber, string channelName, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) { return GetListingsProvider(info.Country).GetProgramsAsync(info, channelNumber, startDateUtc, endDateUtc, cancellationToken); } diff --git a/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 2efa91137..434578718 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -60,7 +60,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings return dates; } - public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelNumber, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) + public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelNumber, string channelName, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) { List<ProgramInfo> programsInfo = new List<ProgramInfo>(); @@ -82,19 +82,22 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings UserAgent = UserAgent, CancellationToken = cancellationToken, // The data can be large so give it some extra time - TimeoutMs = 60000 + TimeoutMs = 60000, + LogErrorResponseBody = true }; httpOptions.RequestHeaders["token"] = token; var dates = GetScheduleRequestDates(startDateUtc, endDateUtc); - ScheduleDirect.Station station = null; + ScheduleDirect.Station station = GetStation(channelNumber, channelName); - if (!_channelPair.TryGetValue(channelNumber, out station)) + if (station == null) { + _logger.Info("No Schedules Direct Station found for channel {0} with name {1}", channelNumber, channelName); return programsInfo; } + string stationID = station.stationID; _logger.Info("Channel Station ID is: " + stationID); @@ -122,7 +125,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings { Url = ApiUrl + "/programs", UserAgent = UserAgent, - CancellationToken = cancellationToken + CancellationToken = cancellationToken, + LogErrorResponseBody = true }; httpOptions.RequestHeaders["token"] = token; @@ -152,10 +156,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings // schedule.programID + " which says it has images? " + // programDict[schedule.programID].hasImageArtwork); - var imageIndex = images.FindIndex(i => i.programID == schedule.programID.Substring(0, 10)); - if (imageIndex > -1) + if (images != null) { - programDict[schedule.programID].images = GetProgramLogo(ApiUrl, images[imageIndex]); + var imageIndex = images.FindIndex(i => i.programID == schedule.programID.Substring(0, 10)); + if (imageIndex > -1) + { + programDict[schedule.programID].images = GetProgramLogo(ApiUrl, images[imageIndex]); + } } programsInfo.Add(GetProgram(channelNumber, schedule, programDict[schedule.programID])); @@ -167,6 +174,30 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings return programsInfo; } + private ScheduleDirect.Station GetStation(string channelNumber, string channelName) + { + ScheduleDirect.Station station; + + if (_channelPair.TryGetValue(channelNumber, out station)) + { + return station; + } + + if (string.IsNullOrWhiteSpace(channelName)) + { + return null; + } + + channelName = NormalizeName(channelName); + + return _channelPair.Values.FirstOrDefault(i => string.Equals(NormalizeName(i.callsign ?? string.Empty), channelName, StringComparison.OrdinalIgnoreCase)); + } + + private string NormalizeName(string value) + { + return value.Replace(" ", string.Empty).Replace("-", string.Empty); + } + public async Task AddMetadata(ListingsProviderInfo info, List<ChannelInfo> channels, CancellationToken cancellationToken) { @@ -188,7 +219,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings { Url = ApiUrl + "/lineups/" + info.ListingsId, UserAgent = UserAgent, - CancellationToken = cancellationToken + CancellationToken = cancellationToken, + LogErrorResponseBody = true }; httpOptions.RequestHeaders["token"] = token; @@ -200,34 +232,42 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings _logger.Info("Mapping Stations to Channel"); foreach (ScheduleDirect.Map map in root.map) { - var channel = (map.channel ?? (map.atscMajor + "." + map.atscMinor)).TrimStart('0'); - _logger.Debug("Found channel: " + channel + " in Schedules Direct"); - var schChannel = root.stations.FirstOrDefault(item => item.stationID == map.stationID); + var channelNumber = map.logicalChannelNumber; - if (!_channelPair.ContainsKey(channel) && channel != "0.0" && schChannel != null) + if (string.IsNullOrWhiteSpace(channelNumber)) + { + channelNumber = map.channel; + } + if (string.IsNullOrWhiteSpace(channelNumber)) { - _channelPair.TryAdd(channel, schChannel); + channelNumber = (map.atscMajor + "." + map.atscMinor); } + channelNumber = channelNumber.TrimStart('0'); + + _logger.Debug("Found channel: " + channelNumber + " in Schedules Direct"); + var schChannel = root.stations.FirstOrDefault(item => item.stationID == map.stationID); + + _channelPair.TryAdd(channelNumber, schChannel); } - _logger.Info("Added " + _channelPair.Count() + " channels to the dictionary"); + _logger.Info("Added " + _channelPair.Count + " channels to the dictionary"); foreach (ChannelInfo channel in channels) { - // Helper.logger.Info("Modifyin channel " + channel.Number); - if (_channelPair.ContainsKey(channel.Number)) + var station = GetStation(channel.Number, channel.Name); + + if (station != null) { - if (_channelPair[channel.Number].logo != null) + if (station.logo != null) { - channel.ImageUrl = _channelPair[channel.Number].logo.URL; + channel.ImageUrl = station.logo.URL; channel.HasImage = true; } - string channelName = _channelPair[channel.Number].name; + string channelName = station.name; channel.Name = channelName; } else { - _logger.Info("Schedules Direct doesnt have data for channel: " + channel.Number + " " + - channel.Name); + _logger.Info("Schedules Direct doesnt have data for channel: " + channel.Number + " " + channel.Name); } } } @@ -293,7 +333,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings IsRepeat = repeat, IsSeries = showType.IndexOf("series", StringComparison.OrdinalIgnoreCase) != -1, ImageUrl = imageUrl, - HasImage = details.hasImageArtwork, IsKids = string.Equals(details.audience, "children", StringComparison.OrdinalIgnoreCase), IsSports = showType.IndexOf("sports", StringComparison.OrdinalIgnoreCase) != -1, IsMovie = showType.IndexOf("movie", StringComparison.OrdinalIgnoreCase) != -1 || showType.IndexOf("film", StringComparison.OrdinalIgnoreCase) != -1, @@ -362,13 +401,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings private DateTime GetDate(string value) { - return DateTime.ParseExact(value, "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'", - CultureInfo.InvariantCulture); + var date = DateTime.ParseExact(value, "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'", CultureInfo.InvariantCulture); + + if (date.Kind != DateTimeKind.Utc) + { + date = DateTime.SpecifyKind(date, DateTimeKind.Utc); + } + return date; } private string GetProgramLogo(string apiUrl, ScheduleDirect.ShowImages images) { - string url = ""; + string url = null; if (images.data != null) { var smallImages = images.data.Where(i => i.size == "Sm").ToList(); @@ -381,13 +425,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings { logoIndex = 0; } - if (images.data[logoIndex].uri.Contains("http")) - { - url = images.data[logoIndex].uri; - } - else + var uri = images.data[logoIndex].uri; + + if (!string.IsNullOrWhiteSpace(uri)) { - url = apiUrl + "/image/" + images.data[logoIndex].uri; + if (uri.IndexOf("http", StringComparison.OrdinalIgnoreCase) != -1) + { + url = uri; + } + else + { + url = apiUrl + "/image/" + uri; + } } //_logger.Debug("URL for image is : " + url); } @@ -413,7 +462,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings Url = ApiUrl + "/metadata/programs", UserAgent = UserAgent, CancellationToken = cancellationToken, - RequestContent = imageIdString + RequestContent = imageIdString, + LogErrorResponseBody = true }; List<ScheduleDirect.ShowImages> images; using (var innerResponse2 = await _httpClient.Post(httpOptions)) @@ -440,7 +490,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings { Url = ApiUrl + "/headends?country=" + country + "&postalcode=" + location, UserAgent = UserAgent, - CancellationToken = cancellationToken + CancellationToken = cancellationToken, + LogErrorResponseBody = true }; options.RequestHeaders["token"] = token; @@ -557,7 +608,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings Url = ApiUrl + "/token", UserAgent = UserAgent, RequestContent = "{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}", - CancellationToken = cancellationToken + CancellationToken = cancellationToken, + LogErrorResponseBody = true }; //_logger.Info("Obtaining token from Schedules Direct from addres: " + httpOptions.Url + " with body " + // httpOptions.RequestContent); @@ -595,7 +647,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings { Url = ApiUrl + "/lineups/" + info.ListingsId, UserAgent = UserAgent, - CancellationToken = cancellationToken + CancellationToken = cancellationToken, + LogErrorResponseBody = true }; httpOptions.RequestHeaders["token"] = token; @@ -635,7 +688,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings { Url = ApiUrl + "/lineups", UserAgent = UserAgent, - CancellationToken = cancellationToken + CancellationToken = cancellationToken, + LogErrorResponseBody = true }; options.RequestHeaders["token"] = token; @@ -736,6 +790,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings { public string stationID { get; set; } public string channel { get; set; } + public string logicalChannelNumber { get; set; } public int uhfVhf { get; set; } public int atscMajor { get; set; } public int atscMinor { get; set; } diff --git a/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTv.cs b/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTv.cs index de107ced8..ac316f9a1 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTv.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTv.cs @@ -20,7 +20,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings get { return "xmltv"; } } - public Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelNumber, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) + public Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelNumber, string channelName, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs index 7a26bf87c..edfca0d6c 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs @@ -29,6 +29,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; namespace MediaBrowser.Server.Implementations.LiveTv { @@ -553,8 +554,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv isNew = true; } - item.ChannelType = channelInfo.ChannelType; + if (!string.Equals(channelInfo.Id, item.ExternalId)) + { + isNew = true; + } item.ExternalId = channelInfo.Id; + + item.ChannelType = channelInfo.ChannelType; item.ServiceName = serviceName; item.Number = channelInfo.Number; @@ -571,16 +577,21 @@ namespace MediaBrowser.Server.Implementations.LiveTv // replaceImages.Add(ImageType.Primary); //} - item.ProviderImageUrl = channelInfo.ImageUrl; - item.HasProviderImage = channelInfo.HasImage; - item.ProviderImagePath = channelInfo.ImagePath; + if (!string.IsNullOrWhiteSpace(channelInfo.ImagePath)) + { + item.SetImagePath(ImageType.Primary, channelInfo.ImagePath); + } + else if (!string.IsNullOrWhiteSpace(channelInfo.ImageUrl)) + { + item.SetImagePath(ImageType.Primary, channelInfo.ImageUrl); + } if (string.IsNullOrEmpty(item.Name)) { item.Name = channelInfo.Name; } - await item.RefreshMetadata(new MetadataRefreshOptions + await item.RefreshMetadata(new MetadataRefreshOptions(_fileSystem) { ForceSave = isNew, ReplaceImages = replaceImages.Distinct().ToList() @@ -606,7 +617,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv Id = id, DateCreated = DateTime.UtcNow, DateModified = DateTime.UtcNow, - Etag = info.Etag + ExternalEtag = info.Etag }; } @@ -620,7 +631,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv item.EpisodeTitle = info.EpisodeTitle; item.ExternalId = info.Id; item.Genres = info.Genres; - item.HasProviderImage = info.HasImage; item.IsHD = info.IsHD; item.IsKids = info.IsKids; item.IsLive = info.IsLive; @@ -633,33 +643,46 @@ namespace MediaBrowser.Server.Implementations.LiveTv item.Name = info.Name; item.OfficialRating = item.OfficialRating ?? info.OfficialRating; item.Overview = item.Overview ?? info.Overview; - item.OriginalAirDate = info.OriginalAirDate; - item.ProviderImagePath = info.ImagePath; - item.ProviderImageUrl = info.ImageUrl; item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks; item.StartDate = info.StartDate; item.HomePageUrl = info.HomePageUrl; item.ProductionYear = info.ProductionYear; - item.PremiereDate = item.PremiereDate ?? info.OriginalAirDate; + item.PremiereDate = info.OriginalAirDate; item.IndexNumber = info.EpisodeNumber; item.ParentIndexNumber = info.SeasonNumber; + if (!string.IsNullOrWhiteSpace(info.ImagePath)) + { + item.SetImagePath(ImageType.Primary, info.ImagePath); + } + else if (!string.IsNullOrWhiteSpace(info.ImageUrl)) + { + item.SetImagePath(ImageType.Primary, info.ImageUrl); + } + if (isNew) { await _libraryManager.CreateItem(item, cancellationToken).ConfigureAwait(false); } + else if (string.IsNullOrWhiteSpace(info.Etag)) + { + await _libraryManager.UpdateItem(item, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); + } else { - if (string.IsNullOrWhiteSpace(info.Etag) || !string.Equals(info.Etag, item.Etag, StringComparison.OrdinalIgnoreCase)) + // Increment this whenver some internal change deems it necessary + var etag = info.Etag + "4"; + + if (!string.Equals(etag, item.ExternalEtag, StringComparison.OrdinalIgnoreCase)) { - item.Etag = info.Etag; + item.ExternalEtag = etag; await _libraryManager.UpdateItem(item, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); } } - _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions()); + _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem)); return item; } @@ -705,18 +728,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv item.Overview = info.Overview; item.EndDate = info.EndDate; item.Genres = info.Genres; + item.PremiereDate = info.OriginalAirDate; var recording = (ILiveTvRecording)item; recording.ExternalId = info.Id; - recording.ProgramId = _tvDtoService.GetInternalProgramId(serviceName, info.ProgramId).ToString("N"); recording.Audio = info.Audio; - recording.ChannelType = info.ChannelType; recording.EndDate = info.EndDate; recording.EpisodeTitle = info.EpisodeTitle; - recording.ProviderImagePath = info.ImagePath; - recording.ProviderImageUrl = info.ImageUrl; recording.IsHD = info.IsHD; recording.IsKids = info.IsKids; recording.IsLive = info.IsLive; @@ -726,40 +746,57 @@ namespace MediaBrowser.Server.Implementations.LiveTv recording.IsRepeat = info.IsRepeat; recording.IsSeries = info.IsSeries; recording.IsSports = info.IsSports; - recording.OriginalAirDate = info.OriginalAirDate; recording.SeriesTimerId = info.SeriesTimerId; recording.StartDate = info.StartDate; + + if (!string.IsNullOrWhiteSpace(info.ImagePath)) + { + item.SetImagePath(ImageType.Primary, info.ImagePath); + } + else if (!string.IsNullOrWhiteSpace(info.ImageUrl)) + { + item.SetImagePath(ImageType.Primary, info.ImageUrl); + } + + var statusChanged = info.Status != recording.Status; + recording.Status = info.Status; recording.ServiceName = serviceName; - var originalPath = item.Path; + var pathChanged = false; if (!string.IsNullOrEmpty(info.Path)) { - item.Path = info.Path; - var fileInfo = new FileInfo(info.Path); + pathChanged = !string.Equals(item.Path, info.Path); + var fileInfo = _fileSystem.GetFileInfo(info.Path); recording.DateCreated = _fileSystem.GetCreationTimeUtc(fileInfo); recording.DateModified = _fileSystem.GetLastWriteTimeUtc(fileInfo); + item.Path = info.Path; } else if (!string.IsNullOrEmpty(info.Url)) { + pathChanged = !string.Equals(item.Path, info.Url); item.Path = info.Url; } - var pathChanged = !string.Equals(originalPath, item.Path); + var metadataRefreshMode = MetadataRefreshMode.Default; if (isNew) { await _libraryManager.CreateItem(item, cancellationToken).ConfigureAwait(false); } - else if (pathChanged || info.DateLastUpdated > recording.DateLastSaved) + else if (pathChanged || info.DateLastUpdated > recording.DateLastSaved || statusChanged) { + metadataRefreshMode = MetadataRefreshMode.FullRefresh; await _libraryManager.UpdateItem(item, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); } - _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions()); + _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem) + { + MetadataRefreshMode = metadataRefreshMode + }); return item.Id; } @@ -1163,6 +1200,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv foreach (var program in channelPrograms) { var programItem = await GetProgram(program, channelId, currentChannel.ChannelType, service.Name, cancellationToken).ConfigureAwait(false); + programs.Add(programItem.Id); } } @@ -1200,6 +1238,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv { cancellationToken.ThrowIfCancellationRequested(); + if (itemId == Guid.Empty) + { + // Somehow some invalid data got into the db. It probably predates the boundary checking + continue; + } + if (!currentIdList.Contains(itemId)) { var item = _libraryManager.GetItemById(itemId); @@ -1286,7 +1330,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv var idList = await Task.WhenAll(recordingTasks).ConfigureAwait(false); - CleanDatabaseInternal(idList.ToList(), new[] { typeof(LiveTvVideoRecording).Name, typeof(LiveTvAudioRecording).Name }, new Progress<double>(), cancellationToken).ConfigureAwait(false); + await CleanDatabaseInternal(idList.ToList(), new[] { typeof(LiveTvVideoRecording).Name, typeof(LiveTvAudioRecording).Name }, new Progress<double>(), cancellationToken).ConfigureAwait(false); _lastRecordingRefreshTime = DateTime.UtcNow; } @@ -1382,25 +1426,25 @@ namespace MediaBrowser.Server.Implementations.LiveTv }; } - public void AddInfoToProgramDto(BaseItem item, BaseItemDto dto, User user = null) + public void AddInfoToProgramDto(BaseItem item, BaseItemDto dto, bool addChannelInfo, User user = null) { var program = (LiveTvProgram)item; var service = GetService(program); - var channel = GetInternalChannel(program.ChannelId); - dto.Id = _tvDtoService.GetInternalProgramId(service.Name, program.ExternalId).ToString("N"); dto.StartDate = program.StartDate; - dto.IsRepeat = program.IsRepeat; dto.EpisodeTitle = program.EpisodeTitle; - dto.ChannelType = program.ChannelType; dto.Audio = program.Audio; if (program.IsHD.HasValue && program.IsHD.Value) { dto.IsHD = program.IsHD; } + if (program.IsRepeat) + { + dto.IsRepeat = program.IsRepeat; + } if (program.IsMovie) { dto.IsMovie = program.IsMovie; @@ -1430,15 +1474,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv dto.IsPremiere = program.IsPremiere; } - dto.OriginalAirDate = program.OriginalAirDate; - - if (channel != null) + if (addChannelInfo) { - dto.ChannelName = channel.Name; + var channel = GetInternalChannel(program.ChannelId); - if (!string.IsNullOrEmpty(channel.PrimaryImagePath)) + if (channel != null) { - dto.ChannelPrimaryImageTag = _tvDtoService.GetImageTag(channel); + dto.ChannelName = channel.Name; + + if (channel.HasImage(ImageType.Primary)) + { + dto.ChannelPrimaryImageTag = _tvDtoService.GetImageTag(channel); + } } } } @@ -1461,7 +1508,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv dto.RecordingStatus = info.Status; dto.IsRepeat = info.IsRepeat; dto.EpisodeTitle = info.EpisodeTitle; - dto.ChannelType = info.ChannelType; dto.Audio = info.Audio; dto.IsHD = info.IsHD; dto.IsMovie = info.IsMovie; @@ -1471,7 +1517,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv dto.IsNews = info.IsNews; dto.IsKids = info.IsKids; dto.IsPremiere = info.IsPremiere; - dto.OriginalAirDate = info.OriginalAirDate; dto.CanDelete = user == null ? recording.CanDelete() @@ -1499,13 +1544,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv dto.CompletionPercentage = pct; } - dto.ProgramId = info.ProgramId; - if (channel != null) { dto.ChannelName = channel.Name; - if (!string.IsNullOrEmpty(channel.PrimaryImagePath)) + if (channel.HasImage(ImageType.Primary)) { dto.ChannelPrimaryImageTag = _tvDtoService.GetImageTag(channel); } @@ -1601,7 +1644,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv var service = GetService(recording.ServiceName); - await service.DeleteRecordingAsync(recording.ExternalId, CancellationToken.None).ConfigureAwait(false); + try + { + await service.DeleteRecordingAsync(recording.ExternalId, CancellationToken.None).ConfigureAwait(false); + } + catch (ResourceNotFoundException) + { + + } + + await _libraryManager.DeleteItem((BaseItem)recording).ConfigureAwait(false); + _lastRecordingRefreshTime = DateTime.MinValue; } @@ -1787,7 +1840,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv EndDate = program.EndDate ?? DateTime.MinValue, EpisodeTitle = program.EpisodeTitle, Genres = program.Genres, - HasImage = program.HasProviderImage, Id = program.ExternalId, IsHD = program.IsHD, IsKids = program.IsKids, @@ -1801,8 +1853,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv OriginalAirDate = program.PremiereDate, Overview = program.Overview, StartDate = program.StartDate, - ImagePath = program.ProviderImagePath, - ImageUrl = program.ProviderImageUrl, + //ImagePath = program.ExternalImagePath, Name = program.Name, OfficialRating = program.OfficialRating }; @@ -2352,4 +2403,4 @@ namespace MediaBrowser.Server.Implementations.LiveTv }); } } -} +}
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs index 134e24ef0..ab8ec720b 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs @@ -1,9 +1,7 @@ -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; using System.Linq; @@ -15,14 +13,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv public class ProgramImageProvider : IDynamicImageProvider, IHasItemChangeMonitor, IHasOrder { private readonly ILiveTvManager _liveTvManager; - private readonly IHttpClient _httpClient; - private readonly ILogger _logger; - public ProgramImageProvider(ILiveTvManager liveTvManager, IHttpClient httpClient, ILogger logger) + public ProgramImageProvider(ILiveTvManager liveTvManager) { _liveTvManager = liveTvManager; - _httpClient = httpClient; - _logger = logger; } public IEnumerable<ImageType> GetSupportedImages(IHasImages item) @@ -36,55 +30,26 @@ namespace MediaBrowser.Server.Implementations.LiveTv var imageResponse = new DynamicImageResponse(); - if (!string.IsNullOrEmpty(liveTvItem.ProviderImagePath)) - { - imageResponse.Path = liveTvItem.ProviderImagePath; - imageResponse.HasImage = true; - } - else if (!string.IsNullOrEmpty(liveTvItem.ProviderImageUrl)) - { - var options = new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = liveTvItem.ProviderImageUrl - }; + var service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, liveTvItem.ServiceName, StringComparison.OrdinalIgnoreCase)); - var response = await _httpClient.GetResponse(options).ConfigureAwait(false); - - if (response.ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) - { - imageResponse.HasImage = true; - imageResponse.Stream = response.Content; - imageResponse.SetFormatFromMimeType(response.ContentType); - } - else - { - _logger.Error("Provider did not return an image content type."); - } - } - else if (liveTvItem.HasProviderImage ?? true) + if (service != null) { - var service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, liveTvItem.ServiceName, StringComparison.OrdinalIgnoreCase)); - - if (service != null) + try { - try - { - var channel = _liveTvManager.GetInternalChannel(liveTvItem.ChannelId); + var channel = _liveTvManager.GetInternalChannel(liveTvItem.ChannelId); - var response = await service.GetProgramImageAsync(liveTvItem.ExternalId, channel.ExternalId, cancellationToken).ConfigureAwait(false); + var response = await service.GetProgramImageAsync(liveTvItem.ExternalId, channel.ExternalId, cancellationToken).ConfigureAwait(false); - if (response != null) - { - imageResponse.HasImage = true; - imageResponse.Stream = response.Stream; - imageResponse.Format = response.Format; - } - } - catch (NotImplementedException) + if (response != null) { + imageResponse.HasImage = true; + imageResponse.Stream = response.Stream; + imageResponse.Format = response.Format; } } + catch (NotImplementedException) + { + } } return imageResponse; @@ -115,7 +80,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv if (liveTvItem != null) { - return !liveTvItem.HasImage(ImageType.Primary) && (liveTvItem.HasProviderImage ?? true); + return !liveTvItem.HasImage(ImageType.Primary); } return false; } diff --git a/MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs index adf1e7516..fce3223ea 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs @@ -1,9 +1,7 @@ -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; using System.Linq; @@ -15,14 +13,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv public class RecordingImageProvider : IDynamicImageProvider, IHasItemChangeMonitor { private readonly ILiveTvManager _liveTvManager; - private readonly IHttpClient _httpClient; - private readonly ILogger _logger; - public RecordingImageProvider(ILiveTvManager liveTvManager, IHttpClient httpClient, ILogger logger) + public RecordingImageProvider(ILiveTvManager liveTvManager) { _liveTvManager = liveTvManager; - _httpClient = httpClient; - _logger = logger; } public IEnumerable<ImageType> GetSupportedImages(IHasImages item) @@ -36,53 +30,24 @@ namespace MediaBrowser.Server.Implementations.LiveTv var imageResponse = new DynamicImageResponse(); - if (!string.IsNullOrEmpty(liveTvItem.ProviderImagePath)) - { - imageResponse.Path = liveTvItem.ProviderImagePath; - imageResponse.HasImage = true; - } - else if (!string.IsNullOrEmpty(liveTvItem.ProviderImageUrl)) - { - var options = new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = liveTvItem.ProviderImageUrl - }; + var service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, liveTvItem.ServiceName, StringComparison.OrdinalIgnoreCase)); - var response = await _httpClient.GetResponse(options).ConfigureAwait(false); - - if (response.ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) - { - imageResponse.HasImage = true; - imageResponse.Stream = response.Content; - imageResponse.SetFormatFromMimeType(response.ContentType); - } - else - { - _logger.Error("Provider did not return an image content type."); - } - } - else + if (service != null) { - var service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, liveTvItem.ServiceName, StringComparison.OrdinalIgnoreCase)); - - if (service != null) + try { - try - { - var response = await service.GetRecordingImageAsync(liveTvItem.ExternalId, cancellationToken).ConfigureAwait(false); + var response = await service.GetRecordingImageAsync(liveTvItem.ExternalId, cancellationToken).ConfigureAwait(false); - if (response != null) - { - imageResponse.HasImage = true; - imageResponse.Stream = response.Stream; - imageResponse.Format = response.Format; - } - } - catch (NotImplementedException) + if (response != null) { + imageResponse.HasImage = true; + imageResponse.Stream = response.Stream; + imageResponse.Format = response.Format; } } + catch (NotImplementedException) + { + } } return imageResponse; @@ -109,7 +74,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv if (liveTvItem != null) { - return !liveTvItem.HasImage(ImageType.Primary) && (!string.IsNullOrWhiteSpace(liveTvItem.ProviderImagePath) || !string.IsNullOrWhiteSpace(liveTvItem.ProviderImageUrl)); + return !liveTvItem.HasImage(ImageType.Primary); } return false; } diff --git a/MediaBrowser.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs b/MediaBrowser.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs index d8d91c2f9..3fb1d9661 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; namespace MediaBrowser.Server.Implementations.LiveTv { - class RefreshChannelsScheduledTask : IScheduledTask, IConfigurableScheduledTask, IHasKey + public class RefreshChannelsScheduledTask : IScheduledTask, IConfigurableScheduledTask, IHasKey { private readonly ILiveTvManager _liveTvManager; private readonly IConfigurationManager _config; diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs index 909e2bba5..d811152c2 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs @@ -9,6 +9,9 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Serialization; namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts { @@ -16,14 +19,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts { protected readonly IConfigurationManager Config; protected readonly ILogger Logger; + protected IJsonSerializer JsonSerializer; + protected readonly IMediaEncoder MediaEncoder; private readonly ConcurrentDictionary<string, ChannelCache> _channelCache = new ConcurrentDictionary<string, ChannelCache>(StringComparer.OrdinalIgnoreCase); - public BaseTunerHost(IConfigurationManager config, ILogger logger) + protected BaseTunerHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder) { Config = config; Logger = logger; + JsonSerializer = jsonSerializer; + MediaEncoder = mediaEncoder; } protected abstract Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken); @@ -44,8 +51,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts var result = await GetChannelsInternal(tuner, cancellationToken).ConfigureAwait(false); var list = result.ToList(); + Logger.Debug("Channels from {0}: {1}", tuner.Url, JsonSerializer.SerializeToString(list)); - if (!string.IsNullOrWhiteSpace(key)) + if (!string.IsNullOrWhiteSpace(key) && list.Count > 0) { cache = cache ?? new ChannelCache(); cache.Date = DateTime.UtcNow; @@ -109,23 +117,22 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts foreach (var host in hostsWithChannel) { - // Check to make sure the tuner is available - // If there's only one tuner, don't bother with the check and just let the tuner be the one to throw an error - if (hostsWithChannel.Count > 1 && !await IsAvailable(host, channelId, cancellationToken).ConfigureAwait(false)) + try { - Logger.Error("Tuner is not currently available"); - continue; - } + var mediaSources = await GetChannelStreamMediaSources(host, channelId, cancellationToken).ConfigureAwait(false); - var mediaSources = await GetChannelStreamMediaSources(host, channelId, cancellationToken).ConfigureAwait(false); + // Prefix the id with the host Id so that we can easily find it + foreach (var mediaSource in mediaSources) + { + mediaSource.Id = host.Id + mediaSource.Id; + } - // Prefix the id with the host Id so that we can easily find it - foreach (var mediaSource in mediaSources) + return mediaSources; + } + catch (Exception ex) { - mediaSource.Id = host.Id + mediaSource.Id; + Logger.Error("Error opening tuner", ex); } - - return mediaSources; } } @@ -134,7 +141,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts protected abstract Task<MediaSourceInfo> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken); - public async Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken) + public async Task<Tuple<MediaSourceInfo, SemaphoreSlim>> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken) { if (IsValidChannelId(channelId)) { @@ -163,23 +170,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts foreach (var host in hostsWithChannel) { - // Check to make sure the tuner is available - // If there's only one tuner, don't bother with the check and just let the tuner be the one to throw an error - // If a streamId is specified then availibility has already been checked in GetChannelStreamMediaSources - if (string.IsNullOrWhiteSpace(streamId) && hostsWithChannel.Count > 1) + try { - if (!await IsAvailable(host, channelId, cancellationToken).ConfigureAwait(false)) - { - Logger.Error("Tuner is not currently available"); - continue; - } - } - - var stream = await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false); + var stream = await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false); + var resourcePool = GetLock(host.Url); - if (stream != null) + await AddMediaInfo(stream, false, resourcePool, cancellationToken).ConfigureAwait(false); + return new Tuple<MediaSourceInfo, SemaphoreSlim>(stream, resourcePool); + } + catch (Exception ex) { - return stream; + Logger.Error("Error opening tuner", ex); } } } @@ -187,20 +188,116 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts throw new LiveTvConflictException(); } - protected async Task<bool> IsAvailable(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken) + /// <summary> + /// The _semaphoreLocks + /// </summary> + private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks = new ConcurrentDictionary<string, SemaphoreSlim>(StringComparer.OrdinalIgnoreCase); + /// <summary> + /// Gets the lock. + /// </summary> + /// <param name="url">The filename.</param> + /// <returns>System.Object.</returns> + private SemaphoreSlim GetLock(string url) + { + return _semaphoreLocks.GetOrAdd(url, key => new SemaphoreSlim(1, 1)); + } + + private async Task AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio, SemaphoreSlim resourcePool, CancellationToken cancellationToken) { + await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); + try { - return await IsAvailableInternal(tuner, channelId, cancellationToken).ConfigureAwait(false); + await AddMediaInfoInternal(mediaSource, isAudio, cancellationToken).ConfigureAwait(false); + + // Leave the resource locked. it will be released upstream } - catch (Exception ex) + catch (Exception) { - Logger.ErrorException("Error checking tuner availability", ex); - return false; + // Release the resource if there's some kind of failure. + resourcePool.Release(); + + throw; } } - protected abstract Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken); + private async Task AddMediaInfoInternal(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken) + { + var originalRuntime = mediaSource.RunTimeTicks; + + var info = await MediaEncoder.GetMediaInfo(new MediaInfoRequest + { + InputPath = mediaSource.Path, + Protocol = mediaSource.Protocol, + MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video, + ExtractChapters = false + + }, cancellationToken).ConfigureAwait(false); + + mediaSource.Bitrate = info.Bitrate; + mediaSource.Container = info.Container; + mediaSource.Formats = info.Formats; + mediaSource.MediaStreams = info.MediaStreams; + mediaSource.RunTimeTicks = info.RunTimeTicks; + mediaSource.Size = info.Size; + mediaSource.Timestamp = info.Timestamp; + mediaSource.Video3DFormat = info.Video3DFormat; + mediaSource.VideoType = info.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 = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == 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 == Model.Entities.MediaStreamType.Video); + if (videoStream != null) + { + if (!videoStream.BitRate.HasValue) + { + var width = videoStream.Width ?? 1920; + + if (width >= 1900) + { + videoStream.BitRate = 8000000; + } + + else if (width >= 1260) + { + videoStream.BitRate = 3000000; + } + + else if (width >= 700) + { + videoStream.BitRate = 1000000; + } + } + } + + // Try to estimate this + if (!mediaSource.Bitrate.HasValue) + { + var total = mediaSource.MediaStreams.Select(i => i.BitRate ?? 0).Sum(); + + if (total > 0) + { + mediaSource.Bitrate = total; + } + } + } protected abstract bool IsValidChannelId(string channelId); diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs index c74220137..92a33993a 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs @@ -9,6 +9,7 @@ using MediaBrowser.Model.Logging; using System; using System.Linq; using System.Threading; +using MediaBrowser.Common.Net; namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun { @@ -19,13 +20,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun private readonly ILogger _logger; private readonly ILiveTvManager _liveTvManager; private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); + private readonly IHttpClient _httpClient; - public HdHomerunDiscovery(IDeviceDiscovery deviceDiscovery, IServerConfigurationManager config, ILogger logger, ILiveTvManager liveTvManager) + public HdHomerunDiscovery(IDeviceDiscovery deviceDiscovery, IServerConfigurationManager config, ILogger logger, ILiveTvManager liveTvManager, IHttpClient httpClient) { _deviceDiscovery = deviceDiscovery; _config = config; _logger = logger; _liveTvManager = liveTvManager; + _httpClient = httpClient; } public void Run() @@ -75,6 +78,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun // Strip off the port url = new Uri(url).GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped).TrimEnd('/'); + // Test it by pulling down the lineup + using (await _httpClient.Get(new HttpRequestOptions + { + Url = string.Format("{0}/lineup.json", url), + CancellationToken = CancellationToken.None + })) + { + } + await _liveTvManager.SaveTunerHost(new TunerHostInfo { Type = HdHomerunHost.DeviceType, diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index bccb0db0a..eebb381af 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -14,19 +14,18 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Dlna; namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun { public class HdHomerunHost : BaseTunerHost, ITunerHost { private readonly IHttpClient _httpClient; - private readonly IJsonSerializer _jsonSerializer; - public HdHomerunHost(IConfigurationManager config, ILogger logger, IHttpClient httpClient, IJsonSerializer jsonSerializer) - : base(config, logger) + public HdHomerunHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IHttpClient httpClient) : base(config, logger, jsonSerializer, mediaEncoder) { _httpClient = httpClient; - _jsonSerializer = jsonSerializer; } public string Name @@ -55,7 +54,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun }; using (var stream = await _httpClient.Get(options)) { - var root = _jsonSerializer.DeserializeFromStream<List<Channels>>(stream); + var root = JsonSerializer.DeserializeFromStream<List<Channels>>(stream); if (root != null) { @@ -88,7 +87,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun Url = string.Format("{0}/", GetApiUrl(info, false)), CancellationToken = cancellationToken, CacheLength = TimeSpan.FromDays(1), - CacheMode = CacheMode.Unconditional + CacheMode = CacheMode.Unconditional, + TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds) })) { using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8)) @@ -103,7 +103,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun } } - return null; + return model; } public async Task<List<LiveTvTunerInfo>> GetTunerInfos(TunerHostInfo info, CancellationToken cancellationToken) @@ -113,7 +113,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun using (var stream = await _httpClient.Get(new HttpRequestOptions() { Url = string.Format("{0}/tuners.html", GetApiUrl(info, false)), - CancellationToken = cancellationToken + CancellationToken = cancellationToken, + TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds) })) { var tuners = new List<LiveTvTunerInfo>(); @@ -325,11 +326,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun BitRate = 128000 } }, - RequiresOpening = false, - RequiresClosing = false, + RequiresOpening = true, + RequiresClosing = true, BufferMs = 1000, Container = "ts", - Id = profile + Id = profile, + SupportsDirectPlay = true, + SupportsDirectStream = true, + SupportsTranscoding = true }; return mediaSource; @@ -372,16 +376,21 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun protected override bool IsValidChannelId(string channelId) { + if (string.IsNullOrWhiteSpace(channelId)) + { + throw new ArgumentNullException("channelId"); + } + return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase); } protected override async Task<MediaSourceInfo> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken) { - Logger.Debug("GetChannelStream: channel id: {0}. stream id: {1}", channelId, streamId ?? string.Empty); + Logger.Info("GetChannelStream: channel id: {0}. stream id: {1}", channelId, streamId ?? string.Empty); if (!channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase)) { - return null; + throw new ArgumentException("Channel not found"); } channelId = channelId.Substring(ChannelIdPrefix.Length); @@ -395,12 +404,5 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun await GetChannels(info, false, CancellationToken.None).ConfigureAwait(false); } } - - protected override async Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken) - { - var info = await GetTunerInfos(tuner, cancellationToken).ConfigureAwait(false); - - return info.Any(i => i.Status == LiveTvTunerStatus.Available || string.Equals(i.ChannelId, channelId, StringComparison.OrdinalIgnoreCase)); - } } } diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 3783e4b08..afd41e3d8 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -12,14 +12,23 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Serialization; namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts { public class M3UTunerHost : BaseTunerHost, ITunerHost { - public M3UTunerHost(IConfigurationManager config, ILogger logger) - : base(config, logger) + private readonly IFileSystem _fileSystem; + private readonly IHttpClient _httpClient; + + public M3UTunerHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient) : base(config, logger, jsonSerializer, mediaEncoder) { + _fileSystem = fileSystem; + _httpClient = httpClient; } public override string Type @@ -41,47 +50,48 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts string line; // Read the file and display it line by line. - var file = new StreamReader(url); - var channels = new List<M3UChannel>(); + using (var file = new StreamReader(await GetListingsStream(info, cancellationToken).ConfigureAwait(false))) + { + var channels = new List<M3UChannel>(); - string channnelName = null; - string channelNumber = null; + string channnelName = null; + string channelNumber = null; - while ((line = file.ReadLine()) != null) - { - line = line.Trim(); - if (string.IsNullOrWhiteSpace(line)) + while ((line = file.ReadLine()) != null) { - continue; - } + line = line.Trim(); + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } - if (line.StartsWith("#EXTM3U", StringComparison.OrdinalIgnoreCase)) - { - continue; - } + if (line.StartsWith("#EXTM3U", StringComparison.OrdinalIgnoreCase)) + { + continue; + } - if (line.StartsWith("#EXTINF:", StringComparison.OrdinalIgnoreCase)) - { - var parts = line.Split(new[] { ':' }, 2).Last().Split(new[] { ',' }, 2); - channelNumber = parts[0]; - channnelName = parts[1]; - } - else if (!string.IsNullOrWhiteSpace(channelNumber)) - { - channels.Add(new M3UChannel + if (line.StartsWith("#EXTINF:", StringComparison.OrdinalIgnoreCase)) { - Name = channnelName, - Number = channelNumber, - Id = ChannelIdPrefix + urlHash + channelNumber, - Path = line - }); - - channelNumber = null; - channnelName = null; + var parts = line.Split(new[] { ':' }, 2).Last().Split(new[] { ',' }, 2); + channelNumber = parts[0]; + channnelName = parts[1]; + } + else if (!string.IsNullOrWhiteSpace(channelNumber)) + { + channels.Add(new M3UChannel + { + Name = channnelName, + Number = channelNumber, + Id = ChannelIdPrefix + urlHash + channelNumber, + Path = line + }); + + channelNumber = null; + channnelName = null; + } } + return channels; } - file.Close(); - return channels; } public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken) @@ -119,9 +129,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts public async Task Validate(TunerHostInfo info) { - if (!File.Exists(info.Url)) + using (var stream = await GetListingsStream(info, CancellationToken.None).ConfigureAwait(false)) { - throw new FileNotFoundException(); + } } @@ -130,6 +140,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase); } + private Task<Stream> GetListingsStream(TunerHostInfo info, CancellationToken cancellationToken) + { + if (info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + return _httpClient.Get(info.Url, cancellationToken); + } + return Task.FromResult(_fileSystem.OpenRead(info.Url)); + } + protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, string channelId, CancellationToken cancellationToken) { var urlHash = info.Url.GetMD5().ToString("N"); @@ -190,10 +209,5 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts } return new List<MediaSourceInfo> { }; } - - protected override Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken) - { - return Task.FromResult(true); - } } } diff --git a/MediaBrowser.Server.Implementations/Localization/Core/ar.json b/MediaBrowser.Server.Implementations/Localization/Core/ar.json index 3d15d7b2e..fd47027c2 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/ar.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/ar.json @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Parental Ratings", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/bg-BG.json b/MediaBrowser.Server.Implementations/Localization/Core/bg-BG.json index 8854611a0..aeebd2038 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/bg-BG.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/bg-BG.json @@ -17,7 +17,7 @@ "ValueSpecialEpisodeName": "Special - {0}", "LabelChapterName": "Chapter {0}", "NameSeasonNumber": "Season {0}", - "LabelExit": "\u0418\u0437\u043b\u0435\u0437", + "LabelExit": "\u0418\u0437\u0445\u043e\u0434", "LabelVisitCommunity": "\u041f\u043e\u0441\u0435\u0442\u0438 \u043e\u0431\u0449\u0435\u0441\u0442\u0432\u043e\u0442\u043e", "LabelGithub": "Github", "LabelApiDocumentation": "API \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f", @@ -25,7 +25,7 @@ "LabelBrowseLibrary": "\u0420\u0430\u0437\u0433\u043b\u0435\u0434\u0430\u0439 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430\u0442\u0430", "LabelConfigureServer": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0439 Emby", "LabelRestartServer": "\u0420\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0439 \u0441\u044a\u0440\u0432\u044a\u0440\u0430", - "CategorySync": "Sync", + "CategorySync": "\u0421\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437.", "CategoryUser": "User", "CategorySystem": "System", "CategoryApplication": "Application", @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Parental Ratings", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/ca.json b/MediaBrowser.Server.Implementations/Localization/Core/ca.json index 35f2bafb8..46004f52c 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/ca.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/ca.json @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Parental Ratings", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/core.json b/MediaBrowser.Server.Implementations/Localization/Core/core.json index 4eb66929d..5f11b9436 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/core.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/core.json @@ -173,5 +173,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Parental Ratings", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." } diff --git a/MediaBrowser.Server.Implementations/Localization/Core/cs.json b/MediaBrowser.Server.Implementations/Localization/Core/cs.json index ee30a6028..771e735e9 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/cs.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/cs.json @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Parental Ratings", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/da.json b/MediaBrowser.Server.Implementations/Localization/Core/da.json index 21d7e9e14..e6e4d9835 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/da.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/da.json @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Aldersgr\u00e6nser", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/de.json b/MediaBrowser.Server.Implementations/Localization/Core/de.json index 277a224ce..1939df199 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/de.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/de.json @@ -25,7 +25,7 @@ "LabelBrowseLibrary": "Bibliothek durchsuchen", "LabelConfigureServer": "Konfiguriere Emby", "LabelRestartServer": "Server neustarten", - "CategorySync": "Synchronisieren", + "CategorySync": "Sync", "CategoryUser": "Benutzer", "CategorySystem": "System", "CategoryApplication": "Anwendung", @@ -172,5 +172,6 @@ "HeaderProducer": "Produzenten", "HeaderWriter": "Autoren", "HeaderParentalRatings": "Altersbeschr\u00e4nkung", - "HeaderCommunityRatings": "Community Bewertungen" + "HeaderCommunityRatings": "Community Bewertungen", + "StartupEmbyServerIsLoading": "Emby Server startet, bitte versuchen Sie es gleich noch einmal." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/el.json b/MediaBrowser.Server.Implementations/Localization/Core/el.json index 213868042..7e94f7987 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/el.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/el.json @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Parental Ratings", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/en-GB.json b/MediaBrowser.Server.Implementations/Localization/Core/en-GB.json index 566d2cf09..73534b08d 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/en-GB.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/en-GB.json @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Parental Ratings", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/en-US.json b/MediaBrowser.Server.Implementations/Localization/Core/en-US.json index abe203804..e444c0e93 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/en-US.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/en-US.json @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Parental Ratings", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/es-AR.json b/MediaBrowser.Server.Implementations/Localization/Core/es-AR.json index 9c1ea6cb9..8068e32a9 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/es-AR.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/es-AR.json @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Parental Ratings", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/es-MX.json b/MediaBrowser.Server.Implementations/Localization/Core/es-MX.json index 9e1030dbd..972f94100 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/es-MX.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/es-MX.json @@ -172,5 +172,6 @@ "HeaderProducer": "Productores", "HeaderWriter": "Guionistas", "HeaderParentalRatings": "Clasificaci\u00f3n Parental", - "HeaderCommunityRatings": "Clasificaciones de la comunidad" + "HeaderCommunityRatings": "Clasificaciones de la comunidad", + "StartupEmbyServerIsLoading": "El servidor Emby esta cargando. Por favor intente de nuevo dentro de poco." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/es.json b/MediaBrowser.Server.Implementations/Localization/Core/es.json index f93fd7219..9bf56e4af 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/es.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/es.json @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Parental Ratings", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/fi.json b/MediaBrowser.Server.Implementations/Localization/Core/fi.json index 5df95146c..f0ab5b0d0 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/fi.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/fi.json @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Parental Ratings", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/fr.json b/MediaBrowser.Server.Implementations/Localization/Core/fr.json index fe76ac64e..182e89bf6 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/fr.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/fr.json @@ -20,7 +20,7 @@ "LabelExit": "Quitter", "LabelVisitCommunity": "Visiter la Communaut\u00e9", "LabelGithub": "Github", - "LabelApiDocumentation": "Documentation API", + "LabelApiDocumentation": "Documentation de l'API", "LabelDeveloperResources": "Ressources pour d\u00e9veloppeurs", "LabelBrowseLibrary": "Parcourir la biblioth\u00e8que", "LabelConfigureServer": "Configurer Emby", @@ -172,5 +172,6 @@ "HeaderProducer": "Producteurs", "HeaderWriter": "Auteur(e)s", "HeaderParentalRatings": "Note parentale", - "HeaderCommunityRatings": "Classification de la communaut\u00e9" + "HeaderCommunityRatings": "Classification de la communaut\u00e9", + "StartupEmbyServerIsLoading": "Le serveur Emby est en cours de chargement. Veuillez r\u00e9essayer dans quelques instant." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/gsw.json b/MediaBrowser.Server.Implementations/Localization/Core/gsw.json index 7334b24ce..736c3d18f 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/gsw.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/gsw.json @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Parental Ratings", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/he.json b/MediaBrowser.Server.Implementations/Localization/Core/he.json index b3bec3f02..a66438482 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/he.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/he.json @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Parental Ratings", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/hr.json b/MediaBrowser.Server.Implementations/Localization/Core/hr.json index 451ff4f36..2b9d56566 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/hr.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/hr.json @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Parental Ratings", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/it.json b/MediaBrowser.Server.Implementations/Localization/Core/it.json index 90f800634..2030ea110 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/it.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/it.json @@ -20,8 +20,8 @@ "LabelExit": "Esci", "LabelVisitCommunity": "Visita la Community", "LabelGithub": "Github", - "LabelApiDocumentation": "Documentazione sulle Api", - "LabelDeveloperResources": "Risorse per i programmatori", + "LabelApiDocumentation": "Documentazione Api", + "LabelDeveloperResources": "Risorse programmatori", "LabelBrowseLibrary": "Esplora la libreria", "LabelConfigureServer": "Configura Emby", "LabelRestartServer": "Riavvia Server", @@ -120,36 +120,36 @@ "UserStartedPlayingItemWithValues": "{0} \u00e8 partito da {1}", "UserStoppedPlayingItemWithValues": "{0} stoppato {1}", "SubtitleDownloadFailureForItem": "Sottotitoli non scaricati per {0}", - "HeaderUnidentified": "Unidentified", - "HeaderImagePrimary": "Primary", - "HeaderImageBackdrop": "Backdrop", + "HeaderUnidentified": "Non identificata", + "HeaderImagePrimary": "Primaria", + "HeaderImageBackdrop": "Sfondo", "HeaderImageLogo": "Logo", - "HeaderUserPrimaryImage": "User Image", - "HeaderOverview": "Overview", - "HeaderShortOverview": "Short Overview", - "HeaderType": "Type", - "HeaderSeverity": "Severity", + "HeaderUserPrimaryImage": "Immagine utente", + "HeaderOverview": "Panoramica", + "HeaderShortOverview": "breve panoramica", + "HeaderType": "Tipo", + "HeaderSeverity": "gravit\u00e0", "HeaderUser": "Utente", "HeaderName": "Nome", "HeaderDate": "Data", - "HeaderPremiereDate": "Premiere Date", - "HeaderDateAdded": "Date Added", + "HeaderPremiereDate": "Data della prima", + "HeaderDateAdded": "Aggiunto il", "HeaderReleaseDate": "Data Rilascio", "HeaderRuntime": "Durata", - "HeaderPlayCount": "Play Count", + "HeaderPlayCount": "Visto N\u00b0", "HeaderSeason": "Stagione", "HeaderSeasonNumber": "Stagione Numero", - "HeaderSeries": "Series:", + "HeaderSeries": "Serie:", "HeaderNetwork": "Rete", - "HeaderYear": "Year:", - "HeaderYears": "Years:", - "HeaderParentalRating": "Parental Rating", + "HeaderYear": "Anno:", + "HeaderYears": "Anni", + "HeaderParentalRating": "Valutazione parentale", "HeaderCommunityRating": "Voto Comunit\u00e0", "HeaderTrailers": "Trailers", "HeaderSpecials": "Speciali", - "HeaderGameSystems": "Game Systems", - "HeaderPlayers": "Players:", - "HeaderAlbumArtists": "Album Artists", + "HeaderGameSystems": "Sistemi di gioco", + "HeaderPlayers": "Giocatori", + "HeaderAlbumArtists": "Album Artisti", "HeaderAlbums": "Album", "HeaderDisc": "Disco", "HeaderTrack": "Traccia", @@ -162,15 +162,16 @@ "HeaderCountries": "Paesi", "HeaderStatus": "Stato", "HeaderTracks": "Traccia", - "HeaderMusicArtist": "Music artist", - "HeaderLocked": "Locked", + "HeaderMusicArtist": "Musica artisti", + "HeaderLocked": "Bloccato", "HeaderStudios": "Studios", - "HeaderActor": "Actors", - "HeaderComposer": "Composers", - "HeaderDirector": "Directors", - "HeaderGuestStar": "Guest star", - "HeaderProducer": "Producers", - "HeaderWriter": "Writers", + "HeaderActor": "Attori", + "HeaderComposer": "Compositori", + "HeaderDirector": "Registi", + "HeaderGuestStar": "Personaggi famosi", + "HeaderProducer": "Produttori", + "HeaderWriter": "Sceneggiatori", "HeaderParentalRatings": "Valutazioni genitori", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Valutazione Comunity", + "StartupEmbyServerIsLoading": "Emby server si sta avviando. Riprova tra un po" }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/kk.json b/MediaBrowser.Server.Implementations/Localization/Core/kk.json index 7e17dea6b..be7c4c56e 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/kk.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/kk.json @@ -12,7 +12,7 @@ "FolderTypeBooks": "\u041a\u0456\u0442\u0430\u043f\u0442\u0430\u0440", "FolderTypeTvShows": "\u0422\u0414", "FolderTypeInherit": "\u041c\u04b1\u0440\u0430\u0493\u0430 \u0438\u0435\u043b\u0435\u043d\u0443", - "HeaderCastCrew": "\u0422\u04af\u0441\u0456\u0440\u0443\u0433\u0435 \u049b\u0430\u0442\u044b\u0441\u049b\u0430\u043d\u0434\u0430\u0440", + "HeaderCastCrew": "\u0421\u043e\u043c\u0434\u0430\u0443\u0448\u044b\u043b\u0430\u0440 \u043c\u0435\u043d \u0442\u04af\u0441\u0456\u0440\u0443\u0448\u0456\u043b\u0435\u0440", "HeaderPeople": "\u0410\u0434\u0430\u043c\u0434\u0430\u0440", "ValueSpecialEpisodeName": "\u0410\u0440\u043d\u0430\u0439\u044b - {0}", "LabelChapterName": "{0}-\u0441\u0430\u0445\u043d\u0430", @@ -104,7 +104,7 @@ "DeviceOnlineWithName": "{0} \u049b\u043e\u0441\u044b\u043b\u0493\u0430\u043d", "UserOnlineFromDevice": "{0} - {1} \u0430\u0440\u049b\u044b\u043b\u044b \u049b\u043e\u0441\u044b\u043b\u0493\u0430\u043d", "ProviderValue": "\u0416\u0435\u0442\u043a\u0456\u0437\u0443\u0448\u0456: {0}", - "SubtitlesDownloadedForItem": "\u0421\u0443\u0431\u0442\u0438\u0442\u0440\u043b\u0435\u0440 {0} \u04af\u0448\u0456\u043d \u0436\u04af\u043a\u0442\u0435\u043f \u0430\u043b\u044b\u043d\u0434\u044b", + "SubtitlesDownloadedForItem": "\u0421\u0443\u0431\u0442\u0438\u0442\u0440\u043b\u0435\u0440 {0} \u04af\u0448\u0456\u043d \u0436\u04af\u043a\u0442\u0435\u043b\u0456\u043f \u0430\u043b\u044b\u043d\u0434\u044b", "UserConfigurationUpdatedWithName": "\u041f\u0430\u0439\u0434\u0430\u043b\u0430\u043d\u0443\u0448\u044b {0} \u04af\u0448\u0456\u043d \u0442\u0435\u04a3\u0448\u0435\u043b\u0456\u043c \u0436\u0430\u04a3\u0430\u0440\u0442\u044b\u043b\u0434\u044b", "UserCreatedWithName": "\u041f\u0430\u0439\u0434\u0430\u043b\u0430\u043d\u0443\u0448\u044b {0} \u0436\u0430\u0441\u0430\u043b\u0493\u0430\u043d", "UserPasswordChangedWithName": "\u041f\u0430\u0439\u0434\u0430\u043b\u0430\u043d\u0443\u0448\u044b {0} \u04af\u0448\u0456\u043d \u049b\u04b1\u043f\u0438\u044f \u0441\u04e9\u0437 \u04e9\u0437\u0433\u0435\u0440\u0442\u0456\u043b\u0434\u0456", @@ -117,9 +117,9 @@ "DeviceOfflineWithName": "{0} \u0430\u0436\u044b\u0440\u0430\u0442\u044b\u043b\u0493\u0430\u043d", "UserLockedOutWithName": "\u041f\u0430\u0439\u0434\u0430\u043b\u0430\u043d\u0443\u0448\u044b {0} \u049b\u04b1\u0440\u0441\u0430\u0443\u043b\u044b", "UserOfflineFromDevice": "{0} - {1} \u0430\u0440\u049b\u044b\u043b\u044b \u0430\u0436\u044b\u0440\u0430\u0442\u044b\u043b\u0493\u0430\u043d", - "UserStartedPlayingItemWithValues": "{0} - {1} \u043e\u0439\u043d\u0430\u0442\u0443\u044b \u0431\u0430\u0441\u0442\u0430\u043b\u0434\u044b", - "UserStoppedPlayingItemWithValues": "{0} - {1} \u043e\u0439\u043d\u0430\u0442\u0443\u044b \u0442\u043e\u049b\u0442\u0430\u043b\u0434\u044b", - "SubtitleDownloadFailureForItem": "\u0421\u0443\u0431\u0442\u0438\u0442\u0440\u043b\u0435\u0440 {0} \u04af\u0448\u0456\u043d \u0436\u04af\u043a\u0442\u0435\u043f \u0430\u043b\u044b\u043d\u0443\u044b \u0441\u04d9\u0442\u0441\u0456\u0437", + "UserStartedPlayingItemWithValues": "{0} - {1} \u043e\u0439\u043d\u0430\u0442\u0443\u044b\u043d \u0431\u0430\u0441\u0442\u0430\u0434\u044b", + "UserStoppedPlayingItemWithValues": "{0} - {1} \u043e\u0439\u043d\u0430\u0442\u0443\u044b\u043d \u0442\u043e\u049b\u0442\u0430\u0442\u0442\u044b", + "SubtitleDownloadFailureForItem": "\u0421\u0443\u0431\u0442\u0438\u0442\u0440\u043b\u0435\u0440 {0} \u04af\u0448\u0456\u043d \u0436\u04af\u043a\u0442\u0435\u043b\u0456\u043f \u0430\u043b\u044b\u043d\u0443\u044b \u0441\u04d9\u0442\u0441\u0456\u0437", "HeaderUnidentified": "\u0410\u043d\u044b\u049b\u0442\u0430\u043b\u043c\u0430\u0493\u0430\u043d", "HeaderImagePrimary": "\u041d\u0435\u0433\u0456\u0437\u0433\u0456", "HeaderImageBackdrop": "\u0410\u0440\u0442\u049b\u044b \u0441\u0443\u0440\u0435\u0442", @@ -172,5 +172,6 @@ "HeaderProducer": "\u041f\u0440\u043e\u0434\u044e\u0441\u0435\u0440\u043b\u0435\u0440", "HeaderWriter": "\u0421\u0446\u0435\u043d\u0430\u0440\u0438\u0439\u0448\u0456\u043b\u0435\u0440", "HeaderParentalRatings": "\u0416\u0430\u0441\u0442\u0430\u0441 \u0441\u0430\u043d\u0430\u0442\u0442\u0430\u0440", - "HeaderCommunityRatings": "\u049a\u0430\u0443\u044b\u043c \u0431\u0430\u0493\u0430\u043b\u0430\u0443\u043b\u0430\u0440\u044b" + "HeaderCommunityRatings": "\u049a\u0430\u0443\u044b\u043c \u0431\u0430\u0493\u0430\u043b\u0430\u0443\u043b\u0430\u0440\u044b", + "StartupEmbyServerIsLoading": "Emby Server \u0436\u04af\u043a\u0442\u0435\u043b\u0443\u0434\u0435. \u04d8\u0440\u0435\u043a\u0435\u0442\u0442\u0456 \u043a\u04e9\u043f \u04b1\u0437\u0430\u043c\u0430\u0439 \u049b\u0430\u0439\u0442\u0430\u043b\u0430\u04a3\u044b\u0437." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/ko.json b/MediaBrowser.Server.Implementations/Localization/Core/ko.json index 8bc5b5672..6044efb62 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/ko.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/ko.json @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Parental Ratings", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/ms.json b/MediaBrowser.Server.Implementations/Localization/Core/ms.json index a85f00132..6b5c4393f 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/ms.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/ms.json @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Parental Ratings", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/nb.json b/MediaBrowser.Server.Implementations/Localization/Core/nb.json index 2ab601701..f4ebe89b6 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/nb.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/nb.json @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Foreldresensur", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/nl.json b/MediaBrowser.Server.Implementations/Localization/Core/nl.json index 8658518a4..b32ebefc3 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/nl.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/nl.json @@ -157,7 +157,7 @@ "HeaderVideo": "Video", "HeaderEmbeddedImage": "Ingesloten afbeelding", "HeaderResolution": "Resolutie", - "HeaderSubtitles": "Ondertitels", + "HeaderSubtitles": "Ondertiteling", "HeaderGenres": "Genres", "HeaderCountries": "Landen", "HeaderStatus": "Status", @@ -172,5 +172,6 @@ "HeaderProducer": "Producenten", "HeaderWriter": "Schrijvers", "HeaderParentalRatings": "Ouderlijke toezicht", - "HeaderCommunityRatings": "Gemeenschapswaardering" + "HeaderCommunityRatings": "Gemeenschapswaardering", + "StartupEmbyServerIsLoading": "Emby Server is aan het laden, probeer het later opnieuw." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/pl.json b/MediaBrowser.Server.Implementations/Localization/Core/pl.json index b7b0e228e..13dca8889 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/pl.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/pl.json @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Parental Ratings", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/pt-BR.json b/MediaBrowser.Server.Implementations/Localization/Core/pt-BR.json index e5e8afaa5..9548262c9 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/pt-BR.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/pt-BR.json @@ -172,5 +172,6 @@ "HeaderProducer": "Produtores", "HeaderWriter": "Escritores", "HeaderParentalRatings": "Classifica\u00e7\u00f5es Parentais", - "HeaderCommunityRatings": "Avalia\u00e7\u00f5es da comunidade" + "HeaderCommunityRatings": "Avalia\u00e7\u00f5es da comunidade", + "StartupEmbyServerIsLoading": "O Servidor Emby est\u00e1 carregando. Por favor, tente novamente em breve." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/pt-PT.json b/MediaBrowser.Server.Implementations/Localization/Core/pt-PT.json index 5aa832e16..c3d514c8a 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/pt-PT.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/pt-PT.json @@ -46,7 +46,7 @@ "NotificationOptionInstallationFailed": "Falha na instala\u00e7\u00e3o", "NotificationOptionNewLibraryContent": "Adicionado novo conte\u00fado", "NotificationOptionNewLibraryContentMultiple": "Novo conte\u00fado adicionado (m\u00faltiplo)", - "NotificationOptionCameraImageUploaded": "Camera image uploaded", + "NotificationOptionCameraImageUploaded": "Imagem da c\u00e2mara carregada", "NotificationOptionUserLockedOut": "User locked out", "NotificationOptionServerRestartRequired": "\u00c9 necess\u00e1rio reiniciar o servidor", "ViewTypePlaylists": "Playlists", @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Parental Ratings", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/ro.json b/MediaBrowser.Server.Implementations/Localization/Core/ro.json index 6c945006a..a6aa9577b 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/ro.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/ro.json @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Parental Ratings", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/ru.json b/MediaBrowser.Server.Implementations/Localization/Core/ru.json index c705ddef4..f6b33e304 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/ru.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/ru.json @@ -6,13 +6,13 @@ "FolderTypeMusic": "\u041c\u0443\u0437\u044b\u043a\u0430", "FolderTypeAdultVideos": "\u0412\u0437\u0440\u043e\u0441\u043b\u044b\u0435 \u0432\u0438\u0434\u0435\u043e", "FolderTypePhotos": "\u0424\u043e\u0442\u043e\u0433\u0440\u0430\u0444\u0438\u0438", - "FolderTypeMusicVideos": "\u041c\u0443\u0437\u044b\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0432\u0438\u0434\u0435\u043e", - "FolderTypeHomeVideos": "\u0414\u043e\u043c\u0430\u0448\u043d\u0438\u0435 \u0432\u0438\u0434\u0435\u043e", + "FolderTypeMusicVideos": "\u041c\u0443\u0437-\u044b\u0435 \u0432\u0438\u0434\u0435\u043e", + "FolderTypeHomeVideos": "\u0414\u043e\u043c-\u0438\u0435 \u0432\u0438\u0434\u0435\u043e", "FolderTypeGames": "\u0418\u0433\u0440\u044b", "FolderTypeBooks": "\u041a\u043d\u0438\u0433\u0438", "FolderTypeTvShows": "\u0422\u0412", "FolderTypeInherit": "\u041d\u0430\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u043d\u0438\u0435", - "HeaderCastCrew": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438 \u0441\u044a\u0451\u043c\u043e\u043a", + "HeaderCastCrew": "\u0421\u043d\u0438\u043c\u0430\u043b\u0438\u0441\u044c \u0438 \u0441\u043d\u0438\u043c\u0430\u043b\u0438", "HeaderPeople": "\u041b\u044e\u0434\u0438", "ValueSpecialEpisodeName": "\u0421\u043f\u0435\u0446\u044d\u043f\u0438\u0437\u043e\u0434 - {0}", "LabelChapterName": "\u0421\u0446\u0435\u043d\u0430 {0}", @@ -46,10 +46,10 @@ "NotificationOptionInstallationFailed": "\u0421\u0431\u043e\u0439 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438", "NotificationOptionNewLibraryContent": "\u041d\u043e\u0432\u043e\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e", "NotificationOptionNewLibraryContentMultiple": "\u041d\u043e\u0432\u043e\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e (\u043c\u043d\u043e\u0433\u043e\u043a\u0440\u0430\u0442\u043d\u043e)", - "NotificationOptionCameraImageUploaded": "\u0424\u043e\u0442\u043e\u0433\u0440\u0430\u0444\u0438\u044f \u0441 \u043a\u0430\u043c\u0435\u0440\u044b \u0432\u044b\u043b\u043e\u0436\u0435\u043d\u0430", + "NotificationOptionCameraImageUploaded": "\u041f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0430 \u0432\u044b\u043a\u043b\u0430\u0434\u043a\u0430 \u043e\u0442\u0441\u043d\u044f\u0442\u043e\u0433\u043e \u0441 \u043a\u0430\u043c\u0435\u0440\u044b", "NotificationOptionUserLockedOut": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d", "NotificationOptionServerRestartRequired": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0430", - "ViewTypePlaylists": "\u0421\u043f\u0438\u0441\u043a\u0438 \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u044f", + "ViewTypePlaylists": "\u041f\u043b\u0435\u0439-\u043b\u0438\u0441\u0442\u044b", "ViewTypeMovies": "\u041a\u0438\u043d\u043e", "ViewTypeTvShows": "\u0422\u0412", "ViewTypeGames": "\u0418\u0433\u0440\u044b", @@ -79,9 +79,9 @@ "ViewTypeMovieFavorites": "\u0418\u0437\u0431\u0440\u0430\u043d\u043d\u043e\u0435", "ViewTypeMovieGenres": "\u0416\u0430\u043d\u0440\u044b", "ViewTypeMusicLatest": "\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0435", - "ViewTypeMusicPlaylists": "\u0421\u043f\u0438\u0441\u043a\u0438 \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u044f", + "ViewTypeMusicPlaylists": "\u041f\u043b\u0435\u0439-\u043b\u0438\u0441\u0442\u044b", "ViewTypeMusicAlbums": "\u0410\u043b\u044c\u0431\u043e\u043c\u044b", - "ViewTypeMusicAlbumArtists": "\u0410\u043b\u044c\u0431\u043e\u043c\u043d\u044b\u0435 \u0438\u0441\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u0438", + "ViewTypeMusicAlbumArtists": "\u0418\u0441\u043f-\u043b\u0438 \u0430\u043b\u044c\u0431\u043e\u043c\u0430", "HeaderOtherDisplaySettings": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f", "ViewTypeMusicSongs": "\u041a\u043e\u043c\u043f\u043e\u0437\u0438\u0446\u0438\u0438", "ViewTypeMusicFavorites": "\u0418\u0437\u0431\u0440\u0430\u043d\u043d\u043e\u0435", @@ -101,8 +101,8 @@ "ItemAddedWithName": "{0} (\u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 \u043c\u0435\u0434\u0438\u0430\u0442\u0435\u043a\u0443)", "ItemRemovedWithName": "{0} (\u0438\u0437\u044a\u044f\u0442\u043e \u0438\u0437 \u043c\u0435\u0434\u0438\u0430\u0442\u0435\u043a\u0438)", "LabelIpAddressValue": "IP-\u0430\u0434\u0440\u0435\u0441: {0}", - "DeviceOnlineWithName": "{0} - \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e", - "UserOnlineFromDevice": "{0} - \u043f\u043e\u0434\u043a\u043b. \u0441 {1} \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e", + "DeviceOnlineWithName": "{0} - \u043f\u043e\u0434\u043a\u043b. \u0443\u0441\u0442-\u043d\u043e", + "UserOnlineFromDevice": "{0} - \u043f\u043e\u0434\u043a\u043b. \u0441 {1} \u0443\u0441\u0442-\u043d\u043e", "ProviderValue": "\u041f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a: {0}", "SubtitlesDownloadedForItem": "\u0421\u0443\u0431\u0442\u0438\u0442\u0440\u044b \u0434\u043b\u044f {0} \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u043b\u0438\u0441\u044c", "UserConfigurationUpdatedWithName": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u043b\u044c\u0437-\u043b\u044f {0} \u0431\u044b\u043b\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430", @@ -117,11 +117,11 @@ "DeviceOfflineWithName": "{0} - \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043f\u0440\u0435\u0440\u0432\u0430\u043d\u043e", "UserLockedOutWithName": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c {0} \u0431\u044b\u043b \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d", "UserOfflineFromDevice": "{0} - \u043f\u043e\u0434\u043a\u043b. \u0441 {1} \u043f\u0440\u0435\u0440\u0432\u0430\u043d\u043e", - "UserStartedPlayingItemWithValues": "{0} - \u0432\u043e\u0441\u043f\u0440-\u0438\u0435 \u00ab{1}\u00bb \u0437\u0430\u043f-\u043d\u043e", - "UserStoppedPlayingItemWithValues": "{0} - \u0432\u043e\u0441\u043f\u0440-\u0438\u0435 \u00ab{1}\u00bb \u043e\u0441\u0442-\u043d\u043e", + "UserStartedPlayingItemWithValues": "{0} - \u0432\u043e\u0441\u043f\u0440. \u00ab{1}\u00bb \u0437\u0430\u043f-\u043d\u043e", + "UserStoppedPlayingItemWithValues": "{0} - \u0432\u043e\u0441\u043f\u0440. \u00ab{1}\u00bb \u043e\u0441\u0442-\u043d\u043e", "SubtitleDownloadFailureForItem": "\u0421\u0443\u0431\u0442\u0438\u0442\u0440\u044b \u043a {0} \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c", "HeaderUnidentified": "\u041d\u0435 \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u043d\u043e", - "HeaderImagePrimary": "\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0439", + "HeaderImagePrimary": "\u0413\u043e\u043b\u043e\u0432\u043d\u043e\u0439", "HeaderImageBackdrop": "\u0417\u0430\u0434\u043d\u0438\u043a", "HeaderImageLogo": "\u041b\u043e\u0433\u043e\u0442\u0438\u043f", "HeaderUserPrimaryImage": "\u0420\u0438\u0441\u0443\u043d\u043e\u043a \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", @@ -133,23 +133,23 @@ "HeaderName": "\u0418\u043c\u044f (\u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435)", "HeaderDate": "\u0414\u0430\u0442\u0430", "HeaderPremiereDate": "\u0414\u0430\u0442\u0430 \u043f\u0440\u0435\u043c\u044c\u0435\u0440\u044b", - "HeaderDateAdded": "\u0414\u0430\u0442\u0430 \u0434\u043e\u0431-\u0438\u044f", + "HeaderDateAdded": "\u0414\u0430\u0442\u0430 \u0434\u043e\u0431.", "HeaderReleaseDate": "\u0414\u0430\u0442\u0430 \u0432\u044b\u043f.", "HeaderRuntime": "\u0414\u043b\u0438\u0442.", - "HeaderPlayCount": "\u0427\u0438\u0441\u043b\u043e \u0432\u043e\u0441\u043f\u0440-\u0438\u0439", + "HeaderPlayCount": "\u041a\u043e\u043b-\u0432\u043e \u0432\u043e\u0441\u043f\u0440.", "HeaderSeason": "\u0421\u0435\u0437\u043e\u043d", - "HeaderSeasonNumber": "\u041d\u043e\u043c\u0435\u0440 \u0441\u0435\u0437\u043e\u043d\u0430", + "HeaderSeasonNumber": "\u2116 \u0441\u0435\u0437\u043e\u043d\u0430", "HeaderSeries": "\u0421\u0435\u0440\u0438\u0430\u043b:", "HeaderNetwork": "\u0422\u0435\u043b\u0435\u0441\u0435\u0442\u044c", "HeaderYear": "\u0413\u043e\u0434:", "HeaderYears": "\u0413\u043e\u0434\u044b:", - "HeaderParentalRating": "\u0412\u043e\u0437\u0440. \u043a\u0430\u0442-\u0438\u044f", + "HeaderParentalRating": "\u0412\u043e\u0437\u0440. \u043a\u0430\u0442.", "HeaderCommunityRating": "\u041e\u0431\u0449. \u043e\u0446\u0435\u043d\u043a\u0430", "HeaderTrailers": "\u0422\u0440\u0435\u0439\u043b.", "HeaderSpecials": "\u0421\u043f\u0435\u0446.", "HeaderGameSystems": "\u0418\u0433\u0440. \u0441\u0438\u0441\u0442\u0435\u043c\u044b", "HeaderPlayers": "\u0418\u0433\u0440\u043e\u043a\u0438:", - "HeaderAlbumArtists": "\u0410\u043b\u044c\u0431\u043e\u043c. \u0438\u0441\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u0438", + "HeaderAlbumArtists": "\u0418\u0441\u043f-\u043b\u0438 \u0430\u043b\u044c\u0431\u043e\u043c\u0430", "HeaderAlbums": "\u0410\u043b\u044c\u0431\u043e\u043c\u044b", "HeaderDisc": "\u0414\u0438\u0441\u043a", "HeaderTrack": "\u0414\u043e\u0440-\u043a\u0430", @@ -162,7 +162,7 @@ "HeaderCountries": "\u0421\u0442\u0440\u0430\u043d\u044b", "HeaderStatus": "\u0421\u043e\u0441\u0442-\u0438\u0435", "HeaderTracks": "\u0414\u043e\u0440-\u043a\u0438", - "HeaderMusicArtist": "\u041c\u0443\u0437. \u0438\u0441\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c", + "HeaderMusicArtist": "\u0418\u0441\u043f. \u043c\u0443\u0437\u044b\u043a\u0438", "HeaderLocked": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e", "HeaderStudios": "\u0421\u0442\u0443\u0434\u0438\u0438", "HeaderActor": "\u0410\u043a\u0442\u0451\u0440\u044b", @@ -172,5 +172,6 @@ "HeaderProducer": "\u041f\u0440\u043e\u0434\u044e\u0441\u0435\u0440\u044b", "HeaderWriter": "\u0421\u0446\u0435\u043d\u0430\u0440\u0438\u0441\u0442\u044b", "HeaderParentalRatings": "\u0412\u043e\u0437\u0440\u0430\u0441\u0442\u043d\u0430\u044f \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f", - "HeaderCommunityRatings": "\u041e\u0431\u0449. \u043e\u0446\u0435\u043d\u043a\u0438" + "HeaderCommunityRatings": "\u041e\u0431\u0449. \u043e\u0446\u0435\u043d\u043a\u0438", + "StartupEmbyServerIsLoading": "Emby Server \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u0442\u0441\u044f. \u041f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u0432\u0441\u043a\u043e\u0440\u0435." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/sl-SI.json b/MediaBrowser.Server.Implementations/Localization/Core/sl-SI.json index abe203804..e444c0e93 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/sl-SI.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/sl-SI.json @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Parental Ratings", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/sv.json b/MediaBrowser.Server.Implementations/Localization/Core/sv.json index 3a5a322c6..667b8e4b4 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/sv.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/sv.json @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Parental Ratings", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/tr.json b/MediaBrowser.Server.Implementations/Localization/Core/tr.json index f127cc816..810df2665 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/tr.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/tr.json @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Parental Ratings", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/uk.json b/MediaBrowser.Server.Implementations/Localization/Core/uk.json index ec9ca5a39..73c9b26f9 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/uk.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/uk.json @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Parental Ratings", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/vi.json b/MediaBrowser.Server.Implementations/Localization/Core/vi.json index 6e0c3ea20..75a5dbea7 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/vi.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/vi.json @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Parental Ratings", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/zh-CN.json b/MediaBrowser.Server.Implementations/Localization/Core/zh-CN.json index 54cd7e59b..a9430c0c2 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/zh-CN.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/zh-CN.json @@ -21,9 +21,9 @@ "LabelVisitCommunity": "\u8bbf\u95ee\u793e\u533a", "LabelGithub": "Github", "LabelApiDocumentation": "API\u6587\u6863", - "LabelDeveloperResources": "\u5f00\u53d1\u8d44\u6e90", + "LabelDeveloperResources": "\u5f00\u53d1\u8005\u8d44\u6e90", "LabelBrowseLibrary": "\u6d4f\u89c8\u5a92\u4f53\u5e93", - "LabelConfigureServer": "Configure Emby", + "LabelConfigureServer": "\u914d\u7f6eEmby", "LabelRestartServer": "\u91cd\u542f\u670d\u52a1\u5668", "CategorySync": "\u540c\u6b65", "CategoryUser": "\u7528\u6237", @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "\u5bb6\u957f\u5206\u7ea7", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/zh-HK.json b/MediaBrowser.Server.Implementations/Localization/Core/zh-HK.json new file mode 100644 index 000000000..8d25a28c4 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Localization/Core/zh-HK.json @@ -0,0 +1,177 @@ +{ + "AppDeviceValues": "App: {0}, Device: {1}", + "UserDownloadingItemWithValues": "{0} is downloading {1}", + "FolderTypeMixed": "\u6df7\u5408\u5167\u5bb9", + "FolderTypeMovies": "\u96fb\u5f71", + "FolderTypeMusic": "\u97f3\u6a02", + "FolderTypeAdultVideos": "\u6210\u4eba\u5f71\u7247", + "FolderTypePhotos": "\u76f8\u7247", + "FolderTypeMusicVideos": "MV", + "FolderTypeHomeVideos": "\u500b\u4eba\u5f71\u7247", + "FolderTypeGames": "\u904a\u6232", + "FolderTypeBooks": "\u66f8\u85c9", + "FolderTypeTvShows": "\u96fb\u8996\u7bc0\u76ee", + "FolderTypeInherit": "\u7e7c\u627f", + "HeaderCastCrew": "\u6f14\u54e1\u9663\u5bb9", + "HeaderPeople": "\u4eba\u7269", + "ValueSpecialEpisodeName": "Special - {0}", + "LabelChapterName": "Chapter {0}", + "NameSeasonNumber": "\u5287\u96c6\u5b63\u5ea6 {0}", + "LabelExit": "\u96e2\u958b", + "LabelVisitCommunity": "\u8a2a\u554f\u8a0e\u8ad6\u5340", + "LabelGithub": "Github", + "LabelApiDocumentation": "Api \u6587\u4ef6", + "LabelDeveloperResources": "\u958b\u767c\u8005\u8cc7\u6e90", + "LabelBrowseLibrary": "\u700f\u89bd\u8cc7\u6599\u5eab", + "LabelConfigureServer": "\u8a2d\u7f6e Emby", + "LabelRestartServer": "\u91cd\u65b0\u555f\u52d5\u4f3a\u670d\u5668", + "CategorySync": "\u540c\u6b65", + "CategoryUser": "User", + "CategorySystem": "System", + "CategoryApplication": "Application", + "CategoryPlugin": "Plugin", + "NotificationOptionPluginError": "Plugin failure", + "NotificationOptionApplicationUpdateAvailable": "Application update available", + "NotificationOptionApplicationUpdateInstalled": "Application update installed", + "NotificationOptionPluginUpdateInstalled": "Plugin update installed", + "NotificationOptionPluginInstalled": "Plugin installed", + "NotificationOptionPluginUninstalled": "Plugin uninstalled", + "NotificationOptionVideoPlayback": "Video playback started", + "NotificationOptionAudioPlayback": "Audio playback started", + "NotificationOptionGamePlayback": "Game playback started", + "NotificationOptionVideoPlaybackStopped": "Video playback stopped", + "NotificationOptionAudioPlaybackStopped": "Audio playback stopped", + "NotificationOptionGamePlaybackStopped": "Game playback stopped", + "NotificationOptionTaskFailed": "Scheduled task failure", + "NotificationOptionInstallationFailed": "Installation failure", + "NotificationOptionNewLibraryContent": "New content added", + "NotificationOptionNewLibraryContentMultiple": "New content added (multiple)", + "NotificationOptionCameraImageUploaded": "Camera image uploaded", + "NotificationOptionUserLockedOut": "User locked out", + "NotificationOptionServerRestartRequired": "\u9700\u8981\u91cd\u65b0\u555f\u52d5", + "ViewTypePlaylists": "Playlists", + "ViewTypeMovies": "Movies", + "ViewTypeTvShows": "TV", + "ViewTypeGames": "\u904a\u6232", + "ViewTypeMusic": "Music", + "ViewTypeMusicGenres": "Genres", + "ViewTypeMusicArtists": "Artists", + "ViewTypeBoxSets": "\u85cf\u54c1", + "ViewTypeChannels": "Channels", + "ViewTypeLiveTV": "Live TV", + "ViewTypeLiveTvNowPlaying": "Now Airing", + "ViewTypeLatestGames": "\u6700\u8fd1\u904a\u6232", + "ViewTypeRecentlyPlayedGames": "\u6700\u8fd1\u64ad\u653e", + "ViewTypeGameFavorites": "Favorites", + "ViewTypeGameSystems": "\u904a\u6232\u7cfb\u7d71", + "ViewTypeGameGenres": "Genres", + "ViewTypeTvResume": "Resume", + "ViewTypeTvNextUp": "Next Up", + "ViewTypeTvLatest": "Latest", + "ViewTypeTvShowSeries": "\u96fb\u8996\u5287", + "ViewTypeTvGenres": "Genres", + "ViewTypeTvFavoriteSeries": "\u6211\u7684\u6700\u611b\u96fb\u8996\u5287", + "ViewTypeTvFavoriteEpisodes": "\u6211\u7684\u6700\u611b\u5287\u96c6", + "ViewTypeMovieResume": "Resume", + "ViewTypeMovieLatest": "Latest", + "ViewTypeMovieMovies": "Movies", + "ViewTypeMovieCollections": "\u85cf\u54c1", + "ViewTypeMovieFavorites": "Favorites", + "ViewTypeMovieGenres": "Genres", + "ViewTypeMusicLatest": "Latest", + "ViewTypeMusicPlaylists": "Playlists", + "ViewTypeMusicAlbums": "Albums", + "ViewTypeMusicAlbumArtists": "Album Artists", + "HeaderOtherDisplaySettings": "Display Settings", + "ViewTypeMusicSongs": "\u6b4c\u66f2", + "ViewTypeMusicFavorites": "Favorites", + "ViewTypeMusicFavoriteAlbums": "Favorite Albums", + "ViewTypeMusicFavoriteArtists": "Favorite Artists", + "ViewTypeMusicFavoriteSongs": "\u6211\u7684\u6700\u611b\u6b4c\u66f2", + "ViewTypeFolders": "Folders", + "ViewTypeLiveTvRecordingGroups": "Recordings", + "ViewTypeLiveTvChannels": "Channels", + "ScheduledTaskFailedWithName": "{0} failed", + "LabelRunningTimeValue": "Running time: {0}", + "ScheduledTaskStartedWithName": "{0} started", + "VersionNumber": "\u7248\u672c {0}", + "PluginInstalledWithName": "{0} was installed", + "PluginUpdatedWithName": "{0} was updated", + "PluginUninstalledWithName": "{0} was uninstalled", + "ItemAddedWithName": "{0} was added to the library", + "ItemRemovedWithName": "{0} was removed from the library", + "LabelIpAddressValue": "Ip address: {0}", + "DeviceOnlineWithName": "{0} is connected", + "UserOnlineFromDevice": "{0} is online from {1}", + "ProviderValue": "Provider: {0}", + "SubtitlesDownloadedForItem": "\u5df2\u7d93\u70ba {0} \u4e0b\u8f09\u4e86\u5b57\u5e55", + "UserConfigurationUpdatedWithName": "User configuration has been updated for {0}", + "UserCreatedWithName": "User {0} has been created", + "UserPasswordChangedWithName": "Password has been changed for user {0}", + "UserDeletedWithName": "User {0} has been deleted", + "MessageServerConfigurationUpdated": "Server configuration has been updated", + "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", + "MessageApplicationUpdated": "Emby Server has been updated", + "FailedLoginAttemptWithUserName": "Failed login attempt from {0}", + "AuthenticationSucceededWithUserName": "{0} successfully authenticated", + "DeviceOfflineWithName": "{0} has disconnected", + "UserLockedOutWithName": "User {0} has been locked out", + "UserOfflineFromDevice": "{0} has disconnected from {1}", + "UserStartedPlayingItemWithValues": "{0} has started playing {1}", + "UserStoppedPlayingItemWithValues": "{0} has stopped playing {1}", + "SubtitleDownloadFailureForItem": "\u70ba {0} \u4e0b\u8f09\u5b57\u5e55\u5931\u6557", + "HeaderUnidentified": "Unidentified", + "HeaderImagePrimary": "Primary", + "HeaderImageBackdrop": "Backdrop", + "HeaderImageLogo": "Logo", + "HeaderUserPrimaryImage": "User Image", + "HeaderOverview": "Overview", + "HeaderShortOverview": "Short Overview", + "HeaderType": "Type", + "HeaderSeverity": "Severity", + "HeaderUser": "User", + "HeaderName": "\u540d\u7a31", + "HeaderDate": "\u65e5\u671f", + "HeaderPremiereDate": "Premiere Date", + "HeaderDateAdded": "Date Added", + "HeaderReleaseDate": "Release date", + "HeaderRuntime": "Runtime", + "HeaderPlayCount": "Play Count", + "HeaderSeason": "\u5287\u96c6\u5b63\u5ea6", + "HeaderSeasonNumber": "\u5287\u96c6\u5b63\u5ea6\u6578\u76ee", + "HeaderSeries": "\u96fb\u8996\u5287\uff1a", + "HeaderNetwork": "Network", + "HeaderYear": "Year:", + "HeaderYears": "Years:", + "HeaderParentalRating": "Parental Rating", + "HeaderCommunityRating": "Community rating", + "HeaderTrailers": "Trailers", + "HeaderSpecials": "Specials", + "HeaderGameSystems": "\u904a\u6232\u7cfb\u7d71", + "HeaderPlayers": "Players:", + "HeaderAlbumArtists": "Album Artists", + "HeaderAlbums": "Albums", + "HeaderDisc": "Disc", + "HeaderTrack": "Track", + "HeaderAudio": "\u97f3\u8a0a", + "HeaderVideo": "\u5f71\u7247", + "HeaderEmbeddedImage": "Embedded image", + "HeaderResolution": "Resolution", + "HeaderSubtitles": "\u5b57\u5e55", + "HeaderGenres": "Genres", + "HeaderCountries": "Countries", + "HeaderStatus": "\u72c0\u614b", + "HeaderTracks": "Tracks", + "HeaderMusicArtist": "\u6b4c\u624b", + "HeaderLocked": "Locked", + "HeaderStudios": "Studios", + "HeaderActor": "Actors", + "HeaderComposer": "Composers", + "HeaderDirector": "Directors", + "HeaderGuestStar": "\u7279\u7d04\u660e\u661f", + "HeaderProducer": "Producers", + "HeaderWriter": "Writers", + "HeaderParentalRatings": "Parental Ratings", + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." +}
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/zh-TW.json b/MediaBrowser.Server.Implementations/Localization/Core/zh-TW.json index 787508176..2bcb5c132 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/zh-TW.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/zh-TW.json @@ -172,5 +172,6 @@ "HeaderProducer": "Producers", "HeaderWriter": "Writers", "HeaderParentalRatings": "Parental Ratings", - "HeaderCommunityRatings": "Community ratings" + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/LocalizationManager.cs b/MediaBrowser.Server.Implementations/Localization/LocalizationManager.cs index 8c893c8d1..cf6eb8f9d 100644 --- a/MediaBrowser.Server.Implementations/Localization/LocalizationManager.cs +++ b/MediaBrowser.Server.Implementations/Localization/LocalizationManager.cs @@ -12,6 +12,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Reflection; +using CommonIO; namespace MediaBrowser.Server.Implementations.Localization { @@ -58,7 +59,7 @@ namespace MediaBrowser.Server.Implementations.Localization var localizationPath = LocalizationPath; - Directory.CreateDirectory(localizationPath); + _fileSystem.CreateDirectory(localizationPath); var existingFiles = Directory.EnumerateFiles(localizationPath, "ratings-*.txt", SearchOption.TopDirectoryOnly) .Select(Path.GetFileName) @@ -212,7 +213,7 @@ namespace MediaBrowser.Server.Implementations.Localization /// <returns>Dictionary{System.StringParentalRating}.</returns> private void LoadRatings(string file) { - var dict = File.ReadAllLines(file).Select(i => + var dict = File.ReadAllLines(file).Select(i => { if (!string.IsNullOrWhiteSpace(i)) { diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index f0312cef6..99ef3f1f2 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -11,10 +11,9 @@ <AssemblyName>MediaBrowser.Server.Implementations</AssemblyName> <FileAlignment>512</FileAlignment> <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir> - <ProductVersion>10.0.0</ProductVersion> - <SchemaVersion>2.0</SchemaVersion> <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> - <RestorePackages>true</RestorePackages> + <ReleaseVersion> + </ReleaseVersion> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> @@ -24,7 +23,6 @@ <DefineConstants>DEBUG;TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> - <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <DebugType>none</DebugType> @@ -33,7 +31,6 @@ <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> - <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release Mono|AnyCPU' "> <DebugType>none</DebugType> @@ -42,34 +39,28 @@ <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> - <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> </PropertyGroup> <ItemGroup> - <Reference Include="Interfaces.IO"> - <HintPath>..\packages\Interfaces.IO.1.0.0.5\lib\portable-net45+sl4+wp71+win8+wpa81\Interfaces.IO.dll</HintPath> - </Reference> - <Reference Include="MediaBrowser.Naming, Version=1.0.5614.25103, Culture=neutral, processorArchitecture=MSIL"> + <Reference Include="CommonIO, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL"> <SpecificVersion>False</SpecificVersion> - <HintPath>..\packages\MediaBrowser.Naming.1.0.0.37\lib\portable-net45+sl4+wp71+win8+wpa81\MediaBrowser.Naming.dll</HintPath> + <HintPath>..\packages\CommonIO.1.0.0.5\lib\net45\CommonIO.dll</HintPath> </Reference> - <Reference Include="Mono.Nat, Version=1.2.24.0, Culture=neutral, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\packages\Mono.Nat.1.2.24.0\lib\net40\Mono.Nat.dll</HintPath> + <Reference Include="Interfaces.IO"> + <HintPath>..\packages\Interfaces.IO.1.0.0.5\lib\portable-net45+sl4+wp71+win8+wpa81\Interfaces.IO.dll</HintPath> </Reference> - <Reference Include="MoreLinq, Version=1.1.17511.0, Culture=neutral, PublicKeyToken=384d532d7e88985d, processorArchitecture=MSIL"> + <Reference Include="MediaBrowser.Naming, Version=1.0.5748.36038, Culture=neutral, processorArchitecture=MSIL"> <SpecificVersion>False</SpecificVersion> - <HintPath>..\packages\morelinq.1.1.0\lib\net35\MoreLinq.dll</HintPath> + <HintPath>..\packages\MediaBrowser.Naming.1.0.0.38\lib\portable-net45+sl4+wp71+win8+wpa81\MediaBrowser.Naming.dll</HintPath> </Reference> <Reference Include="Patterns.Logging"> <HintPath>..\packages\Patterns.Logging.1.0.0.2\lib\portable-net45+sl4+wp71+win8+wpa81\Patterns.Logging.dll</HintPath> - <Private>True</Private> </Reference> <Reference Include="ServiceStack.Api.Swagger"> <HintPath>..\ThirdParty\ServiceStack\ServiceStack.Api.Swagger.dll</HintPath> </Reference> - <Reference Include="SocketHttpListener, Version=1.0.5634.16042, Culture=neutral, processorArchitecture=MSIL"> + <Reference Include="SocketHttpListener, Version=1.0.5754.42244, Culture=neutral, processorArchitecture=MSIL"> <SpecificVersion>False</SpecificVersion> - <HintPath>..\packages\SocketHttpListener.1.0.0.7\lib\net45\SocketHttpListener.dll</HintPath> + <HintPath>..\packages\SocketHttpListener.1.0.0.10\lib\net45\SocketHttpListener.dll</HintPath> </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> @@ -100,6 +91,12 @@ <Reference Include="UniversalDetector"> <HintPath>..\ThirdParty\UniversalDetector\UniversalDetector.dll</HintPath> </Reference> + <Reference Include="Mono.Nat"> + <HintPath>..\packages\Mono.Nat.1.2.24.0\lib\net40\Mono.Nat.dll</HintPath> + </Reference> + <Reference Include="MoreLinq"> + <HintPath>..\packages\morelinq.1.1.1\lib\net35\MoreLinq.dll</HintPath> + </Reference> </ItemGroup> <ItemGroup> <Compile Include="..\SharedVersion.cs"> @@ -111,7 +108,6 @@ <Compile Include="Channels\ChannelConfigurations.cs" /> <Compile Include="Channels\ChannelDynamicMediaSourceProvider.cs" /> <Compile Include="Channels\ChannelImageProvider.cs" /> - <Compile Include="Channels\ChannelItemImageProvider.cs" /> <Compile Include="Channels\ChannelManager.cs" /> <Compile Include="Channels\ChannelPostScanTask.cs" /> <Compile Include="Channels\RefreshChannelsScheduledTask.cs" /> @@ -240,8 +236,10 @@ <Compile Include="Localization\LocalizationManager.cs" /> <Compile Include="Logging\PatternsLogger.cs" /> <Compile Include="MediaEncoder\EncodingManager.cs" /> + <Compile Include="Notifications\IConfigurableNotificationService.cs" /> <Compile Include="Persistence\BaseSqliteRepository.cs" /> <Compile Include="Persistence\CleanDatabaseScheduledTask.cs" /> + <Compile Include="Persistence\MediaStreamColumns.cs" /> <Compile Include="Social\SharingManager.cs" /> <Compile Include="Social\SharingRepository.cs" /> <Compile Include="Sorting\StartDateComparer.cs" /> @@ -255,10 +253,8 @@ <Compile Include="Notifications\InternalNotificationService.cs" /> <Compile Include="Notifications\NotificationConfigurationFactory.cs" /> <Compile Include="Notifications\NotificationManager.cs" /> - <Compile Include="Persistence\SqliteChapterRepository.cs" /> <Compile Include="Persistence\SqliteExtensions.cs" /> <Compile Include="Persistence\SqliteFileOrganizationRepository.cs" /> - <Compile Include="Persistence\SqliteMediaStreamsRepository.cs" /> <Compile Include="Notifications\SqliteNotificationsRepository.cs" /> <Compile Include="Persistence\SqliteProviderInfoRepository.cs" /> <Compile Include="Persistence\TypeMapper.cs" /> @@ -334,7 +330,6 @@ <Compile Include="Sync\SyncRepository.cs" /> <Compile Include="Sync\SyncConvertScheduledTask.cs" /> <Compile Include="Sync\TargetDataProvider.cs" /> - <Compile Include="Themes\AppThemeManager.cs" /> <Compile Include="TV\TVSeriesManager.cs" /> <Compile Include="Udp\UdpMessageReceivedEventArgs.cs" /> <Compile Include="Udp\UdpServer.cs" /> @@ -413,6 +408,7 @@ <EmbeddedResource Include="Localization\Core\vi.json" /> <EmbeddedResource Include="Localization\Core\zh-CN.json" /> <EmbeddedResource Include="Localization\Core\zh-TW.json" /> + <EmbeddedResource Include="Localization\Core\zh-HK.json" /> <None Include="packages.config" /> </ItemGroup> <ItemGroup> diff --git a/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs b/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs index 6b99883a5..bd634ed73 100644 --- a/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -14,6 +14,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; namespace MediaBrowser.Server.Implementations.MediaEncoder { @@ -134,11 +135,11 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder var protocol = MediaProtocol.File; - var inputPath = MediaEncoderHelpers.GetInputArgument(video.Path, protocol, null, video.PlayableStreamFileNames); + var inputPath = MediaEncoderHelpers.GetInputArgument(_fileSystem, video.Path, protocol, null, video.PlayableStreamFileNames); try { - Directory.CreateDirectory(Path.GetDirectoryName(path)); + _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); using (var stream = await _encoder.ExtractVideoImage(inputPath, protocol, video.Video3DFormat, time, cancellationToken).ConfigureAwait(false)) { @@ -194,7 +195,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder try { - return Directory.EnumerateFiles(path) + return _fileSystem.GetFilePaths(path) .ToList(); } catch (DirectoryNotFoundException) diff --git a/MediaBrowser.Server.Implementations/News/NewsEntryPoint.cs b/MediaBrowser.Server.Implementations/News/NewsEntryPoint.cs index 619384b6f..e8f910f81 100644 --- a/MediaBrowser.Server.Implementations/News/NewsEntryPoint.cs +++ b/MediaBrowser.Server.Implementations/News/NewsEntryPoint.cs @@ -16,6 +16,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Xml; +using CommonIO; namespace MediaBrowser.Server.Implementations.News { @@ -71,7 +72,7 @@ namespace MediaBrowser.Server.Implementations.News { DateTime? lastUpdate = null; - if (File.Exists(path)) + if (_fileSystem.FileExists(path)) { lastUpdate = _fileSystem.GetLastWriteTimeUtc(path); } @@ -79,7 +80,8 @@ namespace MediaBrowser.Server.Implementations.News var requestOptions = new HttpRequestOptions { Url = "http://emby.media/community/index.php?/blog/rss/1-media-browser-developers-blog", - Progress = new Progress<double>() + Progress = new Progress<double>(), + UserAgent = "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.42 Safari/537.36" }; using (var stream = await _httpClient.Get(requestOptions).ConfigureAwait(false)) diff --git a/MediaBrowser.Server.Implementations/Notifications/IConfigurableNotificationService.cs b/MediaBrowser.Server.Implementations/Notifications/IConfigurableNotificationService.cs new file mode 100644 index 000000000..5c4f400b0 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Notifications/IConfigurableNotificationService.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.Notifications +{ + public interface IConfigurableNotificationService + { + bool IsHidden { get; } + bool IsEnabled(string notificationType); + } +} diff --git a/MediaBrowser.Server.Implementations/Notifications/InternalNotificationService.cs b/MediaBrowser.Server.Implementations/Notifications/InternalNotificationService.cs index 56cb52f10..4a625f0fb 100644 --- a/MediaBrowser.Server.Implementations/Notifications/InternalNotificationService.cs +++ b/MediaBrowser.Server.Implementations/Notifications/InternalNotificationService.cs @@ -3,10 +3,11 @@ using MediaBrowser.Controller.Notifications; using MediaBrowser.Model.Notifications; using System.Threading; using System.Threading.Tasks; +using System; namespace MediaBrowser.Server.Implementations.Notifications { - public class InternalNotificationService : INotificationService + public class InternalNotificationService : INotificationService, IConfigurableNotificationService { private readonly INotificationsRepository _repo; @@ -36,6 +37,24 @@ namespace MediaBrowser.Server.Implementations.Notifications public bool IsEnabledForUser(User user) { + return user.Policy.IsAdministrator; + } + + public bool IsHidden + { + get { return true; } + } + + public bool IsEnabled(string notificationType) + { + if (notificationType.IndexOf("playback", StringComparison.OrdinalIgnoreCase) != -1) + { + return false; + } + if (notificationType.IndexOf("newlibrarycontent", StringComparison.OrdinalIgnoreCase) != -1) + { + return false; + } return true; } } diff --git a/MediaBrowser.Server.Implementations/Notifications/NotificationManager.cs b/MediaBrowser.Server.Implementations/Notifications/NotificationManager.cs index 1ff928cd5..f19ff8a5f 100644 --- a/MediaBrowser.Server.Implementations/Notifications/NotificationManager.cs +++ b/MediaBrowser.Server.Implementations/Notifications/NotificationManager.cs @@ -230,8 +230,19 @@ namespace MediaBrowser.Server.Implementations.Notifications private bool IsEnabled(INotificationService service, string notificationType) { - return string.IsNullOrEmpty(notificationType) || - GetConfiguration().IsServiceEnabled(service.Name, notificationType); + if (string.IsNullOrEmpty(notificationType)) + { + return true; + } + + var configurable = service as IConfigurableNotificationService; + + if (configurable != null) + { + return configurable.IsEnabled(notificationType); + } + + return GetConfiguration().IsServiceEnabled(service.Name, notificationType); } public void AddParts(IEnumerable<INotificationService> services, IEnumerable<INotificationTypeFactory> notificationTypeFactories) @@ -268,7 +279,13 @@ namespace MediaBrowser.Server.Implementations.Notifications public IEnumerable<NotificationServiceInfo> GetNotificationServices() { - return _services.Select(i => new NotificationServiceInfo + return _services.Where(i => + { + var configurable = i as IConfigurableNotificationService; + + return configurable == null || !configurable.IsHidden; + + }).Select(i => new NotificationServiceInfo { Name = i.Name, Id = i.Name.GetMD5().ToString("N") diff --git a/MediaBrowser.Server.Implementations/Persistence/BaseSqliteRepository.cs b/MediaBrowser.Server.Implementations/Persistence/BaseSqliteRepository.cs index 15d76fb60..cac112b6c 100644 --- a/MediaBrowser.Server.Implementations/Persistence/BaseSqliteRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/BaseSqliteRepository.cs @@ -46,11 +46,6 @@ namespace MediaBrowser.Server.Implementations.Persistence } } - protected virtual void DisposeInternal() - { - - } - protected abstract void CloseConnection(); } } diff --git a/MediaBrowser.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs b/MediaBrowser.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs index 9f87483ba..60b8c00bd 100644 --- a/MediaBrowser.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs +++ b/MediaBrowser.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs @@ -5,27 +5,32 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using CommonIO; +using MediaBrowser.Controller.Entities.Audio; namespace MediaBrowser.Server.Implementations.Persistence { - class CleanDatabaseScheduledTask : IScheduledTask + public class CleanDatabaseScheduledTask : IScheduledTask { private readonly ILibraryManager _libraryManager; private readonly IItemRepository _itemRepo; private readonly ILogger _logger; private readonly IServerConfigurationManager _config; + private readonly IFileSystem _fileSystem; - public CleanDatabaseScheduledTask(ILibraryManager libraryManager, IItemRepository itemRepo, ILogger logger, IServerConfigurationManager config) + public CleanDatabaseScheduledTask(ILibraryManager libraryManager, IItemRepository itemRepo, ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem) { _libraryManager = libraryManager; _itemRepo = itemRepo; _logger = logger; _config = config; + _fileSystem = fileSystem; } public string Name @@ -46,15 +51,18 @@ namespace MediaBrowser.Server.Implementations.Persistence public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress) { var innerProgress = new ActionableProgress<double>(); - innerProgress.RegisterAction(p => progress.Report(.95 * p)); + innerProgress.RegisterAction(p => progress.Report(.4 * p)); await UpdateToLatestSchema(cancellationToken, innerProgress).ConfigureAwait(false); innerProgress = new ActionableProgress<double>(); - innerProgress.RegisterAction(p => progress.Report(95 + (.05 * p))); - - //await CleanDeadItems(cancellationToken, innerProgress).ConfigureAwait(false); + innerProgress.RegisterAction(p => progress.Report(40 + (.05 * p))); + await CleanDeadItems(cancellationToken, innerProgress).ConfigureAwait(false); + progress.Report(45); + innerProgress = new ActionableProgress<double>(); + innerProgress.RegisterAction(p => progress.Report(45 + (.55 * p))); + await CleanDeletedItems(cancellationToken, innerProgress).ConfigureAwait(false); progress.Report(100); } @@ -77,6 +85,12 @@ namespace MediaBrowser.Server.Implementations.Persistence { cancellationToken.ThrowIfCancellationRequested(); + if (itemId == Guid.Empty) + { + // Somehow some invalid data got into the db. It probably predates the boundary checking + continue; + } + var item = _libraryManager.GetItemById(itemId); if (item != null) @@ -147,11 +161,69 @@ namespace MediaBrowser.Server.Implementations.Persistence progress.Report(100); } + private async Task CleanDeletedItems(CancellationToken cancellationToken, IProgress<double> progress) + { + var result = _itemRepo.GetItemIdsWithPath(new InternalItemsQuery + { + IsOffline = false, + LocationType = LocationType.FileSystem, + //Limit = limit, + + // These have their own cleanup routines + ExcludeItemTypes = new[] { typeof(Person).Name, typeof(Genre).Name, typeof(MusicGenre).Name, typeof(GameGenre).Name, typeof(Studio).Name, typeof(Year).Name } + }); + + var numComplete = 0; + var numItems = result.Items.Length; + + foreach (var item in result.Items) + { + cancellationToken.ThrowIfCancellationRequested(); + + var path = item.Item2; + + try + { + if (_fileSystem.FileExists(path) || _fileSystem.DirectoryExists(path)) + { + continue; + } + + var libraryItem = _libraryManager.GetItemById(item.Item1); + + if (Folder.IsPathOffline(path)) + { + libraryItem.IsOffline = true; + await libraryItem.UpdateToRepository(ItemUpdateType.None, cancellationToken).ConfigureAwait(false); + continue; + } + + await _libraryManager.DeleteItem(libraryItem, new DeleteOptions + { + DeleteFileLocation = false + }); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.ErrorException("Error in CleanDeletedItems. File {0}", ex, path); + } + + numComplete++; + double percent = numComplete; + percent /= numItems; + progress.Report(percent * 100); + } + } + public IEnumerable<ITaskTrigger> GetDefaultTriggers() { return new ITaskTrigger[] { - new IntervalTrigger{ Interval = TimeSpan.FromDays(1)} + new IntervalTrigger{ Interval = TimeSpan.FromHours(24)} }; } } diff --git a/MediaBrowser.Server.Implementations/Persistence/MediaStreamColumns.cs b/MediaBrowser.Server.Implementations/Persistence/MediaStreamColumns.cs new file mode 100644 index 000000000..f54e702d6 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Persistence/MediaStreamColumns.cs @@ -0,0 +1,219 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Model.Logging; + +namespace MediaBrowser.Server.Implementations.Persistence +{ + public class MediaStreamColumns + { + private readonly IDbConnection _connection; + private readonly ILogger _logger; + + public MediaStreamColumns(IDbConnection connection, ILogger logger) + { + _connection = connection; + _logger = logger; + } + + public void AddColumns() + { + AddPixelFormatColumnCommand(); + AddBitDepthCommand(); + AddIsAnamorphicColumn(); + AddIsCabacColumn(); + AddKeyFramesColumn(); + AddRefFramesCommand(); + } + + private void AddPixelFormatColumnCommand() + { + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA table_info(mediastreams)"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + if (!reader.IsDBNull(1)) + { + var name = reader.GetString(1); + + if (string.Equals(name, "PixelFormat", StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + + var builder = new StringBuilder(); + + builder.AppendLine("alter table mediastreams"); + builder.AppendLine("add column PixelFormat TEXT"); + + _connection.RunQueries(new[] { builder.ToString() }, _logger); + } + + private void AddBitDepthCommand() + { + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA table_info(mediastreams)"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + if (!reader.IsDBNull(1)) + { + var name = reader.GetString(1); + + if (string.Equals(name, "BitDepth", StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + + var builder = new StringBuilder(); + + builder.AppendLine("alter table mediastreams"); + builder.AppendLine("add column BitDepth INT NULL"); + + _connection.RunQueries(new[] { builder.ToString() }, _logger); + } + + private void AddRefFramesCommand() + { + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA table_info(mediastreams)"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + if (!reader.IsDBNull(1)) + { + var name = reader.GetString(1); + + if (string.Equals(name, "RefFrames", StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + + var builder = new StringBuilder(); + + builder.AppendLine("alter table mediastreams"); + builder.AppendLine("add column RefFrames INT NULL"); + + _connection.RunQueries(new[] { builder.ToString() }, _logger); + } + + private void AddIsCabacColumn() + { + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA table_info(mediastreams)"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + if (!reader.IsDBNull(1)) + { + var name = reader.GetString(1); + + if (string.Equals(name, "IsCabac", StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + + var builder = new StringBuilder(); + + builder.AppendLine("alter table mediastreams"); + builder.AppendLine("add column IsCabac BIT NULL"); + + _connection.RunQueries(new[] { builder.ToString() }, _logger); + } + + private void AddKeyFramesColumn() + { + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA table_info(mediastreams)"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + if (!reader.IsDBNull(1)) + { + var name = reader.GetString(1); + + if (string.Equals(name, "KeyFrames", StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + + var builder = new StringBuilder(); + + builder.AppendLine("alter table mediastreams"); + builder.AppendLine("add column KeyFrames TEXT NULL"); + + _connection.RunQueries(new[] { builder.ToString() }, _logger); + } + + private void AddIsAnamorphicColumn() + { + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA table_info(mediastreams)"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + if (!reader.IsDBNull(1)) + { + var name = reader.GetString(1); + + if (string.Equals(name, "IsAnamorphic", StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + + var builder = new StringBuilder(); + + builder.AppendLine("alter table mediastreams"); + builder.AppendLine("add column IsAnamorphic BIT NULL"); + + _connection.RunQueries(new[] { builder.ToString() }, _logger); + } + + } +} diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteChapterRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteChapterRepository.cs deleted file mode 100644 index 075ef4239..000000000 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteChapterRepository.cs +++ /dev/null @@ -1,304 +0,0 @@ -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Logging; -using System; -using System.Collections.Generic; -using System.Data; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Server.Implementations.Persistence -{ - public class SqliteChapterRepository - { - private IDbConnection _connection; - - private readonly ILogger _logger; - - private IDbCommand _deleteChaptersCommand; - private IDbCommand _saveChapterCommand; - - /// <summary> - /// Initializes a new instance of the <see cref="SqliteItemRepository" /> class. - /// </summary> - /// <param name="connection">The connection.</param> - /// <param name="logManager">The log manager.</param> - /// <exception cref="System.ArgumentNullException">appPaths - /// or - /// jsonSerializer</exception> - public SqliteChapterRepository(IDbConnection connection, ILogManager logManager) - { - _connection = connection; - - _logger = logManager.GetLogger(GetType().Name); - } - - /// <summary> - /// Opens the connection to the database - /// </summary> - /// <returns>Task.</returns> - public void Initialize() - { - string[] queries = { - - "create table if not exists chapters (ItemId GUID, ChapterIndex INT, StartPositionTicks BIGINT, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))", - "create index if not exists idx_chapters on chapters(ItemId, ChapterIndex)", - - //pragmas - "pragma temp_store = memory", - - "pragma shrink_memory" - }; - - _connection.RunQueries(queries, _logger); - - PrepareStatements(); - } - - /// <summary> - /// The _write lock - /// </summary> - private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1); - - /// <summary> - /// Prepares the statements. - /// </summary> - private void PrepareStatements() - { - _deleteChaptersCommand = _connection.CreateCommand(); - _deleteChaptersCommand.CommandText = "delete from chapters where ItemId=@ItemId"; - _deleteChaptersCommand.Parameters.Add(_deleteChaptersCommand, "@ItemId"); - - _saveChapterCommand = _connection.CreateCommand(); - _saveChapterCommand.CommandText = "replace into chapters (ItemId, ChapterIndex, StartPositionTicks, Name, ImagePath) values (@ItemId, @ChapterIndex, @StartPositionTicks, @Name, @ImagePath)"; - - _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@ItemId"); - _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@ChapterIndex"); - _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@StartPositionTicks"); - _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@Name"); - _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@ImagePath"); - } - - /// <summary> - /// Gets chapters for an item - /// </summary> - /// <param name="id">The id.</param> - /// <returns>IEnumerable{ChapterInfo}.</returns> - /// <exception cref="System.ArgumentNullException">id</exception> - public IEnumerable<ChapterInfo> GetChapters(Guid id) - { - if (id == Guid.Empty) - { - throw new ArgumentNullException("id"); - } - - using (var cmd = _connection.CreateCommand()) - { - cmd.CommandText = "select StartPositionTicks,Name,ImagePath from Chapters where ItemId = @ItemId order by ChapterIndex asc"; - - cmd.Parameters.Add(cmd, "@ItemId", DbType.Guid).Value = id; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) - { - while (reader.Read()) - { - yield return GetChapter(reader); - } - } - } - } - - /// <summary> - /// Gets a single chapter for an item - /// </summary> - /// <param name="id">The id.</param> - /// <param name="index">The index.</param> - /// <returns>ChapterInfo.</returns> - /// <exception cref="System.ArgumentNullException">id</exception> - public ChapterInfo GetChapter(Guid id, int index) - { - if (id == Guid.Empty) - { - throw new ArgumentNullException("id"); - } - - using (var cmd = _connection.CreateCommand()) - { - cmd.CommandText = "select StartPositionTicks,Name,ImagePath from Chapters where ItemId = @ItemId and ChapterIndex=@ChapterIndex"; - - cmd.Parameters.Add(cmd, "@ItemId", DbType.Guid).Value = id; - cmd.Parameters.Add(cmd, "@ChapterIndex", DbType.Int32).Value = index; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) - { - if (reader.Read()) - { - return GetChapter(reader); - } - } - return null; - } - } - - /// <summary> - /// Gets the chapter. - /// </summary> - /// <param name="reader">The reader.</param> - /// <returns>ChapterInfo.</returns> - private ChapterInfo GetChapter(IDataReader reader) - { - var chapter = new ChapterInfo - { - StartPositionTicks = reader.GetInt64(0) - }; - - if (!reader.IsDBNull(1)) - { - chapter.Name = reader.GetString(1); - } - - if (!reader.IsDBNull(2)) - { - chapter.ImagePath = reader.GetString(2); - } - - return chapter; - } - - /// <summary> - /// Saves the chapters. - /// </summary> - /// <param name="id">The id.</param> - /// <param name="chapters">The chapters.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - /// <exception cref="System.ArgumentNullException"> - /// id - /// or - /// chapters - /// or - /// cancellationToken - /// </exception> - public async Task SaveChapters(Guid id, IEnumerable<ChapterInfo> chapters, CancellationToken cancellationToken) - { - if (id == Guid.Empty) - { - throw new ArgumentNullException("id"); - } - - if (chapters == null) - { - throw new ArgumentNullException("chapters"); - } - - cancellationToken.ThrowIfCancellationRequested(); - - await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); - - IDbTransaction transaction = null; - - try - { - transaction = _connection.BeginTransaction(); - - // First delete chapters - _deleteChaptersCommand.GetParameter(0).Value = id; - - _deleteChaptersCommand.Transaction = transaction; - - _deleteChaptersCommand.ExecuteNonQuery(); - - var index = 0; - - foreach (var chapter in chapters) - { - cancellationToken.ThrowIfCancellationRequested(); - - _saveChapterCommand.GetParameter(0).Value = id; - _saveChapterCommand.GetParameter(1).Value = index; - _saveChapterCommand.GetParameter(2).Value = chapter.StartPositionTicks; - _saveChapterCommand.GetParameter(3).Value = chapter.Name; - _saveChapterCommand.GetParameter(4).Value = chapter.ImagePath; - - _saveChapterCommand.Transaction = transaction; - - _saveChapterCommand.ExecuteNonQuery(); - - index++; - } - - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - catch (Exception e) - { - _logger.ErrorException("Failed to save chapters:", e); - - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); - } - - _writeLock.Release(); - } - } - - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private readonly object _disposeLock = new object(); - - /// <summary> - /// Releases unmanaged and - optionally - managed resources. - /// </summary> - /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool dispose) - { - if (dispose) - { - try - { - lock (_disposeLock) - { - if (_connection != null) - { - if (_connection.IsOpen()) - { - _connection.Close(); - } - - _connection.Dispose(); - _connection = null; - } - } - } - catch (Exception ex) - { - _logger.ErrorException("Error disposing database", ex); - } - } - } - } -} diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteExtensions.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteExtensions.cs index f8cb6c9f4..011cbce1c 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteExtensions.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteExtensions.cs @@ -154,6 +154,15 @@ namespace MediaBrowser.Server.Implementations.Persistence return connection; } + public static void Attach(IDbConnection db, string path, string alias) + { + using (var cmd = db.CreateCommand()) + { + cmd.CommandText = string.Format("attach '{0}' as {1};", path, alias); + cmd.ExecuteNonQuery(); + } + } + /// <summary> /// Serializes to bytes. /// </summary> diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs index 00ebf7ea6..7fbd9ee89 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs @@ -18,6 +18,7 @@ using System.Linq; using System.Runtime.Serialization; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Controller.Channels; namespace MediaBrowser.Server.Implementations.Persistence { @@ -62,9 +63,6 @@ namespace MediaBrowser.Server.Implementations.Persistence private readonly string _criticReviewsPath; - private SqliteChapterRepository _chapterRepository; - private SqliteMediaStreamsRepository _mediaStreamsRepository; - private IDbCommand _deleteChildrenCommand; private IDbCommand _saveChildrenCommand; private IDbCommand _deleteItemCommand; @@ -72,7 +70,13 @@ namespace MediaBrowser.Server.Implementations.Persistence private IDbCommand _deletePeopleCommand; private IDbCommand _savePersonCommand; - private const int LatestSchemaVersion = 6; + private IDbCommand _deleteChaptersCommand; + private IDbCommand _saveChapterCommand; + + private IDbCommand _deleteStreamsCommand; + private IDbCommand _saveStreamCommand; + + private const int LatestSchemaVersion = 13; /// <summary> /// Initializes a new instance of the <see cref="SqliteItemRepository"/> class. @@ -102,16 +106,10 @@ namespace MediaBrowser.Server.Implementations.Persistence _criticReviewsPath = Path.Combine(_appPaths.DataPath, "critic-reviews"); _logger = logManager.GetLogger(GetType().Name); - - var chapterDbFile = Path.Combine(_appPaths.DataPath, "chapters.db"); - var chapterConnection = SqliteExtensions.ConnectToDb(chapterDbFile, _logger).Result; - _chapterRepository = new SqliteChapterRepository(chapterConnection, logManager); - - var mediaStreamsDbFile = Path.Combine(_appPaths.DataPath, "mediainfo.db"); - var mediaStreamsConnection = SqliteExtensions.ConnectToDb(mediaStreamsDbFile, _logger).Result; - _mediaStreamsRepository = new SqliteMediaStreamsRepository(mediaStreamsConnection, logManager); } + private const string ChaptersTableName = "Chapters2"; + /// <summary> /// Opens the connection to the database /// </summary> @@ -122,6 +120,9 @@ namespace MediaBrowser.Server.Implementations.Persistence _connection = await SqliteExtensions.ConnectToDb(dbFile, _logger).ConfigureAwait(false); + var createMediaStreamsTableCommand + = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, IsCabac BIT NULL, KeyFrames TEXT NULL, PRIMARY KEY (ItemId, StreamIndex))"; + string[] queries = { "create table if not exists TypedBaseItems (guid GUID primary key, type TEXT, data BLOB)", @@ -132,6 +133,12 @@ namespace MediaBrowser.Server.Implementations.Persistence "create table if not exists People (ItemId GUID, Name TEXT NOT NULL, Role TEXT, PersonType TEXT, SortOrder int, ListOrder int)", + "create table if not exists "+ChaptersTableName+" (ItemId GUID, ChapterIndex INT, StartPositionTicks BIGINT, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))", + "create index if not exists idx_"+ChaptersTableName+" on "+ChaptersTableName+"(ItemId, ChapterIndex)", + + createMediaStreamsTableCommand, + "create index if not exists idx_mediastreams on mediastreams(ItemId, StreamIndex)", + //pragmas "pragma temp_store = memory", @@ -175,11 +182,81 @@ namespace MediaBrowser.Server.Implementations.Persistence _connection.AddColumn(_logger, "TypedBaseItems", "ForcedSortName", "Text"); _connection.AddColumn(_logger, "TypedBaseItems", "IsOffline", "BIT"); + _connection.AddColumn(_logger, "TypedBaseItems", "LocationType", "Text"); + + _connection.AddColumn(_logger, "TypedBaseItems", "IsSeries", "BIT"); + _connection.AddColumn(_logger, "TypedBaseItems", "IsLive", "BIT"); + _connection.AddColumn(_logger, "TypedBaseItems", "IsNews", "BIT"); + _connection.AddColumn(_logger, "TypedBaseItems", "IsPremiere", "BIT"); + + _connection.AddColumn(_logger, "TypedBaseItems", "EpisodeTitle", "Text"); + _connection.AddColumn(_logger, "TypedBaseItems", "IsRepeat", "BIT"); + + _connection.AddColumn(_logger, "TypedBaseItems", "PreferredMetadataLanguage", "Text"); + _connection.AddColumn(_logger, "TypedBaseItems", "PreferredMetadataCountryCode", "Text"); + _connection.AddColumn(_logger, "TypedBaseItems", "IsHD", "BIT"); + _connection.AddColumn(_logger, "TypedBaseItems", "ExternalEtag", "Text"); + _connection.AddColumn(_logger, "TypedBaseItems", "DateLastRefreshed", "DATETIME"); PrepareStatements(); - _mediaStreamsRepository.Initialize(); - _chapterRepository.Initialize(); + new MediaStreamColumns(_connection, _logger).AddColumns(); + + var chapterDbFile = Path.Combine(_appPaths.DataPath, "chapters.db"); + if (File.Exists(chapterDbFile)) + { + MigrateChapters(chapterDbFile); + } + + var mediaStreamsDbFile = Path.Combine(_appPaths.DataPath, "mediainfo.db"); + if (File.Exists(mediaStreamsDbFile)) + { + MigrateMediaStreams(mediaStreamsDbFile); + } + } + + private void MigrateMediaStreams(string file) + { + var backupFile = file + ".bak"; + File.Copy(file, backupFile, true); + SqliteExtensions.Attach(_connection, backupFile, "MediaInfoOld"); + + var columns = string.Join(",", _mediaStreamSaveColumns); + + string[] queries = { + "REPLACE INTO mediastreams("+columns+") SELECT "+columns+" FROM MediaInfoOld.mediastreams;" + }; + + try + { + _connection.RunQueries(queries, _logger); + File.Delete(file); + } + catch (Exception ex) + { + _logger.ErrorException("Error migrating media info database", ex); + } + } + + private void MigrateChapters(string file) + { + var backupFile = file + ".bak"; + File.Copy(file, backupFile, true); + SqliteExtensions.Attach(_connection, backupFile, "ChaptersOld"); + + string[] queries = { + "REPLACE INTO "+ChaptersTableName+"(ItemId, ChapterIndex, StartPositionTicks, Name, ImagePath) SELECT ItemId, ChapterIndex, StartPositionTicks, Name, ImagePath FROM ChaptersOld.Chapters;" + }; + + try + { + _connection.RunQueries(queries, _logger); + File.Delete(file); + } + catch (Exception ex) + { + _logger.ErrorException("Error migrating chapter database", ex); + } } /// <summary> @@ -187,11 +264,63 @@ namespace MediaBrowser.Server.Implementations.Persistence /// </summary> private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1); - private string[] _retriveItemColumns = + private readonly string[] _retriveItemColumns = { "type", "data", - "IsOffline" + "StartDate", + "EndDate", + "IsOffline", + "ChannelId", + "IsMovie", + "IsSports", + "IsKids", + "IsSeries", + "IsLive", + "IsNews", + "IsPremiere", + "EpisodeTitle", + "IsRepeat", + "CommunityRating", + "CustomRating", + "IndexNumber", + "IsLocked", + "PreferredMetadataLanguage", + "PreferredMetadataCountryCode", + "IsHD", + "ExternalEtag", + "DateLastRefreshed" + }; + + private readonly string[] _mediaStreamSaveColumns = + { + "ItemId", + "StreamIndex", + "StreamType", + "Codec", + "Language", + "ChannelLayout", + "Profile", + "AspectRatio", + "Path", + "IsInterlaced", + "BitRate", + "Channels", + "SampleRate", + "IsDefault", + "IsForced", + "IsExternal", + "Height", + "Width", + "AverageFrameRate", + "RealFrameRate", + "Level", + "PixelFormat", + "BitDepth", + "IsAnamorphic", + "RefFrames", + "IsCabac", + "KeyFrames" }; /// <summary> @@ -211,6 +340,12 @@ namespace MediaBrowser.Server.Implementations.Persistence "IsKids", "IsMovie", "IsSports", + "IsSeries", + "IsLive", + "IsNews", + "IsPremiere", + "EpisodeTitle", + "IsRepeat", "CommunityRating", "CustomRating", "IndexNumber", @@ -235,7 +370,13 @@ namespace MediaBrowser.Server.Implementations.Persistence "DateCreated", "DateModified", "ForcedSortName", - "IsOffline" + "IsOffline", + "LocationType", + "PreferredMetadataLanguage", + "PreferredMetadataCountryCode", + "IsHD", + "ExternalEtag", + "DateLastRefreshed" }; _saveItemCommand = _connection.CreateCommand(); _saveItemCommand.CommandText = "replace into TypedBaseItems (" + string.Join(",", saveColumns.ToArray()) + ") values ("; @@ -265,6 +406,7 @@ namespace MediaBrowser.Server.Implementations.Persistence _saveChildrenCommand.Parameters.Add(_saveChildrenCommand, "@ParentId"); _saveChildrenCommand.Parameters.Add(_saveChildrenCommand, "@ItemId"); + // People _deletePeopleCommand = _connection.CreateCommand(); _deletePeopleCommand.CommandText = "delete from People where ItemId=@Id"; _deletePeopleCommand.Parameters.Add(_deletePeopleCommand, "@Id"); @@ -277,6 +419,36 @@ namespace MediaBrowser.Server.Implementations.Persistence _savePersonCommand.Parameters.Add(_savePersonCommand, "@PersonType"); _savePersonCommand.Parameters.Add(_savePersonCommand, "@SortOrder"); _savePersonCommand.Parameters.Add(_savePersonCommand, "@ListOrder"); + + // Chapters + _deleteChaptersCommand = _connection.CreateCommand(); + _deleteChaptersCommand.CommandText = "delete from " + ChaptersTableName + " where ItemId=@ItemId"; + _deleteChaptersCommand.Parameters.Add(_deleteChaptersCommand, "@ItemId"); + + _saveChapterCommand = _connection.CreateCommand(); + _saveChapterCommand.CommandText = "replace into " + ChaptersTableName + " (ItemId, ChapterIndex, StartPositionTicks, Name, ImagePath) values (@ItemId, @ChapterIndex, @StartPositionTicks, @Name, @ImagePath)"; + + _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@ItemId"); + _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@ChapterIndex"); + _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@StartPositionTicks"); + _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@Name"); + _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@ImagePath"); + + // MediaStreams + _deleteStreamsCommand = _connection.CreateCommand(); + _deleteStreamsCommand.CommandText = "delete from mediastreams where ItemId=@ItemId"; + _deleteStreamsCommand.Parameters.Add(_deleteStreamsCommand, "@ItemId"); + + _saveStreamCommand = _connection.CreateCommand(); + + _saveStreamCommand.CommandText = string.Format("replace into mediastreams ({0}) values ({1})", + string.Join(",", _mediaStreamSaveColumns), + string.Join(",", _mediaStreamSaveColumns.Select(i => "@" + i).ToArray())); + + foreach (var col in _mediaStreamSaveColumns) + { + _saveStreamCommand.Parameters.Add(_saveStreamCommand, "@" + col); + } } /// <summary> @@ -357,12 +529,24 @@ namespace MediaBrowser.Server.Implementations.Persistence _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsKids; _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsMovie; _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsSports; + _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsSeries; + _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsLive; + _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsNews; + _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsPremiere; + _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.EpisodeTitle; + _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsRepeat; } else { _saveItemCommand.GetParameter(index++).Value = null; _saveItemCommand.GetParameter(index++).Value = null; _saveItemCommand.GetParameter(index++).Value = null; + _saveItemCommand.GetParameter(index++).Value = null; + _saveItemCommand.GetParameter(index++).Value = null; + _saveItemCommand.GetParameter(index++).Value = null; + _saveItemCommand.GetParameter(index++).Value = null; + _saveItemCommand.GetParameter(index++).Value = null; + _saveItemCommand.GetParameter(index++).Value = null; } _saveItemCommand.GetParameter(index++).Value = item.CommunityRating; @@ -405,6 +589,21 @@ namespace MediaBrowser.Server.Implementations.Persistence _saveItemCommand.GetParameter(index++).Value = item.ForcedSortName; _saveItemCommand.GetParameter(index++).Value = item.IsOffline; + _saveItemCommand.GetParameter(index++).Value = item.LocationType.ToString(); + + _saveItemCommand.GetParameter(index++).Value = item.PreferredMetadataLanguage; + _saveItemCommand.GetParameter(index++).Value = item.PreferredMetadataCountryCode; + _saveItemCommand.GetParameter(index++).Value = item.IsHD; + _saveItemCommand.GetParameter(index++).Value = item.ExternalEtag; + + if (item.DateLastRefreshed == default(DateTime)) + { + _saveItemCommand.GetParameter(index++).Value = null; + } + else + { + _saveItemCommand.GetParameter(index++).Value = item.DateLastRefreshed; + } _saveItemCommand.Transaction = transaction; @@ -511,7 +710,120 @@ namespace MediaBrowser.Server.Implementations.Persistence if (!reader.IsDBNull(2)) { - item.IsOffline = reader.GetBoolean(2); + var hasStartDate = item as IHasStartDate; + if (hasStartDate != null) + { + hasStartDate.StartDate = reader.GetDateTime(2).ToUniversalTime(); + } + } + + if (!reader.IsDBNull(3)) + { + item.EndDate = reader.GetDateTime(3).ToUniversalTime(); + } + + if (!reader.IsDBNull(4)) + { + item.IsOffline = reader.GetBoolean(4); + } + + if (!reader.IsDBNull(5)) + { + item.ChannelId = reader.GetString(5); + } + + var hasProgramAttributes = item as IHasProgramAttributes; + if (hasProgramAttributes != null) + { + if (!reader.IsDBNull(6)) + { + hasProgramAttributes.IsMovie = reader.GetBoolean(6); + } + + if (!reader.IsDBNull(7)) + { + hasProgramAttributes.IsSports = reader.GetBoolean(7); + } + + if (!reader.IsDBNull(8)) + { + hasProgramAttributes.IsKids = reader.GetBoolean(8); + } + + if (!reader.IsDBNull(9)) + { + hasProgramAttributes.IsSeries = reader.GetBoolean(9); + } + + if (!reader.IsDBNull(10)) + { + hasProgramAttributes.IsLive = reader.GetBoolean(10); + } + + if (!reader.IsDBNull(11)) + { + hasProgramAttributes.IsNews = reader.GetBoolean(11); + } + + if (!reader.IsDBNull(12)) + { + hasProgramAttributes.IsPremiere = reader.GetBoolean(12); + } + + if (!reader.IsDBNull(13)) + { + hasProgramAttributes.EpisodeTitle = reader.GetString(13); + } + + if (!reader.IsDBNull(14)) + { + hasProgramAttributes.IsRepeat = reader.GetBoolean(14); + } + } + + if (!reader.IsDBNull(15)) + { + item.CommunityRating = reader.GetFloat(15); + } + + if (!reader.IsDBNull(16)) + { + item.CustomRating = reader.GetString(16); + } + + if (!reader.IsDBNull(17)) + { + item.IndexNumber = reader.GetInt32(17); + } + + if (!reader.IsDBNull(18)) + { + item.IsLocked = reader.GetBoolean(18); + } + + if (!reader.IsDBNull(19)) + { + item.PreferredMetadataLanguage = reader.GetString(19); + } + + if (!reader.IsDBNull(20)) + { + item.PreferredMetadataCountryCode = reader.GetString(20); + } + + if (!reader.IsDBNull(21)) + { + item.IsHD = reader.GetBoolean(21); + } + + if (!reader.IsDBNull(22)) + { + item.ExternalEtag = reader.GetString(22); + } + + if (!reader.IsDBNull(23)) + { + item.DateLastRefreshed = reader.GetDateTime(23).ToUniversalTime(); } return item; @@ -567,7 +879,25 @@ namespace MediaBrowser.Server.Implementations.Persistence public IEnumerable<ChapterInfo> GetChapters(Guid id) { CheckDisposed(); - return _chapterRepository.GetChapters(id); + if (id == Guid.Empty) + { + throw new ArgumentNullException("id"); + } + + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "select StartPositionTicks,Name,ImagePath from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc"; + + cmd.Parameters.Add(cmd, "@ItemId", DbType.Guid).Value = id; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + yield return GetChapter(reader); + } + } + } } /// <summary> @@ -580,7 +910,52 @@ namespace MediaBrowser.Server.Implementations.Persistence public ChapterInfo GetChapter(Guid id, int index) { CheckDisposed(); - return _chapterRepository.GetChapter(id, index); + if (id == Guid.Empty) + { + throw new ArgumentNullException("id"); + } + + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "select StartPositionTicks,Name,ImagePath from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex"; + + cmd.Parameters.Add(cmd, "@ItemId", DbType.Guid).Value = id; + cmd.Parameters.Add(cmd, "@ChapterIndex", DbType.Int32).Value = index; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) + { + if (reader.Read()) + { + return GetChapter(reader); + } + } + return null; + } + } + + /// <summary> + /// Gets the chapter. + /// </summary> + /// <param name="reader">The reader.</param> + /// <returns>ChapterInfo.</returns> + private ChapterInfo GetChapter(IDataReader reader) + { + var chapter = new ChapterInfo + { + StartPositionTicks = reader.GetInt64(0) + }; + + if (!reader.IsDBNull(1)) + { + chapter.Name = reader.GetString(1); + } + + if (!reader.IsDBNull(2)) + { + chapter.ImagePath = reader.GetString(2); + } + + return chapter; } /// <summary> @@ -597,10 +972,87 @@ namespace MediaBrowser.Server.Implementations.Persistence /// or /// cancellationToken /// </exception> - public Task SaveChapters(Guid id, IEnumerable<ChapterInfo> chapters, CancellationToken cancellationToken) + public async Task SaveChapters(Guid id, IEnumerable<ChapterInfo> chapters, CancellationToken cancellationToken) { CheckDisposed(); - return _chapterRepository.SaveChapters(id, chapters, cancellationToken); + + if (id == Guid.Empty) + { + throw new ArgumentNullException("id"); + } + + if (chapters == null) + { + throw new ArgumentNullException("chapters"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); + + IDbTransaction transaction = null; + + try + { + transaction = _connection.BeginTransaction(); + + // First delete chapters + _deleteChaptersCommand.GetParameter(0).Value = id; + + _deleteChaptersCommand.Transaction = transaction; + + _deleteChaptersCommand.ExecuteNonQuery(); + + var index = 0; + + foreach (var chapter in chapters) + { + cancellationToken.ThrowIfCancellationRequested(); + + _saveChapterCommand.GetParameter(0).Value = id; + _saveChapterCommand.GetParameter(1).Value = index; + _saveChapterCommand.GetParameter(2).Value = chapter.StartPositionTicks; + _saveChapterCommand.GetParameter(3).Value = chapter.Name; + _saveChapterCommand.GetParameter(4).Value = chapter.ImagePath; + + _saveChapterCommand.Transaction = transaction; + + _saveChapterCommand.ExecuteNonQuery(); + + index++; + } + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + _logger.ErrorException("Failed to save chapters:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + + _writeLock.Release(); + } } /// <summary> @@ -649,18 +1101,6 @@ namespace MediaBrowser.Server.Implementations.Persistence _connection.Dispose(); _connection = null; } - - if (_chapterRepository != null) - { - _chapterRepository.Dispose(); - _chapterRepository = null; - } - - if (_mediaStreamsRepository != null) - { - _mediaStreamsRepository.Dispose(); - _mediaStreamsRepository = null; - } } } catch (Exception ex) @@ -882,6 +1322,75 @@ namespace MediaBrowser.Server.Implementations.Persistence } } + public QueryResult<Tuple<Guid, string>> GetItemIdsWithPath(InternalItemsQuery query) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + CheckDisposed(); + + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "select guid,path from TypedBaseItems"; + + var whereClauses = GetWhereClauses(query, cmd, false); + + var whereTextWithoutPaging = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + whereClauses = GetWhereClauses(query, cmd, true); + + var whereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + cmd.CommandText += whereText; + + cmd.CommandText += GetOrderByText(query); + + if (query.Limit.HasValue) + { + cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(CultureInfo.InvariantCulture); + } + + cmd.CommandText += "; select count (guid) from TypedBaseItems" + whereTextWithoutPaging; + + var list = new List<Tuple<Guid, string>>(); + var count = 0; + + _logger.Debug(cmd.CommandText); + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) + { + while (reader.Read()) + { + var id = reader.GetGuid(0); + string path = null; + + if (!reader.IsDBNull(1)) + { + path = reader.GetString(1); + } + list.Add(new Tuple<Guid, string>(id, path)); + } + + if (reader.NextResult() && reader.Read()) + { + count = reader.GetInt32(0); + } + } + + return new QueryResult<Tuple<Guid, string>>() + { + Items = list.ToArray(), + TotalRecordCount = count + }; + } + } + public QueryResult<Guid> GetItemIds(InternalItemsQuery query) { if (query == null) @@ -960,6 +1469,16 @@ namespace MediaBrowser.Server.Implementations.Persistence } cmd.Parameters.Add(cmd, "@SchemaVersion", DbType.Int32).Value = LatestSchemaVersion; } + if (query.IsOffline.HasValue) + { + whereClauses.Add("IsOffline=@IsOffline"); + cmd.Parameters.Add(cmd, "@IsOffline", DbType.Boolean).Value = query.IsOffline; + } + if (query.LocationType.HasValue) + { + whereClauses.Add("LocationType=@LocationType"); + cmd.Parameters.Add(cmd, "@LocationType", DbType.String).Value = query.LocationType.Value; + } if (query.IsMovie.HasValue) { whereClauses.Add("IsMovie=@IsMovie"); @@ -1011,6 +1530,12 @@ namespace MediaBrowser.Server.Implementations.Persistence whereClauses.Add(string.Format("ChannelId in ({0})", inClause)); } + if (query.ParentId.HasValue) + { + whereClauses.Add("ParentId=@ParentId"); + cmd.Parameters.Add(cmd, "@ParentId", DbType.Guid).Value = query.ParentId.Value; + } + if (query.MinEndDate.HasValue) { whereClauses.Add("EndDate>=@MinEndDate"); @@ -1130,8 +1655,6 @@ namespace MediaBrowser.Server.Implementations.Persistence typeof(LiveTvVideoRecording), typeof(LiveTvAudioRecording), typeof(Series), - typeof(LiveTvAudioRecording), - typeof(LiveTvVideoRecording), typeof(Audio), typeof(MusicAlbum), typeof(MusicArtist), @@ -1156,7 +1679,8 @@ namespace MediaBrowser.Server.Implementations.Persistence typeof(UserRootFolder), typeof(UserView), typeof(Video), - typeof(Year) + typeof(Year), + typeof(Channel) }; private static Dictionary<string, string[]> GetTypeMapDictionary() @@ -1216,11 +1740,21 @@ namespace MediaBrowser.Server.Implementations.Persistence _deletePeopleCommand.Transaction = transaction; _deletePeopleCommand.ExecuteNonQuery(); + // Delete chapters + _deleteChaptersCommand.GetParameter(0).Value = id; + _deleteChaptersCommand.Transaction = transaction; + _deleteChaptersCommand.ExecuteNonQuery(); + + // Delete media streams + _deleteStreamsCommand.GetParameter(0).Value = id; + _deleteStreamsCommand.Transaction = transaction; + _deleteStreamsCommand.ExecuteNonQuery(); + // Delete the item _deleteItemCommand.GetParameter(0).Value = id; _deleteItemCommand.Transaction = transaction; _deleteItemCommand.ExecuteNonQuery(); - + transaction.Commit(); } catch (OperationCanceledException) @@ -1327,18 +1861,6 @@ namespace MediaBrowser.Server.Implementations.Persistence } } - public IEnumerable<MediaStream> GetMediaStreams(MediaStreamQuery query) - { - CheckDisposed(); - return _mediaStreamsRepository.GetMediaStreams(query); - } - - public Task SaveMediaStreams(Guid id, IEnumerable<MediaStream> streams, CancellationToken cancellationToken) - { - CheckDisposed(); - return _mediaStreamsRepository.SaveMediaStreams(id, streams, cancellationToken); - } - public List<string> GetPeopleNames(InternalPeopleQuery query) { if (query == null) @@ -1567,5 +2089,289 @@ namespace MediaBrowser.Server.Implementations.Persistence return item; } + + public IEnumerable<MediaStream> GetMediaStreams(MediaStreamQuery query) + { + CheckDisposed(); + + if (query == null) + { + throw new ArgumentNullException("query"); + } + + using (var cmd = _connection.CreateCommand()) + { + var cmdText = "select " + string.Join(",", _mediaStreamSaveColumns) + " from mediastreams where"; + + cmdText += " ItemId=@ItemId"; + cmd.Parameters.Add(cmd, "@ItemId", DbType.Guid).Value = query.ItemId; + + if (query.Type.HasValue) + { + cmdText += " AND StreamType=@StreamType"; + cmd.Parameters.Add(cmd, "@StreamType", DbType.String).Value = query.Type.Value.ToString(); + } + + if (query.Index.HasValue) + { + cmdText += " AND StreamIndex=@StreamIndex"; + cmd.Parameters.Add(cmd, "@StreamIndex", DbType.Int32).Value = query.Index.Value; + } + + cmdText += " order by StreamIndex ASC"; + + cmd.CommandText = cmdText; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + yield return GetMediaStream(reader); + } + } + } + } + + public async Task SaveMediaStreams(Guid id, IEnumerable<MediaStream> streams, CancellationToken cancellationToken) + { + CheckDisposed(); + + if (id == Guid.Empty) + { + throw new ArgumentNullException("id"); + } + + if (streams == null) + { + throw new ArgumentNullException("streams"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); + + IDbTransaction transaction = null; + + try + { + transaction = _connection.BeginTransaction(); + + // First delete chapters + _deleteStreamsCommand.GetParameter(0).Value = id; + + _deleteStreamsCommand.Transaction = transaction; + + _deleteStreamsCommand.ExecuteNonQuery(); + + foreach (var stream in streams) + { + cancellationToken.ThrowIfCancellationRequested(); + + var index = 0; + + _saveStreamCommand.GetParameter(index++).Value = id; + _saveStreamCommand.GetParameter(index++).Value = stream.Index; + _saveStreamCommand.GetParameter(index++).Value = stream.Type.ToString(); + _saveStreamCommand.GetParameter(index++).Value = stream.Codec; + _saveStreamCommand.GetParameter(index++).Value = stream.Language; + _saveStreamCommand.GetParameter(index++).Value = stream.ChannelLayout; + _saveStreamCommand.GetParameter(index++).Value = stream.Profile; + _saveStreamCommand.GetParameter(index++).Value = stream.AspectRatio; + _saveStreamCommand.GetParameter(index++).Value = stream.Path; + + _saveStreamCommand.GetParameter(index++).Value = stream.IsInterlaced; + + _saveStreamCommand.GetParameter(index++).Value = stream.BitRate; + _saveStreamCommand.GetParameter(index++).Value = stream.Channels; + _saveStreamCommand.GetParameter(index++).Value = stream.SampleRate; + + _saveStreamCommand.GetParameter(index++).Value = stream.IsDefault; + _saveStreamCommand.GetParameter(index++).Value = stream.IsForced; + _saveStreamCommand.GetParameter(index++).Value = stream.IsExternal; + + _saveStreamCommand.GetParameter(index++).Value = stream.Width; + _saveStreamCommand.GetParameter(index++).Value = stream.Height; + _saveStreamCommand.GetParameter(index++).Value = stream.AverageFrameRate; + _saveStreamCommand.GetParameter(index++).Value = stream.RealFrameRate; + _saveStreamCommand.GetParameter(index++).Value = stream.Level; + _saveStreamCommand.GetParameter(index++).Value = stream.PixelFormat; + _saveStreamCommand.GetParameter(index++).Value = stream.BitDepth; + _saveStreamCommand.GetParameter(index++).Value = stream.IsAnamorphic; + _saveStreamCommand.GetParameter(index++).Value = stream.RefFrames; + _saveStreamCommand.GetParameter(index++).Value = stream.IsCabac; + + if (stream.KeyFrames == null || stream.KeyFrames.Count == 0) + { + _saveStreamCommand.GetParameter(index++).Value = null; + } + else + { + _saveStreamCommand.GetParameter(index++).Value = string.Join(",", stream.KeyFrames.Select(i => i.ToString(CultureInfo.InvariantCulture)).ToArray()); + } + + _saveStreamCommand.Transaction = transaction; + _saveStreamCommand.ExecuteNonQuery(); + } + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + _logger.ErrorException("Failed to save media streams:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + + _writeLock.Release(); + } + } + + /// <summary> + /// Gets the chapter. + /// </summary> + /// <param name="reader">The reader.</param> + /// <returns>ChapterInfo.</returns> + private MediaStream GetMediaStream(IDataReader reader) + { + var item = new MediaStream + { + Index = reader.GetInt32(1) + }; + + item.Type = (MediaStreamType)Enum.Parse(typeof(MediaStreamType), reader.GetString(2), true); + + if (!reader.IsDBNull(3)) + { + item.Codec = reader.GetString(3); + } + + if (!reader.IsDBNull(4)) + { + item.Language = reader.GetString(4); + } + + if (!reader.IsDBNull(5)) + { + item.ChannelLayout = reader.GetString(5); + } + + if (!reader.IsDBNull(6)) + { + item.Profile = reader.GetString(6); + } + + if (!reader.IsDBNull(7)) + { + item.AspectRatio = reader.GetString(7); + } + + if (!reader.IsDBNull(8)) + { + item.Path = reader.GetString(8); + } + + item.IsInterlaced = reader.GetBoolean(9); + + if (!reader.IsDBNull(10)) + { + item.BitRate = reader.GetInt32(10); + } + + if (!reader.IsDBNull(11)) + { + item.Channels = reader.GetInt32(11); + } + + if (!reader.IsDBNull(12)) + { + item.SampleRate = reader.GetInt32(12); + } + + item.IsDefault = reader.GetBoolean(13); + item.IsForced = reader.GetBoolean(14); + item.IsExternal = reader.GetBoolean(15); + + if (!reader.IsDBNull(16)) + { + item.Width = reader.GetInt32(16); + } + + if (!reader.IsDBNull(17)) + { + item.Height = reader.GetInt32(17); + } + + if (!reader.IsDBNull(18)) + { + item.AverageFrameRate = reader.GetFloat(18); + } + + if (!reader.IsDBNull(19)) + { + item.RealFrameRate = reader.GetFloat(19); + } + + if (!reader.IsDBNull(20)) + { + item.Level = reader.GetFloat(20); + } + + if (!reader.IsDBNull(21)) + { + item.PixelFormat = reader.GetString(21); + } + + if (!reader.IsDBNull(22)) + { + item.BitDepth = reader.GetInt32(22); + } + + if (!reader.IsDBNull(23)) + { + item.IsAnamorphic = reader.GetBoolean(23); + } + + if (!reader.IsDBNull(24)) + { + item.RefFrames = reader.GetInt32(24); + } + + if (!reader.IsDBNull(25)) + { + item.IsCabac = reader.GetBoolean(25); + } + + if (!reader.IsDBNull(26)) + { + var frames = reader.GetString(26); + if (!string.IsNullOrWhiteSpace(frames)) + { + item.KeyFrames = frames.Split(',').Select(i => int.Parse(i, CultureInfo.InvariantCulture)).ToList(); + } + } + + return item; + } + } }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteMediaStreamsRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteMediaStreamsRepository.cs deleted file mode 100644 index 9e97858d0..000000000 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteMediaStreamsRepository.cs +++ /dev/null @@ -1,633 +0,0 @@ -using System.Globalization; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Logging; -using System; -using System.Collections.Generic; -using System.Data; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Server.Implementations.Persistence -{ - class SqliteMediaStreamsRepository - { - private IDbConnection _connection; - - private readonly ILogger _logger; - - private IDbCommand _deleteStreamsCommand; - private IDbCommand _saveStreamCommand; - - public SqliteMediaStreamsRepository(IDbConnection connection, ILogManager logManager) - { - _connection = connection; - - _logger = logManager.GetLogger(GetType().Name); - } - - /// <summary> - /// Opens the connection to the database - /// </summary> - /// <returns>Task.</returns> - public void Initialize() - { - var createTableCommand - = "create table if not exists mediastreams "; - - // Add PixelFormat column - - createTableCommand += "(ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, IsCabac BIT NULL, KeyFrames TEXT NULL, PRIMARY KEY (ItemId, StreamIndex))"; - - string[] queries = { - - createTableCommand, - - "create index if not exists idx_mediastreams on mediastreams(ItemId, StreamIndex)", - - //pragmas - "pragma temp_store = memory", - - "pragma shrink_memory" - }; - - _connection.RunQueries(queries, _logger); - - AddPixelFormatColumnCommand(); - AddBitDepthCommand(); - AddIsAnamorphicColumn(); - AddIsCabacColumn(); - AddKeyFramesColumn(); - AddRefFramesCommand(); - - PrepareStatements(); - } - - private void AddPixelFormatColumnCommand() - { - using (var cmd = _connection.CreateCommand()) - { - cmd.CommandText = "PRAGMA table_info(mediastreams)"; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) - { - while (reader.Read()) - { - if (!reader.IsDBNull(1)) - { - var name = reader.GetString(1); - - if (string.Equals(name, "PixelFormat", StringComparison.OrdinalIgnoreCase)) - { - return; - } - } - } - } - } - - var builder = new StringBuilder(); - - builder.AppendLine("alter table mediastreams"); - builder.AppendLine("add column PixelFormat TEXT"); - - _connection.RunQueries(new[] { builder.ToString() }, _logger); - } - - private void AddBitDepthCommand() - { - using (var cmd = _connection.CreateCommand()) - { - cmd.CommandText = "PRAGMA table_info(mediastreams)"; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) - { - while (reader.Read()) - { - if (!reader.IsDBNull(1)) - { - var name = reader.GetString(1); - - if (string.Equals(name, "BitDepth", StringComparison.OrdinalIgnoreCase)) - { - return; - } - } - } - } - } - - var builder = new StringBuilder(); - - builder.AppendLine("alter table mediastreams"); - builder.AppendLine("add column BitDepth INT NULL"); - - _connection.RunQueries(new[] { builder.ToString() }, _logger); - } - - private void AddRefFramesCommand() - { - using (var cmd = _connection.CreateCommand()) - { - cmd.CommandText = "PRAGMA table_info(mediastreams)"; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) - { - while (reader.Read()) - { - if (!reader.IsDBNull(1)) - { - var name = reader.GetString(1); - - if (string.Equals(name, "RefFrames", StringComparison.OrdinalIgnoreCase)) - { - return; - } - } - } - } - } - - var builder = new StringBuilder(); - - builder.AppendLine("alter table mediastreams"); - builder.AppendLine("add column RefFrames INT NULL"); - - _connection.RunQueries(new[] { builder.ToString() }, _logger); - } - - private void AddIsCabacColumn() - { - using (var cmd = _connection.CreateCommand()) - { - cmd.CommandText = "PRAGMA table_info(mediastreams)"; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) - { - while (reader.Read()) - { - if (!reader.IsDBNull(1)) - { - var name = reader.GetString(1); - - if (string.Equals(name, "IsCabac", StringComparison.OrdinalIgnoreCase)) - { - return; - } - } - } - } - } - - var builder = new StringBuilder(); - - builder.AppendLine("alter table mediastreams"); - builder.AppendLine("add column IsCabac BIT NULL"); - - _connection.RunQueries(new[] { builder.ToString() }, _logger); - } - - private void AddKeyFramesColumn() - { - using (var cmd = _connection.CreateCommand()) - { - cmd.CommandText = "PRAGMA table_info(mediastreams)"; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) - { - while (reader.Read()) - { - if (!reader.IsDBNull(1)) - { - var name = reader.GetString(1); - - if (string.Equals(name, "KeyFrames", StringComparison.OrdinalIgnoreCase)) - { - return; - } - } - } - } - } - - var builder = new StringBuilder(); - - builder.AppendLine("alter table mediastreams"); - builder.AppendLine("add column KeyFrames TEXT NULL"); - - _connection.RunQueries(new[] { builder.ToString() }, _logger); - } - - private void AddIsAnamorphicColumn() - { - using (var cmd = _connection.CreateCommand()) - { - cmd.CommandText = "PRAGMA table_info(mediastreams)"; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) - { - while (reader.Read()) - { - if (!reader.IsDBNull(1)) - { - var name = reader.GetString(1); - - if (string.Equals(name, "IsAnamorphic", StringComparison.OrdinalIgnoreCase)) - { - return; - } - } - } - } - } - - var builder = new StringBuilder(); - - builder.AppendLine("alter table mediastreams"); - builder.AppendLine("add column IsAnamorphic BIT NULL"); - - _connection.RunQueries(new[] { builder.ToString() }, _logger); - } - - private readonly string[] _saveColumns = - { - "ItemId", - "StreamIndex", - "StreamType", - "Codec", - "Language", - "ChannelLayout", - "Profile", - "AspectRatio", - "Path", - "IsInterlaced", - "BitRate", - "Channels", - "SampleRate", - "IsDefault", - "IsForced", - "IsExternal", - "Height", - "Width", - "AverageFrameRate", - "RealFrameRate", - "Level", - "PixelFormat", - "BitDepth", - "IsAnamorphic", - "RefFrames", - "IsCabac", - "KeyFrames" - }; - - /// <summary> - /// The _write lock - /// </summary> - private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1); - - /// <summary> - /// Prepares the statements. - /// </summary> - private void PrepareStatements() - { - _deleteStreamsCommand = _connection.CreateCommand(); - _deleteStreamsCommand.CommandText = "delete from mediastreams where ItemId=@ItemId"; - _deleteStreamsCommand.Parameters.Add(_deleteStreamsCommand, "@ItemId"); - - _saveStreamCommand = _connection.CreateCommand(); - - _saveStreamCommand.CommandText = string.Format("replace into mediastreams ({0}) values ({1})", - string.Join(",", _saveColumns), - string.Join(",", _saveColumns.Select(i => "@" + i).ToArray())); - - foreach (var col in _saveColumns) - { - _saveStreamCommand.Parameters.Add(_saveStreamCommand, "@" + col); - } - } - - public IEnumerable<MediaStream> GetMediaStreams(MediaStreamQuery query) - { - if (query == null) - { - throw new ArgumentNullException("query"); - } - - using (var cmd = _connection.CreateCommand()) - { - var cmdText = "select " + string.Join(",", _saveColumns) + " from mediastreams where"; - - cmdText += " ItemId=@ItemId"; - cmd.Parameters.Add(cmd, "@ItemId", DbType.Guid).Value = query.ItemId; - - if (query.Type.HasValue) - { - cmdText += " AND StreamType=@StreamType"; - cmd.Parameters.Add(cmd, "@StreamType", DbType.String).Value = query.Type.Value.ToString(); - } - - if (query.Index.HasValue) - { - cmdText += " AND StreamIndex=@StreamIndex"; - cmd.Parameters.Add(cmd, "@StreamIndex", DbType.Int32).Value = query.Index.Value; - } - - cmdText += " order by StreamIndex ASC"; - - cmd.CommandText = cmdText; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) - { - while (reader.Read()) - { - yield return GetMediaStream(reader); - } - } - } - } - - /// <summary> - /// Gets the chapter. - /// </summary> - /// <param name="reader">The reader.</param> - /// <returns>ChapterInfo.</returns> - private MediaStream GetMediaStream(IDataReader reader) - { - var item = new MediaStream - { - Index = reader.GetInt32(1) - }; - - item.Type = (MediaStreamType)Enum.Parse(typeof(MediaStreamType), reader.GetString(2), true); - - if (!reader.IsDBNull(3)) - { - item.Codec = reader.GetString(3); - } - - if (!reader.IsDBNull(4)) - { - item.Language = reader.GetString(4); - } - - if (!reader.IsDBNull(5)) - { - item.ChannelLayout = reader.GetString(5); - } - - if (!reader.IsDBNull(6)) - { - item.Profile = reader.GetString(6); - } - - if (!reader.IsDBNull(7)) - { - item.AspectRatio = reader.GetString(7); - } - - if (!reader.IsDBNull(8)) - { - item.Path = reader.GetString(8); - } - - item.IsInterlaced = reader.GetBoolean(9); - - if (!reader.IsDBNull(10)) - { - item.BitRate = reader.GetInt32(10); - } - - if (!reader.IsDBNull(11)) - { - item.Channels = reader.GetInt32(11); - } - - if (!reader.IsDBNull(12)) - { - item.SampleRate = reader.GetInt32(12); - } - - item.IsDefault = reader.GetBoolean(13); - item.IsForced = reader.GetBoolean(14); - item.IsExternal = reader.GetBoolean(15); - - if (!reader.IsDBNull(16)) - { - item.Width = reader.GetInt32(16); - } - - if (!reader.IsDBNull(17)) - { - item.Height = reader.GetInt32(17); - } - - if (!reader.IsDBNull(18)) - { - item.AverageFrameRate = reader.GetFloat(18); - } - - if (!reader.IsDBNull(19)) - { - item.RealFrameRate = reader.GetFloat(19); - } - - if (!reader.IsDBNull(20)) - { - item.Level = reader.GetFloat(20); - } - - if (!reader.IsDBNull(21)) - { - item.PixelFormat = reader.GetString(21); - } - - if (!reader.IsDBNull(22)) - { - item.BitDepth = reader.GetInt32(22); - } - - if (!reader.IsDBNull(23)) - { - item.IsAnamorphic = reader.GetBoolean(23); - } - - if (!reader.IsDBNull(24)) - { - item.RefFrames = reader.GetInt32(24); - } - - if (!reader.IsDBNull(25)) - { - item.IsCabac = reader.GetBoolean(25); - } - - if (!reader.IsDBNull(26)) - { - var frames = reader.GetString(26); - if (!string.IsNullOrWhiteSpace(frames)) - { - item.KeyFrames = frames.Split(',').Select(i => int.Parse(i, CultureInfo.InvariantCulture)).ToList(); - } - } - - return item; - } - - public async Task SaveMediaStreams(Guid id, IEnumerable<MediaStream> streams, CancellationToken cancellationToken) - { - if (id == Guid.Empty) - { - throw new ArgumentNullException("id"); - } - - if (streams == null) - { - throw new ArgumentNullException("streams"); - } - - cancellationToken.ThrowIfCancellationRequested(); - - await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); - - IDbTransaction transaction = null; - - try - { - transaction = _connection.BeginTransaction(); - - // First delete chapters - _deleteStreamsCommand.GetParameter(0).Value = id; - - _deleteStreamsCommand.Transaction = transaction; - - _deleteStreamsCommand.ExecuteNonQuery(); - - foreach (var stream in streams) - { - cancellationToken.ThrowIfCancellationRequested(); - - var index = 0; - - _saveStreamCommand.GetParameter(index++).Value = id; - _saveStreamCommand.GetParameter(index++).Value = stream.Index; - _saveStreamCommand.GetParameter(index++).Value = stream.Type.ToString(); - _saveStreamCommand.GetParameter(index++).Value = stream.Codec; - _saveStreamCommand.GetParameter(index++).Value = stream.Language; - _saveStreamCommand.GetParameter(index++).Value = stream.ChannelLayout; - _saveStreamCommand.GetParameter(index++).Value = stream.Profile; - _saveStreamCommand.GetParameter(index++).Value = stream.AspectRatio; - _saveStreamCommand.GetParameter(index++).Value = stream.Path; - - _saveStreamCommand.GetParameter(index++).Value = stream.IsInterlaced; - - _saveStreamCommand.GetParameter(index++).Value = stream.BitRate; - _saveStreamCommand.GetParameter(index++).Value = stream.Channels; - _saveStreamCommand.GetParameter(index++).Value = stream.SampleRate; - - _saveStreamCommand.GetParameter(index++).Value = stream.IsDefault; - _saveStreamCommand.GetParameter(index++).Value = stream.IsForced; - _saveStreamCommand.GetParameter(index++).Value = stream.IsExternal; - - _saveStreamCommand.GetParameter(index++).Value = stream.Width; - _saveStreamCommand.GetParameter(index++).Value = stream.Height; - _saveStreamCommand.GetParameter(index++).Value = stream.AverageFrameRate; - _saveStreamCommand.GetParameter(index++).Value = stream.RealFrameRate; - _saveStreamCommand.GetParameter(index++).Value = stream.Level; - _saveStreamCommand.GetParameter(index++).Value = stream.PixelFormat; - _saveStreamCommand.GetParameter(index++).Value = stream.BitDepth; - _saveStreamCommand.GetParameter(index++).Value = stream.IsAnamorphic; - _saveStreamCommand.GetParameter(index++).Value = stream.RefFrames; - _saveStreamCommand.GetParameter(index++).Value = stream.IsCabac; - - if (stream.KeyFrames == null || stream.KeyFrames.Count == 0) - { - _saveStreamCommand.GetParameter(index++).Value = null; - } - else - { - _saveStreamCommand.GetParameter(index++).Value = string.Join(",", stream.KeyFrames.Select(i => i.ToString(CultureInfo.InvariantCulture)).ToArray()); - } - - _saveStreamCommand.Transaction = transaction; - _saveStreamCommand.ExecuteNonQuery(); - } - - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - catch (Exception e) - { - _logger.ErrorException("Failed to save media streams:", e); - - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); - } - - _writeLock.Release(); - } - } - - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private readonly object _disposeLock = new object(); - - /// <summary> - /// Releases unmanaged and - optionally - managed resources. - /// </summary> - /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool dispose) - { - if (dispose) - { - try - { - lock (_disposeLock) - { - if (_connection != null) - { - if (_connection.IsOpen()) - { - _connection.Close(); - } - - _connection.Dispose(); - _connection = null; - } - } - } - catch (Exception ex) - { - _logger.ErrorException("Error disposing database", ex); - } - } - } - } -} - diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteProviderInfoRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteProviderInfoRepository.cs index bce33e834..e7853b458 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteProviderInfoRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteProviderInfoRepository.cs @@ -47,7 +47,7 @@ namespace MediaBrowser.Server.Implementations.Persistence string[] queries = { - "create table if not exists MetadataStatus (ItemId GUID PRIMARY KEY, ItemName TEXT, ItemType TEXT, SeriesName TEXT, DateLastMetadataRefresh datetime, DateLastImagesRefresh datetime, LastStatus TEXT, LastErrorMessage TEXT, MetadataProvidersRefreshed TEXT, ImageProvidersRefreshed TEXT, ItemDateModified DateTimeNull)", + "create table if not exists MetadataStatus (ItemId GUID PRIMARY KEY, ItemName TEXT, ItemType TEXT, SeriesName TEXT, DateLastMetadataRefresh datetime, DateLastImagesRefresh datetime, LastErrorMessage TEXT, ItemDateModified DateTimeNull)", "create index if not exists idx_MetadataStatus on MetadataStatus(ItemId)", //pragmas @@ -71,10 +71,7 @@ namespace MediaBrowser.Server.Implementations.Persistence "SeriesName", "DateLastMetadataRefresh", "DateLastImagesRefresh", - "LastStatus", "LastErrorMessage", - "MetadataProvidersRefreshed", - "ImageProvidersRefreshed", "ItemDateModified" }; @@ -188,19 +185,12 @@ namespace MediaBrowser.Server.Implementations.Persistence if (!reader.IsDBNull(6)) { - result.LastStatus = (ProviderRefreshStatus)Enum.Parse(typeof(ProviderRefreshStatus), reader.GetString(6), true); + result.LastErrorMessage = reader.GetString(6); } if (!reader.IsDBNull(7)) { - result.LastErrorMessage = reader.GetString(7); - } - - // Skip metadata and image providers - - if (!reader.IsDBNull(10)) - { - result.ItemDateModified = reader.GetDateTime(10).ToUniversalTime(); + result.ItemDateModified = reader.GetDateTime(7).ToUniversalTime(); } return result; @@ -229,11 +219,8 @@ namespace MediaBrowser.Server.Implementations.Persistence _saveStatusCommand.GetParameter(3).Value = status.SeriesName; _saveStatusCommand.GetParameter(4).Value = status.DateLastMetadataRefresh; _saveStatusCommand.GetParameter(5).Value = status.DateLastImagesRefresh; - _saveStatusCommand.GetParameter(6).Value = status.LastStatus.ToString(); - _saveStatusCommand.GetParameter(7).Value = status.LastErrorMessage; - _saveStatusCommand.GetParameter(8).Value = string.Empty; - _saveStatusCommand.GetParameter(9).Value = string.Empty; - _saveStatusCommand.GetParameter(10).Value = status.ItemDateModified; + _saveStatusCommand.GetParameter(6).Value = status.LastErrorMessage; + _saveStatusCommand.GetParameter(7).Value = status.ItemDateModified; _saveStatusCommand.Transaction = transaction; diff --git a/MediaBrowser.Server.Implementations/Photos/BaseDynamicImageProvider.cs b/MediaBrowser.Server.Implementations/Photos/BaseDynamicImageProvider.cs index 4b7e1c3a7..2402d3ec1 100644 --- a/MediaBrowser.Server.Implementations/Photos/BaseDynamicImageProvider.cs +++ b/MediaBrowser.Server.Implementations/Photos/BaseDynamicImageProvider.cs @@ -13,6 +13,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; namespace MediaBrowser.Server.Implementations.Photos { @@ -32,7 +33,7 @@ namespace MediaBrowser.Server.Implementations.Photos ImageProcessor = imageProcessor; } - public virtual bool Supports(IHasImages item) + protected virtual bool Supports(IHasImages item) { return true; } @@ -91,11 +92,11 @@ namespace MediaBrowser.Server.Implementations.Photos string cacheKey, CancellationToken cancellationToken) { - var outputPath = Path.Combine(ApplicationPaths.TempDirectory, Guid.NewGuid() + ".png"); - Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); - var imageCreated = await CreateImage(item, itemsWithImages, outputPath, imageType, 0).ConfigureAwait(false); + var outputPathWithoutExtension = Path.Combine(ApplicationPaths.TempDirectory, Guid.NewGuid().ToString("N")); + FileSystem.CreateDirectory(Path.GetDirectoryName(outputPathWithoutExtension)); + string outputPath = await CreateImage(item, itemsWithImages, outputPathWithoutExtension, imageType, 0).ConfigureAwait(false); - if (!imageCreated) + if (string.IsNullOrWhiteSpace(outputPath)) { return ItemUpdateType.None; } @@ -116,7 +117,7 @@ namespace MediaBrowser.Server.Implementations.Photos return parts.GetMD5().ToString("N"); } - protected Task<bool> CreateThumbCollage(IHasImages primaryItem, List<BaseItem> items, string outputPath) + protected Task<string> CreateThumbCollage(IHasImages primaryItem, List<BaseItem> items, string outputPath) { return CreateCollage(primaryItem, items, outputPath, 640, 360); } @@ -124,28 +125,43 @@ namespace MediaBrowser.Server.Implementations.Photos protected virtual IEnumerable<string> GetStripCollageImagePaths(IHasImages primaryItem, IEnumerable<BaseItem> items) { return items - .Select(i => i.GetImagePath(ImageType.Primary) ?? i.GetImagePath(ImageType.Thumb)) + .Select(i => + { + var image = i.GetImageInfo(ImageType.Primary, 0); + + if (image != null && image.IsLocalFile) + { + return image.Path; + } + image = i.GetImageInfo(ImageType.Thumb, 0); + + if (image != null && image.IsLocalFile) + { + return image.Path; + } + return null; + }) .Where(i => !string.IsNullOrWhiteSpace(i)); } - protected Task<bool> CreatePosterCollage(IHasImages primaryItem, List<BaseItem> items, string outputPath) + protected Task<string> CreatePosterCollage(IHasImages primaryItem, List<BaseItem> items, string outputPath) { return CreateCollage(primaryItem, items, outputPath, 400, 600); } - protected Task<bool> CreateSquareCollage(IHasImages primaryItem, List<BaseItem> items, string outputPath) + protected Task<string> CreateSquareCollage(IHasImages primaryItem, List<BaseItem> items, string outputPath) { return CreateCollage(primaryItem, items, outputPath, 600, 600); } - protected Task<bool> CreateThumbCollage(IHasImages primaryItem, List<BaseItem> items, string outputPath, int width, int height) + protected Task<string> CreateThumbCollage(IHasImages primaryItem, List<BaseItem> items, string outputPath, int width, int height) { return CreateCollage(primaryItem, items, outputPath, width, height); } - private Task<bool> CreateCollage(IHasImages primaryItem, List<BaseItem> items, string outputPath, int width, int height) + private async Task<string> CreateCollage(IHasImages primaryItem, List<BaseItem> items, string outputPath, int width, int height) { - Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); + FileSystem.CreateDirectory(Path.GetDirectoryName(outputPath)); var options = new ImageCollageOptions { @@ -157,11 +173,16 @@ namespace MediaBrowser.Server.Implementations.Photos if (options.InputPaths.Length == 0) { - return Task.FromResult(false); + return null; + } + + if (!ImageProcessor.SupportsImageCollageCreation) + { + return null; } - ImageProcessor.CreateImageCollage(options); - return Task.FromResult(true); + await ImageProcessor.CreateImageCollage(options).ConfigureAwait(false); + return outputPath; } public string Name @@ -169,17 +190,19 @@ namespace MediaBrowser.Server.Implementations.Photos get { return "Dynamic Image Provider"; } } - protected virtual async Task<bool> CreateImage(IHasImages item, + protected virtual async Task<string> CreateImage(IHasImages item, List<BaseItem> itemsWithImages, - string outputPath, + string outputPathWithoutExtension, ImageType imageType, int imageIndex) { if (itemsWithImages.Count == 0) { - return false; + return null; } + string outputPath = Path.ChangeExtension(outputPathWithoutExtension, ".png"); + if (imageType == ImageType.Thumb) { return await CreateThumbCollage(item, itemsWithImages, outputPath).ConfigureAwait(false); @@ -191,7 +214,7 @@ namespace MediaBrowser.Server.Implementations.Photos { return await CreateSquareCollage(item, itemsWithImages, outputPath).ConfigureAwait(false); } - if (item is PhotoAlbum || item is Playlist) + if (item is Playlist) { return await CreateSquareCollage(item, itemsWithImages, outputPath).ConfigureAwait(false); } @@ -247,6 +270,11 @@ namespace MediaBrowser.Server.Implementations.Photos if (image != null) { + if (!image.IsLocalFile) + { + return false; + } + if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path)) { return false; @@ -268,6 +296,11 @@ namespace MediaBrowser.Server.Implementations.Photos if (image != null) { + if (!image.IsLocalFile) + { + return false; + } + if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path)) { return false; @@ -294,7 +327,7 @@ namespace MediaBrowser.Server.Implementations.Photos var random = DateTime.Now.DayOfYear % MaxImageAgeDays; return items - .OrderBy(i => (random + "" + items.IndexOf(i)).GetMD5()) + .OrderBy(i => (random + string.Empty + items.IndexOf(i)).GetMD5()) .Take(limit) .OrderBy(i => i.Name) .ToList(); diff --git a/MediaBrowser.Server.Implementations/Photos/PhotoAlbumImageProvider.cs b/MediaBrowser.Server.Implementations/Photos/PhotoAlbumImageProvider.cs index b55c76b8f..8b73d03d8 100644 --- a/MediaBrowser.Server.Implementations/Photos/PhotoAlbumImageProvider.cs +++ b/MediaBrowser.Server.Implementations/Photos/PhotoAlbumImageProvider.cs @@ -1,11 +1,12 @@ using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.IO; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; +using CommonIO; namespace MediaBrowser.Server.Implementations.Photos { @@ -19,9 +20,26 @@ namespace MediaBrowser.Server.Implementations.Photos protected override Task<List<BaseItem>> GetItemsWithImages(IHasImages item) { var photoAlbum = (PhotoAlbum)item; - var items = GetFinalItems(photoAlbum.Children.ToList()); + var items = GetFinalItems(photoAlbum.Children.ToList(), 1); return Task.FromResult(items); } + + protected override async Task<string> CreateImage(IHasImages item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, Model.Entities.ImageType imageType, int imageIndex) + { + var photoFile = itemsWithImages.Where(i => Path.HasExtension(i.Path)).Select(i => i.Path).FirstOrDefault(); + + if (string.IsNullOrWhiteSpace(photoFile)) + { + return null; + } + + var ext = Path.GetExtension(photoFile); + + var outputPath = Path.ChangeExtension(outputPathWithoutExtension, ext); + File.Copy(photoFile, outputPath); + + return outputPath; + } } } diff --git a/MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs b/MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs index 3ec41b6dc..4d3e091b1 100644 --- a/MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs +++ b/MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs @@ -4,6 +4,8 @@ using MediaBrowser.Controller.Playlists; using System.Collections.Generic; using System.IO; using System.Linq; +using CommonIO; +using MediaBrowser.Common.IO; namespace MediaBrowser.Server.Implementations.Playlists { @@ -46,17 +48,19 @@ namespace MediaBrowser.Server.Implementations.Playlists public class PlaylistsDynamicFolder : IVirtualFolderCreator { private readonly IApplicationPaths _appPaths; + private readonly IFileSystem _fileSystem; - public PlaylistsDynamicFolder(IApplicationPaths appPaths) + public PlaylistsDynamicFolder(IApplicationPaths appPaths, IFileSystem fileSystem) { _appPaths = appPaths; + _fileSystem = fileSystem; } public BasePluginFolder GetFolder() { var path = Path.Combine(_appPaths.DataPath, "playlists"); - Directory.CreateDirectory(path); + _fileSystem.CreateDirectory(path); return new PlaylistsFolder { diff --git a/MediaBrowser.Server.Implementations/Playlists/PlaylistImageProvider.cs b/MediaBrowser.Server.Implementations/Playlists/PlaylistImageProvider.cs index dcd9d21ea..d9e9fac2a 100644 --- a/MediaBrowser.Server.Implementations/Playlists/PlaylistImageProvider.cs +++ b/MediaBrowser.Server.Implementations/Playlists/PlaylistImageProvider.cs @@ -12,6 +12,7 @@ using MoreLinq; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using CommonIO; namespace MediaBrowser.Server.Implementations.Playlists { diff --git a/MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs b/MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs index 857cf743f..d9b3ed755 100644 --- a/MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs +++ b/MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs @@ -13,6 +13,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; namespace MediaBrowser.Server.Implementations.Playlists { @@ -110,7 +111,7 @@ namespace MediaBrowser.Server.Implementations.Playlists try { - Directory.CreateDirectory(path); + _fileSystem.CreateDirectory(path); var playlist = new Playlist { @@ -128,7 +129,7 @@ namespace MediaBrowser.Server.Implementations.Playlists await parentFolder.AddChild(playlist, CancellationToken.None).ConfigureAwait(false); - await playlist.RefreshMetadata(new MetadataRefreshOptions { ForceSave = true }, CancellationToken.None) + await playlist.RefreshMetadata(new MetadataRefreshOptions(_fileSystem) { ForceSave = true }, CancellationToken.None) .ConfigureAwait(false); if (options.ItemIdList.Count > 0) @@ -150,7 +151,7 @@ namespace MediaBrowser.Server.Implementations.Playlists private string GetTargetPath(string path) { - while (Directory.Exists(path)) + while (_fileSystem.DirectoryExists(path)) { path += "1"; } @@ -196,7 +197,7 @@ namespace MediaBrowser.Server.Implementations.Playlists await playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); - _providerManager.QueueRefresh(playlist.Id, new MetadataRefreshOptions + _providerManager.QueueRefresh(playlist.Id, new MetadataRefreshOptions(_fileSystem) { ForceSave = true }); @@ -223,12 +224,43 @@ namespace MediaBrowser.Server.Implementations.Playlists await playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); - _providerManager.QueueRefresh(playlist.Id, new MetadataRefreshOptions + _providerManager.QueueRefresh(playlist.Id, new MetadataRefreshOptions(_fileSystem) { ForceSave = true }); } + public async Task MoveItem(string playlistId, string entryId, int newIndex) + { + var playlist = _libraryManager.GetItemById(playlistId) as Playlist; + + if (playlist == null) + { + throw new ArgumentException("No Playlist exists with the supplied Id"); + } + + var children = playlist.GetManageableItems().ToList(); + + var oldIndex = children.FindIndex(i => string.Equals(entryId, i.Item1.Id, StringComparison.OrdinalIgnoreCase)); + + if (oldIndex == newIndex) + { + return; + } + + if (newIndex > oldIndex) + { + newIndex--; + } + + var item = playlist.LinkedChildren[oldIndex]; + + playlist.LinkedChildren.Remove(item); + playlist.LinkedChildren.Insert(newIndex, item); + + await playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); + } + public Folder GetPlaylistsFolder(string userId) { return _libraryManager.RootFolder.Children.OfType<PlaylistsFolder>() diff --git a/MediaBrowser.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs b/MediaBrowser.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs index b23aaeeff..355603fae 100644 --- a/MediaBrowser.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs +++ b/MediaBrowser.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs @@ -11,6 +11,8 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; +using MediaBrowser.Common.IO; namespace MediaBrowser.Server.Implementations.ScheduledTasks { @@ -39,6 +41,7 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks private readonly IApplicationPaths _appPaths; private readonly IEncodingManager _encodingManager; + private readonly IFileSystem _fileSystem; /// <summary> /// Initializes a new instance of the <see cref="ChapterImagesTask" /> class. @@ -46,13 +49,14 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks /// <param name="logManager">The log manager.</param> /// <param name="libraryManager">The library manager.</param> /// <param name="itemRepo">The item repo.</param> - public ChapterImagesTask(ILogManager logManager, ILibraryManager libraryManager, IItemRepository itemRepo, IApplicationPaths appPaths, IEncodingManager encodingManager) + public ChapterImagesTask(ILogManager logManager, ILibraryManager libraryManager, IItemRepository itemRepo, IApplicationPaths appPaths, IEncodingManager encodingManager, IFileSystem fileSystem) { _logger = logManager.GetLogger(GetType().Name); _libraryManager = libraryManager; _itemRepo = itemRepo; _appPaths = appPaths; _encodingManager = encodingManager; + _fileSystem = fileSystem; } /// <summary> @@ -94,7 +98,7 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks try { - previouslyFailedImages = File.ReadAllText(failHistoryPath) + previouslyFailedImages = _fileSystem.ReadAllText(failHistoryPath) .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) .ToList(); } @@ -132,9 +136,9 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks var parentPath = Path.GetDirectoryName(failHistoryPath); - Directory.CreateDirectory(parentPath); + _fileSystem.CreateDirectory(parentPath); - File.WriteAllText(failHistoryPath, string.Join("|", previouslyFailedImages.ToArray())); + _fileSystem.WriteAllText(failHistoryPath, string.Join("|", previouslyFailedImages.ToArray())); } numComplete++; diff --git a/MediaBrowser.Server.Implementations/ScheduledTasks/RefreshIntrosTask.cs b/MediaBrowser.Server.Implementations/ScheduledTasks/RefreshIntrosTask.cs index a65b46f64..90682d5b0 100644 --- a/MediaBrowser.Server.Implementations/ScheduledTasks/RefreshIntrosTask.cs +++ b/MediaBrowser.Server.Implementations/ScheduledTasks/RefreshIntrosTask.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; namespace MediaBrowser.Server.Implementations.ScheduledTasks { diff --git a/MediaBrowser.Server.Implementations/Session/SessionManager.cs b/MediaBrowser.Server.Implementations/Session/SessionManager.cs index 70a4cb439..8d3273421 100644 --- a/MediaBrowser.Server.Implementations/Session/SessionManager.cs +++ b/MediaBrowser.Server.Implementations/Session/SessionManager.cs @@ -29,6 +29,7 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Controller.Net; namespace MediaBrowser.Server.Implementations.Session { @@ -1276,7 +1277,7 @@ namespace MediaBrowser.Server.Implementations.Session { if (!_deviceManager.CanAccessDevice(user.Id.ToString("N"), request.DeviceId)) { - throw new UnauthorizedAccessException("User is not allowed access from this device."); + throw new SecurityException("User is not allowed access from this device."); } } @@ -1286,7 +1287,7 @@ namespace MediaBrowser.Server.Implementations.Session { EventHelper.FireEventIfNotNull(AuthenticationFailed, this, new GenericEventArgs<AuthenticationRequest>(request), _logger); - throw new UnauthorizedAccessException("Invalid user or password entered."); + throw new SecurityException("Invalid user or password entered."); } var token = await GetAuthorizationToken(user.Id.ToString("N"), request.DeviceId, request.App, request.AppVersion, request.DeviceName).ConfigureAwait(false); @@ -1322,8 +1323,9 @@ namespace MediaBrowser.Server.Implementations.Session if (existing.Items.Length > 0) { - _logger.Debug("Reissuing access token"); - return existing.Items[0].AccessToken; + var token = existing.Items[0].AccessToken; + _logger.Info("Reissuing access token: " + token); + return token; } var newToken = new AuthenticationInfo @@ -1338,7 +1340,7 @@ namespace MediaBrowser.Server.Implementations.Session AccessToken = Guid.NewGuid().ToString("N") }; - _logger.Debug("Creating new access token for user {0}", userId); + _logger.Info("Creating new access token for user {0}", userId); await _authRepo.Create(newToken, CancellationToken.None).ConfigureAwait(false); return newToken.AccessToken; @@ -1351,6 +1353,8 @@ namespace MediaBrowser.Server.Implementations.Session throw new ArgumentNullException("accessToken"); } + _logger.Info("Logging out access token {0}", accessToken); + var existing = _authRepo.Get(new AuthenticationInfoQuery { Limit = 1, diff --git a/MediaBrowser.Server.Implementations/Sync/MediaSync.cs b/MediaBrowser.Server.Implementations/Sync/MediaSync.cs index 86ef58e42..cff72ae58 100644 --- a/MediaBrowser.Server.Implementations/Sync/MediaSync.cs +++ b/MediaBrowser.Server.Implementations/Sync/MediaSync.cs @@ -18,6 +18,7 @@ using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; +using CommonIO; using Interfaces.IO; namespace MediaBrowser.Server.Implementations.Sync @@ -83,12 +84,7 @@ namespace MediaBrowser.Server.Implementations.Sync foreach (var localItem in localItems) { - // TODO: Remove this after a while - if (string.IsNullOrWhiteSpace(localItem.FileId)) - { - jobItemIds.Add(localItem.SyncJobItemId); - } - else if (remoteIds.Contains(localItem.FileId, StringComparer.OrdinalIgnoreCase)) + if (remoteIds.Contains(localItem.FileId, StringComparer.OrdinalIgnoreCase)) { jobItemIds.Add(localItem.SyncJobItemId); } @@ -147,7 +143,14 @@ namespace MediaBrowser.Server.Implementations.Sync progress.Report(totalProgress); }); - await GetItem(provider, dataProvider, target, serverId, serverName, jobItem, innerProgress, cancellationToken).ConfigureAwait(false); + try + { + await GetItem(provider, dataProvider, target, serverId, serverName, jobItem, innerProgress, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error syncing item", ex); + } numComplete++; startingPercent = numComplete; @@ -322,12 +325,6 @@ namespace MediaBrowser.Server.Implementations.Sync { var files = localItem.AdditionalFiles.ToList(); - // TODO: Remove this. Have to check it for now since this is a new property - if (!string.IsNullOrWhiteSpace(localItem.FileId)) - { - files.Insert(0, localItem.FileId); - } - foreach (var file in files) { _logger.Debug("Removing {0} from {1}.", file, target.Name); diff --git a/MediaBrowser.Server.Implementations/Sync/MultiProviderSync.cs b/MediaBrowser.Server.Implementations/Sync/MultiProviderSync.cs index 6f09e96f0..dca831f73 100644 --- a/MediaBrowser.Server.Implementations/Sync/MultiProviderSync.cs +++ b/MediaBrowser.Server.Implementations/Sync/MultiProviderSync.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; namespace MediaBrowser.Server.Implementations.Sync { diff --git a/MediaBrowser.Server.Implementations/Sync/ServerSyncScheduledTask.cs b/MediaBrowser.Server.Implementations/Sync/ServerSyncScheduledTask.cs index 9477a23f1..52c9f9cee 100644 --- a/MediaBrowser.Server.Implementations/Sync/ServerSyncScheduledTask.cs +++ b/MediaBrowser.Server.Implementations/Sync/ServerSyncScheduledTask.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; namespace MediaBrowser.Server.Implementations.Sync { diff --git a/MediaBrowser.Server.Implementations/Sync/SyncConvertScheduledTask.cs b/MediaBrowser.Server.Implementations/Sync/SyncConvertScheduledTask.cs index 5347e1d83..2efed7992 100644 --- a/MediaBrowser.Server.Implementations/Sync/SyncConvertScheduledTask.cs +++ b/MediaBrowser.Server.Implementations/Sync/SyncConvertScheduledTask.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using CommonIO; namespace MediaBrowser.Server.Implementations.Sync { diff --git a/MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs b/MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs index 1061a373e..95934908d 100644 --- a/MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs +++ b/MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs @@ -23,6 +23,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; namespace MediaBrowser.Server.Implementations.Sync { @@ -178,9 +179,13 @@ namespace MediaBrowser.Server.Implementations.Sync job.Progress = null; } - if (jobItems.All(i => i.Status == SyncJobItemStatus.Queued)) + if (jobItems.Any(i => i.Status == SyncJobItemStatus.Transferring)) { - job.Status = SyncJobStatus.Queued; + job.Status = SyncJobStatus.Transferring; + } + else if (jobItems.Any(i => i.Status == SyncJobItemStatus.Converting)) + { + job.Status = SyncJobStatus.Converting; } else if (jobItems.All(i => i.Status == SyncJobItemStatus.Failed)) { @@ -194,14 +199,6 @@ namespace MediaBrowser.Server.Implementations.Sync { job.Status = SyncJobStatus.ReadyToTransfer; } - else if (jobItems.All(i => i.Status == SyncJobItemStatus.Transferring)) - { - job.Status = SyncJobStatus.Transferring; - } - else if (jobItems.Any(i => i.Status == SyncJobItemStatus.Converting)) - { - job.Status = SyncJobStatus.Converting; - } else if (jobItems.All(i => i.Status == SyncJobItemStatus.Cancelled || i.Status == SyncJobItemStatus.Failed || i.Status == SyncJobItemStatus.Synced || i.Status == SyncJobItemStatus.RemovedFromDevice)) { if (jobItems.Any(i => i.Status == SyncJobItemStatus.Failed)) @@ -702,7 +699,7 @@ namespace MediaBrowser.Server.Implementations.Sync var path = Path.Combine(temporaryPath, filename); - Directory.CreateDirectory(Path.GetDirectoryName(path)); + _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); using (var stream = await _subtitleEncoder.GetSubtitles(streamInfo.ItemId, streamInfo.MediaSourceId, subtitleStreamIndex, subtitleStreamInfo.Format, 0, null, cancellationToken).ConfigureAwait(false)) { @@ -868,7 +865,7 @@ namespace MediaBrowser.Server.Implementations.Sync private async Task<MediaSourceInfo> GetEncodedMediaSource(string path, User user, bool isVideo) { - var item = _libraryManager.ResolvePath(new FileInfo(path)); + var item = _libraryManager.ResolvePath(_fileSystem.GetFileSystemInfo(path)); await item.RefreshMetadata(CancellationToken.None).ConfigureAwait(false); diff --git a/MediaBrowser.Server.Implementations/Sync/SyncManager.cs b/MediaBrowser.Server.Implementations/Sync/SyncManager.cs index 18fcb4e79..c715c3f50 100644 --- a/MediaBrowser.Server.Implementations/Sync/SyncManager.cs +++ b/MediaBrowser.Server.Implementations/Sync/SyncManager.cs @@ -32,6 +32,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; namespace MediaBrowser.Server.Implementations.Sync { @@ -516,60 +517,25 @@ namespace MediaBrowser.Server.Implementations.Sync return false; } - if (!item.RunTimeTicks.HasValue) - { - return false; - } - var video = item as Video; if (video != null) { - if (video.VideoType == VideoType.Iso || video.VideoType == VideoType.HdDvd) - { - return false; - } - if (video.IsPlaceHolder) { return false; } - if (video.IsArchive) - { - return false; - } - - if (video.IsStacked) - { - return false; - } - if (video.IsShortcut) { return false; } } - var game = item as Game; - if (game != null) - { - if (game.IsMultiPart) - { - return false; - } - } - if (item is LiveTvChannel || item is IChannelItem) { return false; } - // It would be nice to support these later - if (item is Game || item is Book) - { - return false; - } - return true; } @@ -784,27 +750,27 @@ namespace MediaBrowser.Server.Implementations.Sync if (jobItem.IsMarkedForRemoval) { // Tell the device to remove it since it has been marked for removal - _logger.Debug("Adding ItemIdsToRemove {0} because IsMarkedForRemoval is set.", jobItem.ItemId); + _logger.Info("Adding ItemIdsToRemove {0} because IsMarkedForRemoval is set.", jobItem.ItemId); removeFromDevice = true; } else if (user == null) { // Tell the device to remove it since the user is gone now - _logger.Debug("Adding ItemIdsToRemove {0} because the user is no longer valid.", jobItem.ItemId); + _logger.Info("Adding ItemIdsToRemove {0} because the user is no longer valid.", jobItem.ItemId); removeFromDevice = true; } else if (!IsLibraryItemAvailable(libraryItem)) { // Tell the device to remove it since it's no longer available - _logger.Debug("Adding ItemIdsToRemove {0} because it is no longer available.", jobItem.ItemId); + _logger.Info("Adding ItemIdsToRemove {0} because it is no longer available.", jobItem.ItemId); removeFromDevice = true; } else if (job.UnwatchedOnly) { - if (libraryItem.IsPlayed(user) && libraryItem is Video) + if (libraryItem is Video && libraryItem.IsPlayed(user)) { // Tell the device to remove it since it has been played - _logger.Debug("Adding ItemIdsToRemove {0} because it has been marked played.", jobItem.ItemId); + _logger.Info("Adding ItemIdsToRemove {0} because it has been marked played.", jobItem.ItemId); removeFromDevice = true; } } @@ -818,8 +784,9 @@ namespace MediaBrowser.Server.Implementations.Sync } else { - _logger.Debug("Setting status to Queued for {0} because it is no longer on the device.", jobItem.ItemId); + _logger.Info("Setting status to Queued for {0} because it is no longer on the device.", jobItem.ItemId); jobItem.Status = SyncJobItemStatus.Queued; + jobItem.Progress = 0; } requiresSaving = true; } @@ -889,27 +856,27 @@ namespace MediaBrowser.Server.Implementations.Sync if (jobItem.IsMarkedForRemoval) { // Tell the device to remove it since it has been marked for removal - _logger.Debug("Adding ItemIdsToRemove {0} because IsMarkedForRemoval is set.", jobItem.Id); + _logger.Info("Adding ItemIdsToRemove {0} because IsMarkedForRemoval is set.", jobItem.Id); removeFromDevice = true; } else if (user == null) { // Tell the device to remove it since the user is gone now - _logger.Debug("Adding ItemIdsToRemove {0} because the user is no longer valid.", jobItem.Id); + _logger.Info("Adding ItemIdsToRemove {0} because the user is no longer valid.", jobItem.Id); removeFromDevice = true; } else if (!IsLibraryItemAvailable(libraryItem)) { // Tell the device to remove it since it's no longer available - _logger.Debug("Adding ItemIdsToRemove {0} because it is no longer available.", jobItem.Id); + _logger.Info("Adding ItemIdsToRemove {0} because it is no longer available.", jobItem.Id); removeFromDevice = true; } else if (job.UnwatchedOnly) { - if (libraryItem.IsPlayed(user) && libraryItem is Video) + if (libraryItem is Video && libraryItem.IsPlayed(user)) { // Tell the device to remove it since it has been played - _logger.Debug("Adding ItemIdsToRemove {0} because it has been marked played.", jobItem.Id); + _logger.Info("Adding ItemIdsToRemove {0} because it has been marked played.", jobItem.Id); removeFromDevice = true; } } @@ -923,8 +890,9 @@ namespace MediaBrowser.Server.Implementations.Sync } else { - _logger.Debug("Setting status to Queued for {0} because it is no longer on the device.", jobItem.Id); + _logger.Info("Setting status to Queued for {0} because it is no longer on the device.", jobItem.Id); jobItem.Status = SyncJobItemStatus.Queued; + jobItem.Progress = 0; } requiresSaving = true; } @@ -997,8 +965,6 @@ namespace MediaBrowser.Server.Implementations.Sync return false; } - // TODO: Make sure it hasn't been deleted - return true; } diff --git a/MediaBrowser.Server.Implementations/Sync/TargetDataProvider.cs b/MediaBrowser.Server.Implementations/Sync/TargetDataProvider.cs index 676adad34..41d56d959 100644 --- a/MediaBrowser.Server.Implementations/Sync/TargetDataProvider.cs +++ b/MediaBrowser.Server.Implementations/Sync/TargetDataProvider.cs @@ -11,6 +11,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; using Interfaces.IO; namespace MediaBrowser.Server.Implementations.Sync diff --git a/MediaBrowser.Server.Implementations/Themes/AppThemeManager.cs b/MediaBrowser.Server.Implementations/Themes/AppThemeManager.cs deleted file mode 100644 index 2711c08aa..000000000 --- a/MediaBrowser.Server.Implementations/Themes/AppThemeManager.cs +++ /dev/null @@ -1,168 +0,0 @@ -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.IO; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Themes; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Themes; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace MediaBrowser.Server.Implementations.Themes -{ - public class AppThemeManager : IAppThemeManager - { - private readonly IServerApplicationPaths _appPaths; - private readonly IFileSystem _fileSystem; - private readonly IJsonSerializer _json; - private readonly ILogger _logger; - - private readonly string[] _supportedImageExtensions = { ".png", ".jpg", ".jpeg" }; - - public AppThemeManager(IServerApplicationPaths appPaths, IFileSystem fileSystem, IJsonSerializer json, ILogger logger) - { - _appPaths = appPaths; - _fileSystem = fileSystem; - _json = json; - _logger = logger; - } - - private string ThemePath - { - get - { - return Path.Combine(_appPaths.ProgramDataPath, "appthemes"); - } - } - - private string GetThemesPath(string applicationName) - { - if (string.IsNullOrWhiteSpace(applicationName)) - { - throw new ArgumentNullException("applicationName"); - } - - // Force everything lowercase for consistency and maximum compatibility with case-sensitive file systems - var name = _fileSystem.GetValidFilename(applicationName.ToLower()); - - return Path.Combine(ThemePath, name); - } - - private string GetThemePath(string applicationName, string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException("name"); - } - - // Force everything lowercase for consistency and maximum compatibility with case-sensitive file systems - name = _fileSystem.GetValidFilename(name.ToLower()); - - return Path.Combine(GetThemesPath(applicationName), name); - } - - private string GetImagesPath(string applicationName, string themeName) - { - return Path.Combine(GetThemePath(applicationName, themeName), "images"); - } - - public IEnumerable<AppThemeInfo> GetThemes(string applicationName) - { - var path = GetThemesPath(applicationName); - - try - { - return Directory - .EnumerateFiles(path, "*", SearchOption.AllDirectories) - .Where(i => string.Equals(Path.GetExtension(i), ".json", StringComparison.OrdinalIgnoreCase)) - .Select(i => - { - try - { - return _json.DeserializeFromFile<AppThemeInfo>(i); - } - catch (Exception ex) - { - _logger.ErrorException("Error deserializing {0}", ex, i); - return null; - } - - }).Where(i => i != null); - } - catch (DirectoryNotFoundException) - { - return new List<AppThemeInfo>(); - } - } - - public AppTheme GetTheme(string applicationName, string name) - { - var themePath = GetThemePath(applicationName, name); - var file = Path.Combine(themePath, "theme.json"); - - var imagesPath = GetImagesPath(applicationName, name); - - var theme = _json.DeserializeFromFile<AppTheme>(file); - - theme.Images = new DirectoryInfo(imagesPath) - .EnumerateFiles("*", SearchOption.TopDirectoryOnly) - .Where(i => _supportedImageExtensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase)) - .Select(GetThemeImage) - .ToList(); - - return theme; - } - - private ThemeImage GetThemeImage(FileInfo file) - { - var dateModified = _fileSystem.GetLastWriteTimeUtc(file); - - var cacheTag = (file.FullName + dateModified.Ticks).GetMD5().ToString("N"); - - return new ThemeImage - { - CacheTag = cacheTag, - Name = file.Name - }; - } - - public void SaveTheme(AppTheme theme) - { - var themePath = GetThemePath(theme.AppName, theme.Name); - var file = Path.Combine(themePath, "theme.json"); - - Directory.CreateDirectory(themePath); - - // Clone it so that we don't serialize all the images - they're always dynamic - var clone = new AppTheme - { - AppName = theme.AppName, - Name = theme.Name, - Options = theme.Options, - Images = null - }; - - _json.SerializeToFile(clone, file); - } - - public InternalThemeImage GetImageImageInfo(string applicationName, string themeName, string imageName) - { - var imagesPath = GetImagesPath(applicationName, themeName); - - var file = new DirectoryInfo(imagesPath).EnumerateFiles("*", SearchOption.TopDirectoryOnly) - .First(i => string.Equals(i.Name, imageName, StringComparison.OrdinalIgnoreCase)); - - var themeImage = GetThemeImage(file); - - return new InternalThemeImage - { - CacheTag = themeImage.CacheTag, - Name = themeImage.Name, - Path = file.FullName, - DateModified = _fileSystem.GetLastWriteTimeUtc(file) - }; - } - } -} diff --git a/MediaBrowser.Server.Implementations/UserViews/CollectionFolderImageProvider.cs b/MediaBrowser.Server.Implementations/UserViews/CollectionFolderImageProvider.cs index 7054515b6..de0b0e758 100644 --- a/MediaBrowser.Server.Implementations/UserViews/CollectionFolderImageProvider.cs +++ b/MediaBrowser.Server.Implementations/UserViews/CollectionFolderImageProvider.cs @@ -10,8 +10,10 @@ using MediaBrowser.Server.Implementations.Photos; using MoreLinq; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; +using CommonIO; namespace MediaBrowser.Server.Implementations.UserViews { @@ -91,18 +93,20 @@ namespace MediaBrowser.Server.Implementations.UserViews return GetFinalItems(items.Where(i => i.HasImage(ImageType.Primary) || i.HasImage(ImageType.Thumb)).ToList(), 8); } - public override bool Supports(IHasImages item) + protected override bool Supports(IHasImages item) { return item is CollectionFolder; } - protected override async Task<bool> CreateImage(IHasImages item, List<BaseItem> itemsWithImages, string outputPath, ImageType imageType, int imageIndex) + protected override async Task<string> CreateImage(IHasImages item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) { + var outputPath = Path.ChangeExtension(outputPathWithoutExtension, ".png"); + if (imageType == ImageType.Primary) { if (itemsWithImages.Count == 0) { - return false; + return null; } return await CreateThumbCollage(item, itemsWithImages, outputPath, 960, 540).ConfigureAwait(false); diff --git a/MediaBrowser.Server.Implementations/UserViews/DynamicImageProvider.cs b/MediaBrowser.Server.Implementations/UserViews/DynamicImageProvider.cs index 8321ab952..cdffadcd7 100644 --- a/MediaBrowser.Server.Implementations/UserViews/DynamicImageProvider.cs +++ b/MediaBrowser.Server.Implementations/UserViews/DynamicImageProvider.cs @@ -11,8 +11,10 @@ using MediaBrowser.Server.Implementations.Photos; using MoreLinq; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; +using CommonIO; namespace MediaBrowser.Server.Implementations.UserViews { @@ -66,14 +68,14 @@ namespace MediaBrowser.Server.Implementations.UserViews } var isUsingCollectionStrip = IsUsingCollectionStrip(view); - var recursive = isUsingCollectionStrip && !new[] { CollectionType.Playlists, CollectionType.Channels }.Contains(view.ViewType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + var recursive = isUsingCollectionStrip && !new[] { CollectionType.Channels, CollectionType.BoxSets, CollectionType.Playlists }.Contains(view.ViewType ?? string.Empty, StringComparer.OrdinalIgnoreCase); var result = await view.GetItems(new InternalItemsQuery { User = (view.UserId.HasValue ? _userManager.GetUserById(view.UserId.Value) : null), CollapseBoxSetItems = false, Recursive = recursive, - ExcludeItemTypes = new[] { "UserView", "CollectionFolder", "Playlist" } + ExcludeItemTypes = new[] { "UserView", "CollectionFolder" } }).ConfigureAwait(false); @@ -130,7 +132,7 @@ namespace MediaBrowser.Server.Implementations.UserViews return GetFinalItems(items.Where(i => i.HasImage(ImageType.Primary)).ToList()); } - public override bool Supports(IHasImages item) + protected override bool Supports(IHasImages item) { var view = item as UserView; if (view != null) @@ -147,20 +149,29 @@ namespace MediaBrowser.Server.Implementations.UserViews { CollectionType.Movies, CollectionType.TvShows, - CollectionType.Music + CollectionType.Music, + CollectionType.Games, + CollectionType.Books, + CollectionType.MusicVideos, + CollectionType.HomeVideos, + CollectionType.BoxSets, + CollectionType.Playlists, + CollectionType.Photos }; return collectionStripViewTypes.Contains(view.ViewType ?? string.Empty); } - protected override async Task<bool> CreateImage(IHasImages item, List<BaseItem> itemsWithImages, string outputPath, ImageType imageType, int imageIndex) + protected override async Task<string> CreateImage(IHasImages item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) { + var outputPath = Path.ChangeExtension(outputPathWithoutExtension, ".png"); + var view = (UserView)item; if (imageType == ImageType.Primary && IsUsingCollectionStrip(view)) { if (itemsWithImages.Count == 0) { - return false; + return null; } return await CreateThumbCollage(item, itemsWithImages, outputPath, 960, 540).ConfigureAwait(false); diff --git a/MediaBrowser.Server.Implementations/packages.config b/MediaBrowser.Server.Implementations/packages.config index 92388c99e..22206997f 100644 --- a/MediaBrowser.Server.Implementations/packages.config +++ b/MediaBrowser.Server.Implementations/packages.config @@ -1,9 +1,10 @@ <?xml version="1.0" encoding="utf-8"?>
<packages>
+ <package id="CommonIO" version="1.0.0.5" targetFramework="net45" />
<package id="Interfaces.IO" version="1.0.0.5" targetFramework="net45" />
- <package id="MediaBrowser.Naming" version="1.0.0.37" targetFramework="net45" />
+ <package id="MediaBrowser.Naming" version="1.0.0.38" targetFramework="net45" />
<package id="Mono.Nat" version="1.2.24.0" targetFramework="net45" />
- <package id="morelinq" version="1.1.0" targetFramework="net45" />
+ <package id="morelinq" version="1.1.1" targetFramework="net45" />
<package id="Patterns.Logging" version="1.0.0.2" targetFramework="net45" />
- <package id="SocketHttpListener" version="1.0.0.7" targetFramework="net45" />
+ <package id="SocketHttpListener" version="1.0.0.10" targetFramework="net45" />
</packages>
\ No newline at end of file |
